@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,587 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Loader2, Pencil, Plus, Trash2, X } from 'lucide-react'
5
+ import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
7
+ import { TabEmptyState } from '@open-mercato/ui/backend/detail'
8
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
9
+ import { AddressView, formatAddressString, type AddressFormatStrategy } from './addressFormat'
10
+ import AddressEditor, { type AddressTypesAdapter } from './AddressEditor'
11
+ import {
12
+ Dialog,
13
+ DialogContent,
14
+ DialogDescription,
15
+ DialogFooter,
16
+ DialogHeader,
17
+ DialogTitle,
18
+ DialogTrigger,
19
+ } from '@open-mercato/ui/primitives/dialog'
20
+
21
+ export type Translator = (
22
+ key: string,
23
+ fallback?: string,
24
+ params?: Record<string, string | number>,
25
+ ) => string
26
+
27
+ export type AddressInput = {
28
+ name?: string
29
+ purpose?: string
30
+ companyName?: string
31
+ addressLine1: string
32
+ addressLine2?: string
33
+ buildingNumber?: string
34
+ flatNumber?: string
35
+ city?: string
36
+ region?: string
37
+ postalCode?: string
38
+ country?: string
39
+ isPrimary?: boolean
40
+ }
41
+
42
+ export type AddressValue = AddressInput & {
43
+ id: string
44
+ purpose?: string | null
45
+ companyName?: string | null
46
+ }
47
+
48
+ type AddressTilesProps<C = unknown> = {
49
+ addresses: AddressValue[]
50
+ onCreate: (payload: AddressInput) => Promise<void> | void
51
+ onUpdate?: (id: string, payload: AddressInput) => Promise<void> | void
52
+ onDelete?: (id: string) => Promise<void> | void
53
+ t: Translator
54
+ emptyLabel: string
55
+ isSubmitting?: boolean
56
+ gridClassName?: string
57
+ hideAddButton?: boolean
58
+ onAddActionChange?: (action: { openCreateForm: () => void; addDisabled: boolean } | null) => void
59
+ emptyStateTitle?: string
60
+ emptyStateActionLabel?: string
61
+ labelPrefix?: string
62
+ addressTypesAdapter?: AddressTypesAdapter<C>
63
+ addressTypesContext?: C
64
+ loadFormat?: (context?: C) => Promise<AddressFormatStrategy>
65
+ formatContext?: C
66
+ }
67
+
68
+ type DraftAddressState = {
69
+ name: string
70
+ purpose: string
71
+ companyName: string
72
+ addressLine1: string
73
+ addressLine2: string
74
+ buildingNumber: string
75
+ flatNumber: string
76
+ city: string
77
+ region: string
78
+ postalCode: string
79
+ country: string
80
+ isPrimary: boolean
81
+ }
82
+
83
+ type DraftFieldKey = keyof DraftAddressState
84
+
85
+ type AddressValidationDetail = {
86
+ path?: Array<string | number>
87
+ code?: string
88
+ message?: string
89
+ minimum?: number
90
+ maximum?: number
91
+ type?: string
92
+ }
93
+
94
+ const defaultDraft: DraftAddressState = {
95
+ name: '',
96
+ purpose: '',
97
+ companyName: '',
98
+ addressLine1: '',
99
+ addressLine2: '',
100
+ buildingNumber: '',
101
+ flatNumber: '',
102
+ city: '',
103
+ region: '',
104
+ postalCode: '',
105
+ country: '',
106
+ isPrimary: false,
107
+ }
108
+
109
+ const serverFieldMap: Record<string, DraftFieldKey> = {
110
+ name: 'name',
111
+ purpose: 'purpose',
112
+ companyName: 'companyName',
113
+ addressLine1: 'addressLine1',
114
+ addressLine2: 'addressLine2',
115
+ buildingNumber: 'buildingNumber',
116
+ flatNumber: 'flatNumber',
117
+ city: 'city',
118
+ region: 'region',
119
+ postalCode: 'postalCode',
120
+ country: 'country',
121
+ isPrimary: 'isPrimary',
122
+ }
123
+
124
+ function normalizeOptional(value: string): string | undefined {
125
+ const trimmed = value.trim()
126
+ return trimmed.length ? trimmed : undefined
127
+ }
128
+
129
+ function extractValidationDetails(error: unknown): AddressValidationDetail[] {
130
+ if (!error || typeof error !== 'object') return []
131
+ const candidate = (error as { details?: unknown }).details
132
+ if (!Array.isArray(candidate)) return []
133
+ return candidate
134
+ .map((entry) => (entry && typeof entry === 'object' ? (entry as AddressValidationDetail) : null))
135
+ .filter((entry): entry is AddressValidationDetail => entry !== null)
136
+ }
137
+
138
+ function resolveFieldMessage(detail: AddressValidationDetail, fieldLabel: string, t: Translator, prefix: string): string {
139
+ const label = (suffix: string, fallback: string) => t(`${prefix}.${suffix}`, fallback)
140
+ switch (detail.code) {
141
+ case 'invalid_type':
142
+ return label('validation.invalid', 'Invalid value for {{field}}').replace('{{field}}', fieldLabel)
143
+ case 'too_small':
144
+ if (detail.minimum === 1 && detail.type === 'string') {
145
+ return label('validation.required', '{{field}} is required').replace('{{field}}', fieldLabel)
146
+ }
147
+ return label('validation.generic', 'Invalid value for {{field}}').replace('{{field}}', fieldLabel)
148
+ case 'too_big':
149
+ if (typeof detail.maximum === 'number') {
150
+ return label('validation.tooLong', '{{field}} is too long').replace('{{field}}', fieldLabel)
151
+ .replace('{{max}}', `${detail.maximum}`)
152
+ }
153
+ return label('validation.generic', 'Invalid value for {{field}}').replace('{{field}}', fieldLabel)
154
+ default:
155
+ return label('validation.generic', 'Invalid value for {{field}}').replace('{{field}}', fieldLabel)
156
+ }
157
+ }
158
+
159
+ export function AddressTiles<C = unknown>({
160
+ addresses,
161
+ onCreate,
162
+ onUpdate,
163
+ onDelete,
164
+ t,
165
+ emptyLabel,
166
+ isSubmitting = false,
167
+ gridClassName = 'grid gap-4 min-[480px]:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4',
168
+ hideAddButton = false,
169
+ onAddActionChange,
170
+ emptyStateTitle,
171
+ emptyStateActionLabel,
172
+ labelPrefix = 'customers.people.detail.addresses',
173
+ addressTypesAdapter,
174
+ addressTypesContext,
175
+ loadFormat,
176
+ formatContext,
177
+ }: AddressTilesProps<C>) {
178
+ const scopeVersion = useOrganizationScopeVersion()
179
+ const [isFormOpen, setIsFormOpen] = React.useState(false)
180
+ const [editingId, setEditingId] = React.useState<string | null>(null)
181
+ const [draft, setDraft] = React.useState<DraftAddressState>(defaultDraft)
182
+ const [saving, setSaving] = React.useState(false)
183
+ const [deletingId, setDeletingId] = React.useState<string | null>(null)
184
+ const [generalError, setGeneralError] = React.useState<string | null>(null)
185
+ const [fieldErrors, setFieldErrors] = React.useState<Partial<Record<DraftFieldKey, string>>>({})
186
+ const [format, setFormat] = React.useState<AddressFormatStrategy>('line_first')
187
+ const [formatLoading, setFormatLoading] = React.useState(false)
188
+
189
+ const label = React.useCallback(
190
+ (suffix: string, fallback?: string, params?: Record<string, string | number>) =>
191
+ t(`${labelPrefix}.${suffix}`, fallback, params),
192
+ [labelPrefix, t],
193
+ )
194
+
195
+ const fieldLabels = React.useMemo(
196
+ () => ({
197
+ name: label('fields.label', 'Label'),
198
+ purpose: label('fields.type', 'Address type'),
199
+ companyName: label('fields.companyName', 'Company name'),
200
+ addressLine1: label('fields.line1', 'Address line 1'),
201
+ addressLine2: label('fields.line2', 'Address line 2'),
202
+ street: label('fields.street', 'Street'),
203
+ buildingNumber: label('fields.buildingNumber', 'Building number'),
204
+ flatNumber: label('fields.flatNumber', 'Flat number'),
205
+ city: label('fields.city', 'City'),
206
+ region: label('fields.region', 'Region'),
207
+ postalCode: label('fields.postalCode', 'Postal code'),
208
+ country: label('fields.country', 'Country'),
209
+ isPrimary: label('fields.primary', 'Primary address'),
210
+ }),
211
+ [label],
212
+ )
213
+
214
+ const resetForm = React.useCallback(() => {
215
+ setDraft(defaultDraft)
216
+ setFieldErrors({})
217
+ setGeneralError(null)
218
+ setEditingId(null)
219
+ }, [])
220
+
221
+ React.useEffect(() => {
222
+ let cancelled = false
223
+ async function loadFormatValue() {
224
+ if (!loadFormat) {
225
+ setFormat('line_first')
226
+ setFormatLoading(false)
227
+ return
228
+ }
229
+ setFormatLoading(true)
230
+ try {
231
+ const value = await loadFormat(formatContext)
232
+ if (!cancelled && (value === 'street_first' || value === 'line_first')) {
233
+ setFormat(value)
234
+ }
235
+ } catch (err) {
236
+ if (!cancelled) {
237
+ const message =
238
+ err instanceof Error && err.message
239
+ ? err.message
240
+ : label('formatLoadError', 'Failed to load address configuration')
241
+ flash(message, 'error')
242
+ }
243
+ } finally {
244
+ if (!cancelled) setFormatLoading(false)
245
+ }
246
+ }
247
+ void loadFormatValue()
248
+ return () => {
249
+ cancelled = true
250
+ }
251
+ }, [formatContext, label, loadFormat, scopeVersion])
252
+
253
+ const openCreateForm = React.useCallback(() => {
254
+ resetForm()
255
+ setIsFormOpen(true)
256
+ }, [resetForm])
257
+
258
+ const handleCancel = React.useCallback(() => {
259
+ resetForm()
260
+ setIsFormOpen(false)
261
+ }, [resetForm])
262
+
263
+ const handleEdit = React.useCallback((value: AddressValue) => {
264
+ setDraft({
265
+ name: value.name ?? '',
266
+ purpose: value.purpose ?? '',
267
+ companyName: value.companyName ?? '',
268
+ addressLine1: value.addressLine1 ?? '',
269
+ addressLine2: value.addressLine2 ?? '',
270
+ buildingNumber: value.buildingNumber ?? '',
271
+ flatNumber: value.flatNumber ?? '',
272
+ city: value.city ?? '',
273
+ region: value.region ?? '',
274
+ postalCode: value.postalCode ?? '',
275
+ country: value.country ?? '',
276
+ isPrimary: value.isPrimary ?? false,
277
+ })
278
+ setEditingId(value.id)
279
+ setIsFormOpen(true)
280
+ setFieldErrors({})
281
+ setGeneralError(null)
282
+ }, [])
283
+
284
+ const validate = React.useCallback((): boolean => {
285
+ const errors: Partial<Record<DraftFieldKey, string>> = {}
286
+ if (!draft.addressLine1.trim()) {
287
+ errors.addressLine1 = label('validation.required', '{{field}} is required').replace('{{field}}', fieldLabels.addressLine1)
288
+ }
289
+ if (Object.keys(errors).length > 0) {
290
+ setFieldErrors(errors)
291
+ return false
292
+ }
293
+ return true
294
+ }, [draft.addressLine1, fieldLabels.addressLine1, label])
295
+
296
+ const handleSave = React.useCallback(async () => {
297
+ if (!validate()) return
298
+ setSaving(true)
299
+ setGeneralError(null)
300
+ try {
301
+ const payload: AddressInput = {
302
+ name: normalizeOptional(draft.name),
303
+ purpose: normalizeOptional(draft.purpose),
304
+ companyName: normalizeOptional(draft.companyName),
305
+ addressLine1: draft.addressLine1.trim(),
306
+ addressLine2: normalizeOptional(draft.addressLine2),
307
+ buildingNumber: normalizeOptional(draft.buildingNumber),
308
+ flatNumber: normalizeOptional(draft.flatNumber),
309
+ city: normalizeOptional(draft.city),
310
+ region: normalizeOptional(draft.region),
311
+ postalCode: normalizeOptional(draft.postalCode),
312
+ country: normalizeOptional(draft.country)?.toUpperCase(),
313
+ isPrimary: draft.isPrimary,
314
+ }
315
+ if (editingId && onUpdate) {
316
+ await onUpdate(editingId, payload)
317
+ } else {
318
+ await onCreate(payload)
319
+ }
320
+ resetForm()
321
+ setIsFormOpen(false)
322
+ } catch (err) {
323
+ const details = extractValidationDetails(err)
324
+ if (details.length) {
325
+ const nextErrors: Partial<Record<DraftFieldKey, string>> = {}
326
+ details.forEach((detail) => {
327
+ const path = Array.isArray(detail.path) ? detail.path : []
328
+ const key = typeof path[0] === 'string' ? path[0] : undefined
329
+ if (!key) return
330
+ const fieldKey = serverFieldMap[key]
331
+ if (!fieldKey) return
332
+ const fieldLabel = fieldLabels[fieldKey] ?? key
333
+ nextErrors[fieldKey] = resolveFieldMessage(detail, fieldLabel, t, labelPrefix)
334
+ })
335
+ setFieldErrors(nextErrors)
336
+ setGeneralError(label('validation.summary', 'Please fix the highlighted fields.'))
337
+ return
338
+ }
339
+ const message =
340
+ err instanceof Error && err.message
341
+ ? err.message
342
+ : label('error', 'Failed to save address')
343
+ setGeneralError(message)
344
+ flash(message, 'error')
345
+ } finally {
346
+ setSaving(false)
347
+ }
348
+ }, [draft, editingId, fieldLabels, label, labelPrefix, onCreate, onUpdate, resetForm, t, validate])
349
+
350
+ const handleDelete = React.useCallback(
351
+ async (id: string) => {
352
+ if (!onDelete) return
353
+ setDeletingId(id)
354
+ try {
355
+ await onDelete(id)
356
+ if (editingId === id) {
357
+ resetForm()
358
+ setIsFormOpen(false)
359
+ }
360
+ } catch (err) {
361
+ const message =
362
+ err instanceof Error && err.message
363
+ ? err.message
364
+ : label('error', 'Failed to delete address')
365
+ flash(message, 'error')
366
+ } finally {
367
+ setDeletingId(null)
368
+ }
369
+ },
370
+ [editingId, label, onDelete, resetForm]
371
+ )
372
+
373
+ const disableActions = saving || isSubmitting || deletingId !== null
374
+ const isEditing = editingId !== null
375
+ const addDisabled = disableActions || isEditing
376
+ const hasAddresses = addresses.length > 0
377
+ const emptyTitle = emptyStateTitle ?? emptyLabel
378
+ const emptyActionLabel = emptyStateActionLabel ?? label('add', 'Add address')
379
+
380
+ React.useEffect(() => {
381
+ if (!onAddActionChange) return
382
+ onAddActionChange({ openCreateForm, addDisabled })
383
+ }, [onAddActionChange, openCreateForm, addDisabled])
384
+
385
+ React.useEffect(
386
+ () => () => {
387
+ if (onAddActionChange) onAddActionChange(null)
388
+ },
389
+ [onAddActionChange]
390
+ )
391
+
392
+ const renderFormTile = React.useCallback(
393
+ (key: string) => (
394
+ <div
395
+ key={key}
396
+ className="rounded-lg border-2 border-dashed border-muted-foreground/50 bg-muted/20 p-4 text-sm"
397
+ onKeyDown={(event) => {
398
+ if (!(event.metaKey || event.ctrlKey)) return
399
+ if (event.key !== 'Enter') return
400
+ event.preventDefault()
401
+ if (disableActions) return
402
+ void handleSave()
403
+ }}
404
+ >
405
+ <div className="flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-muted-foreground">
406
+ <span>
407
+ {editingId
408
+ ? label('editTitle', 'Edit address')
409
+ : label('addTitle', 'Add address')}
410
+ </span>
411
+ <Button type="button" variant="ghost" size="icon" onClick={handleCancel} disabled={disableActions}>
412
+ <X className="h-4 w-4" />
413
+ </Button>
414
+ </div>
415
+ <div className="mt-3 space-y-3">
416
+ {formatLoading ? (
417
+ <p className="text-xs text-muted-foreground">
418
+ {label('formatLoading', 'Loading address preferences…')}
419
+ </p>
420
+ ) : null}
421
+ <AddressEditor
422
+ value={draft}
423
+ onChange={(next) => {
424
+ setDraft(next)
425
+ if (Object.keys(fieldErrors).length) {
426
+ const nextErrors = { ...fieldErrors }
427
+ ;(Object.keys(nextErrors) as DraftFieldKey[]).forEach((key) => {
428
+ const candidate = (next as Record<string, unknown>)[key]
429
+ if (candidate !== undefined && candidate !== null && `${candidate}`.length) {
430
+ delete nextErrors[key]
431
+ }
432
+ })
433
+ setFieldErrors(nextErrors)
434
+ }
435
+ }}
436
+ format={format}
437
+ t={t}
438
+ disabled={disableActions}
439
+ errors={fieldErrors}
440
+ showFormatHint={!formatLoading}
441
+ labelPrefix={labelPrefix}
442
+ addressTypesAdapter={addressTypesAdapter}
443
+ addressTypesContext={addressTypesContext}
444
+ />
445
+ {generalError ? <p className="text-xs text-red-600">{generalError}</p> : null}
446
+ <div className="flex flex-wrap justify-end gap-2">
447
+ <Button type="button" variant="outline" onClick={handleCancel} disabled={disableActions}>
448
+ {label('cancel', 'Cancel')}
449
+ </Button>
450
+ <Button type="button" onClick={handleSave} disabled={disableActions}>
451
+ {saving ? (
452
+ <>
453
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
454
+ {editingId
455
+ ? label('updating', 'Updating…')
456
+ : label('saving', 'Saving…')}
457
+ </>
458
+ ) : editingId ? (
459
+ label('update', 'Update address')
460
+ ) : (
461
+ label('save', 'Save address')
462
+ )}
463
+ </Button>
464
+ </div>
465
+ </div>
466
+ </div>
467
+ ),
468
+ [
469
+ addressTypesAdapter,
470
+ addressTypesContext,
471
+ disableActions,
472
+ draft,
473
+ editingId,
474
+ fieldErrors,
475
+ format,
476
+ formatLoading,
477
+ handleCancel,
478
+ handleSave,
479
+ generalError,
480
+ label,
481
+ labelPrefix,
482
+ saving,
483
+ t,
484
+ ]
485
+ )
486
+
487
+ return (
488
+ <div className="space-y-4">
489
+ {!hideAddButton ? (
490
+ <div className="flex justify-end">
491
+ <Button
492
+ type="button"
493
+ variant="outline"
494
+ size="sm"
495
+ onClick={openCreateForm}
496
+ disabled={addDisabled}
497
+ >
498
+ <Plus className="mr-2 h-4 w-4" />
499
+ {label('add', 'Add address')}
500
+ </Button>
501
+ </div>
502
+ ) : null}
503
+ {hasAddresses ? (
504
+ <div className={gridClassName}>
505
+ {addresses.map((address) => {
506
+ if (isFormOpen && editingId === address.id) {
507
+ return renderFormTile(`form-${address.id}`)
508
+ }
509
+ const isDeleting = deletingId === address.id
510
+ return (
511
+ <div
512
+ key={address.id}
513
+ className="group rounded-lg border border-border/60 bg-card p-4 text-sm transition hover:border-border"
514
+ >
515
+ <div className="flex items-start justify-between gap-2">
516
+ <div className="space-y-1">
517
+ <div className="flex flex-wrap items-center gap-2">
518
+ <p className="text-sm font-semibold text-foreground">
519
+ {address.name ?? label('labelFallback', 'Address')}
520
+ </p>
521
+ {address.isPrimary ? (
522
+ <span className="rounded-full border border-border bg-muted px-2 py-0.5 text-[10px] font-semibold uppercase">
523
+ {label('primaryBadge', 'Primary')}
524
+ </span>
525
+ ) : null}
526
+ </div>
527
+ {address.purpose ? (
528
+ <p className="text-xs text-muted-foreground">
529
+ {address.purpose}
530
+ </p>
531
+ ) : null}
532
+ <AddressView address={address} format={format} className="text-sm text-foreground" />
533
+ <p className="text-xs text-muted-foreground">
534
+ {formatAddressString(address, format)}
535
+ </p>
536
+ </div>
537
+ <div className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100">
538
+ <Button
539
+ type="button"
540
+ variant="ghost"
541
+ size="icon"
542
+ onClick={() => handleEdit(address)}
543
+ disabled={disableActions}
544
+ >
545
+ <Pencil className="h-4 w-4" />
546
+ </Button>
547
+ <Button
548
+ type="button"
549
+ variant="ghost"
550
+ size="icon"
551
+ onClick={() => handleDelete(address.id)}
552
+ disabled={disableActions}
553
+ >
554
+ {isDeleting ? (
555
+ <span className="relative flex h-4 w-4 items-center justify-center text-destructive">
556
+ <span className="absolute h-4 w-4 animate-spin rounded-full border border-destructive border-t-transparent" />
557
+ </span>
558
+ ) : (
559
+ <Trash2 className="h-4 w-4" />
560
+ )}
561
+ </Button>
562
+ </div>
563
+ </div>
564
+ </div>
565
+ )
566
+ })}
567
+ {isFormOpen && !editingId ? renderFormTile('create') : null}
568
+ </div>
569
+ ) : isFormOpen ? (
570
+ <div className={gridClassName}>
571
+ {renderFormTile('create')}
572
+ </div>
573
+ ) : (
574
+ <TabEmptyState
575
+ title={emptyTitle}
576
+ action={{
577
+ label: emptyActionLabel,
578
+ onClick: openCreateForm,
579
+ disabled: addDisabled,
580
+ }}
581
+ />
582
+ )}
583
+ </div>
584
+ )
585
+ }
586
+
587
+ export default AddressTiles