@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,88 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+
4
+ export type PartialIndexNotice = {
5
+ entity: string
6
+ entityLabel: string
7
+ baseCount: number | null
8
+ indexedCount: number | null
9
+ scope: 'scoped' | 'global'
10
+ receivedAt: number
11
+ }
12
+
13
+ type PartialIndexInput = {
14
+ entity: string
15
+ entityLabel?: string
16
+ baseCount?: number | null
17
+ indexedCount?: number | null
18
+ scope?: 'scoped' | 'global'
19
+ }
20
+
21
+ const TTL_MS = 120_000
22
+
23
+ let current: PartialIndexNotice | null = null
24
+ const emitter = new EventTarget()
25
+
26
+ function now(): number {
27
+ return typeof performance !== 'undefined' && performance.now
28
+ ? Math.round(performance.timeOrigin + performance.now())
29
+ : Date.now()
30
+ }
31
+
32
+ function subscribe(listener: () => void) {
33
+ const wrapped = () => listener()
34
+ emitter.addEventListener('change', wrapped)
35
+ return () => emitter.removeEventListener('change', wrapped)
36
+ }
37
+
38
+ function emit() {
39
+ emitter.dispatchEvent(new Event('change'))
40
+ }
41
+
42
+ function normalizeInput(input: PartialIndexInput): PartialIndexNotice {
43
+ const label = typeof input.entityLabel === 'string' && input.entityLabel.trim()
44
+ ? input.entityLabel.trim()
45
+ : input.entity
46
+ return {
47
+ entity: input.entity,
48
+ entityLabel: label,
49
+ baseCount: typeof input.baseCount === 'number' ? input.baseCount : null,
50
+ indexedCount: typeof input.indexedCount === 'number' ? input.indexedCount : null,
51
+ scope: input.scope === 'global' ? 'global' : 'scoped',
52
+ receivedAt: now(),
53
+ }
54
+ }
55
+
56
+ function prune(stale: PartialIndexNotice | null): PartialIndexNotice | null {
57
+ if (!stale) return null
58
+ const age = now() - stale.receivedAt
59
+ if (!Number.isFinite(age) || age > TTL_MS) return null
60
+ return stale
61
+ }
62
+
63
+ function getSnapshot(): PartialIndexNotice | null {
64
+ current = prune(current)
65
+ return current
66
+ }
67
+
68
+ export function usePartialIndexWarning(): PartialIndexNotice | null {
69
+ return React.useSyncExternalStore(
70
+ subscribe,
71
+ () => getSnapshot(),
72
+ () => null,
73
+ )
74
+ }
75
+
76
+ export function pushPartialIndexWarning(input: PartialIndexInput) {
77
+ if (typeof window === 'undefined') return
78
+ if (!input.entity) return
79
+ const next = normalizeInput(input)
80
+ current = next
81
+ emit()
82
+ }
83
+
84
+ export function dismissPartialIndexWarning() {
85
+ if (typeof window === 'undefined') return
86
+ current = null
87
+ emit()
88
+ }
@@ -0,0 +1,236 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import type {
4
+ InjectionSpotId,
5
+ InjectionWidgetModule,
6
+ WidgetInjectionEventHandlers,
7
+ WidgetBeforeSaveResult,
8
+ } from '@open-mercato/shared/modules/widgets/injection'
9
+ import { loadInjectionWidgetsForSpot, type LoadedInjectionWidget } from '@open-mercato/shared/modules/widgets/injection-loader'
10
+
11
+ export type InjectionSpotProps<TContext = unknown, TData = unknown> = {
12
+ spotId: InjectionSpotId
13
+ context: TContext
14
+ data?: TData
15
+ onDataChange?: (data: TData) => void
16
+ disabled?: boolean
17
+ onEvent?: (event: 'onLoad' | 'onBeforeSave' | 'onSave' | 'onAfterSave', widgetId: string) => void
18
+ widgetsOverride?: LoadedWidget[]
19
+ }
20
+
21
+ type LoadedWidget = {
22
+ widgetId: string
23
+ module: InjectionWidgetModule<any, any>
24
+ moduleId: string
25
+ key: string
26
+ placement?: LoadedInjectionWidget['placement']
27
+ }
28
+
29
+ export function useInjectionWidgets<TContext = unknown>(
30
+ spotId: InjectionSpotId | null | undefined,
31
+ options?: {
32
+ context?: TContext
33
+ triggerOnLoad?: boolean
34
+ onEvent?: (event: 'onLoad', widgetId: string) => void
35
+ }
36
+ ) {
37
+ const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])
38
+ const [loading, setLoading] = React.useState(true)
39
+ const [error, setError] = React.useState<string | null>(null)
40
+ const loadedRef = React.useRef(false)
41
+
42
+ React.useEffect(() => {
43
+ if (!spotId) {
44
+ setWidgets([])
45
+ setLoading(false)
46
+ setError(null)
47
+ return
48
+ }
49
+ let mounted = true
50
+ const load = async () => {
51
+ try {
52
+ setLoading(true)
53
+ setError(null)
54
+ const loaded = await loadInjectionWidgetsForSpot(spotId)
55
+ if (!mounted) return
56
+ const widgetList: LoadedWidget[] = loaded.map((w) => ({
57
+ widgetId: w.metadata.id,
58
+ module: w,
59
+ moduleId: w.moduleId,
60
+ key: w.key,
61
+ placement: w.placement,
62
+ }))
63
+ setWidgets(widgetList)
64
+
65
+ // Trigger onLoad for all widgets
66
+ if (!loadedRef.current && options?.triggerOnLoad) {
67
+ loadedRef.current = true
68
+ for (const widget of widgetList) {
69
+ if (widget.module.eventHandlers?.onLoad) {
70
+ try {
71
+ await widget.module.eventHandlers.onLoad(options.context as TContext)
72
+ options.onEvent?.('onLoad', widget.widgetId)
73
+ } catch (err) {
74
+ console.error(`[InjectionSpot] Error in onLoad for widget ${widget.widgetId}:`, err)
75
+ }
76
+ }
77
+ }
78
+ }
79
+ } catch (err) {
80
+ if (!mounted) return
81
+ console.error(`[InjectionSpot] Failed to load widgets for spot ${spotId}:`, err)
82
+ setError(err instanceof Error ? err.message : String(err))
83
+ } finally {
84
+ if (mounted) setLoading(false)
85
+ }
86
+ }
87
+ load()
88
+ return () => {
89
+ mounted = false
90
+ }
91
+ }, [spotId, options?.context, options?.triggerOnLoad, options?.onEvent])
92
+
93
+ return { widgets, loading, error }
94
+ }
95
+
96
+ export function InjectionSpot<TContext = unknown, TData = unknown>({
97
+ spotId,
98
+ context,
99
+ data,
100
+ onDataChange,
101
+ disabled,
102
+ onEvent,
103
+ widgetsOverride,
104
+ }: InjectionSpotProps<TContext, TData>) {
105
+ const useSpotId = widgetsOverride ? null : spotId
106
+ const { widgets, loading, error } = useInjectionWidgets<TContext>(useSpotId, {
107
+ context,
108
+ triggerOnLoad: !widgetsOverride,
109
+ onEvent: onEvent ? (event, id) => onEvent(event, id) : undefined,
110
+ })
111
+ const effectiveWidgets = widgetsOverride ?? widgets
112
+ const effectiveLoading = widgetsOverride ? false : loading
113
+ const effectiveError = widgetsOverride ? null : error
114
+
115
+ if (effectiveLoading) {
116
+ return null
117
+ }
118
+
119
+ if (effectiveError) {
120
+ console.error(`[InjectionSpot] Error loading widgets for spot ${spotId}:`, effectiveError)
121
+ return null
122
+ }
123
+
124
+ if (effectiveWidgets.length === 0) {
125
+ return null
126
+ }
127
+
128
+ return (
129
+ <>
130
+ {effectiveWidgets.map((widget) => {
131
+ const { Widget } = widget.module
132
+ return (
133
+ <Widget
134
+ key={widget.widgetId}
135
+ context={context}
136
+ data={data}
137
+ onDataChange={onDataChange}
138
+ disabled={disabled}
139
+ />
140
+ )
141
+ })}
142
+ </>
143
+ )
144
+ }
145
+
146
+ /**
147
+ * Hook to trigger injection widget events imperatively
148
+ */
149
+ export function useInjectionSpotEvents<TContext = unknown, TData = unknown>(spotId: InjectionSpotId, prefetchedWidgets?: LoadedWidget[]) {
150
+ const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])
151
+
152
+ React.useEffect(() => {
153
+ if (prefetchedWidgets && prefetchedWidgets.length) {
154
+ setWidgets(prefetchedWidgets)
155
+ return
156
+ }
157
+ let mounted = true
158
+ const load = async () => {
159
+ try {
160
+ const loaded = await loadInjectionWidgetsForSpot(spotId)
161
+ if (!mounted) return
162
+ setWidgets(
163
+ loaded.map((w) => ({
164
+ widgetId: w.metadata.id,
165
+ module: w,
166
+ moduleId: w.moduleId,
167
+ key: w.key,
168
+ placement: w.placement,
169
+ }))
170
+ )
171
+ } catch (err) {
172
+ console.error(`[useInjectionSpotEvents] Failed to load widgets for spot ${spotId}:`, err)
173
+ }
174
+ }
175
+ load()
176
+ return () => {
177
+ mounted = false
178
+ }
179
+ }, [spotId, prefetchedWidgets])
180
+
181
+ const triggerEvent = React.useCallback(
182
+ async (
183
+ event: keyof WidgetInjectionEventHandlers<TContext, TData>,
184
+ data: TData,
185
+ context: TContext
186
+ ): Promise<{ ok: boolean; message?: string; fieldErrors?: Record<string, string> }> => {
187
+ const normalizeBeforeSave = (result: WidgetBeforeSaveResult): { ok: boolean; message?: string; fieldErrors?: Record<string, string> } => {
188
+ if (result === false) return { ok: false }
189
+ if (result === true || typeof result === 'undefined') return { ok: true }
190
+ if (result && typeof result === 'object') {
191
+ const ok = typeof result.ok === 'boolean' ? result.ok : true
192
+ const message = typeof result.message === 'string' ? result.message : undefined
193
+ const fieldErrors =
194
+ result.fieldErrors && typeof result.fieldErrors === 'object'
195
+ ? Object.fromEntries(
196
+ Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),
197
+ )
198
+ : undefined
199
+ return { ok, message, fieldErrors }
200
+ }
201
+ return { ok: true }
202
+ }
203
+
204
+ for (const widget of widgets) {
205
+ const handler = widget.module.eventHandlers?.[event]
206
+ if (handler) {
207
+ try {
208
+ const result = await (handler as any)(data, context)
209
+ if (event === 'onBeforeSave') {
210
+ const normalized = normalizeBeforeSave(result as WidgetBeforeSaveResult)
211
+ if (!normalized.ok) {
212
+ console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)
213
+ return normalized
214
+ }
215
+ }
216
+ } catch (err) {
217
+ console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)
218
+ if (event === 'onBeforeSave') {
219
+ const message =
220
+ err instanceof Error
221
+ ? err.message || 'Validation blocked'
222
+ : typeof err === 'string'
223
+ ? err
224
+ : undefined
225
+ return { ok: false, message }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ return { ok: true }
231
+ },
232
+ [widgets]
233
+ )
234
+
235
+ return { triggerEvent, widgets }
236
+ }
@@ -0,0 +1,31 @@
1
+ import type { ReactNode } from 'react'
2
+ import { InjectionSpot } from './InjectionSpot'
3
+
4
+ function normalizePath(path: string): string {
5
+ const trimmed = path.replace(/\?.*$/, '').replace(/\/+$/, '')
6
+ const withoutLeading = trimmed.startsWith('/') ? trimmed.slice(1) : trimmed
7
+ const safe = withoutLeading.length ? withoutLeading : 'root'
8
+ return safe.replace(/[^a-zA-Z0-9]+/g, ':')
9
+ }
10
+
11
+ export function PageInjectionBoundary({
12
+ path,
13
+ context,
14
+ children,
15
+ }: {
16
+ path: string
17
+ context?: Record<string, unknown>
18
+ children: ReactNode
19
+ }) {
20
+ const handle = normalizePath(path || '/')
21
+ const beforeSpotId = `admin.page:${handle}:before`
22
+ const afterSpotId = `admin.page:${handle}:after`
23
+
24
+ return (
25
+ <>
26
+ <InjectionSpot spotId={beforeSpotId} context={context ?? { path }} />
27
+ {children}
28
+ <InjectionSpot spotId={afterSpotId} context={context ?? { path }} />
29
+ </>
30
+ )
31
+ }
@@ -0,0 +1,35 @@
1
+ import type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'
2
+
3
+ /**
4
+ * Generate a standard injection spot ID for CRUD forms
5
+ * @param formName The name/identifier of the form (e.g., 'catalog.product', 'catalog.variant')
6
+ * @returns A standardized injection spot ID
7
+ */
8
+ export function generateCrudFormInjectionSpotId(formName: string): InjectionSpotId {
9
+ return `crud-form:${formName}`
10
+ }
11
+
12
+ /**
13
+ * Generate injection spot IDs for common CRUD form locations
14
+ */
15
+ export const CrudFormInjectionSpots = {
16
+ /**
17
+ * Generate injection spot ID for before the form fields
18
+ */
19
+ beforeFields: (formName: string): InjectionSpotId => `${generateCrudFormInjectionSpotId(formName)}:before-fields`,
20
+
21
+ /**
22
+ * Generate injection spot ID for after the form fields
23
+ */
24
+ afterFields: (formName: string): InjectionSpotId => `${generateCrudFormInjectionSpotId(formName)}:after-fields`,
25
+
26
+ /**
27
+ * Generate injection spot ID for the header area
28
+ */
29
+ header: (formName: string): InjectionSpotId => `${generateCrudFormInjectionSpotId(formName)}:header`,
30
+
31
+ /**
32
+ * Generate injection spot ID for the footer/actions area
33
+ */
34
+ footer: (formName: string): InjectionSpotId => `${generateCrudFormInjectionSpotId(formName)}:footer`,
35
+ }
@@ -0,0 +1,68 @@
1
+ import type { ModuleInjectionWidgetEntry } from '@open-mercato/shared/modules/registry'
2
+ import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
3
+
4
+ type Entry = ModuleInjectionWidgetEntry
5
+
6
+ // Registration pattern for publishable packages
7
+ let _injectionWidgetEntries: Entry[] | null = null
8
+
9
+ export function registerInjectionWidgets(entries: Entry[]) {
10
+ if (_injectionWidgetEntries !== null && process.env.NODE_ENV === 'development') {
11
+ console.debug('[Bootstrap] Injection widgets re-registered (this may occur during HMR)')
12
+ }
13
+ _injectionWidgetEntries = entries
14
+ }
15
+
16
+ export function getInjectionWidgets(): Entry[] {
17
+ if (!_injectionWidgetEntries) {
18
+ // On client-side, bootstrap doesn't run - return empty array gracefully
19
+ if (typeof window !== 'undefined') {
20
+ return []
21
+ }
22
+ throw new Error('[Bootstrap] Injection widgets not registered. Call registerInjectionWidgets() at bootstrap.')
23
+ }
24
+ return _injectionWidgetEntries
25
+ }
26
+
27
+ let entriesPromise: Promise<Entry[]> | null = null
28
+
29
+ async function getEntries(): Promise<Entry[]> {
30
+ if (!entriesPromise) {
31
+ const promise = Promise.resolve().then(() => getInjectionWidgets())
32
+ entriesPromise = promise.catch((err) => {
33
+ // Clear cache on error so next call can retry after registration
34
+ if (entriesPromise === promise) {
35
+ entriesPromise = null
36
+ }
37
+ throw err
38
+ })
39
+ }
40
+ return entriesPromise
41
+ }
42
+
43
+ type LoadedWidgetModule = InjectionWidgetModule<any, any>
44
+
45
+ const cache = new Map<string, Promise<LoadedWidgetModule>>()
46
+
47
+ async function findEntry(loaderKey: string): Promise<Entry | undefined> {
48
+ const entries = await getEntries()
49
+ return entries.find((entry) => entry.key === loaderKey)
50
+ }
51
+
52
+ export async function loadInjectionWidgetModule(loaderKey: string): Promise<LoadedWidgetModule | null> {
53
+ const entry = await findEntry(loaderKey)
54
+ if (!entry) return null
55
+ if (!cache.has(loaderKey)) {
56
+ cache.set(
57
+ loaderKey,
58
+ entry
59
+ .loader()
60
+ .then((mod) => {
61
+ const candidate = mod as LoadedWidgetModule
62
+ const maybeDefault = (mod as { default?: LoadedWidgetModule }).default
63
+ return maybeDefault ?? candidate
64
+ })
65
+ )
66
+ }
67
+ return cache.get(loaderKey) ?? null
68
+ }