refactor(ui): promote current design system (#43200)

This commit is contained in:
Luke Parker
2026-08-19 11:36:11 +10:00
committed by GitHub
parent 044d04df06
commit 3b8d3ca639
290 changed files with 3941 additions and 10093 deletions
+262
View File
@@ -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>
)
},
}
+35
View File
@@ -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>
)
}