@verdify/ui 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +12 -0
- package/dist/components/accordion/accordion.variants.d.ts.map +1 -1
- package/dist/components/accordion/accordion.variants.js +2 -1
- package/dist/components/accordion/accordion.variants.js.map +1 -1
- package/dist/components/agent-badge/agent-badge.variants.d.ts.map +1 -1
- package/dist/components/agent-badge/agent-badge.variants.js.map +1 -1
- package/dist/components/alert/alert.variants.d.ts.map +1 -1
- package/dist/components/alert/alert.variants.js +3 -2
- package/dist/components/alert/alert.variants.js.map +1 -1
- package/dist/components/breadcrumb/breadcrumb.variants.d.ts.map +1 -1
- package/dist/components/breadcrumb/breadcrumb.variants.js +2 -1
- package/dist/components/breadcrumb/breadcrumb.variants.js.map +1 -1
- package/dist/components/button/button.variants.d.ts.map +1 -1
- package/dist/components/button/button.variants.js +2 -1
- package/dist/components/button/button.variants.js.map +1 -1
- package/dist/components/card/card.variants.d.ts.map +1 -1
- package/dist/components/card/card.variants.js +2 -1
- package/dist/components/card/card.variants.js.map +1 -1
- package/dist/components/checkbox/checkbox.js +1 -1
- package/dist/components/checkbox/checkbox.js.map +1 -1
- package/dist/components/checkbox/checkbox.variants.d.ts.map +1 -1
- package/dist/components/checkbox/checkbox.variants.js +2 -1
- package/dist/components/checkbox/checkbox.variants.js.map +1 -1
- package/dist/components/command-palette/command-palette.variants.d.ts +1 -1
- package/dist/components/command-palette/command-palette.variants.d.ts.map +1 -1
- package/dist/components/command-palette/command-palette.variants.js +5 -3
- package/dist/components/command-palette/command-palette.variants.js.map +1 -1
- package/dist/components/consent-toggle/consent-toggle.variants.d.ts +1 -1
- package/dist/components/consent-toggle/consent-toggle.variants.d.ts.map +1 -1
- package/dist/components/consent-toggle/consent-toggle.variants.js +1 -1
- package/dist/components/consent-toggle/consent-toggle.variants.js.map +1 -1
- package/dist/components/credential-card/credential-card.variants.d.ts +3 -3
- package/dist/components/credential-card/credential-card.variants.d.ts.map +1 -1
- package/dist/components/credential-card/credential-card.variants.js +3 -3
- package/dist/components/credential-card/credential-card.variants.js.map +1 -1
- package/dist/components/data-grid/data-grid.variants.d.ts +1 -1
- package/dist/components/data-grid/data-grid.variants.d.ts.map +1 -1
- package/dist/components/data-grid/data-grid.variants.js +11 -10
- package/dist/components/data-grid/data-grid.variants.js.map +1 -1
- package/dist/components/dialog/dialog.variants.d.ts.map +1 -1
- package/dist/components/dialog/dialog.variants.js +3 -2
- package/dist/components/dialog/dialog.variants.js.map +1 -1
- package/dist/components/identity-chip/identity-chip.variants.d.ts.map +1 -1
- package/dist/components/identity-chip/identity-chip.variants.js +3 -2
- package/dist/components/identity-chip/identity-chip.variants.js.map +1 -1
- package/dist/components/input/input.variants.d.ts.map +1 -1
- package/dist/components/input/input.variants.js +3 -2
- package/dist/components/input/input.variants.js.map +1 -1
- package/dist/components/label/label.variants.js +1 -1
- package/dist/components/label/label.variants.js.map +1 -1
- package/dist/components/menu/menu.d.ts.map +1 -1
- package/dist/components/menu/menu.js +1 -1
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/menu/menu.variants.d.ts +1 -1
- package/dist/components/menu/menu.variants.d.ts.map +1 -1
- package/dist/components/menu/menu.variants.js +3 -2
- package/dist/components/menu/menu.variants.js.map +1 -1
- package/dist/components/pagination/pagination.variants.d.ts.map +1 -1
- package/dist/components/pagination/pagination.variants.js +2 -1
- package/dist/components/pagination/pagination.variants.js.map +1 -1
- package/dist/components/popover/popover.variants.d.ts.map +1 -1
- package/dist/components/popover/popover.variants.js +4 -3
- package/dist/components/popover/popover.variants.js.map +1 -1
- package/dist/components/progress/progress.variants.d.ts +1 -1
- package/dist/components/progress/progress.variants.d.ts.map +1 -1
- package/dist/components/progress/progress.variants.js +1 -1
- package/dist/components/progress/progress.variants.js.map +1 -1
- package/dist/components/radio/radio.d.ts.map +1 -1
- package/dist/components/radio/radio.js +2 -1
- package/dist/components/radio/radio.js.map +1 -1
- package/dist/components/select/select.variants.d.ts +3 -3
- package/dist/components/select/select.variants.d.ts.map +1 -1
- package/dist/components/select/select.variants.js +5 -4
- package/dist/components/select/select.variants.js.map +1 -1
- package/dist/components/sheet/sheet.variants.d.ts.map +1 -1
- package/dist/components/sheet/sheet.variants.js +3 -2
- package/dist/components/sheet/sheet.variants.js.map +1 -1
- package/dist/components/sidebar/sidebar.variants.d.ts +1 -1
- package/dist/components/sidebar/sidebar.variants.d.ts.map +1 -1
- package/dist/components/sidebar/sidebar.variants.js +4 -3
- package/dist/components/sidebar/sidebar.variants.js.map +1 -1
- package/dist/components/switch/switch.variants.d.ts.map +1 -1
- package/dist/components/switch/switch.variants.js +2 -1
- package/dist/components/switch/switch.variants.js.map +1 -1
- package/dist/components/table/table.d.ts.map +1 -1
- package/dist/components/table/table.js +1 -1
- package/dist/components/table/table.js.map +1 -1
- package/dist/components/table/table.variants.d.ts.map +1 -1
- package/dist/components/table/table.variants.js +9 -7
- package/dist/components/table/table.variants.js.map +1 -1
- package/dist/components/tabs/tabs.variants.d.ts.map +1 -1
- package/dist/components/tabs/tabs.variants.js +3 -2
- package/dist/components/tabs/tabs.variants.js.map +1 -1
- package/dist/components/textarea/textarea.js +2 -2
- package/dist/components/textarea/textarea.js.map +1 -1
- package/dist/components/textarea/textarea.variants.d.ts.map +1 -1
- package/dist/components/textarea/textarea.variants.js +2 -1
- package/dist/components/textarea/textarea.variants.js.map +1 -1
- package/dist/components/toast/toast.variants.d.ts.map +1 -1
- package/dist/components/toast/toast.variants.js +3 -2
- package/dist/components/toast/toast.variants.js.map +1 -1
- package/dist/components/trust-score/trust-score.variants.d.ts +1 -1
- package/dist/components/trust-score/trust-score.variants.d.ts.map +1 -1
- package/dist/components/trust-score/trust-score.variants.js +1 -1
- package/dist/components/trust-score/trust-score.variants.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/focus-ring.d.ts +2 -0
- package/dist/lib/focus-ring.d.ts.map +1 -0
- package/dist/lib/focus-ring.js +5 -0
- package/dist/lib/focus-ring.js.map +1 -0
- package/package.json +18 -19
- package/registry/accordion.json +3 -2
- package/registry/agent-badge.json +1 -1
- package/registry/alert.json +3 -2
- package/registry/breadcrumb.json +3 -2
- package/registry/button.json +3 -2
- package/registry/card.json +3 -2
- package/registry/checkbox.json +4 -3
- package/registry/command-palette.json +3 -2
- package/registry/consent-toggle.json +1 -1
- package/registry/credential-card.json +1 -1
- package/registry/data-grid.json +2 -1
- package/registry/dialog.json +3 -2
- package/registry/focus-ring.json +16 -0
- package/registry/identity-chip.json +2 -1
- package/registry/init.json +1 -1
- package/registry/input.json +3 -2
- package/registry/label.json +1 -1
- package/registry/menu.json +4 -3
- package/registry/pagination.json +3 -2
- package/registry/popover.json +3 -2
- package/registry/progress.json +1 -1
- package/registry/radio.json +3 -2
- package/registry/select.json +3 -2
- package/registry/sheet.json +3 -2
- package/registry/sidebar.json +3 -2
- package/registry/switch.json +3 -2
- package/registry/table.json +3 -2
- package/registry/tabs.json +3 -2
- package/registry/textarea.json +3 -2
- package/registry/toast.json +3 -2
- package/registry/trust-score.json +1 -1
- package/registry.json +4 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"type": "registry:ui"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// An AgentBadge is a small, NON-INTERACTIVE marker that the actor in view is an AI\n// agent, not a human (spec §1). It is a pill that holds a label and an optional\n// decorative icon — NOT a control: no focus ring, no target-size floor, no state\n// transition (spec §4/§5/§6).\n//\n// `variant` is the state the badge reports (spec §3). `neutral` is the default and\n// the common case: an AgentBadge has ONE meaning — \"this actor is an AI agent\" — so\n// neutrals carry the surface. It is a quiet, persistent marker, not an alarm. A\n// status color is spent ONLY when the agent ITSELF is in a state that needs\n// attention (authority expiring) or has failed (access revoked) — never to draw\n// attention to \"agent-ness\".\n//\n// The brand color (Sovereign Violet) is NEVER an AgentBadge fill — the brand is not\n// a status and not an actor-kind marker, so the family binds nothing from the action\n// tier. The verified-status green is reserved for the VerifiedBadge placed beside it,\n// never painted here, and \"agent\" is not the verified or signal status — so the only\n// status trios this badge ever reaches for are caution and critical (spec §3/§5/§8,\n// brand != state).\n//\n// Container fill: neutral AND each status paint the SAME one raised surface. The\n// status trio's `-bg` resolves to that same surface, so the agent's state is carried\n// by the fg (label + icon) and the border, never a saturated fill (spec §3/§5/§C).\n//\n//
|
|
14
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// An AgentBadge is a small, NON-INTERACTIVE marker that the actor in view is an AI\n// agent, not a human (spec §1). It is a pill that holds a label and an optional\n// decorative icon — NOT a control: no focus ring, no target-size floor, no state\n// transition (spec §4/§5/§6).\n//\n// `variant` is the state the badge reports (spec §3). `neutral` is the default and\n// the common case: an AgentBadge has ONE meaning — \"this actor is an AI agent\" — so\n// neutrals carry the surface. It is a quiet, persistent marker, not an alarm. A\n// status color is spent ONLY when the agent ITSELF is in a state that needs\n// attention (authority expiring) or has failed (access revoked) — never to draw\n// attention to \"agent-ness\".\n//\n// The brand color (Sovereign Violet) is NEVER an AgentBadge fill — the brand is not\n// a status and not an actor-kind marker, so the family binds nothing from the action\n// tier. The verified-status green is reserved for the VerifiedBadge placed beside it,\n// never painted here, and \"agent\" is not the verified or signal status — so the only\n// status trios this badge ever reaches for are caution and critical (spec §3/§5/§8,\n// brand != state).\n//\n// Container fill: neutral AND each status paint the SAME one raised surface. The\n// status trio's `-bg` resolves to that same surface, so the agent's state is carried\n// by the fg (label + icon) and the border, never a saturated fill (spec §3/§5/§C).\n//\n// TOKEN-TIER CONTRAST — RESOLVED at the token tier (tokens 0.6.0); the spec §7 AA claim holds.\n// The caution/critical bindings below are the spec §5 token table verbatim and an\n// exact mirror of the committed Badge template. On the one raised surface (where every\n// --color-status-*-bg resolves, identical to --color-surface-raised) the status fg/border\n// now MEET WCAG 2.2 AA: tokens 0.6.0 darkened --color-status-{caution,critical}-{fg,border}\n// to in-hue AA-passing shades (~4.6:1 measured against --color-surface-raised), clearing the\n// 4.5:1 text floor (1.4.3) for the 12px caption label and the 3:1 non-text bar (1.4.11) for\n// the border. (The bright saturated status colors moved to --color-status-*-accent, used only\n// on decorative aria-hidden glyphs elsewhere, never on this label.) So the spec §7 claim that\n// the label \"meets the AA text-contrast floor and its border meets the 3:1 non-text bar\" is\n// TRUE for the status variants. The neutral variant's border (--color-surface-border-muted,\n// ~2.55:1) is still under 3:1, but neutral is not status-bearing. Neither lint:gates nor the\n// jest-axe sweep checks contrast (gates do not; jsdom resolves no computed colors), so the\n// AA-passing ratios are PINNED as a regression tripwire in agent-badge.test.tsx — a re-lighten\n// below the floors trips there. (Hex values are intentionally omitted here: the token-binding\n// gate's raw-hex matcher scans this comment.)\nexport const agentBadgeVariants = cva(\n [\n // shape / layout: a pill holding the optional icon + label at the small gap\n \"inline-flex items-center gap-(--space-1) rounded-(--radius-full) border px-(--space-1)\",\n // type ROLE — caption (spec §5); the label always reads on its own, so the\n // human/agent distinction never rests on color or shape alone\n \"text-caption font-medium\",\n // global-first: never wrap (the marker stays a single self-contained chip)\n \"whitespace-nowrap\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3 (the state of the agent the badge reports)\n variant: {\n // neutral (default): the standard agent marker — neutral surface, text, and\n // border roles, no status color (spec §3)\n neutral: \"bg-surface-raised border-surface-border-muted text-text-secondary\",\n // caution: the agent itself needs attention (authority expiring, grant pending)\n // — the matching status trio; bg is the neutral surface (spec §3)\n caution: \"bg-status-caution-bg border-status-caution-border text-status-caution-fg\",\n // critical: the agent itself failed or was revoked (access revoked, invalid\n // credentials) — the matching status trio; bg is the neutral surface (spec §3)\n critical: \"bg-status-critical-bg border-status-critical-border text-status-critical-fg\",\n },\n },\n defaultVariants: { variant: \"neutral\" },\n },\n);\n\n// The optional leading icon (spec §2): one small decorative glyph at the sm icon\n// role that reinforces the label. It inherits the variant fg via `currentColor`; the\n// label still carries the meaning if the icon is dropped, so meaning never rests on\n// color OR icon alone.\nexport const agentBadgeIconClass = \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0\";\n\nexport type AgentBadgeVariantProps = VariantProps<typeof agentBadgeVariants>;\n",
|
|
15
15
|
"path": "agent-badge/agent-badge.variants.ts",
|
|
16
16
|
"target": "@ui/agent-badge/agent-badge.variants.ts",
|
|
17
17
|
"type": "registry:ui"
|
package/registry/alert.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"type": "registry:ui"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// An Alert is an inline, in-page feedback surface — a message that reports the status of a\n// region or task and, where one exists, what to do next (spec §1). It is NOT the brand: its\n// color comes from the status it reports, never from Sovereign Violet, so this file binds\n// nothing from the --color-action-* tier (brand != state, G-U2). This is the ONLY token-binding\n// site (skill §5 hard rule).\n//\n// The alert is a message surface, not a control (spec §4): the container has no hover, pressed,\n// loading, or disabled state — those belong to the controls inside it (the actions and the\n// dismiss button, which carry their own tokens). The container's only own state is FOCUS, shown\n// only when it is programmatically focused to move a reader to a blocking error; the focus ring\n// is in the base and is never removed (spec §4/§7).\n//\n// Container fill (spec §3/§5): every variant paints the SAME one neutral raised surface — the\n// status trio's `-bg` resolves to that surface — so the status color is a quiet TINT and EDGE,\n// not a flood. The meaning is carried by the border (the status -border) and the decorative\n// leading icon (the BRIGHT status accent, tokens 0.6.0), never a saturated fill; restraint over\n// volume. Status is paired with the fixed per-variant icon and the readable title/body text so it\n// survives for a reader who cannot perceive color (WCAG 1.4.1, spec §8).\nexport const alertVariants = cva(\n [\n // layout: the bordered container is a row — leading status icon, then the stacked\n // title/body/actions content; logical-property gap so it mirrors under dir=\"rtl\" (G-U6)\n \"flex items-start gap-(--space-2)\",\n // the bordered container: internal padding off the edge, the md corner radius, a 1px edge\n \"rounded-(--radius-md) border p-(--space-3)\",\n // logical-property text alignment so the alert mirrors under dir=\"rtl\" (G-U6)\n \"text-start\",\n // appearance + dismiss motion: the FAST functional duration on verdify easing, collapsing to\n // the instant endpoint under reduced motion (spec §5). Even a `verified` alert fades in on\n // the fast duration — the 350ms VerifiedBadge-only theatre is NEVER borrowed by an alert\n // appearing (G-U3 motion-theatre gate).\n \"transition-[opacity,transform] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // FOCUS: the container takes focus only when programmatically focused to move a reader to a\n // blocking error (spec §4/§6/§7). It is NOT a tab stop by default (no tabIndex set in tsx);\n // the caller passes tabIndex={-1} for that move. When it is focused it shows the visible 2px\n // ring at a 2px offset, and the ring is never removed (2.4.7).\n \"outline-none\",\n
|
|
14
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// An Alert is an inline, in-page feedback surface — a message that reports the status of a\n// region or task and, where one exists, what to do next (spec §1). It is NOT the brand: its\n// color comes from the status it reports, never from Sovereign Violet, so this file binds\n// nothing from the --color-action-* tier (brand != state, G-U2). This is the ONLY token-binding\n// site (skill §5 hard rule).\n//\n// The alert is a message surface, not a control (spec §4): the container has no hover, pressed,\n// loading, or disabled state — those belong to the controls inside it (the actions and the\n// dismiss button, which carry their own tokens). The container's only own state is FOCUS, shown\n// only when it is programmatically focused to move a reader to a blocking error; the focus ring\n// is in the base and is never removed (spec §4/§7).\n//\n// Container fill (spec §3/§5): every variant paints the SAME one neutral raised surface — the\n// status trio's `-bg` resolves to that surface — so the status color is a quiet TINT and EDGE,\n// not a flood. The meaning is carried by the border (the status -border) and the decorative\n// leading icon (the BRIGHT status accent, tokens 0.6.0), never a saturated fill; restraint over\n// volume. Status is paired with the fixed per-variant icon and the readable title/body text so it\n// survives for a reader who cannot perceive color (WCAG 1.4.1, spec §8).\nexport const alertVariants = cva(\n [\n // layout: the bordered container is a row — leading status icon, then the stacked\n // title/body/actions content; logical-property gap so it mirrors under dir=\"rtl\" (G-U6)\n \"flex items-start gap-(--space-2)\",\n // the bordered container: internal padding off the edge, the md corner radius, a 1px edge\n \"rounded-(--radius-md) border p-(--space-3)\",\n // logical-property text alignment so the alert mirrors under dir=\"rtl\" (G-U6)\n \"text-start\",\n // appearance + dismiss motion: the FAST functional duration on verdify easing, collapsing to\n // the instant endpoint under reduced motion (spec §5). Even a `verified` alert fades in on\n // the fast duration — the 350ms VerifiedBadge-only theatre is NEVER borrowed by an alert\n // appearing (G-U3 motion-theatre gate).\n \"transition-[opacity,transform] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // FOCUS: the container takes focus only when programmatically focused to move a reader to a\n // blocking error (spec §4/§6/§7). It is NOT a tab stop by default (no tabIndex set in tsx);\n // the caller passes tabIndex={-1} for that move. When it is focused it shows the visible 2px\n // ring at a 2px offset, and the ring is never removed (2.4.7).\n \"outline-none\",\n focusRing,\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3: the status the alert reports. The four map one-to-one to the\n // --color-status-* trios; there is no neutral or brand-colored alert. Each trio tints the\n // surface (-bg = the one neutral raised surface) and edges it (-border); the decorative\n // leading icon takes the matching -accent (the readable title/body keep their AA text\n // tokens). NONE binds --color-action-* (brand != state, spec §3/§8).\n variant: {\n // a verification succeeded / a claim is proven — the in-product Verified Green status,\n // NEVER the brand and NEVER generic success (spec §3/§8)\n verified: \"bg-status-verified-bg border-status-verified-border\",\n // neutral, informational, needs no action — the lowest-urgency variant (spec §3)\n signal: \"bg-status-signal-bg border-status-signal-border\",\n // needs attention but not yet broken — a soft limit, a pending step (spec §3)\n caution: \"bg-status-caution-bg border-status-caution-border\",\n // something failed or blocks progress — a rejected proof, a failed validation (spec §3)\n critical: \"bg-status-critical-bg border-status-critical-border\",\n },\n },\n // the lowest-urgency status is the default; a louder status is spent only when warranted\n defaultVariants: { variant: \"signal\" },\n },\n);\n\n// The leading status glyph (spec §2/§5): one fixed per-variant icon at the sm icon role. It\n// pairs with the color AND the text so status is never carried by color alone (1.4.1). Because\n// the glyph is DECORATIVE (aria-hidden in tsx) and the message TITLE/body text carries the\n// status meaning, the icon is exempt from the AA text floor and takes the BRIGHT variant ACCENT\n// via the matching --color-status-*-accent (tokens 0.6.0) — the vivid status color reads as an\n// emphasis mark on the quiet surface, while the readable title/body keep their AA text tokens.\nexport const alertIconVariants = cva(\"inline-flex shrink-0 h-(--size-icon-sm) w-(--size-icon-sm)\", {\n variants: {\n variant: {\n verified: \"text-status-verified-accent\",\n signal: \"text-status-signal-accent\",\n caution: \"text-status-caution-accent\",\n critical: \"text-status-critical-accent\",\n },\n },\n defaultVariants: { variant: \"signal\" },\n});\n\n// The stacked content column (spec §2): title, body, then actions, at the small stacked gap.\n// min-w-0 lets long body text wrap instead of overflowing the row.\nexport const alertContentClass = \"flex min-w-0 flex-1 flex-col gap-(--space-2)\";\n\n// The title (spec §2/§5): a short heading stating the status as a sentence-case statement, in\n// the h3 type role + primary text color. The type role already avoids label tracking — no\n// all-caps, no exclamation mark. Brand violet never paints the title.\nexport const alertTitleClass = \"text-h3 text-text-primary\";\n\n// The body (spec §2/§5): the message, in the body type role + primary text color. It names what\n// happened and, for a caution or critical alert, what to do next — honest about hard things,\n// never blaming the reader (spec §1/§8). Supporting/secondary lines use --color-text-secondary\n// (text-text-secondary) where the caller needs them.\nexport const alertBodyClass = \"text-body text-text-primary\";\n\n// The actions slot (spec §2): at most one or two controls closing the message (a retry, a link\n// to the failing step), holding at most one primary action (restraint over volume). The controls\n// are Buttons — the alert does not restate their --color-action-* bindings. Logical-property row\n// with the small gap; a little top margin separates it from the body.\nexport const alertActionsClass = \"flex items-center gap-(--space-2) mt-(--space-1)\";\n\n// The dismiss control (spec §2/§6/§7): a close button on the inline-end edge, present only on the\n// dismissible variant. A NEUTRAL ghost surface — the glyph in --color-action-ghost-fg at rest,\n// the restrained ghost hover fill (no bg/border at rest) — so the close affordance is neutral and\n// never competes with the status. It is a real focus stop with the target-size floor (44px touch\n// / 40px pointer, spec §7 / DEC-B; the height EMERGES from the floor, never fixed below it), the\n// persistent focus ring, and the fast functional hover motion + verdify easing, instant under\n// reduced motion (G-U3). The ghost fg/hover is the control's OWN action treatment, not the\n// alert's status (the brand != state gate scopes status keys to the container variant only).\nexport const alertDismissClass =\n \"inline-flex shrink-0 items-center justify-center rounded-(--radius-md) \" +\n \"text-action-ghost-fg hover:bg-action-ghost-bg-hover \" +\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"min-h-(--size-target-mobile) min-w-(--size-target-mobile) \" +\n \"sm:min-h-(--size-target-desktop) sm:min-w-(--size-target-desktop) \" +\n focusRing;\n\n// The dismiss glyph (spec §7): a neutral X at the sm icon role, drawn with currentColor so it\n// inherits the button's ghost fg. Decorative (aria-hidden in tsx) — the button carries the\n// accessible name.\nexport const alertDismissGlyphClass = \"h-(--size-icon-sm) w-(--size-icon-sm)\";\n\nexport type AlertVariantProps = VariantProps<typeof alertVariants>;\n",
|
|
15
15
|
"path": "alert/alert.variants.ts",
|
|
16
16
|
"target": "@ui/alert/alert.variants.ts",
|
|
17
17
|
"type": "registry:ui"
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
],
|
|
26
26
|
"name": "alert",
|
|
27
27
|
"registryDependencies": [
|
|
28
|
-
"@verdify/cn"
|
|
28
|
+
"@verdify/cn",
|
|
29
|
+
"@verdify/focus-ring"
|
|
29
30
|
],
|
|
30
31
|
"title": "alert",
|
|
31
32
|
"type": "registry:ui"
|
package/registry/breadcrumb.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"type": "registry:ui"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// A Breadcrumb is structural wayfinding in NEUTRAL text (spec §3): it names the trail from a\n// root to the current page. It has no status-colored and no brand-accented variant at rest —\n// a crumb reports a location in the hierarchy, not a verification result, so it binds nothing\n// from the status tier and nothing from the action(brand) tier. The trail paints from the\n// text, ghost-action, border, and surface aliases only (spec §5).\n\n// The nav landmark wrapping the trail. A neutral canvas surface with logical-property block\n// padding so the trail mirrors under dir=\"rtl\" (G-U6). The optional hairline separating the\n// breadcrumb from the content below it (spec §5, border-default) is a caller decision, not a\n// default binding — apply it via className where the surface needs the divide.\nexport const breadcrumbNavClass = \"bg-surface-canvas py-(--space-2)\";\n\n// The ordered list: root -> current. Inline row of items + separators, wrapping when narrow,\n// with the small inter-item gap. The list itself carries no text color — each item sets its own.\nexport const breadcrumbListClass =\n \"flex flex-wrap items-center gap-(--space-1) text-caption\";\n\n// A single trail item (the <li>). Inline so the item and its trailing separator sit on one row.\nexport const breadcrumbItemClass = \"inline-flex items-center gap-(--space-1)\";\n\n// The link item (spec §4). At rest it is the trail label type role in the SECONDARY text color;\n// on hover it takes a restrained ghost fill and the label lifts to the PRIMARY text color (the\n// ghost-action hover fill is the only fill a crumb ever paints). The focus ring is part of the\n// base, on every state, never removed. Motion is the fast token transition on the verdify easing,\n// collapsing to the instant endpoint under reduced motion — never the 350ms VerifiedBadge-only\n// theatre duration. A disabled (non-returnable) ancestor dims via the disabled TOKEN, not a\n// blanket opacity (DEC-C), and is taken out of the tab order + pointer flow by the component.\nexport const breadcrumbLinkClass = cva(\n [\n // type ROLE + resting color; logical inline padding + the small icon gap for an optional glyph\n \"inline-flex items-center gap-(--space-1) rounded-(--radius-sm) px-(--space-1)\",\n \"text-caption text-text-secondary\",\n // hover: the restrained ghost fill, label lifts to the primary text color, pointer cursor\n \"cursor-pointer hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n // active text where the ghost treatment applies (spec §5)\n \"active:text-action-ghost-fg\",\n // motion: fast + verdify easing, instant under reduced motion (NEVER the check theatre)\n \"transition-[color,background-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor — 44px touch / 40px pointer (spec §7, 2.5.8)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // focus ring — identical on every state, never removed (spec §4 / 2.4.7)\n \"outline-none\",\n
|
|
15
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// A Breadcrumb is structural wayfinding in NEUTRAL text (spec §3): it names the trail from a\n// root to the current page. It has no status-colored and no brand-accented variant at rest —\n// a crumb reports a location in the hierarchy, not a verification result, so it binds nothing\n// from the status tier and nothing from the action(brand) tier. The trail paints from the\n// text, ghost-action, border, and surface aliases only (spec §5).\n\n// The nav landmark wrapping the trail. A neutral canvas surface with logical-property block\n// padding so the trail mirrors under dir=\"rtl\" (G-U6). The optional hairline separating the\n// breadcrumb from the content below it (spec §5, border-default) is a caller decision, not a\n// default binding — apply it via className where the surface needs the divide.\nexport const breadcrumbNavClass = \"bg-surface-canvas py-(--space-2)\";\n\n// The ordered list: root -> current. Inline row of items + separators, wrapping when narrow,\n// with the small inter-item gap. The list itself carries no text color — each item sets its own.\nexport const breadcrumbListClass =\n \"flex flex-wrap items-center gap-(--space-1) text-caption\";\n\n// A single trail item (the <li>). Inline so the item and its trailing separator sit on one row.\nexport const breadcrumbItemClass = \"inline-flex items-center gap-(--space-1)\";\n\n// The link item (spec §4). At rest it is the trail label type role in the SECONDARY text color;\n// on hover it takes a restrained ghost fill and the label lifts to the PRIMARY text color (the\n// ghost-action hover fill is the only fill a crumb ever paints). The focus ring is part of the\n// base, on every state, never removed. Motion is the fast token transition on the verdify easing,\n// collapsing to the instant endpoint under reduced motion — never the 350ms VerifiedBadge-only\n// theatre duration. A disabled (non-returnable) ancestor dims via the disabled TOKEN, not a\n// blanket opacity (DEC-C), and is taken out of the tab order + pointer flow by the component.\nexport const breadcrumbLinkClass = cva(\n [\n // type ROLE + resting color; logical inline padding + the small icon gap for an optional glyph\n \"inline-flex items-center gap-(--space-1) rounded-(--radius-sm) px-(--space-1)\",\n \"text-caption text-text-secondary\",\n // hover: the restrained ghost fill, label lifts to the primary text color, pointer cursor\n \"cursor-pointer hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n // active text where the ghost treatment applies (spec §5)\n \"active:text-action-ghost-fg\",\n // motion: fast + verdify easing, instant under reduced motion (NEVER the check theatre)\n \"transition-[color,background-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor — 44px touch / 40px pointer (spec §7, 2.5.8)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // focus ring — identical on every state, never removed (spec §4 / 2.4.7)\n \"outline-none\",\n focusRing,\n // disabled (non-returnable) ancestor — DEC-C: dim via the disabled TOKEN, never opacity.\n // aria-disabled drives it because a breadcrumb ancestor is an <a>, which has no native\n // disabled; the component also strips href + tabindex so it cannot navigate or be tabbed to.\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled\",\n ],\n { variants: {}, defaultVariants: {} },\n);\n\n// The current page (spec §2/§4/§5): the last item. PLAIN text in the PRIMARY color, NOT a link —\n// no focus ring, no target-size floor, not focusable. The non-interactive guidance: a current\n// crumb is a label, not a control.\nexport const breadcrumbPageClass =\n \"inline-flex items-center gap-(--space-1) px-(--space-1) text-caption text-text-primary\";\n\n// The separator glyph between two items (spec §2/§4): decoration only, in the MUTED text color,\n// at the sm icon role. Removed from the a11y tree by the component (aria-hidden + role=presentation)\n// so the trail is not announced as \"Home slash Billing slash Invoice\".\nexport const breadcrumbSeparatorClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center text-text-muted\";\n\n// The overflow indicator for the collapsed variant (spec §2): the ellipsis standing in for the\n// middle of a long trail. It renders here as a DECORATIVE, non-interactive glyph in the muted\n// text color — the interactive overflow Menu (a Menu button revealing the hidden ancestors)\n// defers to the Menu primitive, which the library has not built yet (see component TSDoc).\nexport const breadcrumbEllipsisClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center text-text-muted\";\n\nexport type BreadcrumbLinkVariantProps = VariantProps<typeof breadcrumbLinkClass>;\n",
|
|
16
16
|
"path": "breadcrumb/breadcrumb.variants.ts",
|
|
17
17
|
"target": "@ui/breadcrumb/breadcrumb.variants.ts",
|
|
18
18
|
"type": "registry:ui"
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
],
|
|
27
27
|
"name": "breadcrumb",
|
|
28
28
|
"registryDependencies": [
|
|
29
|
-
"@verdify/cn"
|
|
29
|
+
"@verdify/cn",
|
|
30
|
+
"@verdify/focus-ring"
|
|
30
31
|
],
|
|
31
32
|
"title": "breadcrumb",
|
|
32
33
|
"type": "registry:ui"
|
package/registry/button.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"type": "registry:ui"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\nexport const buttonVariants = cva(\n [\n // shape, type role, layout, icon-to-label gap, horizontal padding, elevation\n \"inline-flex items-center justify-center gap-2 rounded-md px-4 shadow-sm\",\n \"text-label font-medium whitespace-nowrap select-none\",\n // hover/pressed transition — fast + verdify easing, no theatre\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n // target-size floor: 44px touch, 40px pointer — logical block-size\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px signal-blue ring at 2px offset, on every variant, never removed\n \"outline-none\",\n
|
|
15
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\nexport const buttonVariants = cva(\n [\n // shape, type role, layout, icon-to-label gap, horizontal padding, elevation\n \"inline-flex items-center justify-center gap-2 rounded-md px-4 shadow-sm\",\n \"text-label font-medium whitespace-nowrap select-none\",\n // hover/pressed transition — fast + verdify easing, no theatre\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n // target-size floor: 44px touch, 40px pointer — logical block-size\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px signal-blue ring at 2px offset, on every variant, never removed\n \"outline-none\",\n focusRing,\n // disabled: reduced-emphasis label, no pointer, no hover/pressed response\n \"disabled:pointer-events-none disabled:text-text-disabled disabled:shadow-none\",\n \"aria-busy:pointer-events-none\",\n ],\n {\n variants: {\n variant: {\n primary: [\n \"bg-action-primary-bg text-action-primary-fg border border-action-primary-border\",\n \"hover:bg-action-primary-bg-hover active:bg-action-primary-bg-active\",\n ],\n secondary: [\n \"bg-action-secondary-bg text-action-secondary-fg border border-action-secondary-border\",\n \"hover:bg-action-secondary-bg-hover\",\n ],\n ghost: [\n \"bg-transparent text-action-ghost-fg border border-transparent\",\n \"hover:bg-action-ghost-bg-hover\",\n ],\n destructive: [\n \"bg-action-destructive-bg text-action-destructive-fg\",\n \"border border-action-destructive-border\",\n ],\n },\n },\n defaultVariants: { variant: \"primary\" },\n },\n);\n\nexport type ButtonVariantProps = VariantProps<typeof buttonVariants>;\n",
|
|
16
16
|
"path": "button/button.variants.ts",
|
|
17
17
|
"target": "@ui/button/button.variants.ts",
|
|
18
18
|
"type": "registry:ui"
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
],
|
|
27
27
|
"name": "button",
|
|
28
28
|
"registryDependencies": [
|
|
29
|
-
"@verdify/cn"
|
|
29
|
+
"@verdify/cn",
|
|
30
|
+
"@verdify/focus-ring"
|
|
30
31
|
],
|
|
31
32
|
"title": "button",
|
|
32
33
|
"type": "registry:ui"
|
package/registry/card.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"type": "registry:ui"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// The card container. A card is a NEUTRAL layout surface (spec §1/§5): it paints from the\n// surface-* roles only — never from a --color-action-* or --color-status-* token, which belong\n// to the controls and badges it holds. The structural axis is the container BEHAVIOR (spec §3):\n// `static` (a non-interactive grouping, the common case) vs `interactive` (the whole surface is\n// one link/button). A second `emphasis` axis flips the resting hairline between the default and\n// the quieter muted border for a low-emphasis grouping (spec §4). The `media` slot composes via\n// the CardMedia subcomponent, so it is not a container-behavior variant here.\nexport const cardVariants = cva(\n [\n // raised surface, rounded container, resting elevation, internal padding from --space-4\n \"block bg-surface-raised border rounded-lg shadow-sm p-(--space-4)\",\n // logical-property text alignment so the card mirrors under dir=rtl (G-U6)\n \"text-start text-text-primary\",\n ],\n {\n variants: {\n variant: {\n // a non-interactive grouping: the whole card does nothing; the controls inside it do\n static: \"\",\n // the whole surface is one control: a restrained hover lift (fast + verdify easing,\n // NEVER the deliberate verified-check theatre), a stronger hover border, and the\n // settle-back on press; the focus ring + target-size floor live in the interactive base\n interactive: [\n \"cursor-pointer\",\n \"transition-[color,box-shadow,border-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // hover lift: the resting hairline (border-surface-border / -muted, set by `emphasis`)\n // strengthens to the higher-contrast border-border-strong, alongside the shadow-sm->md\n // elevation. Two DISTINCT tokens, so the §4 \"slightly stronger border\" is a real delta,\n // not the no-op that mapping both rest+hover to --color-surface-border would produce.\n \"hover:shadow-md hover:border-border-strong\",\n \"active:shadow-sm\",\n // target-size floor: 44px touch / 40px pointer (§7 2.5.8)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px focus ring around the whole card, on every state, never removed (§4)\n \"outline-none\",\n
|
|
15
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// The card container. A card is a NEUTRAL layout surface (spec §1/§5): it paints from the\n// surface-* roles only — never from a --color-action-* or --color-status-* token, which belong\n// to the controls and badges it holds. The structural axis is the container BEHAVIOR (spec §3):\n// `static` (a non-interactive grouping, the common case) vs `interactive` (the whole surface is\n// one link/button). A second `emphasis` axis flips the resting hairline between the default and\n// the quieter muted border for a low-emphasis grouping (spec §4). The `media` slot composes via\n// the CardMedia subcomponent, so it is not a container-behavior variant here.\nexport const cardVariants = cva(\n [\n // raised surface, rounded container, resting elevation, internal padding from --space-4\n \"block bg-surface-raised border rounded-lg shadow-sm p-(--space-4)\",\n // logical-property text alignment so the card mirrors under dir=rtl (G-U6)\n \"text-start text-text-primary\",\n ],\n {\n variants: {\n variant: {\n // a non-interactive grouping: the whole card does nothing; the controls inside it do\n static: \"\",\n // the whole surface is one control: a restrained hover lift (fast + verdify easing,\n // NEVER the deliberate verified-check theatre), a stronger hover border, and the\n // settle-back on press; the focus ring + target-size floor live in the interactive base\n interactive: [\n \"cursor-pointer\",\n \"transition-[color,box-shadow,border-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // hover lift: the resting hairline (border-surface-border / -muted, set by `emphasis`)\n // strengthens to the higher-contrast border-border-strong, alongside the shadow-sm->md\n // elevation. Two DISTINCT tokens, so the §4 \"slightly stronger border\" is a real delta,\n // not the no-op that mapping both rest+hover to --color-surface-border would produce.\n \"hover:shadow-md hover:border-border-strong\",\n \"active:shadow-sm\",\n // target-size floor: 44px touch / 40px pointer (§7 2.5.8)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px focus ring around the whole card, on every state, never removed (§4)\n \"outline-none\",\n focusRing,\n // disabled — DEC-C: dim via the disabled TOKEN, never a blanket opacity; remove the\n // pointer + hover/pressed response. aria-disabled covers the <a> path (native disabled\n // already blocks the <button> path).\n \"disabled:pointer-events-none disabled:text-text-disabled disabled:shadow-none\",\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled aria-disabled:shadow-none\",\n ],\n },\n emphasis: {\n // the default container hairline\n default: \"border-surface-border\",\n // the quieter hairline for a low-emphasis grouping (spec §4)\n muted: \"border-surface-border-muted\",\n },\n },\n defaultVariants: { variant: \"static\", emphasis: \"default\" },\n },\n);\n\n// The media slot: a leading image/illustration/chart spanning the inline edges above the header.\n// It negates the container's --space-4 padding on the top + inline edges so the media bleeds to\n// the card edge, and re-establishes the --space-2 stacked-slot gap below it.\nexport const cardMediaClass =\n \"-mt-(--space-4) -mx-(--space-4) mb-(--space-2) overflow-hidden rounded-t-lg\";\n\n// The header slot: title + supporting text on the inline-start, an optional control set on the\n// inline-end. --space-2 stacked-slot gap below it.\nexport const cardHeaderClass = \"flex items-start justify-between gap-(--space-2) mb-(--space-2)\";\n\n// The title: a statement in the h3 type role + the primary text color. Sentence case, no all-caps\n// (the type role already avoids the label tracking); brand violet never paints the title.\nexport const cardTitleClass = \"text-h3 text-text-primary\";\n\n// The header-actions slot: a small control set aligned to the header's inline-end edge.\nexport const cardHeaderActionsClass =\n \"flex shrink-0 items-center gap-(--space-2) -mt-(--space-1) -me-(--space-1)\";\n\n// The body slot: the card's primary content, in the body type role + secondary text color.\nexport const cardBodyClass = \"text-body text-text-secondary\";\n\n// The footer slot: actions or closing metadata, in the label type role + secondary text color.\n// --space-2 stacked-slot gap above it.\nexport const cardFooterClass =\n \"flex items-center justify-between gap-(--space-2) mt-(--space-2) text-label text-text-secondary\";\n\nexport type CardVariantProps = VariantProps<typeof cardVariants>;\n",
|
|
16
16
|
"path": "card/card.variants.ts",
|
|
17
17
|
"target": "@ui/card/card.variants.ts",
|
|
18
18
|
"type": "registry:ui"
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
],
|
|
27
27
|
"name": "card",
|
|
28
28
|
"registryDependencies": [
|
|
29
|
-
"@verdify/cn"
|
|
29
|
+
"@verdify/cn",
|
|
30
|
+
"@verdify/focus-ring"
|
|
30
31
|
],
|
|
31
32
|
"title": "card",
|
|
32
33
|
"type": "registry:ui"
|
package/registry/checkbox.json
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
],
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
|
-
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport {\n checkboxBoxVariants,\n checkboxLabelVariants,\n checkboxCheckGlyphVariants,\n checkboxBarGlyphVariants,\n type CheckboxBoxVariantProps,\n} from \"./checkbox.variants\";\n\ntype NativeInputProps = Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n \"size\" | \"type\" | \"onChange\"\n>;\n\nexport interface CheckboxProps extends NativeInputProps, CheckboxBoxVariantProps {\n /** The visible text naming the choice. Becomes the accessible name via <label for>. */\n label: string;\n /** Secondary helper text beneath the label. Linked via aria-describedby. */\n description?: string;\n /** Validation message; sets aria-invalid and the critical-bordered error state. */\n error?: string;\n /** standalone = one value; parent = summarizes children (the only place mixed rests). */\n variant?: \"standalone\" | \"parent\";\n /** Mixed/“some children” state. Honored ONLY on variant=\"parent\"; ignored on standalone. */\n indeterminate?: boolean;\n /** Fired with the resolved boolean; a parent toggle resolves mixed → checked/unchecked. */\n onCheckedChange?: (checked: boolean) => void;\n}\n\nlet idSeq = 0;\n\nexport const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(\n function Checkbox(\n {\n className,\n label,\n description,\n error,\n size,\n variant = \"standalone\",\n indeterminate = false,\n disabled = false,\n id,\n onCheckedChange,\n ...props\n },\n forwardedRef,\n ) {\n const reactId = React.useId();\n const baseId = id ?? `checkbox-${reactId}-${(idSeq += 1)}`;\n const descId = description ? `${baseId}-description` : undefined;\n const errorId = error ? `${baseId}-error` : undefined;\n\n // indeterminate is a parent-only RESTING state — standalone never rests on mixed.\n const isMixed = variant === \"parent\" && indeterminate;\n\n const innerRef = React.useRef<HTMLInputElement>(null);\n // bridge the forwarded ref to our inner ref (we need direct DOM access for indeterminate)\n React.useImperativeHandle(forwardedRef, () => innerRef.current as HTMLInputElement);\n\n // indeterminate is a DOM property, not an attribute — write it through the ref.\n // This effect is why the component is a client component ('use client' above).\n React.useEffect(() => {\n if (innerRef.current) innerRef.current.indeterminate = isMixed;\n }, [isMixed]);\n\n const describedBy =\n [descId, errorId].filter(Boolean).join(\" \") || undefined;\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n // a direct parent toggle resolves mixed → a single state; never rests on mixed\n onCheckedChange?.(e.currentTarget.checked);\n };\n\n return (\n <div className=\"flex flex-col gap-1\">\n {/* control row: the box + label share one hit area no smaller than the target token */}\n <div\n data-testid=\"checkbox-control\"\n className=\"flex min-h-(--size-target-mobile) items-center gap-2 sm:min-h-(--size-target-desktop)\"\n >\n {/* relative overlay: the box (peer) + the indicator glyphs it drives.\n The glyphs are siblings AFTER the input so peer-* can reach them. */}\n <span className=\"relative inline-flex shrink-0\">\n <input\n ref={innerRef}\n id={baseId}\n type=\"checkbox\"\n disabled={disabled}\n aria-checked={isMixed ? \"mixed\" : undefined}\n aria-invalid={error ? \"true\" : undefined}\n aria-describedby={describedBy}\n onChange={handleChange}\n className={cn(checkboxBoxVariants({ size }), className)}\n {...props}\n />\n {/* check: only when :checked; never rendered in the mixed state so the\n bar wins for an indeterminate parent (check and bar stay exclusive) */}\n {!isMixed ? (\n <svg\n data-testid=\"checkbox-check\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n className={checkboxCheckGlyphVariants({ disabled })}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n >\n <path d=\"M3.5 8.5l3 3 6-6.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n ) : null}\n {/* horizontal bar: the single indeterminate mark, only when :indeterminate */}\n <svg\n data-testid=\"checkbox-bar\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n className={checkboxBarGlyphVariants({ disabled })}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n >\n <path d=\"M4 8h8\" strokeLinecap=\"round\" />\n </svg>\n </span>\n <label htmlFor={baseId} className={checkboxLabelVariants({ disabled })}>\n {label}\n </label>\n </div>\n {description ? (\n <p id={descId} className=\"text-caption text-text-secondary\">\n {description}\n </p>\n ) : null}\n {error ? (\n <p id={errorId} className=\"text-caption text-status-critical-
|
|
8
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport {\n checkboxBoxVariants,\n checkboxLabelVariants,\n checkboxCheckGlyphVariants,\n checkboxBarGlyphVariants,\n type CheckboxBoxVariantProps,\n} from \"./checkbox.variants\";\n\ntype NativeInputProps = Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n \"size\" | \"type\" | \"onChange\"\n>;\n\nexport interface CheckboxProps extends NativeInputProps, CheckboxBoxVariantProps {\n /** The visible text naming the choice. Becomes the accessible name via <label for>. */\n label: string;\n /** Secondary helper text beneath the label. Linked via aria-describedby. */\n description?: string;\n /** Validation message; sets aria-invalid and the critical-bordered error state. */\n error?: string;\n /** standalone = one value; parent = summarizes children (the only place mixed rests). */\n variant?: \"standalone\" | \"parent\";\n /** Mixed/“some children” state. Honored ONLY on variant=\"parent\"; ignored on standalone. */\n indeterminate?: boolean;\n /** Fired with the resolved boolean; a parent toggle resolves mixed → checked/unchecked. */\n onCheckedChange?: (checked: boolean) => void;\n}\n\nlet idSeq = 0;\n\nexport const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(\n function Checkbox(\n {\n className,\n label,\n description,\n error,\n size,\n variant = \"standalone\",\n indeterminate = false,\n disabled = false,\n id,\n onCheckedChange,\n ...props\n },\n forwardedRef,\n ) {\n const reactId = React.useId();\n const baseId = id ?? `checkbox-${reactId}-${(idSeq += 1)}`;\n const descId = description ? `${baseId}-description` : undefined;\n const errorId = error ? `${baseId}-error` : undefined;\n\n // indeterminate is a parent-only RESTING state — standalone never rests on mixed.\n const isMixed = variant === \"parent\" && indeterminate;\n\n const innerRef = React.useRef<HTMLInputElement>(null);\n // bridge the forwarded ref to our inner ref (we need direct DOM access for indeterminate)\n React.useImperativeHandle(forwardedRef, () => innerRef.current as HTMLInputElement);\n\n // indeterminate is a DOM property, not an attribute — write it through the ref.\n // This effect is why the component is a client component ('use client' above).\n React.useEffect(() => {\n if (innerRef.current) innerRef.current.indeterminate = isMixed;\n }, [isMixed]);\n\n const describedBy =\n [descId, errorId].filter(Boolean).join(\" \") || undefined;\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n // a direct parent toggle resolves mixed → a single state; never rests on mixed\n onCheckedChange?.(e.currentTarget.checked);\n };\n\n return (\n <div className=\"flex flex-col gap-1\">\n {/* control row: the box + label share one hit area no smaller than the target token */}\n <div\n data-testid=\"checkbox-control\"\n className=\"flex min-h-(--size-target-mobile) items-center gap-2 sm:min-h-(--size-target-desktop)\"\n >\n {/* relative overlay: the box (peer) + the indicator glyphs it drives.\n The glyphs are siblings AFTER the input so peer-* can reach them. */}\n <span className=\"relative inline-flex shrink-0\">\n <input\n ref={innerRef}\n id={baseId}\n type=\"checkbox\"\n disabled={disabled}\n aria-checked={isMixed ? \"mixed\" : undefined}\n aria-invalid={error ? \"true\" : undefined}\n aria-describedby={describedBy}\n onChange={handleChange}\n className={cn(checkboxBoxVariants({ size }), className)}\n {...props}\n />\n {/* check: only when :checked; never rendered in the mixed state so the\n bar wins for an indeterminate parent (check and bar stay exclusive) */}\n {!isMixed ? (\n <svg\n data-testid=\"checkbox-check\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n className={checkboxCheckGlyphVariants({ disabled })}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n >\n <path d=\"M3.5 8.5l3 3 6-6.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n ) : null}\n {/* horizontal bar: the single indeterminate mark, only when :indeterminate */}\n <svg\n data-testid=\"checkbox-bar\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n className={checkboxBarGlyphVariants({ disabled })}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n >\n <path d=\"M4 8h8\" strokeLinecap=\"round\" />\n </svg>\n </span>\n <label htmlFor={baseId} className={checkboxLabelVariants({ disabled })}>\n {label}\n </label>\n </div>\n {description ? (\n <p id={descId} className=\"text-caption text-text-secondary\">\n {description}\n </p>\n ) : null}\n {error ? (\n <p id={errorId} className=\"text-caption text-status-critical-on-surface\">\n {error}\n </p>\n ) : null}\n </div>\n );\n },\n);\n",
|
|
9
9
|
"path": "checkbox/checkbox.tsx",
|
|
10
10
|
"target": "@ui/checkbox/checkbox.tsx",
|
|
11
11
|
"type": "registry:ui"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// The square box that carries the visual state. Token binding lives here ONLY.\n// `peer` so the overlaid indicator glyphs can react to :checked / :indeterminate.\nexport const checkboxBoxVariants = cva(\n [\n // shape + neutral control tier at rest (unchecked)\n \"peer shrink-0 appearance-none rounded-sm border bg-control-bg text-control-fg\",\n \"border-border-default\",\n // hover: the box border strengthens; box + label read as one target\n \"hover:border-border-strong\",\n // selection accent (Sovereign Violet) — the brand, NOT a status — when set\n \"checked:bg-action-primary-bg checked:text-action-primary-fg\",\n \"checked:border-action-primary-bg\",\n \"indeterminate:bg-action-primary-bg indeterminate:text-action-primary-fg\",\n \"indeterminate:border-action-primary-bg\",\n // functional toggle: fast + verdify easing, instant under reduced motion.\n // Never the deliberate theatre — a checkbox toggle is not the verified-check moment.\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // visible 2px signal-blue ring at 2px offset, never removed\n \"outline-none\",\n
|
|
14
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// The square box that carries the visual state. Token binding lives here ONLY.\n// `peer` so the overlaid indicator glyphs can react to :checked / :indeterminate.\nexport const checkboxBoxVariants = cva(\n [\n // shape + neutral control tier at rest (unchecked)\n \"peer shrink-0 appearance-none rounded-sm border bg-control-bg text-control-fg\",\n \"border-border-default\",\n // hover: the box border strengthens; box + label read as one target\n \"hover:border-border-strong\",\n // selection accent (Sovereign Violet) — the brand, NOT a status — when set\n \"checked:bg-action-primary-bg checked:text-action-primary-fg\",\n \"checked:border-action-primary-bg\",\n \"indeterminate:bg-action-primary-bg indeterminate:text-action-primary-fg\",\n \"indeterminate:border-action-primary-bg\",\n // functional toggle: fast + verdify easing, instant under reduced motion.\n // Never the deliberate theatre — a checkbox toggle is not the verified-check moment.\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // visible 2px signal-blue ring at 2px offset, never removed\n \"outline-none\",\n focusRing,\n // error: critical border marks the FIELD; the fill stays the selection accent\n \"aria-invalid:border-status-critical-border\",\n // disabled: not interactive; state still reads (a disabled-checked box reads checked).\n // DEC-C / spec §4: a disabled control is dimmed via the disabled TOKEN on the\n // indicator (text-text-disabled, below), NOT a blanket opacity-60 on the box.\n \"disabled:pointer-events-none\",\n ],\n {\n variants: {\n size: {\n sm: \"h-(--size-icon-sm) w-(--size-icon-sm)\",\n md: \"h-(--size-icon-md) w-(--size-icon-md)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// The overlaid indicator glyphs sit absolutely over the box and are colored by\n// the action-primary FOREGROUND — the glyph on the brand accent fill (spec §5),\n// never a status color. `peer-*` drives visibility from the sibling input's\n// native :checked / :indeterminate state; both glyphs are pointer-transparent\n// and aria-hidden (the input carries role/state). Each glyph is centered over\n// the box and drawn at three-quarters of the box so it never touches the edge.\n//\n// DEC-C / spec §4·§5: when the control is disabled the indicator (check / bar)\n// renders in --color-text-disabled — the SAME token the label dims to — NOT a\n// blanket opacity-60 dim of the whole box. The glyph is a sibling AFTER the peer\n// input, so a `disabled` cva variant (driven by the explicit prop, mirroring\n// checkboxLabelVariants) flips text-action-primary-fg → text-text-disabled.\nconst indicatorBase = [\n \"pointer-events-none absolute inset-0 m-auto hidden h-3/4 w-3/4\",\n];\nconst indicatorColorVariants = {\n variants: {\n disabled: {\n true: \"text-text-disabled\",\n false: \"text-action-primary-fg\",\n },\n },\n defaultVariants: { disabled: false },\n} as const;\n// Check glyph: shown when the peer input is :checked. The mixed (parent-only)\n// case never renders this glyph (it is omitted in JSX when isMixed), so the\n// check and the bar stay mutually exclusive without brittle variant stacking.\nexport const checkboxCheckGlyphVariants = cva(\n [...indicatorBase, \"peer-checked:block\"],\n indicatorColorVariants,\n);\n// Horizontal-bar glyph: shown only when the peer input is :indeterminate.\nexport const checkboxBarGlyphVariants = cva(\n [...indicatorBase, \"peer-indeterminate:block\"],\n indicatorColorVariants,\n);\n\n// The label naming the choice; part of the hit area.\nexport const checkboxLabelVariants = cva(\"text-text-primary select-none\", {\n variants: {\n disabled: { true: \"text-text-disabled\", false: \"\" },\n },\n defaultVariants: { disabled: false },\n});\n\nexport type CheckboxBoxVariantProps = VariantProps<typeof checkboxBoxVariants>;\n",
|
|
15
15
|
"path": "checkbox/checkbox.variants.ts",
|
|
16
16
|
"target": "@ui/checkbox/checkbox.variants.ts",
|
|
17
17
|
"type": "registry:ui"
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
],
|
|
26
26
|
"name": "checkbox",
|
|
27
27
|
"registryDependencies": [
|
|
28
|
-
"@verdify/cn"
|
|
28
|
+
"@verdify/cn",
|
|
29
|
+
"@verdify/focus-ring"
|
|
29
30
|
],
|
|
30
31
|
"title": "checkbox",
|
|
31
32
|
"type": "registry:ui"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"type": "registry:ui"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// The command palette is a NEUTRAL overlay surface (spec §1/§3/§5/§8): brand violet and Verified\n// Green are accents, neutrals carry the surface. The SCRIM, the PANEL, the INPUT, and the RESULT\n// ROWS are neutral; the active-row highlight is the SECONDARY hover fill, NEVER a status or brand\n// color — the active row reports WHERE you are in the list, not a verified result, so it never\n// takes Verified Green, and Sovereign Violet never marks a row (brand != state, G-U2). A status\n// color appears only when a result row legitimately carries a status (a verified entity in its\n// subtitle), and then it is the --color-status-* accent on that row's OWN affordance, never a flood\n// of the palette — that affordance is the caller's (VerifiedBadge), not bound here. So NOTHING in\n// this file binds an --color-action-primary-* or --color-status-* fill. This is the ONLY\n// token-binding site (skill §5 hard rule).\n\n// The scrim (spec §2 scrim, §5 --color-scrim-*): the dimming layer behind the panel that separates\n// the palette from the page and absorbs outside clicks. A neutral dim on the modal z-layer,\n// decorative (no role). The fade is a PLAIN base transition + verdify easing, instant under reduced\n// motion — never the 350ms VerifiedBadge-only theatre (G-U3). Enter/exit ride Radix's data-state on\n// the overlay (attribute-selector variants, not arbitrary values). On a light surface the dark\n// scrim token applies (spec §5: scrim-dark behind the panel on a light surface; the committed\n// overlays bind scrim-dark with no auto light-surface inversion).\nexport const commandPaletteScrimClass =\n \"fixed inset-0 z-(--z-index-modal) bg-scrim-dark \" +\n \"transition-opacity duration-(--motion-duration-base) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\";\n\n// The panel (spec §2 panel, §5): the raised container holding the input and results; it takes\n// role=dialog + the focus trap (Radix). A NEUTRAL raised surface (--color-surface-raised) with the\n// outer surface border, the lg corner radius, the lg elevation shadow above the scrim, anchored near\n// the top of the modal z-layer. It never exceeds the viewport — the listbox owns the scroll. The\n// open/close transition is the BASE duration + verdify easing, instant under reduced motion, and\n// rides Radix's data-state (attribute-selector enter/exit, not arbitrary values). NEVER the\n// deliberate verified-check theatre (G-U3). Panel padding/gaps come from --space-*.\nexport const commandPalettePanelClass =\n // Horizontally centered, anchored in the UPPER region of the viewport (a palette sits high, not\n // dead-center like a Dialog): top-1/4 is a Tailwind FRACTION utility (no arbitrary raw value), so\n // it stays clean against the token-binding gate while keeping the panel near the top.\n \"fixed left-1/2 top-1/4 z-(--z-index-modal) -translate-x-1/2 \" +\n // never exceeds the viewport — the listbox owns the scroll. calc()-bodied brackets are structural\n // (not raw-value-leading), gate-legitimate, and mirror the Dialog panel's viewport cap.\n \"flex w-[calc(100%-var(--space-8))] max-w-(--container-md) flex-col gap-(--space-3) \" +\n \"max-h-[calc(75dvh)] \" +\n \"bg-surface-raised border border-surface-border rounded-(--radius-lg) shadow-(--shadow-lg) \" +\n \"p-(--space-3) \" +\n \"transition-[opacity,transform] duration-(--motion-duration-base) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0 \" +\n \"data-[state=open]:scale-100 data-[state=closed]:scale-95 \" +\n // the panel owns no focus stop itself (focus lives in the input); its ring is never removed\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\";\n\n// The input (spec §2 input, §4 Focus, §5): the single-line search field at the top, focused on\n// open. It IS the combobox. A control-tier field — control bg/border/fg + the placeholder token\n// (spec §5 --color-control-*). The query text is the body type role in the control fg. While focused\n// the input border takes the focus color and the visible 2px focus ring shows (never removed, spec\n// §4 Focus). The target-size floor (44px touch / 40px pointer, spec §7 2.5.8 / DEC-B) with the\n// height EMERGING from the floor, never fixed below it. The body metrics ride along\n// (leading/tracking) without binding the text-body SIZE — DEC-A: a form field's value size stays\n// text-base (the iOS no-zoom floor), the body type role contributes only its leading + tracking.\nexport const commandPaletteInputClass =\n \"w-full rounded-(--radius-md) ps-(--space-9) pe-(--space-3) \" +\n \"bg-control-bg text-control-fg border border-control-border \" +\n \"placeholder:text-control-placeholder \" +\n // DEC-A: form-field value size is text-base (iOS no-zoom floor); body type role rides via metrics\n \"text-base leading-(--text-body--line-height) tracking-(--text-body--letter-spacing) \" +\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) \" +\n // the input border takes the focus color while focused (spec §5 --color-border-focus)\n \"focus-visible:border-border-focus \" +\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\";\n\n// The leading search icon (spec §2 input, §5 --size-icon-md): decorative (aria-hidden) — the input\n// names itself by its aria-label, not the glyph. Positioned at the inline-start of the input via a\n// logical offset so it mirrors under RTL (G-U6); inherits the placeholder color via currentColor.\nexport const commandPaletteSearchIconClass =\n \"pointer-events-none absolute start-(--space-3) top-1/2 -translate-y-1/2 \" +\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) items-center justify-center text-control-placeholder\";\n\n// The listbox (spec §2 listbox, §5): the results region below the input. A neutral raised surface\n// region with the input-to-results divider above it (a neutral hairline in the default border\n// color), scrolling within the panel when results overflow. Logical inline padding so it mirrors\n// under RTL (G-U6). No motion of its own — a wait is a plain wait (spec §4 Loading), the busy state\n// rides aria-busy, not theatre.\nexport const commandPaletteListboxClass =\n \"min-h-0 flex-1 overflow-y-auto border-t border-border-default pt-(--space-2)\";\n\n// The group label (spec §2 group-label, §5): a non-selectable heading that partitions results by\n// kind; it is NOT a result and is skipped by arrow movement. The MUTED text color at the LABEL type\n// role (spec §5 --color-text-muted / --text-label). Logical inline padding (G-U6).\nexport const commandPaletteGroupLabelClass =\n \"px-(--space-2) py-(--space-1) text-label text-text-muted select-none\";\n\n// One result row (spec §2 option, §4 states). A neutral row at rest; the active (highlighted) AND\n// the pointer-hovered row share ONE state — aria-selected — painted with the SECONDARY hover fill\n// (spec §4 Hover, §5 --color-action-secondary-bg-hover). Pointer hover and keyboard active are kept\n// in sync by setting aria-selected on whichever the user touched, so Enter always runs what is\n// highlighted. The active row reports WHERE you are, NOT a verified result — never Verified Green,\n// never the brand (spec §3/§8, G-U2). The label is the PRIMARY text color at the BODY type role\n// (spec §5 --color-text-primary / --text-body).\n//\n// FOCUS (spec §4 Focus): DOM focus stays in the INPUT the whole time; the active row is conveyed by\n// aria-activedescendant, not by moving focus into the list (spec §8 Don't). So a row does NOT paint\n// its own focus-visible ring — the active fill is the affordance, and the visible focus ring lives\n// on the input.\n//\n// DISABLED (spec §4 Disabled): a non-runnable row dims via the disabled TOKEN (DEC-C), never a\n// blanket opacity; it is aria-disabled, skipped by arrow movement (handled in the roving logic), and\n// not activatable. Its short reason sits in the secondary line.\nexport const commandPaletteOptionVariants = cva(\n [\n // shape + the icon-to-label gap + logical inline padding so it mirrors under RTL (G-U6)\n \"relative flex items-center gap-(--space-3) rounded-(--radius-md) px-(--space-2)\",\n // the resting (neutral) label color; pointer cursor; no underline\n \"text-text-primary cursor-pointer select-none\",\n // the shared pointer+keyboard highlight: the secondary hover fill on the ACTIVE row\n // (aria-selected). NEVER a status/brand fill — the active row is location, not a verified result.\n \"aria-selected:bg-action-secondary-bg-hover\",\n // target-size floor — 44px touch / 40px pointer, on every row (spec §7 2.5.8), never a fixed\n // height below the floor\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // focus stays in the input (spec §4 Focus / §8) — the row never paints its own focus ring\n \"outline-none\",\n // disabled (non-runnable) row — DEC-C: dim via the disabled TOKEN, never opacity; not operable\n \"aria-disabled:text-text-disabled aria-disabled:pointer-events-none aria-disabled:cursor-default\",\n ],\n {\n variants: {\n // The result label/secondary type role. There is one row treatment; the axis only exists to\n // keep the binding file shaped like the other compounds. md is the only value the spec needs.\n density: {\n md: \"py-(--space-2)\",\n },\n },\n defaultVariants: { density: \"md\" },\n },\n);\n\nexport type CommandPaletteOptionVariantProps = VariantProps<typeof commandPaletteOptionVariants>;\n\n// The result label (spec §2 option, §5): the primary line, body type role in the primary text color,\n// truncating when long so the row keeps its shape (--color-text-primary / --text-body).\nexport const commandPaletteLabelClass = \"min-w-0 flex-1 truncate text-body text-text-primary\";\n\n// The result secondary line (spec §2 option, §5): the optional second line under/after the label, in\n// the SECONDARY text color (spec §5 --color-text-secondary). Truncates when long.\nexport const commandPaletteSecondaryClass = \"truncate text-label text-text-secondary\";\n\n// The leading result icon (spec §2 option, §5 --size-icon-md): decorative — the row names itself by\n// its label text, not the glyph. Inherits the row color via currentColor; never collapses.\nexport const commandPaletteOptionIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center\";\n\n// The trailing keyboard-shortcut hint (spec §2 option, §5): pushed to the inline-end, in the MUTED\n// text color at the LABEL type role (spec §5 --color-text-muted / --text-label). Decorative\n// wayfinding, never a focus stop; logical inline-end placement (G-U6). The monospace ID/shortcut\n// stays isolated LTR inside RTL text (G-U6).\nexport const commandPaletteShortcutClass =\n \"ms-auto ps-(--space-4) text-label text-text-muted\";\n\n// The empty (no-match) state (spec §2 empty, §4): plain text that says nothing matched and what to\n// try — never a dead end, never a blamed query (principles voice). The SECONDARY text color at the\n// body type role, with breathing room (spec §5 --color-text-secondary). Announced as text, not\n// silence (spec §7 4.1.3).\nexport const commandPaletteEmptyClass =\n \"px-(--space-2) py-(--space-6) text-center text-body text-text-secondary\";\n\n// The footer (spec §2 footer, §5): a thin hint row showing the active keys (move, run, dismiss) so\n// the keyboard model is discoverable in place. The SECONDARY text color at the LABEL type role with\n// a neutral hairline divider above it (spec §5 --color-text-secondary / --text-label /\n// --color-border-default). Logical inline layout (G-U6).\nexport const commandPaletteFooterClass =\n \"flex items-center gap-(--space-4) border-t border-border-default pt-(--space-2) \" +\n \"text-label text-text-secondary\";\n",
|
|
15
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// The command palette is a NEUTRAL overlay surface (spec §1/§3/§5/§8): brand violet and Verified\n// Green are accents, neutrals carry the surface. The SCRIM, the PANEL, the INPUT, and the RESULT\n// ROWS are neutral; the active-row highlight is the SECONDARY hover fill, NEVER a status or brand\n// color — the active row reports WHERE you are in the list, not a verified result, so it never\n// takes Verified Green, and Sovereign Violet never marks a row (brand != state, G-U2). A status\n// color appears only when a result row legitimately carries a status (a verified entity in its\n// subtitle), and then it is the --color-status-* accent on that row's OWN affordance, never a flood\n// of the palette — that affordance is the caller's (VerifiedBadge), not bound here. So NOTHING in\n// this file binds an --color-action-primary-* or --color-status-* fill. This is the ONLY\n// token-binding site (skill §5 hard rule).\n\n// The scrim (spec §2 scrim, §5 --color-scrim-*): the dimming layer behind the panel that separates\n// the palette from the page and absorbs outside clicks. A neutral dim on the modal z-layer,\n// decorative (no role). The fade is a PLAIN base transition + verdify easing, instant under reduced\n// motion — never the 350ms VerifiedBadge-only theatre (G-U3). Enter/exit ride Radix's data-state on\n// the overlay (attribute-selector variants, not arbitrary values). On a light surface the dark\n// scrim token applies (spec §5: scrim-dark behind the panel on a light surface; the committed\n// overlays bind scrim-dark with no auto light-surface inversion).\nexport const commandPaletteScrimClass =\n \"fixed inset-0 z-(--z-index-modal) bg-scrim-dark \" +\n \"transition-opacity duration-(--motion-duration-base) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\";\n\n// The panel (spec §2 panel, §5): the raised container holding the input and results; it takes\n// role=dialog + the focus trap (Radix). A NEUTRAL raised surface (--color-surface-raised) with the\n// outer surface border, the lg corner radius, the lg elevation shadow above the scrim, anchored near\n// the top of the modal z-layer. It never exceeds the viewport — the listbox owns the scroll. The\n// open/close transition is the BASE duration + verdify easing, instant under reduced motion, and\n// rides Radix's data-state (attribute-selector enter/exit, not arbitrary values). NEVER the\n// deliberate verified-check theatre (G-U3). Panel padding/gaps come from --space-*.\nexport const commandPalettePanelClass =\n // Horizontally centered, anchored in the UPPER region of the viewport (a palette sits high, not\n // dead-center like a Dialog): top-1/4 is a Tailwind FRACTION utility (no arbitrary raw value), so\n // it stays clean against the token-binding gate while keeping the panel near the top.\n \"fixed left-1/2 top-1/4 z-(--z-index-modal) -translate-x-1/2 \" +\n // never exceeds the viewport — the listbox owns the scroll. calc()-bodied brackets are structural\n // (not raw-value-leading), gate-legitimate, and mirror the Dialog panel's viewport cap.\n \"flex w-[calc(100%-var(--space-8))] max-w-(--container-md) flex-col gap-(--space-3) \" +\n \"max-h-[calc(75dvh)] \" +\n \"bg-surface-raised border border-surface-border rounded-(--radius-lg) shadow-(--shadow-lg) \" +\n \"p-(--space-3) \" +\n \"transition-[opacity,transform] duration-(--motion-duration-base) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0 \" +\n \"data-[state=open]:scale-100 data-[state=closed]:scale-95 \" +\n // the panel owns no focus stop itself (focus lives in the input); its ring is never removed\n focusRing;\n\n// The input (spec §2 input, §4 Focus, §5): the single-line search field at the top, focused on\n// open. It IS the combobox. A control-tier field — control bg/border/fg + the placeholder token\n// (spec §5 --color-control-*). The query text is the body type role in the control fg. While focused\n// the input border takes the focus color and the visible 2px focus ring shows (never removed, spec\n// §4 Focus). The target-size floor (44px touch / 40px pointer, spec §7 2.5.8 / DEC-B) with the\n// height EMERGING from the floor, never fixed below it. The body metrics ride along\n// (leading/tracking) without binding the text-body SIZE — DEC-A: a form field's value size stays\n// text-base (the iOS no-zoom floor), the body type role contributes only its leading + tracking.\nexport const commandPaletteInputClass =\n \"w-full rounded-(--radius-md) ps-(--space-9) pe-(--space-3) \" +\n \"bg-control-bg text-control-fg border border-control-border \" +\n \"placeholder:text-control-placeholder \" +\n // DEC-A: form-field value size is text-base (iOS no-zoom floor); body type role rides via metrics\n \"text-base leading-(--text-body--line-height) tracking-(--text-body--letter-spacing) \" +\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) \" +\n // the input border takes the focus color while focused (spec §5 --color-border-focus)\n \"focus-visible:border-border-focus \" +\n focusRing;\n\n// The leading search icon (spec §2 input, §5 --size-icon-md): decorative (aria-hidden) — the input\n// names itself by its aria-label, not the glyph. Positioned at the inline-start of the input via a\n// logical offset so it mirrors under RTL (G-U6); inherits the placeholder color via currentColor.\nexport const commandPaletteSearchIconClass =\n \"pointer-events-none absolute start-(--space-3) top-1/2 -translate-y-1/2 \" +\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) items-center justify-center text-control-placeholder\";\n\n// The listbox (spec §2 listbox, §5): the results region below the input. A neutral raised surface\n// region with the input-to-results divider above it (a neutral hairline in the default border\n// color), scrolling within the panel when results overflow. Logical inline padding so it mirrors\n// under RTL (G-U6). No motion of its own — a wait is a plain wait (spec §4 Loading), the busy state\n// rides aria-busy, not theatre.\nexport const commandPaletteListboxClass =\n \"min-h-0 flex-1 overflow-y-auto border-t border-border-default pt-(--space-2)\";\n\n// The group label (spec §2 group-label, §5): a non-selectable heading that partitions results by\n// kind; it is NOT a result and is skipped by arrow movement. It is essential de-emphasized text (it\n// names the group), so it uses the SECONDARY text color (AA) at the LABEL type role (spec §5\n// --color-text-secondary / --text-label) — not the decorative-only muted role (accessibility.md).\nexport const commandPaletteGroupLabelClass =\n \"px-(--space-2) py-(--space-1) text-label text-text-secondary select-none\";\n\n// One result row (spec §2 option, §4 states). A neutral row at rest; the active (highlighted) AND\n// the pointer-hovered row share ONE state — aria-selected — painted with the SECONDARY hover fill\n// (spec §4 Hover, §5 --color-action-secondary-bg-hover). Pointer hover and keyboard active are kept\n// in sync by setting aria-selected on whichever the user touched, so Enter always runs what is\n// highlighted. The active row reports WHERE you are, NOT a verified result — never Verified Green,\n// never the brand (spec §3/§8, G-U2). The label is the PRIMARY text color at the BODY type role\n// (spec §5 --color-text-primary / --text-body).\n//\n// FOCUS (spec §4 Focus): DOM focus stays in the INPUT the whole time; the active row is conveyed by\n// aria-activedescendant, not by moving focus into the list (spec §8 Don't). So a row does NOT paint\n// its own focus-visible ring — the active fill is the affordance, and the visible focus ring lives\n// on the input.\n//\n// DISABLED (spec §4 Disabled): a non-runnable row dims via the disabled TOKEN (DEC-C), never a\n// blanket opacity; it is aria-disabled, skipped by arrow movement (handled in the roving logic), and\n// not activatable. Its short reason sits in the secondary line.\nexport const commandPaletteOptionVariants = cva(\n [\n // shape + the icon-to-label gap + logical inline padding so it mirrors under RTL (G-U6)\n \"relative flex items-center gap-(--space-3) rounded-(--radius-md) px-(--space-2)\",\n // the resting (neutral) label color; pointer cursor; no underline\n \"text-text-primary cursor-pointer select-none\",\n // the shared pointer+keyboard highlight: the secondary hover fill on the ACTIVE row\n // (aria-selected). NEVER a status/brand fill — the active row is location, not a verified result.\n \"aria-selected:bg-action-secondary-bg-hover\",\n // target-size floor — 44px touch / 40px pointer, on every row (spec §7 2.5.8), never a fixed\n // height below the floor\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // focus stays in the input (spec §4 Focus / §8) — the row never paints its own focus ring\n \"outline-none\",\n // disabled (non-runnable) row — DEC-C: dim via the disabled TOKEN, never opacity; not operable\n \"aria-disabled:text-text-disabled aria-disabled:pointer-events-none aria-disabled:cursor-default\",\n ],\n {\n variants: {\n // The result label/secondary type role. There is one row treatment; the axis only exists to\n // keep the binding file shaped like the other compounds. md is the only value the spec needs.\n density: {\n md: \"py-(--space-2)\",\n },\n },\n defaultVariants: { density: \"md\" },\n },\n);\n\nexport type CommandPaletteOptionVariantProps = VariantProps<typeof commandPaletteOptionVariants>;\n\n// The result label (spec §2 option, §5): the primary line, body type role in the primary text color,\n// truncating when long so the row keeps its shape (--color-text-primary / --text-body).\nexport const commandPaletteLabelClass = \"min-w-0 flex-1 truncate text-body text-text-primary\";\n\n// The result secondary line (spec §2 option, §5): the optional second line under/after the label, in\n// the SECONDARY text color (spec §5 --color-text-secondary). Truncates when long.\nexport const commandPaletteSecondaryClass = \"truncate text-label text-text-secondary\";\n\n// The leading result icon (spec §2 option, §5 --size-icon-md): decorative — the row names itself by\n// its label text, not the glyph. Inherits the row color via currentColor; never collapses.\nexport const commandPaletteOptionIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center\";\n\n// The trailing keyboard-shortcut hint (spec §2 option, §5): pushed to the inline-end, in the MUTED\n// text color at the LABEL type role (spec §5 --color-text-muted / --text-label). Decorative\n// wayfinding, never a focus stop; logical inline-end placement (G-U6). The monospace ID/shortcut\n// stays isolated LTR inside RTL text (G-U6).\nexport const commandPaletteShortcutClass =\n \"ms-auto ps-(--space-4) text-label text-text-muted\";\n\n// The empty (no-match) state (spec §2 empty, §4): plain text that says nothing matched and what to\n// try — never a dead end, never a blamed query (principles voice). The SECONDARY text color at the\n// body type role, with breathing room (spec §5 --color-text-secondary). Announced as text, not\n// silence (spec §7 4.1.3).\nexport const commandPaletteEmptyClass =\n \"px-(--space-2) py-(--space-6) text-center text-body text-text-secondary\";\n\n// The footer (spec §2 footer, §5): a thin hint row showing the active keys (move, run, dismiss) so\n// the keyboard model is discoverable in place. The SECONDARY text color at the LABEL type role with\n// a neutral hairline divider above it (spec §5 --color-text-secondary / --text-label /\n// --color-border-default). Logical inline layout (G-U6).\nexport const commandPaletteFooterClass =\n \"flex items-center gap-(--space-4) border-t border-border-default pt-(--space-2) \" +\n \"text-label text-text-secondary\";\n",
|
|
16
16
|
"path": "command-palette/command-palette.variants.ts",
|
|
17
17
|
"target": "@ui/command-palette/command-palette.variants.ts",
|
|
18
18
|
"type": "registry:ui"
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
],
|
|
27
27
|
"name": "command-palette",
|
|
28
28
|
"registryDependencies": [
|
|
29
|
-
"@verdify/cn"
|
|
29
|
+
"@verdify/cn",
|
|
30
|
+
"@verdify/focus-ring"
|
|
30
31
|
],
|
|
31
32
|
"title": "command-palette",
|
|
32
33
|
"type": "registry:ui"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"type": "registry:ui"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// ConsentToggle is the explicit, revocable affordance by which an actor grants or\n// withdraws permission for one specific use of their data or identity (spec §1). It\n// COMPOSES the committed Switch as its control — the granted axis maps to the Switch's\n// `on` — and adds the scope, the named recipient, the optional detail, the optional\n// evidence affordance, and the failure message AROUND it. So the track / thumb / focus\n// ring / target-size floor / slide-and-tint motion are bound ONCE in switch.variants.ts\n// and are NOT re-bound here; this file binds only the surrounding text + layout roles.\n//\n// brand != state (spec §4/§5/§8). The granted track takes the primary ACTION accent\n// (the Switch's `aria-checked:bg-action-primary-bg`), NEVER --color-status-verified-*:\n// verified green is the in-product verified status, never a consent affordance, and a\n// grant reports permission, not a verification result. ConsentToggle therefore binds\n// NOTHING from the status tier on its granted state — the one status color it ever\n// reaches for is --color-status-critical-fg, and ONLY on the failure message (a stated\n// failed grant/withdrawal), never on the granted/checked state. This component-scoped\n// invariant (no status color on the grant; verified green never a consent affordance) is\n// pinned as a test in consent-toggle.test.tsx — the action accent IS legitimate here, so\n// the negative forbids only the STATUS tier, per the build-on-brand skill's scoping note.\n//\n// The motion is the Switch's FAST functional slide/tint on verdify easing, collapsing to\n// the instant endpoint under reduced motion — never the 350ms VerifiedBadge-only theatre\n// duration: a consent grant is not a verification (G-U3).\n\n// The root layout (spec §2): a column stacking the control row, the optional evidence\n// affordance, and the optional failure message at the --space-2 gap. The `variant` axis\n// (spec §3) is a form of how the consent is PRESENTED — a plain grant, a grant with the\n// evidence affordance, or a grant scoped to an AI agent — never a level of color or alarm,\n// so NONE of the variants recolors anything: the granted accent and every text role are\n// identical across all three. Non-interactive container: the focus ring, the keyboard\n// model, and the target-size floor all live on the composed Switch control, not here.\nexport const consentToggleVariants = cva(\"flex flex-col gap-(--space-2)\", {\n variants: {\n // STRUCTURAL axis = spec §3. Display/composition forms, never alarm levels.\n variant: {\n // grant (default): a single permission the actor turns on to allow, off to withhold.\n grant: \"\",\n // with-evidence: adds the evidence affordance so the actor can review the recorded\n // grant — what, to whom, when. Use wherever the grant is consequential.\n \"with-evidence\": \"\",\n // agent-scoped: the recipient is an AI-agent actor, named AS an agent (AgentBadge),\n // so the actor knows they are granting to an agent, not a human.\n \"agent-scoped\": \"\",\n },\n },\n defaultVariants: { variant: \"grant\" },\n});\n\n// The recipient + detail block (spec §2/§5): who receives the data and the optional bound\n// of the grant, in the SECONDARY text color at the --text-body type role. It is composed\n// into the control's accessible description (aria-describedby), so a screen reader\n// announces \"to whom\" alongside the scope — never a bare on/off with the meaning missing.\nexport const consentToggleRecipientClass = \"text-body text-text-secondary\";\n\n// The optional detail line (spec §2/§5): one statement clarifying the bound or duration of\n// the grant (for example, what stops when consent is withdrawn), under the recipient, in\n// the secondary text color at the body role.\nexport const consentToggleDetailClass = \"text-body text-text-secondary\";\n\n// The optional evidence affordance row (spec §2/§3): a slot holding the caller's link or\n// Button to the PROOF of the grant — what was consented to, to whom, and when — so the act\n// is reviewable later. It is the caller's own focus stop with its own keyboard model and\n// focus ring (the evidence control is not folded into the busy switch); this row only\n// positions it. Surfaces a proof of the consent event, not a stored document.\nexport const consentToggleEvidenceClass = \"flex items-center text-body\";\n\n// The failure message (spec §4/§5/§7): shown next to the control on a FAILED grant or\n// withdrawal, naming what failed and what to do next without blaming the reader, in the\n// CRITICAL status foreground at the --text-body role. It is the ONLY status color the\n// component binds, and only here — never on the granted state (brand != state). It is\n// announced through an assertive live region (role=alert), set in the tsx, per 4.1.3.\nexport const consentToggleFailureClass = \"text-body text-status-critical-
|
|
14
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// ConsentToggle is the explicit, revocable affordance by which an actor grants or\n// withdraws permission for one specific use of their data or identity (spec §1). It\n// COMPOSES the committed Switch as its control — the granted axis maps to the Switch's\n// `on` — and adds the scope, the named recipient, the optional detail, the optional\n// evidence affordance, and the failure message AROUND it. So the track / thumb / focus\n// ring / target-size floor / slide-and-tint motion are bound ONCE in switch.variants.ts\n// and are NOT re-bound here; this file binds only the surrounding text + layout roles.\n//\n// brand != state (spec §4/§5/§8). The granted track takes the primary ACTION accent\n// (the Switch's `aria-checked:bg-action-primary-bg`), NEVER --color-status-verified-*:\n// verified green is the in-product verified status, never a consent affordance, and a\n// grant reports permission, not a verification result. ConsentToggle therefore binds\n// NOTHING from the status tier on its granted state — the one status color it ever\n// reaches for is --color-status-critical-fg, and ONLY on the failure message (a stated\n// failed grant/withdrawal), never on the granted/checked state. This component-scoped\n// invariant (no status color on the grant; verified green never a consent affordance) is\n// pinned as a test in consent-toggle.test.tsx — the action accent IS legitimate here, so\n// the negative forbids only the STATUS tier, per the build-on-brand skill's scoping note.\n//\n// The motion is the Switch's FAST functional slide/tint on verdify easing, collapsing to\n// the instant endpoint under reduced motion — never the 350ms VerifiedBadge-only theatre\n// duration: a consent grant is not a verification (G-U3).\n\n// The root layout (spec §2): a column stacking the control row, the optional evidence\n// affordance, and the optional failure message at the --space-2 gap. The `variant` axis\n// (spec §3) is a form of how the consent is PRESENTED — a plain grant, a grant with the\n// evidence affordance, or a grant scoped to an AI agent — never a level of color or alarm,\n// so NONE of the variants recolors anything: the granted accent and every text role are\n// identical across all three. Non-interactive container: the focus ring, the keyboard\n// model, and the target-size floor all live on the composed Switch control, not here.\nexport const consentToggleVariants = cva(\"flex flex-col gap-(--space-2)\", {\n variants: {\n // STRUCTURAL axis = spec §3. Display/composition forms, never alarm levels.\n variant: {\n // grant (default): a single permission the actor turns on to allow, off to withhold.\n grant: \"\",\n // with-evidence: adds the evidence affordance so the actor can review the recorded\n // grant — what, to whom, when. Use wherever the grant is consequential.\n \"with-evidence\": \"\",\n // agent-scoped: the recipient is an AI-agent actor, named AS an agent (AgentBadge),\n // so the actor knows they are granting to an agent, not a human.\n \"agent-scoped\": \"\",\n },\n },\n defaultVariants: { variant: \"grant\" },\n});\n\n// The recipient + detail block (spec §2/§5): who receives the data and the optional bound\n// of the grant, in the SECONDARY text color at the --text-body type role. It is composed\n// into the control's accessible description (aria-describedby), so a screen reader\n// announces \"to whom\" alongside the scope — never a bare on/off with the meaning missing.\nexport const consentToggleRecipientClass = \"text-body text-text-secondary\";\n\n// The optional detail line (spec §2/§5): one statement clarifying the bound or duration of\n// the grant (for example, what stops when consent is withdrawn), under the recipient, in\n// the secondary text color at the body role.\nexport const consentToggleDetailClass = \"text-body text-text-secondary\";\n\n// The optional evidence affordance row (spec §2/§3): a slot holding the caller's link or\n// Button to the PROOF of the grant — what was consented to, to whom, and when — so the act\n// is reviewable later. It is the caller's own focus stop with its own keyboard model and\n// focus ring (the evidence control is not folded into the busy switch); this row only\n// positions it. Surfaces a proof of the consent event, not a stored document.\nexport const consentToggleEvidenceClass = \"flex items-center text-body\";\n\n// The failure message (spec §4/§5/§7): shown next to the control on a FAILED grant or\n// withdrawal, naming what failed and what to do next without blaming the reader, in the\n// CRITICAL status foreground at the --text-body role. It is the ONLY status color the\n// component binds, and only here — never on the granted state (brand != state). It is\n// announced through an assertive live region (role=alert), set in the tsx, per 4.1.3.\nexport const consentToggleFailureClass = \"text-body text-status-critical-on-surface\";\n\nexport type ConsentToggleVariantProps = VariantProps<typeof consentToggleVariants>;\n",
|
|
15
15
|
"path": "consent-toggle/consent-toggle.variants.ts",
|
|
16
16
|
"target": "@ui/consent-toggle/consent-toggle.variants.ts",
|
|
17
17
|
"type": "registry:ui"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"type": "registry:ui"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// CredentialCard is one ROW in the list of credentials ATTACHED to an identity — a single way\n// to reach that identity (an email, phone, passkey, wallet, or enterprise SSO connection). It\n// encodes the platform's first invariant — identity is not credentials — in its UI contract: a\n// card is a LINK to an identity, never the identity itself, so it never reads as \"you\" and never\n// stands in for the account (spec §1/§8).\n//\n// It COMPOSES the committed primitives rather than reinventing them — a Card-like neutral surface\n// (the surface-* roles, bound here), the destructive Button for `remove`, the secondary Button\n// for an optional `action`, the Badge for a `status`, the Checkbox for `selectable`, and the\n// Button's in-place loading spinner for a resolving removal. So the focus ring, the target-size\n// floor, the keyboard model, and the control motion all come from those proven primitives; this\n// file binds ONLY the surrounding neutral surface + text-role + layout classes.\n//\n// brand != state (spec §3/§5/§8). The card SURFACE consumes NO action or status token of its own\n// — neutrals carry the card. The brand violet (Sovereign Violet, the action accent) is never a\n// card fill, never the icon, never the identifier, and never marks a credential as special. The\n// verified-status green is reserved for the `verified` status Badge and is never spent on the\n// card surface, the icon, or the identifier — coloring the card green would imply the IDENTITY is\n// verified, which is the one misreport this molecule forbids. The card therefore binds nothing\n// from the action tier and nothing from the status tier; those colors live on the controls and\n// Badges it holds (asserted positively in their own primitives' tests), never on this surface.\n//\n// The motion the card adds is none of its own: control hover/press uses the composed primitive's\n// fast functional transition on verdify easing, collapsing to the instant endpoint under reduced\n// motion. A resolving removal is a plain wait on the Button's ambient spinner — never the 350ms\n// VerifiedBadge-only theatre duration: a removal is not a verification (G-U3).\n\n// The card container (spec §2/§4/§5): a NEUTRAL raised surface, the same Card-static surface the\n// committed Card binds, but it is a LIST ROW (`<li role=\"listitem\">`, set in the tsx) so it sits\n// in the credential list's `<ul role=\"list\">` (spec §7). It is a static container, NOT a single\n// clickable control — the whole row does not map to one action — so it carries no focus ring and\n// no target-size floor of its own (those live on its controls, spec §4/§5). The `kind` axis is\n// which credential the card represents; it is carried by the `icon` + `label` TEXT, never by\n// color, so NONE of the kinds recolors the surface — every kind is the identical neutral surface\n// (spec §3). `selectable` composes with any kind and changes layout (a leading Checkbox), not\n// color, so it is not a color variant here.\nexport const credentialCardVariants = cva(\n [\n // raised neutral surface, rounded container, resting elevation, internal padding from --space-4\n \"relative flex items-start gap-(--space-2) rounded-lg border bg-surface-raised shadow-sm p-(--space-4)\",\n // the default container hairline (spec §5)\n \"border-surface-border\",\n // logical-property text alignment so the row mirrors under dir=rtl (G-U6); list rows carry\n // no bullet marker\n \"text-start text-text-primary list-none\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3 (the credential KIND the card represents). The kind is carried\n // by the icon + label text, never color, so every kind is the SAME neutral surface.\n kind: {\n email: \"\",\n phone: \"\",\n passkey: \"\",\n wallet: \"\",\n \"enterprise-sso\": \"\",\n },\n },\n defaultVariants: { kind: \"email\" },\n },\n);\n\n// The kind icon (spec §2/§5/§7): one small glyph for the credential kind at the md icon role. It\n// reinforces the kind shown in the label and is decorative (aria-hidden, set in the tsx) — the\n// label text still carries the kind if the icon is dropped, so the kind never rests on the icon\n// or color alone. Its fill is the SECONDARY text role (a neutral role), never a status color and\n// never the brand (spec §2/§3).\nexport const credentialCardIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center text-text-secondary\";\n\n// The label + identifier block (spec §2): the human-readable kind name above the value that\n// identifies this specific credential. Takes the remaining inline space between the icon and the\n// trailing status/controls; stacks the two lines at the --space-1 gap.\nexport const credentialCardBodyClass = \"flex min-w-0 flex-1 flex-col gap-(--space-1)\";\n\n// The label (spec §2/§5): the human-readable name of the credential KIND, a statement in\n// sentence case (for example \"Email\", \"Passkey\", \"Wallet\"), in the label type role + the PRIMARY\n// text color. The label says what kind of link this is, not who the identity is.\nexport const credentialCardLabelClass = \"text-label text-text-primary\";\n\n// The identifier (spec §2/§5/G-U6): the value that identifies this specific credential — the\n// email, the masked phone, the passkey device name, the truncated wallet address, or the SSO\n// provider + domain. It renders in the MONOSPACE type role (never the UI font for credential\n// strings) in the SECONDARY text color, isolated left-to-right so addresses, hashes, and wallet\n// strings stay readable even inside RTL text, and truncates rather than wrapping.\nexport const credentialCardIdentifierClass =\n \"text-mono text-text-secondary [direction:ltr] truncate\";\n\n// The status row (spec §2): at most one or two small Badges stating a fact about the credential —\n// `verified` (the green status, on the composed Badge's verified variant) or `primary` (the\n// credential you currently sign in with, a NEUTRAL Badge — \"primary\" is which credential is used,\n// a fact, not a status color and never the brand). The status describes the credential, not the\n// identity. This row only positions the composed Badges; their color lives in badge.variants.ts.\nexport const credentialCardStatusClass = \"flex shrink-0 flex-wrap items-center gap-(--space-1)\";\n\n// The meta line (spec §2/§5): quiet secondary text such as when the credential was added or last\n// used,
|
|
14
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// CredentialCard is one ROW in the list of credentials ATTACHED to an identity — a single way\n// to reach that identity (an email, phone, passkey, wallet, or enterprise SSO connection). It\n// encodes the platform's first invariant — identity is not credentials — in its UI contract: a\n// card is a LINK to an identity, never the identity itself, so it never reads as \"you\" and never\n// stands in for the account (spec §1/§8).\n//\n// It COMPOSES the committed primitives rather than reinventing them — a Card-like neutral surface\n// (the surface-* roles, bound here), the destructive Button for `remove`, the secondary Button\n// for an optional `action`, the Badge for a `status`, the Checkbox for `selectable`, and the\n// Button's in-place loading spinner for a resolving removal. So the focus ring, the target-size\n// floor, the keyboard model, and the control motion all come from those proven primitives; this\n// file binds ONLY the surrounding neutral surface + text-role + layout classes.\n//\n// brand != state (spec §3/§5/§8). The card SURFACE consumes NO action or status token of its own\n// — neutrals carry the card. The brand violet (Sovereign Violet, the action accent) is never a\n// card fill, never the icon, never the identifier, and never marks a credential as special. The\n// verified-status green is reserved for the `verified` status Badge and is never spent on the\n// card surface, the icon, or the identifier — coloring the card green would imply the IDENTITY is\n// verified, which is the one misreport this molecule forbids. The card therefore binds nothing\n// from the action tier and nothing from the status tier; those colors live on the controls and\n// Badges it holds (asserted positively in their own primitives' tests), never on this surface.\n//\n// The motion the card adds is none of its own: control hover/press uses the composed primitive's\n// fast functional transition on verdify easing, collapsing to the instant endpoint under reduced\n// motion. A resolving removal is a plain wait on the Button's ambient spinner — never the 350ms\n// VerifiedBadge-only theatre duration: a removal is not a verification (G-U3).\n\n// The card container (spec §2/§4/§5): a NEUTRAL raised surface, the same Card-static surface the\n// committed Card binds, but it is a LIST ROW (`<li role=\"listitem\">`, set in the tsx) so it sits\n// in the credential list's `<ul role=\"list\">` (spec §7). It is a static container, NOT a single\n// clickable control — the whole row does not map to one action — so it carries no focus ring and\n// no target-size floor of its own (those live on its controls, spec §4/§5). The `kind` axis is\n// which credential the card represents; it is carried by the `icon` + `label` TEXT, never by\n// color, so NONE of the kinds recolors the surface — every kind is the identical neutral surface\n// (spec §3). `selectable` composes with any kind and changes layout (a leading Checkbox), not\n// color, so it is not a color variant here.\nexport const credentialCardVariants = cva(\n [\n // raised neutral surface, rounded container, resting elevation, internal padding from --space-4\n \"relative flex items-start gap-(--space-2) rounded-lg border bg-surface-raised shadow-sm p-(--space-4)\",\n // the default container hairline (spec §5)\n \"border-surface-border\",\n // logical-property text alignment so the row mirrors under dir=rtl (G-U6); list rows carry\n // no bullet marker\n \"text-start text-text-primary list-none\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3 (the credential KIND the card represents). The kind is carried\n // by the icon + label text, never color, so every kind is the SAME neutral surface.\n kind: {\n email: \"\",\n phone: \"\",\n passkey: \"\",\n wallet: \"\",\n \"enterprise-sso\": \"\",\n },\n },\n defaultVariants: { kind: \"email\" },\n },\n);\n\n// The kind icon (spec §2/§5/§7): one small glyph for the credential kind at the md icon role. It\n// reinforces the kind shown in the label and is decorative (aria-hidden, set in the tsx) — the\n// label text still carries the kind if the icon is dropped, so the kind never rests on the icon\n// or color alone. Its fill is the SECONDARY text role (a neutral role), never a status color and\n// never the brand (spec §2/§3).\nexport const credentialCardIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center text-text-secondary\";\n\n// The label + identifier block (spec §2): the human-readable kind name above the value that\n// identifies this specific credential. Takes the remaining inline space between the icon and the\n// trailing status/controls; stacks the two lines at the --space-1 gap.\nexport const credentialCardBodyClass = \"flex min-w-0 flex-1 flex-col gap-(--space-1)\";\n\n// The label (spec §2/§5): the human-readable name of the credential KIND, a statement in\n// sentence case (for example \"Email\", \"Passkey\", \"Wallet\"), in the label type role + the PRIMARY\n// text color. The label says what kind of link this is, not who the identity is.\nexport const credentialCardLabelClass = \"text-label text-text-primary\";\n\n// The identifier (spec §2/§5/G-U6): the value that identifies this specific credential — the\n// email, the masked phone, the passkey device name, the truncated wallet address, or the SSO\n// provider + domain. It renders in the MONOSPACE type role (never the UI font for credential\n// strings) in the SECONDARY text color, isolated left-to-right so addresses, hashes, and wallet\n// strings stay readable even inside RTL text, and truncates rather than wrapping.\nexport const credentialCardIdentifierClass =\n \"text-mono text-text-secondary [direction:ltr] truncate\";\n\n// The status row (spec §2): at most one or two small Badges stating a fact about the credential —\n// `verified` (the green status, on the composed Badge's verified variant) or `primary` (the\n// credential you currently sign in with, a NEUTRAL Badge — \"primary\" is which credential is used,\n// a fact, not a status color and never the brand). The status describes the credential, not the\n// identity. This row only positions the composed Badges; their color lives in badge.variants.ts.\nexport const credentialCardStatusClass = \"flex shrink-0 flex-wrap items-center gap-(--space-1)\";\n\n// The meta line (spec §2/§5): quiet secondary text such as when the credential was added or last\n// used, at the caption role. Essential de-emphasized text (it conveys when), so it uses the SECONDARY\n// text color (AA), not the decorative-only muted role (accessibility.md). Never an alarm color.\nexport const credentialCardMetaClass = \"text-caption text-text-secondary\";\n\n// The trailing controls cluster (spec §2): the optional `action` beside the required `remove`,\n// aligned to the inline-end edge at the --space-2 gap. Keep the card to one primary action\n// (`remove`) plus at most one further low-emphasis control (restraint over volume). It only lays\n// the controls out; each control's treatment + focus ring + target-size floor live on the\n// composed Button.\nexport const credentialCardControlsClass = \"flex shrink-0 items-center gap-(--space-2)\";\n\n// The disabled-reason note (spec §4/§5/§7): when a control is disabled (for example `remove` on\n// the last sign-in credential), the reason is given as adjacent text at the caption role — wired to\n// the control as its accessible description (so the reason is heard, not just seen). It is essential\n// text, so it uses the SECONDARY text color (AA), not the decorative-only muted role\n// (accessibility.md), and is never communicated by graying alone.\nexport const credentialCardReasonClass = \"text-caption text-text-secondary\";\n\n// The removal-failure message (spec §4/§7): on a FAILED removal the credential stays in the list\n// and the failure is stated plainly where it happened, in the CRITICAL status foreground at the\n// caption role — naming what failed and what to do next, never blaming the reader and never an\n// exclamation mark. It is the ONLY status color this component binds, and only here (never on the\n// card surface, brand != state). It is announced through an assertive live region (role=alert,\n// set in the tsx) per 4.1.3.\nexport const credentialCardErrorClass = \"text-caption text-status-critical-on-surface\";\n\nexport type CredentialCardVariantProps = VariantProps<typeof credentialCardVariants>;\n",
|
|
15
15
|
"path": "credential-card/credential-card.variants.ts",
|
|
16
16
|
"target": "@ui/credential-card/credential-card.variants.ts",
|
|
17
17
|
"type": "registry:ui"
|
package/registry/data-grid.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"type": "registry:ui"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// A DataGrid shows many rows of structured records in a scrollable, operable, two-dimensional grid\n// you navigate one cell at a time (spec §1). It is a NEUTRAL data surface (spec §1/§3): neutrals\n// carry roughly 90% of it, and a dense grid earns its legibility from restraint. It paints from the\n// surface, text, and border roles; it reaches the --color-action-* tier only for the controls it\n// hosts (the sortable-header ghost accent, the row-hover affordance, the bulk-bar actions, and the\n// NEUTRAL selection accent) and the --color-status-* tier ONLY inside a cell that reports a real\n// state, paired with text — never as a row tint, a header fill, or the selection accent, and NEVER\n// a brand token as a status (brand != state, G-U2). Selection and the active cell are NEUTRAL action\n// states; a verified/trust state is a status cell (spec §3/§4/§8).\n\n// The scroll container <div role=\"grid\"> (spec §2 grid / §5). The neutral canvas surface and the\n// default cell text role, framed by the outer surface border at the md radius, with a fixed-height\n// scroll viewport (the caller sizes it via className — the token set has no grid-height scale, the\n// caller-owned-dimension precedent J). It NEVER wears the brand violet or a status fill (spec §3/§8)\n// — those belong to the controls and the status cells inside it. The active cell is kept scrolled\n// clear of the sticky header and pinned columns by scroll-margin (2.4.11 Focus Not Obscured).\nexport const dataGridVariants = cva([\n \"relative w-full overflow-auto\",\n \"bg-surface-canvas text-body text-text-primary\",\n \"border border-surface-border rounded-(--radius-md)\",\n]);\n\nexport type DataGridVariantProps = VariantProps<typeof dataGridVariants>;\n\n// The inner table element. The grid is a real <table> for the row/column relationship (1.3.1),\n// border-collapsed so the gridlines read as single hairlines, and start-aligned so it mirrors under\n// dir=\"rtl\" (G-U6).\nexport const dataGridTableClass = \"w-full border-collapse text-start\";\n\n// The column-header row <tr> (spec §2 column-header-row / §4 Default / §5). It is STICKY — it stays\n// pinned to the top of the scroll viewport while rows scroll under it — on the raised neutral surface\n// with the sm elevation so it reads ABOVE the scrolling rows (spec §4/§5). z on the sticky layer so a\n// scrolled cell never paints over the pinned header. A header row NEVER wears a status or brand tint\n// (spec §3/§8).\nexport const dataGridHeaderRowClass =\n \"sticky top-0 z-(--z-index-sticky) bg-surface-raised shadow-sm \" +\n \"border-b border-border-default\";\n\n// The shared cell padding by density (spec §3 density / §5 --space-*). Density tightens the VERTICAL\n// padding only, ABOVE the a11y floor — any in-cell control keeps its own --size-target-* floor\n// (DEC-B: never a fixed height below the floor). Horizontal inline padding is constant.\nconst cellPaddingVariants = {\n density: {\n comfortable: \"py-(--space-3)\",\n compact: \"py-(--space-1)\",\n },\n} as const;\n\n// A data row <tr> (spec §4 Default/Hover/Selected). RESTING: no fill, on the canvas. HOVER: a\n// restrained GHOST fill to track the eye across a wide row — an AFFORDANCE, never the sole carrier of\n// meaning, and never a selection (nothing is selected until you act, spec §4 Hover). SELECTED\n// (aria-selected): the NEUTRAL secondary-action selection accent — selection is a neutral action\n// state, NOT verified green and NOT the brand violet (selecting a row never implies it is verified;\n// brand != state, G-U2, spec §4/§8). Selection is encoded by the row checkbox + aria-selected, so a\n// grayscale reader reads it from the checkbox, not the fill (1.4.1). Motion is the fast token\n// transition on the verdify easing, instant under reduced motion — never the 350ms VerifiedBadge-only\n// theatre (a row hover/select is a plain transition, G-U3).\nexport const dataGridRowClass =\n \"border-b border-border-default \" +\n \"hover:bg-action-ghost-bg-hover \" +\n \"aria-selected:bg-action-secondary-bg-hover \" +\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\n// A data cell <td role=\"gridcell\"> (spec §2 cell, §4/§5). It is ONE focusable unit in the roving\n// grid: exactly one cell is the active cell (tabindex=0) and shows the visible 2px focus ring; every\n// other cell is tabindex=-1. The ring is part of the base and is NEVER removed (spec §4 Focus /\n// 2.4.7). scroll-margin keeps the active cell clear of the sticky header + pinned columns before it\n// takes focus (spec §7, 2.4.11 Focus Not Obscured). Default: the primary text color at the body type\n// role. A `mono` cell takes the monospace role and is isolated LTR so an identifier/key/timestamp\n// stays readable inside an RTL layout (spec §3/§5, G-U6). A `secondary` cell is de-emphasized\n// auxiliary text. A `status` cell carries the status fg paired with the cell's words — the status\n// color lives in the CELL only, never the row or header (spec §3), and NEVER a brand token (brand !=\n// state, G-U2); status-*-bg is the one neutral raised surface, so meaning is carried by the fg + the\n// text, not a saturated fill.\nexport const dataGridCellVariants = cva(\n [\n \"px-(--space-3) align-middle text-start text-body text-text-primary\",\n // the roving active cell's focus ring — always visible, never removed (2.4.7)\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // keep the active cell clear of the sticky header + a pinned column before it takes focus (2.4.11)\n \"scroll-mt-(--space-12) scroll-ms-(--space-12)\",\n ],\n {\n variants: {\n ...cellPaddingVariants,\n mono: {\n // identifier/key/timestamp: the monospace role, isolated LTR inside RTL text (G-U6)\n true: \"text-mono [direction:ltr]\",\n false: \"\",\n },\n secondary: {\n // de-emphasized secondary cell text (spec §5 --color-text-secondary)\n true: \"text-text-secondary\",\n false: \"\",\n },\n status: {\n none: \"\",\n // each status is the fg only, paired with the cell's text — the words carry the meaning,\n // the fg reinforces it (spec §3/§5); never a saturated -bg fill, never the brand\n verified: \"text-status-verified-fg\",\n signal: \"text-status-signal-fg\",\n caution: \"text-status-caution-fg\",\n critical: \"text-status-critical-fg\",\n },\n },\n defaultVariants: {\n density: \"comfortable\",\n mono: false,\n secondary: false,\n status: \"none\",\n },\n },\n);\n\nexport type DataGridCellVariantProps = VariantProps<typeof dataGridCellVariants>;\n\n// A column-header cell <th role=\"columnheader\"> (spec §2 column-header / §4/§5). The header LABEL is\n// the SECONDARY text color at the label type role (the quiet, tracked column label) on the raised\n// header surface — a header NEVER wears a status or brand tint (spec §3/§8). It is also a roving\n// active-cell target, so it carries the same focus ring + scroll-margin as a data cell.\nexport const dataGridColumnHeaderVariants = cva(\n [\n \"px-(--space-3) align-middle text-start text-label text-text-secondary\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n \"scroll-mt-(--space-12) scroll-ms-(--space-12)\",\n ],\n {\n variants: { ...cellPaddingVariants },\n defaultVariants: { density: \"comfortable\" },\n },\n);\n\nexport type DataGridColumnHeaderVariantProps = VariantProps<typeof dataGridColumnHeaderVariants>;\n\n// The SORTABLE-header control (spec §2/§4 Sorted/§6/§7): a real <button> inside the columnheader, so\n// it reads as the control it is and toggles the sort on Enter. It is the GHOST action accent — the\n// label + caret in the ghost fg with the restrained ghost hover fill (spec §4 sortable-header hover /\n// §5) — the action tier is legitimate here because it is a control the grid HOSTS, not a header tint.\n// It carries the target-size floor (40px desktop / 44px touch, spec §7) and inherits the active\n// cell's focus ring from the columnheader. Motion is the fast token transition, never the deliberate\n// verified-check theatre (G-U3). aria-sort lives on the parent th, and the caret encodes direction\n// alongside it so it never rests on color alone (spec §4 Sorted / 1.4.1).\nexport const dataGridSortButtonClass =\n \"inline-flex items-center gap-(--space-1) -mx-(--space-1) px-(--space-1) rounded-(--radius-sm) \" +\n \"text-label text-action-ghost-fg cursor-pointer select-none \" +\n \"hover:bg-action-ghost-bg-hover \" +\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) \" +\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\";\n\n// The sort-direction caret (spec §4 Sorted / §5): the sm icon role, decorative (the direction is also\n// encoded by aria-sort on the th + the glyph's shape via data-direction, so it never rests on color\n// alone — 1.4.1). It inherits the ghost accent color from the button.\nexport const dataGridSortCaretClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center\";\n\n// The selection cell <td role=\"gridcell\"> and the select-all header cell (spec §2 selection-cell /\n// §5): the leading cell that holds the row's Checkbox, carrying the active-cell focus ring +\n// scroll-margin like any other cell. The checkbox is the committed Checkbox component (reused, not\n// re-rolled), which already binds the control surface + border tokens (spec §5) and the brand action\n// accent on the CHECKED box — a checked checkbox is the brand accent, never status-verified (G-U2):\n// selection is a neutral action state. This wrapper is just the cell padding + the roving focus ring.\nexport const dataGridSelectionCellClass =\n \"px-(--space-3) align-middle text-start \" +\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 \" +\n \"scroll-mt-(--space-12) scroll-ms-(--space-12)\";\n\n// The bulk-action bar (spec §2 bulk-action-bar / §4/§5): a toolbar that appears when rows are\n// selected, on the raised neutral surface with the sm elevation and a hairline top border, holding\n// the selection actions + a clear-selection control. It is a NEUTRAL surface — the COLOR lives on the\n// action buttons it holds, never on the bar (spec §3/§8). Motion is the fast token transition (the\n// bar's appear/transition), never the deliberate verified-check theatre (G-U3).\nexport const dataGridBulkBarClass =\n \"flex items-center gap-(--space-3) px-(--space-3) py-(--space-2) \" +\n \"bg-surface-raised border-t border-border-default \" +\n \"transition-opacity duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\n// The selected-count label in the bulk-action bar (spec §2): the secondary text at the label role —\n// the count of selected rows, in words, so selection is announced, never color alone (1.4.1 / 4.1.3).\nexport const dataGridBulkCountClass = \"text-label text-text-secondary\";\n\n// A bulk-action button (spec §2/§5): the PRIMARY selection action is the primary ACTION accent; a\n// `destructive` action (for example, revoke a key) is the destructive ACTION accent — a risk signal\n// named in TEXT, never a status color (spec §5/§8). Both carry the visible focus ring + target-size\n// floor. Motion is the fast token transition, never the deliberate verified-check theatre (G-U3).\nexport const dataGridBulkActionVariants = cva(\n [\n \"inline-flex items-center justify-center gap-(--space-1) rounded-(--radius-md) px-(--space-3)\",\n \"text-label font-medium cursor-pointer\",\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n ],\n {\n variants: {\n destructive: {\n // a destructive bulk action — a risk signal in the ACTION tier, NEVER a status token\n true: \"bg-action-destructive-bg text-action-destructive-fg border border-action-destructive-border\",\n // the default bulk action — the primary action accent\n false: \"bg-action-primary-bg text-action-primary-fg border border-action-primary-border hover:bg-action-primary-bg-hover\",\n },\n },\n defaultVariants: { destructive: false },\n },\n);\n\nexport type DataGridBulkActionVariantProps = VariantProps<typeof dataGridBulkActionVariants>;\n\n// The empty-state cell (spec §2/§4 Empty): a plain line spanning the full grid width, in the muted\n// text color — an empty grid is NOT an error and never reads as one (no status color). Its copy says\n// why it is empty and what to do next, in plain words ending in a period (spec §4 Empty / voice).\nexport const dataGridEmptyClass =\n \"px-(--space-3) py-(--space-6) text-center text-body text-text-muted\";\n\n// The off-screen-capable live region (spec §2 status-region / §7 4.1.3): announces the resolved row\n// count, sort + filter changes, and the selection count politely; a blocking row-load error\n// assertively. Always sr-only (it never paints — the visual state carries the same information for\n// sighted users), so it reaches assistive tech as TEXT, never color alone (1.4.1 / 4.1.3).\nexport const dataGridStatusRegionClass = \"sr-only\";\n",
|
|
14
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// A DataGrid shows many rows of structured records in a scrollable, operable, two-dimensional grid\n// you navigate one cell at a time (spec §1). It is a NEUTRAL data surface (spec §1/§3): neutrals\n// carry roughly 90% of it, and a dense grid earns its legibility from restraint. It paints from the\n// surface, text, and border roles; it reaches the --color-action-* tier only for the controls it\n// hosts (the sortable-header ghost accent, the row-hover affordance, the bulk-bar actions, and the\n// NEUTRAL selection accent) and the --color-status-* tier ONLY inside a cell that reports a real\n// state, paired with text — never as a row tint, a header fill, or the selection accent, and NEVER\n// a brand token as a status (brand != state, G-U2). Selection and the active cell are NEUTRAL action\n// states; a verified/trust state is a status cell (spec §3/§4/§8).\n\n// The scroll container <div role=\"grid\"> (spec §2 grid / §5). The neutral canvas surface and the\n// default cell text role, framed by the outer surface border at the md radius, with a fixed-height\n// scroll viewport (the caller sizes it via className — the token set has no grid-height scale, the\n// caller-owned-dimension precedent J). It NEVER wears the brand violet or a status fill (spec §3/§8)\n// — those belong to the controls and the status cells inside it. The active cell is kept scrolled\n// clear of the sticky header and pinned columns by scroll-margin (2.4.11 Focus Not Obscured).\nexport const dataGridVariants = cva([\n \"relative w-full overflow-auto\",\n \"bg-surface-canvas text-body text-text-primary\",\n \"border border-surface-border rounded-(--radius-md)\",\n]);\n\nexport type DataGridVariantProps = VariantProps<typeof dataGridVariants>;\n\n// The inner table element. The grid is a real <table> for the row/column relationship (1.3.1),\n// border-collapsed so the gridlines read as single hairlines, and start-aligned so it mirrors under\n// dir=\"rtl\" (G-U6).\nexport const dataGridTableClass = \"w-full border-collapse text-start\";\n\n// The column-header row <tr> (spec §2 column-header-row / §4 Default / §5). It is STICKY — it stays\n// pinned to the top of the scroll viewport while rows scroll under it — on the raised neutral surface\n// with the sm elevation so it reads ABOVE the scrolling rows (spec §4/§5). z on the sticky layer so a\n// scrolled cell never paints over the pinned header. A header row NEVER wears a status or brand tint\n// (spec §3/§8).\nexport const dataGridHeaderRowClass =\n \"sticky top-0 z-(--z-index-sticky) bg-surface-raised shadow-sm \" +\n \"border-b border-border-default\";\n\n// The shared cell padding by density (spec §3 density / §5 --space-*). Density tightens the VERTICAL\n// padding only, ABOVE the a11y floor — any in-cell control keeps its own --size-target-* floor\n// (DEC-B: never a fixed height below the floor). Horizontal inline padding is constant.\nconst cellPaddingVariants = {\n density: {\n comfortable: \"py-(--space-3)\",\n compact: \"py-(--space-1)\",\n },\n} as const;\n\n// A data row <tr> (spec §4 Default/Hover/Selected). RESTING: no fill, on the canvas. HOVER: a\n// restrained GHOST fill to track the eye across a wide row — an AFFORDANCE, never the sole carrier of\n// meaning, and never a selection (nothing is selected until you act, spec §4 Hover). SELECTED\n// (aria-selected): the NEUTRAL secondary-action selection accent — selection is a neutral action\n// state, NOT verified green and NOT the brand violet (selecting a row never implies it is verified;\n// brand != state, G-U2, spec §4/§8). Selection is encoded by the row checkbox + aria-selected, so a\n// grayscale reader reads it from the checkbox, not the fill (1.4.1). Motion is the fast token\n// transition on the verdify easing, instant under reduced motion — never the 350ms VerifiedBadge-only\n// theatre (a row hover/select is a plain transition, G-U3).\nexport const dataGridRowClass =\n \"border-b border-border-default \" +\n \"hover:bg-action-ghost-bg-hover \" +\n \"aria-selected:bg-action-secondary-bg-hover \" +\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\n// A data cell <td role=\"gridcell\"> (spec §2 cell, §4/§5). It is ONE focusable unit in the roving\n// grid: exactly one cell is the active cell (tabindex=0) and shows the visible 2px focus ring; every\n// other cell is tabindex=-1. The ring is part of the base and is NEVER removed (spec §4 Focus /\n// 2.4.7). scroll-margin keeps the active cell clear of the sticky header + pinned columns before it\n// takes focus (spec §7, 2.4.11 Focus Not Obscured). Default: the primary text color at the body type\n// role. A `mono` cell takes the monospace role and is isolated LTR so an identifier/key/timestamp\n// stays readable inside an RTL layout (spec §3/§5, G-U6). A `secondary` cell is de-emphasized\n// auxiliary text. A `status` cell carries the status fg paired with the cell's words — the status\n// color lives in the CELL only, never the row or header (spec §3), and NEVER a brand token (brand !=\n// state, G-U2); status-*-bg is the one neutral raised surface, so meaning is carried by the fg + the\n// text, not a saturated fill.\nexport const dataGridCellVariants = cva(\n [\n \"px-(--space-3) align-middle text-start text-body text-text-primary\",\n // the roving active cell's focus ring — always visible, never removed (2.4.7)\n focusRing,\n // keep the active cell clear of the sticky header + a pinned column before it takes focus (2.4.11)\n \"scroll-mt-(--space-12) scroll-ms-(--space-12)\",\n ],\n {\n variants: {\n ...cellPaddingVariants,\n mono: {\n // identifier/key/timestamp: the monospace role, isolated LTR inside RTL text (G-U6)\n true: \"text-mono [direction:ltr]\",\n false: \"\",\n },\n secondary: {\n // de-emphasized secondary cell text (spec §5 --color-text-secondary)\n true: \"text-text-secondary\",\n false: \"\",\n },\n status: {\n none: \"\",\n // each status is the fg only, paired with the cell's text — the words carry the meaning,\n // the fg reinforces it (spec §3/§5); never a saturated -bg fill, never the brand\n verified: \"text-status-verified-on-surface\",\n signal: \"text-status-signal-on-surface\",\n caution: \"text-status-caution-on-surface\",\n critical: \"text-status-critical-on-surface\",\n },\n },\n defaultVariants: {\n density: \"comfortable\",\n mono: false,\n secondary: false,\n status: \"none\",\n },\n },\n);\n\nexport type DataGridCellVariantProps = VariantProps<typeof dataGridCellVariants>;\n\n// A column-header cell <th role=\"columnheader\"> (spec §2 column-header / §4/§5). The header LABEL is\n// the SECONDARY text color at the label type role (the quiet, tracked column label) on the raised\n// header surface — a header NEVER wears a status or brand tint (spec §3/§8). It is also a roving\n// active-cell target, so it carries the same focus ring + scroll-margin as a data cell.\nexport const dataGridColumnHeaderVariants = cva(\n [\n \"px-(--space-3) align-middle text-start text-label text-text-secondary\",\n focusRing,\n \"scroll-mt-(--space-12) scroll-ms-(--space-12)\",\n ],\n {\n variants: { ...cellPaddingVariants },\n defaultVariants: { density: \"comfortable\" },\n },\n);\n\nexport type DataGridColumnHeaderVariantProps = VariantProps<typeof dataGridColumnHeaderVariants>;\n\n// The SORTABLE-header control (spec §2/§4 Sorted/§6/§7): a real <button> inside the columnheader, so\n// it reads as the control it is and toggles the sort on Enter. It is the GHOST action accent — the\n// label + caret in the ghost fg with the restrained ghost hover fill (spec §4 sortable-header hover /\n// §5) — the action tier is legitimate here because it is a control the grid HOSTS, not a header tint.\n// It carries the target-size floor (40px desktop / 44px touch, spec §7) and inherits the active\n// cell's focus ring from the columnheader. Motion is the fast token transition, never the deliberate\n// verified-check theatre (G-U3). aria-sort lives on the parent th, and the caret encodes direction\n// alongside it so it never rests on color alone (spec §4 Sorted / 1.4.1).\nexport const dataGridSortButtonClass =\n \"inline-flex items-center gap-(--space-1) -mx-(--space-1) px-(--space-1) rounded-(--radius-sm) \" +\n \"text-label text-action-ghost-fg cursor-pointer select-none \" +\n \"hover:bg-action-ghost-bg-hover \" +\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) \" +\n focusRing;\n\n// The sort-direction caret (spec §4 Sorted / §5): the sm icon role, decorative (the direction is also\n// encoded by aria-sort on the th + the glyph's shape via data-direction, so it never rests on color\n// alone — 1.4.1). It inherits the ghost accent color from the button.\nexport const dataGridSortCaretClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center\";\n\n// The selection cell <td role=\"gridcell\"> and the select-all header cell (spec §2 selection-cell /\n// §5): the leading cell that holds the row's Checkbox, carrying the active-cell focus ring +\n// scroll-margin like any other cell. The checkbox is the committed Checkbox component (reused, not\n// re-rolled), which already binds the control surface + border tokens (spec §5) and the brand action\n// accent on the CHECKED box — a checked checkbox is the brand accent, never status-verified (G-U2):\n// selection is a neutral action state. This wrapper is just the cell padding + the roving focus ring.\nexport const dataGridSelectionCellClass =\n \"px-(--space-3) align-middle text-start \" +\n focusRing + \" \" +\n \"scroll-mt-(--space-12) scroll-ms-(--space-12)\";\n\n// The bulk-action bar (spec §2 bulk-action-bar / §4/§5): a toolbar that appears when rows are\n// selected, on the raised neutral surface with the sm elevation and a hairline top border, holding\n// the selection actions + a clear-selection control. It is a NEUTRAL surface — the COLOR lives on the\n// action buttons it holds, never on the bar (spec §3/§8). Motion is the fast token transition (the\n// bar's appear/transition), never the deliberate verified-check theatre (G-U3).\nexport const dataGridBulkBarClass =\n \"flex items-center gap-(--space-3) px-(--space-3) py-(--space-2) \" +\n \"bg-surface-raised border-t border-border-default \" +\n \"transition-opacity duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\n// The selected-count label in the bulk-action bar (spec §2): the secondary text at the label role —\n// the count of selected rows, in words, so selection is announced, never color alone (1.4.1 / 4.1.3).\nexport const dataGridBulkCountClass = \"text-label text-text-secondary\";\n\n// A bulk-action button (spec §2/§5): the PRIMARY selection action is the primary ACTION accent; a\n// `destructive` action (for example, revoke a key) is the destructive ACTION accent — a risk signal\n// named in TEXT, never a status color (spec §5/§8). Both carry the visible focus ring + target-size\n// floor. Motion is the fast token transition, never the deliberate verified-check theatre (G-U3).\nexport const dataGridBulkActionVariants = cva(\n [\n \"inline-flex items-center justify-center gap-(--space-1) rounded-(--radius-md) px-(--space-3)\",\n \"text-label font-medium cursor-pointer\",\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n focusRing,\n ],\n {\n variants: {\n destructive: {\n // a destructive bulk action — a risk signal in the ACTION tier, NEVER a status token\n true: \"bg-action-destructive-bg text-action-destructive-fg border border-action-destructive-border\",\n // the default bulk action — the primary action accent\n false: \"bg-action-primary-bg text-action-primary-fg border border-action-primary-border hover:bg-action-primary-bg-hover\",\n },\n },\n defaultVariants: { destructive: false },\n },\n);\n\nexport type DataGridBulkActionVariantProps = VariantProps<typeof dataGridBulkActionVariants>;\n\n// The empty-state cell (spec §2/§4 Empty): a plain line spanning the full grid width — an empty grid\n// is NOT an error and never reads as one (no status color). Its copy says why it is empty and what to\n// do next (spec §4 Empty / voice). Essential text, so it uses the SECONDARY text color (AA), not the\n// decorative-only muted role (accessibility.md).\nexport const dataGridEmptyClass =\n \"px-(--space-3) py-(--space-6) text-center text-body text-text-secondary\";\n\n// The off-screen-capable live region (spec §2 status-region / §7 4.1.3): announces the resolved row\n// count, sort + filter changes, and the selection count politely; a blocking row-load error\n// assertively. Always sr-only (it never paints — the visual state carries the same information for\n// sighted users), so it reaches assistive tech as TEXT, never color alone (1.4.1 / 4.1.3).\nexport const dataGridStatusRegionClass = \"sr-only\";\n",
|
|
15
15
|
"path": "data-grid/data-grid.variants.ts",
|
|
16
16
|
"target": "@ui/data-grid/data-grid.variants.ts",
|
|
17
17
|
"type": "registry:ui"
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"name": "data-grid",
|
|
27
27
|
"registryDependencies": [
|
|
28
28
|
"@verdify/cn",
|
|
29
|
+
"@verdify/focus-ring",
|
|
29
30
|
"@verdify/checkbox"
|
|
30
31
|
],
|
|
31
32
|
"title": "data-grid",
|