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,116 @@
1
+ import * as React from "react"
2
+ import { MoreHorizontalIcon } from "lucide-react"
3
+
4
+ import { Button } from "@/components/ui/button"
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuLabel,
10
+ DropdownMenuTrigger,
11
+ } from "@/components/ui/dropdown-menu"
12
+ import { cn } from "@/lib/utils"
13
+
14
+ type MaybeFn<TItem, TResult> = TResult | ((item: TItem) => TResult)
15
+
16
+ function resolveMaybeFn<TItem, TResult>(value: MaybeFn<TItem, TResult> | undefined, item: TItem): TResult | undefined {
17
+ return typeof value === "function" ? (value as (item: TItem) => TResult)(item) : value
18
+ }
19
+
20
+ export type ActionSystemAction<TItem = unknown> = {
21
+ key: string
22
+ label: React.ReactNode
23
+ icon?: React.ReactNode
24
+ description?: React.ReactNode
25
+ variant?: React.ComponentProps<typeof Button>["variant"]
26
+ disabled?: MaybeFn<TItem, boolean>
27
+ hidden?: MaybeFn<TItem, boolean>
28
+ confirm?: MaybeFn<TItem, string | { title?: string; description?: string } | undefined>
29
+ onClick?: (item: TItem, action: ActionSystemAction<TItem>) => void
30
+ }
31
+
32
+ export type ActionSystemLabels = {
33
+ more?: React.ReactNode
34
+ menu?: React.ReactNode
35
+ confirmFallback?: string
36
+ }
37
+
38
+ export type ActionSystemProps<TItem = unknown> = React.ComponentProps<"div"> & {
39
+ item: TItem
40
+ actions: ActionSystemAction<TItem>[]
41
+ mode?: "inline" | "menu" | "hybrid"
42
+ maxInline?: number
43
+ size?: React.ComponentProps<typeof Button>["size"]
44
+ labels?: ActionSystemLabels
45
+ onAction?: (item: TItem, action: ActionSystemAction<TItem>) => void
46
+ renderAction?: (action: ActionSystemAction<TItem>, item: TItem, state: { disabled: boolean }) => React.ReactNode
47
+ renderMenuItem?: (action: ActionSystemAction<TItem>, item: TItem, state: { disabled: boolean }) => React.ReactNode
48
+ }
49
+
50
+ function ActionSystem<TItem = unknown>({
51
+ item,
52
+ actions,
53
+ mode = "hybrid",
54
+ maxInline = 2,
55
+ size = "sm",
56
+ labels,
57
+ onAction,
58
+ renderAction,
59
+ renderMenuItem,
60
+ className,
61
+ ...props
62
+ }: ActionSystemProps<TItem>) {
63
+ const visibleActions = actions.filter((action) => !resolveMaybeFn(action.hidden, item))
64
+ const inlineActions = mode === "menu" ? [] : visibleActions.slice(0, mode === "inline" ? visibleActions.length : maxInline)
65
+ const menuActions = mode === "inline" ? [] : visibleActions.slice(mode === "menu" ? 0 : maxInline)
66
+
67
+ const runAction = (action: ActionSystemAction<TItem>) => {
68
+ const confirm = resolveMaybeFn(action.confirm, item)
69
+ if (confirm) {
70
+ const message = typeof confirm === "string" ? confirm : confirm.description ?? confirm.title ?? labels?.confirmFallback ?? "Are you sure?"
71
+ if (typeof window !== "undefined" && !window.confirm(message)) return
72
+ }
73
+
74
+ action.onClick?.(item, action)
75
+ onAction?.(item, action)
76
+ }
77
+
78
+ return (
79
+ <div data-slot="action-system" className={cn("inline-flex items-center gap-2", className)} {...props}>
80
+ {inlineActions.map((action) => {
81
+ const disabled = Boolean(resolveMaybeFn(action.disabled, item))
82
+ return renderAction?.(action, item, { disabled }) ?? (
83
+ <Button key={action.key} type="button" size={size} variant={action.variant ?? "outline"} disabled={disabled} onClick={() => runAction(action)}>
84
+ {action.icon}
85
+ {action.label}
86
+ </Button>
87
+ )
88
+ })}
89
+ {menuActions.length > 0 && (
90
+ <DropdownMenu>
91
+ <DropdownMenuTrigger render={<Button type="button" size={size} variant="outline" />}>
92
+ <MoreHorizontalIcon />
93
+ <span className="sr-only">{labels?.more ?? "More actions"}</span>
94
+ </DropdownMenuTrigger>
95
+ <DropdownMenuContent align="end" className="min-w-48">
96
+ {labels?.menu && <DropdownMenuLabel>{labels.menu}</DropdownMenuLabel>}
97
+ {menuActions.map((action) => {
98
+ const disabled = Boolean(resolveMaybeFn(action.disabled, item))
99
+ return renderMenuItem?.(action, item, { disabled }) ?? (
100
+ <DropdownMenuItem key={action.key} disabled={disabled} variant={action.variant === "destructive" ? "destructive" : "default"} onClick={() => runAction(action)}>
101
+ {action.icon}
102
+ <span className="grid gap-0.5">
103
+ <span>{action.label}</span>
104
+ {action.description && <span className="text-xs text-muted-foreground">{action.description}</span>}
105
+ </span>
106
+ </DropdownMenuItem>
107
+ )
108
+ })}
109
+ </DropdownMenuContent>
110
+ </DropdownMenu>
111
+ )}
112
+ </div>
113
+ )
114
+ }
115
+
116
+ export { ActionSystem }
@@ -0,0 +1,53 @@
1
+ import * as React from "react"
2
+
3
+ import { ActionSystem, type ActionSystemAction } from "@/components/patterns/action-system"
4
+ import { ResourceSystem, type ResourceSystemProps } from "@/components/patterns/resource-system"
5
+ import { Button } from "@/components/ui/button"
6
+ import { cn } from "@/lib/utils"
7
+
8
+ export type CrudSystemLabels = {
9
+ create?: React.ReactNode
10
+ edit?: React.ReactNode
11
+ delete?: React.ReactNode
12
+ view?: React.ReactNode
13
+ }
14
+
15
+ export type CrudSystemProps<TItem = unknown> = Omit<ResourceSystemProps<TItem>, "actions"> & {
16
+ labels?: CrudSystemLabels
17
+ canCreate?: boolean
18
+ createAction?: React.ReactNode
19
+ extraActions?: React.ReactNode
20
+ itemActions?: ActionSystemAction<TItem>[]
21
+ onCreate?: () => void
22
+ onView?: (item: TItem) => void
23
+ onEdit?: (item: TItem) => void
24
+ onDelete?: (item: TItem) => void
25
+ renderActions?: (args: { createAction?: React.ReactNode; extraActions?: React.ReactNode }) => React.ReactNode
26
+ actionsClassName?: string
27
+ }
28
+
29
+ function CrudSystem<TItem = unknown>({ labels, canCreate = true, createAction, extraActions, itemActions, onCreate, onView, onEdit, onDelete, renderActions, actionsClassName, list, ...props }: CrudSystemProps<TItem>) {
30
+ const resolvedItemActions = itemActions ?? [
31
+ onView && { key: "view", label: labels?.view ?? "View", onClick: onView },
32
+ onEdit && { key: "edit", label: labels?.edit ?? "Edit", onClick: onEdit },
33
+ onDelete && { key: "delete", label: labels?.delete ?? "Delete", variant: "destructive" as const, confirm: "Are you sure?", onClick: onDelete },
34
+ ].filter(Boolean) as ActionSystemAction<TItem>[]
35
+
36
+ const actions = renderActions?.({ createAction, extraActions }) ?? (
37
+ <div className={cn("flex items-center gap-2", actionsClassName)}>
38
+ {extraActions}
39
+ {canCreate && (createAction ?? <Button type="button" onClick={onCreate}>{labels?.create ?? "Create"}</Button>)}
40
+ </div>
41
+ )
42
+
43
+ const renderItem = list.renderItem ?? (resolvedItemActions.length ? (item: TItem) => (
44
+ <div className="flex items-center justify-between gap-3 rounded-lg border bg-card p-3">
45
+ <pre className="min-w-0 flex-1 overflow-hidden text-xs">{JSON.stringify(item, null, 2)}</pre>
46
+ <ActionSystem item={item} actions={resolvedItemActions} />
47
+ </div>
48
+ ) : undefined)
49
+
50
+ return <ResourceSystem {...props} actions={actions} list={{ ...list, renderItem }} />
51
+ }
52
+
53
+ export { CrudSystem }
@@ -0,0 +1,84 @@
1
+ import * as React from "react"
2
+
3
+ import { InlineState } from "@/components/feedback/page-state"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ export type DataViewState = "idle" | "loading" | "error" | "empty"
7
+
8
+ export type DataViewLabels = {
9
+ loadingTitle?: React.ReactNode
10
+ loadingDescription?: React.ReactNode
11
+ errorTitle?: React.ReactNode
12
+ emptyTitle?: React.ReactNode
13
+ emptyDescription?: React.ReactNode
14
+ }
15
+
16
+ export type DataViewProps<TItem = unknown> = React.ComponentProps<"div"> & {
17
+ data: TItem[]
18
+ loading?: boolean
19
+ error?: unknown
20
+ view?: string
21
+ labels?: DataViewLabels
22
+ toolbar?: React.ReactNode
23
+ filters?: React.ReactNode
24
+ selectionBar?: React.ReactNode
25
+ pagination?: React.ReactNode
26
+ actions?: React.ReactNode
27
+ empty?: React.ReactNode
28
+ loadingState?: React.ReactNode
29
+ errorState?: React.ReactNode
30
+ getState?: (args: { data: TItem[]; loading: boolean; error: unknown }) => DataViewState
31
+ renderContent?: (data: TItem[], meta: { view?: string; state: DataViewState }) => React.ReactNode
32
+ renderItem?: (item: TItem, index: number) => React.ReactNode
33
+ contentClassName?: string
34
+ toolbarClassName?: string
35
+ filtersClassName?: string
36
+ }
37
+
38
+ function DataView<TItem = unknown>({
39
+ data,
40
+ loading = false,
41
+ error,
42
+ view,
43
+ labels,
44
+ toolbar,
45
+ filters,
46
+ selectionBar,
47
+ pagination,
48
+ actions,
49
+ empty,
50
+ loadingState,
51
+ errorState,
52
+ getState,
53
+ renderContent,
54
+ renderItem,
55
+ contentClassName,
56
+ toolbarClassName,
57
+ filtersClassName,
58
+ className,
59
+ ...props
60
+ }: DataViewProps<TItem>) {
61
+ const state = getState?.({ data, loading, error }) ?? (loading ? "loading" : error ? "error" : data.length === 0 ? "empty" : "idle")
62
+
63
+ return (
64
+ <div data-slot="data-view" data-state={state} className={cn("grid gap-4", className)} {...props}>
65
+ {(toolbar || actions) && (
66
+ <div data-slot="data-view-toolbar" className={cn("flex flex-wrap items-center justify-between gap-3", toolbarClassName)}>
67
+ <div className="min-w-0 flex-1">{toolbar}</div>
68
+ {actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
69
+ </div>
70
+ )}
71
+ {filters && <div data-slot="data-view-filters" className={filtersClassName}>{filters}</div>}
72
+ {selectionBar}
73
+ <div data-slot="data-view-content" className={contentClassName}>
74
+ {state === "loading" ? loadingState ?? <InlineState tone="loading" title={labels?.loadingTitle ?? "Loading"} description={labels?.loadingDescription} /> : null}
75
+ {state === "error" ? errorState ?? <InlineState tone="error" title={labels?.errorTitle ?? "Something went wrong"} /> : null}
76
+ {state === "empty" ? empty ?? <InlineState tone="empty" title={labels?.emptyTitle ?? "No data"} description={labels?.emptyDescription} /> : null}
77
+ {state === "idle" ? renderContent?.(data, { view, state }) ?? <div className="grid gap-3">{data.map((item, index) => renderItem?.(item, index) ?? <pre key={index} className="rounded-lg border bg-card p-3 text-xs">{JSON.stringify(item, null, 2)}</pre>)}</div> : null}
78
+ </div>
79
+ {pagination && <div data-slot="data-view-pagination">{pagination}</div>}
80
+ </div>
81
+ )
82
+ }
83
+
84
+ export { DataView }
@@ -0,0 +1,66 @@
1
+ import * as React from "react"
2
+
3
+ import { PropertyGrid, type PropertyGridItem } from "@/components/display/property-grid"
4
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ export type EntityDetailsTab = {
8
+ key: string
9
+ label: React.ReactNode
10
+ content: React.ReactNode
11
+ disabled?: boolean
12
+ }
13
+
14
+ export type EntityDetailsSection = {
15
+ key: string
16
+ title?: React.ReactNode
17
+ content: React.ReactNode
18
+ }
19
+
20
+ export type EntityDetailsProps = React.ComponentProps<"div"> & {
21
+ title: React.ReactNode
22
+ subtitle?: React.ReactNode
23
+ avatar?: React.ReactNode
24
+ status?: React.ReactNode
25
+ actions?: React.ReactNode
26
+ metadata?: PropertyGridItem[]
27
+ sections?: EntityDetailsSection[]
28
+ tabs?: EntityDetailsTab[]
29
+ tab?: string
30
+ defaultTab?: string
31
+ onTabChange?: (key: string) => void
32
+ renderHeader?: () => React.ReactNode
33
+ }
34
+
35
+ function EntityDetails({ title, subtitle, avatar, status, actions, metadata, sections, tabs, tab, defaultTab, onTabChange, renderHeader, className, children, ...props }: EntityDetailsProps) {
36
+ return (
37
+ <div data-slot="entity-details" className={cn("grid gap-6", className)} {...props}>
38
+ {renderHeader?.() ?? (
39
+ <div className="flex flex-wrap items-start justify-between gap-4">
40
+ <div className="flex min-w-0 items-start gap-3">
41
+ {avatar}
42
+ <div className="grid min-w-0 gap-1">
43
+ <div className="flex flex-wrap items-center gap-2">
44
+ <h1 className="truncate text-2xl font-semibold tracking-tight text-foreground">{title}</h1>
45
+ {status}
46
+ </div>
47
+ {subtitle && <div className="text-sm text-muted-foreground">{subtitle}</div>}
48
+ </div>
49
+ </div>
50
+ {actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
51
+ </div>
52
+ )}
53
+ {metadata?.length ? <PropertyGrid items={metadata} columns={3} /> : null}
54
+ {sections?.length ? <div className="grid gap-4">{sections.map((section) => <section key={section.key} className="grid gap-3 rounded-lg border bg-card p-4">{section.title && <h2 className="text-base font-semibold text-foreground">{section.title}</h2>}{section.content}</section>)}</div> : null}
55
+ {tabs?.length ? (
56
+ <Tabs value={tab} defaultValue={defaultTab ?? tabs[0]?.key} onValueChange={onTabChange}>
57
+ <TabsList>{tabs.map((item) => <TabsTrigger key={item.key} value={item.key} disabled={item.disabled}>{item.label}</TabsTrigger>)}</TabsList>
58
+ {tabs.map((item) => <TabsContent key={item.key} value={item.key}>{item.content}</TabsContent>)}
59
+ </Tabs>
60
+ ) : null}
61
+ {children}
62
+ </div>
63
+ )
64
+ }
65
+
66
+ export { EntityDetails }
@@ -0,0 +1,113 @@
1
+ import * as React from "react"
2
+ import { RotateCcwIcon, SearchIcon } from "lucide-react"
3
+
4
+ import { Button } from "@/components/ui/button"
5
+ import { Input } from "@/components/ui/input"
6
+ import { cn } from "@/lib/utils"
7
+
8
+ export type FilterValue = string | number | boolean | null | undefined | string[] | [string | undefined, string | undefined]
9
+
10
+ export type FilterOption = {
11
+ value: string
12
+ label: React.ReactNode
13
+ disabled?: boolean
14
+ }
15
+
16
+ export type FilterField = {
17
+ key: string
18
+ label?: React.ReactNode
19
+ type?: "text" | "search" | "select" | "date" | "date-range" | "custom"
20
+ placeholder?: string
21
+ options?: FilterOption[]
22
+ disabled?: boolean
23
+ hidden?: boolean
24
+ className?: string
25
+ }
26
+
27
+ export type FilterBuilderLabels = {
28
+ reset?: React.ReactNode
29
+ apply?: React.ReactNode
30
+ search?: string
31
+ from?: string
32
+ to?: string
33
+ }
34
+
35
+ export type FilterBuilderProps<TFilters extends Record<string, FilterValue> = Record<string, FilterValue>> = React.ComponentProps<"div"> & {
36
+ value: TFilters
37
+ fields: FilterField[]
38
+ labels?: FilterBuilderLabels
39
+ layout?: "inline" | "grid"
40
+ columns?: 1 | 2 | 3 | 4
41
+ showApply?: boolean
42
+ showReset?: boolean
43
+ onChange: (value: TFilters) => void
44
+ onApply?: (value: TFilters) => void
45
+ onReset?: () => void
46
+ renderField?: (field: FilterField, value: FilterValue, helpers: { setValue: (value: FilterValue) => void; filters: TFilters }) => React.ReactNode
47
+ }
48
+
49
+ function FilterBuilder<TFilters extends Record<string, FilterValue> = Record<string, FilterValue>>({
50
+ value,
51
+ fields,
52
+ labels,
53
+ layout = "inline",
54
+ columns = 3,
55
+ showApply = false,
56
+ showReset = true,
57
+ onChange,
58
+ onApply,
59
+ onReset,
60
+ renderField,
61
+ className,
62
+ ...props
63
+ }: FilterBuilderProps<TFilters>) {
64
+ const setFieldValue = (key: string, nextValue: FilterValue) => onChange({ ...value, [key]: nextValue } as TFilters)
65
+ const visibleFields = fields.filter((field) => !field.hidden)
66
+
67
+ return (
68
+ <div data-slot="filter-builder" className={cn("flex flex-wrap items-end gap-3", layout === "grid" && "grid w-full", columns === 2 && "sm:grid-cols-2", columns === 3 && "sm:grid-cols-2 lg:grid-cols-3", columns === 4 && "sm:grid-cols-2 lg:grid-cols-4", className)} {...props}>
69
+ {visibleFields.map((field) => (
70
+ <div key={field.key} data-slot="filter-builder-field" className={cn("grid min-w-48 gap-1.5", field.className)}>
71
+ {field.label && <label className="text-xs font-medium text-muted-foreground">{field.label}</label>}
72
+ {renderField?.(field, value[field.key], { setValue: (nextValue) => setFieldValue(field.key, nextValue), filters: value }) ?? <DefaultFilterField field={field} value={value[field.key]} labels={labels} onChange={(nextValue) => setFieldValue(field.key, nextValue)} />}
73
+ </div>
74
+ ))}
75
+ {(showReset || showApply) && (
76
+ <div data-slot="filter-builder-actions" className="flex items-center gap-2">
77
+ {showReset && <Button type="button" variant="outline" size="sm" onClick={onReset}><RotateCcwIcon data-icon="inline-start" />{labels?.reset ?? "Reset"}</Button>}
78
+ {showApply && <Button type="button" size="sm" onClick={() => onApply?.(value)}>{labels?.apply ?? "Apply"}</Button>}
79
+ </div>
80
+ )}
81
+ </div>
82
+ )
83
+ }
84
+
85
+ function DefaultFilterField({ field, value, labels, onChange }: { field: FilterField; value: FilterValue; labels?: FilterBuilderLabels; onChange: (value: FilterValue) => void }) {
86
+ if (field.type === "select") {
87
+ return (
88
+ <select disabled={field.disabled} value={String(value ?? "")} className="h-8 rounded-md border bg-background px-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring" onChange={(event) => onChange(event.currentTarget.value || undefined)}>
89
+ <option value="">{field.placeholder ?? "All"}</option>
90
+ {field.options?.map((option) => <option key={option.value} value={option.value} disabled={option.disabled}>{typeof option.label === "string" ? option.label : option.value}</option>)}
91
+ </select>
92
+ )
93
+ }
94
+
95
+ if (field.type === "date-range") {
96
+ const range = Array.isArray(value) ? value as [string | undefined, string | undefined] : [undefined, undefined]
97
+ return (
98
+ <div className="grid grid-cols-2 gap-2">
99
+ <Input type="date" disabled={field.disabled} value={range[0] ?? ""} aria-label={labels?.from ?? "From"} onChange={(event) => onChange([event.currentTarget.value || undefined, range[1]])} />
100
+ <Input type="date" disabled={field.disabled} value={range[1] ?? ""} aria-label={labels?.to ?? "To"} onChange={(event) => onChange([range[0], event.currentTarget.value || undefined])} />
101
+ </div>
102
+ )
103
+ }
104
+
105
+ return (
106
+ <div className="relative">
107
+ {field.type === "search" && <SearchIcon className="pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />}
108
+ <Input type={field.type === "date" ? "date" : "text"} disabled={field.disabled} value={String(value ?? "")} placeholder={field.placeholder ?? labels?.search} className={cn(field.type === "search" && "pl-8")} onChange={(event) => onChange(event.currentTarget.value || undefined)} />
109
+ </div>
110
+ )
111
+ }
112
+
113
+ export { FilterBuilder }
@@ -0,0 +1,131 @@
1
+ import type { FieldPath, FieldValues } from "react-hook-form"
2
+
3
+ import type {
4
+ FormBuilderAsyncSelectField,
5
+ FormBuilderCustomField,
6
+ FormBuilderDateField,
7
+ FormBuilderDateRangeField,
8
+ FormBuilderInputField,
9
+ FormBuilderNumberField,
10
+ FormBuilderPhoneField,
11
+ FormBuilderSection,
12
+ FormBuilderSelectField,
13
+ FormBuilderSwitchField,
14
+ FormBuilderTextareaField,
15
+ } from "./form-builder"
16
+
17
+ type FieldBase = {
18
+ id: string
19
+ hidden?: boolean
20
+ className?: string
21
+ colSpan?: 1 | 2 | 3 | 4 | "full"
22
+ }
23
+
24
+ type FieldPresetOptions<TProps> = FieldBase & {
25
+ props: TProps
26
+ }
27
+
28
+ function splitFieldOptions<TProps>(options: FieldPresetOptions<TProps>) {
29
+ const { id, hidden, className, colSpan, props } = options
30
+ return { base: { id, hidden, className, colSpan }, props }
31
+ }
32
+
33
+ function inputField<
34
+ TFieldValues extends FieldValues,
35
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
36
+ >(options: FieldPresetOptions<FormBuilderInputField<TFieldValues, TName>["props"]>): FormBuilderInputField<TFieldValues, TName> {
37
+ const { base, props } = splitFieldOptions(options)
38
+ return { ...base, type: "input", props }
39
+ }
40
+
41
+ function textareaField<
42
+ TFieldValues extends FieldValues,
43
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
44
+ >(options: FieldPresetOptions<FormBuilderTextareaField<TFieldValues, TName>["props"]>): FormBuilderTextareaField<TFieldValues, TName> {
45
+ const { base, props } = splitFieldOptions(options)
46
+ return { ...base, type: "textarea", props }
47
+ }
48
+
49
+ function selectField<
50
+ TFieldValues extends FieldValues,
51
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
52
+ >(options: FieldPresetOptions<FormBuilderSelectField<TFieldValues, TName>["props"]>): FormBuilderSelectField<TFieldValues, TName> {
53
+ const { base, props } = splitFieldOptions(options)
54
+ return { ...base, type: "select", props }
55
+ }
56
+
57
+ function asyncSelectField<
58
+ TFieldValues extends FieldValues,
59
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
60
+ >(options: FieldPresetOptions<FormBuilderAsyncSelectField<TFieldValues, TName>["props"]>): FormBuilderAsyncSelectField<TFieldValues, TName> {
61
+ const { base, props } = splitFieldOptions(options)
62
+ return { ...base, type: "async-select", props }
63
+ }
64
+
65
+ function switchField<
66
+ TFieldValues extends FieldValues,
67
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
68
+ >(options: FieldPresetOptions<FormBuilderSwitchField<TFieldValues, TName>["props"]>): FormBuilderSwitchField<TFieldValues, TName> {
69
+ const { base, props } = splitFieldOptions(options)
70
+ return { ...base, type: "switch", props }
71
+ }
72
+
73
+ function numberField<
74
+ TFieldValues extends FieldValues,
75
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
76
+ >(options: FieldPresetOptions<FormBuilderNumberField<TFieldValues, TName>["props"]>): FormBuilderNumberField<TFieldValues, TName> {
77
+ const { base, props } = splitFieldOptions(options)
78
+ return { ...base, type: "number", props }
79
+ }
80
+
81
+ function phoneField<
82
+ TFieldValues extends FieldValues,
83
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
84
+ >(options: FieldPresetOptions<FormBuilderPhoneField<TFieldValues, TName>["props"]>): FormBuilderPhoneField<TFieldValues, TName> {
85
+ const { base, props } = splitFieldOptions(options)
86
+ return { ...base, type: "phone", props }
87
+ }
88
+
89
+ function dateField<
90
+ TFieldValues extends FieldValues,
91
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
92
+ >(options: FieldPresetOptions<FormBuilderDateField<TFieldValues, TName>["props"]>): FormBuilderDateField<TFieldValues, TName> {
93
+ const { base, props } = splitFieldOptions(options)
94
+ return { ...base, type: "date", props }
95
+ }
96
+
97
+ function dateRangeField<
98
+ TFieldValues extends FieldValues,
99
+ TFromName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
100
+ TToName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
101
+ >(options: FieldPresetOptions<FormBuilderDateRangeField<TFieldValues, TFromName, TToName>["props"]>): FormBuilderDateRangeField<TFieldValues, TFromName, TToName> {
102
+ const { base, props } = splitFieldOptions(options)
103
+ return { ...base, type: "date-range", props }
104
+ }
105
+
106
+ function customField<TFieldValues extends FieldValues>(
107
+ options: FieldBase & Pick<FormBuilderCustomField<TFieldValues>, "render">
108
+ ): FormBuilderCustomField<TFieldValues> {
109
+ const { id, hidden, className, colSpan, render } = options
110
+ return { id, hidden, className, colSpan, type: "custom", render }
111
+ }
112
+
113
+ function formSection<TFieldValues extends FieldValues>(
114
+ section: FormBuilderSection<TFieldValues>
115
+ ): FormBuilderSection<TFieldValues> {
116
+ return section
117
+ }
118
+
119
+ export {
120
+ asyncSelectField,
121
+ customField,
122
+ dateField,
123
+ dateRangeField,
124
+ formSection,
125
+ inputField,
126
+ numberField,
127
+ phoneField,
128
+ selectField,
129
+ switchField,
130
+ textareaField,
131
+ }