@gunjo/ui 0.0.1-alpha.0 → 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,483 @@
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 { ChartColor } from "./chart-utils"
14
+ import {
15
+ chartLabelToString,
16
+ defaultChartValueFormatter,
17
+ getChartColor,
18
+ normalizeChartValue,
19
+ } from "./chart-utils"
20
+ import { ChartTooltip } from "./chart-tooltip"
21
+ import type { ActivityTimelineCardVariantKey } from "./generated/variant-keys"
22
+ import { activityTimelineCardDefaultVariantKey } from "./generated/default-variant-keys"
23
+
24
+ export interface ActivityTimelineMetric {
25
+ label: React.ReactNode
26
+ value: React.ReactNode
27
+ description?: React.ReactNode
28
+ tooltip?: React.ReactNode
29
+ }
30
+
31
+ export interface ActivityTimelineSlot {
32
+ label: React.ReactNode
33
+ value: number
34
+ color?: ChartColor
35
+ segments?: ActivityTimelineSegment[]
36
+ description?: React.ReactNode
37
+ }
38
+
39
+ export interface ActivityTimelineSegment {
40
+ label: React.ReactNode
41
+ value: number
42
+ color?: ChartColor
43
+ description?: React.ReactNode
44
+ }
45
+
46
+ export interface ActivityTimelineCardProps
47
+ extends Omit<React.ComponentPropsWithoutRef<typeof Card>, "title"> {
48
+ metrics: ActivityTimelineMetric[]
49
+ slots: ActivityTimelineSlot[]
50
+ segments: ActivityTimelineSegment[]
51
+ title?: React.ReactNode
52
+ description?: React.ReactNode
53
+ delta?: React.ReactNode
54
+ deltaDescription?: React.ReactNode
55
+ caption?: React.ReactNode
56
+ variant?: ActivityTimelineCardVariantKey
57
+ max?: number
58
+ selectedSlot?: number
59
+ showSlotValues?: boolean
60
+ formatValue?: (value: number) => React.ReactNode
61
+ totalLabel?: React.ReactNode
62
+ onSlotSelect?: (slot: ActivityTimelineSlot, index: number) => void
63
+ }
64
+
65
+ type ActivityTimelineCardClassNames = {
66
+ card: string
67
+ header: string
68
+ content: string
69
+ timeline: string
70
+ bar: string
71
+ title: string
72
+ }
73
+
74
+ const variantClasses: Record<ActivityTimelineCardVariantKey, ActivityTimelineCardClassNames> = {
75
+ compact: {
76
+ card: "rounded-md",
77
+ header: "p-4 pb-3",
78
+ content: "px-4 pb-4",
79
+ timeline: "h-28",
80
+ bar: "rounded-t",
81
+ title: "text-sm",
82
+ },
83
+ default: {
84
+ card: "rounded-lg",
85
+ header: "p-5 pb-3",
86
+ content: "px-5 pb-5",
87
+ timeline: "h-36",
88
+ bar: "rounded-t-md",
89
+ title: "text-base",
90
+ },
91
+ }
92
+
93
+ function getPositiveValue(value: number) {
94
+ return Number.isFinite(value) ? Math.max(value, 0) : 0
95
+ }
96
+
97
+ function getMaxValue(slots: ActivityTimelineSlot[], max?: number) {
98
+ return Math.max(max ?? 0, ...slots.map((slot) => getPositiveValue(slot.value)), 1)
99
+ }
100
+
101
+ function getSegmentTotal(segments: ActivityTimelineSegment[]) {
102
+ return Math.max(
103
+ segments.reduce((sum, segment) => sum + getPositiveValue(segment.value), 0),
104
+ 1
105
+ )
106
+ }
107
+
108
+ const ActivityTimelineCard = React.forwardRef<
109
+ HTMLDivElement,
110
+ ActivityTimelineCardProps
111
+ >(
112
+ (
113
+ {
114
+ className,
115
+ metrics,
116
+ slots,
117
+ segments,
118
+ title = "Activity timeline",
119
+ description,
120
+ delta,
121
+ deltaDescription,
122
+ caption,
123
+ variant = activityTimelineCardDefaultVariantKey,
124
+ max,
125
+ selectedSlot,
126
+ showSlotValues = false,
127
+ formatValue = defaultChartValueFormatter,
128
+ totalLabel = "Total",
129
+ onSlotSelect,
130
+ ...props
131
+ },
132
+ ref
133
+ ) => {
134
+ const styles = variantClasses[variant]
135
+ const maxValue = getMaxValue(slots, max)
136
+ const segmentTotal = getSegmentTotal(segments)
137
+ const canSelectSlots = typeof onSlotSelect === "function"
138
+ const [selectedSegmentIndex, setSelectedSegmentIndex] = React.useState<
139
+ number | null
140
+ >(null)
141
+ const [hoveredSegmentIndex, setHoveredSegmentIndex] = React.useState<
142
+ number | null
143
+ >(null)
144
+ const activeSegmentIndex = hoveredSegmentIndex ?? selectedSegmentIndex
145
+
146
+ return (
147
+ <Card
148
+ ref={ref}
149
+ className={cn("w-full min-w-0 overflow-hidden p-0", styles.card, className)}
150
+ {...props}
151
+ >
152
+ <CardHeader className={styles.header}>
153
+ <div className="flex min-w-0 items-start justify-between gap-3">
154
+ <div className="min-w-0 space-y-1">
155
+ <CardTitle className={cn("truncate", styles.title)}>
156
+ {title}
157
+ </CardTitle>
158
+ {description ? (
159
+ <CardDescription className="text-xs">
160
+ {description}
161
+ </CardDescription>
162
+ ) : null}
163
+ </div>
164
+ {delta !== undefined && delta !== null ? (
165
+ <div
166
+ className="shrink-0 text-right text-sm font-semibold text-success-strong tabular-nums"
167
+ title={chartLabelToString(
168
+ deltaDescription,
169
+ "Delta description"
170
+ )}
171
+ >
172
+ {delta}
173
+ </div>
174
+ ) : null}
175
+ </div>
176
+ </CardHeader>
177
+ <CardContent className={cn("space-y-4", styles.content)}>
178
+ <div className="grid min-w-0 grid-cols-3 gap-3">
179
+ {metrics.slice(0, 3).map((metric, index) => {
180
+ const tooltipDescription =
181
+ metric.tooltip ?? metric.description
182
+
183
+ return (
184
+ <ChartTooltip
185
+ key={`${chartLabelToString(metric.label, "Metric")}-${index}`}
186
+ label={metric.label}
187
+ value={metric.value}
188
+ description={tooltipDescription}
189
+ >
190
+ <div
191
+ className="min-w-0 rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
192
+ tabIndex={0}
193
+ >
194
+ <div className="truncate text-2xl font-semibold leading-none tabular-nums">
195
+ {metric.value}
196
+ </div>
197
+ <div className="mt-1 truncate text-xs text-muted-foreground">
198
+ {metric.label}
199
+ </div>
200
+ {metric.description ? (
201
+ <div className="mt-0.5 truncate text-xs text-muted-foreground">
202
+ {metric.description}
203
+ </div>
204
+ ) : null}
205
+ </div>
206
+ </ChartTooltip>
207
+ )
208
+ })}
209
+ </div>
210
+
211
+ <div className="min-w-0 space-y-2">
212
+ <div
213
+ className={cn(
214
+ "flex min-w-0 items-end gap-1 border-b border-dashed border-border/70",
215
+ styles.timeline
216
+ )}
217
+ >
218
+ {slots.map((slot, index) => {
219
+ const value = getPositiveValue(slot.value)
220
+ const percent = normalizeChartValue(value, maxValue)
221
+ const isSelected = selectedSlot === index
222
+ const color = getChartColor(slot.color, index)
223
+ const label = chartLabelToString(slot.label, "Time")
224
+ const slotSegments = slot.segments ?? []
225
+ const slotSegmentTotal = getSegmentTotal(slotSegments)
226
+ const hasSlotSegments = slotSegments.some(
227
+ (segment) => getPositiveValue(segment.value) > 0
228
+ )
229
+ const barContent = (
230
+ <span
231
+ className={cn(
232
+ "relative block w-full min-w-0 overflow-hidden bg-muted",
233
+ styles.bar,
234
+ canSelectSlots &&
235
+ "transition-opacity hover:opacity-85 group-focus-visible:ring-2 group-focus-visible:ring-ring group-focus-visible:ring-offset-1 group-focus-visible:ring-offset-background",
236
+ isSelected &&
237
+ "ring-2 ring-foreground ring-offset-1 ring-offset-background"
238
+ )}
239
+ style={{
240
+ height: `${Math.max(percent, 4)}%`,
241
+ backgroundColor: hasSlotSegments
242
+ ? undefined
243
+ : color,
244
+ }}
245
+ >
246
+ {hasSlotSegments ? (
247
+ <span
248
+ className="absolute inset-0 flex flex-col-reverse"
249
+ aria-hidden="true"
250
+ >
251
+ {slotSegments.map((segment, segmentIndex) => {
252
+ const segmentValue = getPositiveValue(
253
+ segment.value
254
+ )
255
+ const segmentPercent = normalizeChartValue(
256
+ segmentValue,
257
+ slotSegmentTotal
258
+ )
259
+
260
+ if (segmentValue <= 0) return null
261
+
262
+ return (
263
+ <span
264
+ key={`${chartLabelToString(segment.label, "Segment")}-${segmentIndex}`}
265
+ className="block min-h-px w-full transition-opacity duration-150 ease-out"
266
+ style={{
267
+ height: `${segmentPercent}%`,
268
+ backgroundColor: getChartColor(
269
+ segment.color,
270
+ segmentIndex
271
+ ),
272
+ opacity:
273
+ activeSegmentIndex ===
274
+ null ||
275
+ activeSegmentIndex ===
276
+ segmentIndex
277
+ ? 1
278
+ : 0.28,
279
+ boxShadow:
280
+ segmentIndex <
281
+ slotSegments.length - 1
282
+ ? "inset 0 1px 0 hsl(var(--background) / 0.75)"
283
+ : undefined,
284
+ }}
285
+ />
286
+ )
287
+ })}
288
+ </span>
289
+ ) : null}
290
+ {showSlotValues ? (
291
+ <span className="absolute inset-x-0 top-1 z-10 truncate px-0.5 text-center text-[10px] font-medium text-background">
292
+ {formatValue(value)}
293
+ </span>
294
+ ) : null}
295
+ </span>
296
+ )
297
+
298
+ return (
299
+ <ChartTooltip
300
+ key={`${label}-${index}`}
301
+ label={slot.label}
302
+ value={formatValue(value)}
303
+ description={slot.description}
304
+ >
305
+ {canSelectSlots ? (
306
+ <button
307
+ type="button"
308
+ className={cn(
309
+ "group flex h-full min-h-0 min-w-0 flex-1 appearance-none items-end border-0 bg-transparent p-0 text-left focus-visible:outline-none",
310
+ "cursor-pointer"
311
+ )}
312
+ aria-current={isSelected ? "true" : undefined}
313
+ aria-pressed={isSelected}
314
+ aria-label={`${label}: ${chartLabelToString(formatValue(value), "Value")}`}
315
+ onClick={() => onSlotSelect?.(slot, index)}
316
+ >
317
+ {barContent}
318
+ </button>
319
+ ) : (
320
+ <span
321
+ className="flex h-full min-h-0 min-w-0 flex-1 items-end focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
322
+ tabIndex={0}
323
+ aria-current={isSelected ? "true" : undefined}
324
+ aria-label={`${label}: ${chartLabelToString(formatValue(value), "Value")}`}
325
+ >
326
+ {barContent}
327
+ </span>
328
+ )}
329
+ </ChartTooltip>
330
+ )
331
+ })}
332
+ </div>
333
+ <div
334
+ className="grid min-w-0 gap-1"
335
+ style={{
336
+ gridTemplateColumns: `repeat(${slots.length}, minmax(0, 1fr))`,
337
+ }}
338
+ >
339
+ {slots.map((slot, index) => (
340
+ <span
341
+ key={`${chartLabelToString(slot.label, "Time")}-label-${index}`}
342
+ className="min-w-0 truncate text-center text-xs text-muted-foreground"
343
+ >
344
+ {slot.label}
345
+ </span>
346
+ ))}
347
+ </div>
348
+ </div>
349
+
350
+ <div className="space-y-3">
351
+ <div className="h-4 overflow-hidden rounded-full bg-muted">
352
+ <div className="flex h-full w-full">
353
+ {segments.map((segment, index) => {
354
+ const value = getPositiveValue(segment.value)
355
+ const percent = normalizeChartValue(value, segmentTotal)
356
+ const percentText = `${defaultChartValueFormatter(percent)}%`
357
+
358
+ return (
359
+ <ChartTooltip
360
+ key={`${chartLabelToString(segment.label, "Segment")}-${index}`}
361
+ label={segment.label}
362
+ value={percentText}
363
+ description={
364
+ segment.description ?? [
365
+ totalLabel,
366
+ ": ",
367
+ formatValue(value),
368
+ ]
369
+ }
370
+ >
371
+ <button
372
+ type="button"
373
+ className="block h-full min-w-px appearance-none border-0 p-0 transition-opacity duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
374
+ style={{
375
+ width: `${percent}%`,
376
+ backgroundColor: getChartColor(
377
+ segment.color,
378
+ index
379
+ ),
380
+ opacity:
381
+ activeSegmentIndex === null ||
382
+ activeSegmentIndex === index
383
+ ? 1
384
+ : 0.35,
385
+ boxShadow:
386
+ index < segments.length - 1
387
+ ? "inset -1px 0 0 hsl(var(--background) / 0.75)"
388
+ : undefined,
389
+ }}
390
+ onClick={() =>
391
+ setSelectedSegmentIndex((current) =>
392
+ current === index ? null : index
393
+ )
394
+ }
395
+ onPointerEnter={() =>
396
+ setHoveredSegmentIndex(index)
397
+ }
398
+ onPointerLeave={() =>
399
+ setHoveredSegmentIndex(null)
400
+ }
401
+ onFocus={() => setHoveredSegmentIndex(index)}
402
+ onBlur={() => setHoveredSegmentIndex(null)}
403
+ aria-pressed={selectedSegmentIndex === index}
404
+ aria-label={`${chartLabelToString(segment.label, "Segment")}: ${percentText}`}
405
+ />
406
+ </ChartTooltip>
407
+ )
408
+ })}
409
+ </div>
410
+ </div>
411
+ <div className="grid min-w-0 grid-cols-1 gap-2 sm:grid-cols-3">
412
+ {segments.map((segment, index) => {
413
+ const value = getPositiveValue(segment.value)
414
+ const percent = normalizeChartValue(value, segmentTotal)
415
+ const percentText = `${defaultChartValueFormatter(percent)}%`
416
+
417
+ return (
418
+ <ChartTooltip
419
+ key={`${chartLabelToString(segment.label, "Segment")}-stat-${index}`}
420
+ label={segment.label}
421
+ value={formatValue(value)}
422
+ description={`${percentText} / ${chartLabelToString(totalLabel, "Total")}`}
423
+ >
424
+ <button
425
+ type="button"
426
+ className={cn(
427
+ "w-full min-w-0 rounded-md p-1 text-left transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
428
+ activeSegmentIndex === index && "bg-muted/60"
429
+ )}
430
+ onClick={() =>
431
+ setSelectedSegmentIndex((current) =>
432
+ current === index ? null : index
433
+ )
434
+ }
435
+ onPointerEnter={() =>
436
+ setHoveredSegmentIndex(index)
437
+ }
438
+ onPointerLeave={() =>
439
+ setHoveredSegmentIndex(null)
440
+ }
441
+ onFocus={() => setHoveredSegmentIndex(index)}
442
+ onBlur={() => setHoveredSegmentIndex(null)}
443
+ aria-pressed={selectedSegmentIndex === index}
444
+ aria-label={`${chartLabelToString(segment.label, "Segment")}: ${chartLabelToString(formatValue(value), "Value")}`}
445
+ >
446
+ <span className="flex min-w-0 items-center gap-1.5">
447
+ <span
448
+ className="h-2 w-2 shrink-0 rounded-full"
449
+ style={{
450
+ backgroundColor: getChartColor(
451
+ segment.color,
452
+ index
453
+ ),
454
+ }}
455
+ aria-hidden="true"
456
+ />
457
+ <span className="truncate text-xs text-muted-foreground">
458
+ {segment.label}
459
+ </span>
460
+ </span>
461
+ <span className="mt-1 block truncate text-lg font-semibold leading-none tabular-nums">
462
+ {formatValue(value)}
463
+ </span>
464
+ </button>
465
+ </ChartTooltip>
466
+ )
467
+ })}
468
+ </div>
469
+ </div>
470
+
471
+ {caption ? (
472
+ <div className="rounded-md border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
473
+ {caption}
474
+ </div>
475
+ ) : null}
476
+ </CardContent>
477
+ </Card>
478
+ )
479
+ }
480
+ )
481
+ ActivityTimelineCard.displayName = "ActivityTimelineCard"
482
+
483
+ export { ActivityTimelineCard }
@@ -0,0 +1,167 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconArrowDown as ArrowDown, IconArrowUp as ArrowUp, IconMinus as Minus } from "@tabler/icons-react";
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import {
8
+ Card,
9
+ CardContent,
10
+ CardDescription,
11
+ CardFooter,
12
+ CardHeader,
13
+ CardTitle,
14
+ } from "./Card"
15
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip"
16
+ import type { AnalyticsCardVariantKey } from "./generated/variant-keys"
17
+ import { analyticsCardDefaultVariantKey } from "./generated/default-variant-keys"
18
+
19
+ export type AnalyticsCardTrend = "up" | "down" | "flat"
20
+
21
+ export interface AnalyticsCardProps
22
+ extends Omit<React.ComponentPropsWithoutRef<typeof Card>, "title"> {
23
+ title: React.ReactNode
24
+ description?: React.ReactNode
25
+ value?: React.ReactNode
26
+ delta?: React.ReactNode
27
+ deltaDescription?: React.ReactNode
28
+ trend?: AnalyticsCardTrend
29
+ variant?: AnalyticsCardVariantKey
30
+ action?: React.ReactNode
31
+ footer?: React.ReactNode
32
+ }
33
+
34
+ type AnalyticsCardClassNames = {
35
+ card: string
36
+ header: string
37
+ content: string
38
+ footer: string
39
+ }
40
+
41
+ const variantClasses: Record<AnalyticsCardVariantKey, AnalyticsCardClassNames> = {
42
+ default: {
43
+ card: "",
44
+ header: "",
45
+ content: "",
46
+ footer: "",
47
+ },
48
+ positive: {
49
+ card: "border-success-border bg-success-subtle",
50
+ header: "",
51
+ content: "",
52
+ footer: "",
53
+ },
54
+ riskIncrease: {
55
+ card: "border-destructive-border bg-destructive-subtle",
56
+ header: "",
57
+ content: "",
58
+ footer: "",
59
+ },
60
+ flatWithFooter: {
61
+ card: "bg-muted/20",
62
+ header: "",
63
+ content: "",
64
+ footer: "border-t pt-4 text-xs text-muted-foreground",
65
+ },
66
+ }
67
+
68
+ const trendClasses: Record<AnalyticsCardTrend, string> = {
69
+ up: "text-success-strong",
70
+ down: "text-destructive",
71
+ flat: "text-muted-foreground",
72
+ }
73
+
74
+ const trendIcons: Record<
75
+ AnalyticsCardTrend,
76
+ React.ComponentType<{ className?: string }>
77
+ > = {
78
+ up: ArrowUp,
79
+ down: ArrowDown,
80
+ flat: Minus,
81
+ }
82
+
83
+ const AnalyticsCard = React.forwardRef<HTMLDivElement, AnalyticsCardProps>(
84
+ (
85
+ {
86
+ className,
87
+ title,
88
+ description,
89
+ value,
90
+ delta,
91
+ deltaDescription,
92
+ trend = "flat",
93
+ variant = analyticsCardDefaultVariantKey,
94
+ action,
95
+ footer,
96
+ children,
97
+ ...props
98
+ },
99
+ ref
100
+ ) => {
101
+ const TrendIcon = trendIcons[trend]
102
+ const styles = variantClasses[variant]
103
+
104
+ return (
105
+ <Card ref={ref} className={cn("w-full min-w-0 p-0", styles.card, className)} {...props}>
106
+ <CardHeader className={cn("flex-row items-start justify-between gap-4 space-y-0", styles.header)}>
107
+ <div className="min-w-0 space-y-1">
108
+ <CardTitle className="text-sm font-medium leading-snug">
109
+ {title}
110
+ </CardTitle>
111
+ {description ? (
112
+ <CardDescription>{description}</CardDescription>
113
+ ) : null}
114
+ </div>
115
+ {action ? <div className="flex-shrink-0">{action}</div> : null}
116
+ </CardHeader>
117
+ <CardContent className={cn("space-y-4", styles.content)}>
118
+ {value !== undefined || delta !== undefined ? (
119
+ <div className="flex min-w-0 flex-wrap items-start justify-between gap-x-3 gap-y-1">
120
+ {value !== undefined ? (
121
+ <div className="min-w-0 text-2xl font-bold leading-tight tracking-tight [overflow-wrap:anywhere]">
122
+ {value}
123
+ </div>
124
+ ) : null}
125
+ {delta !== undefined ? (
126
+ deltaDescription !== undefined ? (
127
+ <Tooltip>
128
+ <TooltipTrigger asChild>
129
+ <button
130
+ type="button"
131
+ className={cn(
132
+ "mt-1 inline-flex shrink-0 items-center gap-1 rounded-sm text-xs font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
133
+ trendClasses[trend]
134
+ )}
135
+ >
136
+ <TrendIcon className="h-3 w-3" />
137
+ <span>{delta}</span>
138
+ </button>
139
+ </TooltipTrigger>
140
+ <TooltipContent className="max-w-56 text-left text-xs">
141
+ {deltaDescription}
142
+ </TooltipContent>
143
+ </Tooltip>
144
+ ) : (
145
+ <div
146
+ className={cn(
147
+ "mt-1 inline-flex shrink-0 items-center gap-1 text-xs font-medium",
148
+ trendClasses[trend]
149
+ )}
150
+ >
151
+ <TrendIcon className="h-3 w-3" />
152
+ <span>{delta}</span>
153
+ </div>
154
+ )
155
+ ) : null}
156
+ </div>
157
+ ) : null}
158
+ {children}
159
+ </CardContent>
160
+ {footer ? <CardFooter className={styles.footer}>{footer}</CardFooter> : null}
161
+ </Card>
162
+ )
163
+ }
164
+ )
165
+ AnalyticsCard.displayName = "AnalyticsCard"
166
+
167
+ export { AnalyticsCard }