@verdify/ui 0.1.0 → 0.2.1

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.
Files changed (42) hide show
  1. package/README.md +48 -17
  2. package/package.json +42 -34
  3. package/registry/accordion.json +33 -0
  4. package/registry/agent-badge.json +32 -0
  5. package/registry/alert.json +32 -0
  6. package/registry/avatar.json +34 -0
  7. package/registry/badge.json +32 -0
  8. package/registry/breadcrumb.json +33 -0
  9. package/registry/button.json +33 -0
  10. package/registry/card.json +33 -0
  11. package/registry/checkbox.json +32 -0
  12. package/registry/cn.json +19 -0
  13. package/registry/command-palette.json +33 -0
  14. package/registry/consent-toggle.json +34 -0
  15. package/registry/credential-card.json +35 -0
  16. package/registry/data-grid.json +33 -0
  17. package/registry/dialog.json +33 -0
  18. package/registry/identity-chip.json +36 -0
  19. package/registry/init.json +17 -0
  20. package/registry/input.json +32 -0
  21. package/registry/label.json +32 -0
  22. package/registry/menu.json +33 -0
  23. package/registry/pagination.json +33 -0
  24. package/registry/popover.json +32 -0
  25. package/registry/progress.json +32 -0
  26. package/registry/radio.json +32 -0
  27. package/registry/select.json +33 -0
  28. package/registry/separator.json +33 -0
  29. package/registry/sheet.json +33 -0
  30. package/registry/sidebar.json +33 -0
  31. package/registry/skeleton.json +32 -0
  32. package/registry/spinner.json +32 -0
  33. package/registry/switch.json +32 -0
  34. package/registry/table.json +33 -0
  35. package/registry/tabs.json +33 -0
  36. package/registry/textarea.json +33 -0
  37. package/registry/toast.json +33 -0
  38. package/registry/tooltip.json +32 -0
  39. package/registry/trust-score.json +33 -0
  40. package/registry/verified-badge.json +32 -0
  41. package/registry.json +159 -0
  42. package/LICENSE +0 -12
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [
4
+ "class-variance-authority@^0.7.0",
5
+ "radix-ui@^1.1.0"
6
+ ],
7
+ "files": [
8
+ {
9
+ "content": "export {\n Sidebar,\n SidebarHeader,\n SidebarGroup,\n SidebarItem,\n SidebarFooter,\n SidebarCollapseToggle,\n type SidebarProps,\n type SidebarHeaderProps,\n type SidebarGroupProps,\n type SidebarItemProps,\n type SidebarFooterProps,\n type SidebarCollapseToggleProps,\n} from \"./sidebar\";\nexport {\n sidebarRailVariants,\n sidebarHeaderClass,\n sidebarGroupClass,\n sidebarGroupLabelClass,\n sidebarListClass,\n sidebarItemVariants,\n sidebarItemIconClass,\n sidebarItemLabelClass,\n sidebarItemTrailingClass,\n sidebarFooterClass,\n sidebarCollapseToggleClass,\n sidebarCollapseIconClass,\n type SidebarRailVariantProps,\n type SidebarItemVariantProps,\n type SidebarCollapseToggleVariantProps,\n} from \"./sidebar.variants\";\n",
10
+ "path": "sidebar/index.ts",
11
+ "target": "@ui/sidebar/index.ts",
12
+ "type": "registry:ui"
13
+ },
14
+ {
15
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Slot } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\nimport {\n sidebarRailVariants,\n sidebarHeaderClass,\n sidebarGroupClass,\n sidebarGroupLabelClass,\n sidebarListClass,\n sidebarItemVariants,\n sidebarItemIconClass,\n sidebarItemLabelClass,\n sidebarItemTrailingClass,\n sidebarFooterClass,\n sidebarCollapseToggleClass,\n sidebarCollapseIconClass,\n type SidebarRailVariantProps,\n} from \"./sidebar.variants\";\n\ntype SidebarSide = NonNullable<SidebarRailVariantProps[\"side\"]>;\n\n// The rail context carries the presentation/state axes set ONCE on the root down to every slot\n// (mirrors the Tabs/Sheet root-context precedent): the docked `side`, the live `collapsed` state\n// (so an item can visually hide its label while keeping it in the a11y tree), and the toggle's\n// handler. Items also register their DOM ref here so the rail can run roving arrow-key focus over\n// the ENABLED items in DOM order — the spec §6 keyboard model has no Radix primitive to lean on\n// (it is a list of native <a> links), so it is hand-composed here, the same way Separator's labeled\n// anatomy is hand-rolled when no primitive expresses the spec.\ntype SidebarRegistration = { el: HTMLElement; disabled: boolean };\ntype SidebarContextValue = {\n side: SidebarSide;\n collapsed: boolean;\n toggle: () => void;\n register: (entry: SidebarRegistration) => () => void;\n order: () => HTMLElement[];\n};\nconst SidebarContext = React.createContext<SidebarContextValue | null>(null);\n\nfunction useSidebar(): SidebarContextValue {\n const ctx = React.useContext(SidebarContext);\n if (!ctx) {\n throw new Error(\"Sidebar slots must be rendered inside <Sidebar>.\");\n }\n return ctx;\n}\n\nexport interface SidebarProps extends Omit<React.ComponentPropsWithoutRef<\"nav\">, \"onChange\"> {\n /** The docked edge (spec §3). `inline-start` (default) or `inline-end` — logical, mirrors under RTL. */\n side?: SidebarSide;\n /**\n * The rail starts collapsed when uncontrolled (spec §3 `collapsed`): a narrow rail of icons only.\n * For a controlled rail set `collapsed` + `onCollapsedChange` instead.\n */\n defaultCollapsed?: boolean;\n /** Controlled collapsed state (spec §3). When set, the rail is controlled — pair with `onCollapsedChange`. */\n collapsed?: boolean;\n /** Notified when the collapse-toggle flips the rail width (spec §3/§4). */\n onCollapsedChange?: (collapsed: boolean) => void;\n /**\n * The landmark's accessible name (spec §7), so multiple `navigation` landmarks on a page are\n * distinguishable — for example \"Primary\". Use `aria-labelledby` pointing at a visible heading\n * (e.g. the `SidebarHeader`) instead when the rail has a visible title.\n */\n \"aria-label\"?: string;\n}\n\n/**\n * A Sidebar is the primary navigation rail down one edge of an app shell: it lists the destinations\n * a person or an agent moves between and marks where they are now (spec §1). It is the `navigation`\n * landmark wrapping a list of real `<a href>` links. Reach for a Menu for a short transient list of\n * actions, Tabs when panels are peers within one page, and Breadcrumb for depth within a destination.\n *\n * The rail is a NEUTRAL surface — it does not wear the brand (spec §3): the current item takes the\n * primary ACTION accent (where you are), never a status color; a verified meaning belongs to\n * VerifiedBadge, never to a navigation item (brand != state). On narrow/touch viewports the rail\n * leaves the layout and opens as a modal drawer — use the Sheet `side=inline-start` pattern\n * (./sheet.md) for that; this component covers the DOCKED rail.\n *\n * It owns the collapse state and the roving arrow-key focus over its items, so it is `'use client'`.\n */\nexport const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(function Sidebar(\n { className, side = \"inline-start\", defaultCollapsed = false, collapsed: collapsedProp, onCollapsedChange, children, ...props },\n ref,\n) {\n const isControlled = collapsedProp !== undefined;\n const [uncontrolled, setUncontrolled] = React.useState(defaultCollapsed);\n const collapsed = isControlled ? collapsedProp : uncontrolled;\n\n const toggle = React.useCallback(() => {\n const next = !collapsed;\n if (!isControlled) setUncontrolled(next);\n onCollapsedChange?.(next);\n }, [collapsed, isControlled, onCollapsedChange]);\n\n // Items register their live DOM ref + disabled flag here; `order()` returns the enabled items in\n // current DOM order so the arrow-key handler can move focus and wrap at the ends (spec §6).\n const registry = React.useRef<Set<SidebarRegistration>>(new Set());\n const register = React.useCallback((entry: SidebarRegistration) => {\n registry.current.add(entry);\n return () => {\n registry.current.delete(entry);\n };\n }, []);\n const order = React.useCallback(() => {\n return Array.from(registry.current)\n .filter((e) => !e.disabled)\n .map((e) => e.el)\n // sort by DOM order so footer items follow group items regardless of registration order\n .sort((a, b) =>\n a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1,\n );\n }, []);\n\n const ctx = React.useMemo<SidebarContextValue>(\n () => ({ side, collapsed, toggle, register, order }),\n [side, collapsed, toggle, register, order],\n );\n\n // Down/Up move to the next/previous ENABLED item, wrapping; Home/End jump to the first/last\n // enabled item. Disabled items are not registered as targets, so they are skipped for free. The\n // rail never moves focus on its own — only on the user's keypress (spec §6/§7). Enter follows the\n // focused link (native); Space on an item follows it without scrolling the rail (preventDefault).\n const onKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n const items = order();\n if (items.length === 0) return;\n const active = document.activeElement as HTMLElement | null;\n const currentIndex = active ? items.indexOf(active) : -1;\n switch (event.key) {\n case \"ArrowDown\": {\n event.preventDefault();\n const next = currentIndex < 0 ? 0 : (currentIndex + 1) % items.length;\n items[next]?.focus();\n break;\n }\n case \"ArrowUp\": {\n event.preventDefault();\n const prev = currentIndex < 0 ? items.length - 1 : (currentIndex - 1 + items.length) % items.length;\n items[prev]?.focus();\n break;\n }\n case \"Home\": {\n event.preventDefault();\n items[0]?.focus();\n break;\n }\n case \"End\": {\n event.preventDefault();\n items[items.length - 1]?.focus();\n break;\n }\n // Space on a focused item follows the link without scrolling the rail (spec §6). It is\n // ignored on the toggle here — the toggle handles its own Space (native button activation).\n case \" \": {\n if (active?.dataset.sidebarItem === \"true\") {\n event.preventDefault();\n active.click();\n }\n break;\n }\n default:\n break;\n }\n };\n\n return (\n <SidebarContext.Provider value={ctx}>\n {/* the navigation landmark, named so a screen reader can jump to and skip the rail (spec §7) */}\n {/* The rail WIDTH is a layout concern the app sets via `className` (the token set has no\n sidebar-width scale, and a rail's width is shell-specific) — the component exposes the\n collapsed state via `data-collapsed` and the `transition-[width]` base motion so the\n caller animates between its own two widths. The collapsed rail hides labels (icons only)\n through each slot's `sr-only`, independent of the exact width. */}\n <nav\n ref={ref}\n data-collapsed={collapsed || undefined}\n className={cn(sidebarRailVariants({ side }), className)}\n onKeyDown={onKeyDown}\n {...props}\n >\n {children}\n </nav>\n </SidebarContext.Provider>\n );\n});\n\nexport type SidebarHeaderProps = React.ComponentPropsWithoutRef<\"div\">;\n\n/**\n * The top slot for a product mark or workspace switcher (spec §2). It is NOT a navigation item and\n * is skipped by item arrow movement. Give it an `id` and point the rail's `aria-labelledby` at it to\n * name the landmark from a visible heading.\n */\nexport const SidebarHeader = React.forwardRef<HTMLDivElement, SidebarHeaderProps>(\n function SidebarHeader({ className, ...props }, ref) {\n return <div ref={ref} className={cn(sidebarHeaderClass, className)} {...props} />;\n },\n);\n\nexport interface SidebarGroupProps extends React.ComponentPropsWithoutRef<\"div\"> {\n /** The cluster's visible heading (spec §2 group). It also names the group for assistive tech. */\n label?: React.ReactNode;\n}\n\n/**\n * A labeled cluster of items (spec §2 group). Use groups only when the labels earn their space;\n * restraint over volume keeps the rail scannable. The heading names the group via `aria-labelledby`,\n * so its items read as a related set (1.3.1 Info and Relationships). The items live in a `<ul>` so\n * they expose the list relationship. In the collapsed rail the heading is hidden (icons only) but the\n * group keeps its accessible name.\n */\nexport const SidebarGroup = React.forwardRef<HTMLDivElement, SidebarGroupProps>(\n function SidebarGroup({ className, label, children, ...props }, ref) {\n const { collapsed } = useSidebar();\n const headingId = React.useId();\n return (\n <div\n ref={ref}\n role=\"group\"\n aria-labelledby={label ? headingId : undefined}\n className={cn(sidebarGroupClass, className)}\n {...props}\n >\n {label ? (\n <div id={headingId} className={cn(sidebarGroupLabelClass, collapsed && \"sr-only\")}>\n {label}\n </div>\n ) : null}\n <ul className={sidebarListClass}>{children}</ul>\n </div>\n );\n },\n);\n\nexport interface SidebarItemProps extends Omit<React.ComponentPropsWithoutRef<\"a\">, \"color\"> {\n /** The destination (spec §7). Items are real links; omit it only on a disabled item. */\n href?: string;\n /** The leading icon (spec §2): decorative, sized by `--size-icon-md`. The item names itself by its label text. */\n icon?: React.ReactNode;\n /** A trailing count or status (spec §2): text or an `aria-label`, never color alone (spec §7). */\n trailing?: React.ReactNode;\n /**\n * This is the item for the page you are on (spec §4 Current). It sets `aria-current=\"page\"`, lifts\n * its label to the primary text color, and shows the brand action indicator bar — where you are,\n * never a status color. Exactly one item in the rail is current.\n */\n current?: boolean;\n /**\n * The item is present but not operable (spec §4 Disabled): it is skipped by arrow movement and the\n * tab order and drops its `href`, while its label stays readable to assistive technology.\n */\n disabled?: boolean;\n /**\n * Project the item styling onto a caller-supplied anchor (a framework router `<Link>` rendered as an\n * `<a>`) via Radix Slot, instead of the default native `<a>` (spec §2). Slot runs\n * `React.Children.only` — pass exactly one anchor child holding the icon + label.\n */\n asChild?: boolean;\n}\n\n/**\n * One navigation item (spec §2 item, §4 states): a native `<a href>` link holding a leading icon, a\n * label, and an optional trailing count or status. It exposes the link role and is operable without\n * extra wiring; it gets the visible focus ring, the restrained ghost hover fill, and the target-size\n * floor. The current item is a link, not a button — it still navigates (spec §7). In the collapsed\n * rail the label is visually hidden but kept in the accessibility tree, so the item never loses its\n * accessible name (spec §4 Collapsed / §7).\n */\nexport const SidebarItem = React.forwardRef<HTMLAnchorElement, SidebarItemProps>(function SidebarItem(\n { className, icon, trailing, children, current = false, disabled = false, asChild = false, href, ...props },\n ref,\n) {\n const { collapsed, register } = useSidebar();\n const innerRef = React.useRef<HTMLAnchorElement | null>(null);\n React.useImperativeHandle(ref, () => innerRef.current as HTMLAnchorElement);\n\n // register this item with the rail (for roving arrow-key focus) while it is mounted; a disabled\n // item registers as disabled so the rail skips it as a target (spec §6).\n React.useEffect(() => {\n const el = innerRef.current;\n if (!el) return;\n return register({ el, disabled });\n }, [register, disabled]);\n\n const Comp = asChild ? Slot.Root : \"a\";\n return (\n <li>\n <Comp\n ref={innerRef}\n // the only item carrying aria-current; exactly one per rail (spec §4/§7)\n aria-current={current ? \"page\" : undefined}\n // a disabled item drops its href (cannot navigate), leaves the tab order, and is marked\n // aria-disabled so AT still reads its label (spec §4/§7); an <a> has no native disabled\n href={disabled ? undefined : href}\n aria-disabled={disabled || undefined}\n tabIndex={disabled ? -1 : undefined}\n data-sidebar-item=\"true\"\n className={cn(sidebarItemVariants(), className)}\n {...props}\n >\n {icon ? (\n <span aria-hidden=\"true\" className={sidebarItemIconClass}>\n {icon}\n </span>\n ) : null}\n {/* the label stays in the a11y tree when collapsed (sr-only), so the link keeps its name\n without depending on a tooltip being open (spec §4 Collapsed / §7) */}\n <span className={cn(sidebarItemLabelClass, collapsed && \"sr-only\")}>{children}</span>\n {trailing && !collapsed ? <span className={sidebarItemTrailingClass}>{trailing}</span> : null}\n </Comp>\n </li>\n );\n});\n\nexport type SidebarFooterProps = React.ComponentPropsWithoutRef<\"div\">;\n\n/**\n * A bottom slot for an account or settings entry (spec §2). Its items follow the same item rules as\n * the rest of the list and join the rail's roving arrow-key order. Wrap its items in a `<ul>` so they\n * keep the list relationship.\n */\nexport const SidebarFooter = React.forwardRef<HTMLDivElement, SidebarFooterProps>(\n function SidebarFooter({ className, children, ...props }, ref) {\n return (\n <div ref={ref} className={cn(sidebarFooterClass, className)} {...props}>\n <ul className={sidebarListClass}>{children}</ul>\n </div>\n );\n },\n);\n\nexport interface SidebarCollapseToggleProps\n extends Omit<React.ComponentPropsWithoutRef<\"button\">, \"onClick\" | \"children\"> {\n /** The accessible name when expanded (icon-only). Defaults to \"Collapse sidebar\" (spec §7). */\n collapseLabel?: string;\n /** The accessible name when collapsed (icon-only). Defaults to \"Expand sidebar\" (spec §7). */\n expandLabel?: string;\n}\n\n/**\n * The control that switches the rail between expanded and collapsed (spec §2/§4/§6/§7). It is a\n * native `<button>`, NOT an item, reachable in the rail's tab order. It sets `aria-expanded` to\n * reflect the rail width and names its action with `aria-label` (\"Collapse sidebar\" / \"Expand\n * sidebar\"); its glyph is `aria-hidden`. Toggling it does not move focus off the toggle, so a\n * keyboard user keeps their place when switching the rail's width (spec §6). Render it only on a\n * collapsible rail.\n */\nexport const SidebarCollapseToggle = React.forwardRef<HTMLButtonElement, SidebarCollapseToggleProps>(\n function SidebarCollapseToggle(\n { className, collapseLabel = \"Collapse sidebar\", expandLabel = \"Expand sidebar\", ...props },\n ref,\n ) {\n const { collapsed, toggle } = useSidebar();\n return (\n <button\n ref={ref}\n type=\"button\"\n // aria-expanded reflects the rail width; the action is named for the icon-only control (spec §7)\n aria-expanded={!collapsed}\n aria-label={collapsed ? expandLabel : collapseLabel}\n onClick={toggle}\n className={cn(sidebarCollapseToggleClass(), className)}\n {...props}\n >\n <ChevronIcon collapsed={collapsed} />\n </button>\n );\n },\n);\n\n// The toggle glyph — inline SVG (no icon dep), --size-icon-md, rotates to mirror the rail width.\n// Decorative: aria-hidden; aria-expanded carries the state, not the glyph (spec §2/§7).\nfunction ChevronIcon({ collapsed }: { collapsed: boolean }) {\n return (\n <span\n data-testid=\"sidebar-collapse-icon\"\n aria-hidden=\"true\"\n className={cn(sidebarCollapseIconClass, collapsed && \"rotate-180\")}\n >\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M10 4L6 8l4 4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </span>\n );\n}\n",
16
+ "path": "sidebar/sidebar.tsx",
17
+ "target": "@ui/sidebar/sidebar.tsx",
18
+ "type": "registry:ui"
19
+ },
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",
22
+ "path": "sidebar/sidebar.variants.ts",
23
+ "target": "@ui/sidebar/sidebar.variants.ts",
24
+ "type": "registry:ui"
25
+ }
26
+ ],
27
+ "name": "sidebar",
28
+ "registryDependencies": [
29
+ "@verdify/cn"
30
+ ],
31
+ "title": "sidebar",
32
+ "type": "registry:ui"
33
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [
4
+ "class-variance-authority@^0.7.0"
5
+ ],
6
+ "files": [
7
+ {
8
+ "content": "export { Skeleton, type SkeletonProps, type SkeletonVariant } from \"./skeleton\";\nexport {\n skeletonVariants,\n skeletonShapeClass,\n skeletonLineClass,\n skeletonShimmerClass,\n type SkeletonVariantProps,\n} from \"./skeleton.variants\";\n",
9
+ "path": "skeleton/index.ts",
10
+ "target": "@ui/skeleton/index.ts",
11
+ "type": "registry:ui"
12
+ },
13
+ {
14
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport {\n skeletonVariants,\n skeletonShapeClass,\n skeletonLineClass,\n skeletonShimmerClass,\n type SkeletonVariantProps,\n} from \"./skeleton.variants\";\n\n/** The shape of the placeholder (spec §3). Defaults to `text`. */\nexport type SkeletonVariant = NonNullable<SkeletonVariantProps[\"variant\"]>;\n\nexport interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * The shape of the placeholder (spec §3) — `text` (default), `block`, `circle`,\n * or `group`. Variant is the footprint of the incoming content, not decoration.\n * Every variant paints from neutral surface roles only; a skeleton is a wait, never\n * the brand color and never a status color.\n */\n variant?: SkeletonVariant;\n /**\n * For the `text` variant only: how many placeholder lines to stack (spec §2/§3),\n * default `1`. Match the line count to the real content; the last line of a\n * multi-line block is rendered shorter to mirror a real paragraph's ragged final\n * line.\n */\n lines?: number;\n}\n\n/**\n * The decorative shimmer band (spec §2/§4): the lighter raised-surface sweep, read\n * against a leaf shape's input-surface fill, animated on the ambient duration and\n * removed under reduced motion. Always paired with a leaf shape, never on its own.\n */\nfunction Shimmer() {\n return <span data-testid=\"skeleton-shimmer\" aria-hidden=\"true\" className={skeletonShimmerClass} />;\n}\n\n/**\n * A Skeleton is a placeholder that holds the SHAPE of content while it loads, so the\n * layout does not jump when the real content arrives (spec §1). Use it for a known,\n * bounded wait — a card, a row, an avatar, a block of text — where you can predict the\n * shape of what is coming.\n *\n * It is a neutral, decorative placeholder. It carries `aria-hidden=\"true\"` so a screen\n * reader skips the placeholder shapes entirely (spec §7); the loading meaning is NOT\n * carried by the skeleton itself. The container the skeleton sits in owns the wait: set\n * `aria-busy=\"true\"` and expose a polite status message (\"Loading\") on THAT region while\n * loading, then clear it and let the real content's own semantics take over once\n * resolved. Do not put `aria-busy` or a live-region announcement on the skeleton, where\n * it would be ignored.\n *\n * It is non-interactive (spec §4/§6): it takes no focus, binds no keys, is never a tab\n * stop, and renders no focus ring or target-size floor. It never carries the brand color\n * (Sovereign Violet) or a status color (for example Verified Green) — a skeleton is a\n * wait, not a state, and the deliberate verified \"check\" motion is reserved for the\n * verified moment and never spent on a placeholder (brand != state, spec §1/§3/§8).\n */\nexport const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(\n function Skeleton({ className, variant = \"text\", lines = 1, children, ...props }, ref) {\n const count = Math.max(1, lines);\n return (\n // the skeleton is decorative — aria-hidden so a screen reader skips it entirely;\n // no role, no tabIndex, no live region (spec §7). The wait is announced by the\n // consumer's container, never here.\n <div\n ref={ref}\n aria-hidden=\"true\"\n className={cn(skeletonVariants({ variant }), className)}\n {...props}\n >\n {variant === \"text\" &&\n // a layout column of leaf line-shapes; the last of many is shorter to mirror\n // a real paragraph's ragged final line (spec §2/§3).\n Array.from({ length: count }, (_, i) => (\n <span\n key={i}\n data-testid=\"skeleton-line\"\n className={cn(\n skeletonShapeClass,\n skeletonLineClass,\n count > 1 && i === count - 1 ? \"w-3/5\" : \"w-full\",\n )}\n >\n <Shimmer />\n </span>\n ))}\n\n {/* block / circle: the root IS the leaf shape, so the fill + shimmer sit on it.\n The corner radius came from the variant on the root (spec §3/§5). */}\n {(variant === \"block\" || variant === \"circle\") && (\n <span className={cn(skeletonShapeClass, \"absolute inset-0 rounded-[inherit]\")}>\n <Shimmer />\n </span>\n )}\n\n {/* a composed silhouette: pure layout — its children carry the fill + shimmer,\n so the placeholder reads as the thing it replaces (spec §2/§3). */}\n {variant === \"group\" && children}\n </div>\n );\n },\n);\n",
15
+ "path": "skeleton/skeleton.tsx",
16
+ "target": "@ui/skeleton/skeleton.tsx",
17
+ "type": "registry:ui"
18
+ },
19
+ {
20
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// A Skeleton is a placeholder that holds the SHAPE of content while it loads, so the\n// layout does not jump when the real content arrives (spec §1). It is a neutral,\n// decorative placeholder: it never carries the brand color (Sovereign Violet) and\n// never carries a status color (for example Verified Green) — a skeleton is a WAIT,\n// not a state, so it binds nothing from the action tier or the status tier\n// (brand != state, spec §1/§3/§8).\n//\n// The model: a LEAF shape (`block`, `circle`, and each line of a `text` block) is the\n// element that carries the neutral input-surface FILL plus one decorative shimmer band\n// (see `skeletonShapeClass` / `skeletonShimmerClass`). A COMPOSING container (a `text`\n// block of lines, or a `group`) carries NO fill and NO shimmer — it is pure layout that\n// stacks its child shapes at the --space-2 gap. Variant therefore differs only by shape\n// (corner radius / composition), never by color (spec §3/§5).\nexport const skeletonVariants = cva(\"block\", {\n variants: {\n // STRUCTURAL axis = spec §3: the shape of the placeholder, not decoration.\n variant: {\n // one or more placeholder lines for incoming text. The root is a layout column\n // (no fill / no shimmer); each line is a leaf shape at the small corner radius.\n text: \"flex flex-col gap-(--space-2)\",\n // a rectangular placeholder (image / chart / thumbnail / media). The root IS the\n // leaf shape: input-surface fill + shimmer + the larger corner radius (spec §3/§5).\n block: \"rounded-(--radius-md)\",\n // a round placeholder for an avatar or icon — full radius (spec §3/§5).\n circle: \"rounded-(--radius-full)\",\n // a composed silhouette (card, list row) built from the shapes above, when the\n // whole unit loads as one. Pure layout: no fill, no shimmer — its children carry\n // both, so the placeholder reads as the thing it replaces (spec §2/§3).\n group: \"flex flex-col gap-(--space-2)\",\n },\n },\n defaultVariants: { variant: \"text\" },\n});\n\n// A LEAF shape's fill (spec §5): the neutral input surface, clipped so the shimmer band\n// sweeping inside it never bleeds out. This fill stays legible on its own, so removing\n// the shimmer under reduced motion loses nothing.\nexport const skeletonShapeClass = \"relative block overflow-hidden bg-surface-input\";\n\n// Each text line is a leaf shape at the small corner radius (spec §3/§5). Height tracks\n// a body line; width is full, except the LAST line of a multi-line block, which is\n// shorter to mirror a real paragraph's ragged final line (spec §2/§3 — applied in tsx).\nexport const skeletonLineClass = \"h-(--space-4) rounded-(--radius-sm)\";\n\n// The optional ambient shimmer (spec §2/§4): a decorative aria-hidden band in the\n// lighter RAISED surface role, read against the input-surface fill. It sweeps on the\n// AMBIENT duration with the ambient (steady) easing — a continuous wait indicator, like\n// the loading spinner — NOT the 350ms verified-check theatre duration, which is reserved\n// for the verified moment and is never spent on a placeholder (spec §4/§5).\n//\n// `animate-pulse` is the built-in ambient keyframe (the spinner reuses the built-in\n// `animate-spin` the same way); its duration is driven onto the ambient token via the\n// arbitrary `animation-duration` PROPERTY — a keyword arbitrary property, not a raw\n// value, so the token-binding gate does not flag it. Under prefers-reduced-motion the\n// band is removed entirely (`motion-reduce:hidden`); no information is carried by the\n// shimmer alone, so the static input-surface shape remains legible (spec §4/§7).\nexport const skeletonShimmerClass =\n \"absolute inset-0 bg-surface-raised animate-pulse \" +\n \"[animation-duration:var(--motion-duration-ambient)] ease-(--motion-easing-ambient) \" +\n \"motion-reduce:hidden\";\n\nexport type SkeletonVariantProps = VariantProps<typeof skeletonVariants>;\n",
21
+ "path": "skeleton/skeleton.variants.ts",
22
+ "target": "@ui/skeleton/skeleton.variants.ts",
23
+ "type": "registry:ui"
24
+ }
25
+ ],
26
+ "name": "skeleton",
27
+ "registryDependencies": [
28
+ "@verdify/cn"
29
+ ],
30
+ "title": "skeleton",
31
+ "type": "registry:ui"
32
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [
4
+ "class-variance-authority@^0.7.0"
5
+ ],
6
+ "files": [
7
+ {
8
+ "content": "export { Spinner, type SpinnerProps } from \"./spinner\";\nexport {\n spinnerIndicatorVariants,\n spinnerRootClass,\n spinnerLabelClass,\n type SpinnerIndicatorVariantProps,\n} from \"./spinner.variants\";\n",
9
+ "path": "spinner/index.ts",
10
+ "target": "@ui/spinner/index.ts",
11
+ "type": "registry:ui"
12
+ },
13
+ {
14
+ "content": "\"use client\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport {\n spinnerIndicatorVariants,\n spinnerRootClass,\n spinnerLabelClass,\n type SpinnerIndicatorVariantProps,\n} from \"./spinner.variants\";\n\nexport interface SpinnerProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"aria-label\">,\n SpinnerIndicatorVariantProps {\n /**\n * The visible label naming what is running, written as a plain statement ending\n * with a period (spec §2/§8): \"Checking your ID.\" When present it is rendered as\n * text beside the indicator AND becomes the accessible name via `aria-labelledby`.\n */\n label?: React.ReactNode;\n /**\n * The accessible name when there is no visible {@link label} (spec §7). State what\n * is running (\"Verifying your ID.\"). A Spinner is never nameless to assistive\n * technology, so EITHER a `label` or an `aria-label` must be provided.\n */\n \"aria-label\"?: string;\n /**\n * Whether work is running (spec §4/§7). `true` (the default) sets `aria-busy=\"true\"`\n * on the live region; pass `false` on resolution so assistive technology hears the\n * wait has ended. A rendered Spinner means work is running — if nothing is running,\n * render no Spinner at all (spec §4 Disabled).\n */\n busy?: boolean;\n}\n\n/**\n * A Spinner signals that work is running when you cannot measure how far it has gone\n * or how long is left — the unmeasured-wait affordance (spec §1). When the work\n * becomes measurable (a known percentage or step count) switch to determinate\n * Progress, because an honest number beats a turning circle.\n *\n * It is a status OUTPUT, not a control: it takes no focus, binds no keys, and is never\n * a tab stop (spec §4/§6). The owning region carries `role=\"status\"` so its presence\n * and resolution are announced politely; the turning glyph itself is decorative\n * (`aria-hidden`). The Spinner is named from its visible `label` (`aria-labelledby`)\n * or, when none exists, an `aria-label` — it is never nameless (spec §7).\n *\n * The arc takes the primary action accent, never the verified status color, and rotates\n * on the ambient duration, never the verified check's only theatre duration: a Spinner\n * means work is running, not that anything is verified (brand != state, spec §3/§5/§8).\n */\nexport const Spinner = React.forwardRef<HTMLDivElement, SpinnerProps>(\n function Spinner(\n { className, size, label, busy = true, \"aria-label\": ariaLabel, ...props },\n ref,\n ) {\n const labelId = React.useId();\n const hasVisibleLabel = label != null && label !== false;\n return (\n // role=\"status\" lives on the OWNING region (spec §7) — a polite live region.\n // Non-interactive: no tabIndex, no focus ring, no target-size floor (spec §4/§6).\n // Named by the visible label (aria-labelledby) or, when none, an aria-label —\n // never both, and never nameless.\n <div\n ref={ref}\n role=\"status\"\n aria-busy={busy}\n aria-labelledby={hasVisibleLabel ? labelId : undefined}\n aria-label={hasVisibleLabel ? undefined : ariaLabel}\n className={cn(spinnerRootClass, className)}\n {...props}\n >\n {/* the turning glyph is decorative — the accessible name carries the meaning,\n not the spin (spec §2/§7) */}\n <span\n data-testid=\"spinner-indicator\"\n aria-hidden=\"true\"\n className={spinnerIndicatorVariants({ size })}\n />\n {hasVisibleLabel ? (\n <span id={labelId} className={spinnerLabelClass}>\n {label}\n </span>\n ) : null}\n </div>\n );\n },\n);\n",
15
+ "path": "spinner/spinner.tsx",
16
+ "target": "@ui/spinner/spinner.tsx",
17
+ "type": "registry:ui"
18
+ },
19
+ {
20
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// A Spinner is the unmeasured-wait affordance: a status OUTPUT, not a control\n// (spec §1/§4/§6). It takes no focus, binds no keys, and renders no focus ring or\n// target-size floor — the owning region carries role=\"status\" and the busy state\n// reaches assistive tech through the accessible name + live region, never motion.\n//\n// Brand != state (spec §3/§5/§8): a Spinner reports ACTIVITY, not a verification\n// result. The turning arc takes the primary ACTION accent (the brand violet, the\n// visible action color on the surface), NEVER --color-status-verified-* — verified\n// green is the in-product verified status and is never a generic activity\n// affordance. There is no \"verified\" Spinner; surfacing a verified outcome is the\n// job of VerifiedBadge, whose deliberate check animation is never borrowed here.\n\n// The turning glyph (spec §2): a partial ring rendered as a CSS border circle. The\n// visible ARC takes the action accent via `border-action-primary-border`, and the\n// unfilled REMAINDER of the ring (the three other sides) takes `border-border-default`\n// behind it. Mirrors the Button loading-spinner technique, but pins the arc to the\n// action border token so a STANDALONE Spinner reads on the surface rather than\n// inheriting a foreground color (spec §5).\n//\n// Motion (spec §4/§5): one full rotation runs on the AMBIENT duration with the\n// ambient (steady, linear) easing — restrained activity, never the 350ms verified-\n// check theatre duration. Under prefers-reduced-motion the rotation is suppressed and\n// the indicator holds as a static glyph; the busy state is carried by the name + live\n// region, not motion alone.\nexport const spinnerIndicatorVariants = cva(\n [\n // a circle drawn from a 2px border; one side transparent reads as the arc/gap\n \"inline-block rounded-(--radius-full) border-2\",\n // the visible arc = the action accent; the rest of the ring = the default border\n \"border-border-default border-t-action-primary-border\",\n // continuous rotation on the ambient duration + steady linear ambient easing\n \"animate-spin [animation-duration:var(--motion-duration-ambient)] ease-(--motion-easing-ambient)\",\n // reduced motion: hold static — the live region + name carry the busy state\n \"motion-reduce:animate-none\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3: the indicator matches the adjacent icon size\n size: {\n sm: \"h-(--size-icon-sm) w-(--size-icon-sm)\",\n md: \"h-(--size-icon-md) w-(--size-icon-md)\",\n lg: \"h-(--size-icon-lg) w-(--size-icon-lg)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// The owning region (spec §7): role=\"status\" lives here, a polite live region. It is\n// laid out inline with its optional label at the --space-2 gap. Non-interactive: no\n// focus ring, no tab stop, no target-size floor (spec §4/§6).\nexport const spinnerRootClass = \"inline-flex items-center gap-(--space-2)\";\n\n// The optional visible label (spec §2/§5): a plain statement at the caption type role\n// in the primary text color. The label still names what is running; when omitted, an\n// aria-label carries the name instead — a Spinner is never nameless.\nexport const spinnerLabelClass = \"text-caption text-text-primary\";\n\nexport type SpinnerIndicatorVariantProps = VariantProps<typeof spinnerIndicatorVariants>;\n",
21
+ "path": "spinner/spinner.variants.ts",
22
+ "target": "@ui/spinner/spinner.variants.ts",
23
+ "type": "registry:ui"
24
+ }
25
+ ],
26
+ "name": "spinner",
27
+ "registryDependencies": [
28
+ "@verdify/cn"
29
+ ],
30
+ "title": "spinner",
31
+ "type": "registry:ui"
32
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [
4
+ "class-variance-authority@^0.7.0"
5
+ ],
6
+ "files": [
7
+ {
8
+ "content": "export { Switch, type SwitchProps } from \"./switch\";\nexport {\n switchTrackVariants,\n switchThumbVariants,\n switchHitAreaVariants,\n switchLabelVariants,\n type SwitchVariantProps,\n} from \"./switch.variants\";\n",
9
+ "path": "switch/index.ts",
10
+ "target": "@ui/switch/index.ts",
11
+ "type": "registry:ui"
12
+ },
13
+ {
14
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport {\n switchTrackVariants,\n switchThumbVariants,\n switchHitAreaVariants,\n switchLabelVariants,\n type SwitchVariantProps,\n} from \"./switch.variants\";\n\nexport interface SwitchProps\n extends Omit<\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n \"onChange\" | \"type\" | \"role\" | \"aria-checked\"\n >,\n SwitchVariantProps {\n /** Visible label naming the setting. A Switch is never unlabeled. */\n label: string;\n /** One-line clarification under the label; bound via aria-describedby. */\n description?: string;\n /** Controlled on/off value. Omit for the uncontrolled pattern. */\n checked?: boolean;\n /** Uncontrolled initial value. Ignored when `checked` is provided. */\n defaultChecked?: boolean;\n /** Fires with the next value on every toggle. */\n onCheckedChange?: (checked: boolean) => void;\n /** Async result pending: sets aria-busy and blocks input until it resolves. */\n loading?: boolean;\n /** Default `before` — label precedes the control; `after` follows it. */\n labelPlacement?: \"before\" | \"after\";\n}\n\nexport const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(\n function Switch(\n {\n className,\n label,\n description,\n checked,\n defaultChecked = false,\n onCheckedChange,\n loading = false,\n disabled,\n size,\n labelPlacement = \"before\",\n id,\n ...props\n },\n ref,\n ) {\n const reactId = React.useId();\n const switchId = id ?? `${reactId}-switch`;\n const labelId = `${reactId}-label`;\n const descId = description ? `${reactId}-desc` : undefined;\n\n const isControlled = checked !== undefined;\n const [internal, setInternal] = React.useState(defaultChecked);\n const isOn = isControlled ? checked : internal;\n\n const toggle = React.useCallback(() => {\n const next = !isOn;\n if (!isControlled) setInternal(next);\n onCheckedChange?.(next);\n }, [isOn, isControlled, onCheckedChange]);\n\n return (\n <div\n data-testid=\"switch-root\"\n className={cn(\n \"inline-flex items-center gap-3\",\n labelPlacement === \"after\" ? \"flex-row-reverse\" : \"flex-row\",\n )}\n >\n {/* hit-area wrapper carries the target-size floor so the visible track can\n keep its smaller h-5/h-6 height — the size variant stays visible. */}\n <span data-testid=\"switch-hit-area\" className={cn(switchHitAreaVariants())}>\n <button\n ref={ref}\n id={switchId}\n type=\"button\"\n role=\"switch\"\n // checked value updates on every toggle and stays readable when disabled\n aria-checked={isOn}\n aria-busy={loading || undefined}\n // a native <button> is NOT named by <label for> (that names form\n // controls only), so the visible label is bound via aria-labelledby —\n // the frozen ARIA contract's permitted naming path for role=\"switch\".\n aria-labelledby={labelId}\n aria-describedby={descId}\n // native <button> activates on Enter AND Space → spec keyboard model\n disabled={(disabled ?? false) || loading}\n onClick={toggle}\n className={cn(switchTrackVariants({ size }), className)}\n {...props}\n >\n {loading ? (\n <span\n data-testid=\"switch-spinner\"\n aria-hidden=\"true\"\n className={cn(\n \"absolute inset-0 m-auto h-4 w-4 animate-spin\",\n \"rounded-full border-2 border-current border-r-transparent\",\n \"[animation-duration:var(--motion-duration-ambient)]\",\n \"motion-reduce:animate-none\",\n )}\n />\n ) : (\n <span\n data-testid=\"switch-thumb\"\n aria-hidden=\"true\"\n aria-checked={isOn}\n className={cn(\n switchThumbVariants({ size, disabled: (disabled ?? false) || loading }),\n )}\n />\n )}\n </button>\n </span>\n <span className=\"inline-flex flex-col\">\n {/* disabled colour from the explicit cva variant — the <label> is nested in\n this column, not a sibling of the button, so peer-disabled cannot reach it. */}\n <label\n id={labelId}\n htmlFor={switchId}\n className={cn(switchLabelVariants({ disabled: (disabled ?? false) || loading }))}\n >\n {label}\n </label>\n {description ? (\n <span id={descId} className=\"text-caption text-text-secondary\">\n {description}\n </span>\n ) : null}\n </span>\n </div>\n );\n },\n);\n",
15
+ "path": "switch/switch.tsx",
16
+ "target": "@ui/switch/switch.tsx",
17
+ "type": "registry:ui"
18
+ },
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 \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\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
+ "path": "switch/switch.variants.ts",
22
+ "target": "@ui/switch/switch.variants.ts",
23
+ "type": "registry:ui"
24
+ }
25
+ ],
26
+ "name": "switch",
27
+ "registryDependencies": [
28
+ "@verdify/cn"
29
+ ],
30
+ "title": "switch",
31
+ "type": "registry:ui"
32
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [
4
+ "class-variance-authority@^0.7.0"
5
+ ],
6
+ "files": [
7
+ {
8
+ "content": "export {\n Table,\n TableCaption,\n TableHeader,\n TableBody,\n TableFooter,\n TableRow,\n TableHead,\n TableRowHeader,\n TableCell,\n TableEmpty,\n type TableProps,\n type TableCaptionProps,\n type TableHeaderProps,\n type TableBodyProps,\n type TableFooterProps,\n type TableRowProps,\n type TableHeadProps,\n type TableRowHeaderProps,\n type TableCellProps,\n type TableEmptyProps,\n type TableDensity,\n type TableRule,\n type TableCellStatus,\n type TableSortDirection,\n} from \"./table\";\nexport {\n tableVariants,\n tableCaptionClass,\n tableHeaderVariants,\n tableBodyVariants,\n tableRowVariants,\n tableCellVariants,\n tableRowHeaderVariants,\n tableHeadVariants,\n tableSortButtonClass,\n tableSortCaretClass,\n tableEmptyClass,\n tableSkeletonCellClass,\n type TableVariantProps,\n type TableHeaderVariantProps,\n type TableBodyVariantProps,\n type TableRowVariantProps,\n type TableCellVariantProps,\n type TableRowHeaderVariantProps,\n type TableHeadVariantProps,\n} from \"./table.variants\";\n",
9
+ "path": "table/index.ts",
10
+ "target": "@ui/table/index.ts",
11
+ "type": "registry:ui"
12
+ },
13
+ {
14
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n tableVariants,\n tableCaptionClass,\n tableHeaderVariants,\n tableBodyVariants,\n tableRowVariants,\n tableCellVariants,\n tableRowHeaderVariants,\n tableHeadVariants,\n tableSortButtonClass,\n tableSortCaretClass,\n tableEmptyClass,\n tableSkeletonCellClass,\n type TableCellVariantProps,\n} from \"./table.variants\";\n\n/** The row density (spec §3): `comfortable` (default) for general reading, `compact` for dense console views. */\nexport type TableDensity = \"comfortable\" | \"compact\";\n/** The rule treatment (spec §3): row hairlines (`horizontal`, default), `grid` (rows + columns), or `zebra` (alternating tint). */\nexport type TableRule = \"horizontal\" | \"grid\" | \"zebra\";\n/** A cell's reported state (spec §3/§4): a status color appears only inside a cell that reports a real state, paired with text. */\nexport type TableCellStatus = NonNullable<TableCellVariantProps[\"status\"]>;\n/** A sort direction (spec §4/§7): reflected as `aria-sort` on the header and a non-color caret. */\nexport type TableSortDirection = \"ascending\" | \"descending\" | \"none\";\n\n// The presentation axes set ONCE on the root <table> travel to every cell via context (the proven\n// Dialog/Sheet/Tabs root-context pattern), so callers set density/rule on the Table and never repeat\n// them on each cell. `sticky` is read by the header. This is why the file is `'use client'` — it\n// uses React context (a hook). No cell roving is wired: a static Table's cells are read, not\n// operated; focus belongs to the interactive controls only (spec §6) — cell-by-cell arrow movement\n// is DataGrid behavior, not Table behavior, so there is no hand-rolled roving layer here.\ntype TableContextValue = { density: TableDensity; rule: TableRule };\nconst TableContext = React.createContext<TableContextValue>({\n density: \"comfortable\",\n rule: \"horizontal\",\n});\n\nexport interface TableProps extends React.TableHTMLAttributes<HTMLTableElement> {\n /** Row density (spec §3). `comfortable` (default) or `compact`. Applies to all cells via context. */\n density?: TableDensity;\n /** Rule treatment (spec §3). `horizontal` (default), `grid`, or `zebra`. Applies to rows/cells via context. */\n rule?: TableRule;\n /**\n * Pin the header row while the body scrolls (spec §3 sticky-header). The header gets the stronger\n * border-strong divider where a heavier separation reads better. Selection and sort are unchanged.\n */\n stickyHeader?: boolean;\n /**\n * The polite live-region message (spec §7/§8, WCAG 4.1.3 Status Messages). When a sort or a filter\n * changes the visible rows, set this to the result so it is announced — for example\n * \"Sorted by status, ascending. 42 rows.\" The visual reorder is not announced on its own, so a\n * silent re-sort breaks §8 (\"Don't re-sort silently\"). The caller owns the count (mirroring\n * CommandPalette): pass the new string whenever the visible rows change. Rendered into an sr-only\n * `role=\"status\" aria-live=\"polite\"` node, so it reaches assistive tech as text, never color alone.\n */\n announcement?: string;\n}\n\n/**\n * A Table presents structured data in rows and columns so you can read, compare, and sort records —\n * a list of API keys, verification events, or registered agents (spec §1). It is a SEMANTIC\n * `<table>`: the data has a real row-and-column relationship and the native markup carries that\n * relationship into the accessibility tree (1.3.1), not just the pixels. Reach for a DataGrid when\n * the data needs virtualized rows, in-cell editing, column resizing, or roving-focus cell navigation.\n *\n * Neutrals carry the table — it is a reading surface, not an accent surface (spec §3): most of it is\n * neutral text and hairline borders. A status color appears ONLY inside a cell that reports a real\n * state, paired with text (never a header, a whole row, or a decoration); the brand violet is NEVER\n * a Table variant — the brand is not a status, so it never tints a row, header, sort, or selection\n * (brand != state). For a first-class verified result in a cell, use the VerifiedBadge molecule.\n *\n * Name the table with a `<caption>` (preferred) or `aria-labelledby`/`aria-label`. It owns no\n * keyboard model of its own — focus belongs to the controls it hosts (a sortable header, a row\n * Checkbox, a row action), which keep their native tab stops in reading order (spec §6).\n */\nexport const Table = React.forwardRef<HTMLTableElement, TableProps>(function Table(\n { className, density = \"comfortable\", rule = \"horizontal\", stickyHeader = false, announcement, children, ...props },\n ref,\n) {\n const ctx = React.useMemo<TableContextValue>(() => ({ density, rule }), [density, rule]);\n // sticky is read by the header through a second, header-only context so the <thead> can pin\n // without the cell context carrying a presentation flag it does not use.\n const stickyCtx = React.useMemo(() => ({ sticky: stickyHeader }), [stickyHeader]);\n return (\n <TableContext.Provider value={ctx}>\n <TableStickyContext.Provider value={stickyCtx}>\n <table ref={ref} className={cn(tableVariants(), className)} {...props}>\n {children}\n </table>\n {/* The polite live region (spec §7/§8, WCAG 4.1.3): the result of a sort or filter — the new\n order and row count — announced as text, since the visual reorder is not announced on its\n own (a silent re-sort is the §8 \"Don't\"). Always present (the live region must exist before\n its text changes to be announced); sr-only so it never paints, and a sibling of the <table>,\n not a child, since a <span> is not valid table content. The caller feeds `announcement`. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {announcement}\n </span>\n </TableStickyContext.Provider>\n </TableContext.Provider>\n );\n});\n\nconst TableStickyContext = React.createContext<{ sticky: boolean }>({ sticky: false });\n\nexport type TableCaptionProps = React.HTMLAttributes<HTMLTableCaptionElement>;\n\n/**\n * The table's accessible name (spec §2/§7) — a `<caption>` naming what the data is, for example\n * \"Verification events, last 30 days\". Prefer it over a detached heading; add `className=\"sr-only\"`\n * to keep the name in the accessibility tree while hiding it visually.\n */\nexport const TableCaption = React.forwardRef<HTMLTableCaptionElement, TableCaptionProps>(\n function TableCaption({ className, ...props }, ref) {\n return <caption ref={ref} className={cn(tableCaptionClass, className)} {...props} />;\n },\n);\n\nexport type TableHeaderProps = React.HTMLAttributes<HTMLTableSectionElement>;\n\n/**\n * The `<thead>` holding the column-header row (spec §2). When the table's `stickyHeader` is set, the\n * header pins to the top of the scroll container with the stronger divider (spec §3/§5). The pinning\n * is read from the Table via context, so callers set it once on the Table.\n */\nexport const TableHeader = React.forwardRef<HTMLTableSectionElement, TableHeaderProps>(\n function TableHeader({ className, ...props }, ref) {\n const { sticky } = React.useContext(TableStickyContext);\n return <thead ref={ref} className={cn(tableHeaderVariants({ sticky }), className)} {...props} />;\n },\n);\n\nexport interface TableBodyProps extends React.HTMLAttributes<HTMLTableSectionElement> {\n /**\n * The body is resolving (spec §4 Loading). Renders `skeletonRows` × `columns` Skeleton cells in the\n * table's own column layout — keeping the header and column widths stable so the table does not\n * reflow when data arrives — and marks the body `aria-busy=\"true\"`. A wait is a plain wait, not\n * theatre: the deliberate verified-check duration is never spent here.\n */\n loading?: boolean;\n /** How many skeleton rows to show while loading (spec §4 Loading). Default `3`. */\n skeletonRows?: number;\n /** How many columns the skeleton rows span, so the placeholder matches the real column layout. Default `1`. */\n columns?: number;\n}\n\n/**\n * The `<tbody>` holding the data rows (spec §2). In the `zebra` rule it alternates a neutral\n * raised-surface tint on even rows (read from the Table via context). While `loading`, it is\n * `aria-busy` and shows Skeleton rows in the column layout; the skeletons are decorative and the\n * body owns the wait (the Skeleton itself announces nothing, spec §4 Loading / §7).\n */\nexport const TableBody = React.forwardRef<HTMLTableSectionElement, TableBodyProps>(\n function TableBody({ className, loading = false, skeletonRows = 3, columns = 1, children, ...props }, ref) {\n const { rule } = React.useContext(TableContext);\n return (\n <tbody\n ref={ref}\n aria-busy={loading || undefined}\n className={cn(tableBodyVariants({ rule }), className)}\n {...props}\n >\n {loading\n ? Array.from({ length: Math.max(1, skeletonRows) }, (_, r) => (\n <TableRow key={`skeleton-${r}`}>\n {Array.from({ length: Math.max(1, columns) }, (_, c) => (\n // a placeholder data cell in the column layout; decorative, so the wait is owned by\n // the body's aria-busy, not the skeleton (spec §4 Loading / §7).\n <td key={c} data-testid=\"table-skeleton-cell\" className={tableSkeletonCellClass}>\n <Skeleton variant=\"text\" />\n </td>\n ))}\n </TableRow>\n ))\n : children}\n </tbody>\n );\n },\n);\n\nexport type TableFooterProps = React.HTMLAttributes<HTMLTableSectionElement>;\n\n/**\n * The `<tfoot>` summary row (spec §2) — a total or an aggregate. It is DATA, not pagination;\n * pagination is a separate Pagination control beside the table.\n */\nexport const TableFooter = React.forwardRef<HTMLTableSectionElement, TableFooterProps>(\n function TableFooter({ className, ...props }, ref) {\n return (\n <tfoot ref={ref} className={cn(\"border-t border-border-default\", className)} {...props} />\n );\n },\n);\n\nexport interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {\n /**\n * A selectable row is currently selected (spec §4 Selected). Sets `aria-selected=\"true\"` and the\n * restrained neutral raised-surface fill. Selection is encoded by the row Checkbox's checked state\n * AND `aria-selected`, never by the fill alone, and NEVER a brand or status tint (brand != state).\n */\n selected?: boolean;\n}\n\n/**\n * A `<tr>` (spec §2/§4). A body row gets the restrained raised-surface hover fill (an affordance, to\n * track the eye across a wide row — nothing is selected until you act) and, when `selected`, the same\n * neutral fill plus `aria-selected`. The rule treatment (hairline / grid / zebra) is read from the\n * Table via context. Used inside `<thead>`, `<tbody>`, and `<tfoot>`.\n */\nexport const TableRow = React.forwardRef<HTMLTableRowElement, TableRowProps>(\n function TableRow({ className, selected = false, ...props }, ref) {\n const { rule } = React.useContext(TableContext);\n return (\n <tr\n ref={ref}\n aria-selected={selected || undefined}\n className={cn(tableRowVariants({ rule }), className)}\n {...props}\n />\n );\n },\n);\n\nexport interface TableHeadProps extends React.ThHTMLAttributes<HTMLTableCellElement> {\n /**\n * This column can be re-sorted from its header (spec §3 sortable). Renders the header label as a\n * real `<button>` (the control it is) with a direction caret, and reflects `aria-sort` on the\n * `<th>`. Enable per column, not table-wide, so only meaningfully sortable columns advertise it.\n */\n sortable?: boolean;\n /**\n * The current sort direction for this column (spec §4 Sorted), reflected as `aria-sort` on the\n * `<th>` and as the caret glyph. Only one column is the sort column at a time — set `\"ascending\"`\n * or `\"descending\"` on it and `\"none\"` (the default) on the rest.\n */\n sortDirection?: TableSortDirection;\n /** Fired when the sortable header is activated (click / Enter / Space), so the caller re-sorts and updates `sortDirection`. */\n onSort?: () => void;\n /**\n * The column name used in the sort control's accessible name (\"Sort by {label}, {direction}\"),\n * when it differs from the visible children. Defaults to the children's text.\n */\n sortLabel?: string;\n}\n\n// The direction caret (spec §4 Sorted): decorative — aria-sort on the th + the glyph SHAPE\n// (data-direction) encode the direction, so it never rests on color alone (1.4.1). Inline SVG, no\n// icon dep; it points up for ascending, down for descending, and shows a neutral both-ways glyph\n// when the column is sortable but not the active sort column.\nfunction SortCaret({ direction }: { direction: TableSortDirection }) {\n return (\n <span\n data-testid=\"table-sort-caret\"\n data-direction={direction}\n aria-hidden=\"true\"\n className={tableSortCaretClass}\n >\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" focusable=\"false\" aria-hidden=\"true\">\n {direction === \"ascending\" ? (\n <path d=\"M4 10l4-4 4 4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n ) : direction === \"descending\" ? (\n <path d=\"M4 6l4 4 4-4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n ) : (\n // sortable but not the active column: a quiet both-directions glyph\n <path d=\"M5 6.5l3-3 3 3M5 9.5l3 3 3-3\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n )}\n </svg>\n </span>\n );\n}\n\nconst NEXT_DIRECTION_WORD: Record<TableSortDirection, string> = {\n none: \"ascending\",\n ascending: \"descending\",\n descending: \"ascending\",\n};\n\n/**\n * A `<th scope=\"col\">` column header (spec §2/§7). A plain header is a quiet, tracked label in the\n * secondary text color. A `sortable` header wraps that label in a real `<button>` carrying the\n * ghost-action accent, a visible focus ring, the target-size floor, and a direction caret, and the\n * `<th>` reflects `aria-sort` — so sort direction reaches assistive tech as data and never rests on\n * color alone (1.4.1). The sort button's accessible name names the action and the column (\"Sort by\n * status, ascending\"). Density + grid rule are read from the Table via context.\n */\nexport const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps>(function TableHead(\n { className, sortable = false, sortDirection = \"none\", onSort, sortLabel, children, ...props },\n ref,\n) {\n const { density, rule } = React.useContext(TableContext);\n const ariaSort = sortable ? sortDirection : undefined;\n const label = sortLabel ?? (typeof children === \"string\" ? children : undefined);\n return (\n <th\n ref={ref}\n scope=\"col\"\n // exactly one header carries aria-sort at a time (spec §4 Sorted); the rest are \"none\"/absent\n aria-sort={ariaSort}\n className={cn(tableHeadVariants({ density, rule }), className)}\n {...props}\n >\n {sortable ? (\n <button\n type=\"button\"\n onClick={onSort}\n // the control's name includes the ACTION and the COLUMN, plus the NEXT direction it will\n // sort to — so a screen-reader user knows what activating it does (spec §7). aria-sort on\n // the th already exposes the CURRENT state.\n aria-label={label ? `Sort by ${label}, ${NEXT_DIRECTION_WORD[sortDirection]}` : undefined}\n className={tableSortButtonClass}\n >\n {children}\n <SortCaret direction={sortDirection} />\n </button>\n ) : (\n children\n )}\n </th>\n );\n});\n\nexport type TableRowHeaderProps = React.ThHTMLAttributes<HTMLTableCellElement>;\n\n/**\n * A `<th scope=\"row\">` row-header cell (spec §2/§7) — the row's natural label (an identifier, a\n * name), tying the row's cells to it. It reads at the same body weight as a data cell but is promoted\n * to a header for the relationship (1.3.1). Density + grid rule are read from the Table via context.\n */\nexport const TableRowHeader = React.forwardRef<HTMLTableCellElement, TableRowHeaderProps>(\n function TableRowHeader({ className, ...props }, ref) {\n const { density, rule } = React.useContext(TableContext);\n return (\n <th\n ref={ref}\n scope=\"row\"\n className={cn(tableRowHeaderVariants({ density, rule }), className)}\n {...props}\n />\n );\n },\n);\n\nexport interface TableCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {\n /**\n * The cell reports a real STATE (spec §3/§4): `verified`, `signal`, `caution`, or `critical`. The\n * status color lives in the CELL only (never the row or header), paired with text — so a grayscale\n * reader still reads the state from the words. NEVER a brand token. For a first-class verified\n * result use the VerifiedBadge molecule inside a plain cell, not a hand-tinted cell.\n */\n status?: TableCellStatus;\n /** A numeric cell (spec §4/§5): tabular figures so digits align down the column, end-aligned, in the PRIMARY data text role. */\n numeric?: boolean;\n /** De-emphasized AUXILIARY cell text (spec §5) — a timestamp, a unit. Takes the muted role; independent of `numeric`. */\n auxiliary?: boolean;\n}\n\n/**\n * A `<td>` data cell (spec §2 cell). Holds text, a number, a Badge, an Avatar, or a small inline\n * control. A plain cell is neutral primary text. A `numeric` cell uses tabular figures and stays in\n * the PRIMARY data text role (the muted role is reserved for `auxiliary` text — a timestamp, a unit —\n * per spec §5); a cell that reports a real state takes a `status` treatment (the status fg paired with\n * text). It is NOT a focus stop — focus belongs to any interactive control inside it (spec §6).\n * Density + grid rule are read from the Table via context.\n */\nexport const TableCell = React.forwardRef<HTMLTableCellElement, TableCellProps>(function TableCell(\n { className, status = \"none\", numeric = false, auxiliary = false, ...props },\n ref,\n) {\n const { density, rule } = React.useContext(TableContext);\n return (\n <td\n ref={ref}\n className={cn(tableCellVariants({ density, rule, numeric, auxiliary, status }), className)}\n {...props}\n />\n );\n});\n\nexport interface TableEmptyProps extends React.TdHTMLAttributes<HTMLTableCellElement> {\n /** How many columns the empty line spans, so it fills the table's own width (spec §2/§4 Empty). */\n colSpan?: number;\n}\n\n/**\n * The empty-state row (spec §2/§4 Empty): a single full-width cell stating there is nothing yet and\n * what to do next, in plain words ending in a period. An empty table is NOT an error and never reads\n * as one — no status color. Render it inside the `<tbody>` when there are zero rows after loading.\n */\nexport const TableEmpty = React.forwardRef<HTMLTableCellElement, TableEmptyProps>(\n function TableEmpty({ className, colSpan = 1, children, ...props }, ref) {\n return (\n <tr>\n <td ref={ref} colSpan={colSpan} className={cn(tableEmptyClass, className)} {...props}>\n {children}\n </td>\n </tr>\n );\n },\n);\n",
15
+ "path": "table/table.tsx",
16
+ "target": "@ui/table/table.tsx",
17
+ "type": "registry:ui"
18
+ },
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-fg\",\n signal: \"text-status-signal-fg\",\n caution: \"text-status-caution-fg\",\n critical: \"text-status-critical-fg\",\n },\n },\n defaultVariants: {\n density: \"comfortable\",\n 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",
21
+ "path": "table/table.variants.ts",
22
+ "target": "@ui/table/table.variants.ts",
23
+ "type": "registry:ui"
24
+ }
25
+ ],
26
+ "name": "table",
27
+ "registryDependencies": [
28
+ "@verdify/cn",
29
+ "@verdify/skeleton"
30
+ ],
31
+ "title": "table",
32
+ "type": "registry:ui"
33
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [
4
+ "class-variance-authority@^0.7.0",
5
+ "radix-ui@^1.1.0"
6
+ ],
7
+ "files": [
8
+ {
9
+ "content": "export {\n Tabs,\n TabsList,\n TabsTab,\n TabsPanel,\n type TabsProps,\n type TabsListProps,\n type TabsTabProps,\n type TabsPanelProps,\n} from \"./tabs\";\nexport {\n tabsListVariants,\n tabsTabVariants,\n tabsPanelVariants,\n type TabsListVariantProps,\n type TabsTabVariantProps,\n type TabsPanelVariantProps,\n} from \"./tabs.variants\";\n",
10
+ "path": "tabs/index.ts",
11
+ "target": "@ui/tabs/index.ts",
12
+ "type": "registry:ui"
13
+ },
14
+ {
15
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\nimport {\n tabsListVariants,\n tabsTabVariants,\n tabsPanelVariants,\n type TabsTabVariantProps,\n} from \"./tabs.variants\";\n\ntype TabsVariant = NonNullable<TabsTabVariantProps[\"variant\"]>;\ntype TabsSize = NonNullable<TabsTabVariantProps[\"size\"]>;\ntype TabsOrientation = \"horizontal\" | \"vertical\";\n\n// The presentation axes (spec §3) are set ONCE on the root and travel to every tab + panel via\n// context, so callers don't repeat `variant`/`size`/`orientation` on each part (mirrors the\n// Accordion headingLevel-context precedent). Orientation also drives Radix's arrow-key axis.\ntype TabsContextValue = {\n variant: TabsVariant;\n size: TabsSize;\n orientation: TabsOrientation;\n};\nconst TabsContext = React.createContext<TabsContextValue>({\n variant: \"underline\",\n size: \"md\",\n orientation: \"horizontal\",\n});\n\nexport interface TabsProps\n extends Omit<React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>, \"orientation\"> {\n /** Indicator style (spec §3): `underline` (default, in-page) or `pill` (dense/boxed surfaces). */\n variant?: TabsVariant;\n /** Tab density (spec §3): `md` (default) or `sm` for side rails. Both hold the target-size floor. */\n size?: TabsSize;\n /** Layout + arrow-key axis (spec §3): `horizontal` (default, Left/Right) or `vertical` (Up/Down). */\n orientation?: TabsOrientation;\n}\n\n/**\n * Tabs split one region into sibling panels and show one at a time, with a row of tabs that\n * switches between them (spec §1). It is a neutral layout container: the selected tab and its\n * indicator take the BRAND primary action accent, never a status color — a tab reports which\n * panel is open, not a verified result (spec §3/§5/§8, brand != state). Wraps the Radix Tabs\n * primitive (WAI-ARIA APG tabs pattern) — a stateful primitive, so this file is `'use client'`.\n *\n * Activation follows the spec §6 default: `activationMode=\"manual\"`, so arrow keys browse tabs\n * without switching the panel until you commit with Space/Enter. Pass `activationMode=\"automatic\"`\n * only when switching panels is cheap and side-effect free.\n */\nexport function Tabs({\n variant = \"underline\",\n size = \"md\",\n orientation = \"horizontal\",\n activationMode = \"manual\",\n className,\n children,\n ...rootProps\n}: TabsProps) {\n return (\n <TabsContext.Provider value={{ variant, size, orientation }}>\n <TabsPrimitive.Root\n orientation={orientation}\n activationMode={activationMode}\n className={cn(\n // logical-property layout (G-U6): horizontal stacks list-above-panel, vertical\n // places the rail inline-start of the panel\n orientation === \"vertical\" ? \"flex flex-row gap-(--space-4)\" : \"flex flex-col\",\n className,\n )}\n {...rootProps}\n >\n {children}\n </TabsPrimitive.Root>\n </TabsContext.Provider>\n );\n}\n\nexport interface TabsListProps\n extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> {}\n\n/**\n * The row (or column) that holds the tabs and carries the `tablist` role (spec §2). It is one\n * stop in the page tab order via Radix's roving tabindex. It takes its accessible name from a\n * visible heading via `aria-labelledby`, or from `aria-label` when no heading is present (spec §7).\n */\nexport const TabsList = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(function TabsList({ className, ...props }, ref) {\n const { orientation } = React.useContext(TabsContext);\n return (\n <TabsPrimitive.List\n ref={ref}\n className={cn(tabsListVariants({ orientation }), className)}\n {...props}\n />\n );\n});\n\nexport interface TabsTabProps\n extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> {\n /** Stable identity for this tab; pairs the tab with its panel (Radix wires aria-controls). */\n value: string;\n /** The tab is present but not operable, and is skipped by arrow movement (spec §4 Disabled). */\n disabled?: boolean;\n}\n\n/**\n * One selectable label in the tablist (spec §2 `tab`): holds text, an optional leading icon, and\n * an optional trailing count. Radix renders it as `role=\"tab\"` with roving tabindex (selected =\n * `tabindex=\"0\"`, the rest `-1`), `aria-selected`, and `aria-controls` to its panel. The selected\n * indicator (underline or pill) reinforces selection but never carries it alone — `aria-selected`\n * and the visible panel do (spec §2 indicator / §4 Selected / use-of-color).\n */\nexport const TabsTab = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.Trigger>,\n TabsTabProps\n>(function TabsTab({ className, ...props }, ref) {\n const { variant, size } = React.useContext(TabsContext);\n return (\n <TabsPrimitive.Trigger\n ref={ref}\n className={cn(tabsTabVariants({ variant, size }), className)}\n {...props}\n />\n );\n});\n\nexport interface TabsPanelProps\n extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> {\n /** Stable identity tying this panel to its tab (Radix wires role + aria-labelledby). */\n value: string;\n /**\n * The panel content is resolving (spec §4 Loading): announces `aria-busy` while the tablist\n * stays operable so you can switch away. A wait is a plain wait, never deliberate motion theatre.\n */\n loading?: boolean;\n}\n\n/**\n * The content region tied to the selected tab (spec §2 `panel`): one panel is visible, the rest\n * hidden and out of the tab order until selected. Radix sets `role=\"tabpanel\"` and\n * `aria-labelledby` back to its tab, and makes the panel focusable (`tabindex=\"0\"`) when it holds\n * no focusable content of its own, so Tab always reaches it (spec §7 focus management).\n */\nexport const TabsPanel = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.Content>,\n TabsPanelProps\n>(function TabsPanel({ className, loading, ...props }, ref) {\n const { orientation } = React.useContext(TabsContext);\n return (\n <TabsPrimitive.Content\n ref={ref}\n aria-busy={loading || undefined}\n className={cn(tabsPanelVariants({ orientation }), className)}\n {...props}\n />\n );\n});\n",
16
+ "path": "tabs/tabs.tsx",
17
+ "target": "@ui/tabs/tabs.tsx",
18
+ "type": "registry:ui"
19
+ },
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 \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\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 \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\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
+ "path": "tabs/tabs.variants.ts",
23
+ "target": "@ui/tabs/tabs.variants.ts",
24
+ "type": "registry:ui"
25
+ }
26
+ ],
27
+ "name": "tabs",
28
+ "registryDependencies": [
29
+ "@verdify/cn"
30
+ ],
31
+ "title": "tabs",
32
+ "type": "registry:ui"
33
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [
4
+ "class-variance-authority@^0.7.0"
5
+ ],
6
+ "files": [
7
+ {
8
+ "content": "export { Textarea, type TextareaProps } from \"./textarea\";\nexport { textareaVariants, type TextareaVariantProps } from \"./textarea.variants\";\n",
9
+ "path": "textarea/index.ts",
10
+ "target": "@ui/textarea/index.ts",
11
+ "type": "registry:ui"
12
+ },
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-fg\">\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-muted tabular-nums\"\n >\n {length}/{maxLength}\n </span>\n ) : null}\n </div>\n </div>\n );\n },\n);\n",
15
+ "path": "textarea/textarea.tsx",
16
+ "target": "@ui/textarea/textarea.tsx",
17
+ "type": "registry:ui"
18
+ },
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 \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\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
+ "path": "textarea/textarea.variants.ts",
22
+ "target": "@ui/textarea/textarea.variants.ts",
23
+ "type": "registry:ui"
24
+ }
25
+ ],
26
+ "name": "textarea",
27
+ "registryDependencies": [
28
+ "@verdify/cn",
29
+ "@verdify/label"
30
+ ],
31
+ "title": "textarea",
32
+ "type": "registry:ui"
33
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [
4
+ "class-variance-authority@^0.7.0",
5
+ "radix-ui@^1.1.0"
6
+ ],
7
+ "files": [
8
+ {
9
+ "content": "export {\n ToastProvider,\n ToastViewport,\n Toast,\n ToastIcon,\n ToastContent,\n ToastTitle,\n ToastDescription,\n ToastAction,\n ToastClose,\n type ToastProviderProps,\n type ToastViewportProps,\n type ToastProps,\n type ToastIconProps,\n type ToastContentProps,\n type ToastTitleProps,\n type ToastDescriptionProps,\n type ToastActionProps,\n type ToastCloseProps,\n} from \"./toast\";\nexport {\n toastViewportClass,\n toastVariants,\n toastIconVariants,\n toastContentClass,\n toastTitleClass,\n toastDescriptionClass,\n toastActionClass,\n toastCloseClass,\n toastCloseGlyphClass,\n type ToastVariantProps,\n} from \"./toast.variants\";\n",
10
+ "path": "toast/index.ts",
11
+ "target": "@ui/toast/index.ts",
12
+ "type": "registry:ui"
13
+ },
14
+ {
15
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Toast as ToastPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\nimport {\n toastViewportClass,\n toastVariants,\n toastIconVariants,\n toastContentClass,\n toastTitleClass,\n toastDescriptionClass,\n toastActionClass,\n toastCloseClass,\n toastCloseGlyphClass,\n type ToastVariantProps,\n} from \"./toast.variants\";\n\ntype ToastVariant = NonNullable<ToastVariantProps[\"variant\"]>;\n\n// The variant set on the Toast root travels to the ToastIcon via context (the Dialog/Sheet/Tabs/\n// Accordion/Alert precedent), so the icon takes the matching status fg without the caller repeating\n// `variant` on ToastIcon.\nconst ToastContext = React.createContext<{ variant: ToastVariant }>({ variant: \"signal\" });\n\nexport interface ToastProviderProps\n extends React.ComponentPropsWithoutRef<typeof ToastPrimitive.Provider> {}\n\n/**\n * The queue + announcement manager for a surface's toasts (spec §2 region). Wraps the Radix Toast\n * provider — a stateful primitive owning the stack, the auto-dismiss timers, and the swipe context,\n * so this file is `'use client'`. One per surface. `swipeDirection=\"right\"` makes a toast\n * swipe-dismissable toward the inline-end edge (spec §4 swipe dismissal); `label` names the region\n * landmark for screen readers (spec §7). The region owns the announcement behavior so individual\n * toasts do not each fight to be read (spec §2).\n */\nexport function ToastProvider({\n swipeDirection = \"right\",\n label = \"Notifications\",\n children,\n ...props\n}: ToastProviderProps) {\n return (\n <ToastPrimitive.Provider swipeDirection={swipeDirection} label={label} {...props}>\n {children}\n </ToastPrimitive.Provider>\n );\n}\n\nexport interface ToastViewportProps\n extends React.ComponentPropsWithoutRef<typeof ToastPrimitive.Viewport> {}\n\n/**\n * The corner-anchored live region that holds the stack of toasts (spec §2 region, §7). Rendered as\n * an `<ol>` with `role=\"region\"` (Radix) — the landmark a screen-reader or keyboard user jumps to.\n *\n * The keyboard \"jump to notifications\" key is pinned to `F6` to honor the frozen spec §6, overriding\n * Radix's default `F8` (the hotkey is configurable, so this is a setting, not a behavior deviation).\n * On the jump, focus moves into the region; pressing it again returns focus to where you were\n * (Radix). The region sits on the dedicated `--z-index-toast` layer above page content, NOT the\n * modal layer the overlays use — a toast floats over content without the inert/scrim modality of a\n * Dialog.\n */\nexport const ToastViewport = React.forwardRef<\n React.ElementRef<typeof ToastPrimitive.Viewport>,\n ToastViewportProps\n>(function ToastViewport({ className, hotkey = [\"F6\"], ...props }, ref) {\n return (\n <ToastPrimitive.Viewport\n ref={ref}\n hotkey={hotkey}\n className={cn(toastViewportClass, className)}\n {...props}\n />\n );\n});\n\nexport interface ToastProps\n extends Omit<React.ComponentPropsWithoutRef<typeof ToastPrimitive.Root>, \"type\">,\n ToastVariantProps {\n /**\n * The type the toast reports (spec §3): `verified` (an action succeeded or a claim is now proven —\n * the in-product Verified Green status, NEVER the brand), `signal` (default — a neutral,\n * informational result that needs no action), `caution` (a non-blocking warning completed with a\n * caveat), or `critical` (a failure or error). The variant sets the border accent and the paired\n * icon, and drives the announcement politeness; it never sets the brand color (brand != state).\n */\n variant?: ToastVariant;\n}\n\n/**\n * Toast is a brief, transient message that floats over the page to confirm an action or report an\n * async result without taking focus or blocking the page (spec §1). Use it for a passing\n * acknowledgement — \"Profile saved\", \"Link copied\"; use Alert for a message that must persist in the\n * layout, and a Dialog for a choice that must be answered before continuing.\n *\n * It is a feedback surface, not the brand: its color comes from the status it reports, never from\n * Sovereign Violet — the brand violet is never a status, and a `verified` toast is the green status,\n * never a branded moment (spec §3/§8, brand != state). The status is carried by the border color AND\n * the per-variant icon AND the message text, so it survives for a reader who cannot perceive color\n * (WCAG 1.4.1). Wraps the Radix Toast primitive (a stateful primitive — portal, queue, swipe, and\n * the offscreen announce live region), so this file is `'use client'`.\n *\n * Severity drives announcement politeness (spec §7/§8): `critical` maps to Radix `type=\"foreground\"`\n * (the announce region is `aria-live=\"assertive\"` — a failure must not be missed), while `verified`,\n * `signal`, and `caution` map to `type=\"background\"` (`aria-live=\"polite\"` — a routine result waits\n * for a pause and never interrupts). The toast does NOT take focus on appearance; the page's focus\n * stays put, and the result is announced through the offscreen live region (WCAG 4.1.3).\n *\n * RADIX-VS-SPEC DEVIATION (skill step E): spec §7 names `role=\"alert\"` for the `critical` type. The\n * Radix Toast renders a single offscreen announce node as `role=\"status\"` and switches only its\n * `aria-live` by the toast `type` — so `critical` announces via `role=\"status\"` + `aria-live=\"assertive\"`\n * rather than `role=\"alert\"`. Both are APG-conformant (an alert role IS an implicit assertive live\n * region; an explicit `aria-live=\"assertive\"` on a status region produces the same announcement),\n * and hand-rolling the announce node to emit `role=\"alert\"` would mean abandoning the Radix queue and\n * focus model. We ACCEPT the Radix behavior and PIN it with a dedicated test asserting the assertive\n * politeness on `critical`; the spec note is to be amended.\n *\n * RADIX-VS-SPEC DEVIATION (skill step E): spec §7 also names `aria-atomic=\"true\"` on the region \"so\n * the whole message is read, not a fragment\". Radix does NOT set `aria-atomic` on its `role=\"status\"`\n * announce node; instead it CLONES the toast subtree's text (title + description, action `altText`)\n * into the offscreen announce node as a single batch of children injected in one render — so the\n * whole message is composed and announced as one unit, which IS the intent `aria-atomic` encodes (an\n * atomic announcement is only needed to coalesce piecemeal updates to a persistent region; Radix's\n * announce node is populated once, not mutated fragment-by-fragment). The live region is that\n * offscreen announce node, not the viewport `<ol>` landmark, and Radix does not forward arbitrary\n * `aria-*` onto it — hand-rolling the announce node to carry `aria-atomic` would mean abandoning the\n * Radix queue + announce model. We ACCEPT the Radix behavior and PIN it with a dedicated test\n * documenting that the announce-clone (not a literal `aria-atomic`) satisfies §7's intent; the spec\n * note is to be amended.\n */\nexport const Toast = React.forwardRef<\n React.ElementRef<typeof ToastPrimitive.Root>,\n ToastProps\n>(function Toast({ className, variant = \"signal\", ...props }, ref) {\n // severity -> Radix announce politeness: critical interrupts (assertive), the rest wait (polite)\n const type = variant === \"critical\" ? \"foreground\" : \"background\";\n return (\n <ToastContext.Provider value={{ variant }}>\n <ToastPrimitive.Root\n ref={ref}\n type={type}\n className={cn(toastVariants({ variant }), className)}\n {...props}\n />\n </ToastContext.Provider>\n );\n});\n\nexport interface ToastIconProps extends React.HTMLAttributes<HTMLSpanElement> {}\n\n/**\n * The optional type icon that reinforces the status (spec §2). It pairs with the variant color and\n * the message text so the status is never the only carrier of meaning, since status is not conveyed\n * by color or icon alone (WCAG 1.4.1). Decorative — `aria-hidden`, so it is not announced twice; the\n * message text carries the status if the icon is dropped. Being decorative, it takes the BRIGHT\n * variant accent color (tokens 0.6.0) via the ToastContext, at the md icon role.\n */\nexport const ToastIcon = React.forwardRef<HTMLSpanElement, ToastIconProps>(\n function ToastIcon({ className, ...props }, ref) {\n const { variant } = React.useContext(ToastContext);\n return (\n <span\n ref={ref}\n aria-hidden=\"true\"\n className={cn(toastIconVariants({ variant }), className)}\n {...props}\n />\n );\n },\n);\n\nexport interface ToastContentProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * The stacking message column (spec §2): wraps the message and any secondary line so they stack\n * vertically beside the leading icon. `min-w-0` lets long message text wrap instead of overflowing\n * the row, and `flex-1` lets it take the available width before the action and dismiss controls.\n */\nexport const ToastContent = React.forwardRef<HTMLDivElement, ToastContentProps>(\n function ToastContent({ className, ...props }, ref) {\n return <div ref={ref} className={cn(toastContentClass, className)} {...props} />;\n },\n);\n\nexport interface ToastTitleProps\n extends React.ComponentPropsWithoutRef<typeof ToastPrimitive.Title> {}\n\n/**\n * The required message (spec §2): a plain statement ending in a period; for a failure it names what\n * failed and what to do next and never blames you. It IS the toast's accessible name, wired to the\n * toast via `aria-labelledby` by Radix (spec §7). The body type role in primary text.\n */\nexport const ToastTitle = React.forwardRef<\n React.ElementRef<typeof ToastPrimitive.Title>,\n ToastTitleProps\n>(function ToastTitle({ className, ...props }, ref) {\n return <ToastPrimitive.Title ref={ref} className={cn(toastTitleClass, className)} {...props} />;\n});\n\nexport interface ToastDescriptionProps\n extends React.ComponentPropsWithoutRef<typeof ToastPrimitive.Description> {}\n\n/**\n * The optional secondary message line under the message (spec §2). Associated with the toast for\n * screen readers via `aria-describedby` (Radix). The body type role in secondary text (spec §5).\n */\nexport const ToastDescription = React.forwardRef<\n React.ElementRef<typeof ToastPrimitive.Description>,\n ToastDescriptionProps\n>(function ToastDescription({ className, ...props }, ref) {\n return (\n <ToastPrimitive.Description\n ref={ref}\n className={cn(toastDescriptionClass, className)}\n {...props}\n />\n );\n});\n\nexport interface ToastActionProps\n extends React.ComponentPropsWithoutRef<typeof ToastPrimitive.Action> {}\n\n/**\n * The optional inline action (spec §2/§6): at most one, phrased as a verb (\"Undo\", \"View\"). A real\n * native `<button>` (Radix ToastAction) so it is named, focusable, and operable on Enter / Space; it\n * dismisses the toast on activation. A toast is not a place for a primary decision — if a choice is\n * required, reach for a Dialog instead (spec §2/§8). Requires `altText`: a short description for\n * screen-reader users who will not navigate to the button quickly (Radix). A neutral ghost surface\n * at the label type role so it never competes with the status accent.\n */\nexport const ToastAction = React.forwardRef<\n React.ElementRef<typeof ToastPrimitive.Action>,\n ToastActionProps\n>(function ToastAction({ className, ...props }, ref) {\n return (\n <ToastPrimitive.Action ref={ref} className={cn(toastActionClass, className)} {...props} />\n );\n});\n\n// A neutral X glyph at the md icon role, drawn with currentColor so it inherits the close button's\n// ghost fg. Decorative (aria-hidden) — the button carries the accessible name (spec §7).\nfunction CloseGlyph() {\n return (\n <svg\n data-testid=\"toast-close-glyph\"\n aria-hidden=\"true\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n className={toastCloseGlyphClass}\n >\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n );\n}\n\nexport interface ToastCloseProps\n extends React.ComponentPropsWithoutRef<typeof ToastPrimitive.Close> {\n /** The accessible name for the close control (spec §7). Defaults to `\"Dismiss\"`. */\n \"aria-label\"?: string;\n}\n\n/**\n * The optional dismiss control (spec §2/§6/§7): an explicit close for toasts that do not auto-dismiss\n * and for pointer users clearing one early. A real native `<button>` (Radix ToastClose) so it is a\n * focus stop and activates on Enter / Space for free; closing flows through the toast's\n * `onOpenChange(false)` and returns focus to where it was (Radix). It has an accessible name (default\n * \"Dismiss\") and a decorative `aria-hidden` glyph (the placeholder is never the name). A neutral ghost\n * icon-button with the square target-size floor and the persistent focus ring.\n */\nexport const ToastClose = React.forwardRef<\n React.ElementRef<typeof ToastPrimitive.Close>,\n ToastCloseProps\n>(function ToastClose(\n { className, children, \"aria-label\": ariaLabel = \"Dismiss\", ...props },\n ref,\n) {\n return (\n <ToastPrimitive.Close\n ref={ref}\n aria-label={ariaLabel}\n className={cn(toastCloseClass, className)}\n {...props}\n >\n {children ?? <CloseGlyph />}\n </ToastPrimitive.Close>\n );\n});\n",
16
+ "path": "toast/toast.tsx",
17
+ "target": "@ui/toast/toast.tsx",
18
+ "type": "registry:ui"
19
+ },
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",
22
+ "path": "toast/toast.variants.ts",
23
+ "target": "@ui/toast/toast.variants.ts",
24
+ "type": "registry:ui"
25
+ }
26
+ ],
27
+ "name": "toast",
28
+ "registryDependencies": [
29
+ "@verdify/cn"
30
+ ],
31
+ "title": "toast",
32
+ "type": "registry:ui"
33
+ }