@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,242 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconCheck as Check, IconHeart as Heart, IconStarFilled as Star } from "@tabler/icons-react"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { Badge } from "./Badge"
8
+ import { Icon } from "./Icon"
9
+ import { ImagePreview } from "./ImagePreview"
10
+ import { assetCardDefaultVariantKey } from "./generated/default-variant-keys"
11
+ import type { AssetCardVariantKey } from "./generated/variant-keys"
12
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip"
13
+
14
+ export interface AssetCardAsset {
15
+ id: string
16
+ title: string
17
+ src?: string
18
+ alt?: string
19
+ type?: string
20
+ size?: string
21
+ width?: number
22
+ height?: number
23
+ createdAt?: string
24
+ isFavorite?: boolean
25
+ rating?: number
26
+ score?: string | number
27
+ meta?: React.ReactNode
28
+ }
29
+
30
+ export interface AssetCardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
31
+ asset: AssetCardAsset
32
+ selected?: boolean
33
+ variant?: AssetCardVariantKey
34
+ selectionMode?: "single" | "multiple" | "none"
35
+ imageFit?: "contain" | "cover"
36
+ previewLabel?: string
37
+ favoriteLabel?: string
38
+ unfavoriteLabel?: string
39
+ ratingLabel?: string
40
+ noImageLabel?: string
41
+ portalContainer?: HTMLElement | null
42
+ onSelect?: (asset: AssetCardAsset) => void
43
+ onPreview?: (asset: AssetCardAsset) => void
44
+ onFavorite?: (asset: AssetCardAsset) => void
45
+ renderMeta?: (asset: AssetCardAsset) => React.ReactNode
46
+ }
47
+
48
+ const densityClasses: Record<AssetCardVariantKey, { root: string; image: string; title: string; meta: string }> = {
49
+ default: {
50
+ root: "gap-2",
51
+ image: "rounded-lg",
52
+ title: "text-base",
53
+ meta: "text-sm",
54
+ },
55
+ compact: {
56
+ root: "gap-1.5",
57
+ image: "rounded-md",
58
+ title: "text-sm",
59
+ meta: "text-xs",
60
+ },
61
+ }
62
+
63
+ function defaultMeta(asset: AssetCardAsset) {
64
+ const dimensions = asset.width && asset.height ? `${asset.width} x ${asset.height}` : null
65
+ const typeSize = [asset.type, asset.size].filter(Boolean).join(" ")
66
+ return [dimensions, typeSize, asset.createdAt].filter(Boolean).join(" / ")
67
+ }
68
+
69
+ const AssetCard = React.forwardRef<HTMLDivElement, AssetCardProps>(
70
+ (
71
+ {
72
+ asset,
73
+ selected,
74
+ variant = assetCardDefaultVariantKey,
75
+ selectionMode = "single",
76
+ imageFit = "cover",
77
+ previewLabel = "Open preview",
78
+ favoriteLabel = "Favorite",
79
+ unfavoriteLabel = "Remove favorite",
80
+ ratingLabel = "Rating",
81
+ noImageLabel = "Preview image not found",
82
+ portalContainer,
83
+ onSelect,
84
+ onPreview,
85
+ onFavorite,
86
+ renderMeta,
87
+ className,
88
+ onKeyDown,
89
+ ...props
90
+ },
91
+ ref
92
+ ) => {
93
+ const interactive = Boolean(onSelect)
94
+ const classes = densityClasses[variant]
95
+ const meta = renderMeta ? renderMeta(asset) : asset.meta ?? defaultMeta(asset)
96
+ const ratingValue = typeof asset.rating === "number" && asset.rating > 0
97
+ ? asset.rating.toFixed(1)
98
+ : asset.score !== undefined
99
+ ? String(asset.score)
100
+ : null
101
+ const favoriteButtonRef = React.useRef<HTMLButtonElement>(null)
102
+ const favoriteBlurTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
103
+
104
+ React.useEffect(() => {
105
+ if (asset.isFavorite && favoriteBlurTimerRef.current) {
106
+ clearTimeout(favoriteBlurTimerRef.current)
107
+ favoriteBlurTimerRef.current = null
108
+ }
109
+ }, [asset.isFavorite])
110
+
111
+ React.useEffect(() => {
112
+ return () => {
113
+ if (favoriteBlurTimerRef.current) {
114
+ clearTimeout(favoriteBlurTimerRef.current)
115
+ }
116
+ }
117
+ }, [])
118
+
119
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
120
+ onKeyDown?.(event)
121
+ if (event.defaultPrevented || !interactive) return
122
+ if (event.key === "Enter" || event.key === " ") {
123
+ event.preventDefault()
124
+ onSelect?.(asset)
125
+ }
126
+ }
127
+
128
+ return (
129
+ <div
130
+ ref={ref}
131
+ role={interactive ? "button" : undefined}
132
+ tabIndex={interactive ? 0 : undefined}
133
+ aria-pressed={interactive ? selected : undefined}
134
+ data-state={selected ? "selected" : "idle"}
135
+ className={cn(
136
+ "group flex w-full min-w-0 flex-col p-0 outline-none",
137
+ interactive && "cursor-pointer",
138
+ classes.root,
139
+ className
140
+ )}
141
+ onClick={() => onSelect?.(asset)}
142
+ onKeyDown={handleKeyDown}
143
+ {...props}
144
+ >
145
+ <ImagePreview
146
+ src={asset.src}
147
+ alt={asset.src ? asset.alt ?? asset.title : undefined}
148
+ objectFit={imageFit}
149
+ className={cn(
150
+ classes.image,
151
+ selected
152
+ ? "border-primary-border bg-primary-subtle ring-2 ring-primary-border"
153
+ : "border-border group-hover:border-primary-border"
154
+ )}
155
+ noImageLabel={noImageLabel}
156
+ previewLabel={previewLabel}
157
+ portalContainer={portalContainer}
158
+ onPreview={onPreview ? () => onPreview(asset) : undefined}
159
+ >
160
+ {(asset.isFavorite || onFavorite) ? (
161
+ <Tooltip>
162
+ <TooltipTrigger asChild>
163
+ <button
164
+ ref={favoriteButtonRef}
165
+ type="button"
166
+ className={cn(
167
+ "absolute left-2 top-2 z-20 inline-flex h-7 w-7 items-center justify-center rounded-full border bg-background/95 shadow-sm transition-opacity hover:text-primary focus:text-primary focus:opacity-100",
168
+ asset.isFavorite
169
+ ? "text-primary opacity-100"
170
+ : "text-muted-foreground opacity-0 group-hover:opacity-100"
171
+ )}
172
+ aria-label={asset.isFavorite ? unfavoriteLabel : favoriteLabel}
173
+ aria-pressed={asset.isFavorite}
174
+ onClick={(event) => {
175
+ event.stopPropagation()
176
+ const wasFavorite = Boolean(asset.isFavorite)
177
+ if (favoriteBlurTimerRef.current) {
178
+ clearTimeout(favoriteBlurTimerRef.current)
179
+ favoriteBlurTimerRef.current = null
180
+ }
181
+ onFavorite?.(asset)
182
+ if (wasFavorite) {
183
+ favoriteBlurTimerRef.current = setTimeout(() => {
184
+ favoriteButtonRef.current?.blur()
185
+ favoriteBlurTimerRef.current = null
186
+ }, 1000)
187
+ }
188
+ }}
189
+ >
190
+ <Icon icon={Heart} size="xs" className={cn(asset.isFavorite && "fill-current")} />
191
+ </button>
192
+ </TooltipTrigger>
193
+ <TooltipContent portalContainer={portalContainer}>{asset.isFavorite ? unfavoriteLabel : favoriteLabel}</TooltipContent>
194
+ </Tooltip>
195
+ ) : null}
196
+
197
+ {(ratingValue || selectionMode === "multiple" || (selected && selectionMode !== "none")) ? (
198
+ <div className="absolute right-2 top-2 z-20 flex max-w-[calc(100%-1rem)] items-center gap-1">
199
+ {ratingValue ? (
200
+ <Badge
201
+ className="gap-1 bg-background/95 text-foreground shadow-sm"
202
+ aria-label={`${ratingLabel}: ${ratingValue}`}
203
+ >
204
+ {typeof asset.rating === "number" && asset.rating > 0 ? (
205
+ <Icon icon={Star} size="xs" className="fill-current text-warning" />
206
+ ) : null}
207
+ {ratingValue}
208
+ </Badge>
209
+ ) : null}
210
+ {(selectionMode === "multiple" || (selected && selectionMode !== "none")) ? (
211
+ <span
212
+ className={cn(
213
+ "inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full border shadow-sm",
214
+ selected
215
+ ? "border-primary bg-primary text-primary-foreground"
216
+ : "border-border bg-background/80 text-muted-foreground"
217
+ )}
218
+ aria-hidden="true"
219
+ >
220
+ {selected ? <Icon icon={Check} size="xs" strokeWidth={2.5} /> : null}
221
+ </span>
222
+ ) : null}
223
+ </div>
224
+ ) : null}
225
+ </ImagePreview>
226
+ <div className="min-w-0 px-0.5">
227
+ <div className={cn("truncate font-medium leading-tight text-foreground", classes.title)}>
228
+ {asset.title}
229
+ </div>
230
+ {meta ? (
231
+ <div className={cn("mt-1 truncate leading-tight text-muted-foreground", classes.meta)}>
232
+ {meta}
233
+ </div>
234
+ ) : null}
235
+ </div>
236
+ </div>
237
+ )
238
+ }
239
+ )
240
+ AssetCard.displayName = "AssetCard"
241
+
242
+ export { AssetCard }
@@ -0,0 +1,164 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconPhoto as ImageIcon } from "@tabler/icons-react"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { AssetCard, type AssetCardAsset, type AssetCardProps } from "./AssetCard"
8
+ import { EmptyState } from "./EmptyState"
9
+ import { assetGridDefaultVariantKey } from "./generated/default-variant-keys"
10
+ import type { AssetGridVariantKey } from "./generated/variant-keys"
11
+
12
+ export interface AssetGridGroup<TAsset extends AssetCardAsset = AssetCardAsset> {
13
+ id: string
14
+ label: React.ReactNode
15
+ items: TAsset[]
16
+ description?: React.ReactNode
17
+ }
18
+
19
+ export interface AssetGridProps<TAsset extends AssetCardAsset = AssetCardAsset>
20
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "onSelect"> {
21
+ items?: TAsset[]
22
+ groups?: AssetGridGroup<TAsset>[]
23
+ selectedIds?: Iterable<string>
24
+ variant?: AssetGridVariantKey
25
+ layout?: "fill" | "content"
26
+ minColumnWidth?: number
27
+ gap?: number
28
+ selectionMode?: AssetCardProps["selectionMode"]
29
+ portalContainer?: HTMLElement | null
30
+ emptyTitle?: React.ReactNode
31
+ emptyDescription?: React.ReactNode
32
+ onSelect?: (asset: TAsset) => void
33
+ onPreview?: (asset: TAsset) => void
34
+ onFavorite?: (asset: TAsset) => void
35
+ renderMeta?: (asset: TAsset) => React.ReactNode
36
+ renderItem?: (asset: TAsset, state: { selected: boolean }) => React.ReactNode
37
+ }
38
+
39
+ const variantDefaults: Record<AssetGridVariantKey, { minColumnWidth: number; gap: number; cardVariant: AssetCardProps["variant"] }> = {
40
+ default: { minColumnWidth: 180, gap: 24, cardVariant: "default" },
41
+ compact: { minColumnWidth: 140, gap: 16, cardVariant: "compact" },
42
+ }
43
+
44
+ function normalizeGroups<TAsset extends AssetCardAsset>(
45
+ items: TAsset[] | undefined,
46
+ groups: AssetGridGroup<TAsset>[] | undefined
47
+ ) {
48
+ if (groups) return groups
49
+ return [{ id: "all", label: null, items: items ?? [] }]
50
+ }
51
+
52
+ const AssetGrid = React.forwardRef<HTMLDivElement, AssetGridProps>(
53
+ (
54
+ {
55
+ items,
56
+ groups,
57
+ selectedIds,
58
+ variant = assetGridDefaultVariantKey,
59
+ layout = "fill",
60
+ minColumnWidth,
61
+ gap,
62
+ selectionMode = "single",
63
+ portalContainer,
64
+ emptyTitle = "No assets",
65
+ emptyDescription = "Assets added to this library will appear here.",
66
+ onSelect,
67
+ onPreview,
68
+ onFavorite,
69
+ renderMeta,
70
+ renderItem,
71
+ className,
72
+ style,
73
+ ...props
74
+ },
75
+ ref
76
+ ) => {
77
+ const defaults = variantDefaults[variant]
78
+ const resolvedGroups = normalizeGroups(items, groups)
79
+ const selectedSet = React.useMemo(() => new Set(selectedIds ?? []), [selectedIds])
80
+ const hasAssets = resolvedGroups.some((group) => group.items.length > 0)
81
+ const columnWidth = minColumnWidth ?? defaults.minColumnWidth
82
+ const gridGap = gap ?? defaults.gap
83
+ const stretchColumns = layout === "fill"
84
+
85
+ if (!hasAssets) {
86
+ return (
87
+ <div ref={ref} className={cn("flex min-h-64 w-full items-center justify-center p-0", className)} {...props}>
88
+ <EmptyState
89
+ icon={<ImageIcon className="h-8 w-8" aria-hidden="true" />}
90
+ title={emptyTitle}
91
+ description={emptyDescription}
92
+ />
93
+ </div>
94
+ )
95
+ }
96
+
97
+ return (
98
+ <div ref={ref} className={cn("w-full space-y-8 p-0", className)} style={style} {...props}>
99
+ {resolvedGroups.map((group) => {
100
+ const contentMaxWidth =
101
+ group.items.length > 0
102
+ ? group.items.length * columnWidth + Math.max(0, group.items.length - 1) * gridGap
103
+ : undefined
104
+
105
+ return (
106
+ <section
107
+ key={group.id}
108
+ className={cn("space-y-3", layout === "content" && "mx-auto w-full")}
109
+ style={layout === "content" && contentMaxWidth ? { maxWidth: `${contentMaxWidth}px` } : undefined}
110
+ >
111
+ {group.label ? (
112
+ <div className="flex min-w-0 items-end justify-between gap-3">
113
+ <div className="min-w-0">
114
+ <h3 className="truncate text-sm font-semibold text-foreground">{group.label}</h3>
115
+ {group.description ? (
116
+ <p className="mt-1 truncate text-xs text-muted-foreground">{group.description}</p>
117
+ ) : null}
118
+ </div>
119
+ <span className="text-xs text-muted-foreground">{group.items.length}</span>
120
+ </div>
121
+ ) : null}
122
+ <div
123
+ className="grid"
124
+ style={{
125
+ gridTemplateColumns: `repeat(auto-fit, minmax(min(100%, ${columnWidth}px), ${stretchColumns ? "1fr" : `${columnWidth}px`}))`,
126
+ gap: `${gridGap}px`,
127
+ justifyContent: layout === "content" ? "center" : undefined,
128
+ }}
129
+ >
130
+ {group.items.map((asset) => {
131
+ const selected = selectedSet.has(asset.id)
132
+ if (renderItem) {
133
+ return (
134
+ <React.Fragment key={asset.id}>
135
+ {renderItem(asset, { selected })}
136
+ </React.Fragment>
137
+ )
138
+ }
139
+ return (
140
+ <AssetCard
141
+ key={asset.id}
142
+ asset={asset}
143
+ selected={selected}
144
+ variant={defaults.cardVariant}
145
+ selectionMode={selectionMode}
146
+ portalContainer={portalContainer}
147
+ onSelect={onSelect as ((asset: AssetCardAsset) => void) | undefined}
148
+ onPreview={onPreview as ((asset: AssetCardAsset) => void) | undefined}
149
+ onFavorite={onFavorite as ((asset: AssetCardAsset) => void) | undefined}
150
+ renderMeta={renderMeta as ((asset: AssetCardAsset) => React.ReactNode) | undefined}
151
+ />
152
+ )
153
+ })}
154
+ </div>
155
+ </section>
156
+ )
157
+ })}
158
+ </div>
159
+ )
160
+ }
161
+ )
162
+ AssetGrid.displayName = "AssetGrid"
163
+
164
+ export { AssetGrid }
@@ -0,0 +1,127 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip"
8
+ import type { AvatarVariantKey } from "./generated/variant-keys"
9
+ import { avatarDefaultVariantKey } from "./generated/default-variant-keys"
10
+
11
+ export type AvatarPresence = "online" | "away" | "busy" | "offline"
12
+ type TooltipContentProps = React.ComponentPropsWithoutRef<typeof TooltipContent>
13
+
14
+ export interface AvatarProps
15
+ extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> {
16
+ tooltip?: React.ReactNode
17
+ tooltipSide?: TooltipContentProps["side"]
18
+ tooltipAlign?: TooltipContentProps["align"]
19
+ tooltipPortalContainer?: TooltipContentProps["portalContainer"]
20
+ tooltipContentClassName?: string
21
+ presence?: AvatarPresence
22
+ presenceLabel?: React.ReactNode
23
+ }
24
+
25
+ const avatarSlotClasses: Record<AvatarVariantKey, string> = {
26
+ fallback: "flex h-full w-full items-center justify-center rounded-full bg-secondary text-sm font-medium text-muted-foreground",
27
+ image: "aspect-square h-full w-full rounded-full object-cover",
28
+ }
29
+
30
+ const avatarPresenceClasses: Record<AvatarPresence, string> = {
31
+ online: "bg-success",
32
+ away: "bg-warning",
33
+ busy: "bg-destructive",
34
+ offline: "bg-muted-foreground",
35
+ }
36
+
37
+ const Avatar = React.forwardRef<
38
+ React.ElementRef<typeof AvatarPrimitive.Root>,
39
+ AvatarProps
40
+ >(
41
+ (
42
+ {
43
+ className,
44
+ children,
45
+ tooltip,
46
+ tooltipSide,
47
+ tooltipAlign,
48
+ tooltipPortalContainer,
49
+ tooltipContentClassName,
50
+ presence,
51
+ presenceLabel,
52
+ ...props
53
+ },
54
+ ref
55
+ ) => {
56
+ const root = (
57
+ <AvatarPrimitive.Root
58
+ ref={ref}
59
+ className={cn(
60
+ "relative inline-flex h-10 w-10 shrink-0 items-center rounded-full",
61
+ className
62
+ )}
63
+ {...props}
64
+ >
65
+ {children}
66
+ {presence ? (
67
+ <span
68
+ className={cn(
69
+ "absolute bottom-0 right-0 h-2.5 w-2.5 rounded-full border-2 border-background",
70
+ avatarPresenceClasses[presence]
71
+ )}
72
+ aria-label={typeof presenceLabel === "string" ? presenceLabel : undefined}
73
+ aria-hidden={presenceLabel ? undefined : true}
74
+ />
75
+ ) : null}
76
+ </AvatarPrimitive.Root>
77
+ )
78
+
79
+ if (!tooltip) {
80
+ return root
81
+ }
82
+
83
+ return (
84
+ <Tooltip>
85
+ <TooltipTrigger asChild>{root}</TooltipTrigger>
86
+ <TooltipContent
87
+ side={tooltipSide}
88
+ align={tooltipAlign}
89
+ portalContainer={tooltipPortalContainer}
90
+ className={tooltipContentClassName}
91
+ >
92
+ {tooltip}
93
+ </TooltipContent>
94
+ </Tooltip>
95
+ )
96
+ }
97
+ )
98
+ Avatar.displayName = AvatarPrimitive.Root.displayName
99
+
100
+ const AvatarImage = React.forwardRef<
101
+ React.ElementRef<typeof AvatarPrimitive.Image>,
102
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
103
+ >(({ className, ...props }, ref) => (
104
+ <AvatarPrimitive.Image
105
+ ref={ref}
106
+ className={cn(avatarSlotClasses.image, className)}
107
+ {...props}
108
+ />
109
+ ))
110
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName
111
+
112
+ const AvatarFallback = React.forwardRef<
113
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
114
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
115
+ >(({ className, ...props }, ref) => (
116
+ <AvatarPrimitive.Fallback
117
+ ref={ref}
118
+ className={cn(
119
+ avatarSlotClasses[avatarDefaultVariantKey],
120
+ className
121
+ )}
122
+ {...props}
123
+ />
124
+ ))
125
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
126
+
127
+ export { Avatar, AvatarImage, AvatarFallback }
@@ -0,0 +1,131 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+ import { Popover, PopoverContent, PopoverTrigger } from "../overlay/Popover"
7
+ import { Avatar, AvatarFallback } from "./Avatar"
8
+
9
+ type PopoverContentProps = React.ComponentPropsWithoutRef<typeof PopoverContent>
10
+
11
+ export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ /** Maximum avatars to show before collapsing into a +N indicator. */
13
+ max?: number
14
+ /** Pixel overlap between avatars (negative margin). Default 8. */
15
+ overlap?: number
16
+ /** Size class applied to each Avatar. */
17
+ avatarClassName?: string
18
+ /** Tooltip content shown on the +N overflow avatar. */
19
+ overflowTooltip?: React.ReactNode
20
+ /** Popover content opened when the +N overflow avatar is clicked. */
21
+ overflowContent?: React.ReactNode
22
+ /** Accessible label for the clickable +N overflow avatar. */
23
+ overflowAriaLabel?: string
24
+ /** Class applied to the +N overflow popover content. */
25
+ overflowContentClassName?: string
26
+ /** Portal container for the +N overflow popover. */
27
+ overflowPortalContainer?: HTMLElement | null
28
+ /** Side used by the +N overflow popover. */
29
+ overflowContentSide?: PopoverContentProps["side"]
30
+ /** Alignment used by the +N overflow popover. */
31
+ overflowContentAlign?: PopoverContentProps["align"]
32
+ /** Offset used by the +N overflow popover. */
33
+ overflowContentSideOffset?: PopoverContentProps["sideOffset"]
34
+ /** Whether the +N overflow popover may flip to avoid viewport collisions. */
35
+ overflowContentAvoidCollisions?: PopoverContentProps["avoidCollisions"]
36
+ }
37
+
38
+ const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
39
+ (
40
+ {
41
+ className,
42
+ max,
43
+ overlap = 8,
44
+ avatarClassName,
45
+ overflowTooltip,
46
+ overflowContent,
47
+ overflowAriaLabel,
48
+ overflowContentClassName,
49
+ overflowPortalContainer,
50
+ overflowContentSide = "bottom",
51
+ overflowContentAlign = "end",
52
+ overflowContentSideOffset = 8,
53
+ overflowContentAvoidCollisions,
54
+ children,
55
+ ...props
56
+ },
57
+ ref
58
+ ) => {
59
+ const items = React.Children.toArray(children).filter(React.isValidElement)
60
+ const visible = max !== undefined ? items.slice(0, max) : items
61
+ const overflow = max !== undefined ? items.length - visible.length : 0
62
+ const overflowAvatar = (
63
+ <Avatar
64
+ className={cn(
65
+ "ring-2 ring-background bg-muted",
66
+ avatarClassName
67
+ )}
68
+ tooltip={overflowTooltip}
69
+ >
70
+ <AvatarFallback className="text-xs font-medium">
71
+ +{overflow}
72
+ </AvatarFallback>
73
+ </Avatar>
74
+ )
75
+
76
+ return (
77
+ <div
78
+ ref={ref}
79
+ className={cn("flex items-center", className)}
80
+ {...props}
81
+ >
82
+ {visible.map((child, idx) => {
83
+ const isFirst = idx === 0
84
+ return (
85
+ <div
86
+ key={idx}
87
+ style={isFirst ? undefined : { marginLeft: -overlap }}
88
+ className={cn(
89
+ "rounded-full ring-2 ring-background",
90
+ avatarClassName
91
+ )}
92
+ >
93
+ {child}
94
+ </div>
95
+ )
96
+ })}
97
+ {overflow > 0 && overflowContent ? (
98
+ <Popover>
99
+ <PopoverTrigger asChild>
100
+ <button
101
+ type="button"
102
+ style={{ marginLeft: -overlap }}
103
+ className="inline-flex shrink-0 rounded-full border-0 bg-transparent p-0 text-inherit outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
104
+ aria-label={overflowAriaLabel ?? `Show ${overflow} more avatars`}
105
+ >
106
+ {overflowAvatar}
107
+ </button>
108
+ </PopoverTrigger>
109
+ <PopoverContent
110
+ align={overflowContentAlign}
111
+ side={overflowContentSide}
112
+ sideOffset={overflowContentSideOffset}
113
+ avoidCollisions={overflowContentAvoidCollisions}
114
+ className={cn("w-72 p-0", overflowContentClassName)}
115
+ portalContainer={overflowPortalContainer}
116
+ >
117
+ {overflowContent}
118
+ </PopoverContent>
119
+ </Popover>
120
+ ) : overflow > 0 ? (
121
+ <div style={{ marginLeft: -overlap }}>
122
+ {overflowAvatar}
123
+ </div>
124
+ ) : null}
125
+ </div>
126
+ )
127
+ }
128
+ )
129
+ AvatarGroup.displayName = "AvatarGroup"
130
+
131
+ export { AvatarGroup }
@@ -4,9 +4,9 @@ import type { BadgeVariantKey } from "./generated/variant-keys"
4
4
  import { badgeDefaultVariantKey } from "./generated/default-variant-keys"
5
5
 
6
6
  const badgeVariantClasses: Record<BadgeVariantKey, string> = {
7
- default: "bg-foreground text-primary-foreground hover:bg-foreground/80",
8
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
9
- destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/80",
7
+ default: "bg-foreground text-background",
8
+ secondary: "bg-secondary text-secondary-foreground",
9
+ destructive: "bg-destructive-strong text-destructive-strong-foreground",
10
10
  outline: "border-border bg-transparent text-foreground",
11
11
  }
12
12