@verdify/ui 0.2.2 → 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/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/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.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/credential-card/credential-card.variants.d.ts +2 -2
- package/dist/components/credential-card/credential-card.variants.d.ts.map +1 -1
- package/dist/components/credential-card/credential-card.variants.js +2 -2
- 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 +7 -6
- 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 +2 -1
- package/dist/components/input/input.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/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 +1 -1
- package/dist/components/select/select.variants.d.ts.map +1 -1
- package/dist/components/select/select.variants.js +3 -2
- 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.variants.d.ts.map +1 -1
- package/dist/components/table/table.variants.js +5 -3
- 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 +1 -1
- 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 +3 -3
- package/registry/accordion.json +3 -2
- 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 +3 -2
- package/registry/command-palette.json +3 -2
- 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/menu.json +4 -3
- package/registry/pagination.json +3 -2
- package/registry/popover.json +3 -2
- 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 +2 -1
- 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
package/registry/sidebar.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"type": "registry:ui"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// A Sidebar is the primary navigation rail down one edge of an app shell (spec §1). It is a\n// NEUTRAL surface (spec §3): the rail does not wear the brand. The only accent is on the CURRENT\n// item, which takes the primary ACTION (brand) alias — where you are — never a status color: a\n// current item reports your location, not a verification result, so it binds nothing from the\n// status tier (brand != state, G-U2). A verified meaning belongs to VerifiedBadge. The rail\n// paints from the surface, text, action(primary + ghost), and border aliases only (spec §5).\n\n// The nav landmark wrapping the rail. The raised neutral surface against the page canvas, an edge\n// against the content area (a logical inline-end border when docked inline-start, inline-start when\n// docked inline-end — G-U6, so it mirrors under dir=\"rtl\"), the sm elevation where it floats above\n// content, and the expand/collapse WIDTH transition. Motion is the BASE token transition on the\n// verdify easing, collapsing to the instant endpoint under reduced motion — never the 350ms\n// VerifiedBadge-only theatre duration (toggling the rail is a plain transition, not theatre — spec\n// §4 Collapsed, G-U3). The rail is NEVER tinted with the brand or a status fill (spec §3/§8).\nexport const sidebarRailVariants = cva(\n [\n // surface + elevation + column layout with the rail insets\n \"flex flex-col gap-(--space-2) bg-surface-raised shadow-sm\",\n \"p-(--space-2)\",\n // the expand/collapse width transition — base duration + verdify easing, instant under\n // reduced motion (NEVER the deliberate verified-check theatre, G-U3)\n \"transition-[width] duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n ],\n {\n variants: {\n // SIDE axis (spec §3): the rail docks at a logical edge and carries its border on the edge\n // that faces the content area — inline-end when docked inline-start, inline-start when docked\n // inline-end. Logical properties only (border-e / border-s), so it mirrors under dir=\"rtl\".\n side: {\n \"inline-start\": \"border-e border-surface-border\",\n \"inline-end\": \"border-s border-surface-border\",\n },\n },\n defaultVariants: { side: \"inline-start\" },\n },\n);\n\nexport type SidebarRailVariantProps = VariantProps<typeof sidebarRailVariants>;\n\n// The header slot (spec §2): the top slot for a product mark or workspace switcher. It is NOT a\n// navigation item and is skipped by item arrow movement. A separator from the list below it via a\n// muted hairline (spec §5, surface-border-muted). Plain structural slot — no variant axis.\nexport const sidebarHeaderClass =\n \"flex items-center gap-(--space-2) px-(--space-2) py-(--space-2) border-b border-surface-border-muted\";\n\n// A labeled cluster of items (spec §2 group). Restraint over volume keeps the rail scannable, so a\n// group is plain structure; its dividers use the muted hairline (spec §5). No variant axis.\nexport const sidebarGroupClass = \"flex flex-col gap-(--space-1) py-(--space-2)\";\n\n// The group heading (spec §2/§5): the cluster label in the MUTED text color at the CAPTION type\n// role. Decorative-weight wayfinding, not a navigation item.\nexport const sidebarGroupLabelClass =\n \"px-(--space-2) py-(--space-1) text-caption text-text-muted select-none\";\n\n// The item list (the <ul>): a flush column of items. The list carries no text color — each item\n// sets its own. The roving arrow-key handler is wired by the component on this element.\nexport const sidebarListClass = \"flex flex-col gap-(--space-1) m-0 p-0 list-none\";\n\n// One navigation item (spec §2 item, §4 states). A native <a href> link.\n//\n// RESTING (default): the LABEL in the SECONDARY text color and the icon in the GHOST action fg, on\n// the rail surface with NO fill (spec §4 Default).\n// HOVER: a restrained ghost-action hover fill; the cursor is a pointer (spec §4 Hover). The only\n// fill a resting item ever paints.\n// CURRENT (aria-current=page): the leading INDICATOR BAR is painted in the primary ACTION (brand)\n// alias — where you are — and the label lifts to the PRIMARY text color. This spec offers two\n// indicator treatments (spec §5: an indicator \"bar OR fill\"); this component uses the restrained\n// BAR (a logical inline-start border accent) so the rail never wears a brand SURFACE (spec §3/§8).\n// The current state is never carried by color alone: the indicator bar shape AND aria-current also\n// encode it, so it survives a contrast or color-blind reading (spec §4 Current / 1.4.1, brand !=\n// state — NEVER status-verified).\n// FOCUS: the visible 2px focus ring, part of the base on every state, never removed; it persists in\n// both the expanded and collapsed rail (spec §4 Focus / 2.4.7).\n// DISABLED (aria-disabled): dims via the disabled TOKEN (DEC-C), never a blanket opacity; the\n// component also strips href + tabindex and skips it in arrow movement, while the label stays\n// readable to AT (spec §4 Disabled / §7).\n// Motion is the fast token transition on the verdify easing, instant under reduced motion — never\n// the 350ms VerifiedBadge-only theatre duration (G-U3).\nexport const sidebarItemVariants = cva(\n [\n // shape + the icon-to-label gap + logical inline padding so it mirrors under RTL; the leading\n // indicator bar is a left/start accent rendered via a logical inline-start border that is\n // transparent at rest and painted in the action alias when current\n \"relative flex items-center gap-(--space-3) rounded-(--radius-md) px-(--space-3)\",\n \"border-s-2 border-transparent\",\n // type ROLE + resting label color, no fill, pointer cursor\n \"text-label text-text-secondary no-underline cursor-pointer select-none\",\n // hover: the restrained ghost-action fill (the only fill a resting item paints)\n \"hover:bg-action-ghost-bg-hover\",\n // CURRENT: the leading indicator BAR is painted in the brand action alias and the label lifts to\n // the primary text color. This is the action(primary) alias (where you are), NEVER status-verified\n // (brand != state, G-U2). The indicator bar SHAPE + aria-current carry the state alongside the\n // color, so it survives a color-blind read; the rail itself stays a neutral surface (no brand fill).\n \"aria-[current=page]:border-s-action-primary-bg aria-[current=page]:text-text-primary\",\n // motion: fast + verdify easing, instant under reduced motion (NEVER the check theatre, G-U3)\n \"transition-[color,background-color,border-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor — 44px touch / 40px pointer, on every item (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 ring — identical on every state, never removed; persists expanded and collapsed\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // disabled (non-operable) item — DEC-C: dim via the disabled TOKEN, never opacity. aria-disabled\n // drives it because an <a> has no native disabled; the component also strips href + tabindex.\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled\",\n ],\n { variants: {}, defaultVariants: {} },\n);\n\nexport type SidebarItemVariantProps = VariantProps<typeof sidebarItemVariants>;\n\n// The leading item icon (spec §5): the md icon role, decorative (the item names itself by its label\n// text, not the glyph). At rest it is the GHOST action fg; it inherits the disabled token when the\n// item is disabled (aria-disabled on the parent link). When the item is current it lifts with the\n// label to the primary text color. shrink-0 so it never collapses when labels are hidden.\nexport const sidebarItemIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center text-action-ghost-fg\";\n\n// The item label text (spec §5): the part hidden in the collapsed rail. When collapsed the label is\n// visually hidden but stays in the accessibility tree (sr-only), so the link keeps its accessible\n// name without depending on a tooltip being open (spec §4 Collapsed / §7).\nexport const sidebarItemLabelClass = \"min-w-0 flex-1 truncate\";\n\n// A trailing count or status on an item (spec §2): text or an aria-label, never color alone (spec\n// §7). Plain secondary caption text, hidden with the label when collapsed.\nexport const sidebarItemTrailingClass =\n \"ms-auto inline-flex items-center text-caption text-text-secondary\";\n\n// The footer slot (spec §2): a bottom slot for an account or settings entry; its items follow the\n// same item rules. Pushed to the bottom; separated from the list above by the muted hairline.\nexport const sidebarFooterClass =\n \"mt-auto flex flex-col gap-(--space-1) pt-(--space-2) border-t border-surface-border-muted\";\n\n// The collapse-toggle (spec §2/§4/§6/§7): a native <button>, NOT an item, reachable in the rail's\n// tab order. It carries the ghost-action glyph and the same neutral hover fill, focus ring, and\n// target-size floor as an item. Its glyph is decorative (aria-hidden); aria-expanded + aria-label\n// carry the action. Same fast motion, never the deliberate theatre.\nexport const sidebarCollapseToggleClass = cva(\n [\n \"inline-flex items-center justify-center gap-(--space-2) rounded-(--radius-md) px-(--space-2)\",\n \"text-action-ghost-fg cursor-pointer select-none\",\n \"hover:bg-action-ghost-bg-hover\",\n \"transition-[color,background-color] 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\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n \"disabled:pointer-events-none disabled:text-text-disabled\",\n ],\n { variants: {}, defaultVariants: {} },\n);\n\nexport type SidebarCollapseToggleVariantProps = VariantProps<typeof sidebarCollapseToggleClass>;\n\n// The collapse-toggle glyph (spec §5): the md icon role, decorative. It rotates to mirror the rail\n// width (collapsed vs expanded) as a reinforcement; aria-expanded carries the state, not the glyph.\nexport const sidebarCollapseIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center transition-transform duration-(--motion-duration-base) ease-(--motion-easing-verdify) motion-reduce:duration-(--motion-duration-instant)\";\n",
|
|
21
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// A Sidebar is the primary navigation rail down one edge of an app shell (spec §1). It is a\n// NEUTRAL surface (spec §3): the rail does not wear the brand. The only accent is on the CURRENT\n// item, which takes the primary ACTION (brand) alias — where you are — never a status color: a\n// current item reports your location, not a verification result, so it binds nothing from the\n// status tier (brand != state, G-U2). A verified meaning belongs to VerifiedBadge. The rail\n// paints from the surface, text, action(primary + ghost), and border aliases only (spec §5).\n\n// The nav landmark wrapping the rail. The raised neutral surface against the page canvas, an edge\n// against the content area (a logical inline-end border when docked inline-start, inline-start when\n// docked inline-end — G-U6, so it mirrors under dir=\"rtl\"), the sm elevation where it floats above\n// content, and the expand/collapse WIDTH transition. Motion is the BASE token transition on the\n// verdify easing, collapsing to the instant endpoint under reduced motion — never the 350ms\n// VerifiedBadge-only theatre duration (toggling the rail is a plain transition, not theatre — spec\n// §4 Collapsed, G-U3). The rail is NEVER tinted with the brand or a status fill (spec §3/§8).\nexport const sidebarRailVariants = cva(\n [\n // surface + elevation + column layout with the rail insets\n \"flex flex-col gap-(--space-2) bg-surface-raised shadow-sm\",\n \"p-(--space-2)\",\n // the expand/collapse width transition — base duration + verdify easing, instant under\n // reduced motion (NEVER the deliberate verified-check theatre, G-U3)\n \"transition-[width] duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n ],\n {\n variants: {\n // SIDE axis (spec §3): the rail docks at a logical edge and carries its border on the edge\n // that faces the content area — inline-end when docked inline-start, inline-start when docked\n // inline-end. Logical properties only (border-e / border-s), so it mirrors under dir=\"rtl\".\n side: {\n \"inline-start\": \"border-e border-surface-border\",\n \"inline-end\": \"border-s border-surface-border\",\n },\n },\n defaultVariants: { side: \"inline-start\" },\n },\n);\n\nexport type SidebarRailVariantProps = VariantProps<typeof sidebarRailVariants>;\n\n// The header slot (spec §2): the top slot for a product mark or workspace switcher. It is NOT a\n// navigation item and is skipped by item arrow movement. A separator from the list below it via a\n// muted hairline (spec §5, surface-border-muted). Plain structural slot — no variant axis.\nexport const sidebarHeaderClass =\n \"flex items-center gap-(--space-2) px-(--space-2) py-(--space-2) border-b border-surface-border-muted\";\n\n// A labeled cluster of items (spec §2 group). Restraint over volume keeps the rail scannable, so a\n// group is plain structure; its dividers use the muted hairline (spec §5). No variant axis.\nexport const sidebarGroupClass = \"flex flex-col gap-(--space-1) py-(--space-2)\";\n\n// The group heading (spec §2/§5): the cluster label in the SECONDARY text color at the CAPTION type\n// role. It is essential de-emphasized text — it names the cluster of items — so it uses\n// --color-text-secondary (AA), not the decorative-only muted role (accessibility.md).\nexport const sidebarGroupLabelClass =\n \"px-(--space-2) py-(--space-1) text-caption text-text-secondary select-none\";\n\n// The item list (the <ul>): a flush column of items. The list carries no text color — each item\n// sets its own. The roving arrow-key handler is wired by the component on this element.\nexport const sidebarListClass = \"flex flex-col gap-(--space-1) m-0 p-0 list-none\";\n\n// One navigation item (spec §2 item, §4 states). A native <a href> link.\n//\n// RESTING (default): the LABEL in the SECONDARY text color and the icon in the GHOST action fg, on\n// the rail surface with NO fill (spec §4 Default).\n// HOVER: a restrained ghost-action hover fill; the cursor is a pointer (spec §4 Hover). The only\n// fill a resting item ever paints.\n// CURRENT (aria-current=page): the leading INDICATOR BAR is painted in the primary ACTION (brand)\n// alias — where you are — and the label lifts to the PRIMARY text color. This spec offers two\n// indicator treatments (spec §5: an indicator \"bar OR fill\"); this component uses the restrained\n// BAR (a logical inline-start border accent) so the rail never wears a brand SURFACE (spec §3/§8).\n// The current state is never carried by color alone: the indicator bar shape AND aria-current also\n// encode it, so it survives a contrast or color-blind reading (spec §4 Current / 1.4.1, brand !=\n// state — NEVER status-verified).\n// FOCUS: the visible 2px focus ring, part of the base on every state, never removed; it persists in\n// both the expanded and collapsed rail (spec §4 Focus / 2.4.7).\n// DISABLED (aria-disabled): dims via the disabled TOKEN (DEC-C), never a blanket opacity; the\n// component also strips href + tabindex and skips it in arrow movement, while the label stays\n// readable to AT (spec §4 Disabled / §7).\n// Motion is the fast token transition on the verdify easing, instant under reduced motion — never\n// the 350ms VerifiedBadge-only theatre duration (G-U3).\nexport const sidebarItemVariants = cva(\n [\n // shape + the icon-to-label gap + logical inline padding so it mirrors under RTL; the leading\n // indicator bar is a left/start accent rendered via a logical inline-start border that is\n // transparent at rest and painted in the action alias when current\n \"relative flex items-center gap-(--space-3) rounded-(--radius-md) px-(--space-3)\",\n \"border-s-2 border-transparent\",\n // type ROLE + resting label color, no fill, pointer cursor\n \"text-label text-text-secondary no-underline cursor-pointer select-none\",\n // hover: the restrained ghost-action fill (the only fill a resting item paints)\n \"hover:bg-action-ghost-bg-hover\",\n // CURRENT: the leading indicator BAR is painted in the brand action alias and the label lifts to\n // the primary text color. This is the action(primary) alias (where you are), NEVER status-verified\n // (brand != state, G-U2). The indicator bar SHAPE + aria-current carry the state alongside the\n // color, so it survives a color-blind read; the rail itself stays a neutral surface (no brand fill).\n \"aria-[current=page]:border-s-action-primary-bg aria-[current=page]:text-text-primary\",\n // motion: fast + verdify easing, instant under reduced motion (NEVER the check theatre, G-U3)\n \"transition-[color,background-color,border-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor — 44px touch / 40px pointer, on every item (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 ring — identical on every state, never removed; persists expanded and collapsed\n \"outline-none\",\n focusRing,\n // disabled (non-operable) item — DEC-C: dim via the disabled TOKEN, never opacity. aria-disabled\n // drives it because an <a> has no native disabled; the component also strips href + tabindex.\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled\",\n ],\n { variants: {}, defaultVariants: {} },\n);\n\nexport type SidebarItemVariantProps = VariantProps<typeof sidebarItemVariants>;\n\n// The leading item icon (spec §5): the md icon role, decorative (the item names itself by its label\n// text, not the glyph). At rest it is the GHOST action fg; it inherits the disabled token when the\n// item is disabled (aria-disabled on the parent link). When the item is current it lifts with the\n// label to the primary text color. shrink-0 so it never collapses when labels are hidden.\nexport const sidebarItemIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center text-action-ghost-fg\";\n\n// The item label text (spec §5): the part hidden in the collapsed rail. When collapsed the label is\n// visually hidden but stays in the accessibility tree (sr-only), so the link keeps its accessible\n// name without depending on a tooltip being open (spec §4 Collapsed / §7).\nexport const sidebarItemLabelClass = \"min-w-0 flex-1 truncate\";\n\n// A trailing count or status on an item (spec §2): text or an aria-label, never color alone (spec\n// §7). Plain secondary caption text, hidden with the label when collapsed.\nexport const sidebarItemTrailingClass =\n \"ms-auto inline-flex items-center text-caption text-text-secondary\";\n\n// The footer slot (spec §2): a bottom slot for an account or settings entry; its items follow the\n// same item rules. Pushed to the bottom; separated from the list above by the muted hairline.\nexport const sidebarFooterClass =\n \"mt-auto flex flex-col gap-(--space-1) pt-(--space-2) border-t border-surface-border-muted\";\n\n// The collapse-toggle (spec §2/§4/§6/§7): a native <button>, NOT an item, reachable in the rail's\n// tab order. It carries the ghost-action glyph and the same neutral hover fill, focus ring, and\n// target-size floor as an item. Its glyph is decorative (aria-hidden); aria-expanded + aria-label\n// carry the action. Same fast motion, never the deliberate theatre.\nexport const sidebarCollapseToggleClass = cva(\n [\n \"inline-flex items-center justify-center gap-(--space-2) rounded-(--radius-md) px-(--space-2)\",\n \"text-action-ghost-fg cursor-pointer select-none\",\n \"hover:bg-action-ghost-bg-hover\",\n \"transition-[color,background-color] 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\",\n focusRing,\n \"disabled:pointer-events-none disabled:text-text-disabled\",\n ],\n { variants: {}, defaultVariants: {} },\n);\n\nexport type SidebarCollapseToggleVariantProps = VariantProps<typeof sidebarCollapseToggleClass>;\n\n// The collapse-toggle glyph (spec §5): the md icon role, decorative. It rotates to mirror the rail\n// width (collapsed vs expanded) as a reinforcement; aria-expanded carries the state, not the glyph.\nexport const sidebarCollapseIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center transition-transform duration-(--motion-duration-base) ease-(--motion-easing-verdify) motion-reduce:duration-(--motion-duration-instant)\";\n",
|
|
22
22
|
"path": "sidebar/sidebar.variants.ts",
|
|
23
23
|
"target": "@ui/sidebar/sidebar.variants.ts",
|
|
24
24
|
"type": "registry:ui"
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
],
|
|
27
27
|
"name": "sidebar",
|
|
28
28
|
"registryDependencies": [
|
|
29
|
-
"@verdify/cn"
|
|
29
|
+
"@verdify/cn",
|
|
30
|
+
"@verdify/focus-ring"
|
|
30
31
|
],
|
|
31
32
|
"title": "sidebar",
|
|
32
33
|
"type": "registry:ui"
|
package/registry/switch.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"type": "registry:ui"
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// The track: a native <button role=\"switch\">. Off binds control-*; the\n// aria-checked: state-variant binds the on (action-primary) track — never a\n// status-* utility (brand ≠ state). The visible 2px focus ring is never removed.\n// The target-size floor lives on the hit-area wrapper (switchHitAreaVariants), NOT\n// here: a min-height floor on the SAME node as the h-5/h-6 track would force both\n// sizes to the 44px floor (used height = max(min-height, height)), erasing the\n// size variant. The track keeps only its own h-5/h-6 visible height.\nexport const switchTrackVariants = cva(\n [\n \"relative inline-flex shrink-0 items-center rounded-full border\",\n // off (resting) track + resting border\n \"bg-control-bg border-control-border\",\n // on track (state-variant) + on hover / pressed\n \"aria-checked:bg-action-primary-bg aria-checked:border-action-primary-bg\",\n \"aria-checked:hover:bg-action-primary-bg-hover aria-checked:active:bg-action-primary-bg-active\",\n // fast, functional track-tint — no theatre\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n // visible 2px signal-blue ring at 2px offset, on every state, never removed\n \"outline-none\",\n
|
|
20
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// The track: a native <button role=\"switch\">. Off binds control-*; the\n// aria-checked: state-variant binds the on (action-primary) track — never a\n// status-* utility (brand ≠ state). The visible 2px focus ring is never removed.\n// The target-size floor lives on the hit-area wrapper (switchHitAreaVariants), NOT\n// here: a min-height floor on the SAME node as the h-5/h-6 track would force both\n// sizes to the 44px floor (used height = max(min-height, height)), erasing the\n// size variant. The track keeps only its own h-5/h-6 visible height.\nexport const switchTrackVariants = cva(\n [\n \"relative inline-flex shrink-0 items-center rounded-full border\",\n // off (resting) track + resting border\n \"bg-control-bg border-control-border\",\n // on track (state-variant) + on hover / pressed\n \"aria-checked:bg-action-primary-bg aria-checked:border-action-primary-bg\",\n \"aria-checked:hover:bg-action-primary-bg-hover aria-checked:active:bg-action-primary-bg-active\",\n // fast, functional track-tint — no theatre\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n // visible 2px signal-blue ring at 2px offset, on every state, never removed\n \"outline-none\",\n focusRing,\n // disabled: no pointer; the checked value still reads to AT. DEC-C / spec §4:\n // reduced emphasis comes from dimming the thumb (the indicator) to the disabled\n // TOKEN — see switchThumbVariants — NOT a blanket opacity-60 dim of the track.\n \"disabled:pointer-events-none\",\n \"aria-busy:pointer-events-none\",\n ],\n {\n variants: {\n // visible track height/width only — the hit-area floor is a separate wrapper,\n // so sm renders a genuinely shorter visible track than md\n size: {\n md: \"h-6 w-11 px-0.5\",\n sm: \"h-5 w-9 px-0.5\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// The thumb: control-fg fill, slides start→end. Position (not color) carries on/off\n// state, via the aria-checked: translate variant on the parent button.\n//\n// DEC-C / spec §4: a disabled switch dims the thumb (its indicator) to\n// --color-text-disabled — the SAME token the label dims to — NOT a blanket\n// opacity-60 dim of the track. The thumb is a child of the button, not a sibling\n// of a peer input, so the colour is driven by an explicit `disabled` cva variant\n// the component passes (mirrors switchLabelVariants); enabled keeps control-fg.\nexport const switchThumbVariants = cva(\n [\n \"pointer-events-none block rounded-full shadow-sm\",\n \"translate-x-0 aria-checked:translate-x-full\",\n \"transition-transform duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n ],\n {\n variants: {\n size: {\n md: \"h-5 w-5\",\n sm: \"h-4 w-4\",\n },\n disabled: { true: \"bg-text-disabled\", false: \"bg-control-fg\" },\n },\n defaultVariants: { size: \"md\", disabled: false },\n },\n);\n\n// The hit-area wrapper around the track. This — NOT the visible track — carries the\n// target-size floor (44px touch / 40px pointer) so the touch target always meets the\n// minimum while the track keeps its smaller h-5/h-6 visible height. inline-flex +\n// items-center vertically centres the shorter track within the taller hit area.\nexport const switchHitAreaVariants = cva([\n \"inline-flex shrink-0 items-center justify-center\",\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n]);\n\n// The label naming the setting. The <label> element is nested in a column wrapper,\n// not a sibling of the button, so peer-disabled never reaches it; the disabled\n// colour is driven by the explicit `disabled` variant the component passes (mirrors\n// checkboxLabelVariants / radioLabelVariants).\nexport const switchLabelVariants = cva(\n [\"text-label text-text-primary select-none\"],\n {\n variants: {\n disabled: { true: \"text-text-disabled\", false: \"\" },\n },\n defaultVariants: { disabled: false },\n },\n);\n\nexport type SwitchVariantProps = VariantProps<typeof switchTrackVariants>;\n",
|
|
21
21
|
"path": "switch/switch.variants.ts",
|
|
22
22
|
"target": "@ui/switch/switch.variants.ts",
|
|
23
23
|
"type": "registry:ui"
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
],
|
|
26
26
|
"name": "switch",
|
|
27
27
|
"registryDependencies": [
|
|
28
|
-
"@verdify/cn"
|
|
28
|
+
"@verdify/cn",
|
|
29
|
+
"@verdify/focus-ring"
|
|
29
30
|
],
|
|
30
31
|
"title": "switch",
|
|
31
32
|
"type": "registry:ui"
|
package/registry/table.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"type": "registry:ui"
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// A Table presents structured data in rows and columns (spec §1). Neutrals carry the table —\n// restraint over volume (spec §3): it is a READING surface, not an accent surface. It paints from\n// the surface, text, and border roles only; it reaches the --color-action-* tier ONLY for the\n// controls it hosts (a sortable header's ghost accent + focus affordances) and the --color-status-*\n// tier ONLY for a cell that reports a real state, paired with text — never for a row, a header, or\n// a selected state, and NEVER a brand token at all (brand != state, G-U2). The status meaning lives\n// in the cell's words + the status fg, so a grayscale or color-blind reader still reads it (1.4.1).\n\n// The <table> container (spec §2): the neutral canvas surface and the default cell text role. It\n// collapses its borders so the row/column hairline rules read as single lines, and aligns text on\n// the logical start edge so it mirrors under dir=\"rtl\" (G-U6). The table NEVER wears the brand\n// violet or a status fill (spec §3/§8) — those belong to the controls and badges inside cells.\nexport const tableVariants = cva([\n \"w-full border-collapse text-start\",\n \"bg-surface-canvas text-body text-text-primary\",\n]);\n\nexport type TableVariantProps = VariantProps<typeof tableVariants>;\n\n// The <caption> (spec §2/§7): the table's accessible name, in the secondary text color at the\n// label type role. A reading caption, not an accent — never the brand or a status color.\nexport const tableCaptionClass = \"text-start text-label text-text-secondary mb-(--space-2)\";\n\n// The <thead> (spec §2). The header divider is a hairline by default; on a sticky-header table the\n// header pins to the top of the scroll container and the divider strengthens to border-strong,\n// where a heavier separation reads better as the body scrolls under it (spec §4/§5). z on the\n// sticky layer so a scrolled body cell never paints over the pinned header.\nexport const tableHeaderVariants = cva(\"border-b border-border-default\", {\n variants: {\n sticky: {\n true: \"sticky top-0 z-(--z-index-sticky) bg-surface-canvas border-b-border-strong\",\n false: \"\",\n },\n },\n defaultVariants: { sticky: false },\n});\n\nexport type TableHeaderVariantProps = VariantProps<typeof tableHeaderVariants>;\n\n// The <tbody> (spec §3 rule). The `zebra` rule replaces row hairlines with an alternating NEUTRAL\n// surface step on even rows, for a long table — the tint is a surface step, NEVER a status or brand\n// color (spec §3/§8). The arbitrary selector `[&>tr:nth-child(even)]` is a SELECTOR (its body\n// starts with `&`, not a raw value or a bare `--token`), so it is gate-legitimate, not an arbitrary\n// VALUE. While loading, the body is aria-busy (set on the element, not bound here) and shows\n// skeleton rows in the column layout (spec §4 Loading).\nexport const tableBodyVariants = cva(\"\", {\n variants: {\n rule: {\n // hairlines/grid carry their rules on the rows + cells, so the body adds nothing\n horizontal: \"\",\n grid: \"\",\n // zebra: the neutral raised surface step on even rows (no rules)\n zebra: \"[&>tr:nth-child(even)]:bg-surface-raised\",\n },\n },\n defaultVariants: { rule: \"horizontal\" },\n});\n\nexport type TableBodyVariantProps = VariantProps<typeof tableBodyVariants>;\n\n// A <tr> body row (spec §4 Default/Hover/Selected). RESTING: no fill, on the canvas. HOVER: a\n// restrained raised-surface fill to track the eye across a wide row — an AFFORDANCE, not a\n// selection (nothing is selected until you act, spec §4 Hover). SELECTED (aria-selected): the same\n// restrained raised-surface fill, encoded by the Checkbox state + aria-selected, NEVER a brand or\n// status tint (brand != state, G-U2) — so a grayscale reader reads selection from the checkbox, not\n// the fill. Motion is the fast token transition on the verdify easing, instant under reduced motion\n// — never the 350ms VerifiedBadge-only theatre (a row hover/select is a plain transition, G-U3).\n// The `zebra` rule turns off the per-row hairline; `horizontal`/`grid` keep the bottom hairline.\nexport const tableRowVariants = cva(\n [\n \"hover:bg-surface-raised\",\n \"aria-selected:bg-surface-raised\",\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n ],\n {\n variants: {\n rule: {\n horizontal: \"border-b border-border-default\",\n grid: \"border-b border-border-default\",\n zebra: \"\",\n },\n },\n defaultVariants: { rule: \"horizontal\" },\n },\n);\n\nexport type TableRowVariantProps = VariantProps<typeof tableRowVariants>;\n\n// The shared cell padding by density (spec §3 density / §5 --space-*). Density tightens the VERTICAL\n// padding only, ABOVE the a11y floor — the row controls keep their own --size-target-* floor (DEC-B:\n// never a fixed height below the floor). The grid rule adds a logical inline-end column rule so it\n// mirrors under dir=\"rtl\" (G-U6). Horizontal inline padding is constant.\nconst cellPaddingVariants = {\n density: {\n comfortable: \"py-(--space-3)\",\n compact: \"py-(--space-1)\",\n },\n rule: {\n horizontal: \"\",\n // a logical column rule on each cell, for a wide numeric table's column guide (spec §3 grid)\n grid: \"border-e border-border-default last:border-e-0\",\n zebra: \"\",\n },\n} as const;\n\n// A <td> data cell (spec §2 cell, §4/§5). Default: the primary text color at the body type role.\n// A `numeric` cell uses TABULAR figures so digits align down the column and end-aligns them (spec §4\n// Default/§5) — numeric DATA is primary text, NOT muted (spec §4 assigns cell text the primary role;\n// the muted role is reserved by spec §5 for de-emphasized AUXILIARY text, a timestamp or a unit). An\n// `auxiliary` cell takes the muted role explicitly (spec §5 --color-text-muted), independent of\n// `numeric`. A cell that reports a real STATE carries the status fg paired with text — 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// cell's words, not a saturated fill.\nexport const tableCellVariants = cva(\n [\"px-(--space-3) align-middle text-start text-body text-text-primary\"],\n {\n variants: {\n ...cellPaddingVariants,\n numeric: {\n // tabular figures + end-aligned numbers down the column — primary data text, not muted\n true: \"text-end tabular-nums\",\n false: \"\",\n },\n auxiliary: {\n // de-emphasized auxiliary cell text — a timestamp, a unit (spec §5 --color-text-muted)\n true: \"text-text-muted\",\n false: \"\",\n },\n status: {\n none: \"\",\n // each status is the fg only, paired with the cell's text — the cell's words carry the\n // meaning, 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 rule: \"horizontal\",\n numeric: false,\n auxiliary: false,\n status: \"none\",\n },\n },\n);\n\nexport type TableCellVariantProps = VariantProps<typeof tableCellVariants>;\n\n// A <th scope=\"row\"> row-header cell (spec §2/§7): the row's natural label (an identifier, a name),\n// tying its cells to the row. The primary text color at the body type role — the same reading weight\n// as a data cell, just promoted to a header for the relationship (1.3.1). Density + grid rule apply.\nexport const tableRowHeaderVariants = cva(\n [\"px-(--space-3) align-middle text-start font-normal text-body text-text-primary\"],\n {\n variants: { ...cellPaddingVariants },\n defaultVariants: { density: \"comfortable\", rule: \"horizontal\" },\n },\n);\n\nexport type TableRowHeaderVariantProps = VariantProps<typeof tableRowHeaderVariants>;\n\n// A <th scope=\"col\"> column header (spec §2/§4/§5). The header LABEL is the SECONDARY text color at\n// the label type role (the quiet, tracked column label), on the canvas — a header NEVER wears a\n// status or brand tint (spec §3/§8). Density + grid rule apply to the cell padding.\nexport const tableHeadVariants = cva(\n [\"px-(--space-3) align-middle text-start text-label text-text-secondary\"],\n {\n variants: { ...cellPaddingVariants },\n defaultVariants: { density: \"comfortable\", rule: \"horizontal\" },\n },\n);\n\nexport type TableHeadVariantProps = VariantProps<typeof tableHeadVariants>;\n\n// The SORTABLE-header control (spec §2/§4/§6/§7): a real <button> inside the <th>, so it reads as\n// the control it is and re-sorts on Enter/Space. It is the GHOST action accent — the label + caret\n// in the ghost fg with the restrained ghost hover fill (spec §4 Sortable-header hover / §5) — the\n// action tier is legitimate here because it is a control the table HOSTS, not a row/header tint. It\n// carries the visible 2px focus ring (never removed) and the target-size floor (40px desktop / 44px\n// touch, spec §7). Motion is the fast token transition, never the deliberate verified-check theatre\n// (G-U3). aria-sort lives on the parent <th>, and the direction caret encodes direction alongside\n// it so it never rests on color alone (spec §4 Sorted / 1.4.1).\nexport const tableSortButtonClass =\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\n// also encoded by aria-sort on the th + the glyph's shape via data-direction, so it never rests on\n// color alone — 1.4.1). It inherits the ghost accent color from the button.\nexport const tableSortCaretClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center\";\n\n// The empty-state cell (spec §2/§4 Empty): a plain line spanning the full table width, in the\n// secondary text color — an empty table is not an error and never reads as one (no status color).\nexport const tableEmptyClass =\n \"px-(--space-3) py-(--space-4) text-center text-body text-text-secondary\";\n\n// One skeleton placeholder cell while the body resolves (spec §4 Loading): keeps the column layout\n// stable so the table does not reflow when data arrives. The Skeleton itself is decorative + neutral\n// (it binds no brand/status — that invariant lives in the Skeleton component); this is just the cell\n// padding wrapping it.\nexport const tableSkeletonCellClass = \"px-(--space-3) py-(--space-3)\";\n",
|
|
20
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// A Table presents structured data in rows and columns (spec §1). Neutrals carry the table —\n// restraint over volume (spec §3): it is a READING surface, not an accent surface. It paints from\n// the surface, text, and border roles only; it reaches the --color-action-* tier ONLY for the\n// controls it hosts (a sortable header's ghost accent + focus affordances) and the --color-status-*\n// tier ONLY for a cell that reports a real state, paired with text — never for a row, a header, or\n// a selected state, and NEVER a brand token at all (brand != state, G-U2). The status meaning lives\n// in the cell's words + the status fg, so a grayscale or color-blind reader still reads it (1.4.1).\n\n// The <table> container (spec §2): the neutral canvas surface and the default cell text role. It\n// collapses its borders so the row/column hairline rules read as single lines, and aligns text on\n// the logical start edge so it mirrors under dir=\"rtl\" (G-U6). The table NEVER wears the brand\n// violet or a status fill (spec §3/§8) — those belong to the controls and badges inside cells.\nexport const tableVariants = cva([\n \"w-full border-collapse text-start\",\n \"bg-surface-canvas text-body text-text-primary\",\n]);\n\nexport type TableVariantProps = VariantProps<typeof tableVariants>;\n\n// The <caption> (spec §2/§7): the table's accessible name, in the secondary text color at the\n// label type role. A reading caption, not an accent — never the brand or a status color.\nexport const tableCaptionClass = \"text-start text-label text-text-secondary mb-(--space-2)\";\n\n// The <thead> (spec §2). The header divider is a hairline by default; on a sticky-header table the\n// header pins to the top of the scroll container and the divider strengthens to border-strong,\n// where a heavier separation reads better as the body scrolls under it (spec §4/§5). z on the\n// sticky layer so a scrolled body cell never paints over the pinned header.\nexport const tableHeaderVariants = cva(\"border-b border-border-default\", {\n variants: {\n sticky: {\n true: \"sticky top-0 z-(--z-index-sticky) bg-surface-canvas border-b-border-strong\",\n false: \"\",\n },\n },\n defaultVariants: { sticky: false },\n});\n\nexport type TableHeaderVariantProps = VariantProps<typeof tableHeaderVariants>;\n\n// The <tbody> (spec §3 rule). The `zebra` rule replaces row hairlines with an alternating NEUTRAL\n// surface step on even rows, for a long table — the tint is a surface step, NEVER a status or brand\n// color (spec §3/§8). The arbitrary selector `[&>tr:nth-child(even)]` is a SELECTOR (its body\n// starts with `&`, not a raw value or a bare `--token`), so it is gate-legitimate, not an arbitrary\n// VALUE. While loading, the body is aria-busy (set on the element, not bound here) and shows\n// skeleton rows in the column layout (spec §4 Loading).\nexport const tableBodyVariants = cva(\"\", {\n variants: {\n rule: {\n // hairlines/grid carry their rules on the rows + cells, so the body adds nothing\n horizontal: \"\",\n grid: \"\",\n // zebra: the neutral raised surface step on even rows (no rules)\n zebra: \"[&>tr:nth-child(even)]:bg-surface-raised\",\n },\n },\n defaultVariants: { rule: \"horizontal\" },\n});\n\nexport type TableBodyVariantProps = VariantProps<typeof tableBodyVariants>;\n\n// A <tr> body row (spec §4 Default/Hover/Selected). RESTING: no fill, on the canvas. HOVER: a\n// restrained raised-surface fill to track the eye across a wide row — an AFFORDANCE, not a\n// selection (nothing is selected until you act, spec §4 Hover). SELECTED (aria-selected): the same\n// restrained raised-surface fill, encoded by the Checkbox state + aria-selected, NEVER a brand or\n// status tint (brand != state, G-U2) — so a grayscale reader reads selection from the checkbox, not\n// the fill. Motion is the fast token transition on the verdify easing, instant under reduced motion\n// — never the 350ms VerifiedBadge-only theatre (a row hover/select is a plain transition, G-U3).\n// The `zebra` rule turns off the per-row hairline; `horizontal`/`grid` keep the bottom hairline.\nexport const tableRowVariants = cva(\n [\n \"hover:bg-surface-raised\",\n \"aria-selected:bg-surface-raised\",\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n ],\n {\n variants: {\n rule: {\n horizontal: \"border-b border-border-default\",\n grid: \"border-b border-border-default\",\n zebra: \"\",\n },\n },\n defaultVariants: { rule: \"horizontal\" },\n },\n);\n\nexport type TableRowVariantProps = VariantProps<typeof tableRowVariants>;\n\n// The shared cell padding by density (spec §3 density / §5 --space-*). Density tightens the VERTICAL\n// padding only, ABOVE the a11y floor — the row controls keep their own --size-target-* floor (DEC-B:\n// never a fixed height below the floor). The grid rule adds a logical inline-end column rule so it\n// mirrors under dir=\"rtl\" (G-U6). Horizontal inline padding is constant.\nconst cellPaddingVariants = {\n density: {\n comfortable: \"py-(--space-3)\",\n compact: \"py-(--space-1)\",\n },\n rule: {\n horizontal: \"\",\n // a logical column rule on each cell, for a wide numeric table's column guide (spec §3 grid)\n grid: \"border-e border-border-default last:border-e-0\",\n zebra: \"\",\n },\n} as const;\n\n// A <td> data cell (spec §2 cell, §4/§5). Default: the primary text color at the body type role.\n// A `numeric` cell uses TABULAR figures so digits align down the column and end-aligns them (spec §4\n// Default/§5) — numeric DATA is primary text, NOT muted (spec §4 assigns cell text the primary role;\n// the muted role is reserved by spec §5 for de-emphasized AUXILIARY text, a timestamp or a unit). An\n// `auxiliary` cell takes the muted role explicitly (spec §5 --color-text-muted), independent of\n// `numeric`. A cell that reports a real STATE carries the status fg paired with text — 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// cell's words, not a saturated fill.\nexport const tableCellVariants = cva(\n [\"px-(--space-3) align-middle text-start text-body text-text-primary\"],\n {\n variants: {\n ...cellPaddingVariants,\n numeric: {\n // tabular figures + end-aligned numbers down the column — primary data text, not muted\n true: \"text-end tabular-nums\",\n false: \"\",\n },\n auxiliary: {\n // de-emphasized auxiliary cell text — a timestamp, a unit. Essential (it conveys data), so\n // it uses secondary (AA), not the decorative-only muted role (accessibility.md).\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 cell's words carry the\n // meaning, 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 rule: \"horizontal\",\n numeric: false,\n auxiliary: false,\n status: \"none\",\n },\n },\n);\n\nexport type TableCellVariantProps = VariantProps<typeof tableCellVariants>;\n\n// A <th scope=\"row\"> row-header cell (spec §2/§7): the row's natural label (an identifier, a name),\n// tying its cells to the row. The primary text color at the body type role — the same reading weight\n// as a data cell, just promoted to a header for the relationship (1.3.1). Density + grid rule apply.\nexport const tableRowHeaderVariants = cva(\n [\"px-(--space-3) align-middle text-start font-normal text-body text-text-primary\"],\n {\n variants: { ...cellPaddingVariants },\n defaultVariants: { density: \"comfortable\", rule: \"horizontal\" },\n },\n);\n\nexport type TableRowHeaderVariantProps = VariantProps<typeof tableRowHeaderVariants>;\n\n// A <th scope=\"col\"> column header (spec §2/§4/§5). The header LABEL is the SECONDARY text color at\n// the label type role (the quiet, tracked column label), on the canvas — a header NEVER wears a\n// status or brand tint (spec §3/§8). Density + grid rule apply to the cell padding.\nexport const tableHeadVariants = cva(\n [\"px-(--space-3) align-middle text-start text-label text-text-secondary\"],\n {\n variants: { ...cellPaddingVariants },\n defaultVariants: { density: \"comfortable\", rule: \"horizontal\" },\n },\n);\n\nexport type TableHeadVariantProps = VariantProps<typeof tableHeadVariants>;\n\n// The SORTABLE-header control (spec §2/§4/§6/§7): a real <button> inside the <th>, so it reads as\n// the control it is and re-sorts on Enter/Space. It is the GHOST action accent — the label + caret\n// in the ghost fg with the restrained ghost hover fill (spec §4 Sortable-header hover / §5) — the\n// action tier is legitimate here because it is a control the table HOSTS, not a row/header tint. It\n// carries the visible 2px focus ring (never removed) and the target-size floor (40px desktop / 44px\n// touch, spec §7). Motion is the fast token transition, never the deliberate verified-check theatre\n// (G-U3). aria-sort lives on the parent <th>, and the direction caret encodes direction alongside\n// it so it never rests on color alone (spec §4 Sorted / 1.4.1).\nexport const tableSortButtonClass =\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\n// also encoded by aria-sort on the th + the glyph's shape via data-direction, so it never rests on\n// color alone — 1.4.1). It inherits the ghost accent color from the button.\nexport const tableSortCaretClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center\";\n\n// The empty-state cell (spec §2/§4 Empty): a plain line spanning the full table width, in the\n// secondary text color — an empty table is not an error and never reads as one (no status color).\nexport const tableEmptyClass =\n \"px-(--space-3) py-(--space-4) text-center text-body text-text-secondary\";\n\n// One skeleton placeholder cell while the body resolves (spec §4 Loading): keeps the column layout\n// stable so the table does not reflow when data arrives. The Skeleton itself is decorative + neutral\n// (it binds no brand/status — that invariant lives in the Skeleton component); this is just the cell\n// padding wrapping it.\nexport const tableSkeletonCellClass = \"px-(--space-3) py-(--space-3)\";\n",
|
|
21
21
|
"path": "table/table.variants.ts",
|
|
22
22
|
"target": "@ui/table/table.variants.ts",
|
|
23
23
|
"type": "registry:ui"
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"name": "table",
|
|
27
27
|
"registryDependencies": [
|
|
28
28
|
"@verdify/cn",
|
|
29
|
+
"@verdify/focus-ring",
|
|
29
30
|
"@verdify/skeleton"
|
|
30
31
|
],
|
|
31
32
|
"title": "table",
|
package/registry/tabs.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"type": "registry:ui"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// Tabs is a NEUTRAL layout container (spec §1/§3/§5/§8): brand violet and Verified Green are\n// accents, neutrals carry the surface. The SELECTED tab + its indicator take the BRAND primary\n// action accent (--color-action-primary-*), NEVER a status color — a selected tab reports which\n// panel is open, not a verified result. Surfacing a verified result is VerifiedBadge's job\n// (brand != state, G-U2). This file is the ONLY token-binding site (skill §5 hard rule).\n\n// The tablist: the row (or column) of tabs. A neutral baseline divider (border-border-default)\n// runs along the inline-end edge of the tabs; in horizontal it is the bottom border under the\n// row, in vertical the inline-end border of the column. The canvas surface backs it.\nexport const tabsListVariants = cva(\n [\n // logical-property layout (G-U6) — flex row by default, gapped; the canvas backs the row\n \"flex bg-surface-canvas\",\n // the neutral baseline the underline indicator sits on (spec §5 border-default)\n \"border-border-default\",\n ],\n {\n variants: {\n orientation: {\n // horizontal: tabs sit in a row above a 1px baseline; gap between tabs\n horizontal: \"flex-row items-end gap-(--space-1) border-b\",\n // vertical: tabs stack in a column beside a 1px inline-end rail (side rail)\n vertical: \"flex-col items-stretch gap-(--space-1) border-e\",\n },\n },\n defaultVariants: { orientation: \"horizontal\" },\n },\n);\n\n// The tab: one selectable label in the tablist. A NEUTRAL ghost surface at rest — the unselected\n// label in --color-text-secondary, the leading icon + label in the ghost fg, a restrained ghost\n// hover fill. SELECTED lifts the label to --color-text-primary and paints the indicator in the\n// BRAND action-primary (underline border, or pill fill). The persistent focus ring; the\n// target-size floor (padding density above it, DEC-B); fast functional indicator-slide motion\n// (NEVER the deliberate verified-check theatre, G-U3); DEC-C disabled via the disabled TOKEN.\n//\n// Radix Tabs.Trigger drives data-state=active|inactive, data-disabled, and data-orientation on\n// the tab button, so the selected/disabled bindings are attribute-selector variants (allowed —\n// not arbitrary values). aria-selected + the visible panel ALSO encode selection, so color is\n// never the sole signal (spec §4 Selected / use-of-color 1.4.1).\nexport const tabsTabVariants = cva(\n [\n // layout: label (+ optional leading icon / trailing count) on a single centered row\n \"group inline-flex items-center justify-center gap-(--space-2) px-(--space-4)\",\n // type role + weight; ghost fg is the resting label + leading-icon color (spec §5 ghost-fg);\n // unselected label color is text-secondary, lifting to text-primary when active\n \"text-label font-medium cursor-pointer select-none whitespace-nowrap\",\n \"text-text-secondary data-[state=active]:text-text-primary\",\n // restrained ghost hover fill — never a brand fill on an unselected tab (spec §4 Hover)\n \"hover:bg-action-ghost-bg-hover\",\n // the indicator slide is a PLAIN, fast transition + verdify easing, instant under reduced\n // motion. Never the 350ms VerifiedBadge-only theatre duration (G-U3 motion-theatre gate).\n \"transition-[color,background-color,border-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor: 44px touch / 40px pointer (§7 2.5.5/Material), padding density above it;\n // the height EMERGES from the floor + py, never a fixed height below the a11y floor (DEC-B)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px focus ring at 2px offset; persists whether the tab is selected or not, and is\n // DISTINCT from selection — arrows move focus across tabs while selection stays put (spec §4)\n \"outline-none\",\n
|
|
21
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// Tabs is a NEUTRAL layout container (spec §1/§3/§5/§8): brand violet and Verified Green are\n// accents, neutrals carry the surface. The SELECTED tab + its indicator take the BRAND primary\n// action accent (--color-action-primary-*), NEVER a status color — a selected tab reports which\n// panel is open, not a verified result. Surfacing a verified result is VerifiedBadge's job\n// (brand != state, G-U2). This file is the ONLY token-binding site (skill §5 hard rule).\n\n// The tablist: the row (or column) of tabs. A neutral baseline divider (border-border-default)\n// runs along the inline-end edge of the tabs; in horizontal it is the bottom border under the\n// row, in vertical the inline-end border of the column. The canvas surface backs it.\nexport const tabsListVariants = cva(\n [\n // logical-property layout (G-U6) — flex row by default, gapped; the canvas backs the row\n \"flex bg-surface-canvas\",\n // the neutral baseline the underline indicator sits on (spec §5 border-default)\n \"border-border-default\",\n ],\n {\n variants: {\n orientation: {\n // horizontal: tabs sit in a row above a 1px baseline; gap between tabs\n horizontal: \"flex-row items-end gap-(--space-1) border-b\",\n // vertical: tabs stack in a column beside a 1px inline-end rail (side rail)\n vertical: \"flex-col items-stretch gap-(--space-1) border-e\",\n },\n },\n defaultVariants: { orientation: \"horizontal\" },\n },\n);\n\n// The tab: one selectable label in the tablist. A NEUTRAL ghost surface at rest — the unselected\n// label in --color-text-secondary, the leading icon + label in the ghost fg, a restrained ghost\n// hover fill. SELECTED lifts the label to --color-text-primary and paints the indicator in the\n// BRAND action-primary (underline border, or pill fill). The persistent focus ring; the\n// target-size floor (padding density above it, DEC-B); fast functional indicator-slide motion\n// (NEVER the deliberate verified-check theatre, G-U3); DEC-C disabled via the disabled TOKEN.\n//\n// Radix Tabs.Trigger drives data-state=active|inactive, data-disabled, and data-orientation on\n// the tab button, so the selected/disabled bindings are attribute-selector variants (allowed —\n// not arbitrary values). aria-selected + the visible panel ALSO encode selection, so color is\n// never the sole signal (spec §4 Selected / use-of-color 1.4.1).\nexport const tabsTabVariants = cva(\n [\n // layout: label (+ optional leading icon / trailing count) on a single centered row\n \"group inline-flex items-center justify-center gap-(--space-2) px-(--space-4)\",\n // type role + weight; ghost fg is the resting label + leading-icon color (spec §5 ghost-fg);\n // unselected label color is text-secondary, lifting to text-primary when active\n \"text-label font-medium cursor-pointer select-none whitespace-nowrap\",\n \"text-text-secondary data-[state=active]:text-text-primary\",\n // restrained ghost hover fill — never a brand fill on an unselected tab (spec §4 Hover)\n \"hover:bg-action-ghost-bg-hover\",\n // the indicator slide is a PLAIN, fast transition + verdify easing, instant under reduced\n // motion. Never the 350ms VerifiedBadge-only theatre duration (G-U3 motion-theatre gate).\n \"transition-[color,background-color,border-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor: 44px touch / 40px pointer (§7 2.5.5/Material), padding density above it;\n // the height EMERGES from the floor + py, never a fixed height below the a11y floor (DEC-B)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px focus ring at 2px offset; persists whether the tab is selected or not, and is\n // DISTINCT from selection — arrows move focus across tabs while selection stays put (spec §4)\n \"outline-none\",\n focusRing,\n // disabled — DEC-C: reduced emphasis via the disabled TOKEN (Radix sets data-disabled +\n // removes it from the roving sequence), never a blanket opacity on the control\n \"data-[disabled]:pointer-events-none data-[disabled]:text-text-disabled\",\n ],\n {\n variants: {\n variant: {\n // underline (default): a transparent bottom border at rest that paints to the BRAND\n // action-primary when selected (spec §5: action-primary indicator). The border box is\n // always reserved so the row does not reflow on selection.\n underline: [\n \"border-b-2 border-b-transparent -mb-px\",\n \"data-[state=active]:border-action-primary-bg\",\n ],\n // pill: the selected tab is a filled brand chip — action-primary fill, its fg label, md\n // radius (spec §5: action-primary-bg fill, action-primary-fg label, radius-md)\n pill: [\n \"rounded-(--radius-md)\",\n \"data-[state=active]:bg-action-primary-bg data-[state=active]:text-action-primary-fg\",\n ],\n },\n size: {\n // DEC-B: both sizes hold the shared target-size floor; they differ by vertical padding\n // density ABOVE it (md roomier, sm denser for side rails), never a fixed height below it.\n md: \"py-(--space-2)\",\n sm: \"py-(--space-1)\",\n },\n },\n defaultVariants: { variant: \"underline\", size: \"md\" },\n },\n);\n\n// The panel: the content region tied to the selected tab. The canvas surface, the primary body\n// text at the body type role, panel insets from --space-4. In horizontal layout a divider above\n// it (border-t) continues the neutral hairline; in vertical it insets beside the rail. Radix\n// gives it role=tabpanel + aria-labelledby back to its tab, and tabindex=0 when it holds no\n// focusable content so Tab always reaches it (spec §7 focus management).\nexport const tabsPanelVariants = cva(\n [\n \"bg-surface-canvas text-text-primary text-body\",\n \"px-(--space-4) py-(--space-4)\",\n // visible focus ring when the panel itself takes focus (it is tabindex=0); never removed\n \"outline-none\",\n focusRing,\n ],\n {\n variants: {\n orientation: {\n // horizontal: a neutral hairline above the panel continues the tablist baseline\n horizontal: \"border-t border-border-default\",\n // vertical: the panel sits beside the column rail; no top divider\n vertical: \"\",\n },\n },\n defaultVariants: { orientation: \"horizontal\" },\n },\n);\n\nexport type TabsListVariantProps = VariantProps<typeof tabsListVariants>;\nexport type TabsTabVariantProps = VariantProps<typeof tabsTabVariants>;\nexport type TabsPanelVariantProps = VariantProps<typeof tabsPanelVariants>;\n",
|
|
22
22
|
"path": "tabs/tabs.variants.ts",
|
|
23
23
|
"target": "@ui/tabs/tabs.variants.ts",
|
|
24
24
|
"type": "registry:ui"
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
],
|
|
27
27
|
"name": "tabs",
|
|
28
28
|
"registryDependencies": [
|
|
29
|
-
"@verdify/cn"
|
|
29
|
+
"@verdify/cn",
|
|
30
|
+
"@verdify/focus-ring"
|
|
30
31
|
],
|
|
31
32
|
"title": "tabs",
|
|
32
33
|
"type": "registry:ui"
|
package/registry/textarea.json
CHANGED
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"type": "registry:ui"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Label } from \"@/components/ui/label\";\nimport { textareaVariants, type TextareaVariantProps } from \"./textarea.variants\";\n\nexport interface TextareaProps\n extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, \"id\">,\n TextareaVariantProps {\n /** Bound visible label text (for/id). Required — the placeholder is never the name. */\n label: React.ReactNode;\n /** Help text below the field, linked via aria-describedby. */\n description?: React.ReactNode;\n /** Error message: reds the border, sets aria-invalid, joins aria-describedby. */\n error?: React.ReactNode;\n /** Field id; auto-generated from React.useId when omitted. */\n id?: string;\n /** auto-grow lower bound (rows). */\n minRows?: number;\n /** auto-grow upper bound (rows) — beyond it the field scrolls. */\n maxRows?: number;\n}\n\n/** Merge non-null describedby ids into a single attribute value (or undefined). */\nfunction describedBy(...ids: (string | false | undefined)[]): string | undefined {\n const present = ids.filter(Boolean) as string[];\n return present.length ? present.join(\" \") : undefined;\n}\n\nexport const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n function Textarea(\n {\n className,\n resize = \"vertical\",\n label,\n description,\n error,\n id,\n minRows = 3,\n maxRows = 8,\n rows,\n maxLength,\n value,\n defaultValue,\n onChange,\n disabled,\n readOnly,\n required,\n ...props\n },\n forwardedRef,\n ) {\n const autoId = React.useId();\n const fieldId = id ?? autoId;\n const descId = `${fieldId}-desc`;\n const errId = `${fieldId}-err`;\n const counterId = `${fieldId}-counter`;\n\n const innerRef = React.useRef<HTMLTextAreaElement>(null);\n React.useImperativeHandle(\n forwardedRef,\n () => innerRef.current as HTMLTextAreaElement,\n );\n\n // counter state: read controlled value when present, else track length locally\n const isControlled = value !== undefined;\n const [count, setCount] = React.useState(\n String(value ?? defaultValue ?? \"\").length,\n );\n const length = isControlled ? String(value ?? \"\").length : count;\n\n const isAutoGrow = resize === \"auto-grow\";\n\n // auto-grow: measure scrollHeight, then clamp the height to a ceiling computed\n // from maxRows × line-height. Beyond the ceiling the field scrolls (overflowY\n // auto) instead of growing unbounded; below it the field hugs its content\n // (overflowY hidden). The ceiling is written to el.style.maxHeight — there is no\n // class for it, so nothing references an undefined custom property.\n const resizeToContent = React.useCallback(() => {\n const el = innerRef.current;\n if (!el || !isAutoGrow) return;\n const style = window.getComputedStyle(el);\n // jsdom and \"normal\" line-height both yield NaN here; px() coerces any\n // non-finite value to 0 (or the fallback) so the ceiling stays a real number.\n const px = (v: string, fallback = 0) => {\n const n = parseFloat(v);\n return Number.isFinite(n) ? n : fallback;\n };\n // line-height may compute to \"normal\"; fall back to ~1.5× font size, else 20px.\n const lineHeight = px(style.lineHeight) || px(style.fontSize) * 1.5 || 20;\n const borders = px(style.borderTopWidth) + px(style.borderBottomWidth);\n const padding = px(style.paddingTop) + px(style.paddingBottom);\n const ceiling = maxRows * lineHeight + padding + borders;\n // measure the natural content height from a collapsed baseline\n el.style.height = \"auto\";\n const next = Math.min(el.scrollHeight, ceiling);\n el.style.height = `${next}px`;\n el.style.maxHeight = `${ceiling}px`;\n // scroll only once content exceeds the ceiling; hug content otherwise\n el.style.overflowY = el.scrollHeight > ceiling ? \"auto\" : \"hidden\";\n }, [isAutoGrow, maxRows]);\n\n React.useLayoutEffect(() => {\n if (isAutoGrow) resizeToContent();\n }, [isAutoGrow, resizeToContent, value]);\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n if (!isControlled) setCount(e.target.value.length);\n if (isAutoGrow) resizeToContent();\n onChange?.(e);\n };\n\n const hasCounter = maxLength !== undefined;\n const hasError = error != null && error !== false;\n\n return (\n <div className=\"flex flex-col gap-(--space-3)\">\n {/* Compose the Label primitive (define-once): it owns the canonical\n label role/color (text-label / text-text-primary) and the disabled\n dim. Passing `disabled` lets a disabled Textarea's name dim to\n --color-text-disabled via Label's own cva variant, and `required`\n surfaces the required mark there (the field still carries the real\n required / aria-required state below). textarea.md §5 lists\n text-secondary for the label, but label.md §3–5 is authoritative for\n the Label primitive: resting label text is text-text-primary. */}\n <Label htmlFor={fieldId} disabled={disabled} required={required}>\n {label}\n </Label>\n <textarea\n ref={innerRef}\n id={fieldId}\n // native <textarea> already carries an implicit aria-multiline; make the\n // multi-line contract explicit per the frozen ARIA contract (spec §7).\n aria-multiline=\"true\"\n rows={isAutoGrow ? minRows : rows}\n maxLength={maxLength}\n value={value}\n defaultValue={defaultValue}\n onChange={handleChange}\n disabled={disabled}\n readOnly={readOnly}\n required={required}\n aria-required={required || undefined}\n aria-invalid={hasError || undefined}\n aria-describedby={describedBy(\n // coerce each guard to a boolean so the expression narrows to\n // string | false (React.ReactNode can be null/0/0n, which would widen\n // the union past the describedBy signature otherwise)\n !!description && descId,\n hasError && errId,\n hasCounter && counterId,\n )}\n style={isAutoGrow ? { overflowY: \"hidden\" } : undefined}\n className={cn(textareaVariants({ resize }), className)}\n {...props}\n />\n <div className=\"flex items-start justify-between gap-(--space-3)\">\n <div className=\"text-caption text-text-secondary\">\n {description ? (\n // spec §5: label AND description text are text-secondary; text-muted is\n // reserved for the character counter at rest (see the counter below).\n <span id={descId} className=\"text-caption text-text-secondary\">\n {description}\n </span>\n ) : null}\n {hasError ? (\n <span id={errId} className=\"text-caption text-status-critical-on-surface\">\n {error}\n </span>\n ) : null}\n </div>\n {hasCounter ? (\n <span\n id={counterId}\n data-testid=\"textarea-counter\"\n aria-live=\"polite\"\n className=\"text-caption text-text-
|
|
14
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Label } from \"@/components/ui/label\";\nimport { textareaVariants, type TextareaVariantProps } from \"./textarea.variants\";\n\nexport interface TextareaProps\n extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, \"id\">,\n TextareaVariantProps {\n /** Bound visible label text (for/id). Required — the placeholder is never the name. */\n label: React.ReactNode;\n /** Help text below the field, linked via aria-describedby. */\n description?: React.ReactNode;\n /** Error message: reds the border, sets aria-invalid, joins aria-describedby. */\n error?: React.ReactNode;\n /** Field id; auto-generated from React.useId when omitted. */\n id?: string;\n /** auto-grow lower bound (rows). */\n minRows?: number;\n /** auto-grow upper bound (rows) — beyond it the field scrolls. */\n maxRows?: number;\n}\n\n/** Merge non-null describedby ids into a single attribute value (or undefined). */\nfunction describedBy(...ids: (string | false | undefined)[]): string | undefined {\n const present = ids.filter(Boolean) as string[];\n return present.length ? present.join(\" \") : undefined;\n}\n\nexport const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n function Textarea(\n {\n className,\n resize = \"vertical\",\n label,\n description,\n error,\n id,\n minRows = 3,\n maxRows = 8,\n rows,\n maxLength,\n value,\n defaultValue,\n onChange,\n disabled,\n readOnly,\n required,\n ...props\n },\n forwardedRef,\n ) {\n const autoId = React.useId();\n const fieldId = id ?? autoId;\n const descId = `${fieldId}-desc`;\n const errId = `${fieldId}-err`;\n const counterId = `${fieldId}-counter`;\n\n const innerRef = React.useRef<HTMLTextAreaElement>(null);\n React.useImperativeHandle(\n forwardedRef,\n () => innerRef.current as HTMLTextAreaElement,\n );\n\n // counter state: read controlled value when present, else track length locally\n const isControlled = value !== undefined;\n const [count, setCount] = React.useState(\n String(value ?? defaultValue ?? \"\").length,\n );\n const length = isControlled ? String(value ?? \"\").length : count;\n\n const isAutoGrow = resize === \"auto-grow\";\n\n // auto-grow: measure scrollHeight, then clamp the height to a ceiling computed\n // from maxRows × line-height. Beyond the ceiling the field scrolls (overflowY\n // auto) instead of growing unbounded; below it the field hugs its content\n // (overflowY hidden). The ceiling is written to el.style.maxHeight — there is no\n // class for it, so nothing references an undefined custom property.\n const resizeToContent = React.useCallback(() => {\n const el = innerRef.current;\n if (!el || !isAutoGrow) return;\n const style = window.getComputedStyle(el);\n // jsdom and \"normal\" line-height both yield NaN here; px() coerces any\n // non-finite value to 0 (or the fallback) so the ceiling stays a real number.\n const px = (v: string, fallback = 0) => {\n const n = parseFloat(v);\n return Number.isFinite(n) ? n : fallback;\n };\n // line-height may compute to \"normal\"; fall back to ~1.5× font size, else 20px.\n const lineHeight = px(style.lineHeight) || px(style.fontSize) * 1.5 || 20;\n const borders = px(style.borderTopWidth) + px(style.borderBottomWidth);\n const padding = px(style.paddingTop) + px(style.paddingBottom);\n const ceiling = maxRows * lineHeight + padding + borders;\n // measure the natural content height from a collapsed baseline\n el.style.height = \"auto\";\n const next = Math.min(el.scrollHeight, ceiling);\n el.style.height = `${next}px`;\n el.style.maxHeight = `${ceiling}px`;\n // scroll only once content exceeds the ceiling; hug content otherwise\n el.style.overflowY = el.scrollHeight > ceiling ? \"auto\" : \"hidden\";\n }, [isAutoGrow, maxRows]);\n\n React.useLayoutEffect(() => {\n if (isAutoGrow) resizeToContent();\n }, [isAutoGrow, resizeToContent, value]);\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n if (!isControlled) setCount(e.target.value.length);\n if (isAutoGrow) resizeToContent();\n onChange?.(e);\n };\n\n const hasCounter = maxLength !== undefined;\n const hasError = error != null && error !== false;\n\n return (\n <div className=\"flex flex-col gap-(--space-3)\">\n {/* Compose the Label primitive (define-once): it owns the canonical\n label role/color (text-label / text-text-primary) and the disabled\n dim. Passing `disabled` lets a disabled Textarea's name dim to\n --color-text-disabled via Label's own cva variant, and `required`\n surfaces the required mark there (the field still carries the real\n required / aria-required state below). textarea.md §5 lists\n text-secondary for the label, but label.md §3–5 is authoritative for\n the Label primitive: resting label text is text-text-primary. */}\n <Label htmlFor={fieldId} disabled={disabled} required={required}>\n {label}\n </Label>\n <textarea\n ref={innerRef}\n id={fieldId}\n // native <textarea> already carries an implicit aria-multiline; make the\n // multi-line contract explicit per the frozen ARIA contract (spec §7).\n aria-multiline=\"true\"\n rows={isAutoGrow ? minRows : rows}\n maxLength={maxLength}\n value={value}\n defaultValue={defaultValue}\n onChange={handleChange}\n disabled={disabled}\n readOnly={readOnly}\n required={required}\n aria-required={required || undefined}\n aria-invalid={hasError || undefined}\n aria-describedby={describedBy(\n // coerce each guard to a boolean so the expression narrows to\n // string | false (React.ReactNode can be null/0/0n, which would widen\n // the union past the describedBy signature otherwise)\n !!description && descId,\n hasError && errId,\n hasCounter && counterId,\n )}\n style={isAutoGrow ? { overflowY: \"hidden\" } : undefined}\n className={cn(textareaVariants({ resize }), className)}\n {...props}\n />\n <div className=\"flex items-start justify-between gap-(--space-3)\">\n <div className=\"text-caption text-text-secondary\">\n {description ? (\n // spec §5: label AND description text are text-secondary; text-muted is\n // reserved for the character counter at rest (see the counter below).\n <span id={descId} className=\"text-caption text-text-secondary\">\n {description}\n </span>\n ) : null}\n {hasError ? (\n <span id={errId} className=\"text-caption text-status-critical-on-surface\">\n {error}\n </span>\n ) : null}\n </div>\n {hasCounter ? (\n <span\n id={counterId}\n data-testid=\"textarea-counter\"\n aria-live=\"polite\"\n className=\"text-caption text-text-secondary tabular-nums\"\n >\n {length}/{maxLength}\n </span>\n ) : null}\n </div>\n </div>\n );\n },\n);\n",
|
|
15
15
|
"path": "textarea/textarea.tsx",
|
|
16
16
|
"target": "@ui/textarea/textarea.tsx",
|
|
17
17
|
"type": "registry:ui"
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// The multi-line text field. Token binding lives ONLY here. Native <textarea>, no Radix.\n// The closed state set is default·hover·focus·disabled·read-only·error (textarea.md §4) —\n// loading and pressed do NOT apply and are dropped. Neutrals carry the field; the only\n// accent is the critical status in the error state — never brand violet for a state,\n// never a status color as a flourish (§3, §8).\nexport const textareaVariants = cva(\n [\n // shape + radius + internal padding; control intent tier carries the field\n \"block w-full rounded-md px-(--space-2) py-(--space-2)\",\n // DEC-A — the value SIZE is text-base (16px) so iOS never zooms on focus; the\n // brand BODY line-height + letter-spacing ride along via the role-suffix vars.\n // text-body itself (0.9375rem / 15px) is NEVER bound on a form field: under the\n // role-aware cn it would collapse against text-base, and 15px would reintroduce\n // the iOS focus-zoom that the 16px reset exists to prevent.\n \"text-base leading-(--text-body--line-height) tracking-(--text-body--letter-spacing)\",\n // resting fill / border / text / placeholder — control intent tier\n \"bg-control-bg border border-control-border text-control-fg\",\n \"placeholder:text-control-placeholder\",\n // hover darkens the border only; fill is unchanged (restraint)\n \"hover:border-border-strong\",\n // focus: visible 2px signal-blue ring at 2px offset + focused border, never\n // removed (2.4.7); fast functional transition, no theatre\n \"outline-none transition-[color,border-color,box-shadow] duration-(--motion-duration-fast)\",\n
|
|
20
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// The multi-line text field. Token binding lives ONLY here. Native <textarea>, no Radix.\n// The closed state set is default·hover·focus·disabled·read-only·error (textarea.md §4) —\n// loading and pressed do NOT apply and are dropped. Neutrals carry the field; the only\n// accent is the critical status in the error state — never brand violet for a state,\n// never a status color as a flourish (§3, §8).\nexport const textareaVariants = cva(\n [\n // shape + radius + internal padding; control intent tier carries the field\n \"block w-full rounded-md px-(--space-2) py-(--space-2)\",\n // DEC-A — the value SIZE is text-base (16px) so iOS never zooms on focus; the\n // brand BODY line-height + letter-spacing ride along via the role-suffix vars.\n // text-body itself (0.9375rem / 15px) is NEVER bound on a form field: under the\n // role-aware cn it would collapse against text-base, and 15px would reintroduce\n // the iOS focus-zoom that the 16px reset exists to prevent.\n \"text-base leading-(--text-body--line-height) tracking-(--text-body--letter-spacing)\",\n // resting fill / border / text / placeholder — control intent tier\n \"bg-control-bg border border-control-border text-control-fg\",\n \"placeholder:text-control-placeholder\",\n // hover darkens the border only; fill is unchanged (restraint)\n \"hover:border-border-strong\",\n // focus: visible 2px signal-blue ring at 2px offset + focused border, never\n // removed (2.4.7); fast functional transition, no theatre\n \"outline-none transition-[color,border-color,box-shadow] duration-(--motion-duration-fast)\",\n focusRing,\n \"focus-visible:border-border-focus\",\n \"motion-reduce:transition-none\",\n // ERROR is the only colored field state — it borrows the STATUS color, never the\n // brand (§3, §8). Driven by the native aria-invalid attribute.\n \"aria-invalid:border-status-critical-border\",\n // disabled: reduced-emphasis text + placeholder, not editable\n \"disabled:cursor-not-allowed disabled:text-text-disabled\",\n \"disabled:placeholder:text-text-disabled\",\n // 44px mobile / 40px desktop minimum block-size floor, logical (a11y target size)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n ],\n {\n variants: {\n resize: {\n // reader drags the height; content may exceed the start height\n vertical: \"resize-y\",\n // fixed height; reader scrolls within it\n none: \"resize-none\",\n // height is driven programmatically between minRows/maxRows, then scrolls;\n // the manual grip is disabled. The ceiling is NOT a class — it is computed\n // from maxRows × line-height in the layout effect and written to\n // el.style.maxHeight (a bare CSS-var class here would reference an undefined\n // custom property and the token-binding gate would flag a non-token var).\n \"auto-grow\": \"resize-none\",\n },\n },\n defaultVariants: { resize: \"vertical\" },\n },\n);\n\nexport type TextareaVariantProps = VariantProps<typeof textareaVariants>;\n",
|
|
21
21
|
"path": "textarea/textarea.variants.ts",
|
|
22
22
|
"target": "@ui/textarea/textarea.variants.ts",
|
|
23
23
|
"type": "registry:ui"
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"name": "textarea",
|
|
27
27
|
"registryDependencies": [
|
|
28
28
|
"@verdify/cn",
|
|
29
|
+
"@verdify/focus-ring",
|
|
29
30
|
"@verdify/label"
|
|
30
31
|
],
|
|
31
32
|
"title": "textarea",
|
package/registry/toast.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"type": "registry:ui"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// A Toast is a brief, transient feedback surface that floats over the page to confirm an action or\n// report an async result without taking focus (spec §1). It is NOT the brand: its type is carried\n// by the status it reports, never by Sovereign Violet, so this file binds nothing from the\n// --color-action-primary-* tier as a status (brand != state, G-U2). The only action-tier utility\n// used is the NEUTRAL ghost treatment on the inline action and dismiss controls — that is the\n// control's own treatment, not the toast's status (the brand != state gate scopes status keys to\n// the toast variant axis only). This is the ONLY token-binding site (skill §5 hard rule).\n\n// The viewport: the corner-anchored live region that holds the stack of toasts (spec §2 region). It\n// is the fixed <ol> on the toast z-layer (spec §5 --z-index-toast — the dedicated layer above page\n// content, distinct from the modal layer the overlays use). Logical-property anchoring (G-U6): the\n// stack sits at the block-end inline-end corner and flows as a column with the stack gap. It is a\n// landmark region (Radix role=region), decorative of fill — neutral, no status color.\nexport const toastViewportClass =\n \"fixed bottom-0 end-0 z-(--z-index-toast) \" +\n \"flex max-h-screen w-full flex-col gap-(--space-3) p-(--space-4) \" +\n \"sm:max-w-(--container-sm)\";\n\n// The toast: one message in the stack (spec §2 toast). A NEUTRAL raised surface (spec §3/§5:\n// neutrals carry the body) with the lg elevation shadow above the page, the md corner radius, and a\n// 1px border whose COLOR is the single status accent — the status is an edge + the icon, never a\n// saturated fill that floods the surface (spec §3/§5, restraint over volume). The status border is\n// the ONLY status binding on the container; bg/text stay neutral.\n//\n// Enter/exit is a plain slide-and-fade on the BASE duration + AMBIENT easing (spec §4/§5), riding\n// Radix's data-state and data-swipe attributes (attribute-selector variants, not arbitrary values).\n// It collapses to the instant endpoint under reduced motion. A toast NEVER uses the 350ms\n// VerifiedBadge-only theatre duration — that motion is the one piece of theatre in the system and is\n// reserved for the verified moment (G-U3 motion-theatre gate). Even a `verified` toast slides in on\n// the base duration.\n//\n// variant = the type the toast reports (spec §3): the four statuses map one-to-one to the\n// --color-status-*-border accent; there is no neutral or brand-colored toast. NONE binds\n// --color-action-* as a status (brand != state, spec §3/§8).\nexport const toastVariants = cva(\n [\n // layout: a row holding the leading type icon, the stacked message column, then the inline\n // action and the dismiss control; logical-property gap so it mirrors under dir=\"rtl\" (G-U6)\n \"flex items-start gap-(--space-3)\",\n // the raised surface: neutral fill, neutral outer-surface fallback, internal padding off the\n // edge, the md corner radius, the lg elevation shadow above the page content (spec §5)\n \"bg-surface-raised rounded-(--radius-md) border p-(--space-3) shadow-(--shadow-lg)\",\n // logical-property text alignment so the toast mirrors under dir=\"rtl\" (G-U6)\n \"text-start\",\n // enter/exit slide-and-fade: the BASE duration on AMBIENT easing (spec §4/§5), collapsing to\n // the instant endpoint under reduced motion. NEVER the verified-check theatre (G-U3).\n \"transition-[opacity,transform] duration-(--motion-duration-base) ease-(--motion-easing-ambient)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // enter/exit ride Radix's data-state — attribute-selector variants, not arbitrary values\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\",\n // swipe-to-dismiss rides Radix's data-swipe — the toast follows the pointer on move, snaps back\n // on cancel, and fades out on end (attribute-selector variants, not arbitrary values, spec §4)\n \"data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)]\",\n \"data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform]\",\n \"data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]\",\n \"data-[swipe=end]:opacity-0\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3: the type the toast reports. Each maps to the matching\n // --color-status-*-border accent on the neutral surface (spec §3/§5).\n variant: {\n // an action succeeded / a claim is now proven — the in-product Verified Green status,\n // NEVER the brand (spec §3/§8)\n verified: \"border-status-verified-border\",\n // a neutral, informational result that needs no action (spec §3)\n signal: \"border-status-signal-border\",\n // a non-blocking warning — completed with a caveat worth seeing (spec §3)\n caution: \"border-status-caution-border\",\n // a failure or error — the highest-urgency type (spec §3)\n critical: \"border-status-critical-border\",\n },\n },\n // the lowest-urgency status is the default; a louder type is spent only when warranted (spec §8)\n defaultVariants: { variant: \"signal\" },\n },\n);\n\n// The type icon (spec §2/§5): a type icon reinforcing the status at the md icon role. It pairs\n// with the message text so status is never carried by color or icon alone (WCAG 1.4.1, spec §2).\n// Because the icon is DECORATIVE (aria-hidden in tsx) and the message text carries the status\n// meaning, it is exempt from the AA text floor and takes the BRIGHT variant ACCENT via the\n// matching --color-status-*-accent (tokens 0.6.0) — the vivid status color reads as an emphasis\n// mark, while the readable message keeps its AA text token.\nexport const toastIconVariants = cva(\n \"inline-flex shrink-0 h-(--size-icon-md) w-(--size-icon-md)\",\n {\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\n// The stacked message column (spec §2): the primary message and an optional secondary line stack\n// vertically beside the leading icon. min-w-0 lets long message text wrap instead of overflowing the\n// row, and flex-1 lets it take the available width before the action/dismiss controls.\nexport const toastContentClass = \"flex min-w-0 flex-1 flex-col gap-(--space-2)\";\n\n// The message (spec §2/§5): the text. A plain statement ending in a period; for a failure it names\n// what failed and what to do next, and never blames you (spec §2). The body type role in primary\n// text. It is the toast's accessible name (Radix wires aria-labelledby). Brand violet never paints it.\nexport const toastTitleClass = \"text-body text-text-primary\";\n\n// The secondary message line (spec §2/§5): a supporting line under the message, where used. The body\n// type role in --color-text-secondary. It is the toast's accessible description (Radix wires\n// aria-describedby).\nexport const toastDescriptionClass = \"text-body text-text-secondary\";\n\n// The inline action (spec §2/§6): at most one inline action, phrased as a verb (\"Undo\", \"View\"). A\n// NEUTRAL ghost surface — the label in --color-action-ghost-fg at rest, the restrained ghost hover\n// fill (no bg/border at rest) — so the action is neutral and never competes with the status accent.\n// The ghost fg/hover is the control's OWN action treatment, not the toast's status (the brand !=\n// state gate scopes status keys to the toast container variant). It is a real focus stop (Radix\n// ToastAction renders a native <button>) with the label type role, the target-size floor (44px touch\n// / 40px pointer, spec §5 / 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).\nexport const toastActionClass =\n \"inline-flex shrink-0 items-center justify-center rounded-(--radius-md) px-(--space-3) \" +\n \"text-label font-medium \" +\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) 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 dismiss control (spec §2/§6/§7): an explicit close on the inline-end edge for toasts that do\n// not auto-dismiss and for pointer users clearing one early. A NEUTRAL ghost icon-button — the glyph\n// in --color-action-ghost-fg at rest, the restrained ghost hover fill — so it never competes with\n// the status. A real focus stop (Radix ToastClose renders a native <button>) with the square\n// target-size floor (44px touch / 40px pointer, spec §5 / DEC-B; height EMERGES from the floor,\n// never fixed below it), the persistent focus ring, and the fast functional hover motion + verdify\n// easing, instant under reduced motion (G-U3).\nexport const toastCloseClass =\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 \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\";\n\n// The dismiss glyph (spec §7): a neutral X at the md icon role, drawn with currentColor so it\n// inherits the button's ghost fg. Decorative (aria-hidden in tsx) — the button carries the name.\nexport const toastCloseGlyphClass = \"h-(--size-icon-md) w-(--size-icon-md)\";\n\nexport type ToastVariantProps = VariantProps<typeof toastVariants>;\n",
|
|
21
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { focusRing } from \"@/lib/focus-ring\";\n\n// A Toast is a brief, transient feedback surface that floats over the page to confirm an action or\n// report an async result without taking focus (spec §1). It is NOT the brand: its type is carried\n// by the status it reports, never by Sovereign Violet, so this file binds nothing from the\n// --color-action-primary-* tier as a status (brand != state, G-U2). The only action-tier utility\n// used is the NEUTRAL ghost treatment on the inline action and dismiss controls — that is the\n// control's own treatment, not the toast's status (the brand != state gate scopes status keys to\n// the toast variant axis only). This is the ONLY token-binding site (skill §5 hard rule).\n\n// The viewport: the corner-anchored live region that holds the stack of toasts (spec §2 region). It\n// is the fixed <ol> on the toast z-layer (spec §5 --z-index-toast — the dedicated layer above page\n// content, distinct from the modal layer the overlays use). Logical-property anchoring (G-U6): the\n// stack sits at the block-end inline-end corner and flows as a column with the stack gap. It is a\n// landmark region (Radix role=region), decorative of fill — neutral, no status color.\nexport const toastViewportClass =\n \"fixed bottom-0 end-0 z-(--z-index-toast) \" +\n \"flex max-h-screen w-full flex-col gap-(--space-3) p-(--space-4) \" +\n \"sm:max-w-(--container-sm)\";\n\n// The toast: one message in the stack (spec §2 toast). A NEUTRAL raised surface (spec §3/§5:\n// neutrals carry the body) with the lg elevation shadow above the page, the md corner radius, and a\n// 1px border whose COLOR is the single status accent — the status is an edge + the icon, never a\n// saturated fill that floods the surface (spec §3/§5, restraint over volume). The status border is\n// the ONLY status binding on the container; bg/text stay neutral.\n//\n// Enter/exit is a plain slide-and-fade on the BASE duration + AMBIENT easing (spec §4/§5), riding\n// Radix's data-state and data-swipe attributes (attribute-selector variants, not arbitrary values).\n// It collapses to the instant endpoint under reduced motion. A toast NEVER uses the 350ms\n// VerifiedBadge-only theatre duration — that motion is the one piece of theatre in the system and is\n// reserved for the verified moment (G-U3 motion-theatre gate). Even a `verified` toast slides in on\n// the base duration.\n//\n// variant = the type the toast reports (spec §3): the four statuses map one-to-one to the\n// --color-status-*-border accent; there is no neutral or brand-colored toast. NONE binds\n// --color-action-* as a status (brand != state, spec §3/§8).\nexport const toastVariants = cva(\n [\n // layout: a row holding the leading type icon, the stacked message column, then the inline\n // action and the dismiss control; logical-property gap so it mirrors under dir=\"rtl\" (G-U6)\n \"flex items-start gap-(--space-3)\",\n // the raised surface: neutral fill, neutral outer-surface fallback, internal padding off the\n // edge, the md corner radius, the lg elevation shadow above the page content (spec §5)\n \"bg-surface-raised rounded-(--radius-md) border p-(--space-3) shadow-(--shadow-lg)\",\n // logical-property text alignment so the toast mirrors under dir=\"rtl\" (G-U6)\n \"text-start\",\n // enter/exit slide-and-fade: the BASE duration on AMBIENT easing (spec §4/§5), collapsing to\n // the instant endpoint under reduced motion. NEVER the verified-check theatre (G-U3).\n \"transition-[opacity,transform] duration-(--motion-duration-base) ease-(--motion-easing-ambient)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // enter/exit ride Radix's data-state — attribute-selector variants, not arbitrary values\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\",\n // swipe-to-dismiss rides Radix's data-swipe — the toast follows the pointer on move, snaps back\n // on cancel, and fades out on end (attribute-selector variants, not arbitrary values, spec §4)\n \"data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)]\",\n \"data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform]\",\n \"data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]\",\n \"data-[swipe=end]:opacity-0\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3: the type the toast reports. Each maps to the matching\n // --color-status-*-border accent on the neutral surface (spec §3/§5).\n variant: {\n // an action succeeded / a claim is now proven — the in-product Verified Green status,\n // NEVER the brand (spec §3/§8)\n verified: \"border-status-verified-border\",\n // a neutral, informational result that needs no action (spec §3)\n signal: \"border-status-signal-border\",\n // a non-blocking warning — completed with a caveat worth seeing (spec §3)\n caution: \"border-status-caution-border\",\n // a failure or error — the highest-urgency type (spec §3)\n critical: \"border-status-critical-border\",\n },\n },\n // the lowest-urgency status is the default; a louder type is spent only when warranted (spec §8)\n defaultVariants: { variant: \"signal\" },\n },\n);\n\n// The type icon (spec §2/§5): a type icon reinforcing the status at the md icon role. It pairs\n// with the message text so status is never carried by color or icon alone (WCAG 1.4.1, spec §2).\n// Because the icon is DECORATIVE (aria-hidden in tsx) and the message text carries the status\n// meaning, it is exempt from the AA text floor and takes the BRIGHT variant ACCENT via the\n// matching --color-status-*-accent (tokens 0.6.0) — the vivid status color reads as an emphasis\n// mark, while the readable message keeps its AA text token.\nexport const toastIconVariants = cva(\n \"inline-flex shrink-0 h-(--size-icon-md) w-(--size-icon-md)\",\n {\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\n// The stacked message column (spec §2): the primary message and an optional secondary line stack\n// vertically beside the leading icon. min-w-0 lets long message text wrap instead of overflowing the\n// row, and flex-1 lets it take the available width before the action/dismiss controls.\nexport const toastContentClass = \"flex min-w-0 flex-1 flex-col gap-(--space-2)\";\n\n// The message (spec §2/§5): the text. A plain statement ending in a period; for a failure it names\n// what failed and what to do next, and never blames you (spec §2). The body type role in primary\n// text. It is the toast's accessible name (Radix wires aria-labelledby). Brand violet never paints it.\nexport const toastTitleClass = \"text-body text-text-primary\";\n\n// The secondary message line (spec §2/§5): a supporting line under the message, where used. The body\n// type role in --color-text-secondary. It is the toast's accessible description (Radix wires\n// aria-describedby).\nexport const toastDescriptionClass = \"text-body text-text-secondary\";\n\n// The inline action (spec §2/§6): at most one inline action, phrased as a verb (\"Undo\", \"View\"). A\n// NEUTRAL ghost surface — the label in --color-action-ghost-fg at rest, the restrained ghost hover\n// fill (no bg/border at rest) — so the action is neutral and never competes with the status accent.\n// The ghost fg/hover is the control's OWN action treatment, not the toast's status (the brand !=\n// state gate scopes status keys to the toast container variant). It is a real focus stop (Radix\n// ToastAction renders a native <button>) with the label type role, the target-size floor (44px touch\n// / 40px pointer, spec §5 / 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).\nexport const toastActionClass =\n \"inline-flex shrink-0 items-center justify-center rounded-(--radius-md) px-(--space-3) \" +\n \"text-label font-medium \" +\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) sm:min-h-(--size-target-desktop) \" +\n focusRing;\n\n// The dismiss control (spec §2/§6/§7): an explicit close on the inline-end edge for toasts that do\n// not auto-dismiss and for pointer users clearing one early. A NEUTRAL ghost icon-button — the glyph\n// in --color-action-ghost-fg at rest, the restrained ghost hover fill — so it never competes with\n// the status. A real focus stop (Radix ToastClose renders a native <button>) with the square\n// target-size floor (44px touch / 40px pointer, spec §5 / DEC-B; height EMERGES from the floor,\n// never fixed below it), the persistent focus ring, and the fast functional hover motion + verdify\n// easing, instant under reduced motion (G-U3).\nexport const toastCloseClass =\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 md icon role, drawn with currentColor so it\n// inherits the button's ghost fg. Decorative (aria-hidden in tsx) — the button carries the name.\nexport const toastCloseGlyphClass = \"h-(--size-icon-md) w-(--size-icon-md)\";\n\nexport type ToastVariantProps = VariantProps<typeof toastVariants>;\n",
|
|
22
22
|
"path": "toast/toast.variants.ts",
|
|
23
23
|
"target": "@ui/toast/toast.variants.ts",
|
|
24
24
|
"type": "registry:ui"
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
],
|
|
27
27
|
"name": "toast",
|
|
28
28
|
"registryDependencies": [
|
|
29
|
-
"@verdify/cn"
|
|
29
|
+
"@verdify/cn",
|
|
30
|
+
"@verdify/focus-ring"
|
|
30
31
|
],
|
|
31
32
|
"title": "toast",
|
|
32
33
|
"type": "registry:ui"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"type": "registry:ui"
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// TrustScore displays the cross-ecosystem trust score Verdify owns and computes — a\n// single rolled-up number, shown so the reader can read where an identity stands at a\n// glance (spec §1). It is a status OUTPUT, not a control: it takes no focus, binds no\n// keys, is never a tab stop, and renders no focus ring or target-size floor — the value\n// reaches assistive technology through a named group + a polite live region, never\n// through motion (spec §4/§6/§7).\n//\n// CALM BY CONTRACT — brand != state (spec §1/§3/§5/§8). A trust score is a MEASURE, not\n// a verdict and not a credential. TrustScore therefore paints from NEUTRAL text/surface\n// roles and the neutral --color-control-* roles for the optional gauge, and binds:\n// * NOTHING from the action tier — the brand (Sovereign Violet) is never used to make\n// the score look important, because the brand is not a status; and\n// * NOTHING from the status tier — the verified-status green is never used to color a\n// \"high\" score (that green means one verified result and nothing else, so lending it\n// to a number tells the reader the score is a verification it is not), and there is\n// no caution/critical \"danger\" band — banding the number by color turns a calm\n// measure into an alarmist verdict.\n// There is no \"good\"/\"warning\"/\"danger\" variant: the variants below are forms of DISPLAY\n// (value / meter / compact), never levels of alarm. This whole-tree neutrality is pinned\n// as a component test in trust-score.test.tsx (the static brand!=state gate only catches\n// a cva-keyed status<->action cross-wire, not a neutral-leak — see the build-on-brand\n// skill, note B).\n//\n// The value also never animates on the deliberate verified-check duration — that 350ms\n// theatre is the verified check's alone, and a score update is not a verification; the\n// update settles on the fast duration with verdify easing (spec §4).\n\n// The root container (spec §2): a layout column stacking the value/scale row, the optional\n// label, the optional gauge, and the optional description at the --space-2 gap. Non-\n// interactive: no focus ring, no tab stop, no target-size floor (spec §4/§6/§7). When the\n// score sits in its own card the `carded` axis adds the neutral raised surface, the muted\n// surface border, the medium corner radius, and the --space-3 internal padding (spec §5).\nexport const trustScoreVariants = cva(\"flex flex-col gap-(--space-2)\", {\n variants: {\n // STRUCTURAL axis = spec §3: a form of DISPLAY, never a level of alarm. None of the\n // variants recolors the readout — they differ only in what is shown and at which\n // type role, so the meaning is identical across all three.\n variant: {\n // value (default): the number with its label and optional scale, no gauge — the\n // common, calmest case (a profile header, a row, a card).\n value: \"\",\n // meter: the number plus the neutral track gauge, for a surface where seeing the\n // value against its scale helps. The gauge is a calm rail in neutral control roles.\n meter: \"\",\n // compact: the bare number at a smaller footprint for dense rows and inline\n // contexts; the full label + scale still reach assistive technology via the group\n // name. Same meaning at a smaller type role, not a different component.\n compact: \"\",\n },\n // When carded, paint the neutral container: the raised surface, the muted surface\n // border, the medium corner radius, and the internal padding (spec §5). No status,\n // no action — a calm container for a calm measure.\n carded: {\n true: \"rounded-(--radius-md) border bg-surface-raised border-surface-border-muted p-(--space-3)\",\n false: \"\",\n },\n },\n defaultVariants: { variant: \"value\", carded: false },\n});\n\n// The value/scale row (spec §2): the number and its optional inline scale, baseline-\n// aligned at the small gap.\nexport const trustScoreRowClass = \"flex items-baseline gap-(--space-2)\";\n\n// The score numeric readout (spec §2/§4/§5): the trust score itself, in TABULAR figures\n// so digits hold their place when the value updates, at the h2 type role in the PRIMARY\n// text color. It is a polite live region (set in tsx) so a changed score is announced\n// without interruption; the value-settle transition runs on the FAST duration with\n// verdify easing, collapsing to the instant endpoint under reduced motion — a moving\n// trust score is information, not an event, and NEVER the 350ms verified-check theatre\n// duration (spec §4). The compact form drops the type role to the label role below.\nexport const trustScoreValueClass =\n \"tabular-nums text-h2 text-text-primary \" +\n \"transition-[color,opacity] duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\n// The compact value (spec §3): the same primary readout at the smaller label type role\n// for dense rows. Same tabular figures and the same settle transition.\nexport const trustScoreValueCompactClass =\n \"tabular-nums text-label text-text-primary \" +\n \"transition-[color,opacity] duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\n// The label (spec §2/§5): the text naming what the number is (\"Trust score\"), in the\n// secondary text color at the label type role. TrustScore is never an unlabeled number.\nexport const trustScoreLabelClass = \"text-label text-text-secondary\";\n\n// The scale (spec §2/§5): the plain bound the number reads against (\"out of 100\"), in the\n// secondary text color at the label type role, so the value is legible without guessing\n// the range.\nexport const trustScoreScaleClass = \"text-label text-text-secondary\";\n\n// The optional one-line description (spec §2/§5): a statement of what the score means or\n// when it last changed, in the secondary text color at the caption role.\nexport const trustScoreDescriptionClass = \"text-caption text-text-secondary\";\n\n// The unavailable message (spec §4/§5/§7): plain text where the number would be (\"Not yet\n// scored\" / \"Score unavailable\"),
|
|
20
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// TrustScore displays the cross-ecosystem trust score Verdify owns and computes — a\n// single rolled-up number, shown so the reader can read where an identity stands at a\n// glance (spec §1). It is a status OUTPUT, not a control: it takes no focus, binds no\n// keys, is never a tab stop, and renders no focus ring or target-size floor — the value\n// reaches assistive technology through a named group + a polite live region, never\n// through motion (spec §4/§6/§7).\n//\n// CALM BY CONTRACT — brand != state (spec §1/§3/§5/§8). A trust score is a MEASURE, not\n// a verdict and not a credential. TrustScore therefore paints from NEUTRAL text/surface\n// roles and the neutral --color-control-* roles for the optional gauge, and binds:\n// * NOTHING from the action tier — the brand (Sovereign Violet) is never used to make\n// the score look important, because the brand is not a status; and\n// * NOTHING from the status tier — the verified-status green is never used to color a\n// \"high\" score (that green means one verified result and nothing else, so lending it\n// to a number tells the reader the score is a verification it is not), and there is\n// no caution/critical \"danger\" band — banding the number by color turns a calm\n// measure into an alarmist verdict.\n// There is no \"good\"/\"warning\"/\"danger\" variant: the variants below are forms of DISPLAY\n// (value / meter / compact), never levels of alarm. This whole-tree neutrality is pinned\n// as a component test in trust-score.test.tsx (the static brand!=state gate only catches\n// a cva-keyed status<->action cross-wire, not a neutral-leak — see the build-on-brand\n// skill, note B).\n//\n// The value also never animates on the deliberate verified-check duration — that 350ms\n// theatre is the verified check's alone, and a score update is not a verification; the\n// update settles on the fast duration with verdify easing (spec §4).\n\n// The root container (spec §2): a layout column stacking the value/scale row, the optional\n// label, the optional gauge, and the optional description at the --space-2 gap. Non-\n// interactive: no focus ring, no tab stop, no target-size floor (spec §4/§6/§7). When the\n// score sits in its own card the `carded` axis adds the neutral raised surface, the muted\n// surface border, the medium corner radius, and the --space-3 internal padding (spec §5).\nexport const trustScoreVariants = cva(\"flex flex-col gap-(--space-2)\", {\n variants: {\n // STRUCTURAL axis = spec §3: a form of DISPLAY, never a level of alarm. None of the\n // variants recolors the readout — they differ only in what is shown and at which\n // type role, so the meaning is identical across all three.\n variant: {\n // value (default): the number with its label and optional scale, no gauge — the\n // common, calmest case (a profile header, a row, a card).\n value: \"\",\n // meter: the number plus the neutral track gauge, for a surface where seeing the\n // value against its scale helps. The gauge is a calm rail in neutral control roles.\n meter: \"\",\n // compact: the bare number at a smaller footprint for dense rows and inline\n // contexts; the full label + scale still reach assistive technology via the group\n // name. Same meaning at a smaller type role, not a different component.\n compact: \"\",\n },\n // When carded, paint the neutral container: the raised surface, the muted surface\n // border, the medium corner radius, and the internal padding (spec §5). No status,\n // no action — a calm container for a calm measure.\n carded: {\n true: \"rounded-(--radius-md) border bg-surface-raised border-surface-border-muted p-(--space-3)\",\n false: \"\",\n },\n },\n defaultVariants: { variant: \"value\", carded: false },\n});\n\n// The value/scale row (spec §2): the number and its optional inline scale, baseline-\n// aligned at the small gap.\nexport const trustScoreRowClass = \"flex items-baseline gap-(--space-2)\";\n\n// The score numeric readout (spec §2/§4/§5): the trust score itself, in TABULAR figures\n// so digits hold their place when the value updates, at the h2 type role in the PRIMARY\n// text color. It is a polite live region (set in tsx) so a changed score is announced\n// without interruption; the value-settle transition runs on the FAST duration with\n// verdify easing, collapsing to the instant endpoint under reduced motion — a moving\n// trust score is information, not an event, and NEVER the 350ms verified-check theatre\n// duration (spec §4). The compact form drops the type role to the label role below.\nexport const trustScoreValueClass =\n \"tabular-nums text-h2 text-text-primary \" +\n \"transition-[color,opacity] duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\n// The compact value (spec §3): the same primary readout at the smaller label type role\n// for dense rows. Same tabular figures and the same settle transition.\nexport const trustScoreValueCompactClass =\n \"tabular-nums text-label text-text-primary \" +\n \"transition-[color,opacity] duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\n// The label (spec §2/§5): the text naming what the number is (\"Trust score\"), in the\n// secondary text color at the label type role. TrustScore is never an unlabeled number.\nexport const trustScoreLabelClass = \"text-label text-text-secondary\";\n\n// The scale (spec §2/§5): the plain bound the number reads against (\"out of 100\"), in the\n// secondary text color at the label type role, so the value is legible without guessing\n// the range.\nexport const trustScoreScaleClass = \"text-label text-text-secondary\";\n\n// The optional one-line description (spec §2/§5): a statement of what the score means or\n// when it last changed, in the secondary text color at the caption role.\nexport const trustScoreDescriptionClass = \"text-caption text-text-secondary\";\n\n// The unavailable message (spec §4/§5/§7): plain text where the number would be (\"Not yet\n// scored\" / \"Score unavailable\"), at the caption role, naming the absence honestly rather than\n// showing a misleading 0 or a low number. It is essential text, so it uses the SECONDARY text color\n// (AA), not the decorative-only muted role (accessibility.md). Honest about hard things: an absent\n// score is said, not faked.\nexport const trustScoreUnavailableClass = \"text-caption text-text-secondary\";\n\n// The optional meter track (spec §2/§5): a restrained gauge rail visualising the score\n// against its scale, in the NEUTRAL control surface with the control border, clipped so\n// the rounded indicator end never bleeds past the rail, on the full radius. It is a calm\n// gauge, NEVER a progress fill racing toward a target and NEVER recolored to a status to\n// imply alarm or approval (spec §2/§3/§4) — the non-text contrast bar (1.4.11) is met by\n// the control roles, not by a colored band.\nexport const trustScoreTrackClass =\n \"relative block h-(--space-2) w-full overflow-hidden \" +\n \"rounded-(--radius-full) bg-control-bg border border-control-border\";\n\n// The meter filled-portion indicator (spec §2/§4/§5): the portion of the rail up to the\n// score, in the NEUTRAL control foreground, rounded on the full radius. Its inline length\n// is the score as a percentage of the scale (set as an inline width in tsx — a data\n// percentage, not a design token). The length CHANGES on the FAST duration with verdify\n// easing, collapsing to the instant endpoint under reduced motion — a calm settle, never\n// the verified-check theatre duration (spec §4). It is neutral: no status, no action.\nexport const trustScoreIndicatorClass =\n \"block h-full rounded-(--radius-full) bg-control-fg \" +\n \"transition-[inline-size] duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant)\";\n\nexport type TrustScoreVariantProps = VariantProps<typeof trustScoreVariants>;\n",
|
|
21
21
|
"path": "trust-score/trust-score.variants.ts",
|
|
22
22
|
"target": "@ui/trust-score/trust-score.variants.ts",
|
|
23
23
|
"type": "registry:ui"
|