@open-mercato/ui 0.4.5-develop-03023b2707 → 0.4.5-develop-0c30cb4b11

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 (54) hide show
  1. package/AGENTS.md +8 -0
  2. package/dist/backend/AppShell.js +395 -134
  3. package/dist/backend/AppShell.js.map +2 -2
  4. package/dist/backend/CrudForm.js +232 -21
  5. package/dist/backend/CrudForm.js.map +2 -2
  6. package/dist/backend/ProfileDropdown.js +214 -94
  7. package/dist/backend/ProfileDropdown.js.map +2 -2
  8. package/dist/backend/injection/InjectionSpot.js +74 -4
  9. package/dist/backend/injection/InjectionSpot.js.map +2 -2
  10. package/dist/backend/injection/SseEventIndicator.js +16 -0
  11. package/dist/backend/injection/SseEventIndicator.js.map +7 -0
  12. package/dist/backend/injection/WidgetSharedState.js +49 -0
  13. package/dist/backend/injection/WidgetSharedState.js.map +7 -0
  14. package/dist/backend/injection/eventBridge.js +105 -0
  15. package/dist/backend/injection/eventBridge.js.map +7 -0
  16. package/dist/backend/injection/mergeMenuItems.js +43 -0
  17. package/dist/backend/injection/mergeMenuItems.js.map +7 -0
  18. package/dist/backend/injection/resolveInjectedIcon.js +23 -0
  19. package/dist/backend/injection/resolveInjectedIcon.js.map +7 -0
  20. package/dist/backend/injection/spotIds.js +40 -1
  21. package/dist/backend/injection/spotIds.js.map +2 -2
  22. package/dist/backend/injection/useAppEvent.js +35 -0
  23. package/dist/backend/injection/useAppEvent.js.map +7 -0
  24. package/dist/backend/injection/useInjectedMenuItems.js +92 -0
  25. package/dist/backend/injection/useInjectedMenuItems.js.map +7 -0
  26. package/dist/backend/injection/useInjectionDataWidgets.js +36 -0
  27. package/dist/backend/injection/useInjectionDataWidgets.js.map +7 -0
  28. package/dist/backend/injection/useOperationProgress.js +64 -0
  29. package/dist/backend/injection/useOperationProgress.js.map +7 -0
  30. package/dist/backend/injection/useWidgetSharedState.js +26 -0
  31. package/dist/backend/injection/useWidgetSharedState.js.map +7 -0
  32. package/dist/backend/section-page/SectionNav.js +22 -2
  33. package/dist/backend/section-page/SectionNav.js.map +2 -2
  34. package/dist/backend/utils/api.js +9 -1
  35. package/dist/backend/utils/api.js.map +2 -2
  36. package/package.json +2 -2
  37. package/src/backend/AGENTS.md +50 -0
  38. package/src/backend/AppShell.tsx +317 -30
  39. package/src/backend/CrudForm.tsx +238 -21
  40. package/src/backend/ProfileDropdown.tsx +199 -78
  41. package/src/backend/injection/InjectionSpot.tsx +118 -16
  42. package/src/backend/injection/SseEventIndicator.tsx +24 -0
  43. package/src/backend/injection/WidgetSharedState.ts +58 -0
  44. package/src/backend/injection/eventBridge.ts +134 -0
  45. package/src/backend/injection/mergeMenuItems.ts +71 -0
  46. package/src/backend/injection/resolveInjectedIcon.tsx +30 -0
  47. package/src/backend/injection/spotIds.ts +38 -0
  48. package/src/backend/injection/useAppEvent.ts +76 -0
  49. package/src/backend/injection/useInjectedMenuItems.ts +125 -0
  50. package/src/backend/injection/useInjectionDataWidgets.ts +41 -0
  51. package/src/backend/injection/useOperationProgress.ts +105 -0
  52. package/src/backend/injection/useWidgetSharedState.ts +28 -0
  53. package/src/backend/section-page/SectionNav.tsx +22 -1
  54. package/src/backend/utils/api.ts +14 -5
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/injection/InjectionSpot.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type {\n InjectionSpotId,\n InjectionWidgetModule,\n WidgetInjectionEventHandlers,\n WidgetBeforeDeleteResult,\n WidgetBeforeSaveResult,\n} from '@open-mercato/shared/modules/widgets/injection'\nimport { loadInjectionWidgetsForSpot, type LoadedInjectionWidget } from '@open-mercato/shared/modules/widgets/injection-loader'\n\nexport type InjectionSpotProps<TContext = unknown, TData = unknown> = {\n spotId: InjectionSpotId\n context: TContext\n data?: TData\n onDataChange?: (data: TData) => void\n disabled?: boolean\n onEvent?: (\n event:\n | 'onLoad'\n | 'onBeforeSave'\n | 'onSave'\n | 'onAfterSave'\n | 'onBeforeDelete'\n | 'onDelete'\n | 'onAfterDelete'\n | 'onDeleteError',\n widgetId: string,\n ) => void\n widgetsOverride?: LoadedWidget[]\n}\n\ntype LoadedWidget = {\n widgetId: string\n module: InjectionWidgetModule<any, any>\n moduleId: string\n key: string\n placement?: LoadedInjectionWidget['placement']\n}\n\nexport function useInjectionWidgets<TContext = unknown>(\n spotId: InjectionSpotId | null | undefined,\n options?: {\n context?: TContext\n triggerOnLoad?: boolean\n onEvent?: (event: 'onLoad', widgetId: string) => void\n }\n) {\n const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const loadedRef = React.useRef(false)\n\n React.useEffect(() => {\n if (!spotId) {\n setWidgets([])\n setLoading(false)\n setError(null)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n setLoading(true)\n setError(null)\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n const widgetList: LoadedWidget[] = loaded.map((w) => ({\n widgetId: w.metadata.id,\n module: w,\n moduleId: w.moduleId,\n key: w.key,\n placement: w.placement,\n }))\n setWidgets(widgetList)\n \n // Trigger onLoad for all widgets\n if (!loadedRef.current && options?.triggerOnLoad) {\n loadedRef.current = true\n for (const widget of widgetList) {\n if (widget.module.eventHandlers?.onLoad) {\n try {\n await widget.module.eventHandlers.onLoad(options.context as TContext)\n options.onEvent?.('onLoad', widget.widgetId)\n } catch (err) {\n console.error(`[InjectionSpot] Error in onLoad for widget ${widget.widgetId}:`, err)\n }\n }\n }\n }\n } catch (err) {\n if (!mounted) return\n console.error(`[InjectionSpot] Failed to load widgets for spot ${spotId}:`, err)\n setError(err instanceof Error ? err.message : String(err))\n } finally {\n if (mounted) setLoading(false)\n }\n }\n load()\n return () => {\n mounted = false\n }\n }, [spotId, options?.context, options?.triggerOnLoad, options?.onEvent])\n\n return { widgets, loading, error }\n}\n\nexport function InjectionSpot<TContext = unknown, TData = unknown>({\n spotId,\n context,\n data,\n onDataChange,\n disabled,\n onEvent,\n widgetsOverride,\n}: InjectionSpotProps<TContext, TData>) {\n const useSpotId = widgetsOverride ? null : spotId\n const { widgets, loading, error } = useInjectionWidgets<TContext>(useSpotId, {\n context,\n triggerOnLoad: !widgetsOverride,\n onEvent: onEvent ? (event, id) => onEvent(event, id) : undefined,\n })\n const effectiveWidgets = widgetsOverride ?? widgets\n const effectiveLoading = widgetsOverride ? false : loading\n const effectiveError = widgetsOverride ? null : error\n\n if (effectiveLoading) {\n return null\n }\n\n if (effectiveError) {\n console.error(`[InjectionSpot] Error loading widgets for spot ${spotId}:`, effectiveError)\n return null\n }\n\n if (effectiveWidgets.length === 0) {\n return null\n }\n\n return (\n <>\n {effectiveWidgets.map((widget) => {\n const { Widget } = widget.module\n return (\n <Widget\n key={widget.widgetId}\n context={context}\n data={data}\n onDataChange={onDataChange}\n disabled={disabled}\n />\n )\n })}\n </>\n )\n}\n\n/**\n * Hook to trigger injection widget events imperatively\n */\nexport function useInjectionSpotEvents<TContext = unknown, TData = unknown>(spotId: InjectionSpotId, prefetchedWidgets?: LoadedWidget[]) {\n const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])\n\n React.useEffect(() => {\n if (prefetchedWidgets && prefetchedWidgets.length) {\n setWidgets(prefetchedWidgets)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n setWidgets(\n loaded.map((w) => ({\n widgetId: w.metadata.id,\n module: w,\n moduleId: w.moduleId,\n key: w.key,\n placement: w.placement,\n }))\n )\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Failed to load widgets for spot ${spotId}:`, err)\n }\n }\n load()\n return () => {\n mounted = false\n }\n }, [spotId, prefetchedWidgets])\n\n const triggerEvent = React.useCallback(\n async (\n event: keyof WidgetInjectionEventHandlers<TContext, TData>,\n data: TData,\n context: TContext,\n meta?: { error?: unknown }\n ): Promise<{ ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown }> => {\n const normalizeBeforeSave = (\n result: WidgetBeforeSaveResult,\n ): { ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown } => {\n if (result === false) return { ok: false }\n if (result === true || typeof result === 'undefined') return { ok: true }\n if (result && typeof result === 'object') {\n const ok = typeof result.ok === 'boolean' ? result.ok : true\n const message = typeof result.message === 'string' ? result.message : undefined\n const fieldErrors =\n result.fieldErrors && typeof result.fieldErrors === 'object'\n ? Object.fromEntries(\n Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n const requestHeaders =\n result.requestHeaders && typeof result.requestHeaders === 'object'\n ? Object.fromEntries(\n Object.entries(result.requestHeaders).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n return { ok, message, fieldErrors, requestHeaders, details: result.details }\n }\n return { ok: true }\n }\n\n const normalizeBeforeDelete = (\n result: WidgetBeforeDeleteResult,\n ): { ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown } => {\n if (result === false) return { ok: false }\n if (result === true || typeof result === 'undefined') return { ok: true }\n if (result && typeof result === 'object') {\n const ok = typeof result.ok === 'boolean' ? result.ok : true\n const message = typeof result.message === 'string' ? result.message : undefined\n const fieldErrors =\n result.fieldErrors && typeof result.fieldErrors === 'object'\n ? Object.fromEntries(\n Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n const requestHeaders =\n result.requestHeaders && typeof result.requestHeaders === 'object'\n ? Object.fromEntries(\n Object.entries(result.requestHeaders).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n return { ok, message, fieldErrors, requestHeaders, details: result.details }\n }\n return { ok: true }\n }\n\n const mergedRequestHeaders: Record<string, string> = {}\n let hasRequestHeaders = false\n\n for (const widget of widgets) {\n const eventHandlers = widget.module.eventHandlers\n let handler = eventHandlers?.[event]\n if (!handler && event === 'onBeforeDelete') handler = eventHandlers?.onBeforeSave as typeof handler\n if (!handler && event === 'onDelete') handler = eventHandlers?.onSave as typeof handler\n if (!handler && event === 'onAfterDelete') handler = eventHandlers?.onAfterSave as typeof handler\n if (handler) {\n try {\n const result =\n event === 'onDeleteError'\n ? await (handler as any)(data, context, meta?.error)\n : await (handler as any)(data, context)\n if (event === 'onBeforeSave') {\n const normalized = normalizeBeforeSave(result as WidgetBeforeSaveResult)\n if (!normalized.ok) {\n console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)\n return normalized\n }\n if (normalized.requestHeaders && Object.keys(normalized.requestHeaders).length > 0) {\n Object.assign(mergedRequestHeaders, normalized.requestHeaders)\n hasRequestHeaders = true\n }\n }\n if (event === 'onBeforeDelete') {\n const normalized = normalizeBeforeDelete(result as WidgetBeforeDeleteResult)\n if (!normalized.ok) {\n console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)\n return normalized\n }\n if (normalized.requestHeaders && Object.keys(normalized.requestHeaders).length > 0) {\n Object.assign(mergedRequestHeaders, normalized.requestHeaders)\n hasRequestHeaders = true\n }\n }\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)\n if (event === 'onBeforeSave' || event === 'onBeforeDelete') {\n const message =\n err instanceof Error\n ? err.message || 'Validation blocked'\n : typeof err === 'string'\n ? err\n : undefined\n return { ok: false, message }\n }\n }\n }\n }\n if ((event === 'onBeforeSave' || event === 'onBeforeDelete') && hasRequestHeaders) {\n return { ok: true, requestHeaders: mergedRequestHeaders }\n }\n return { ok: true }\n },\n [widgets]\n )\n\n return { triggerEvent, widgets }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type {\n InjectionSpotId,\n InjectionWidgetModule,\n WidgetInjectionEventHandlers,\n WidgetBeforeDeleteResult,\n WidgetBeforeSaveResult,\n FieldChangeResult,\n NavigateGuardResult,\n} from '@open-mercato/shared/modules/widgets/injection'\nimport { loadInjectionWidgetsForSpot, type LoadedInjectionWidget } from '@open-mercato/shared/modules/widgets/injection-loader'\nimport { getWidgetSharedState } from './WidgetSharedState'\n\nexport type InjectionSpotProps<TContext = unknown, TData = unknown> = {\n spotId: InjectionSpotId\n context: TContext\n data?: TData\n onDataChange?: (data: TData) => void\n disabled?: boolean\n onEvent?: (\n event: keyof WidgetInjectionEventHandlers<TContext, TData>,\n widgetId: string,\n ) => void\n widgetsOverride?: LoadedWidget[]\n}\n\n/**\n * Transformer events use pipeline dispatch: output of widget N becomes input of widget N+1.\n */\nconst TRANSFORMER_EVENTS = new Set<string>([\n 'transformFormData',\n 'transformDisplayData',\n 'transformValidation',\n])\n\ntype LoadedWidget = {\n widgetId: string\n module: InjectionWidgetModule<any, any>\n moduleId: string\n key: string\n placement?: LoadedInjectionWidget['placement']\n}\n\nfunction injectSharedStateIntoContext<TContext>(context: TContext, moduleId: string): TContext {\n const sharedState = getWidgetSharedState(moduleId)\n if (typeof context === 'object' && context !== null && !Array.isArray(context)) {\n return {\n ...(context as Record<string, unknown>),\n sharedState,\n } as TContext\n }\n return {\n value: context,\n sharedState,\n } as TContext\n}\n\nexport function useInjectionWidgets<TContext = unknown>(\n spotId: InjectionSpotId | null | undefined,\n options?: {\n context?: TContext\n triggerOnLoad?: boolean\n onEvent?: (event: 'onLoad', widgetId: string) => void\n }\n) {\n const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const loadedRef = React.useRef(false)\n\n React.useEffect(() => {\n if (!spotId) {\n setWidgets([])\n setLoading(false)\n setError(null)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n setLoading(true)\n setError(null)\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n const widgetList: LoadedWidget[] = loaded.map((w) => ({\n widgetId: w.metadata.id,\n module: w,\n moduleId: w.moduleId,\n key: w.key,\n placement: w.placement,\n }))\n setWidgets(widgetList)\n \n // Trigger onLoad for all widgets\n if (!loadedRef.current && options?.triggerOnLoad) {\n loadedRef.current = true\n for (const widget of widgetList) {\n if (widget.module.eventHandlers?.onLoad) {\n try {\n const widgetContext = injectSharedStateIntoContext(options.context as TContext, widget.moduleId)\n await widget.module.eventHandlers.onLoad(widgetContext)\n options.onEvent?.('onLoad', widget.widgetId)\n } catch (err) {\n console.error(`[InjectionSpot] Error in onLoad for widget ${widget.widgetId}:`, err)\n }\n }\n }\n }\n } catch (err) {\n if (!mounted) return\n console.error(`[InjectionSpot] Failed to load widgets for spot ${spotId}:`, err)\n setError(err instanceof Error ? err.message : String(err))\n } finally {\n if (mounted) setLoading(false)\n }\n }\n load()\n return () => {\n mounted = false\n }\n }, [spotId, options?.context, options?.triggerOnLoad, options?.onEvent])\n\n return { widgets, loading, error }\n}\n\nexport function InjectionSpot<TContext = unknown, TData = unknown>({\n spotId,\n context,\n data,\n onDataChange,\n disabled,\n onEvent,\n widgetsOverride,\n}: InjectionSpotProps<TContext, TData>) {\n const useSpotId = widgetsOverride ? null : spotId\n const { widgets, loading, error } = useInjectionWidgets<TContext>(useSpotId, {\n context,\n triggerOnLoad: !widgetsOverride,\n onEvent: onEvent ? (event, id) => onEvent(event, id) : undefined,\n })\n const effectiveWidgets = widgetsOverride ?? widgets\n const effectiveLoading = widgetsOverride ? false : loading\n const effectiveError = widgetsOverride ? null : error\n\n if (effectiveLoading) {\n return null\n }\n\n if (effectiveError) {\n console.error(`[InjectionSpot] Error loading widgets for spot ${spotId}:`, effectiveError)\n return null\n }\n\n if (effectiveWidgets.length === 0) {\n return null\n }\n\n return (\n <>\n {effectiveWidgets.map((widget) => {\n const { Widget } = widget.module\n return (\n <Widget\n key={widget.widgetId}\n context={injectSharedStateIntoContext(context, widget.moduleId)}\n data={data}\n onDataChange={onDataChange}\n disabled={disabled}\n />\n )\n })}\n </>\n )\n}\n\n/**\n * Hook to trigger injection widget events imperatively\n */\nexport function useInjectionSpotEvents<TContext = unknown, TData = unknown>(spotId: InjectionSpotId, prefetchedWidgets?: LoadedWidget[]) {\n const [widgets, setWidgets] = React.useState<LoadedWidget[]>([])\n\n React.useEffect(() => {\n if (prefetchedWidgets && prefetchedWidgets.length) {\n setWidgets(prefetchedWidgets)\n return\n }\n let mounted = true\n const load = async () => {\n try {\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n setWidgets(\n loaded.map((w) => ({\n widgetId: w.metadata.id,\n module: w,\n moduleId: w.moduleId,\n key: w.key,\n placement: w.placement,\n }))\n )\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Failed to load widgets for spot ${spotId}:`, err)\n }\n }\n load()\n return () => {\n mounted = false\n }\n }, [spotId, prefetchedWidgets])\n\n const triggerEvent = React.useCallback(\n async (\n event: keyof WidgetInjectionEventHandlers<TContext, TData>,\n data: TData,\n context: TContext,\n meta?: {\n error?: unknown\n fieldId?: string\n fieldValue?: unknown\n originalData?: TData\n target?: unknown\n visible?: boolean\n appEvent?: unknown\n }\n ): Promise<{\n ok: boolean\n message?: string\n fieldErrors?: Record<string, string>\n requestHeaders?: Record<string, string>\n details?: unknown\n data?: TData\n fieldChange?: {\n value?: unknown\n sideEffects?: Record<string, unknown>\n messages?: Array<{ text: string; severity: 'info' | 'warning' | 'error' }>\n }\n }> => {\n const normalizeBeforeSave = (\n result: WidgetBeforeSaveResult,\n ): { ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown } => {\n if (result === false) return { ok: false }\n if (result === true || typeof result === 'undefined') return { ok: true }\n if (result && typeof result === 'object') {\n const ok = typeof result.ok === 'boolean' ? result.ok : true\n const message = typeof result.message === 'string' ? result.message : undefined\n const fieldErrors =\n result.fieldErrors && typeof result.fieldErrors === 'object'\n ? Object.fromEntries(\n Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n const requestHeaders =\n result.requestHeaders && typeof result.requestHeaders === 'object'\n ? Object.fromEntries(\n Object.entries(result.requestHeaders).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n return { ok, message, fieldErrors, requestHeaders, details: result.details }\n }\n return { ok: true }\n }\n\n const normalizeBeforeDelete = (\n result: WidgetBeforeDeleteResult,\n ): { ok: boolean; message?: string; fieldErrors?: Record<string, string>; requestHeaders?: Record<string, string>; details?: unknown } => {\n if (result === false) return { ok: false }\n if (result === true || typeof result === 'undefined') return { ok: true }\n if (result && typeof result === 'object') {\n const ok = typeof result.ok === 'boolean' ? result.ok : true\n const message = typeof result.message === 'string' ? result.message : undefined\n const fieldErrors =\n result.fieldErrors && typeof result.fieldErrors === 'object'\n ? Object.fromEntries(\n Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n const requestHeaders =\n result.requestHeaders && typeof result.requestHeaders === 'object'\n ? Object.fromEntries(\n Object.entries(result.requestHeaders).map(([key, value]) => [key, String(value)]),\n )\n : undefined\n return { ok, message, fieldErrors, requestHeaders, details: result.details }\n }\n return { ok: true }\n }\n\n // --- Transformer events: pipeline dispatch ---\n // Output of widget N becomes input of widget N+1\n if (TRANSFORMER_EVENTS.has(event)) {\n let pipelineData = data\n for (const widget of widgets) {\n const handler = widget.module.eventHandlers?.[event]\n if (!handler) continue\n try {\n const widgetContext = injectSharedStateIntoContext(context, widget.moduleId)\n if (event === 'transformValidation') {\n pipelineData = await (handler as any)(pipelineData, meta?.originalData ?? data, widgetContext)\n } else {\n pipelineData = await (handler as any)(pipelineData, widgetContext)\n }\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)\n }\n }\n return { ok: true, data: pipelineData }\n }\n\n // --- Action events: sequential dispatch ---\n const mergedRequestHeaders: Record<string, string> = {}\n let hasRequestHeaders = false\n let fieldValue = meta?.fieldValue\n let fieldSideEffects: Record<string, unknown> | undefined\n let fieldMessages: Array<{ text: string; severity: 'info' | 'warning' | 'error' }> | undefined\n\n for (const widget of widgets) {\n const eventHandlers = widget.module.eventHandlers\n let handler = eventHandlers?.[event]\n // Delete-to-save fallback chain\n if (!handler && event === 'onBeforeDelete') handler = eventHandlers?.onBeforeSave as typeof handler\n if (!handler && event === 'onDelete') handler = eventHandlers?.onSave as typeof handler\n if (!handler && event === 'onAfterDelete') handler = eventHandlers?.onAfterSave as typeof handler\n if (handler) {\n try {\n const widgetContext = injectSharedStateIntoContext(context, widget.moduleId)\n const result =\n event === 'onDeleteError'\n ? await (handler as any)(data, widgetContext, meta?.error)\n : event === 'onFieldChange'\n ? await (handler as any)(meta?.fieldId, fieldValue, data, widgetContext)\n : event === 'onBeforeNavigate'\n ? await (handler as any)(meta?.target, widgetContext)\n : event === 'onVisibilityChange'\n ? await (handler as any)(meta?.visible, widgetContext)\n : event === 'onAppEvent'\n ? await (handler as any)(meta?.appEvent, widgetContext)\n : await (handler as any)(data, widgetContext)\n if (event === 'onBeforeSave') {\n const normalized = normalizeBeforeSave(result as WidgetBeforeSaveResult)\n if (!normalized.ok) {\n console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)\n return normalized\n }\n if (normalized.requestHeaders && Object.keys(normalized.requestHeaders).length > 0) {\n Object.assign(mergedRequestHeaders, normalized.requestHeaders)\n hasRequestHeaders = true\n }\n }\n if (event === 'onBeforeDelete') {\n const normalized = normalizeBeforeDelete(result as WidgetBeforeDeleteResult)\n if (!normalized.ok) {\n console.log(`[useInjectionSpotEvents] Widget ${widget.widgetId} prevented ${event}`)\n return normalized\n }\n if (normalized.requestHeaders && Object.keys(normalized.requestHeaders).length > 0) {\n Object.assign(mergedRequestHeaders, normalized.requestHeaders)\n hasRequestHeaders = true\n }\n }\n if (event === 'onBeforeNavigate') {\n const navResult = result as NavigateGuardResult | undefined\n if (navResult && navResult.ok === false) {\n return { ok: false, message: navResult.message }\n }\n }\n if (event === 'onFieldChange') {\n const changeResult = result as FieldChangeResult | void\n if (changeResult?.value !== undefined) {\n fieldValue = changeResult.value\n }\n if (changeResult?.sideEffects && typeof changeResult.sideEffects === 'object') {\n fieldSideEffects = { ...(fieldSideEffects ?? {}), ...changeResult.sideEffects }\n }\n if (changeResult?.message?.text) {\n fieldMessages = [...(fieldMessages ?? []), changeResult.message]\n }\n }\n } catch (err) {\n console.error(`[useInjectionSpotEvents] Error in ${event} for widget ${widget.widgetId}:`, err)\n if (event === 'onBeforeSave' || event === 'onBeforeDelete' || event === 'onBeforeNavigate') {\n const message =\n err instanceof Error\n ? err.message || 'Validation blocked'\n : typeof err === 'string'\n ? err\n : undefined\n return { ok: false, message }\n }\n }\n }\n }\n if ((event === 'onBeforeSave' || event === 'onBeforeDelete') && hasRequestHeaders) {\n return { ok: true, requestHeaders: mergedRequestHeaders }\n }\n if (event === 'onFieldChange') {\n return {\n ok: true,\n fieldChange: {\n value: fieldValue,\n sideEffects: fieldSideEffects,\n messages: fieldMessages,\n },\n }\n }\n return { ok: true }\n },\n [widgets]\n )\n\n return { triggerEvent, widgets }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import { useAppEvent } from "./useAppEvent.js";
3
+ import { flash } from "../FlashMessages.js";
4
+ function SseEventIndicator() {
5
+ useAppEvent("*", (event) => {
6
+ const parts = event.id.split(".");
7
+ const module = parts[0] ?? "";
8
+ const action = parts[parts.length - 1] ?? "event";
9
+ flash(`[SSE] ${module}: ${action} (${event.id})`, "info");
10
+ });
11
+ return null;
12
+ }
13
+ export {
14
+ SseEventIndicator
15
+ };
16
+ //# sourceMappingURL=SseEventIndicator.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/injection/SseEventIndicator.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport { useAppEvent } from './useAppEvent'\nimport { flash } from '../FlashMessages'\n\n/**\n * Global SSE Event Indicator\n *\n * Mount once in AppShell to provide visible feedback when server events\n * arrive via the DOM Event Bridge. Shows a flash message for every\n * broadcast event received.\n *\n * This component renders nothing \u2014 it only listens and triggers flash messages.\n */\nexport function SseEventIndicator(): null {\n useAppEvent('*', (event) => {\n const parts = event.id.split('.')\n const module = parts[0] ?? ''\n const action = parts[parts.length - 1] ?? 'event'\n flash(`[SSE] ${module}: ${action} (${event.id})`, 'info')\n })\n\n return null\n}\n"],
5
+ "mappings": ";AAEA,SAAS,mBAAmB;AAC5B,SAAS,aAAa;AAWf,SAAS,oBAA0B;AACxC,cAAY,KAAK,CAAC,UAAU;AAC1B,UAAM,QAAQ,MAAM,GAAG,MAAM,GAAG;AAChC,UAAM,SAAS,MAAM,CAAC,KAAK;AAC3B,UAAM,SAAS,MAAM,MAAM,SAAS,CAAC,KAAK;AAC1C,UAAM,SAAS,MAAM,KAAK,MAAM,KAAK,MAAM,EAAE,KAAK,MAAM;AAAA,EAC1D,CAAC;AAED,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,49 @@
1
+ class NamespacedWidgetSharedState {
2
+ constructor(namespace) {
3
+ this.namespace = namespace;
4
+ this.values = /* @__PURE__ */ new Map();
5
+ this.subscribers = /* @__PURE__ */ new Map();
6
+ }
7
+ get(key) {
8
+ return this.values.get(this.toScopedKey(key));
9
+ }
10
+ set(key, value) {
11
+ const scopedKey = this.toScopedKey(key);
12
+ this.values.set(scopedKey, value);
13
+ const handlers = this.subscribers.get(scopedKey);
14
+ if (!handlers || handlers.size === 0) return;
15
+ for (const handler of handlers) {
16
+ handler(value);
17
+ }
18
+ }
19
+ subscribe(key, handler) {
20
+ const scopedKey = this.toScopedKey(key);
21
+ const handlers = this.subscribers.get(scopedKey) ?? /* @__PURE__ */ new Set();
22
+ handlers.add(handler);
23
+ this.subscribers.set(scopedKey, handlers);
24
+ return () => {
25
+ const current = this.subscribers.get(scopedKey);
26
+ if (!current) return;
27
+ current.delete(handler);
28
+ if (current.size === 0) {
29
+ this.subscribers.delete(scopedKey);
30
+ }
31
+ };
32
+ }
33
+ toScopedKey(key) {
34
+ return `${this.namespace}:${key}`;
35
+ }
36
+ }
37
+ const storeByNamespace = /* @__PURE__ */ new Map();
38
+ function getWidgetSharedState(namespace) {
39
+ const normalized = namespace.trim().length > 0 ? namespace.trim() : "global";
40
+ const existing = storeByNamespace.get(normalized);
41
+ if (existing) return existing;
42
+ const created = new NamespacedWidgetSharedState(normalized);
43
+ storeByNamespace.set(normalized, created);
44
+ return created;
45
+ }
46
+ export {
47
+ getWidgetSharedState
48
+ };
49
+ //# sourceMappingURL=WidgetSharedState.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/injection/WidgetSharedState.ts"],
4
+ "sourcesContent": ["type Subscriber = (value: unknown) => void\n\nexport interface WidgetSharedState {\n get<T>(key: string): T | undefined\n set<T>(key: string, value: T): void\n subscribe(key: string, handler: Subscriber): () => void\n}\n\nclass NamespacedWidgetSharedState implements WidgetSharedState {\n private readonly values = new Map<string, unknown>()\n private readonly subscribers = new Map<string, Set<Subscriber>>()\n\n constructor(private readonly namespace: string) {}\n\n get<T>(key: string): T | undefined {\n return this.values.get(this.toScopedKey(key)) as T | undefined\n }\n\n set<T>(key: string, value: T): void {\n const scopedKey = this.toScopedKey(key)\n this.values.set(scopedKey, value)\n const handlers = this.subscribers.get(scopedKey)\n if (!handlers || handlers.size === 0) return\n for (const handler of handlers) {\n handler(value)\n }\n }\n\n subscribe(key: string, handler: Subscriber): () => void {\n const scopedKey = this.toScopedKey(key)\n const handlers = this.subscribers.get(scopedKey) ?? new Set<Subscriber>()\n handlers.add(handler)\n this.subscribers.set(scopedKey, handlers)\n return () => {\n const current = this.subscribers.get(scopedKey)\n if (!current) return\n current.delete(handler)\n if (current.size === 0) {\n this.subscribers.delete(scopedKey)\n }\n }\n }\n\n private toScopedKey(key: string): string {\n return `${this.namespace}:${key}`\n }\n}\n\nconst storeByNamespace = new Map<string, WidgetSharedState>()\n\nexport function getWidgetSharedState(namespace: string): WidgetSharedState {\n const normalized = namespace.trim().length > 0 ? namespace.trim() : 'global'\n const existing = storeByNamespace.get(normalized)\n if (existing) return existing\n const created = new NamespacedWidgetSharedState(normalized)\n storeByNamespace.set(normalized, created)\n return created\n}\n"],
5
+ "mappings": "AAQA,MAAM,4BAAyD;AAAA,EAI7D,YAA6B,WAAmB;AAAnB;AAH7B,SAAiB,SAAS,oBAAI,IAAqB;AACnD,SAAiB,cAAc,oBAAI,IAA6B;AAAA,EAEf;AAAA,EAEjD,IAAO,KAA4B;AACjC,WAAO,KAAK,OAAO,IAAI,KAAK,YAAY,GAAG,CAAC;AAAA,EAC9C;AAAA,EAEA,IAAO,KAAa,OAAgB;AAClC,UAAM,YAAY,KAAK,YAAY,GAAG;AACtC,SAAK,OAAO,IAAI,WAAW,KAAK;AAChC,UAAM,WAAW,KAAK,YAAY,IAAI,SAAS;AAC/C,QAAI,CAAC,YAAY,SAAS,SAAS,EAAG;AACtC,eAAW,WAAW,UAAU;AAC9B,cAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAAA,EAEA,UAAU,KAAa,SAAiC;AACtD,UAAM,YAAY,KAAK,YAAY,GAAG;AACtC,UAAM,WAAW,KAAK,YAAY,IAAI,SAAS,KAAK,oBAAI,IAAgB;AACxE,aAAS,IAAI,OAAO;AACpB,SAAK,YAAY,IAAI,WAAW,QAAQ;AACxC,WAAO,MAAM;AACX,YAAM,UAAU,KAAK,YAAY,IAAI,SAAS;AAC9C,UAAI,CAAC,QAAS;AACd,cAAQ,OAAO,OAAO;AACtB,UAAI,QAAQ,SAAS,GAAG;AACtB,aAAK,YAAY,OAAO,SAAS;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,YAAY,KAAqB;AACvC,WAAO,GAAG,KAAK,SAAS,IAAI,GAAG;AAAA,EACjC;AACF;AAEA,MAAM,mBAAmB,oBAAI,IAA+B;AAErD,SAAS,qBAAqB,WAAsC;AACzE,QAAM,aAAa,UAAU,KAAK,EAAE,SAAS,IAAI,UAAU,KAAK,IAAI;AACpE,QAAM,WAAW,iBAAiB,IAAI,UAAU;AAChD,MAAI,SAAU,QAAO;AACrB,QAAM,UAAU,IAAI,4BAA4B,UAAU;AAC1D,mBAAiB,IAAI,YAAY,OAAO;AACxC,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,105 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "react";
3
+ import { APP_EVENT_DOM_NAME } from "./useAppEvent.js";
4
+ const SSE_ENDPOINT = "/api/events/stream";
5
+ const HEARTBEAT_TIMEOUT = 45e3;
6
+ const RECONNECT_BASE_MS = 1e3;
7
+ const RECONNECT_MAX_MS = 3e4;
8
+ const DEDUP_WINDOW_MS = 500;
9
+ function useEventBridge() {
10
+ const sourceRef = useRef(null);
11
+ const reconnectAttempts = useRef(0);
12
+ const reconnectTimer = useRef(null);
13
+ const heartbeatTimer = useRef(null);
14
+ const recentEvents = useRef(/* @__PURE__ */ new Map());
15
+ useEffect(() => {
16
+ let mounted = true;
17
+ function isDuplicate(eventPayload) {
18
+ const key = `${eventPayload.id}:${JSON.stringify(eventPayload.payload ?? {})}`;
19
+ const lastSeen = recentEvents.current.get(key);
20
+ if (lastSeen && Date.now() - lastSeen < DEDUP_WINDOW_MS) return true;
21
+ recentEvents.current.set(key, Date.now());
22
+ if (recentEvents.current.size > 100) {
23
+ const now = Date.now();
24
+ for (const [k, v] of recentEvents.current) {
25
+ if (now - v > DEDUP_WINDOW_MS * 2) recentEvents.current.delete(k);
26
+ }
27
+ }
28
+ return false;
29
+ }
30
+ function resetHeartbeatTimer() {
31
+ if (heartbeatTimer.current) clearTimeout(heartbeatTimer.current);
32
+ heartbeatTimer.current = setTimeout(() => {
33
+ console.warn("[EventBridge] Heartbeat timeout \u2014 reconnecting");
34
+ disconnect();
35
+ scheduleReconnect();
36
+ }, HEARTBEAT_TIMEOUT);
37
+ }
38
+ function connect() {
39
+ if (!mounted) return;
40
+ if (sourceRef.current) return;
41
+ try {
42
+ const source = new EventSource(SSE_ENDPOINT, { withCredentials: true });
43
+ sourceRef.current = source;
44
+ source.onopen = () => {
45
+ reconnectAttempts.current = 0;
46
+ resetHeartbeatTimer();
47
+ };
48
+ source.onmessage = (event) => {
49
+ resetHeartbeatTimer();
50
+ if (!event.data || event.data === ":heartbeat") return;
51
+ try {
52
+ const parsed = JSON.parse(event.data);
53
+ if (!parsed.id || typeof parsed.id !== "string") return;
54
+ if (isDuplicate(parsed)) return;
55
+ window.dispatchEvent(
56
+ new CustomEvent(APP_EVENT_DOM_NAME, { detail: parsed })
57
+ );
58
+ } catch {
59
+ }
60
+ };
61
+ source.onerror = () => {
62
+ disconnect();
63
+ if (mounted) scheduleReconnect();
64
+ };
65
+ } catch {
66
+ if (mounted) scheduleReconnect();
67
+ }
68
+ }
69
+ function disconnect() {
70
+ if (sourceRef.current) {
71
+ sourceRef.current.close();
72
+ sourceRef.current = null;
73
+ }
74
+ if (heartbeatTimer.current) {
75
+ clearTimeout(heartbeatTimer.current);
76
+ heartbeatTimer.current = null;
77
+ }
78
+ }
79
+ function scheduleReconnect() {
80
+ if (reconnectTimer.current) return;
81
+ const delay = Math.min(
82
+ RECONNECT_BASE_MS * Math.pow(2, reconnectAttempts.current),
83
+ RECONNECT_MAX_MS
84
+ );
85
+ reconnectAttempts.current++;
86
+ reconnectTimer.current = setTimeout(() => {
87
+ reconnectTimer.current = null;
88
+ connect();
89
+ }, delay);
90
+ }
91
+ connect();
92
+ return () => {
93
+ mounted = false;
94
+ disconnect();
95
+ if (reconnectTimer.current) {
96
+ clearTimeout(reconnectTimer.current);
97
+ reconnectTimer.current = null;
98
+ }
99
+ };
100
+ }, []);
101
+ }
102
+ export {
103
+ useEventBridge
104
+ };
105
+ //# sourceMappingURL=eventBridge.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/injection/eventBridge.ts"],
4
+ "sourcesContent": ["\"use client\"\nimport { useEffect, useRef } from 'react'\nimport type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'\nimport { APP_EVENT_DOM_NAME } from './useAppEvent'\n\nconst SSE_ENDPOINT = '/api/events/stream'\nconst HEARTBEAT_TIMEOUT = 45_000 // Expect heartbeat every 30s, allow 45s grace\nconst RECONNECT_BASE_MS = 1_000\nconst RECONNECT_MAX_MS = 30_000\nconst DEDUP_WINDOW_MS = 500\n\n/**\n * React hook that establishes a singleton SSE connection to the event bridge.\n *\n * Mount once in the app shell/layout. Receives server-side events with\n * `clientBroadcast: true` and dispatches them as `om:event` CustomEvents\n * on the window object for consumption by `useAppEvent`.\n *\n * Features:\n * - Auto-reconnect with exponential backoff\n * - Deduplication within 500ms window\n * - Heartbeat-based liveness detection\n * - Singleton connection per browser tab\n */\nexport function useEventBridge(): void {\n const sourceRef = useRef<EventSource | null>(null)\n const reconnectAttempts = useRef(0)\n const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n const heartbeatTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n const recentEvents = useRef<Map<string, number>>(new Map())\n\n useEffect(() => {\n let mounted = true\n\n function isDuplicate(eventPayload: AppEventPayload): boolean {\n const key = `${eventPayload.id}:${JSON.stringify(eventPayload.payload ?? {})}`\n const lastSeen = recentEvents.current.get(key)\n if (lastSeen && Date.now() - lastSeen < DEDUP_WINDOW_MS) return true\n recentEvents.current.set(key, Date.now())\n // Prune old entries\n if (recentEvents.current.size > 100) {\n const now = Date.now()\n for (const [k, v] of recentEvents.current) {\n if (now - v > DEDUP_WINDOW_MS * 2) recentEvents.current.delete(k)\n }\n }\n return false\n }\n\n function resetHeartbeatTimer() {\n if (heartbeatTimer.current) clearTimeout(heartbeatTimer.current)\n heartbeatTimer.current = setTimeout(() => {\n console.warn('[EventBridge] Heartbeat timeout \u2014 reconnecting')\n disconnect()\n scheduleReconnect()\n }, HEARTBEAT_TIMEOUT)\n }\n\n function connect() {\n if (!mounted) return\n if (sourceRef.current) return\n\n try {\n const source = new EventSource(SSE_ENDPOINT, { withCredentials: true })\n sourceRef.current = source\n\n source.onopen = () => {\n reconnectAttempts.current = 0\n resetHeartbeatTimer()\n }\n\n source.onmessage = (event) => {\n resetHeartbeatTimer()\n if (!event.data || event.data === ':heartbeat') return\n\n try {\n const parsed = JSON.parse(event.data) as AppEventPayload\n if (!parsed.id || typeof parsed.id !== 'string') return\n\n if (isDuplicate(parsed)) return\n\n window.dispatchEvent(\n new CustomEvent(APP_EVENT_DOM_NAME, { detail: parsed }),\n )\n } catch {\n // Ignore malformed events\n }\n }\n\n source.onerror = () => {\n disconnect()\n if (mounted) scheduleReconnect()\n }\n } catch {\n if (mounted) scheduleReconnect()\n }\n }\n\n function disconnect() {\n if (sourceRef.current) {\n sourceRef.current.close()\n sourceRef.current = null\n }\n if (heartbeatTimer.current) {\n clearTimeout(heartbeatTimer.current)\n heartbeatTimer.current = null\n }\n }\n\n function scheduleReconnect() {\n if (reconnectTimer.current) return\n const delay = Math.min(\n RECONNECT_BASE_MS * Math.pow(2, reconnectAttempts.current),\n RECONNECT_MAX_MS,\n )\n reconnectAttempts.current++\n reconnectTimer.current = setTimeout(() => {\n reconnectTimer.current = null\n connect()\n }, delay)\n }\n\n connect()\n\n return () => {\n mounted = false\n disconnect()\n if (reconnectTimer.current) {\n clearTimeout(reconnectTimer.current)\n reconnectTimer.current = null\n }\n }\n }, [])\n}\n"],
5
+ "mappings": ";AACA,SAAS,WAAW,cAAc;AAElC,SAAS,0BAA0B;AAEnC,MAAM,eAAe;AACrB,MAAM,oBAAoB;AAC1B,MAAM,oBAAoB;AAC1B,MAAM,mBAAmB;AACzB,MAAM,kBAAkB;AAejB,SAAS,iBAAuB;AACrC,QAAM,YAAY,OAA2B,IAAI;AACjD,QAAM,oBAAoB,OAAO,CAAC;AAClC,QAAM,iBAAiB,OAA6C,IAAI;AACxE,QAAM,iBAAiB,OAA6C,IAAI;AACxE,QAAM,eAAe,OAA4B,oBAAI,IAAI,CAAC;AAE1D,YAAU,MAAM;AACd,QAAI,UAAU;AAEd,aAAS,YAAY,cAAwC;AAC3D,YAAM,MAAM,GAAG,aAAa,EAAE,IAAI,KAAK,UAAU,aAAa,WAAW,CAAC,CAAC,CAAC;AAC5E,YAAM,WAAW,aAAa,QAAQ,IAAI,GAAG;AAC7C,UAAI,YAAY,KAAK,IAAI,IAAI,WAAW,gBAAiB,QAAO;AAChE,mBAAa,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AAExC,UAAI,aAAa,QAAQ,OAAO,KAAK;AACnC,cAAM,MAAM,KAAK,IAAI;AACrB,mBAAW,CAAC,GAAG,CAAC,KAAK,aAAa,SAAS;AACzC,cAAI,MAAM,IAAI,kBAAkB,EAAG,cAAa,QAAQ,OAAO,CAAC;AAAA,QAClE;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAEA,aAAS,sBAAsB;AAC7B,UAAI,eAAe,QAAS,cAAa,eAAe,OAAO;AAC/D,qBAAe,UAAU,WAAW,MAAM;AACxC,gBAAQ,KAAK,qDAAgD;AAC7D,mBAAW;AACX,0BAAkB;AAAA,MACpB,GAAG,iBAAiB;AAAA,IACtB;AAEA,aAAS,UAAU;AACjB,UAAI,CAAC,QAAS;AACd,UAAI,UAAU,QAAS;AAEvB,UAAI;AACF,cAAM,SAAS,IAAI,YAAY,cAAc,EAAE,iBAAiB,KAAK,CAAC;AACtE,kBAAU,UAAU;AAEpB,eAAO,SAAS,MAAM;AACpB,4BAAkB,UAAU;AAC5B,8BAAoB;AAAA,QACtB;AAEA,eAAO,YAAY,CAAC,UAAU;AAC5B,8BAAoB;AACpB,cAAI,CAAC,MAAM,QAAQ,MAAM,SAAS,aAAc;AAEhD,cAAI;AACF,kBAAM,SAAS,KAAK,MAAM,MAAM,IAAI;AACpC,gBAAI,CAAC,OAAO,MAAM,OAAO,OAAO,OAAO,SAAU;AAEjD,gBAAI,YAAY,MAAM,EAAG;AAEzB,mBAAO;AAAA,cACL,IAAI,YAAY,oBAAoB,EAAE,QAAQ,OAAO,CAAC;AAAA,YACxD;AAAA,UACF,QAAQ;AAAA,UAER;AAAA,QACF;AAEA,eAAO,UAAU,MAAM;AACrB,qBAAW;AACX,cAAI,QAAS,mBAAkB;AAAA,QACjC;AAAA,MACF,QAAQ;AACN,YAAI,QAAS,mBAAkB;AAAA,MACjC;AAAA,IACF;AAEA,aAAS,aAAa;AACpB,UAAI,UAAU,SAAS;AACrB,kBAAU,QAAQ,MAAM;AACxB,kBAAU,UAAU;AAAA,MACtB;AACA,UAAI,eAAe,SAAS;AAC1B,qBAAa,eAAe,OAAO;AACnC,uBAAe,UAAU;AAAA,MAC3B;AAAA,IACF;AAEA,aAAS,oBAAoB;AAC3B,UAAI,eAAe,QAAS;AAC5B,YAAM,QAAQ,KAAK;AAAA,QACjB,oBAAoB,KAAK,IAAI,GAAG,kBAAkB,OAAO;AAAA,QACzD;AAAA,MACF;AACA,wBAAkB;AAClB,qBAAe,UAAU,WAAW,MAAM;AACxC,uBAAe,UAAU;AACzB,gBAAQ;AAAA,MACV,GAAG,KAAK;AAAA,IACV;AAEA,YAAQ;AAER,WAAO,MAAM;AACX,gBAAU;AACV,iBAAW;AACX,UAAI,eAAe,SAAS;AAC1B,qBAAa,eAAe,OAAO;AACnC,uBAAe,UAAU;AAAA,MAC3B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AACP;",
6
+ "names": []
7
+ }
@@ -0,0 +1,43 @@
1
+ import { insertByInjectionPlacement } from "@open-mercato/shared/modules/widgets/injection-position";
2
+ function toMergedInjectedItem(item) {
3
+ return {
4
+ id: item.id,
5
+ label: item.label,
6
+ labelKey: item.labelKey,
7
+ icon: item.icon,
8
+ href: item.href,
9
+ onClick: item.onClick,
10
+ separator: item.separator,
11
+ badge: item.badge,
12
+ groupId: item.groupId,
13
+ groupLabel: item.groupLabel,
14
+ groupLabelKey: item.groupLabelKey,
15
+ groupOrder: item.groupOrder,
16
+ children: item.children,
17
+ source: "injected"
18
+ };
19
+ }
20
+ function mergeMenuItems(builtIn, injected) {
21
+ let merged = builtIn.map((item) => ({
22
+ ...item,
23
+ id: item.id,
24
+ source: "built-in"
25
+ }));
26
+ for (const item of injected) {
27
+ const nextItem = toMergedInjectedItem(item);
28
+ if (!item.placement && item.groupId) {
29
+ const existingGroupIndexes = merged.map((entry, index) => ({ entry, index })).filter(({ entry }) => entry.groupId === item.groupId);
30
+ if (existingGroupIndexes.length > 0) {
31
+ const insertAfter = existingGroupIndexes[existingGroupIndexes.length - 1]?.index ?? -1;
32
+ merged.splice(insertAfter + 1, 0, nextItem);
33
+ continue;
34
+ }
35
+ }
36
+ merged = insertByInjectionPlacement(merged, nextItem, item.placement, (entry) => entry.id);
37
+ }
38
+ return merged;
39
+ }
40
+ export {
41
+ mergeMenuItems
42
+ };
43
+ //# sourceMappingURL=mergeMenuItems.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/injection/mergeMenuItems.ts"],
4
+ "sourcesContent": ["import type { InjectionMenuItem } from '@open-mercato/shared/modules/widgets/injection'\nimport { insertByInjectionPlacement } from '@open-mercato/shared/modules/widgets/injection-position'\n\nexport type MergedMenuItem = {\n id: string\n label?: string\n labelKey?: string\n icon?: string\n href?: string\n onClick?: () => void\n separator?: boolean\n badge?: string | number\n groupId?: string\n groupLabel?: string\n groupLabelKey?: string\n groupOrder?: number\n children?: Omit<InjectionMenuItem, 'children'>[]\n source: 'built-in' | 'injected'\n}\n\ntype BuiltInMenuItem = {\n id: string\n [key: string]: unknown\n}\n\nfunction toMergedInjectedItem(item: InjectionMenuItem): MergedMenuItem {\n return {\n id: item.id,\n label: item.label,\n labelKey: item.labelKey,\n icon: item.icon,\n href: item.href,\n onClick: item.onClick,\n separator: item.separator,\n badge: item.badge,\n groupId: item.groupId,\n groupLabel: item.groupLabel,\n groupLabelKey: item.groupLabelKey,\n groupOrder: item.groupOrder,\n children: item.children,\n source: 'injected',\n }\n}\n\nexport function mergeMenuItems(\n builtIn: BuiltInMenuItem[],\n injected: InjectionMenuItem[],\n): MergedMenuItem[] {\n let merged: MergedMenuItem[] = builtIn.map((item) => ({\n ...(item as Record<string, unknown>),\n id: item.id,\n source: 'built-in',\n })) as MergedMenuItem[]\n\n for (const item of injected) {\n const nextItem = toMergedInjectedItem(item)\n if (!item.placement && item.groupId) {\n const existingGroupIndexes = merged\n .map((entry, index) => ({ entry, index }))\n .filter(({ entry }) => entry.groupId === item.groupId)\n if (existingGroupIndexes.length > 0) {\n const insertAfter = existingGroupIndexes[existingGroupIndexes.length - 1]?.index ?? -1\n merged.splice(insertAfter + 1, 0, nextItem)\n continue\n }\n }\n merged = insertByInjectionPlacement(merged, nextItem, item.placement, (entry) => entry.id)\n }\n\n return merged\n}\n"],
5
+ "mappings": "AACA,SAAS,kCAAkC;AAwB3C,SAAS,qBAAqB,MAAyC;AACrE,SAAO;AAAA,IACL,IAAI,KAAK;AAAA,IACT,OAAO,KAAK;AAAA,IACZ,UAAU,KAAK;AAAA,IACf,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,IACX,SAAS,KAAK;AAAA,IACd,WAAW,KAAK;AAAA,IAChB,OAAO,KAAK;AAAA,IACZ,SAAS,KAAK;AAAA,IACd,YAAY,KAAK;AAAA,IACjB,eAAe,KAAK;AAAA,IACpB,YAAY,KAAK;AAAA,IACjB,UAAU,KAAK;AAAA,IACf,QAAQ;AAAA,EACV;AACF;AAEO,SAAS,eACd,SACA,UACkB;AAClB,MAAI,SAA2B,QAAQ,IAAI,CAAC,UAAU;AAAA,IACpD,GAAI;AAAA,IACJ,IAAI,KAAK;AAAA,IACT,QAAQ;AAAA,EACV,EAAE;AAEF,aAAW,QAAQ,UAAU;AAC3B,UAAM,WAAW,qBAAqB,IAAI;AAC1C,QAAI,CAAC,KAAK,aAAa,KAAK,SAAS;AACnC,YAAM,uBAAuB,OAC1B,IAAI,CAAC,OAAO,WAAW,EAAE,OAAO,MAAM,EAAE,EACxC,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM,YAAY,KAAK,OAAO;AACvD,UAAI,qBAAqB,SAAS,GAAG;AACnC,cAAM,cAAc,qBAAqB,qBAAqB,SAAS,CAAC,GAAG,SAAS;AACpF,eAAO,OAAO,cAAc,GAAG,GAAG,QAAQ;AAC1C;AAAA,MACF;AAAA,IACF;AACA,aAAS,2BAA2B,QAAQ,UAAU,KAAK,WAAW,CAAC,UAAU,MAAM,EAAE;AAAA,EAC3F;AAEA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,23 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import * as LucideIcons from "lucide-react";
3
+ function toPascalCaseIconName(name) {
4
+ if (!name.includes("-") && !name.includes("_") && !name.includes(" ")) return name;
5
+ return name.split(/[-_\s]+/).filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
6
+ }
7
+ function resolveInjectedIcon(icon, className = "size-4") {
8
+ if (!icon) return null;
9
+ const normalized = icon.trim();
10
+ if (!normalized) return null;
11
+ const candidates = [normalized, toPascalCaseIconName(normalized)];
12
+ const registry = LucideIcons;
13
+ for (const candidate of candidates) {
14
+ const IconComponent = registry[candidate];
15
+ if (!IconComponent) continue;
16
+ return /* @__PURE__ */ jsx(IconComponent, { className });
17
+ }
18
+ return null;
19
+ }
20
+ export {
21
+ resolveInjectedIcon
22
+ };
23
+ //# sourceMappingURL=resolveInjectedIcon.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/injection/resolveInjectedIcon.tsx"],
4
+ "sourcesContent": ["import * as React from 'react'\nimport * as LucideIcons from 'lucide-react'\n\ntype LucideIconComponent = React.ComponentType<{ className?: string }>\n\nfunction toPascalCaseIconName(name: string): string {\n if (!name.includes('-') && !name.includes('_') && !name.includes(' ')) return name\n return name\n .split(/[-_\\s]+/)\n .filter((part) => part.length > 0)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('')\n}\n\nexport function resolveInjectedIcon(icon?: string, className = 'size-4'): React.ReactNode | null {\n if (!icon) return null\n const normalized = icon.trim()\n if (!normalized) return null\n\n const candidates = [normalized, toPascalCaseIconName(normalized)]\n const registry = LucideIcons as unknown as Record<string, LucideIconComponent | undefined>\n\n for (const candidate of candidates) {\n const IconComponent = registry[candidate]\n if (!IconComponent) continue\n return <IconComponent className={className} />\n }\n\n return null\n}\n"],
5
+ "mappings": "AAyBW;AAxBX,YAAY,iBAAiB;AAI7B,SAAS,qBAAqB,MAAsB;AAClD,MAAI,CAAC,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,SAAS,GAAG,EAAG,QAAO;AAC9E,SAAO,KACJ,MAAM,SAAS,EACf,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,EAChC,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,EAC1D,KAAK,EAAE;AACZ;AAEO,SAAS,oBAAoB,MAAe,YAAY,UAAkC;AAC/F,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,aAAa,KAAK,KAAK;AAC7B,MAAI,CAAC,WAAY,QAAO;AAExB,QAAM,aAAa,CAAC,YAAY,qBAAqB,UAAU,CAAC;AAChE,QAAM,WAAW;AAEjB,aAAW,aAAa,YAAY;AAClC,UAAM,gBAAgB,SAAS,SAAS;AACxC,QAAI,CAAC,cAAe;AACpB,WAAO,oBAAC,iBAAc,WAAsB;AAAA,EAC9C;AAEA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -3,11 +3,50 @@ const BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID = "backend:layout:top";
3
3
  const BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID = "backend:layout:footer";
4
4
  const BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID = "backend:sidebar:top";
5
5
  const BACKEND_SIDEBAR_FOOTER_INJECTION_SPOT_ID = "backend:sidebar:footer";
6
+ const BACKEND_TOPBAR_PROFILE_MENU_INJECTION_SPOT_ID = "backend:topbar:profile-menu";
7
+ const BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID = "backend:topbar:actions";
8
+ const BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID = "backend:sidebar:nav";
9
+ const BACKEND_SIDEBAR_NAV_FOOTER_INJECTION_SPOT_ID = "backend:sidebar:nav:footer";
10
+ const GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID = "global:sidebar:status-badges";
11
+ const GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID = "global:header:status-indicators";
12
+ const CrudFormInjectionSpots = {
13
+ base: (entityId) => `crud-form:${entityId}`,
14
+ beforeFields: (entityId) => `crud-form:${entityId}:before-fields`,
15
+ afterFields: (entityId) => `crud-form:${entityId}:after-fields`,
16
+ header: (entityId) => `crud-form:${entityId}:header`,
17
+ footer: (entityId) => `crud-form:${entityId}:footer`,
18
+ sidebar: (entityId) => `crud-form:${entityId}:sidebar`,
19
+ group: (entityId, groupId) => `crud-form:${entityId}:group:${groupId}`,
20
+ fieldBefore: (entityId, fieldId) => `crud-form:${entityId}:field:${fieldId}:before`,
21
+ fieldAfter: (entityId, fieldId) => `crud-form:${entityId}:field:${fieldId}:after`
22
+ };
23
+ const DataTableInjectionSpots = {
24
+ header: (tableId) => `data-table:${tableId}:header`,
25
+ footer: (tableId) => `data-table:${tableId}:footer`,
26
+ toolbar: (tableId) => `data-table:${tableId}:toolbar`,
27
+ emptyState: (tableId) => `data-table:${tableId}:empty-state`
28
+ };
29
+ const DetailInjectionSpots = {
30
+ header: (entityId) => `detail:${entityId}:header`,
31
+ tabs: (entityId) => `detail:${entityId}:tabs`,
32
+ sidebar: (entityId) => `detail:${entityId}:sidebar`,
33
+ footer: (entityId) => `detail:${entityId}:footer`,
34
+ statusBadges: (entityId) => `detail:${entityId}:status-badges`
35
+ };
6
36
  export {
7
37
  BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID,
8
38
  BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID,
9
39
  BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID,
10
40
  BACKEND_SIDEBAR_FOOTER_INJECTION_SPOT_ID,
11
- BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID
41
+ BACKEND_SIDEBAR_NAV_FOOTER_INJECTION_SPOT_ID,
42
+ BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID,
43
+ BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID,
44
+ BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID,
45
+ BACKEND_TOPBAR_PROFILE_MENU_INJECTION_SPOT_ID,
46
+ CrudFormInjectionSpots,
47
+ DataTableInjectionSpots,
48
+ DetailInjectionSpots,
49
+ GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID,
50
+ GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID
12
51
  };
13
52
  //# sourceMappingURL=spotIds.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/injection/spotIds.ts"],
4
- "sourcesContent": ["import type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'\n\nexport const BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID: InjectionSpotId = 'backend:record:current'\nexport const BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID: InjectionSpotId = 'backend:layout:top'\nexport const BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID: InjectionSpotId = 'backend:layout:footer'\nexport const BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:top'\nexport const BACKEND_SIDEBAR_FOOTER_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:footer'\n"],
5
- "mappings": "AAEO,MAAM,2CAA4D;AAClE,MAAM,uCAAwD;AAC9D,MAAM,0CAA2D;AACjE,MAAM,wCAAyD;AAC/D,MAAM,2CAA4D;",
4
+ "sourcesContent": ["import type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'\n\nexport const BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID: InjectionSpotId = 'backend:record:current'\nexport const BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID: InjectionSpotId = 'backend:layout:top'\nexport const BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID: InjectionSpotId = 'backend:layout:footer'\nexport const BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:top'\nexport const BACKEND_SIDEBAR_FOOTER_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:footer'\n\n// Standardized backend chrome spot ids\nexport const BACKEND_TOPBAR_PROFILE_MENU_INJECTION_SPOT_ID: InjectionSpotId = 'backend:topbar:profile-menu'\nexport const BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID: InjectionSpotId = 'backend:topbar:actions'\nexport const BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:nav'\nexport const BACKEND_SIDEBAR_NAV_FOOTER_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:nav:footer'\n\n// Standardized global status spot ids\nexport const GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID: InjectionSpotId = 'global:sidebar:status-badges'\nexport const GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID: InjectionSpotId = 'global:header:status-indicators'\n\n// Standardized pattern helpers\nexport const CrudFormInjectionSpots = {\n base: (entityId: string): InjectionSpotId => `crud-form:${entityId}`,\n beforeFields: (entityId: string): InjectionSpotId => `crud-form:${entityId}:before-fields`,\n afterFields: (entityId: string): InjectionSpotId => `crud-form:${entityId}:after-fields`,\n header: (entityId: string): InjectionSpotId => `crud-form:${entityId}:header`,\n footer: (entityId: string): InjectionSpotId => `crud-form:${entityId}:footer`,\n sidebar: (entityId: string): InjectionSpotId => `crud-form:${entityId}:sidebar`,\n group: (entityId: string, groupId: string): InjectionSpotId => `crud-form:${entityId}:group:${groupId}`,\n fieldBefore: (entityId: string, fieldId: string): InjectionSpotId => `crud-form:${entityId}:field:${fieldId}:before`,\n fieldAfter: (entityId: string, fieldId: string): InjectionSpotId => `crud-form:${entityId}:field:${fieldId}:after`,\n} as const\n\nexport const DataTableInjectionSpots = {\n header: (tableId: string): InjectionSpotId => `data-table:${tableId}:header`,\n footer: (tableId: string): InjectionSpotId => `data-table:${tableId}:footer`,\n toolbar: (tableId: string): InjectionSpotId => `data-table:${tableId}:toolbar`,\n emptyState: (tableId: string): InjectionSpotId => `data-table:${tableId}:empty-state`,\n} as const\n\nexport const DetailInjectionSpots = {\n header: (entityId: string): InjectionSpotId => `detail:${entityId}:header`,\n tabs: (entityId: string): InjectionSpotId => `detail:${entityId}:tabs`,\n sidebar: (entityId: string): InjectionSpotId => `detail:${entityId}:sidebar`,\n footer: (entityId: string): InjectionSpotId => `detail:${entityId}:footer`,\n statusBadges: (entityId: string): InjectionSpotId => `detail:${entityId}:status-badges`,\n} as const\n"],
5
+ "mappings": "AAEO,MAAM,2CAA4D;AAClE,MAAM,uCAAwD;AAC9D,MAAM,0CAA2D;AACjE,MAAM,wCAAyD;AAC/D,MAAM,2CAA4D;AAGlE,MAAM,gDAAiE;AACvE,MAAM,2CAA4D;AAClE,MAAM,wCAAyD;AAC/D,MAAM,+CAAgE;AAGtE,MAAM,iDAAkE;AACxE,MAAM,oDAAqE;AAG3E,MAAM,yBAAyB;AAAA,EACpC,MAAM,CAAC,aAAsC,aAAa,QAAQ;AAAA,EAClE,cAAc,CAAC,aAAsC,aAAa,QAAQ;AAAA,EAC1E,aAAa,CAAC,aAAsC,aAAa,QAAQ;AAAA,EACzE,QAAQ,CAAC,aAAsC,aAAa,QAAQ;AAAA,EACpE,QAAQ,CAAC,aAAsC,aAAa,QAAQ;AAAA,EACpE,SAAS,CAAC,aAAsC,aAAa,QAAQ;AAAA,EACrE,OAAO,CAAC,UAAkB,YAAqC,aAAa,QAAQ,UAAU,OAAO;AAAA,EACrG,aAAa,CAAC,UAAkB,YAAqC,aAAa,QAAQ,UAAU,OAAO;AAAA,EAC3G,YAAY,CAAC,UAAkB,YAAqC,aAAa,QAAQ,UAAU,OAAO;AAC5G;AAEO,MAAM,0BAA0B;AAAA,EACrC,QAAQ,CAAC,YAAqC,cAAc,OAAO;AAAA,EACnE,QAAQ,CAAC,YAAqC,cAAc,OAAO;AAAA,EACnE,SAAS,CAAC,YAAqC,cAAc,OAAO;AAAA,EACpE,YAAY,CAAC,YAAqC,cAAc,OAAO;AACzE;AAEO,MAAM,uBAAuB;AAAA,EAClC,QAAQ,CAAC,aAAsC,UAAU,QAAQ;AAAA,EACjE,MAAM,CAAC,aAAsC,UAAU,QAAQ;AAAA,EAC/D,SAAS,CAAC,aAAsC,UAAU,QAAQ;AAAA,EAClE,QAAQ,CAAC,aAAsC,UAAU,QAAQ;AAAA,EACjE,cAAc,CAAC,aAAsC,UAAU,QAAQ;AACzE;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "react";
3
+ const APP_EVENT_DOM_NAME = "om:event";
4
+ function matchesPattern(pattern, eventId) {
5
+ if (pattern === "*") return true;
6
+ if (!pattern.includes("*")) return pattern === eventId;
7
+ const escapedPattern = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
8
+ const regex = new RegExp(
9
+ "^" + escapedPattern + "$"
10
+ );
11
+ return regex.test(eventId);
12
+ }
13
+ function useAppEvent(eventPattern, handler, deps = []) {
14
+ const handlerRef = useRef(handler);
15
+ handlerRef.current = handler;
16
+ useEffect(() => {
17
+ const listener = (e) => {
18
+ const detail = e.detail;
19
+ if (!detail || typeof detail.id !== "string") return;
20
+ if (matchesPattern(eventPattern, detail.id)) {
21
+ handlerRef.current(detail);
22
+ }
23
+ };
24
+ window.addEventListener(APP_EVENT_DOM_NAME, listener);
25
+ return () => {
26
+ window.removeEventListener(APP_EVENT_DOM_NAME, listener);
27
+ };
28
+ }, [eventPattern, ...deps]);
29
+ }
30
+ export {
31
+ APP_EVENT_DOM_NAME,
32
+ matchesPattern,
33
+ useAppEvent
34
+ };
35
+ //# sourceMappingURL=useAppEvent.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/injection/useAppEvent.ts"],
4
+ "sourcesContent": ["\"use client\"\nimport { useEffect, useRef } from 'react'\nimport type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'\n\n/**\n * DOM Event Bridge event name.\n * Server-side events with `clientBroadcast: true` are dispatched as\n * CustomEvents with this name on the window object.\n */\nexport const APP_EVENT_DOM_NAME = 'om:event'\n\n/**\n * Match an event ID against a wildcard pattern.\n *\n * Supports:\n * - Exact match: `'example.todo.created'`\n * - Wildcard suffix: `'example.todo.*'` matches `'example.todo.created'`, `'example.todo.updated'`\n * - Global wildcard: `'*'` matches everything\n *\n * @example\n * matchesPattern('example.todo.*', 'example.todo.created') // true\n * matchesPattern('example.todo.*', 'example.item.created') // false\n * matchesPattern('*', 'anything.here') // true\n */\nexport function matchesPattern(pattern: string, eventId: string): boolean {\n if (pattern === '*') return true\n if (!pattern.includes('*')) return pattern === eventId\n const escapedPattern = pattern\n .replace(/[|\\\\{}()[\\]^$+?.]/g, '\\\\$&')\n .replace(/\\*/g, '.*')\n const regex = new RegExp(\n '^' + escapedPattern + '$',\n )\n return regex.test(eventId)\n}\n\n/**\n * React hook that listens for app events delivered via the DOM Event Bridge.\n *\n * Events are dispatched as `om:event` CustomEvents on `window` by the event bridge\n * (see `eventBridge.ts`). This hook filters events by pattern and calls the handler.\n *\n * @param eventPattern - Pattern to match event IDs against (e.g., 'example.todo.*')\n * @param handler - Callback invoked when a matching event arrives\n * @param deps - Optional dependency array for the handler (defaults to [])\n *\n * @example\n * useAppEvent('example.todo.*', (event) => {\n * console.log('Todo event:', event.id, event.payload)\n * setRefreshKey(k => k + 1)\n * })\n */\nexport function useAppEvent(\n eventPattern: string,\n handler: (payload: AppEventPayload) => void,\n deps: unknown[] = [],\n): void {\n const handlerRef = useRef(handler)\n handlerRef.current = handler\n\n useEffect(() => {\n const listener = (e: Event) => {\n const detail = (e as CustomEvent<AppEventPayload>).detail\n if (!detail || typeof detail.id !== 'string') return\n if (matchesPattern(eventPattern, detail.id)) {\n handlerRef.current(detail)\n }\n }\n\n window.addEventListener(APP_EVENT_DOM_NAME, listener)\n return () => {\n window.removeEventListener(APP_EVENT_DOM_NAME, listener)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [eventPattern, ...deps])\n}\n"],
5
+ "mappings": ";AACA,SAAS,WAAW,cAAc;AAQ3B,MAAM,qBAAqB;AAe3B,SAAS,eAAe,SAAiB,SAA0B;AACxE,MAAI,YAAY,IAAK,QAAO;AAC5B,MAAI,CAAC,QAAQ,SAAS,GAAG,EAAG,QAAO,YAAY;AAC/C,QAAM,iBAAiB,QACpB,QAAQ,sBAAsB,MAAM,EACpC,QAAQ,OAAO,IAAI;AACtB,QAAM,QAAQ,IAAI;AAAA,IAChB,MAAM,iBAAiB;AAAA,EACzB;AACA,SAAO,MAAM,KAAK,OAAO;AAC3B;AAkBO,SAAS,YACd,cACA,SACA,OAAkB,CAAC,GACb;AACN,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AAErB,YAAU,MAAM;AACd,UAAM,WAAW,CAAC,MAAa;AAC7B,YAAM,SAAU,EAAmC;AACnD,UAAI,CAAC,UAAU,OAAO,OAAO,OAAO,SAAU;AAC9C,UAAI,eAAe,cAAc,OAAO,EAAE,GAAG;AAC3C,mBAAW,QAAQ,MAAM;AAAA,MAC3B;AAAA,IACF;AAEA,WAAO,iBAAiB,oBAAoB,QAAQ;AACpD,WAAO,MAAM;AACX,aAAO,oBAAoB,oBAAoB,QAAQ;AAAA,IACzD;AAAA,EAEF,GAAG,CAAC,cAAc,GAAG,IAAI,CAAC;AAC5B;",
6
+ "names": []
7
+ }
@@ -0,0 +1,92 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useInjectionDataWidgets } from "./useInjectionDataWidgets.js";
4
+ import { apiCall } from "../utils/apiCall.js";
5
+ function collectRequiredFeatures(items) {
6
+ const set = /* @__PURE__ */ new Set();
7
+ for (const item of items) {
8
+ for (const feature of item.features ?? []) {
9
+ if (!feature || feature.trim().length === 0) continue;
10
+ set.add(feature);
11
+ }
12
+ }
13
+ return Array.from(set);
14
+ }
15
+ async function readGrantedFeatures(features) {
16
+ if (features.length === 0) return /* @__PURE__ */ new Set();
17
+ const call = await apiCall("/api/auth/feature-check", {
18
+ method: "POST",
19
+ headers: { "content-type": "application/json" },
20
+ body: JSON.stringify({ features })
21
+ });
22
+ if (!call.ok) return /* @__PURE__ */ new Set();
23
+ return new Set(call.result?.granted ?? []);
24
+ }
25
+ async function readUserRoles() {
26
+ const call = await apiCall("/api/auth/profile");
27
+ if (!call.ok) return /* @__PURE__ */ new Set();
28
+ const roles = Array.isArray(call.result?.roles) ? call.result.roles : [];
29
+ return new Set(roles.filter((role) => typeof role === "string" && role.trim().length > 0));
30
+ }
31
+ function useInjectedMenuItems(surfaceId) {
32
+ const { widgets, isLoading } = useInjectionDataWidgets(surfaceId);
33
+ const [grantedFeatures, setGrantedFeatures] = React.useState(/* @__PURE__ */ new Set());
34
+ const [userRoles, setUserRoles] = React.useState(/* @__PURE__ */ new Set());
35
+ const rawItems = React.useMemo(() => {
36
+ const entries = [];
37
+ for (const widget of widgets) {
38
+ if (!("menuItems" in widget)) continue;
39
+ const metadataFeatures = widget.metadata.features ?? [];
40
+ for (const menuItem of widget.menuItems) {
41
+ const features = [...metadataFeatures, ...menuItem.features ?? []];
42
+ const normalizedLabelKey = menuItem.labelKey ?? (typeof menuItem.label === "string" && menuItem.label.includes(".") ? menuItem.label : void 0);
43
+ entries.push({
44
+ ...menuItem,
45
+ labelKey: normalizedLabelKey,
46
+ features
47
+ });
48
+ }
49
+ }
50
+ return entries;
51
+ }, [widgets]);
52
+ React.useEffect(() => {
53
+ let mounted = true;
54
+ const run = async () => {
55
+ const features = collectRequiredFeatures(rawItems);
56
+ const next = await readGrantedFeatures(features);
57
+ if (!mounted) return;
58
+ setGrantedFeatures(next);
59
+ };
60
+ void run();
61
+ return () => {
62
+ mounted = false;
63
+ };
64
+ }, [rawItems]);
65
+ React.useEffect(() => {
66
+ let mounted = true;
67
+ const run = async () => {
68
+ const roles = await readUserRoles();
69
+ if (!mounted) return;
70
+ setUserRoles(roles);
71
+ };
72
+ void run();
73
+ return () => {
74
+ mounted = false;
75
+ };
76
+ }, []);
77
+ const items = React.useMemo(
78
+ () => rawItems.filter((item) => {
79
+ const features = item.features ?? [];
80
+ const roles = item.roles ?? [];
81
+ const featuresOk = features.length === 0 || features.every((feature) => grantedFeatures.has(feature));
82
+ const rolesOk = roles.length === 0 || roles.some((role) => userRoles.has(role));
83
+ return featuresOk && rolesOk;
84
+ }),
85
+ [rawItems, grantedFeatures, userRoles]
86
+ );
87
+ return { items, isLoading };
88
+ }
89
+ export {
90
+ useInjectedMenuItems
91
+ };
92
+ //# sourceMappingURL=useInjectedMenuItems.js.map