@elabs-ai/components-ui 4.0.0 → 4.1.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 (223) hide show
  1. package/README.md +10 -8
  2. package/dist/chunk-JKPPTL63.js +48 -0
  3. package/dist/chunk-JKPPTL63.js.map +1 -0
  4. package/dist/form.d.ts +28 -0
  5. package/dist/form.js +123 -0
  6. package/dist/form.js.map +1 -0
  7. package/dist/index.d.ts +2485 -167
  8. package/dist/index.js +10716 -2706
  9. package/dist/index.js.map +1 -1
  10. package/package.json +21 -7
  11. package/src/_peer-deps.test.ts +59 -0
  12. package/src/blocks-comparison-table.stories.tsx +2 -2
  13. package/src/components/accordion/accordion.tsx +1 -1
  14. package/src/components/advanced-group/advanced-group.tsx +1 -1
  15. package/src/components/alert/alert.tsx +4 -1
  16. package/src/components/app-shell/app-shell.stories.tsx +44 -2
  17. package/src/components/app-shell/app-shell.tsx +35 -2
  18. package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
  19. package/src/components/attribution-panel/attribution-panel.tsx +1 -1
  20. package/src/components/attribution-panel/attributions.generated.ts +81 -77
  21. package/src/components/badge/badge.stories.tsx +8 -0
  22. package/src/components/badge/badge.tsx +1 -1
  23. package/src/components/bento-grid/bento-grid.test.tsx +1 -1
  24. package/src/components/bento-grid/bento-grid.tsx +1 -1
  25. package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
  26. package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
  27. package/src/components/breadcrumb/breadcrumb.tsx +7 -2
  28. package/src/components/button/button.stories.tsx +116 -8
  29. package/src/components/button/button.tsx +1 -1
  30. package/src/components/card/card.stories.tsx +11 -6
  31. package/src/components/card/card.tsx +1 -1
  32. package/src/components/change-review/change-review.stories.tsx +145 -1
  33. package/src/components/change-review/change-review.test.tsx +143 -0
  34. package/src/components/change-review/change-review.tsx +185 -4
  35. package/src/components/checkbox/checkbox.tsx +1 -1
  36. package/src/components/color-picker/color-picker.tsx +6 -5
  37. package/src/components/command/command.stories.tsx +16 -6
  38. package/src/components/command/command.test.tsx +73 -0
  39. package/src/components/command/command.tsx +30 -7
  40. package/src/components/command/index.ts +1 -0
  41. package/src/components/command-trigger/command-trigger.stories.tsx +39 -0
  42. package/src/components/command-trigger/command-trigger.test.tsx +18 -0
  43. package/src/components/command-trigger/command-trigger.tsx +70 -0
  44. package/src/components/command-trigger/index.ts +1 -0
  45. package/src/components/context-rail/context-rail.stories.tsx +285 -0
  46. package/src/components/context-rail/context-rail.test.tsx +263 -0
  47. package/src/components/context-rail/context-rail.tsx +678 -0
  48. package/src/components/context-rail/index.ts +1 -0
  49. package/src/components/copyable-value/copyable-value.tsx +1 -1
  50. package/src/components/dialog/dialog.tsx +2 -2
  51. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
  52. package/src/components/empty-state/empty-state.test.tsx +26 -0
  53. package/src/components/empty-state/empty-state.tsx +6 -1
  54. package/src/components/expand-dialog/expand-dialog.tsx +1 -2
  55. package/src/components/field/field-context.ts +59 -0
  56. package/src/components/field/field.stories.tsx +142 -0
  57. package/src/components/field/field.test.tsx +415 -0
  58. package/src/components/field/field.tsx +340 -0
  59. package/src/components/field/index.ts +9 -0
  60. package/src/components/field-row/field-row.stories.tsx +85 -1
  61. package/src/components/field-row/field-row.test.tsx +145 -0
  62. package/src/components/field-row/field-row.tsx +48 -6
  63. package/src/components/file-upload/file-upload.stories.tsx +16 -0
  64. package/src/components/file-upload/file-upload.test.tsx +7 -2
  65. package/src/components/file-upload/file-upload.tsx +15 -4
  66. package/src/components/form/form.tsx +5 -2
  67. package/src/components/icon-button/icon-button.test.tsx +1 -1
  68. package/src/components/icon-button/icon-button.tsx +2 -2
  69. package/src/components/input/input.stories.tsx +121 -0
  70. package/src/components/input/input.test.tsx +13 -0
  71. package/src/components/input/input.tsx +2 -2
  72. package/src/components/input-group/input-group.stories.tsx +74 -0
  73. package/src/components/input-group/input-group.tsx +13 -4
  74. package/src/components/input-otp/input-otp.tsx +5 -1
  75. package/src/components/keyboard-shortcuts/index.ts +6 -0
  76. package/src/components/keyboard-shortcuts/keyboard-shortcuts.stories.tsx +177 -0
  77. package/src/components/keyboard-shortcuts/keyboard-shortcuts.test.tsx +73 -0
  78. package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +203 -0
  79. package/src/components/link-preview/link-preview.tsx +1 -1
  80. package/src/components/locale-provider/index.ts +6 -1
  81. package/src/components/locale-provider/locale-provider.stories.tsx +89 -1
  82. package/src/components/locale-provider/locale-provider.test.tsx +257 -1
  83. package/src/components/locale-provider/locale-provider.tsx +63 -8
  84. package/src/components/locale-provider/messages.ts +501 -2
  85. package/src/components/match-highlight/match-highlight.stories.tsx +1 -1
  86. package/src/components/mention-input/mention-input-mirror.stories.tsx +9 -1
  87. package/src/components/mention-input/mention-input.tsx +8 -38
  88. package/src/components/mention-input/mention-value.ts +8 -2
  89. package/src/components/metric-card/metric-card.stories.tsx +5 -3
  90. package/src/components/metric-card/metric-card.tsx +21 -2
  91. package/src/components/model-picker/model-picker.stories.tsx +63 -3
  92. package/src/components/model-picker/model-picker.test.tsx +63 -0
  93. package/src/components/model-picker/model-picker.tsx +62 -36
  94. package/src/components/nav-main/nav-main.tsx +1 -1
  95. package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
  96. package/src/components/nav-notifications/nav-notifications.tsx +57 -5
  97. package/src/components/navigation-menu/navigation-menu.stories.tsx +22 -0
  98. package/src/components/navigation-menu/navigation-menu.test.tsx +174 -0
  99. package/src/components/navigation-menu/navigation-menu.tsx +189 -12
  100. package/src/components/page-shell/page-shell.stories.tsx +163 -0
  101. package/src/components/page-shell/page-shell.test.tsx +229 -0
  102. package/src/components/page-shell/page-shell.tsx +108 -25
  103. package/src/components/pagination/pagination.test.tsx +90 -0
  104. package/src/components/pagination/pagination.tsx +9 -5
  105. package/src/components/radio-group/radio-group.tsx +1 -1
  106. package/src/components/rating/rating.tsx +1 -1
  107. package/src/components/resizable/resizable.stories.tsx +114 -2
  108. package/src/components/resizable/resizable.tsx +1 -1
  109. package/src/components/revision-timeline/revision-timeline.stories.tsx +8 -1
  110. package/src/components/revision-timeline/revision-timeline.test.tsx +26 -0
  111. package/src/components/revision-timeline/revision-timeline.tsx +46 -3
  112. package/src/components/schema-form/from-json-schema.test.ts +390 -0
  113. package/src/components/schema-form/from-json-schema.ts +375 -0
  114. package/src/components/schema-form/index.ts +81 -0
  115. package/src/components/schema-form/schema-form-spec.ts +698 -0
  116. package/src/components/schema-form/schema-form.stories.tsx +566 -0
  117. package/src/components/schema-form/schema-form.test.tsx +900 -0
  118. package/src/components/schema-form/schema-form.tsx +1545 -0
  119. package/src/components/section-header/section-header.stories.tsx +37 -0
  120. package/src/components/section-header/section-header.tsx +12 -1
  121. package/src/components/select/select.tsx +6 -2
  122. package/src/components/sheet/sheet.tsx +1 -1
  123. package/src/components/side-dock/index.ts +1 -0
  124. package/src/components/side-dock/side-dock.stories.tsx +137 -0
  125. package/src/components/side-dock/side-dock.test.tsx +273 -0
  126. package/src/components/side-dock/side-dock.tsx +502 -0
  127. package/src/components/sidebar/index.ts +1 -0
  128. package/src/components/sidebar/sidebar-frame.test.tsx +162 -0
  129. package/src/components/sidebar/sidebar.stories.tsx +421 -1
  130. package/src/components/sidebar/sidebar.test.tsx +36 -1
  131. package/src/components/sidebar/sidebar.tsx +245 -33
  132. package/src/components/skip-link/index.ts +1 -0
  133. package/src/components/skip-link/skip-link.stories.tsx +106 -0
  134. package/src/components/skip-link/skip-link.test.tsx +20 -0
  135. package/src/components/skip-link/skip-link.tsx +34 -0
  136. package/src/components/slider/slider.tsx +1 -1
  137. package/src/components/split-panel/index.ts +6 -1
  138. package/src/components/split-panel/split-panel.stories.tsx +18 -2
  139. package/src/components/split-panel/split-panel.test.tsx +20 -0
  140. package/src/components/split-panel/split-panel.tsx +28 -8
  141. package/src/components/state-panel/state-panel.stories.tsx +52 -0
  142. package/src/components/state-panel/state-panel.test.tsx +125 -0
  143. package/src/components/state-panel/state-panel.tsx +96 -17
  144. package/src/components/status-badge/status-badge.stories.tsx +8 -0
  145. package/src/components/switch/switch.tsx +1 -1
  146. package/src/components/table/table.stories.tsx +62 -0
  147. package/src/components/table/table.test.tsx +142 -1
  148. package/src/components/table/table.tsx +60 -1
  149. package/src/components/tabs/tabs.tsx +8 -2
  150. package/src/components/tag-input/tag-input.tsx +3 -2
  151. package/src/components/team-switcher/team-switcher.tsx +34 -5
  152. package/src/components/textarea/textarea.tsx +1 -1
  153. package/src/components/theme-switcher/theme-switcher.tsx +6 -2
  154. package/src/components/theme-switcher/use-theme-transition.ts +2 -2
  155. package/src/components/timeline/timeline.stories.tsx +27 -0
  156. package/src/components/timeline/timeline.tsx +11 -1
  157. package/src/components/toggle/toggle.tsx +1 -1
  158. package/src/components/toolbar/toolbar.stories.tsx +10 -2
  159. package/src/components/tree/tree.stories.tsx +2 -2
  160. package/src/components/tree/tree.tsx +14 -9
  161. package/src/components/typography/prose.tsx +1 -1
  162. package/src/components/typography/typography.stories.tsx +228 -0
  163. package/src/components/view-toolbar/view-toolbar.stories.tsx +41 -1
  164. package/src/components/view-toolbar/view-toolbar.test.tsx +33 -0
  165. package/src/components/view-toolbar/view-toolbar.tsx +37 -4
  166. package/src/components/virtual-select/virtual-select.test.tsx +1 -1
  167. package/src/components/wizard/wizard.tsx +1 -1
  168. package/src/components/workspace-picker/index.ts +3 -0
  169. package/src/components/workspace-picker/workspace-picker-state.ts +50 -0
  170. package/src/components/workspace-picker/workspace-picker.stories.tsx +126 -0
  171. package/src/components/workspace-picker/workspace-picker.test.tsx +207 -0
  172. package/src/components/workspace-picker/workspace-picker.tsx +172 -0
  173. package/src/illustrations/empty-list-illustration.tsx +26 -0
  174. package/src/illustrations/error-illustration.tsx +60 -0
  175. package/src/illustrations/first-run-illustration.tsx +27 -0
  176. package/src/illustrations/illustration-base.tsx +108 -0
  177. package/src/illustrations/illustrations.stories.tsx +94 -0
  178. package/src/illustrations/illustrations.test.tsx +98 -0
  179. package/src/illustrations/index.ts +16 -0
  180. package/src/illustrations/no-access-illustration.tsx +45 -0
  181. package/src/illustrations/no-results-illustration.tsx +46 -0
  182. package/src/illustrations/offline-illustration.tsx +37 -0
  183. package/src/illustrations/success-illustration.tsx +40 -0
  184. package/src/index.ts +75 -2
  185. package/src/lib/agent-event-model.test.ts +12 -0
  186. package/src/lib/agent-event-model.ts +42 -0
  187. package/src/lib/approval-option.test.ts +17 -0
  188. package/src/lib/approval-option.ts +53 -0
  189. package/src/lib/check-result.ts +33 -0
  190. package/src/lib/diff-line.test.ts +36 -0
  191. package/src/lib/diff-line.ts +63 -0
  192. package/src/lib/diff-rows.test.ts +76 -0
  193. package/src/lib/diff-rows.ts +92 -0
  194. package/src/lib/format-duration.test.ts +22 -0
  195. package/src/lib/format-duration.ts +30 -0
  196. package/src/lib/has-renderable-content.test.ts +49 -0
  197. package/src/lib/has-renderable-content.ts +32 -0
  198. package/src/lib/operating-mode.test.ts +36 -0
  199. package/src/lib/operating-mode.ts +81 -0
  200. package/src/lib/optional-peer.ts +59 -0
  201. package/src/lib/session-launch.ts +37 -0
  202. package/src/lib/slash-command.test.ts +44 -0
  203. package/src/lib/slash-command.ts +45 -0
  204. package/src/lib/trigger-query.test.ts +94 -0
  205. package/src/lib/trigger-query.ts +88 -0
  206. package/src/lib/use-mobile.ts +12 -6
  207. package/src/motion.stories.tsx +6 -0
  208. package/src/templates-object-detail-hub.stories.tsx +2 -2
  209. package/src/templates-screen-states.stories.tsx +5 -5
  210. package/src/templates-settings.stories.tsx +1 -1
  211. package/src/blocks/sidebar-02/app-sidebar.tsx +0 -160
  212. package/src/blocks/sidebar-02/logo.tsx +0 -18
  213. package/src/blocks/sidebar-02/nav-main.tsx +0 -13
  214. package/src/blocks/sidebar-02/nav-notifications.tsx +0 -7
  215. package/src/blocks/sidebar-02/sidebar-02.stories.tsx +0 -23
  216. package/src/blocks/sidebar-02/team-switcher.tsx +0 -7
  217. package/src/blocks/sidebar-04/app-sidebar.tsx +0 -236
  218. package/src/blocks/sidebar-04/mail-context.tsx +0 -29
  219. package/src/blocks/sidebar-04/nav-user.tsx +0 -6
  220. package/src/blocks/sidebar-04/sidebar-04.stories.tsx +0 -150
  221. package/src/blocks/sidebar-05/app-sidebar.tsx +0 -375
  222. package/src/blocks/sidebar-05/sidebar-05.stories.tsx +0 -18
  223. package/src/blocks/sidebar-05/team-switcher.tsx +0 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elabs-ai/components-ui",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,6 +21,10 @@
21
21
  "./lib/cn": {
22
22
  "types": "./dist/lib/cn.d.ts",
23
23
  "default": "./dist/lib/cn.js"
24
+ },
25
+ "./form": {
26
+ "types": "./dist/form.d.ts",
27
+ "default": "./dist/form.js"
24
28
  }
25
29
  },
26
30
  "publishConfig": {
@@ -28,7 +32,6 @@
28
32
  "registry": "https://registry.npmjs.org/"
29
33
  },
30
34
  "dependencies": {
31
- "@hookform/resolvers": "^3.9.1",
32
35
  "@radix-ui/react-accordion": "^1.2.2",
33
36
  "@radix-ui/react-alert-dialog": "^1.1.4",
34
37
  "@radix-ui/react-aspect-ratio": "^1.1.1",
@@ -65,19 +68,28 @@
65
68
  "input-otp": "^1.4.1",
66
69
  "lucide-react": "^0.577.0",
67
70
  "react-day-picker": "^9.4.4",
68
- "react-hook-form": "^7.54.2",
69
71
  "react-resizable-panels": "^2.1.7",
70
72
  "sonner": "^1.7.1",
71
73
  "tailwind-merge": "^2.6.0",
72
- "vaul": "^1.1.2",
73
- "zod": "^3.24.1"
74
+ "vaul": "^1.1.2"
74
75
  },
75
76
  "peerDependencies": {
77
+ "@hookform/resolvers": "^3.9.1",
76
78
  "react": "^18.2.0 || ^19.0.0",
77
79
  "react-dom": "^18.2.0 || ^19.0.0",
78
- "@elabs-ai/components-tokens": "4.0.0"
80
+ "react-hook-form": "^7.54.2",
81
+ "@elabs-ai/components-tokens": "4.1.0"
82
+ },
83
+ "peerDependenciesMeta": {
84
+ "@hookform/resolvers": {
85
+ "optional": true
86
+ },
87
+ "react-hook-form": {
88
+ "optional": true
89
+ }
79
90
  },
80
91
  "devDependencies": {
92
+ "@hookform/resolvers": "^3.9.1",
81
93
  "@testing-library/jest-dom": "^6.6.3",
82
94
  "@testing-library/react": "^16.1.0",
83
95
  "@testing-library/user-event": "^14.5.2",
@@ -88,12 +100,14 @@
88
100
  "jsdom": "^25.0.1",
89
101
  "react": "^19.0.0",
90
102
  "react-dom": "^19.0.0",
103
+ "react-hook-form": "^7.54.2",
91
104
  "tsup": "^8.3.5",
92
105
  "typescript": "^5.7.3",
93
106
  "vitest": "^3.0.2",
107
+ "zod": "^3.24.1",
94
108
  "@elabs-ai/components-eslint-config": "0.1.0",
95
109
  "@elabs-ai/components-typescript-config": "0.1.0",
96
- "@elabs-ai/components-tokens": "4.0.0"
110
+ "@elabs-ai/components-tokens": "4.1.0"
97
111
  },
98
112
  "scripts": {
99
113
  "build": "rm -rf dist && tsup",
@@ -0,0 +1,59 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ /**
7
+ * Regression guard for issue #26 — `react-hook-form` / `@hookform/resolvers`
8
+ * are used ONLY by the RHF-bound `Form` family
9
+ * (`packages/ui/src/components/form/form.tsx`); `FieldRow` and every other
10
+ * component in this package render from plain props and never import either.
11
+ *
12
+ * A consumer who never imports `Form` should not be forced to install (or
13
+ * have their bundler resolve) react-hook-form. Declaring both as
14
+ * `dependencies` defeats that — npm/pnpm install them unconditionally for
15
+ * every consumer of `@elabs-ai/components-ui`, regardless of whether `Form`
16
+ * is ever imported. This test locks the fix: both packages are declared as
17
+ * OPTIONAL peers, and neither reappears as a hard `dependency`.
18
+ *
19
+ * `zod` is used only inside `form.stories.tsx` (the `zodResolver` demo) and
20
+ * never in shipped runtime source — it belongs in `devDependencies`, never as
21
+ * a runtime `dependency` or a peer a consumer must install.
22
+ */
23
+ const PACKAGE_JSON_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
24
+
25
+ function readPackageJson(): {
26
+ dependencies?: Record<string, string>;
27
+ devDependencies?: Record<string, string>;
28
+ peerDependencies?: Record<string, string>;
29
+ peerDependenciesMeta?: Record<string, { optional?: boolean }>;
30
+ } {
31
+ return JSON.parse(readFileSync(PACKAGE_JSON_PATH, "utf8"));
32
+ }
33
+
34
+ describe("packages/ui/package.json — react-hook-form is an optional peer, not a hard dependency", () => {
35
+ const pkg = readPackageJson();
36
+
37
+ it.each(["react-hook-form", "@hookform/resolvers"])(
38
+ "declares %s as an OPTIONAL peerDependency",
39
+ (name) => {
40
+ expect(pkg.dependencies?.[name]).toBeUndefined();
41
+ expect(pkg.peerDependencies?.[name]).toBeTruthy();
42
+ expect(pkg.peerDependenciesMeta?.[name]?.optional).toBe(true);
43
+ },
44
+ );
45
+
46
+ it("still lists react-hook-form + @hookform/resolvers as devDependencies for the workspace's own build/test", () => {
47
+ // The package's own `Form` tests/stories (and `form.tsx` itself) need the
48
+ // real library to typecheck/run inside this workspace — peer alone does
49
+ // not guarantee resolution here, only for an external consumer.
50
+ expect(pkg.devDependencies?.["react-hook-form"]).toBeTruthy();
51
+ expect(pkg.devDependencies?.["@hookform/resolvers"]).toBeTruthy();
52
+ });
53
+
54
+ it("declares zod as a devDependency only — never a runtime dependency or peer", () => {
55
+ expect(pkg.dependencies?.zod).toBeUndefined();
56
+ expect(pkg.peerDependencies?.zod).toBeUndefined();
57
+ expect(pkg.devDependencies?.zod).toBeTruthy();
58
+ });
59
+ });
@@ -7,7 +7,7 @@
7
7
  * cells, a price row, and a per-plan CTA row. Reuses brand-ui's strongest
8
8
  * assets — Table + Badge + Button — and stays token-only.
9
9
  *
10
- * Reads in all three themes. Verify with globals=theme:<slug>.
10
+ * Reads in every theme. Verify with globals=theme:<slug>.
11
11
  */
12
12
  import type { Meta, StoryObj } from "@storybook/react-vite";
13
13
  import { Check, Minus } from "lucide-react";
@@ -152,7 +152,7 @@ const meta = {
152
152
  docs: {
153
153
  description: {
154
154
  component:
155
- "The canonical plan/feature comparison matrix (brand-ui's Compare gap): a header with one featured column (badge + recessed wash), feature rows with check / dash / value cells, a price row, and a per-plan CTA row. Composes Table + Badge + Button; semantic tokens only; reads in all three themes.",
155
+ "The canonical plan/feature comparison matrix (brand-ui's Compare gap): a header with one featured column (badge + recessed wash), feature rows with check / dash / value cells, a price row, and a per-plan CTA row. Composes Table + Badge + Button; semantic tokens only; reads in every theme.",
156
156
  },
157
157
  },
158
158
  },
@@ -21,7 +21,7 @@ export const AccordionTrigger = forwardRef<
21
21
  <AccordionPrimitive.Trigger
22
22
  ref={ref}
23
23
  className={cn(
24
- "flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&[data-state=open]>svg]:rotate-180",
24
+ "flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline focus-ring [&[data-state=open]>svg]:rotate-180",
25
25
  className,
26
26
  )}
27
27
  {...props}
@@ -85,7 +85,7 @@ export function AdvancedGroup({
85
85
  >
86
86
  <CollapsibleTrigger
87
87
  data-slot="advanced-group-trigger"
88
- className="group -mx-2 flex items-center gap-2 rounded-md px-2 py-1.5 text-body font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
88
+ className="group -mx-2 flex items-center gap-2 rounded-md px-2 py-1.5 text-body font-medium hover:bg-accent hover:text-accent-foreground focus-ring"
89
89
  >
90
90
  <ChevronDown
91
91
  aria-hidden="true"
@@ -11,8 +11,11 @@ export const alertVariants = cva(
11
11
  info: "border-info/40 bg-info/10 text-foreground",
12
12
  success: "border-success/40 bg-success/10 text-foreground",
13
13
  warning: "border-warning/50 bg-warning/10 text-foreground",
14
+ // #124: the bare `text-destructive` set the ambient colour for the
15
+ // Title/Description running text — ink rung. `[&>svg]:text-destructive`
16
+ // (the icon mark) is unchanged.
14
17
  destructive:
15
- "border-destructive/40 bg-destructive/10 text-destructive [&>svg]:text-destructive",
18
+ "border-destructive/40 bg-destructive/10 text-destructive-text [&>svg]:text-destructive",
16
19
  },
17
20
  },
18
21
  defaultVariants: { variant: "default" },
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, within } from "storybook/test";
2
3
  import { AppShell } from "./app-shell";
3
4
  import { TopNav } from "../top-nav";
4
5
  import { PageShell } from "../page-shell";
@@ -6,7 +7,7 @@ import { SectionHeader } from "../section-header";
6
7
  import { Button } from "../button";
7
8
 
8
9
  const meta = {
9
- title: "Layout/App Shell/Basic",
10
+ title: "Layout/App Shell/Minimal",
10
11
  component: AppShell,
11
12
  parameters: { layout: "fullscreen" },
12
13
  } satisfies Meta<typeof AppShell>;
@@ -46,8 +47,10 @@ export const Default: Story = {
46
47
  <PageShell
47
48
  header={
48
49
  <SectionHeader
50
+ // The page's own title, so it owns the document outline root.
51
+ as="h1"
49
52
  title="Overview"
50
- description="A lightweight generic shell. For full sidebar behavior use the Sidebar primitive."
53
+ description="A minimal, unstyled two-region shell — the honest answer for a simple layout. For full sidebar behavior use the Sidebar primitive; for a fully wired enterprise console see Layout/App Shell/Flagship."
51
54
  />
52
55
  }
53
56
  >
@@ -58,4 +61,43 @@ export const Default: Story = {
58
61
  </AppShell>
59
62
  </div>
60
63
  ),
64
+ play: async ({ canvasElement }) => {
65
+ const canvas = within(canvasElement);
66
+
67
+ // The shell ships a skip link, so a keyboard user can pass the nav rail
68
+ // instead of tabbing through it on every route (WCAG 2.4.1). Resolve the
69
+ // TARGET FROM THE LINK, never from a hard-coded `#main-content`: querying
70
+ // the id directly stays green when the link and the target are deleted
71
+ // together.
72
+ const skip = canvas.getByRole("link", { name: "Skip to main content" });
73
+ const targetId = (skip.getAttribute("href") ?? "").replace(/^#/, "");
74
+ await expect(targetId).not.toBe("");
75
+ const target = canvasElement.querySelector(`#${CSS.escape(targetId)}`) as HTMLElement;
76
+ await expect(target).toBeVisible();
77
+ await expect(target.tagName).toBe("MAIN");
78
+ await expect(canvas.getByRole("main")).toBe(target);
79
+
80
+ // …and here <main> IS the scroll port (in the registry blocks the skip
81
+ // target and the port are separate elements), so it takes a REAL tab stop,
82
+ // not the `-1` a pure skip target would carry: a region that scrolls has to
83
+ // be keyboard-operable even when nothing inside it is focusable (WCAG
84
+ // 2.1.1, axe `scrollable-region-focusable`). Read as an ATTRIBUTE — the
85
+ // `tabIndex` IDL getter answers -1 for any non-focusable element whether or
86
+ // not anyone set it, so it cannot tell "-1 was set" from "nothing was set".
87
+ await expect(target).toHaveAttribute("tabindex", "0");
88
+ await expect(getComputedStyle(target).overflowY).toBe("auto");
89
+ // The INSET rung: the shell root above carries `overflow-hidden`, which
90
+ // clips both layers of the plain `focus-ring`. Asserting only that
91
+ // `focus-ring-inset` is present would still pass with both classes on the
92
+ // element — and then the clipped one is what paints.
93
+ await expect(target.classList.contains("focus-ring-inset")).toBe(true);
94
+ await expect(target.classList.contains("focus-ring")).toBe(false);
95
+
96
+ // The outline has a root. axe cannot catch its absence here:
97
+ // `page-has-heading-one` is a best-practice rule outside the wcag2a/wcag2aa
98
+ // tag set, and `heading-order` passes happily on levels that start at 2.
99
+ const h1s = canvasElement.querySelectorAll("h1");
100
+ await expect(h1s.length).toBe(1);
101
+ await expect(canvas.getByRole("heading", { level: 1 })).toHaveTextContent("Overview");
102
+ },
61
103
  };
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { cn } from "../../lib/cn";
3
+ import { SkipLink } from "../skip-link";
3
4
 
4
5
  export interface AppShellProps {
5
6
  /** Persistent left navigation (often a <Sidebar />). */
@@ -10,21 +11,53 @@ export interface AppShellProps {
10
11
  className?: string;
11
12
  /** Class for the scrolling main content region. */
12
13
  mainClassName?: string;
14
+ /**
15
+ * The `<main>` region's id, and the skip link's target. Override it when a
16
+ * page mounts more than one shell — two elements sharing `main-content`
17
+ * would make the skip link ambiguous. @default "main-content"
18
+ */
19
+ mainId?: string;
13
20
  }
14
21
 
15
22
  /**
16
23
  * App-first layout: fixed-height viewport with an optional sidebar, a top bar,
17
24
  * and a scrollable main region. Pure layout — bring your own nav components.
18
25
  */
19
- export function AppShell({ sidebar, topNav, children, className, mainClassName }: AppShellProps) {
26
+ export function AppShell({
27
+ sidebar,
28
+ topNav,
29
+ children,
30
+ className,
31
+ mainClassName,
32
+ mainId = "main-content",
33
+ }: AppShellProps) {
20
34
  return (
21
35
  <div
22
36
  className={cn("flex h-dvh w-full overflow-hidden bg-background text-foreground", className)}
23
37
  >
38
+ {/* The first focusable element of the shell, so a keyboard user can pass
39
+ the nav rail instead of tabbing through it on every route (WCAG 2.4.1).
40
+ All four registry app-shell blocks ship one; the primitive they sit
41
+ beside must not be the exception, and it is the surface with the widest
42
+ reach — it is imported, not copy-owned. */}
43
+ <SkipLink targetId={mainId} />
24
44
  {sidebar ? <div className="hidden md:flex">{sidebar}</div> : null}
25
45
  <div className="flex min-w-0 flex-1 flex-col">
26
46
  {topNav}
27
- <main className={cn("min-h-0 flex-1 overflow-y-auto", mainClassName)}>{children}</main>
47
+ {/* `<main>` IS the scroll port here (in the blocks the skip target and
48
+ the scroll port are separate elements), so it takes `tabIndex={0}`
49
+ rather than `{-1}`: a region that scrolls has to be keyboard-operable
50
+ even when the content inside it is not focusable (WCAG 2.1.1, axe
51
+ `scrollable-region-focusable`). `focus-ring-inset`, not `focus-ring`
52
+ — the root above carries `overflow-hidden`, which clips both layers
53
+ of the plain rung. */}
54
+ <main
55
+ id={mainId}
56
+ tabIndex={0}
57
+ className={cn("min-h-0 flex-1 overflow-y-auto focus-ring-inset", mainClassName)}
58
+ >
59
+ {children}
60
+ </main>
28
61
  </div>
29
62
  </div>
30
63
  );
@@ -17,7 +17,15 @@ const meta = {
17
17
  title: "Layout/AppSidebar",
18
18
  component: AppSidebar,
19
19
  tags: ["autodocs"],
20
- parameters: { layout: "fullscreen" },
20
+ parameters: {
21
+ layout: "fullscreen",
22
+ docs: {
23
+ description: {
24
+ component:
25
+ "The opinionated application sidebar — the raw primitive set it composes is `Layout/Sidebar`; see [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). `AppSidebar` wraps the `Sidebar` → `SidebarHeader` → `SidebarContent` → `SidebarFooter` skeleton behind typed `header` / `footer` slots with the navigation as `children`, and forwards every `Sidebar` prop. The registry’s sidebar blocks import this rather than rebuilding the skeleton.",
26
+ },
27
+ },
28
+ },
21
29
  decorators: [
22
30
  (Story) => (
23
31
  <SidebarProvider>
@@ -74,7 +74,7 @@ function AttributionItem({ item, requiredLabel }: { item: Attribution; requiredL
74
74
  href={item.url}
75
75
  target="_blank"
76
76
  rel="noopener noreferrer"
77
- className="text-body font-medium underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-xs"
77
+ className="text-body font-medium underline underline-offset-2 focus-ring rounded-xs"
78
78
  >
79
79
  {item.name}
80
80
  </a>
@@ -2,7 +2,7 @@
2
2
  // Run `pnpm gen:attributions` after changing a dependency, a vendored font, or
3
3
  // scripts/attributions.sources.json. `pnpm attributions:check` fails on a stale copy.
4
4
  //
5
- // 116 entries (data: 2, source: 16, font: 2, dependency: 96).
5
+ // 116 entries (data: 2, source: 18, font: 2, dependency: 94).
6
6
 
7
7
  import type { Attribution } from "./attribution-types";
8
8
 
@@ -93,14 +93,14 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
93
93
  },
94
94
  {
95
95
  version: null,
96
- note: "The sidebar-02 / sidebar-04 / sidebar-05 copy-owned blocks, re-tokenized with Tabler icons mapped to Lucide.",
97
- id: "blocks-so",
96
+ note: "Agent-session UI logic re-expressed on our tokens: the line-level diff model and its screen-reader polarity prefixes, the scoped N-option permission anatomy, the slash-palette prefix-filter/clamp behaviour, the turn-status elapsed formatter, the hook/lifecycle event line, and the grouped keyboard-shortcuts sheet. No upstream code, styling or terminal palette is shipped.",
97
+ id: "brainless",
98
98
  category: "source",
99
- name: "blocks.so",
99
+ name: "brainless",
100
100
  license: "MIT",
101
- copyright: "Copyright (c) 2025 Ephraim Duncan",
102
- url: "https://github.com/ephraimduncan/blocks",
103
- usedBy: ["@elabs-ai/components-ui"],
101
+ copyright: "Copyright (c) 2026 Ben Swerdlow",
102
+ url: "https://github.com/theswerd/brainless",
103
+ usedBy: ["@elabs-ai/components-ai", "@elabs-ai/components-ui", "@elabs-ai/components-terminal"],
104
104
  required: false,
105
105
  },
106
106
  {
@@ -115,6 +115,18 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
115
115
  usedBy: ["@elabs-ai/components-viewer"],
116
116
  required: false,
117
117
  },
118
+ {
119
+ version: null,
120
+ note: "The editorial chart gallery behind the 2026-09-04 charts gap analysis: 71 files across the charts and tokens packages cite it as the source of a value, a mark shape or a composition (unit decomposition, hairline furniture, the sequential/diverging ramps, the reveal timing), and three copy-own registry blocks (chart-editorial-hourglass, -patchwork, -almanac) reproduce three of its gallery cards' DATA-SHAPE DESCRIPTIONS from that same analysis' one-line prose. No code was copied verbatim — each was re-expressed onto this repo's tokens and primitives. Its LICENSE is PolyForm Noncommercial 1.0.0, read verbatim from the upstream file; GitHub's own detection reports NOASSERTION, which is why nothing flagged it earlier. The licence's Required Notice slot is left at the template's example value, so upstream states no copyright holder and none is claimed here. Noncommercial-only is unresolved for a repo headed for public release — a licensing decision for the maintainer, not something attribution settles.",
121
+ id: "lieflat-charts",
122
+ category: "source",
123
+ name: "lieflat-charts",
124
+ license: "PolyForm-Noncommercial-1.0.0",
125
+ copyright: null,
126
+ url: "https://github.com/larashero3-dotcom/lieflat-charts",
127
+ usedBy: ["@elabs-ai/components-charts", "@elabs-ai/components-tokens"],
128
+ required: false,
129
+ },
118
130
  {
119
131
  version: null,
120
132
  note: "The live-axis tick-interval picker behind live-y-axis.tsx. Upstream now names that function niceTimeInterval.",
@@ -151,6 +163,18 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
151
163
  usedBy: ["@elabs-ai/components-editor"],
152
164
  required: false,
153
165
  },
166
+ {
167
+ version: null,
168
+ note: "Algorithm shape for the framework-free process-mining core: which aggregates a directly-follows graph carries (discoverGraph) and how traces are grouped into variants (extractVariants), with durationStats as their shared summary. Re-typed in TypeScript from the published algorithms, not copied.",
169
+ id: "pm4js",
170
+ category: "source",
171
+ name: "pm4js",
172
+ license: "BSD-3-Clause",
173
+ copyright: "Copyright 2021 Alessandro Berti",
174
+ url: "https://github.com/pm4js/pm4js-core",
175
+ usedBy: ["@elabs-ai/components-process"],
176
+ required: false,
177
+ },
154
178
  {
155
179
  version: null,
156
180
  note: "Beyond the npm dependency below, the shipped dist of react-scroll-area and react-select is modified in patches/ — disclosed here so the change is not invisible.",
@@ -261,14 +285,38 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
261
285
  note: null,
262
286
  },
263
287
  {
264
- id: "@hookform/resolvers",
288
+ id: "@dnd-kit/core",
265
289
  category: "dependency",
266
- name: "@hookform/resolvers",
267
- version: "3.10.0",
290
+ name: "@dnd-kit/core",
291
+ version: "6.3.1",
268
292
  license: "MIT",
269
- copyright: "bluebill1049",
270
- url: "https://www.npmjs.com/package/@hookform/resolvers",
271
- usedBy: ["@elabs-ai/components-ui"],
293
+ copyright: "Claudéric Demers",
294
+ url: "https://www.npmjs.com/package/@dnd-kit/core",
295
+ usedBy: ["@elabs-ai/components-data"],
296
+ required: false,
297
+ note: null,
298
+ },
299
+ {
300
+ id: "@dnd-kit/sortable",
301
+ category: "dependency",
302
+ name: "@dnd-kit/sortable",
303
+ version: "10.0.0",
304
+ license: "MIT",
305
+ copyright: "Claudéric Demers",
306
+ url: "https://www.npmjs.com/package/@dnd-kit/sortable",
307
+ usedBy: ["@elabs-ai/components-data"],
308
+ required: false,
309
+ note: null,
310
+ },
311
+ {
312
+ id: "@dnd-kit/utilities",
313
+ category: "dependency",
314
+ name: "@dnd-kit/utilities",
315
+ version: "3.2.2",
316
+ license: "MIT",
317
+ copyright: "Claudéric Demers",
318
+ url: "https://www.npmjs.com/package/@dnd-kit/utilities",
319
+ usedBy: ["@elabs-ai/components-data"],
272
320
  required: false,
273
321
  note: null,
274
322
  },
@@ -656,18 +704,6 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
656
704
  required: false,
657
705
  note: null,
658
706
  },
659
- {
660
- id: "@rive-app/react-webgl2",
661
- category: "dependency",
662
- name: "@rive-app/react-webgl2",
663
- version: "4.28.6",
664
- license: "MIT",
665
- copyright: null,
666
- url: "https://www.npmjs.com/package/@rive-app/react-webgl2",
667
- usedBy: ["@elabs-ai/components-ai"],
668
- required: false,
669
- note: null,
670
- },
671
707
  {
672
708
  id: "@streamdown/cjk",
673
709
  category: "dependency",
@@ -901,30 +937,6 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
901
937
  required: false,
902
938
  note: null,
903
939
  },
904
- {
905
- id: "@xterm/addon-fit",
906
- category: "dependency",
907
- name: "@xterm/addon-fit",
908
- version: "0.11.0",
909
- license: "MIT",
910
- copyright: "The xterm.js authors",
911
- url: "https://www.npmjs.com/package/@xterm/addon-fit",
912
- usedBy: ["@elabs-ai/components-ai"],
913
- required: false,
914
- note: null,
915
- },
916
- {
917
- id: "@xterm/xterm",
918
- category: "dependency",
919
- name: "@xterm/xterm",
920
- version: "6.0.0",
921
- license: "MIT",
922
- copyright: null,
923
- url: "https://www.npmjs.com/package/@xterm/xterm",
924
- usedBy: ["@elabs-ai/components-ai"],
925
- required: false,
926
- note: null,
927
- },
928
940
  {
929
941
  id: "ansi-to-react",
930
942
  category: "dependency",
@@ -933,7 +945,7 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
933
945
  license: "BSD-3-Clause",
934
946
  copyright: "Kyle Kelley",
935
947
  url: "https://www.npmjs.com/package/ansi-to-react",
936
- usedBy: ["@elabs-ai/components-ai"],
948
+ usedBy: ["@elabs-ai/components-terminal"],
937
949
  required: false,
938
950
  note: null,
939
951
  },
@@ -949,6 +961,8 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
949
961
  "@elabs-ai/components-ai",
950
962
  "@elabs-ai/components-charts",
951
963
  "@elabs-ai/components-editor",
964
+ "@elabs-ai/components-process",
965
+ "@elabs-ai/components-terminal",
952
966
  "@elabs-ai/components-ui",
953
967
  ],
954
968
  required: false,
@@ -998,7 +1012,7 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
998
1012
  license: "ISC",
999
1013
  copyright: "Mike Bostock",
1000
1014
  url: "https://www.npmjs.com/package/d3-force",
1001
- usedBy: ["@elabs-ai/components-flow"],
1015
+ usedBy: ["@elabs-ai/components-charts", "@elabs-ai/components-flow"],
1002
1016
  required: false,
1003
1017
  note: null,
1004
1018
  },
@@ -1014,6 +1028,18 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
1014
1028
  required: false,
1015
1029
  note: null,
1016
1030
  },
1031
+ {
1032
+ id: "d3-hierarchy",
1033
+ category: "dependency",
1034
+ name: "d3-hierarchy",
1035
+ version: "3.1.2",
1036
+ license: "ISC",
1037
+ copyright: "Mike Bostock",
1038
+ url: "https://www.npmjs.com/package/d3-hierarchy",
1039
+ usedBy: ["@elabs-ai/components-charts"],
1040
+ required: false,
1041
+ note: null,
1042
+ },
1017
1043
  {
1018
1044
  id: "d3-sankey",
1019
1045
  category: "dependency",
@@ -1113,24 +1139,14 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
1113
1139
  "@elabs-ai/components-editor",
1114
1140
  "@elabs-ai/components-flow",
1115
1141
  "@elabs-ai/components-maps",
1142
+ "@elabs-ai/components-process",
1143
+ "@elabs-ai/components-terminal",
1116
1144
  "@elabs-ai/components-ui",
1117
1145
  "@elabs-ai/components-viewer",
1118
1146
  ],
1119
1147
  required: false,
1120
1148
  note: null,
1121
1149
  },
1122
- {
1123
- id: "media-chrome",
1124
- category: "dependency",
1125
- name: "media-chrome",
1126
- version: "4.19.1",
1127
- license: "MIT",
1128
- copyright: "@muxinc",
1129
- url: "https://www.npmjs.com/package/media-chrome",
1130
- usedBy: ["@elabs-ai/components-ai"],
1131
- required: false,
1132
- note: null,
1133
- },
1134
1150
  {
1135
1151
  id: "mermaid",
1136
1152
  category: "dependency",
@@ -1139,7 +1155,7 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
1139
1155
  license: "MIT",
1140
1156
  copyright: "Knut Sveidqvist",
1141
1157
  url: "https://www.npmjs.com/package/mermaid",
1142
- usedBy: ["@elabs-ai/components-ai", "@elabs-ai/components-editor"],
1158
+ usedBy: ["@elabs-ai/components-editor"],
1143
1159
  required: false,
1144
1160
  note: null,
1145
1161
  },
@@ -1179,18 +1195,6 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
1179
1195
  required: false,
1180
1196
  note: null,
1181
1197
  },
1182
- {
1183
- id: "react-hook-form",
1184
- category: "dependency",
1185
- name: "react-hook-form",
1186
- version: "7.77.0",
1187
- license: "MIT",
1188
- copyright: "Beier(Bill) Luo",
1189
- url: "https://www.npmjs.com/package/react-hook-form",
1190
- usedBy: ["@elabs-ai/components-ui"],
1191
- required: false,
1192
- note: null,
1193
- },
1194
1198
  {
1195
1199
  id: "react-jsx-parser",
1196
1200
  category: "dependency",
@@ -1415,7 +1419,7 @@ export const ATTRIBUTIONS: readonly Attribution[] = [
1415
1419
  license: "MIT",
1416
1420
  copyright: "Colin McDonnell",
1417
1421
  url: "https://www.npmjs.com/package/zod",
1418
- usedBy: ["@elabs-ai/components-ai", "@elabs-ai/components-ui"],
1422
+ usedBy: ["@elabs-ai/components-ai"],
1419
1423
  required: false,
1420
1424
  note: null,
1421
1425
  },
@@ -5,6 +5,14 @@ const meta = {
5
5
  title: "Core/Badge",
6
6
  component: Badge,
7
7
  tags: ["autodocs"],
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component:
12
+ "A neutral label or count. The closed seven-state execution-status vocabulary — each state carrying its own glyph, so it survives greyscale — is `Core/StatusBadge`; see [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). Reach for `Badge` when the text IS the meaning and the tone is decoration.",
13
+ },
14
+ },
15
+ },
8
16
  args: { children: "Badge" },
9
17
  argTypes: {
10
18
  variant: {
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { cn } from "../../lib/cn";
4
4
 
5
5
  export const badgeVariants = cva(
6
- "inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-meta font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
6
+ "inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-meta font-medium transition-colors focus-ring focus-visible:ring-offset-2",
7
7
  {
8
8
  variants: {
9
9
  variant: {
@@ -104,7 +104,7 @@ describe("BentoGridItem", () => {
104
104
  // The tile stays a div; the inner anchor is the focusable element.
105
105
  const item = screen.getByTestId("item");
106
106
  expect(item.tagName).toBe("DIV");
107
- expect(item.className).toMatch(/focus-within:ring/);
107
+ expect(item.className).toMatch(/focus-ring-within/);
108
108
  expect(screen.getByRole("link", { name: "go" })).toBeInTheDocument();
109
109
  });
110
110
 
@@ -178,7 +178,7 @@ export const bentoGridItemVariants = cva(
178
178
  * (elevation + edge) is on the base — every tile lifts, clickable or not.
179
179
  */
180
180
  interactive: {
181
- true: "cursor-pointer focus-within:outline-none focus-within:ring-2 focus-within:ring-ring",
181
+ true: "cursor-pointer focus-ring-within",
182
182
  false: "",
183
183
  },
184
184
  },