@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,545 @@
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
+ normalizeChartValue,
20
+ } from "./chart-utils"
21
+ import { ChartFloatingTooltip, ChartTooltip } from "./chart-tooltip"
22
+ import type { ConcentricProgressCardVariantKey } from "./generated/variant-keys"
23
+ import { concentricProgressCardDefaultVariantKey } from "./generated/default-variant-keys"
24
+
25
+ export interface ConcentricProgressRing extends ChartDataPoint {
26
+ description?: React.ReactNode
27
+ detail?: React.ReactNode
28
+ }
29
+
30
+ export interface ConcentricProgressMetric {
31
+ label: React.ReactNode
32
+ value: React.ReactNode
33
+ description?: React.ReactNode
34
+ }
35
+
36
+ export interface ConcentricProgressCardProps
37
+ extends Omit<React.ComponentPropsWithoutRef<typeof Card>, "title"> {
38
+ rings: ConcentricProgressRing[]
39
+ title?: React.ReactNode
40
+ description?: React.ReactNode
41
+ value?: React.ReactNode
42
+ centerLabel?: React.ReactNode
43
+ delta?: React.ReactNode
44
+ deltaDescription?: React.ReactNode
45
+ metrics?: ConcentricProgressMetric[]
46
+ selectedIndex?: number
47
+ max?: number
48
+ caption?: React.ReactNode
49
+ variant?: ConcentricProgressCardVariantKey
50
+ thickness?: number
51
+ gap?: number
52
+ showLegend?: boolean
53
+ formatValue?: (value: number) => React.ReactNode
54
+ maxLabel?: React.ReactNode
55
+ onRingSelect?: (ring: ConcentricProgressRing, index: number) => void
56
+ }
57
+
58
+ type ConcentricProgressCardClassNames = {
59
+ card: string
60
+ header: string
61
+ content: string
62
+ chart: string
63
+ metric: string
64
+ title: string
65
+ }
66
+
67
+ const variantClasses: Record<ConcentricProgressCardVariantKey, ConcentricProgressCardClassNames> = {
68
+ compact: {
69
+ card: "rounded-md",
70
+ header: "p-4 pb-3",
71
+ content: "px-4 pb-4",
72
+ chart: "max-w-52",
73
+ metric: "px-3 py-2",
74
+ title: "text-sm",
75
+ },
76
+ default: {
77
+ card: "rounded-lg",
78
+ header: "p-5 pb-3",
79
+ content: "px-5 pb-5",
80
+ chart: "max-w-64",
81
+ metric: "px-3 py-2.5",
82
+ title: "text-base",
83
+ },
84
+ }
85
+
86
+ function getPositiveValue(value: number) {
87
+ return Number.isFinite(value) ? Math.max(value, 0) : 0
88
+ }
89
+
90
+ function getRingIndexAtPoint(
91
+ rect: DOMRect,
92
+ clientX: number,
93
+ clientY: number,
94
+ rings: ConcentricProgressRing[],
95
+ maxValue: number,
96
+ thickness: number,
97
+ gap: number
98
+ ) {
99
+ const radius = Math.min(rect.width, rect.height) / 2
100
+ const centerX = rect.left + rect.width / 2
101
+ const centerY = rect.top + rect.height / 2
102
+ const dx = clientX - centerX
103
+ const dy = clientY - centerY
104
+ const distance = Math.sqrt(dx * dx + dy * dy)
105
+ const step = thickness + gap
106
+ const hitSlop = Math.max(2, gap / 2)
107
+ const anglePercent =
108
+ ((((Math.atan2(dx, -dy) * 180) / Math.PI + 360) % 360) / 360) * 100
109
+ const angleSlop = 1.5
110
+
111
+ return rings.findIndex((ring, index) => {
112
+ const outerRadius = radius - index * step
113
+ const innerRadius = outerRadius - thickness
114
+ const activePercent = normalizeChartValue(
115
+ getPositiveValue(ring.value),
116
+ maxValue
117
+ )
118
+
119
+ return (
120
+ distance <= outerRadius + hitSlop &&
121
+ distance >= innerRadius - hitSlop &&
122
+ activePercent > 0 &&
123
+ anglePercent <= Math.min(100, activePercent + angleSlop)
124
+ )
125
+ })
126
+ }
127
+
128
+ function buildRingMask(ringThickness: number) {
129
+ const feather = 1
130
+ return `radial-gradient(circle closest-side, transparent calc(100% - ${ringThickness + feather}px), hsl(var(--palette-black) / 0.65) calc(100% - ${ringThickness + feather / 2}px), hsl(var(--palette-black)) calc(100% - ${ringThickness}px))`
131
+ }
132
+
133
+ const ConcentricProgressCard = React.forwardRef<
134
+ HTMLDivElement,
135
+ ConcentricProgressCardProps
136
+ >(
137
+ (
138
+ {
139
+ className,
140
+ rings,
141
+ title = "Concentric progress",
142
+ description,
143
+ value,
144
+ centerLabel,
145
+ delta,
146
+ deltaDescription,
147
+ metrics = [],
148
+ selectedIndex,
149
+ max,
150
+ caption,
151
+ variant = concentricProgressCardDefaultVariantKey,
152
+ thickness = variant === "compact" ? 12 : 14,
153
+ gap = variant === "compact" ? 4 : 5,
154
+ showLegend = true,
155
+ formatValue = defaultChartValueFormatter,
156
+ maxLabel = "Max",
157
+ onRingSelect,
158
+ ...props
159
+ },
160
+ ref
161
+ ) => {
162
+ const styles = variantClasses[variant]
163
+ const fallbackMaxValue = Math.max(...rings.map((ring) => getPositiveValue(ring.value)), 1)
164
+ const maxValue = Math.max(max ?? fallbackMaxValue, 1)
165
+ const step = thickness + gap
166
+ const centerInset = rings.length * step
167
+ const centerClearance = variant === "compact" ? 4 : 6
168
+ const initialActiveIndex = Math.min(
169
+ rings.length - 1,
170
+ Math.max(0, selectedIndex ?? 0)
171
+ )
172
+ const [activeIndex, setActiveIndex] = React.useState(initialActiveIndex)
173
+ const [tooltipOpen, setTooltipOpen] = React.useState(false)
174
+ const [tooltipPosition, setTooltipPosition] = React.useState({
175
+ x: 50,
176
+ y: 12,
177
+ })
178
+ const chartRef = React.useRef<HTMLDivElement | null>(null)
179
+ const touchTooltipStickyRef = React.useRef(false)
180
+ const activeRing = rings[activeIndex] ?? rings[0]
181
+ const selectedRing =
182
+ selectedIndex === undefined ? undefined : rings[selectedIndex]
183
+ const activePercent =
184
+ activeRing === undefined
185
+ ? 0
186
+ : normalizeChartValue(getPositiveValue(activeRing.value), maxValue)
187
+ const activePercentLabel = `${defaultChartValueFormatter(activePercent)}%`
188
+ const selectedPercent =
189
+ selectedRing === undefined
190
+ ? 0
191
+ : normalizeChartValue(getPositiveValue(selectedRing.value), maxValue)
192
+ const selectedHighlightInset =
193
+ selectedIndex === undefined ? 0 : selectedIndex * step + 2
194
+ const selectedHighlightThickness = Math.max(thickness - 4, 2)
195
+
196
+ React.useEffect(() => {
197
+ setActiveIndex(initialActiveIndex)
198
+ }, [initialActiveIndex])
199
+
200
+ const updateTooltipAtPoint = (
201
+ element: HTMLDivElement,
202
+ clientX: number,
203
+ clientY: number
204
+ ) => {
205
+ const rect = element.getBoundingClientRect()
206
+ const nextIndex = getRingIndexAtPoint(
207
+ rect,
208
+ clientX,
209
+ clientY,
210
+ rings,
211
+ maxValue,
212
+ thickness,
213
+ gap
214
+ )
215
+
216
+ if (nextIndex < 0) {
217
+ setTooltipOpen(false)
218
+ return
219
+ }
220
+
221
+ setActiveIndex(nextIndex)
222
+ setTooltipPosition({
223
+ x: clamp(((clientX - rect.left) / rect.width) * 100),
224
+ y: clamp(((clientY - rect.top) / rect.height) * 100),
225
+ })
226
+ setTooltipOpen(true)
227
+ }
228
+ const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
229
+ if (event.pointerType !== "touch") {
230
+ touchTooltipStickyRef.current = false
231
+ }
232
+ updateTooltipAtPoint(event.currentTarget, event.clientX, event.clientY)
233
+ }
234
+ const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
235
+ touchTooltipStickyRef.current = event.pointerType === "touch"
236
+ if (event.pointerType === "touch") {
237
+ event.preventDefault()
238
+ }
239
+ event.currentTarget.focus({ preventScroll: true })
240
+ updateTooltipAtPoint(event.currentTarget, event.clientX, event.clientY)
241
+ }
242
+ const handleTouchStart = (event: React.TouchEvent<HTMLDivElement>) => {
243
+ const touch = event.touches[0]
244
+ if (!touch) return
245
+ touchTooltipStickyRef.current = true
246
+ event.preventDefault()
247
+ event.currentTarget.focus({ preventScroll: true })
248
+ updateTooltipAtPoint(event.currentTarget, touch.clientX, touch.clientY)
249
+ }
250
+
251
+ const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
252
+ if (!onRingSelect) return
253
+
254
+ const rect = event.currentTarget.getBoundingClientRect()
255
+ const nextIndex = getRingIndexAtPoint(
256
+ rect,
257
+ event.clientX,
258
+ event.clientY,
259
+ rings,
260
+ maxValue,
261
+ thickness,
262
+ gap
263
+ )
264
+ const nextRing = rings[nextIndex]
265
+
266
+ if (nextRing) onRingSelect(nextRing, nextIndex)
267
+ }
268
+
269
+ return (
270
+ <Card
271
+ ref={ref}
272
+ className={cn("w-full min-w-0 overflow-hidden p-0", styles.card, className)}
273
+ {...props}
274
+ >
275
+ <CardHeader className={styles.header}>
276
+ <div className="flex min-w-0 items-start justify-between gap-3">
277
+ <div className="min-w-0 space-y-1">
278
+ <CardTitle className={cn("truncate", styles.title)}>
279
+ {title}
280
+ </CardTitle>
281
+ {description ? (
282
+ <CardDescription className="text-xs">
283
+ {description}
284
+ </CardDescription>
285
+ ) : null}
286
+ </div>
287
+ {delta !== undefined && delta !== null ? (
288
+ <div
289
+ className="shrink-0 text-right text-sm font-semibold text-success-strong tabular-nums"
290
+ title={chartLabelToString(
291
+ deltaDescription,
292
+ "Delta description"
293
+ )}
294
+ >
295
+ {delta}
296
+ </div>
297
+ ) : null}
298
+ </div>
299
+ </CardHeader>
300
+ <CardContent className={cn("space-y-4", styles.content)}>
301
+ {metrics.length > 0 ? (
302
+ <div className="grid min-w-0 gap-2 sm:grid-cols-3">
303
+ {metrics.map((metric, index) => (
304
+ <div
305
+ key={`${chartLabelToString(metric.label, "Metric")}-${index}`}
306
+ className={cn(
307
+ "min-w-0 rounded-md border bg-card",
308
+ styles.metric
309
+ )}
310
+ >
311
+ <div className="truncate text-xl font-semibold tracking-tight tabular-nums">
312
+ {metric.value}
313
+ </div>
314
+ <div className="mt-1 truncate text-xs text-muted-foreground">
315
+ {metric.label}
316
+ </div>
317
+ {metric.description ? (
318
+ <div className="mt-1 truncate text-xs text-muted-foreground">
319
+ {metric.description}
320
+ </div>
321
+ ) : null}
322
+ </div>
323
+ ))}
324
+ </div>
325
+ ) : null}
326
+
327
+ <div
328
+ className={cn(
329
+ "grid min-w-0 gap-4",
330
+ showLegend
331
+ ? "lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] lg:items-center"
332
+ : "justify-items-center"
333
+ )}
334
+ >
335
+ <div
336
+ className={cn(
337
+ "flex min-w-0 justify-center",
338
+ !showLegend && "w-full"
339
+ )}
340
+ >
341
+ <div
342
+ ref={chartRef}
343
+ className={cn(
344
+ "relative aspect-square w-full rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
345
+ onRingSelect && "cursor-pointer",
346
+ styles.chart
347
+ )}
348
+ role="img"
349
+ aria-label={
350
+ activeRing
351
+ ? `${chartLabelToString(activeRing.label)}: ${formatValue(activeRing.value)} (${activePercentLabel} / ${chartLabelToString(maxLabel, "Max")}: ${formatValue(maxValue)})`
352
+ : props["aria-label"]
353
+ }
354
+ tabIndex={0}
355
+ onFocus={() => {
356
+ setActiveIndex(initialActiveIndex)
357
+ setTooltipPosition({ x: 50, y: 12 })
358
+ setTooltipOpen(true)
359
+ }}
360
+ onBlur={() => {
361
+ touchTooltipStickyRef.current = false
362
+ setTooltipOpen(false)
363
+ }}
364
+ onPointerDown={handlePointerDown}
365
+ onPointerMove={handlePointerMove}
366
+ onTouchStart={handleTouchStart}
367
+ onPointerLeave={() => {
368
+ if (touchTooltipStickyRef.current) return
369
+ setTooltipOpen(false)
370
+ }}
371
+ onPointerCancel={() => {
372
+ touchTooltipStickyRef.current = false
373
+ setTooltipOpen(false)
374
+ }}
375
+ onClick={handleClick}
376
+ >
377
+ {rings.map((ring, index) => {
378
+ const inset = index * step
379
+ const ringValue = getPositiveValue(ring.value)
380
+ const percent = normalizeChartValue(ringValue, maxValue)
381
+ const color = getChartColor(ring.color, index)
382
+ const trackColor = "hsl(var(--muted))"
383
+ const background = `conic-gradient(${color} 0% ${percent}%, ${trackColor} ${percent}% 100%)`
384
+ const ringMask = buildRingMask(thickness)
385
+ const isSelected = selectedIndex === index
386
+
387
+ return (
388
+ <span
389
+ key={`${chartLabelToString(ring.label, "Ring")}-${index}`}
390
+ className={cn(
391
+ "pointer-events-none absolute rounded-full transition-opacity",
392
+ selectedIndex !== undefined &&
393
+ !isSelected &&
394
+ "opacity-45"
395
+ )}
396
+ style={{
397
+ inset,
398
+ background,
399
+ WebkitMaskImage: ringMask,
400
+ maskImage: ringMask,
401
+ zIndex: rings.length - index,
402
+ }}
403
+ aria-hidden="true"
404
+ />
405
+ )
406
+ })}
407
+ {selectedRing ? (
408
+ <span
409
+ className="pointer-events-none absolute rounded-full"
410
+ style={{
411
+ inset: selectedHighlightInset,
412
+ background: `conic-gradient(hsl(var(--foreground) / 0.22) 0% ${selectedPercent}%, transparent ${selectedPercent}% 100%)`,
413
+ WebkitMaskImage: buildRingMask(selectedHighlightThickness),
414
+ maskImage: buildRingMask(selectedHighlightThickness),
415
+ zIndex: rings.length + 1,
416
+ }}
417
+ aria-hidden="true"
418
+ />
419
+ ) : null}
420
+ {value !== undefined || centerLabel !== undefined ? (
421
+ <div
422
+ className="absolute flex flex-col items-center justify-center rounded-full px-3 text-center"
423
+ style={{
424
+ inset: Math.max(
425
+ centerInset - gap + centerClearance,
426
+ thickness
427
+ ),
428
+ zIndex: rings.length + 2,
429
+ }}
430
+ >
431
+ {value !== undefined ? (
432
+ <span
433
+ className={cn(
434
+ "whitespace-nowrap font-semibold tracking-tight tabular-nums leading-none",
435
+ variant === "compact" ? "text-lg" : "text-xl"
436
+ )}
437
+ >
438
+ {value}
439
+ </span>
440
+ ) : null}
441
+ {centerLabel !== undefined ? (
442
+ <span className="mt-1 max-w-full whitespace-nowrap text-[11px] leading-none text-muted-foreground">
443
+ {centerLabel}
444
+ </span>
445
+ ) : null}
446
+ </div>
447
+ ) : null}
448
+ <ChartFloatingTooltip
449
+ label={activeRing?.label}
450
+ value={
451
+ activeRing
452
+ ? formatValue(activeRing.value)
453
+ : undefined
454
+ }
455
+ description={
456
+ activeRing?.description ?? [
457
+ activePercentLabel,
458
+ " / ",
459
+ maxLabel,
460
+ ": ",
461
+ formatValue(maxValue),
462
+ ]
463
+ }
464
+ position={tooltipPosition}
465
+ open={tooltipOpen}
466
+ anchorRef={chartRef}
467
+ onOpenChange={setTooltipOpen}
468
+ />
469
+ </div>
470
+ </div>
471
+
472
+ {showLegend ? (
473
+ <div className="grid min-w-0 gap-2">
474
+ {rings.map((ring, index) => {
475
+ const percent = normalizeChartValue(
476
+ getPositiveValue(ring.value),
477
+ maxValue
478
+ )
479
+ const isSelected = selectedIndex === index
480
+
481
+ return (
482
+ <ChartTooltip
483
+ key={`${chartLabelToString(ring.label, "Ring")}-row-${index}`}
484
+ label={ring.label}
485
+ value={formatValue(ring.value)}
486
+ description={
487
+ ring.description ??
488
+ `${defaultChartValueFormatter(percent)}%`
489
+ }
490
+ >
491
+ <button
492
+ type="button"
493
+ className={cn(
494
+ "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",
495
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
496
+ onRingSelect
497
+ ? "cursor-pointer hover:bg-accent/10"
498
+ : "cursor-default",
499
+ isSelected &&
500
+ "border-foreground shadow-sm"
501
+ )}
502
+ onClick={() => onRingSelect?.(ring, index)}
503
+ >
504
+ <span
505
+ className="h-2.5 w-2.5 rounded-full"
506
+ style={{
507
+ backgroundColor: getChartColor(
508
+ ring.color,
509
+ index
510
+ ),
511
+ }}
512
+ aria-hidden="true"
513
+ />
514
+ <span className="min-w-0 truncate text-xs text-muted-foreground">
515
+ {ring.label}
516
+ </span>
517
+ <span className="text-sm font-semibold tabular-nums">
518
+ {formatValue(ring.value)}
519
+ </span>
520
+ {ring.detail ? (
521
+ <span className="col-span-3 truncate text-xs text-muted-foreground">
522
+ {ring.detail}
523
+ </span>
524
+ ) : null}
525
+ </button>
526
+ </ChartTooltip>
527
+ )
528
+ })}
529
+ </div>
530
+ ) : null}
531
+ </div>
532
+
533
+ {caption ? (
534
+ <div className="rounded-md border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
535
+ {caption}
536
+ </div>
537
+ ) : null}
538
+ </CardContent>
539
+ </Card>
540
+ )
541
+ }
542
+ )
543
+ ConcentricProgressCard.displayName = "ConcentricProgressCard"
544
+
545
+ export { ConcentricProgressCard }