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,375 @@
1
+ import * as React from "react"
2
+ import { Loader2Icon, SearchIcon } from "lucide-react"
3
+
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from "@/components/ui/dialog"
11
+ import { Input } from "@/components/ui/input"
12
+ import { cn } from "@/lib/utils"
13
+
14
+ export type CommandPaletteItem = {
15
+ id: string
16
+ label: React.ReactNode
17
+ value?: string
18
+ description?: React.ReactNode
19
+ icon?: React.ReactNode
20
+ shortcut?: React.ReactNode
21
+ disabled?: boolean
22
+ disabledReason?: React.ReactNode
23
+ hidden?: boolean
24
+ keywords?: string[]
25
+ closeOnSelect?: boolean
26
+ onSelect?: () => void | Promise<void>
27
+ }
28
+
29
+ export type CommandPaletteGroup = {
30
+ id: string
31
+ label?: React.ReactNode
32
+ items?: CommandPaletteItem[]
33
+ loadItems?: (search: string) => Promise<CommandPaletteItem[]>
34
+ loadingLabel?: React.ReactNode
35
+ emptyLabel?: React.ReactNode
36
+ hidden?: boolean
37
+ }
38
+
39
+ export type CommandPaletteRecentConfig = {
40
+ enabled?: boolean
41
+ label?: React.ReactNode
42
+ limit?: number
43
+ }
44
+
45
+ export type CommandPaletteProps = {
46
+ open?: boolean
47
+ onOpenChange?: (open: boolean) => void
48
+ title?: React.ReactNode
49
+ description?: React.ReactNode
50
+ placeholder?: string
51
+ emptyLabel?: React.ReactNode
52
+ loadingLabel?: React.ReactNode
53
+ groups: CommandPaletteGroup[]
54
+ value?: string
55
+ onValueChange?: (value: string) => void
56
+ debounceMs?: number
57
+ recent?: CommandPaletteRecentConfig | boolean
58
+ filterItem?: (item: CommandPaletteItem, search: string) => boolean
59
+ renderEmpty?: (search: string) => React.ReactNode
60
+ renderLoading?: (search: string) => React.ReactNode
61
+ onItemSelect?: (item: CommandPaletteItem, group: CommandPaletteGroup) => void
62
+ className?: string
63
+ contentClassName?: string
64
+ inputClassName?: string
65
+ listClassName?: string
66
+ }
67
+
68
+ type AsyncGroupState = {
69
+ items: CommandPaletteItem[]
70
+ loading: boolean
71
+ error?: unknown
72
+ }
73
+
74
+ type RuntimeCommandPaletteGroup = CommandPaletteGroup & AsyncGroupState & {
75
+ items: CommandPaletteItem[]
76
+ }
77
+
78
+ function itemText(item: CommandPaletteItem) {
79
+ return [
80
+ item.value,
81
+ typeof item.label === "string" || typeof item.label === "number" ? String(item.label) : item.id,
82
+ typeof item.description === "string" || typeof item.description === "number" ? String(item.description) : "",
83
+ ...(item.keywords ?? []),
84
+ ]
85
+ .filter(Boolean)
86
+ .join(" ")
87
+ .toLowerCase()
88
+ }
89
+
90
+ function defaultFilterItem(item: CommandPaletteItem, search: string) {
91
+ if (!search) return true
92
+ return itemText(item).includes(search.trim().toLowerCase())
93
+ }
94
+
95
+ function resolveRecentConfig(recent?: CommandPaletteRecentConfig | boolean): Required<CommandPaletteRecentConfig> {
96
+ if (recent === false) {
97
+ return { enabled: false, label: "Recent", limit: 5 }
98
+ }
99
+
100
+ if (recent === true || recent === undefined) {
101
+ return { enabled: true, label: "Recent", limit: 5 }
102
+ }
103
+
104
+ return {
105
+ enabled: recent.enabled ?? true,
106
+ label: recent.label ?? "Recent",
107
+ limit: recent.limit ?? 5,
108
+ }
109
+ }
110
+
111
+ function CommandPalette({
112
+ open,
113
+ onOpenChange,
114
+ title = "Command palette",
115
+ description = "Search commands and actions.",
116
+ placeholder = "Search...",
117
+ emptyLabel = "No results found.",
118
+ loadingLabel = "Loading commands...",
119
+ groups,
120
+ value,
121
+ onValueChange,
122
+ debounceMs = 180,
123
+ recent,
124
+ filterItem = defaultFilterItem,
125
+ renderEmpty,
126
+ renderLoading,
127
+ onItemSelect,
128
+ className,
129
+ contentClassName,
130
+ inputClassName,
131
+ listClassName,
132
+ }: CommandPaletteProps) {
133
+ const [internalValue, setInternalValue] = React.useState("")
134
+ const [debouncedSearch, setDebouncedSearch] = React.useState("")
135
+ const [asyncGroups, setAsyncGroups] = React.useState<Record<string, AsyncGroupState>>({})
136
+ const [recentItems, setRecentItems] = React.useState<CommandPaletteItem[]>([])
137
+ const [loadingKey, setLoadingKey] = React.useState<string | null>(null)
138
+ const search = value ?? internalValue
139
+ const normalizedSearch = search.trim().toLowerCase()
140
+ const recentConfig = resolveRecentConfig(recent)
141
+
142
+ React.useEffect(() => {
143
+ const timeout = window.setTimeout(() => setDebouncedSearch(search), debounceMs)
144
+ return () => window.clearTimeout(timeout)
145
+ }, [debounceMs, search])
146
+
147
+ React.useEffect(() => {
148
+ if (!open) return
149
+
150
+ let active = true
151
+ const asyncLoaders = groups.filter((group) => !group.hidden && group.loadItems)
152
+
153
+ asyncLoaders.forEach((group) => {
154
+ setAsyncGroups((current) => ({
155
+ ...current,
156
+ [group.id]: {
157
+ items: current[group.id]?.items ?? [],
158
+ loading: true,
159
+ error: undefined,
160
+ },
161
+ }))
162
+
163
+ void group.loadItems?.(debouncedSearch).then(
164
+ (items) => {
165
+ if (!active) return
166
+ setAsyncGroups((current) => ({ ...current, [group.id]: { items, loading: false } }))
167
+ },
168
+ (error) => {
169
+ if (!active) return
170
+ setAsyncGroups((current) => ({ ...current, [group.id]: { items: [], loading: false, error } }))
171
+ },
172
+ )
173
+ })
174
+
175
+ return () => {
176
+ active = false
177
+ }
178
+ }, [debouncedSearch, groups, open])
179
+
180
+ const setSearch = (nextValue: string) => {
181
+ setInternalValue(nextValue)
182
+ onValueChange?.(nextValue)
183
+ }
184
+
185
+ const allItemsById = React.useMemo(() => {
186
+ const map = new Map<string, CommandPaletteItem>()
187
+
188
+ groups.forEach((group) => {
189
+ group.items?.forEach((item) => map.set(item.id, item))
190
+ asyncGroups[group.id]?.items.forEach((item) => map.set(item.id, item))
191
+ })
192
+
193
+ return map
194
+ }, [asyncGroups, groups])
195
+
196
+ const visibleGroups = React.useMemo<RuntimeCommandPaletteGroup[]>(
197
+ () =>
198
+ groups
199
+ .filter((group) => !group.hidden)
200
+ .map((group) => {
201
+ const asyncState = asyncGroups[group.id]
202
+ const mergedItems = [...(group.items ?? []), ...(asyncState?.items ?? [])]
203
+ const items = mergedItems.filter((item) => {
204
+ if (item.hidden) return false
205
+ return filterItem(item, normalizedSearch)
206
+ })
207
+
208
+ return {
209
+ ...group,
210
+ items,
211
+ loading: asyncState?.loading ?? false,
212
+ error: asyncState?.error,
213
+ } as RuntimeCommandPaletteGroup
214
+ })
215
+ .filter((group) => group.items.length > 0 || group.loading || group.error),
216
+ [asyncGroups, filterItem, groups, normalizedSearch],
217
+ )
218
+
219
+ const recentGroup = React.useMemo(() => {
220
+ if (!recentConfig.enabled || normalizedSearch || recentItems.length === 0) return null
221
+
222
+ return {
223
+ id: "__recent",
224
+ label: recentConfig.label,
225
+ items: recentItems.slice(0, recentConfig.limit),
226
+ loading: false,
227
+ error: undefined,
228
+ } as RuntimeCommandPaletteGroup
229
+ }, [normalizedSearch, recentConfig.enabled, recentConfig.label, recentConfig.limit, recentItems])
230
+
231
+ const renderedGroups = recentGroup ? [recentGroup, ...visibleGroups] : visibleGroups
232
+ const hasResults = renderedGroups.some((group) => group.items.length > 0)
233
+ const hasLoading = renderedGroups.some((group) => group.loading)
234
+
235
+ const pushRecentItem = (item: CommandPaletteItem) => {
236
+ if (!recentConfig.enabled) return
237
+
238
+ const source = allItemsById.get(item.id) ?? item
239
+ setRecentItems((current) => [source, ...current.filter((entry) => entry.id !== item.id)].slice(0, recentConfig.limit))
240
+ }
241
+
242
+ const handleSelect = async (item: CommandPaletteItem, group: RuntimeCommandPaletteGroup) => {
243
+ if (item.disabled || loadingKey) return
244
+
245
+ try {
246
+ setLoadingKey(item.id)
247
+ await item.onSelect?.()
248
+ onItemSelect?.(item, group)
249
+ pushRecentItem(item)
250
+ if (item.closeOnSelect !== false) {
251
+ onOpenChange?.(false)
252
+ }
253
+ } finally {
254
+ setLoadingKey(null)
255
+ }
256
+ }
257
+
258
+ return (
259
+ <Dialog open={open} onOpenChange={onOpenChange}>
260
+ <DialogContent className={cn("overflow-hidden p-0 sm:max-w-xl", contentClassName)}>
261
+ <DialogHeader className="sr-only">
262
+ <DialogTitle>{title}</DialogTitle>
263
+ <DialogDescription>{description}</DialogDescription>
264
+ </DialogHeader>
265
+
266
+ <div
267
+ data-slot="command-palette"
268
+ className={cn("flex max-h-[32rem] flex-col overflow-hidden rounded-lg bg-popover text-popover-foreground", className)}
269
+ >
270
+ <div className="relative border-b p-2">
271
+ <SearchIcon className="pointer-events-none absolute left-4 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
272
+ <Input
273
+ value={search}
274
+ onChange={(event) => setSearch(event.target.value)}
275
+ placeholder={placeholder}
276
+ className={cn("border-0 pl-8 shadow-none focus-visible:ring-0", inputClassName)}
277
+ autoFocus
278
+ />
279
+ </div>
280
+
281
+ <div className={cn("overflow-y-auto p-2", listClassName)}>
282
+ {!hasResults && hasLoading && (
283
+ <div className="flex items-center justify-center gap-2 py-8 text-sm text-muted-foreground">
284
+ {renderLoading?.(search) ?? (
285
+ <>
286
+ <Loader2Icon className="size-4 animate-spin" />
287
+ {loadingLabel}
288
+ </>
289
+ )}
290
+ </div>
291
+ )}
292
+
293
+ {!hasResults && !hasLoading && (
294
+ <div className="py-8 text-center text-sm text-muted-foreground">
295
+ {renderEmpty?.(search) ?? emptyLabel}
296
+ </div>
297
+ )}
298
+
299
+ {renderedGroups.map((group) => (
300
+ <div key={group.id} className="py-1">
301
+ {group.label && (
302
+ <div className="flex items-center justify-between px-2 py-1.5 text-xs font-medium text-muted-foreground">
303
+ <span>{group.label}</span>
304
+ {group.loading && <Loader2Icon className="size-3.5 animate-spin" />}
305
+ </div>
306
+ )}
307
+
308
+ {group.error ? (
309
+ <div className="rounded-md px-2 py-2 text-xs text-destructive">
310
+ Could not load commands.
311
+ </div>
312
+ ) : null}
313
+
314
+ {group.loading && group.items.length === 0 && (
315
+ <div className="rounded-md px-2 py-2 text-xs text-muted-foreground">
316
+ {group.loadingLabel ?? loadingLabel}
317
+ </div>
318
+ )}
319
+
320
+ {group.items.length === 0 && !group.loading && !group.error && group.emptyLabel && (
321
+ <div className="rounded-md px-2 py-2 text-xs text-muted-foreground">{group.emptyLabel}</div>
322
+ )}
323
+
324
+ {group.items.map((item) => {
325
+ const isLoading = loadingKey === item.id
326
+
327
+ return (
328
+ <button
329
+ key={item.id}
330
+ type="button"
331
+ disabled={item.disabled || isLoading}
332
+ data-disabled={item.disabled || undefined}
333
+ className="flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm outline-none hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
334
+ onClick={() => void handleSelect(item, group)}
335
+ >
336
+ {item.icon && <span className="shrink-0 text-muted-foreground [&_svg]:size-4">{item.icon}</span>}
337
+ <span className="min-w-0 flex-1">
338
+ <span className="block truncate">{item.label}</span>
339
+ {(item.description || item.disabledReason) && (
340
+ <span className="block truncate text-xs text-muted-foreground">
341
+ {item.disabled ? item.disabledReason ?? item.description : item.description}
342
+ </span>
343
+ )}
344
+ </span>
345
+ {isLoading && <Loader2Icon className="size-3.5 animate-spin text-muted-foreground" />}
346
+ {item.shortcut && <span className="text-xs text-muted-foreground">{item.shortcut}</span>}
347
+ </button>
348
+ )
349
+ })}
350
+ </div>
351
+ ))}
352
+ </div>
353
+ </div>
354
+ </DialogContent>
355
+ </Dialog>
356
+ )
357
+ }
358
+
359
+ function useCommandPaletteShortcut(onOpenChange: (open: boolean) => void, enabled = true) {
360
+ React.useEffect(() => {
361
+ if (!enabled) return
362
+
363
+ const handleKeyDown = (event: KeyboardEvent) => {
364
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
365
+ event.preventDefault()
366
+ onOpenChange(true)
367
+ }
368
+ }
369
+
370
+ window.addEventListener("keydown", handleKeyDown)
371
+ return () => window.removeEventListener("keydown", handleKeyDown)
372
+ }, [enabled, onOpenChange])
373
+ }
374
+
375
+ export { CommandPalette, useCommandPaletteShortcut }
@@ -0,0 +1 @@
1
+ export * from "./command-palette"
@@ -0,0 +1,58 @@
1
+ import * as React from "react"
2
+ import type { ColumnDef, Row } from "@tanstack/react-table"
3
+
4
+ import {
5
+ DataTableRowActions,
6
+ type DataTableRowAction,
7
+ } from "@/components/data-table/data-table-row-actions"
8
+ import { cn } from "@/lib/utils"
9
+
10
+ export type DataTableActionsColumnOptions<TData> = {
11
+ id?: string
12
+ size?: number
13
+ header?: React.ReactNode
14
+ headerClassName?: string
15
+ cellClassName?: string
16
+ actions?: DataTableRowAction<TData>[]
17
+ getActions?: (row: Row<TData>, original: TData) => DataTableRowAction<TData>[]
18
+ label?: React.ReactNode
19
+ emptyLabel?: React.ReactNode
20
+ }
21
+
22
+ function createDataTableActionsColumn<TData>({
23
+ id = "actions",
24
+ size = 56,
25
+ header,
26
+ headerClassName,
27
+ cellClassName,
28
+ actions,
29
+ getActions,
30
+ label,
31
+ emptyLabel,
32
+ }: DataTableActionsColumnOptions<TData> = {}): ColumnDef<TData> {
33
+ return {
34
+ id,
35
+ size,
36
+ enableSorting: false,
37
+ enableHiding: false,
38
+ header: () =>
39
+ header ? (
40
+ <div className={cn("flex items-center justify-end", headerClassName)}>
41
+ {header}
42
+ </div>
43
+ ) : null,
44
+ cell: ({ row }) => (
45
+ <div className={cn("flex items-center justify-end", cellClassName)}>
46
+ <DataTableRowActions
47
+ row={row}
48
+ actions={actions}
49
+ getActions={getActions}
50
+ label={label}
51
+ emptyLabel={emptyLabel}
52
+ />
53
+ </div>
54
+ ),
55
+ }
56
+ }
57
+
58
+ export { createDataTableActionsColumn }
@@ -0,0 +1,84 @@
1
+ import * as React from "react"
2
+ import { ChevronDownIcon } from "lucide-react"
3
+
4
+ import {
5
+ ActionMenu,
6
+ type ActionMenuItem,
7
+ type ActionMenuProps,
8
+ } from "@/components/actions/action-menu"
9
+ import { Button } from "@/components/ui/button"
10
+ import { cn } from "@/lib/utils"
11
+
12
+ export type DataTableBulkAction<TData> = Omit<ActionMenuItem, "onSelect"> & {
13
+ onSelect?: (rows: TData[]) => void | Promise<void>
14
+ }
15
+
16
+ export type DataTableBulkActionsProps<TData> = Omit<
17
+ ActionMenuProps,
18
+ "actions" | "trigger"
19
+ > & {
20
+ rows: TData[]
21
+ actions: DataTableBulkAction<TData>[]
22
+ label?: React.ReactNode
23
+ selectedLabel?: (count: number) => React.ReactNode
24
+ clearLabel?: React.ReactNode
25
+ onClearSelection?: () => void
26
+ hideWhenEmpty?: boolean
27
+ triggerClassName?: string
28
+ }
29
+
30
+ function DataTableBulkActions<TData>({
31
+ rows,
32
+ actions,
33
+ label = "Bulk actions",
34
+ selectedLabel = (count) => `${count} selected`,
35
+ clearLabel = "Clear",
36
+ onClearSelection,
37
+ hideWhenEmpty = true,
38
+ triggerClassName,
39
+ disabled,
40
+ ...props
41
+ }: DataTableBulkActionsProps<TData>) {
42
+ const count = rows.length
43
+
44
+ if (hideWhenEmpty && count === 0) {
45
+ return null
46
+ }
47
+
48
+ const resolvedActions: ActionMenuItem[] = actions.map((action) => ({
49
+ ...action,
50
+ disabled: action.disabled || count === 0,
51
+ onSelect: action.onSelect ? () => action.onSelect?.(rows) : undefined,
52
+ }))
53
+
54
+ return (
55
+ <div data-slot="data-table-bulk-actions" className="flex items-center gap-2">
56
+ <ActionMenu
57
+ label={label}
58
+ actions={resolvedActions}
59
+ disabled={disabled || count === 0}
60
+ trigger={
61
+ <Button
62
+ type="button"
63
+ variant="outline"
64
+ size="sm"
65
+ disabled={disabled || count === 0}
66
+ className={cn("gap-1.5", triggerClassName)}
67
+ >
68
+ {selectedLabel(count)}
69
+ <ChevronDownIcon className="size-3.5" />
70
+ </Button>
71
+ }
72
+ {...props}
73
+ />
74
+
75
+ {onClearSelection && count > 0 && (
76
+ <Button type="button" variant="ghost" size="sm" onClick={onClearSelection}>
77
+ {clearLabel}
78
+ </Button>
79
+ )}
80
+ </div>
81
+ )
82
+ }
83
+
84
+ export { DataTableBulkActions }
@@ -0,0 +1,79 @@
1
+ import * as React from "react"
2
+ import { Columns3Icon } from "lucide-react"
3
+ import type { Table as TanStackTable } from "@tanstack/react-table"
4
+
5
+ import { Button } from "@/components/ui/button"
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuCheckboxItem,
9
+ DropdownMenuContent,
10
+ DropdownMenuLabel,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuTrigger,
13
+ } from "@/components/ui/dropdown-menu"
14
+ import { cn } from "@/lib/utils"
15
+
16
+ export type DataTableColumnVisibilityMenuProps<TData> = {
17
+ table: TanStackTable<TData>
18
+ label?: React.ReactNode
19
+ triggerLabel?: React.ReactNode
20
+ align?: "start" | "center" | "end"
21
+ side?: "top" | "right" | "bottom" | "left"
22
+ triggerClassName?: string
23
+ contentClassName?: string
24
+ getColumnLabel?: (columnId: string) => React.ReactNode
25
+ }
26
+
27
+ function defaultColumnLabel(columnId: string) {
28
+ return columnId
29
+ .replace(/_/g, " ")
30
+ .replace(/-/g, " ")
31
+ .replace(/\b\w/g, (char) => char.toUpperCase())
32
+ }
33
+
34
+ function DataTableColumnVisibilityMenu<TData>({
35
+ table,
36
+ label = "Columns",
37
+ triggerLabel,
38
+ align = "end",
39
+ side = "bottom",
40
+ triggerClassName,
41
+ contentClassName,
42
+ getColumnLabel = defaultColumnLabel,
43
+ }: DataTableColumnVisibilityMenuProps<TData>) {
44
+ const columns = table
45
+ .getAllLeafColumns()
46
+ .filter((column) => column.getCanHide())
47
+
48
+ return (
49
+ <DropdownMenu>
50
+ <DropdownMenuTrigger
51
+ render={
52
+ <Button type="button" variant="outline" size="sm" className={triggerClassName} />
53
+ }
54
+ >
55
+ <Columns3Icon data-icon="inline-start" />
56
+ {triggerLabel ?? label}
57
+ </DropdownMenuTrigger>
58
+ <DropdownMenuContent
59
+ align={align}
60
+ side={side}
61
+ className={cn("min-w-48", contentClassName)}
62
+ >
63
+ <DropdownMenuLabel>{label}</DropdownMenuLabel>
64
+ <DropdownMenuSeparator />
65
+ {columns.map((column) => (
66
+ <DropdownMenuCheckboxItem
67
+ key={column.id}
68
+ checked={column.getIsVisible()}
69
+ onCheckedChange={(value) => column.toggleVisibility(Boolean(value))}
70
+ >
71
+ {getColumnLabel(column.id)}
72
+ </DropdownMenuCheckboxItem>
73
+ ))}
74
+ </DropdownMenuContent>
75
+ </DropdownMenu>
76
+ )
77
+ }
78
+
79
+ export { DataTableColumnVisibilityMenu }
@@ -0,0 +1,91 @@
1
+ import * as React from "react"
2
+
3
+ import { SimpleSelect } from "@/components/inputs/simple-select"
4
+ import { Pagination, type PaginationLabels } from "@/components/navigation/pagination"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ export type DataTablePaginationLabels = PaginationLabels & {
8
+ rowsPerPage?: React.ReactNode
9
+ pageInfo?: (page: number, pageCount: number, rowCount?: number) => React.ReactNode
10
+ }
11
+
12
+ export type DataTablePaginationProps = React.ComponentProps<"div"> & {
13
+ pageIndex: number
14
+ pageSize: number
15
+ pageCount?: number
16
+ rowCount?: number
17
+ pageSizeOptions?: number[]
18
+ disabled?: boolean
19
+ showPageSize?: boolean
20
+ labels?: DataTablePaginationLabels
21
+ onPageChange?: (pageIndex: number) => void
22
+ onPageSizeChange?: (pageSize: number) => void
23
+ }
24
+
25
+ function getDataTablePageCount(pageSize: number, pageCount?: number, rowCount?: number) {
26
+ if (typeof pageCount === "number") return Math.max(pageCount, 1)
27
+ if (typeof rowCount === "number") return Math.max(Math.ceil(rowCount / pageSize), 1)
28
+ return 1
29
+ }
30
+
31
+ function DataTablePagination({
32
+ className,
33
+ pageIndex,
34
+ pageSize,
35
+ pageCount,
36
+ rowCount,
37
+ pageSizeOptions = [10, 20, 30, 50, 100],
38
+ disabled = false,
39
+ showPageSize = true,
40
+ labels,
41
+ onPageChange,
42
+ onPageSizeChange,
43
+ ...props
44
+ }: DataTablePaginationProps) {
45
+ const resolvedPageCount = getDataTablePageCount(pageSize, pageCount, rowCount)
46
+ const currentPage = Math.min(Math.max(pageIndex + 1, 1), resolvedPageCount)
47
+
48
+ return (
49
+ <div
50
+ data-slot="data-table-pagination"
51
+ className={cn(
52
+ "flex flex-col gap-3 border-t border-border/80 px-4 py-3 sm:flex-row sm:items-center sm:justify-between",
53
+ className
54
+ )}
55
+ {...props}
56
+ >
57
+ <div className="text-sm font-medium text-muted-foreground">
58
+ {labels?.pageInfo?.(currentPage, resolvedPageCount, rowCount) ??
59
+ `Page ${currentPage} of ${resolvedPageCount}`}
60
+ </div>
61
+
62
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
63
+ {showPageSize && onPageSizeChange && (
64
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
65
+ <span>{labels?.rowsPerPage ?? "Rows per page"}</span>
66
+ <SimpleSelect
67
+ value={String(pageSize)}
68
+ onValueChange={(value) => onPageSizeChange(Number(value))}
69
+ options={pageSizeOptions.map((option) => ({
70
+ label: String(option),
71
+ value: String(option),
72
+ }))}
73
+ disabled={disabled}
74
+ triggerClassName="h-9 w-20 rounded-full border-border/80 bg-background/90"
75
+ />
76
+ </div>
77
+ )}
78
+
79
+ <Pagination
80
+ page={currentPage}
81
+ pageCount={resolvedPageCount}
82
+ disabled={disabled}
83
+ labels={labels}
84
+ onPageChange={(nextPage) => onPageChange?.(nextPage - 1)}
85
+ />
86
+ </div>
87
+ </div>
88
+ )
89
+ }
90
+
91
+ export { DataTablePagination, getDataTablePageCount }