@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,234 @@
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
+ chartLabelToString,
9
+ defaultChartValueFormatter,
10
+ } from "./chart-utils"
11
+ import { ChartLegend } from "./ChartLegend"
12
+ import { ChartFloatingTooltip } from "./chart-tooltip"
13
+ import {
14
+ buildActiveCircularSegmentGradient,
15
+ buildConicGradient,
16
+ buildLegendItems,
17
+ getCircularSegmentAtPercent,
18
+ getCircularSegmentPosition,
19
+ getCircularSegmentShare,
20
+ type NormalizedCircularSegment,
21
+ normalizeCircularSegments,
22
+ } from "./circular-chart-utils"
23
+ import type { PieChartVariantKey } from "./generated/variant-keys"
24
+ import { pieChartDefaultVariantKey } from "./generated/default-variant-keys"
25
+
26
+ export interface PieChartProps extends React.HTMLAttributes<HTMLDivElement> {
27
+ segments: ChartDataPoint[]
28
+ variant?: PieChartVariantKey
29
+ showLegend?: boolean
30
+ formatValue?: (value: number) => React.ReactNode
31
+ totalLabel?: React.ReactNode
32
+ }
33
+
34
+ const pieChartVariantClasses: Record<PieChartVariantKey, string> = {
35
+ compact: "min-h-[144px] w-full p-0",
36
+ default: "min-h-[192px] w-full p-0",
37
+ }
38
+
39
+ const pieChartSizeClasses: Record<PieChartVariantKey, string> = {
40
+ compact: "max-w-36",
41
+ default: "max-w-48",
42
+ }
43
+
44
+ const PieChart = React.forwardRef<HTMLDivElement, PieChartProps>(
45
+ (
46
+ {
47
+ className,
48
+ segments,
49
+ variant = pieChartDefaultVariantKey,
50
+ showLegend = false,
51
+ formatValue = defaultChartValueFormatter,
52
+ totalLabel = "Total",
53
+ ...props
54
+ },
55
+ ref
56
+ ) => {
57
+ const normalizedSegments = React.useMemo(
58
+ () => normalizeCircularSegments(segments),
59
+ [segments]
60
+ )
61
+ const background = buildConicGradient(normalizedSegments)
62
+ const legendItems = buildLegendItems(segments, formatValue, totalLabel)
63
+ const [activeSegment, setActiveSegment] = React.useState<
64
+ NormalizedCircularSegment | undefined
65
+ >(
66
+ normalizedSegments[0]
67
+ )
68
+ const [tooltipOpen, setTooltipOpen] = React.useState(false)
69
+ const [tooltipPosition, setTooltipPosition] = React.useState({
70
+ x: 50,
71
+ y: 18,
72
+ })
73
+ const chartRef = React.useRef<HTMLDivElement | null>(null)
74
+ const touchTooltipStickyRef = React.useRef(false)
75
+ const tooltipSegment = activeSegment ?? normalizedSegments[0]
76
+ const tooltipShare = getCircularSegmentShare(tooltipSegment)
77
+ const tooltipShareLabel =
78
+ tooltipShare !== undefined
79
+ ? `${defaultChartValueFormatter(tooltipShare)}%`
80
+ : undefined
81
+ const activeOverlay = tooltipOpen
82
+ ? buildActiveCircularSegmentGradient(tooltipSegment)
83
+ : undefined
84
+ const activeIndex = Math.max(
85
+ 0,
86
+ normalizedSegments.findIndex(
87
+ (segment) =>
88
+ segment === tooltipSegment ||
89
+ segment.label === tooltipSegment?.label
90
+ )
91
+ )
92
+
93
+ React.useEffect(() => {
94
+ setActiveSegment(normalizedSegments[0])
95
+ setTooltipPosition(getCircularSegmentPosition(normalizedSegments[0]))
96
+ }, [normalizedSegments])
97
+
98
+ const updateTooltipAtPoint = (
99
+ element: HTMLDivElement,
100
+ clientX: number,
101
+ clientY: number
102
+ ) => {
103
+ const rect = element.getBoundingClientRect()
104
+ const radius = rect.width / 2
105
+ const centerX = rect.left + radius
106
+ const centerY = rect.top + radius
107
+ const dx = clientX - centerX
108
+ const dy = clientY - centerY
109
+ const distance = Math.sqrt(dx * dx + dy * dy)
110
+
111
+ if (distance > radius) {
112
+ setTooltipOpen(false)
113
+ return
114
+ }
115
+
116
+ const angle = (Math.atan2(dy, dx) * 180) / Math.PI
117
+ const percent = ((((angle + 90) % 360) + 360) % 360) / 360 * 100
118
+ setActiveSegment(getCircularSegmentAtPercent(normalizedSegments, percent))
119
+ setTooltipPosition({
120
+ x: Math.min(100, Math.max(0, ((clientX - rect.left) / rect.width) * 100)),
121
+ y: Math.min(100, Math.max(0, ((clientY - rect.top) / rect.height) * 100)),
122
+ })
123
+ setTooltipOpen(true)
124
+ }
125
+ const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
126
+ if (event.pointerType !== "touch") {
127
+ touchTooltipStickyRef.current = false
128
+ }
129
+ updateTooltipAtPoint(event.currentTarget, event.clientX, event.clientY)
130
+ }
131
+ const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
132
+ touchTooltipStickyRef.current = event.pointerType === "touch"
133
+ if (event.pointerType === "touch") {
134
+ event.preventDefault()
135
+ }
136
+ event.currentTarget.focus({ preventScroll: true })
137
+ updateTooltipAtPoint(event.currentTarget, event.clientX, event.clientY)
138
+ }
139
+ const handleTouchStart = (event: React.TouchEvent<HTMLDivElement>) => {
140
+ const touch = event.touches[0]
141
+ if (!touch) return
142
+ touchTooltipStickyRef.current = true
143
+ event.preventDefault()
144
+ event.currentTarget.focus({ preventScroll: true })
145
+ updateTooltipAtPoint(event.currentTarget, touch.clientX, touch.clientY)
146
+ }
147
+ return (
148
+ <div
149
+ ref={ref}
150
+ className={cn(
151
+ pieChartVariantClasses[variant],
152
+ "flex flex-col items-center justify-center gap-3",
153
+ className
154
+ )}
155
+ {...props}
156
+ >
157
+ <div
158
+ ref={chartRef}
159
+ className={cn(
160
+ "relative aspect-square w-full rounded-full shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
161
+ pieChartSizeClasses[variant]
162
+ )}
163
+ style={{ background }}
164
+ role="img"
165
+ aria-label={
166
+ tooltipSegment
167
+ ? `${chartLabelToString(tooltipSegment.label)}: ${formatValue(tooltipSegment.value)}${
168
+ tooltipShareLabel ? ` (${tooltipShareLabel})` : ""
169
+ }`
170
+ : props["aria-label"]
171
+ }
172
+ tabIndex={0}
173
+ onFocus={() => {
174
+ setActiveSegment(normalizedSegments[0])
175
+ setTooltipPosition(getCircularSegmentPosition(normalizedSegments[0]))
176
+ setTooltipOpen(true)
177
+ }}
178
+ onBlur={() => {
179
+ touchTooltipStickyRef.current = false
180
+ setTooltipOpen(false)
181
+ }}
182
+ onPointerDown={handlePointerDown}
183
+ onPointerMove={handlePointerMove}
184
+ onTouchStart={handleTouchStart}
185
+ onPointerLeave={() => {
186
+ if (touchTooltipStickyRef.current) return
187
+ setActiveSegment(normalizedSegments[0])
188
+ setTooltipPosition(getCircularSegmentPosition(normalizedSegments[0]))
189
+ setTooltipOpen(false)
190
+ }}
191
+ onPointerCancel={() => {
192
+ touchTooltipStickyRef.current = false
193
+ setTooltipOpen(false)
194
+ }}
195
+ >
196
+ {activeOverlay ? (
197
+ <span
198
+ className="pointer-events-none absolute inset-0 rounded-full mix-blend-multiply dark:mix-blend-screen"
199
+ style={{
200
+ background: activeOverlay,
201
+ filter: "drop-shadow(0 0 8px hsl(var(--foreground) / 0.32)) drop-shadow(0 0 14px hsl(var(--ring) / 0.26))",
202
+ }}
203
+ data-chart-active-overlay="true"
204
+ aria-hidden="true"
205
+ />
206
+ ) : null}
207
+ <ChartFloatingTooltip
208
+ label={tooltipSegment?.label}
209
+ value={
210
+ tooltipSegment
211
+ ? formatValue(tooltipSegment.value)
212
+ : undefined
213
+ }
214
+ description={tooltipShareLabel}
215
+ position={tooltipPosition}
216
+ open={tooltipOpen}
217
+ anchorRef={chartRef}
218
+ onOpenChange={setTooltipOpen}
219
+ />
220
+ </div>
221
+ {showLegend ? (
222
+ <ChartLegend
223
+ items={legendItems}
224
+ activeIndex={activeIndex}
225
+ className="justify-center"
226
+ />
227
+ ) : null}
228
+ </div>
229
+ )
230
+ }
231
+ )
232
+ PieChart.displayName = "PieChart"
233
+
234
+ export { PieChart }
@@ -0,0 +1,330 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+ import type { ChartColor } from "./chart-utils"
7
+ import {
8
+ chartLabelToString,
9
+ clamp,
10
+ defaultChartValueFormatter,
11
+ getChartColor,
12
+ normalizeChartValue,
13
+ } from "./chart-utils"
14
+ import { ChartTooltip } from "./chart-tooltip"
15
+ import type { QuadrantMatrixVariantKey } from "./generated/variant-keys"
16
+ import { quadrantMatrixDefaultVariantKey } from "./generated/default-variant-keys"
17
+
18
+ export interface QuadrantMatrixItem {
19
+ id?: string
20
+ label: string
21
+ value: number
22
+ x: number
23
+ y: number
24
+ color?: ChartColor
25
+ description?: React.ReactNode
26
+ }
27
+
28
+ export interface QuadrantMatrixLabels {
29
+ topLeft?: React.ReactNode
30
+ topRight?: React.ReactNode
31
+ bottomLeft?: React.ReactNode
32
+ bottomRight?: React.ReactNode
33
+ }
34
+
35
+ export interface QuadrantMatrixProps extends React.HTMLAttributes<HTMLDivElement> {
36
+ items: QuadrantMatrixItem[]
37
+ variant?: QuadrantMatrixVariantKey
38
+ max?: number
39
+ color?: ChartColor
40
+ selectedId?: string
41
+ showRanking?: boolean
42
+ rankingLimit?: number
43
+ xAxisLabel?: React.ReactNode
44
+ yAxisLabel?: React.ReactNode
45
+ quadrantLabels?: QuadrantMatrixLabels
46
+ formatValue?: (value: number) => React.ReactNode
47
+ onItemSelect?: (item: QuadrantMatrixItem, id: string) => void
48
+ rankingPlacement?: "side" | "bottom"
49
+ }
50
+
51
+ const quadrantMatrixVariantClasses: Record<QuadrantMatrixVariantKey, string> = {
52
+ compact: "w-full p-0",
53
+ default: "w-full p-0",
54
+ }
55
+
56
+ const quadrantMatrixPanelClasses: Record<QuadrantMatrixVariantKey, string> = {
57
+ compact: "min-h-44",
58
+ default: "min-h-56",
59
+ }
60
+
61
+ function itemKey(item: QuadrantMatrixItem, index: number) {
62
+ return item.id ?? `${item.label}-${index}`
63
+ }
64
+
65
+ interface QuadrantRankingContentProps {
66
+ item: QuadrantMatrixItem
67
+ percent: number
68
+ activeColor: string
69
+ formatValue: (value: number) => React.ReactNode
70
+ }
71
+
72
+ function QuadrantRankingContent({
73
+ item,
74
+ percent,
75
+ activeColor,
76
+ formatValue,
77
+ }: QuadrantRankingContentProps) {
78
+ return (
79
+ <>
80
+ <span className="flex min-w-0 items-center justify-between gap-3">
81
+ <span className="min-w-0">
82
+ <span className="block truncate text-sm font-medium">
83
+ {item.label}
84
+ </span>
85
+ {item.description ? (
86
+ <span className="block truncate text-xs text-muted-foreground">
87
+ {item.description}
88
+ </span>
89
+ ) : null}
90
+ </span>
91
+ <span className="shrink-0 text-sm font-semibold tabular-nums">
92
+ {formatValue(item.value)}
93
+ </span>
94
+ </span>
95
+ <span className="relative h-1.5 overflow-hidden rounded-full bg-muted">
96
+ <span
97
+ className="absolute inset-y-0 left-0 rounded-full"
98
+ style={{
99
+ width: `${percent}%`,
100
+ backgroundColor: activeColor,
101
+ }}
102
+ />
103
+ </span>
104
+ </>
105
+ )
106
+ }
107
+
108
+ const QuadrantMatrix = React.forwardRef<HTMLDivElement, QuadrantMatrixProps>(
109
+ (
110
+ {
111
+ className,
112
+ items,
113
+ variant = quadrantMatrixDefaultVariantKey,
114
+ max,
115
+ color,
116
+ selectedId,
117
+ showRanking = true,
118
+ rankingLimit = 5,
119
+ xAxisLabel,
120
+ yAxisLabel,
121
+ quadrantLabels,
122
+ formatValue = defaultChartValueFormatter,
123
+ onItemSelect,
124
+ rankingPlacement = "side",
125
+ ...props
126
+ },
127
+ ref
128
+ ) => {
129
+ const values = items.map((item) => item.value)
130
+ const maxValue = Math.max(max ?? 0, ...values, 1)
131
+ const rankedItems = [...items]
132
+ .sort((a, b) => b.value - a.value)
133
+ .slice(0, rankingLimit)
134
+ const canSelectItems = typeof onItemSelect === "function"
135
+
136
+ return (
137
+ <div
138
+ ref={ref}
139
+ className={cn(quadrantMatrixVariantClasses[variant], "min-w-0", className)}
140
+ {...props}
141
+ >
142
+ <div
143
+ className={cn(
144
+ "grid min-w-0 gap-4",
145
+ showRanking &&
146
+ rankingPlacement === "side" &&
147
+ "md:grid-cols-[minmax(0,1fr)_minmax(10rem,14rem)]"
148
+ )}
149
+ >
150
+ <div
151
+ className={cn(
152
+ "relative min-w-0 overflow-hidden rounded-md border bg-muted/20",
153
+ quadrantMatrixPanelClasses[variant]
154
+ )}
155
+ >
156
+ <div
157
+ className="absolute inset-0 grid grid-cols-2 grid-rows-2"
158
+ aria-hidden="true"
159
+ >
160
+ <span className="border-b border-r border-border/70 bg-muted/20" />
161
+ <span className="border-b border-border/70 bg-muted/10" />
162
+ <span className="border-r border-border/70 bg-muted/10" />
163
+ <span className="bg-muted/20" />
164
+ </div>
165
+ <span
166
+ className="absolute inset-x-0 top-1/2 border-t border-dashed border-border"
167
+ aria-hidden="true"
168
+ />
169
+ <span
170
+ className="absolute inset-y-0 left-1/2 border-l border-dashed border-border"
171
+ aria-hidden="true"
172
+ />
173
+ {quadrantLabels ? (
174
+ <div
175
+ className="pointer-events-none absolute inset-3 grid grid-cols-2 grid-rows-2 gap-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground"
176
+ aria-hidden="true"
177
+ >
178
+ <span>{quadrantLabels.topLeft}</span>
179
+ <span className="text-right">{quadrantLabels.topRight}</span>
180
+ <span className="self-end">{quadrantLabels.bottomLeft}</span>
181
+ <span className="self-end text-right">
182
+ {quadrantLabels.bottomRight}
183
+ </span>
184
+ </div>
185
+ ) : null}
186
+ {yAxisLabel ? (
187
+ <span className="absolute left-2 top-1/2 -translate-y-1/2 -rotate-90 text-xs font-medium text-muted-foreground">
188
+ {yAxisLabel}
189
+ </span>
190
+ ) : null}
191
+ {xAxisLabel ? (
192
+ <span className="absolute bottom-3 left-1/2 -translate-x-1/2 text-xs font-medium text-muted-foreground">
193
+ {xAxisLabel}
194
+ </span>
195
+ ) : null}
196
+ {items.map((item, index) => {
197
+ const id = itemKey(item, index)
198
+ const percent = normalizeChartValue(item.value, maxValue)
199
+ const size =
200
+ variant === "compact"
201
+ ? 1.1 + (percent / 100) * 1.4
202
+ : 1.25 + (percent / 100) * 1.9
203
+ const isSelected = selectedId === id
204
+ const activeColor = getChartColor(item.color ?? color, index)
205
+
206
+ const itemStyle = {
207
+ left: `${clamp(item.x)}%`,
208
+ top: `${clamp(item.y)}%`,
209
+ width: `${size}rem`,
210
+ height: `${size}rem`,
211
+ backgroundColor: activeColor,
212
+ }
213
+ const itemClassName = cn(
214
+ "absolute z-10 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-background/80 p-0 shadow-sm",
215
+ canSelectItems &&
216
+ "cursor-pointer transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
217
+ !canSelectItems && "cursor-default",
218
+ isSelected &&
219
+ "z-20 ring-2 ring-foreground ring-offset-2 ring-offset-background"
220
+ )
221
+ const itemContents = (
222
+ <>
223
+ <span
224
+ className="absolute inset-0 rounded-full bg-background/20"
225
+ aria-hidden="true"
226
+ />
227
+ <span className="relative h-1.5 w-1.5 rounded-full bg-background" />
228
+ </>
229
+ )
230
+
231
+ return (
232
+ <ChartTooltip
233
+ key={id}
234
+ label={item.label}
235
+ value={formatValue(item.value)}
236
+ description={item.description}
237
+ >
238
+ {canSelectItems ? (
239
+ <button
240
+ type="button"
241
+ className={itemClassName}
242
+ style={itemStyle}
243
+ aria-label={`${chartLabelToString(item.label)}: ${formatValue(item.value)}`}
244
+ aria-current={isSelected ? "true" : undefined}
245
+ onClick={() => onItemSelect?.(item, id)}
246
+ >
247
+ {itemContents}
248
+ </button>
249
+ ) : (
250
+ <span
251
+ className={itemClassName}
252
+ style={itemStyle}
253
+ tabIndex={0}
254
+ aria-label={`${chartLabelToString(item.label)}: ${formatValue(item.value)}`}
255
+ aria-current={isSelected ? "true" : undefined}
256
+ >
257
+ {itemContents}
258
+ </span>
259
+ )}
260
+ </ChartTooltip>
261
+ )
262
+ })}
263
+ </div>
264
+ {showRanking ? (
265
+ <ol className="grid min-w-0 content-start gap-2">
266
+ {rankedItems.map((item, index) => {
267
+ const id = itemKey(item, items.indexOf(item))
268
+ const percent = normalizeChartValue(item.value, maxValue)
269
+ const activeColor = getChartColor(item.color ?? color, index)
270
+ const isSelected = selectedId === id
271
+
272
+ return (
273
+ <li key={id}>
274
+ <ChartTooltip
275
+ label={item.label}
276
+ value={formatValue(item.value)}
277
+ description={item.description}
278
+ >
279
+ {canSelectItems ? (
280
+ <button
281
+ type="button"
282
+ className={cn(
283
+ "grid w-full min-w-0 gap-1 rounded-md border bg-card p-2 text-left cursor-pointer transition-colors hover:border-primary-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
284
+ isSelected && "ring-1 ring-foreground"
285
+ )}
286
+ aria-current={
287
+ isSelected ? "true" : undefined
288
+ }
289
+ onClick={() => onItemSelect?.(item, id)}
290
+ >
291
+ <QuadrantRankingContent
292
+ item={item}
293
+ percent={percent}
294
+ activeColor={activeColor}
295
+ formatValue={formatValue}
296
+ />
297
+ </button>
298
+ ) : (
299
+ <span
300
+ className={cn(
301
+ "grid w-full min-w-0 cursor-default gap-1 rounded-md border bg-card p-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
302
+ isSelected && "ring-1 ring-foreground"
303
+ )}
304
+ tabIndex={0}
305
+ aria-current={
306
+ isSelected ? "true" : undefined
307
+ }
308
+ >
309
+ <QuadrantRankingContent
310
+ item={item}
311
+ percent={percent}
312
+ activeColor={activeColor}
313
+ formatValue={formatValue}
314
+ />
315
+ </span>
316
+ )}
317
+ </ChartTooltip>
318
+ </li>
319
+ )
320
+ })}
321
+ </ol>
322
+ ) : null}
323
+ </div>
324
+ </div>
325
+ )
326
+ }
327
+ )
328
+ QuadrantMatrix.displayName = "QuadrantMatrix"
329
+
330
+ export { QuadrantMatrix }