@open-mercato/ui 0.4.2-canary-c02407ff85

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 (319) hide show
  1. package/build.mjs +62 -0
  2. package/dist/backend/AppShell.js +902 -0
  3. package/dist/backend/AppShell.js.map +7 -0
  4. package/dist/backend/ConfirmDialog.js +17 -0
  5. package/dist/backend/ConfirmDialog.js.map +7 -0
  6. package/dist/backend/ContextHelp.js +31 -0
  7. package/dist/backend/ContextHelp.js.map +7 -0
  8. package/dist/backend/CrudForm.js +2028 -0
  9. package/dist/backend/CrudForm.js.map +7 -0
  10. package/dist/backend/DataTable.js +1363 -0
  11. package/dist/backend/DataTable.js.map +7 -0
  12. package/dist/backend/EmptyState.js +52 -0
  13. package/dist/backend/EmptyState.js.map +7 -0
  14. package/dist/backend/FilterBar.js +140 -0
  15. package/dist/backend/FilterBar.js.map +7 -0
  16. package/dist/backend/FilterOverlay.js +279 -0
  17. package/dist/backend/FilterOverlay.js.map +7 -0
  18. package/dist/backend/FlashMessages.js +66 -0
  19. package/dist/backend/FlashMessages.js.map +7 -0
  20. package/dist/backend/JsonBuilder.js +322 -0
  21. package/dist/backend/JsonBuilder.js.map +7 -0
  22. package/dist/backend/JsonDisplay.js +203 -0
  23. package/dist/backend/JsonDisplay.js.map +7 -0
  24. package/dist/backend/Page.js +27 -0
  25. package/dist/backend/Page.js.map +7 -0
  26. package/dist/backend/PerspectiveSidebar.js +282 -0
  27. package/dist/backend/PerspectiveSidebar.js.map +7 -0
  28. package/dist/backend/RowActions.js +148 -0
  29. package/dist/backend/RowActions.js.map +7 -0
  30. package/dist/backend/TruncatedCell.js +92 -0
  31. package/dist/backend/TruncatedCell.js.map +7 -0
  32. package/dist/backend/UserMenu.js +107 -0
  33. package/dist/backend/UserMenu.js.map +7 -0
  34. package/dist/backend/ValueIcons.js +34 -0
  35. package/dist/backend/ValueIcons.js.map +7 -0
  36. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +1264 -0
  37. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +7 -0
  38. package/dist/backend/custom-fields/FieldDefinitionsManager.js +332 -0
  39. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +7 -0
  40. package/dist/backend/dashboard/DashboardScreen.js +578 -0
  41. package/dist/backend/dashboard/DashboardScreen.js.map +7 -0
  42. package/dist/backend/dashboard/index.js +5 -0
  43. package/dist/backend/dashboard/index.js.map +7 -0
  44. package/dist/backend/dashboard/widgetRegistry.js +55 -0
  45. package/dist/backend/dashboard/widgetRegistry.js.map +7 -0
  46. package/dist/backend/detail/ActivitiesSection.js +962 -0
  47. package/dist/backend/detail/ActivitiesSection.js.map +7 -0
  48. package/dist/backend/detail/AddressEditor.js +413 -0
  49. package/dist/backend/detail/AddressEditor.js.map +7 -0
  50. package/dist/backend/detail/AddressTiles.js +437 -0
  51. package/dist/backend/detail/AddressTiles.js.map +7 -0
  52. package/dist/backend/detail/AddressesSection.js +264 -0
  53. package/dist/backend/detail/AddressesSection.js.map +7 -0
  54. package/dist/backend/detail/AttachmentDeleteDialog.js +41 -0
  55. package/dist/backend/detail/AttachmentDeleteDialog.js.map +7 -0
  56. package/dist/backend/detail/AttachmentMetadataDialog.js +517 -0
  57. package/dist/backend/detail/AttachmentMetadataDialog.js.map +7 -0
  58. package/dist/backend/detail/AttachmentsSection.js +367 -0
  59. package/dist/backend/detail/AttachmentsSection.js.map +7 -0
  60. package/dist/backend/detail/CustomDataSection.js +433 -0
  61. package/dist/backend/detail/CustomDataSection.js.map +7 -0
  62. package/dist/backend/detail/DetailFieldsSection.js +75 -0
  63. package/dist/backend/detail/DetailFieldsSection.js.map +7 -0
  64. package/dist/backend/detail/ErrorMessage.js +28 -0
  65. package/dist/backend/detail/ErrorMessage.js.map +7 -0
  66. package/dist/backend/detail/InlineEditors.js +681 -0
  67. package/dist/backend/detail/InlineEditors.js.map +7 -0
  68. package/dist/backend/detail/LoadingMessage.js +14 -0
  69. package/dist/backend/detail/LoadingMessage.js.map +7 -0
  70. package/dist/backend/detail/NotesSection.js +1032 -0
  71. package/dist/backend/detail/NotesSection.js.map +7 -0
  72. package/dist/backend/detail/TabEmptyState.js +25 -0
  73. package/dist/backend/detail/TabEmptyState.js.map +7 -0
  74. package/dist/backend/detail/TagsSection.js +254 -0
  75. package/dist/backend/detail/TagsSection.js.map +7 -0
  76. package/dist/backend/detail/addressFormat.js +77 -0
  77. package/dist/backend/detail/addressFormat.js.map +7 -0
  78. package/dist/backend/detail/index.js +34 -0
  79. package/dist/backend/detail/index.js.map +7 -0
  80. package/dist/backend/fields/registry.generated.js +8 -0
  81. package/dist/backend/fields/registry.generated.js.map +7 -0
  82. package/dist/backend/fields/registry.js +29 -0
  83. package/dist/backend/fields/registry.js.map +7 -0
  84. package/dist/backend/indexes/PartialIndexBanner.js +58 -0
  85. package/dist/backend/indexes/PartialIndexBanner.js.map +7 -0
  86. package/dist/backend/indexes/store.js +62 -0
  87. package/dist/backend/indexes/store.js.map +7 -0
  88. package/dist/backend/injection/InjectionSpot.js +179 -0
  89. package/dist/backend/injection/InjectionSpot.js.map +7 -0
  90. package/dist/backend/injection/PageInjectionBoundary.js +26 -0
  91. package/dist/backend/injection/PageInjectionBoundary.js.map +7 -0
  92. package/dist/backend/injection/helpers.js +26 -0
  93. package/dist/backend/injection/helpers.js.map +7 -0
  94. package/dist/backend/injection/widgetRegistry.js +55 -0
  95. package/dist/backend/injection/widgetRegistry.js.map +7 -0
  96. package/dist/backend/inputs/ComboboxInput.js +225 -0
  97. package/dist/backend/inputs/ComboboxInput.js.map +7 -0
  98. package/dist/backend/inputs/LookupSelect.js +191 -0
  99. package/dist/backend/inputs/LookupSelect.js.map +7 -0
  100. package/dist/backend/inputs/PhoneNumberField.js +100 -0
  101. package/dist/backend/inputs/PhoneNumberField.js.map +7 -0
  102. package/dist/backend/inputs/SwitchableMarkdownInput.js +92 -0
  103. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +7 -0
  104. package/dist/backend/inputs/TagsInput.js +222 -0
  105. package/dist/backend/inputs/TagsInput.js.map +7 -0
  106. package/dist/backend/inputs/index.js +6 -0
  107. package/dist/backend/inputs/index.js.map +7 -0
  108. package/dist/backend/operations/LastOperationBanner.js +80 -0
  109. package/dist/backend/operations/LastOperationBanner.js.map +7 -0
  110. package/dist/backend/operations/store.js +183 -0
  111. package/dist/backend/operations/store.js.map +7 -0
  112. package/dist/backend/schedule/ScheduleAgenda.js +107 -0
  113. package/dist/backend/schedule/ScheduleAgenda.js.map +7 -0
  114. package/dist/backend/schedule/ScheduleGrid.js +107 -0
  115. package/dist/backend/schedule/ScheduleGrid.js.map +7 -0
  116. package/dist/backend/schedule/ScheduleToolbar.js +166 -0
  117. package/dist/backend/schedule/ScheduleToolbar.js.map +7 -0
  118. package/dist/backend/schedule/ScheduleView.js +165 -0
  119. package/dist/backend/schedule/ScheduleView.js.map +7 -0
  120. package/dist/backend/schedule/index.js +6 -0
  121. package/dist/backend/schedule/index.js.map +7 -0
  122. package/dist/backend/schedule/recurrence.js +83 -0
  123. package/dist/backend/schedule/recurrence.js.map +7 -0
  124. package/dist/backend/schedule/types.js +1 -0
  125. package/dist/backend/schedule/types.js.map +7 -0
  126. package/dist/backend/upgrades/UpgradeActionBanner.js +91 -0
  127. package/dist/backend/upgrades/UpgradeActionBanner.js.map +7 -0
  128. package/dist/backend/utils/api.js +127 -0
  129. package/dist/backend/utils/api.js.map +7 -0
  130. package/dist/backend/utils/apiCall.js +48 -0
  131. package/dist/backend/utils/apiCall.js.map +7 -0
  132. package/dist/backend/utils/crud.js +126 -0
  133. package/dist/backend/utils/crud.js.map +7 -0
  134. package/dist/backend/utils/customFieldColumns.js +56 -0
  135. package/dist/backend/utils/customFieldColumns.js.map +7 -0
  136. package/dist/backend/utils/customFieldDefs.js +143 -0
  137. package/dist/backend/utils/customFieldDefs.js.map +7 -0
  138. package/dist/backend/utils/customFieldFilters.js +126 -0
  139. package/dist/backend/utils/customFieldFilters.js.map +7 -0
  140. package/dist/backend/utils/customFieldForms.js +162 -0
  141. package/dist/backend/utils/customFieldForms.js.map +7 -0
  142. package/dist/backend/utils/customFieldValues.js +26 -0
  143. package/dist/backend/utils/customFieldValues.js.map +7 -0
  144. package/dist/backend/utils/flash.js +16 -0
  145. package/dist/backend/utils/flash.js.map +7 -0
  146. package/dist/backend/utils/nav.js +185 -0
  147. package/dist/backend/utils/nav.js.map +7 -0
  148. package/dist/backend/utils/serverErrors.js +230 -0
  149. package/dist/backend/utils/serverErrors.js.map +7 -0
  150. package/dist/frontend/AuthFooter.js +23 -0
  151. package/dist/frontend/AuthFooter.js.map +7 -0
  152. package/dist/frontend/LanguageSwitcher.js +57 -0
  153. package/dist/frontend/LanguageSwitcher.js.map +7 -0
  154. package/dist/frontend/Layout.js +14 -0
  155. package/dist/frontend/Layout.js.map +7 -0
  156. package/dist/index.js +32 -0
  157. package/dist/index.js.map +7 -0
  158. package/dist/primitives/DataLoader.js +67 -0
  159. package/dist/primitives/DataLoader.js.map +7 -0
  160. package/dist/primitives/ErrorNotice.js +20 -0
  161. package/dist/primitives/ErrorNotice.js.map +7 -0
  162. package/dist/primitives/alert.js +38 -0
  163. package/dist/primitives/alert.js.map +7 -0
  164. package/dist/primitives/badge.js +28 -0
  165. package/dist/primitives/badge.js.map +7 -0
  166. package/dist/primitives/button.js +44 -0
  167. package/dist/primitives/button.js.map +7 -0
  168. package/dist/primitives/card.js +91 -0
  169. package/dist/primitives/card.js.map +7 -0
  170. package/dist/primitives/checkbox.js +28 -0
  171. package/dist/primitives/checkbox.js.map +7 -0
  172. package/dist/primitives/dialog.js +90 -0
  173. package/dist/primitives/dialog.js.map +7 -0
  174. package/dist/primitives/input.js +22 -0
  175. package/dist/primitives/input.js.map +7 -0
  176. package/dist/primitives/label.js +21 -0
  177. package/dist/primitives/label.js.map +7 -0
  178. package/dist/primitives/separator.js +9 -0
  179. package/dist/primitives/separator.js.map +7 -0
  180. package/dist/primitives/spinner.js +24 -0
  181. package/dist/primitives/spinner.js.map +7 -0
  182. package/dist/primitives/switch.js +80 -0
  183. package/dist/primitives/switch.js.map +7 -0
  184. package/dist/primitives/table.js +29 -0
  185. package/dist/primitives/table.js.map +7 -0
  186. package/dist/primitives/tabs.js +87 -0
  187. package/dist/primitives/tabs.js.map +7 -0
  188. package/dist/primitives/textarea.js +21 -0
  189. package/dist/primitives/textarea.js.map +7 -0
  190. package/dist/primitives/tooltip.js +60 -0
  191. package/dist/primitives/tooltip.js.map +7 -0
  192. package/dist/theme/QueryProvider.js +44 -0
  193. package/dist/theme/QueryProvider.js.map +7 -0
  194. package/dist/theme/ThemeProvider.js +95 -0
  195. package/dist/theme/ThemeProvider.js.map +7 -0
  196. package/dist/theme/ThemeToggle.js +88 -0
  197. package/dist/theme/ThemeToggle.js.map +7 -0
  198. package/dist/theme/index.js +10 -0
  199. package/dist/theme/index.js.map +7 -0
  200. package/dist/types/react-big-calendar.d.js +1 -0
  201. package/dist/types/react-big-calendar.d.js.map +7 -0
  202. package/jest.config.cjs +23 -0
  203. package/jest.setup.ts +55 -0
  204. package/package.json +105 -0
  205. package/src/backend/AppShell.tsx +1096 -0
  206. package/src/backend/ConfirmDialog.tsx +19 -0
  207. package/src/backend/ContextHelp.tsx +38 -0
  208. package/src/backend/CrudForm.tsx +2503 -0
  209. package/src/backend/DataTable.tsx +1730 -0
  210. package/src/backend/EmptyState.tsx +65 -0
  211. package/src/backend/FilterBar.tsx +161 -0
  212. package/src/backend/FilterOverlay.tsx +328 -0
  213. package/src/backend/FlashMessages.tsx +82 -0
  214. package/src/backend/JsonBuilder.tsx +362 -0
  215. package/src/backend/JsonDisplay.tsx +254 -0
  216. package/src/backend/Page.tsx +30 -0
  217. package/src/backend/PerspectiveSidebar.tsx +337 -0
  218. package/src/backend/RowActions.tsx +151 -0
  219. package/src/backend/TruncatedCell.tsx +133 -0
  220. package/src/backend/UserMenu.tsx +118 -0
  221. package/src/backend/ValueIcons.tsx +48 -0
  222. package/src/backend/__tests__/AppShell.test.tsx +115 -0
  223. package/src/backend/__tests__/CrudForm.render.test.tsx +30 -0
  224. package/src/backend/__tests__/DataTable.render.test.tsx +48 -0
  225. package/src/backend/__tests__/custom-field-filters.test.ts +72 -0
  226. package/src/backend/__tests__/custom-field-forms.test.ts +54 -0
  227. package/src/backend/__tests__/serverErrors.test.ts +83 -0
  228. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +1292 -0
  229. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +381 -0
  230. package/src/backend/dashboard/DashboardScreen.tsx +684 -0
  231. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +112 -0
  232. package/src/backend/dashboard/index.ts +1 -0
  233. package/src/backend/dashboard/widgetRegistry.ts +68 -0
  234. package/src/backend/detail/ActivitiesSection.tsx +1284 -0
  235. package/src/backend/detail/AddressEditor.tsx +472 -0
  236. package/src/backend/detail/AddressTiles.tsx +587 -0
  237. package/src/backend/detail/AddressesSection.tsx +346 -0
  238. package/src/backend/detail/AttachmentDeleteDialog.tsx +56 -0
  239. package/src/backend/detail/AttachmentMetadataDialog.tsx +672 -0
  240. package/src/backend/detail/AttachmentsSection.tsx +414 -0
  241. package/src/backend/detail/CustomDataSection.tsx +530 -0
  242. package/src/backend/detail/DetailFieldsSection.tsx +147 -0
  243. package/src/backend/detail/ErrorMessage.tsx +32 -0
  244. package/src/backend/detail/InlineEditors.tsx +877 -0
  245. package/src/backend/detail/LoadingMessage.tsx +14 -0
  246. package/src/backend/detail/NotesSection.tsx +1275 -0
  247. package/src/backend/detail/TabEmptyState.tsx +48 -0
  248. package/src/backend/detail/TagsSection.tsx +314 -0
  249. package/src/backend/detail/addressFormat.tsx +121 -0
  250. package/src/backend/detail/index.ts +44 -0
  251. package/src/backend/fields/registry.generated.ts +8 -0
  252. package/src/backend/fields/registry.ts +38 -0
  253. package/src/backend/indexes/PartialIndexBanner.tsx +68 -0
  254. package/src/backend/indexes/store.ts +88 -0
  255. package/src/backend/injection/InjectionSpot.tsx +236 -0
  256. package/src/backend/injection/PageInjectionBoundary.tsx +31 -0
  257. package/src/backend/injection/helpers.ts +35 -0
  258. package/src/backend/injection/widgetRegistry.ts +68 -0
  259. package/src/backend/inputs/ComboboxInput.tsx +269 -0
  260. package/src/backend/inputs/LookupSelect.tsx +247 -0
  261. package/src/backend/inputs/PhoneNumberField.tsx +129 -0
  262. package/src/backend/inputs/SwitchableMarkdownInput.tsx +128 -0
  263. package/src/backend/inputs/TagsInput.tsx +259 -0
  264. package/src/backend/inputs/index.ts +5 -0
  265. package/src/backend/operations/LastOperationBanner.tsx +85 -0
  266. package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +99 -0
  267. package/src/backend/operations/store.ts +230 -0
  268. package/src/backend/schedule/ScheduleAgenda.tsx +136 -0
  269. package/src/backend/schedule/ScheduleGrid.tsx +136 -0
  270. package/src/backend/schedule/ScheduleToolbar.tsx +178 -0
  271. package/src/backend/schedule/ScheduleView.tsx +198 -0
  272. package/src/backend/schedule/index.ts +5 -0
  273. package/src/backend/schedule/recurrence.ts +99 -0
  274. package/src/backend/schedule/types.ts +26 -0
  275. package/src/backend/upgrades/UpgradeActionBanner.tsx +128 -0
  276. package/src/backend/utils/__tests__/apiCall.test.ts +109 -0
  277. package/src/backend/utils/__tests__/crud.test.ts +87 -0
  278. package/src/backend/utils/__tests__/customFieldDefs.test.ts +25 -0
  279. package/src/backend/utils/__tests__/customFieldValues.test.ts +35 -0
  280. package/src/backend/utils/api.ts +149 -0
  281. package/src/backend/utils/apiCall.ts +96 -0
  282. package/src/backend/utils/crud.ts +174 -0
  283. package/src/backend/utils/customFieldColumns.ts +71 -0
  284. package/src/backend/utils/customFieldDefs.ts +245 -0
  285. package/src/backend/utils/customFieldFilters.ts +145 -0
  286. package/src/backend/utils/customFieldForms.ts +196 -0
  287. package/src/backend/utils/customFieldValues.ts +41 -0
  288. package/src/backend/utils/flash.ts +17 -0
  289. package/src/backend/utils/nav.ts +238 -0
  290. package/src/backend/utils/serverErrors.ts +302 -0
  291. package/src/frontend/AuthFooter.tsx +29 -0
  292. package/src/frontend/LanguageSwitcher.tsx +66 -0
  293. package/src/frontend/Layout.tsx +13 -0
  294. package/src/index.ts +32 -0
  295. package/src/primitives/DataLoader.tsx +92 -0
  296. package/src/primitives/ErrorNotice.tsx +26 -0
  297. package/src/primitives/alert.tsx +52 -0
  298. package/src/primitives/badge.tsx +31 -0
  299. package/src/primitives/button.tsx +47 -0
  300. package/src/primitives/card.tsx +92 -0
  301. package/src/primitives/checkbox.tsx +28 -0
  302. package/src/primitives/dialog.tsx +110 -0
  303. package/src/primitives/input.tsx +20 -0
  304. package/src/primitives/label.tsx +18 -0
  305. package/src/primitives/separator.tsx +7 -0
  306. package/src/primitives/spinner.tsx +27 -0
  307. package/src/primitives/switch.tsx +86 -0
  308. package/src/primitives/table.tsx +27 -0
  309. package/src/primitives/tabs.tsx +128 -0
  310. package/src/primitives/textarea.tsx +20 -0
  311. package/src/primitives/tooltip.tsx +85 -0
  312. package/src/theme/QueryProvider.tsx +46 -0
  313. package/src/theme/ThemeProvider.tsx +120 -0
  314. package/src/theme/ThemeToggle.tsx +88 -0
  315. package/src/theme/index.ts +3 -0
  316. package/src/types/react-big-calendar.d.ts +16 -0
  317. package/tsconfig.build.json +11 -0
  318. package/tsconfig.json +9 -0
  319. package/watch.mjs +6 -0
@@ -0,0 +1,92 @@
1
+ import * as React from 'react'
2
+ import { Spinner } from './spinner'
3
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
4
+
5
+ export interface DataLoaderProps {
6
+ isLoading: boolean
7
+ children: React.ReactNode
8
+ loadingMessage?: string
9
+ spinnerSize?: 'sm' | 'md' | 'lg'
10
+ className?: string
11
+ loadingClassName?: string
12
+ // Optional: show a skeleton or placeholder instead of just spinner
13
+ showSkeleton?: boolean
14
+ skeletonComponent?: React.ReactNode
15
+ }
16
+
17
+ export function DataLoader({
18
+ isLoading,
19
+ children,
20
+ loadingMessage,
21
+ spinnerSize = 'md',
22
+ className = '',
23
+ loadingClassName = '',
24
+ showSkeleton = false,
25
+ skeletonComponent
26
+ }: DataLoaderProps) {
27
+ const t = useT()
28
+ const resolvedLoadingMessage = loadingMessage ?? t('ui.dataLoader.loading', 'Loading...')
29
+ if (isLoading) {
30
+ if (showSkeleton && skeletonComponent) {
31
+ return <div className={className}>{skeletonComponent}</div>
32
+ }
33
+
34
+ return (
35
+ <div className={`flex items-center justify-center gap-2 py-4 ${loadingClassName} ${className}`}>
36
+ <Spinner size={spinnerSize} />
37
+ <span className="text-sm text-muted-foreground">{resolvedLoadingMessage}</span>
38
+ </div>
39
+ )
40
+ }
41
+
42
+ return <div className={className}>{children}</div>
43
+ }
44
+
45
+ // Convenience component for inline loading states
46
+ export function InlineLoader({
47
+ isLoading,
48
+ children,
49
+ loadingMessage,
50
+ spinnerSize = 'sm'
51
+ }: {
52
+ isLoading: boolean
53
+ children: React.ReactNode
54
+ loadingMessage?: string
55
+ spinnerSize?: 'sm' | 'md' | 'lg'
56
+ }) {
57
+ return (
58
+ <DataLoader
59
+ isLoading={isLoading}
60
+ loadingMessage={loadingMessage}
61
+ spinnerSize={spinnerSize}
62
+ className="inline-flex items-center"
63
+ loadingClassName="py-2"
64
+ >
65
+ {children}
66
+ </DataLoader>
67
+ )
68
+ }
69
+
70
+ // Convenience component for full-page loading states
71
+ export function PageLoader({
72
+ isLoading,
73
+ children,
74
+ loadingMessage,
75
+ spinnerSize = 'lg'
76
+ }: {
77
+ isLoading: boolean
78
+ children: React.ReactNode
79
+ loadingMessage?: string
80
+ spinnerSize?: 'sm' | 'md' | 'lg'
81
+ }) {
82
+ return (
83
+ <DataLoader
84
+ isLoading={isLoading}
85
+ loadingMessage={loadingMessage}
86
+ spinnerSize={spinnerSize}
87
+ className="min-h-[200px] flex items-center justify-center"
88
+ >
89
+ {children}
90
+ </DataLoader>
91
+ )
92
+ }
@@ -0,0 +1,26 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
4
+
5
+ export function ErrorNotice({ title, message, action }: {
6
+ title?: string
7
+ message?: string
8
+ action?: React.ReactNode
9
+ }) {
10
+ const t = useT()
11
+ const defaultTitle = title ?? t('ui.errors.defaultTitle', 'Something went wrong')
12
+ const defaultMessage = message ?? t('ui.errors.defaultMessage', 'Unable to load data. Please try again.')
13
+ return (
14
+ <div className="rounded-md border border-red-200 bg-red-50 p-4 text-red-800">
15
+ <div className="flex items-start gap-3">
16
+ <span className="inline-block mt-0.5 h-4 w-4 rounded-full border-2 border-red-500" aria-hidden />
17
+ <div className="space-y-1">
18
+ <div className="text-sm font-medium">{defaultTitle}</div>
19
+ <div className="text-sm opacity-90">{defaultMessage}</div>
20
+ {action ? <div className="mt-2">{action}</div> : null}
21
+ </div>
22
+ </div>
23
+ </div>
24
+ )
25
+ }
26
+
@@ -0,0 +1,52 @@
1
+ import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+
4
+ import { cn } from '@open-mercato/shared/lib/utils'
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-8 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'border-border bg-background text-foreground',
12
+ destructive:
13
+ 'border-destructive/60 bg-destructive/10 text-destructive dark:border-destructive/40 dark:bg-destructive/20 dark:text-destructive-foreground',
14
+ success:
15
+ 'border-emerald-600/30 bg-emerald-500/10 text-emerald-900 dark:border-emerald-500/50 dark:bg-emerald-500/15 dark:text-emerald-50',
16
+ warning:
17
+ 'border-amber-500/30 bg-amber-400/10 text-amber-950 dark:border-amber-400/40 dark:bg-amber-400/20 dark:text-amber-50',
18
+ info:
19
+ 'border-sky-600/30 bg-sky-500/10 text-sky-900 dark:border-sky-500/40 dark:bg-sky-500/20 dark:text-sky-50',
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: 'default',
24
+ },
25
+ }
26
+ )
27
+
28
+ type AlertProps = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
29
+
30
+ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(({ className, variant, ...props }, ref) => (
31
+ <div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
32
+ ))
33
+
34
+ Alert.displayName = 'Alert'
35
+
36
+ const AlertTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
37
+ ({ className, ...props }, ref) => (
38
+ <h5 ref={ref} className={cn('mb-1 text-sm font-semibold leading-tight', className)} {...props} />
39
+ )
40
+ )
41
+
42
+ AlertTitle.displayName = 'AlertTitle'
43
+
44
+ const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
45
+ ({ className, ...props }, ref) => (
46
+ <p ref={ref} className={cn('text-sm leading-relaxed', className)} {...props} />
47
+ )
48
+ )
49
+
50
+ AlertDescription.displayName = 'AlertDescription'
51
+
52
+ export { Alert, AlertDescription, AlertTitle, alertVariants }
@@ -0,0 +1,31 @@
1
+ import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+ import { cn } from '@open-mercato/shared/lib/utils'
4
+
5
+ const badgeVariants = cva(
6
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: 'border-transparent bg-primary text-primary-foreground shadow',
11
+ secondary: 'border-transparent bg-secondary text-secondary-foreground',
12
+ destructive: 'border-transparent bg-destructive text-destructive-foreground shadow',
13
+ outline: 'text-foreground',
14
+ muted: 'border-transparent bg-muted text-muted-foreground',
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: 'default',
19
+ },
20
+ }
21
+ )
22
+
23
+ export type BadgeProps = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof badgeVariants>
24
+
25
+ export const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(({ className, variant, ...props }, ref) => (
26
+ <div ref={ref} className={cn(badgeVariants({ variant }), className)} {...props} />
27
+ ))
28
+
29
+ Badge.displayName = 'Badge'
30
+
31
+ export { badgeVariants }
@@ -0,0 +1,47 @@
1
+ import * as React from 'react'
2
+ import { Slot } from '@radix-ui/react-slot'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+ import { cn } from '@open-mercato/shared/lib/utils'
5
+
6
+ const buttonVariants = cva(
7
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
12
+ destructive:
13
+ 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
14
+ outline:
15
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
16
+ secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
17
+ ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
18
+ link: 'text-primary underline-offset-4 hover:underline',
19
+ },
20
+ size: {
21
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
22
+ sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
23
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
24
+ icon: 'size-9',
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ variant: 'default',
29
+ size: 'default',
30
+ },
31
+ }
32
+ )
33
+
34
+ export function Button({
35
+ className,
36
+ variant,
37
+ size,
38
+ asChild = false,
39
+ ...props
40
+ }: React.ComponentProps<'button'> &
41
+ VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
42
+ const Comp = asChild ? Slot : 'button'
43
+ return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />
44
+ }
45
+
46
+ export { buttonVariants }
47
+
@@ -0,0 +1,92 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from '@open-mercato/shared/lib/utils'
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }
@@ -0,0 +1,28 @@
1
+ import * as React from "react"
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3
+ import { Check } from "lucide-react"
4
+
5
+ import { cn } from "@open-mercato/shared/lib/utils"
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "peer size-4 shrink-0 rounded-sm border border-input bg-background shadow-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary",
15
+ className
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator
20
+ className={cn("flex items-center justify-center text-current")}
21
+ >
22
+ <Check className="size-3.5" />
23
+ </CheckboxPrimitive.Indicator>
24
+ </CheckboxPrimitive.Root>
25
+ ))
26
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
27
+
28
+ export { Checkbox }
@@ -0,0 +1,110 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import * as DialogPrimitive from '@radix-ui/react-dialog'
5
+ import { X } from 'lucide-react'
6
+ import { cn } from '@open-mercato/shared/lib/utils'
7
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
8
+
9
+ const Dialog = DialogPrimitive.Root
10
+
11
+ const DialogTrigger = DialogPrimitive.Trigger
12
+
13
+ const DialogPortal = DialogPrimitive.Portal
14
+
15
+ const DialogClose = DialogPrimitive.Close
16
+
17
+ const DialogOverlay = React.forwardRef<
18
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
19
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20
+ >(({ className, ...props }, ref) => (
21
+ <DialogPrimitive.Overlay
22
+ ref={ref}
23
+ className={cn(
24
+ 'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity data-[state=open]:animate-in data-[state=closed]:animate-out',
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ ))
30
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31
+
32
+ const DialogContent = React.forwardRef<
33
+ React.ElementRef<typeof DialogPrimitive.Content>,
34
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35
+ >(({ className, children, ...props }, ref) => {
36
+ const t = useT()
37
+
38
+ return (
39
+ <DialogPortal>
40
+ <DialogOverlay />
41
+ <DialogPrimitive.Content
42
+ ref={ref}
43
+ data-dialog-content=""
44
+ className={cn(
45
+ 'fixed inset-x-0 bottom-0 z-50 flex min-h-[50vh] max-h-[70vh] w-full translate-x-0 translate-y-0 flex-col gap-4 overflow-y-auto rounded-t-2xl border-t bg-card p-6 shadow-lg',
46
+ 'sm:inset-auto sm:left-1/2 sm:top-1/2 sm:min-h-0 sm:h-auto sm:w-full sm:max-w-lg sm:max-h-[90vh] sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-xl sm:border',
47
+ 'focus:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out',
48
+ className,
49
+ )}
50
+ {...props}
51
+ >
52
+ <DialogClose
53
+ className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
54
+ aria-label={t('ui.dialog.close.ariaLabel', 'Close')}
55
+ >
56
+ <X className="h-4 w-4" />
57
+ </DialogClose>
58
+ {children}
59
+ </DialogPrimitive.Content>
60
+ </DialogPortal>
61
+ )
62
+ })
63
+ DialogContent.displayName = DialogPrimitive.Content.displayName
64
+
65
+ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
66
+ <div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
67
+ )
68
+ DialogHeader.displayName = 'DialogHeader'
69
+
70
+ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
71
+ <div className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)} {...props} />
72
+ )
73
+ DialogFooter.displayName = 'DialogFooter'
74
+
75
+ const DialogTitle = React.forwardRef<
76
+ React.ElementRef<typeof DialogPrimitive.Title>,
77
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
78
+ >(({ className, ...props }, ref) => (
79
+ <DialogPrimitive.Title
80
+ ref={ref}
81
+ className={cn('text-lg font-semibold leading-none tracking-tight', className)}
82
+ {...props}
83
+ />
84
+ ))
85
+ DialogTitle.displayName = DialogPrimitive.Title.displayName
86
+
87
+ const DialogDescription = React.forwardRef<
88
+ React.ElementRef<typeof DialogPrimitive.Description>,
89
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
90
+ >(({ className, ...props }, ref) => (
91
+ <DialogPrimitive.Description
92
+ ref={ref}
93
+ className={cn('text-sm text-muted-foreground', className)}
94
+ {...props}
95
+ />
96
+ ))
97
+ DialogDescription.displayName = DialogPrimitive.Description.displayName
98
+
99
+ export {
100
+ Dialog,
101
+ DialogPortal,
102
+ DialogOverlay,
103
+ DialogTrigger,
104
+ DialogClose,
105
+ DialogContent,
106
+ DialogHeader,
107
+ DialogFooter,
108
+ DialogTitle,
109
+ DialogDescription,
110
+ }
@@ -0,0 +1,20 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@open-mercato/shared/lib/utils'
3
+
4
+ type InputProps = React.ComponentPropsWithoutRef<'input'>
5
+
6
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
7
+ ({ className, type = 'text', ...props }, ref) => (
8
+ <input
9
+ ref={ref}
10
+ type={type}
11
+ className={cn(
12
+ 'flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ )
18
+ )
19
+
20
+ Input.displayName = 'Input'
@@ -0,0 +1,18 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import * as LabelPrimitive from '@radix-ui/react-label'
4
+ import { cn } from '@open-mercato/shared/lib/utils'
5
+
6
+ export function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
7
+ return (
8
+ <LabelPrimitive.Root
9
+ data-slot="label"
10
+ className={cn(
11
+ 'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
12
+ className
13
+ )}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
@@ -0,0 +1,7 @@
1
+ import * as React from 'react'
2
+
3
+ export function Separator({ className = '', orientation = 'horizontal' }: { className?: string; orientation?: 'horizontal' | 'vertical' }) {
4
+ const base = orientation === 'vertical' ? 'w-px h-full' : 'h-px w-full'
5
+ return <div role="separator" aria-orientation={orientation} className={`${base} bg-border ${className}`} />
6
+ }
7
+
@@ -0,0 +1,27 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
4
+
5
+ export interface SpinnerProps {
6
+ className?: string
7
+ size?: 'sm' | 'md' | 'lg'
8
+ }
9
+
10
+ export function Spinner({ className = '', size = 'md' }: SpinnerProps) {
11
+ const t = useT()
12
+ const sizeClasses = {
13
+ sm: 'h-4 w-4',
14
+ md: 'h-6 w-6',
15
+ lg: 'h-8 w-8',
16
+ }
17
+
18
+ return (
19
+ <span
20
+ className={`inline-flex items-center justify-center animate-spin rounded-full border-2 border-border border-t-foreground ${sizeClasses[size]} ${className}`}
21
+ role="status"
22
+ aria-label={t('ui.spinner.ariaLabel', 'Loading')}
23
+ >
24
+ <span className="sr-only">{t('ui.spinner.srOnly', 'Loading...')}</span>
25
+ </span>
26
+ )
27
+ }
@@ -0,0 +1,86 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '@open-mercato/shared/lib/utils'
4
+
5
+ type SwitchProps = {
6
+ checked?: boolean
7
+ defaultChecked?: boolean
8
+ onCheckedChange?: (checked: boolean) => void
9
+ } & Omit<React.ComponentProps<'button'>, 'onChange'>
10
+
11
+ export const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
12
+ ({ checked, defaultChecked, onCheckedChange, disabled, className, onClick, onKeyDown, ...props }, ref) => {
13
+ const isControlled = typeof checked === 'boolean'
14
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultChecked ?? false)
15
+
16
+ const currentChecked = isControlled ? checked : uncontrolledValue
17
+
18
+ const toggle = React.useCallback(
19
+ (event: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>) => {
20
+ event.preventDefault()
21
+ if (disabled) {
22
+ return
23
+ }
24
+ const next = !currentChecked
25
+ if (!isControlled) {
26
+ setUncontrolledValue(next)
27
+ }
28
+ onCheckedChange?.(next)
29
+ },
30
+ [currentChecked, disabled, isControlled, onCheckedChange]
31
+ )
32
+
33
+ const handleClick = React.useCallback(
34
+ (event: React.MouseEvent<HTMLButtonElement>) => {
35
+ onClick?.(event)
36
+ if (!event.defaultPrevented) {
37
+ toggle(event)
38
+ }
39
+ },
40
+ [onClick, toggle]
41
+ )
42
+
43
+ const handleKeyDown = React.useCallback(
44
+ (event: React.KeyboardEvent<HTMLButtonElement>) => {
45
+ onKeyDown?.(event)
46
+ if (event.defaultPrevented) {
47
+ return
48
+ }
49
+ if (event.key === ' ' || event.key === 'Enter') {
50
+ toggle(event)
51
+ }
52
+ },
53
+ [onKeyDown, toggle]
54
+ )
55
+
56
+ return (
57
+ <button
58
+ type="button"
59
+ role="switch"
60
+ aria-checked={currentChecked}
61
+ aria-disabled={disabled}
62
+ data-state={currentChecked ? 'checked' : 'unchecked'}
63
+ data-disabled={disabled ? '' : undefined}
64
+ ref={ref}
65
+ onClick={handleClick}
66
+ onKeyDown={handleKeyDown}
67
+ disabled={disabled}
68
+ className={cn(
69
+ 'inline-flex h-6 w-11 items-center rounded-full border border-transparent bg-input/60 transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary',
70
+ className
71
+ )}
72
+ {...props}
73
+ >
74
+ <span
75
+ aria-hidden
76
+ className={cn(
77
+ 'inline-block size-5 translate-x-0 rounded-full bg-background shadow transition-transform duration-200',
78
+ currentChecked ? 'translate-x-5' : 'translate-x-0'
79
+ )}
80
+ />
81
+ </button>
82
+ )
83
+ }
84
+ )
85
+
86
+ Switch.displayName = 'Switch'
@@ -0,0 +1,27 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@open-mercato/shared/lib/utils'
3
+
4
+ export function Table({ className, ...props }: React.HTMLAttributes<HTMLTableElement>) {
5
+ return <table className={cn('w-full text-sm', className)} {...props} />
6
+ }
7
+
8
+ export function TableHeader(props: React.HTMLAttributes<HTMLTableSectionElement>) {
9
+ return <thead {...props} />
10
+ }
11
+
12
+ export function TableBody(props: React.HTMLAttributes<HTMLTableSectionElement>) {
13
+ return <tbody {...props} />
14
+ }
15
+
16
+ export function TableRow({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) {
17
+ return <tr className={cn('border-b last:border-b-0', className)} {...props} />
18
+ }
19
+
20
+ export function TableHead({ className, ...props }: React.ThHTMLAttributes<HTMLTableCellElement>) {
21
+ return <th className={cn('text-left font-medium px-4 py-2 whitespace-nowrap text-muted-foreground', className)} {...props} />
22
+ }
23
+
24
+ export function TableCell({ className, ...props }: React.TdHTMLAttributes<HTMLTableCellElement>) {
25
+ return <td className={cn('px-4 py-2', className)} {...props} />
26
+ }
27
+