@verdify/ui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +48 -17
  2. package/package.json +30 -24
  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
@@ -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": "import * as React from \"react\";\nimport { Slot } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\nimport {\n cardVariants,\n cardMediaClass,\n cardHeaderClass,\n cardTitleClass,\n cardHeaderActionsClass,\n cardBodyClass,\n cardFooterClass,\n type CardVariantProps,\n} from \"./card.variants\";\n\nexport interface CardProps\n extends React.HTMLAttributes<HTMLElement>,\n CardVariantProps {\n /**\n * `static` (default) — a non-interactive grouping: the whole card does nothing, the\n * controls inside it do. `interactive` — the whole surface is a single link or button to\n * exactly one destination or action. Never make a card interactive while it still holds its\n * own focusable controls (spec §8): choose one.\n */\n variant?: \"static\" | \"interactive\";\n /**\n * Expose a static grouping as a navigation landmark (`role=\"region\"`). Give it an accessible\n * name via `aria-label`/`aria-labelledby` (usually the header title). Ignored for the\n * interactive variant, which is already a single named control.\n */\n asRegion?: boolean;\n /**\n * Interactive only — render the child element (a real `<a>` when it navigates, a `<button>`\n * when it acts) with card styling via Radix Slot, instead of the default native `<button>`.\n * Do not fake interactivity with a generic `<div>` (spec §7).\n */\n asChild?: boolean;\n /**\n * Interactive only — the action is unavailable: native `disabled` on the `<button>`, or\n * `aria-disabled` + removal from the tab order on the `<a>`. A static card is never disabled;\n * it simply has no action to disable.\n */\n disabled?: boolean;\n}\n\n/**\n * A card groups related content and actions on one raised, neutral surface. It is a layout\n * container, not a status indicator: it never carries the brand violet or a status color across\n * its whole surface — those are accents on the controls and badges it holds (spec §1/§3/§8).\n * It is render-only (no hook / stateful Radix), so it needs no `'use client'`.\n */\nexport const Card = React.forwardRef<HTMLElement, CardProps>(function Card(\n {\n className,\n variant = \"static\",\n emphasis,\n asRegion = false,\n asChild = false,\n disabled = false,\n role,\n ...props\n },\n ref,\n) {\n const classes = cn(cardVariants({ variant, emphasis }), className);\n\n if (variant === \"interactive\") {\n // The interactive card is a single native control. `asChild` projects the styling onto a\n // caller-supplied <a>/<button> (Slot runs React.Children.only — one child, no spinner-style\n // sibling). The default is a native <button>. Disabled is native on the <button>; on the\n // projected element we set aria-disabled + tabindex=-1 and let the caller suppress the href.\n if (asChild) {\n return (\n <Slot.Root\n ref={ref as React.Ref<HTMLElement>}\n className={classes}\n aria-disabled={disabled || undefined}\n tabIndex={disabled ? -1 : undefined}\n {...props}\n />\n );\n }\n return (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n type=\"button\"\n disabled={disabled}\n className={classes}\n {...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}\n />\n );\n }\n\n // Static: a non-interactive grouping <div>. Exposed as a named region only when asRegion is set\n // (otherwise a generic grouping with no interactive role — never a faked role=\"button\").\n return (\n <div\n ref={ref as React.Ref<HTMLDivElement>}\n role={asRegion ? \"region\" : role}\n className={classes}\n {...(props as React.HTMLAttributes<HTMLDivElement>)}\n />\n );\n});\n\n/**\n * The media slot — a leading image, illustration, or chart spanning the inline edges above the\n * header. A decorative image is `aria-hidden`; an informative one carries alt text on its `<img>`.\n */\nexport const CardMedia = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement> & { decorative?: boolean }\n>(function CardMedia({ className, decorative = false, ...props }, ref) {\n return (\n <div\n ref={ref}\n aria-hidden={decorative || undefined}\n className={cn(cardMediaClass, className)}\n {...props}\n />\n );\n});\n\n/** The header slot — a title and optional supporting text plus an optional `header-actions` set. */\nexport const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function CardHeader({ className, ...props }, ref) {\n return <div ref={ref} className={cn(cardHeaderClass, className)} {...props} />;\n },\n);\n\n/**\n * The card title — a statement in sentence case (no all-caps, no exclamation mark). Defaults to a\n * `<div>`; pass `asChild` to render the correct heading element for the document outline.\n */\nexport const CardTitle = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement> & { asChild?: boolean }\n>(function CardTitle({ className, asChild = false, ...props }, ref) {\n const Comp = asChild ? Slot.Root : \"div\";\n return <Comp ref={ref} className={cn(cardTitleClass, className)} {...props} />;\n});\n\n/** The header-actions slot — a small control set aligned to the header's inline-end edge. */\nexport const CardHeaderActions = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(function CardHeaderActions({ className, ...props }, ref) {\n return <div ref={ref} className={cn(cardHeaderActionsClass, className)} {...props} />;\n});\n\n/** The body slot — the card's primary content. */\nexport const CardBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function CardBody({ className, ...props }, ref) {\n return <div ref={ref} className={cn(cardBodyClass, className)} {...props} />;\n },\n);\n\n/** The footer slot — actions or closing metadata; holds at most one primary button (restraint). */\nexport const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function CardFooter({ className, ...props }, ref) {\n return <div ref={ref} className={cn(cardFooterClass, className)} {...props} />;\n },\n);\n",
10
+ "path": "card/card.tsx",
11
+ "target": "@ui/card/card.tsx",
12
+ "type": "registry:ui"
13
+ },
14
+ {
15
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// The card container. A card is a NEUTRAL layout surface (spec §1/§5): it paints from the\n// surface-* roles only — never from a --color-action-* or --color-status-* token, which belong\n// to the controls and badges it holds. The structural axis is the container BEHAVIOR (spec §3):\n// `static` (a non-interactive grouping, the common case) vs `interactive` (the whole surface is\n// one link/button). A second `emphasis` axis flips the resting hairline between the default and\n// the quieter muted border for a low-emphasis grouping (spec §4). The `media` slot composes via\n// the CardMedia subcomponent, so it is not a container-behavior variant here.\nexport const cardVariants = cva(\n [\n // raised surface, rounded container, resting elevation, internal padding from --space-4\n \"block bg-surface-raised border rounded-lg shadow-sm p-(--space-4)\",\n // logical-property text alignment so the card mirrors under dir=rtl (G-U6)\n \"text-start text-text-primary\",\n ],\n {\n variants: {\n variant: {\n // a non-interactive grouping: the whole card does nothing; the controls inside it do\n static: \"\",\n // the whole surface is one control: a restrained hover lift (fast + verdify easing,\n // NEVER the deliberate verified-check theatre), a stronger hover border, and the\n // settle-back on press; the focus ring + target-size floor live in the interactive base\n interactive: [\n \"cursor-pointer\",\n \"transition-[color,box-shadow,border-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // hover lift: the resting hairline (border-surface-border / -muted, set by `emphasis`)\n // strengthens to the higher-contrast border-border-strong, alongside the shadow-sm->md\n // elevation. Two DISTINCT tokens, so the §4 \"slightly stronger border\" is a real delta,\n // not the no-op that mapping both rest+hover to --color-surface-border would produce.\n \"hover:shadow-md hover:border-border-strong\",\n \"active:shadow-sm\",\n // target-size floor: 44px touch / 40px pointer (§7 2.5.8)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px focus ring around the whole card, on every state, never removed (§4)\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // disabled — DEC-C: dim via the disabled TOKEN, never a blanket opacity; remove the\n // pointer + hover/pressed response. aria-disabled covers the <a> path (native disabled\n // already blocks the <button> path).\n \"disabled:pointer-events-none disabled:text-text-disabled disabled:shadow-none\",\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled aria-disabled:shadow-none\",\n ],\n },\n emphasis: {\n // the default container hairline\n default: \"border-surface-border\",\n // the quieter hairline for a low-emphasis grouping (spec §4)\n muted: \"border-surface-border-muted\",\n },\n },\n defaultVariants: { variant: \"static\", emphasis: \"default\" },\n },\n);\n\n// The media slot: a leading image/illustration/chart spanning the inline edges above the header.\n// It negates the container's --space-4 padding on the top + inline edges so the media bleeds to\n// the card edge, and re-establishes the --space-2 stacked-slot gap below it.\nexport const cardMediaClass =\n \"-mt-(--space-4) -mx-(--space-4) mb-(--space-2) overflow-hidden rounded-t-lg\";\n\n// The header slot: title + supporting text on the inline-start, an optional control set on the\n// inline-end. --space-2 stacked-slot gap below it.\nexport const cardHeaderClass = \"flex items-start justify-between gap-(--space-2) mb-(--space-2)\";\n\n// The title: a statement in the h3 type role + the primary text color. Sentence case, no all-caps\n// (the type role already avoids the label tracking); brand violet never paints the title.\nexport const cardTitleClass = \"text-h3 text-text-primary\";\n\n// The header-actions slot: a small control set aligned to the header's inline-end edge.\nexport const cardHeaderActionsClass =\n \"flex shrink-0 items-center gap-(--space-2) -mt-(--space-1) -me-(--space-1)\";\n\n// The body slot: the card's primary content, in the body type role + secondary text color.\nexport const cardBodyClass = \"text-body text-text-secondary\";\n\n// The footer slot: actions or closing metadata, in the label type role + secondary text color.\n// --space-2 stacked-slot gap above it.\nexport const cardFooterClass =\n \"flex items-center justify-between gap-(--space-2) mt-(--space-2) text-label text-text-secondary\";\n\nexport type CardVariantProps = VariantProps<typeof cardVariants>;\n",
16
+ "path": "card/card.variants.ts",
17
+ "target": "@ui/card/card.variants.ts",
18
+ "type": "registry:ui"
19
+ },
20
+ {
21
+ "content": "export {\n Card,\n CardMedia,\n CardHeader,\n CardTitle,\n CardHeaderActions,\n CardBody,\n CardFooter,\n type CardProps,\n} from \"./card\";\nexport {\n cardVariants,\n cardMediaClass,\n cardHeaderClass,\n cardTitleClass,\n cardHeaderActionsClass,\n cardBodyClass,\n cardFooterClass,\n type CardVariantProps,\n} from \"./card.variants\";\n",
22
+ "path": "card/index.ts",
23
+ "target": "@ui/card/index.ts",
24
+ "type": "registry:ui"
25
+ }
26
+ ],
27
+ "name": "card",
28
+ "registryDependencies": [
29
+ "@verdify/cn"
30
+ ],
31
+ "title": "card",
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": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport {\n checkboxBoxVariants,\n checkboxLabelVariants,\n checkboxCheckGlyphVariants,\n checkboxBarGlyphVariants,\n type CheckboxBoxVariantProps,\n} from \"./checkbox.variants\";\n\ntype NativeInputProps = Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n \"size\" | \"type\" | \"onChange\"\n>;\n\nexport interface CheckboxProps extends NativeInputProps, CheckboxBoxVariantProps {\n /** The visible text naming the choice. Becomes the accessible name via <label for>. */\n label: string;\n /** Secondary helper text beneath the label. Linked via aria-describedby. */\n description?: string;\n /** Validation message; sets aria-invalid and the critical-bordered error state. */\n error?: string;\n /** standalone = one value; parent = summarizes children (the only place mixed rests). */\n variant?: \"standalone\" | \"parent\";\n /** Mixed/“some children” state. Honored ONLY on variant=\"parent\"; ignored on standalone. */\n indeterminate?: boolean;\n /** Fired with the resolved boolean; a parent toggle resolves mixed → checked/unchecked. */\n onCheckedChange?: (checked: boolean) => void;\n}\n\nlet idSeq = 0;\n\nexport const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(\n function Checkbox(\n {\n className,\n label,\n description,\n error,\n size,\n variant = \"standalone\",\n indeterminate = false,\n disabled = false,\n id,\n onCheckedChange,\n ...props\n },\n forwardedRef,\n ) {\n const reactId = React.useId();\n const baseId = id ?? `checkbox-${reactId}-${(idSeq += 1)}`;\n const descId = description ? `${baseId}-description` : undefined;\n const errorId = error ? `${baseId}-error` : undefined;\n\n // indeterminate is a parent-only RESTING state — standalone never rests on mixed.\n const isMixed = variant === \"parent\" && indeterminate;\n\n const innerRef = React.useRef<HTMLInputElement>(null);\n // bridge the forwarded ref to our inner ref (we need direct DOM access for indeterminate)\n React.useImperativeHandle(forwardedRef, () => innerRef.current as HTMLInputElement);\n\n // indeterminate is a DOM property, not an attribute — write it through the ref.\n // This effect is why the component is a client component ('use client' above).\n React.useEffect(() => {\n if (innerRef.current) innerRef.current.indeterminate = isMixed;\n }, [isMixed]);\n\n const describedBy =\n [descId, errorId].filter(Boolean).join(\" \") || undefined;\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n // a direct parent toggle resolves mixed → a single state; never rests on mixed\n onCheckedChange?.(e.currentTarget.checked);\n };\n\n return (\n <div className=\"flex flex-col gap-1\">\n {/* control row: the box + label share one hit area no smaller than the target token */}\n <div\n data-testid=\"checkbox-control\"\n className=\"flex min-h-(--size-target-mobile) items-center gap-2 sm:min-h-(--size-target-desktop)\"\n >\n {/* relative overlay: the box (peer) + the indicator glyphs it drives.\n The glyphs are siblings AFTER the input so peer-* can reach them. */}\n <span className=\"relative inline-flex shrink-0\">\n <input\n ref={innerRef}\n id={baseId}\n type=\"checkbox\"\n disabled={disabled}\n aria-checked={isMixed ? \"mixed\" : undefined}\n aria-invalid={error ? \"true\" : undefined}\n aria-describedby={describedBy}\n onChange={handleChange}\n className={cn(checkboxBoxVariants({ size }), className)}\n {...props}\n />\n {/* check: only when :checked; never rendered in the mixed state so the\n bar wins for an indeterminate parent (check and bar stay exclusive) */}\n {!isMixed ? (\n <svg\n data-testid=\"checkbox-check\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n className={checkboxCheckGlyphVariants({ disabled })}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n >\n <path d=\"M3.5 8.5l3 3 6-6.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n ) : null}\n {/* horizontal bar: the single indeterminate mark, only when :indeterminate */}\n <svg\n data-testid=\"checkbox-bar\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n className={checkboxBarGlyphVariants({ disabled })}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n >\n <path d=\"M4 8h8\" strokeLinecap=\"round\" />\n </svg>\n </span>\n <label htmlFor={baseId} className={checkboxLabelVariants({ disabled })}>\n {label}\n </label>\n </div>\n {description ? (\n <p id={descId} className=\"text-caption text-text-secondary\">\n {description}\n </p>\n ) : null}\n {error ? (\n <p id={errorId} className=\"text-caption text-status-critical-fg\">\n {error}\n </p>\n ) : null}\n </div>\n );\n },\n);\n",
9
+ "path": "checkbox/checkbox.tsx",
10
+ "target": "@ui/checkbox/checkbox.tsx",
11
+ "type": "registry:ui"
12
+ },
13
+ {
14
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// The square box that carries the visual state. Token binding lives here ONLY.\n// `peer` so the overlaid indicator glyphs can react to :checked / :indeterminate.\nexport const checkboxBoxVariants = cva(\n [\n // shape + neutral control tier at rest (unchecked)\n \"peer shrink-0 appearance-none rounded-sm border bg-control-bg text-control-fg\",\n \"border-border-default\",\n // hover: the box border strengthens; box + label read as one target\n \"hover:border-border-strong\",\n // selection accent (Sovereign Violet) — the brand, NOT a status — when set\n \"checked:bg-action-primary-bg checked:text-action-primary-fg\",\n \"checked:border-action-primary-bg\",\n \"indeterminate:bg-action-primary-bg indeterminate:text-action-primary-fg\",\n \"indeterminate:border-action-primary-bg\",\n // functional toggle: fast + verdify easing, instant under reduced motion.\n // Never the deliberate theatre — a checkbox toggle is not the verified-check moment.\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // visible 2px signal-blue ring at 2px offset, never removed\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // error: critical border marks the FIELD; the fill stays the selection accent\n \"aria-invalid:border-status-critical-border\",\n // disabled: not interactive; state still reads (a disabled-checked box reads checked).\n // DEC-C / spec §4: a disabled control is dimmed via the disabled TOKEN on the\n // indicator (text-text-disabled, below), NOT a blanket opacity-60 on the box.\n \"disabled:pointer-events-none\",\n ],\n {\n variants: {\n size: {\n sm: \"h-(--size-icon-sm) w-(--size-icon-sm)\",\n md: \"h-(--size-icon-md) w-(--size-icon-md)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// The overlaid indicator glyphs sit absolutely over the box and are colored by\n// the action-primary FOREGROUND — the glyph on the brand accent fill (spec §5),\n// never a status color. `peer-*` drives visibility from the sibling input's\n// native :checked / :indeterminate state; both glyphs are pointer-transparent\n// and aria-hidden (the input carries role/state). Each glyph is centered over\n// the box and drawn at three-quarters of the box so it never touches the edge.\n//\n// DEC-C / spec §4·§5: when the control is disabled the indicator (check / bar)\n// renders in --color-text-disabled — the SAME token the label dims to — NOT a\n// blanket opacity-60 dim of the whole box. The glyph is a sibling AFTER the peer\n// input, so a `disabled` cva variant (driven by the explicit prop, mirroring\n// checkboxLabelVariants) flips text-action-primary-fg → text-text-disabled.\nconst indicatorBase = [\n \"pointer-events-none absolute inset-0 m-auto hidden h-3/4 w-3/4\",\n];\nconst indicatorColorVariants = {\n variants: {\n disabled: {\n true: \"text-text-disabled\",\n false: \"text-action-primary-fg\",\n },\n },\n defaultVariants: { disabled: false },\n} as const;\n// Check glyph: shown when the peer input is :checked. The mixed (parent-only)\n// case never renders this glyph (it is omitted in JSX when isMixed), so the\n// check and the bar stay mutually exclusive without brittle variant stacking.\nexport const checkboxCheckGlyphVariants = cva(\n [...indicatorBase, \"peer-checked:block\"],\n indicatorColorVariants,\n);\n// Horizontal-bar glyph: shown only when the peer input is :indeterminate.\nexport const checkboxBarGlyphVariants = cva(\n [...indicatorBase, \"peer-indeterminate:block\"],\n indicatorColorVariants,\n);\n\n// The label naming the choice; part of the hit area.\nexport const checkboxLabelVariants = cva(\"text-text-primary select-none\", {\n variants: {\n disabled: { true: \"text-text-disabled\", false: \"\" },\n },\n defaultVariants: { disabled: false },\n});\n\nexport type CheckboxBoxVariantProps = VariantProps<typeof checkboxBoxVariants>;\n",
15
+ "path": "checkbox/checkbox.variants.ts",
16
+ "target": "@ui/checkbox/checkbox.variants.ts",
17
+ "type": "registry:ui"
18
+ },
19
+ {
20
+ "content": "export { Checkbox, type CheckboxProps } from \"./checkbox\";\nexport {\n checkboxBoxVariants,\n checkboxLabelVariants,\n type CheckboxBoxVariantProps,\n} from \"./checkbox.variants\";\n",
21
+ "path": "checkbox/index.ts",
22
+ "target": "@ui/checkbox/index.ts",
23
+ "type": "registry:ui"
24
+ }
25
+ ],
26
+ "name": "checkbox",
27
+ "registryDependencies": [
28
+ "@verdify/cn"
29
+ ],
30
+ "title": "checkbox",
31
+ "type": "registry:ui"
32
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [
4
+ "clsx@^2.1.0",
5
+ "tailwind-merge@^2.5.0"
6
+ ],
7
+ "files": [
8
+ {
9
+ "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n// The @verdify/tokens theme exposes ten custom type-role font sizes as Tailwind v4\n// `--text-{role}` keys, which generate `text-{role}` font-size utilities. tailwind-merge's\n// defaults don't know these role names, so it files `text-label`, `text-body`, … under its\n// `text-color` group and lets a later `text-text-*` / `text-action-*` COLOR utility evict the\n// font-size (verified: `twMerge(\"text-label\", \"text-action-primary-fg\")` drops `text-label`).\n// Registering the roles into the `font-size` group restores the correct font-size-vs-color\n// split so a role and a color coexist. Defined once here; every component reuses this `cn`.\nconst TEXT_ROLES = [\n \"display\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"body-lg\",\n \"body\",\n \"caption\",\n \"label\",\n \"mono-sm\",\n \"mono\",\n] as const;\n\nconst twMerge = extendTailwindMerge({\n extend: { classGroups: { \"font-size\": [{ text: [...TEXT_ROLES] }] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n",
10
+ "path": "lib/cn.ts",
11
+ "target": "@lib/cn.ts",
12
+ "type": "registry:lib"
13
+ }
14
+ ],
15
+ "name": "cn",
16
+ "registryDependencies": [],
17
+ "title": "cn (Verdify token-aware)",
18
+ "type": "registry:lib"
19
+ }
@@ -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": "\"use client\";\n\nimport * as React from \"react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\nimport {\n commandPaletteScrimClass,\n commandPalettePanelClass,\n commandPaletteInputClass,\n commandPaletteSearchIconClass,\n commandPaletteListboxClass,\n commandPaletteGroupLabelClass,\n commandPaletteOptionVariants,\n commandPaletteLabelClass,\n commandPaletteSecondaryClass,\n commandPaletteOptionIconClass,\n commandPaletteShortcutClass,\n commandPaletteEmptyClass,\n commandPaletteFooterClass,\n} from \"./command-palette.variants\";\n\nexport type CommandPaletteVariant =\n | \"commands\"\n | \"navigation\"\n | \"with-recent\"\n | \"scoped\";\n\nexport interface CommandPaletteItem {\n /** Stable id — the option's DOM id derives from it (spec §7 aria-activedescendant target). */\n id: string;\n /** The result label — the row's accessible name; the row names itself by this, not its icon (spec §2). */\n label: string;\n /** Optional secondary line under the label (spec §2 option). Where a disabled row's short reason fits (spec §4 Disabled). */\n secondary?: string;\n /** Optional leading icon (spec §2): decorative, --size-icon-md; the row names itself by its label. */\n icon?: React.ReactNode;\n /** Optional trailing keyboard-shortcut hint (spec §2): muted label role, never a focus stop. */\n shortcut?: React.ReactNode;\n /** A non-runnable row (spec §4 Disabled): aria-disabled, skipped by arrow movement, not activatable. */\n disabled?: boolean;\n /** The group heading this row sits under (spec §2 group-label). Consecutive same-group rows are grouped. */\n group?: string;\n}\n\nexport interface CommandPaletteProps {\n /** Controlled open state — the host surface owns the open shortcut (commonly Cmd/Ctrl+K), not this component (spec §6). */\n open: boolean;\n /** Reports open changes; called with `false` on Escape, scrim click, or running a result (spec §6/§7). */\n onOpenChange: (open: boolean) => void;\n /**\n * The input's accessible name (spec §7): the input has no visible label, so it names itself via\n * `aria-label` (for example \"Search commands\"). The placeholder is NEVER the accessible name.\n */\n inputLabel: string;\n /** The runnable results, optionally grouped by `group` (spec §2 listbox / option). */\n items: CommandPaletteItem[];\n /**\n * The recent commands/destinations shown BEFORE any query (spec §3 `with-recent`), so the frequent\n * case is one keystroke away. Replaced by filtered `items` as soon as you type.\n */\n recent?: CommandPaletteItem[];\n /** Intent (spec §3): `commands` (default), `navigation`, `with-recent`, or `scoped`. */\n variant?: CommandPaletteVariant;\n /** The input placeholder (spec §2): de-emphasised, never the accessible name. */\n placeholder?: string;\n /**\n * Results are resolving (spec §4 Loading): sets the listbox `aria-busy` and announces busy state;\n * the input stays focused and typeable so a slow source never freezes the keyboard. A wait is a\n * plain wait, not theatre.\n */\n loading?: boolean;\n /** The no-match text (spec §2 empty, §4): says nothing matched and what to try — never a dead end, never a blamed query. */\n emptyText?: string;\n /** A thin hint row showing the active keys (spec §2 footer), so the keyboard model is discoverable in place. */\n footer?: React.ReactNode;\n /** Run the active/clicked result (spec §6 Enter / pointer): fired with the item, after which the palette closes. */\n onRun?: (item: CommandPaletteItem) => void;\n className?: string;\n}\n\n// A neutral magnifier glyph, --size-icon-md, drawn with currentColor so it inherits the input's\n// placeholder color. Decorative (aria-hidden) — the input carries the accessible name (spec §7).\nfunction SearchGlyph() {\n return (\n <svg\n data-testid=\"command-palette-search-glyph\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n className=\"h-(--size-icon-md) w-(--size-icon-md)\"\n >\n <circle cx=\"7\" cy=\"7\" r=\"4.5\" />\n <path d=\"M10.5 10.5L14 14\" strokeLinecap=\"round\" />\n </svg>\n );\n}\n\n/** Group rows by their `group` key, preserving order (spec §2 group: consecutive same-group rows group). */\nfunction groupItems(items: CommandPaletteItem[]): { group?: string; rows: CommandPaletteItem[] }[] {\n const out: { group?: string; rows: CommandPaletteItem[] }[] = [];\n for (const item of items) {\n const last = out[out.length - 1];\n if (last && last.group === item.group) last.rows.push(item);\n else out.push({ group: item.group, rows: [item] });\n }\n return out;\n}\n\nconst matches = (item: CommandPaletteItem, query: string): boolean => {\n if (!query) return true;\n const q = query.toLowerCase();\n return (\n item.label.toLowerCase().includes(q) ||\n (item.secondary ? item.secondary.toLowerCase().includes(q) : false)\n );\n};\n\n/**\n * CommandPalette is a keyboard-first launcher (spec §1): open it from anywhere, type a few letters,\n * and run a command or jump to a place without leaving the keyboard. It is the accelerator OVER the\n * visible navigation, not a replacement — every command in it is also reachable by pointer somewhere\n * in the UI. Reach for a Menu to fire actions behind a trigger, a Select to pick a value, and the\n * Sidebar for page-level navigation.\n *\n * It is a NEUTRAL overlay surface (spec §3): the scrim, panel, input, and result rows are neutral,\n * and the active-row highlight is the SECONDARY hover fill — never Verified Green and never the\n * brand violet. The active row reports WHERE you are in the list, not a verified result, so brand is\n * not a state (G-U2); a verified meaning belongs to VerifiedBadge, never a palette row.\n *\n * It uses the WAI-ARIA APG combobox-with-listbox pattern: the input is the `role=\"combobox\"` and the\n * results are its `role=\"listbox\"` popup, with the active option tracked by `aria-activedescendant`\n * so DOM focus STAYS in the input the whole time (type-ahead keeps working, spec §4/§6/§7/§8). The\n * OVERLAY shell — portal, focus trap, Escape, inert siblings, scrim — is the Radix `Dialog`\n * primitive; the combobox/listbox roving is hand-rolled INSIDE it, because the spec's\n * focus-stays-in-the-input contract is the OPPOSITE of Radix's focus-moving roving listbox and §8\n * names focus-moving as a forbidden anti-pattern (skill: compose a role by hand when Radix can't\n * express the spec's anatomy). A stateful component, so this file is `'use client'`.\n */\nexport function CommandPalette({\n open,\n onOpenChange,\n inputLabel,\n items,\n recent,\n variant = \"commands\",\n placeholder,\n loading = false,\n emptyText = \"Nothing matched. Try a different word.\",\n footer,\n onRun,\n className,\n}: CommandPaletteProps) {\n void variant; // the structural axis is expressed by the result set the caller passes (spec §3 grouped results)\n const reactId = React.useId();\n const listboxId = `${reactId}-listbox`;\n const inputId = `${reactId}-input`;\n const statusId = `${reactId}-status`;\n const inputRef = React.useRef<HTMLInputElement>(null);\n // The element that had focus when the palette opened — focus returns HERE on close (spec §6/§7),\n // so a keyboard user is never dropped at the page top. The host owns the open shortcut (Cmd/Ctrl+K\n // from anywhere), so there is no single DialogTrigger for Radix to restore to; we capture the\n // opener ourselves and restore it when the palette closes.\n const openerRef = React.useRef<HTMLElement | null>(null);\n\n const [query, setQuery] = React.useState(\"\");\n\n // Before any query, show recent (spec §3 with-recent); as soon as you type, recent is replaced by\n // the filtered items (spec §3 / §4 Default).\n const showRecent = query.length === 0 && recent !== undefined && recent.length > 0;\n const source = showRecent ? recent! : items;\n const visible = React.useMemo(\n () => source.filter((item) => matches(item, query)),\n [source, query],\n );\n // The runnable rows (enabled) — arrow movement and Home/End operate over these; group labels and\n // disabled rows are skipped (spec §6).\n const runnable = React.useMemo(() => visible.filter((item) => !item.disabled), [visible]);\n\n const [activeId, setActiveId] = React.useState<string | null>(null);\n // The active row resets to the FIRST runnable match whenever the result set changes (spec §6\n // \"the active row resets to the first match\"; spec §4 Default \"the first item is active\").\n React.useEffect(() => {\n setActiveId(runnable.length > 0 ? runnable[0].id : null);\n }, [runnable]);\n\n const optionDomId = (id: string) => `${reactId}-opt-${id}`;\n const activeIndex = runnable.findIndex((item) => item.id === activeId);\n\n const moveActive = (next: number) => {\n if (runnable.length === 0) return;\n const wrapped = ((next % runnable.length) + runnable.length) % runnable.length;\n setActiveId(runnable[wrapped].id);\n };\n\n const runItem = (item: CommandPaletteItem) => {\n if (item.disabled) return;\n onRun?.(item);\n onOpenChange(false); // Enter/click runs, then closes; Radix returns focus to the opener (spec §6/§7)\n };\n\n const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n moveActive(activeIndex < 0 ? 0 : activeIndex + 1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n moveActive(activeIndex < 0 ? runnable.length - 1 : activeIndex - 1);\n break;\n case \"Home\":\n event.preventDefault();\n moveActive(0);\n break;\n case \"End\":\n event.preventDefault();\n moveActive(runnable.length - 1);\n break;\n case \"PageDown\":\n // Move by a viewport of rows (spec §6). Without measured row heights in this headless layer\n // a fixed page step is the conformant approximation; it clamps to the last runnable row.\n event.preventDefault();\n moveActive(Math.min(activeIndex + PAGE_STEP, runnable.length - 1));\n break;\n case \"PageUp\":\n event.preventDefault();\n moveActive(Math.max(activeIndex - PAGE_STEP, 0));\n break;\n case \"Enter\": {\n event.preventDefault();\n const item = runnable[activeIndex];\n if (item) runItem(item);\n break;\n }\n // Escape is handled on Content's onEscapeKeyDown (below), not here: Radix listens for Escape at\n // the document level, so a React-bubbled stopPropagation on the input would not stop Radix's\n // own close. Intercepting it on Content's onEscapeKeyDown is the only reliable seam.\n default:\n break;\n }\n };\n\n // First Escape with a query CLEARS the query (palette stays open); the next Escape closes (spec\n // §6). We intercept Radix's Escape at its source: when a query is present, preventDefault keeps the\n // Dialog open and we clear the query; an empty query lets Radix close as usual.\n const handleEscapeKeyDown = (event: KeyboardEvent) => {\n if (query.length > 0) {\n event.preventDefault();\n setQuery(\"\");\n }\n };\n\n // The match count a query produced (spec §7 4.1.3): the VISIBLE matches a screen-reader user hears,\n // including a matched-but-disabled row (it still matched). Arrow movement, by contrast, operates\n // over the runnable subset.\n const matchCount = visible.length;\n const hasOptions = matchCount > 0;\n // The polite live region copy (spec §7 4.1.3 Status Messages): how many matches a query produced.\n // The COUNT rides the live region; the actionable empty prose is the visible empty <p> (announced\n // as text in its own right, spec §2 empty) — so the live region says \"No results.\" rather than\n // repeating the empty prose, which would make a screen reader hear the same sentence twice.\n const statusMessage = loading\n ? \"Loading results.\"\n : matchCount === 0\n ? \"No results.\"\n : `${matchCount} result${matchCount === 1 ? \"\" : \"s\"}.`;\n\n const groups = groupItems(visible);\n\n // Restore focus to the opener when the palette closes (spec §6/§7). Radix's FocusScope would\n // restore to its own Trigger, but a palette is opened from a host shortcut with no single Trigger,\n // so we take over the close-focus seam: onCloseAutoFocus is the Radix-blessed hook (mirrors\n // onOpenAutoFocus) — preventDefault its default restoration and focus the captured opener instead,\n // so a keyboard user lands back where they were, never at the page top.\n const handleCloseAutoFocus = (event: Event) => {\n const opener = openerRef.current;\n openerRef.current = null;\n if (opener?.isConnected) {\n event.preventDefault();\n opener.focus();\n }\n };\n\n return (\n <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay\n data-testid=\"command-palette-scrim\"\n className={commandPaletteScrimClass}\n />\n <DialogPrimitive.Content\n // The spec §7 ARIA contract names aria-modal=\"true\" explicitly. This Radix version makes\n // the rest of the page inert (pointer-events:none on body + aria-hidden on siblings) but\n // does not emit aria-modal, so we set it to honor the frozen contract literally; the panel\n // IS modal (the focus trap + inert siblings back the claim).\n aria-modal=\"true\"\n // Name the panel by the input's purpose without a visible heading (the input owns the\n // visible affordance). The input is the operable combobox; the panel is just its shell.\n aria-label={inputLabel}\n className={cn(commandPalettePanelClass, className)}\n // On open, focus moves to the input (spec §4 Focus / §7) rather than Radix's default first\n // focusable, so type-ahead is live immediately and DOM focus is in the combobox. This fires\n // while DOM focus is still on the OPENER (before Radix moves it), so capture it here for the\n // close round-trip — the host owns the open shortcut, so there is no single Trigger for\n // Radix to restore to (spec §6/§7).\n onOpenAutoFocus={(event) => {\n event.preventDefault();\n openerRef.current = (document.activeElement as HTMLElement) ?? null;\n inputRef.current?.focus();\n }}\n // First Escape with a query clears the query; the next closes (spec §6).\n onEscapeKeyDown={handleEscapeKeyDown}\n // On close, return focus to the captured opener (spec §6/§7), not Radix's default.\n onCloseAutoFocus={handleCloseAutoFocus}\n >\n <div className=\"relative\">\n <span className={commandPaletteSearchIconClass}>\n <SearchGlyph />\n </span>\n <input\n ref={inputRef}\n id={inputId}\n type=\"text\"\n role=\"combobox\"\n // The combobox contract (spec §7): expanded while results are shown, controlling the\n // listbox, with a listbox popup. aria-activedescendant points at the active option and\n // moves with the arrows so DOM focus never leaves the input (spec §4 Focus / §8).\n aria-expanded={hasOptions}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n aria-activedescendant={activeId ? optionDomId(activeId) : undefined}\n // The input has no visible label — it names itself (the placeholder is never the name).\n aria-label={inputLabel}\n aria-describedby={statusId}\n autoComplete=\"off\"\n spellCheck={false}\n placeholder={placeholder}\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onKeyDown={handleInputKeyDown}\n className={commandPaletteInputClass}\n />\n </div>\n\n <div\n id={listboxId}\n // The results region is role=listbox ONLY while it holds options (spec §7): a listbox\n // requires option children (axe aria-required-children), so the empty/loading region is a\n // plain region — aria-expanded=false on the input already signals there is no popup, and\n // the empty prompt + the polite live region announce the state (spec §7 4.1.3).\n role={hasOptions ? \"listbox\" : undefined}\n aria-label={hasOptions ? inputLabel : undefined}\n // While results resolve the listbox is busy; the input stays focused/typeable (spec §4).\n aria-busy={loading || undefined}\n className={commandPaletteListboxClass}\n >\n {!hasOptions && !loading ? (\n // The empty state is announced as TEXT, never silence (spec §2 empty, §7 4.1.3).\n <p className={commandPaletteEmptyClass}>{emptyText}</p>\n ) : (\n groups.map((group, groupIndex) => {\n const groupLabelId = `${reactId}-group-${groupIndex}`;\n const rows = group.rows.map((item) => {\n const isActive = item.id === activeId;\n return (\n <div\n key={item.id}\n id={optionDomId(item.id)}\n role=\"option\"\n // The active option is aria-selected=true and the rest false (spec §7); the\n // active row is the SAME for pointer and keyboard (spec §4 Hover).\n aria-selected={isActive}\n aria-disabled={item.disabled || undefined}\n className={cn(commandPaletteOptionVariants())}\n // Pointer hover makes the row the active row, so Enter runs what is highlighted\n // (spec §4 Hover / §6). Disabled rows are not activatable.\n onMouseMove={() => {\n if (!item.disabled) setActiveId(item.id);\n }}\n onClick={() => runItem(item)}\n >\n {item.icon ? (\n <span aria-hidden=\"true\" className={commandPaletteOptionIconClass}>\n {item.icon}\n </span>\n ) : null}\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className={commandPaletteLabelClass}>{item.label}</span>\n {item.secondary ? (\n <span className={commandPaletteSecondaryClass}>{item.secondary}</span>\n ) : null}\n </span>\n {item.shortcut ? (\n <span aria-hidden=\"true\" className={commandPaletteShortcutClass}>\n {item.shortcut}\n </span>\n ) : null}\n </div>\n );\n });\n if (!group.group) {\n return <React.Fragment key={groupIndex}>{rows}</React.Fragment>;\n }\n return (\n <div key={groupIndex} role=\"group\" aria-labelledby={groupLabelId}>\n <p id={groupLabelId} className={commandPaletteGroupLabelClass}>\n {group.group}\n </p>\n {rows}\n </div>\n );\n })\n )}\n </div>\n\n {footer ? <div className={commandPaletteFooterClass}>{footer}</div> : null}\n\n {/* The polite live region (spec §7 4.1.3): the result count / empty prompt, announced as\n text. Visually hidden but in the accessibility tree; it backs the input's\n aria-describedby. */}\n <span\n id={statusId}\n role=\"status\"\n aria-live=\"polite\"\n className=\"sr-only\"\n >\n {statusMessage}\n </span>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n </DialogPrimitive.Root>\n );\n}\n\n// PageUp/PageDown move the active row by a viewport of rows (spec §6). Without measured row heights\n// in the headless layer, a fixed page step is the conformant approximation; the browser layer can\n// refine it against real row metrics later.\nconst PAGE_STEP = 8;\n",
10
+ "path": "command-palette/command-palette.tsx",
11
+ "target": "@ui/command-palette/command-palette.tsx",
12
+ "type": "registry:ui"
13
+ },
14
+ {
15
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// The command palette is a NEUTRAL overlay surface (spec §1/§3/§5/§8): brand violet and Verified\n// Green are accents, neutrals carry the surface. The SCRIM, the PANEL, the INPUT, and the RESULT\n// ROWS are neutral; the active-row highlight is the SECONDARY hover fill, NEVER a status or brand\n// color — the active row reports WHERE you are in the list, not a verified result, so it never\n// takes Verified Green, and Sovereign Violet never marks a row (brand != state, G-U2). A status\n// color appears only when a result row legitimately carries a status (a verified entity in its\n// subtitle), and then it is the --color-status-* accent on that row's OWN affordance, never a flood\n// of the palette — that affordance is the caller's (VerifiedBadge), not bound here. So NOTHING in\n// this file binds an --color-action-primary-* or --color-status-* fill. This is the ONLY\n// token-binding site (skill §5 hard rule).\n\n// The scrim (spec §2 scrim, §5 --color-scrim-*): the dimming layer behind the panel that separates\n// the palette from the page and absorbs outside clicks. A neutral dim on the modal z-layer,\n// decorative (no role). The fade is a PLAIN base transition + verdify easing, instant under reduced\n// motion — never the 350ms VerifiedBadge-only theatre (G-U3). Enter/exit ride Radix's data-state on\n// the overlay (attribute-selector variants, not arbitrary values). On a light surface the dark\n// scrim token applies (spec §5: scrim-dark behind the panel on a light surface; the committed\n// overlays bind scrim-dark with no auto light-surface inversion).\nexport const commandPaletteScrimClass =\n \"fixed inset-0 z-(--z-index-modal) bg-scrim-dark \" +\n \"transition-opacity duration-(--motion-duration-base) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0\";\n\n// The panel (spec §2 panel, §5): the raised container holding the input and results; it takes\n// role=dialog + the focus trap (Radix). A NEUTRAL raised surface (--color-surface-raised) with the\n// outer surface border, the lg corner radius, the lg elevation shadow above the scrim, anchored near\n// the top of the modal z-layer. It never exceeds the viewport — the listbox owns the scroll. The\n// open/close transition is the BASE duration + verdify easing, instant under reduced motion, and\n// rides Radix's data-state (attribute-selector enter/exit, not arbitrary values). NEVER the\n// deliberate verified-check theatre (G-U3). Panel padding/gaps come from --space-*.\nexport const commandPalettePanelClass =\n // Horizontally centered, anchored in the UPPER region of the viewport (a palette sits high, not\n // dead-center like a Dialog): top-1/4 is a Tailwind FRACTION utility (no arbitrary raw value), so\n // it stays clean against the token-binding gate while keeping the panel near the top.\n \"fixed left-1/2 top-1/4 z-(--z-index-modal) -translate-x-1/2 \" +\n // never exceeds the viewport — the listbox owns the scroll. calc()-bodied brackets are structural\n // (not raw-value-leading), gate-legitimate, and mirror the Dialog panel's viewport cap.\n \"flex w-[calc(100%-var(--space-8))] max-w-(--container-md) flex-col gap-(--space-3) \" +\n \"max-h-[calc(75dvh)] \" +\n \"bg-surface-raised border border-surface-border rounded-(--radius-lg) shadow-(--shadow-lg) \" +\n \"p-(--space-3) \" +\n \"transition-[opacity,transform] duration-(--motion-duration-base) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"data-[state=open]:opacity-100 data-[state=closed]:opacity-0 \" +\n \"data-[state=open]:scale-100 data-[state=closed]:scale-95 \" +\n // the panel owns no focus stop itself (focus lives in the input); its ring is never removed\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\";\n\n// The input (spec §2 input, §4 Focus, §5): the single-line search field at the top, focused on\n// open. It IS the combobox. A control-tier field — control bg/border/fg + the placeholder token\n// (spec §5 --color-control-*). The query text is the body type role in the control fg. While focused\n// the input border takes the focus color and the visible 2px focus ring shows (never removed, spec\n// §4 Focus). The target-size floor (44px touch / 40px pointer, spec §7 2.5.8 / DEC-B) with the\n// height EMERGING from the floor, never fixed below it. The body metrics ride along\n// (leading/tracking) without binding the text-body SIZE — DEC-A: a form field's value size stays\n// text-base (the iOS no-zoom floor), the body type role contributes only its leading + tracking.\nexport const commandPaletteInputClass =\n \"w-full rounded-(--radius-md) ps-(--space-9) pe-(--space-3) \" +\n \"bg-control-bg text-control-fg border border-control-border \" +\n \"placeholder:text-control-placeholder \" +\n // DEC-A: form-field value size is text-base (iOS no-zoom floor); body type role rides via metrics\n \"text-base leading-(--text-body--line-height) tracking-(--text-body--letter-spacing) \" +\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) \" +\n // the input border takes the focus color while focused (spec §5 --color-border-focus)\n \"focus-visible:border-border-focus \" +\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\";\n\n// The leading search icon (spec §2 input, §5 --size-icon-md): decorative (aria-hidden) — the input\n// names itself by its aria-label, not the glyph. Positioned at the inline-start of the input via a\n// logical offset so it mirrors under RTL (G-U6); inherits the placeholder color via currentColor.\nexport const commandPaletteSearchIconClass =\n \"pointer-events-none absolute start-(--space-3) top-1/2 -translate-y-1/2 \" +\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) items-center justify-center text-control-placeholder\";\n\n// The listbox (spec §2 listbox, §5): the results region below the input. A neutral raised surface\n// region with the input-to-results divider above it (a neutral hairline in the default border\n// color), scrolling within the panel when results overflow. Logical inline padding so it mirrors\n// under RTL (G-U6). No motion of its own — a wait is a plain wait (spec §4 Loading), the busy state\n// rides aria-busy, not theatre.\nexport const commandPaletteListboxClass =\n \"min-h-0 flex-1 overflow-y-auto border-t border-border-default pt-(--space-2)\";\n\n// The group label (spec §2 group-label, §5): a non-selectable heading that partitions results by\n// kind; it is NOT a result and is skipped by arrow movement. The MUTED text color at the LABEL type\n// role (spec §5 --color-text-muted / --text-label). Logical inline padding (G-U6).\nexport const commandPaletteGroupLabelClass =\n \"px-(--space-2) py-(--space-1) text-label text-text-muted select-none\";\n\n// One result row (spec §2 option, §4 states). A neutral row at rest; the active (highlighted) AND\n// the pointer-hovered row share ONE state — aria-selected — painted with the SECONDARY hover fill\n// (spec §4 Hover, §5 --color-action-secondary-bg-hover). Pointer hover and keyboard active are kept\n// in sync by setting aria-selected on whichever the user touched, so Enter always runs what is\n// highlighted. The active row reports WHERE you are, NOT a verified result — never Verified Green,\n// never the brand (spec §3/§8, G-U2). The label is the PRIMARY text color at the BODY type role\n// (spec §5 --color-text-primary / --text-body).\n//\n// FOCUS (spec §4 Focus): DOM focus stays in the INPUT the whole time; the active row is conveyed by\n// aria-activedescendant, not by moving focus into the list (spec §8 Don't). So a row does NOT paint\n// its own focus-visible ring — the active fill is the affordance, and the visible focus ring lives\n// on the input.\n//\n// DISABLED (spec §4 Disabled): a non-runnable row dims via the disabled TOKEN (DEC-C), never a\n// blanket opacity; it is aria-disabled, skipped by arrow movement (handled in the roving logic), and\n// not activatable. Its short reason sits in the secondary line.\nexport const commandPaletteOptionVariants = cva(\n [\n // shape + the icon-to-label gap + logical inline padding so it mirrors under RTL (G-U6)\n \"relative flex items-center gap-(--space-3) rounded-(--radius-md) px-(--space-2)\",\n // the resting (neutral) label color; pointer cursor; no underline\n \"text-text-primary cursor-pointer select-none\",\n // the shared pointer+keyboard highlight: the secondary hover fill on the ACTIVE row\n // (aria-selected). NEVER a status/brand fill — the active row is location, not a verified result.\n \"aria-selected:bg-action-secondary-bg-hover\",\n // target-size floor — 44px touch / 40px pointer, on every row (spec §7 2.5.8), never a fixed\n // height below the floor\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // focus stays in the input (spec §4 Focus / §8) — the row never paints its own focus ring\n \"outline-none\",\n // disabled (non-runnable) row — DEC-C: dim via the disabled TOKEN, never opacity; not operable\n \"aria-disabled:text-text-disabled aria-disabled:pointer-events-none aria-disabled:cursor-default\",\n ],\n {\n variants: {\n // The result label/secondary type role. There is one row treatment; the axis only exists to\n // keep the binding file shaped like the other compounds. md is the only value the spec needs.\n density: {\n md: \"py-(--space-2)\",\n },\n },\n defaultVariants: { density: \"md\" },\n },\n);\n\nexport type CommandPaletteOptionVariantProps = VariantProps<typeof commandPaletteOptionVariants>;\n\n// The result label (spec §2 option, §5): the primary line, body type role in the primary text color,\n// truncating when long so the row keeps its shape (--color-text-primary / --text-body).\nexport const commandPaletteLabelClass = \"min-w-0 flex-1 truncate text-body text-text-primary\";\n\n// The result secondary line (spec §2 option, §5): the optional second line under/after the label, in\n// the SECONDARY text color (spec §5 --color-text-secondary). Truncates when long.\nexport const commandPaletteSecondaryClass = \"truncate text-label text-text-secondary\";\n\n// The leading result icon (spec §2 option, §5 --size-icon-md): decorative — the row names itself by\n// its label text, not the glyph. Inherits the row color via currentColor; never collapses.\nexport const commandPaletteOptionIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center\";\n\n// The trailing keyboard-shortcut hint (spec §2 option, §5): pushed to the inline-end, in the MUTED\n// text color at the LABEL type role (spec §5 --color-text-muted / --text-label). Decorative\n// wayfinding, never a focus stop; logical inline-end placement (G-U6). The monospace ID/shortcut\n// stays isolated LTR inside RTL text (G-U6).\nexport const commandPaletteShortcutClass =\n \"ms-auto ps-(--space-4) text-label text-text-muted\";\n\n// The empty (no-match) state (spec §2 empty, §4): plain text that says nothing matched and what to\n// try — never a dead end, never a blamed query (principles voice). The SECONDARY text color at the\n// body type role, with breathing room (spec §5 --color-text-secondary). Announced as text, not\n// silence (spec §7 4.1.3).\nexport const commandPaletteEmptyClass =\n \"px-(--space-2) py-(--space-6) text-center text-body text-text-secondary\";\n\n// The footer (spec §2 footer, §5): a thin hint row showing the active keys (move, run, dismiss) so\n// the keyboard model is discoverable in place. The SECONDARY text color at the LABEL type role with\n// a neutral hairline divider above it (spec §5 --color-text-secondary / --text-label /\n// --color-border-default). Logical inline layout (G-U6).\nexport const commandPaletteFooterClass =\n \"flex items-center gap-(--space-4) border-t border-border-default pt-(--space-2) \" +\n \"text-label text-text-secondary\";\n",
16
+ "path": "command-palette/command-palette.variants.ts",
17
+ "target": "@ui/command-palette/command-palette.variants.ts",
18
+ "type": "registry:ui"
19
+ },
20
+ {
21
+ "content": "export {\n CommandPalette,\n type CommandPaletteProps,\n type CommandPaletteItem,\n type CommandPaletteVariant,\n} from \"./command-palette\";\nexport {\n commandPaletteScrimClass,\n commandPalettePanelClass,\n commandPaletteInputClass,\n commandPaletteSearchIconClass,\n commandPaletteListboxClass,\n commandPaletteGroupLabelClass,\n commandPaletteOptionVariants,\n commandPaletteLabelClass,\n commandPaletteSecondaryClass,\n commandPaletteOptionIconClass,\n commandPaletteShortcutClass,\n commandPaletteEmptyClass,\n commandPaletteFooterClass,\n type CommandPaletteOptionVariantProps,\n} from \"./command-palette.variants\";\n",
22
+ "path": "command-palette/index.ts",
23
+ "target": "@ui/command-palette/index.ts",
24
+ "type": "registry:ui"
25
+ }
26
+ ],
27
+ "name": "command-palette",
28
+ "registryDependencies": [
29
+ "@verdify/cn"
30
+ ],
31
+ "title": "command-palette",
32
+ "type": "registry:ui"
33
+ }
@@ -0,0 +1,34 @@
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": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { AgentBadge } from \"@/components/ui/agent-badge\";\nimport {\n consentToggleVariants,\n consentToggleRecipientClass,\n consentToggleDetailClass,\n consentToggleEvidenceClass,\n consentToggleFailureClass,\n type ConsentToggleVariantProps,\n} from \"./consent-toggle.variants\";\n\n/** The presentation form of the ConsentToggle (spec §3). Defaults to `grant`. */\nexport type ConsentToggleVariant = NonNullable<ConsentToggleVariantProps[\"variant\"]>;\n\nexport interface ConsentToggleProps\n extends Pick<ConsentToggleVariantProps, \"variant\"> {\n /**\n * The visible statement of WHAT is being consented to (spec §2), written as a plain\n * sentence ending with a period. It is the control's accessible name — the toggle is\n * never unlabeled and never a bare on/off with the meaning left implicit. Required.\n */\n scope: string;\n /**\n * WHO receives or acts on the data — an app, a partner, or an AI agent (spec §2).\n * \"To whom\" is not optional; a consent without a named recipient is incomplete. It is\n * wired to the control via aria-describedby, so a screen reader announces both what is\n * being consented to AND to whom — never a bare on/off with the meaning missing. In the\n * `agent-scoped` variant it is rendered AS an agent (an AgentBadge) so the actor knows\n * they are granting to an agent, not a human. Required.\n */\n recipient: React.ReactNode;\n /**\n * One optional line under the scope clarifying the bound or the duration of the grant —\n * for example, what stops when consent is withdrawn (spec §2) — written as a statement.\n * Announced with the recipient via aria-describedby.\n */\n detail?: React.ReactNode;\n /**\n * The optional evidence affordance (spec §2/§3) — a link or control to the PROOF of the\n * grant: what was consented to, to whom, and when, so the act is reviewable later. Pass\n * the interactive element itself (a link or Button); it is its OWN focus stop with its\n * own keyboard model and focus ring, never folded into the busy control. Surfaces a\n * proof of the consent event, not a stored document. Pairs with `variant=\"with-evidence\"`.\n */\n evidence?: React.ReactNode;\n /**\n * The plain message shown next to the control on a FAILED grant or withdrawal (spec\n * §4/§7), naming what failed and what to do next without blaming the reader. It is\n * announced through an assertive live region (4.1.3) and rendered in the critical status\n * color — never a silent revert, and never a grant the actor did not confirm.\n */\n failureMessage?: React.ReactNode;\n /**\n * The controlled granted value (spec §4). `true` is granted, `false` is not granted.\n * Omit for the uncontrolled pattern. Consent is never pre-granted, so the control rests\n * NOT granted until the actor turns it on.\n */\n checked?: boolean;\n /**\n * The uncontrolled initial granted value (spec §4). Ignored when `checked` is provided.\n * Defaults to `false` — a ConsentToggle never renders granted by default.\n */\n defaultChecked?: boolean;\n /** Fires with the next granted value on every toggle — granting OR withdrawing (spec §6). */\n onCheckedChange?: (granted: boolean) => void;\n /**\n * The grant or withdrawal awaits an async result (spec §4): sets aria-busy and blocks\n * further input until it resolves. Withdrawal is treated exactly like granting.\n */\n loading?: boolean;\n /**\n * A consent the actor cannot change is shown disabled (spec §4) — reduced emphasis, no\n * pointer events — and the current granted / not-granted value still reads to assistive\n * technology. A consent is never silently locked in the granted position.\n */\n disabled?: boolean;\n /** Visible track size, forwarded to the Switch. Defaults to `md`. */\n size?: \"sm\" | \"md\";\n /** Extra classes for the root layout column. */\n className?: string;\n}\n\n/**\n * ConsentToggle is the explicit, revocable affordance by which a person or an AI-agent\n * actor grants or withdraws permission for one specific use of their data or identity\n * (spec §1). It names what is being consented to AND who the data is shared with, in plain\n * words, before any grant exists. It encodes ACCOUNTABLE BY DESIGN: consent is a deliberate\n * act the actor takes, never a default the product assumes, and it can be withdrawn as\n * easily as it was given (spec §1/§4/§6).\n *\n * It COMPOSES the committed Switch as its control rather than reinventing a track and thumb\n * — the granted axis maps to the Switch's `on`, so the role=switch, the Space/Enter\n * keyboard model, the aria-checked granted/not-granted value, the visible focus ring, the\n * target-size floor, and the fast functional slide-and-tint motion all come from the proven\n * primitive. ConsentToggle adds the scope (the control's accessible name, via the Switch's\n * visible label), the named recipient and optional detail (rendered as its own block and\n * wired to the control via aria-describedby so a screen reader announces what AND to whom),\n * an optional evidence affordance to review the recorded grant, and a failure message for a\n * failed grant or withdrawal.\n *\n * It keeps brand and state apart (spec §4/§5/§8, brand != state): the granted state takes\n * the BRAND action accent (via the Switch), never the verified-status green — a grant\n * reports permission, not a verification result, so coloring a grant \"verified\" both breaks\n * the invariant and dilutes the meaning of verified. The only status color it binds is the\n * critical foreground on the failure message, never on the granted state. The motion is the\n * Switch's fast functional slide/tint, never the 350ms VerifiedBadge-only theatre.\n *\n * It rests NOT granted by default: consent is never pre-granted, so the control never\n * renders granted until the actor turns it on, and the scope and recipient read at rest so\n * the actor knows what they are about to grant, and to whom, before they grant it (spec\n * §4/§8). Granting and withdrawing are the same gesture; withdrawal is never harder to\n * reach than granting (spec §6).\n *\n * It is `'use client'` for the `React.useId()` calls that mint the recipient/detail ids it\n * wires to the control (any hook makes the file a client component); the composed Switch\n * owns its own state and `'use client'` independently.\n */\nexport const ConsentToggle = React.forwardRef<HTMLButtonElement, ConsentToggleProps>(\n function ConsentToggle(\n {\n variant = \"grant\",\n scope,\n recipient,\n detail,\n evidence,\n failureMessage,\n checked,\n defaultChecked,\n onCheckedChange,\n loading,\n disabled,\n size,\n className,\n },\n ref,\n ) {\n const reactId = React.useId();\n const recipientId = `${reactId}-recipient`;\n const detailId = detail != null ? `${reactId}-detail` : undefined;\n const isAgent = variant === \"agent-scoped\";\n\n // The recipient (and optional detail) are ConsentToggle's OWN elements with stable ids,\n // wired to the control via aria-describedby (spec §2/§7) — so a screen reader announces\n // \"to whom\" alongside the scope, never a bare on/off with the meaning missing. The ids\n // are space-joined (recipient first), the aria-describedby convention for multiple\n // targets. This OVERRIDES the Switch's own internal aria-describedby (a caller-passed\n // aria-describedby lands last in the Switch's prop spread), so the Switch is composed,\n // not modified — its description slot is left unused here.\n const describedBy = detailId ? `${recipientId} ${detailId}` : recipientId;\n\n // In the agent-scoped variant the recipient is named AS an agent: an AgentBadge marks\n // the actor-kind so the human/agent distinction is never the silent default; the\n // recipient text still reaches AT inside the badge.\n const recipientNode = isAgent ? (\n <AgentBadge\n id={recipientId}\n data-testid=\"consent-toggle-agent-recipient\"\n >\n {recipient}\n </AgentBadge>\n ) : (\n <span id={recipientId} className={consentToggleRecipientClass}>\n {recipient}\n </span>\n );\n\n return (\n <div className={cn(consentToggleVariants({ variant }), className)}>\n {/* the control row: the composed Switch. `scope` is its visible label AND its\n accessible name (the Switch's aria-labelledby); the recipient + detail are wired\n as the accessible DESCRIPTION via the overriding aria-describedby. It rests NOT\n granted unless the actor (or a controlled `checked`) turns it on — consent is\n never pre-granted (spec §4/§8). */}\n <Switch\n ref={ref}\n label={scope}\n aria-describedby={describedBy}\n checked={checked}\n defaultChecked={defaultChecked}\n onCheckedChange={onCheckedChange}\n loading={loading}\n disabled={disabled}\n size={size}\n />\n\n {/* the recipient + optional detail (spec §2): WHO receives the data, and the\n optional bound of the grant. Visible at rest so the actor reads to whom and what\n stops before they act; referenced by the control's aria-describedby above. */}\n {recipientNode}\n {detail != null ? (\n <span id={detailId} className={consentToggleDetailClass}>\n {detail}\n </span>\n ) : null}\n\n {/* the optional evidence affordance (spec §2/§3): the caller's own link/Button to\n the proof of the grant. It is a separate focus stop with its own keyboard model\n and focus ring — never folded into the busy control. */}\n {evidence != null ? (\n <span className={consentToggleEvidenceClass}>{evidence}</span>\n ) : null}\n\n {/* the failure message on a failed grant or withdrawal (spec §4/§7): stated next to\n the control, in the critical color, through an ASSERTIVE live region (role=alert,\n 4.1.3) so it is announced — never a silent revert. It names what failed and what\n to do next without blaming the reader. */}\n {failureMessage != null ? (\n <span role=\"alert\" className={consentToggleFailureClass}>\n {failureMessage}\n </span>\n ) : null}\n </div>\n );\n },\n);\n",
9
+ "path": "consent-toggle/consent-toggle.tsx",
10
+ "target": "@ui/consent-toggle/consent-toggle.tsx",
11
+ "type": "registry:ui"
12
+ },
13
+ {
14
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// ConsentToggle is the explicit, revocable affordance by which an actor grants or\n// withdraws permission for one specific use of their data or identity (spec §1). It\n// COMPOSES the committed Switch as its control — the granted axis maps to the Switch's\n// `on` — and adds the scope, the named recipient, the optional detail, the optional\n// evidence affordance, and the failure message AROUND it. So the track / thumb / focus\n// ring / target-size floor / slide-and-tint motion are bound ONCE in switch.variants.ts\n// and are NOT re-bound here; this file binds only the surrounding text + layout roles.\n//\n// brand != state (spec §4/§5/§8). The granted track takes the primary ACTION accent\n// (the Switch's `aria-checked:bg-action-primary-bg`), NEVER --color-status-verified-*:\n// verified green is the in-product verified status, never a consent affordance, and a\n// grant reports permission, not a verification result. ConsentToggle therefore binds\n// NOTHING from the status tier on its granted state — the one status color it ever\n// reaches for is --color-status-critical-fg, and ONLY on the failure message (a stated\n// failed grant/withdrawal), never on the granted/checked state. This component-scoped\n// invariant (no status color on the grant; verified green never a consent affordance) is\n// pinned as a test in consent-toggle.test.tsx — the action accent IS legitimate here, so\n// the negative forbids only the STATUS tier, per the build-on-brand skill's scoping note.\n//\n// The motion is the Switch's FAST functional slide/tint on verdify easing, collapsing to\n// the instant endpoint under reduced motion — never the 350ms VerifiedBadge-only theatre\n// duration: a consent grant is not a verification (G-U3).\n\n// The root layout (spec §2): a column stacking the control row, the optional evidence\n// affordance, and the optional failure message at the --space-2 gap. The `variant` axis\n// (spec §3) is a form of how the consent is PRESENTED — a plain grant, a grant with the\n// evidence affordance, or a grant scoped to an AI agent — never a level of color or alarm,\n// so NONE of the variants recolors anything: the granted accent and every text role are\n// identical across all three. Non-interactive container: the focus ring, the keyboard\n// model, and the target-size floor all live on the composed Switch control, not here.\nexport const consentToggleVariants = cva(\"flex flex-col gap-(--space-2)\", {\n variants: {\n // STRUCTURAL axis = spec §3. Display/composition forms, never alarm levels.\n variant: {\n // grant (default): a single permission the actor turns on to allow, off to withhold.\n grant: \"\",\n // with-evidence: adds the evidence affordance so the actor can review the recorded\n // grant — what, to whom, when. Use wherever the grant is consequential.\n \"with-evidence\": \"\",\n // agent-scoped: the recipient is an AI-agent actor, named AS an agent (AgentBadge),\n // so the actor knows they are granting to an agent, not a human.\n \"agent-scoped\": \"\",\n },\n },\n defaultVariants: { variant: \"grant\" },\n});\n\n// The recipient + detail block (spec §2/§5): who receives the data and the optional bound\n// of the grant, in the SECONDARY text color at the --text-body type role. It is composed\n// into the control's accessible description (aria-describedby), so a screen reader\n// announces \"to whom\" alongside the scope — never a bare on/off with the meaning missing.\nexport const consentToggleRecipientClass = \"text-body text-text-secondary\";\n\n// The optional detail line (spec §2/§5): one statement clarifying the bound or duration of\n// the grant (for example, what stops when consent is withdrawn), under the recipient, in\n// the secondary text color at the body role.\nexport const consentToggleDetailClass = \"text-body text-text-secondary\";\n\n// The optional evidence affordance row (spec §2/§3): a slot holding the caller's link or\n// Button to the PROOF of the grant — what was consented to, to whom, and when — so the act\n// is reviewable later. It is the caller's own focus stop with its own keyboard model and\n// focus ring (the evidence control is not folded into the busy switch); this row only\n// positions it. Surfaces a proof of the consent event, not a stored document.\nexport const consentToggleEvidenceClass = \"flex items-center text-body\";\n\n// The failure message (spec §4/§5/§7): shown next to the control on a FAILED grant or\n// withdrawal, naming what failed and what to do next without blaming the reader, in the\n// CRITICAL status foreground at the --text-body role. It is the ONLY status color the\n// component binds, and only here — never on the granted state (brand != state). It is\n// announced through an assertive live region (role=alert), set in the tsx, per 4.1.3.\nexport const consentToggleFailureClass = \"text-body text-status-critical-fg\";\n\nexport type ConsentToggleVariantProps = VariantProps<typeof consentToggleVariants>;\n",
15
+ "path": "consent-toggle/consent-toggle.variants.ts",
16
+ "target": "@ui/consent-toggle/consent-toggle.variants.ts",
17
+ "type": "registry:ui"
18
+ },
19
+ {
20
+ "content": "export {\n ConsentToggle,\n type ConsentToggleProps,\n type ConsentToggleVariant,\n} from \"./consent-toggle\";\nexport {\n consentToggleVariants,\n consentToggleRecipientClass,\n consentToggleDetailClass,\n consentToggleEvidenceClass,\n consentToggleFailureClass,\n type ConsentToggleVariantProps,\n} from \"./consent-toggle.variants\";\n",
21
+ "path": "consent-toggle/index.ts",
22
+ "target": "@ui/consent-toggle/index.ts",
23
+ "type": "registry:ui"
24
+ }
25
+ ],
26
+ "name": "consent-toggle",
27
+ "registryDependencies": [
28
+ "@verdify/cn",
29
+ "@verdify/agent-badge",
30
+ "@verdify/switch"
31
+ ],
32
+ "title": "consent-toggle",
33
+ "type": "registry:ui"
34
+ }
@@ -0,0 +1,35 @@
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": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n credentialCardVariants,\n credentialCardIconClass,\n credentialCardBodyClass,\n credentialCardLabelClass,\n credentialCardIdentifierClass,\n credentialCardStatusClass,\n credentialCardMetaClass,\n credentialCardControlsClass,\n credentialCardReasonClass,\n credentialCardErrorClass,\n type CredentialCardVariantProps,\n} from \"./credential-card.variants\";\n\n/** The credential kind a CredentialCard represents (spec §3). */\nexport type CredentialKind = NonNullable<CredentialCardVariantProps[\"kind\"]>;\n\n/**\n * One small fact shown about the credential (spec §2 `status`). `verified` is the green status\n * Badge (a proof about THIS credential, never the identity); `primary` is the credential you\n * currently sign in with — a NEUTRAL fact, not a status color and never the brand. The status\n * describes the credential, not the identity: a verified credential does not make the person\n * verified.\n */\nexport interface CredentialCardStatus {\n /** `verified` (the green status) or `primary` (the current sign-in credential). */\n kind: \"verified\" | \"primary\";\n /** The Badge's visible label and accessible name, a plain word (\"Verified\", \"Primary\"). */\n label: React.ReactNode;\n}\n\nexport interface CredentialCardProps\n extends Omit<React.LiHTMLAttributes<HTMLLIElement>, \"title\">,\n CredentialCardVariantProps {\n /**\n * The credential kind this card represents (spec §3): `email`, `phone`, `passkey`, `wallet`,\n * or `enterprise-sso`. The kind is carried by the {@link icon} + {@link label} text, never by\n * color — every kind is the same neutral surface. Defaults to `email`, the common case.\n */\n kind?: CredentialKind;\n /**\n * The human-readable name of the credential KIND (spec §2 `label`), a statement in sentence\n * case — \"Email\", \"Passkey\", \"Wallet\". It says what kind of link this is, not who the identity\n * is. Required.\n */\n label: React.ReactNode;\n /**\n * The value that identifies this specific credential (spec §2 `identifier`): the email, the\n * masked phone, the passkey device name, the truncated wallet address, or the SSO provider and\n * domain. Rendered in the monospace type role and isolated left-to-right so addresses, hashes,\n * and wallet strings are never garbled, even inside RTL text. Selectable text so it can be\n * copied; that does not make the card a control. Required.\n */\n identifier: React.ReactNode;\n /**\n * A short, plain string used to NAME the credential in the control accessible names — the\n * `remove` button reads \"Remove {kindNoun} {identifierText}\" and the `selectable` checkbox\n * reads \"Select {kindNoun} {identifierText}\", so a screen-reader user is never asked to remove\n * or select an unnamed thing (spec §7). When omitted it is built from the visible {@link label}\n * and the {@link identifier} (when it is a plain string).\n */\n identifierText?: string;\n /**\n * The required kind glyph (spec §2 `icon`): a small decorative mark for the credential kind. It\n * reinforces the kind shown in the label and is rendered `aria-hidden` so the kind is announced\n * once, from the label, not twice. Its fill is a neutral role — never a status color, never the\n * brand.\n */\n icon?: React.ReactNode;\n /**\n * At most one or two small status Badges (spec §2 `status`): `verified` (the green status) or\n * `primary` (the current sign-in credential). The status describes the credential, not the\n * identity.\n */\n status?: CredentialCardStatus[];\n /**\n * Quiet secondary text (spec §2 `meta`) such as when the credential was added or last used.\n * De-emphasized; never an alarm color.\n */\n meta?: React.ReactNode;\n /**\n * Fires when the required `remove` control is activated (spec §2 `remove`/§8). Removing a\n * credential detaches a login method from the identity — it never deletes the person. In a real\n * app this opens the confirm step (spec §8); the card stays mounted and the identity intact. The\n * `remove` control is the card's primary focus stop. Omit only when the row is display-only.\n */\n onRemove?: () => void;\n /**\n * Disable `remove` (spec §4 Disabled) — for example on the last remaining sign-in credential,\n * because removing it would lock the identity out. Shown via `aria-disabled` and removed from\n * the tab order; the credential is still shown (the card itself is never \"disabled\").\n */\n removeDisabled?: boolean;\n /**\n * The reason `remove` is disabled (spec §4/§7), given as adjacent text and wired to the control\n * as its accessible description — so the reason is heard, not just seen, never communicated by\n * graying alone. Required reading whenever {@link removeDisabled} is set.\n */\n removeDisabledReason?: React.ReactNode;\n /**\n * A removal is resolving on the server (spec §4 Loading): `remove` shows a Spinner in place,\n * is `aria-busy`, and is not re-triggerable. A wait is a plain wait — never the deliberate\n * verified-check duration.\n */\n loading?: boolean;\n /**\n * The plain message shown when a removal FAILS (spec §4/§7): the credential stays in the list\n * and the failure is stated where it happened, naming what failed and what to do next — never\n * blaming the reader and never an exclamation mark. Announced through an assertive live region.\n */\n error?: React.ReactNode;\n /**\n * At most one further low-emphasis control beside `remove` (spec §2 `actions`) — for example\n * \"Make primary\" or \"Re-verify\". Pass the interactive element itself; it is its own focus stop\n * with its own keyboard model and focus ring. Keep the card to one primary action.\n */\n actions?: React.ReactNode;\n /**\n * Render a leading Checkbox for bulk actions on a managed list (spec §3 `selectable`). Composes\n * with any kind. The checkbox's accessible name is tied to the same credential.\n */\n selectable?: boolean;\n /** The controlled selected value for the `selectable` Checkbox (spec §3/§4). */\n selected?: boolean;\n /** Fires with the next selected value when the `selectable` Checkbox toggles (spec §6). */\n onSelectedChange?: (selected: boolean) => void;\n}\n\n// The kind → noun used in the control accessible names (\"Remove email …\", \"Select email …\").\nconst KIND_NOUN: Record<CredentialKind, string> = {\n email: \"email\",\n phone: \"phone\",\n passkey: \"passkey\",\n wallet: \"wallet\",\n \"enterprise-sso\": \"enterprise SSO\",\n};\n\n/**\n * A CredentialCard is one ROW in the list of credentials attached to an identity (spec §1) — a\n * single way to reach that identity, such as an email, a phone number, a passkey, a wallet, or an\n * enterprise single sign-on connection. It shows what the credential is, how to recognize it, and\n * lets you remove it. Each card is a LINK to an identity, NOT the identity itself: it is removable,\n * and removing it never deletes the person.\n *\n * It encodes the platform's first invariant — identity is not credentials — in its UI contract. A\n * credential is a means of signing in, so a CredentialCard never reads as \"you\" and never stands\n * in for the account: one identity holds many credentials the way it holds many profiles, and a\n * card is one of those means, not the whole. The card may show that a credential is verified (the\n * green status, never the brand) or that it is the one you currently sign in with, but neither\n * makes the card into the identity behind it (spec §1/§8).\n *\n * It COMPOSES the committed primitives — a Card-like neutral surface, the destructive Button for\n * `remove`, the secondary control for an optional `action`, the Badge for a `status`, and the\n * Checkbox for `selectable` — so the focus ring, the target-size floor, the keyboard model, and\n * the control motion all come from those proven primitives. The card surface itself is a static\n * `listitem` container, not a single clickable control: the whole row does not map to one action,\n * so the controls inside it each take focus in DOM order while the row takes none (spec §4/§6/§7).\n *\n * brand != state (spec §3/§5/§8): the card surface, the icon, and the identifier are NEUTRAL — the\n * brand violet never marks a credential as special, and the verified-status green is reserved for\n * the `verified` status Badge alone, never spent on the surface, the icon, or the identifier.\n * Coloring the card green would imply the IDENTITY is verified, the one misreport this molecule\n * forbids. The only status color it binds is the critical foreground on a removal-failure message.\n *\n * It is `'use client'` for the `React.useId()` calls that mint the ids wiring the disabled reason\n * and the failure region to the controls (any hook makes the file a client component); the\n * composed Button/Checkbox own their own state and `'use client'` independently.\n */\nexport const CredentialCard = React.forwardRef<HTMLLIElement, CredentialCardProps>(\n function CredentialCard(\n {\n className,\n kind = \"email\",\n label,\n identifier,\n identifierText,\n icon,\n status,\n meta,\n onRemove,\n removeDisabled = false,\n removeDisabledReason,\n loading = false,\n error,\n actions,\n selectable = false,\n selected,\n onSelectedChange,\n ...props\n },\n ref,\n ) {\n const reactId = React.useId();\n const reasonId = removeDisabledReason != null ? `${reactId}-reason` : undefined;\n const errorId = error != null ? `${reactId}-error` : undefined;\n\n // The credential's name used in the control accessible names (spec §7) — a screen-reader user\n // is never asked to remove or select an unnamed thing. Built from the kind noun + a plain text\n // form of the identifier (the caller-supplied identifierText, else the identifier when it is a\n // plain string), so \"Remove email jordan@example.com\" / \"Select email jordan@example.com\".\n const idText =\n identifierText ?? (typeof identifier === \"string\" ? identifier : \"\");\n const credentialName = `${KIND_NOUN[kind]} ${idText}`.trim();\n\n return (\n // The card is a listitem within the credential list's <ul role=\"list\"> (spec §7): no\n // interactive role of its own, not a focus stop, not a single control (spec §4/§6).\n <li\n ref={ref}\n className={cn(credentialCardVariants({ kind }), className)}\n {...props}\n >\n {/* selectable: a leading Checkbox for bulk actions (spec §3). Its accessible name is tied\n to the same credential. A checked box is the brand action accent (bound in the Checkbox\n primitive), never the verified-status green — selection is a neutral action state. */}\n {selectable ? (\n <Checkbox\n data-testid=\"credential-card-select\"\n label={`Select ${credentialName}`}\n checked={selected}\n onCheckedChange={onSelectedChange}\n />\n ) : null}\n\n {/* the kind glyph (spec §2): decorative (aria-hidden), neutral fill, md icon role — the\n label text carries the kind if the icon is dropped, so the kind never rests on the icon\n or color alone (spec §7). */}\n {icon != null ? (\n <span\n data-testid=\"credential-card-icon\"\n aria-hidden=\"true\"\n className={credentialCardIconClass}\n >\n {icon}\n </span>\n ) : null}\n\n {/* the label + identifier block (spec §2): what kind of link this is, then the value that\n identifies this specific credential, in the monospace role, isolated LTR. Both reach the\n accessibility tree as text (1.3.1) — never carried by the icon or a color alone. */}\n <span className={credentialCardBodyClass}>\n <span className={credentialCardLabelClass}>{label}</span>\n <span className={credentialCardIdentifierClass}>{identifier}</span>\n {meta != null ? <span className={credentialCardMetaClass}>{meta}</span> : null}\n {/* the disabled reason (spec §4/§7): adjacent muted text, wired to the remove control as\n its accessible description so the reason is heard, not just seen — never gray alone. */}\n {removeDisabledReason != null ? (\n <span id={reasonId} className={credentialCardReasonClass}>\n {removeDisabledReason}\n </span>\n ) : null}\n {/* the removal-failure message (spec §4/§7): the credential stays and the failure is\n stated where it happened, in the critical color, through an ASSERTIVE live region\n (role=alert, 4.1.3) — naming what failed and what to do next, never blaming, never an\n exclamation mark. */}\n {error != null ? (\n <span id={errorId} role=\"alert\" className={credentialCardErrorClass}>\n {error}\n </span>\n ) : null}\n </span>\n\n {/* the status Badges (spec §2): at most one or two facts about the credential. `verified`\n is the green status Badge (a proof about THIS credential, never the identity); `primary`\n is a NEUTRAL Badge — which credential is used, a fact, not a status color or the brand. */}\n {status != null && status.length > 0 ? (\n <span className={credentialCardStatusClass}>\n {status.map((s, i) =>\n s.kind === \"verified\" ? (\n <Badge key={i} status=\"verified\">\n {s.label}\n </Badge>\n ) : (\n <Badge key={i}>{s.label}</Badge>\n ),\n )}\n </span>\n ) : null}\n\n {/* the trailing controls (spec §2): the optional low-emphasis `action` beside the required\n destructive `remove`. The `remove` control names the credential it detaches (spec §7);\n detaching is consequential, so it takes the destructive action treatment (spec §2/§5).\n Loading shows the Button's in-place spinner + aria-busy and is not re-triggerable; the\n wait runs on the Button's ambient spinner, never the verified-check theatre (spec §4). */}\n <span className={credentialCardControlsClass}>\n {actions}\n {onRemove != null ? (\n <Button\n variant=\"destructive\"\n loading={loading}\n aria-label={`Remove ${credentialName}`}\n aria-disabled={removeDisabled || undefined}\n aria-describedby={\n [reasonId, errorId].filter(Boolean).join(\" \") || undefined\n }\n // a disabled remove stays NAMED and described (aria-disabled, not native disabled),\n // out of the tab order, and inert — never a silently dropped name (spec §4/§7). DEC-C:\n // it dims via the disabled text TOKEN, set here, never a blanket opacity.\n tabIndex={removeDisabled ? -1 : undefined}\n className={removeDisabled ? \"pointer-events-none text-text-disabled\" : undefined}\n onClick={removeDisabled || loading ? undefined : onRemove}\n >\n Remove\n </Button>\n ) : null}\n </span>\n </li>\n );\n },\n);\n",
9
+ "path": "credential-card/credential-card.tsx",
10
+ "target": "@ui/credential-card/credential-card.tsx",
11
+ "type": "registry:ui"
12
+ },
13
+ {
14
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// CredentialCard is one ROW in the list of credentials ATTACHED to an identity — a single way\n// to reach that identity (an email, phone, passkey, wallet, or enterprise SSO connection). It\n// encodes the platform's first invariant — identity is not credentials — in its UI contract: a\n// card is a LINK to an identity, never the identity itself, so it never reads as \"you\" and never\n// stands in for the account (spec §1/§8).\n//\n// It COMPOSES the committed primitives rather than reinventing them — a Card-like neutral surface\n// (the surface-* roles, bound here), the destructive Button for `remove`, the secondary Button\n// for an optional `action`, the Badge for a `status`, the Checkbox for `selectable`, and the\n// Button's in-place loading spinner for a resolving removal. So the focus ring, the target-size\n// floor, the keyboard model, and the control motion all come from those proven primitives; this\n// file binds ONLY the surrounding neutral surface + text-role + layout classes.\n//\n// brand != state (spec §3/§5/§8). The card SURFACE consumes NO action or status token of its own\n// — neutrals carry the card. The brand violet (Sovereign Violet, the action accent) is never a\n// card fill, never the icon, never the identifier, and never marks a credential as special. The\n// verified-status green is reserved for the `verified` status Badge and is never spent on the\n// card surface, the icon, or the identifier — coloring the card green would imply the IDENTITY is\n// verified, which is the one misreport this molecule forbids. The card therefore binds nothing\n// from the action tier and nothing from the status tier; those colors live on the controls and\n// Badges it holds (asserted positively in their own primitives' tests), never on this surface.\n//\n// The motion the card adds is none of its own: control hover/press uses the composed primitive's\n// fast functional transition on verdify easing, collapsing to the instant endpoint under reduced\n// motion. A resolving removal is a plain wait on the Button's ambient spinner — never the 350ms\n// VerifiedBadge-only theatre duration: a removal is not a verification (G-U3).\n\n// The card container (spec §2/§4/§5): a NEUTRAL raised surface, the same Card-static surface the\n// committed Card binds, but it is a LIST ROW (`<li role=\"listitem\">`, set in the tsx) so it sits\n// in the credential list's `<ul role=\"list\">` (spec §7). It is a static container, NOT a single\n// clickable control — the whole row does not map to one action — so it carries no focus ring and\n// no target-size floor of its own (those live on its controls, spec §4/§5). The `kind` axis is\n// which credential the card represents; it is carried by the `icon` + `label` TEXT, never by\n// color, so NONE of the kinds recolors the surface — every kind is the identical neutral surface\n// (spec §3). `selectable` composes with any kind and changes layout (a leading Checkbox), not\n// color, so it is not a color variant here.\nexport const credentialCardVariants = cva(\n [\n // raised neutral surface, rounded container, resting elevation, internal padding from --space-4\n \"relative flex items-start gap-(--space-2) rounded-lg border bg-surface-raised shadow-sm p-(--space-4)\",\n // the default container hairline (spec §5)\n \"border-surface-border\",\n // logical-property text alignment so the row mirrors under dir=rtl (G-U6); list rows carry\n // no bullet marker\n \"text-start text-text-primary list-none\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3 (the credential KIND the card represents). The kind is carried\n // by the icon + label text, never color, so every kind is the SAME neutral surface.\n kind: {\n email: \"\",\n phone: \"\",\n passkey: \"\",\n wallet: \"\",\n \"enterprise-sso\": \"\",\n },\n },\n defaultVariants: { kind: \"email\" },\n },\n);\n\n// The kind icon (spec §2/§5/§7): one small glyph for the credential kind at the md icon role. It\n// reinforces the kind shown in the label and is decorative (aria-hidden, set in the tsx) — the\n// label text still carries the kind if the icon is dropped, so the kind never rests on the icon\n// or color alone. Its fill is the SECONDARY text role (a neutral role), never a status color and\n// never the brand (spec §2/§3).\nexport const credentialCardIconClass =\n \"inline-flex h-(--size-icon-md) w-(--size-icon-md) shrink-0 items-center justify-center text-text-secondary\";\n\n// The label + identifier block (spec §2): the human-readable kind name above the value that\n// identifies this specific credential. Takes the remaining inline space between the icon and the\n// trailing status/controls; stacks the two lines at the --space-1 gap.\nexport const credentialCardBodyClass = \"flex min-w-0 flex-1 flex-col gap-(--space-1)\";\n\n// The label (spec §2/§5): the human-readable name of the credential KIND, a statement in\n// sentence case (for example \"Email\", \"Passkey\", \"Wallet\"), in the label type role + the PRIMARY\n// text color. The label says what kind of link this is, not who the identity is.\nexport const credentialCardLabelClass = \"text-label text-text-primary\";\n\n// The identifier (spec §2/§5/G-U6): the value that identifies this specific credential — the\n// email, the masked phone, the passkey device name, the truncated wallet address, or the SSO\n// provider + domain. It renders in the MONOSPACE type role (never the UI font for credential\n// strings) in the SECONDARY text color, isolated left-to-right so addresses, hashes, and wallet\n// strings stay readable even inside RTL text, and truncates rather than wrapping.\nexport const credentialCardIdentifierClass =\n \"text-mono text-text-secondary [direction:ltr] truncate\";\n\n// The status row (spec §2): at most one or two small Badges stating a fact about the credential —\n// `verified` (the green status, on the composed Badge's verified variant) or `primary` (the\n// credential you currently sign in with, a NEUTRAL Badge — \"primary\" is which credential is used,\n// a fact, not a status color and never the brand). The status describes the credential, not the\n// identity. This row only positions the composed Badges; their color lives in badge.variants.ts.\nexport const credentialCardStatusClass = \"flex shrink-0 flex-wrap items-center gap-(--space-1)\";\n\n// The meta line (spec §2/§5): quiet secondary text such as when the credential was added or last\n// used, in the MUTED text color at the caption role. De-emphasized; never an alarm color.\nexport const credentialCardMetaClass = \"text-caption text-text-muted\";\n\n// The trailing controls cluster (spec §2): the optional `action` beside the required `remove`,\n// aligned to the inline-end edge at the --space-2 gap. Keep the card to one primary action\n// (`remove`) plus at most one further low-emphasis control (restraint over volume). It only lays\n// the controls out; each control's treatment + focus ring + target-size floor live on the\n// composed Button.\nexport const credentialCardControlsClass = \"flex shrink-0 items-center gap-(--space-2)\";\n\n// The disabled-reason note (spec §4/§5/§7): when a control is disabled (for example `remove` on\n// the last sign-in credential), the reason is given as adjacent text in the MUTED text color at\n// the caption role — wired to the control as its accessible description (so the reason is heard,\n// not just seen), never communicated by graying alone.\nexport const credentialCardReasonClass = \"text-caption text-text-muted\";\n\n// The removal-failure message (spec §4/§7): on a FAILED removal the credential stays in the list\n// and the failure is stated plainly where it happened, in the CRITICAL status foreground at the\n// caption role — naming what failed and what to do next, never blaming the reader and never an\n// exclamation mark. It is the ONLY status color this component binds, and only here (never on the\n// card surface, brand != state). It is announced through an assertive live region (role=alert,\n// set in the tsx) per 4.1.3.\nexport const credentialCardErrorClass = \"text-caption text-status-critical-fg\";\n\nexport type CredentialCardVariantProps = VariantProps<typeof credentialCardVariants>;\n",
15
+ "path": "credential-card/credential-card.variants.ts",
16
+ "target": "@ui/credential-card/credential-card.variants.ts",
17
+ "type": "registry:ui"
18
+ },
19
+ {
20
+ "content": "export {\n CredentialCard,\n type CredentialCardProps,\n type CredentialKind,\n type CredentialCardStatus,\n} from \"./credential-card\";\nexport {\n credentialCardVariants,\n credentialCardIconClass,\n credentialCardBodyClass,\n credentialCardLabelClass,\n credentialCardIdentifierClass,\n credentialCardStatusClass,\n credentialCardMetaClass,\n credentialCardControlsClass,\n credentialCardReasonClass,\n credentialCardErrorClass,\n type CredentialCardVariantProps,\n} from \"./credential-card.variants\";\n",
21
+ "path": "credential-card/index.ts",
22
+ "target": "@ui/credential-card/index.ts",
23
+ "type": "registry:ui"
24
+ }
25
+ ],
26
+ "name": "credential-card",
27
+ "registryDependencies": [
28
+ "@verdify/cn",
29
+ "@verdify/badge",
30
+ "@verdify/button",
31
+ "@verdify/checkbox"
32
+ ],
33
+ "title": "credential-card",
34
+ "type": "registry:ui"
35
+ }