@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,335 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+ import type { ChartColor, ChartDataPoint } from "./chart-utils"
7
+ import {
8
+ chartLabelToString,
9
+ defaultChartValueFormatter,
10
+ getChartColor,
11
+ normalizeChartValue,
12
+ } from "./chart-utils"
13
+ import { ChartTooltip } from "./chart-tooltip"
14
+ import type { RadarChartVariantKey } from "./generated/variant-keys"
15
+ import { radarChartDefaultVariantKey } from "./generated/default-variant-keys"
16
+
17
+ interface RadarPoint {
18
+ index: number
19
+ label: React.ReactNode
20
+ value: number
21
+ x: number
22
+ y: number
23
+ }
24
+
25
+ export interface RadarChartSeries {
26
+ id?: string
27
+ label?: React.ReactNode
28
+ data: ChartDataPoint[]
29
+ color?: ChartColor
30
+ fillOpacity?: number
31
+ }
32
+
33
+ export interface RadarChartProps
34
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
35
+ data: ChartDataPoint[]
36
+ series?: RadarChartSeries[]
37
+ variant?: RadarChartVariantKey
38
+ max?: number
39
+ color?: ChartColor
40
+ showGrid?: boolean
41
+ showLabels?: boolean
42
+ showDots?: boolean
43
+ fillOpacity?: number
44
+ formatValue?: (value: number) => React.ReactNode
45
+ maxLabel?: React.ReactNode
46
+ }
47
+
48
+ const radarChartVariantClasses: Record<RadarChartVariantKey, string> = {
49
+ compact: "h-[208px] w-full p-0",
50
+ default: "h-[256px] w-full p-0",
51
+ }
52
+
53
+ const chartCenter = 50
54
+ const chartRadius = 34
55
+ const labelRadius = 45
56
+ const minimumAxisCount = 3
57
+
58
+ function getRadarPosition(index: number, count: number, ratio = 1, radius = chartRadius) {
59
+ const angle = -90 + (index / count) * 360
60
+ const radians = (angle * Math.PI) / 180
61
+ return {
62
+ angle,
63
+ x: chartCenter + Math.cos(radians) * radius * ratio,
64
+ y: chartCenter + Math.sin(radians) * radius * ratio,
65
+ }
66
+ }
67
+
68
+ function normalizeRadarData(data: ChartDataPoint[]) {
69
+ return data.filter((item) => Number.isFinite(item.value))
70
+ }
71
+
72
+ function buildRadarPoints(data: ChartDataPoint[], maxValue: number, axisCount?: number) {
73
+ const normalizedData = normalizeRadarData(data)
74
+ const count = axisCount ?? Math.max(normalizedData.length, minimumAxisCount)
75
+
76
+ return normalizedData.map((item, index) => {
77
+ const position = getRadarPosition(
78
+ index,
79
+ count,
80
+ normalizeChartValue(item.value, maxValue) / 100
81
+ )
82
+
83
+ return {
84
+ index,
85
+ label: item.label,
86
+ value: item.value,
87
+ x: position.x,
88
+ y: position.y,
89
+ }
90
+ })
91
+ }
92
+
93
+ function buildRingPoints(count: number, ratio: number) {
94
+ return Array.from({ length: count }, (_, index) =>
95
+ getRadarPosition(index, count, ratio)
96
+ )
97
+ }
98
+
99
+ function formatChartNumber(value: number) {
100
+ return Number(value.toFixed(3))
101
+ }
102
+
103
+ function buildSvgPointList(points: Array<{ x: number; y: number }>) {
104
+ if (points.length < minimumAxisCount) return undefined
105
+ return [...points, points[0]]
106
+ .map((point) => `${formatChartNumber(point.x)},${formatChartNumber(point.y)}`)
107
+ .join(" ")
108
+ }
109
+
110
+ function normalizeRadarSeries(
111
+ data: ChartDataPoint[],
112
+ series?: RadarChartSeries[]
113
+ ): Array<RadarChartSeries & { id: string; data: ChartDataPoint[] }> {
114
+ if (series?.length) {
115
+ return series
116
+ .map((item, index) => ({
117
+ ...item,
118
+ id: item.id ?? `series-${index}`,
119
+ data: normalizeRadarData(item.data),
120
+ }))
121
+ .filter((item) => item.data.length > 0)
122
+ }
123
+
124
+ return [
125
+ {
126
+ id: "data",
127
+ data: normalizeRadarData(data),
128
+ },
129
+ ].filter((item) => item.data.length > 0)
130
+ }
131
+
132
+ const RadarChart = React.forwardRef<HTMLDivElement, RadarChartProps>(
133
+ (
134
+ {
135
+ className,
136
+ data,
137
+ series,
138
+ variant = radarChartDefaultVariantKey,
139
+ max,
140
+ color,
141
+ showGrid = true,
142
+ showLabels = true,
143
+ showDots = true,
144
+ fillOpacity = 0.16,
145
+ formatValue = defaultChartValueFormatter,
146
+ maxLabel = "Max",
147
+ ...props
148
+ },
149
+ ref
150
+ ) => {
151
+ const normalizedSeries = normalizeRadarSeries(data, series)
152
+ const labelData = normalizedSeries[0]?.data ?? []
153
+ const axisCount = Math.max(
154
+ ...normalizedSeries.map((item) => item.data.length),
155
+ minimumAxisCount
156
+ )
157
+ const maxValue = Math.max(
158
+ max ?? 0,
159
+ ...normalizedSeries.flatMap((item) => item.data.map((point) => point.value)),
160
+ 1
161
+ )
162
+ const renderedSeries = normalizedSeries.map((item, index) => {
163
+ const points = buildRadarPoints(item.data, maxValue, axisCount)
164
+ const activeColor = getChartColor(item.color ?? color, index)
165
+
166
+ return {
167
+ ...item,
168
+ seriesIndex: index,
169
+ color: activeColor,
170
+ fillOpacity: item.fillOpacity ?? fillOpacity,
171
+ points,
172
+ svgPoints: buildSvgPointList(points),
173
+ }
174
+ })
175
+ const gridRings = [0.25, 0.5, 0.75, 1].map((ratio) => ({
176
+ ratio,
177
+ points: buildSvgPointList(buildRingPoints(axisCount, ratio)),
178
+ }))
179
+ const axisLines = Array.from({ length: axisCount }, (_, index) =>
180
+ getRadarPosition(index, axisCount, 1)
181
+ )
182
+
183
+ return (
184
+ <div
185
+ ref={ref}
186
+ role="img"
187
+ className={cn(
188
+ radarChartVariantClasses[variant],
189
+ "flex items-center justify-center overflow-hidden",
190
+ className
191
+ )}
192
+ {...props}
193
+ >
194
+ <div className="relative aspect-square h-full max-h-full max-w-full">
195
+ <svg
196
+ className="pointer-events-none absolute inset-0 h-full w-full overflow-visible"
197
+ viewBox="0 0 100 100"
198
+ preserveAspectRatio="none"
199
+ aria-hidden="true"
200
+ >
201
+ {showGrid
202
+ ? gridRings.map((ring) =>
203
+ ring.points ? (
204
+ <polyline
205
+ key={`ring-${ring.ratio}`}
206
+ points={ring.points}
207
+ fill="none"
208
+ stroke="hsl(var(--border) / 0.7)"
209
+ strokeWidth={1}
210
+ vectorEffect="non-scaling-stroke"
211
+ />
212
+ ) : null
213
+ )
214
+ : null}
215
+ {showGrid
216
+ ? axisLines.map((point, index) => (
217
+ <line
218
+ key={`axis-${index}`}
219
+ x1={chartCenter}
220
+ y1={chartCenter}
221
+ x2={formatChartNumber(point.x)}
222
+ y2={formatChartNumber(point.y)}
223
+ stroke="hsl(var(--border) / 0.5)"
224
+ strokeWidth={1}
225
+ vectorEffect="non-scaling-stroke"
226
+ />
227
+ ))
228
+ : null}
229
+ {renderedSeries.map((item) =>
230
+ item.svgPoints ? (
231
+ <polygon
232
+ key={`${item.id}-fill`}
233
+ points={item.svgPoints}
234
+ fill={item.color}
235
+ opacity={item.fillOpacity}
236
+ />
237
+ ) : null
238
+ )}
239
+ {renderedSeries.map((item) =>
240
+ item.svgPoints ? (
241
+ <polyline
242
+ key={`${item.id}-line`}
243
+ points={item.svgPoints}
244
+ fill="none"
245
+ stroke={item.color}
246
+ strokeWidth={2}
247
+ strokeLinecap="round"
248
+ strokeLinejoin="round"
249
+ vectorEffect="non-scaling-stroke"
250
+ />
251
+ ) : null
252
+ )}
253
+ {showDots
254
+ ? renderedSeries.flatMap((item) =>
255
+ item.points.map((point, index) => (
256
+ <circle
257
+ key={`${item.id}-dot-${index}`}
258
+ cx={formatChartNumber(point.x)}
259
+ cy={formatChartNumber(point.y)}
260
+ r={1.4}
261
+ fill="hsl(var(--background))"
262
+ stroke={item.color}
263
+ strokeWidth={2}
264
+ vectorEffect="non-scaling-stroke"
265
+ />
266
+ ))
267
+ )
268
+ : null}
269
+ </svg>
270
+ {showLabels
271
+ ? labelData.map((item, index) => {
272
+ const position = getRadarPosition(
273
+ index,
274
+ axisCount,
275
+ 1,
276
+ labelRadius
277
+ )
278
+ return (
279
+ <span
280
+ key={`${String(item.label)}-label-${index}`}
281
+ className="pointer-events-none absolute max-w-20 -translate-x-1/2 -translate-y-1/2 truncate text-center text-xs text-muted-foreground"
282
+ style={{
283
+ left: `${position.x}%`,
284
+ top: `${position.y}%`,
285
+ }}
286
+ aria-hidden="true"
287
+ >
288
+ {item.label}
289
+ </span>
290
+ )
291
+ })
292
+ : null}
293
+ {renderedSeries.flatMap((item) =>
294
+ item.points.map((point, index) => {
295
+ const seriesLabel = chartLabelToString(
296
+ item.label,
297
+ renderedSeries.length > 1
298
+ ? `Series ${item.seriesIndex + 1}`
299
+ : ""
300
+ )
301
+ const description =
302
+ renderedSeries.length > 1 && item.label
303
+ ? [item.label, " / ", maxLabel, ": ", formatValue(maxValue)]
304
+ : [maxLabel, ": ", formatValue(maxValue)]
305
+
306
+ return (
307
+ <ChartTooltip
308
+ key={`${item.id}-${String(point.label)}-${index}`}
309
+ label={point.label}
310
+ value={formatValue(point.value)}
311
+ description={description}
312
+ >
313
+ <span
314
+ className="absolute z-10 h-7 w-7 -translate-x-1/2 -translate-y-1/2 rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
315
+ style={{
316
+ left: `${point.x}%`,
317
+ top: `${point.y}%`,
318
+ }}
319
+ tabIndex={0}
320
+ aria-label={`${seriesLabel ? `${seriesLabel} ` : ""}${chartLabelToString(point.label)}: ${formatValue(point.value)} (${chartLabelToString(maxLabel, "Max")}: ${formatValue(maxValue)})`}
321
+ >
322
+ <span className="sr-only">{point.label}</span>
323
+ </span>
324
+ </ChartTooltip>
325
+ )
326
+ })
327
+ )}
328
+ </div>
329
+ </div>
330
+ )
331
+ }
332
+ )
333
+ RadarChart.displayName = "RadarChart"
334
+
335
+ export { RadarChart }
@@ -0,0 +1,264 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+ import type { ChartDataPoint } from "./chart-utils"
7
+ import {
8
+ clamp,
9
+ chartLabelToString,
10
+ defaultChartValueFormatter,
11
+ getChartColor,
12
+ normalizeChartValue,
13
+ } from "./chart-utils"
14
+ import { ChartLegend } from "./ChartLegend"
15
+ import { ChartFloatingTooltip } from "./chart-tooltip"
16
+ import type { RadialBarChartVariantKey } from "./generated/variant-keys"
17
+ import { radialBarChartDefaultVariantKey } from "./generated/default-variant-keys"
18
+
19
+ export interface RadialBarChartProps extends React.HTMLAttributes<HTMLDivElement> {
20
+ data: ChartDataPoint[]
21
+ variant?: RadialBarChartVariantKey
22
+ max?: number
23
+ centerLabel?: React.ReactNode
24
+ centerValue?: React.ReactNode
25
+ thickness?: number
26
+ gap?: number
27
+ showLegend?: boolean
28
+ formatValue?: (value: number) => React.ReactNode
29
+ maxLabel?: React.ReactNode
30
+ }
31
+
32
+ const radialBarChartVariantClasses: Record<RadialBarChartVariantKey, string> = {
33
+ compact: "min-h-[176px] w-full p-0",
34
+ default: "min-h-[224px] w-full p-0",
35
+ }
36
+
37
+ const radialBarChartSizeClasses: Record<RadialBarChartVariantKey, string> = {
38
+ compact: "max-w-44",
39
+ default: "max-w-56",
40
+ }
41
+
42
+ function getPositiveValue(value: number) {
43
+ return Number.isFinite(value) ? Math.max(0, value) : 0
44
+ }
45
+
46
+ const RadialBarChart = React.forwardRef<HTMLDivElement, RadialBarChartProps>(
47
+ (
48
+ {
49
+ className,
50
+ data,
51
+ variant = radialBarChartDefaultVariantKey,
52
+ max = 100,
53
+ centerLabel,
54
+ centerValue,
55
+ thickness = variant === "compact" ? 14 : 18,
56
+ gap = variant === "compact" ? 6 : 8,
57
+ showLegend = false,
58
+ formatValue = defaultChartValueFormatter,
59
+ maxLabel = "Max",
60
+ ...props
61
+ },
62
+ ref
63
+ ) => {
64
+ const maxValue = Math.max(max, ...data.map((item) => getPositiveValue(item.value)), 1)
65
+ const step = thickness + gap
66
+ const centerInset = data.length * step
67
+ const centerClearance = variant === "compact" ? 4 : 6
68
+ const [activeIndex, setActiveIndex] = React.useState(0)
69
+ const [tooltipOpen, setTooltipOpen] = React.useState(false)
70
+ const [tooltipPosition, setTooltipPosition] = React.useState({
71
+ x: 50,
72
+ y: 12,
73
+ })
74
+ const chartRef = React.useRef<HTMLDivElement | null>(null)
75
+ const touchTooltipStickyRef = React.useRef(false)
76
+ const tooltipItem = data[activeIndex] ?? data[0]
77
+ const tooltipPercent = tooltipItem
78
+ ? normalizeChartValue(getPositiveValue(tooltipItem.value), maxValue)
79
+ : 0
80
+ const tooltipPercentLabel = `${defaultChartValueFormatter(tooltipPercent)}%`
81
+ const legendItems = data.map((item, index) => ({
82
+ label: item.label,
83
+ value: formatValue(item.value),
84
+ color: item.color ?? getChartColor(undefined, index),
85
+ description: [maxLabel, ": ", formatValue(maxValue)],
86
+ }))
87
+
88
+ const updateTooltipAtPoint = (
89
+ element: HTMLDivElement,
90
+ clientX: number,
91
+ clientY: number
92
+ ) => {
93
+ const rect = element.getBoundingClientRect()
94
+ const center = rect.width / 2
95
+ const dx = clientX - rect.left - center
96
+ const dy = clientY - rect.top - center
97
+ const distance = Math.sqrt(dx * dx + dy * dy)
98
+ const hitSlop = Math.max(2, gap / 2)
99
+ const anglePercent =
100
+ (((Math.atan2(dx, -dy) * 180) / Math.PI + 360) % 360) / 360 * 100
101
+ const angleSlop = 1.5
102
+ const nextActiveIndex = data.findIndex((item, index) => {
103
+ const outerRadius = center - index * step
104
+ const innerRadius = outerRadius - thickness
105
+ const activePercent = normalizeChartValue(
106
+ getPositiveValue(item.value),
107
+ maxValue
108
+ )
109
+
110
+ return (
111
+ distance <= outerRadius + hitSlop &&
112
+ distance >= innerRadius - hitSlop &&
113
+ activePercent > 0 &&
114
+ anglePercent <= Math.min(100, activePercent + angleSlop)
115
+ )
116
+ })
117
+
118
+ if (nextActiveIndex < 0) {
119
+ setTooltipOpen(false)
120
+ return
121
+ }
122
+
123
+ setActiveIndex(nextActiveIndex)
124
+ setTooltipPosition({
125
+ x: clamp(((clientX - rect.left) / rect.width) * 100),
126
+ y: clamp(((clientY - rect.top) / rect.height) * 100),
127
+ })
128
+ setTooltipOpen(true)
129
+ }
130
+ const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
131
+ if (event.pointerType !== "touch") {
132
+ touchTooltipStickyRef.current = false
133
+ }
134
+ updateTooltipAtPoint(event.currentTarget, event.clientX, event.clientY)
135
+ }
136
+ const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
137
+ touchTooltipStickyRef.current = event.pointerType === "touch"
138
+ if (event.pointerType === "touch") {
139
+ event.preventDefault()
140
+ }
141
+ event.currentTarget.focus({ preventScroll: true })
142
+ updateTooltipAtPoint(event.currentTarget, event.clientX, event.clientY)
143
+ }
144
+ const handleTouchStart = (event: React.TouchEvent<HTMLDivElement>) => {
145
+ const touch = event.touches[0]
146
+ if (!touch) return
147
+ touchTooltipStickyRef.current = true
148
+ event.preventDefault()
149
+ event.currentTarget.focus({ preventScroll: true })
150
+ updateTooltipAtPoint(event.currentTarget, touch.clientX, touch.clientY)
151
+ }
152
+
153
+ return (
154
+ <div
155
+ ref={ref}
156
+ className={cn(
157
+ radialBarChartVariantClasses[variant],
158
+ "flex flex-col items-center justify-center gap-4",
159
+ className
160
+ )}
161
+ {...props}
162
+ >
163
+ <div
164
+ ref={chartRef}
165
+ className={cn(
166
+ "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",
167
+ radialBarChartSizeClasses[variant]
168
+ )}
169
+ role="img"
170
+ aria-label={
171
+ tooltipItem
172
+ ? `${chartLabelToString(tooltipItem.label)}: ${formatValue(tooltipItem.value)} (${tooltipPercentLabel} / ${chartLabelToString(maxLabel, "Max")}: ${formatValue(maxValue)})`
173
+ : props["aria-label"]
174
+ }
175
+ tabIndex={0}
176
+ onFocus={() => {
177
+ setActiveIndex(0)
178
+ setTooltipPosition({ x: 50, y: 12 })
179
+ setTooltipOpen(true)
180
+ }}
181
+ onBlur={() => {
182
+ touchTooltipStickyRef.current = false
183
+ setTooltipOpen(false)
184
+ }}
185
+ onPointerDown={handlePointerDown}
186
+ onPointerMove={handlePointerMove}
187
+ onTouchStart={handleTouchStart}
188
+ onPointerLeave={() => {
189
+ if (touchTooltipStickyRef.current) return
190
+ setTooltipOpen(false)
191
+ }}
192
+ onPointerCancel={() => {
193
+ touchTooltipStickyRef.current = false
194
+ setTooltipOpen(false)
195
+ }}
196
+ >
197
+ {data.map((item, index) => {
198
+ const inset = index * step
199
+ const value = getPositiveValue(item.value)
200
+ const percent = normalizeChartValue(value, maxValue)
201
+ const color = getChartColor(item.color, index)
202
+ const background = `conic-gradient(${color} 0% ${percent}%, hsl(var(--muted)) ${percent}% 100%)`
203
+ const ringMask = `radial-gradient(circle closest-side, transparent calc(100% - ${thickness}px), hsl(var(--palette-black)) calc(100% - ${thickness}px))`
204
+
205
+ return (
206
+ <React.Fragment key={`${chartLabelToString(item.label, "Item")}-${index}`}>
207
+ <span
208
+ className="pointer-events-none absolute rounded-full"
209
+ style={{
210
+ inset,
211
+ background,
212
+ WebkitMaskImage: ringMask,
213
+ maskImage: ringMask,
214
+ zIndex: data.length - index,
215
+ }}
216
+ aria-hidden="true"
217
+ />
218
+ </React.Fragment>
219
+ )
220
+ })}
221
+ {centerLabel !== undefined || centerValue !== undefined ? (
222
+ <div
223
+ className="absolute flex flex-col items-center justify-center rounded-full px-3 text-center"
224
+ style={{
225
+ inset: Math.max(centerInset - gap + centerClearance, thickness),
226
+ zIndex: data.length + 1,
227
+ }}
228
+ >
229
+ {centerValue !== undefined ? (
230
+ <span className="text-2xl font-semibold tracking-tight">
231
+ {centerValue}
232
+ </span>
233
+ ) : null}
234
+ {centerLabel !== undefined ? (
235
+ <span className="text-xs text-muted-foreground">
236
+ {centerLabel}
237
+ </span>
238
+ ) : null}
239
+ </div>
240
+ ) : null}
241
+ <ChartFloatingTooltip
242
+ label={tooltipItem?.label}
243
+ value={tooltipItem ? formatValue(tooltipItem.value) : undefined}
244
+ description={[
245
+ tooltipPercentLabel,
246
+ " / ",
247
+ maxLabel,
248
+ ": ",
249
+ formatValue(maxValue),
250
+ ]}
251
+ position={tooltipPosition}
252
+ open={tooltipOpen}
253
+ anchorRef={chartRef}
254
+ onOpenChange={setTooltipOpen}
255
+ />
256
+ </div>
257
+ {showLegend ? <ChartLegend className="justify-center" items={legendItems} /> : null}
258
+ </div>
259
+ )
260
+ }
261
+ )
262
+ RadialBarChart.displayName = "RadialBarChart"
263
+
264
+ export { RadialBarChart }