refactor(ui): promote current design system (#43200)
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: geometricPrecision;
|
||||
}
|
||||
|
||||
[data-component="button-v2"] {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border-radius: 6px;
|
||||
font-style: normal;
|
||||
font-weight: 530;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--v2-text-text-base);
|
||||
text-shadow: none;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.04px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-component="button-v2"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-component="button-v2"]:is(:focus-visible, [data-state="focus"]):not(:disabled):not([data-variant="loading"]) {
|
||||
outline: 2px solid var(--v2-border-border-focus);
|
||||
outline-offset: 2.5px;
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-size="normal"] {
|
||||
height: 28px;
|
||||
padding: 0 11px;
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-size="small"] {
|
||||
height: 24px;
|
||||
padding: 0 9px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-size="large"] {
|
||||
height: 32px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-icon][data-size="small"] {
|
||||
padding-left: 9px;
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-icon][data-size="normal"] {
|
||||
padding-left: 11px;
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-icon][data-size="large"] {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
[data-component="button-v2"] [data-slot="icon-svg"] {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
/* Neutral */
|
||||
[data-component="button-v2"][data-variant="neutral"] {
|
||||
background-color: var(--v2-background-bg-button-neutral);
|
||||
color: var(--v2-text-text-base);
|
||||
box-shadow: var(--v2-elevation-button-neutral);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="neutral"]:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
background-image:
|
||||
linear-gradient(90deg, var(--v2-overlay-simple-overlay-hover) 0%, var(--v2-overlay-simple-overlay-hover) 100%),
|
||||
linear-gradient(90deg, var(--v2-background-bg-button-neutral) 0%, var(--v2-background-bg-button-neutral) 100%);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="neutral"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
||||
background-image:
|
||||
linear-gradient(90deg, var(--v2-overlay-simple-overlay-pressed) 0%, var(--v2-overlay-simple-overlay-pressed) 100%),
|
||||
linear-gradient(90deg, var(--v2-background-bg-button-neutral) 0%, var(--v2-background-bg-button-neutral) 100%);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="neutral"]:is(:disabled, [data-state="disabled"]) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Danger */
|
||||
[data-component="button-v2"][data-variant="danger"] {
|
||||
background-color: var(--v2-background-bg-button-neutral);
|
||||
color: var(--v2-state-fg-danger);
|
||||
box-shadow: var(--v2-elevation-button-neutral);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="danger"]:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
background-image:
|
||||
linear-gradient(90deg, var(--v2-overlay-simple-overlay-hover) 0%, var(--v2-overlay-simple-overlay-hover) 100%),
|
||||
linear-gradient(90deg, var(--v2-background-bg-button-neutral) 0%, var(--v2-background-bg-button-neutral) 100%);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="danger"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
||||
background-image:
|
||||
linear-gradient(90deg, var(--v2-overlay-simple-overlay-pressed) 0%, var(--v2-overlay-simple-overlay-pressed) 100%),
|
||||
linear-gradient(90deg, var(--v2-background-bg-button-neutral) 0%, var(--v2-background-bg-button-neutral) 100%);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="danger"]:is(:disabled, [data-state="disabled"]) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="warning"] {
|
||||
background-color: var(--v2-background-bg-button-neutral);
|
||||
color: var(--v2-state-fg-warning);
|
||||
box-shadow: var(--v2-elevation-button-neutral);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="warning"]:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
background-image:
|
||||
linear-gradient(90deg, var(--v2-overlay-simple-overlay-hover) 0%, var(--v2-overlay-simple-overlay-hover) 100%),
|
||||
linear-gradient(90deg, var(--v2-background-bg-button-neutral) 0%, var(--v2-background-bg-button-neutral) 100%);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="warning"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
||||
background-image:
|
||||
linear-gradient(90deg, var(--v2-overlay-simple-overlay-pressed) 0%, var(--v2-overlay-simple-overlay-pressed) 100%),
|
||||
linear-gradient(90deg, var(--v2-background-bg-button-neutral) 0%, var(--v2-background-bg-button-neutral) 100%);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="warning"]:is(:disabled, [data-state="disabled"]) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Outline */
|
||||
[data-component="button-v2"][data-variant="outline"] {
|
||||
background-color: transparent;
|
||||
color: var(--v2-text-text-base);
|
||||
box-shadow: inset 0 0 0 1px var(--v2-border-border-muted);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="outline"]:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="outline"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="outline"]:is(:disabled, [data-state="disabled"]) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Contrast */
|
||||
[data-component="button-v2"][data-variant="contrast"] {
|
||||
background-image:
|
||||
linear-gradient(180deg, var(--v2-alpha-light-20) 0%, var(--v2-alpha-light-0) 100%),
|
||||
linear-gradient(90deg, var(--v2-background-bg-contrast) 0%, var(--v2-background-bg-contrast) 100%);
|
||||
color: var(--v2-text-text-contrast);
|
||||
text-shadow: 0 0.5px 0.5px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: var(--v2-elevation-button-contrast);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="contrast"]:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
background-image:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
var(--v2-overlay-simple-overlay-contrast-hover) 0%,
|
||||
var(--v2-overlay-simple-overlay-contrast-hover) 100%
|
||||
),
|
||||
linear-gradient(180deg, var(--v2-alpha-light-20) 0%, var(--v2-alpha-light-0) 100%),
|
||||
linear-gradient(90deg, var(--v2-background-bg-contrast) 0%, var(--v2-background-bg-contrast) 100%);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="contrast"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
||||
background-image:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
var(--v2-overlay-simple-overlay-contrast-pressed) 0%,
|
||||
var(--v2-overlay-simple-overlay-contrast-pressed) 100%
|
||||
),
|
||||
linear-gradient(180deg, var(--v2-alpha-light-20) 0%, var(--v2-alpha-light-0) 100%),
|
||||
linear-gradient(90deg, var(--v2-background-bg-contrast) 0%, var(--v2-background-bg-contrast) 100%);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="contrast"]:is(:disabled, [data-state="disabled"]) {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
[data-component="button-v2"][data-variant="loading"] {
|
||||
background: var(--v2-background-bg-layer-02);
|
||||
color: var(--v2-text-text-base);
|
||||
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-muted);
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Ghost */
|
||||
[data-component="button-v2"][data-variant="ghost"] {
|
||||
background-color: transparent;
|
||||
color: var(--v2-text-text-base);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="ghost"]:is(:hover, [data-state="hover"]):not(:disabled):not(
|
||||
[data-expanded]
|
||||
) {
|
||||
background-color: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="ghost"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="ghost"]:where([data-expanded]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="ghost"]:is(:disabled, [data-state="disabled"]) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Ghost muted */
|
||||
[data-component="button-v2"][data-variant="ghost-muted"] {
|
||||
background-color: transparent;
|
||||
color: var(--v2-text-text-muted);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="ghost-muted"] [data-slot="icon-svg"] {
|
||||
color: var(--v2-icon-icon-muted);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="ghost-muted"]:is(:hover, [data-state="hover"]):not(:disabled):not(
|
||||
[data-expanded]
|
||||
) {
|
||||
background-color: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="ghost-muted"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="ghost-muted"]:where([data-expanded]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||
color: var(--v2-text-text-base);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="ghost-muted"]:where([data-expanded]):not(:disabled) [data-slot="icon-svg"] {
|
||||
color: var(--v2-icon-icon-base);
|
||||
}
|
||||
|
||||
[data-component="button-v2"][data-variant="ghost-muted"]:is(:disabled, [data-state="disabled"]) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { For } from "solid-js"
|
||||
import { Button } from "./button"
|
||||
|
||||
const docs = `### Overview
|
||||
Button v2 with visual variants and three sizes.
|
||||
|
||||
### API
|
||||
- \`variant\`: "neutral" | "danger" | "warning" | "contrast" | "ghost" | "ghost-muted" | "loading".
|
||||
- \`size\`: "small" | "normal" | "large".
|
||||
- \`icon\`: Optional icon name.
|
||||
- Inherits Kobalte Button props and native button attributes.
|
||||
|
||||
### States
|
||||
- default, hover, pressed, focus, disabled.
|
||||
- State selectors are available via pseudo-classes and \`[data-state]\`.
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/Button",
|
||||
id: "ui-button",
|
||||
component: Button,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
frameHeight: "240px",
|
||||
frameBackground: "#fff",
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "neutral",
|
||||
size: "normal",
|
||||
},
|
||||
argTypes: {
|
||||
icon: {
|
||||
control: "text",
|
||||
},
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["neutral", "danger", "warning", "contrast", "ghost", "ghost-muted", "loading"],
|
||||
},
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["normal", "large"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Playground = {}
|
||||
|
||||
export const Variants = {
|
||||
render: () => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "12px",
|
||||
"align-items": "center",
|
||||
"flex-wrap": "wrap",
|
||||
}}
|
||||
>
|
||||
<Button variant="neutral">Neutral</Button>
|
||||
<Button variant="danger">Danger</Button>
|
||||
<Button variant="warning">Warning</Button>
|
||||
<Button variant="contrast">Contrast</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="ghost-muted" icon="edit">
|
||||
Ghost muted
|
||||
</Button>
|
||||
<Button variant="loading">Loading</Button>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "12px",
|
||||
"align-items": "center",
|
||||
"flex-wrap": "wrap",
|
||||
}}
|
||||
>
|
||||
<Button size="small" variant="neutral">
|
||||
Small
|
||||
</Button>
|
||||
<Button size="normal" variant="neutral">
|
||||
Normal
|
||||
</Button>
|
||||
<Button size="large" variant="neutral">
|
||||
Large
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Icon = {
|
||||
render: () => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "12px",
|
||||
"align-items": "center",
|
||||
"flex-wrap": "wrap",
|
||||
}}
|
||||
>
|
||||
<Button variant="neutral" size="normal" icon="plus">
|
||||
Normal
|
||||
</Button>
|
||||
<Button variant="contrast" size="large" icon="plus">
|
||||
Large
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const AllStates = {
|
||||
render: () => {
|
||||
const variants = ["neutral", "danger", "warning", "contrast", "ghost", "ghost-muted", "loading"] as const
|
||||
const states = ["default", "hover", "pressed", "focus", "disabled"] as const
|
||||
const toTitleCase = (value: string) => value.charAt(0).toUpperCase() + value.slice(1)
|
||||
return (
|
||||
<div style={{ display: "grid", gap: "12px" }}>
|
||||
<For each={variants}>
|
||||
{(variant) => (
|
||||
<div style={{ display: "grid", gap: "8px" }}>
|
||||
<div
|
||||
style={{
|
||||
"font-size": "12px",
|
||||
color: "var(--text-weak)",
|
||||
"text-transform": "capitalize",
|
||||
}}
|
||||
>
|
||||
{variant}
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
|
||||
<For each={states}>
|
||||
{(state) => (
|
||||
<Button
|
||||
variant={variant}
|
||||
data-state={state === "default" ? undefined : state}
|
||||
disabled={state === "disabled"}
|
||||
>
|
||||
{toTitleCase(state)}
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Root } from "@kobalte/core/button"
|
||||
import { type ComponentProps, Show, createMemo, splitProps } from "solid-js"
|
||||
import { Icon, type IconProps } from "@opencode-ai/ui/icon"
|
||||
import "./button.css"
|
||||
|
||||
export interface ButtonProps
|
||||
extends ComponentProps<typeof Root>,
|
||||
Pick<ComponentProps<"button">, "class" | "classList" | "children"> {
|
||||
size?: "small" | "normal" | "large"
|
||||
variant?: "neutral" | "danger" | "warning" | "outline" | "contrast" | "ghost" | "ghost-muted" | "loading"
|
||||
icon?: IconProps["name"]
|
||||
}
|
||||
|
||||
export function Button(props: ButtonProps) {
|
||||
const [split, rest] = splitProps(props, ["variant", "size", "icon", "class", "classList"])
|
||||
const resolvedIcon = createMemo(() => split.icon)
|
||||
return (
|
||||
<Root
|
||||
{...rest}
|
||||
data-component="button-v2"
|
||||
data-size={split.size || "normal"}
|
||||
data-variant={split.variant || "neutral"}
|
||||
data-icon={resolvedIcon()}
|
||||
classList={{
|
||||
...split.classList,
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
>
|
||||
<Show when={resolvedIcon()}>
|
||||
<Icon name={resolvedIcon()!} />
|
||||
</Show>
|
||||
{props.children}
|
||||
</Root>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user