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
@@ -13,13 +13,29 @@ type CollapsibleTriggerProps = React.ComponentPropsWithoutRef<
13
13
  type CollapsibleContentProps = React.ComponentPropsWithoutRef<
14
14
  typeof CollapsiblePrimitive.Content
15
15
  >
16
+ type CollapsiblePanelProps = CollapsibleContentProps
16
17
 
17
18
  const Collapsible = CollapsiblePrimitive.Root
18
19
 
19
20
  const CollapsibleTrigger = React.forwardRef<
20
21
  React.ElementRef<typeof CollapsiblePrimitive.Trigger>,
21
22
  CollapsibleTriggerProps
22
- >(function CollapsibleTrigger({ children, className, ...props }, ref) {
23
+ >(function CollapsibleTrigger({ asChild, children, className, ...props }, ref) {
24
+ // With asChild the consumer supplies and styles the trigger element.
25
+ if (asChild) {
26
+ return (
27
+ <CollapsiblePrimitive.Trigger
28
+ asChild
29
+ data-slot="collapsible-trigger"
30
+ ref={ref}
31
+ className={className}
32
+ {...props}
33
+ >
34
+ {children}
35
+ </CollapsiblePrimitive.Trigger>
36
+ )
37
+ }
38
+
23
39
  return (
24
40
  <CollapsiblePrimitive.Trigger
25
41
  data-slot="collapsible-trigger"
@@ -27,7 +43,7 @@ const CollapsibleTrigger = React.forwardRef<
27
43
  className={cn(
28
44
  "group/collapsible-trigger flex w-full items-center justify-between gap-[var(--bh-space-md-8)] rounded-[var(--bh-radius-lg-8)] px-[var(--bh-space-xl-12)] py-[var(--bh-space-lg-10)] text-start",
29
45
  "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)] tracking-[var(--bh-text-body-md-medium-letter-spacing)] text-[var(--bh-content-default)]",
30
- "outline-none transition-[background-color,box-shadow,color] hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:shadow-[var(--shadow-button-focus)]",
46
+ "outline-none transition-[background-color,box-shadow,color] hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:shadow-[var(--elevation-button-focus)]",
31
47
  className
32
48
  )}
33
49
  {...props}
@@ -61,9 +77,33 @@ const CollapsibleContent = React.forwardRef<
61
77
  )
62
78
  })
63
79
 
64
- export { Collapsible, CollapsibleContent, CollapsibleTrigger }
80
+ /**
81
+ * Unstyled content that animates its height open and closed. Closed content
82
+ * unmounts once the animation ends, so nothing in it can be focused or read.
83
+ * Tune the motion with `--collapsible-panel-duration` and
84
+ * `--collapsible-panel-easing`.
85
+ */
86
+ const CollapsiblePanel = React.forwardRef<
87
+ React.ElementRef<typeof CollapsiblePrimitive.Content>,
88
+ CollapsiblePanelProps
89
+ >(function CollapsiblePanel({ className, ...props }, ref) {
90
+ return (
91
+ <CollapsiblePrimitive.Content
92
+ data-slot="collapsible-panel"
93
+ ref={ref}
94
+ className={cn(
95
+ "overflow-hidden data-[state=closed]:animate-[collapsible-up_var(--collapsible-panel-duration,var(--bh-duration-moderate))_var(--collapsible-panel-easing,var(--bh-easing-emphasized))] data-[state=open]:animate-[collapsible-down_var(--collapsible-panel-duration,var(--bh-duration-moderate))_var(--collapsible-panel-easing,var(--bh-easing-emphasized))]",
96
+ className
97
+ )}
98
+ {...props}
99
+ />
100
+ )
101
+ })
102
+
103
+ export { Collapsible, CollapsibleContent, CollapsiblePanel, CollapsibleTrigger }
65
104
  export type {
66
105
  CollapsibleContentProps,
106
+ CollapsiblePanelProps,
67
107
  CollapsibleProps,
68
108
  CollapsibleTriggerProps,
69
109
  }
@@ -26,7 +26,7 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>(function Command(
26
26
  data-slot="command"
27
27
  ref={ref}
28
28
  className={cn(
29
- "flex w-full max-w-[var(--bh-space-19xl-384)] flex-col overflow-hidden rounded-[var(--bh-radius-2xl-12)] bg-[var(--bh-bg-raised)] text-[var(--bh-content-default)] shadow-[var(--shadow-menu)]",
29
+ "flex w-full max-w-[var(--bh-space-19xl-384)] flex-col overflow-hidden rounded-[var(--bh-radius-2xl-12)] bg-[var(--bh-bg-raised)] text-[var(--bh-content-default)] shadow-[var(--elevation-menu)]",
30
30
  className
31
31
  )}
32
32
  {...props}
@@ -50,7 +50,7 @@ const ContextMenuSub = ContextMenuPrimitive.Sub
50
50
  const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
51
51
 
52
52
  const menuContentClassName =
53
- "z-[var(--bh-z-popover)] min-w-[var(--bh-menu-width)] overflow-hidden rounded-[var(--bh-radius-xl-10)] bg-[var(--bh-bg-raised)] p-[var(--bh-space-xs-4)] text-[var(--bh-content-default)] shadow-[var(--shadow-menu)]"
53
+ "z-[var(--bh-z-popover)] min-w-[var(--bh-menu-width)] overflow-hidden rounded-[var(--bh-radius-xl-10)] bg-[var(--bh-bg-raised)] p-[var(--bh-space-xs-4)] text-[var(--bh-content-default)] shadow-[var(--elevation-menu)]"
54
54
 
55
55
  const menuItemClassName =
56
56
  "relative flex min-h-[var(--bh-menu-item-default-height)] cursor-default select-none items-center gap-[var(--bh-space-md-8)] rounded-[var(--bh-radius-md-6)] px-[var(--bh-space-md-8)] py-[var(--bh-space-sm-6)] text-start text-[length:var(--bh-text-body-sm-regular-font-size)] font-[var(--bh-text-body-sm-regular-font-weight)] leading-[var(--bh-text-body-sm-regular-line-height)] tracking-[var(--bh-text-body-sm-regular-letter-spacing)] text-[var(--bh-content-default)] outline-none data-[disabled]:pointer-events-none data-[disabled]:text-[var(--bh-content-disabled)] data-[highlighted]:bg-[var(--bh-menu-item-hover-bg)]"
@@ -0,0 +1,40 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { EChart, type EChartProps, type EChartTheme } from "./echart"
5
+ import { createChartOption, type ChartSpec } from "./chart-options"
6
+ import { attachChartInteractions } from "./chart-interactions"
7
+ import { cn } from "@/lib/utils"
8
+
9
+ export type DataChartProps = Omit<EChartProps, "option"> & {
10
+ spec: ChartSpec
11
+ loading?: boolean
12
+ loadingLabel?: string
13
+ emptyLabel?: string
14
+ }
15
+
16
+ /** The ready-to-use Banhaten chart surface, including the source interactions. */
17
+ export function DataChart({ spec, label, description, loading = false, loadingLabel = "Loading chart…", emptyLabel = "No data available", onReady, className, ...props }: DataChartProps) {
18
+ const option = React.useCallback((theme: EChartTheme) => createChartOption(theme, spec), [spec])
19
+ const setup = React.useCallback<NonNullable<EChartProps["onReady"]>>((chart, theme) => {
20
+ const cleanup = attachChartInteractions(chart, theme, spec)
21
+ const cleanupConsumer = onReady?.(chart, theme)
22
+ return () => { cleanup(); cleanupConsumer?.() }
23
+ }, [spec, onReady])
24
+ const empty = spec.family === "sankey" ? !((spec.nodes?.length ?? spec.data.length) && spec.links?.length) : spec.data.length === 0
25
+ if (loading || empty) {
26
+ return (
27
+ <div
28
+ {...props}
29
+ role="status"
30
+ aria-label={label}
31
+ aria-busy={loading}
32
+ data-slot="data-chart-state"
33
+ className={cn("flex h-[var(--bh-space-19xl-384)] w-full items-center justify-center rounded-[var(--bh-radius-lg-8)] bg-[var(--bh-bg-neutral-subtle)] text-[length:var(--bh-text-body-xs-regular-font-size)] font-[var(--bh-text-body-xs-regular-font-weight)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)] text-[var(--bh-content-muted)]", className)}
34
+ >
35
+ {loading ? loadingLabel : emptyLabel}
36
+ </div>
37
+ )
38
+ }
39
+ return <EChart {...props} label={label} description={description} className={className} option={option} onReady={setup} />
40
+ }
@@ -173,7 +173,7 @@ const calendarSurface = cva(
173
173
  [
174
174
  "relative bg-[var(--bh-bg-raised)]",
175
175
  "[--bh-date-surface-border:var(--bh-border-subtle)]",
176
- "shadow-[var(--shadow-date-surface)]",
176
+ "shadow-[var(--elevation-date-surface)]",
177
177
  ],
178
178
  {
179
179
  variants: {
@@ -199,10 +199,10 @@ const pickerTrigger = cva(
199
199
  [
200
200
  "group/date-trigger relative flex w-[var(--bh-date-trigger-width)] max-w-full items-center gap-[var(--bh-space-md-8)] rounded-[var(--bh-radius-lg-8)]",
201
201
  "bg-[var(--bh-interactive-input-default)] text-start outline-none",
202
- "[--bh-date-trigger-border:var(--bh-border-input)] [--bh-date-trigger-shadow:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-date-trigger-border),var(--shadow-component-default)]",
202
+ "[--bh-date-trigger-border:var(--bh-border-input)] [--bh-date-trigger-elevation:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-date-trigger-border),var(--elevation-component-default)]",
203
203
  "text-[length:var(--bh-text-body-md-regular-font-size)] font-[var(--bh-text-body-md-regular-font-weight)] leading-[var(--bh-text-body-md-regular-line-height)] tracking-[var(--bh-text-body-md-regular-letter-spacing)]",
204
- "text-[var(--bh-content-default)] shadow-[var(--bh-date-trigger-shadow)] transition-[background-color,box-shadow]",
205
- "hover:bg-[var(--bh-bg-default-hover)] focus-visible:shadow-[var(--shadow-input-focus-ring)]",
204
+ "text-[var(--bh-content-default)] shadow-[var(--bh-date-trigger-elevation)] transition-[background-color,box-shadow]",
205
+ "hover:bg-[var(--bh-bg-default-hover)] focus-visible:shadow-[var(--elevation-input-focus-ring)]",
206
206
  "disabled:pointer-events-none disabled:bg-[var(--bh-interactive-input-disabled)] disabled:text-[var(--bh-content-disabled)] disabled:shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)]",
207
207
  ],
208
208
  {
@@ -223,7 +223,7 @@ const monthButton = cva(
223
223
  [
224
224
  "flex size-[var(--bh-date-nav-button-size)] shrink-0 items-center justify-center rounded-[var(--bh-control-default)]",
225
225
  "bg-[var(--bh-interactive-soft-default)] text-[var(--bh-content-default)] outline-none transition-[background-color,box-shadow,color]",
226
- "hover:bg-[var(--bh-interactive-soft-hover)] focus-visible:shadow-[var(--shadow-button-focus)] disabled:opacity-[var(--bh-opacity-0)]",
226
+ "hover:bg-[var(--bh-interactive-soft-hover)] focus-visible:shadow-[var(--elevation-button-focus)] disabled:opacity-[var(--bh-opacity-0)]",
227
227
  "[&_[data-slot=date-picker-nav-icon]>svg]:size-[var(--bh-date-nav-icon-size)]",
228
228
  ]
229
229
  )
@@ -233,7 +233,7 @@ const calendarTitleButton = cva(
233
233
  "flex h-[var(--bh-date-title-height)] min-w-0 flex-1 items-center justify-center gap-[var(--bh-space-xs-4)] rounded-[var(--bh-radius-lg-8)]",
234
234
  "px-[var(--bh-space-md-8)] text-center outline-none transition-[background-color,box-shadow]",
235
235
  "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)] tracking-[var(--bh-text-body-md-medium-letter-spacing)] text-[var(--bh-content-default)]",
236
- "hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:shadow-[var(--shadow-button-focus)]",
236
+ "hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:shadow-[var(--elevation-button-focus)]",
237
237
  ],
238
238
  {
239
239
  variants: {
@@ -258,7 +258,7 @@ const dayCellButton = cva(
258
258
  [
259
259
  "relative flex h-[var(--bh-date-day-cell-height)] min-w-0 flex-1 items-center justify-center text-center outline-none",
260
260
  "text-[length:var(--bh-text-body-md-regular-font-size)] font-[var(--bh-text-body-md-regular-font-weight)] leading-[var(--bh-text-body-md-regular-line-height)] tracking-[var(--bh-text-body-md-regular-letter-spacing)]",
261
- "transition-[background-color,color,box-shadow] focus-visible:z-[var(--bh-z-raised)] focus-visible:shadow-[var(--shadow-button-focus)]",
261
+ "transition-[background-color,color,box-shadow] focus-visible:z-[var(--bh-z-raised)] focus-visible:shadow-[var(--elevation-button-focus)]",
262
262
  ],
263
263
  {
264
264
  variants: {
@@ -1091,7 +1091,7 @@ function CalendarTime({
1091
1091
  </span>
1092
1092
  <span
1093
1093
  data-slot="calendar-time-value"
1094
- className="flex min-h-[var(--bh-date-time-input-min-height)] min-w-0 flex-1 items-center gap-[var(--bh-space-xs-4)] rounded-[var(--bh-radius-lg-8)] bg-[var(--bh-interactive-input-default)] px-[var(--bh-space-md-8)] py-[var(--bh-space-sm-6)] shadow-[var(--shadow-date-time-input)]"
1094
+ className="flex min-h-[var(--bh-date-time-input-min-height)] min-w-0 flex-1 items-center gap-[var(--bh-space-xs-4)] rounded-[var(--bh-radius-lg-8)] bg-[var(--bh-interactive-input-default)] px-[var(--bh-space-md-8)] py-[var(--bh-space-sm-6)] shadow-[var(--elevation-date-time-input)]"
1095
1095
  >
1096
1096
  <CalendarTimeSegment active>{resolved.hours}</CalendarTimeSegment>
1097
1097
  <span className="text-[var(--bh-content-subtle)]">:</span>
@@ -1136,7 +1136,7 @@ function MonthYearList({
1136
1136
  return (
1137
1137
  <div
1138
1138
  data-slot="calendar-month-year-list"
1139
- className="absolute bottom-[var(--bh-space-md-8)] left-1/2 top-[var(--bh-date-month-year-list-top)] flex w-[var(--bh-date-month-year-list-width)] -translate-x-1/2 flex-col justify-center overflow-hidden rounded-[var(--bh-radius-md-6)] bg-[var(--bh-bg-default)] p-[var(--bh-space-md-8)] shadow-[var(--shadow-date-month-year-list)] rtl:translate-x-1/2"
1139
+ className="absolute bottom-[var(--bh-space-md-8)] left-1/2 top-[var(--bh-date-month-year-list-top)] flex w-[var(--bh-date-month-year-list-width)] -translate-x-1/2 flex-col justify-center overflow-hidden rounded-[var(--bh-radius-md-6)] bg-[var(--bh-bg-default)] p-[var(--bh-space-md-8)] shadow-[var(--elevation-date-month-year-list)] rtl:translate-x-1/2"
1140
1140
  >
1141
1141
  {items.map((item, index) => (
1142
1142
  <button
@@ -0,0 +1,246 @@
1
+ import type { EChartsOption } from "echarts"
2
+
3
+ export type EChartTheme = {
4
+ palette: string[]
5
+ foreground: string
6
+ muted: string
7
+ border: string
8
+ surface: string
9
+ subtle: string
10
+ neutral: string
11
+ fontFamily: string
12
+ fontSize: number
13
+ fontWeight: number
14
+ lineHeight: number
15
+ letterSpacing: number
16
+ labelFontSize: number
17
+ mediumWeight: number
18
+ headingFontSize: number
19
+ headingLineHeight: number
20
+ headingWeight: number
21
+ space: { xs: number; sm: number; md: number; lg: number; xl: number; xxl: number; axis: number }
22
+ radius: number
23
+ markRadius: number
24
+ radii?: { sm: number; md: number; lg: number; xl: number }
25
+ translucentOpacity?: number
26
+ borderWidth: number
27
+ lineWidth: number
28
+ opacity: { area: number; muted: number; soft: number }
29
+ duration: number
30
+ reducedMotion: boolean
31
+ rtl: boolean
32
+ }
33
+
34
+ const paletteTokens = [
35
+ "--bh-chart-categorical-brand",
36
+ "--bh-chart-categorical-success",
37
+ "--bh-chart-categorical-warning",
38
+ "--bh-chart-categorical-danger",
39
+ "--bh-chart-categorical-information",
40
+ "--bh-chart-categorical-purple",
41
+ "--bh-chart-categorical-aqua",
42
+ "--bh-chart-categorical-fuchsia",
43
+ ] as const
44
+
45
+ /** Canvas-style APIs need resolved values, read from this chart's inherited theme. */
46
+ export function readEChartTheme(element: HTMLElement, reducedMotion: boolean): EChartTheme {
47
+ const styles = getComputedStyle(element)
48
+ const read = (token: string) => {
49
+ const value = styles.getPropertyValue(token).trim()
50
+ if (!value) throw new Error(`EChart requires the Banhaten token ${token}.`)
51
+ return value
52
+ }
53
+ const number = (token: string) => {
54
+ const value = Number.parseFloat(read(token))
55
+ if (!Number.isFinite(value)) throw new Error(`EChart requires a numeric Banhaten token ${token}.`)
56
+ return value
57
+ }
58
+ const duration = read("--bh-duration-default")
59
+
60
+ return {
61
+ palette: paletteTokens.map(read),
62
+ foreground: read("--bh-content-default"),
63
+ muted: read("--bh-content-muted"),
64
+ border: read("--bh-border-subtle"),
65
+ surface: read("--bh-bg-raised"),
66
+ subtle: read("--bh-bg-neutral-subtle"),
67
+ neutral: read("--bh-chart-neutral-default"),
68
+ fontFamily: read("--bh-font-family"),
69
+ fontSize: number("--bh-text-body-xs-regular-font-size"),
70
+ fontWeight: number("--bh-text-body-xs-regular-font-weight"),
71
+ lineHeight: number("--bh-text-body-xs-regular-line-height"),
72
+ letterSpacing: number("--bh-text-body-xs-regular-letter-spacing"),
73
+ labelFontSize: number("--bh-text-body-3xs-regular-font-size"),
74
+ mediumWeight: number("--bh-text-body-xs-medium-font-weight"),
75
+ headingFontSize: number("--bh-text-heading-md-semibold-font-size"),
76
+ headingLineHeight: number("--bh-text-heading-md-semibold-line-height"),
77
+ headingWeight: number("--bh-text-heading-md-semibold-font-weight"),
78
+ space: {
79
+ xs: number("--bh-space-xs-4"),
80
+ sm: number("--bh-space-sm-6"),
81
+ md: number("--bh-space-md-8"),
82
+ lg: number("--bh-space-xl-12"),
83
+ xl: number("--bh-space-3xl-16"),
84
+ xxl: number("--bh-space-5xl-24"),
85
+ axis: number("--bh-space-7xl-40"),
86
+ },
87
+ radius: number("--bh-radius-lg-8"),
88
+ markRadius: number("--bh-radius-sm-4"),
89
+ radii: {
90
+ sm: number("--bh-radius-sm-4"),
91
+ md: number("--bh-radius-md-6"),
92
+ lg: number("--bh-radius-lg-8"),
93
+ xl: number("--bh-radius-xl-10"),
94
+ },
95
+ translucentOpacity: number("--bh-opacity-90"),
96
+ borderWidth: number("--bh-border-width-default"),
97
+ lineWidth: number("--bh-icon-stroke-200"),
98
+ opacity: {
99
+ area: number("--bh-opacity-15"),
100
+ muted: number("--bh-opacity-muted"),
101
+ soft: number("--bh-opacity-30"),
102
+ },
103
+ duration: Number.parseFloat(duration) * (duration.endsWith("ms") ? 1 : 1000),
104
+ reducedMotion,
105
+ rtl: styles.direction === "rtl",
106
+ }
107
+ }
108
+
109
+ /** Shared defaults also cover native ECharts labels that a block does not specify. */
110
+ export function createEChartTheme(theme: EChartTheme) {
111
+ const textStyle = {
112
+ color: theme.foreground,
113
+ fontFamily: theme.fontFamily,
114
+ fontSize: theme.fontSize,
115
+ fontWeight: theme.fontWeight,
116
+ lineHeight: theme.lineHeight,
117
+ textShadowBlur: 0,
118
+ textShadowOffsetX: 0,
119
+ textShadowOffsetY: 0,
120
+ }
121
+ const axis = {
122
+ axisLine: { lineStyle: { color: theme.border, width: theme.borderWidth } },
123
+ axisTick: { show: false, lineStyle: { color: theme.border, width: theme.borderWidth } },
124
+ axisLabel: { ...textStyle, color: theme.muted, fontSize: theme.labelFontSize, margin: theme.space.md },
125
+ nameTextStyle: { ...textStyle, color: theme.muted },
126
+ splitLine: { lineStyle: { color: theme.border, width: theme.borderWidth } },
127
+ splitArea: { areaStyle: { color: [theme.surface, theme.subtle] } },
128
+ }
129
+ const label = { ...textStyle, fontSize: theme.labelFontSize }
130
+ const shadowless = { shadowBlur: 0, shadowOffsetX: 0, shadowOffsetY: 0 }
131
+ const seriesStates = {
132
+ emphasis: { label, itemStyle: { color: "inherit", ...shadowless } },
133
+ select: { label, itemStyle: { color: "inherit", borderColor: theme.border, borderWidth: theme.borderWidth, ...shadowless } },
134
+ blur: { label: { ...label, opacity: theme.opacity.muted }, itemStyle: { opacity: theme.opacity.muted, ...shadowless } },
135
+ }
136
+ const axisPointer = {
137
+ lineStyle: { color: theme.muted, width: theme.borderWidth },
138
+ shadowStyle: { color: theme.subtle, shadowBlur: 0 },
139
+ label: { ...textStyle, backgroundColor: theme.surface, borderColor: theme.border, borderWidth: theme.borderWidth, padding: theme.space.sm },
140
+ }
141
+
142
+ return {
143
+ color: theme.palette,
144
+ backgroundColor: "transparent",
145
+ textStyle,
146
+ categoryAxis: axis,
147
+ valueAxis: axis,
148
+ logAxis: axis,
149
+ timeAxis: axis,
150
+ angleAxis: axis,
151
+ radiusAxis: axis,
152
+ axisPointer,
153
+ legend: {
154
+ textStyle,
155
+ itemWidth: theme.space.lg,
156
+ itemHeight: theme.space.md,
157
+ itemGap: theme.space.xl,
158
+ padding: theme.space.xs,
159
+ inactiveColor: theme.neutral,
160
+ inactiveBorderColor: theme.neutral,
161
+ pageTextStyle: textStyle,
162
+ pageIconColor: theme.foreground,
163
+ pageIconInactiveColor: theme.neutral,
164
+ },
165
+ tooltip: {
166
+ ...shadowless,
167
+ textStyle,
168
+ backgroundColor: theme.surface,
169
+ borderColor: theme.border,
170
+ borderWidth: theme.borderWidth,
171
+ borderRadius: theme.radius,
172
+ padding: [theme.space.md, theme.space.lg],
173
+ axisPointer,
174
+ },
175
+ line: {
176
+ ...seriesStates,
177
+ symbolSize: theme.space.sm,
178
+ lineStyle: { width: theme.lineWidth, shadowBlur: 0 },
179
+ areaStyle: { opacity: theme.opacity.area, shadowBlur: 0 },
180
+ label,
181
+ },
182
+ bar: { ...seriesStates, label, itemStyle: { shadowBlur: 0, borderWidth: 0 } },
183
+ pictorialBar: { ...seriesStates, label, itemStyle: { shadowBlur: 0, borderWidth: 0 } },
184
+ pie: { ...seriesStates, label, labelLine: { lineStyle: { width: theme.borderWidth } }, itemStyle: { borderColor: theme.surface, shadowBlur: 0 } },
185
+ radar: {
186
+ ...seriesStates,
187
+ axisName: label,
188
+ axisLine: axis.axisLine,
189
+ splitLine: axis.splitLine,
190
+ splitArea: axis.splitArea,
191
+ lineStyle: { width: theme.lineWidth, shadowBlur: 0 },
192
+ areaStyle: { opacity: theme.opacity.area, shadowBlur: 0 },
193
+ symbolSize: theme.space.sm,
194
+ label,
195
+ },
196
+ sankey: { ...seriesStates, label, lineStyle: { color: "source", opacity: theme.opacity.soft, shadowBlur: 0 }, itemStyle: { borderWidth: 0, shadowBlur: 0 } },
197
+ gauge: {
198
+ ...seriesStates,
199
+ title: textStyle,
200
+ detail: { ...textStyle, fontSize: theme.headingFontSize, fontWeight: theme.headingWeight },
201
+ axisLine: { lineStyle: { color: [[1, theme.neutral]], width: theme.space.lg } },
202
+ axisLabel: label,
203
+ axisTick: { lineStyle: { color: theme.border, width: theme.borderWidth } },
204
+ splitLine: { lineStyle: { color: theme.border, width: theme.borderWidth } },
205
+ pointer: { itemStyle: { color: theme.palette[0], shadowBlur: 0 } },
206
+ progress: { itemStyle: { color: theme.palette[0], shadowBlur: 0 } },
207
+ },
208
+ dataZoom: {
209
+ backgroundColor: theme.subtle,
210
+ borderColor: theme.border,
211
+ textStyle,
212
+ fillerColor: theme.subtle,
213
+ handleStyle: { color: theme.surface, borderColor: theme.border, borderWidth: theme.borderWidth, shadowBlur: 0 },
214
+ moveHandleStyle: { color: theme.neutral },
215
+ dataBackground: { lineStyle: { color: theme.neutral }, areaStyle: { color: theme.neutral, opacity: theme.opacity.area } },
216
+ selectedDataBackground: { lineStyle: { color: theme.palette[0] }, areaStyle: { color: theme.palette[0], opacity: theme.opacity.area } },
217
+ },
218
+ }
219
+ }
220
+
221
+ export function createEChartBaseOption(theme: EChartTheme): EChartsOption {
222
+ return {
223
+ color: theme.palette,
224
+ backgroundColor: "transparent",
225
+ textStyle: {
226
+ color: theme.foreground,
227
+ fontFamily: theme.fontFamily,
228
+ fontSize: theme.fontSize,
229
+ fontWeight: theme.fontWeight,
230
+ },
231
+ animation: !theme.reducedMotion,
232
+ animationDuration: theme.duration,
233
+ animationDurationUpdate: theme.duration,
234
+ // The labelled figure and optional data table provide the accessible content.
235
+ aria: { enabled: false },
236
+ tooltip: {
237
+ confine: true,
238
+ className: "bh-echart__tooltip",
239
+ shadowBlur: 0,
240
+ shadowOffsetX: 0,
241
+ shadowOffsetY: 0,
242
+ extraCssText: "box-shadow: none;",
243
+ transitionDuration: 0,
244
+ },
245
+ }
246
+ }
@@ -0,0 +1,49 @@
1
+ .bh-echart {
2
+ position: relative;
3
+ width: 100%;
4
+ height: var(--bh-space-17xl-288);
5
+ min-width: 0;
6
+ color: var(--bh-content-default);
7
+ font-family: var(--bh-font-family);
8
+ font-size: var(--bh-text-body-xs-regular-font-size);
9
+ font-weight: var(--bh-text-body-xs-regular-font-weight);
10
+ line-height: var(--bh-text-body-xs-regular-line-height);
11
+ letter-spacing: var(--bh-text-body-xs-regular-letter-spacing);
12
+ }
13
+
14
+ .bh-echart__viewport {
15
+ width: 100%;
16
+ height: 100%;
17
+ min-width: 0;
18
+ }
19
+
20
+ /* ECharts positions text in physical SVG coordinates; axes handle RTL order. */
21
+ .bh-echart__viewport svg {
22
+ direction: ltr;
23
+ }
24
+
25
+ .bh-echart .bh-echart__tooltip {
26
+ box-shadow: none !important;
27
+ font-family: var(--bh-font-family) !important;
28
+ letter-spacing: var(--bh-text-body-xs-regular-letter-spacing);
29
+ }
30
+
31
+ .bh-echart__status {
32
+ position: absolute;
33
+ inset: 0;
34
+ display: grid;
35
+ place-content: center;
36
+ padding: var(--bh-space-3xl-16);
37
+ color: var(--bh-content-muted);
38
+ text-align: center;
39
+ }
40
+
41
+ .bh-echart__description {
42
+ position: absolute;
43
+ width: var(--bh-space-xxxs-1);
44
+ height: var(--bh-space-xxxs-1);
45
+ padding: 0;
46
+ overflow: hidden;
47
+ clip-path: inset(50%);
48
+ white-space: nowrap;
49
+ }
@@ -0,0 +1,178 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { init, use, type ECharts } from "echarts/core"
5
+ import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, SankeyChart } from "echarts/charts"
6
+ import { AriaComponent, DataZoomComponent, GraphicComponent, GridComponent, LegendComponent, PolarComponent, TooltipComponent } from "echarts/components"
7
+ import { SVGRenderer } from "echarts/renderers"
8
+ import type { EChartsOption } from "echarts"
9
+
10
+ import { cn } from "@/lib/utils"
11
+ import { createEChartBaseOption, createEChartTheme, readEChartTheme, type EChartTheme } from "./echart-theme"
12
+ import "./echart.css"
13
+
14
+ use([BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, SankeyChart, AriaComponent, DataZoomComponent, GraphicComponent, GridComponent, LegendComponent, PolarComponent, TooltipComponent, SVGRenderer])
15
+
16
+ export type EChartProps = Omit<React.ComponentProps<"div">, "children"> & {
17
+ /** Build all chart styling from the resolved, inherited Banhaten tokens. */
18
+ option: (theme: EChartTheme) => EChartsOption
19
+ label: string
20
+ description?: string
21
+ /** Optional instance interactions; return a cleanup for listeners and timers. */
22
+ onReady?: (chart: ECharts, theme: EChartTheme) => void | (() => void)
23
+ }
24
+
25
+ export function EChart({ className, option, label, description, onReady, ...props }: EChartProps) {
26
+ const viewportRef = React.useRef<HTMLDivElement>(null)
27
+ const refreshRef = React.useRef<(() => void) | null>(null)
28
+ const latest = React.useRef({ option, onReady })
29
+ latest.current = { option, onReady }
30
+ const descriptionId = React.useId()
31
+ const [error, setError] = React.useState(false)
32
+
33
+ React.useEffect(() => {
34
+ const viewport = viewportRef.current
35
+ if (!viewport) return
36
+
37
+ let instance: ECharts | undefined
38
+ let cleanupInteraction: void | (() => void)
39
+ let themeSignature = ""
40
+ let frame: number | undefined
41
+ let visible = typeof IntersectionObserver === "undefined"
42
+ let disposed = false
43
+ const motion = window.matchMedia("(prefers-reduced-motion: reduce)")
44
+
45
+ const paint = () => {
46
+ frame = undefined
47
+ if (disposed || !visible || viewport.clientWidth === 0 || viewport.clientHeight === 0) return
48
+
49
+ try {
50
+ const theme = readEChartTheme(viewport, motion.matches)
51
+ const nextSignature = JSON.stringify(theme)
52
+ const viewState = instance ? readViewState(instance) : undefined
53
+ cleanupInteraction?.()
54
+ cleanupInteraction = undefined
55
+ if (!instance || themeSignature !== nextSignature) {
56
+ instance?.dispose()
57
+ instance = init(viewport, createEChartTheme(theme), { renderer: "svg" })
58
+ themeSignature = nextSignature
59
+ }
60
+ const requested = latest.current.option(theme)
61
+ const base = createEChartBaseOption(theme)
62
+ const series = requested.series
63
+ ? (Array.isArray(requested.series) ? requested.series : [requested.series])
64
+ : []
65
+ const duration = theme.reducedMotion ? 0 : theme.duration
66
+ const shadowlessTooltip = { shadowBlur: 0, shadowOffsetX: 0, shadowOffsetY: 0, extraCssText: "box-shadow: none;" }
67
+ instance.setOption({
68
+ ...base,
69
+ ...requested,
70
+ animation: theme.reducedMotion ? false : requested.animation ?? base.animation,
71
+ animationDuration: duration,
72
+ animationDurationUpdate: duration,
73
+ // Native series have their own durations, which override global defaults.
74
+ series: series.map((item) => ({
75
+ ...item,
76
+ animation: !theme.reducedMotion && requested.animation !== false && item.animation !== false,
77
+ animationDuration: duration,
78
+ animationDurationUpdate: duration,
79
+ stateAnimation: { ...item.stateAnimation, duration },
80
+ })),
81
+ tooltip: requested.tooltip === undefined
82
+ ? base.tooltip
83
+ : Array.isArray(requested.tooltip)
84
+ ? requested.tooltip.map((tooltip) => ({ ...base.tooltip, ...tooltip, ...shadowlessTooltip }))
85
+ : { ...base.tooltip, ...requested.tooltip, ...shadowlessTooltip },
86
+ }, { notMerge: true })
87
+ if (viewState) instance.setOption(viewState, { silent: true })
88
+ instance.resize()
89
+ cleanupInteraction = latest.current.onReady?.(instance, theme)
90
+ setError(false)
91
+ } catch (cause) {
92
+ instance?.dispose()
93
+ instance = undefined
94
+ setError(true)
95
+ console.error("Unable to render Banhaten chart.", cause)
96
+ }
97
+ }
98
+ const schedule = () => {
99
+ if (!disposed && frame === undefined) frame = window.requestAnimationFrame(paint)
100
+ }
101
+ refreshRef.current = schedule
102
+
103
+ const intersection = typeof IntersectionObserver === "undefined" ? undefined : new IntersectionObserver((entries) => {
104
+ visible = entries.some((entry) => entry.isIntersecting)
105
+ if (visible) schedule()
106
+ })
107
+ intersection?.observe(viewport)
108
+
109
+ const resizeChart = () => { if (instance) instance.resize(); else schedule() }
110
+ const resize = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(resizeChart)
111
+ resize?.observe(viewport)
112
+ window.addEventListener("resize", resizeChart)
113
+ motion.addEventListener("change", schedule)
114
+
115
+ // Observe only this chart's ancestry, avoiding rerenders from SVG mutations.
116
+ const mutations = new MutationObserver(schedule)
117
+ let ancestor: HTMLElement | null = viewport.parentElement
118
+ while (ancestor) {
119
+ mutations.observe(ancestor, {
120
+ attributes: true,
121
+ attributeFilter: [
122
+ "class", "style", "dir", "data-theme", "data-color-mode",
123
+ "data-english-font", "data-arabic-font", "data-radius",
124
+ ],
125
+ })
126
+ ancestor = ancestor.parentElement
127
+ }
128
+ document.fonts?.addEventListener("loadingdone", schedule)
129
+ void document.fonts?.ready.then(schedule)
130
+ schedule()
131
+
132
+ return () => {
133
+ disposed = true
134
+ refreshRef.current = null
135
+ if (frame !== undefined) window.cancelAnimationFrame(frame)
136
+ intersection?.disconnect()
137
+ resize?.disconnect()
138
+ mutations.disconnect()
139
+ window.removeEventListener("resize", resizeChart)
140
+ motion.removeEventListener("change", schedule)
141
+ document.fonts?.removeEventListener("loadingdone", schedule)
142
+ cleanupInteraction?.()
143
+ instance?.dispose()
144
+ }
145
+ }, [])
146
+
147
+ React.useEffect(() => { refreshRef.current?.() }, [option, onReady])
148
+
149
+ return (
150
+ <div
151
+ role="figure"
152
+ aria-label={label}
153
+ aria-describedby={description ? descriptionId : undefined}
154
+ data-slot="echart"
155
+ className={cn("bh-echart", className)}
156
+ {...props}
157
+ >
158
+ <div ref={viewportRef} className="bh-echart__viewport" aria-hidden="true" />
159
+ {description ? <span id={descriptionId} className="bh-echart__description">{description}</span> : null}
160
+ {error ? <span className="bh-echart__status" role="status">Chart preview unavailable.</span> : null}
161
+ </div>
162
+ )
163
+ }
164
+
165
+ /** Theme refreshes retain the user's selected series and viewport. */
166
+ function readViewState(instance: ECharts): EChartsOption {
167
+ const current = instance.getOption()
168
+ const result: EChartsOption = {}
169
+ if (Array.isArray(current.legend)) {
170
+ result.legend = current.legend.map((legend: { selected?: Record<string, boolean> }) => ({ selected: legend.selected }))
171
+ }
172
+ if (Array.isArray(current.dataZoom)) {
173
+ result.dataZoom = current.dataZoom.map((zoom: { start?: number; end?: number }) => ({ start: zoom.start, end: zoom.end }))
174
+ }
175
+ return result
176
+ }
177
+
178
+ export type { EChartTheme }