@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,48 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '@open-mercato/shared/lib/utils'
5
+ import { EmptyState } from '../EmptyState'
6
+
7
+ type TabEmptyStateProps = {
8
+ title: string
9
+ description?: string
10
+ actionLabel?: string
11
+ onAction?: () => void
12
+ disabled?: boolean
13
+ action?: {
14
+ label: string
15
+ onClick?: () => void
16
+ icon?: React.ReactNode
17
+ disabled?: boolean
18
+ }
19
+ className?: string
20
+ children?: React.ReactNode
21
+ }
22
+
23
+ export function TabEmptyState({
24
+ title,
25
+ description,
26
+ actionLabel,
27
+ onAction,
28
+ disabled,
29
+ action,
30
+ className,
31
+ children,
32
+ }: TabEmptyStateProps) {
33
+ const resolvedAction =
34
+ action ??
35
+ (actionLabel
36
+ ? {
37
+ label: actionLabel,
38
+ onClick: onAction,
39
+ disabled,
40
+ }
41
+ : undefined)
42
+
43
+ return (
44
+ <EmptyState title={title} description={description} action={resolvedAction} className={cn('w-full', className)}>
45
+ {children}
46
+ </EmptyState>
47
+ )
48
+ }
@@ -0,0 +1,314 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Pencil, X } from 'lucide-react'
5
+ import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { TagsInput } from '@open-mercato/ui/backend/inputs/TagsInput'
7
+ import { DataLoader } from '@open-mercato/ui/primitives/DataLoader'
8
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
9
+
10
+ export type TagOption = {
11
+ id: string
12
+ label: string
13
+ color?: string | null
14
+ }
15
+
16
+ export type TagsSectionLabels = {
17
+ loading: string
18
+ placeholder: string
19
+ empty: string
20
+ loadError: string
21
+ createError: string
22
+ updateError: string
23
+ labelRequired: string
24
+ saveShortcut: string
25
+ cancelShortcut: string
26
+ edit?: string
27
+ cancel?: string
28
+ success?: string
29
+ }
30
+
31
+ export type TagsSectionProps = {
32
+ title: string
33
+ tags: TagOption[]
34
+ onChange?: (next: TagOption[]) => void
35
+ isSubmitting?: boolean
36
+ canEdit?: boolean
37
+ loadOptions: (query?: string) => Promise<TagOption[]>
38
+ createTag: (label: string) => Promise<TagOption>
39
+ onSave: (params: {
40
+ next: TagOption[]
41
+ added: TagOption[]
42
+ removed: TagOption[]
43
+ }) => Promise<void>
44
+ labels: TagsSectionLabels
45
+ }
46
+
47
+ export function TagsSection({
48
+ title,
49
+ tags,
50
+ onChange,
51
+ isSubmitting = false,
52
+ canEdit = true,
53
+ loadOptions,
54
+ createTag,
55
+ onSave,
56
+ labels,
57
+ }: TagsSectionProps) {
58
+ const [editing, setEditing] = React.useState(false)
59
+ const [draft, setDraft] = React.useState<string[]>([])
60
+ const [saving, setSaving] = React.useState(false)
61
+ const [options, setOptions] = React.useState<Map<string, TagOption>>(() => new Map())
62
+ const [loadingOptions, setLoadingOptions] = React.useState(false)
63
+ const [error, setError] = React.useState<string | null>(null)
64
+
65
+ React.useEffect(() => {
66
+ setOptions((prev) => {
67
+ const next = new Map(prev)
68
+ for (const tag of tags) {
69
+ next.set(tag.label.toLowerCase(), tag)
70
+ }
71
+ return next
72
+ })
73
+ }, [tags])
74
+
75
+ const syncFetchedOptions = React.useCallback((fetched: TagOption[]) => {
76
+ if (!fetched.length) return
77
+ setOptions((prev) => {
78
+ const next = new Map(prev)
79
+ for (const tag of fetched) {
80
+ next.set(tag.label.toLowerCase(), tag)
81
+ }
82
+ return next
83
+ })
84
+ }, [])
85
+
86
+ const loadSuggestions = React.useCallback(
87
+ async (query?: string) => {
88
+ try {
89
+ const fetched = await loadOptions(query)
90
+ syncFetchedOptions(fetched)
91
+ return fetched.map((tag) => tag.label)
92
+ } catch (err) {
93
+ console.error('tags.section.loadSuggestions', err)
94
+ return []
95
+ }
96
+ },
97
+ [loadOptions, syncFetchedOptions],
98
+ )
99
+
100
+ const startEditing = React.useCallback(async () => {
101
+ if (editing || isSubmitting || !canEdit) return
102
+ setError(null)
103
+ setDraft(tags.map((tag) => tag.label))
104
+ setEditing(true)
105
+ setLoadingOptions(true)
106
+ try {
107
+ const fetched = await loadOptions()
108
+ syncFetchedOptions(fetched)
109
+ } catch (err) {
110
+ const message = err instanceof Error ? err.message : labels.loadError
111
+ setError(message)
112
+ flash(message, 'error')
113
+ } finally {
114
+ setLoadingOptions(false)
115
+ }
116
+ }, [canEdit, editing, isSubmitting, labels.loadError, loadOptions, syncFetchedOptions, tags])
117
+
118
+ const cancelEditing = React.useCallback(() => {
119
+ setEditing(false)
120
+ setDraft([])
121
+ setError(null)
122
+ }, [])
123
+
124
+ const ensureTagOption = React.useCallback(
125
+ async (label: string): Promise<TagOption> => {
126
+ const normalized = label.trim()
127
+ if (!normalized.length) {
128
+ throw new Error(labels.labelRequired)
129
+ }
130
+ const existing = options.get(normalized.toLowerCase())
131
+ if (existing) return existing
132
+ try {
133
+ const created = await createTag(normalized)
134
+ setOptions((prev) => {
135
+ const next = new Map(prev)
136
+ next.set(created.label.toLowerCase(), created)
137
+ return next
138
+ })
139
+ return created
140
+ } catch (err) {
141
+ const message = err instanceof Error ? err.message : labels.createError
142
+ throw new Error(message)
143
+ }
144
+ },
145
+ [createTag, labels.createError, labels.labelRequired, options],
146
+ )
147
+
148
+ const handleSave = React.useCallback(async () => {
149
+ if (saving) return
150
+ const trimmed = draft.map((label) => label.trim()).filter((label) => label.length > 0)
151
+ const uniqueLabels = Array.from(new Set(trimmed.map((label) => label.toLowerCase())))
152
+
153
+ const currentIds = new Set(tags.map((tag) => tag.id))
154
+ const finalTagOptions: TagOption[] = []
155
+
156
+ setSaving(true)
157
+ setError(null)
158
+ try {
159
+ for (const normalized of uniqueLabels) {
160
+ const existing = options.get(normalized)
161
+ if (existing) {
162
+ finalTagOptions.push(existing)
163
+ continue
164
+ }
165
+ const matchingLabel = trimmed.find((label) => label.toLowerCase() === normalized) ?? normalized
166
+ const created = await ensureTagOption(matchingLabel)
167
+ finalTagOptions.push(created)
168
+ }
169
+
170
+ const finalIds = new Set(finalTagOptions.map((tag) => tag.id))
171
+ const added = finalTagOptions.filter((tag) => !currentIds.has(tag.id))
172
+ const removed = tags.filter((tag) => !finalIds.has(tag.id))
173
+
174
+ await onSave({ next: finalTagOptions, added, removed })
175
+
176
+ onChange?.(finalTagOptions)
177
+ setEditing(false)
178
+ setDraft([])
179
+ if (labels.success) flash(labels.success, 'success')
180
+ } catch (err) {
181
+ const message = err instanceof Error ? err.message : labels.updateError
182
+ setError(message)
183
+ flash(message, 'error')
184
+ } finally {
185
+ setSaving(false)
186
+ }
187
+ }, [draft, ensureTagOption, labels.success, labels.updateError, onChange, onSave, options, saving, tags])
188
+
189
+ const activeTags = editing ? draft : tags.map((tag) => tag.label)
190
+
191
+ const handleEditingKeyDown = React.useCallback(
192
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
193
+ if (!editing) return
194
+ if (event.key === 'Escape') {
195
+ event.preventDefault()
196
+ cancelEditing()
197
+ return
198
+ }
199
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
200
+ event.preventDefault()
201
+ if (saving || isSubmitting) return
202
+ void handleSave()
203
+ }
204
+ },
205
+ [cancelEditing, editing, handleSave, isSubmitting, saving],
206
+ )
207
+
208
+ const disableInteraction = isSubmitting || !canEdit
209
+
210
+ return (
211
+ <div className="space-y-3">
212
+ <div className="flex items-center justify-between group">
213
+ <h2 className="text-sm font-semibold">
214
+ {title}
215
+ </h2>
216
+ <Button
217
+ type="button"
218
+ variant="ghost"
219
+ size="icon"
220
+ onClick={editing ? cancelEditing : startEditing}
221
+ disabled={disableInteraction || saving}
222
+ className={
223
+ editing
224
+ ? 'opacity-100 transition-opacity duration-150'
225
+ : 'opacity-0 transition-opacity duration-150 group-hover:opacity-100 focus-visible:opacity-100'
226
+ }
227
+ >
228
+ {editing ? <X className="h-4 w-4" /> : <Pencil className="h-4 w-4" />}
229
+ <span className="sr-only">
230
+ {editing ? labels.cancel ?? 'Cancel' : labels.edit ?? 'Edit'}
231
+ </span>
232
+ </Button>
233
+ </div>
234
+
235
+ {editing ? (
236
+ <div className="rounded-lg border bg-card p-4 space-y-3">
237
+ <DataLoader
238
+ isLoading={loadingOptions}
239
+ loadingMessage={labels.loading}
240
+ spinnerSize="sm"
241
+ >
242
+ <div className="space-y-3" onKeyDown={handleEditingKeyDown}>
243
+ <TagsInput
244
+ value={activeTags}
245
+ onChange={(values) => setDraft(values)}
246
+ placeholder={labels.placeholder}
247
+ loadSuggestions={loadSuggestions}
248
+ autoFocus
249
+ />
250
+ {error ? <p className="text-xs text-red-600">{error}</p> : null}
251
+ <div className="flex items-center gap-2 mt-3 mb-2">
252
+ <Button type="button" size="sm" onClick={handleSave} disabled={saving || isSubmitting}>
253
+ {saving ? (
254
+ <span className="mr-2 h-4 w-4 animate-spin rounded-full border border-background border-t-primary" />
255
+ ) : null}
256
+ {labels.saveShortcut}
257
+ </Button>
258
+ <Button
259
+ type="button"
260
+ size="sm"
261
+ variant="ghost"
262
+ onClick={cancelEditing}
263
+ disabled={saving || isSubmitting}
264
+ >
265
+ {labels.cancelShortcut}
266
+ </Button>
267
+ </div>
268
+ </div>
269
+ </DataLoader>
270
+ </div>
271
+ ) : (
272
+ <div
273
+ className="group/tags relative rounded-lg border bg-muted/20 p-4 transition-colors hover:border-primary/40 focus-visible:border-primary focus-visible:outline-none"
274
+ role={disableInteraction ? undefined : 'button'}
275
+ tabIndex={disableInteraction ? -1 : 0}
276
+ onClick={disableInteraction ? undefined : startEditing}
277
+ onKeyDown={(event) => {
278
+ if (disableInteraction) return
279
+ if (event.key === 'Enter' || event.key === ' ') {
280
+ event.preventDefault()
281
+ void startEditing()
282
+ }
283
+ }}
284
+ >
285
+ <span
286
+ aria-hidden="true"
287
+ className="pointer-events-none absolute right-3 top-3 text-muted-foreground opacity-0 transition-opacity duration-150 group-hover/tags:opacity-100 group-focus-within/tags:opacity-100"
288
+ >
289
+ <Pencil className="h-4 w-4" />
290
+ </span>
291
+ {tags.length === 0 ? (
292
+ <p className="text-sm text-muted-foreground">
293
+ {labels.empty}
294
+ </p>
295
+ ) : (
296
+ <div className="flex flex-wrap gap-2">
297
+ {tags.map((tag) => (
298
+ <span
299
+ key={tag.id}
300
+ className="inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium"
301
+ style={tag.color ? { borderColor: tag.color, color: tag.color } : undefined}
302
+ >
303
+ {tag.label}
304
+ </span>
305
+ ))}
306
+ </div>
307
+ )}
308
+ </div>
309
+ )}
310
+ </div>
311
+ )
312
+ }
313
+
314
+ export default TagsSection
@@ -0,0 +1,121 @@
1
+ import * as React from 'react'
2
+
3
+ export type AddressFormatStrategy = 'line_first' | 'street_first'
4
+
5
+ export type AddressValue = {
6
+ addressLine1: string | null | undefined
7
+ addressLine2?: string | null
8
+ buildingNumber?: string | null
9
+ flatNumber?: string | null
10
+ city?: string | null
11
+ region?: string | null
12
+ postalCode?: string | null
13
+ country?: string | null
14
+ companyName?: string | null
15
+ }
16
+
17
+ export type AddressJsonShape = {
18
+ format: AddressFormatStrategy
19
+ companyName: string | null
20
+ addressLine1: string | null
21
+ addressLine2: string | null
22
+ buildingNumber: string | null
23
+ flatNumber: string | null
24
+ postalCode: string | null
25
+ city: string | null
26
+ region: string | null
27
+ country: string | null
28
+ }
29
+
30
+ function normalize(value: string | null | undefined): string | null {
31
+ if (typeof value !== 'string') return null
32
+ const trimmed = value.trim()
33
+ return trimmed.length ? trimmed : null
34
+ }
35
+
36
+ function mergeStreetLine(address: AddressValue): string | null {
37
+ const street = normalize(address.addressLine1)
38
+ const building = normalize(address.buildingNumber)
39
+ const flat = normalize(address.flatNumber)
40
+ if (!street && !building && !flat) return null
41
+ let line = street ?? ''
42
+ if (building) line = line ? `${line} ${building}` : building
43
+ if (flat) line = line ? `${line}/${flat}` : flat
44
+ return line.length ? line : null
45
+ }
46
+
47
+ export function formatAddressJson(address: AddressValue, format: AddressFormatStrategy): AddressJsonShape {
48
+ return {
49
+ format,
50
+ companyName: normalize(address.companyName),
51
+ addressLine1: normalize(address.addressLine1),
52
+ addressLine2: normalize(address.addressLine2),
53
+ buildingNumber: normalize(address.buildingNumber),
54
+ flatNumber: normalize(address.flatNumber),
55
+ postalCode: normalize(address.postalCode),
56
+ city: normalize(address.city),
57
+ region: normalize(address.region),
58
+ country: normalize(address.country),
59
+ }
60
+ }
61
+
62
+ export function formatAddressLines(address: AddressValue, format: AddressFormatStrategy): string[] {
63
+ const json = formatAddressJson(address, format)
64
+ const lines: string[] = []
65
+
66
+ if (json.companyName) lines.push(json.companyName)
67
+
68
+ if (format === 'street_first') {
69
+ const streetLine = mergeStreetLine(address)
70
+ if (streetLine) lines.push(streetLine)
71
+ const supplemental = normalize(address.addressLine2)
72
+ if (supplemental) lines.push(supplemental)
73
+ const postalCity = [json.postalCode, json.city].filter(Boolean).join(' ')
74
+ if (postalCity.length) lines.push(postalCity)
75
+ if (json.region) lines.push(json.region)
76
+ if (json.country) lines.push(json.country)
77
+ } else {
78
+ if (json.addressLine1) {
79
+ const baseLine1 = json.addressLine1
80
+ const appended = mergeStreetLine(address)
81
+ if (!json.buildingNumber && !json.flatNumber) {
82
+ lines.push(baseLine1)
83
+ } else {
84
+ const composite = appended ?? baseLine1
85
+ lines.push(composite)
86
+ }
87
+ }
88
+ if (json.addressLine2) lines.push(json.addressLine2)
89
+ const postalCity = [json.postalCode, json.city].filter(Boolean).join(' ')
90
+ if (postalCity.length) lines.push(postalCity)
91
+ if (json.region) lines.push(json.region)
92
+ if (json.country) lines.push(json.country)
93
+ }
94
+
95
+ return lines
96
+ }
97
+
98
+ export function formatAddressString(address: AddressValue, format: AddressFormatStrategy, separator = ', '): string {
99
+ return formatAddressLines(address, format).filter(Boolean).join(separator)
100
+ }
101
+
102
+ type AddressViewProps = {
103
+ address: AddressValue
104
+ format: AddressFormatStrategy
105
+ className?: string
106
+ lineClassName?: string
107
+ }
108
+
109
+ export function AddressView({ address, format, className, lineClassName }: AddressViewProps): React.ReactElement | null {
110
+ const lines = formatAddressLines(address, format)
111
+ if (!lines.length) return null
112
+ return (
113
+ <div className={className}>
114
+ {lines.map((line, index) => (
115
+ <div key={`${index}-${line}`} className={lineClassName}>
116
+ {line}
117
+ </div>
118
+ ))}
119
+ </div>
120
+ )
121
+ }
@@ -0,0 +1,44 @@
1
+ export * from './InlineEditors'
2
+ export * from './DetailFieldsSection'
3
+ export * from './LoadingMessage'
4
+ export * from './ErrorMessage'
5
+ export * from './TabEmptyState'
6
+ export * from './CustomDataSection'
7
+ export * from './TagsSection'
8
+ export { NotesSection, mapCommentSummary } from './NotesSection'
9
+ export type { NotesSectionProps, CommentSummary, SectionAction, TabEmptyStateConfig } from './NotesSection'
10
+ export { ActivitiesSection } from './ActivitiesSection'
11
+ export type {
12
+ ActivitiesSectionProps,
13
+ ActivitiesDataAdapter,
14
+ ActivitySummary,
15
+ ActivityCreatePayload,
16
+ ActivityUpdatePayload,
17
+ ActivityFormBaseValues,
18
+ ActivityFormSubmitPayload,
19
+ } from './ActivitiesSection'
20
+ export { AddressesSection } from './AddressesSection'
21
+ export type { AddressesSectionProps, AddressDataAdapter, AddressSummary } from './AddressesSection'
22
+ export { default as AddressTiles } from './AddressTiles'
23
+ export type { AddressInput, AddressValue as AddressTileValue, Translator as AddressTilesTranslator } from './AddressTiles'
24
+ export { default as AddressEditor } from './AddressEditor'
25
+ export type {
26
+ AddressTypeOption,
27
+ AddressTypesAdapter,
28
+ AddressEditorDraft,
29
+ AddressEditorField,
30
+ } from './AddressEditor'
31
+ export {
32
+ AddressView,
33
+ formatAddressJson,
34
+ formatAddressLines,
35
+ formatAddressString,
36
+ } from './addressFormat'
37
+ export type {
38
+ AddressFormatStrategy,
39
+ AddressJsonShape,
40
+ AddressValue as AddressFormatValue,
41
+ } from './addressFormat'
42
+ export * from './AttachmentMetadataDialog'
43
+ export * from './AttachmentDeleteDialog'
44
+ export * from './AttachmentsSection'
@@ -0,0 +1,8 @@
1
+ // Auto-registered field components loaded at runtime.
2
+ // This file can be regenerated by module generators. Keep imports side-effectful.
3
+ import '@open-mercato/core/modules/attachments/fields/attachment'
4
+ import '@open-mercato/core/modules/dictionaries/fields/dictionary'
5
+
6
+ export function loadAll() {
7
+ // No-op: imports above perform registration via side effects
8
+ }
@@ -0,0 +1,38 @@
1
+ import * as React from 'react'
2
+ import type { CrudCustomFieldRenderProps } from '../CrudForm'
3
+
4
+ export type FieldInputComponent = (props: CrudCustomFieldRenderProps & { def?: any }) => React.ReactNode
5
+ export type FieldDefEditorComponent = (props: { def: any; onChange: (patch: any) => void }) => React.ReactNode
6
+
7
+ type Entry = { input?: FieldInputComponent; defEditor?: FieldDefEditorComponent }
8
+
9
+ class FieldRegistryImpl {
10
+ private map = new Map<string, Entry>()
11
+
12
+ register(kind: string, entry: Entry) {
13
+ const k = kind.toLowerCase()
14
+ const prev = this.map.get(k) || {}
15
+ this.map.set(k, { ...prev, ...entry })
16
+ }
17
+
18
+ getInput(kind: string): FieldInputComponent | undefined {
19
+ return this.map.get(kind.toLowerCase())?.input
20
+ }
21
+
22
+ getDefEditor(kind: string): FieldDefEditorComponent | undefined {
23
+ return this.map.get(kind.toLowerCase())?.defEditor
24
+ }
25
+ }
26
+
27
+ export const FieldRegistry = new FieldRegistryImpl()
28
+
29
+ // Placeholder for generator to auto-register fields from modules
30
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
31
+ export async function loadGeneratedFieldRegistrations(): Promise<void> {
32
+ try {
33
+ const gen = await import('./registry.generated')
34
+ if (typeof gen.loadAll === 'function') gen.loadAll()
35
+ } catch {
36
+ // ignore when not present
37
+ }
38
+ }
@@ -0,0 +1,68 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import Link from 'next/link'
4
+ import { AlertTriangle, X } from 'lucide-react'
5
+ import { Button } from '../../primitives/button'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { dismissPartialIndexWarning, usePartialIndexWarning } from './store'
8
+
9
+ export function PartialIndexBanner() {
10
+ const t = useT()
11
+ const warning = usePartialIndexWarning()
12
+
13
+ if (!warning) return null
14
+
15
+ const entityLabel = warning.entityLabel || warning.entity
16
+ const base = warning.baseCount
17
+ const indexed = warning.indexedCount
18
+ const hasCounts = typeof base === 'number' && typeof indexed === 'number'
19
+
20
+ return (
21
+ <div className="mb-4 flex flex-col gap-3 rounded-md border border-amber-300 bg-amber-50 px-3 py-3 text-sm text-amber-900 md:flex-row md:items-center md:justify-between">
22
+ <div className="flex flex-col gap-2 text-sm">
23
+ <div className="flex items-center gap-2 font-medium text-amber-950">
24
+ <AlertTriangle className="size-4" aria-hidden="true" />
25
+ <span>{t('query_index.banner.partial_title')}</span>
26
+ </div>
27
+ <p className="text-amber-900">
28
+ {t('query_index.banner.partial_description')}
29
+ </p>
30
+ <p className="text-xs text-amber-900/90">
31
+ {t('query_index.banner.partial_entity', { entity: entityLabel })}
32
+ </p>
33
+ {hasCounts && (
34
+ <p className="text-xs text-amber-900/90">
35
+ {t('query_index.banner.partial_counts', { indexed, total: base })}
36
+ </p>
37
+ )}
38
+ {warning.scope === 'global' && (
39
+ <p className="text-xs text-amber-900/80">
40
+ {t('query_index.banner.partial_global_note')}
41
+ </p>
42
+ )}
43
+ </div>
44
+ <div className="flex flex-wrap items-center gap-2">
45
+ <Button
46
+ asChild
47
+ variant="outline"
48
+ size="sm"
49
+ className="border-amber-300 text-amber-900 hover:bg-amber-100"
50
+ >
51
+ <Link href="/backend/query-indexes">
52
+ {t('query_index.banner.manage_indexes')}
53
+ </Link>
54
+ </Button>
55
+ <Button
56
+ variant="ghost"
57
+ size="sm"
58
+ onClick={() => dismissPartialIndexWarning()}
59
+ className="text-amber-900 hover:bg-amber-100"
60
+ aria-label={t('query_index.banner.dismiss')}
61
+ >
62
+ <X className="mr-1 size-4" aria-hidden="true" />
63
+ {t('query_index.banner.dismiss')}
64
+ </Button>
65
+ </div>
66
+ </div>
67
+ )
68
+ }