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,104 @@
1
+ import * as React from "react"
2
+
3
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ export type KanbanCard = {
7
+ key: string
8
+ title: React.ReactNode
9
+ description?: React.ReactNode
10
+ meta?: React.ReactNode
11
+ extra?: React.ReactNode
12
+ disabled?: boolean
13
+ }
14
+
15
+ export type KanbanColumn = {
16
+ key: string
17
+ title: React.ReactNode
18
+ description?: React.ReactNode
19
+ cards: KanbanCard[]
20
+ count?: React.ReactNode
21
+ }
22
+
23
+ export type KanbanBoardProps = React.ComponentProps<"div"> & {
24
+ columns: KanbanColumn[]
25
+ renderCard?: (card: KanbanCard, column: KanbanColumn) => React.ReactNode
26
+ onCardClick?: (card: KanbanCard, column: KanbanColumn) => void
27
+ columnClassName?: string
28
+ cardClassName?: string
29
+ }
30
+
31
+ function KanbanBoard({ columns, renderCard, onCardClick, columnClassName, cardClassName, className, ...props }: KanbanBoardProps) {
32
+ return (
33
+ <div data-slot="kanban-board" className={cn("grid auto-cols-[minmax(280px,1fr)] grid-flow-col gap-4 overflow-x-auto pb-2", className)} {...props}>
34
+ {columns.map((column) => (
35
+ <KanbanColumnView
36
+ key={column.key}
37
+ column={column}
38
+ renderCard={renderCard}
39
+ onCardClick={onCardClick}
40
+ columnClassName={columnClassName}
41
+ cardClassName={cardClassName}
42
+ />
43
+ ))}
44
+ </div>
45
+ )
46
+ }
47
+
48
+ function KanbanColumnView({
49
+ column,
50
+ renderCard,
51
+ onCardClick,
52
+ columnClassName,
53
+ cardClassName,
54
+ }: {
55
+ column: KanbanColumn
56
+ renderCard?: (card: KanbanCard, column: KanbanColumn) => React.ReactNode
57
+ onCardClick?: (card: KanbanCard, column: KanbanColumn) => void
58
+ columnClassName?: string
59
+ cardClassName?: string
60
+ }) {
61
+ return (
62
+ <section data-slot="kanban-column" className={cn("grid min-h-40 content-start gap-3 rounded-lg border bg-muted/35 p-3", columnClassName)}>
63
+ <div className="flex items-start justify-between gap-3">
64
+ <div className="grid gap-0.5">
65
+ <h3 className="text-sm font-semibold text-foreground">{column.title}</h3>
66
+ {column.description && <p className="text-xs text-muted-foreground">{column.description}</p>}
67
+ </div>
68
+ <div className="rounded-full bg-background px-2 py-0.5 text-xs text-muted-foreground">{column.count ?? column.cards.length}</div>
69
+ </div>
70
+ <div className="grid gap-2">
71
+ {column.cards.map((card) => (
72
+ renderCard?.(card, column) ?? (
73
+ <Card
74
+ key={card.key}
75
+ data-slot="kanban-card"
76
+ className={cn(
77
+ "transition-colors hover:bg-muted/40",
78
+ onCardClick && !card.disabled && "cursor-pointer",
79
+ card.disabled && "pointer-events-none opacity-55",
80
+ cardClassName
81
+ )}
82
+ onClick={() => onCardClick?.(card, column)}
83
+ >
84
+ <CardHeader className="p-3 pb-1">
85
+ <div className="flex items-start justify-between gap-2">
86
+ <CardTitle className="text-sm">{card.title}</CardTitle>
87
+ {card.extra}
88
+ </div>
89
+ </CardHeader>
90
+ {(card.description || card.meta) && (
91
+ <CardContent className="grid gap-2 p-3 pt-0 text-xs text-muted-foreground">
92
+ {card.description}
93
+ {card.meta && <div>{card.meta}</div>}
94
+ </CardContent>
95
+ )}
96
+ </Card>
97
+ )
98
+ ))}
99
+ </div>
100
+ </section>
101
+ )
102
+ }
103
+
104
+ export { KanbanBoard }
@@ -0,0 +1,31 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export type KeyboardShortcutProps = React.ComponentProps<"kbd"> & {
6
+ keys?: React.ReactNode[]
7
+ separator?: React.ReactNode
8
+ }
9
+
10
+ function KeyboardShortcut({ keys, separator = "+", className, children, ...props }: KeyboardShortcutProps) {
11
+ const content = keys?.length
12
+ ? keys.map((key, index) => (
13
+ <React.Fragment key={index}>
14
+ {index > 0 && <span className="text-muted-foreground">{separator}</span>}
15
+ <span>{key}</span>
16
+ </React.Fragment>
17
+ ))
18
+ : children
19
+
20
+ return (
21
+ <kbd
22
+ data-slot="keyboard-shortcut"
23
+ className={cn("inline-flex items-center gap-1 rounded border bg-muted px-1.5 py-0.5 font-mono text-[11px] font-medium text-muted-foreground", className)}
24
+ {...props}
25
+ >
26
+ {content}
27
+ </kbd>
28
+ )
29
+ }
30
+
31
+ export { KeyboardShortcut }
@@ -0,0 +1,100 @@
1
+ import * as React from "react"
2
+ import { ChevronRightIcon } from "lucide-react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ export type ListItem = {
7
+ key: string
8
+ title: React.ReactNode
9
+ description?: React.ReactNode
10
+ avatar?: React.ReactNode
11
+ extra?: React.ReactNode
12
+ href?: string
13
+ disabled?: boolean
14
+ onClick?: () => void
15
+ }
16
+
17
+ export type ListProps = React.ComponentProps<"div"> & {
18
+ items?: ListItem[]
19
+ bordered?: boolean
20
+ split?: boolean
21
+ size?: "sm" | "md" | "lg"
22
+ renderItem?: (item: ListItem, index: number) => React.ReactNode
23
+ }
24
+
25
+ const itemPadding = {
26
+ sm: "px-3 py-2",
27
+ md: "px-4 py-3",
28
+ lg: "px-5 py-4",
29
+ }
30
+
31
+ function List({ items, bordered = true, split = true, size = "md", renderItem, className, children, ...props }: ListProps) {
32
+ return (
33
+ <div
34
+ data-slot="list"
35
+ className={cn("overflow-hidden bg-card", bordered && "rounded-lg border", className)}
36
+ {...props}
37
+ >
38
+ {items?.map((item, index) => renderItem?.(item, index) ?? <ListRow key={item.key} item={item} split={split} size={size} />)}
39
+ {children}
40
+ </div>
41
+ )
42
+ }
43
+
44
+ export type ListRowProps = React.ComponentProps<"div"> & {
45
+ item: ListItem
46
+ split?: boolean
47
+ size?: "sm" | "md" | "lg"
48
+ }
49
+
50
+ function ListRow({ item, split = true, size = "md", className, ...props }: ListRowProps) {
51
+ const clickable = Boolean(item.href || item.onClick)
52
+ const content = (
53
+ <>
54
+ {item.avatar && <div className="shrink-0">{item.avatar}</div>}
55
+ <div className="min-w-0 flex-1">
56
+ <div className="truncate text-sm font-medium text-foreground">{item.title}</div>
57
+ {item.description && <div className="mt-0.5 line-clamp-2 text-sm text-muted-foreground">{item.description}</div>}
58
+ </div>
59
+ {item.extra && <div className="shrink-0 text-sm text-muted-foreground">{item.extra}</div>}
60
+ {clickable && <ChevronRightIcon className="size-4 shrink-0 text-muted-foreground" />}
61
+ </>
62
+ )
63
+
64
+ const rowClassName = cn(
65
+ "flex items-center gap-3 bg-card transition-colors",
66
+ itemPadding[size],
67
+ split && "border-b last:border-b-0",
68
+ clickable && !item.disabled && "cursor-pointer hover:bg-muted/50",
69
+ item.disabled && "pointer-events-none opacity-55",
70
+ className
71
+ )
72
+
73
+ if (item.href) {
74
+ return (
75
+ <a
76
+ data-slot="list-row"
77
+ href={item.href}
78
+ className={rowClassName}
79
+ {...(props as React.ComponentProps<"a">)}
80
+ >
81
+ {content}
82
+ </a>
83
+ )
84
+ }
85
+
86
+ return (
87
+ <div
88
+ data-slot="list-row"
89
+ role={clickable ? "button" : undefined}
90
+ tabIndex={clickable && !item.disabled ? 0 : undefined}
91
+ className={rowClassName}
92
+ onClick={item.onClick}
93
+ {...(props as React.ComponentProps<"div">)}
94
+ >
95
+ {content}
96
+ </div>
97
+ )
98
+ }
99
+
100
+ export { List, ListRow }
@@ -0,0 +1,86 @@
1
+ import * as React from "react"
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ export type MetricTone = "default" | "success" | "warning" | "danger" | "info" | "muted"
7
+ export type MetricGridColumn = 1 | 2 | 3 | 4
8
+
9
+ export type MetricItem = {
10
+ key: string
11
+ label: React.ReactNode
12
+ value: React.ReactNode
13
+ description?: React.ReactNode
14
+ icon?: React.ReactNode
15
+ trend?: React.ReactNode
16
+ tone?: MetricTone
17
+ hidden?: boolean
18
+ className?: string
19
+ }
20
+
21
+ export type MetricGridProps = React.ComponentProps<"div"> & {
22
+ items: MetricItem[]
23
+ columns?: MetricGridColumn
24
+ compact?: boolean
25
+ itemClassName?: string
26
+ }
27
+
28
+ const columnsClassName: Record<MetricGridColumn, string> = {
29
+ 1: "grid-cols-1",
30
+ 2: "grid-cols-1 sm:grid-cols-2",
31
+ 3: "grid-cols-1 sm:grid-cols-2 xl:grid-cols-3",
32
+ 4: "grid-cols-1 sm:grid-cols-2 xl:grid-cols-4",
33
+ }
34
+
35
+ const toneClassName: Record<MetricTone, string> = {
36
+ default: "",
37
+ success: "border-emerald-500/20 bg-emerald-500/5",
38
+ warning: "border-amber-500/20 bg-amber-500/5",
39
+ danger: "border-destructive/20 bg-destructive/5",
40
+ info: "border-blue-500/20 bg-blue-500/5",
41
+ muted: "bg-muted/40",
42
+ }
43
+
44
+ function MetricGrid({
45
+ className,
46
+ items,
47
+ columns = 4,
48
+ compact = false,
49
+ itemClassName,
50
+ ...props
51
+ }: MetricGridProps) {
52
+ const visibleItems = items.filter((item) => !item.hidden)
53
+
54
+ return (
55
+ <div
56
+ data-slot="metric-grid"
57
+ className={cn("grid gap-3", columnsClassName[columns], className)}
58
+ {...props}
59
+ >
60
+ {visibleItems.map((item) => (
61
+ <Card
62
+ key={item.key}
63
+ data-slot="metric-card"
64
+ data-tone={item.tone ?? "default"}
65
+ className={cn("min-w-0 overflow-hidden", toneClassName[item.tone ?? "default"], itemClassName, item.className)}
66
+ >
67
+ <CardHeader className={cn("flex flex-row items-start justify-between gap-3 space-y-0", compact ? "p-3 pb-1" : "pb-2")}>
68
+ <div className="min-w-0 space-y-1">
69
+ <CardDescription className="truncate">{item.label}</CardDescription>
70
+ <CardTitle className={cn("truncate", compact ? "text-xl" : "text-2xl")}>{item.value}</CardTitle>
71
+ </div>
72
+ {item.icon && <div className="shrink-0 rounded-lg bg-muted p-2 text-muted-foreground [&_svg]:size-4">{item.icon}</div>}
73
+ </CardHeader>
74
+ {(item.description || item.trend) && (
75
+ <CardContent className={cn("flex min-w-0 items-center justify-between gap-2", compact ? "p-3 pt-1" : "pt-0")}>
76
+ {item.description && <div className="min-w-0 truncate text-xs text-muted-foreground">{item.description}</div>}
77
+ {item.trend && <div className="shrink-0 text-xs font-medium">{item.trend}</div>}
78
+ </CardContent>
79
+ )}
80
+ </Card>
81
+ ))}
82
+ </div>
83
+ )
84
+ }
85
+
86
+ export { MetricGrid }
@@ -0,0 +1,162 @@
1
+ import * as React from "react"
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ export type ProgressTone = "default" | "success" | "info" | "warning" | "danger" | "muted"
7
+ export type ProgressSize = "sm" | "default" | "lg"
8
+
9
+ export type ProgressProps = Omit<React.ComponentProps<"div">, "children"> & {
10
+ value?: number | null
11
+ max?: number
12
+ min?: number
13
+ label?: React.ReactNode
14
+ description?: React.ReactNode
15
+ showValue?: boolean
16
+ valueFormatter?: (value: number, percent: number) => React.ReactNode
17
+ tone?: ProgressTone
18
+ size?: ProgressSize
19
+ indeterminate?: boolean
20
+ trackClassName?: string
21
+ indicatorClassName?: string
22
+ }
23
+
24
+ export type ProgressCardProps = React.ComponentProps<typeof Card> & {
25
+ title?: React.ReactNode
26
+ description?: React.ReactNode
27
+ value?: number | null
28
+ max?: number
29
+ min?: number
30
+ tone?: ProgressTone
31
+ size?: ProgressSize
32
+ showValue?: boolean
33
+ valueFormatter?: ProgressProps["valueFormatter"]
34
+ footer?: React.ReactNode
35
+ progressClassName?: string
36
+ }
37
+
38
+ const toneClassName: Record<ProgressTone, string> = {
39
+ default: "bg-primary",
40
+ success: "bg-emerald-500",
41
+ info: "bg-blue-500",
42
+ warning: "bg-amber-500",
43
+ danger: "bg-destructive",
44
+ muted: "bg-muted-foreground",
45
+ }
46
+
47
+ const sizeClassName: Record<ProgressSize, string> = {
48
+ sm: "h-1.5",
49
+ default: "h-2",
50
+ lg: "h-3",
51
+ }
52
+
53
+ function clampValue(value: number, min: number, max: number) {
54
+ return Math.min(Math.max(value, min), max)
55
+ }
56
+
57
+ function getProgressPercent(value: number | null | undefined, min: number, max: number) {
58
+ if (value === null || value === undefined || !Number.isFinite(value)) return 0
59
+ if (max <= min) return 0
60
+
61
+ return ((clampValue(value, min, max) - min) / (max - min)) * 100
62
+ }
63
+
64
+ function Progress({
65
+ value = 0,
66
+ min = 0,
67
+ max = 100,
68
+ label,
69
+ description,
70
+ showValue = false,
71
+ valueFormatter,
72
+ tone = "default",
73
+ size = "default",
74
+ indeterminate = false,
75
+ className,
76
+ trackClassName,
77
+ indicatorClassName,
78
+ ...props
79
+ }: ProgressProps) {
80
+ const percent = getProgressPercent(value, min, max)
81
+ const resolvedValue = value ?? min
82
+
83
+ return (
84
+ <div data-slot="progress" className={cn("grid gap-1.5", className)} {...props}>
85
+ {(label || description || showValue) && (
86
+ <div className="flex min-w-0 items-start justify-between gap-3 text-sm">
87
+ <div className="min-w-0 space-y-0.5">
88
+ {label && <div className="font-medium leading-none">{label}</div>}
89
+ {description && <div className="text-xs text-muted-foreground">{description}</div>}
90
+ </div>
91
+ {showValue && (
92
+ <div className="shrink-0 text-xs font-medium text-muted-foreground">
93
+ {valueFormatter ? valueFormatter(resolvedValue, percent) : `${Math.round(percent)}%`}
94
+ </div>
95
+ )}
96
+ </div>
97
+ )}
98
+ <div
99
+ data-slot="progress-track"
100
+ className={cn("w-full overflow-hidden rounded-full bg-muted", sizeClassName[size], trackClassName)}
101
+ role="progressbar"
102
+ aria-valuemin={min}
103
+ aria-valuemax={max}
104
+ aria-valuenow={indeterminate ? undefined : resolvedValue}
105
+ aria-label={typeof label === "string" ? label : undefined}
106
+ >
107
+ <div
108
+ data-slot="progress-indicator"
109
+ data-indeterminate={indeterminate || undefined}
110
+ className={cn(
111
+ "h-full rounded-full transition-all data-[indeterminate=true]:w-1/3 data-[indeterminate=true]:animate-pulse",
112
+ toneClassName[tone],
113
+ indicatorClassName
114
+ )}
115
+ style={indeterminate ? undefined : { width: `${percent}%` }}
116
+ />
117
+ </div>
118
+ </div>
119
+ )
120
+ }
121
+
122
+ function ProgressCard({
123
+ title,
124
+ description,
125
+ value,
126
+ min,
127
+ max,
128
+ tone,
129
+ size,
130
+ showValue = true,
131
+ valueFormatter,
132
+ footer,
133
+ progressClassName,
134
+ className,
135
+ ...props
136
+ }: ProgressCardProps) {
137
+ return (
138
+ <Card data-slot="progress-card" className={cn("min-w-0", className)} {...props}>
139
+ <CardHeader>
140
+ <div className="min-w-0 space-y-1">
141
+ {title && <CardTitle>{title}</CardTitle>}
142
+ {description && <CardDescription>{description}</CardDescription>}
143
+ </div>
144
+ </CardHeader>
145
+ <CardContent className="space-y-3">
146
+ <Progress
147
+ value={value}
148
+ min={min}
149
+ max={max}
150
+ tone={tone}
151
+ size={size}
152
+ showValue={showValue}
153
+ valueFormatter={valueFormatter}
154
+ className={progressClassName}
155
+ />
156
+ {footer}
157
+ </CardContent>
158
+ </Card>
159
+ )
160
+ }
161
+
162
+ export { Progress, ProgressCard, getProgressPercent }
@@ -0,0 +1,54 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export type PropertyGridItem = {
6
+ key: string
7
+ label: React.ReactNode
8
+ value: React.ReactNode
9
+ description?: React.ReactNode
10
+ span?: 1 | 2 | 3 | 4
11
+ }
12
+
13
+ export type PropertyGridProps = React.ComponentProps<"div"> & {
14
+ items: PropertyGridItem[]
15
+ columns?: 1 | 2 | 3 | 4
16
+ bordered?: boolean
17
+ }
18
+
19
+ function PropertyGrid({ items, columns = 3, bordered = true, className, ...props }: PropertyGridProps) {
20
+ return (
21
+ <div
22
+ data-slot="property-grid"
23
+ className={cn(
24
+ "grid gap-3",
25
+ columns === 1 && "grid-cols-1",
26
+ columns === 2 && "grid-cols-1 sm:grid-cols-2",
27
+ columns === 3 && "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
28
+ columns === 4 && "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
29
+ className
30
+ )}
31
+ {...props}
32
+ >
33
+ {items.map((item) => (
34
+ <div
35
+ key={item.key}
36
+ data-slot="property-grid-item"
37
+ className={cn(
38
+ "min-w-0 rounded-lg bg-card p-3",
39
+ bordered && "border",
40
+ item.span === 2 && "sm:col-span-2",
41
+ item.span === 3 && "lg:col-span-3",
42
+ item.span === 4 && "lg:col-span-4"
43
+ )}
44
+ >
45
+ <div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">{item.label}</div>
46
+ <div className="mt-1 break-words text-sm font-medium text-foreground">{item.value}</div>
47
+ {item.description && <div className="mt-1 text-xs text-muted-foreground">{item.description}</div>}
48
+ </div>
49
+ ))}
50
+ </div>
51
+ )
52
+ }
53
+
54
+ export { PropertyGrid }
@@ -0,0 +1,90 @@
1
+ import * as React from "react"
2
+ import { AlertCircleIcon, CheckCircle2Icon, InfoIcon, ShieldAlertIcon, TriangleAlertIcon } from "lucide-react"
3
+
4
+ import { Button } from "@/components/ui/button"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ export type ResultStatus = "success" | "error" | "warning" | "info" | "not-found" | "forbidden" | "server-error"
8
+
9
+ export type ResultProps = React.ComponentProps<"div"> & {
10
+ status?: ResultStatus
11
+ icon?: React.ReactNode
12
+ title?: React.ReactNode
13
+ description?: React.ReactNode
14
+ actions?: React.ReactNode
15
+ extra?: React.ReactNode
16
+ compact?: boolean
17
+ iconClassName?: string
18
+ titleClassName?: string
19
+ descriptionClassName?: string
20
+ }
21
+
22
+ const statusIcon: Record<ResultStatus, React.ReactNode> = {
23
+ success: <CheckCircle2Icon className="size-8" />,
24
+ error: <AlertCircleIcon className="size-8" />,
25
+ warning: <TriangleAlertIcon className="size-8" />,
26
+ info: <InfoIcon className="size-8" />,
27
+ "not-found": <InfoIcon className="size-8" />,
28
+ forbidden: <ShieldAlertIcon className="size-8" />,
29
+ "server-error": <AlertCircleIcon className="size-8" />,
30
+ }
31
+
32
+ const statusClassName: Record<ResultStatus, string> = {
33
+ success: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
34
+ error: "bg-destructive/10 text-destructive",
35
+ warning: "bg-amber-500/10 text-amber-600 dark:text-amber-400",
36
+ info: "bg-blue-500/10 text-blue-600 dark:text-blue-400",
37
+ "not-found": "bg-muted text-muted-foreground",
38
+ forbidden: "bg-amber-500/10 text-amber-600 dark:text-amber-400",
39
+ "server-error": "bg-destructive/10 text-destructive",
40
+ }
41
+
42
+ const defaultTitle: Record<ResultStatus, React.ReactNode> = {
43
+ success: "Success",
44
+ error: "Something went wrong",
45
+ warning: "Check this action",
46
+ info: "Information",
47
+ "not-found": "Page not found",
48
+ forbidden: "Access forbidden",
49
+ "server-error": "Server error",
50
+ }
51
+
52
+ function Result({
53
+ status = "info",
54
+ icon,
55
+ title,
56
+ description,
57
+ actions,
58
+ extra,
59
+ compact = false,
60
+ iconClassName,
61
+ titleClassName,
62
+ descriptionClassName,
63
+ className,
64
+ ...props
65
+ }: ResultProps) {
66
+ return (
67
+ <div
68
+ data-slot="result"
69
+ data-status={status}
70
+ className={cn("flex flex-col items-center justify-center text-center", compact ? "gap-3 p-4" : "gap-4 p-8", className)}
71
+ {...props}
72
+ >
73
+ <div className={cn("flex items-center justify-center rounded-full", compact ? "size-12" : "size-16", statusClassName[status], iconClassName)}>
74
+ {icon ?? statusIcon[status]}
75
+ </div>
76
+ <div className="max-w-lg space-y-2">
77
+ <h3 className={cn("font-semibold tracking-tight", compact ? "text-base" : "text-2xl", titleClassName)}>{title ?? defaultTitle[status]}</h3>
78
+ {description && <p className={cn("text-sm leading-6 text-muted-foreground", descriptionClassName)}>{description}</p>}
79
+ </div>
80
+ {actions && <div className="flex flex-wrap justify-center gap-2">{actions}</div>}
81
+ {extra && <div className="w-full max-w-2xl">{extra}</div>}
82
+ </div>
83
+ )
84
+ }
85
+
86
+ function ResultAction({ className, ...props }: React.ComponentProps<typeof Button>) {
87
+ return <Button className={cn("min-w-28", className)} {...props} />
88
+ }
89
+
90
+ export { Result, ResultAction }