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
@@ -1,10 +1,4 @@
1
- import { Line, LineChart, XAxis, YAxis } from "recharts"
2
-
3
- import {
4
- ChartContainer,
5
- ChartTooltip,
6
- ChartTooltipContent,
7
- } from "@/components/ui/chart"
1
+ import { DataChart } from "@/components/ui/data-chart"
8
2
 
9
3
  const data = [
10
4
  { month: "Jan", revenue: 120 },
@@ -15,19 +9,10 @@ const data = [
15
9
 
16
10
  export function ChartDemo() {
17
11
  return (
18
- <ChartContainer config={{ revenue: { color: "var(--bh-chart-1)" } }}>
19
- <LineChart data={data}>
20
- <XAxis dataKey="month" tickLine={false} axisLine={false} />
21
- <YAxis tickLine={false} axisLine={false} width={32} />
22
- <ChartTooltip content={<ChartTooltipContent />} />
23
- <Line
24
- dataKey="revenue"
25
- type="monotone"
26
- stroke="var(--color-revenue)"
27
- strokeWidth="var(--bh-icon-stroke-200)"
28
- dot={false}
29
- />
30
- </LineChart>
31
- </ChartContainer>
12
+ <DataChart
13
+ label="Monthly revenue"
14
+ description="Illustrative revenue from January through April."
15
+ spec={{ family: "area", data, categoryKey: "month", variant: "gradient", series: [{ key: "revenue", label: "Revenue" }], legend: true }}
16
+ />
32
17
  )
33
18
  }
@@ -36,7 +36,7 @@ export function CollectionFilterBarDemo() {
36
36
  <form
37
37
  aria-label="Collection filters"
38
38
  data-recipe="collection-filter-bar"
39
- className="grid w-full min-w-0 gap-[var(--bh-space-xl-12)] rounded-[var(--bh-radius-2xl-12)] bg-[var(--bh-bg-raised)] p-[var(--bh-space-3xl-16)] shadow-[var(--shadow-component-default)]"
39
+ className="grid w-full min-w-0 gap-[var(--bh-space-xl-12)] rounded-[var(--bh-radius-2xl-12)] bg-[var(--bh-bg-raised)] p-[var(--bh-space-3xl-16)] shadow-[var(--elevation-component-default)]"
40
40
  onReset={() => {
41
41
  setTypeFilter("everything")
42
42
  setOrder("recent")
@@ -32,7 +32,11 @@ export function ControlDensityContractDemo() {
32
32
  hasLabel={false}
33
33
  placeholder="Input"
34
34
  />
35
- <Select density={density} value="Select" />
35
+ <Select
36
+ aria-label={`${density} density select`}
37
+ density={density}
38
+ value="Select"
39
+ />
36
40
  <DatePicker
37
41
  density={density}
38
42
  placeholder="Date"
@@ -0,0 +1,31 @@
1
+ import {
2
+ CitationMark,
3
+ CitationProse,
4
+ InlineCitations,
5
+ type CitationSource,
6
+ } from "@/components/ui/inline-citations"
7
+
8
+ const sources: CitationSource[] = [
9
+ {
10
+ host: "arxiv.org",
11
+ label: "Attention Is All You Need",
12
+ url: "https://arxiv.org/abs/1706.03762",
13
+ },
14
+ {
15
+ host: "arxiv.org",
16
+ label: "Efficient Transformers: A Survey",
17
+ url: "https://arxiv.org/abs/2009.06732",
18
+ },
19
+ ]
20
+
21
+ export function InlineCitationsDemo() {
22
+ return (
23
+ <InlineCitations className="max-w-[var(--bh-space-19xl-384)]" sources={sources}>
24
+ <CitationProse>
25
+ Transformers scale well with data and compute
26
+ <CitationMark n={1} />, though attention is quadratic in sequence length
27
+ <CitationMark n={2} />.
28
+ </CitationProse>
29
+ </InlineCitations>
30
+ )
31
+ }
@@ -0,0 +1,36 @@
1
+ import * as React from "react"
2
+
3
+ import { Input } from "@/components/ui/input"
4
+
5
+ export function InputActionsDemo() {
6
+ const [quantity, setQuantity] = React.useState("1")
7
+ const [reference, setReference] = React.useState("")
8
+ const [applied, setApplied] = React.useState("Choose a quantity, then apply your reference.")
9
+
10
+ return (
11
+ <div className="grid gap-[var(--bh-space-xl-12)]">
12
+ <Input
13
+ kind="quantity"
14
+ label="Order quantity"
15
+ quantityLabel="order quantity"
16
+ min={0}
17
+ max={10}
18
+ step={1}
19
+ value={quantity}
20
+ onChange={(event) => setQuantity(event.currentTarget.value)}
21
+ />
22
+ <Input
23
+ kind="trailing-button"
24
+ label="Order reference"
25
+ placeholder="Reference"
26
+ buttonLabel="Apply"
27
+ value={reference}
28
+ onChange={(event) => setReference(event.currentTarget.value)}
29
+ buttonProps={{
30
+ onClick: () => setApplied(`${quantity || "0"} items${reference ? ` · ${reference}` : ""}`),
31
+ }}
32
+ />
33
+ <p role="status" className="text-[var(--bh-content-subtle)]">{applied}</p>
34
+ </div>
35
+ )
36
+ }
@@ -89,7 +89,7 @@ export function MenuDemo() {
89
89
  </MenuSubContent>
90
90
  </MenuSub>
91
91
  <MenuSeparator />
92
- <MenuItem>
92
+ <MenuItem onSelect={(event: Event) => event.preventDefault()}>
93
93
  <MenuItemIcon>
94
94
  <PanelRightIcon aria-hidden="true" />
95
95
  </MenuItemIcon>
@@ -450,6 +450,7 @@ function FilterContent() {
450
450
  ))}
451
451
  </div>
452
452
  <Select
453
+ aria-label="Date range"
453
454
  className="[--bh-select-width:100%]"
454
455
  expandIcon={<ChevronDownIcon aria-hidden="true" />}
455
456
  hasLabel={false}
@@ -0,0 +1,39 @@
1
+ import * as React from "react"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import { RollingCount } from "@/components/ui/rolling-count"
5
+
6
+ const total = 5
7
+
8
+ export function RollingCountDemo() {
9
+ const [done, setDone] = React.useState(2)
10
+
11
+ return (
12
+ <div className="grid justify-items-start gap-[var(--bh-space-xl-12)]">
13
+ <p className="m-0 text-[length:var(--bh-text-body-md-medium-font-size)] font-[var(--bh-text-body-md-medium-font-weight)] leading-[var(--bh-text-body-md-medium-line-height)] text-[var(--bh-content-default)]">
14
+ <RollingCount
15
+ aria-label={`${done} of ${total} tasks done`}
16
+ value={`${done}/${total}`}
17
+ />
18
+ </p>
19
+
20
+ <div className="flex gap-[var(--bh-space-md-8)]">
21
+ <Button
22
+ disabled={done === 0}
23
+ onClick={() => setDone((value) => Math.max(0, value - 1))}
24
+ size="sm"
25
+ variant="soft"
26
+ >
27
+ Undo
28
+ </Button>
29
+ <Button
30
+ disabled={done === total}
31
+ onClick={() => setDone((value) => Math.min(total, value + 1))}
32
+ size="sm"
33
+ >
34
+ Complete a task
35
+ </Button>
36
+ </div>
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,20 @@
1
+ import { ShimmerText } from "@/components/ui/shimmer-text"
2
+
3
+ export function ShimmerTextDemo() {
4
+ return (
5
+ <div className="grid gap-[var(--bh-space-3xl-16)] text-[length:var(--bh-text-body-sm-regular-font-size)] leading-[var(--bh-text-body-sm-regular-line-height)] text-[var(--bh-content-default)]">
6
+ <p className="m-0">
7
+ Searching the workspace for{" "}
8
+ <ShimmerText tone="default">billing/invoices.ts</ShimmerText>
9
+ </p>
10
+
11
+ <p className="m-0">
12
+ <ShimmerText>Reading 12 files</ShimmerText>
13
+ </p>
14
+
15
+ <p className="m-0" dir="rtl">
16
+ جارٍ البحث في <ShimmerText tone="default">billing/invoices.ts</ShimmerText>
17
+ </p>
18
+ </div>
19
+ )
20
+ }
@@ -0,0 +1,28 @@
1
+ import * as React from "react"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import { StreamingText, useTypewriter } from "@/components/ui/streaming-text"
5
+
6
+ const response =
7
+ "The migration touches three files. I will add the sessions table first, wire the auth middleware next, and finish with the login flow so every step stays independently testable."
8
+
9
+ function TypedResponse({ text }: { text: string }) {
10
+ const typed = useTypewriter(text)
11
+ return <StreamingText streaming={typed.typing} text={typed.text} />
12
+ }
13
+
14
+ export function StreamingTextDemo() {
15
+ const [run, setRun] = React.useState(0)
16
+
17
+ return (
18
+ <div className="grid max-w-[var(--bh-space-19xl-384)] gap-[var(--bh-space-3xl-16)]">
19
+ <TypedResponse key={run} text={response} />
20
+
21
+ <div>
22
+ <Button onClick={() => setRun((value) => value + 1)} size="sm" variant="soft">
23
+ Replay
24
+ </Button>
25
+ </div>
26
+ </div>
27
+ )
28
+ }
@@ -0,0 +1,74 @@
1
+ import * as React from "react"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import { TaskList, type TaskListItem } from "@/components/ui/task-list"
5
+
6
+ const labels = [
7
+ "Scaffold the project structure",
8
+ "Build the component registry",
9
+ "Implement entitlement gating",
10
+ "Wire up Stripe checkout",
11
+ "Polish the landing page",
12
+ ]
13
+
14
+ const startDelay = 700
15
+ // How long each task stays active before completing.
16
+ const stepDuration = 2250
17
+
18
+ /**
19
+ * Walks the scripted tasks from pending to active to done. A real agent
20
+ * updates each item status as its work progresses.
21
+ */
22
+ function useScriptedTasks(run: number): TaskListItem[] {
23
+ // -1 = not started, 0..n-1 = index of the active task, n = all done.
24
+ const [current, setCurrent] = React.useState(-1)
25
+
26
+ React.useEffect(() => {
27
+ setCurrent(-1)
28
+
29
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
30
+ setCurrent(labels.length)
31
+ return
32
+ }
33
+
34
+ const timers = [window.setTimeout(() => setCurrent(0), startDelay)]
35
+ labels.forEach((_, index) => {
36
+ timers.push(
37
+ window.setTimeout(
38
+ () => setCurrent(index + 1),
39
+ startDelay + (index + 1) * stepDuration
40
+ )
41
+ )
42
+ })
43
+
44
+ return () => timers.forEach((timer) => window.clearTimeout(timer))
45
+ }, [run])
46
+
47
+ return labels.map((label, index) => ({
48
+ id: String(index),
49
+ label,
50
+ status:
51
+ current >= 0 && index < current
52
+ ? "done"
53
+ : index === current && current < labels.length
54
+ ? "active"
55
+ : "pending",
56
+ }))
57
+ }
58
+
59
+ export function TaskListDemo() {
60
+ const [run, setRun] = React.useState(0)
61
+ const items = useScriptedTasks(run)
62
+
63
+ return (
64
+ <div className="grid max-w-[var(--bh-space-18xl-320)] gap-[var(--bh-space-3xl-16)]">
65
+ <TaskList items={items} />
66
+
67
+ <div>
68
+ <Button onClick={() => setRun((value) => value + 1)} size="sm" variant="soft">
69
+ Replay
70
+ </Button>
71
+ </div>
72
+ </div>
73
+ )
74
+ }
@@ -0,0 +1,74 @@
1
+ import * as React from "react"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import { ThinkingReasoning } from "@/components/ui/thinking-reasoning"
5
+
6
+ const reasoning = [
7
+ "Reading the request and the current selection, then locating the jwt.verify call inside the auth middleware.",
8
+ "The verify call sets no algorithms allowlist, so a token signed with none or a weak cipher could be accepted.",
9
+ "Tracing where the signing secret is loaded from and confirming it is never logged or sent back to the client.",
10
+ "Planning to pin the algorithm to HS256 and to validate the issuer and audience claims on every incoming request.",
11
+ "Scanning the existing tests around the middleware so the fix stays covered and nothing downstream regresses.",
12
+ "Drafting the patch with a focused regression test that rejects tampered, expired, and unsigned tokens.",
13
+ ]
14
+
15
+ // Per-sentence arrival cadence in milliseconds; sums to roughly five seconds.
16
+ const delays = [700, 900, 800, 850, 800, 900]
17
+ const collapseBeat = 360
18
+
19
+ /**
20
+ * Replays a scripted reasoning stream. A real integration appends each
21
+ * sentence as it arrives and flips the status to done when the model stops.
22
+ */
23
+ function useScriptedReasoning(run: number) {
24
+ const [revealed, setRevealed] = React.useState(0)
25
+ const [status, setStatus] = React.useState<"thinking" | "done">("thinking")
26
+
27
+ React.useEffect(() => {
28
+ setRevealed(0)
29
+ setStatus("thinking")
30
+
31
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
32
+ setRevealed(reasoning.length)
33
+ setStatus("done")
34
+ return
35
+ }
36
+
37
+ const timers: number[] = []
38
+ let elapsed = 0
39
+ delays.forEach((delay, index) => {
40
+ elapsed += delay
41
+ timers.push(window.setTimeout(() => setRevealed(index + 1), elapsed))
42
+ })
43
+ timers.push(
44
+ window.setTimeout(() => setStatus("done"), elapsed + collapseBeat)
45
+ )
46
+
47
+ return () => timers.forEach((timer) => window.clearTimeout(timer))
48
+ }, [run])
49
+
50
+ return { status, steps: reasoning.slice(0, revealed) }
51
+ }
52
+
53
+ export function ThinkingReasoningDemo() {
54
+ const [run, setRun] = React.useState(0)
55
+ const { status, steps } = useScriptedReasoning(run)
56
+
57
+ return (
58
+ <div className="grid gap-[var(--bh-space-3xl-16)]">
59
+ <ThinkingReasoning
60
+ duration={5}
61
+ key={run}
62
+ reserveSpace
63
+ status={status}
64
+ steps={steps}
65
+ />
66
+
67
+ <div>
68
+ <Button onClick={() => setRun((value) => value + 1)} size="sm" variant="soft">
69
+ Replay
70
+ </Button>
71
+ </div>
72
+ </div>
73
+ )
74
+ }
@@ -0,0 +1,13 @@
1
+ import { ThinkingState } from "@/components/ui/thinking-state"
2
+
3
+ export function ThinkingStateDemo() {
4
+ return (
5
+ <div className="grid gap-[var(--bh-space-3xl-16)]">
6
+ <ThinkingState />
7
+
8
+ <div dir="rtl">
9
+ <ThinkingState>جارٍ التفكير</ThinkingState>
10
+ </div>
11
+ </div>
12
+ )
13
+ }
@@ -0,0 +1,24 @@
1
+ Banhaten chart designs and illustrative block datasets are adapted from EvilCharts:
2
+ https://github.com/legions-developer/evilcharts
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2026 Gurbinder
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
@@ -0,0 +1,33 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ /**
6
+ * State a parent may control. While `value` is undefined the hook keeps its
7
+ * own copy, seeded from `defaultValue`; either way `onChange` hears every
8
+ * change made through the returned setter.
9
+ */
10
+ export function useControllableState<T>({
11
+ defaultValue,
12
+ onChange,
13
+ value,
14
+ }: {
15
+ defaultValue: T
16
+ onChange?: (value: T) => void
17
+ value?: T
18
+ }) {
19
+ const [uncontrolled, setUncontrolled] = React.useState(defaultValue)
20
+ const controlled = value !== undefined
21
+ const onChangeRef = React.useRef(onChange)
22
+ onChangeRef.current = onChange
23
+
24
+ const setValue = React.useCallback(
25
+ (next: T) => {
26
+ if (!controlled) setUncontrolled(next)
27
+ onChangeRef.current?.(next)
28
+ },
29
+ [controlled]
30
+ )
31
+
32
+ return [controlled ? value : uncontrolled, setValue] as const
33
+ }
@@ -0,0 +1,73 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ /**
6
+ * Active-option state for a listbox that a text field drives through
7
+ * `aria-activedescendant`. Arrow moves wrap and skip disabled options; the
8
+ * active option stays put while it remains valid and otherwise falls back to
9
+ * the first enabled one, so filtering never strands it.
10
+ */
11
+ export function useListboxNavigation<T>({
12
+ idPrefix,
13
+ isDisabled,
14
+ open,
15
+ options,
16
+ }: {
17
+ idPrefix: string
18
+ isDisabled?: (option: T) => boolean
19
+ open: boolean
20
+ options: readonly T[]
21
+ }) {
22
+ const [activeIndex, setActiveIndex] = React.useState(-1)
23
+ const isDisabledRef = React.useRef(isDisabled)
24
+ isDisabledRef.current = isDisabled
25
+
26
+ const enabledIndexes = () =>
27
+ options.flatMap((option, index) =>
28
+ isDisabledRef.current?.(option) ? [] : [index]
29
+ )
30
+
31
+ React.useEffect(() => {
32
+ if (!open) {
33
+ setActiveIndex(-1)
34
+ return
35
+ }
36
+ setActiveIndex((current) => {
37
+ const enabled = options.flatMap((option, index) =>
38
+ isDisabledRef.current?.(option) ? [] : [index]
39
+ )
40
+ return enabled.includes(current) ? current : enabled[0] ?? -1
41
+ })
42
+ }, [open, options])
43
+
44
+ /** Moves by one enabled option, wrapping at either end. */
45
+ const move = (offset: 1 | -1) => {
46
+ const enabled = enabledIndexes()
47
+ setActiveIndex((current) => {
48
+ if (enabled.length === 0) return -1
49
+ const position = enabled.indexOf(current)
50
+ if (position === -1) return offset > 0 ? enabled[0] : enabled[enabled.length - 1]
51
+ return enabled[(position + offset + enabled.length) % enabled.length]
52
+ })
53
+ }
54
+
55
+ /** Jumps to the first or last enabled option. */
56
+ const moveToEdge = (edge: "first" | "last") => {
57
+ const enabled = enabledIndexes()
58
+ setActiveIndex((edge === "first" ? enabled[0] : enabled[enabled.length - 1]) ?? -1)
59
+ }
60
+
61
+ const optionId = (index: number) => `${idPrefix}-option-${index}`
62
+ const current =
63
+ open && activeIndex >= 0 && activeIndex < options.length ? activeIndex : -1
64
+
65
+ return {
66
+ activeIndex: current,
67
+ activeOptionId: current >= 0 ? optionId(current) : undefined,
68
+ move,
69
+ moveToEdge,
70
+ optionId,
71
+ setActiveIndex,
72
+ }
73
+ }
@@ -0,0 +1,55 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ /**
6
+ * Calls `done` once the CSS animations and transitions running on `element`
7
+ * have finished, so script timing follows the stylesheet and its motion
8
+ * tokens instead of copying their values. When nothing is running (styles
9
+ * missing, `animation: none`, or no Web Animations support) it calls `done`
10
+ * right away. Returns a function that cancels the pending call.
11
+ */
12
+ export function waitForMotion(element: Element, done: () => void) {
13
+ let pending = true
14
+ const running =
15
+ typeof element.getAnimations === "function"
16
+ ? element
17
+ .getAnimations()
18
+ .filter(
19
+ (animation) => animation.effect?.getTiming().iterations !== Infinity
20
+ )
21
+ : []
22
+
23
+ if (running.length === 0) {
24
+ done()
25
+ } else {
26
+ void Promise.allSettled(running.map((animation) => animation.finished)).then(
27
+ () => {
28
+ if (pending) done()
29
+ }
30
+ )
31
+ }
32
+
33
+ return () => {
34
+ pending = false
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Calls `onDone` after the motion that turning `active` on starts on the
40
+ * element in `ref`, for example an exit animation before unmounting.
41
+ */
42
+ export function useMotionEnd(
43
+ ref: React.RefObject<Element | null>,
44
+ active: boolean,
45
+ onDone: () => void
46
+ ) {
47
+ const onDoneRef = React.useRef(onDone)
48
+ onDoneRef.current = onDone
49
+
50
+ React.useLayoutEffect(() => {
51
+ const element = ref.current
52
+ if (!active || !element) return
53
+ return waitForMotion(element, () => onDoneRef.current())
54
+ }, [active, ref])
55
+ }
@@ -0,0 +1,28 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ const query = "(prefers-reduced-motion: reduce)"
6
+
7
+ function canMatchMedia() {
8
+ return typeof window !== "undefined" && typeof window.matchMedia === "function"
9
+ }
10
+
11
+ function subscribe(onChange: () => void) {
12
+ if (!canMatchMedia()) return () => undefined
13
+ const media = window.matchMedia(query)
14
+ media.addEventListener?.("change", onChange)
15
+ return () => media.removeEventListener?.("change", onChange)
16
+ }
17
+
18
+ function getSnapshot() {
19
+ return canMatchMedia() && window.matchMedia(query).matches
20
+ }
21
+
22
+ /**
23
+ * Whether the user asked for reduced motion. Re-renders when that setting
24
+ * changes, and reads false where it cannot be queried (server, tests).
25
+ */
26
+ export function useReducedMotion() {
27
+ return React.useSyncExternalStore(subscribe, getSnapshot, () => false)
28
+ }