@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,618 @@
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
+ clamp,
10
+ defaultChartValueFormatter,
11
+ getChartColor,
12
+ getChartLabel,
13
+ getChartValue,
14
+ } from "./chart-utils"
15
+ import { ChartLegend } from "./ChartLegend"
16
+ import { ChartFloatingTooltip } from "./chart-tooltip"
17
+ import type { RibbonChartVariantKey } from "./generated/variant-keys"
18
+ import { ribbonChartDefaultVariantKey } from "./generated/default-variant-keys"
19
+
20
+ export interface RibbonChartLayer {
21
+ label?: React.ReactNode
22
+ data: Array<ChartDataPoint | number>
23
+ color?: ChartColor
24
+ }
25
+
26
+ export interface RibbonChartProps
27
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
28
+ layers: RibbonChartLayer[]
29
+ variant?: RibbonChartVariantKey
30
+ max?: number
31
+ totalLabel?: React.ReactNode
32
+ showGrid?: boolean
33
+ showLegend?: boolean
34
+ showLabels?: boolean
35
+ formatValue?: (value: number) => React.ReactNode
36
+ }
37
+
38
+ interface RibbonPoint {
39
+ index: number
40
+ label: React.ReactNode
41
+ value: number
42
+ total: number
43
+ x: number
44
+ top: number
45
+ bottom: number
46
+ }
47
+
48
+ interface RibbonPadding {
49
+ top: number
50
+ right: number
51
+ bottom: number
52
+ left: number
53
+ }
54
+
55
+ interface RibbonTooltipState {
56
+ label: React.ReactNode
57
+ value: React.ReactNode
58
+ description: React.ReactNode
59
+ ariaLabel: string
60
+ }
61
+
62
+ interface ActiveRibbonPoint {
63
+ layerIndex: number
64
+ pointIndex: number
65
+ }
66
+
67
+ interface RibbonHitBox {
68
+ left: number
69
+ right: number
70
+ centerY: number
71
+ height: number
72
+ }
73
+
74
+ const ribbonChartVariantClasses: Record<RibbonChartVariantKey, string> = {
75
+ flow: "h-[260px] w-full p-0",
76
+ stacked: "h-[260px] w-full p-0",
77
+ }
78
+
79
+ const plotPadding: RibbonPadding = {
80
+ top: 14,
81
+ right: 12,
82
+ bottom: 10,
83
+ left: 28,
84
+ }
85
+
86
+ function getPositiveValue(value: number) {
87
+ return Number.isFinite(value) ? Math.max(0, value) : 0
88
+ }
89
+
90
+ function getLayerLabel(layer: RibbonChartLayer, index: number) {
91
+ return layer.label ?? `Layer ${index + 1}`
92
+ }
93
+
94
+ function getMaxPointCount(layers: RibbonChartLayer[]) {
95
+ return Math.max(1, ...layers.map((layer) => layer.data.length))
96
+ }
97
+
98
+ function getRibbonLabels(layers: RibbonChartLayer[]) {
99
+ const longestLayer = layers.reduce<RibbonChartLayer | null>((longest, layer) => {
100
+ if (!longest || layer.data.length > longest.data.length) return layer
101
+ return longest
102
+ }, null)
103
+
104
+ return longestLayer?.data.map((item, index) => getChartLabel(item, index)) ?? []
105
+ }
106
+
107
+ function getLayerPointLabel(
108
+ layer: RibbonChartLayer,
109
+ labels: React.ReactNode[],
110
+ index: number
111
+ ) {
112
+ const point = layer.data[index]
113
+ if (point !== undefined) return getChartLabel(point, index)
114
+ return labels[index] ?? `#${index + 1}`
115
+ }
116
+
117
+ function getLayerPointValue(layer: RibbonChartLayer, index: number) {
118
+ const point = layer.data[index]
119
+ return point === undefined ? 0 : getPositiveValue(getChartValue(point))
120
+ }
121
+
122
+ function buildLayerValues(layers: RibbonChartLayer[], pointCount: number) {
123
+ return layers.map((layer) =>
124
+ Array.from({ length: pointCount }, (_, index) => getLayerPointValue(layer, index))
125
+ )
126
+ }
127
+
128
+ function getMaxTotal(
129
+ layerValues: number[][],
130
+ pointCount: number,
131
+ max: number | undefined
132
+ ) {
133
+ const totals = Array.from({ length: pointCount }, (_, pointIndex) =>
134
+ layerValues.reduce((sum, values) => sum + (values[pointIndex] ?? 0), 0)
135
+ )
136
+
137
+ return Math.max(max ?? 0, ...totals, 1)
138
+ }
139
+
140
+ function getXPosition(
141
+ index: number,
142
+ pointCount: number,
143
+ width: number,
144
+ padding: RibbonPadding
145
+ ) {
146
+ if (pointCount <= 1) return width / 2
147
+ const usableWidth = Math.max(1, width - padding.left - padding.right)
148
+ return padding.left + (index + 0.5) * (usableWidth / pointCount)
149
+ }
150
+
151
+ function getPointHitRange(
152
+ index: number,
153
+ pointCount: number,
154
+ width: number,
155
+ padding: RibbonPadding
156
+ ) {
157
+ const startX = padding.left
158
+ const endX = width - padding.right
159
+
160
+ if (pointCount <= 1) {
161
+ return { start: startX, end: endX }
162
+ }
163
+
164
+ const currentX = getXPosition(index, pointCount, width, padding)
165
+ const nextX =
166
+ index === pointCount - 1
167
+ ? endX
168
+ : getXPosition(index + 1, pointCount, width, padding)
169
+
170
+ return {
171
+ start: index === 0 ? startX : currentX,
172
+ end: nextX,
173
+ }
174
+ }
175
+
176
+ function buildRibbonPoints({
177
+ layers,
178
+ width,
179
+ height,
180
+ padding,
181
+ max,
182
+ variant,
183
+ }: {
184
+ layers: RibbonChartLayer[]
185
+ width: number
186
+ height: number
187
+ padding: RibbonPadding
188
+ max?: number
189
+ variant: RibbonChartVariantKey
190
+ }) {
191
+ const pointCount = getMaxPointCount(layers)
192
+ const labels = getRibbonLabels(layers)
193
+ const layerValues = buildLayerValues(layers, pointCount)
194
+ const maxTotal = getMaxTotal(layerValues, pointCount, max)
195
+ const usableHeight = Math.max(1, height - padding.top - padding.bottom)
196
+ const layerPoints = layers.map<RibbonPoint[]>(() => [])
197
+
198
+ for (let pointIndex = 0; pointIndex < pointCount; pointIndex += 1) {
199
+ const columnTotal = layerValues.reduce(
200
+ (sum, values) => sum + (values[pointIndex] ?? 0),
201
+ 0
202
+ )
203
+ const columnHeight = (columnTotal / maxTotal) * usableHeight
204
+ const columnTop =
205
+ variant === "flow"
206
+ ? padding.top + (usableHeight - columnHeight) / 2
207
+ : height - padding.bottom - columnHeight
208
+ let cursor = columnTop
209
+
210
+ layers.forEach((layer, layerIndex) => {
211
+ const value = layerValues[layerIndex]?.[pointIndex] ?? 0
212
+ const pointHeight = (value / maxTotal) * usableHeight
213
+ layerPoints[layerIndex].push({
214
+ index: pointIndex,
215
+ label: getLayerPointLabel(layer, labels, pointIndex),
216
+ value,
217
+ total: columnTotal,
218
+ x: getXPosition(pointIndex, pointCount, width, padding),
219
+ top: cursor,
220
+ bottom: cursor + pointHeight,
221
+ })
222
+ cursor += pointHeight
223
+ })
224
+ }
225
+
226
+ return { labels, layerPoints }
227
+ }
228
+
229
+ function buildRibbonClipPath(points: RibbonPoint[], width: number, height: number) {
230
+ if (points.length === 0) return undefined
231
+
232
+ const topPoints = points
233
+ .map((point) => `${(point.x / width) * 100}% ${(point.top / height) * 100}%`)
234
+ .join(", ")
235
+ const bottomPoints = [...points]
236
+ .reverse()
237
+ .map((point) => `${(point.x / width) * 100}% ${(point.bottom / height) * 100}%`)
238
+ .join(", ")
239
+
240
+ return `polygon(${topPoints}, ${bottomPoints})`
241
+ }
242
+
243
+ function useElementSize<T extends HTMLElement>() {
244
+ const [node, setNode] = React.useState<T | null>(null)
245
+ const [size, setSize] = React.useState({ width: 0, height: 0 })
246
+
247
+ React.useEffect(() => {
248
+ if (!node) return undefined
249
+ const updateSize = () => {
250
+ const rect = node.getBoundingClientRect()
251
+ setSize({ width: rect.width, height: rect.height })
252
+ }
253
+ updateSize()
254
+
255
+ const observer = new ResizeObserver(updateSize)
256
+ observer.observe(node)
257
+ return () => observer.disconnect()
258
+ }, [node])
259
+
260
+ return [setNode, size] as const
261
+ }
262
+
263
+ const RibbonChart = React.forwardRef<HTMLDivElement, RibbonChartProps>(
264
+ (
265
+ {
266
+ className,
267
+ layers,
268
+ variant = ribbonChartDefaultVariantKey,
269
+ max,
270
+ totalLabel = "Total",
271
+ showGrid = true,
272
+ showLegend = false,
273
+ showLabels = true,
274
+ formatValue = defaultChartValueFormatter,
275
+ ...props
276
+ },
277
+ ref
278
+ ) => {
279
+ const [setPlotNode, plotSize] = useElementSize<HTMLDivElement>()
280
+ const [tooltipOpen, setTooltipOpen] = React.useState(false)
281
+ const [tooltipPosition, setTooltipPosition] = React.useState({
282
+ x: 50,
283
+ y: 20,
284
+ })
285
+ const [tooltipContent, setTooltipContent] =
286
+ React.useState<RibbonTooltipState | null>(null)
287
+ const [activePoint, setActivePoint] =
288
+ React.useState<ActiveRibbonPoint | null>(null)
289
+ const touchTooltipStickyRef = React.useRef(false)
290
+ const width = plotSize.width || 480
291
+ const height = plotSize.height || 212
292
+ const pointCount = getMaxPointCount(layers)
293
+ const { labels, layerPoints } = buildRibbonPoints({
294
+ layers,
295
+ width,
296
+ height,
297
+ padding: plotPadding,
298
+ max,
299
+ variant,
300
+ })
301
+ const layerTotals = layers.map((layer) =>
302
+ layer.data.reduce<number>(
303
+ (sum, point) => sum + getPositiveValue(getChartValue(point)),
304
+ 0
305
+ )
306
+ )
307
+ const allLayerTotal = Math.max(
308
+ layerTotals.reduce((sum, value) => sum + value, 0),
309
+ 1
310
+ )
311
+ const legendItems = layers.map((layer, index) => {
312
+ const value = layerTotals[index] ?? 0
313
+ const share = (value / allLayerTotal) * 100
314
+
315
+ return {
316
+ label: getLayerLabel(layer, index),
317
+ value: formatValue(value),
318
+ color: layer.color,
319
+ description: [totalLabel, ": ", `${defaultChartValueFormatter(share)}%`],
320
+ }
321
+ })
322
+ const getPointHitBox = (point: RibbonPoint): RibbonHitBox => {
323
+ const hitRange = getPointHitRange(
324
+ point.index,
325
+ pointCount,
326
+ width,
327
+ plotPadding
328
+ )
329
+ return {
330
+ left: hitRange.start,
331
+ right: hitRange.end,
332
+ centerY: (point.top + point.bottom) / 2,
333
+ height: Math.max(point.bottom - point.top, 32),
334
+ }
335
+ }
336
+ const getPointTooltip = (
337
+ layerIndex: number,
338
+ point: RibbonPoint
339
+ ): RibbonTooltipState => {
340
+ const layer = layers[layerIndex]
341
+ const share =
342
+ point.total > 0 ? (point.value / point.total) * 100 : 0
343
+ const shareLabel = `${defaultChartValueFormatter(share)}%`
344
+ const totalText = chartLabelToString(totalLabel, "Total")
345
+ const layerText = chartLabelToString(
346
+ getLayerLabel(layer, layerIndex),
347
+ "Layer"
348
+ )
349
+ const pointText = chartLabelToString(point.label)
350
+ const value = formatValue(point.value)
351
+ const valueText = chartLabelToString(value, String(point.value))
352
+ const totalValue = formatValue(point.total)
353
+ const totalValueText = chartLabelToString(
354
+ totalValue,
355
+ String(point.total)
356
+ )
357
+
358
+ return {
359
+ label: `${layerText} / ${pointText}`,
360
+ value,
361
+ description: (
362
+ <>
363
+ {totalLabel}
364
+ {" "}
365
+ {totalValue}
366
+ {" / "}
367
+ {shareLabel}
368
+ </>
369
+ ),
370
+ ariaLabel: `${layerText} ${pointText}: ${valueText} (${totalText} ${totalValueText} / ${shareLabel})`,
371
+ }
372
+ }
373
+ const openPointTooltip = (
374
+ layerIndex: number,
375
+ point: RibbonPoint,
376
+ position: { x: number; y: number }
377
+ ) => {
378
+ setTooltipContent(getPointTooltip(layerIndex, point))
379
+ setActivePoint({ layerIndex, pointIndex: point.index })
380
+ setTooltipPosition(position)
381
+ setTooltipOpen(true)
382
+ }
383
+ const closePointTooltip = () => {
384
+ touchTooltipStickyRef.current = false
385
+ setTooltipOpen(false)
386
+ setActivePoint(null)
387
+ }
388
+ const handlePlotPointerMove = (
389
+ event:
390
+ | React.PointerEvent<HTMLDivElement>
391
+ | React.MouseEvent<HTMLDivElement>
392
+ ) => {
393
+ if ("pointerType" in event && event.pointerType !== "touch") {
394
+ touchTooltipStickyRef.current = false
395
+ }
396
+ const rect = event.currentTarget.getBoundingClientRect()
397
+ const localX = event.clientX - rect.left
398
+ const localY = event.clientY - rect.top
399
+
400
+ for (let layerIndex = 0; layerIndex < layerPoints.length; layerIndex += 1) {
401
+ const points = layerPoints[layerIndex] ?? []
402
+
403
+ for (const point of points) {
404
+ const hitBox = getPointHitBox(point)
405
+ const top = hitBox.centerY - hitBox.height / 2
406
+ const bottom = hitBox.centerY + hitBox.height / 2
407
+
408
+ const isInsideX =
409
+ localX >= hitBox.left &&
410
+ (localX < hitBox.right ||
411
+ point.index === pointCount - 1)
412
+
413
+ if (isInsideX && localY >= top && localY <= bottom) {
414
+ openPointTooltip(layerIndex, point, {
415
+ x: clamp((localX / rect.width) * 100),
416
+ y: clamp((localY / rect.height) * 100),
417
+ })
418
+ return
419
+ }
420
+ }
421
+ }
422
+
423
+ closePointTooltip()
424
+ }
425
+ const handlePlotPointerDown = (
426
+ event: React.PointerEvent<HTMLDivElement>
427
+ ) => {
428
+ touchTooltipStickyRef.current = event.pointerType === "touch"
429
+ if (event.pointerType === "touch") {
430
+ event.preventDefault()
431
+ }
432
+ handlePlotPointerMove(event)
433
+ }
434
+ const activeLayerIndex = tooltipOpen ? activePoint?.layerIndex : undefined
435
+
436
+ return (
437
+ <div
438
+ ref={ref}
439
+ role="img"
440
+ className={cn(
441
+ ribbonChartVariantClasses[variant],
442
+ "flex min-w-0 flex-col gap-3",
443
+ className
444
+ )}
445
+ {...props}
446
+ >
447
+ <div
448
+ ref={setPlotNode}
449
+ className="relative min-h-0 flex-1"
450
+ onPointerDown={handlePlotPointerDown}
451
+ onPointerMove={handlePlotPointerMove}
452
+ onMouseMove={handlePlotPointerMove}
453
+ onMouseEnter={handlePlotPointerMove}
454
+ onPointerLeave={() => {
455
+ if (touchTooltipStickyRef.current) return
456
+ closePointTooltip()
457
+ }}
458
+ onMouseLeave={closePointTooltip}
459
+ >
460
+ {showGrid
461
+ ? [0, 25, 50, 75, 100].map((percent) => (
462
+ <span
463
+ key={`y-${percent}`}
464
+ className="pointer-events-none absolute inset-x-0 border-t border-dashed border-border/65"
465
+ style={{
466
+ top: `${
467
+ plotPadding.top +
468
+ ((height - plotPadding.top - plotPadding.bottom) *
469
+ percent) /
470
+ 100
471
+ }px`,
472
+ }}
473
+ aria-hidden="true"
474
+ />
475
+ ))
476
+ : null}
477
+ {showGrid && labels.length > 1
478
+ ? labels.map((label, index) => (
479
+ <span
480
+ key={`x-${chartLabelToString(label, "Label")}-${index}`}
481
+ className="pointer-events-none absolute inset-y-0 border-l border-border/45"
482
+ style={{
483
+ left: `${
484
+ getXPosition(
485
+ index,
486
+ labels.length,
487
+ width,
488
+ plotPadding
489
+ )
490
+ }px`,
491
+ }}
492
+ aria-hidden="true"
493
+ />
494
+ ))
495
+ : null}
496
+ {layers.map((layer, layerIndex) => {
497
+ const points = layerPoints[layerIndex] ?? []
498
+ const color = getChartColor(layer.color, layerIndex)
499
+ const clipPath = buildRibbonClipPath(points, width, height)
500
+ const isActiveLayer = activeLayerIndex === layerIndex
501
+
502
+ return (
503
+ <React.Fragment key={`${chartLabelToString(layer.label, "Layer")}-${layerIndex}`}>
504
+ {clipPath ? (
505
+ <span
506
+ className="pointer-events-none absolute inset-0"
507
+ style={{
508
+ backgroundColor: color,
509
+ clipPath,
510
+ opacity:
511
+ activeLayerIndex !== undefined && !isActiveLayer
512
+ ? 0.42
513
+ : 0.86,
514
+ zIndex: 1,
515
+ }}
516
+ aria-hidden="true"
517
+ />
518
+ ) : null}
519
+ {clipPath && isActiveLayer ? (
520
+ <span
521
+ className="pointer-events-none absolute inset-0"
522
+ style={{
523
+ backgroundColor: color,
524
+ clipPath,
525
+ filter: `drop-shadow(0 0 10px ${color}) drop-shadow(0 8px 16px hsl(var(--foreground) / 0.2))`,
526
+ opacity: 0.48,
527
+ zIndex: 2,
528
+ }}
529
+ data-chart-active-overlay="true"
530
+ aria-hidden="true"
531
+ />
532
+ ) : null}
533
+ {points.map((point, pointIndex) => {
534
+ const hitBox = getPointHitBox(point)
535
+ const tooltip = getPointTooltip(layerIndex, point)
536
+
537
+ return (
538
+ <span
539
+ key={`${layerIndex}-${point.x}-${pointIndex}`}
540
+ className="absolute z-10 -translate-y-1/2 rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
541
+ style={{
542
+ left: `${(hitBox.left / width) * 100}%`,
543
+ top: `${(hitBox.centerY / height) * 100}%`,
544
+ width: `${((hitBox.right - hitBox.left) / width) * 100}%`,
545
+ height: `${hitBox.height}px`,
546
+ }}
547
+ tabIndex={0}
548
+ aria-label={tooltip.ariaLabel}
549
+ onFocus={() => {
550
+ setTooltipPosition({
551
+ x:
552
+ (((hitBox.left + hitBox.right) / 2) /
553
+ width) *
554
+ 100,
555
+ y: (hitBox.centerY / height) * 100,
556
+ })
557
+ setTooltipContent(tooltip)
558
+ setActivePoint({
559
+ layerIndex,
560
+ pointIndex: point.index,
561
+ })
562
+ setTooltipOpen(true)
563
+ }}
564
+ onBlur={closePointTooltip}
565
+ />
566
+ )
567
+ })}
568
+ </React.Fragment>
569
+ )
570
+ })}
571
+ <ChartFloatingTooltip
572
+ label={tooltipContent?.label}
573
+ value={tooltipContent?.value}
574
+ description={tooltipContent?.description}
575
+ position={tooltipPosition}
576
+ open={tooltipOpen}
577
+ onOpenChange={(open) => {
578
+ setTooltipOpen(open)
579
+ if (!open) {
580
+ touchTooltipStickyRef.current = false
581
+ setActivePoint(null)
582
+ }
583
+ }}
584
+ />
585
+ </div>
586
+ {showLabels && labels.length > 0 ? (
587
+ <div
588
+ className="grid min-w-0 text-xs text-muted-foreground"
589
+ style={{
590
+ gridTemplateColumns: `repeat(${labels.length}, minmax(0, 1fr))`,
591
+ paddingLeft: plotPadding.left,
592
+ paddingRight: plotPadding.right,
593
+ }}
594
+ >
595
+ {labels.map((label, index) => (
596
+ <span
597
+ key={`${chartLabelToString(label, "Label")}-${index}`}
598
+ className="min-w-0 truncate text-center"
599
+ >
600
+ {label}
601
+ </span>
602
+ ))}
603
+ </div>
604
+ ) : null}
605
+ {showLegend ? (
606
+ <ChartLegend
607
+ items={legendItems}
608
+ variant="horizontal"
609
+ activeIndex={activeLayerIndex}
610
+ />
611
+ ) : null}
612
+ </div>
613
+ )
614
+ }
615
+ )
616
+ RibbonChart.displayName = "RibbonChart"
617
+
618
+ export { RibbonChart }