@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,613 @@
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 { ChoroplethMapVariantKey } from "./generated/variant-keys"
16
+ import { choroplethMapDefaultVariantKey } from "./generated/default-variant-keys"
17
+
18
+ export interface ChoroplethMapRegion {
19
+ id?: string
20
+ label: string
21
+ value: number
22
+ geometry: ChoroplethMapGeometry
23
+ color?: ChartColor
24
+ description?: React.ReactNode
25
+ }
26
+
27
+ export interface ChoroplethMapMarker {
28
+ id?: string
29
+ label: string
30
+ regionId?: string
31
+ coordinate?: ChoroplethMapCoordinate
32
+ x?: number
33
+ y?: number
34
+ value?: number
35
+ color?: ChartColor
36
+ }
37
+
38
+ export type ChoroplethMapCoordinate = [number, number]
39
+
40
+ export type ChoroplethMapGeometry =
41
+ | {
42
+ type: "Polygon"
43
+ coordinates: ChoroplethMapCoordinate[][]
44
+ }
45
+ | {
46
+ type: "MultiPolygon"
47
+ coordinates: ChoroplethMapCoordinate[][][]
48
+ }
49
+
50
+ export interface ChoroplethMapProps extends React.HTMLAttributes<HTMLDivElement> {
51
+ regions: ChoroplethMapRegion[]
52
+ markers?: ChoroplethMapMarker[]
53
+ variant?: ChoroplethMapVariantKey
54
+ max?: number
55
+ color?: ChartColor
56
+ selectedId?: string
57
+ showRanking?: boolean
58
+ rankingLimit?: number
59
+ showSelectedRegion?: boolean
60
+ preserveAspectRatio?: boolean
61
+ formatValue?: (value: number) => React.ReactNode
62
+ selectedLabel?: React.ReactNode
63
+ rankLabel?: React.ReactNode
64
+ onRegionSelect?: (region: ChoroplethMapRegion, id: string) => void
65
+ onMarkerSelect?: (marker: ChoroplethMapMarker, id: string) => void
66
+ }
67
+
68
+ const choroplethMapVariantClasses: Record<ChoroplethMapVariantKey, string> = {
69
+ compact: "w-full p-0",
70
+ default: "w-full p-0",
71
+ }
72
+
73
+ const choroplethMapPanelClasses: Record<ChoroplethMapVariantKey, string> = {
74
+ compact: "min-h-56",
75
+ default: "min-h-72",
76
+ }
77
+
78
+ const CHOROPLETH_MAP_SPLIT_LAYOUT_MIN_WIDTH = 640
79
+
80
+ function regionKey(region: ChoroplethMapRegion, index: number) {
81
+ return region.id ?? `${region.label}-${index}`
82
+ }
83
+
84
+ function markerKey(marker: ChoroplethMapMarker, index: number) {
85
+ return marker.id ?? `${marker.label}-${index}`
86
+ }
87
+
88
+ function mapPieceOutlineColor(isSelected: boolean) {
89
+ return isSelected ? "hsl(var(--foreground) / 0.8)" : "hsl(var(--foreground) / 0.28)"
90
+ }
91
+
92
+ interface GeometryBounds {
93
+ minX: number
94
+ maxX: number
95
+ minY: number
96
+ maxY: number
97
+ width: number
98
+ height: number
99
+ aspectRatio: number
100
+ }
101
+
102
+ interface RegionPiece {
103
+ left: number
104
+ top: number
105
+ width: number
106
+ height: number
107
+ clipPath: string
108
+ }
109
+
110
+ const fallbackBounds: GeometryBounds = {
111
+ minX: 0,
112
+ maxX: 100,
113
+ minY: 0,
114
+ maxY: 100,
115
+ width: 100,
116
+ height: 100,
117
+ aspectRatio: 1,
118
+ }
119
+
120
+ function getExteriorRings(geometry: ChoroplethMapGeometry) {
121
+ if (geometry.type === "Polygon") {
122
+ return geometry.coordinates[0] ? [geometry.coordinates[0]] : []
123
+ }
124
+
125
+ return geometry.coordinates
126
+ .map((polygon) => polygon[0])
127
+ .filter((ring): ring is ChoroplethMapCoordinate[] => Boolean(ring))
128
+ }
129
+
130
+ function computeBounds(regions: ChoroplethMapRegion[], markers: ChoroplethMapMarker[]) {
131
+ const coordinates: ChoroplethMapCoordinate[] = []
132
+
133
+ regions.forEach((region) => {
134
+ getExteriorRings(region.geometry).forEach((ring) => coordinates.push(...ring))
135
+ })
136
+ markers.forEach((marker) => {
137
+ if (marker.coordinate) coordinates.push(marker.coordinate)
138
+ })
139
+
140
+ if (coordinates.length === 0) return fallbackBounds
141
+
142
+ const xs = coordinates.map((coordinate) => coordinate[0])
143
+ const ys = coordinates.map((coordinate) => coordinate[1])
144
+ const minX = Math.min(...xs)
145
+ const maxX = Math.max(...xs)
146
+ const minY = Math.min(...ys)
147
+ const maxY = Math.max(...ys)
148
+ const width = Math.max(maxX - minX, Number.EPSILON)
149
+ const height = Math.max(maxY - minY, Number.EPSILON)
150
+
151
+ return {
152
+ minX,
153
+ maxX,
154
+ minY,
155
+ maxY,
156
+ width,
157
+ height,
158
+ aspectRatio: clamp(width / height, 0.5, 2),
159
+ }
160
+ }
161
+
162
+ function projectCoordinate(coordinate: ChoroplethMapCoordinate, bounds: GeometryBounds) {
163
+ return {
164
+ x: clamp(((coordinate[0] - bounds.minX) / bounds.width) * 100),
165
+ y: clamp(((bounds.maxY - coordinate[1]) / bounds.height) * 100),
166
+ }
167
+ }
168
+
169
+ function getRegionPieces(region: ChoroplethMapRegion, bounds: GeometryBounds) {
170
+ return getExteriorRings(region.geometry)
171
+ .map((ring) => {
172
+ const points = ring.map((coordinate) => projectCoordinate(coordinate, bounds))
173
+
174
+ if (points.length < 3) return null
175
+
176
+ const xs = points.map((point) => point.x)
177
+ const ys = points.map((point) => point.y)
178
+ const left = Math.min(...xs)
179
+ const right = Math.max(...xs)
180
+ const top = Math.min(...ys)
181
+ const bottom = Math.max(...ys)
182
+ const width = Math.max(right - left, 0.1)
183
+ const height = Math.max(bottom - top, 0.1)
184
+ const clipPath = `polygon(${points
185
+ .map(
186
+ (point) =>
187
+ `${((point.x - left) / width) * 100}% ${((point.y - top) / height) * 100}%`
188
+ )
189
+ .join(", ")})`
190
+
191
+ return {
192
+ left,
193
+ top,
194
+ width,
195
+ height,
196
+ clipPath,
197
+ }
198
+ })
199
+ .filter((piece): piece is RegionPiece => Boolean(piece))
200
+ }
201
+
202
+ function getMarkerPosition(marker: ChoroplethMapMarker, bounds: GeometryBounds) {
203
+ if (marker.coordinate) return projectCoordinate(marker.coordinate, bounds)
204
+
205
+ return {
206
+ x: clamp(marker.x ?? 0),
207
+ y: clamp(marker.y ?? 0),
208
+ }
209
+ }
210
+
211
+ const ChoroplethMap = React.forwardRef<HTMLDivElement, ChoroplethMapProps>(
212
+ (
213
+ {
214
+ className,
215
+ regions,
216
+ markers = [],
217
+ variant = choroplethMapDefaultVariantKey,
218
+ max,
219
+ color = "destructive",
220
+ selectedId,
221
+ showRanking = true,
222
+ rankingLimit = 6,
223
+ showSelectedRegion = true,
224
+ preserveAspectRatio = true,
225
+ formatValue = defaultChartValueFormatter,
226
+ selectedLabel = "Selected",
227
+ rankLabel = "Rank",
228
+ onRegionSelect,
229
+ onMarkerSelect,
230
+ ...props
231
+ },
232
+ ref
233
+ ) => {
234
+ const rootRef = React.useRef<HTMLDivElement | null>(null)
235
+ const [useSplitRankingLayout, setUseSplitRankingLayout] = React.useState(false)
236
+ const values = regions.map((region) => region.value)
237
+ const maxValue = Math.max(max ?? 0, ...values, 1)
238
+ const geometryBounds = computeBounds(regions, markers)
239
+ const regionEntries = regions.map((region, index) => ({
240
+ region,
241
+ index,
242
+ id: regionKey(region, index),
243
+ }))
244
+ const selectedRegionEntry = selectedId
245
+ ? regionEntries.find((entry) => entry.id === selectedId)
246
+ : undefined
247
+ const rankedRegionEntries = [...regionEntries]
248
+ .sort((a, b) => b.region.value - a.region.value)
249
+ const displayedRegionEntries = rankedRegionEntries.slice(0, rankingLimit)
250
+ const canSelectRegions = typeof onRegionSelect === "function"
251
+ const canSelectMarkers = typeof onMarkerSelect === "function"
252
+ const setRootRef = React.useCallback(
253
+ (node: HTMLDivElement | null) => {
254
+ rootRef.current = node
255
+ if (typeof ref === "function") {
256
+ ref(node)
257
+ return
258
+ }
259
+ if (ref) {
260
+ const mutableRef = ref as React.MutableRefObject<HTMLDivElement | null>
261
+ mutableRef.current = node
262
+ }
263
+ },
264
+ [ref]
265
+ )
266
+
267
+ React.useEffect(() => {
268
+ const element = rootRef.current
269
+ if (!element) return
270
+
271
+ const updateLayout = () => {
272
+ const next = element.getBoundingClientRect().width >= CHOROPLETH_MAP_SPLIT_LAYOUT_MIN_WIDTH
273
+ setUseSplitRankingLayout((current) => (current === next ? current : next))
274
+ }
275
+
276
+ updateLayout()
277
+
278
+ if (typeof ResizeObserver === "undefined") {
279
+ window.addEventListener("resize", updateLayout)
280
+ return () => window.removeEventListener("resize", updateLayout)
281
+ }
282
+
283
+ const observer = new ResizeObserver(updateLayout)
284
+ observer.observe(element)
285
+ return () => observer.disconnect()
286
+ }, [])
287
+
288
+ return (
289
+ <div
290
+ ref={setRootRef}
291
+ className={cn(choroplethMapVariantClasses[variant], "min-w-0", className)}
292
+ {...props}
293
+ >
294
+ <div
295
+ className={cn(
296
+ "grid min-w-0 gap-4",
297
+ showRanking && "items-start"
298
+ )}
299
+ style={
300
+ showRanking && useSplitRankingLayout
301
+ ? {
302
+ gridTemplateColumns:
303
+ "minmax(0, 1.35fr) minmax(min(42%, 18rem), 0.85fr)",
304
+ }
305
+ : undefined
306
+ }
307
+ >
308
+ <div className="grid min-w-0 gap-3">
309
+ <div
310
+ className={cn(
311
+ "relative min-w-0 overflow-hidden rounded-md border bg-muted/20",
312
+ choroplethMapPanelClasses[variant]
313
+ )}
314
+ >
315
+ <div className="absolute inset-3 flex items-center justify-center sm:inset-4">
316
+ <div
317
+ className={cn(
318
+ "relative max-h-full max-w-full",
319
+ preserveAspectRatio ? "h-full" : "h-full w-full"
320
+ )}
321
+ style={
322
+ preserveAspectRatio
323
+ ? { aspectRatio: geometryBounds.aspectRatio }
324
+ : undefined
325
+ }
326
+ >
327
+ <span
328
+ className="absolute inset-x-0 top-1/4 border-t border-dashed border-border/60"
329
+ aria-hidden="true"
330
+ />
331
+ <span
332
+ className="absolute inset-x-2 top-2/3 border-t border-dashed border-border/50"
333
+ aria-hidden="true"
334
+ />
335
+ <span
336
+ className="absolute inset-y-2 left-1/3 border-l border-dashed border-border/50"
337
+ aria-hidden="true"
338
+ />
339
+ <span
340
+ className="absolute inset-y-0 left-2/3 border-l border-dashed border-border/60"
341
+ aria-hidden="true"
342
+ />
343
+ {regions.map((region, index) => {
344
+ const id = regionKey(region, index)
345
+ const percent = normalizeChartValue(region.value, maxValue)
346
+ const activeColor = getChartColor(region.color ?? color, index)
347
+ const isSelected = selectedId === id
348
+ const pieces = getRegionPieces(region, geometryBounds)
349
+
350
+ return (
351
+ <React.Fragment key={id}>
352
+ {pieces.map((piece, pieceIndex) => {
353
+ const fillStrength = isSelected
354
+ ? 96
355
+ : 18 + (clamp(percent, 0, 100) / 100) * 72
356
+ const mapPieceStyle = {
357
+ left: `${piece.left}%`,
358
+ top: `${piece.top}%`,
359
+ width: `${piece.width}%`,
360
+ height: `${piece.height}%`,
361
+ clipPath: piece.clipPath,
362
+ backgroundColor:
363
+ mapPieceOutlineColor(isSelected),
364
+ }
365
+ const mapPieceFill = (
366
+ <span
367
+ className="pointer-events-none absolute inset-0 block"
368
+ style={{
369
+ clipPath: piece.clipPath,
370
+ backgroundColor: `color-mix(in srgb, ${activeColor} ${fillStrength}%, transparent)`,
371
+ transform: isSelected
372
+ ? "scale(0.965)"
373
+ : "scale(0.985)",
374
+ transformOrigin: "center",
375
+ }}
376
+ aria-hidden="true"
377
+ />
378
+ )
379
+ const mapPieceClassName = cn(
380
+ "absolute overflow-hidden transition-[opacity,box-shadow]",
381
+ canSelectRegions &&
382
+ "cursor-pointer hover:opacity-90 focus-visible:z-30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
383
+ isSelected && "z-20"
384
+ )
385
+
386
+ return (
387
+ <ChartTooltip
388
+ key={`${id}-${pieceIndex}`}
389
+ label={region.label}
390
+ value={formatValue(region.value)}
391
+ description={region.description}
392
+ >
393
+ {canSelectRegions ? (
394
+ <button
395
+ type="button"
396
+ className={mapPieceClassName}
397
+ style={mapPieceStyle}
398
+ aria-label={`${chartLabelToString(region.label)}: ${formatValue(region.value)}`}
399
+ aria-current={
400
+ isSelected
401
+ ? "true"
402
+ : undefined
403
+ }
404
+ onClick={() =>
405
+ onRegionSelect?.(
406
+ region,
407
+ id
408
+ )
409
+ }
410
+ >
411
+ {mapPieceFill}
412
+ </button>
413
+ ) : (
414
+ <span
415
+ className={mapPieceClassName}
416
+ style={mapPieceStyle}
417
+ tabIndex={0}
418
+ aria-label={`${chartLabelToString(region.label)}: ${formatValue(region.value)}`}
419
+ aria-current={
420
+ isSelected
421
+ ? "true"
422
+ : undefined
423
+ }
424
+ >
425
+ {mapPieceFill}
426
+ </span>
427
+ )}
428
+ </ChartTooltip>
429
+ )
430
+ })}
431
+ </React.Fragment>
432
+ )
433
+ })}
434
+ {markers.map((marker, index) => {
435
+ const id = markerKey(marker, index)
436
+ const position = getMarkerPosition(marker, geometryBounds)
437
+ const size =
438
+ marker.value === undefined
439
+ ? 0.9
440
+ : 0.8 +
441
+ (normalizeChartValue(marker.value, maxValue) /
442
+ 100) *
443
+ 1.1
444
+ const activeColor = getChartColor(marker.color ?? "muted", index)
445
+
446
+ const markerStyle = {
447
+ left: `${position.x}%`,
448
+ top: `${position.y}%`,
449
+ width: `${size}rem`,
450
+ height: `${size}rem`,
451
+ }
452
+ const markerLabel =
453
+ marker.value === undefined
454
+ ? marker.label
455
+ : `${marker.label}: ${formatValue(marker.value)}`
456
+ const markerClassName = cn(
457
+ "absolute z-30 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-foreground/40 bg-background shadow-sm transition-[filter,transform]",
458
+ canSelectMarkers &&
459
+ "cursor-pointer hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
460
+ )
461
+ const markerDot = (
462
+ <span
463
+ className="h-1.5 w-1.5 rounded-full"
464
+ style={{ backgroundColor: activeColor }}
465
+ aria-hidden="true"
466
+ />
467
+ )
468
+
469
+ return (
470
+ <ChartTooltip
471
+ key={id}
472
+ label={marker.label}
473
+ value={
474
+ marker.value === undefined
475
+ ? undefined
476
+ : formatValue(marker.value)
477
+ }
478
+ >
479
+ {canSelectMarkers ? (
480
+ <button
481
+ type="button"
482
+ className={markerClassName}
483
+ style={markerStyle}
484
+ aria-label={markerLabel}
485
+ onClick={() =>
486
+ onMarkerSelect?.(marker, id)
487
+ }
488
+ >
489
+ {markerDot}
490
+ </button>
491
+ ) : (
492
+ <span
493
+ className={markerClassName}
494
+ style={markerStyle}
495
+ tabIndex={0}
496
+ aria-label={markerLabel}
497
+ >
498
+ {markerDot}
499
+ </span>
500
+ )}
501
+ </ChartTooltip>
502
+ )
503
+ })}
504
+ </div>
505
+ </div>
506
+ </div>
507
+ {showSelectedRegion && selectedRegionEntry ? (
508
+ <div className="grid min-w-0 gap-1 rounded-md border bg-card p-3">
509
+ <div className="flex min-w-0 items-center justify-between gap-3">
510
+ <div className="min-w-0">
511
+ <div className="text-xs font-medium text-muted-foreground">
512
+ {selectedLabel}
513
+ </div>
514
+ <div className="truncate text-sm font-semibold">
515
+ {selectedRegionEntry.region.label}
516
+ </div>
517
+ {selectedRegionEntry.region.description ? (
518
+ <div className="truncate text-xs text-muted-foreground">
519
+ {selectedRegionEntry.region.description}
520
+ </div>
521
+ ) : null}
522
+ </div>
523
+ <span className="shrink-0 text-lg font-semibold tabular-nums">
524
+ {formatValue(selectedRegionEntry.region.value)}
525
+ </span>
526
+ </div>
527
+ <div className="relative h-1.5 overflow-hidden rounded-full bg-muted">
528
+ <span
529
+ className="absolute inset-y-0 left-0 rounded-full"
530
+ style={{
531
+ width: `${normalizeChartValue(selectedRegionEntry.region.value, maxValue)}%`,
532
+ backgroundColor: getChartColor(
533
+ selectedRegionEntry.region.color ?? color,
534
+ selectedRegionEntry.index
535
+ ),
536
+ }}
537
+ />
538
+ </div>
539
+ </div>
540
+ ) : null}
541
+ </div>
542
+ {showRanking ? (
543
+ <ol className="grid min-w-0 content-start gap-2">
544
+ {displayedRegionEntries.map(({ region, id }, index) => {
545
+ const percent = normalizeChartValue(region.value, maxValue)
546
+ const activeColor = getChartColor(region.color ?? color, index)
547
+ const isSelected = selectedId === id
548
+
549
+ return (
550
+ <li
551
+ key={id}
552
+ aria-current={isSelected ? "true" : undefined}
553
+ >
554
+ <button
555
+ type="button"
556
+ className={cn(
557
+ "grid w-full min-w-0 gap-2 rounded-md border bg-card p-2 text-left transition-colors",
558
+ canSelectRegions &&
559
+ "cursor-pointer hover:border-primary-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
560
+ !canSelectRegions && "cursor-default",
561
+ isSelected && "ring-1 ring-foreground"
562
+ )}
563
+ disabled={!canSelectRegions}
564
+ onClick={() => onRegionSelect?.(region, id)}
565
+ >
566
+ <span className="grid min-w-0 grid-cols-[2rem_minmax(0,1fr)_auto] items-center gap-3">
567
+ <span
568
+ className={cn(
569
+ "flex h-8 w-8 items-center justify-center rounded-md bg-muted text-xs font-semibold tabular-nums text-muted-foreground",
570
+ index < 3 &&
571
+ "bg-primary-subtle text-primary-subtle-foreground"
572
+ )}
573
+ aria-label={`${chartLabelToString(rankLabel, "Rank")} ${index + 1}`}
574
+ >
575
+ {index + 1}
576
+ </span>
577
+ <span className="min-w-0">
578
+ <span className="block truncate text-sm font-medium">
579
+ {region.label}
580
+ </span>
581
+ {region.description ? (
582
+ <span className="block truncate text-xs text-muted-foreground">
583
+ {region.description}
584
+ </span>
585
+ ) : null}
586
+ </span>
587
+ <span className="shrink-0 text-sm font-semibold tabular-nums">
588
+ {formatValue(region.value)}
589
+ </span>
590
+ </span>
591
+ <span className="relative h-1.5 overflow-hidden rounded-full bg-muted">
592
+ <span
593
+ className="absolute inset-y-0 left-0 rounded-full"
594
+ style={{
595
+ width: `${percent}%`,
596
+ backgroundColor: activeColor,
597
+ }}
598
+ />
599
+ </span>
600
+ </button>
601
+ </li>
602
+ )
603
+ })}
604
+ </ol>
605
+ ) : null}
606
+ </div>
607
+ </div>
608
+ )
609
+ }
610
+ )
611
+ ChoroplethMap.displayName = "ChoroplethMap"
612
+
613
+ export { ChoroplethMap }
@@ -0,0 +1,42 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "../../lib/utils"
5
+
6
+ export const codeVariants = cva(
7
+ "relative rounded font-mono text-sm font-medium",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-muted text-foreground px-[0.4rem] py-[0.15rem]",
12
+ muted: "bg-secondary text-muted-foreground border border-border px-[0.4rem] py-[0.15rem]",
13
+ },
14
+ size: {
15
+ sm: "text-xs",
16
+ default: "text-sm",
17
+ lg: "text-base",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ size: "default",
23
+ },
24
+ }
25
+ )
26
+
27
+ export interface CodeProps
28
+ extends React.HTMLAttributes<HTMLElement>,
29
+ VariantProps<typeof codeVariants> {}
30
+
31
+ const Code = React.forwardRef<HTMLElement, CodeProps>(
32
+ ({ className, variant, size, ...props }, ref) => (
33
+ <code
34
+ ref={ref}
35
+ className={cn(codeVariants({ variant, size }), className)}
36
+ {...props}
37
+ />
38
+ )
39
+ )
40
+ Code.displayName = "Code"
41
+
42
+ export { Code }