@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,157 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import ReactMarkdown, { type Options as ReactMarkdownOptions } from "react-markdown"
5
+ import remarkGfm from "remark-gfm"
6
+
7
+ import { cn } from "../../lib/utils"
8
+ import {
9
+ Table,
10
+ TableBody,
11
+ TableCell,
12
+ TableHead,
13
+ TableHeader,
14
+ TableRow,
15
+ } from "./Table"
16
+ import { TextLink } from "../navigation/TextLink"
17
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip"
18
+
19
+ export interface MarkdownRendererProps {
20
+ /** Markdown source text. */
21
+ content: string
22
+ /** Wrapper class. */
23
+ className?: string
24
+ /** Disable GFM (tables, task lists, strikethrough). Default false (GFM enabled). */
25
+ disableGfm?: boolean
26
+ /** Override or extend react-markdown components. */
27
+ components?: ReactMarkdownOptions["components"]
28
+ }
29
+
30
+ const defaultMarkdownComponents: NonNullable<ReactMarkdownOptions["components"]> = {
31
+ h1: ({ className, ...props }) => (
32
+ <h1 className={cn("mb-4 mt-0 text-2xl font-bold tracking-tight text-foreground", className)} {...props} />
33
+ ),
34
+ h2: ({ className, ...props }) => (
35
+ <h2 className={cn("mb-3 mt-6 border-b border-border/60 pb-2 text-xl font-semibold tracking-tight text-foreground", className)} {...props} />
36
+ ),
37
+ h3: ({ className, ...props }) => (
38
+ <h3 className={cn("mb-2 mt-5 text-lg font-semibold tracking-tight text-foreground", className)} {...props} />
39
+ ),
40
+ p: ({ className, ...props }) => (
41
+ <p className={cn("my-3 leading-7 text-foreground first:mt-0 last:mb-0", className)} {...props} />
42
+ ),
43
+ a: ({ className, ...props }) => (
44
+ <TextLink className={className} {...props} />
45
+ ),
46
+ ul: ({ className, ...props }) => {
47
+ const isTaskList = String(className ?? "").includes("contains-task-list")
48
+
49
+ return (
50
+ <ul
51
+ className={cn(
52
+ "my-4 ml-5 list-disc space-y-1.5 text-foreground",
53
+ isTaskList && "ml-0 list-none space-y-2",
54
+ className
55
+ )}
56
+ {...props}
57
+ />
58
+ )
59
+ },
60
+ ol: ({ className, ...props }) => (
61
+ <ol className={cn("my-4 ml-5 list-decimal space-y-1.5 text-foreground", className)} {...props} />
62
+ ),
63
+ li: ({ className, ...props }) => {
64
+ const isTaskItem = String(className ?? "").includes("task-list-item")
65
+
66
+ return (
67
+ <li
68
+ className={cn(
69
+ "pl-1 leading-7 text-foreground marker:text-muted-foreground",
70
+ isTaskItem && "list-none pl-0",
71
+ className
72
+ )}
73
+ {...props}
74
+ />
75
+ )
76
+ },
77
+ blockquote: ({ className, ...props }) => (
78
+ <blockquote className={cn("my-4 border-l-2 border-primary-border pl-4 text-muted-foreground", className)} {...props} />
79
+ ),
80
+ hr: ({ className, ...props }) => (
81
+ <hr className={cn("my-6 border-border/70", className)} {...props} />
82
+ ),
83
+ pre: ({ className, ...props }) => (
84
+ <pre className={cn("my-4 overflow-auto rounded-md border border-border/70 bg-muted p-4 text-sm text-foreground", className)} {...props} />
85
+ ),
86
+ code: ({ className, ...props }) => (
87
+ <code className={cn("rounded bg-muted px-1 py-0.5 font-mono text-sm text-foreground", className)} {...props} />
88
+ ),
89
+ table: ({ className, ...props }) => (
90
+ <div className="my-4">
91
+ <Table className={cn("min-w-[360px]", className)} {...props} />
92
+ </div>
93
+ ),
94
+ thead: ({ className, ...props }) => (
95
+ <TableHeader className={className} {...props} />
96
+ ),
97
+ tbody: ({ className, ...props }) => (
98
+ <TableBody className={className} {...props} />
99
+ ),
100
+ tr: ({ className, ...props }) => (
101
+ <TableRow className={className} {...props} />
102
+ ),
103
+ th: ({ className, ...props }) => (
104
+ <TableHead className={cn("bg-muted/50", className)} {...props} />
105
+ ),
106
+ td: ({ className, ...props }) => (
107
+ <TableCell className={className} {...props} />
108
+ ),
109
+ input: ({ className, checked, type, ...props }) => {
110
+ if (type !== "checkbox") {
111
+ return <input className={className} type={type} {...props} />
112
+ }
113
+
114
+ const tooltip = checked ? "Read-only completed task" : "Read-only pending task"
115
+
116
+ return (
117
+ <Tooltip>
118
+ <TooltipTrigger asChild>
119
+ <span className="mr-2 inline-flex translate-y-0.5 cursor-default items-center justify-center" tabIndex={0}>
120
+ <input
121
+ aria-label={tooltip}
122
+ checked={Boolean(checked)}
123
+ className={cn("h-4 w-4 rounded border-border accent-foreground disabled:cursor-default disabled:opacity-100", className)}
124
+ disabled
125
+ readOnly
126
+ type="checkbox"
127
+ {...props}
128
+ />
129
+ </span>
130
+ </TooltipTrigger>
131
+ <TooltipContent>{tooltip}</TooltipContent>
132
+ </Tooltip>
133
+ )
134
+ },
135
+ }
136
+
137
+ const MarkdownRenderer = React.forwardRef<HTMLDivElement, MarkdownRendererProps>(
138
+ ({ content, className, disableGfm = false, components }, ref) => (
139
+ <div
140
+ ref={ref}
141
+ className={cn(
142
+ "max-w-none text-sm text-foreground",
143
+ className
144
+ )}
145
+ >
146
+ <ReactMarkdown
147
+ remarkPlugins={disableGfm ? [] : [remarkGfm]}
148
+ components={{ ...defaultMarkdownComponents, ...components }}
149
+ >
150
+ {content}
151
+ </ReactMarkdown>
152
+ </div>
153
+ )
154
+ )
155
+ MarkdownRenderer.displayName = "MarkdownRenderer"
156
+
157
+ export { MarkdownRenderer }
@@ -0,0 +1,81 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../../lib/utils"
4
+ import { metadataListDefaultVariantKey } from "./generated/default-variant-keys"
5
+ import type { MetadataListVariantKey } from "./generated/variant-keys"
6
+
7
+ export interface MetadataListItem {
8
+ label: React.ReactNode
9
+ value: React.ReactNode
10
+ description?: React.ReactNode
11
+ icon?: React.ReactNode
12
+ }
13
+
14
+ export interface MetadataListProps extends React.HTMLAttributes<HTMLDListElement> {
15
+ items: MetadataListItem[]
16
+ variant?: MetadataListVariantKey
17
+ layout?: "vertical" | "horizontal"
18
+ emptyMessage?: React.ReactNode
19
+ }
20
+
21
+ const variantClasses: Record<MetadataListVariantKey, { root: string; row: string; horizontalRow: string; value: string }> = {
22
+ default: {
23
+ root: "space-y-2",
24
+ row: "grid grid-cols-[minmax(0,1fr)_auto] gap-4 rounded-md px-0 py-1.5",
25
+ horizontalRow: "rounded-md border border-border/70 bg-card px-3 py-2.5",
26
+ value: "text-sm",
27
+ },
28
+ compact: {
29
+ root: "space-y-1",
30
+ row: "grid grid-cols-[minmax(0,1fr)_auto] gap-3 rounded-md px-0 py-1",
31
+ horizontalRow: "rounded-md border border-border/70 bg-card px-3 py-2",
32
+ value: "text-xs",
33
+ },
34
+ }
35
+
36
+ const MetadataList = React.forwardRef<HTMLDListElement, MetadataListProps>(
37
+ ({ items, variant = metadataListDefaultVariantKey, layout = "vertical", emptyMessage = "No metadata", className, ...props }, ref) => {
38
+ const classes = variantClasses[variant]
39
+ const isHorizontal = layout === "horizontal"
40
+
41
+ if (items.length === 0) {
42
+ return (
43
+ <div className={cn("w-full rounded-md border border-dashed p-4 text-sm text-muted-foreground", className)}>
44
+ {emptyMessage}
45
+ </div>
46
+ )
47
+ }
48
+
49
+ return (
50
+ <dl
51
+ ref={ref}
52
+ className={cn(
53
+ "w-full p-0",
54
+ isHorizontal ? "grid grid-cols-1 gap-3 sm:grid-cols-2" : classes.root,
55
+ className
56
+ )}
57
+ {...props}
58
+ >
59
+ {items.map((item, index) => (
60
+ <div key={index} className={isHorizontal ? classes.horizontalRow : classes.row}>
61
+ <dt className="flex min-w-0 items-start gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
62
+ {item.icon ? <span className="mt-0.5 shrink-0 text-muted-foreground">{item.icon}</span> : null}
63
+ <span className="min-w-0 truncate">{item.label}</span>
64
+ </dt>
65
+ <dd className={cn("min-w-0 font-medium text-foreground", isHorizontal ? "mt-1 text-left" : "text-right", classes.value)}>
66
+ <div className="truncate">{item.value}</div>
67
+ {item.description ? (
68
+ <div className="mt-0.5 truncate text-xs font-normal text-muted-foreground">
69
+ {item.description}
70
+ </div>
71
+ ) : null}
72
+ </dd>
73
+ </div>
74
+ ))}
75
+ </dl>
76
+ )
77
+ }
78
+ )
79
+ MetadataList.displayName = "MetadataList"
80
+
81
+ export { MetadataList }
@@ -0,0 +1,314 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "./Card"
13
+ import type { ChartDataPoint } from "./chart-utils"
14
+ import {
15
+ chartLabelToString,
16
+ defaultChartValueFormatter,
17
+ getChartColor,
18
+ normalizeChartValue,
19
+ } from "./chart-utils"
20
+ import { ChartTooltip } from "./chart-tooltip"
21
+ import type { MiniDistributionBarCardVariantKey } from "./generated/variant-keys"
22
+ import { miniDistributionBarCardDefaultVariantKey } from "./generated/default-variant-keys"
23
+
24
+ export interface MiniDistributionBarCardSegment extends ChartDataPoint {
25
+ detail?: React.ReactNode
26
+ description?: React.ReactNode
27
+ }
28
+
29
+ export interface MiniDistributionBarCardProps
30
+ extends Omit<React.ComponentPropsWithoutRef<typeof Card>, "title"> {
31
+ segments: MiniDistributionBarCardSegment[]
32
+ title?: React.ReactNode
33
+ description?: React.ReactNode
34
+ value?: React.ReactNode
35
+ delta?: React.ReactNode
36
+ deltaDescription?: React.ReactNode
37
+ selectedIndex?: number
38
+ tickCount?: number
39
+ max?: number
40
+ caption?: React.ReactNode
41
+ variant?: MiniDistributionBarCardVariantKey
42
+ formatValue?: (value: number) => React.ReactNode
43
+ totalLabel?: React.ReactNode
44
+ onSegmentSelect?: (segment: MiniDistributionBarCardSegment, index: number) => void
45
+ }
46
+
47
+ type MiniDistributionBarCardClassNames = {
48
+ card: string
49
+ header: string
50
+ content: string
51
+ tick: string
52
+ title: string
53
+ }
54
+
55
+ const variantClasses: Record<MiniDistributionBarCardVariantKey, MiniDistributionBarCardClassNames> = {
56
+ compact: {
57
+ card: "rounded-md",
58
+ header: "p-4 pb-3",
59
+ content: "px-4 pb-4",
60
+ tick: "h-7 rounded-sm",
61
+ title: "text-sm",
62
+ },
63
+ default: {
64
+ card: "rounded-lg",
65
+ header: "p-5 pb-3",
66
+ content: "px-5 pb-5",
67
+ tick: "h-9 rounded",
68
+ title: "text-base",
69
+ },
70
+ }
71
+
72
+ function getPositiveValue(value: number) {
73
+ return Number.isFinite(value) ? Math.max(value, 0) : 0
74
+ }
75
+
76
+ function getTotal(segments: MiniDistributionBarCardSegment[], max?: number) {
77
+ return Math.max(
78
+ max ?? 0,
79
+ segments.reduce((sum, segment) => sum + getPositiveValue(segment.value), 0),
80
+ 1
81
+ )
82
+ }
83
+
84
+ function buildTicks(
85
+ segments: MiniDistributionBarCardSegment[],
86
+ tickCount: number,
87
+ total: number
88
+ ) {
89
+ const safeTickCount = Math.max(1, Math.round(tickCount))
90
+ const rawCounts = segments.map((segment) =>
91
+ Math.max(0, (getPositiveValue(segment.value) / total) * safeTickCount)
92
+ )
93
+ const counts = rawCounts.map(Math.floor)
94
+ let remaining = safeTickCount - counts.reduce((sum, count) => sum + count, 0)
95
+ const order = rawCounts
96
+ .map((count, index) => ({ index, fraction: count - Math.floor(count) }))
97
+ .sort((a, b) => b.fraction - a.fraction)
98
+
99
+ for (const item of order) {
100
+ if (remaining <= 0) break
101
+ counts[item.index] += 1
102
+ remaining -= 1
103
+ }
104
+
105
+ return counts.flatMap((count, segmentIndex) =>
106
+ Array.from({ length: count }, (_, tickIndex) => ({
107
+ segmentIndex,
108
+ tickIndex,
109
+ }))
110
+ )
111
+ }
112
+
113
+ const MiniDistributionBarCard = React.forwardRef<
114
+ HTMLDivElement,
115
+ MiniDistributionBarCardProps
116
+ >(
117
+ (
118
+ {
119
+ className,
120
+ segments,
121
+ title = "Product categories",
122
+ description,
123
+ value,
124
+ delta,
125
+ deltaDescription,
126
+ selectedIndex,
127
+ tickCount = 32,
128
+ max,
129
+ caption,
130
+ variant = miniDistributionBarCardDefaultVariantKey,
131
+ formatValue = defaultChartValueFormatter,
132
+ totalLabel = "Total",
133
+ onSegmentSelect,
134
+ ...props
135
+ },
136
+ ref
137
+ ) => {
138
+ const styles = variantClasses[variant]
139
+ const total = getTotal(segments, max)
140
+ const ticks = buildTicks(segments, tickCount, total)
141
+ const selectedSegment =
142
+ selectedIndex === undefined ? undefined : segments[selectedIndex]
143
+ const selectedPercent =
144
+ selectedSegment === undefined
145
+ ? undefined
146
+ : `${defaultChartValueFormatter(
147
+ normalizeChartValue(getPositiveValue(selectedSegment.value), total)
148
+ )}%`
149
+
150
+ return (
151
+ <Card
152
+ ref={ref}
153
+ className={cn("w-full min-w-0 overflow-hidden p-0", styles.card, className)}
154
+ {...props}
155
+ >
156
+ <CardHeader className={styles.header}>
157
+ <div className="flex min-w-0 items-start justify-between gap-3">
158
+ <div className="min-w-0 space-y-1">
159
+ <CardTitle className={cn("truncate", styles.title)}>
160
+ {title}
161
+ </CardTitle>
162
+ {description ? (
163
+ <CardDescription className="text-xs">
164
+ {description}
165
+ </CardDescription>
166
+ ) : null}
167
+ </div>
168
+ {delta !== undefined && delta !== null ? (
169
+ <div
170
+ className="shrink-0 text-right text-sm font-semibold text-success-strong tabular-nums"
171
+ title={chartLabelToString(
172
+ deltaDescription,
173
+ "Delta description"
174
+ )}
175
+ >
176
+ {delta}
177
+ </div>
178
+ ) : null}
179
+ </div>
180
+ </CardHeader>
181
+ <CardContent className={cn("space-y-4", styles.content)}>
182
+ <div className="flex min-w-0 items-end justify-between gap-4">
183
+ <div className="min-w-0">
184
+ {value !== undefined ? (
185
+ <div className="truncate text-3xl font-semibold tracking-tight tabular-nums">
186
+ {value}
187
+ </div>
188
+ ) : null}
189
+ {selectedSegment ? (
190
+ <div className="mt-1 truncate text-xs text-muted-foreground">
191
+ {selectedSegment.label}
192
+ {selectedPercent ? ` · ${selectedPercent}` : ""}
193
+ </div>
194
+ ) : null}
195
+ </div>
196
+ </div>
197
+
198
+ <div
199
+ className="grid min-w-0 gap-1"
200
+ style={{
201
+ gridTemplateColumns: `repeat(${Math.max(
202
+ ticks.length,
203
+ 1
204
+ )}, minmax(0, 1fr))`,
205
+ }}
206
+ >
207
+ {ticks.map(({ segmentIndex, tickIndex }, index) => {
208
+ const segment = segments[segmentIndex]
209
+ const percent = normalizeChartValue(
210
+ getPositiveValue(segment.value),
211
+ total
212
+ )
213
+ const percentLabel = `${defaultChartValueFormatter(percent)}%`
214
+ const isSelected = selectedIndex === segmentIndex
215
+
216
+ return (
217
+ <ChartTooltip
218
+ key={`${segmentIndex}-${tickIndex}-${index}`}
219
+ label={segment.label}
220
+ value={percentLabel}
221
+ description={
222
+ segment.description ?? [
223
+ totalLabel,
224
+ ": ",
225
+ formatValue(segment.value),
226
+ ]
227
+ }
228
+ >
229
+ <button
230
+ type="button"
231
+ className={cn(
232
+ "min-w-0 transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
233
+ styles.tick,
234
+ selectedIndex !== undefined &&
235
+ !isSelected &&
236
+ "opacity-45"
237
+ )}
238
+ style={{
239
+ backgroundColor: getChartColor(
240
+ segment.color,
241
+ segmentIndex
242
+ ),
243
+ }}
244
+ onClick={() => onSegmentSelect?.(segment, segmentIndex)}
245
+ aria-label={`${chartLabelToString(segment.label, "Segment")}: ${percentLabel}`}
246
+ />
247
+ </ChartTooltip>
248
+ )
249
+ })}
250
+ </div>
251
+
252
+ <div className="grid min-w-0 gap-2 sm:grid-cols-3">
253
+ {segments.map((segment, index) => {
254
+ const percent = `${defaultChartValueFormatter(
255
+ normalizeChartValue(getPositiveValue(segment.value), total)
256
+ )}%`
257
+ const isSelected = selectedIndex === index
258
+
259
+ return (
260
+ <ChartTooltip
261
+ key={`${chartLabelToString(segment.label, "Segment")}-row-${index}`}
262
+ label={segment.label}
263
+ value={formatValue(segment.value)}
264
+ description={segment.description ?? percent}
265
+ >
266
+ <button
267
+ type="button"
268
+ className={cn(
269
+ "grid min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-2 rounded-md border bg-card px-3 py-2 text-left transition-colors",
270
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
271
+ isSelected && "border-foreground shadow-sm"
272
+ )}
273
+ onClick={() => onSegmentSelect?.(segment, index)}
274
+ >
275
+ <span
276
+ className="h-2.5 w-2.5 rounded-full"
277
+ style={{
278
+ backgroundColor: getChartColor(
279
+ segment.color,
280
+ index
281
+ ),
282
+ }}
283
+ aria-hidden="true"
284
+ />
285
+ <span className="min-w-0 truncate text-xs text-muted-foreground">
286
+ {segment.label}
287
+ </span>
288
+ <span className="text-sm font-semibold tabular-nums">
289
+ {percent}
290
+ </span>
291
+ {segment.detail ? (
292
+ <span className="col-span-3 truncate text-xs text-muted-foreground">
293
+ {segment.detail}
294
+ </span>
295
+ ) : null}
296
+ </button>
297
+ </ChartTooltip>
298
+ )
299
+ })}
300
+ </div>
301
+
302
+ {caption ? (
303
+ <div className="rounded-md border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
304
+ {caption}
305
+ </div>
306
+ ) : null}
307
+ </CardContent>
308
+ </Card>
309
+ )
310
+ }
311
+ )
312
+ MiniDistributionBarCard.displayName = "MiniDistributionBarCard"
313
+
314
+ export { MiniDistributionBarCard }