@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,393 @@
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
+ defaultChartValueFormatter,
10
+ getChartColor,
11
+ normalizeChartValue,
12
+ } from "./chart-utils"
13
+ import { ChartLegend } from "./ChartLegend"
14
+ import { ChartTooltip } from "./chart-tooltip"
15
+ import type { StackedBarChartVariantKey } from "./generated/variant-keys"
16
+ import { stackedBarChartDefaultVariantKey } from "./generated/default-variant-keys"
17
+
18
+ export interface StackedBarChartSegment {
19
+ label?: React.ReactNode
20
+ value: number
21
+ color?: ChartColor
22
+ }
23
+
24
+ export interface StackedBarChartGroup {
25
+ label?: React.ReactNode
26
+ segments: StackedBarChartSegment[]
27
+ }
28
+
29
+ export interface StackedBarChartProps
30
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
31
+ data: StackedBarChartGroup[]
32
+ variant?: StackedBarChartVariantKey
33
+ max?: number
34
+ normalize?: boolean
35
+ showGrid?: boolean
36
+ showLabels?: boolean
37
+ showValues?: boolean
38
+ showLegend?: boolean
39
+ formatValue?: (value: number) => React.ReactNode
40
+ totalLabel?: React.ReactNode
41
+ }
42
+
43
+ const stackedBarChartVariantClasses: Record<StackedBarChartVariantKey, string> = {
44
+ horizontal: "h-[224px] w-full p-0",
45
+ vertical: "h-[224px] w-full p-0",
46
+ }
47
+
48
+ function getPositiveValue(value: number) {
49
+ return Number.isFinite(value) ? Math.max(value, 0) : 0
50
+ }
51
+
52
+ function getGroupTotal(group: StackedBarChartGroup) {
53
+ return group.segments.reduce(
54
+ (sum, segment) => sum + getPositiveValue(segment.value),
55
+ 0
56
+ )
57
+ }
58
+
59
+ function getMaxTotal(data: StackedBarChartGroup[], max?: number) {
60
+ return Math.max(max ?? 0, ...data.map(getGroupTotal), 1)
61
+ }
62
+
63
+ function getHorizontalGridClass(showLabels: boolean, showValues: boolean) {
64
+ if (showLabels && showValues) {
65
+ return "grid-cols-[minmax(0,0.8fr)_minmax(0,2.8fr)_auto]"
66
+ }
67
+
68
+ if (showLabels) {
69
+ return "grid-cols-[minmax(0,1fr)_minmax(0,3fr)]"
70
+ }
71
+
72
+ if (showValues) {
73
+ return "grid-cols-[minmax(0,1fr)_auto]"
74
+ }
75
+
76
+ return "grid-cols-1"
77
+ }
78
+
79
+ function getStackedSegmentDescription(
80
+ totalLabel: React.ReactNode,
81
+ total: number,
82
+ percentText: string,
83
+ formatValue: (value: number) => React.ReactNode
84
+ ) {
85
+ return (
86
+ <>
87
+ {totalLabel}: {formatValue(total)} / {percentText}
88
+ </>
89
+ )
90
+ }
91
+
92
+ function getLegendItems(
93
+ data: StackedBarChartGroup[],
94
+ formatValue: (value: number) => React.ReactNode,
95
+ totalLabel: React.ReactNode
96
+ ) {
97
+ const segmentCount = Math.max(0, ...data.map((group) => group.segments.length))
98
+ const grandTotal = Math.max(
99
+ data.reduce((sum, group) => sum + getGroupTotal(group), 0),
100
+ 1
101
+ )
102
+
103
+ return Array.from({ length: segmentCount }, (_, index) => {
104
+ const firstSegment = data.find((group) => group.segments[index])?.segments[index]
105
+ const total = data.reduce(
106
+ (sum, group) => sum + getPositiveValue(group.segments[index]?.value ?? 0),
107
+ 0
108
+ )
109
+ const percentText = `${defaultChartValueFormatter((total / grandTotal) * 100)}%`
110
+
111
+ return {
112
+ label: firstSegment?.label ?? `Segment ${index + 1}`,
113
+ value: formatValue(total),
114
+ color: firstSegment?.color,
115
+ description: [totalLabel, ": ", formatValue(grandTotal), " / ", percentText],
116
+ }
117
+ })
118
+ }
119
+
120
+ const StackedBarChart = React.forwardRef<HTMLDivElement, StackedBarChartProps>(
121
+ (
122
+ {
123
+ className,
124
+ data,
125
+ variant = stackedBarChartDefaultVariantKey,
126
+ max,
127
+ normalize = false,
128
+ showGrid = true,
129
+ showLabels = true,
130
+ showValues = false,
131
+ showLegend = false,
132
+ formatValue = defaultChartValueFormatter,
133
+ totalLabel = "Total",
134
+ ...props
135
+ },
136
+ ref
137
+ ) => {
138
+ const maxTotal = getMaxTotal(data, max)
139
+ const legendItems = getLegendItems(data, formatValue, totalLabel)
140
+ const shouldConstrainVerticalTrack = variant === "vertical" && data.length <= 5
141
+ const verticalTrackStyle: React.CSSProperties = {
142
+ width: "100%",
143
+ maxWidth: shouldConstrainVerticalTrack
144
+ ? `${Math.max(280, data.length * 88)}px`
145
+ : undefined,
146
+ marginInline: shouldConstrainVerticalTrack ? "auto" : undefined,
147
+ }
148
+ const verticalGridStyle: React.CSSProperties = {
149
+ ...verticalTrackStyle,
150
+ gridTemplateColumns: `repeat(${data.length}, minmax(0, 1fr))`,
151
+ }
152
+ const verticalBarMaxWidth =
153
+ data.length <= 3 ? "4.5rem" : data.length <= 5 ? "4rem" : "3rem"
154
+
155
+ if (variant === "horizontal") {
156
+ return (
157
+ <div
158
+ ref={ref}
159
+ className={cn(
160
+ stackedBarChartVariantClasses[variant],
161
+ "flex flex-col justify-center gap-3",
162
+ className
163
+ )}
164
+ {...props}
165
+ >
166
+ {data.map((group, groupIndex) => {
167
+ const total = getGroupTotal(group)
168
+ const stackPercent = normalize
169
+ ? 100
170
+ : normalizeChartValue(total, maxTotal)
171
+
172
+ return (
173
+ <div
174
+ key={`${chartLabelToString(group.label, "Group")}-${groupIndex}`}
175
+ className={cn(
176
+ "grid min-w-0 items-center gap-3 text-sm",
177
+ getHorizontalGridClass(showLabels, showValues)
178
+ )}
179
+ >
180
+ {showLabels ? (
181
+ <span className="truncate text-muted-foreground">
182
+ {group.label}
183
+ </span>
184
+ ) : null}
185
+ <div className="relative h-4 min-w-0 rounded-full bg-muted">
186
+ {showGrid && !normalize
187
+ ? [25, 50, 75].map((percent) => (
188
+ <span
189
+ key={percent}
190
+ className="pointer-events-none absolute inset-y-0 border-l border-dashed border-border/70"
191
+ style={{ left: `${percent}%` }}
192
+ aria-hidden="true"
193
+ />
194
+ ))
195
+ : null}
196
+ <div
197
+ className="flex h-full overflow-hidden rounded-full"
198
+ style={{ width: `${stackPercent}%` }}
199
+ >
200
+ {group.segments.map((segment, segmentIndex) => {
201
+ const percent = normalizeChartValue(
202
+ getPositiveValue(segment.value),
203
+ total
204
+ )
205
+ const percentText = `${formatValue(percent)}%`
206
+
207
+ return (
208
+ <ChartTooltip
209
+ key={`${chartLabelToString(segment.label, "Segment")}-${segmentIndex}`}
210
+ label={
211
+ <>
212
+ {group.label}
213
+ {group.label && segment.label ? " / " : null}
214
+ {segment.label}
215
+ </>
216
+ }
217
+ value={formatValue(segment.value)}
218
+ description={getStackedSegmentDescription(
219
+ totalLabel,
220
+ total,
221
+ percentText,
222
+ formatValue
223
+ )}
224
+ >
225
+ <span
226
+ className="block h-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
227
+ style={{
228
+ width: `${percent}%`,
229
+ backgroundColor: getChartColor(
230
+ segment.color,
231
+ segmentIndex
232
+ ),
233
+ boxShadow:
234
+ segmentIndex < group.segments.length - 1
235
+ ? "inset -1px 0 0 hsl(var(--background) / 0.65)"
236
+ : undefined,
237
+ }}
238
+ tabIndex={0}
239
+ aria-label={`${chartLabelToString(group.label, "Group")} ${chartLabelToString(segment.label, "Segment")}: ${formatValue(segment.value)} (${chartLabelToString(totalLabel, "Total")} ${formatValue(total)} / ${percentText})`}
240
+ />
241
+ </ChartTooltip>
242
+ )
243
+ })}
244
+ </div>
245
+ </div>
246
+ {showValues ? (
247
+ <span className="text-xs tabular-nums text-muted-foreground">
248
+ {formatValue(total)}
249
+ </span>
250
+ ) : null}
251
+ </div>
252
+ )
253
+ })}
254
+ {showLegend ? (
255
+ <ChartLegend items={legendItems} variant="horizontal" />
256
+ ) : null}
257
+ </div>
258
+ )
259
+ }
260
+
261
+ return (
262
+ <div
263
+ ref={ref}
264
+ className={cn(
265
+ stackedBarChartVariantClasses[variant],
266
+ "flex flex-col justify-end",
267
+ className
268
+ )}
269
+ {...props}
270
+ >
271
+ {showValues ? (
272
+ <div
273
+ className="grid pb-1 text-xs font-medium tabular-nums text-foreground"
274
+ style={verticalGridStyle}
275
+ >
276
+ {data.map((group, index) => (
277
+ <span
278
+ key={`${chartLabelToString(group.label, "Group")}-value-${index}`}
279
+ className="min-w-0 truncate text-center"
280
+ >
281
+ {formatValue(getGroupTotal(group))}
282
+ </span>
283
+ ))}
284
+ </div>
285
+ ) : null}
286
+ <div
287
+ className="relative flex min-h-0 flex-1 items-end gap-2 border-b border-border/70"
288
+ style={verticalTrackStyle}
289
+ >
290
+ {showGrid
291
+ ? [25, 50, 75].map((percent) => (
292
+ <span
293
+ key={percent}
294
+ className="pointer-events-none absolute inset-x-0 border-t border-dashed border-border/70"
295
+ style={{ bottom: `${percent}%` }}
296
+ aria-hidden="true"
297
+ />
298
+ ))
299
+ : null}
300
+ {data.map((group, groupIndex) => {
301
+ const total = getGroupTotal(group)
302
+ const stackPercent = normalize
303
+ ? 100
304
+ : normalizeChartValue(total, maxTotal)
305
+
306
+ return (
307
+ <div
308
+ key={`${chartLabelToString(group.label, "Group")}-${groupIndex}`}
309
+ className="relative z-10 flex h-full min-w-0 flex-1 items-end justify-center"
310
+ >
311
+ <div
312
+ className="flex w-full flex-col-reverse overflow-hidden rounded-t-md bg-muted shadow-sm"
313
+ style={{
314
+ height: `${stackPercent}%`,
315
+ maxWidth: verticalBarMaxWidth,
316
+ }}
317
+ >
318
+ {group.segments.map((segment, segmentIndex) => {
319
+ const percent = normalizeChartValue(
320
+ getPositiveValue(segment.value),
321
+ total
322
+ )
323
+ const percentText = `${formatValue(percent)}%`
324
+
325
+ return (
326
+ <ChartTooltip
327
+ key={`${chartLabelToString(segment.label, "Segment")}-${segmentIndex}`}
328
+ label={
329
+ <>
330
+ {group.label}
331
+ {group.label && segment.label ? " / " : null}
332
+ {segment.label}
333
+ </>
334
+ }
335
+ value={formatValue(segment.value)}
336
+ description={getStackedSegmentDescription(
337
+ totalLabel,
338
+ total,
339
+ percentText,
340
+ formatValue
341
+ )}
342
+ >
343
+ <span
344
+ className="block w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
345
+ style={{
346
+ height: `${percent}%`,
347
+ backgroundColor: getChartColor(
348
+ segment.color,
349
+ segmentIndex
350
+ ),
351
+ boxShadow:
352
+ segmentIndex < group.segments.length - 1
353
+ ? "inset 0 1px 0 hsl(var(--background) / 0.65)"
354
+ : undefined,
355
+ }}
356
+ tabIndex={0}
357
+ aria-label={`${chartLabelToString(group.label, "Group")} ${chartLabelToString(segment.label, "Segment")}: ${formatValue(segment.value)} (${chartLabelToString(totalLabel, "Total")} ${formatValue(total)} / ${percentText})`}
358
+ />
359
+ </ChartTooltip>
360
+ )
361
+ })}
362
+ </div>
363
+ </div>
364
+ )
365
+ })}
366
+ </div>
367
+ {showLabels ? (
368
+ <div
369
+ className="grid pt-2 text-xs text-muted-foreground"
370
+ style={verticalGridStyle}
371
+ >
372
+ {data.map((group, index) => (
373
+ <span
374
+ key={`${chartLabelToString(group.label, "Group")}-label-${index}`}
375
+ className="min-w-0 truncate text-center"
376
+ >
377
+ {group.label}
378
+ </span>
379
+ ))}
380
+ </div>
381
+ ) : null}
382
+ {showLegend ? (
383
+ <div className="pt-3">
384
+ <ChartLegend items={legendItems} variant="horizontal" />
385
+ </div>
386
+ ) : null}
387
+ </div>
388
+ )
389
+ }
390
+ )
391
+ StackedBarChart.displayName = "StackedBarChart"
392
+
393
+ export { StackedBarChart }
@@ -0,0 +1,70 @@
1
+ import * as React from "react"
2
+ import { IconArrowDown as ArrowDown, IconArrowUp as ArrowUp, IconMinus as Minus } from "@tabler/icons-react";
3
+
4
+ import { cn } from "../../lib/utils"
5
+
6
+ export type StatisticTrend = "up" | "down" | "flat"
7
+ export type StatisticTone = "positive" | "negative" | "neutral"
8
+
9
+ export interface StatisticProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ label: React.ReactNode
11
+ value: React.ReactNode
12
+ /** Optional change indicator (e.g. "+20.1%", "-3.4%"). */
13
+ change?: React.ReactNode
14
+ /** Trend direction for the change indicator. Default "flat" (muted). */
15
+ trend?: StatisticTrend
16
+ /** Visual meaning for the change indicator. Defaults from trend. */
17
+ tone?: StatisticTone
18
+ /** Optional helper text below the value or after the change. */
19
+ hint?: React.ReactNode
20
+ }
21
+
22
+ const TONE_CLASS: Record<StatisticTone, string> = {
23
+ positive: "text-success",
24
+ negative: "text-destructive",
25
+ neutral: "text-muted-foreground",
26
+ }
27
+
28
+ const TREND_ICON: Record<StatisticTrend, React.ComponentType<{ className?: string }>> = {
29
+ up: ArrowUp,
30
+ down: ArrowDown,
31
+ flat: Minus,
32
+ }
33
+
34
+ const Statistic = React.forwardRef<HTMLDivElement, StatisticProps>(
35
+ (
36
+ { className, label, value, change, trend = "flat", tone, hint, ...props },
37
+ ref
38
+ ) => {
39
+ const TrendIcon = TREND_ICON[trend]
40
+ const resolvedTone = tone ?? (trend === "up" ? "positive" : trend === "down" ? "negative" : "neutral")
41
+
42
+ return (
43
+ <div
44
+ ref={ref}
45
+ className={cn(
46
+ "flex flex-col gap-1 rounded-lg border border-border bg-card p-4 text-card-foreground",
47
+ className
48
+ )}
49
+ {...props}
50
+ >
51
+ <p className="text-xs font-medium text-muted-foreground">{label}</p>
52
+ <p className="text-2xl font-bold tracking-tight [overflow-wrap:anywhere]">{value}</p>
53
+ {change !== undefined ? (
54
+ <div className={cn("flex flex-wrap items-center gap-x-1 gap-y-0.5 text-xs", TONE_CLASS[resolvedTone])}>
55
+ <TrendIcon className="h-3 w-3" />
56
+ <span className="font-medium">{change}</span>
57
+ {hint ? (
58
+ <span className="text-muted-foreground">{hint}</span>
59
+ ) : null}
60
+ </div>
61
+ ) : hint ? (
62
+ <p className="text-xs text-muted-foreground">{hint}</p>
63
+ ) : null}
64
+ </div>
65
+ )
66
+ }
67
+ )
68
+ Statistic.displayName = "Statistic"
69
+
70
+ export { Statistic }
@@ -2,14 +2,23 @@ import * as React from "react"
2
2
 
3
3
  import { cn } from "../../lib/utils"
4
4
 
5
+ interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
6
+ /** Alternates body row background color to improve readability in dense tables. */
7
+ striped?: boolean
8
+ }
9
+
5
10
  const Table = React.forwardRef<
6
11
  HTMLTableElement,
7
- React.HTMLAttributes<HTMLTableElement>
8
- >(({ className, ...props }, ref) => (
12
+ TableProps
13
+ >(({ className, striped = false, ...props }, ref) => (
9
14
  <div className="relative flex flex-col w-[400px] w-full overflow-auto rounded-md rounded-lg border bg-card">
10
15
  <table
11
16
  ref={ref}
12
- className={cn("w-full caption-bottom text-sm", className)}
17
+ className={cn(
18
+ "w-full caption-bottom text-sm",
19
+ striped && "[&_tbody_tr:nth-child(even)]:bg-muted/25",
20
+ className
21
+ )}
13
22
  {...props}
14
23
  />
15
24
  </div>
@@ -43,7 +52,7 @@ const TableFooter = React.forwardRef<
43
52
  <tfoot
44
53
  ref={ref}
45
54
  className={cn(
46
- "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
55
+ "border-t bg-accent/55 font-medium [&>tr]:last:border-b-0",
47
56
  className
48
57
  )}
49
58
  {...props}
@@ -73,7 +82,7 @@ const TableHead = React.forwardRef<
73
82
  <th
74
83
  ref={ref}
75
84
  className={cn(
76
- "h-10 px-3 py-3 text-left align-middle text-xs font-semibold text-muted-foreground [&:has([role=checkbox])]:pr-0",
85
+ "h-10 min-w-0 break-words bg-accent/55 px-3 py-3 text-left align-middle text-xs font-semibold text-muted-foreground [overflow-wrap:anywhere] [&:has([role=checkbox])]:pr-0",
77
86
  className
78
87
  )}
79
88
  {...props}
@@ -87,7 +96,10 @@ const TableCell = React.forwardRef<
87
96
  >(({ className, ...props }, ref) => (
88
97
  <td
89
98
  ref={ref}
90
- className={cn("px-3 py-3 align-middle [&:has([role=checkbox])]:pr-0", className)}
99
+ className={cn(
100
+ "min-w-0 break-words px-3 py-3 align-middle [overflow-wrap:anywhere] [&:has([role=checkbox])]:pr-0",
101
+ className
102
+ )}
91
103
  {...props}
92
104
  />
93
105
  ))
@@ -99,7 +111,10 @@ const TableCaption = React.forwardRef<
99
111
  >(({ className, ...props }, ref) => (
100
112
  <caption
101
113
  ref={ref}
102
- className={cn("mt-4 text-sm text-muted-foreground", className)}
114
+ className={cn(
115
+ "border-t bg-accent/55 px-3 py-2 text-left text-xs leading-5 text-muted-foreground",
116
+ className
117
+ )}
103
118
  {...props}
104
119
  />
105
120
  ))
@@ -0,0 +1,80 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconX as X } from "@tabler/icons-react";
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+
7
+ import { cn } from "../../lib/utils"
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip"
9
+
10
+ export const tagVariants = cva(
11
+ "inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium transition-colors",
12
+ {
13
+ variants: {
14
+ variant: {
15
+ default: "bg-secondary text-secondary-foreground",
16
+ secondary: "bg-foreground text-background",
17
+ outline: "border border-border bg-transparent text-foreground",
18
+ destructive: "bg-destructive-strong text-destructive-strong-foreground",
19
+ },
20
+ size: {
21
+ sm: "h-5 px-1.5 text-[11px]",
22
+ default: "h-6 px-2 text-xs",
23
+ lg: "h-7 px-2.5 text-sm",
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: "default",
28
+ size: "default",
29
+ },
30
+ }
31
+ )
32
+
33
+ export interface TagProps
34
+ extends Omit<React.HTMLAttributes<HTMLSpanElement>, "onRemove">,
35
+ VariantProps<typeof tagVariants> {
36
+ /** When provided, an × button appears at the end. */
37
+ onRemove?: () => void
38
+ removeLabel?: string
39
+ }
40
+
41
+ const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
42
+ (
43
+ {
44
+ className,
45
+ variant,
46
+ size,
47
+ onRemove,
48
+ removeLabel = "Remove",
49
+ children,
50
+ ...props
51
+ },
52
+ ref
53
+ ) => (
54
+ <span
55
+ ref={ref}
56
+ className={cn(tagVariants({ variant, size }), className)}
57
+ {...props}
58
+ >
59
+ {children}
60
+ {onRemove ? (
61
+ <Tooltip>
62
+ <TooltipTrigger asChild>
63
+ <button
64
+ type="button"
65
+ onClick={onRemove}
66
+ className="ml-0.5 -mr-1 inline-flex items-center justify-center rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-1 focus:ring-ring"
67
+ aria-label={removeLabel}
68
+ >
69
+ <X className="h-3 w-3" />
70
+ </button>
71
+ </TooltipTrigger>
72
+ <TooltipContent className="text-xs">{removeLabel}</TooltipContent>
73
+ </Tooltip>
74
+ ) : null}
75
+ </span>
76
+ )
77
+ )
78
+ Tag.displayName = "Tag"
79
+
80
+ export { Tag }