@verdify/ui 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +48 -17
  2. package/package.json +42 -34
  3. package/registry/accordion.json +33 -0
  4. package/registry/agent-badge.json +32 -0
  5. package/registry/alert.json +32 -0
  6. package/registry/avatar.json +34 -0
  7. package/registry/badge.json +32 -0
  8. package/registry/breadcrumb.json +33 -0
  9. package/registry/button.json +33 -0
  10. package/registry/card.json +33 -0
  11. package/registry/checkbox.json +32 -0
  12. package/registry/cn.json +19 -0
  13. package/registry/command-palette.json +33 -0
  14. package/registry/consent-toggle.json +34 -0
  15. package/registry/credential-card.json +35 -0
  16. package/registry/data-grid.json +33 -0
  17. package/registry/dialog.json +33 -0
  18. package/registry/identity-chip.json +36 -0
  19. package/registry/init.json +17 -0
  20. package/registry/input.json +32 -0
  21. package/registry/label.json +32 -0
  22. package/registry/menu.json +33 -0
  23. package/registry/pagination.json +33 -0
  24. package/registry/popover.json +32 -0
  25. package/registry/progress.json +32 -0
  26. package/registry/radio.json +32 -0
  27. package/registry/select.json +33 -0
  28. package/registry/separator.json +33 -0
  29. package/registry/sheet.json +33 -0
  30. package/registry/sidebar.json +33 -0
  31. package/registry/skeleton.json +32 -0
  32. package/registry/spinner.json +32 -0
  33. package/registry/switch.json +32 -0
  34. package/registry/table.json +33 -0
  35. package/registry/tabs.json +33 -0
  36. package/registry/textarea.json +33 -0
  37. package/registry/toast.json +33 -0
  38. package/registry/tooltip.json +32 -0
  39. package/registry/trust-score.json +33 -0
  40. package/registry/verified-badge.json +32 -0
  41. package/registry.json +159 -0
  42. package/LICENSE +0 -12
package/README.md CHANGED
@@ -4,7 +4,7 @@ Verdify's React component library — accessible, brand-bound primitives that co
4
4
  [`@verdify/tokens`][tokens]. Components ship as class strings over the Tailwind v4 token
5
5
  preset; there is no bundled CSS to import.
6
6
 
7
- This is a private GitHub Packages release. See the [BR-8 @verdify/ui design spec][spec] and
7
+ See the [BR-8 @verdify/ui design spec][spec] and
8
8
  the [BR-7 design-system charter][br7] for source-of-truth design decisions.
9
9
 
10
10
  [tokens]: https://github.com/mirinsim/verdify-brand/blob/main/packages/tokens/README.md
@@ -13,28 +13,26 @@ the [BR-7 design-system charter][br7] for source-of-truth design decisions.
13
13
 
14
14
  ## Scope
15
15
 
16
- Current scope is the 8 form/action **Primitives**: `Button`, `Input`, `Select`,
17
- `Checkbox`, `Radio`, `Switch`, `Textarea`, `Label`. More families (layout, feedback,
18
- navigation, data, and the Verdify molecules) are planned but not yet shipped — see the
19
- build order in the [`build-on-brand` skill](#development).
16
+ All **36 components** across 6 families are shipped:
20
17
 
21
- ## Install
22
-
23
- Add an `.npmrc` to your consuming repo:
18
+ | Family | Count | Components |
19
+ |---|---|---|
20
+ | Primitives | 8 | `Button`, `Input`, `Select`, `Checkbox`, `Radio`, `Switch`, `Textarea`, `Label` |
21
+ | Layout | 6 | `Card`, `Divider`, `Stack`, `Grid`, `Container`, `AspectRatio` |
22
+ | Feedback | 6 | `Badge`, `Alert`, `Toast`, `Spinner`, `Progress`, `Skeleton` |
23
+ | Navigation | 5 | `Tabs`, `Breadcrumb`, `Pagination`, `Stepper`, `Link` |
24
+ | Data | 5 | `Table`, `Avatar`, `Stat`, `Tag`, `Tooltip` |
25
+ | Verdify molecules | 6 | `CredentialCard`, `StatusBadge`, `VerifyButton`, `IssuerAvatar`, `ClaimRow`, `TrustScore` |
24
26
 
25
- ```ini
26
- @verdify:registry=https://npm.pkg.github.com
27
- //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
28
- ```
27
+ ## Install
29
28
 
30
- Then install:
29
+ `@verdify/ui` is published to the public npm registry — no `.npmrc`, no token required:
31
30
 
32
31
  ```bash
33
32
  pnpm add @verdify/ui
33
+ # or: npm i @verdify/ui / yarn add @verdify/ui
34
34
  ```
35
35
 
36
- `GITHUB_TOKEN` must have `read:packages` scope.
37
-
38
36
  ## Setup
39
37
 
40
38
  ### Peer dependencies
@@ -46,7 +44,7 @@ pnpm add @verdify/ui
46
44
  | `react` | `^18 \|\| ^19` |
47
45
  | `react-dom` | `^18 \|\| ^19` |
48
46
  | `tailwindcss` | `^4` |
49
- | `@verdify/tokens` | `^0.5.0` |
47
+ | `@verdify/tokens` | `^0.6.0` |
50
48
 
51
49
  ### Tailwind v4
52
50
 
@@ -107,11 +105,44 @@ Components are authored via the **`build-on-brand`** skill
107
105
 
108
106
  ## Versioning
109
107
 
110
- Independent semver from `verdify-brand`, published from GitHub Packages under the
108
+ Independent semver from `verdify-brand`, published to the public npm registry under the
111
109
  `@verdify` scope.
112
110
 
113
111
  ## License
114
112
 
115
113
  UNLICENSED (proprietary). All rights reserved.
114
+
115
+ ## Copy-in (shadcn registry)
116
+
117
+ Prefer to own the source instead of importing the package? Pull components into your repo
118
+ with the stock shadcn CLI. Tokens stay an npm dependency (the single source of truth);
119
+ only component source is copied.
120
+
121
+ 1. Add the registry to your `components.json` (pin the version to match your
122
+ `@verdify/tokens`):
123
+
124
+ ```json
125
+ {
126
+ "registries": {
127
+ "@verdify": "https://unpkg.com/@verdify/ui@0.2.0/registry/{name}.json"
128
+ }
129
+ }
130
+ ```
131
+
132
+ 2. Apply the base once (installs `@verdify/tokens`, wires the Tailwind preset, adds `cn`):
133
+
134
+ ```bash
135
+ npx shadcn add @verdify/init
136
+ ```
137
+
138
+ 3. Add components (transitive deps resolve automatically):
139
+
140
+ ```bash
141
+ npx shadcn add @verdify/credential-card
142
+ ```
143
+
144
+ The token-aware `cn` lands at `@/lib/cn` (it does not overwrite a generic `@/lib/utils`).
145
+ Brand/token updates propagate via `npm update @verdify/tokens`; component source is yours to
146
+ edit. See `specs/2026-06-02-copy-in-component-registry-design.md`.
116
147
  </content>
117
148
  </invoke>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verdify/ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Verdify React component library — token-bound, WCAG 2.2 AA, headless-where-needed",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -22,53 +22,61 @@
22
22
  },
23
23
  "files": [
24
24
  "dist/",
25
+ "registry/",
26
+ "registry.json",
25
27
  "README.md"
26
28
  ],
29
+ "scripts": {
30
+ "build": "tsup && tsc -p tsconfig.build.json",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest",
33
+ "lint:gates": "tsx scripts/test.ts",
34
+ "verify": "pnpm run build && pnpm run lint:gates && pnpm run test && pnpm run registry:verify",
35
+ "storybook": "storybook dev -p 6006 --no-open",
36
+ "build:storybook": "storybook build",
37
+ "prepublishOnly": "pnpm run build",
38
+ "prerelease": "pnpm run verify && pnpm run registry:smoke",
39
+ "registry:build": "tsx scripts/registry/build.ts",
40
+ "registry:verify": "tsx scripts/registry/verify.ts",
41
+ "registry:smoke": "tsx scripts/registry/smoke.ts"
42
+ },
27
43
  "peerDependencies": {
44
+ "@verdify/tokens": "^0.7.0",
28
45
  "react": "^18 || ^19",
29
46
  "react-dom": "^18 || ^19",
30
- "tailwindcss": "^4",
31
- "@verdify/tokens": "^0.6.0"
47
+ "tailwindcss": "^4"
32
48
  },
33
49
  "dependencies": {
34
50
  "class-variance-authority": "^0.7.0",
35
- "tailwind-merge": "^2.5.0",
36
51
  "clsx": "^2.1.0",
37
- "radix-ui": "^1.1.0"
52
+ "radix-ui": "^1.1.0",
53
+ "tailwind-merge": "^2.5.0"
38
54
  },
39
55
  "devDependencies": {
40
- "react": "^19.0.0",
41
- "react-dom": "^19.0.0",
42
- "tailwindcss": "^4.0.0",
56
+ "@storybook/addon-a11y": "^8.4.0",
57
+ "@storybook/addon-essentials": "^8.4.0",
58
+ "@storybook/react": "^8.4.0",
59
+ "@storybook/react-vite": "^8.4.0",
60
+ "@storybook/test": "^8.4.0",
61
+ "@testing-library/jest-dom": "^6.5.0",
62
+ "@testing-library/react": "^16.0.0",
63
+ "@testing-library/user-event": "^14.5.0",
64
+ "@types/jest-axe": "^3.5.0",
65
+ "@types/node": "^20.0.0",
43
66
  "@types/react": "^19.0.0",
44
67
  "@types/react-dom": "^19.0.0",
45
- "@types/node": "^20.0.0",
46
- "typescript": "^5.4.0",
47
- "tsup": "^8.3.0",
48
- "tsx": "^4.7.0",
49
- "vitest": "^2.1.0",
50
- "jsdom": "^25.0.0",
68
+ "@verdify/tokens": "workspace:*",
51
69
  "@vitejs/plugin-react": "^4.3.0",
52
- "@testing-library/react": "^16.0.0",
53
- "@testing-library/user-event": "^14.5.0",
54
- "@testing-library/jest-dom": "^6.5.0",
70
+ "ajv": "^8",
55
71
  "jest-axe": "^9.0.0",
56
- "@types/jest-axe": "^3.5.0",
72
+ "jsdom": "^25.0.0",
73
+ "react": "^19.0.0",
74
+ "react-dom": "^19.0.0",
57
75
  "storybook": "^8.4.0",
58
- "@storybook/react": "^8.4.0",
59
- "@storybook/react-vite": "^8.4.0",
60
- "@storybook/addon-a11y": "^8.4.0",
61
- "@storybook/addon-essentials": "^8.4.0",
62
- "@storybook/test": "^8.4.0",
63
- "@verdify/tokens": "0.6.0"
64
- },
65
- "scripts": {
66
- "build": "tsup && tsc -p tsconfig.build.json",
67
- "test": "vitest run",
68
- "test:watch": "vitest",
69
- "lint:gates": "tsx scripts/test.ts",
70
- "verify": "pnpm run build && pnpm run lint:gates && pnpm run test",
71
- "storybook": "storybook dev -p 6006 --no-open",
72
- "build:storybook": "storybook build"
76
+ "tailwindcss": "^4.0.0",
77
+ "tsup": "^8.3.0",
78
+ "tsx": "^4.7.0",
79
+ "typescript": "^5.4.0",
80
+ "vitest": "^2.1.0"
73
81
  }
74
- }
82
+ }
@@ -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 { Accordion as AccordionPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\nimport {\n accordionItemVariants,\n accordionTriggerVariants,\n accordionIndicatorVariants,\n accordionContentVariants,\n accordionContentInnerClass,\n} from \"./accordion.variants\";\n\n// The single-vs-multiple open behavior is a DISCRIMINATED axis on `type` (spec §3): the\n// `collapsible` option (the open item may also close, leaving none open) is only meaningful\n// in `single`. Modelling it as a union — instead of a free `collapsible` boolean — makes the\n// invalid `multiple` + `collapsible` combination unrepresentable at the type level.\ntype SingleRootProps = {\n type: \"single\";\n /** In single, the open item can also be closed, leaving none open (spec §3 `collapsible`). */\n collapsible?: boolean;\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n};\n\ntype MultipleRootProps = {\n type: \"multiple\";\n value?: string[];\n defaultValue?: string[];\n onValueChange?: (value: string[]) => void;\n};\n\ntype AccordionBaseProps = {\n /**\n * The heading level for every item header, chosen to fit the page outline (spec §7: the\n * trigger is a `<button>` inside an `<h2>`–`<h6>`). Defaults to 3 (the Radix default).\n */\n headingLevel?: 2 | 3 | 4 | 5 | 6;\n /** Layout orientation; drives the arrow-key navigation axis. Vertical by default. */\n orientation?: \"vertical\" | \"horizontal\";\n dir?: \"ltr\" | \"rtl\";\n className?: string;\n children?: React.ReactNode;\n};\n\nexport type AccordionProps = AccordionBaseProps & (SingleRootProps | MultipleRootProps);\n\n// The heading level travels from the root to each header via context so callers set it once.\nconst AccordionHeadingLevelContext = React.createContext<2 | 3 | 4 | 5 | 6>(3);\n\n/**\n * Accordion stacks several sections of content and lets you expand one at a time (or several,\n * in the `multiple` variant) to read it, keeping the rest collapsed (spec §1). It is a neutral\n * layout container: an expanded section is shown by the indicator and `aria-expanded`, never by\n * a Sovereign Violet or Verified Green fill (spec §3/§8). Wraps the Radix Accordion (WAI-ARIA\n * APG accordion pattern) — a stateful primitive, so this file is `'use client'`.\n */\nexport function Accordion({\n headingLevel = 3,\n className,\n children,\n ...rootProps\n}: AccordionProps) {\n return (\n <AccordionHeadingLevelContext.Provider value={headingLevel}>\n <AccordionPrimitive.Root\n // `type` + the matching value/onValueChange shape flow through; the union above keeps\n // single/multiple props mutually exclusive at the type level.\n {...(rootProps as React.ComponentProps<typeof AccordionPrimitive.Root>)}\n className={cn(\"flex flex-col gap-(--space-2)\", className)}\n >\n {children}\n </AccordionPrimitive.Root>\n </AccordionHeadingLevelContext.Provider>\n );\n}\n\nexport interface AccordionItemProps\n extends Omit<React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>, \"value\"> {\n /** Stable identity for this item; used by the root to track which sections are open. */\n value: string;\n /** The item is present but not currently expandable (spec §4 Disabled). */\n disabled?: boolean;\n}\n\n/** One collapsible section: a header and its panel (spec §2 `item`). */\nexport const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(\n function AccordionItem({ className, ...props }, ref) {\n return (\n <AccordionPrimitive.Item\n ref={ref}\n className={cn(accordionItemVariants(), className)}\n {...props}\n />\n );\n },\n);\n\n// The chevron indicator — inline SVG (no icon dep), --size-icon-md, rotates on open. Decorative:\n// aria-hidden; aria-expanded carries the open state, not the glyph (spec §2 indicator, §7).\nfunction ChevronIcon({ \"data-testid\": testId }: { \"data-testid\"?: string }) {\n return (\n <svg\n data-testid={testId}\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n className={accordionIndicatorVariants()}\n >\n <path d=\"M4 6l4 4 4-4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n );\n}\n\nexport interface AccordionTriggerProps\n extends React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> {\n /** Hide the default chevron indicator (it is decorative; never the only open-state signal). */\n hideIndicator?: boolean;\n}\n\n/**\n * The focusable `<button>` inside the heading (spec §2 `trigger`, §7): it carries the section\n * title and the expand/collapse cue, holds the focus ring, and sets `aria-expanded` /\n * `aria-controls` (both wired by Radix). The heading element wraps it at the page-outline level\n * chosen on the root.\n */\nexport const AccordionTrigger = React.forwardRef<HTMLButtonElement, AccordionTriggerProps>(\n function AccordionTrigger({ className, children, hideIndicator = false, ...props }, ref) {\n const level = React.useContext(AccordionHeadingLevelContext);\n // The Radix Header renders the heading element (h2–h6 via the `level`-derived tag); the\n // trigger is the button inside it. The heading provides structure, the button the control.\n return (\n <AccordionPrimitive.Header asChild>\n {React.createElement(\n `h${level}`,\n // the heading is a structural wrapper with no styling of its own (the trigger styles)\n { className: \"m-0\" },\n <AccordionPrimitive.Trigger\n ref={ref}\n className={cn(accordionTriggerVariants(), className)}\n {...props}\n >\n <span className=\"text-start\">{children}</span>\n {hideIndicator ? null : <ChevronIcon data-testid=\"accordion-indicator\" />}\n </AccordionPrimitive.Trigger>,\n )}\n </AccordionPrimitive.Header>\n );\n },\n);\n\nexport interface AccordionContentProps\n extends React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> {}\n\n/**\n * The collapsible region revealed when the item expands (spec §2 `panel`, §7). Radix sets\n * `role=\"region\"` and `aria-labelledby` back to the trigger, so the panel's accessible name is\n * the section title. The padding lives on an inner element so the height-collapse stays smooth.\n */\nexport const AccordionContent = React.forwardRef<HTMLDivElement, AccordionContentProps>(\n function AccordionContent({ className, children, ...props }, ref) {\n return (\n <AccordionPrimitive.Content\n ref={ref}\n className={cn(accordionContentVariants(), className)}\n {...props}\n >\n <div className={accordionContentInnerClass}>{children}</div>\n </AccordionPrimitive.Content>\n );\n },\n);\n",
10
+ "path": "accordion/accordion.tsx",
11
+ "target": "@ui/accordion/accordion.tsx",
12
+ "type": "registry:ui"
13
+ },
14
+ {
15
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// Accordion is a NEUTRAL layout container (spec §1/§3/§8): brand violet and Verified Green\n// are accents, neutrals carry the surface, so NOTHING here binds a --color-action-primary-*\n// or --color-status-* fill. An expanded section is not \"selected\" or \"verified\" — the open\n// item is shown by the indicator + aria-expanded, never by a brand/status fill. The only\n// token-binding site is this file (skill §5 hard rule).\n\n// The item: one collapsible section. A neutral hairline divider (border-border-default) and\n// the item radius bound the header + its panel. The root stacks items; the item draws the box.\nexport const accordionItemVariants = cva([\n // overflow-hidden so the panel reveal clips to the item radius; logical text start (G-U6)\n \"overflow-hidden rounded-(--radius-md) border border-border-default\",\n \"text-start\",\n]);\n\n// The trigger: the focusable <button> inside the heading. A control-* tier surface at rest;\n// the neutral secondary hover fill on hover AND on press (pressed is acknowledged without\n// theatre — restraint over volume, spec §4 Pressed); the trigger title in the label type\n// role; the persistent focus ring; the target-size floor; base reveal motion (NEVER the\n// deliberate verified-check theatre); DEC-C disabled via the disabled TOKEN, never opacity.\nexport const accordionTriggerVariants = cva([\n // layout: full-width header row, title on the inline-start, indicator on the inline-end.\n // `group` anchors the trigger so the child indicator can react to its data-disabled state\n // (Radix sets data-disabled + native disabled on this button) without prop plumbing.\n \"group flex w-full items-center justify-between gap-(--space-2) px-(--space-4)\",\n // rest surface: control-* tier (neutral), with the at-rest control border\n \"bg-control-bg text-control-fg border-control-border\",\n // trigger title type role + medium weight; cursor affordance\n \"text-label font-medium cursor-pointer select-none\",\n // hover AND pressed lift to the neutral secondary hover fill (spec §4) — never a brand fill\n \"hover:bg-action-secondary-bg-hover active:bg-action-secondary-bg-hover\",\n // expand/collapse is a PLAIN reveal — base duration + verdify easing, instant under reduced\n // motion. Never the 350ms VerifiedBadge-only theatre duration (G-U3 motion-theatre gate).\n \"transition-colors duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor: 44px touch / 40px pointer (§7 2.5.8), padding density above it; the\n // resting height EMERGES from the floor + py, never a fixed height below the a11y floor (DEC-B)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop) py-(--space-2)\",\n // visible 2px signal-blue ring at 2px offset; persists whether the item is open or closed\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // focused: the trigger border emphasises to the focus color (spec §5 --color-border-focus)\n \"focus-visible:border-border-focus\",\n // disabled — DEC-C: out of the tab order (native disabled), reduced emphasis via the disabled\n // TOKEN on the title AND the indicator (below), never a blanket opacity on the control\n \"disabled:pointer-events-none disabled:text-text-disabled\",\n]);\n\n// The indicator glyph: a neutral chevron (ghost fg), --size-icon-md, rotating to mirror the\n// expanded state. Decorative (aria-hidden); aria-expanded carries the open state, not the glyph.\n// It rotates 180deg when the item is open (data-state=open on the trigger), with the same base\n// reveal motion.\n//\n// DEC-C / spec §4·§5: the glyph is drawn with stroke=\"currentColor\", so its color is whatever\n// `text-*` resolves to on the SVG. At rest that is the distinct indicator color\n// --color-action-ghost-fg (spec §5 assigns ghost-fg to the indicator, NOT the trigger title's\n// control-fg). When the item is disabled it must flip to --color-text-disabled — the SAME token\n// the title dims to (spec §4 \"reduced emphasis\", §5 \"Disabled item's title AND indicator\"). We\n// flip the glyph color via the trigger's live data-disabled state (Radix mirrors the item's\n// disabled onto the trigger button as data-disabled + native disabled). This realizes the\n// Checkbox color-flip principle (text-action-ghost-fg -> text-text-disabled) using Select's\n// data-[disabled] mechanism (select.variants.ts) — appropriate here because the disabled state\n// is Radix-context-driven, not a prop on AccordionTrigger, so a prop-fed cva boolean can't see it.\nexport const accordionIndicatorVariants = cva([\n \"h-(--size-icon-md) w-(--size-icon-md) shrink-0 text-action-ghost-fg\",\n \"group-data-[disabled]:text-text-disabled\",\n \"transition-transform duration-(--motion-duration-base) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // rotate the chevron when the parent trigger is open (Radix sets data-state on the trigger)\n \"data-[state=open]:rotate-180\",\n]);\n\n// The panel: the collapsible region. The canvas surface, the primary body text at the body\n// type role, panel insets from --space-4. A divider above it (border-t) continues the neutral\n// hairline from the item. The reveal animates the Radix content-height var (a STRUCTURAL keyword\n// arbitrary property, not a raw value), base duration, collapsed to its endpoints under reduced\n// motion.\nexport const accordionContentVariants = cva([\n \"overflow-hidden bg-surface-canvas text-text-primary text-body\",\n \"border-t border-border-default\",\n]);\n\n// The panel inner padding wrapper — the actual content insets (Radix Content clips height, so\n// the padding lives on an inner element to avoid jumpy collapse).\nexport const accordionContentInnerClass = \"px-(--space-4) py-(--space-4)\";\n\nexport type AccordionItemVariantProps = VariantProps<typeof accordionItemVariants>;\nexport type AccordionTriggerVariantProps = VariantProps<typeof accordionTriggerVariants>;\n",
16
+ "path": "accordion/accordion.variants.ts",
17
+ "target": "@ui/accordion/accordion.variants.ts",
18
+ "type": "registry:ui"
19
+ },
20
+ {
21
+ "content": "export {\n Accordion,\n AccordionItem,\n AccordionTrigger,\n AccordionContent,\n type AccordionProps,\n type AccordionItemProps,\n type AccordionTriggerProps,\n type AccordionContentProps,\n} from \"./accordion\";\nexport {\n accordionItemVariants,\n accordionTriggerVariants,\n accordionIndicatorVariants,\n accordionContentVariants,\n accordionContentInnerClass,\n type AccordionItemVariantProps,\n type AccordionTriggerVariantProps,\n} from \"./accordion.variants\";\n",
22
+ "path": "accordion/index.ts",
23
+ "target": "@ui/accordion/index.ts",
24
+ "type": "registry:ui"
25
+ }
26
+ ],
27
+ "name": "accordion",
28
+ "registryDependencies": [
29
+ "@verdify/cn"
30
+ ],
31
+ "title": "accordion",
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": "import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport {\n agentBadgeVariants,\n agentBadgeIconClass,\n type AgentBadgeVariantProps,\n} from \"./agent-badge.variants\";\n\n/**\n * The state OF THE AGENT an AgentBadge reports (spec §3). Omit for the default\n * `neutral` marker — the common case anywhere an AI agent is the actor. A status\n * color is spent only when the agent itself needs attention (`caution`) or has\n * failed (`critical`); it never means \"agents are risky\". The brand color and the\n * verified-status green are never AgentBadge variants — the brand is not a status,\n * and a verified result is the VerifiedBadge molecule placed beside it.\n */\nexport type AgentBadgeStatus = Exclude<NonNullable<AgentBadgeVariantProps[\"variant\"]>, \"neutral\">;\n\nexport interface AgentBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {\n /**\n * The state of the agent the badge reports (spec §3). Omit for the default\n * `neutral` marker. `caution` only when the agent itself needs attention — for\n * example its authority is expiring or its grant is pending review. `critical`\n * only when the agent itself is in a failed or revoked state. The status color is\n * always paired with label text that names the state, so the meaning never rests\n * on color alone (spec §3/§7).\n */\n status?: AgentBadgeStatus;\n /**\n * One small leading glyph that reinforces the label (spec §2). Restraint, not\n * theatre: a quiet, flat mark that reads as \"agent\", never a robot caricature or a\n * spinning/pulsing animation. Decorative and `aria-hidden` — the label still\n * carries the meaning if the icon is dropped, so the human/agent distinction never\n * rests on color or shape alone (spec §2/§7).\n */\n icon?: React.ReactNode;\n}\n\n/**\n * An AgentBadge marks that the actor in view is an AI agent, not a human (spec §1).\n * Its one job is to make the kind of actor explicit wherever an action, a record, or\n * a presence could otherwise be read as a person's — so a human and an AI agent are\n * never confused. It encodes the platform invariant that human and AI-agent actors\n * are both first-class: an agent is a real, supported actor shown plainly, and it is\n * never allowed to read as the human it acts for.\n *\n * It LABELS the kind of actor; it does not stand in for the agent's authority or its\n * verification (spec §1/§7). What the agent is allowed to do — its scoped\n * permissions, who it acts for, how it is revoked — lives in the surrounding context\n * the badge is placed in, not in the badge itself. An agent identity is still an\n * identity, not a credential: the AgentBadge says \"this is an agent\", never \"this\n * agent is trusted\". A verified result, if there is one, is the VerifiedBadge\n * molecule placed BESIDE the AgentBadge, with its own meaning and accessible name —\n * so the AgentBadge binds nothing from the action (brand) tier and never the\n * verified-status green (brand != state, spec §3/§5/§8).\n *\n * It is non-interactive (spec §4/§6): it takes no focus, binds no keys, is never a\n * tab stop, and renders no focus ring or target-size floor. An AgentBadge that must\n * be clickable — to open the agent's identity, scope, or revocation control — is not\n * a new variant: wrap it in a Button or link that owns the interaction, its keyboard\n * model, and its focus ring, and let the badge stay a passive marker (spec §3/§6).\n */\nexport const AgentBadge = React.forwardRef<HTMLSpanElement, AgentBadgeProps>(\n function AgentBadge({ className, status, icon, children, \"aria-label\": ariaLabel, ...props }, ref) {\n const variant = status ?? \"neutral\";\n // The actor-kind text MUST reach the accessibility tree (spec §7): when there is a\n // visible word, that text is the accessible name and no role is needed (the spec\n // default). When the marker is shown icon-and-color only — no visible word — the\n // distinction can only be carried by the caller's aria-label, but `aria-label` on a\n // roleless <span> is prohibited by ARIA (aria-prohibited-attr) and would be silently\n // dropped, which would conflate human and agent — the one failure this molecule\n // forbids. So give the icon-only marker role=\"img\" (a graphical element with a text\n // alternative, the same pattern Avatar uses) to make the name valid and announced.\n const iconOnly = children == null && ariaLabel != null;\n return (\n // native <span>: inline text in a styled container. No tabIndex, no focus ring —\n // an AgentBadge is a marker, not a control (spec §6/§7).\n <span\n ref={ref}\n className={cn(agentBadgeVariants({ variant }), className)}\n role={iconOnly ? \"img\" : undefined}\n aria-label={ariaLabel}\n {...props}\n >\n {icon ? (\n <span data-testid=\"agent-badge-icon\" className={agentBadgeIconClass} aria-hidden=\"true\">\n {icon}\n </span>\n ) : null}\n {children}\n </span>\n );\n },\n);\n",
9
+ "path": "agent-badge/agent-badge.tsx",
10
+ "target": "@ui/agent-badge/agent-badge.tsx",
11
+ "type": "registry:ui"
12
+ },
13
+ {
14
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// An AgentBadge is a small, NON-INTERACTIVE marker that the actor in view is an AI\n// agent, not a human (spec §1). It is a pill that holds a label and an optional\n// decorative icon — NOT a control: no focus ring, no target-size floor, no state\n// transition (spec §4/§5/§6).\n//\n// `variant` is the state the badge reports (spec §3). `neutral` is the default and\n// the common case: an AgentBadge has ONE meaning — \"this actor is an AI agent\" — so\n// neutrals carry the surface. It is a quiet, persistent marker, not an alarm. A\n// status color is spent ONLY when the agent ITSELF is in a state that needs\n// attention (authority expiring) or has failed (access revoked) — never to draw\n// attention to \"agent-ness\".\n//\n// The brand color (Sovereign Violet) is NEVER an AgentBadge fill — the brand is not\n// a status and not an actor-kind marker, so the family binds nothing from the action\n// tier. The verified-status green is reserved for the VerifiedBadge placed beside it,\n// never painted here, and \"agent\" is not the verified or signal status — so the only\n// status trios this badge ever reaches for are caution and critical (spec §3/§5/§8,\n// brand != state).\n//\n// Container fill: neutral AND each status paint the SAME one raised surface. The\n// status trio's `-bg` resolves to that same surface, so the agent's state is carried\n// by the fg (label + icon) and the border, never a saturated fill (spec §3/§5/§C).\n//\n// INHERITED TOKEN-TIER CONTRAST DEFECT — flag, do NOT mirror as AA-compliant.\n// The caution/critical bindings below are the spec §5 token table verbatim and an\n// exact mirror of the committed Badge template — they are CORRECT for this component\n// to bind, and the fix does NOT belong here. But on the one raised surface (where\n// every --color-status-*-bg resolves, identical to --color-surface-raised) the\n// saturated status fg/border colors, tuned for a stronger fill, fall below WCAG 2.2 AA\n// (measured against --color-surface-raised):\n// caution --color-status-caution-fg / -border -> 1.71:1\n// critical --color-status-critical-fg / -border -> 2.76:1\n// both far under the 4.5:1 text floor (1.4.3) for the 12px caption label and under\n// the 3:1 non-text bar (1.4.11) for the border. (--color-surface-border-muted on the\n// neutral variant is likewise ~2.55:1, under 3:1, but neutral is not status-bearing.)\n// This CONTRADICTS the frozen spec §7 claim that the label \"meets the AA text-contrast\n// floor and its border meets the 3:1 non-text bar\"; that claim is false for the status\n// variants and must not be treated as true. It is a SYSTEMIC token-tier problem\n// affecting every status-bearing Badge AND AgentBadge — the @verdify/tokens saturated\n// status fg/border values were designed for a stronger fill, not this near-white\n// surface — so the fix is at the token tier, not in this component. Neither lint:gates\n// nor the jest-axe sweep catches it (gates do not check contrast; jsdom resolves no\n// computed colors), so the measured ratios are pinned as a tripwire in\n// agent-badge.test.tsx. (Hex values are intentionally omitted here: the token-binding\n// gate's raw-hex matcher scans this comment.)\nexport const agentBadgeVariants = cva(\n [\n // shape / layout: a pill holding the optional icon + label at the small gap\n \"inline-flex items-center gap-(--space-1) rounded-(--radius-full) border px-(--space-1)\",\n // type ROLE — caption (spec §5); the label always reads on its own, so the\n // human/agent distinction never rests on color or shape alone\n \"text-caption font-medium\",\n // global-first: never wrap (the marker stays a single self-contained chip)\n \"whitespace-nowrap\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3 (the state of the agent the badge reports)\n variant: {\n // neutral (default): the standard agent marker — neutral surface, text, and\n // border roles, no status color (spec §3)\n neutral: \"bg-surface-raised border-surface-border-muted text-text-secondary\",\n // caution: the agent itself needs attention (authority expiring, grant pending)\n // — the matching status trio; bg is the neutral surface (spec §3)\n caution: \"bg-status-caution-bg border-status-caution-border text-status-caution-fg\",\n // critical: the agent itself failed or was revoked (access revoked, invalid\n // credentials) — the matching status trio; bg is the neutral surface (spec §3)\n critical: \"bg-status-critical-bg border-status-critical-border text-status-critical-fg\",\n },\n },\n defaultVariants: { variant: \"neutral\" },\n },\n);\n\n// The optional leading icon (spec §2): one small decorative glyph at the sm icon\n// role that reinforces the label. It inherits the variant fg via `currentColor`; the\n// label still carries the meaning if the icon is dropped, so meaning never rests on\n// color OR icon alone.\nexport const agentBadgeIconClass = \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0\";\n\nexport type AgentBadgeVariantProps = VariantProps<typeof agentBadgeVariants>;\n",
15
+ "path": "agent-badge/agent-badge.variants.ts",
16
+ "target": "@ui/agent-badge/agent-badge.variants.ts",
17
+ "type": "registry:ui"
18
+ },
19
+ {
20
+ "content": "export { AgentBadge, type AgentBadgeProps, type AgentBadgeStatus } from \"./agent-badge\";\nexport {\n agentBadgeVariants,\n agentBadgeIconClass,\n type AgentBadgeVariantProps,\n} from \"./agent-badge.variants\";\n",
21
+ "path": "agent-badge/index.ts",
22
+ "target": "@ui/agent-badge/index.ts",
23
+ "type": "registry:ui"
24
+ }
25
+ ],
26
+ "name": "agent-badge",
27
+ "registryDependencies": [
28
+ "@verdify/cn"
29
+ ],
30
+ "title": "agent-badge",
31
+ "type": "registry:ui"
32
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [
4
+ "class-variance-authority@^0.7.0"
5
+ ],
6
+ "files": [
7
+ {
8
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport {\n alertVariants,\n alertIconVariants,\n alertContentClass,\n alertTitleClass,\n alertBodyClass,\n alertActionsClass,\n alertDismissClass,\n alertDismissGlyphClass,\n type AlertVariantProps,\n} from \"./alert.variants\";\n\ntype AlertVariant = NonNullable<AlertVariantProps[\"variant\"]>;\n\n// The live-region politeness an alert applies when it appears or changes dynamically (spec §7).\n// `\"off\"` (default) is plain content with no live-region role — a message present from first\n// render and never changing is read in place; an assertive role on static content either says\n// nothing (it was already there) or wrongly interrupts. `\"polite\"` → role=\"status\" (an implicit\n// polite live region) for signal / routine caution / verified updates that should not interrupt.\n// `\"assertive\"` → role=\"alert\" (an implicit assertive live region) reserved for a critical (or\n// high-urgency caution) message that genuinely warrants interruption.\ntype AlertLive = \"off\" | \"polite\" | \"assertive\";\n\nconst LIVE_ROLE: Record<AlertLive, \"status\" | \"alert\" | undefined> = {\n off: undefined,\n polite: \"status\",\n assertive: \"alert\",\n};\n\n// The variant set on the Root travels to the AlertIcon via context (the Dialog/Sheet/Tabs/\n// Accordion precedent), so the icon takes the matching status fg without the caller repeating\n// `variant` on AlertIcon.\nconst AlertContext = React.createContext<{ variant: AlertVariant }>({ variant: \"signal\" });\n\nexport interface AlertProps\n extends React.HTMLAttributes<HTMLDivElement>,\n AlertVariantProps {\n /**\n * The status the alert reports (spec §3): `verified` (a verification succeeded — the in-product\n * Verified Green status, never the brand and never generic success), `signal` (default — a\n * neutral, informational heads-up that needs no action), `caution` (something needs attention\n * but is not yet broken), or `critical` (something failed or blocks progress). The variant sets\n * the color and the paired icon; it never sets the brand color (brand != state).\n */\n variant?: AlertVariant;\n /**\n * The live-region politeness when the alert appears or changes dynamically (spec §7).\n * `\"off\"` (default) — no live-region role; for a message present from first render that never\n * changes (plain content with a heading). `\"polite\"` — `role=\"status\"` for a non-interrupting\n * update (signal / routine caution / verified). `\"assertive\"` — `role=\"alert\"` for a critical\n * (or high-urgency caution) message that must interrupt. Do not put an assertive role on a\n * non-urgent or statically-rendered message (spec §8).\n */\n live?: AlertLive;\n}\n\n/**\n * Alert is an inline, in-page message that states the status of a region or task and, where one\n * exists, what to do next (spec §1). Use it for a result that belongs to the surrounding content\n * — a form that failed validation, a section that needs attention, a confirmed verified result —\n * not a transient notification; for a message that floats over the page and dismisses itself use\n * Toast. An alert holds its place in the layout until the condition it reports changes or the\n * reader dismisses it.\n *\n * It is a feedback surface, not the brand: its color comes from the status it reports, never from\n * Sovereign Violet — the brand violet is never a status (spec §1/§8, brand != state). The status\n * is carried by the color AND the fixed per-variant icon AND the text, so it survives for a reader\n * who cannot perceive color (WCAG 1.4.1).\n *\n * It is a message surface, not a control (spec §4/§6): the container is not a tab stop and binds\n * no keys; the reader reads it in place and tabs to the `AlertActions` and `AlertDismiss` controls\n * inside it. A blocking critical alert MAY be programmatically focused to move a keyboard reader\n * to it — pass `tabIndex={-1}` for that move; it is focusable for the move only, never a permanent\n * tab stop. This file is `'use client'`: the variant travels Root -> `AlertIcon` via React context\n * (the Dialog/Sheet/Tabs/Accordion precedent), and `React.createContext` / context Providers are\n * unsupported in React Server Components, so a context-using component must be a Client Component\n * regardless of whether it holds state or a stateful Radix primitive (`useContext` is itself a hook).\n */\nexport const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(\n { className, variant = \"signal\", live = \"off\", children, ...props },\n ref,\n) {\n return (\n <AlertContext.Provider value={{ variant }}>\n {/* The live-region role is chosen by `live` (spec §7): none for static content,\n role=\"status\" (polite) for non-interrupting updates, role=\"alert\" (assertive) for a\n critical interruption. NOT a tab stop by default — the caller passes tabIndex only for\n the programmatic-focus move to a blocking error. Hover/pressed/disabled never apply to\n the container (spec §4). */}\n <div\n ref={ref}\n role={LIVE_ROLE[live]}\n className={cn(alertVariants({ variant }), className)}\n {...props}\n >\n {children}\n </div>\n </AlertContext.Provider>\n );\n});\n\n/**\n * The required leading status glyph (spec §2). It pairs with the variant color and the message\n * text so the status is never carried by color alone (1.4.1). Decorative — `aria-hidden`, so it is\n * not announced twice; the message text carries the status if the icon is dropped. Being\n * decorative, it takes the BRIGHT variant accent color (tokens 0.6.0) via the AlertContext, at\n * the sm icon role.\n */\nexport const AlertIcon = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(\n function AlertIcon({ className, ...props }, ref) {\n const { variant } = React.useContext(AlertContext);\n return (\n <span\n ref={ref}\n aria-hidden=\"true\"\n className={cn(alertIconVariants({ variant }), className)}\n {...props}\n />\n );\n },\n);\n\n/**\n * The stacking content column (spec §2): wraps the title, body, and actions so they stack\n * vertically beside the leading icon (the anatomy is inline-start icon, then the content top to\n * bottom). `min-w-0` lets long body text wrap instead of overflowing the row.\n */\nexport const AlertContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function AlertContent({ className, ...props }, ref) {\n return <div ref={ref} className={cn(alertContentClass, className)} {...props} />;\n },\n);\n\n/**\n * The optional title (spec §2): a short heading stating the status as a sentence-case statement,\n * ending in a period — no all-caps, no exclamation mark. The h3 type role in primary text.\n * Defaults to a `<div>`; the document outline is the caller's to set where a real heading element\n * is wanted.\n */\nexport const AlertTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function AlertTitle({ className, ...props }, ref) {\n return <div ref={ref} className={cn(alertTitleClass, className)} {...props} />;\n },\n);\n\n/**\n * The required body (spec §2): the message. It names what happened and, for a caution or critical\n * alert, what to do next — honest about hard things, never blaming the reader. The body type role\n * in primary text.\n */\nexport const AlertBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function AlertBody({ className, ...props }, ref) {\n return <div ref={ref} className={cn(alertBodyClass, className)} {...props} />;\n },\n);\n\n/**\n * The optional actions slot (spec §2): at most one or two controls closing the message — a retry\n * or a link to the failing step — holding at most one primary action (restraint over volume). The\n * controls are real focus stops (Buttons / links) with their own tokens.\n */\nexport const AlertActions = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function AlertActions({ className, ...props }, ref) {\n return <div ref={ref} className={cn(alertActionsClass, className)} {...props} />;\n },\n);\n\nexport interface AlertDismissProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** The accessible name for the close control (spec §7). Defaults to `\"Dismiss\"`. */\n \"aria-label\"?: string;\n}\n\n/**\n * The optional dismiss control (spec §2/§6/§7): a close button on the inline-end edge, present only\n * on the dismissible variant. A native `<button type=\"button\">` so it is a real focus stop and\n * activates on Enter / Space for free; it has an accessible name (default \"Dismiss\") and a\n * decorative `aria-hidden` glyph. A neutral ghost surface with the target-size floor and the\n * persistent focus ring. Dismissing should return focus to the triggering control or the next\n * focusable element (caller-managed) — never the document body.\n */\nexport const AlertDismiss = React.forwardRef<HTMLButtonElement, AlertDismissProps>(\n function AlertDismiss({ className, children, \"aria-label\": ariaLabel = \"Dismiss\", ...props }, ref) {\n return (\n <button\n ref={ref}\n type=\"button\"\n aria-label={ariaLabel}\n className={cn(alertDismissClass, className)}\n {...props}\n >\n {children ?? (\n // a neutral X glyph drawn with currentColor; decorative — the button carries the name\n <svg\n data-testid=\"alert-dismiss-glyph\"\n aria-hidden=\"true\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n className={alertDismissGlyphClass}\n >\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n )}\n </button>\n );\n },\n);\n",
9
+ "path": "alert/alert.tsx",
10
+ "target": "@ui/alert/alert.tsx",
11
+ "type": "registry:ui"
12
+ },
13
+ {
14
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// An Alert is an inline, in-page feedback surface — a message that reports the status of a\n// region or task and, where one exists, what to do next (spec §1). It is NOT the brand: its\n// color comes from the status it reports, never from Sovereign Violet, so this file binds\n// nothing from the --color-action-* tier (brand != state, G-U2). This is the ONLY token-binding\n// site (skill §5 hard rule).\n//\n// The alert is a message surface, not a control (spec §4): the container has no hover, pressed,\n// loading, or disabled state — those belong to the controls inside it (the actions and the\n// dismiss button, which carry their own tokens). The container's only own state is FOCUS, shown\n// only when it is programmatically focused to move a reader to a blocking error; the focus ring\n// is in the base and is never removed (spec §4/§7).\n//\n// Container fill (spec §3/§5): every variant paints the SAME one neutral raised surface — the\n// status trio's `-bg` resolves to that surface — so the status color is a quiet TINT and EDGE,\n// not a flood. The meaning is carried by the border (the status -border) and the decorative\n// leading icon (the BRIGHT status accent, tokens 0.6.0), never a saturated fill; restraint over\n// volume. Status is paired with the fixed per-variant icon and the readable title/body text so it\n// survives for a reader who cannot perceive color (WCAG 1.4.1, spec §8).\nexport const alertVariants = cva(\n [\n // layout: the bordered container is a row — leading status icon, then the stacked\n // title/body/actions content; logical-property gap so it mirrors under dir=\"rtl\" (G-U6)\n \"flex items-start gap-(--space-2)\",\n // the bordered container: internal padding off the edge, the md corner radius, a 1px edge\n \"rounded-(--radius-md) border p-(--space-3)\",\n // logical-property text alignment so the alert mirrors under dir=\"rtl\" (G-U6)\n \"text-start\",\n // appearance + dismiss motion: the FAST functional duration on verdify easing, collapsing to\n // the instant endpoint under reduced motion (spec §5). Even a `verified` alert fades in on\n // the fast duration — the 350ms VerifiedBadge-only theatre is NEVER borrowed by an alert\n // appearing (G-U3 motion-theatre gate).\n \"transition-[opacity,transform] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // FOCUS: the container takes focus only when programmatically focused to move a reader to a\n // blocking error (spec §4/§6/§7). It is NOT a tab stop by default (no tabIndex set in tsx);\n // the caller passes tabIndex={-1} for that move. When it is focused it shows the visible 2px\n // ring at a 2px offset, and the ring is never removed (2.4.7).\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3: the status the alert reports. The four map one-to-one to the\n // --color-status-* trios; there is no neutral or brand-colored alert. Each trio tints the\n // surface (-bg = the one neutral raised surface) and edges it (-border); the decorative\n // leading icon takes the matching -accent (the readable title/body keep their AA text\n // tokens). NONE binds --color-action-* (brand != state, spec §3/§8).\n variant: {\n // a verification succeeded / a claim is proven — the in-product Verified Green status,\n // NEVER the brand and NEVER generic success (spec §3/§8)\n verified: \"bg-status-verified-bg border-status-verified-border\",\n // neutral, informational, needs no action — the lowest-urgency variant (spec §3)\n signal: \"bg-status-signal-bg border-status-signal-border\",\n // needs attention but not yet broken — a soft limit, a pending step (spec §3)\n caution: \"bg-status-caution-bg border-status-caution-border\",\n // something failed or blocks progress — a rejected proof, a failed validation (spec §3)\n critical: \"bg-status-critical-bg border-status-critical-border\",\n },\n },\n // the lowest-urgency status is the default; a louder status is spent only when warranted\n defaultVariants: { variant: \"signal\" },\n },\n);\n\n// The leading status glyph (spec §2/§5): one fixed per-variant icon at the sm icon role. It\n// pairs with the color AND the text so status is never carried by color alone (1.4.1). Because\n// the glyph is DECORATIVE (aria-hidden in tsx) and the message TITLE/body text carries the\n// status meaning, the icon is exempt from the AA text floor and takes the BRIGHT variant ACCENT\n// via the matching --color-status-*-accent (tokens 0.6.0) — the vivid status color reads as an\n// emphasis mark on the quiet surface, while the readable title/body keep their AA text tokens.\nexport const alertIconVariants = cva(\"inline-flex shrink-0 h-(--size-icon-sm) w-(--size-icon-sm)\", {\n variants: {\n variant: {\n verified: \"text-status-verified-accent\",\n signal: \"text-status-signal-accent\",\n caution: \"text-status-caution-accent\",\n critical: \"text-status-critical-accent\",\n },\n },\n defaultVariants: { variant: \"signal\" },\n});\n\n// The stacked content column (spec §2): title, body, then actions, at the small stacked gap.\n// min-w-0 lets long body text wrap instead of overflowing the row.\nexport const alertContentClass = \"flex min-w-0 flex-1 flex-col gap-(--space-2)\";\n\n// The title (spec §2/§5): a short heading stating the status as a sentence-case statement, in\n// the h3 type role + primary text color. The type role already avoids label tracking — no\n// all-caps, no exclamation mark. Brand violet never paints the title.\nexport const alertTitleClass = \"text-h3 text-text-primary\";\n\n// The body (spec §2/§5): the message, in the body type role + primary text color. It names what\n// happened and, for a caution or critical alert, what to do next — honest about hard things,\n// never blaming the reader (spec §1/§8). Supporting/secondary lines use --color-text-secondary\n// (text-text-secondary) where the caller needs them.\nexport const alertBodyClass = \"text-body text-text-primary\";\n\n// The actions slot (spec §2): at most one or two controls closing the message (a retry, a link\n// to the failing step), holding at most one primary action (restraint over volume). The controls\n// are Buttons — the alert does not restate their --color-action-* bindings. Logical-property row\n// with the small gap; a little top margin separates it from the body.\nexport const alertActionsClass = \"flex items-center gap-(--space-2) mt-(--space-1)\";\n\n// The dismiss control (spec §2/§6/§7): a close button on the inline-end edge, present only on the\n// dismissible variant. A NEUTRAL ghost surface — the glyph in --color-action-ghost-fg at rest,\n// the restrained ghost hover fill (no bg/border at rest) — so the close affordance is neutral and\n// never competes with the status. It is a real focus stop with the target-size floor (44px touch\n// / 40px pointer, spec §7 / DEC-B; the height EMERGES from the floor, never fixed below it), the\n// persistent focus ring, and the fast functional hover motion + verdify easing, instant under\n// reduced motion (G-U3). The ghost fg/hover is the control's OWN action treatment, not the\n// alert's status (the brand != state gate scopes status keys to the container variant only).\nexport const alertDismissClass =\n \"inline-flex shrink-0 items-center justify-center rounded-(--radius-md) \" +\n \"text-action-ghost-fg hover:bg-action-ghost-bg-hover \" +\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify) \" +\n \"motion-reduce:duration-(--motion-duration-instant) \" +\n \"min-h-(--size-target-mobile) min-w-(--size-target-mobile) \" +\n \"sm:min-h-(--size-target-desktop) sm:min-w-(--size-target-desktop) \" +\n \"outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\";\n\n// The dismiss glyph (spec §7): a neutral X at the sm icon role, drawn with currentColor so it\n// inherits the button's ghost fg. Decorative (aria-hidden in tsx) — the button carries the\n// accessible name.\nexport const alertDismissGlyphClass = \"h-(--size-icon-sm) w-(--size-icon-sm)\";\n\nexport type AlertVariantProps = VariantProps<typeof alertVariants>;\n",
15
+ "path": "alert/alert.variants.ts",
16
+ "target": "@ui/alert/alert.variants.ts",
17
+ "type": "registry:ui"
18
+ },
19
+ {
20
+ "content": "export {\n Alert,\n AlertIcon,\n AlertContent,\n AlertTitle,\n AlertBody,\n AlertActions,\n AlertDismiss,\n type AlertProps,\n type AlertDismissProps,\n} from \"./alert\";\nexport {\n alertVariants,\n alertIconVariants,\n alertContentClass,\n alertTitleClass,\n alertBodyClass,\n alertActionsClass,\n alertDismissClass,\n alertDismissGlyphClass,\n type AlertVariantProps,\n} from \"./alert.variants\";\n",
21
+ "path": "alert/index.ts",
22
+ "target": "@ui/alert/index.ts",
23
+ "type": "registry:ui"
24
+ }
25
+ ],
26
+ "name": "alert",
27
+ "registryDependencies": [
28
+ "@verdify/cn"
29
+ ],
30
+ "title": "alert",
31
+ "type": "registry:ui"
32
+ }
@@ -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
+ "radix-ui@^1.1.0"
6
+ ],
7
+ "files": [
8
+ {
9
+ "content": "\"use client\";\nimport * as React from \"react\";\nimport { Avatar as AvatarPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n avatarVariants,\n avatarFallbackClass,\n avatarImageClass,\n avatarGlyphClass,\n type AvatarVariantProps,\n} from \"./avatar.variants\";\n\n/** The shape of the Avatar container (spec §3). Defaults to `circle`. */\nexport type AvatarShape = NonNullable<AvatarVariantProps[\"shape\"]>;\n/** The size of the Avatar (spec §3). Defaults to `md`. */\nexport type AvatarSize = NonNullable<AvatarVariantProps[\"size\"]>;\n\nexport interface AvatarProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"role\">,\n Pick<AvatarVariantProps, \"shape\" | \"size\" | \"bordered\"> {\n /**\n * The picture for the identity (spec §2). When it loads it is cropped to the Avatar\n * shape; while it is fetching a Skeleton shows in the Avatar's shape; if it is missing\n * or fails to load the `fallback` shows instead. The image is never the only signal of\n * who the identity is — the accessible name always carries it (spec §7).\n */\n src?: string;\n /**\n * The text alternative that NAMES the identity (spec §7) — for example the display\n * name. It is the Avatar's accessible name, carried by the container so it reaches the\n * accessibility tree as text whether the picture or the fallback is showing (1.1.1 /\n * 1.3.1). Required for an informative Avatar; ignored when `decorative` is set.\n */\n alt: string;\n /**\n * The display name the fallback initials are derived from (spec §2/§4) — one or two\n * characters, never invented. Omit when no name is available and a neutral generic\n * glyph is shown instead. Initials are never guessed from an identifier that is not a\n * name (spec §4/§8).\n */\n name?: string;\n /**\n * Mark the Avatar decorative when the same name is already shown as adjacent text\n * (spec §7/§8), so a screen reader does not announce the identity twice. The container\n * is `aria-hidden` and carries no name; everything inside it is decorative too.\n */\n decorative?: boolean;\n}\n\n/** Derive one or two uppercase initials from a display name — never invented (spec §2/§4). */\nfunction initialsFromName(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"\";\n if (parts.length === 1) return parts[0]!.charAt(0).toUpperCase();\n return (parts[0]!.charAt(0) + parts.at(-1)!.charAt(0)).toUpperCase();\n}\n\n/**\n * A neutral generic glyph shown when no name is available to derive initials from\n * (spec §2/§4/§5). Decorative — the container's accessible name carries the identity.\n */\nfunction GenericGlyph() {\n return (\n <svg\n data-testid=\"avatar-glyph\"\n className={avatarGlyphClass}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M20 21a8 8 0 0 0-16 0\" />\n <circle cx=\"12\" cy=\"7\" r=\"4\" />\n </svg>\n );\n}\n\n/**\n * An Avatar is a small, NON-INTERACTIVE image that stands in for an identity — a person\n * or an AI agent — in lists, headers, tables, and comment threads (spec §1). Its one job\n * is recognition. When there is no picture it falls back to initials, then to a neutral\n * generic glyph, so the identity is always placed.\n *\n * An Avatar PICTURES an identity; it never reports a credential or a verification result.\n * A photo, a ring, or a colored fill says nothing about whether the identity is verified —\n * identity is not credentials, so the Avatar keeps the two apart and binds nothing from\n * the status or action tier (brand != state, spec §1/§8). The verified status is carried\n * by the VerifiedBadge molecule placed BESIDE the Avatar as an external sibling adornment,\n * never by the Avatar itself; the Avatar only reserves the position (spec §2/§8).\n *\n * It is non-interactive (spec §4/§6): it takes no focus, binds no keys, is never a tab\n * stop, and renders no focus ring or target-size floor. An Avatar that must be clickable —\n * to open a profile or a menu — is not a new variant: wrap it in a Button or link that\n * owns the interaction, its keyboard model, and its focus ring; the Avatar stays a passive\n * image and is marked `decorative` inside the wrapper (spec §3/§6).\n */\nexport const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(\n function Avatar(\n { className, shape, size, bordered, src, alt, name, decorative, ...props },\n ref,\n ) {\n // Track the image load status so the LOADING state shows a Skeleton in the Avatar's\n // shape rather than a flash of empty container or guessed initials (spec §4). Radix\n // drives idle -> loading -> loaded/error; the initials/glyph fallback is the resting\n // state for any identity without a picture (a normal presentation, not an error).\n const [status, setStatus] = React.useState<\"idle\" | \"loading\" | \"loaded\" | \"error\">(\n \"idle\",\n );\n const isLoading = src !== undefined && (status === \"idle\" || status === \"loading\");\n const initials = name ? initialsFromName(name) : \"\";\n\n // The container is the single carrier of the accessible name (spec §7): role=\"img\"\n // + the alt names the identity in EVERY state (picture, loading, or fallback). When\n // decorative, the same name is shown beside it, so the container is aria-hidden and\n // everything inside is decorative — the identity is not announced twice.\n const identity = decorative\n ? { \"aria-hidden\": true as const }\n : { role: \"img\", \"aria-label\": alt };\n\n return (\n <AvatarPrimitive.Root\n ref={ref}\n className={cn(avatarVariants({ shape, size, bordered }), className)}\n {...identity}\n {...props}\n >\n {src !== undefined ? (\n // the picture: rendered by Radix only once it has loaded, cropped to the shape.\n // it is decorative — the container's accessible name already carries the\n // identity, so the picture is not announced as a second name (spec §7).\n <AvatarPrimitive.Image\n src={src}\n alt=\"\"\n aria-hidden=\"true\"\n className={avatarImageClass}\n onLoadingStatusChange={setStatus}\n />\n ) : null}\n\n {isLoading ? (\n // LOADING (spec §4): a Skeleton in the Avatar's shape while the image is fetched.\n // It is decorative (the Skeleton itself is aria-hidden); the picture or the\n // fallback replaces it on resolve.\n <Skeleton\n variant=\"circle\"\n data-testid=\"avatar-skeleton\"\n className=\"absolute inset-0 rounded-[inherit]\"\n />\n ) : (\n // FALLBACK (spec §2/§4): the resting state for an identity without a picture, and\n // the state the image fails into. The initials (or a neutral generic glyph) are\n // DECORATIVE — the fallback letters are not exposed as their own text; the\n // container's accessible name carries the identity, so a screen reader hears the\n // name, not the letters (spec §7). Neutral surface + secondary text only — never a\n // status color and never the brand (spec §3/§5/§8, brand != state).\n <AvatarPrimitive.Fallback\n data-testid=\"avatar-fallback\"\n aria-hidden=\"true\"\n className={avatarFallbackClass}\n >\n {initials ? initials : <GenericGlyph />}\n </AvatarPrimitive.Fallback>\n )}\n </AvatarPrimitive.Root>\n );\n },\n);\n",
10
+ "path": "avatar/avatar.tsx",
11
+ "target": "@ui/avatar/avatar.tsx",
12
+ "type": "registry:ui"
13
+ },
14
+ {
15
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// An Avatar is a small, NON-INTERACTIVE image standing in for an identity — a person\n// or an AI agent (spec §1). It is NOT a control: no focus ring, no target-size floor,\n// no state transition of its own (spec §4/§5/§6). Its variants are about size and\n// shape, never meaning — an Avatar carries no status, so it has no status variant\n// (spec §3).\n//\n// Neutrals carry the surface here. The fallback fill is the neutral raised surface and\n// the initials/glyph are the secondary text role; the optional container border is the\n// muted border role. An Avatar binds NOTHING from the status tier and NOTHING from the\n// action tier — it reports no state and carries no brand, so it can never be read as a\n// verification signal (brand != state; identity is not credentials, spec §1/§3/§8). The\n// verified status is the VerifiedBadge adornment placed beside it, never painted here.\nexport const avatarVariants = cva(\n [\n // shape / layout: a self-contained unit that clips the image or holds the fallback,\n // centered, never shrinking in a flex row (spec §2)\n \"relative inline-flex shrink-0 items-center justify-center overflow-hidden\",\n // global-first: select-none keeps the decorative initials from being grabbed as text\n \"select-none\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3: shape, not meaning.\n shape: {\n // circle (default): the standard identity Avatar — full radius (spec §3/§5)\n circle: \"rounded-(--radius-full)\",\n // rounded: a square-with-soft-corners Avatar for an organization or non-person\n // entity, where a circle would read as a person — the md radius (spec §3/§5)\n rounded: \"rounded-(--radius-md)\",\n },\n // STRUCTURAL axis = spec §3: size. Tokens expose no avatar width scale, so the\n // footprint is a square sized by the type scale (DEC-B: density above the floor,\n // never a fixed control height); sm/md/lg differ by footprint + initials size.\n size: {\n sm: \"h-(--space-7) w-(--space-7)\",\n md: \"h-(--space-9) w-(--space-9)\",\n lg: \"h-(--space-12) w-(--space-12)\",\n },\n // optional subtle border separating the Avatar from a same-colored surface\n // (spec §2/§5). The muted border role meets the 3:1 non-text-contrast bar (1.4.11),\n // so the edge is visible without relying on color (spec §7).\n bordered: {\n true: \"border border-surface-border-muted\",\n false: \"\",\n },\n },\n defaultVariants: { shape: \"circle\", size: \"md\", bordered: false },\n },\n);\n\n// The fallback (spec §2/§4): what shows when there is no image, or the image failed to\n// load — the identity's initials, or a neutral generic glyph when no name is available.\n// It is a placeholder for a missing picture, NOT a status or a category: a neutral raised\n// surface fill behind secondary-text initials/glyph, NEVER a status color and NEVER the\n// brand (spec §2/§3/§5/§8). It fills the container shape and inherits its clip.\nexport const avatarFallbackClass =\n \"absolute inset-0 flex items-center justify-center \" +\n \"bg-surface-raised text-text-secondary text-caption font-medium uppercase\";\n\n// The rendered picture (spec §2): fills the container and is cropped to its shape.\nexport const avatarImageClass = \"h-full w-full object-cover\";\n\n// The generic fallback glyph when no name is available (spec §5): sized at the md icon\n// role, decorative, in the same secondary text role as the initials.\nexport const avatarGlyphClass = \"h-(--size-icon-md) w-(--size-icon-md)\";\n\nexport type AvatarVariantProps = VariantProps<typeof avatarVariants>;\n",
16
+ "path": "avatar/avatar.variants.ts",
17
+ "target": "@ui/avatar/avatar.variants.ts",
18
+ "type": "registry:ui"
19
+ },
20
+ {
21
+ "content": "export { Avatar, type AvatarProps, type AvatarShape, type AvatarSize } from \"./avatar\";\nexport {\n avatarVariants,\n avatarFallbackClass,\n avatarImageClass,\n avatarGlyphClass,\n type AvatarVariantProps,\n} from \"./avatar.variants\";\n",
22
+ "path": "avatar/index.ts",
23
+ "target": "@ui/avatar/index.ts",
24
+ "type": "registry:ui"
25
+ }
26
+ ],
27
+ "name": "avatar",
28
+ "registryDependencies": [
29
+ "@verdify/cn",
30
+ "@verdify/skeleton"
31
+ ],
32
+ "title": "avatar",
33
+ "type": "registry:ui"
34
+ }
@@ -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": "import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { badgeVariants, badgeIconClass, type BadgeVariantProps } from \"./badge.variants\";\n\n/** The status a Badge can report (spec §3). Omit for the default `neutral` Badge. */\nexport type BadgeStatus = Exclude<NonNullable<BadgeVariantProps[\"variant\"]>, \"neutral\">;\n\nexport interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {\n /**\n * The state the Badge reports (spec §3). Omit for the default `neutral` Badge —\n * a plain category, tag, or count with no state meaning. A status color is spent\n * only when the Badge reports a real state. The brand color is never a Badge\n * variant; for a first-class verified result use the VerifiedBadge molecule.\n */\n status?: BadgeStatus;\n /**\n * One small leading glyph that reinforces the label (spec §2). Decorative and\n * `aria-hidden` — the label still carries the meaning if the icon is dropped, so\n * the meaning never rests on color (or icon) alone.\n */\n icon?: React.ReactNode;\n}\n\n/**\n * A small, non-interactive inline label that classifies or counts the thing it\n * sits on — a category, a count, or a status (spec §1). It carries meaning, not\n * action: it never receives focus and does nothing when clicked. A Badge is\n * `neutral` by default and reaches for a status color only when it reports a real\n * state, because brand and status colors are accents and neutrals carry the\n * surface. A Badge that reports the in-product verified result is the\n * VerifiedBadge molecule, not a hand-colored Badge.\n */\nexport const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(\n function Badge({ className, status, icon, children, \"aria-label\": ariaLabel, ...props }, ref) {\n const variant = status ?? \"neutral\";\n // The label text MUST reach the accessibility tree (spec §7): when there is a visible\n // word, that text is the accessible name and no role is needed (the spec default). When\n // the Badge is shown icon-and-color only — no visible word — the meaning can only be\n // carried by the caller's aria-label, but `aria-label` on a roleless <span> is prohibited\n // by ARIA (aria-prohibited-attr) and would be silently dropped, leaving the state unnamed\n // and resting on color alone — exactly what spec §1/§8 forbids. So give the icon-only\n // marker role=\"img\" (a graphical element with a text alternative, the same pattern Avatar\n // and AgentBadge use) to make the name valid and announced.\n const iconOnly = children == null && ariaLabel != null;\n return (\n // native <span>: inline text in a styled container. No tabIndex, no focus ring — a\n // Badge is a label, not a control (spec §6/§7). role=\"img\" only on the icon-only marker,\n // so the aria-label naming the state is valid and announced.\n <span\n ref={ref}\n className={cn(badgeVariants({ variant }), className)}\n role={iconOnly ? \"img\" : undefined}\n aria-label={ariaLabel}\n {...props}\n >\n {icon ? (\n <span data-testid=\"badge-icon\" className={badgeIconClass} aria-hidden=\"true\">\n {icon}\n </span>\n ) : null}\n {children}\n </span>\n );\n },\n);\n",
9
+ "path": "badge/badge.tsx",
10
+ "target": "@ui/badge/badge.tsx",
11
+ "type": "registry:ui"
12
+ },
13
+ {
14
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// A Badge is a small, non-interactive inline label (spec §1). It is a pill that\n// holds a label and an optional decorative icon — NOT a control: no focus ring,\n// no target-size floor, no state transition (spec §4/§5/§6).\n//\n// `variant` is the meaning the Badge carries (spec §3). `neutral` is the default\n// — restraint over volume; a status color is spent only when the Badge reports a\n// real state. The brand color (Sovereign Violet) is NEVER a Badge variant: the\n// brand is not a status, so the Badge family binds nothing from the action tier.\n//\n// Container fill: every variant — neutral AND each status — paints the SAME one\n// raised surface. The status trio's `-bg` resolves to that same surface, so the\n// meaning is carried by the fg (label + icon) and the border, never a saturated\n// fill (brand != state; color lives in fg/border, spec §3/§5).\nexport const badgeVariants = cva(\n [\n // shape / layout: a pill holding the optional icon + label at the small gap\n \"inline-flex items-center gap-(--space-1) rounded-(--radius-full) border px-(--space-1)\",\n // type ROLE — caption (spec §5); meaning never rests on color alone, so the\n // label text always reads on its own\n \"text-caption font-medium\",\n // global-first: never wrap, isolate so a monospace count stays LTR in RTL text\n \"whitespace-nowrap\",\n ],\n {\n variants: {\n // STRUCTURAL axis = spec §3 (the meaning the Badge carries)\n variant: {\n // neutral: a plain category/tag/count — neutral surface, text, border roles\n neutral: \"bg-surface-raised border-surface-border-muted text-text-secondary\",\n // status: the matching --color-status-* trio; bg is the neutral surface\n verified: \"bg-status-verified-bg border-status-verified-border text-status-verified-fg\",\n signal: \"bg-status-signal-bg border-status-signal-border text-status-signal-fg\",\n caution: \"bg-status-caution-bg border-status-caution-border text-status-caution-fg\",\n critical: \"bg-status-critical-bg border-status-critical-border text-status-critical-fg\",\n },\n },\n defaultVariants: { variant: \"neutral\" },\n },\n);\n\n// The optional leading icon (spec §2): one small decorative glyph at the sm icon\n// role. It inherits the variant fg via `currentColor`; the label still carries\n// the meaning if the icon is dropped.\nexport const badgeIconClass = \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0\";\n\nexport type BadgeVariantProps = VariantProps<typeof badgeVariants>;\n",
15
+ "path": "badge/badge.variants.ts",
16
+ "target": "@ui/badge/badge.variants.ts",
17
+ "type": "registry:ui"
18
+ },
19
+ {
20
+ "content": "export { Badge, type BadgeProps, type BadgeStatus } from \"./badge\";\nexport { badgeVariants, badgeIconClass, type BadgeVariantProps } from \"./badge.variants\";\n",
21
+ "path": "badge/index.ts",
22
+ "target": "@ui/badge/index.ts",
23
+ "type": "registry:ui"
24
+ }
25
+ ],
26
+ "name": "badge",
27
+ "registryDependencies": [
28
+ "@verdify/cn"
29
+ ],
30
+ "title": "badge",
31
+ "type": "registry:ui"
32
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [
4
+ "class-variance-authority@^0.7.0",
5
+ "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 breadcrumbNavClass,\n breadcrumbListClass,\n breadcrumbItemClass,\n breadcrumbLinkClass,\n breadcrumbPageClass,\n breadcrumbSeparatorClass,\n breadcrumbEllipsisClass,\n} from \"./breadcrumb.variants\";\n\nexport interface BreadcrumbProps extends React.ComponentPropsWithoutRef<\"nav\"> {\n /**\n * The landmark's accessible name (spec §7). Defaults to `\"Breadcrumb\"` so the trail is\n * distinguishable from other `navigation` landmarks on the page. Override it (or point\n * `aria-labelledby` at a visible label) when the app names its trails differently.\n */\n \"aria-label\"?: string;\n}\n\n/**\n * A Breadcrumb shows where the current page sits in the hierarchy and lets you step back up it\n * (spec §1). It is the `navigation` landmark wrapping an ordered trail of links from a root to\n * the current page. It is NOT primary navigation and NOT a wizard's step indicator — reach for\n * a Sidebar or Tabs when the choices are siblings rather than ancestors.\n *\n * The trail is structural wayfinding in neutral text (spec §3): it never carries the brand\n * violet or a status color, because a crumb reports a location in the hierarchy, not a\n * verification result. Coloring a crumb with a status hue would break brand != state.\n *\n * It is render-only (no hook / no stateful Radix primitive), so it needs no `'use client'`.\n */\nexport const Breadcrumb = React.forwardRef<HTMLElement, BreadcrumbProps>(function Breadcrumb(\n { className, \"aria-label\": ariaLabel = \"Breadcrumb\", ...props },\n ref,\n) {\n return (\n // the navigation landmark, named so a screen reader can jump to the breadcrumb directly\n <nav\n ref={ref}\n aria-label={ariaLabel}\n className={cn(breadcrumbNavClass, className)}\n {...props}\n />\n );\n});\n\nexport type BreadcrumbListProps = React.ComponentPropsWithoutRef<\"ol\">;\n\n/**\n * The trail's ordered list (spec §2): order is the meaning here, root -> current, so it is an\n * `<ol>`, not a loose group. Holds the alternating `BreadcrumbItem` / `BreadcrumbSeparator`\n * sequence.\n */\nexport const BreadcrumbList = React.forwardRef<HTMLOListElement, BreadcrumbListProps>(\n function BreadcrumbList({ className, ...props }, ref) {\n return <ol ref={ref} className={cn(breadcrumbListClass, className)} {...props} />;\n },\n);\n\nexport type BreadcrumbItemProps = React.ComponentPropsWithoutRef<\"li\">;\n\n/**\n * One ancestor in the trail (spec §2): a `<li>` holding either a `BreadcrumbLink` (an ancestor\n * you can return to) or the `BreadcrumbPage` (the current page).\n */\nexport const BreadcrumbItem = React.forwardRef<HTMLLIElement, BreadcrumbItemProps>(\n function BreadcrumbItem({ className, ...props }, ref) {\n return <li ref={ref} className={cn(breadcrumbItemClass, className)} {...props} />;\n },\n);\n\nexport interface BreadcrumbLinkProps extends React.ComponentPropsWithoutRef<\"a\"> {\n /**\n * Project the link styling onto a caller-supplied anchor (a framework router `<Link>` rendered\n * as an `<a>`) via Radix Slot, instead of the default native `<a>` (spec §2). Slot runs\n * `React.Children.only` — pass exactly one anchor child.\n */\n asChild?: boolean;\n /**\n * The ancestor cannot be returned to in the current context (spec §4, rare). The item dims via\n * the disabled TOKEN (DEC-C), is removed from the tab order, and drops its `href` so it cannot\n * navigate — while its label stays readable to assistive technology. Do not disable the\n * current page; it is already non-interactive by being the current page.\n */\n disabled?: boolean;\n}\n\n/**\n * A link to an ancestor (spec §2/§4). A native `<a>` so it exposes the link role and is operable\n * without extra wiring; it gets the visible focus ring, the restrained ghost hover fill, and the\n * target-size floor. Use `asChild` to project the styling onto a router anchor.\n */\nexport const BreadcrumbLink = React.forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(\n function BreadcrumbLink({ className, asChild = false, disabled = false, href, ...props }, ref) {\n const Comp = asChild ? Slot.Root : \"a\";\n return (\n <Comp\n ref={ref}\n // a disabled ancestor drops its href (cannot navigate), leaves the tab order, and is\n // marked aria-disabled so AT still reads its label (spec §4/§7)\n href={disabled ? undefined : href}\n aria-disabled={disabled || undefined}\n tabIndex={disabled ? -1 : undefined}\n className={cn(breadcrumbLinkClass(), className)}\n {...props}\n />\n );\n },\n);\n\nexport type BreadcrumbPageProps = React.ComponentPropsWithoutRef<\"span\">;\n\n/**\n * The current page (spec §2/§4): the last item, plain text in the primary color carrying\n * `aria-current=\"page\"`. It is NOT a link and is non-interactive — no focus ring, no\n * target-size floor, not focusable — so a screen-reader user is never told the page they are on\n * is somewhere to go.\n */\nexport const BreadcrumbPage = React.forwardRef<HTMLSpanElement, BreadcrumbPageProps>(\n function BreadcrumbPage({ className, ...props }, ref) {\n return (\n <span\n ref={ref}\n // the only item with aria-current; plain text, not a link (spec §7)\n aria-current=\"page\"\n className={cn(breadcrumbPageClass, className)}\n {...props}\n />\n );\n },\n);\n\nexport interface BreadcrumbSeparatorProps extends React.ComponentPropsWithoutRef<\"li\"> {\n /** The separator glyph (spec §2). Defaults to a chevron; pass a slash or other glyph to override. */\n children?: React.ReactNode;\n}\n\n/**\n * The glyph between two items (spec §2/§4): decoration only, in the muted text color. It carries\n * `aria-hidden` and `role=\"presentation\"` so it is removed from the accessibility tree — the\n * trail does not read as \"Home slash Billing slash Invoice\". It is a list item structurally but\n * is not announced as one.\n */\nexport const BreadcrumbSeparator = React.forwardRef<HTMLLIElement, BreadcrumbSeparatorProps>(\n function BreadcrumbSeparator({ className, children, ...props }, ref) {\n return (\n <li\n ref={ref}\n role=\"presentation\"\n aria-hidden=\"true\"\n data-testid=\"breadcrumb-separator\"\n className={cn(breadcrumbSeparatorClass, className)}\n {...props}\n >\n {children ?? <ChevronGlyph />}\n </li>\n );\n },\n);\n\nexport type BreadcrumbEllipsisProps = React.ComponentPropsWithoutRef<\"span\">;\n\n/**\n * The overflow indicator for the collapsed variant (spec §2): an ellipsis standing in for the\n * middle of a long trail when the path is too long for its container.\n *\n * It renders here as a DECORATIVE, non-interactive glyph in the muted text color. The spec's\n * interactive overflow (a Menu button that reveals the hidden ancestors and opens them in a Menu,\n * spec §2/§6/§7) is intentionally NOT wired here: the library has no Menu / Popover primitive\n * yet, and the build order places Navigation overlays after this trail. Once a Menu primitive\n * lands, this slot becomes the Menu trigger (a `button` with `aria-haspopup=\"menu\"` and\n * `aria-expanded`). Until then, render the collapsed middle as this decorative indicator and keep\n * the root and current page visible alongside it.\n */\nexport const BreadcrumbEllipsis = React.forwardRef<HTMLSpanElement, BreadcrumbEllipsisProps>(\n function BreadcrumbEllipsis({ className, children, ...props }, ref) {\n return (\n <span\n ref={ref}\n aria-hidden=\"true\"\n data-testid=\"breadcrumb-ellipsis\"\n className={cn(breadcrumbEllipsisClass, className)}\n {...props}\n >\n {children ?? <EllipsisGlyph />}\n </span>\n );\n },\n);\n\n/** The default separator glyph — a chevron pointing in the reading direction. Decorative. */\nfunction ChevronGlyph() {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <path d=\"M6 4l4 4-4 4\" />\n </svg>\n );\n}\n\n/** The default overflow glyph — three dots. Decorative. */\nfunction EllipsisGlyph() {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <circle cx=\"3\" cy=\"8\" r=\"1.25\" />\n <circle cx=\"8\" cy=\"8\" r=\"1.25\" />\n <circle cx=\"13\" cy=\"8\" r=\"1.25\" />\n </svg>\n );\n}\n",
10
+ "path": "breadcrumb/breadcrumb.tsx",
11
+ "target": "@ui/breadcrumb/breadcrumb.tsx",
12
+ "type": "registry:ui"
13
+ },
14
+ {
15
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\n// A Breadcrumb is structural wayfinding in NEUTRAL text (spec §3): it names the trail from a\n// root to the current page. It has no status-colored and no brand-accented variant at rest —\n// a crumb reports a location in the hierarchy, not a verification result, so it binds nothing\n// from the status tier and nothing from the action(brand) tier. The trail paints from the\n// text, ghost-action, border, and surface aliases only (spec §5).\n\n// The nav landmark wrapping the trail. A neutral canvas surface with logical-property block\n// padding so the trail mirrors under dir=\"rtl\" (G-U6). The optional hairline separating the\n// breadcrumb from the content below it (spec §5, border-default) is a caller decision, not a\n// default binding — apply it via className where the surface needs the divide.\nexport const breadcrumbNavClass = \"bg-surface-canvas py-(--space-2)\";\n\n// The ordered list: root -> current. Inline row of items + separators, wrapping when narrow,\n// with the small inter-item gap. The list itself carries no text color — each item sets its own.\nexport const breadcrumbListClass =\n \"flex flex-wrap items-center gap-(--space-1) text-caption\";\n\n// A single trail item (the <li>). Inline so the item and its trailing separator sit on one row.\nexport const breadcrumbItemClass = \"inline-flex items-center gap-(--space-1)\";\n\n// The link item (spec §4). At rest it is the trail label type role in the SECONDARY text color;\n// on hover it takes a restrained ghost fill and the label lifts to the PRIMARY text color (the\n// ghost-action hover fill is the only fill a crumb ever paints). The focus ring is part of the\n// base, on every state, never removed. Motion is the fast token transition on the verdify easing,\n// collapsing to the instant endpoint under reduced motion — never the 350ms VerifiedBadge-only\n// theatre duration. A disabled (non-returnable) ancestor dims via the disabled TOKEN, not a\n// blanket opacity (DEC-C), and is taken out of the tab order + pointer flow by the component.\nexport const breadcrumbLinkClass = cva(\n [\n // type ROLE + resting color; logical inline padding + the small icon gap for an optional glyph\n \"inline-flex items-center gap-(--space-1) rounded-(--radius-sm) px-(--space-1)\",\n \"text-caption text-text-secondary\",\n // hover: the restrained ghost fill, label lifts to the primary text color, pointer cursor\n \"cursor-pointer hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n // active text where the ghost treatment applies (spec §5)\n \"active:text-action-ghost-fg\",\n // motion: fast + verdify easing, instant under reduced motion (NEVER the check theatre)\n \"transition-[color,background-color] duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n \"motion-reduce:duration-(--motion-duration-instant)\",\n // target-size floor — 44px touch / 40px pointer (spec §7, 2.5.8)\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // focus ring — identical on every state, never removed (spec §4 / 2.4.7)\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // disabled (non-returnable) ancestor — DEC-C: dim via the disabled TOKEN, never opacity.\n // aria-disabled drives it because a breadcrumb ancestor is an <a>, which has no native\n // disabled; the component also strips href + tabindex so it cannot navigate or be tabbed to.\n \"aria-disabled:pointer-events-none aria-disabled:text-text-disabled\",\n ],\n { variants: {}, defaultVariants: {} },\n);\n\n// The current page (spec §2/§4/§5): the last item. PLAIN text in the PRIMARY color, NOT a link —\n// no focus ring, no target-size floor, not focusable. The non-interactive guidance: a current\n// crumb is a label, not a control.\nexport const breadcrumbPageClass =\n \"inline-flex items-center gap-(--space-1) px-(--space-1) text-caption text-text-primary\";\n\n// The separator glyph between two items (spec §2/§4): decoration only, in the MUTED text color,\n// at the sm icon role. Removed from the a11y tree by the component (aria-hidden + role=presentation)\n// so the trail is not announced as \"Home slash Billing slash Invoice\".\nexport const breadcrumbSeparatorClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center text-text-muted\";\n\n// The overflow indicator for the collapsed variant (spec §2): the ellipsis standing in for the\n// middle of a long trail. It renders here as a DECORATIVE, non-interactive glyph in the muted\n// text color — the interactive overflow Menu (a Menu button revealing the hidden ancestors)\n// defers to the Menu primitive, which the library has not built yet (see component TSDoc).\nexport const breadcrumbEllipsisClass =\n \"inline-flex h-(--size-icon-sm) w-(--size-icon-sm) shrink-0 items-center justify-center text-text-muted\";\n\nexport type BreadcrumbLinkVariantProps = VariantProps<typeof breadcrumbLinkClass>;\n",
16
+ "path": "breadcrumb/breadcrumb.variants.ts",
17
+ "target": "@ui/breadcrumb/breadcrumb.variants.ts",
18
+ "type": "registry:ui"
19
+ },
20
+ {
21
+ "content": "export {\n Breadcrumb,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbPage,\n BreadcrumbSeparator,\n BreadcrumbEllipsis,\n type BreadcrumbProps,\n type BreadcrumbListProps,\n type BreadcrumbItemProps,\n type BreadcrumbLinkProps,\n type BreadcrumbPageProps,\n type BreadcrumbSeparatorProps,\n type BreadcrumbEllipsisProps,\n} from \"./breadcrumb\";\nexport {\n breadcrumbNavClass,\n breadcrumbListClass,\n breadcrumbItemClass,\n breadcrumbLinkClass,\n breadcrumbPageClass,\n breadcrumbSeparatorClass,\n breadcrumbEllipsisClass,\n type BreadcrumbLinkVariantProps,\n} from \"./breadcrumb.variants\";\n",
22
+ "path": "breadcrumb/index.ts",
23
+ "target": "@ui/breadcrumb/index.ts",
24
+ "type": "registry:ui"
25
+ }
26
+ ],
27
+ "name": "breadcrumb",
28
+ "registryDependencies": [
29
+ "@verdify/cn"
30
+ ],
31
+ "title": "breadcrumb",
32
+ "type": "registry:ui"
33
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "dependencies": [
4
+ "class-variance-authority@^0.7.0",
5
+ "radix-ui@^1.1.0"
6
+ ],
7
+ "files": [
8
+ {
9
+ "content": "import * as React from \"react\";\nimport { Slot } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\nimport { buttonVariants, type ButtonVariantProps } from \"./button.variants\";\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n ButtonVariantProps {\n /** Render the child element (e.g. an <a>) with button styling via Radix Slot. */\n asChild?: boolean;\n /** Action is in flight: sets aria-busy, swaps the leading icon for the spinner. */\n loading?: boolean;\n}\n\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n function Button(\n { className, variant, asChild = false, loading = false, disabled, type, children, ...props },\n ref,\n ) {\n const Comp = asChild ? Slot.Root : \"button\";\n const isButton = !asChild;\n return (\n <Comp\n ref={ref}\n // native button gets type=\"button\" by default; Slot forwards the child's role\n type={isButton ? (type ?? \"button\") : undefined}\n // loading is non-interactive to prevent a double submit\n disabled={isButton ? (disabled ?? false) || loading : undefined}\n aria-busy={loading || undefined}\n className={cn(buttonVariants({ variant }), className)}\n {...props}\n >\n {/*\n Slot.Root runs React.Children.only, which throws on a [spinner, children] array.\n In the asChild branch the single caller child passes through untouched; the loading\n spinner renders only in the native-<button> path (shadcn's canonical pattern).\n button.md does not require asChild + loading together.\n */}\n {isButton ? (\n <>\n {loading ? (\n <span\n data-testid=\"button-spinner\"\n aria-hidden=\"true\"\n className={cn(\n \"inline-block h-(--size-icon-sm) w-(--size-icon-sm) animate-spin\",\n \"rounded-full border-2 border-current border-r-transparent\",\n \"[animation-duration:var(--motion-duration-ambient)]\",\n \"motion-reduce:animate-none\",\n )}\n />\n ) : null}\n {children}\n </>\n ) : (\n children\n )}\n </Comp>\n );\n },\n);\n",
10
+ "path": "button/button.tsx",
11
+ "target": "@ui/button/button.tsx",
12
+ "type": "registry:ui"
13
+ },
14
+ {
15
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\";\n\nexport const buttonVariants = cva(\n [\n // shape, type role, layout, icon-to-label gap, horizontal padding, elevation\n \"inline-flex items-center justify-center gap-2 rounded-md px-4 shadow-sm\",\n \"text-label font-medium whitespace-nowrap select-none\",\n // hover/pressed transition — fast + verdify easing, no theatre\n \"transition-colors duration-(--motion-duration-fast) ease-(--motion-easing-verdify)\",\n // target-size floor: 44px touch, 40px pointer — logical block-size\n \"min-h-(--size-target-mobile) sm:min-h-(--size-target-desktop)\",\n // visible 2px signal-blue ring at 2px offset, on every variant, never removed\n \"outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2\",\n // disabled: reduced-emphasis label, no pointer, no hover/pressed response\n \"disabled:pointer-events-none disabled:text-text-disabled disabled:shadow-none\",\n \"aria-busy:pointer-events-none\",\n ],\n {\n variants: {\n variant: {\n primary: [\n \"bg-action-primary-bg text-action-primary-fg border border-action-primary-border\",\n \"hover:bg-action-primary-bg-hover active:bg-action-primary-bg-active\",\n ],\n secondary: [\n \"bg-action-secondary-bg text-action-secondary-fg border border-action-secondary-border\",\n \"hover:bg-action-secondary-bg-hover\",\n ],\n ghost: [\n \"bg-transparent text-action-ghost-fg border border-transparent\",\n \"hover:bg-action-ghost-bg-hover\",\n ],\n destructive: [\n \"bg-action-destructive-bg text-action-destructive-fg\",\n \"border border-action-destructive-border\",\n ],\n },\n },\n defaultVariants: { variant: \"primary\" },\n },\n);\n\nexport type ButtonVariantProps = VariantProps<typeof buttonVariants>;\n",
16
+ "path": "button/button.variants.ts",
17
+ "target": "@ui/button/button.variants.ts",
18
+ "type": "registry:ui"
19
+ },
20
+ {
21
+ "content": "export { Button, type ButtonProps } from \"./button\";\nexport { buttonVariants, type ButtonVariantProps } from \"./button.variants\";\n",
22
+ "path": "button/index.ts",
23
+ "target": "@ui/button/index.ts",
24
+ "type": "registry:ui"
25
+ }
26
+ ],
27
+ "name": "button",
28
+ "registryDependencies": [
29
+ "@verdify/cn"
30
+ ],
31
+ "title": "button",
32
+ "type": "registry:ui"
33
+ }