azamat-ui-kit-cli 0.2.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 (213) hide show
  1. package/README.md +8 -0
  2. package/dist/index.js +432 -0
  3. package/package.json +34 -0
  4. package/vendor/package.json +4 -0
  5. package/vendor/src/components/actions/action-bar.tsx +35 -0
  6. package/vendor/src/components/actions/action-menu.tsx +120 -0
  7. package/vendor/src/components/actions/button-group.tsx +47 -0
  8. package/vendor/src/components/actions/copy-button.tsx +91 -0
  9. package/vendor/src/components/actions/copy-field.tsx +31 -0
  10. package/vendor/src/components/actions/floating-action-button.tsx +33 -0
  11. package/vendor/src/components/actions/index.ts +7 -0
  12. package/vendor/src/components/actions/public.ts +5 -0
  13. package/vendor/src/components/actions/quick-action-grid.tsx +162 -0
  14. package/vendor/src/components/calendar/calendar.tsx +328 -0
  15. package/vendor/src/components/calendar/date-picker.tsx +78 -0
  16. package/vendor/src/components/calendar/date-range-picker.tsx +96 -0
  17. package/vendor/src/components/calendar/date-utils.ts +89 -0
  18. package/vendor/src/components/calendar/index.ts +4 -0
  19. package/vendor/src/components/charts/charts.tsx +275 -0
  20. package/vendor/src/components/charts/horizontal-bar-chart.tsx +46 -0
  21. package/vendor/src/components/charts/index.ts +4 -0
  22. package/vendor/src/components/charts/kpi.tsx +68 -0
  23. package/vendor/src/components/charts/progress-ring.tsx +45 -0
  24. package/vendor/src/components/charts/public.ts +1 -0
  25. package/vendor/src/components/command/command-palette.tsx +375 -0
  26. package/vendor/src/components/command/index.ts +1 -0
  27. package/vendor/src/components/data-table/data-table-actions-column.tsx +58 -0
  28. package/vendor/src/components/data-table/data-table-bulk-actions.tsx +84 -0
  29. package/vendor/src/components/data-table/data-table-column-visibility-menu.tsx +79 -0
  30. package/vendor/src/components/data-table/data-table-pagination.tsx +91 -0
  31. package/vendor/src/components/data-table/data-table-row-actions.tsx +48 -0
  32. package/vendor/src/components/data-table/data-table-select-column.tsx +59 -0
  33. package/vendor/src/components/data-table/data-table-sortable-header.tsx +45 -0
  34. package/vendor/src/components/data-table/data-table-toolbar.tsx +76 -0
  35. package/vendor/src/components/data-table/data-table-view-presets.tsx +128 -0
  36. package/vendor/src/components/data-table/data-table.tsx +507 -0
  37. package/vendor/src/components/data-table/index.ts +12 -0
  38. package/vendor/src/components/data-table/public.ts +10 -0
  39. package/vendor/src/components/data-table/table-export-menu.tsx +56 -0
  40. package/vendor/src/components/data-table/table-import-button.tsx +43 -0
  41. package/vendor/src/components/display/activity-feed.tsx +97 -0
  42. package/vendor/src/components/display/avatar.tsx +131 -0
  43. package/vendor/src/components/display/code-block.tsx +33 -0
  44. package/vendor/src/components/display/data-state.tsx +63 -0
  45. package/vendor/src/components/display/description-list.tsx +119 -0
  46. package/vendor/src/components/display/descriptions.tsx +83 -0
  47. package/vendor/src/components/display/entity-card.tsx +53 -0
  48. package/vendor/src/components/display/file-card.tsx +54 -0
  49. package/vendor/src/components/display/index.ts +30 -0
  50. package/vendor/src/components/display/kanban.tsx +104 -0
  51. package/vendor/src/components/display/keyboard-shortcut.tsx +31 -0
  52. package/vendor/src/components/display/list.tsx +100 -0
  53. package/vendor/src/components/display/metric-grid.tsx +86 -0
  54. package/vendor/src/components/display/progress.tsx +162 -0
  55. package/vendor/src/components/display/property-grid.tsx +54 -0
  56. package/vendor/src/components/display/result.tsx +90 -0
  57. package/vendor/src/components/display/smart-card.tsx +168 -0
  58. package/vendor/src/components/display/statistic.tsx +107 -0
  59. package/vendor/src/components/display/status-legend.tsx +108 -0
  60. package/vendor/src/components/display/tag-list.tsx +52 -0
  61. package/vendor/src/components/display/timeline.tsx +132 -0
  62. package/vendor/src/components/display/tree-view.tsx +116 -0
  63. package/vendor/src/components/feedback/alert.tsx +69 -0
  64. package/vendor/src/components/feedback/empty-state.tsx +56 -0
  65. package/vendor/src/components/feedback/index.ts +5 -0
  66. package/vendor/src/components/feedback/loading-state.tsx +39 -0
  67. package/vendor/src/components/feedback/page-state.tsx +69 -0
  68. package/vendor/src/components/feedback/status-badge.tsx +62 -0
  69. package/vendor/src/components/filters/filter-bar.tsx +89 -0
  70. package/vendor/src/components/filters/filter-chips.tsx +69 -0
  71. package/vendor/src/components/filters/index.ts +2 -0
  72. package/vendor/src/components/form/form-actions.tsx +53 -0
  73. package/vendor/src/components/form/form-async-select.tsx +26 -0
  74. package/vendor/src/components/form/form-date-input.tsx +19 -0
  75. package/vendor/src/components/form/form-date-picker.tsx +54 -0
  76. package/vendor/src/components/form/form-date-range-input.tsx +79 -0
  77. package/vendor/src/components/form/form-date-range-picker.tsx +57 -0
  78. package/vendor/src/components/form/form-field-shell.tsx +191 -0
  79. package/vendor/src/components/form/form-input.tsx +480 -0
  80. package/vendor/src/components/form/form-number-input.tsx +19 -0
  81. package/vendor/src/components/form/form-password-input.tsx +19 -0
  82. package/vendor/src/components/form/form-phone-input.tsx +22 -0
  83. package/vendor/src/components/form/form-search-input.tsx +19 -0
  84. package/vendor/src/components/form/form-section.tsx +29 -0
  85. package/vendor/src/components/form/form-select.tsx +194 -0
  86. package/vendor/src/components/form/form-switch.tsx +145 -0
  87. package/vendor/src/components/form/form-textarea.tsx +103 -0
  88. package/vendor/src/components/form/index.ts +17 -0
  89. package/vendor/src/components/form/public.ts +14 -0
  90. package/vendor/src/components/form/smart-form-shell.tsx +59 -0
  91. package/vendor/src/components/inputs/async-select.tsx +1143 -0
  92. package/vendor/src/components/inputs/clearable-input.tsx +78 -0
  93. package/vendor/src/components/inputs/color-input.tsx +47 -0
  94. package/vendor/src/components/inputs/combobox.tsx +89 -0
  95. package/vendor/src/components/inputs/date-input.tsx +32 -0
  96. package/vendor/src/components/inputs/date-range-input.tsx +67 -0
  97. package/vendor/src/components/inputs/index.ts +19 -0
  98. package/vendor/src/components/inputs/input-chrome.tsx +37 -0
  99. package/vendor/src/components/inputs/input-decorator.tsx +64 -0
  100. package/vendor/src/components/inputs/input-value.ts +42 -0
  101. package/vendor/src/components/inputs/masked-input.tsx +51 -0
  102. package/vendor/src/components/inputs/money-input.tsx +73 -0
  103. package/vendor/src/components/inputs/number-input.tsx +87 -0
  104. package/vendor/src/components/inputs/numeric-value.ts +39 -0
  105. package/vendor/src/components/inputs/otp-input.tsx +102 -0
  106. package/vendor/src/components/inputs/password-input.tsx +85 -0
  107. package/vendor/src/components/inputs/phone-input.tsx +46 -0
  108. package/vendor/src/components/inputs/quantity-input.tsx +116 -0
  109. package/vendor/src/components/inputs/quantity-stepper.tsx +49 -0
  110. package/vendor/src/components/inputs/rating.tsx +98 -0
  111. package/vendor/src/components/inputs/search-input.tsx +26 -0
  112. package/vendor/src/components/inputs/simple-select.tsx +72 -0
  113. package/vendor/src/components/inputs/slider.tsx +149 -0
  114. package/vendor/src/components/inputs/tag-input.tsx +104 -0
  115. package/vendor/src/components/layout/app-header.tsx +46 -0
  116. package/vendor/src/components/layout/app-shell.tsx +243 -0
  117. package/vendor/src/components/layout/app-sidebar.tsx +179 -0
  118. package/vendor/src/components/layout/breadcrumbs.tsx +72 -0
  119. package/vendor/src/components/layout/index.ts +11 -0
  120. package/vendor/src/components/layout/page-container.tsx +30 -0
  121. package/vendor/src/components/layout/page-header.tsx +60 -0
  122. package/vendor/src/components/layout/public.ts +10 -0
  123. package/vendor/src/components/layout/section.tsx +76 -0
  124. package/vendor/src/components/layout/sidebar-nav.tsx +147 -0
  125. package/vendor/src/components/layout/stat-card.tsx +88 -0
  126. package/vendor/src/components/layout/sticky-footer-bar.tsx +23 -0
  127. package/vendor/src/components/layout/workspace-shell.tsx +50 -0
  128. package/vendor/src/components/navigation/anchor-nav.tsx +44 -0
  129. package/vendor/src/components/navigation/index.ts +4 -0
  130. package/vendor/src/components/navigation/page-tabs.tsx +67 -0
  131. package/vendor/src/components/navigation/pagination.tsx +179 -0
  132. package/vendor/src/components/navigation/stepper-tabs.tsx +67 -0
  133. package/vendor/src/components/notifications/index.ts +1 -0
  134. package/vendor/src/components/notifications/toast.tsx +259 -0
  135. package/vendor/src/components/overlay/confirm-dialog.tsx +66 -0
  136. package/vendor/src/components/overlay/dialog-actions.tsx +68 -0
  137. package/vendor/src/components/overlay/index.ts +4 -0
  138. package/vendor/src/components/overlay/modal-shell.tsx +93 -0
  139. package/vendor/src/components/overlay/sheet-shell.tsx +212 -0
  140. package/vendor/src/components/patterns/action-system.tsx +116 -0
  141. package/vendor/src/components/patterns/crud-system.tsx +53 -0
  142. package/vendor/src/components/patterns/data-view.tsx +84 -0
  143. package/vendor/src/components/patterns/entity-details.tsx +66 -0
  144. package/vendor/src/components/patterns/filter-builder.tsx +113 -0
  145. package/vendor/src/components/patterns/form-builder-presets.ts +131 -0
  146. package/vendor/src/components/patterns/form-builder.tsx +334 -0
  147. package/vendor/src/components/patterns/index.ts +12 -0
  148. package/vendor/src/components/patterns/public.ts +4 -0
  149. package/vendor/src/components/patterns/resource-detail-page.tsx +160 -0
  150. package/vendor/src/components/patterns/resource-page.tsx +159 -0
  151. package/vendor/src/components/patterns/resource-system.tsx +61 -0
  152. package/vendor/src/components/patterns/settings-section.tsx +46 -0
  153. package/vendor/src/components/patterns/status-system.tsx +89 -0
  154. package/vendor/src/components/theme-provider.tsx +51 -0
  155. package/vendor/src/components/ui/badge.tsx +52 -0
  156. package/vendor/src/components/ui/button.tsx +61 -0
  157. package/vendor/src/components/ui/card.tsx +103 -0
  158. package/vendor/src/components/ui/checkbox.tsx +82 -0
  159. package/vendor/src/components/ui/collapse.tsx +126 -0
  160. package/vendor/src/components/ui/command.tsx +194 -0
  161. package/vendor/src/components/ui/dialog.tsx +160 -0
  162. package/vendor/src/components/ui/divider.tsx +46 -0
  163. package/vendor/src/components/ui/dropdown-menu.tsx +266 -0
  164. package/vendor/src/components/ui/input-group.tsx +158 -0
  165. package/vendor/src/components/ui/input.tsx +20 -0
  166. package/vendor/src/components/ui/popover.tsx +90 -0
  167. package/vendor/src/components/ui/segmented-control.tsx +78 -0
  168. package/vendor/src/components/ui/select.tsx +201 -0
  169. package/vendor/src/components/ui/skeleton.tsx +75 -0
  170. package/vendor/src/components/ui/spinner.tsx +50 -0
  171. package/vendor/src/components/ui/switch.tsx +71 -0
  172. package/vendor/src/components/ui/table.tsx +114 -0
  173. package/vendor/src/components/ui/tabs.tsx +55 -0
  174. package/vendor/src/components/ui/textarea.tsx +18 -0
  175. package/vendor/src/components/ui/tooltip.tsx +38 -0
  176. package/vendor/src/components/upload/file-upload.tsx +483 -0
  177. package/vendor/src/components/upload/image-upload.tsx +118 -0
  178. package/vendor/src/components/upload/index.ts +2 -0
  179. package/vendor/src/components/wizard/index.ts +2 -0
  180. package/vendor/src/components/wizard/stepper.tsx +53 -0
  181. package/vendor/src/components/wizard/wizard.tsx +60 -0
  182. package/vendor/src/families/card-family.ts +28 -0
  183. package/vendor/src/families/catalog.ts +96 -0
  184. package/vendor/src/families/data-table-family.ts +31 -0
  185. package/vendor/src/families/docs-adoption.ts +103 -0
  186. package/vendor/src/families/docs-groups.ts +209 -0
  187. package/vendor/src/families/docs-queries.ts +84 -0
  188. package/vendor/src/families/docs-routing.ts +89 -0
  189. package/vendor/src/families/form-family.ts +45 -0
  190. package/vendor/src/families/index.ts +17 -0
  191. package/vendor/src/families/input-family.ts +61 -0
  192. package/vendor/src/families/member-metadata.ts +466 -0
  193. package/vendor/src/families/member-queries.ts +28 -0
  194. package/vendor/src/families/member-snippet-queries.ts +54 -0
  195. package/vendor/src/families/member-snippets.ts +673 -0
  196. package/vendor/src/families/migration-map.ts +79 -0
  197. package/vendor/src/families/queries.ts +63 -0
  198. package/vendor/src/families/select-family.ts +33 -0
  199. package/vendor/src/families/views.ts +81 -0
  200. package/vendor/src/hooks/index.ts +6 -0
  201. package/vendor/src/hooks/use-before-unload-when-dirty.ts +21 -0
  202. package/vendor/src/hooks/use-data-table-view-state.ts +122 -0
  203. package/vendor/src/hooks/use-debounce.ts +52 -0
  204. package/vendor/src/hooks/use-disclosure.ts +38 -0
  205. package/vendor/src/hooks/use-is-mobile.ts +28 -0
  206. package/vendor/src/hooks/use-session-storage-state.ts +85 -0
  207. package/vendor/src/index.ts +38 -0
  208. package/vendor/src/lib/utils.ts +6 -0
  209. package/vendor/templates/components/button.tsx +0 -0
  210. package/vendor/templates/components/data-table.tsx +0 -0
  211. package/vendor/templates/components/input.tsx +0 -0
  212. package/vendor/templates/lib/utils.ts +0 -0
  213. package/vendor/templates/styles/globals.css +0 -0
@@ -0,0 +1,168 @@
1
+ import * as React from "react"
2
+
3
+ import { Card } from "@/components/ui/card"
4
+ import { Skeleton, SkeletonText } from "@/components/ui/skeleton"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ /** @deprecated Prefer `InfoCardVariant` via `InfoCard` or `CardFamily.Info` for new public usage. */
8
+ export type SmartCardVariant = "default" | "outline" | "elevated" | "ghost"
9
+ /** @deprecated Prefer `InfoCardSize` via `InfoCard` or `CardFamily.Info` for new public usage. */
10
+ export type SmartCardSize = "sm" | "md" | "lg"
11
+ /** @deprecated Prefer `InfoCardDensity` via `InfoCard` or `CardFamily.Info` for new public usage. */
12
+ export type SmartCardDensity = "compact" | "default" | "comfortable"
13
+ /** @deprecated Prefer `InfoCardOrientation` via `InfoCard` or `CardFamily.Info` for new public usage. */
14
+ export type SmartCardOrientation = "vertical" | "horizontal"
15
+
16
+ /** @deprecated Prefer `InfoCardClassNames` via `InfoCard` or `CardFamily.Info` for new public usage. */
17
+ export type SmartCardClassNames = {
18
+ root?: string
19
+ media?: string
20
+ header?: string
21
+ icon?: string
22
+ body?: string
23
+ eyebrow?: string
24
+ title?: string
25
+ description?: string
26
+ content?: string
27
+ footer?: string
28
+ actions?: string
29
+ meta?: string
30
+ status?: string
31
+ }
32
+
33
+ /** @deprecated Prefer `InfoCardRenderContext` via `InfoCard` or `CardFamily.Info` for new public usage. */
34
+ export type SmartCardRenderContext = {
35
+ title?: React.ReactNode
36
+ description?: React.ReactNode
37
+ eyebrow?: React.ReactNode
38
+ media?: React.ReactNode
39
+ icon?: React.ReactNode
40
+ status?: React.ReactNode
41
+ actions?: React.ReactNode
42
+ meta?: React.ReactNode
43
+ content?: React.ReactNode
44
+ footer?: React.ReactNode
45
+ }
46
+
47
+ /** @deprecated Prefer `InfoCardProps` via `InfoCard` or `CardFamily.Info` for new public usage. */
48
+ export type SmartCardProps = Omit<React.ComponentProps<typeof Card>, "title" | "content" | "size"> & SmartCardRenderContext & {
49
+ orientation?: SmartCardOrientation
50
+ variant?: SmartCardVariant
51
+ size?: SmartCardSize
52
+ density?: SmartCardDensity
53
+ loading?: boolean
54
+ disabled?: boolean
55
+ selected?: boolean
56
+ interactive?: boolean
57
+ classNames?: SmartCardClassNames
58
+ renderHeader?: (ctx: SmartCardRenderContext) => React.ReactNode
59
+ renderMedia?: (ctx: SmartCardRenderContext) => React.ReactNode
60
+ renderContent?: (ctx: SmartCardRenderContext) => React.ReactNode
61
+ renderFooter?: (ctx: SmartCardRenderContext) => React.ReactNode
62
+ }
63
+
64
+ const variantClassName: Record<SmartCardVariant, string> = {
65
+ default: "bg-card",
66
+ outline: "border bg-card",
67
+ elevated: "border bg-card shadow-md",
68
+ ghost: "border-transparent bg-transparent shadow-none",
69
+ }
70
+
71
+ const densityClassName: Record<SmartCardDensity, string> = {
72
+ compact: "p-3",
73
+ default: "p-4",
74
+ comfortable: "p-5",
75
+ }
76
+
77
+ const titleClassName: Record<SmartCardSize, string> = {
78
+ sm: "text-sm",
79
+ md: "text-base",
80
+ lg: "text-lg",
81
+ }
82
+
83
+ /** @deprecated Prefer `InfoCard` or `CardFamily.Info` for new public usage. */
84
+ function SmartCard({
85
+ eyebrow,
86
+ title,
87
+ description,
88
+ media,
89
+ icon,
90
+ status,
91
+ actions,
92
+ meta,
93
+ content,
94
+ footer,
95
+ orientation = "vertical",
96
+ variant = "outline",
97
+ size = "md",
98
+ density = "default",
99
+ loading = false,
100
+ disabled = false,
101
+ selected = false,
102
+ interactive,
103
+ className,
104
+ classNames,
105
+ renderHeader,
106
+ renderMedia,
107
+ renderContent,
108
+ renderFooter,
109
+ children,
110
+ onClick,
111
+ ...props
112
+ }: SmartCardProps) {
113
+ const ctx: SmartCardRenderContext = { eyebrow, title, description, media, icon, status, actions, meta, content, footer }
114
+ const clickable = Boolean(onClick || interactive)
115
+
116
+ return (
117
+ <Card
118
+ data-slot="smart-card"
119
+ data-selected={selected || undefined}
120
+ data-disabled={disabled || undefined}
121
+ data-loading={loading || undefined}
122
+ className={cn(
123
+ "overflow-hidden transition-colors data-[selected=true]:border-primary data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-55",
124
+ variantClassName[variant],
125
+ clickable && "cursor-pointer hover:bg-muted/35",
126
+ orientation === "horizontal" && "flex",
127
+ className,
128
+ classNames?.root
129
+ )}
130
+ onClick={disabled ? undefined : onClick}
131
+ {...props}
132
+ >
133
+ {loading ? (
134
+ <div className={cn("grid gap-3", densityClassName[density])}>
135
+ <Skeleton className="h-5 w-1/2" />
136
+ <SkeletonText rows={3} />
137
+ </div>
138
+ ) : (
139
+ <>
140
+ {media && (renderMedia?.(ctx) ?? <div data-slot="smart-card-media" className={cn("bg-muted", orientation === "horizontal" ? "w-40 shrink-0" : "aspect-video", classNames?.media)}>{media}</div>)}
141
+ <div data-slot="smart-card-body" className={cn("grid min-w-0 flex-1 gap-3", densityClassName[density], classNames?.body)}>
142
+ {renderHeader?.(ctx) ?? (
143
+ <div data-slot="smart-card-header" className={cn("flex items-start justify-between gap-3", classNames?.header)}>
144
+ <div className="flex min-w-0 items-start gap-3">
145
+ {icon && <div data-slot="smart-card-icon" className={cn("flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground", classNames?.icon)}>{icon}</div>}
146
+ <div className="grid min-w-0 gap-1">
147
+ {eyebrow && <div data-slot="smart-card-eyebrow" className={cn("text-xs font-medium uppercase tracking-wide text-muted-foreground", classNames?.eyebrow)}>{eyebrow}</div>}
148
+ <div className="flex flex-wrap items-center gap-2">
149
+ {title && <div data-slot="smart-card-title" className={cn("truncate font-semibold text-foreground", titleClassName[size], classNames?.title)}>{title}</div>}
150
+ {status && <div data-slot="smart-card-status" className={classNames?.status}>{status}</div>}
151
+ </div>
152
+ {description && <div data-slot="smart-card-description" className={cn("line-clamp-2 text-sm text-muted-foreground", classNames?.description)}>{description}</div>}
153
+ </div>
154
+ </div>
155
+ {actions && <div data-slot="smart-card-actions" className={cn("shrink-0", classNames?.actions)} onClick={(event) => event.stopPropagation()}>{actions}</div>}
156
+ </div>
157
+ )}
158
+ {meta && <div data-slot="smart-card-meta" className={cn("text-xs text-muted-foreground", classNames?.meta)}>{meta}</div>}
159
+ {(content || children) && (renderContent?.(ctx) ?? <div data-slot="smart-card-content" className={classNames?.content}>{content ?? children}</div>)}
160
+ {footer && (renderFooter?.(ctx) ?? <div data-slot="smart-card-footer" className={cn("border-t pt-3 text-sm text-muted-foreground", classNames?.footer)}>{footer}</div>)}
161
+ </div>
162
+ </>
163
+ )}
164
+ </Card>
165
+ )
166
+ }
167
+
168
+ export { SmartCard }
@@ -0,0 +1,107 @@
1
+ import * as React from "react"
2
+ import { ArrowDownIcon, ArrowUpIcon } from "lucide-react"
3
+
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ export type StatisticTrend = "up" | "down" | "neutral"
8
+
9
+ export type StatisticProps = React.ComponentProps<"div"> & {
10
+ label: React.ReactNode
11
+ value: React.ReactNode
12
+ prefix?: React.ReactNode
13
+ suffix?: React.ReactNode
14
+ description?: React.ReactNode
15
+ trend?: StatisticTrend
16
+ change?: React.ReactNode
17
+ loading?: boolean
18
+ }
19
+
20
+ function Statistic({ label, value, prefix, suffix, description, trend = "neutral", change, loading = false, className, ...props }: StatisticProps) {
21
+ const trendIcon = trend === "up" ? <ArrowUpIcon className="size-3" /> : trend === "down" ? <ArrowDownIcon className="size-3" /> : null
22
+
23
+ return (
24
+ <div data-slot="statistic" className={cn("grid gap-1", className)} {...props}>
25
+ <div className="text-sm text-muted-foreground">{label}</div>
26
+ <div className="flex flex-wrap items-baseline gap-1.5">
27
+ {loading ? (
28
+ <div className="h-8 w-28 animate-pulse rounded-md bg-muted" />
29
+ ) : (
30
+ <>
31
+ {prefix && <span className="text-base text-muted-foreground">{prefix}</span>}
32
+ <span className="text-2xl font-semibold tracking-tight">{value}</span>
33
+ {suffix && <span className="text-sm text-muted-foreground">{suffix}</span>}
34
+ </>
35
+ )}
36
+ </div>
37
+ {(description || change) && (
38
+ <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
39
+ {change && (
40
+ <span
41
+ className={cn(
42
+ "inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 font-medium",
43
+ trend === "up" && "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
44
+ trend === "down" && "bg-destructive/10 text-destructive",
45
+ trend === "neutral" && "bg-muted text-muted-foreground"
46
+ )}
47
+ >
48
+ {trendIcon}
49
+ {change}
50
+ </span>
51
+ )}
52
+ {description && <span>{description}</span>}
53
+ </div>
54
+ )}
55
+ </div>
56
+ )
57
+ }
58
+
59
+ export type StatisticCardProps = React.ComponentProps<typeof Card> & StatisticProps & {
60
+ action?: React.ReactNode
61
+ }
62
+
63
+ function StatisticCard({ action, label, value, prefix, suffix, description, trend, change, loading, className, ...props }: StatisticCardProps) {
64
+ return (
65
+ <Card data-slot="statistic-card" className={className} {...props}>
66
+ <CardHeader className="flex flex-row items-center justify-between gap-3 pb-2">
67
+ <CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
68
+ {action}
69
+ </CardHeader>
70
+ <CardContent>
71
+ <Statistic
72
+ label={<span className="sr-only">{label}</span>}
73
+ value={value}
74
+ prefix={prefix}
75
+ suffix={suffix}
76
+ description={description}
77
+ trend={trend}
78
+ change={change}
79
+ loading={loading}
80
+ />
81
+ </CardContent>
82
+ </Card>
83
+ )
84
+ }
85
+
86
+ export type StatisticGridProps = React.ComponentProps<"div"> & {
87
+ columns?: 1 | 2 | 3 | 4
88
+ }
89
+
90
+ function StatisticGrid({ columns = 4, className, ...props }: StatisticGridProps) {
91
+ return (
92
+ <div
93
+ data-slot="statistic-grid"
94
+ className={cn(
95
+ "grid gap-4",
96
+ columns === 1 && "grid-cols-1",
97
+ columns === 2 && "grid-cols-1 sm:grid-cols-2",
98
+ columns === 3 && "grid-cols-1 sm:grid-cols-2 xl:grid-cols-3",
99
+ columns === 4 && "grid-cols-1 sm:grid-cols-2 xl:grid-cols-4",
100
+ className
101
+ )}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ export { Statistic, StatisticCard, StatisticGrid }
@@ -0,0 +1,108 @@
1
+ import * as React from "react"
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
4
+ import { Badge } from "@/components/ui/badge"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ export type StatusLegendTone = "default" | "success" | "warning" | "danger" | "info" | "muted"
8
+ export type StatusLegendOrientation = "vertical" | "horizontal" | "grid"
9
+
10
+ export type StatusLegendItem = {
11
+ key: string
12
+ label: React.ReactNode
13
+ description?: React.ReactNode
14
+ count?: React.ReactNode
15
+ tone?: StatusLegendTone
16
+ icon?: React.ReactNode
17
+ hidden?: boolean
18
+ className?: string
19
+ }
20
+
21
+ export type StatusLegendProps = React.ComponentProps<typeof Card> & {
22
+ title?: React.ReactNode
23
+ description?: React.ReactNode
24
+ actions?: React.ReactNode
25
+ items: StatusLegendItem[]
26
+ orientation?: StatusLegendOrientation
27
+ compact?: boolean
28
+ showCounts?: boolean
29
+ contentClassName?: string
30
+ itemClassName?: string
31
+ }
32
+
33
+ const dotClassName: Record<StatusLegendTone, string> = {
34
+ default: "bg-primary",
35
+ success: "bg-emerald-500",
36
+ warning: "bg-amber-500",
37
+ danger: "bg-destructive",
38
+ info: "bg-blue-500",
39
+ muted: "bg-muted-foreground",
40
+ }
41
+
42
+ function StatusLegend({
43
+ title,
44
+ description,
45
+ actions,
46
+ items,
47
+ orientation = "vertical",
48
+ compact = false,
49
+ showCounts = true,
50
+ contentClassName,
51
+ itemClassName,
52
+ className,
53
+ ...props
54
+ }: StatusLegendProps) {
55
+ const visibleItems = items.filter((item) => !item.hidden)
56
+ const hasHeader = Boolean(title || description || actions)
57
+
58
+ return (
59
+ <Card data-slot="status-legend" className={cn("min-w-0", className)} {...props}>
60
+ {hasHeader && (
61
+ <CardHeader className={cn(compact && "p-4 pb-2")}>
62
+ <div className="flex min-w-0 items-start justify-between gap-3">
63
+ <div className="min-w-0 space-y-1">
64
+ {title && <CardTitle>{title}</CardTitle>}
65
+ {description && <CardDescription>{description}</CardDescription>}
66
+ </div>
67
+ {actions && <div className="shrink-0">{actions}</div>}
68
+ </div>
69
+ </CardHeader>
70
+ )}
71
+
72
+ <CardContent
73
+ className={cn(
74
+ "gap-2",
75
+ orientation === "vertical" && "grid",
76
+ orientation === "horizontal" && "flex flex-wrap",
77
+ orientation === "grid" && "grid sm:grid-cols-2",
78
+ compact && "p-4 pt-2",
79
+ contentClassName
80
+ )}
81
+ >
82
+ {visibleItems.map((item) => (
83
+ <div
84
+ key={item.key}
85
+ data-slot="status-legend-item"
86
+ data-tone={item.tone ?? "default"}
87
+ className={cn("flex min-w-0 items-start justify-between gap-3 rounded-lg border bg-muted/20 p-3", compact && "p-2", itemClassName, item.className)}
88
+ >
89
+ <div className="flex min-w-0 items-start gap-2">
90
+ {item.icon ? (
91
+ <span className="mt-0.5 shrink-0 text-muted-foreground [&_svg]:size-4">{item.icon}</span>
92
+ ) : (
93
+ <span className={cn("mt-1.5 size-2.5 shrink-0 rounded-full", dotClassName[item.tone ?? "default"])} />
94
+ )}
95
+ <div className="min-w-0 space-y-0.5">
96
+ <div className="truncate text-sm font-medium text-foreground">{item.label}</div>
97
+ {item.description && <div className="text-xs leading-5 text-muted-foreground">{item.description}</div>}
98
+ </div>
99
+ </div>
100
+ {showCounts && item.count !== undefined && <Badge variant="secondary" className="shrink-0">{item.count}</Badge>}
101
+ </div>
102
+ ))}
103
+ </CardContent>
104
+ </Card>
105
+ )
106
+ }
107
+
108
+ export { StatusLegend }
@@ -0,0 +1,52 @@
1
+ import * as React from "react"
2
+ import { XIcon } from "lucide-react"
3
+
4
+ import { Badge } from "@/components/ui/badge"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ export type TagListItem = {
8
+ key: string
9
+ label: React.ReactNode
10
+ variant?: React.ComponentProps<typeof Badge>["variant"]
11
+ disabled?: boolean
12
+ }
13
+
14
+ export type TagListProps = React.ComponentProps<"div"> & {
15
+ items: TagListItem[]
16
+ max?: number
17
+ removable?: boolean
18
+ onRemove?: (item: TagListItem) => void
19
+ overflowLabel?: (count: number) => React.ReactNode
20
+ }
21
+
22
+ function TagList({ items, max, removable = false, onRemove, overflowLabel = (count) => `+${count}`, className, ...props }: TagListProps) {
23
+ const visibleItems = typeof max === "number" ? items.slice(0, max) : items
24
+ const overflowCount = typeof max === "number" ? Math.max(items.length - max, 0) : 0
25
+
26
+ return (
27
+ <div data-slot="tag-list" className={cn("flex flex-wrap items-center gap-1.5", className)} {...props}>
28
+ {visibleItems.map((item) => (
29
+ <Badge
30
+ key={item.key}
31
+ variant={item.variant}
32
+ className={cn("gap-1", item.disabled && "opacity-55")}
33
+ >
34
+ <span>{item.label}</span>
35
+ {removable && !item.disabled && (
36
+ <button
37
+ type="button"
38
+ aria-label="Remove tag"
39
+ className="rounded-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
40
+ onClick={() => onRemove?.(item)}
41
+ >
42
+ <XIcon className="size-3" />
43
+ </button>
44
+ )}
45
+ </Badge>
46
+ ))}
47
+ {overflowCount > 0 && <Badge variant="outline">{overflowLabel(overflowCount)}</Badge>}
48
+ </div>
49
+ )
50
+ }
51
+
52
+ export { TagList }
@@ -0,0 +1,132 @@
1
+ import * as React from "react"
2
+ import { CircleIcon } from "lucide-react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ export type TimelineTone = "default" | "success" | "info" | "warning" | "danger" | "muted"
7
+ export type TimelineOrientation = "vertical" | "horizontal"
8
+
9
+ export type TimelineItem = {
10
+ key: string
11
+ title?: React.ReactNode
12
+ description?: React.ReactNode
13
+ time?: React.ReactNode
14
+ icon?: React.ReactNode
15
+ tone?: TimelineTone
16
+ content?: React.ReactNode
17
+ actions?: React.ReactNode
18
+ hidden?: boolean
19
+ className?: string
20
+ }
21
+
22
+ export type TimelineProps = React.ComponentProps<"div"> & {
23
+ items: TimelineItem[]
24
+ orientation?: TimelineOrientation
25
+ pending?: boolean
26
+ pendingLabel?: React.ReactNode
27
+ compact?: boolean
28
+ itemClassName?: string
29
+ }
30
+
31
+ const dotClassName: Record<TimelineTone, string> = {
32
+ default: "border-primary bg-primary text-primary-foreground",
33
+ success: "border-emerald-500 bg-emerald-500 text-white",
34
+ info: "border-blue-500 bg-blue-500 text-white",
35
+ warning: "border-amber-500 bg-amber-500 text-white",
36
+ danger: "border-destructive bg-destructive text-destructive-foreground",
37
+ muted: "border-muted-foreground bg-muted-foreground text-background",
38
+ }
39
+
40
+ function Timeline({
41
+ items,
42
+ orientation = "vertical",
43
+ pending = false,
44
+ pendingLabel = "Pending",
45
+ compact = false,
46
+ itemClassName,
47
+ className,
48
+ ...props
49
+ }: TimelineProps) {
50
+ const visibleItems = items.filter((item) => !item.hidden)
51
+
52
+ if (orientation === "horizontal") {
53
+ return (
54
+ <div data-slot="timeline" data-orientation="horizontal" className={cn("overflow-x-auto", className)} {...props}>
55
+ <div className="flex min-w-max gap-3 pb-1">
56
+ {visibleItems.map((item) => (
57
+ <TimelineHorizontalItem key={item.key} item={item} compact={compact} className={itemClassName} />
58
+ ))}
59
+ {pending && <TimelineHorizontalItem item={{ key: "pending", title: pendingLabel, tone: "muted" }} compact={compact} className={itemClassName} />}
60
+ </div>
61
+ </div>
62
+ )
63
+ }
64
+
65
+ return (
66
+ <div data-slot="timeline" data-orientation="vertical" className={cn("grid gap-0", className)} {...props}>
67
+ {visibleItems.map((item, index) => (
68
+ <TimelineVerticalItem
69
+ key={item.key}
70
+ item={item}
71
+ compact={compact}
72
+ className={itemClassName}
73
+ isLast={index === visibleItems.length - 1 && !pending}
74
+ />
75
+ ))}
76
+ {pending && <TimelineVerticalItem item={{ key: "pending", title: pendingLabel, tone: "muted" }} compact={compact} className={itemClassName} isLast />}
77
+ </div>
78
+ )
79
+ }
80
+
81
+ function TimelineDot({ item }: { item: TimelineItem }) {
82
+ const tone = item.tone ?? "default"
83
+
84
+ return (
85
+ <span
86
+ data-slot="timeline-dot"
87
+ data-tone={tone}
88
+ className={cn("flex size-7 shrink-0 items-center justify-center rounded-full border text-[10px]", dotClassName[tone])}
89
+ >
90
+ {item.icon ?? <CircleIcon className="size-2 fill-current" />}
91
+ </span>
92
+ )
93
+ }
94
+
95
+ function TimelineVerticalItem({ item, compact, className, isLast }: { item: TimelineItem; compact: boolean; className?: string; isLast?: boolean }) {
96
+ return (
97
+ <div data-slot="timeline-item" className={cn("grid grid-cols-[auto_1fr] gap-3", className, item.className)}>
98
+ <div className="flex flex-col items-center">
99
+ <TimelineDot item={item} />
100
+ {!isLast && <div data-slot="timeline-line" className="w-px flex-1 bg-border" />}
101
+ </div>
102
+ <div className={cn("min-w-0 pb-5", compact && "pb-3")}>
103
+ <div className="flex min-w-0 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
104
+ <div className="min-w-0">
105
+ {item.title && <div className="text-sm font-medium text-foreground">{item.title}</div>}
106
+ {item.description && <div className="text-sm text-muted-foreground">{item.description}</div>}
107
+ </div>
108
+ {item.time && <div className="shrink-0 text-xs text-muted-foreground">{item.time}</div>}
109
+ </div>
110
+ {item.content && <div className="mt-2 text-sm text-muted-foreground">{item.content}</div>}
111
+ {item.actions && <div className="mt-2">{item.actions}</div>}
112
+ </div>
113
+ </div>
114
+ )
115
+ }
116
+
117
+ function TimelineHorizontalItem({ item, compact, className }: { item: TimelineItem; compact: boolean; className?: string }) {
118
+ return (
119
+ <div data-slot="timeline-item" className={cn("min-w-48 rounded-xl border bg-card p-3", compact && "min-w-40 p-2", className, item.className)}>
120
+ <div className="mb-3 flex items-center gap-2">
121
+ <TimelineDot item={item} />
122
+ {item.time && <div className="text-xs text-muted-foreground">{item.time}</div>}
123
+ </div>
124
+ {item.title && <div className="text-sm font-medium">{item.title}</div>}
125
+ {item.description && <div className="mt-1 text-xs text-muted-foreground">{item.description}</div>}
126
+ {item.content && <div className="mt-2 text-sm text-muted-foreground">{item.content}</div>}
127
+ {item.actions && <div className="mt-2">{item.actions}</div>}
128
+ </div>
129
+ )
130
+ }
131
+
132
+ export { Timeline }
@@ -0,0 +1,116 @@
1
+ import * as React from "react"
2
+ import { ChevronRightIcon } from "lucide-react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ export type TreeViewItem = {
7
+ key: string
8
+ label: React.ReactNode
9
+ icon?: React.ReactNode
10
+ extra?: React.ReactNode
11
+ children?: TreeViewItem[]
12
+ disabled?: boolean
13
+ }
14
+
15
+ export type TreeViewProps = React.ComponentProps<"div"> & {
16
+ items: TreeViewItem[]
17
+ defaultExpandedKeys?: string[]
18
+ expandedKeys?: string[]
19
+ onExpandedKeysChange?: (keys: string[]) => void
20
+ selectedKey?: string
21
+ onSelect?: (item: TreeViewItem) => void
22
+ }
23
+
24
+ function TreeView({ items, defaultExpandedKeys = [], expandedKeys, onExpandedKeysChange, selectedKey, onSelect, className, ...props }: TreeViewProps) {
25
+ const [internalExpandedKeys, setInternalExpandedKeys] = React.useState(defaultExpandedKeys)
26
+ const currentExpandedKeys = expandedKeys ?? internalExpandedKeys
27
+
28
+ const setExpandedKeys = (nextKeys: string[]) => {
29
+ if (expandedKeys === undefined) setInternalExpandedKeys(nextKeys)
30
+ onExpandedKeysChange?.(nextKeys)
31
+ }
32
+
33
+ const toggle = (key: string) => {
34
+ setExpandedKeys(currentExpandedKeys.includes(key) ? currentExpandedKeys.filter((item) => item !== key) : [...currentExpandedKeys, key])
35
+ }
36
+
37
+ return (
38
+ <div data-slot="tree-view" role="tree" className={cn("grid gap-1 text-sm", className)} {...props}>
39
+ {items.map((item) => (
40
+ <TreeViewNode
41
+ key={item.key}
42
+ item={item}
43
+ level={1}
44
+ expandedKeys={currentExpandedKeys}
45
+ selectedKey={selectedKey}
46
+ onToggle={toggle}
47
+ onSelect={onSelect}
48
+ />
49
+ ))}
50
+ </div>
51
+ )
52
+ }
53
+
54
+ function TreeViewNode({
55
+ item,
56
+ level,
57
+ expandedKeys,
58
+ selectedKey,
59
+ onToggle,
60
+ onSelect,
61
+ }: {
62
+ item: TreeViewItem
63
+ level: number
64
+ expandedKeys: string[]
65
+ selectedKey?: string
66
+ onToggle: (key: string) => void
67
+ onSelect?: (item: TreeViewItem) => void
68
+ }) {
69
+ const hasChildren = Boolean(item.children?.length)
70
+ const expanded = expandedKeys.includes(item.key)
71
+ const selected = selectedKey === item.key
72
+
73
+ return (
74
+ <div data-slot="tree-node" role="treeitem" aria-expanded={hasChildren ? expanded : undefined} aria-selected={selected || undefined}>
75
+ <div
76
+ className={cn(
77
+ "flex items-center gap-1 rounded-md px-2 py-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
78
+ selected && "bg-muted text-foreground",
79
+ item.disabled && "pointer-events-none opacity-50"
80
+ )}
81
+ style={{ paddingLeft: `${level * 0.75}rem` }}
82
+ >
83
+ <button
84
+ type="button"
85
+ aria-label={expanded ? "Collapse" : "Expand"}
86
+ className={cn("flex size-5 items-center justify-center rounded-sm", !hasChildren && "invisible")}
87
+ onClick={() => onToggle(item.key)}
88
+ >
89
+ <ChevronRightIcon className={cn("size-4 transition-transform", expanded && "rotate-90")} />
90
+ </button>
91
+ {item.icon}
92
+ <button type="button" className="min-w-0 flex-1 truncate text-left" onClick={() => onSelect?.(item)}>
93
+ {item.label}
94
+ </button>
95
+ {item.extra}
96
+ </div>
97
+ {hasChildren && expanded && (
98
+ <div role="group" className="grid gap-1">
99
+ {item.children?.map((child) => (
100
+ <TreeViewNode
101
+ key={child.key}
102
+ item={child}
103
+ level={level + 1}
104
+ expandedKeys={expandedKeys}
105
+ selectedKey={selectedKey}
106
+ onToggle={onToggle}
107
+ onSelect={onSelect}
108
+ />
109
+ ))}
110
+ </div>
111
+ )}
112
+ </div>
113
+ )
114
+ }
115
+
116
+ export { TreeView }