mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-09 22:25:02 +03:00
feat(ui): add the design system checkbox as the compact appearance (#61516)
This commit is contained in:
committed by
GitHub
Unverified
parent
774a3dd510
commit
e756624814
@@ -125,6 +125,34 @@ story("select fields come in the text input sizes and default to the inline trig
|
||||
await expect(fields.nth(3)).toHaveCSS("box-shadow", "none")
|
||||
})
|
||||
|
||||
story("compact checkboxes use the design system control states and focus ring", async ({ mount, page }) => {
|
||||
const root = await mount("ui-checkbox--compact")
|
||||
|
||||
await page.addStyleTag({
|
||||
content: `[data-slot="checkbox-v2"] {
|
||||
--v2-background-bg-base: rgb(20, 30, 40);
|
||||
--v2-background-bg-layer-01: rgb(50, 60, 70);
|
||||
--v2-background-bg-accent: rgb(1, 2, 3);
|
||||
--v2-state-fg-danger: rgb(7, 8, 9);
|
||||
}`,
|
||||
})
|
||||
|
||||
const controls = root.locator('[data-slot="checkbox-v2-control"]')
|
||||
|
||||
await expect(controls.nth(0)).toHaveCSS("background-color", "rgb(20, 30, 40)")
|
||||
await expect(controls.nth(0)).toHaveCSS("background-image", "none")
|
||||
await expect(controls.nth(1)).toHaveCSS("background-color", "rgb(1, 2, 3)")
|
||||
await expect(controls.nth(1)).not.toHaveCSS("background-image", "none")
|
||||
await expect(controls.nth(2)).toHaveCSS("background-color", "rgb(1, 2, 3)")
|
||||
await expect(controls.nth(3)).toHaveCSS("box-shadow", "rgb(7, 8, 9) 0px 0px 0px 0.5px inset")
|
||||
await expect(controls.nth(6)).toHaveCSS("background-color", "rgb(50, 60, 70)")
|
||||
|
||||
await page.keyboard.press("Tab")
|
||||
await expect(root.getByRole("checkbox", { name: "Default" })).toBeFocused()
|
||||
await expect(controls.nth(0)).toHaveCSS("outline-width", "2px")
|
||||
await expect(controls.nth(0)).toHaveCSS("outline-offset", "2px")
|
||||
})
|
||||
|
||||
story(
|
||||
"text inputs keep clear and copy actions equally sized and inset across sizes and directions",
|
||||
async ({ mount, page }) => {
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/* Vite can link this before the app entry CSS, so name every layer here or `components` ranks below Tailwind's base reset. */
|
||||
@layer properties, theme, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
[data-slot="checkbox-v2"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
cursor: default;
|
||||
|
||||
&:where([data-disabled]) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:where([data-readonly]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2-error"] {
|
||||
color: var(--v2-state-fg-danger);
|
||||
font-size: 12px;
|
||||
font-weight: 440;
|
||||
line-height: var(--line-height-compact);
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2-error"]:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2-row"] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2-input"] {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
/* 20px tall so the 16px control centres on the first 20px line of the label. */
|
||||
[data-slot="checkbox-v2-control-stack"] {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 20px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2-control"] {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
color: var(--v2-icon-icon-contrast);
|
||||
background: var(--v2-background-bg-base);
|
||||
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-strong);
|
||||
transition:
|
||||
background 85ms ease-out,
|
||||
box-shadow 85ms ease-out;
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2"][data-surface="layer-1"] [data-slot="checkbox-v2-control"] {
|
||||
background: var(--v2-background-bg-layer-01);
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2-indicator"] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2-text"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2-label"] {
|
||||
user-select: none;
|
||||
color: var(--v2-text-text-base);
|
||||
font-size: 13px;
|
||||
font-weight: 440;
|
||||
line-height: var(--line-height-base);
|
||||
letter-spacing: -0.04px;
|
||||
font-variation-settings: "slnt" 0;
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2-description"] {
|
||||
color: var(--v2-text-text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 440;
|
||||
line-height: var(--line-height-tight);
|
||||
letter-spacing: 0.05px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2"]:where(:not([data-readonly]))
|
||||
[data-slot="checkbox-v2-input"]:focus-visible
|
||||
~ [data-slot="checkbox-v2-control-stack"]
|
||||
[data-slot="checkbox-v2-control"] {
|
||||
outline: 2px solid var(--v2-border-border-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-slot="checkbox-v2"]:where(:hover):where(:not([data-disabled], [data-readonly], [data-invalid])):where(
|
||||
:not([data-checked], [data-indeterminate])
|
||||
)
|
||||
[data-slot="checkbox-v2-control"] {
|
||||
box-shadow:
|
||||
inset 0 0 0 0.5px var(--v2-border-border-strong),
|
||||
inset 0 0 0 100vmax var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2"]:where([data-checked], [data-indeterminate]):where(:hover):where(
|
||||
:not([data-disabled], [data-readonly])
|
||||
)
|
||||
[data-slot="checkbox-v2-control"] {
|
||||
box-shadow:
|
||||
inset 0 0 0 0.5px var(--v2-border-border-strong),
|
||||
inset 0 0 0 100vmax var(--v2-overlay-simple-overlay-contrast-hover);
|
||||
}
|
||||
}
|
||||
|
||||
/* The depth gradient is Figma's white-to-clear fill at 20% opacity over the accent. */
|
||||
[data-slot="checkbox-v2"]:where([data-checked], [data-indeterminate]) [data-slot="checkbox-v2-control"] {
|
||||
background:
|
||||
linear-gradient(180deg, var(--v2-alpha-light-20) 0%, var(--v2-alpha-light-0) 100%), var(--v2-background-bg-accent);
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2"]:where([data-invalid]):where(:not([data-checked], [data-indeterminate]))
|
||||
[data-slot="checkbox-v2-control"] {
|
||||
background: var(--v2-state-bg-danger);
|
||||
box-shadow: inset 0 0 0 0.5px var(--v2-state-fg-danger);
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2"] .checkbox-v2-icon {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2"]:where([data-checked]):where(:not([data-indeterminate])) .checkbox-v2-icon--check,
|
||||
[data-slot="checkbox-v2"]:where([data-indeterminate]) .checkbox-v2-icon--minus {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-slot="checkbox-v2"]:where([data-disabled]) [data-slot="checkbox-v2-row"] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,14 @@ Checkbox control for multi-select or agreement inputs.
|
||||
|
||||
### API
|
||||
- Uses Kobalte Checkbox props.
|
||||
- Optional: \`hideLabel\`, \`description\`, \`icon\`.
|
||||
- Optional: \`hideLabel\`, \`description\`, \`icon\` (standard only).
|
||||
- Children render as the label.
|
||||
- \`appearance\`: \`standard\` (default, legacy) or \`compact\`, the design system checkbox.
|
||||
- \`surface\`: \`base\` (default) or \`layer-1\` for the compact control.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="checkbox"\` and related slots.
|
||||
- Standard uses \`data-component="checkbox"\` and related slots.
|
||||
- Compact uses \`data-slot="checkbox-v2"\` with \`--v2-background-bg-accent\`, \`--v2-border-border-strong\` and \`--v2-state-fg-danger\`.
|
||||
`
|
||||
|
||||
export default {
|
||||
@@ -44,3 +47,31 @@ export const CustomIcon = {
|
||||
}
|
||||
|
||||
export const HiddenLabel = { render: () => <Checkbox hideLabel>Hidden label</Checkbox> }
|
||||
|
||||
export const Compact = {
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: "16px" }}>
|
||||
<Checkbox appearance="compact" description="Suffix">
|
||||
Default
|
||||
</Checkbox>
|
||||
<Checkbox appearance="compact" defaultChecked description="Suffix">
|
||||
Checked
|
||||
</Checkbox>
|
||||
<Checkbox appearance="compact" indeterminate>
|
||||
Indeterminate
|
||||
</Checkbox>
|
||||
<Checkbox appearance="compact" validationState="invalid">
|
||||
Invalid
|
||||
</Checkbox>
|
||||
<Checkbox appearance="compact" disabled description="Suffix">
|
||||
Disabled
|
||||
</Checkbox>
|
||||
<Checkbox appearance="compact" disabled defaultChecked>
|
||||
Checked, disabled
|
||||
</Checkbox>
|
||||
<Checkbox appearance="compact" surface="layer-1">
|
||||
Layer 1
|
||||
</Checkbox>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,44 +1,106 @@
|
||||
import { Control, Description, ErrorMessage, Indicator, Input, Label, Root } from "@kobalte/core/checkbox"
|
||||
import { Show, splitProps } from "solid-js"
|
||||
import { Match, Show, Switch, splitProps } from "solid-js"
|
||||
import type { ComponentProps, JSX, ParentProps } from "solid-js"
|
||||
import "./checkbox-compact.css"
|
||||
|
||||
export interface CheckboxProps extends ParentProps<ComponentProps<typeof Root>> {
|
||||
/** `compact` is the design system checkbox; `standard` keeps the legacy look. */
|
||||
appearance?: "standard" | "compact"
|
||||
/** Background of the compact control. */
|
||||
surface?: "base" | "layer-1"
|
||||
hideLabel?: boolean
|
||||
description?: string
|
||||
description?: JSX.Element
|
||||
icon?: JSX.Element
|
||||
}
|
||||
|
||||
export function Checkbox(props: CheckboxProps) {
|
||||
const [local, others] = splitProps(props, ["children", "class", "label", "hideLabel", "description", "icon"])
|
||||
const [local, others] = splitProps(props, [
|
||||
"children",
|
||||
"class",
|
||||
"label",
|
||||
"appearance",
|
||||
"surface",
|
||||
"hideLabel",
|
||||
"description",
|
||||
"icon",
|
||||
])
|
||||
|
||||
return (
|
||||
<Root {...others} data-component="checkbox">
|
||||
<Input data-slot="checkbox-checkbox-input" />
|
||||
<Control data-slot="checkbox-checkbox-control">
|
||||
<Indicator data-slot="checkbox-checkbox-indicator">
|
||||
{local.icon || (
|
||||
<svg viewBox="0 0 12 12" fill="none" width="10" height="10" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3 7.17905L5.02703 8.85135L9 3.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</Indicator>
|
||||
</Control>
|
||||
<div data-slot="checkbox-checkbox-content">
|
||||
<Show when={props.children}>
|
||||
<Label data-slot="checkbox-checkbox-label" classList={{ "sr-only": local.hideLabel }}>
|
||||
{props.children}
|
||||
</Label>
|
||||
</Show>
|
||||
<Show when={local.description}>
|
||||
<Description data-slot="checkbox-checkbox-description">{local.description}</Description>
|
||||
</Show>
|
||||
<ErrorMessage data-slot="checkbox-checkbox-error" />
|
||||
</div>
|
||||
</Root>
|
||||
<Switch>
|
||||
<Match when={local.appearance === "compact"}>
|
||||
<Root {...others} class={local.class} data-slot="checkbox-v2" data-surface={local.surface ?? "base"}>
|
||||
<div data-slot="checkbox-v2-row">
|
||||
<Input data-slot="checkbox-v2-input" />
|
||||
<div data-slot="checkbox-v2-control-stack">
|
||||
<Control data-slot="checkbox-v2-control">
|
||||
<Indicator data-slot="checkbox-v2-indicator">
|
||||
<svg
|
||||
class="checkbox-v2-icon checkbox-v2-icon--check"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M3.53564 8.17857L6.39279 11.75L12.4642 4.25" stroke="currentColor" stroke-width="1" />
|
||||
</svg>
|
||||
<svg
|
||||
class="checkbox-v2-icon checkbox-v2-icon--minus"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12.75 8H3.25" stroke="currentColor" stroke-linejoin="round" stroke-width="1" />
|
||||
</svg>
|
||||
</Indicator>
|
||||
</Control>
|
||||
</div>
|
||||
<div data-slot="checkbox-v2-text">
|
||||
<Show when={local.children}>
|
||||
<Label data-slot="checkbox-v2-label" classList={{ "sr-only": local.hideLabel }}>
|
||||
{local.children}
|
||||
</Label>
|
||||
</Show>
|
||||
<Show when={local.description}>
|
||||
<Description data-slot="checkbox-v2-description">{local.description}</Description>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<ErrorMessage data-slot="checkbox-v2-error" />
|
||||
</Root>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Root {...others} class={local.class} data-component="checkbox">
|
||||
<Input data-slot="checkbox-checkbox-input" />
|
||||
<Control data-slot="checkbox-checkbox-control">
|
||||
<Indicator data-slot="checkbox-checkbox-indicator">
|
||||
{local.icon || (
|
||||
<svg viewBox="0 0 12 12" fill="none" width="10" height="10" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3 7.17905L5.02703 8.85135L9 3.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</Indicator>
|
||||
</Control>
|
||||
<div data-slot="checkbox-checkbox-content">
|
||||
<Show when={props.children}>
|
||||
<Label data-slot="checkbox-checkbox-label" classList={{ "sr-only": local.hideLabel }}>
|
||||
{props.children}
|
||||
</Label>
|
||||
</Show>
|
||||
<Show when={local.description}>
|
||||
<Description data-slot="checkbox-checkbox-description">{local.description}</Description>
|
||||
</Show>
|
||||
<ErrorMessage data-slot="checkbox-checkbox-error" />
|
||||
</div>
|
||||
</Root>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user