banhaten 0.4.0 → 0.6.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 (214) hide show
  1. package/CHANGELOG.md +78 -1
  2. package/CONTRIBUTING.md +6 -4
  3. package/README.md +4 -2
  4. package/docs/design-spacing.md +13 -0
  5. package/docs/design-system/README.md +2 -0
  6. package/docs/design-system/appearances/default.md +1 -1
  7. package/docs/design-system/appearances/rounded.md +1 -1
  8. package/docs/design-system/appearances/sharp.md +1 -1
  9. package/docs/design-system/component-showcase-consistency-report.md +5 -5
  10. package/docs/design-system/components/README.md +15 -0
  11. package/docs/design-system/components/accordion.md +1 -1
  12. package/docs/design-system/components/activity-feed.md +1 -1
  13. package/docs/design-system/components/activity-heatmap.md +1 -1
  14. package/docs/design-system/components/agent-input.md +85 -0
  15. package/docs/design-system/components/alert-dialog.md +1 -1
  16. package/docs/design-system/components/alert.md +10 -4
  17. package/docs/design-system/components/app-header.md +97 -0
  18. package/docs/design-system/components/approval-card.md +134 -0
  19. package/docs/design-system/components/attribute.md +14 -2
  20. package/docs/design-system/components/avatar.md +1 -1
  21. package/docs/design-system/components/badge.md +1 -1
  22. package/docs/design-system/components/banner.md +1 -1
  23. package/docs/design-system/components/breadcrumbs.md +8 -3
  24. package/docs/design-system/components/button-group.md +1 -1
  25. package/docs/design-system/components/button.md +8 -5
  26. package/docs/design-system/components/card.md +15 -1
  27. package/docs/design-system/components/catalog-components.md +1 -1
  28. package/docs/design-system/components/chart.md +95 -24
  29. package/docs/design-system/components/checkbox.md +1 -1
  30. package/docs/design-system/components/collapsible.md +20 -6
  31. package/docs/design-system/components/command.md +1 -1
  32. package/docs/design-system/components/context-menu.md +1 -1
  33. package/docs/design-system/components/date-picker.md +1 -1
  34. package/docs/design-system/components/file-upload.md +1 -1
  35. package/docs/design-system/components/hover-card.md +1 -1
  36. package/docs/design-system/components/inline-citations.md +93 -0
  37. package/docs/design-system/components/input-group.md +1 -1
  38. package/docs/design-system/components/input.md +59 -3
  39. package/docs/design-system/components/item.md +1 -1
  40. package/docs/design-system/components/kbd.md +6 -4
  41. package/docs/design-system/components/menu.md +35 -4
  42. package/docs/design-system/components/menubar.md +1 -1
  43. package/docs/design-system/components/modal.md +2 -2
  44. package/docs/design-system/components/native-select.md +1 -1
  45. package/docs/design-system/components/navigation-menu.md +1 -1
  46. package/docs/design-system/components/onboarding-step-list-item.md +1 -1
  47. package/docs/design-system/components/page-header.md +58 -17
  48. package/docs/design-system/components/pagination.md +2 -2
  49. package/docs/design-system/components/popover.md +1 -1
  50. package/docs/design-system/components/progress-slider.md +1 -1
  51. package/docs/design-system/components/radio-card.md +1 -1
  52. package/docs/design-system/components/radio-group.md +1 -1
  53. package/docs/design-system/components/resizable.md +1 -1
  54. package/docs/design-system/components/rolling-count.md +61 -0
  55. package/docs/design-system/components/scroll-area.md +1 -1
  56. package/docs/design-system/components/select.md +12 -2
  57. package/docs/design-system/components/sheet.md +1 -1
  58. package/docs/design-system/components/shimmer-text.md +67 -0
  59. package/docs/design-system/components/sidebar.md +75 -8
  60. package/docs/design-system/components/skeleton.md +2 -2
  61. package/docs/design-system/components/slideout.md +1 -1
  62. package/docs/design-system/components/slider.md +2 -2
  63. package/docs/design-system/components/social-button.md +2 -2
  64. package/docs/design-system/components/streaming-text.md +82 -0
  65. package/docs/design-system/components/table.md +103 -21
  66. package/docs/design-system/components/tabs.md +25 -6
  67. package/docs/design-system/components/tag.md +1 -1
  68. package/docs/design-system/components/task-list.md +95 -0
  69. package/docs/design-system/components/textarea.md +1 -1
  70. package/docs/design-system/components/thinking-reasoning.md +72 -0
  71. package/docs/design-system/components/thinking-state.md +67 -0
  72. package/docs/design-system/components/timeline.md +1 -1
  73. package/docs/design-system/components/toast.md +1 -1
  74. package/docs/design-system/components/toggle.md +2 -2
  75. package/docs/design-system/components/toolbar.md +11 -6
  76. package/docs/design-system/components/tooltip.md +6 -4
  77. package/docs/design-system/density-and-elevation.md +16 -16
  78. package/docs/design-system/design-system-test-missing-items.md +8 -8
  79. package/docs/design-system/installation.md +2 -0
  80. package/docs/design-system/tabs.md +2 -1
  81. package/docs/design-system/template-gap-audit.md +32 -0
  82. package/docs/design-system/templates.md +59 -0
  83. package/docs/design-system/token-governance.md +15 -2
  84. package/package.json +2 -2
  85. package/registry/components/accordion.tsx +2 -2
  86. package/registry/components/agent-input-parts.tsx +552 -0
  87. package/registry/components/agent-input-state.ts +156 -0
  88. package/registry/components/agent-input.css +307 -0
  89. package/registry/components/agent-input.tsx +382 -0
  90. package/registry/components/alert-dialog.tsx +2 -2
  91. package/registry/components/alert.tsx +23 -7
  92. package/registry/components/app-header.tsx +151 -0
  93. package/registry/components/approval-card.css +409 -0
  94. package/registry/components/approval-card.tsx +401 -0
  95. package/registry/components/approval-frame.tsx +108 -0
  96. package/registry/components/approval-questions.tsx +559 -0
  97. package/registry/components/attribute.tsx +48 -13
  98. package/registry/components/autocomplete.tsx +15 -68
  99. package/registry/components/avatar.tsx +2 -2
  100. package/registry/components/badge.tsx +8 -7
  101. package/registry/components/button-group.tsx +2 -2
  102. package/registry/components/button.tsx +19 -14
  103. package/registry/components/card.tsx +18 -5
  104. package/registry/components/chart-block-data.ts +177 -0
  105. package/registry/components/chart-block.css +171 -0
  106. package/registry/components/chart-block.tsx +260 -0
  107. package/registry/components/chart-interactions.ts +123 -0
  108. package/registry/components/chart-options.cartesian.ts +162 -0
  109. package/registry/components/chart-options.paint.ts +99 -0
  110. package/registry/components/chart-options.parts.ts +72 -0
  111. package/registry/components/chart-options.radial.ts +109 -0
  112. package/registry/components/chart-options.ts +44 -0
  113. package/registry/components/chart-options.types.ts +67 -0
  114. package/registry/components/chart.tsx +1 -1
  115. package/registry/components/checkbox.tsx +6 -6
  116. package/registry/components/collapsible.tsx +43 -3
  117. package/registry/components/command.tsx +1 -1
  118. package/registry/components/context-menu.tsx +1 -1
  119. package/registry/components/data-chart.tsx +40 -0
  120. package/registry/components/date-picker.tsx +9 -9
  121. package/registry/components/echart-theme.ts +246 -0
  122. package/registry/components/echart.css +49 -0
  123. package/registry/components/echart.tsx +178 -0
  124. package/registry/components/expanded/Breadcrumbs.tsx +5 -1
  125. package/registry/components/expanded/PageHeader.tsx +67 -24
  126. package/registry/components/expanded/Table.tsx +60 -22
  127. package/registry/components/expanded/activityFeed.css +2 -2
  128. package/registry/components/expanded/activityHeatmap.css +1 -1
  129. package/registry/components/expanded/banner.css +2 -2
  130. package/registry/components/expanded/breadcrumbs.css +19 -2
  131. package/registry/components/expanded/catalogComponentsShowcase.css +1 -1
  132. package/registry/components/expanded/fileUpload.css +7 -7
  133. package/registry/components/expanded/onboardingStepListItem.css +2 -2
  134. package/registry/components/expanded/pageHeader.css +38 -44
  135. package/registry/components/expanded/slideout.css +4 -4
  136. package/registry/components/expanded/table.css +58 -9
  137. package/registry/components/expanded/timeline.css +1 -1
  138. package/registry/components/hover-card.tsx +1 -1
  139. package/registry/components/inline-citations.css +126 -0
  140. package/registry/components/inline-citations.tsx +292 -0
  141. package/registry/components/input-group.tsx +4 -4
  142. package/registry/components/input.tsx +150 -137
  143. package/registry/components/item.tsx +1 -1
  144. package/registry/components/kbd.tsx +3 -2
  145. package/registry/components/menu.tsx +117 -12
  146. package/registry/components/menubar.tsx +2 -2
  147. package/registry/components/modal.tsx +3 -3
  148. package/registry/components/native-select.tsx +4 -4
  149. package/registry/components/navigation-menu.tsx +2 -2
  150. package/registry/components/pagination.tsx +4 -4
  151. package/registry/components/popover.tsx +3 -3
  152. package/registry/components/progress-slider.tsx +1 -1
  153. package/registry/components/radio-card.tsx +7 -7
  154. package/registry/components/radio-group.tsx +2 -2
  155. package/registry/components/resizable.tsx +2 -2
  156. package/registry/components/rolling-count.css +37 -0
  157. package/registry/components/rolling-count.tsx +110 -0
  158. package/registry/components/scroll-area.tsx +1 -1
  159. package/registry/components/select.tsx +15 -10
  160. package/registry/components/sheet.tsx +2 -2
  161. package/registry/components/shimmer-text.css +69 -0
  162. package/registry/components/shimmer-text.tsx +43 -0
  163. package/registry/components/sidebar.tsx +528 -223
  164. package/registry/components/skeleton.tsx +1 -1
  165. package/registry/components/slider.tsx +2 -2
  166. package/registry/components/social-button.tsx +1 -1
  167. package/registry/components/streaming-text.css +45 -0
  168. package/registry/components/streaming-text.tsx +165 -0
  169. package/registry/components/table-elements.tsx +164 -5
  170. package/registry/components/tabs.css +32 -4
  171. package/registry/components/tabs.tsx +5 -1
  172. package/registry/components/tag.tsx +1 -1
  173. package/registry/components/task-list.css +328 -0
  174. package/registry/components/task-list.tsx +429 -0
  175. package/registry/components/textarea.tsx +4 -4
  176. package/registry/components/thinking-reasoning.css +178 -0
  177. package/registry/components/thinking-reasoning.tsx +253 -0
  178. package/registry/components/thinking-state.css +8 -0
  179. package/registry/components/thinking-state.tsx +46 -0
  180. package/registry/components/toast.tsx +1 -1
  181. package/registry/components/toggle.tsx +2 -2
  182. package/registry/components/toolbar.tsx +6 -6
  183. package/registry/components/tooltip.tsx +6 -4
  184. package/registry/examples/agent-input-demo.tsx +90 -0
  185. package/registry/examples/app-header-demo.tsx +40 -0
  186. package/registry/examples/approval-card-demo.tsx +83 -0
  187. package/registry/examples/button-demo.tsx +3 -0
  188. package/registry/examples/chart-block-demo.tsx +5 -0
  189. package/registry/examples/chart-demo.tsx +6 -21
  190. package/registry/examples/collection-filter-bar-demo.tsx +1 -1
  191. package/registry/examples/control-density-contract-demo.tsx +5 -1
  192. package/registry/examples/inline-citations-demo.tsx +31 -0
  193. package/registry/examples/input-actions-demo.tsx +36 -0
  194. package/registry/examples/menu-demo.tsx +1 -1
  195. package/registry/examples/popover-demo.tsx +1 -0
  196. package/registry/examples/rolling-count-demo.tsx +39 -0
  197. package/registry/examples/shimmer-text-demo.tsx +20 -0
  198. package/registry/examples/streaming-text-demo.tsx +28 -0
  199. package/registry/examples/task-list-demo.tsx +74 -0
  200. package/registry/examples/thinking-reasoning-demo.tsx +74 -0
  201. package/registry/examples/thinking-state-demo.tsx +13 -0
  202. package/registry/guidance/charts.LICENSE.txt +24 -0
  203. package/registry/hooks/use-controllable-state.ts +33 -0
  204. package/registry/hooks/use-listbox-navigation.ts +73 -0
  205. package/registry/hooks/use-motion-end.ts +55 -0
  206. package/registry/hooks/use-reduced-motion.ts +28 -0
  207. package/registry/index.json +1808 -190
  208. package/registry/styles/globals.css +992 -1088
  209. package/registry.json +385 -10
  210. package/schema/registry.schema.json +5 -1
  211. package/schema/tokens.schema.json +8 -4
  212. package/src/cli/file-transaction.js +15 -8
  213. package/src/cli/index.js +22 -29
  214. package/tokens/banhaten.tokens.json +490 -192
@@ -252,7 +252,7 @@ function SkeletonCard({
252
252
  aria-hidden="true"
253
253
  data-slot="skeleton-card"
254
254
  className={cn(
255
- "grid gap-[var(--bh-space-4xl-20)] rounded-[var(--bh-radius-xl-10)] border border-[var(--bh-border-default)] bg-[var(--bh-bg-raised)] p-[var(--bh-card-desktop-content-padding)] shadow-[var(--shadow-component-default)]",
255
+ "grid gap-[var(--bh-space-4xl-20)] rounded-[var(--bh-radius-xl-10)] border border-[var(--bh-border-default)] bg-[var(--bh-bg-raised)] p-[var(--bh-card-desktop-content-padding)] shadow-[var(--elevation-component-default)]",
256
256
  className
257
257
  )}
258
258
  {...props}
@@ -244,7 +244,7 @@ function Slider({
244
244
  data-has-label={showInlineLabel ? "true" : undefined}
245
245
  data-slot="slider-thumb"
246
246
  key={index}
247
- className="relative z-[var(--bh-z-popover)] flex size-[var(--bh-slider-thumb-size-current)] shrink-0 items-center justify-center rounded-[var(--bh-slider-thumb-radius)] border-[var(--bh-slider-thumb-ring-width-current)] border-[var(--bh-slider-thumb-ring-bg)] bg-[var(--bh-slider-thumb-bg)] text-[var(--bh-content-default)] shadow-[var(--shadow-slider-thumb)] outline-none transition-[background-color,border-color,box-shadow] focus-visible:shadow-[var(--shadow-slider-thumb-focus)] group-data-[variant=pill]/slider:h-[var(--bh-slider-pill-height)] group-data-[variant=pill]/slider:w-[var(--bh-slider-pill-thumb-width)] group-data-[variant=pill]/slider:justify-end group-data-[variant=pill]/slider:rounded-[var(--bh-slider-pill-radius)] group-data-[variant=pill]/slider:border-[length:var(--bh-slider-pill-thumb-border-width)] group-data-[variant=pill]/slider:border-[var(--bh-slider-thumb-pill-border)] group-data-[variant=pill]/slider:bg-[var(--bh-slider-thumb-pill-bg)] group-data-[variant=pill]/slider:px-[var(--bh-space-lg-10)] group-data-[disabled]/slider:border-[var(--bh-bg-default)] group-data-[disabled]/slider:bg-[var(--bh-content-disabled)] group-data-[disabled]/slider:text-[var(--bh-content-disabled)] group-data-[variant=pill]/slider:group-data-[disabled]/slider:border-[var(--bh-border-disabled)] group-data-[variant=pill]/slider:group-data-[disabled]/slider:bg-[var(--bh-bg-disabled)]"
247
+ className="relative z-[var(--bh-z-popover)] flex size-[var(--bh-slider-thumb-size-current)] shrink-0 items-center justify-center rounded-[var(--bh-slider-thumb-radius)] border-[var(--bh-slider-thumb-ring-width-current)] border-[var(--bh-slider-thumb-ring-bg)] bg-[var(--bh-slider-thumb-bg)] text-[var(--bh-content-default)] shadow-[var(--elevation-slider-thumb)] outline-none transition-[background-color,border-color,box-shadow] focus-visible:shadow-[var(--elevation-slider-thumb-focus)] group-data-[variant=pill]/slider:h-[var(--bh-slider-pill-height)] group-data-[variant=pill]/slider:w-[var(--bh-slider-pill-thumb-width)] group-data-[variant=pill]/slider:justify-end group-data-[variant=pill]/slider:rounded-[var(--bh-slider-pill-radius)] group-data-[variant=pill]/slider:border-[length:var(--bh-slider-pill-thumb-border-width)] group-data-[variant=pill]/slider:border-[var(--bh-slider-thumb-pill-border)] group-data-[variant=pill]/slider:bg-[var(--bh-slider-thumb-pill-bg)] group-data-[variant=pill]/slider:px-[var(--bh-space-lg-10)] group-data-[disabled]/slider:border-[var(--bh-bg-default)] group-data-[disabled]/slider:bg-[var(--bh-content-disabled)] group-data-[disabled]/slider:text-[var(--bh-content-disabled)] group-data-[variant=pill]/slider:group-data-[disabled]/slider:border-[var(--bh-border-disabled)] group-data-[variant=pill]/slider:group-data-[disabled]/slider:bg-[var(--bh-bg-disabled)]"
248
248
  >
249
249
  {showIndicator ? (
250
250
  <SliderThumbIndicator
@@ -296,7 +296,7 @@ function SliderThumbIndicator({
296
296
  ? "bottom-[calc(100%+var(--bh-slider-indicator-gap))]"
297
297
  : "top-[calc(100%+var(--bh-slider-indicator-gap))]",
298
298
  isTooltip
299
- ? "rounded-[var(--bh-radius-md-6)] border border-[var(--bh-border-subtle)] bg-[var(--bh-bg-raised)] px-[var(--bh-space-md-8)] py-[var(--bh-space-sm-6)] text-[length:var(--bh-text-body-2xs-medium-font-size)] leading-[var(--bh-text-body-2xs-medium-line-height)] font-[var(--bh-text-body-2xs-medium-font-weight)] tracking-[var(--bh-text-body-2xs-medium-letter-spacing)] text-[var(--bh-content-default)] shadow-[var(--shadow-slider-tooltip)]"
299
+ ? "rounded-[var(--bh-radius-md-6)] border border-[var(--bh-border-subtle)] bg-[var(--bh-bg-raised)] px-[var(--bh-space-md-8)] py-[var(--bh-space-sm-6)] text-[length:var(--bh-text-body-2xs-medium-font-size)] leading-[var(--bh-text-body-2xs-medium-line-height)] font-[var(--bh-text-body-2xs-medium-font-weight)] tracking-[var(--bh-text-body-2xs-medium-letter-spacing)] text-[var(--bh-content-default)] shadow-[var(--elevation-slider-tooltip)]"
300
300
  : "text-[length:var(--bh-text-body-2xs-regular-font-size)] leading-[var(--bh-text-body-2xs-regular-line-height)] font-[var(--bh-text-body-2xs-regular-font-weight)] tracking-[var(--bh-text-body-2xs-regular-letter-spacing)] text-[var(--bh-content-subtle)]"
301
301
  )}
302
302
  >
@@ -116,7 +116,7 @@ const arabicSocialButtonMessages: SocialButtonMessages = {
116
116
  }
117
117
 
118
118
  const socialButtonVariants = cva(
119
- "group/social-button relative inline-flex max-w-full min-w-0 items-center justify-center whitespace-nowrap rounded-[var(--bh-social-button-radius)] bg-[var(--bh-social-button-bg)] text-[var(--bh-social-button-fg)] shadow-[var(--shadow-social-button)] outline-none transition-[background-color,box-shadow,color] [--bh-social-button-current-border:var(--bh-social-button-border)] [--shadow-social-button:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-social-button-current-border,var(--bh-social-button-border,transparent)),var(--shadow-component-default)] hover:bg-[var(--bh-social-button-bg-hover)] focus-visible:[--bh-social-button-current-border:var(--bh-social-button-focus-border)] focus-visible:shadow-[var(--shadow-social-button-focus)] disabled:pointer-events-none disabled:opacity-[var(--bh-opacity-50)] [&_svg]:pointer-events-none [&_svg]:shrink-0",
119
+ "group/social-button relative inline-flex max-w-full min-w-0 items-center justify-center whitespace-nowrap rounded-[var(--bh-social-button-radius)] bg-[var(--bh-social-button-bg)] text-[var(--bh-social-button-fg)] shadow-[var(--elevation-social-button)] outline-none transition-[background-color,box-shadow,color] [--bh-social-button-current-border:var(--bh-social-button-border)] [--elevation-social-button:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-social-button-current-border,var(--bh-social-button-border,transparent)),var(--elevation-component-default)] hover:bg-[var(--bh-social-button-bg-hover)] focus-visible:[--bh-social-button-current-border:var(--bh-social-button-focus-border)] focus-visible:shadow-[var(--elevation-social-button-focus)] disabled:pointer-events-none disabled:opacity-[var(--bh-opacity-50)] [&_svg]:pointer-events-none [&_svg]:shrink-0",
120
120
  {
121
121
  variants: {
122
122
  variant: {
@@ -0,0 +1,45 @@
1
+ [data-slot="streaming-text"] {
2
+ color: var(--bh-content-default);
3
+ font-family: var(--bh-font-family);
4
+ font-size: var(--bh-text-body-sm-regular-font-size);
5
+ font-weight: var(--bh-text-body-sm-regular-font-weight);
6
+ letter-spacing: var(--bh-text-body-sm-regular-letter-spacing);
7
+ line-height: var(--bh-text-body-sm-regular-line-height);
8
+ overflow-wrap: anywhere;
9
+ }
10
+
11
+ /* A slim text caret, not a block cursor: caret width comes from the scale. */
12
+ [data-slot="streaming-text-caret"] {
13
+ animation: bh-streaming-text-caret var(--bh-streaming-text-caret-duration)
14
+ step-end infinite;
15
+ background-color: currentColor;
16
+ block-size: 1em;
17
+ border-radius: var(--bh-radius-full);
18
+ display: inline-block;
19
+ inline-size: var(--bh-streaming-text-caret-width);
20
+ margin-inline-start: var(--bh-space-xxs-2);
21
+ vertical-align: text-bottom;
22
+ }
23
+
24
+ /* Solid while tokens arrive; the blink only starts once the text is complete. */
25
+ [data-slot="streaming-text"][data-state="streaming"]
26
+ [data-slot="streaming-text-caret"] {
27
+ animation: none;
28
+ }
29
+
30
+ @keyframes bh-streaming-text-caret {
31
+ 0%,
32
+ 100% {
33
+ opacity: var(--bh-opacity-100);
34
+ }
35
+
36
+ 50% {
37
+ opacity: var(--bh-opacity-0);
38
+ }
39
+ }
40
+
41
+ @media (prefers-reduced-motion: reduce) {
42
+ [data-slot="streaming-text-caret"] {
43
+ animation: none;
44
+ }
45
+ }
@@ -0,0 +1,165 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+ import { useReducedMotion } from "@/hooks/use-reduced-motion"
7
+
8
+ import "./streaming-text.css"
9
+
10
+ type StreamingTextElement = "p" | "div" | "span"
11
+
12
+ type StreamingTextProps = Omit<React.HTMLAttributes<HTMLElement>, "children"> & {
13
+ /** Render element. Defaults to a paragraph. */
14
+ as?: StreamingTextElement
15
+ /** Hides the caret entirely. */
16
+ caret?: boolean
17
+ /**
18
+ * True while more text is on its way: the caret stays solid and the element
19
+ * reports itself busy.
20
+ */
21
+ streaming?: boolean
22
+ /** The text that has arrived so far. */
23
+ text: string
24
+ }
25
+
26
+ type UseTypewriterOptions = {
27
+ /** Called once each time the reveal reaches the end of a text. */
28
+ onComplete?: () => void
29
+ /** Characters revealed per second. */
30
+ speed?: number
31
+ }
32
+
33
+ type TypewriterState = {
34
+ /** The part of the text revealed so far. */
35
+ text: string
36
+ /** True until the whole text is revealed. */
37
+ typing: boolean
38
+ }
39
+
40
+ const DEFAULT_SPEED = 220
41
+
42
+ /**
43
+ * Reveals a finished text locally, a few characters per frame. Text that grows
44
+ * continues from what is already shown and different text starts over. Under
45
+ * reduced motion the whole text shows at once.
46
+ */
47
+ function useTypewriter(
48
+ text: string,
49
+ { onComplete, speed = DEFAULT_SPEED }: UseTypewriterOptions = {}
50
+ ): TypewriterState {
51
+ const reducedMotion = useReducedMotion()
52
+ const [shown, setShown] = React.useState(() => (reducedMotion ? text : ""))
53
+ const shownRef = React.useRef(shown)
54
+ const onCompleteRef = React.useRef(onComplete)
55
+ // The text each completion was reported for, so a speed change after the
56
+ // reveal (or a reduced-motion rerender) does not report it twice.
57
+ const completedFor = React.useRef<string | null>(null)
58
+
59
+ React.useEffect(() => {
60
+ onCompleteRef.current = onComplete
61
+ })
62
+
63
+ const reveal = React.useCallback((value: string) => {
64
+ shownRef.current = value
65
+ setShown(value)
66
+ }, [])
67
+
68
+ const complete = React.useCallback((value: string) => {
69
+ if (completedFor.current === value) return
70
+ completedFor.current = value
71
+ onCompleteRef.current?.()
72
+ }, [])
73
+
74
+ React.useEffect(() => {
75
+ if (reducedMotion) {
76
+ reveal(text)
77
+ complete(text)
78
+ return
79
+ }
80
+
81
+ // Continue from the current prefix when more text is appended, restart
82
+ // when the text changes to something unrelated.
83
+ const start = text.startsWith(shownRef.current) ? shownRef.current.length : 0
84
+ // Nothing left to type, including an empty response: report it once so a
85
+ // host waiting on onComplete never hangs.
86
+ if (start >= text.length) {
87
+ reveal(text)
88
+ complete(text)
89
+ return
90
+ }
91
+
92
+ const charsPerMs = Math.max(1, speed) / 1000
93
+ let frame = 0
94
+ let origin: number | null = null
95
+
96
+ const tick = (now: number) => {
97
+ if (origin === null) origin = now
98
+ const count = Math.min(
99
+ text.length,
100
+ start + Math.floor((now - origin) * charsPerMs)
101
+ )
102
+ const next = text.slice(0, count)
103
+ if (next !== shownRef.current) reveal(next)
104
+ if (count < text.length) {
105
+ frame = window.requestAnimationFrame(tick)
106
+ } else {
107
+ complete(text)
108
+ }
109
+ }
110
+
111
+ frame = window.requestAnimationFrame(tick)
112
+ return () => window.cancelAnimationFrame(frame)
113
+ }, [complete, reducedMotion, reveal, speed, text])
114
+
115
+ // Always a prefix of the current text, even in the render before the reveal
116
+ // catches up with a new one.
117
+ const visible = text.startsWith(shown) ? shown : ""
118
+ return { text: visible, typing: visible.length < text.length }
119
+ }
120
+
121
+ /**
122
+ * Text that arrives token by token. While `streaming` the caret stays solid;
123
+ * once the text is complete it settles into a slow blink. Pair it with
124
+ * `useTypewriter` to reveal a finished text locally.
125
+ */
126
+ const StreamingText = React.forwardRef<HTMLElement, StreamingTextProps>(
127
+ function StreamingText(
128
+ {
129
+ as: Comp = "p",
130
+ caret = true,
131
+ className,
132
+ streaming = false,
133
+ text,
134
+ ...props
135
+ },
136
+ ref
137
+ ) {
138
+ return (
139
+ <Comp
140
+ aria-busy={streaming || undefined}
141
+ data-caret={caret ? "true" : "false"}
142
+ data-slot="streaming-text"
143
+ data-state={streaming ? "streaming" : "complete"}
144
+ // JSX checks a union tag against one member's ref type; every allowed
145
+ // element is an HTMLElement, which is what the public ref promises.
146
+ ref={ref as React.Ref<HTMLDivElement>}
147
+ className={cn("m-0 min-w-0 max-w-full", className)}
148
+ {...props}
149
+ >
150
+ {text}
151
+ {caret ? (
152
+ <span aria-hidden="true" data-slot="streaming-text-caret" />
153
+ ) : null}
154
+ </Comp>
155
+ )
156
+ }
157
+ )
158
+
159
+ export { StreamingText, useTypewriter }
160
+ export type {
161
+ StreamingTextElement,
162
+ StreamingTextProps,
163
+ TypewriterState,
164
+ UseTypewriterOptions,
165
+ }
@@ -1,15 +1,33 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react"
4
+ import { Slot } from "@radix-ui/react-slot"
4
5
 
5
6
  import { cn } from "@/lib/utils"
6
7
 
7
- type TableRootProps = React.ComponentPropsWithoutRef<"table">
8
+ type TableElementsSize = "comfortable"
9
+ type TableRootProps = React.ComponentPropsWithoutRef<"table"> & {
10
+ /** Applies the Table item 1.2 spec. Without a size the elements stay unstyled. */
11
+ size?: TableElementsSize
12
+ }
13
+ type TableContainerProps = React.ComponentPropsWithoutRef<"div">
8
14
  type TableSectionProps = React.ComponentPropsWithoutRef<"thead">
9
15
  type TableBodyProps = React.ComponentPropsWithoutRef<"tbody">
10
16
  type TableRowElementProps = React.ComponentPropsWithoutRef<"tr">
11
17
  type TableHeadProps = React.ComponentPropsWithoutRef<"th">
12
18
  type TableCellProps = React.ComponentPropsWithoutRef<"td">
19
+ type TableCellTextProps = React.ComponentPropsWithoutRef<"span"> & {
20
+ /** Second line under the label, in body-2xs and content/subtle. */
21
+ caption?: React.ReactNode
22
+ /** Icon, avatar or logo before the text, 6px from it. */
23
+ leading?: React.ReactNode
24
+ tone?: "default" | "subtle"
25
+ weight?: "regular" | "medium"
26
+ }
27
+ type TableCellLinkProps = React.ComponentPropsWithoutRef<"button"> & {
28
+ /** Renders the child element, such as an anchor, with the link styling. */
29
+ asChild?: boolean
30
+ }
13
31
  type DataTableSurfaceProps = React.ComponentPropsWithoutRef<"div">
14
32
 
15
33
  const DataTableSurface = React.forwardRef<
@@ -27,13 +45,44 @@ const DataTableSurface = React.forwardRef<
27
45
  )
28
46
  })
29
47
 
48
+ /**
49
+ * Rounded table frame. Its border is drawn over the content, like a Figma
50
+ * inside stroke, so header fills reach the edge; the table scrolls inside it.
51
+ */
52
+ const TableContainer = React.forwardRef<HTMLDivElement, TableContainerProps>(
53
+ function TableContainer({ children, className, ...props }, ref) {
54
+ return (
55
+ <div
56
+ data-slot="table-container"
57
+ ref={ref}
58
+ className={cn(
59
+ "relative min-w-0 max-w-full overflow-hidden rounded-[var(--bh-table-container-radius)] bg-[var(--bh-table-container-bg)]",
60
+ "after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_var(--bh-border-width-default)_var(--bh-table-container-border)] after:content-['']",
61
+ className
62
+ )}
63
+ {...props}
64
+ >
65
+ <div data-slot="table-scroll" className="max-w-full overflow-x-auto">
66
+ {children}
67
+ </div>
68
+ </div>
69
+ )
70
+ }
71
+ )
72
+
30
73
  const TableRoot = React.forwardRef<HTMLTableElement, TableRootProps>(
31
- function TableRoot({ className, ...props }, ref) {
74
+ function TableRoot({ className, size, ...props }, ref) {
32
75
  return (
33
76
  <table
77
+ data-size={size}
34
78
  data-slot="table-root"
35
79
  ref={ref}
36
- className={cn("w-full border-collapse text-start", className)}
80
+ className={cn(
81
+ "group/table w-full border-collapse text-start",
82
+ size === "comfortable" &&
83
+ "min-w-[var(--bh-table-comfortable-min-width)] text-[var(--bh-table-comfortable-cell-content)]",
84
+ className
85
+ )}
37
86
  {...props}
38
87
  />
39
88
  )
@@ -87,7 +136,12 @@ const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps>(
87
136
  <th
88
137
  data-slot="table-head"
89
138
  ref={ref}
90
- className={cn("text-start", className)}
139
+ className={cn(
140
+ "text-start",
141
+ "group-data-[size=comfortable]/table:h-[var(--bh-table-comfortable-header-height)] group-data-[size=comfortable]/table:bg-[var(--bh-table-comfortable-header-bg)] group-data-[size=comfortable]/table:px-[var(--bh-table-comfortable-cell-padding-x)] group-data-[size=comfortable]/table:py-[var(--bh-space-none)] group-data-[size=comfortable]/table:align-middle",
142
+ "group-data-[size=comfortable]/table:text-[length:var(--bh-text-body-sm-medium-font-size)] group-data-[size=comfortable]/table:font-[var(--bh-text-body-sm-medium-font-weight)] group-data-[size=comfortable]/table:leading-[var(--bh-text-body-sm-medium-line-height)] group-data-[size=comfortable]/table:tracking-[var(--bh-text-body-sm-medium-letter-spacing)] group-data-[size=comfortable]/table:text-[var(--bh-table-comfortable-header-content)]",
143
+ className
144
+ )}
91
145
  scope={scope}
92
146
  {...props}
93
147
  />
@@ -101,7 +155,105 @@ const TableCell = React.forwardRef<HTMLTableCellElement, TableCellProps>(
101
155
  <td
102
156
  data-slot="table-cell"
103
157
  ref={ref}
104
- className={className}
158
+ className={cn(
159
+ "group-data-[size=comfortable]/table:h-[var(--bh-table-comfortable-row-height)] group-data-[size=comfortable]/table:px-[var(--bh-table-comfortable-cell-padding-x)] group-data-[size=comfortable]/table:py-[var(--bh-space-none)] group-data-[size=comfortable]/table:text-start group-data-[size=comfortable]/table:align-middle",
160
+ "group-data-[size=comfortable]/table:text-[length:var(--bh-text-body-md-regular-font-size)] group-data-[size=comfortable]/table:font-[var(--bh-text-body-md-regular-font-weight)] group-data-[size=comfortable]/table:leading-[var(--bh-text-body-md-regular-line-height)] group-data-[size=comfortable]/table:tracking-[var(--bh-text-body-md-regular-letter-spacing)]",
161
+ className
162
+ )}
163
+ {...props}
164
+ />
165
+ )
166
+ }
167
+ )
168
+
169
+ /**
170
+ * Table item 1.2 text content: a label with an optional caption under it
171
+ * (line + subline) and an optional leading icon or avatar. The label keeps
172
+ * the cell's text size, so a captioned cell is 40px of text in a 56px or
173
+ * taller row. When the label opens a record, pass a TableCellLink so the
174
+ * label and caption keep one start edge.
175
+ */
176
+ const TableCellText = React.forwardRef<HTMLSpanElement, TableCellTextProps>(
177
+ function TableCellText(
178
+ {
179
+ caption,
180
+ children,
181
+ className,
182
+ leading,
183
+ tone = "default",
184
+ weight = "regular",
185
+ ...props
186
+ },
187
+ ref
188
+ ) {
189
+ return (
190
+ <span
191
+ data-slot="table-cell-text"
192
+ data-tone={tone}
193
+ data-weight={weight}
194
+ ref={ref}
195
+ className={cn(
196
+ "inline-flex min-w-0 max-w-full items-center gap-[var(--bh-table-cell-text-gap)] align-middle",
197
+ className
198
+ )}
199
+ {...props}
200
+ >
201
+ {leading && (
202
+ <span
203
+ data-slot="table-cell-text-leading"
204
+ className="inline-flex shrink-0 items-center justify-center"
205
+ >
206
+ {leading}
207
+ </span>
208
+ )}
209
+ <span className="grid min-w-0 grid-cols-[minmax(0,1fr)]">
210
+ {/* Label and caption clip long text with an ellipsis; the clip margin leaves room for a link's focus ring and offset. */}
211
+ <span
212
+ data-slot="table-cell-text-label"
213
+ dir="auto"
214
+ className={cn(
215
+ "overflow-clip text-ellipsis whitespace-nowrap [overflow-clip-margin:var(--bh-space-md-8)]",
216
+ weight === "medium"
217
+ ? "font-[var(--bh-font-weight-medium)]"
218
+ : "font-[var(--bh-font-weight-regular)]",
219
+ tone === "subtle" && "text-[var(--bh-content-subtle)]"
220
+ )}
221
+ >
222
+ {children}
223
+ </span>
224
+ {caption && (
225
+ <span
226
+ data-slot="table-cell-text-caption"
227
+ dir="auto"
228
+ className="overflow-clip text-ellipsis whitespace-nowrap [overflow-clip-margin:var(--bh-space-md-8)] text-[length:var(--bh-text-body-2xs-regular-font-size)] font-[var(--bh-text-body-2xs-regular-font-weight)] leading-[var(--bh-text-body-2xs-regular-line-height)] tracking-[var(--bh-text-body-2xs-regular-letter-spacing)] text-[var(--bh-table-caption-content)]"
229
+ >
230
+ {caption}
231
+ </span>
232
+ )}
233
+ </span>
234
+ </span>
235
+ )
236
+ }
237
+ )
238
+
239
+ /**
240
+ * A cell label that opens a record. It reads as the cell's own text, flush
241
+ * with the cell padding, and underlines on hover; Table item 1.2 has no
242
+ * padded control in text cells. Renders a button, or its child with asChild.
243
+ */
244
+ const TableCellLink = React.forwardRef<HTMLButtonElement, TableCellLinkProps>(
245
+ function TableCellLink({ asChild = false, className, type, ...props }, ref) {
246
+ const Comp = asChild ? Slot : "button"
247
+ return (
248
+ <Comp
249
+ data-slot="table-cell-link"
250
+ ref={ref}
251
+ type={asChild ? undefined : (type ?? "button")}
252
+ className={cn(
253
+ "inline-block max-w-full cursor-pointer truncate rounded-[var(--bh-radius-sm-4)] border-0 bg-transparent p-[var(--bh-space-none)] text-start align-top [color:inherit] [font:inherit] tracking-[inherit] no-underline outline-none hover:underline",
254
+ "focus-visible:underline focus-visible:outline-solid focus-visible:outline-[length:var(--bh-focus-ring-width)] focus-visible:outline-offset-[var(--bh-space-xxs-2)] focus-visible:outline-[var(--bh-border-focus)]",
255
+ className
256
+ )}
105
257
  {...props}
106
258
  />
107
259
  )
@@ -112,6 +264,9 @@ export {
112
264
  DataTableSurface,
113
265
  TableBody,
114
266
  TableCell,
267
+ TableCellLink,
268
+ TableCellText,
269
+ TableContainer,
115
270
  TableHead,
116
271
  TableHeader,
117
272
  TableRoot,
@@ -120,7 +275,11 @@ export {
120
275
  export type {
121
276
  DataTableSurfaceProps,
122
277
  TableBodyProps,
278
+ TableCellLinkProps,
123
279
  TableCellProps,
280
+ TableCellTextProps,
281
+ TableContainerProps,
282
+ TableElementsSize,
124
283
  TableHeadProps,
125
284
  TableRootProps,
126
285
  TableRowElementProps,
@@ -22,7 +22,7 @@
22
22
  --tabs-height-underline-md: calc(var(--bh-space-8xl-48) + var(--bh-space-md-8));
23
23
  --tabs-height-underline-sm: var(--bh-space-8xl-48);
24
24
  --tabs-height-underline-content: calc(var(--bh-space-6xl-32) + var(--bh-space-xs-4));
25
- --tabs-shadow-selected: var(--shadow-button-soft);
25
+ --tabs-elevation-selected: var(--elevation-button-soft);
26
26
 
27
27
  box-sizing: border-box;
28
28
  display: flex;
@@ -81,7 +81,7 @@
81
81
  display: flex;
82
82
  align-items: center;
83
83
  justify-content: center;
84
- min-width: 0;
84
+ min-inline-size: max-content;
85
85
  color: var(--tabs-content-subtle);
86
86
  background: transparent;
87
87
  font: inherit;
@@ -90,6 +90,10 @@
90
90
  cursor: pointer;
91
91
  }
92
92
 
93
+ .ds-tabs--hug .ds-tabs__tab {
94
+ flex: 0 0 auto;
95
+ }
96
+
93
97
  .ds-tabs__tab:disabled,
94
98
  .ds-tabs__tab[data-disabled] {
95
99
  cursor: not-allowed;
@@ -143,11 +147,13 @@
143
147
  .ds-tabs--rounded .ds-tabs__tab[aria-selected="true"],
144
148
  .ds-tabs--rounded .ds-tabs__tab[data-state="active"] {
145
149
  background: var(--tabs-bg-default);
146
- box-shadow: var(--tabs-shadow-selected);
150
+ box-shadow: var(--tabs-elevation-selected);
147
151
  }
148
152
 
149
153
  .ds-tabs--segment.ds-tabs--md .ds-tabs__tab,
150
- .ds-tabs--rounded.ds-tabs--md .ds-tabs__tab {
154
+ .ds-tabs--rounded.ds-tabs--md .ds-tabs__tab,
155
+ .ds-tabs--segment.ds-tabs--comfortable .ds-tabs__tab,
156
+ .ds-tabs--rounded.ds-tabs--comfortable .ds-tabs__tab {
151
157
  min-height: var(--tabs-height-segment-md);
152
158
  padding: var(--tabs-space-md) var(--tabs-space-xl);
153
159
  }
@@ -197,6 +203,28 @@
197
203
  flex: 1 0 0;
198
204
  }
199
205
 
206
+ /* Comfortable: view navigation in an app page. Tabs sit md apart with no divider under the row, and the active
207
+ indicator is an inset shadow so the label stays centred in the full tab height. */
208
+ .ds-tabs--underline.ds-tabs--comfortable {
209
+ gap: var(--tabs-space-md);
210
+ border-bottom: 0;
211
+ }
212
+
213
+ .ds-tabs--underline.ds-tabs--comfortable .ds-tabs__tab {
214
+ min-height: var(--tabs-height-underline-sm);
215
+ border-bottom: 0;
216
+ box-shadow: inset 0 calc(var(--tabs-space-xxs) * -1) 0 0 transparent;
217
+ }
218
+
219
+ .ds-tabs--underline.ds-tabs--comfortable .ds-tabs__content {
220
+ height: var(--tabs-height-underline-content);
221
+ }
222
+
223
+ .ds-tabs--underline.ds-tabs--comfortable .ds-tabs__tab[aria-selected="true"],
224
+ .ds-tabs--underline.ds-tabs--comfortable .ds-tabs__tab[data-state="active"] {
225
+ box-shadow: inset 0 calc(var(--tabs-space-xxs) * -1) 0 0 var(--tabs-content-brand);
226
+ }
227
+
200
228
  .ds-tabs--rtl .ds-tabs__tab {
201
229
  text-align: start;
202
230
  }
@@ -9,7 +9,11 @@ import { useResolvedDirection } from "@/hooks/use-direction"
9
9
  import "./tabs.css"
10
10
 
11
11
  export type TabsVariant = "underline" | "segment" | "rounded"
12
- export type TabsSize = "md" | "sm" | "xs"
12
+ /**
13
+ * `comfortable` is the view navigation in an app page (ATS360 frames): underline tabs 48px tall, 8px apart, with the
14
+ * active indicator drawn inside the tab and no divider under the row. Segment and rounded tabs render it like `md`.
15
+ */
16
+ export type TabsSize = "md" | "sm" | "xs" | "comfortable"
13
17
 
14
18
  export type TabsItem = {
15
19
  content?: React.ReactNode
@@ -396,7 +396,7 @@ function TagCloseButton({
396
396
  type={type || "button"}
397
397
  className={cn(
398
398
  "relative z-[var(--bh-z-raised)] inline-flex size-[var(--bh-space-3xl-16)] shrink-0 items-center justify-center rounded-[var(--bh-radius-full)] text-current outline-none transition-[background-color,box-shadow]",
399
- "hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:shadow-[var(--shadow-button-focus)] disabled:pointer-events-none",
399
+ "hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:shadow-[var(--elevation-button-focus)] disabled:pointer-events-none",
400
400
  offset && "ms-[var(--bh-space-xs-4)]",
401
401
  className
402
402
  )}