@tangle-network/ui 11.12.0 → 11.13.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 (276) 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/editor/editor-provider.d.ts +97 -0
  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} +60 -55
  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/files/rich-file-tree.stories.tsx +1 -1
  230. package/src/files/rich-file-tree.tsx +12 -9
  231. package/src/markdown/code-block.test.tsx +80 -2
  232. package/src/markdown/code-block.tsx +76 -80
  233. package/src/primitives/button.stories.tsx +37 -1
  234. package/src/primitives/button.test.tsx +230 -0
  235. package/src/primitives/button.tsx +62 -14
  236. package/src/primitives/card-anatomy.stories.tsx +51 -0
  237. package/src/primitives/card.test.tsx +71 -0
  238. package/src/primitives/card.tsx +25 -13
  239. package/src/primitives/heading.stories.tsx +35 -0
  240. package/src/primitives/heading.test.tsx +56 -0
  241. package/src/primitives/heading.tsx +76 -0
  242. package/src/primitives/index.ts +8 -0
  243. package/src/primitives/input.stories.tsx +28 -0
  244. package/src/primitives/input.test.tsx +161 -0
  245. package/src/primitives/input.tsx +34 -28
  246. package/src/primitives/page-header.stories.tsx +51 -0
  247. package/src/primitives/page-header.test.tsx +48 -0
  248. package/src/primitives/page-header.tsx +35 -35
  249. package/src/primitives/page-shell.stories.tsx +89 -0
  250. package/src/primitives/page-shell.test.tsx +24 -0
  251. package/src/primitives/page-shell.tsx +26 -0
  252. package/src/primitives/table-wrapper.stories.tsx +54 -0
  253. package/src/primitives/table.test.tsx +72 -0
  254. package/src/primitives/table.tsx +33 -12
  255. package/src/primitives/theme-toggle.test.tsx +167 -0
  256. package/src/primitives/theme-toggle.tsx +111 -39
  257. package/dist/active-sessions-store-nl70ZnMC.d.ts +0 -84
  258. package/dist/button-C87nEe2R.js +0 -78
  259. package/dist/chat-DDvF2oGQ.js +0 -672
  260. package/dist/diff-preview-BcK3nRXb.js +0 -533
  261. package/dist/document-editor-pane-BilJfiCo.d.ts +0 -136
  262. package/dist/document-editor-pane-POsq5Q0o.js +0 -1279
  263. package/dist/files-hywocVZz.js +0 -753
  264. package/dist/index-2UntyjBb.d.ts +0 -52
  265. package/dist/index-BgUHilZm.d.ts +0 -108
  266. package/dist/index-CJ8wRfiC.d.ts +0 -236
  267. package/dist/index-CPDET_s1.d.ts +0 -549
  268. package/dist/index-Cg6gYAqh.d.ts +0 -73
  269. package/dist/index-Cs7j8V7l.d.ts +0 -203
  270. package/dist/index-DjKxO357.d.ts +0 -128
  271. package/dist/primitives-Bx59fT-U.js +0 -1239
  272. package/dist/table-Bss73DhB.js +0 -146
  273. package/dist/tool-call-step-DNebqhmX.js +0 -434
  274. package/dist/use-auto-scroll-C5Pjeqon.js +0 -153
  275. package/dist/use-sse-stream-DjqRuDft.js +0 -884
  276. package/dist/use-sse-stream-Dm6R5YWz.d.ts +0 -282
@@ -1,11 +1,11 @@
1
1
  import { Slot } from "@radix-ui/react-slot";
2
- import { cva, type VariantProps } from "class-variance-authority";
2
+ import { cva } from "class-variance-authority";
3
3
  import * as React from "react";
4
4
  import { focusRing } from "../lib/focus";
5
5
  import { cn } from "../lib/utils";
6
6
 
7
7
  const buttonVariants = cva(
8
- `inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium text-sm transition-all ${focusRing} disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`,
8
+ `inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium text-sm transition-all ${focusRing} disabled:pointer-events-none disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 motion-reduce:transition-none motion-reduce:active:scale-100 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`,
9
9
  {
10
10
  variants: {
11
11
  variant: {
@@ -47,6 +47,36 @@ export interface ButtonProps
47
47
  children?: React.ReactNode;
48
48
  }
49
49
 
50
+ type ButtonChildProps = React.HTMLAttributes<HTMLElement> & {
51
+ disabled?: boolean;
52
+ href?: string;
53
+ };
54
+
55
+ function preventActivation(event: React.SyntheticEvent) {
56
+ event.preventDefault();
57
+ event.stopPropagation();
58
+ }
59
+
60
+ function disabledHandlers(props: React.HTMLAttributes<HTMLElement>) {
61
+ const guardKey = (handler?: React.KeyboardEventHandler<HTMLElement>) =>
62
+ (event: React.KeyboardEvent<HTMLElement>) => {
63
+ if (event.key === "Enter" || event.key === " ") preventActivation(event);
64
+ else handler?.(event);
65
+ };
66
+
67
+ return {
68
+ onClickCapture: preventActivation,
69
+ onAuxClickCapture: preventActivation,
70
+ onDoubleClickCapture: preventActivation,
71
+ onPointerDownCapture: preventActivation,
72
+ onPointerUpCapture: preventActivation,
73
+ onMouseDownCapture: preventActivation,
74
+ onMouseUpCapture: preventActivation,
75
+ onKeyDownCapture: guardKey(props.onKeyDownCapture),
76
+ onKeyUpCapture: guardKey(props.onKeyUpCapture),
77
+ };
78
+ }
79
+
50
80
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
51
81
  (
52
82
  {
@@ -61,36 +91,54 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
61
91
  },
62
92
  ref,
63
93
  ) => {
64
- const Comp = asChild ? Slot : "button";
65
-
66
- // When using asChild, we can't add the loading spinner as it would create multiple children
67
- // which breaks Slot's single-child requirement
68
94
  if (asChild) {
95
+ const child = React.isValidElement<ButtonChildProps>(children) ? children : null;
96
+ const isDisabled = disabled || loading || child?.props.disabled;
97
+
98
+ // Slot composes child handlers first. Guard BOTH capture paths so neither
99
+ // child nor Button activation handlers run, without changing enabled order.
100
+ // Keep slotted content intact: its action name must survive loading too.
69
101
  return (
70
- <Comp
102
+ <Slot
71
103
  className={cn(buttonVariants({ variant, size, className }))}
72
104
  ref={ref}
73
105
  {...props}
106
+ {...(isDisabled ? disabledHandlers(props) : {})}
74
107
  >
75
- {children}
76
- </Comp>
108
+ {child ? React.cloneElement(child, {
109
+ ...(loading ? { "aria-busy": true } : {}),
110
+ ...(isDisabled ? {
111
+ "aria-disabled": true,
112
+ ...(child.type === "button" ? { disabled: true } : { tabIndex: -1 }),
113
+ // Removing href also prevents native context-menu / middle-click
114
+ // navigation. Keep link semantics while it is unavailable.
115
+ ...(child.type === "a" ? {
116
+ href: undefined,
117
+ role: child.props.role ?? props.role ?? "link",
118
+ } : {}),
119
+ ...disabledHandlers(child.props),
120
+ } : {}),
121
+ }) : children}
122
+ </Slot>
77
123
  );
78
124
  }
79
125
 
80
126
  return (
81
- <Comp
127
+ <button
82
128
  className={cn(buttonVariants({ variant, size, className }))}
83
129
  ref={ref}
84
- disabled={disabled || loading}
85
130
  {...props}
131
+ disabled={disabled || loading}
132
+ aria-busy={loading || props["aria-busy"]}
86
133
  >
87
134
  {loading && (
88
135
  <svg
89
- className="mr-2 -ml-1 h-4 w-4 animate-spin"
136
+ className="mr-2 -ml-1 h-4 w-4 animate-spin motion-reduce:animate-none"
137
+ aria-hidden="true"
138
+ focusable="false"
90
139
  fill="none"
91
140
  viewBox="0 0 24 24"
92
141
  >
93
- <title>Loading spinner</title>
94
142
  <circle
95
143
  className="opacity-25"
96
144
  cx="12"
@@ -107,7 +155,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
107
155
  </svg>
108
156
  )}
109
157
  {children}
110
- </Comp>
158
+ </button>
111
159
  );
112
160
  },
113
161
  );
@@ -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
+ }