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
@@ -16,6 +16,7 @@ import { Tag, type TagSize } from "./tag"
16
16
  import { Popover, PopoverAnchor, PopoverContent } from "./popover"
17
17
 
18
18
  import { cn } from "@/lib/utils"
19
+ import { useListboxNavigation } from "@/hooks/use-listbox-navigation"
19
20
 
20
21
  type AutocompleteSelectionMode = "single" | "multiple"
21
22
  type AutocompleteValue = string | string[] | undefined
@@ -153,7 +154,6 @@ const Autocomplete = React.forwardRef<HTMLDivElement, AutocompleteProps>(
153
154
  getInitialInputValue(defaultValue, selectionMode, options)
154
155
  )
155
156
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
156
- const [activeIndex, setActiveIndex] = React.useState(-1)
157
157
 
158
158
  const selectedValue = isValueControlled ? value : uncontrolledValue
159
159
  const selectedValues = normalizeSelectedValues(selectedValue, isMultiple)
@@ -182,28 +182,18 @@ const Autocomplete = React.forwardRef<HTMLDivElement, AutocompleteProps>(
182
182
  !isDisabled &&
183
183
  (isOpenControlled ? Boolean(open) : uncontrolledOpen) &&
184
184
  (filteredOptions.length > 0 || hasRenderableContent(emptyText))
185
+ const listbox = useListboxNavigation({
186
+ idPrefix: listboxId,
187
+ isDisabled: (option) => Boolean(option.disabled),
188
+ open: menuOpen,
189
+ options: filteredOptions,
190
+ })
191
+ const activeIndex = listbox.activeIndex
185
192
  const activeOption = filteredOptions[activeIndex]
186
- const activeOptionId =
187
- menuOpen && activeOption ? getOptionId(listboxId, activeIndex) : undefined
188
193
  const resolvedTagSize =
189
194
  tagSize ?? (size === "md" ? ("xs" as const) : ("md" as const))
190
195
  const clearOnSelect = clearInputOnSelect ?? isMultiple
191
196
 
192
- React.useEffect(() => {
193
- if (!menuOpen) {
194
- setActiveIndex(-1)
195
- return
196
- }
197
-
198
- setActiveIndex((currentIndex) => {
199
- if (currentIndex >= 0 && !filteredOptions[currentIndex]?.disabled) {
200
- return currentIndex
201
- }
202
-
203
- return getFirstEnabledIndex(filteredOptions)
204
- })
205
- }, [filteredOptions, menuOpen])
206
-
207
197
  function setRootRef(node: HTMLDivElement | null) {
208
198
  rootRef.current = node
209
199
 
@@ -307,30 +297,26 @@ const Autocomplete = React.forwardRef<HTMLDivElement, AutocompleteProps>(
307
297
  if (event.key === "ArrowDown") {
308
298
  event.preventDefault()
309
299
  setMenuOpen(true)
310
- setActiveIndex((currentIndex) =>
311
- getNextEnabledIndex(filteredOptions, currentIndex, 1)
312
- )
300
+ listbox.move(1)
313
301
  return
314
302
  }
315
303
 
316
304
  if (event.key === "ArrowUp") {
317
305
  event.preventDefault()
318
306
  setMenuOpen(true)
319
- setActiveIndex((currentIndex) =>
320
- getNextEnabledIndex(filteredOptions, currentIndex, -1)
321
- )
307
+ listbox.move(-1)
322
308
  return
323
309
  }
324
310
 
325
311
  if (event.key === "Home" && menuOpen) {
326
312
  event.preventDefault()
327
- setActiveIndex(getFirstEnabledIndex(filteredOptions))
313
+ listbox.moveToEdge("first")
328
314
  return
329
315
  }
330
316
 
331
317
  if (event.key === "End" && menuOpen) {
332
318
  event.preventDefault()
333
- setActiveIndex(getLastEnabledIndex(filteredOptions))
319
+ listbox.moveToEdge("last")
334
320
  return
335
321
  }
336
322
 
@@ -412,7 +398,7 @@ const Autocomplete = React.forwardRef<HTMLDivElement, AutocompleteProps>(
412
398
  >
413
399
  <PopoverAnchor asChild>
414
400
  <Input
415
- aria-activedescendant={activeOptionId}
401
+ aria-activedescendant={listbox.activeOptionId}
416
402
  aria-autocomplete="list"
417
403
  aria-controls={menuOpen ? listboxId : undefined}
418
404
  aria-describedby={shouldRenderHelper ? `${resolvedInputId}-helper` : undefined}
@@ -470,7 +456,7 @@ const Autocomplete = React.forwardRef<HTMLDivElement, AutocompleteProps>(
470
456
  aria-selected={selectedValues.includes(option.value)}
471
457
  data-active={activeIndex === index ? "true" : undefined}
472
458
  disabled={option.disabled}
473
- id={getOptionId(listboxId, index)}
459
+ id={listbox.optionId(index)}
474
460
  itemType={option.itemType}
475
461
  key={option.value}
476
462
  label={option.label}
@@ -486,7 +472,7 @@ const Autocomplete = React.forwardRef<HTMLDivElement, AutocompleteProps>(
486
472
  onClick={() => selectOption(option)}
487
473
  onMouseDown={(event) => event.preventDefault()}
488
474
  onMouseEnter={() => {
489
- if (!option.disabled) setActiveIndex(index)
475
+ if (!option.disabled) listbox.setActiveIndex(index)
490
476
  }}
491
477
  />
492
478
  ))
@@ -598,45 +584,6 @@ function hasRenderableContent(content: React.ReactNode) {
598
584
  )
599
585
  }
600
586
 
601
- function getEnabledIndexes(options: AutocompleteOption[]) {
602
- return options
603
- .map((option, index) => (option.disabled ? -1 : index))
604
- .filter((index) => index >= 0)
605
- }
606
-
607
- function getFirstEnabledIndex(options: AutocompleteOption[]) {
608
- return getEnabledIndexes(options)[0] ?? -1
609
- }
610
-
611
- function getLastEnabledIndex(options: AutocompleteOption[]) {
612
- const enabledIndexes = getEnabledIndexes(options)
613
- return enabledIndexes[enabledIndexes.length - 1] ?? -1
614
- }
615
-
616
- function getNextEnabledIndex(
617
- options: AutocompleteOption[],
618
- currentIndex: number,
619
- offset: 1 | -1
620
- ) {
621
- const enabledIndexes = getEnabledIndexes(options)
622
- if (enabledIndexes.length === 0) return -1
623
-
624
- const currentPosition = enabledIndexes.indexOf(currentIndex)
625
- if (currentPosition === -1) {
626
- return offset > 0
627
- ? enabledIndexes[0]
628
- : enabledIndexes[enabledIndexes.length - 1]
629
- }
630
-
631
- const nextPosition =
632
- (currentPosition + offset + enabledIndexes.length) % enabledIndexes.length
633
- return enabledIndexes[nextPosition]
634
- }
635
-
636
- function getOptionId(listboxId: string, index: number) {
637
- return `${listboxId}-option-${index}`
638
- }
639
-
640
587
  function formatRemoveLabel(option: AutocompleteOption) {
641
588
  const optionText = getOptionText(option)
642
589
  return optionText ? `Remove ${optionText}` : "Remove option"
@@ -74,7 +74,7 @@ const avatarIconVariants = cva(
74
74
  )
75
75
 
76
76
  const avatarStatusVariants = cva(
77
- "absolute end-0 bottom-0 flex size-[var(--bh-avatar-status-size)] items-center justify-center rounded-[var(--bh-radius-full)] border-[var(--bh-avatar-status-ring)] border-[var(--bh-bg-default)] shadow-[var(--shadow-avatar-badge)]",
77
+ "absolute end-0 bottom-0 flex size-[var(--bh-avatar-status-size)] items-center justify-center rounded-[var(--bh-radius-full)] border-[var(--bh-avatar-status-ring)] border-[var(--bh-bg-default)] shadow-[var(--elevation-avatar-badge)]",
78
78
  {
79
79
  variants: {
80
80
  size: {
@@ -235,7 +235,7 @@ function AvatarBadge({
235
235
  <span
236
236
  data-slot="avatar-badge"
237
237
  className={cn(
238
- "absolute end-0 bottom-0 flex size-[var(--bh-space-xl-12)] items-center justify-center rounded-[var(--bh-radius-full)] border-[var(--bh-space-xxs-2)] border-[var(--bh-bg-default)] bg-[var(--bh-interactive-brand-default)] text-[var(--bh-content-on-brand)] shadow-[var(--shadow-avatar-badge)]",
238
+ "absolute end-0 bottom-0 flex size-[var(--bh-space-xl-12)] items-center justify-center rounded-[var(--bh-radius-full)] border-[var(--bh-space-xxs-2)] border-[var(--bh-bg-default)] bg-[var(--bh-interactive-brand-default)] text-[var(--bh-content-on-brand)] shadow-[var(--elevation-avatar-badge)]",
239
239
  className
240
240
  )}
241
241
  {...props}
@@ -67,7 +67,8 @@ const legacyVariantMap: Record<
67
67
  const badgeVariants = cva(
68
68
  [
69
69
  "inline-flex min-w-0 shrink-0 items-center justify-center whitespace-nowrap",
70
- "rounded-[var(--bh-radius-full)] border border-transparent outline-none",
70
+ // The border sits inside the padding, like Figma's inside stroke: each side padding below subtracts its width.
71
+ "rounded-[var(--bh-radius-full)] border-[length:var(--badge-border-width,var(--bh-border-width-default))] border-transparent outline-none",
71
72
  "transition-[background-color,border-color,color,box-shadow]",
72
73
  "focus-visible:border-[var(--bh-border-focus)] focus-visible:ring-[length:var(--bh-badge-focus-ring-width)] focus-visible:ring-[var(--ring)]",
73
74
  "aria-invalid:border-[var(--bh-border-focus-danger)] aria-invalid:ring-[var(--danger-ring)]",
@@ -78,7 +79,7 @@ const badgeVariants = cva(
78
79
  variants: {
79
80
  badgeStyle: {
80
81
  light: "border-transparent",
81
- outline: "border-[length:var(--bh-badge-outline-border-width)] bg-transparent",
82
+ outline: "[--badge-border-width:var(--bh-badge-outline-border-width)] bg-transparent",
82
83
  solid: "border-transparent",
83
84
  },
84
85
  color: {
@@ -145,30 +146,30 @@ const badgeVariants = cva(
145
146
  type: "default",
146
147
  size: ["xs", "sm", "default"],
147
148
  showNumber: false,
148
- class: "px-[var(--bh-space-md-8)]",
149
+ class: "px-[calc(var(--bh-space-md-8)-var(--badge-border-width,var(--bh-border-width-default)))]",
149
150
  },
150
151
  {
151
152
  type: "default",
152
153
  size: "lg",
153
154
  showNumber: false,
154
- class: "px-[var(--bh-space-xl-12)]",
155
+ class: "px-[calc(var(--bh-space-xl-12)-var(--badge-border-width,var(--bh-border-width-default)))]",
155
156
  },
156
157
  {
157
158
  type: ["leading-icon", "trailing-icon", "dot"],
158
159
  showNumber: false,
159
160
  splitAction: false,
160
- class: "px-[var(--bh-space-md-8)]",
161
+ class: "px-[calc(var(--bh-space-md-8)-var(--badge-border-width,var(--bh-border-width-default)))]",
161
162
  },
162
163
  {
163
164
  type: "trailing-icon",
164
165
  showNumber: false,
165
166
  splitAction: true,
166
- class: "ps-[var(--bh-space-md-8)]",
167
+ class: "ps-[calc(var(--bh-space-md-8)-var(--badge-border-width,var(--bh-border-width-default)))]",
167
168
  },
168
169
  {
169
170
  type: "flag",
170
171
  showNumber: false,
171
- class: "ps-[var(--bh-space-xs-4)] pe-[var(--bh-space-md-8)]",
172
+ class: "ps-[calc(var(--bh-space-xs-4)-var(--badge-border-width,var(--bh-border-width-default)))] pe-[calc(var(--bh-space-md-8)-var(--badge-border-width,var(--bh-border-width-default)))]",
172
173
  },
173
174
  {
174
175
  size: ["xs", "sm", "default"],
@@ -32,7 +32,7 @@ type ToggleGroupContextValue = {
32
32
  const toggleGroupContext = React.createContext<ToggleGroupContextValue | null>(null)
33
33
 
34
34
  const buttonGroupVariants = cva(
35
- "inline-flex w-fit max-w-full items-center overflow-x-auto overflow-y-hidden rounded-[var(--bh-button-group-radius)] bg-[var(--bh-button-group-bg)] text-[var(--bh-button-group-content)] shadow-[var(--shadow-button-group)] outline-none focus-visible:shadow-[var(--shadow-button-group-focus)] [--shadow-button-group:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-button-group-border),var(--shadow-component-default)]",
35
+ "inline-flex w-fit max-w-full items-center overflow-x-auto overflow-y-hidden rounded-[var(--bh-button-group-radius)] bg-[var(--bh-button-group-bg)] text-[var(--bh-button-group-content)] shadow-[var(--elevation-button-group)] outline-none focus-visible:shadow-[var(--elevation-button-group-focus)] [--elevation-button-group:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-button-group-border),var(--elevation-component-default)]",
36
36
  {
37
37
  variants: {
38
38
  size: {
@@ -48,7 +48,7 @@ const buttonGroupVariants = cva(
48
48
  )
49
49
 
50
50
  const buttonGroupItemVariants = cva(
51
- "relative inline-flex shrink-0 items-center justify-center whitespace-nowrap bg-[var(--bh-button-group-item-bg)] text-[length:var(--bh-text-button-font-size)] font-[var(--bh-text-button-font-weight)] leading-[var(--bh-text-button-line-height)] tracking-[var(--bh-text-button-letter-spacing)] text-[var(--bh-button-group-content)] outline-none transition-[background-color,box-shadow,color] before:absolute before:inset-y-0 before:start-0 before:w-[var(--bh-border-width-default)] before:bg-[var(--bh-button-group-divider)] before:content-[''] first:before:hidden hover:bg-[var(--bh-button-group-item-hover-bg)] active:bg-[var(--bh-button-group-item-pressed-bg)] focus-visible:z-[var(--bh-z-raised)] focus-visible:shadow-[var(--shadow-button-group-focus)] aria-[pressed=true]:bg-[var(--bh-button-group-item-active-bg)] aria-[pressed=true]:text-[var(--bh-button-group-content-active)] data-[active=true]:bg-[var(--bh-button-group-item-active-bg)] data-[active=true]:text-[var(--bh-button-group-content-active)] disabled:pointer-events-none disabled:bg-[var(--bh-button-group-item-disabled-bg)] disabled:text-[var(--bh-button-group-content-disabled)] [&_svg]:pointer-events-none [&_svg]:size-[var(--bh-button-group-icon-size)] [&_svg]:m-[calc((var(--bh-button-group-icon-slot-size)-var(--bh-button-group-icon-size))/2)] [&_svg]:shrink-0 rtl:[&_svg[data-rtl-flip='true']]:-scale-x-100",
51
+ "relative inline-flex shrink-0 items-center justify-center whitespace-nowrap bg-[var(--bh-button-group-item-bg)] text-[length:var(--bh-text-button-font-size)] font-[var(--bh-text-button-font-weight)] leading-[var(--bh-text-button-line-height)] tracking-[var(--bh-text-button-letter-spacing)] text-[var(--bh-button-group-content)] outline-none transition-[background-color,box-shadow,color] before:absolute before:inset-y-0 before:start-0 before:w-[var(--bh-border-width-default)] before:bg-[var(--bh-button-group-divider)] before:content-[''] first:before:hidden hover:bg-[var(--bh-button-group-item-hover-bg)] active:bg-[var(--bh-button-group-item-pressed-bg)] focus-visible:z-[var(--bh-z-raised)] focus-visible:shadow-[var(--elevation-button-group-focus)] aria-[pressed=true]:bg-[var(--bh-button-group-item-active-bg)] aria-[pressed=true]:text-[var(--bh-button-group-content-active)] data-[active=true]:bg-[var(--bh-button-group-item-active-bg)] data-[active=true]:text-[var(--bh-button-group-content-active)] disabled:pointer-events-none disabled:bg-[var(--bh-button-group-item-disabled-bg)] disabled:text-[var(--bh-button-group-content-disabled)] [&_svg]:pointer-events-none [&_svg]:size-[var(--bh-button-group-icon-size)] [&_svg]:m-[calc((var(--bh-button-group-icon-slot-size)-var(--bh-button-group-icon-size))/2)] [&_svg]:shrink-0 rtl:[&_svg[data-rtl-flip='true']]:-scale-x-100",
52
52
  {
53
53
  variants: {
54
54
  size: {
@@ -6,25 +6,30 @@ import { cn } from "@/lib/utils"
6
6
 
7
7
  type ControlDensity = "compact" | "default" | "comfortable"
8
8
 
9
+ const dangerButtonVariant =
10
+ "bg-[var(--bh-interactive-danger-default)] text-[var(--bh-content-on-color)] shadow-[var(--elevation-button-raised)] hover:bg-[var(--bh-interactive-danger-hover)] active:bg-[var(--bh-interactive-danger-pressed)] focus-visible:shadow-[var(--elevation-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-disabled)] disabled:text-[var(--bh-content-disabled)]"
11
+
12
+ const softDangerButtonVariant =
13
+ "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-danger-soft-default)] text-[var(--bh-content-danger-default)] hover:bg-[var(--bh-interactive-danger-soft-hover)] active:bg-[var(--bh-interactive-danger-soft-pressed)] focus-visible:border-[var(--bh-interactive-danger-default)] focus-visible:shadow-[var(--elevation-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-soft-disabled)] disabled:text-[var(--bh-content-disabled)]"
14
+
9
15
  const buttonVariants = cva(
10
- "group/button inline-flex min-w-0 items-center justify-center whitespace-nowrap rounded-[var(--bh-control-default)] text-[length:var(--bh-text-button-font-size)] font-[var(--bh-text-button-font-weight)] leading-[var(--bh-text-button-line-height)] tracking-[var(--bh-text-button-letter-spacing)] outline-none transition-[background-color,border-color,color,box-shadow] focus-visible:shadow-[var(--shadow-button-focus)] disabled:pointer-events-none disabled:shadow-none aria-invalid:border-[length:var(--bh-border-width-default)] aria-invalid:border-[var(--bh-border-danger-strong)] aria-invalid:shadow-[var(--shadow-button-danger-focus)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[var(--bh-button-icon-size)] [&_svg:not([class*='size-'])]:m-[calc((var(--bh-button-icon-slot-size)-var(--bh-button-icon-size))/2)] [&_svg[data-icon='inline-start']]:ms-[calc(var(--bh-button-icon-offset)+(var(--bh-button-icon-slot-size)-var(--bh-button-icon-size))/2)] [&_svg[data-icon='inline-end']]:me-[calc(var(--bh-button-icon-offset)+(var(--bh-button-icon-slot-size)-var(--bh-button-icon-size))/2)] rtl:[&_svg[data-rtl-flip='true']]:-scale-x-100",
16
+ "group/button inline-flex min-w-0 items-center justify-center whitespace-nowrap rounded-[var(--bh-control-default)] text-[length:var(--bh-text-button-font-size)] font-[var(--bh-text-button-font-weight)] leading-[var(--bh-text-button-line-height)] tracking-[var(--bh-text-button-letter-spacing)] outline-none transition-[background-color,border-color,color,box-shadow] focus-visible:shadow-[var(--elevation-button-focus)] disabled:pointer-events-none disabled:shadow-none aria-invalid:border-[length:var(--bh-border-width-default)] aria-invalid:border-[var(--bh-border-danger-strong)] aria-invalid:shadow-[var(--elevation-button-danger-focus)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[var(--bh-button-icon-size)] [&_svg:not([class*='size-'])]:m-[calc((var(--bh-button-icon-slot-size)-var(--bh-button-icon-size))/2)] [&_svg[data-icon='inline-start']]:ms-[calc(var(--bh-button-icon-offset)+(var(--bh-button-icon-slot-size)-var(--bh-button-icon-size))/2)] [&_svg[data-icon='inline-end']]:me-[calc(var(--bh-button-icon-offset)+(var(--bh-button-icon-slot-size)-var(--bh-button-icon-size))/2)] rtl:[&_svg[data-rtl-flip='true']]:-scale-x-100",
11
17
  {
12
18
  variants: {
13
19
  variant: {
14
20
  default:
15
- "bg-[var(--bh-interactive-brand-default)] text-[var(--bh-content-on-brand)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-brand-hover)] active:bg-[var(--bh-interactive-brand-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
16
- danger:
17
- "bg-[var(--bh-interactive-danger-default)] text-[var(--bh-content-on-color)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-danger-hover)] active:bg-[var(--bh-interactive-danger-pressed)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-disabled)] disabled:text-[var(--bh-content-disabled)]",
18
- destructive:
19
- "bg-[var(--bh-interactive-danger-default)] text-[var(--bh-content-on-color)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-danger-hover)] active:bg-[var(--bh-interactive-danger-pressed)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-disabled)] disabled:text-[var(--bh-content-disabled)]",
21
+ "bg-[var(--bh-interactive-brand-default)] text-[var(--bh-content-on-brand)] shadow-[var(--elevation-button-raised)] hover:bg-[var(--bh-interactive-brand-hover)] active:bg-[var(--bh-interactive-brand-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
22
+ danger: dangerButtonVariant,
23
+ /** @deprecated Use `danger` instead. */
24
+ destructive: dangerButtonVariant,
20
25
  success:
21
- "bg-[var(--bh-interactive-success-default)] text-[var(--bh-content-on-success)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-success-hover)] active:bg-[var(--bh-interactive-success-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
26
+ "bg-[var(--bh-interactive-success-default)] text-[var(--bh-content-on-success)] shadow-[var(--elevation-button-raised)] hover:bg-[var(--bh-interactive-success-hover)] active:bg-[var(--bh-interactive-success-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
22
27
  warning:
23
- "bg-[var(--bh-interactive-warning-default)] text-[var(--bh-content-on-warning)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-warning-hover)] active:bg-[var(--bh-interactive-warning-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
28
+ "bg-[var(--bh-interactive-warning-default)] text-[var(--bh-content-on-warning)] shadow-[var(--elevation-button-raised)] hover:bg-[var(--bh-interactive-warning-hover)] active:bg-[var(--bh-interactive-warning-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
24
29
  secondary:
25
- "bg-[var(--bh-interactive-secondary-default)] text-[var(--bh-content-default)] shadow-[var(--shadow-button-secondary)] [--bh-button-secondary-border:var(--bh-border-default)] hover:bg-[var(--bh-interactive-secondary-hover)] active:bg-[var(--bh-interactive-secondary-pressed)] focus-visible:[--bh-button-secondary-focus-border:var(--bh-border-brand-strong)] focus-visible:shadow-[var(--shadow-button-secondary-focus)] disabled:border-[length:var(--bh-border-width-default)] disabled:border-[var(--bh-border-disabled)] disabled:bg-[var(--bh-interactive-secondary-disabled)] disabled:text-[var(--bh-content-disabled)]",
30
+ "bg-[var(--bh-interactive-secondary-default)] text-[var(--bh-content-default)] shadow-[var(--elevation-button-secondary)] [--bh-button-secondary-border:var(--bh-border-default)] hover:bg-[var(--bh-interactive-secondary-hover)] active:bg-[var(--bh-interactive-secondary-pressed)] focus-visible:[--bh-button-secondary-focus-border:var(--bh-border-brand-strong)] focus-visible:shadow-[var(--elevation-button-secondary-focus)] disabled:border-[length:var(--bh-border-width-default)] disabled:border-[var(--bh-border-disabled)] disabled:bg-[var(--bh-interactive-secondary-disabled)] disabled:text-[var(--bh-content-disabled)]",
26
31
  soft:
27
- "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-soft-default)] text-[var(--bh-content-default)] shadow-[var(--shadow-button-soft)] hover:bg-[var(--bh-interactive-soft-hover)] active:bg-[var(--bh-interactive-soft-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
32
+ "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-soft-default)] text-[var(--bh-content-default)] shadow-[var(--elevation-button-soft)] hover:bg-[var(--bh-interactive-soft-hover)] active:bg-[var(--bh-interactive-soft-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
28
33
  outline:
29
34
  "border-[length:var(--bh-border-width-default)] border-[var(--bh-border-default)] bg-[var(--bh-interactive-outlined-default)] text-[var(--bh-content-default)] hover:bg-[var(--bh-interactive-outlined-hover)] active:bg-[var(--bh-interactive-outlined-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:border-[var(--bh-border-disabled)] disabled:bg-[var(--bh-interactive-outlined-disabled)] disabled:text-[var(--bh-content-disabled)]",
30
35
  ghost:
@@ -33,10 +38,9 @@ const buttonVariants = cva(
33
38
  "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-ghost-default)] text-[var(--bh-content-brand-default)] hover:bg-[var(--bh-interactive-ghost-hover)] active:bg-[var(--bh-interactive-ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-ghost-disabled)] disabled:text-[var(--bh-content-disabled)]",
34
39
  white:
35
40
  "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-bg-always-white)] text-[var(--bh-content-on-light)] focus-visible:border-[var(--bh-border-focus)] disabled:opacity-[var(--bh-opacity-30)]",
36
- "soft-danger":
37
- "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-danger-soft-default)] text-[var(--bh-content-danger-default)] hover:bg-[var(--bh-interactive-danger-soft-hover)] active:bg-[var(--bh-interactive-danger-soft-pressed)] focus-visible:border-[var(--bh-interactive-danger-default)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
38
- "soft-destructive":
39
- "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-danger-soft-default)] text-[var(--bh-content-danger-default)] hover:bg-[var(--bh-interactive-danger-soft-hover)] active:bg-[var(--bh-interactive-danger-soft-pressed)] focus-visible:border-[var(--bh-interactive-danger-default)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
41
+ "soft-danger": softDangerButtonVariant,
42
+ /** @deprecated Use `soft-danger` instead. */
43
+ "soft-destructive": softDangerButtonVariant,
40
44
  link: "bg-[var(--bh-interactive-ghost-default)] text-[var(--bh-content-brand-default)] hover:bg-[var(--bh-interactive-ghost-hover)] active:bg-[var(--bh-interactive-ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:text-[var(--bh-content-disabled)]",
41
45
  },
42
46
  size: {
@@ -84,6 +88,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
84
88
  data-density={density}
85
89
  data-size={resolvedSize}
86
90
  data-slot="button"
91
+ data-variant={variant ?? "default"}
87
92
  ref={ref}
88
93
  className={cn(buttonVariants({ variant, size: resolvedSize, className }))}
89
94
  {...props}
@@ -8,6 +8,7 @@ import { Button, type ButtonProps } from "./button"
8
8
  type CardOrientation = "vertical" | "horizontal"
9
9
  type CardType = "leading-image" | "trailing-image" | "full-image"
10
10
  type CardBreakpoint = "desktop" | "mobile"
11
+ type CardSize = "default" | "comfortable"
11
12
 
12
13
  const cardVariants = cva(
13
14
  [
@@ -16,10 +17,14 @@ const cardVariants = cva(
16
17
  "text-[var(--bh-content-default)] shadow-[inset_0_0_0_var(--bh-border-width-default)_var(--bh-card-border)]",
17
18
  "[--bh-card-bg:var(--bh-bg-raised)]",
18
19
  "[--bh-card-border:var(--bh-border-subtle)]",
19
- "[--bh-card-radius:var(--bh-radius-3xl-16)]",
20
20
  ],
21
21
  {
22
22
  variants: {
23
+ // comfortable is the app-shell panel: a smaller radius than the library cards.
24
+ size: {
25
+ default: "[--bh-card-radius:var(--bh-radius-3xl-16)]",
26
+ comfortable: "[--bh-card-radius:var(--bh-card-comfortable-radius)]",
27
+ },
23
28
  orientation: {
24
29
  vertical: "flex-col",
25
30
  horizontal: "items-stretch",
@@ -67,6 +72,7 @@ const cardVariants = cva(
67
72
  },
68
73
  ],
69
74
  defaultVariants: {
75
+ size: "default",
70
76
  orientation: "vertical",
71
77
  type: "leading-image",
72
78
  breakpoint: "desktop",
@@ -109,15 +115,18 @@ type CardActionsProps = React.ComponentProps<"div"> & {
109
115
  }
110
116
 
111
117
  type CardFooterProps = React.ComponentProps<"div">
112
- type CardSurfaceProps = React.ComponentPropsWithoutRef<"article">
118
+ type CardSurfaceProps = React.ComponentPropsWithoutRef<"article"> & {
119
+ size?: CardSize
120
+ }
113
121
 
114
122
  const CardSurface = React.forwardRef<HTMLElement, CardSurfaceProps>(
115
- function CardSurface({ className, ...props }, ref) {
123
+ function CardSurface({ className, size = "default", ...props }, ref) {
116
124
  return (
117
125
  <article
118
126
  data-slot="card-surface"
127
+ data-size={size}
119
128
  ref={ref}
120
- className={cn(cardVariants(), className)}
129
+ className={cn(cardVariants({ size }), className)}
121
130
  {...props}
122
131
  />
123
132
  )
@@ -144,6 +153,7 @@ const Card = React.forwardRef<HTMLElement, CardProps>(function Card({
144
153
  showImage,
145
154
  showMoreButton,
146
155
  showSubtitle,
156
+ size = "default",
147
157
  subtitle,
148
158
  title,
149
159
  type = "leading-image",
@@ -199,10 +209,12 @@ const Card = React.forwardRef<HTMLElement, CardProps>(function Card({
199
209
  data-slot="card"
200
210
  data-breakpoint={cardBreakpoint}
201
211
  data-orientation={cardOrientation}
212
+ data-size={size ?? "default"}
202
213
  data-type={cardType}
203
214
  ref={ref}
204
215
  className={cn(
205
216
  cardVariants({
217
+ size,
206
218
  orientation: cardOrientation,
207
219
  type: cardType,
208
220
  breakpoint: cardBreakpoint,
@@ -550,7 +562,7 @@ function CardMoreButton({
550
562
  "inline-flex size-[var(--bh-card-more-size)] shrink-0 items-center justify-center",
551
563
  "rounded-[var(--bh-control-default)] outline-none",
552
564
  "transition-[background-color,color,box-shadow]",
553
- "focus-visible:shadow-[var(--shadow-button-focus)]",
565
+ "focus-visible:shadow-[var(--elevation-button-focus)]",
554
566
  onImage
555
567
  ? "text-[var(--bh-content-on-color)] hover:bg-[var(--bh-interactive-ghost-hover)]"
556
568
  : "text-[var(--bh-content-subtle)] hover:bg-[var(--bh-interactive-ghost-hover)] hover:text-[var(--bh-content-default)]"
@@ -633,6 +645,7 @@ export {
633
645
  type CardMediaProps,
634
646
  type CardOrientation,
635
647
  type CardProps,
648
+ type CardSize,
636
649
  type CardSurfaceProps,
637
650
  type CardType,
638
651
  }
@@ -0,0 +1,177 @@
1
+ import type { ChartDatum, ChartSpec } from "./chart-options"
2
+
3
+ export type ChartBlockDefinition = {
4
+ title: string
5
+ description: string
6
+ spec: ChartSpec
7
+ periods?: { label: string; count: number }[]
8
+ }
9
+
10
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
11
+ const monthly = (keys: string[], values: number[][]): ChartDatum[] => values.map((row, index) => ({
12
+ name: months[index % 12] + (values.length > 12 ? ` '${24 + Math.floor(index / 12)}` : ""),
13
+ ...Object.fromEntries(keys.map((key, column) => [key, row[column]])),
14
+ }))
15
+ const monthPeriods = [{ label: "3 months", count: 3 }, { label: "6 months", count: 6 }, { label: "All", count: Infinity }]
16
+ const series = (key: string, label: string, colorIndex: number) => ({ key, label, colorIndex })
17
+
18
+ // Illustrative source datasets, adapted from EvilCharts' MIT-licensed ECharts blocks.
19
+ // All visual decisions live in the shared token-aware option builders and block CSS.
20
+ export const chartBlockDefinitions = {
21
+ latency: {
22
+ title: "Latency Percentiles",
23
+ description: "Request latency by percentile, in milliseconds.",
24
+ periods: [{ label: "5 minutes", count: 5 }, { label: "15 minutes", count: 15 }, { label: "All", count: Infinity }],
25
+ spec: {
26
+ family: "area", curve: "linear", variant: "gradient", hoverHighlight: true,
27
+ series: [series("p99", "P99 (ms)", 3), series("p95", "P95 (ms)", 2), series("p75", "P75 (ms)", 4), series("p50", "P50 (ms)", 0)],
28
+ data: [
29
+ [188, 92, 44, 20], [196, 95, 46, 21], [181, 89, 43, 20], [192, 94, 47, 22],
30
+ [205, 99, 45, 21], [187, 91, 44, 20], [179, 88, 42, 19], [198, 96, 46, 21],
31
+ [210, 101, 48, 22], [194, 93, 45, 21], [202, 97, 47, 22], [215, 104, 49, 23],
32
+ [231, 112, 52, 23], [278, 131, 57, 24], [306, 142, 61, 25], [289, 135, 58, 24],
33
+ [247, 118, 53, 23], [216, 105, 49, 22], [201, 97, 46, 21], [193, 94, 45, 21],
34
+ [186, 90, 44, 20], [199, 96, 46, 21], [207, 100, 47, 22], [191, 92, 45, 20],
35
+ [184, 89, 43, 20], [196, 95, 46, 21], [209, 101, 48, 22], [198, 96, 46, 21], [204, 98, 46, 21],
36
+ ].map(([p99, p95, p75, p50], index) => ({ name: `13:${String(index + 6).padStart(2, "0")}`, p99, p95, p75, p50 })),
37
+ },
38
+ },
39
+ portfolio: {
40
+ title: "Portfolio Comparison", description: "Compare the change in two example portfolios, in USD.",
41
+ periods: [{ label: "7 days", count: 7 }, { label: "14 days", count: 14 }, { label: "All", count: Infinity }],
42
+ spec: {
43
+ family: "area", curve: "step", variant: "dotted", hoverReveal: true,
44
+ series: [series("portfolioA", "Portfolio A (USD)", 0), series("portfolioB", "Portfolio B (USD)", 1)],
45
+ data: [
46
+ [53916, 53670], [54380, 53080], [54760, 52460], [54480, 51880], [54060, 51340],
47
+ [53820, 50900], [53520, 50520], [53240, 50200], [53080, 49960], [52840, 49760],
48
+ [52700, 49640], [52520, 49600], [52420, 49720], [52340, 49980], [52240, 50360],
49
+ [52140, 50820], [52080, 51340], [52020, 51900], [51960, 52440], [51900, 52960],
50
+ [51860, 53420], [51800, 53780], [51740, 54040], [51700, 54200], [51640, 54300],
51
+ [51580, 54320], [51538, 54287],
52
+ ].map(([portfolioA, portfolioB], index) => ({ name: index < 10 ? `Dec ${index + 22}` : `Jan ${index - 9}`, portfolioA, portfolioB })),
53
+ },
54
+ },
55
+ benchmark: {
56
+ title: "Growth Benchmark", description: "Weekly signups compared with the target.",
57
+ periods: [{ label: "Recent 4", count: 4 }, { label: "Recent 12", count: 12 }, { label: "All", count: Infinity }],
58
+ spec: {
59
+ family: "area", curve: "step", legend: true,
60
+ series: [{ ...series("actual", "Actual", 0), variant: "hatched" }, { ...series("target", "Target", 4), type: "line", stroke: "dashed" }],
61
+ data: [
62
+ ["Jan 1", 240, 125], ["Jan 8", 240, 125], ["Jan 15", 175, 200], ["Jan 22", 175, 200],
63
+ ["Feb 1", 175, 200], ["Feb 8", 260, 125], ["Feb 15", 260, 125], ["Feb 22", 260, 125],
64
+ ["Mar 1", 260, 190], ["Mar 8", 335, 190], ["Mar 15", 335, 190], ["Mar 22", 335, 390],
65
+ ["Apr 1", 335, 390], ["Apr 8", 335, 390], ["Apr 15", 190, 390], ["Apr 22", 215, 305],
66
+ ["May 1", 215, 305], ["May 8", 215, 175], ["May 15", 215, 175], ["May 22", 275, 175], ["Jun 1", 275, 245],
67
+ ].map(([name, actual, target]) => ({ name, actual, target })),
68
+ },
69
+ },
70
+ audience: {
71
+ title: "Audience Growth", description: "Monthly listeners across every show and episode.", periods: monthPeriods,
72
+ spec: { family: "area", curve: "monotone", variant: "gradient", series: [{ key: "listeners", label: "Listeners", colorIndices: [1, 4, 5] }],
73
+ data: monthly(["listeners"], [[2980], [3120], [3460], [3380], [3720], [4180], [4560]]) },
74
+ },
75
+ payouts: {
76
+ title: "Payout Trend", description: "Paid and pending amounts by month, in USD.", periods: monthPeriods,
77
+ spec: { family: "line", curve: "monotone", legend: true,
78
+ series: [{ key: "payouts", label: "Paid (USD)", colorIndices: [2, 7] }, { key: "pending", label: "Pending (USD)", colorIndices: [4, 5] }],
79
+ data: monthly(["payouts", "pending"], [[312, 548], [388, 502], [342, 561], [455, 470], [521, 398], [486, 441], [573, 372], [640, 316], [598, 358], [662, 284], [617, 331], [690, 262]]) },
80
+ },
81
+ shipments: {
82
+ title: "Shipments", description: "Orders shipped in corresponding slots this week and last week.",
83
+ periods: [{ label: "2 days", count: 8 }, { label: "4 days", count: 16 }, { label: "All", count: Infinity }],
84
+ spec: { family: "line", curve: "linear", legend: true,
85
+ series: [series("current", "This week", 0), { ...series("previous", "Last week", 4), stroke: "dashed" }],
86
+ data: [[14, 34], [9, 41], [18, 37], [26, 29], [31, 24], [27, 33], [19, 45], [12, 52], [16, 47], [22, 39], [15, 44], [11, 50], [17, 43], [24, 31], [20, 26], [13, 22], [21, 28], [29, 36], [34, 42], [28, 55], [23, 49], [30, 40], [36, 35], [32, 44]]
87
+ .map(([current, previous], index) => ({ name: `${["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][Math.floor(index / 4)]} ${index % 4 + 1}`, current, previous })) },
88
+ },
89
+ peak: {
90
+ title: "Peak Week", description: "Organic and paid signups, with the best week highlighted.",
91
+ periods: [{ label: "4 weeks", count: 4 }, { label: "8 weeks", count: 8 }, { label: "All", count: Infinity }],
92
+ spec: { family: "bar", stack: "stacked", highlightMax: true, legend: true,
93
+ series: [series("organic", "Organic", 0), series("paid", "Paid", 4)],
94
+ data: [[128, 74], [164, 91], [142, 66], [199, 108], [176, 84], [231, 167], [208, 96], [287, 158], [244, 112], [196, 88], [221, 103], [173, 79]]
95
+ .map(([organic, paid], index) => ({ name: `W${String(index + 1).padStart(2, "0")}`, organic, paid })) },
96
+ },
97
+ grid: {
98
+ title: "Grid Bar Chart", description: "Hourly sessions shown as columns of cells.",
99
+ periods: [{ label: "6 hours", count: 6 }, { label: "12 hours", count: 12 }, { label: "All", count: Infinity }],
100
+ spec: { family: "bar", variant: "blocks", series: [series("sessions", "Sessions", 0)],
101
+ data: [42, 28, 19, 14, 12, 18, 34, 66, 98, 124, 147, 163, 158, 171, 186, 174, 152, 138, 119, 96, 84, 71, 58, 47]
102
+ .map((sessions, index) => ({ name: `${String(index).padStart(2, "0")}:00`, sessions })) },
103
+ },
104
+ monospace: {
105
+ title: "Monospace Bar Chart", description: "Monthly sales with bars that expand on hover.", periods: monthPeriods,
106
+ spec: { family: "bar", variant: "expandable", series: [series("sales", "Sales (USD)", 0)],
107
+ data: monthly(["sales"], [342, 876, 512, 629, 458, 781, 394, 925, 647, 532, 803, 271, 388, 912, 564, 671, 499, 838, 427, 968, 702, 585, 861, 314].map((value) => [value])) },
108
+ },
109
+ "market-share": {
110
+ title: "Market Share", description: "Illustrative ecosystem value by company, in billions of USD.",
111
+ spec: { family: "pie", variant: "padded", series: [series("value", "Value (USD billions)", 0)],
112
+ data: [{ name: "Skyline", value: 27 }, { name: "Datawell", value: 21 }, { name: "Cloudpeak", value: 13 }, { name: "Taskbridge", value: 21 }, { name: "Insightloop", value: 6 }, { name: "Streamforge", value: 12 }] },
113
+ },
114
+ "progress-rings": {
115
+ title: "Progress Rings", description: "Where the workday leaks: illustrative findings from 1,240 survey responses.",
116
+ spec: { family: "pie", series: [series("value", "Respondents (%)", 0)], data: [
117
+ { name: "Additional support requests", value: 48 }, { name: "Inaccurate forecasts", value: 67 },
118
+ ] },
119
+ },
120
+ "revenue-mix": {
121
+ title: "Revenue Mix", description: "Revenue by sales channel, in USD.",
122
+ spec: { family: "pie", variant: "padded", series: [series("value", "Revenue (USD)", 0)], data: [
123
+ { name: "Direct", value: 52400 }, { name: "Marketplace", value: 38900 }, { name: "Wholesale", value: 24150 }, { name: "Affiliate", value: 16300 },
124
+ ] },
125
+ },
126
+ "reliability-score": {
127
+ title: "Reliability Score", description: "Delivery reliability: 842 out of 1,000, in the excellent band.",
128
+ spec: { family: "pie", variant: "padded", data: [
129
+ { name: "At risk", from: 0, value: 450 }, { name: "Fair", from: 450, value: 200 }, { name: "Good", from: 650, value: 170 }, { name: "Excellent", from: 820, value: 180 },
130
+ ], series: [series("value", "Band width", 0)] },
131
+ },
132
+ budget: {
133
+ title: "Monthly Budget", description: "The share and amount allocated to each monthly budget category.",
134
+ spec: { family: "radial", max: 100, series: [series("value", "Share (%)", 0)], data: [
135
+ { name: "Payroll", value: 46, amount: 920000 }, { name: "Infrastructure", value: 21, amount: 420000 }, { name: "Marketing", value: 14, amount: 280000 }, { name: "Tooling", value: 10, amount: 200000 }, { name: "Support", value: 9, amount: 180000 },
136
+ ] },
137
+ },
138
+ ride: {
139
+ title: "Ride Summary", description: "Distance, elevation, energy and cadence against their goals.",
140
+ spec: { family: "radial", data: [
141
+ { name: "Distance", value: 18.4, goal: 25, unit: "km" }, { name: "Elevation", value: 312, goal: 450, unit: "m" },
142
+ { name: "Energy", value: 684, goal: 1200, unit: "kJ" }, { name: "Cadence", value: 82, goal: 95, unit: "rpm" },
143
+ ] },
144
+ },
145
+ "cache-tiers": {
146
+ title: "Cache Tiers", description: "Request routing across four cache tiers, from a sample of 1,000 requests.",
147
+ spec: { family: "radial", variant: "semi", max: 100, series: [series("value", "Share (%)", 0)], data: [
148
+ { name: "L1 Memory", value: 61, count: 610 }, { name: "L2 Regional", value: 24, count: 240 },
149
+ { name: "Edge Overflow", value: 10, count: 100 }, { name: "Origin Fetch", value: 5, count: 50 },
150
+ ] },
151
+ },
152
+ allocation: {
153
+ title: "Fund Allocation", description: "Where the fund flows, in thousands of USD.",
154
+ spec: { family: "sankey", variant: "gradient", labels: "inside", data: [],
155
+ nodes: ["Inflow", "Equities", "Bonds", "Cash", "Growth", "Income", "Reserve"].map((name) => ({ name })),
156
+ links: [[0, 1, 78], [0, 2, 46], [0, 3, 24], [1, 4, 52], [1, 5, 26], [2, 5, 19], [2, 6, 27], [3, 4, 9], [3, 6, 15]]
157
+ .map(([source, target, value]) => ({ source, target, value })) },
158
+ },
159
+ pipeline: {
160
+ title: "Revenue Pipeline", description: "Four revenue sources flow into four operating allocations, in USD.",
161
+ spec: { family: "sankey", variant: "gradient", labels: "outside", data: [],
162
+ nodes: ["Retail", "Wholesale", "Licensing", "Services", "Pipeline", "Expansion", "Tooling", "Support", "Reserve"].map((name) => ({ name })),
163
+ links: [[0, 4, 31480], [1, 4, 46220], [2, 4, 14960], [3, 4, 28340], [4, 5, 52640], [4, 6, 9180], [4, 7, 12470], [4, 8, 46710]]
164
+ .map(([source, target, value]) => ({ source, target, value })) },
165
+ },
166
+ } satisfies Record<string, ChartBlockDefinition>
167
+
168
+ export type ChartBlockId = keyof typeof chartBlockDefinitions
169
+
170
+ export const chartBlocks = (Object.entries(chartBlockDefinitions) as [ChartBlockId, ChartBlockDefinition][])
171
+ .map(([id, definition]) => ({ id, title: definition.title, description: definition.description, family: definition.spec.family }))
172
+
173
+ export function getChartBlockSpec(block: ChartBlockId, period = "All"): ChartSpec {
174
+ const definition: ChartBlockDefinition = chartBlockDefinitions[block]
175
+ const count = definition.periods?.find((entry) => entry.label === period)?.count ?? Infinity
176
+ return { ...definition.spec, data: definition.spec.data.slice(-count) }
177
+ }