@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,219 @@
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 { ChartFloatingTooltip } from "./chart-tooltip"
15
+ import type { GaugeChartVariantKey } from "./generated/variant-keys"
16
+ import { gaugeChartDefaultVariantKey } from "./generated/default-variant-keys"
17
+
18
+ export interface GaugeChartProps extends React.HTMLAttributes<HTMLDivElement> {
19
+ value: number
20
+ min?: number
21
+ max?: number
22
+ variant?: GaugeChartVariantKey
23
+ color?: ChartColor
24
+ label?: React.ReactNode
25
+ valueLabel?: React.ReactNode
26
+ thickness?: number
27
+ formatValue?: (value: number) => React.ReactNode
28
+ rangeLabel?: React.ReactNode
29
+ }
30
+
31
+ const gaugeChartVariantClasses: Record<GaugeChartVariantKey, string> = {
32
+ compact: "h-[112px] w-full p-0",
33
+ default: "h-[144px] w-full p-0",
34
+ }
35
+
36
+ const gaugeChartSizeClasses: Record<GaugeChartVariantKey, string> = {
37
+ compact: "max-w-44",
38
+ default: "max-w-56",
39
+ }
40
+
41
+ const GaugeChart = React.forwardRef<HTMLDivElement, GaugeChartProps>(
42
+ (
43
+ {
44
+ className,
45
+ value,
46
+ min = 0,
47
+ max = 100,
48
+ variant = gaugeChartDefaultVariantKey,
49
+ color,
50
+ label,
51
+ valueLabel,
52
+ thickness = variant === "compact" ? 16 : 22,
53
+ formatValue = defaultChartValueFormatter,
54
+ rangeLabel = "Range",
55
+ ...props
56
+ },
57
+ ref
58
+ ) => {
59
+ const [tooltipOpen, setTooltipOpen] = React.useState(false)
60
+ const [tooltipPosition, setTooltipPosition] = React.useState({
61
+ x: 50,
62
+ y: 24,
63
+ })
64
+ const chartRef = React.useRef<HTMLDivElement | null>(null)
65
+ const touchTooltipStickyRef = React.useRef(false)
66
+ const range = Math.max(max - min, 1)
67
+ const normalizedValue = clamp(value, min, max)
68
+ const percent = normalizeChartValue(normalizedValue - min, range)
69
+ const degrees = (percent / 100) * 180
70
+ const activeColor = getChartColor(color, 0)
71
+ const trackColor = "hsl(var(--muted))"
72
+ const background = `conic-gradient(from 270deg, ${activeColor} 0deg ${degrees}deg, ${trackColor} ${degrees}deg 180deg, transparent 180deg 360deg)`
73
+ const formattedValue = formatValue(normalizedValue)
74
+ const displayValue = valueLabel ?? formattedValue
75
+ const rangeValueLabel = `${formatValue(min)} - ${formatValue(max)}`
76
+ const ariaValue = chartLabelToString(
77
+ displayValue,
78
+ chartLabelToString(formattedValue, String(normalizedValue))
79
+ )
80
+ const ariaLabel = `${chartLabelToString(label, "Gauge")}: ${ariaValue} (${chartLabelToString(rangeLabel, "Range")}: ${rangeValueLabel})`
81
+
82
+ const updateTooltipAtPoint = (
83
+ element: HTMLDivElement,
84
+ clientX: number,
85
+ clientY: number
86
+ ) => {
87
+ const rect = element.getBoundingClientRect()
88
+ const radius = rect.width / 2
89
+ const centerX = rect.left + radius
90
+ const centerY = rect.top + radius
91
+ const dx = clientX - centerX
92
+ const dy = clientY - centerY
93
+ const distance = Math.sqrt(dx * dx + dy * dy)
94
+ const innerRadius = Math.max(radius - thickness, 0)
95
+ const angle = (Math.atan2(dy, dx) * 180) / Math.PI
96
+ const cssAngle = (((angle + 90) % 360) + 360) % 360
97
+ const sweep = (cssAngle - 270 + 360) % 360
98
+ const isOnActiveArc =
99
+ distance >= innerRadius &&
100
+ distance <= radius &&
101
+ sweep <= degrees &&
102
+ sweep <= 180
103
+
104
+ if (!isOnActiveArc) {
105
+ setTooltipOpen(false)
106
+ return
107
+ }
108
+
109
+ setTooltipPosition({
110
+ x: clamp(((clientX - rect.left) / rect.width) * 100),
111
+ y: clamp(((clientY - rect.top) / rect.height) * 100),
112
+ })
113
+ setTooltipOpen(true)
114
+ }
115
+ const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
116
+ if (event.pointerType !== "touch") {
117
+ touchTooltipStickyRef.current = false
118
+ }
119
+ updateTooltipAtPoint(event.currentTarget, event.clientX, event.clientY)
120
+ }
121
+ const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
122
+ touchTooltipStickyRef.current = event.pointerType === "touch"
123
+ if (event.pointerType === "touch") {
124
+ event.preventDefault()
125
+ }
126
+ event.currentTarget.focus({ preventScroll: true })
127
+ updateTooltipAtPoint(event.currentTarget, event.clientX, event.clientY)
128
+ }
129
+ const handleTouchStart = (event: React.TouchEvent<HTMLDivElement>) => {
130
+ const touch = event.touches[0]
131
+ if (!touch) return
132
+ touchTooltipStickyRef.current = true
133
+ event.preventDefault()
134
+ event.currentTarget.focus({ preventScroll: true })
135
+ updateTooltipAtPoint(event.currentTarget, touch.clientX, touch.clientY)
136
+ }
137
+
138
+ return (
139
+ <div
140
+ ref={ref}
141
+ className={cn(
142
+ gaugeChartVariantClasses[variant],
143
+ "flex flex-col items-center justify-end",
144
+ className
145
+ )}
146
+ {...props}
147
+ >
148
+ <div
149
+ ref={chartRef}
150
+ className={cn(
151
+ "relative aspect-[2/1] w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
152
+ gaugeChartSizeClasses[variant]
153
+ )}
154
+ role="img"
155
+ aria-label={props["aria-label"] ?? ariaLabel}
156
+ tabIndex={0}
157
+ onFocus={() => {
158
+ setTooltipPosition({ x: percent, y: 24 })
159
+ setTooltipOpen(true)
160
+ }}
161
+ onBlur={() => {
162
+ touchTooltipStickyRef.current = false
163
+ setTooltipOpen(false)
164
+ }}
165
+ onPointerDown={handlePointerDown}
166
+ onPointerMove={handlePointerMove}
167
+ onTouchStart={handleTouchStart}
168
+ onPointerLeave={() => {
169
+ if (touchTooltipStickyRef.current) return
170
+ setTooltipOpen(false)
171
+ }}
172
+ onPointerCancel={() => {
173
+ touchTooltipStickyRef.current = false
174
+ setTooltipOpen(false)
175
+ }}
176
+ >
177
+ <div
178
+ className="pointer-events-none absolute inset-0 overflow-hidden"
179
+ aria-hidden="true"
180
+ >
181
+ <div
182
+ className="absolute inset-x-0 top-0 aspect-square rounded-full"
183
+ style={{ background }}
184
+ />
185
+ <div
186
+ className="absolute rounded-full bg-card"
187
+ style={{
188
+ insetInline: thickness,
189
+ top: thickness,
190
+ width: `calc(100% - ${thickness * 2}px)`,
191
+ aspectRatio: "1 / 1",
192
+ }}
193
+ />
194
+ </div>
195
+ <div className="pointer-events-none absolute inset-x-0 bottom-0 flex flex-col items-center px-4 text-center">
196
+ <span className="text-2xl font-semibold tracking-tight">
197
+ {valueLabel ?? formatValue(normalizedValue)}
198
+ </span>
199
+ {label !== undefined ? (
200
+ <span className="text-xs text-muted-foreground">{label}</span>
201
+ ) : null}
202
+ </div>
203
+ <ChartFloatingTooltip
204
+ label={label}
205
+ value={displayValue}
206
+ description={[rangeLabel, ": ", rangeValueLabel]}
207
+ position={tooltipPosition}
208
+ open={tooltipOpen}
209
+ anchorRef={chartRef}
210
+ onOpenChange={setTooltipOpen}
211
+ />
212
+ </div>
213
+ </div>
214
+ )
215
+ }
216
+ )
217
+ GaugeChart.displayName = "GaugeChart"
218
+
219
+ export { GaugeChart }
@@ -0,0 +1,266 @@
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
+ clamp,
9
+ defaultChartValueFormatter,
10
+ getChartColor,
11
+ normalizeChartValue,
12
+ } from "./chart-utils"
13
+ import { ChartTooltip } from "./chart-tooltip"
14
+ import type { HeatmapChartVariantKey } from "./generated/variant-keys"
15
+ import { heatmapChartDefaultVariantKey } from "./generated/default-variant-keys"
16
+
17
+ export interface HeatmapChartCell {
18
+ x: string
19
+ y: string
20
+ value: number
21
+ color?: ChartColor
22
+ description?: React.ReactNode
23
+ }
24
+
25
+ export interface HeatmapChartSummary {
26
+ x: string
27
+ value: number
28
+ color?: ChartColor
29
+ description?: React.ReactNode
30
+ }
31
+
32
+ export interface HeatmapChartProps extends React.HTMLAttributes<HTMLDivElement> {
33
+ data: HeatmapChartCell[]
34
+ xLabels: string[]
35
+ yLabels: string[]
36
+ summary?: HeatmapChartSummary[]
37
+ variant?: HeatmapChartVariantKey
38
+ max?: number
39
+ summaryMax?: number
40
+ color?: ChartColor
41
+ selectedCell?: { x: string; y: string }
42
+ showValues?: boolean
43
+ showSummaryValues?: boolean
44
+ formatValue?: (value: number) => React.ReactNode
45
+ summaryLabel?: React.ReactNode
46
+ onCellSelect?: (cell: HeatmapChartCell, selection: { x: string; y: string }) => void
47
+ }
48
+
49
+ const heatmapChartVariantClasses: Record<HeatmapChartVariantKey, string> = {
50
+ compact: "w-full p-0",
51
+ default: "w-full p-0",
52
+ }
53
+
54
+ const heatmapCellClasses: Record<HeatmapChartVariantKey, string> = {
55
+ compact: "min-h-6 rounded",
56
+ default: "min-h-8 rounded-md",
57
+ }
58
+
59
+ const heatmapLabelColumnWidth = "2.75rem"
60
+
61
+ const HeatmapChart = React.forwardRef<HTMLDivElement, HeatmapChartProps>(
62
+ (
63
+ {
64
+ className,
65
+ data,
66
+ xLabels,
67
+ yLabels,
68
+ summary,
69
+ variant = heatmapChartDefaultVariantKey,
70
+ max,
71
+ summaryMax,
72
+ color,
73
+ selectedCell,
74
+ showValues = false,
75
+ showSummaryValues = true,
76
+ formatValue = defaultChartValueFormatter,
77
+ summaryLabel,
78
+ onCellSelect,
79
+ ...props
80
+ },
81
+ ref
82
+ ) => {
83
+ const values = data.map((cell) => cell.value)
84
+ const maxValue = Math.max(max ?? 0, ...values, 1)
85
+ const summaryValues = summary?.map((item) => item.value) ?? []
86
+ const maxSummaryValue = Math.max(summaryMax ?? 0, ...summaryValues, 1)
87
+ const cellMap = new Map(data.map((cell) => [`${cell.x}::${cell.y}`, cell]))
88
+ const summaryMap = new Map(summary?.map((item) => [item.x, item]) ?? [])
89
+ const canSelectCells = typeof onCellSelect === "function"
90
+
91
+ return (
92
+ <div
93
+ ref={ref}
94
+ className={cn(
95
+ heatmapChartVariantClasses[variant],
96
+ "min-w-0",
97
+ className
98
+ )}
99
+ {...props}
100
+ >
101
+ {summary ? (
102
+ <div
103
+ className="mb-2 grid w-full min-w-0 items-end gap-1"
104
+ style={{
105
+ gridTemplateColumns: `${heatmapLabelColumnWidth} repeat(${xLabels.length}, minmax(0, 1fr))`,
106
+ }}
107
+ >
108
+ <span className="truncate pr-1 text-right text-xs text-muted-foreground">
109
+ {summaryLabel}
110
+ </span>
111
+ {xLabels.map((label, index) => {
112
+ const item = summaryMap.get(label)
113
+ const value = item?.value ?? 0
114
+ const percent = normalizeChartValue(value, maxSummaryValue)
115
+ const activeColor = getChartColor(
116
+ item?.color ?? color ?? "primary",
117
+ index
118
+ )
119
+
120
+ return (
121
+ <ChartTooltip
122
+ key={`${label}-summary`}
123
+ label={label}
124
+ value={formatValue(value)}
125
+ description={item?.description}
126
+ >
127
+ <span
128
+ className="flex min-h-14 min-w-0 flex-col items-center justify-end gap-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
129
+ tabIndex={0}
130
+ aria-label={`${label}: ${formatValue(value)}`}
131
+ >
132
+ {showSummaryValues ? (
133
+ <span className="truncate text-[10px] font-medium tabular-nums text-muted-foreground">
134
+ {formatValue(value)}
135
+ </span>
136
+ ) : null}
137
+ <span className="flex h-10 w-full items-end justify-center">
138
+ <span
139
+ className="block w-3 rounded-full"
140
+ style={{
141
+ height: `${Math.max(percent, 6)}%`,
142
+ backgroundColor: activeColor,
143
+ opacity: 0.35 + (clamp(percent, 0, 100) / 100) * 0.65,
144
+ }}
145
+ aria-hidden="true"
146
+ />
147
+ </span>
148
+ </span>
149
+ </ChartTooltip>
150
+ )
151
+ })}
152
+ </div>
153
+ ) : null}
154
+ <div
155
+ className="grid w-full min-w-0 items-center gap-1"
156
+ style={{
157
+ gridTemplateColumns: `${heatmapLabelColumnWidth} repeat(${xLabels.length}, minmax(0, 1fr))`,
158
+ }}
159
+ >
160
+ <span aria-hidden="true" />
161
+ {xLabels.map((label) => (
162
+ <span
163
+ key={label}
164
+ className="min-w-0 truncate text-center text-xs text-muted-foreground"
165
+ >
166
+ {label}
167
+ </span>
168
+ ))}
169
+ {yLabels.map((rowLabel) => (
170
+ <React.Fragment key={rowLabel}>
171
+ <span className="min-w-0 truncate pr-1 text-right text-xs text-muted-foreground">
172
+ {rowLabel}
173
+ </span>
174
+ {xLabels.map((columnLabel) => {
175
+ const cell = cellMap.get(`${columnLabel}::${rowLabel}`)
176
+ const value = cell?.value ?? 0
177
+ const percent = normalizeChartValue(value, maxValue)
178
+ const opacity = 0.1 + (clamp(percent, 0, 100) / 100) * 0.9
179
+ const cellColor = cell?.color ?? color ?? "primary"
180
+ const activeColor = getChartColor(
181
+ cellColor,
182
+ 0
183
+ )
184
+ const isSelected =
185
+ selectedCell?.x === columnLabel &&
186
+ selectedCell?.y === rowLabel
187
+ const cellContent = (
188
+ <>
189
+ <span
190
+ className="absolute inset-0"
191
+ style={{
192
+ backgroundColor: activeColor,
193
+ opacity,
194
+ }}
195
+ aria-hidden="true"
196
+ />
197
+ {showValues ? (
198
+ <span
199
+ className={cn(
200
+ "absolute inset-0 z-10 flex items-center justify-center px-0.5 text-center text-foreground"
201
+ )}
202
+ >
203
+ <span className="rounded-[3px] bg-background/75 px-1 shadow-sm">
204
+ {formatValue(value)}
205
+ </span>
206
+ </span>
207
+ ) : null}
208
+ </>
209
+ )
210
+ const cellClassName = cn(
211
+ "relative block overflow-hidden bg-muted text-center text-[10px] font-medium tabular-nums",
212
+ "w-full min-w-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
213
+ canSelectCells &&
214
+ "cursor-pointer border-0 p-0 transition-transform hover:scale-[1.04]",
215
+ !canSelectCells && "cursor-default",
216
+ heatmapCellClasses[variant],
217
+ isSelected &&
218
+ "z-10 ring-2 ring-foreground ring-offset-1 ring-offset-background"
219
+ )
220
+
221
+ return (
222
+ <ChartTooltip
223
+ key={`${columnLabel}-${rowLabel}`}
224
+ label={`${columnLabel} ${rowLabel}`}
225
+ value={formatValue(value)}
226
+ description={cell?.description}
227
+ >
228
+ {canSelectCells && cell ? (
229
+ <button
230
+ type="button"
231
+ className={cellClassName}
232
+ aria-label={`${columnLabel} ${rowLabel}: ${formatValue(value)}`}
233
+ aria-current={isSelected ? "true" : undefined}
234
+ aria-pressed={isSelected}
235
+ onClick={() =>
236
+ onCellSelect?.(cell, {
237
+ x: columnLabel,
238
+ y: rowLabel,
239
+ })
240
+ }
241
+ >
242
+ {cellContent}
243
+ </button>
244
+ ) : (
245
+ <span
246
+ className={cellClassName}
247
+ tabIndex={0}
248
+ aria-label={`${columnLabel} ${rowLabel}: ${formatValue(value)}`}
249
+ aria-current={isSelected ? "true" : undefined}
250
+ >
251
+ {cellContent}
252
+ </span>
253
+ )}
254
+ </ChartTooltip>
255
+ )
256
+ })}
257
+ </React.Fragment>
258
+ ))}
259
+ </div>
260
+ </div>
261
+ )
262
+ }
263
+ )
264
+ HeatmapChart.displayName = "HeatmapChart"
265
+
266
+ export { HeatmapChart }
@@ -0,0 +1,66 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+ import type { IconVariantKey } from "./generated/variant-keys"
7
+ import { iconDefaultVariantKey } from "./generated/default-variant-keys"
8
+
9
+ export type IconGlyph = React.ForwardRefExoticComponent<
10
+ Omit<React.SVGProps<SVGSVGElement>, "ref" | "stroke"> & {
11
+ size?: string | number
12
+ stroke?: string | number
13
+ } & React.RefAttributes<SVGSVGElement>
14
+ >
15
+
16
+ export interface IconProps
17
+ extends Omit<React.SVGProps<SVGSVGElement>, "children" | "size" | "strokeWidth"> {
18
+ icon: IconGlyph
19
+ size?: IconVariantKey
20
+ label?: string
21
+ decorative?: boolean
22
+ strokeWidth?: number
23
+ }
24
+
25
+ const iconSizeClasses: Record<IconVariantKey, string> = {
26
+ xs: "h-3 w-3",
27
+ sm: "h-4 w-4",
28
+ md: "h-5 w-5",
29
+ lg: "h-6 w-6",
30
+ xl: "h-8 w-8",
31
+ }
32
+
33
+ const Icon = React.forwardRef<SVGSVGElement, IconProps>(
34
+ (
35
+ {
36
+ icon: Glyph,
37
+ size = iconDefaultVariantKey,
38
+ label,
39
+ decorative = true,
40
+ strokeWidth = 2,
41
+ className,
42
+ role,
43
+ "aria-hidden": ariaHidden,
44
+ ...props
45
+ },
46
+ ref
47
+ ) => {
48
+ const isDecorative = decorative && !label
49
+
50
+ return (
51
+ <Glyph
52
+ ref={ref}
53
+ aria-hidden={isDecorative ? true : ariaHidden}
54
+ aria-label={isDecorative ? undefined : label}
55
+ role={isDecorative ? role : role ?? "img"}
56
+ focusable="false"
57
+ stroke={strokeWidth}
58
+ className={cn("shrink-0 p-0 text-current", iconSizeClasses[size], className)}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+ )
64
+ Icon.displayName = "Icon"
65
+
66
+ export { Icon }
@@ -0,0 +1,140 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconMaximize as Maximize, IconPhoto as Photo } from "@tabler/icons-react"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip"
8
+ import { Icon } from "./Icon"
9
+ import { Img, type ImgProps } from "./Img"
10
+ import { imagePreviewDefaultVariantKey } from "./generated/default-variant-keys"
11
+ import type { ImagePreviewVariantKey } from "./generated/variant-keys"
12
+
13
+ export interface ImagePreviewProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
14
+ src?: string
15
+ alt?: string
16
+ variant?: ImagePreviewVariantKey
17
+ aspectRatio?: ImgProps["aspectRatio"]
18
+ objectFit?: ImgProps["objectFit"]
19
+ loading?: ImgProps["loading"]
20
+ fallback?: React.ReactNode
21
+ noImageLabel?: string
22
+ showSkeleton?: boolean
23
+ previewLabel?: string
24
+ portalContainer?: HTMLElement | null
25
+ onPreview?: () => void
26
+ children?: React.ReactNode
27
+ imgProps?: Omit<ImgProps, "src" | "alt" | "aspectRatio" | "objectFit" | "fallback" | "showSkeleton" | "className">
28
+ }
29
+
30
+ const imagePreviewVariantClasses: Record<ImagePreviewVariantKey, string> = {
31
+ default: "bg-muted/40",
32
+ contain: "bg-muted/30",
33
+ empty: "bg-muted/30",
34
+ }
35
+
36
+ const imagePreviewAspectRatioClasses: Record<NonNullable<ImgProps["aspectRatio"]>, string> = {
37
+ square: "aspect-square",
38
+ video: "aspect-video",
39
+ portrait: "aspect-[3/4]",
40
+ wide: "aspect-[21/9]",
41
+ auto: "min-h-40",
42
+ }
43
+
44
+ function DefaultNoImagePreview({ alt, label }: { alt?: string; label: string }) {
45
+ return (
46
+ <div className="flex h-full w-full flex-col items-center justify-center gap-3 bg-muted/60 p-4 text-center text-muted-foreground">
47
+ <div className="flex h-12 w-12 items-center justify-center rounded-lg border border-dashed border-muted-foreground/35 bg-background/70">
48
+ <Icon icon={Photo} size="lg" />
49
+ </div>
50
+ <div className="min-w-0 space-y-1">
51
+ <div className="text-xs font-medium text-foreground/70">{label}</div>
52
+ {alt ? (
53
+ <div className="max-w-[12rem] truncate text-[11px] leading-none text-muted-foreground">{alt}</div>
54
+ ) : null}
55
+ </div>
56
+ </div>
57
+ )
58
+ }
59
+
60
+ const ImagePreview = React.forwardRef<HTMLDivElement, ImagePreviewProps>(
61
+ (
62
+ {
63
+ src,
64
+ alt,
65
+ variant = imagePreviewDefaultVariantKey,
66
+ aspectRatio = "square",
67
+ objectFit,
68
+ loading = "lazy",
69
+ fallback,
70
+ noImageLabel = "Image not found",
71
+ showSkeleton = true,
72
+ previewLabel = "Open preview",
73
+ portalContainer,
74
+ onPreview,
75
+ children,
76
+ imgProps,
77
+ className,
78
+ ...props
79
+ },
80
+ ref
81
+ ) => {
82
+ const effectiveVariant = src ? variant : "empty"
83
+ const effectiveObjectFit = objectFit ?? (effectiveVariant === "contain" ? "contain" : "cover")
84
+ const noImageFallback = fallback ?? <DefaultNoImagePreview alt={alt} label={noImageLabel} />
85
+
86
+ return (
87
+ <div
88
+ ref={ref}
89
+ className={cn(
90
+ "relative flex items-center justify-center overflow-hidden border border-border p-0 shadow-sm transition-colors",
91
+ imagePreviewVariantClasses[effectiveVariant],
92
+ imagePreviewAspectRatioClasses[aspectRatio],
93
+ className
94
+ )}
95
+ {...props}
96
+ >
97
+ {src ? (
98
+ <Img
99
+ src={src}
100
+ alt={alt}
101
+ aspectRatio={aspectRatio}
102
+ objectFit={effectiveObjectFit}
103
+ fallback={noImageFallback}
104
+ showSkeleton={showSkeleton}
105
+ className="h-full w-full rounded-none bg-transparent"
106
+ {...imgProps}
107
+ loading={loading}
108
+ />
109
+ ) : (
110
+ noImageFallback
111
+ )}
112
+
113
+ {children}
114
+
115
+ {onPreview ? (
116
+ <Tooltip>
117
+ <TooltipTrigger asChild>
118
+ <button
119
+ type="button"
120
+ className="absolute bottom-2 right-2 z-20 inline-flex h-8 w-8 items-center justify-center rounded-md border bg-background/95 text-muted-foreground opacity-0 shadow-sm transition-opacity hover:text-foreground focus:opacity-100 group-hover:opacity-100"
121
+ aria-label={previewLabel}
122
+ onClick={(event) => {
123
+ event.stopPropagation()
124
+ onPreview()
125
+ }}
126
+ >
127
+ <Icon icon={Maximize} size="sm" />
128
+ </button>
129
+ </TooltipTrigger>
130
+ <TooltipContent portalContainer={portalContainer}>{previewLabel}</TooltipContent>
131
+ </Tooltip>
132
+ ) : null}
133
+ </div>
134
+ )
135
+ }
136
+ )
137
+
138
+ ImagePreview.displayName = "ImagePreview"
139
+
140
+ export { ImagePreview }