@gunjo/ui 0.0.1-alpha.1 → 0.0.1-alpha.2

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 (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +90 -0
  3. package/README.md +52 -91
  4. package/package.json +47 -6
  5. package/src/components/display/Accordion.tsx +185 -0
  6. package/src/components/display/AccordionGroup.tsx +155 -0
  7. package/src/components/display/ActionDataTable.tsx +413 -0
  8. package/src/components/display/ActivityTimelineCard.tsx +483 -0
  9. package/src/components/display/AnalyticsCard.tsx +167 -0
  10. package/src/components/display/AssetCard.tsx +242 -0
  11. package/src/components/display/AssetGrid.tsx +164 -0
  12. package/src/components/display/Avatar.tsx +127 -0
  13. package/src/components/display/AvatarGroup.tsx +131 -0
  14. package/src/components/{atoms → display}/Badge.tsx +3 -3
  15. package/src/components/display/BarChart.tsx +247 -0
  16. package/src/components/{molecules → display}/Card.tsx +1 -1
  17. package/src/components/display/Carousel.tsx +593 -0
  18. package/src/components/display/ChartLegend.tsx +124 -0
  19. package/src/components/display/ChatMessage.tsx +382 -0
  20. package/src/components/display/ChoroplethMap.tsx +613 -0
  21. package/src/components/display/Code.tsx +42 -0
  22. package/src/components/display/CodeBlock.tsx +338 -0
  23. package/src/components/display/ColorSwatch.tsx +71 -0
  24. package/src/components/display/ConcentricProgressCard.tsx +545 -0
  25. package/src/components/display/DataTable.tsx +522 -0
  26. package/src/components/display/DistributionBar.tsx +102 -0
  27. package/src/components/display/DocNote.tsx +36 -0
  28. package/src/components/display/DonutChart.tsx +257 -0
  29. package/src/components/display/EmptyState.tsx +44 -0
  30. package/src/components/display/FileTree.tsx +180 -0
  31. package/src/components/display/GaugeChart.tsx +219 -0
  32. package/src/components/display/HeatmapChart.tsx +266 -0
  33. package/src/components/display/Icon.tsx +66 -0
  34. package/src/components/display/ImagePreview.tsx +140 -0
  35. package/src/components/{atoms → display}/Img.tsx +46 -12
  36. package/src/components/display/LabeledDonutCard.tsx +475 -0
  37. package/src/components/display/LineChart.tsx +464 -0
  38. package/src/components/{molecules → display}/List.tsx +20 -13
  39. package/src/components/display/MarkdownRenderer.tsx +157 -0
  40. package/src/components/display/MetadataList.tsx +81 -0
  41. package/src/components/display/MiniDistributionBarCard.tsx +314 -0
  42. package/src/components/display/PieChart.tsx +234 -0
  43. package/src/components/display/QuadrantMatrix.tsx +330 -0
  44. package/src/components/display/RadarChart.tsx +335 -0
  45. package/src/components/display/RadialBarChart.tsx +264 -0
  46. package/src/components/display/RetentionCohortCard.tsx +350 -0
  47. package/src/components/display/RibbonChart.tsx +618 -0
  48. package/src/components/display/SearchableAccordion.tsx +270 -0
  49. package/src/components/display/SegmentTimelineCard.tsx +452 -0
  50. package/src/components/display/SegmentedGaugeCard.tsx +607 -0
  51. package/src/components/display/Spacer.tsx +51 -0
  52. package/src/components/display/SparklineChart.tsx +394 -0
  53. package/src/components/display/StackedBarChart.tsx +393 -0
  54. package/src/components/display/Statistic.tsx +70 -0
  55. package/src/components/{molecules → display}/Table.tsx +22 -7
  56. package/src/components/display/Tag.tsx +80 -0
  57. package/src/components/display/TagEditor.tsx +141 -0
  58. package/src/components/display/Timeline.tsx +121 -0
  59. package/src/components/{atoms → display}/ToolPill.tsx +42 -18
  60. package/src/components/display/TreeView.tsx +226 -0
  61. package/src/components/display/chart-tooltip.tsx +423 -0
  62. package/src/components/display/chart-utils.ts +71 -0
  63. package/src/components/display/circular-chart-utils.ts +147 -0
  64. package/src/components/display/generated/default-variant-keys.ts +90 -0
  65. package/src/components/display/generated/variant-keys.ts +169 -0
  66. package/src/components/{atoms → feedback}/Alert.tsx +12 -5
  67. package/src/components/feedback/Banner.tsx +90 -0
  68. package/src/components/{molecules → feedback}/NotificationCenter.tsx +64 -31
  69. package/src/components/feedback/ProgressWidget.tsx +44 -0
  70. package/src/components/{atoms → feedback}/Spinner.tsx +2 -2
  71. package/src/components/{molecules → feedback}/StatusBar.tsx +4 -4
  72. package/src/components/feedback/StatusScreen.tsx +148 -0
  73. package/src/components/{molecules → feedback}/Stepper.tsx +10 -5
  74. package/src/components/feedback/Toast.tsx +108 -0
  75. package/src/components/feedback/ToastProvider.tsx +78 -0
  76. package/src/components/feedback/generated/default-variant-keys.ts +16 -0
  77. package/src/components/feedback/generated/variant-keys.ts +21 -0
  78. package/src/components/generated/component-manifest.ts +1568 -454
  79. package/src/components/generated/component-style-hints.ts +1958 -718
  80. package/src/components/{atoms → inputs}/ButtonVariants.ts +13 -3
  81. package/src/components/inputs/Calendar.tsx +212 -0
  82. package/src/components/inputs/ChatComposer.tsx +75 -0
  83. package/src/components/inputs/ChatInput.tsx +528 -0
  84. package/src/components/{atoms → inputs}/Checkbox.tsx +2 -2
  85. package/src/components/inputs/Combobox.tsx +175 -0
  86. package/src/components/inputs/CopyButton.tsx +187 -0
  87. package/src/components/inputs/DatePicker.tsx +519 -0
  88. package/src/components/inputs/DateRangePicker.tsx +878 -0
  89. package/src/components/inputs/EditableField.tsx +182 -0
  90. package/src/components/{organisms → inputs}/FileUploader.tsx +24 -9
  91. package/src/components/inputs/FilterButton.tsx +163 -0
  92. package/src/components/{molecules → inputs}/Form.tsx +20 -3
  93. package/src/components/{atoms → inputs}/Input.tsx +2 -0
  94. package/src/components/inputs/InputOTP.tsx +75 -0
  95. package/src/components/inputs/Mention.tsx +279 -0
  96. package/src/components/inputs/NumberInput.tsx +109 -0
  97. package/src/components/inputs/PasswordGroup.tsx +138 -0
  98. package/src/components/inputs/PasswordInput.tsx +74 -0
  99. package/src/components/inputs/PasswordRequirementList.tsx +96 -0
  100. package/src/components/inputs/PasswordStrengthMeter.tsx +93 -0
  101. package/src/components/inputs/PhoneInput.tsx +99 -0
  102. package/src/components/inputs/PostalCodeInput.tsx +98 -0
  103. package/src/components/inputs/RangeSlider.tsx +129 -0
  104. package/src/components/inputs/SearchInput.tsx +76 -0
  105. package/src/components/inputs/Select.tsx +39 -0
  106. package/src/components/{atoms → inputs}/Slider.tsx +18 -5
  107. package/src/components/{molecules → inputs}/SortButton.tsx +5 -2
  108. package/src/components/{atoms → inputs}/Switch.tsx +15 -4
  109. package/src/components/inputs/TagInput.tsx +114 -0
  110. package/src/components/{atoms → inputs}/Textarea.tsx +1 -0
  111. package/src/components/inputs/TimePicker.tsx +150 -0
  112. package/src/components/inputs/Toggle.tsx +48 -0
  113. package/src/components/{atoms → inputs}/ToggleGroup.tsx +2 -2
  114. package/src/components/inputs/TooltipButton.tsx +148 -0
  115. package/src/components/inputs/VoiceInputButton.tsx +317 -0
  116. package/src/components/inputs/calendar-holidays.ts +56 -0
  117. package/src/components/inputs/generated/default-variant-keys.ts +32 -0
  118. package/src/components/{atoms → inputs}/generated/variant-keys.ts +19 -27
  119. package/src/components/layout/AspectRatio.tsx +12 -0
  120. package/src/components/layout/AssetInspectorPanel.tsx +416 -0
  121. package/src/components/layout/Cluster.tsx +56 -0
  122. package/src/components/layout/CollapsiblePanelToggle.tsx +94 -0
  123. package/src/components/layout/Container.tsx +43 -0
  124. package/src/components/layout/DeviceFrame.tsx +227 -0
  125. package/src/components/layout/Grid.tsx +65 -0
  126. package/src/components/layout/HStack.tsx +73 -0
  127. package/src/components/{organisms → layout}/InspectorPanel.tsx +6 -5
  128. package/src/components/layout/MarqueeFrame.tsx +158 -0
  129. package/src/components/layout/Resizable.tsx +94 -0
  130. package/src/components/layout/ScrollArea.tsx +71 -0
  131. package/src/components/{organisms → layout}/SpatialCanvas.tsx +12 -7
  132. package/src/components/layout/VStack.tsx +69 -0
  133. package/src/components/layout/generated/default-variant-keys.ts +16 -0
  134. package/src/components/layout/generated/variant-keys.ts +21 -0
  135. package/src/components/{molecules → navigation}/Breadcrumb.tsx +5 -4
  136. package/src/components/navigation/Command.tsx +266 -0
  137. package/src/components/navigation/CommandPalette.tsx +83 -0
  138. package/src/components/navigation/DocumentPager.tsx +171 -0
  139. package/src/components/navigation/Footer.tsx +88 -0
  140. package/src/components/navigation/Header.tsx +80 -0
  141. package/src/components/{molecules → navigation}/Menubar.tsx +45 -12
  142. package/src/components/navigation/NavigationMenu.tsx +128 -0
  143. package/src/components/navigation/PageAside.tsx +84 -0
  144. package/src/components/{molecules → navigation}/Pagination.tsx +60 -7
  145. package/src/components/{organisms → navigation}/RightRail.tsx +1 -1
  146. package/src/components/navigation/Sidebar.tsx +223 -0
  147. package/src/components/navigation/SidebarItem.tsx +160 -0
  148. package/src/components/{molecules → navigation}/Tabs.tsx +2 -2
  149. package/src/components/navigation/TextLink.tsx +71 -0
  150. package/src/components/navigation/generated/default-variant-keys.ts +12 -0
  151. package/src/components/navigation/generated/variant-keys.ts +13 -0
  152. package/src/components/overlay/AIChatInput.tsx +5 -0
  153. package/src/components/overlay/AIChatMessage.tsx +6 -0
  154. package/src/components/overlay/AlertDialog.tsx +145 -0
  155. package/src/components/overlay/ChatPanel.tsx +180 -0
  156. package/src/components/{molecules → overlay}/ContextMenu.tsx +65 -29
  157. package/src/components/{molecules → overlay}/Dialog.tsx +21 -13
  158. package/src/components/overlay/Drawer.tsx +131 -0
  159. package/src/components/{molecules → overlay}/DropdownMenu.tsx +52 -17
  160. package/src/components/overlay/FloatingPanel.tsx +90 -0
  161. package/src/components/overlay/HoverCard.tsx +36 -0
  162. package/src/components/overlay/MediaLightbox.tsx +403 -0
  163. package/src/components/overlay/MediaPickerDialog.tsx +198 -0
  164. package/src/components/overlay/Modal.tsx +103 -0
  165. package/src/components/overlay/OnboardingFlow.tsx +172 -0
  166. package/src/components/overlay/Popover.tsx +36 -0
  167. package/src/components/overlay/ShareModal.tsx +324 -0
  168. package/src/components/{molecules → overlay}/Sheet.tsx +76 -19
  169. package/src/components/overlay/Tooltip.tsx +130 -0
  170. package/src/components/overlay/generated/default-variant-keys.ts +14 -0
  171. package/src/components/overlay/generated/variant-keys.ts +17 -0
  172. package/src/components/patterns/BlogTemplate.tsx +46 -0
  173. package/src/components/{templates → patterns}/DashboardTemplate.tsx +2 -2
  174. package/src/components/patterns/DocsTemplate.tsx +41 -0
  175. package/src/components/{templates → patterns}/MediaLibraryTemplate.tsx +1 -1
  176. package/src/components/patterns/OnboardingTemplate.tsx +32 -0
  177. package/src/components/patterns/PricingTemplate.tsx +106 -0
  178. package/src/globals.css +173 -22
  179. package/src/index.ts +177 -76
  180. package/tailwind-theme-extend.cjs +48 -3
  181. package/design/atoms-metadata.json +0 -82
  182. package/design/molecules-metadata.json +0 -130
  183. package/design/organisms-metadata.json +0 -38
  184. package/design/templates-metadata.json +0 -38
  185. package/src/components/atoms/Avatar.tsx +0 -57
  186. package/src/components/atoms/Select.tsx +0 -28
  187. package/src/components/atoms/generated/default-variant-keys.ts +0 -36
  188. package/src/components/molecules/AIChatInput.tsx +0 -140
  189. package/src/components/molecules/AIChatMessage.tsx +0 -109
  190. package/src/components/molecules/Accordion.tsx +0 -99
  191. package/src/components/molecules/Calendar.tsx +0 -60
  192. package/src/components/molecules/Carousel.tsx +0 -261
  193. package/src/components/molecules/Command.tsx +0 -152
  194. package/src/components/molecules/FilterButton.tsx +0 -133
  195. package/src/components/molecules/HoverCard.tsx +0 -29
  196. package/src/components/molecules/Modal.tsx +0 -66
  197. package/src/components/molecules/Popover.tsx +0 -31
  198. package/src/components/molecules/ProgressWidget.tsx +0 -40
  199. package/src/components/molecules/Resizable.tsx +0 -47
  200. package/src/components/molecules/ScrollArea.tsx +0 -48
  201. package/src/components/molecules/SidebarItem.tsx +0 -134
  202. package/src/components/molecules/Toast.tsx +0 -57
  203. package/src/components/molecules/Tooltip.tsx +0 -30
  204. package/src/components/molecules/generated/default-variant-keys.ts +0 -22
  205. package/src/components/molecules/generated/variant-keys.ts +0 -33
  206. package/src/components/organisms/CommandPalette.tsx +0 -58
  207. package/src/components/organisms/FloatingPanel.tsx +0 -46
  208. package/src/components/organisms/ShareModal.tsx +0 -182
  209. package/src/components/organisms/ToastProvider.tsx +0 -49
  210. /package/src/components/{atoms → display}/Kbd.tsx +0 -0
  211. /package/src/components/{atoms → display}/Separator.tsx +0 -0
  212. /package/src/components/{atoms → display}/Skeleton.tsx +0 -0
  213. /package/src/components/{atoms → feedback}/Progress.tsx +0 -0
  214. /package/src/components/{atoms → inputs}/Button.tsx +0 -0
  215. /package/src/components/{atoms → inputs}/Label.tsx +0 -0
  216. /package/src/components/{atoms → inputs}/RadioGroup.tsx +0 -0
  217. /package/src/components/{organisms → navigation}/AppRail.tsx +0 -0
  218. /package/src/components/{templates → patterns}/AuthTemplate.tsx +0 -0
  219. /package/src/components/{templates → patterns}/BannalyzeTemplate.tsx +0 -0
  220. /package/src/components/{templates → patterns}/ChatTemplate.tsx +0 -0
  221. /package/src/components/{templates → patterns}/EditorTemplate.tsx +0 -0
  222. /package/src/components/{templates → patterns}/KanbanTemplate.tsx +0 -0
  223. /package/src/components/{templates → patterns}/LandingTemplate.tsx +0 -0
  224. /package/src/components/{templates → patterns}/SettingsTemplate.tsx +0 -0
@@ -0,0 +1,452 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "./Card"
13
+ import type { ChartDataPoint } from "./chart-utils"
14
+ import {
15
+ chartLabelToString,
16
+ clamp,
17
+ defaultChartValueFormatter,
18
+ getChartColor,
19
+ } from "./chart-utils"
20
+ import { ChartTooltip } from "./chart-tooltip"
21
+ import type { SegmentTimelineCardVariantKey } from "./generated/variant-keys"
22
+ import { segmentTimelineCardDefaultVariantKey } from "./generated/default-variant-keys"
23
+
24
+ export interface SegmentTimelineMetric {
25
+ label: React.ReactNode
26
+ value: React.ReactNode
27
+ description?: React.ReactNode
28
+ }
29
+
30
+ export interface SegmentTimelineSegment extends ChartDataPoint {
31
+ start: number
32
+ end: number
33
+ description?: React.ReactNode
34
+ }
35
+
36
+ export interface SegmentTimelineMarker {
37
+ value: number
38
+ label?: React.ReactNode
39
+ description?: React.ReactNode
40
+ }
41
+
42
+ export interface SegmentTimelineCardProps
43
+ extends Omit<React.ComponentPropsWithoutRef<typeof Card>, "title"> {
44
+ segments: SegmentTimelineSegment[]
45
+ metrics?: SegmentTimelineMetric[]
46
+ title?: React.ReactNode
47
+ description?: React.ReactNode
48
+ delta?: React.ReactNode
49
+ deltaDescription?: React.ReactNode
50
+ caption?: React.ReactNode
51
+ variant?: SegmentTimelineCardVariantKey
52
+ selectedIndex?: number
53
+ min?: number
54
+ max?: number
55
+ startLabel?: React.ReactNode
56
+ endLabel?: React.ReactNode
57
+ markers?: SegmentTimelineMarker[]
58
+ showLegend?: boolean
59
+ formatValue?: (value: number) => React.ReactNode
60
+ rangeLabel?: React.ReactNode
61
+ onSegmentSelect?: (segment: SegmentTimelineSegment, index: number) => void
62
+ }
63
+
64
+ type SegmentTimelineCardClassNames = {
65
+ card: string
66
+ header: string
67
+ content: string
68
+ metric: string
69
+ timeline: string
70
+ title: string
71
+ }
72
+
73
+ const variantClasses: Record<SegmentTimelineCardVariantKey, SegmentTimelineCardClassNames> = {
74
+ compact: {
75
+ card: "rounded-md",
76
+ header: "p-4 pb-3",
77
+ content: "px-4 pb-4",
78
+ metric: "px-3 py-2",
79
+ timeline: "h-20",
80
+ title: "text-sm",
81
+ },
82
+ default: {
83
+ card: "rounded-lg",
84
+ header: "p-5 pb-3",
85
+ content: "px-5 pb-5",
86
+ metric: "px-3 py-2.5",
87
+ timeline: "h-24",
88
+ title: "text-base",
89
+ },
90
+ }
91
+
92
+ function getFiniteValue(value: number, fallback = 0) {
93
+ return Number.isFinite(value) ? value : fallback
94
+ }
95
+
96
+ function getTimelineBounds(
97
+ segments: SegmentTimelineSegment[],
98
+ min?: number,
99
+ max?: number
100
+ ) {
101
+ const segmentStarts = segments.map((segment) => getFiniteValue(segment.start))
102
+ const segmentEnds = segments.map((segment) => getFiniteValue(segment.end))
103
+ const resolvedMin = min ?? Math.min(...segmentStarts, 0)
104
+ const resolvedMax = Math.max(max ?? Math.max(...segmentEnds, 1), resolvedMin + 1)
105
+
106
+ return {
107
+ min: resolvedMin,
108
+ max: resolvedMax,
109
+ range: resolvedMax - resolvedMin,
110
+ }
111
+ }
112
+
113
+ function getSegmentPosition(
114
+ segment: SegmentTimelineSegment,
115
+ min: number,
116
+ range: number
117
+ ) {
118
+ const start = clamp(((getFiniteValue(segment.start) - min) / range) * 100)
119
+ const end = clamp(((getFiniteValue(segment.end) - min) / range) * 100)
120
+ const left = Math.min(start, end)
121
+ const width = Math.max(Math.abs(end - start), 0)
122
+
123
+ return { left, width }
124
+ }
125
+
126
+ function getMarkerPosition(marker: SegmentTimelineMarker, min: number, range: number) {
127
+ return clamp(((getFiniteValue(marker.value) - min) / range) * 100)
128
+ }
129
+
130
+ function getSegmentDuration(segment: SegmentTimelineSegment) {
131
+ return Math.max(
132
+ getFiniteValue(segment.end) - getFiniteValue(segment.start),
133
+ 0
134
+ )
135
+ }
136
+
137
+ function getSegmentLegendItems(segments: SegmentTimelineSegment[]) {
138
+ const items: Array<{
139
+ label: React.ReactNode
140
+ labelString: string
141
+ duration: number
142
+ color?: SegmentTimelineSegment["color"]
143
+ description?: React.ReactNode
144
+ firstIndex: number
145
+ }> = []
146
+ const itemIndexes = new Map<string, number>()
147
+
148
+ segments.forEach((segment, index) => {
149
+ const labelString = chartLabelToString(segment.label, `Segment ${index + 1}`)
150
+ const existingIndex = itemIndexes.get(labelString)
151
+ const duration = getSegmentDuration(segment)
152
+
153
+ if (existingIndex === undefined) {
154
+ itemIndexes.set(labelString, items.length)
155
+ items.push({
156
+ label: segment.label,
157
+ labelString,
158
+ duration,
159
+ color: segment.color,
160
+ description: segment.description,
161
+ firstIndex: index,
162
+ })
163
+ return
164
+ }
165
+
166
+ items[existingIndex].duration += duration
167
+ })
168
+
169
+ return items
170
+ }
171
+
172
+ const SegmentTimelineCard = React.forwardRef<
173
+ HTMLDivElement,
174
+ SegmentTimelineCardProps
175
+ >(
176
+ (
177
+ {
178
+ className,
179
+ segments,
180
+ metrics = [],
181
+ title = "Segment timeline",
182
+ description,
183
+ delta,
184
+ deltaDescription,
185
+ caption,
186
+ variant = segmentTimelineCardDefaultVariantKey,
187
+ selectedIndex,
188
+ min,
189
+ max,
190
+ startLabel,
191
+ endLabel,
192
+ markers = [],
193
+ showLegend = true,
194
+ formatValue = defaultChartValueFormatter,
195
+ rangeLabel = "Range",
196
+ onSegmentSelect,
197
+ ...props
198
+ },
199
+ ref
200
+ ) => {
201
+ const styles = variantClasses[variant]
202
+ const bounds = getTimelineBounds(segments, min, max)
203
+ const selectedSegmentLabel =
204
+ selectedIndex === undefined
205
+ ? undefined
206
+ : segments[selectedIndex]
207
+ ? chartLabelToString(segments[selectedIndex].label, "Segment")
208
+ : undefined
209
+ const legendItems = getSegmentLegendItems(segments)
210
+
211
+ return (
212
+ <Card
213
+ ref={ref}
214
+ className={cn("w-full min-w-0 overflow-hidden p-0", styles.card, className)}
215
+ {...props}
216
+ >
217
+ <CardHeader className={styles.header}>
218
+ <div className="flex min-w-0 items-start justify-between gap-3">
219
+ <div className="min-w-0 space-y-1">
220
+ <CardTitle className={cn("truncate", styles.title)}>
221
+ {title}
222
+ </CardTitle>
223
+ {description ? (
224
+ <CardDescription className="text-xs">
225
+ {description}
226
+ </CardDescription>
227
+ ) : null}
228
+ </div>
229
+ {delta !== undefined && delta !== null ? (
230
+ <div
231
+ className="shrink-0 text-right text-sm font-semibold text-success-strong tabular-nums"
232
+ title={chartLabelToString(
233
+ deltaDescription,
234
+ "Delta description"
235
+ )}
236
+ >
237
+ {delta}
238
+ </div>
239
+ ) : null}
240
+ </div>
241
+ </CardHeader>
242
+ <CardContent className={cn("space-y-4", styles.content)}>
243
+ {metrics.length > 0 ? (
244
+ <div className="grid min-w-0 gap-2 sm:grid-cols-3">
245
+ {metrics.map((metric, index) => {
246
+ const metricLabel = chartLabelToString(metric.label, "Metric")
247
+
248
+ return (
249
+ <ChartTooltip
250
+ key={`${metricLabel}-${index}`}
251
+ label={metric.label}
252
+ value={metric.value}
253
+ description={metric.description}
254
+ >
255
+ <div
256
+ className={cn(
257
+ "min-w-0 rounded-md border bg-card focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
258
+ styles.metric
259
+ )}
260
+ tabIndex={0}
261
+ role="group"
262
+ aria-label={`${metricLabel}: ${chartLabelToString(metric.value, "")}`}
263
+ >
264
+ <div className="truncate text-xl font-semibold tracking-tight tabular-nums">
265
+ {metric.value}
266
+ </div>
267
+ <div className="mt-1 truncate text-xs text-muted-foreground">
268
+ {metric.label}
269
+ </div>
270
+ {metric.description ? (
271
+ <div className="mt-1 truncate text-xs text-muted-foreground">
272
+ {metric.description}
273
+ </div>
274
+ ) : null}
275
+ </div>
276
+ </ChartTooltip>
277
+ )
278
+ })}
279
+ </div>
280
+ ) : null}
281
+
282
+ <div className="min-w-0 overflow-x-auto overflow-y-hidden pb-1">
283
+ <div className="min-w-[36rem] space-y-2 sm:min-w-0">
284
+ <div
285
+ className={cn(
286
+ "relative min-w-0 rounded-lg border bg-muted/20 px-3",
287
+ styles.timeline
288
+ )}
289
+ >
290
+ <div className="absolute inset-x-3 top-1/2 h-9 -translate-y-1/2 rounded-md bg-muted/50" />
291
+ {segments.map((segment, index) => {
292
+ const position = getSegmentPosition(
293
+ segment,
294
+ bounds.min,
295
+ bounds.range
296
+ )
297
+ const segmentLabel = chartLabelToString(
298
+ segment.label,
299
+ `Segment ${index + 1}`
300
+ )
301
+ const isSelected =
302
+ selectedSegmentLabel === undefined
303
+ ? selectedIndex === index
304
+ : selectedSegmentLabel === segmentLabel
305
+ const duration = getSegmentDuration(segment)
306
+
307
+ return (
308
+ <ChartTooltip
309
+ key={`${chartLabelToString(segment.label, "Segment")}-${index}`}
310
+ label={segment.label}
311
+ value={formatValue(duration)}
312
+ description={
313
+ segment.description ?? [
314
+ rangeLabel,
315
+ ": ",
316
+ formatValue(segment.start),
317
+ " - ",
318
+ formatValue(segment.end),
319
+ ]
320
+ }
321
+ >
322
+ <button
323
+ type="button"
324
+ className={cn(
325
+ "absolute top-1/2 h-9 -translate-y-1/2 rounded-md transition-[opacity,box-shadow,transform] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
326
+ onSegmentSelect
327
+ ? "cursor-pointer hover:scale-y-110"
328
+ : "cursor-default",
329
+ selectedIndex !== undefined &&
330
+ !isSelected &&
331
+ "opacity-45",
332
+ isSelected &&
333
+ "shadow-[0_0_0_2px_hsl(var(--foreground))]"
334
+ )}
335
+ style={{
336
+ left: `calc(${position.left}% + 0.75rem)`,
337
+ width: `max(calc(${position.width}% - 1.5rem), 0.625rem)`,
338
+ backgroundColor: getChartColor(
339
+ segment.color,
340
+ index
341
+ ),
342
+ }}
343
+ onClick={() => onSegmentSelect?.(segment, index)}
344
+ aria-label={`${chartLabelToString(segment.label, "Segment")}: ${formatValue(duration)}`}
345
+ />
346
+ </ChartTooltip>
347
+ )
348
+ })}
349
+ {markers.map((marker, index) => {
350
+ const left = getMarkerPosition(
351
+ marker,
352
+ bounds.min,
353
+ bounds.range
354
+ )
355
+
356
+ return (
357
+ <ChartTooltip
358
+ key={`${chartLabelToString(marker.label, "Marker")}-${index}`}
359
+ label={marker.label}
360
+ value={formatValue(marker.value)}
361
+ description={marker.description}
362
+ >
363
+ <span
364
+ className="absolute top-3 h-[calc(100%-1.5rem)] w-px cursor-default border-l border-dashed border-foreground/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
365
+ style={{
366
+ left: `calc(${left}% + 0.75rem)`,
367
+ }}
368
+ tabIndex={0}
369
+ role="img"
370
+ aria-label={`${chartLabelToString(marker.label, "Marker")}: ${formatValue(marker.value)}`}
371
+ />
372
+ </ChartTooltip>
373
+ )
374
+ })}
375
+ </div>
376
+ {(startLabel || endLabel) ? (
377
+ <div className="flex min-w-0 items-center justify-between gap-3 text-xs text-muted-foreground">
378
+ <span className="min-w-0 flex-1 truncate">
379
+ {startLabel}
380
+ </span>
381
+ <span className="min-w-0 flex-1 truncate text-right">
382
+ {endLabel}
383
+ </span>
384
+ </div>
385
+ ) : null}
386
+ </div>
387
+ </div>
388
+
389
+ {showLegend ? (
390
+ <div className="grid min-w-0 gap-2 sm:grid-cols-2">
391
+ {legendItems.map((item) => {
392
+ const segment = segments[item.firstIndex]
393
+ const isSelected =
394
+ selectedSegmentLabel === item.labelString
395
+
396
+ return (
397
+ <ChartTooltip
398
+ key={`${item.labelString}-row`}
399
+ label={item.label}
400
+ value={formatValue(item.duration)}
401
+ description={item.description}
402
+ >
403
+ <button
404
+ type="button"
405
+ className={cn(
406
+ "grid min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-2 rounded-md border bg-card px-3 py-2 text-left transition-colors",
407
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
408
+ onSegmentSelect
409
+ ? "cursor-pointer hover:bg-accent/10"
410
+ : "cursor-default",
411
+ isSelected && "border-foreground shadow-sm"
412
+ )}
413
+ onClick={() =>
414
+ onSegmentSelect?.(segment, item.firstIndex)
415
+ }
416
+ >
417
+ <span
418
+ className="h-2.5 w-2.5 rounded-full"
419
+ style={{
420
+ backgroundColor: getChartColor(
421
+ item.color,
422
+ item.firstIndex
423
+ ),
424
+ }}
425
+ aria-hidden="true"
426
+ />
427
+ <span className="min-w-0 truncate text-xs text-muted-foreground">
428
+ {item.label}
429
+ </span>
430
+ <span className="text-sm font-semibold tabular-nums">
431
+ {formatValue(item.duration)}
432
+ </span>
433
+ </button>
434
+ </ChartTooltip>
435
+ )
436
+ })}
437
+ </div>
438
+ ) : null}
439
+
440
+ {caption ? (
441
+ <div className="rounded-md border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
442
+ {caption}
443
+ </div>
444
+ ) : null}
445
+ </CardContent>
446
+ </Card>
447
+ )
448
+ }
449
+ )
450
+ SegmentTimelineCard.displayName = "SegmentTimelineCard"
451
+
452
+ export { SegmentTimelineCard }