@tangle-network/ui 11.11.8 → 11.13.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 (279) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/_virtual/_rolldown/runtime.js +13 -0
  3. package/dist/auth/auth.d.ts +53 -0
  4. package/dist/{auth-DOETfnd9.js → auth/auth.js} +7 -37
  5. package/dist/auth/login-layout.d.ts +20 -0
  6. package/dist/auth/login-layout.js +36 -0
  7. package/dist/auth.d.ts +2 -1
  8. package/dist/auth.js +2 -1
  9. package/dist/chat/agent-timeline.d.ts +83 -0
  10. package/dist/chat/agent-timeline.js +236 -0
  11. package/dist/chat/chat-container.d.ts +51 -0
  12. package/dist/chat/chat-container.js +309 -0
  13. package/dist/chat/chat-message.d.ts +22 -0
  14. package/dist/chat/chat-message.js +38 -0
  15. package/dist/chat/message-list.d.ts +32 -0
  16. package/dist/chat/message-list.js +37 -0
  17. package/dist/chat/thinking-indicator.d.ts +6 -0
  18. package/dist/chat/thinking-indicator.js +38 -0
  19. package/dist/chat/user-message.d.ts +21 -0
  20. package/dist/chat/user-message.js +42 -0
  21. package/dist/chat.d.ts +6 -1
  22. package/dist/chat.js +6 -1
  23. package/dist/editor/document-editor-pane.d.ts +37 -0
  24. package/dist/editor/document-editor-pane.js +199 -0
  25. package/dist/editor/editor-lazy.js +37 -0
  26. package/dist/editor/editor-loading.js +21 -0
  27. package/dist/editor/editor-peers.d.ts +6 -0
  28. package/dist/editor/editor-peers.js +155 -0
  29. package/dist/{document-editor-pane-DHM8DDhb.d.ts → editor/editor-provider.d.ts} +10 -47
  30. package/dist/editor/editor-provider.js +253 -0
  31. package/dist/editor/editor-toolbar.d.ts +11 -0
  32. package/dist/editor/editor-toolbar.js +106 -0
  33. package/dist/editor/markdown-conversion.js +23 -0
  34. package/dist/editor/markdown-document-editor.js +103 -0
  35. package/dist/editor/tiptap-editor.d.ts +45 -0
  36. package/dist/editor/tiptap-editor.js +271 -0
  37. package/dist/editor/use-editor.d.ts +67 -0
  38. package/dist/editor/use-editor.js +158 -0
  39. package/dist/editor.d.ts +6 -119
  40. package/dist/editor.js +5 -1
  41. package/dist/files/file-artifact-pane.d.ts +47 -0
  42. package/dist/files/file-artifact-pane.js +114 -0
  43. package/dist/files/file-format.d.ts +51 -0
  44. package/dist/{file-format-BgXbr-Ib.js → files/file-format.js} +1 -1
  45. package/dist/files/file-preview.d.ts +27 -0
  46. package/dist/files/file-preview.js +359 -0
  47. package/dist/files/file-tabs.d.ts +19 -0
  48. package/dist/files/file-tabs.js +48 -0
  49. package/dist/files/file-tree.d.ts +31 -0
  50. package/dist/files/file-tree.js +135 -0
  51. package/dist/files/rich-file-tree.d.ts +62 -0
  52. package/dist/files/rich-file-tree.js +122 -0
  53. package/dist/files.d.ts +6 -1
  54. package/dist/files.js +6 -2
  55. package/dist/hooks/use-auth.d.ts +40 -0
  56. package/dist/hooks/use-auth.js +91 -0
  57. package/dist/hooks/use-auto-scroll.d.ts +11 -0
  58. package/dist/hooks/use-auto-scroll.js +48 -0
  59. package/dist/hooks/use-dropdown-menu.d.ts +12 -0
  60. package/dist/hooks/use-dropdown-menu.js +31 -0
  61. package/dist/hooks/use-live-time.d.ts +7 -0
  62. package/dist/hooks/use-live-time.js +22 -0
  63. package/dist/hooks/use-realtime-session.d.ts +32 -0
  64. package/dist/hooks/use-realtime-session.js +235 -0
  65. package/dist/hooks/use-run-collapse-state.d.ts +13 -0
  66. package/dist/hooks/use-run-collapse-state.js +24 -0
  67. package/dist/hooks/use-run-groups.d.ts +11 -0
  68. package/dist/hooks/use-run-groups.js +85 -0
  69. package/dist/hooks/use-sdk-session.d.ts +59 -0
  70. package/dist/hooks/use-sdk-session.js +322 -0
  71. package/dist/hooks/use-sse-stream.d.ts +124 -0
  72. package/dist/hooks/use-sse-stream.js +229 -0
  73. package/dist/hooks/use-tool-call-stream.d.ts +23 -0
  74. package/dist/hooks/use-tool-call-stream.js +75 -0
  75. package/dist/hooks.d.ts +11 -49
  76. package/dist/hooks.js +10 -110
  77. package/dist/index.d.ts +99 -25
  78. package/dist/index.js +86 -36
  79. package/dist/lib/focus.d.ts +55 -0
  80. package/dist/{focus-BcE_SHqz.js → lib/focus.js} +1 -1
  81. package/dist/lib/utils.d.ts +4 -0
  82. package/dist/{utils-C_uf36nf.js → lib/utils.js} +1 -1
  83. package/dist/{code-block-B9miUPht.d.ts → markdown/code-block.d.ts} +5 -6
  84. package/dist/{code-block-B36KVUSN.js → markdown/code-block.js} +22 -47
  85. package/dist/{markdown-C0atSabr.d.ts → markdown/markdown.d.ts} +4 -5
  86. package/dist/{markdown-Db08KAYF.js → markdown/markdown.js} +3 -3
  87. package/dist/markdown.d.ts +2 -2
  88. package/dist/markdown.js +2 -2
  89. package/dist/{openui-artifact-renderer-fkBQaoLO.d.ts → openui/openui-artifact-renderer.d.ts} +20 -21
  90. package/dist/{openui-artifact-renderer-DG_i2lOR.js → openui/openui-artifact-renderer.js} +9 -7
  91. package/dist/{schema-BpW9rSSG.js → openui/schema-compiled.js} +2 -354
  92. package/dist/{schema-DAstk-J4.d.ts → openui/schema-data.d.ts} +3 -24
  93. package/dist/openui/schema-data.js +206 -0
  94. package/dist/openui-schema.d.ts +22 -3
  95. package/dist/openui-schema.js +138 -1
  96. package/dist/openui.d.ts +3 -2
  97. package/dist/openui.js +3 -2
  98. package/dist/{artifact-pane-BL_DFWNl.d.ts → primitives/artifact-pane.d.ts} +3 -4
  99. package/dist/{artifact-pane-DMXT9a8U.js → primitives/artifact-pane.js} +2 -2
  100. package/dist/primitives/avatar.d.ts +8 -0
  101. package/dist/primitives/avatar.js +25 -0
  102. package/dist/primitives/badge.d.ts +22 -0
  103. package/dist/primitives/badge.js +47 -0
  104. package/dist/{button-yoIv77e2.d.ts → primitives/button.d.ts} +2 -2
  105. package/dist/primitives/button.js +113 -0
  106. package/dist/primitives/card.d.ts +18 -0
  107. package/dist/primitives/card.js +52 -0
  108. package/dist/primitives/code-block.d.ts +6 -0
  109. package/dist/primitives/code-block.js +13 -0
  110. package/dist/primitives/dialog.d.ts +23 -0
  111. package/dist/primitives/dialog.js +59 -0
  112. package/dist/primitives/drop-zone.d.ts +23 -0
  113. package/dist/primitives/drop-zone.js +104 -0
  114. package/dist/primitives/dropdown-menu.d.ts +29 -0
  115. package/dist/{dropdown-menu-BpcLa5vF.js → primitives/dropdown-menu.js} +2 -23
  116. package/dist/primitives/empty-state.d.ts +11 -0
  117. package/dist/primitives/empty-state.js +32 -0
  118. package/dist/primitives/heading.d.ts +23 -0
  119. package/dist/primitives/heading.js +38 -0
  120. package/dist/primitives/input.d.ts +23 -0
  121. package/dist/primitives/input.js +110 -0
  122. package/dist/primitives/label.d.ts +6 -0
  123. package/dist/primitives/label.js +16 -0
  124. package/dist/primitives/metric-strip.d.ts +48 -0
  125. package/dist/primitives/metric-strip.js +52 -0
  126. package/dist/primitives/page-header.d.ts +25 -0
  127. package/dist/primitives/page-header.js +47 -0
  128. package/dist/primitives/page-shell.d.ts +14 -0
  129. package/dist/primitives/page-shell.js +17 -0
  130. package/dist/primitives/progress.d.ts +9 -0
  131. package/dist/primitives/progress.js +29 -0
  132. package/dist/primitives/segmented-control.d.ts +40 -0
  133. package/dist/primitives/segmented-control.js +57 -0
  134. package/dist/primitives/select.d.ts +15 -0
  135. package/dist/primitives/select.js +74 -0
  136. package/dist/primitives/sidebar-drop-zone.d.ts +21 -0
  137. package/dist/primitives/sidebar-drop-zone.js +88 -0
  138. package/dist/primitives/skeleton.d.ts +10 -0
  139. package/dist/primitives/skeleton.js +50 -0
  140. package/dist/primitives/stat-card.d.ts +16 -0
  141. package/dist/primitives/stat-card.js +60 -0
  142. package/dist/primitives/status-pill.d.ts +44 -0
  143. package/dist/primitives/status-pill.js +119 -0
  144. package/dist/primitives/switch.d.ts +6 -0
  145. package/dist/primitives/switch.js +16 -0
  146. package/dist/primitives/table.d.ts +22 -0
  147. package/dist/primitives/table.js +63 -0
  148. package/dist/primitives/tabs.d.ts +13 -0
  149. package/dist/{tabs-BVHR6NkO.js → primitives/tabs.js} +3 -3
  150. package/dist/primitives/terminal-display.d.ts +25 -0
  151. package/dist/primitives/terminal-display.js +109 -0
  152. package/dist/primitives/text-shimmer.d.ts +16 -0
  153. package/dist/{text-shimmer-BUUo6I-g.js → primitives/text-shimmer.js} +2 -2
  154. package/dist/primitives/theme-toggle.d.ts +11 -0
  155. package/dist/primitives/theme-toggle.js +154 -0
  156. package/dist/primitives/toast.d.ts +29 -0
  157. package/dist/primitives/toast.js +142 -0
  158. package/dist/primitives/toolbar.d.ts +55 -0
  159. package/dist/primitives/toolbar.js +37 -0
  160. package/dist/primitives/upload-progress.d.ts +23 -0
  161. package/dist/primitives/upload-progress.js +72 -0
  162. package/dist/primitives.d.ts +35 -5
  163. package/dist/primitives.js +35 -9
  164. package/dist/{index-COgqsFAQ.d.ts → redaction/redacted-document.d.ts} +6 -7
  165. package/dist/{redaction-CGGB2ENG.js → redaction/redacted-document.js} +2 -2
  166. package/dist/redaction.d.ts +1 -1
  167. package/dist/redaction.js +1 -1
  168. package/dist/run/expandable-content.js +41 -0
  169. package/dist/run/expanded-tool-detail.d.ts +12 -0
  170. package/dist/run/expanded-tool-detail.js +163 -0
  171. package/dist/run/inline-thinking-item.d.ts +11 -0
  172. package/dist/run/inline-thinking-item.js +60 -0
  173. package/dist/run/inline-tool-item.d.ts +25 -0
  174. package/dist/run/inline-tool-item.js +63 -0
  175. package/dist/run/run-group.d.ts +26 -0
  176. package/dist/{run-group-BWGeesgs.js → run/run-group.js} +9 -60
  177. package/dist/run/run-item-primitives.d.ts +5 -0
  178. package/dist/{run-ZsIKPA6J.js → run/run-item-primitives.js} +2 -2
  179. package/dist/run/run-row-shell.d.ts +53 -0
  180. package/dist/run/run-row-shell.js +114 -0
  181. package/dist/{tool-call-feed-BeB1_4pX.d.ts → run/tool-call-feed.d.ts} +7 -12
  182. package/dist/{tool-call-feed-BpmMLpPX.js → run/tool-call-feed.js} +4 -4
  183. package/dist/run/tool-call-step.d.ts +5 -0
  184. package/dist/run/tool-call-step.js +82 -0
  185. package/dist/run.d.ts +8 -2
  186. package/dist/run.js +7 -4
  187. package/dist/sdk-hooks.d.ts +8 -1
  188. package/dist/sdk-hooks.js +8 -2
  189. package/dist/stores/active-sessions-store.d.ts +83 -0
  190. package/dist/{active-sessions-store-Gt1R0rFG.js → stores/active-sessions-store.js} +1 -1
  191. package/dist/stores/chat-store.d.ts +14 -0
  192. package/dist/stores/chat-store.js +33 -0
  193. package/dist/stores.d.ts +3 -16
  194. package/dist/stores.js +2 -33
  195. package/dist/tool-previews/command-preview.d.ts +11 -0
  196. package/dist/tool-previews/command-preview.js +91 -0
  197. package/dist/tool-previews/diff-preview.d.ts +7 -0
  198. package/dist/tool-previews/diff-preview.js +76 -0
  199. package/dist/tool-previews/glob-results-preview.d.ts +7 -0
  200. package/dist/tool-previews/glob-results-preview.js +53 -0
  201. package/dist/tool-previews/grep-results-preview.d.ts +7 -0
  202. package/dist/tool-previews/grep-results-preview.js +99 -0
  203. package/dist/tool-previews/preview-primitives.js +58 -0
  204. package/dist/tool-previews/question-preview.d.ts +7 -0
  205. package/dist/tool-previews/question-preview.js +63 -0
  206. package/dist/tool-previews/web-search-preview.d.ts +7 -0
  207. package/dist/tool-previews/web-search-preview.js +78 -0
  208. package/dist/tool-previews/write-file-preview.d.ts +11 -0
  209. package/dist/tool-previews/write-file-preview.js +52 -0
  210. package/dist/tool-previews.d.ts +7 -1
  211. package/dist/tool-previews.js +7 -1
  212. package/dist/{branding-26d5ARIr.d.ts → types/branding.d.ts} +2 -3
  213. package/dist/{message-vhUI0c5G.d.ts → types/message.d.ts} +2 -3
  214. package/dist/{parts-5mhy9yJ3.d.ts → types/parts.d.ts} +8 -9
  215. package/dist/{run-OyWmIDqu.d.ts → types/run.d.ts} +9 -10
  216. package/dist/{tool-display-DgmL5rIU.d.ts → types/tool-display.d.ts} +5 -6
  217. package/dist/types.d.ts +5 -5
  218. package/dist/utils/copy-text.d.ts +7 -0
  219. package/dist/{utils-Bl_CZ0Qp.js → utils/copy-text.js} +1 -11
  220. package/dist/utils/format.d.ts +22 -0
  221. package/dist/{format-DfcLWqMa.js → utils/format.js} +1 -1
  222. package/dist/utils/time-ago.d.ts +3 -0
  223. package/dist/utils/time-ago.js +11 -0
  224. package/dist/utils/tool-display.d.ts +16 -0
  225. package/dist/{tool-display-BAuAVa43.js → utils/tool-display.js} +1 -1
  226. package/dist/utils.d.ts +6 -1
  227. package/dist/utils.js +6 -5
  228. package/package.json +2 -2
  229. package/src/editor/document-editor-pane.tsx +4 -1
  230. package/src/files/file-artifact-pane.test.tsx +23 -0
  231. package/src/files/file-artifact-pane.tsx +3 -0
  232. package/src/files/rich-file-tree.stories.tsx +1 -1
  233. package/src/files/rich-file-tree.tsx +12 -9
  234. package/src/markdown/code-block.test.tsx +46 -2
  235. package/src/markdown/code-block.tsx +23 -60
  236. package/src/markdown/markdown.test.tsx +3 -2
  237. package/src/primitives/button.stories.tsx +37 -1
  238. package/src/primitives/button.test.tsx +230 -0
  239. package/src/primitives/button.tsx +62 -14
  240. package/src/primitives/card-anatomy.stories.tsx +51 -0
  241. package/src/primitives/card.test.tsx +71 -0
  242. package/src/primitives/card.tsx +25 -13
  243. package/src/primitives/heading.stories.tsx +35 -0
  244. package/src/primitives/heading.test.tsx +56 -0
  245. package/src/primitives/heading.tsx +76 -0
  246. package/src/primitives/index.ts +8 -0
  247. package/src/primitives/input.stories.tsx +28 -0
  248. package/src/primitives/input.test.tsx +161 -0
  249. package/src/primitives/input.tsx +34 -28
  250. package/src/primitives/page-header.stories.tsx +51 -0
  251. package/src/primitives/page-header.test.tsx +48 -0
  252. package/src/primitives/page-header.tsx +35 -35
  253. package/src/primitives/page-shell.stories.tsx +89 -0
  254. package/src/primitives/page-shell.test.tsx +24 -0
  255. package/src/primitives/page-shell.tsx +26 -0
  256. package/src/primitives/table-wrapper.stories.tsx +54 -0
  257. package/src/primitives/table.test.tsx +72 -0
  258. package/src/primitives/table.tsx +33 -12
  259. package/src/primitives/theme-toggle.test.tsx +167 -0
  260. package/src/primitives/theme-toggle.tsx +111 -39
  261. package/dist/active-sessions-store-nl70ZnMC.d.ts +0 -84
  262. package/dist/button-C87nEe2R.js +0 -78
  263. package/dist/chat-DDvF2oGQ.js +0 -672
  264. package/dist/diff-preview-BcK3nRXb.js +0 -533
  265. package/dist/document-editor-pane-CsFbH2qo.js +0 -1279
  266. package/dist/files--Unn___t.js +0 -752
  267. package/dist/index-2UntyjBb.d.ts +0 -52
  268. package/dist/index-BgUHilZm.d.ts +0 -108
  269. package/dist/index-CPDET_s1.d.ts +0 -549
  270. package/dist/index-Cg6gYAqh.d.ts +0 -73
  271. package/dist/index-Cs7j8V7l.d.ts +0 -203
  272. package/dist/index-CyGSLGVF.d.ts +0 -234
  273. package/dist/index-DjKxO357.d.ts +0 -128
  274. package/dist/primitives-Bx59fT-U.js +0 -1239
  275. package/dist/table-Bss73DhB.js +0 -146
  276. package/dist/tool-call-step-DNebqhmX.js +0 -434
  277. package/dist/use-auto-scroll-C5Pjeqon.js +0 -153
  278. package/dist/use-sse-stream-DjqRuDft.js +0 -884
  279. package/dist/use-sse-stream-Dm6R5YWz.d.ts +0 -282
@@ -0,0 +1,51 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Button } from "./button";
4
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card";
5
+ import { Heading } from "./heading";
6
+
7
+ const meta = {
8
+ title: "Primitives/Card/Anatomy",
9
+ component: Card,
10
+ parameters: { layout: "padded" },
11
+ } satisfies Meta<typeof Card>;
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const AlignedParts: Story = {
16
+ render: () => <div className="max-w-lg space-y-6">
17
+ <Heading variant="page">Activity details</Heading>
18
+ <Card data-anatomy="full">
19
+ <CardHeader data-part="header"><CardTitle as="h2">Latest result</CardTitle><CardDescription>One heading level below the page.</CardDescription></CardHeader>
20
+ <CardContent data-part="content">The header, body and footer share one horizontal gutter.</CardContent>
21
+ <CardFooter data-part="footer"><Button type="button" variant="outline">View result</Button></CardFooter>
22
+ </Card>
23
+ <Card data-anatomy="standalone"><CardContent data-part="content">Content-only cards retain their top padding.</CardContent></Card>
24
+ </div>,
25
+ play: ({ canvasElement }) => {
26
+ const parts = Array.from(canvasElement.querySelectorAll<HTMLElement>('[data-anatomy="full"] > [data-part]'));
27
+ const styles = parts.map((part) => getComputedStyle(part));
28
+ const gutter = styles[0]?.paddingLeft;
29
+ if (!gutter || parseFloat(gutter) <= 0 || styles.some((style) => style.paddingLeft !== gutter || style.paddingRight !== gutter)) {
30
+ throw new Error("Card parts do not share a nonzero horizontal gutter");
31
+ }
32
+ if (parseFloat(styles[1]!.paddingTop) !== 0 || parseFloat(styles[2]!.paddingTop) !== 0) {
33
+ throw new Error("Stacked card parts duplicate top padding");
34
+ }
35
+ const standalone = canvasElement.querySelector<HTMLElement>('[data-anatomy="standalone"] > [data-part]')!;
36
+ const style = getComputedStyle(standalone);
37
+ if (parseFloat(style.paddingTop) <= 0 || style.paddingTop !== style.paddingBottom) {
38
+ throw new Error("Content-only card lost its top padding");
39
+ }
40
+ },
41
+ };
42
+
43
+ function ActionCard() {
44
+ const [count, setCount] = useState(0);
45
+ return <Card hover className="max-w-sm">
46
+ <CardHeader><CardTitle as="h2">Decorative hover, native action</CardTitle><CardDescription>Tab to the button; Enter and Space both work.</CardDescription></CardHeader>
47
+ <CardContent><p role="status">Opened {count} times</p></CardContent>
48
+ <CardFooter><Button type="button" onClick={() => setCount((value) => value + 1)}>Open details</Button></CardFooter>
49
+ </Card>;
50
+ }
51
+ export const KeyboardAction: Story = { render: () => <ActionCard /> };
@@ -0,0 +1,71 @@
1
+ import { createRef } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps } from "./card";
6
+
7
+ describe("Card anatomy", () => {
8
+ it("keeps the existing default title tag and heading ref", () => {
9
+ const ref = createRef<HTMLHeadingElement>();
10
+ render(<CardTitle ref={ref} id="card-title">Details</CardTitle>);
11
+ expect(ref.current).toBe(screen.getByRole("heading", { level: 3 }));
12
+ expect(ref.current).toHaveAttribute("id", "card-title");
13
+ });
14
+
15
+ it.each([1, 2, 3, 4, 5, 6] as const)("allows an h%i without changing its visual treatment", (level) => {
16
+ const { rerender } = render(<CardTitle>Details</CardTitle>);
17
+ const classes = screen.getByRole("heading").className;
18
+ rerender(<CardTitle as={`h${level}` as CardTitleProps["as"]}>Details</CardTitle>);
19
+ expect(screen.getByRole("heading", { level }).className).toBe(classes);
20
+ });
21
+
22
+ it("preserves native attributes and caller classes", () => {
23
+ render(<CardTitle as="h2" role="presentation" className="text-xl" data-kind="summary">Summary</CardTitle>);
24
+ const title = screen.getByText("Summary");
25
+ expect(title).toHaveAttribute("role", "presentation");
26
+ expect(title).toHaveAttribute("data-kind", "summary");
27
+ expect(title).toHaveClass("text-xl");
28
+ });
29
+
30
+ it("uses the same gutter on every part and only removes subsequent top padding", () => {
31
+ render(<Card>
32
+ <CardHeader data-testid="header"><CardTitle>Details</CardTitle><CardDescription>Prose</CardDescription></CardHeader>
33
+ <CardContent data-testid="content">Body</CardContent>
34
+ <CardFooter data-testid="footer">Footer</CardFooter>
35
+ </Card>);
36
+ for (const part of ["header", "content", "footer"]) {
37
+ expect(screen.getByTestId(part)).toHaveClass("p-4");
38
+ expect(screen.getByTestId(part)).not.toHaveClass("p-6");
39
+ }
40
+ expect(screen.getByTestId("content")).toHaveClass("[&:not(:first-child)]:pt-0");
41
+ expect(screen.getByTestId("footer")).toHaveClass("[&:not(:first-child)]:pt-0");
42
+ // These are class/DOM contracts, not a browser-computed padding assertion.
43
+ expect(screen.getByText("Prose").tagName).toBe("P");
44
+ });
45
+
46
+ it("does not strip the top padding unconditionally from content-only cards", () => {
47
+ const ref = createRef<HTMLDivElement>();
48
+ render(<Card><CardContent ref={ref}>Standalone</CardContent></Card>);
49
+ expect(ref.current).toHaveClass("p-4");
50
+ expect(ref.current).not.toHaveClass("pt-0");
51
+ expect(ref.current?.matches(":first-child")).toBe(true);
52
+ });
53
+
54
+ it("keeps hover decorative and leaves keyboard activation to a real button", async () => {
55
+ const user = userEvent.setup();
56
+ const activate = vi.fn();
57
+ const ref = createRef<HTMLDivElement>();
58
+ render(<Card ref={ref} hover>
59
+ <CardContent><button type="button" onClick={activate}>Open details</button></CardContent>
60
+ </Card>);
61
+ expect(ref.current?.tagName).toBe("DIV");
62
+ expect(ref.current).not.toHaveAttribute("tabindex");
63
+ expect(ref.current).not.toHaveAttribute("role");
64
+ expect(ref.current).not.toHaveClass("cursor-pointer");
65
+ await user.tab();
66
+ expect(screen.getByRole("button", { name: "Open details" })).toHaveFocus();
67
+ await user.keyboard("{Enter}");
68
+ await user.keyboard(" ");
69
+ expect(activate).toHaveBeenCalledTimes(2);
70
+ });
71
+ });
@@ -1,10 +1,12 @@
1
1
  import * as React from "react";
2
2
  import { cn } from "../lib/utils";
3
+ import { Heading } from "./heading";
3
4
 
4
5
  const Card = React.forwardRef<
5
6
  HTMLDivElement,
6
7
  React.HTMLAttributes<HTMLDivElement> & {
7
8
  variant?: "default" | "glass" | "sandbox" | "elevated";
9
+ /** Decorative border feedback only. Put actions in a native button or link. */
8
10
  hover?: boolean;
9
11
  }
10
12
  >(({ className, variant = "default", hover = false, ...props }, ref) => {
@@ -22,7 +24,7 @@ const Card = React.forwardRef<
22
24
  "rounded-[var(--radius-lg)] border text-card-foreground transition-[border-color,box-shadow]",
23
25
  "duration-[var(--transition-default)]",
24
26
  variants[variant],
25
- hover && "cursor-pointer hover:border-[var(--border-strong)]",
27
+ hover && "hover:border-[var(--border-strong)]",
26
28
  className,
27
29
  )}
28
30
  {...props}
@@ -43,16 +45,22 @@ const CardHeader = React.forwardRef<
43
45
  ));
44
46
  CardHeader.displayName = "CardHeader";
45
47
 
46
- const CardTitle = React.forwardRef<
47
- HTMLHeadingElement,
48
- React.HTMLAttributes<HTMLHeadingElement>
49
- >(({ className, ...props }, ref) => (
50
- <h3
51
- ref={ref}
52
- className={cn("font-semibold leading-none tracking-tight", className)}
53
- {...props}
54
- />
55
- ));
48
+ export interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
49
+ /** Heading level appropriate to the surrounding document; h3 remains the default. */
50
+ as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
51
+ }
52
+
53
+ const CardTitle = React.forwardRef<HTMLHeadingElement, CardTitleProps>(
54
+ ({ className, as = "h3", ...props }, ref) => (
55
+ <Heading
56
+ ref={ref}
57
+ variant="subsection"
58
+ as={as}
59
+ className={className}
60
+ {...props}
61
+ />
62
+ ),
63
+ );
56
64
  CardTitle.displayName = "CardTitle";
57
65
 
58
66
  const CardDescription = React.forwardRef<
@@ -71,7 +79,11 @@ const CardContent = React.forwardRef<
71
79
  HTMLDivElement,
72
80
  React.HTMLAttributes<HTMLDivElement>
73
81
  >(({ className, ...props }, ref) => (
74
- <div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
82
+ <div
83
+ ref={ref}
84
+ className={cn("p-4 [&:not(:first-child)]:pt-0", className)}
85
+ {...props}
86
+ />
75
87
  ));
76
88
  CardContent.displayName = "CardContent";
77
89
 
@@ -81,7 +93,7 @@ const CardFooter = React.forwardRef<
81
93
  >(({ className, ...props }, ref) => (
82
94
  <div
83
95
  ref={ref}
84
- className={cn("flex items-center p-6 pt-0", className)}
96
+ className={cn("flex flex-wrap items-center gap-2 p-4 [&:not(:first-child)]:pt-0", className)}
85
97
  {...props}
86
98
  />
87
99
  ));
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Heading } from "./heading";
3
+
4
+ const meta = {
5
+ title: "Primitives/Heading",
6
+ component: Heading,
7
+ parameters: { layout: "padded" },
8
+ args: { variant: "page", children: "Activity across your workspace" },
9
+ argTypes: {
10
+ variant: { control: "select", options: ["display", "hero", "page", "section", "subsection", "eyebrow"] },
11
+ as: { control: "select", options: ["h1", "h2", "h3", "h4", "h5", "h6", "p"] },
12
+ },
13
+ } satisfies Meta<typeof Heading>;
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {};
18
+ export const IndependentSemantics: Story = {
19
+ args: { variant: "page", as: "h2", children: "Page-sized title in a nested surface" },
20
+ };
21
+ export const Hierarchy: Story = {
22
+ render: () => <main className="space-y-6">
23
+ <Heading variant="eyebrow">Workspace</Heading>
24
+ <Heading variant="page">Activity</Heading>
25
+ <section className="space-y-4">
26
+ <Heading variant="section">Recent items</Heading>
27
+ <Heading variant="subsection">Build details</Heading>
28
+ <p className="text-muted-foreground">Eyebrows are labels, not extra headings.</p>
29
+ </section>
30
+ </main>,
31
+ play: ({ canvasElement }) => {
32
+ const tags = Array.from(canvasElement.querySelectorAll("h1,h2,h3,h4,h5,h6")).map((node) => node.tagName).join(",");
33
+ if (tags !== "H1,H2,H3") throw new Error(`Unexpected heading hierarchy: ${tags}`);
34
+ },
35
+ };
@@ -0,0 +1,56 @@
1
+ import { createRef } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { describe, expect, it } from "vitest";
4
+ import { Heading, type HeadingVariant } from "./heading";
5
+
6
+ const roles: [HeadingVariant, string][] = [
7
+ ["display", "H1"], ["hero", "H2"], ["page", "H1"],
8
+ ["section", "H2"], ["subsection", "H3"], ["eyebrow", "P"],
9
+ ];
10
+
11
+ describe("Heading", () => {
12
+ it.each(roles)("gives %s its existing default element", (variant, tag) => {
13
+ render(<Heading variant={variant}>Title</Heading>);
14
+ expect(screen.getByText("Title").tagName).toBe(tag);
15
+ });
16
+
17
+ it.each(roles)("maps Sandbox's %s role without emitting an invalid ARIA role", (role, tag) => {
18
+ render(<Heading role={role}>Title</Heading>);
19
+ const title = screen.getByText("Title");
20
+ expect(title.tagName).toBe(tag);
21
+ expect(title).not.toHaveAttribute("role");
22
+ });
23
+
24
+ it("changes semantics without changing the visual variant", () => {
25
+ const { rerender } = render(<Heading variant="page">Title</Heading>);
26
+ const classes = screen.getByRole("heading", { level: 1 }).className;
27
+ rerender(<Heading variant="page" as="h4">Title</Heading>);
28
+ expect(screen.getByRole("heading", { level: 4 }).className).toBe(classes);
29
+ expect(screen.queryByRole("heading", { level: 1 })).toBeNull();
30
+ });
31
+
32
+ it("lets an explicit variant win over the existing visual-role input", () => {
33
+ render(<Heading variant="section" role="page">Title</Heading>);
34
+ expect(screen.getByRole("heading", { level: 2 })).not.toHaveAttribute("role");
35
+ });
36
+
37
+ it("forwards native roles, refs and HTML attributes", () => {
38
+ const ref = createRef<HTMLElement>();
39
+ render(<Heading ref={ref} variant="eyebrow" as="div" role="note" id="scope" className="custom" data-scope="all">Scope</Heading>);
40
+ const note = screen.getByRole("note");
41
+ expect(ref.current).toBe(note);
42
+ expect(note).toHaveAttribute("id", "scope");
43
+ expect(note).toHaveAttribute("data-scope", "all");
44
+ expect(note).toHaveClass("custom");
45
+ });
46
+
47
+ it("keeps a page, section and nested title in document order", () => {
48
+ render(<main>
49
+ <Heading variant="page">Activity</Heading>
50
+ <Heading variant="eyebrow">Last day</Heading>
51
+ <Heading variant="section">Recent activity</Heading>
52
+ <Heading variant="subsection">Details</Heading>
53
+ </main>);
54
+ expect(screen.getAllByRole("heading").map((node) => node.tagName)).toEqual(["H1", "H2", "H3"]);
55
+ });
56
+ });
@@ -0,0 +1,76 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /** Visual roles ported from sandbox-ui/src/primitives/heading.tsx. */
5
+ export type HeadingVariant =
6
+ | "display"
7
+ | "hero"
8
+ | "page"
9
+ | "section"
10
+ | "subsection"
11
+ | "eyebrow";
12
+
13
+ // Keep Brand's existing size, leading, tracking and weight tokens. The literal
14
+ // fallbacks are the existing Sandbox treatment, not a second type scale.
15
+ const VARIANT_CLASS: Record<HeadingVariant, string> = {
16
+ display:
17
+ "font-display font-bold text-foreground text-[length:var(--font-size-display,3rem)] leading-[var(--line-height-display,1.05)] tracking-[var(--tracking-tight,-0.02em)]",
18
+ hero: "font-display font-bold text-foreground text-[length:var(--font-size-hero,2.5rem)] leading-[1.1] tracking-[var(--tracking-tight,-0.02em)]",
19
+ page: "font-display font-semibold text-foreground text-[length:var(--font-size-3xl,1.875rem)] leading-[var(--line-height-heading,1.15)] tracking-[var(--tracking-tight,-0.02em)]",
20
+ section:
21
+ "font-display font-semibold text-foreground text-[length:var(--font-size-xl,1.25rem)] leading-[1.3] tracking-[var(--tracking-snug,-0.01em)]",
22
+ subsection:
23
+ "font-semibold text-foreground text-[length:var(--font-size-lg,1rem)] leading-[1.4]",
24
+ eyebrow:
25
+ "font-semibold uppercase text-muted-foreground text-[length:var(--font-size-sm,0.75rem)] leading-none tracking-[var(--tracking-wide,0.08em)]",
26
+ };
27
+
28
+ const DEFAULT_TAG: Record<HeadingVariant, React.ElementType> = {
29
+ display: "h1",
30
+ hero: "h2",
31
+ page: "h1",
32
+ section: "h2",
33
+ subsection: "h3",
34
+ eyebrow: "p",
35
+ };
36
+
37
+ export interface HeadingProps
38
+ extends Omit<React.HTMLAttributes<HTMLElement>, "role"> {
39
+ /** Visual treatment, independent of the rendered element. Defaults to section. */
40
+ variant?: HeadingVariant;
41
+ /** Semantic element; changing it does not change the visual variant. */
42
+ as?: React.ElementType;
43
+ /**
44
+ * Native ARIA role, or Sandbox's existing visual-role input. Only the six
45
+ * legacy visual values are consumed; they are never emitted as ARIA roles.
46
+ * Prefer variant for new code. An explicit variant wins over a legacy role.
47
+ */
48
+ role?: React.AriaRole | HeadingVariant;
49
+ }
50
+
51
+ /**
52
+ * One renderer for page, section and card titles. Use at most one default
53
+ * display/page h1 per page; choose `as` according to the document hierarchy.
54
+ */
55
+ const Heading = React.forwardRef<HTMLElement, HeadingProps>(
56
+ ({ variant, role, as, className, ...props }, ref) => {
57
+ const legacyVariant =
58
+ role && Object.prototype.hasOwnProperty.call(VARIANT_CLASS, role)
59
+ ? (role as HeadingVariant)
60
+ : undefined;
61
+ const visualVariant = variant ?? legacyVariant ?? "section";
62
+ const Tag = as ?? DEFAULT_TAG[visualVariant];
63
+
64
+ return (
65
+ <Tag
66
+ ref={ref}
67
+ className={cn("[overflow-wrap:anywhere]", VARIANT_CLASS[visualVariant], className)}
68
+ role={legacyVariant ? undefined : role}
69
+ {...props}
70
+ />
71
+ );
72
+ },
73
+ );
74
+ Heading.displayName = "Heading";
75
+
76
+ export { Heading };
@@ -9,6 +9,7 @@ export {
9
9
  CardDescription,
10
10
  CardContent,
11
11
  } from "./card";
12
+ export type { CardTitleProps } from "./card";
12
13
 
13
14
  export {
14
15
  Dialog,
@@ -72,6 +73,7 @@ export {
72
73
  TableCell,
73
74
  TableCaption,
74
75
  } from "./table";
76
+ export type { TableProps } from "./table";
75
77
 
76
78
  export { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs";
77
79
 
@@ -122,6 +124,12 @@ export type { CodeBlockProps, InlineCodeProps } from "./code-block";
122
124
 
123
125
  export { ThemeToggle, useTheme } from "./theme-toggle";
124
126
 
127
+ export { Heading } from "./heading";
128
+ export type { HeadingProps, HeadingVariant } from "./heading";
129
+
130
+ export { PageShell } from "./page-shell";
131
+ export type { PageShellProps } from "./page-shell";
132
+
125
133
  export { PageHeader } from "./page-header";
126
134
  export type { PageHeaderProps } from "./page-header";
127
135
 
@@ -1,5 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react'
2
+ import { useId } from 'react'
2
3
  import { Input, Textarea } from './input'
4
+ import { Label } from './label'
3
5
 
4
6
  const meta: Meta<typeof Input> = {
5
7
  title: 'Primitives/Input',
@@ -111,3 +113,29 @@ export const Overview: Story = {
111
113
  </div>
112
114
  ),
113
115
  }
116
+
117
+ export const FieldRelationships: Story = {
118
+ parameters: {
119
+ docs: {
120
+ description: {
121
+ story: 'Repeated labels must focus their own fields. Inspect accessible descriptions: caller help, hints, and errors coexist. The final field uses an explicit ID and the standalone shared Label.',
122
+ },
123
+ },
124
+ },
125
+ render: function FieldRelationships() {
126
+ const id = useId()
127
+ const descriptionId = `${id}-instructions`
128
+ return (
129
+ <div className="flex w-96 flex-col gap-4">
130
+ <p id={descriptionId}>Shared instructions: choose a descriptive name.</p>
131
+ <Input label="Name" hint="First name." aria-describedby={descriptionId} />
132
+ <Input label="Name" hint="Second name." error="A name is required." aria-describedby={descriptionId} />
133
+ <Textarea label="Name" hint="Explain the name." error="More detail is needed." aria-describedby={descriptionId} />
134
+ <div className="space-y-1.5">
135
+ <Label htmlFor={`${id}-explicit`}>External label</Label>
136
+ <Input id={`${id}-explicit`} hint="Explicit field ID is preserved." />
137
+ </div>
138
+ </div>
139
+ )
140
+ },
141
+ }
@@ -0,0 +1,161 @@
1
+ import * as React from "react";
2
+ import { act, cleanup, render, screen, within } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { hydrateRoot, type Root } from "react-dom/client";
5
+ import { renderToString } from "react-dom/server";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
+ import { Input, Textarea } from "./input";
8
+ import { Label } from "./label";
9
+
10
+ afterEach(cleanup);
11
+
12
+ describe.each([
13
+ ["Input", Input],
14
+ ["Textarea", Textarea],
15
+ ] as const)("%s relationships", (_name, Field) => {
16
+ it("gives repeated labels distinct, stable IDs and focuses the correct field", async () => {
17
+ const user = userEvent.setup();
18
+ const { rerender } = render(<><Field label="Name" /><Field label="Name" /></>);
19
+ const fields = screen.getAllByRole("textbox");
20
+ const ids = fields.map((field) => field.id);
21
+ expect(ids.every(Boolean)).toBe(true);
22
+ expect(new Set(ids).size).toBe(2);
23
+ expect(screen.getAllByLabelText("Name")).toEqual(fields);
24
+ for (const [index, label] of screen.getAllByText("Name", { selector: "label" }).entries()) {
25
+ expect(label).toHaveAttribute("for", ids[index]);
26
+ await user.click(label);
27
+ expect(fields[index]).toHaveFocus();
28
+ }
29
+ rerender(<><Field label="Renamed" /><Field label="Renamed" /></>);
30
+ expect(screen.getAllByRole("textbox").map((field) => field.id)).toEqual(ids);
31
+ });
32
+
33
+ it("preserves an explicit ID and merges caller, hint and error descriptions without duplicates", () => {
34
+ render(<>
35
+ <p id="external">External instructions.</p>
36
+ <Field
37
+ id="profile"
38
+ label="Name"
39
+ hint="Use your full name."
40
+ error="Name is required."
41
+ aria-describedby={" external external\tprofile-hint "}
42
+ aria-invalid={false}
43
+ />
44
+ </>);
45
+ const field = screen.getByRole("textbox", { name: "Name" });
46
+ expect(field).toHaveAttribute("id", "profile");
47
+ expect(field).toHaveAttribute("aria-describedby", "external profile-hint profile-error");
48
+ expect(screen.getByText("Use your full name.")).toHaveAttribute("id", "profile-hint");
49
+ expect(screen.getByText("Name is required.")).toHaveAttribute("id", "profile-error");
50
+ expect(field).toHaveAccessibleDescription("External instructions. Use your full name. Name is required.");
51
+ expect(field).toHaveAttribute("aria-invalid", "true");
52
+ });
53
+
54
+ it("associates descriptions without a convenience label and removes stale relationships", () => {
55
+ const { rerender } = render(<Field aria-label="Name" hint="A hint" error="An error" />);
56
+ const field = screen.getByRole("textbox", { name: "Name" });
57
+ const id = field.id;
58
+ expect(id).not.toBe("");
59
+ expect(field).toHaveAccessibleDescription("A hint An error");
60
+ rerender(<Field aria-label="Name" hint="A hint" />);
61
+ expect(field.id).toBe(id);
62
+ expect(field).toHaveAttribute("aria-describedby", `${id}-hint`);
63
+ expect(field).not.toHaveAttribute("aria-invalid");
64
+ expect(document.getElementById(`${id}-error`)).toBeNull();
65
+ rerender(<Field aria-label="Name" />);
66
+ const bareField = screen.getByRole("textbox", { name: "Name" });
67
+ expect(bareField.id).toBe(id);
68
+ expect(bareField).not.toHaveAttribute("aria-describedby");
69
+ expect(document.getElementById(`${id}-hint`)).toBeNull();
70
+ });
71
+
72
+ it("preserves the bare-element API and caller invalid states", () => {
73
+ const { container, rerender } = render(
74
+ <Field aria-label="Name" aria-describedby="external" aria-invalid="spelling" className="caller-class" />,
75
+ );
76
+ const field = screen.getByRole("textbox");
77
+ expect(container.firstElementChild).toBe(field);
78
+ expect(container.childElementCount).toBe(1);
79
+ expect(field).toHaveAttribute("aria-describedby", "external");
80
+ expect(field).toHaveAttribute("aria-invalid", "spelling");
81
+ expect(field).toHaveClass("caller-class");
82
+ rerender(<Field aria-label="Name" aria-invalid={false} />);
83
+ expect(field).toHaveAttribute("aria-invalid", "false");
84
+ });
85
+
86
+ it("uses the existing Label treatment", () => {
87
+ render(<><Label className="block">Reference</Label><Field label="Name" /></>);
88
+ expect(screen.getByText("Name", { selector: "label" }).className)
89
+ .toBe(screen.getByText("Reference").className);
90
+ });
91
+ });
92
+
93
+ it("keeps Input and Textarea IDs distinct even with the same label", () => {
94
+ render(<><Input label="Notes" /><Textarea label="Notes" /></>);
95
+ expect(new Set(screen.getAllByRole("textbox").map((field) => field.id)).size).toBe(2);
96
+ });
97
+
98
+ it("exposes Input's error variant without requiring error text", () => {
99
+ render(<Input aria-label="Name" variant="error" />);
100
+ expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true");
101
+ });
102
+
103
+ it("forwards native field refs, values and change handlers", async () => {
104
+ const user = userEvent.setup();
105
+ const inputRef = React.createRef<HTMLInputElement>();
106
+ const textareaRef = React.createRef<HTMLTextAreaElement>();
107
+ const inputChange = vi.fn();
108
+ const textareaChange = vi.fn();
109
+ const { unmount } = render(<>
110
+ <Input ref={inputRef} label="Name" defaultValue="A" onChange={inputChange} />
111
+ <Textarea ref={textareaRef} label="Notes" defaultValue="B" onChange={textareaChange} />
112
+ </>);
113
+ expect(inputRef.current).toBe(screen.getByRole("textbox", { name: "Name" }));
114
+ expect(textareaRef.current).toBe(screen.getByRole("textbox", { name: "Notes" }));
115
+ await user.type(inputRef.current!, "lice");
116
+ await user.type(textareaRef.current!, "ody");
117
+ expect(inputRef.current).toHaveValue("Alice");
118
+ expect(textareaRef.current).toHaveValue("Body");
119
+ expect(inputChange).toHaveBeenCalled();
120
+ expect(textareaChange).toHaveBeenCalled();
121
+ unmount();
122
+ expect(inputRef.current).toBeNull();
123
+ expect(textareaRef.current).toBeNull();
124
+ });
125
+
126
+ it("hydrates SSR IDs and all description relationships without mismatch", async () => {
127
+ const ui = <React.StrictMode>
128
+ <p id="external">External instructions.</p>
129
+ <Input label="Name" hint="First hint" />
130
+ <Input label="Name" error="Required" />
131
+ <Textarea label="Name" hint="Notes hint" error="Too short" aria-describedby="external" />
132
+ <Input id="explicit" label="Explicit" hint="Explicit hint" />
133
+ </React.StrictMode>;
134
+ const container = document.createElement("div");
135
+ container.innerHTML = renderToString(ui, { identifierPrefix: "fields-" });
136
+ document.body.appendChild(container);
137
+ const serverHTML = container.innerHTML;
138
+ const serverIds = within(container).getAllByRole("textbox").map((field) => field.id);
139
+ const recoverableError = vi.fn();
140
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
141
+ let root: Root | undefined;
142
+ try {
143
+ await act(async () => {
144
+ root = hydrateRoot(container, ui, { identifierPrefix: "fields-", onRecoverableError: recoverableError });
145
+ });
146
+ expect(new Set(serverIds).size).toBe(4);
147
+ expect(within(container).getAllByRole("textbox").map((field) => field.id)).toEqual(serverIds);
148
+ expect(container.innerHTML).toBe(serverHTML);
149
+ for (const field of within(container).getAllByRole("textbox")) {
150
+ for (const id of field.getAttribute("aria-describedby")!.split(" ")) {
151
+ expect(document.getElementById(id)).not.toBeNull();
152
+ }
153
+ }
154
+ expect(recoverableError).not.toHaveBeenCalled();
155
+ expect(consoleError).not.toHaveBeenCalled();
156
+ } finally {
157
+ await act(async () => { root?.unmount(); });
158
+ consoleError.mockRestore();
159
+ container.remove();
160
+ }
161
+ });