@open-mercato/ui 0.6.7 → 0.6.8-develop.6874.1.982d6097d8

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 (214) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +4 -2
  3. package/agentic/standalone-guide.md +1 -1
  4. package/build.mjs +2 -54
  5. package/dist/ai/AiChat.js +26 -7
  6. package/dist/ai/AiChat.js.map +2 -2
  7. package/dist/ai/LoopDisabledBanner.js +1 -1
  8. package/dist/ai/LoopDisabledBanner.js.map +2 -2
  9. package/dist/ai/parts/ConfirmationCard.js +4 -4
  10. package/dist/ai/parts/ConfirmationCard.js.map +2 -2
  11. package/dist/ai/parts/FieldDiffCard.js +2 -2
  12. package/dist/ai/parts/FieldDiffCard.js.map +2 -2
  13. package/dist/ai/parts/MutationResultCard.js +3 -3
  14. package/dist/ai/parts/MutationResultCard.js.map +2 -2
  15. package/dist/ai/ui-parts/pending-phase3-placeholder.js +1 -1
  16. package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +2 -2
  17. package/dist/backend/AppShell.js +43 -4
  18. package/dist/backend/AppShell.js.map +2 -2
  19. package/dist/backend/AuthSessionGuard.js +14 -0
  20. package/dist/backend/AuthSessionGuard.js.map +2 -2
  21. package/dist/backend/BackendChromeProvider.js +6 -1
  22. package/dist/backend/BackendChromeProvider.js.map +2 -2
  23. package/dist/backend/CrudForm.js +6 -4
  24. package/dist/backend/CrudForm.js.map +2 -2
  25. package/dist/backend/DataTable.js +20 -18
  26. package/dist/backend/DataTable.js.map +2 -2
  27. package/dist/backend/JsonBuilder.js +3 -3
  28. package/dist/backend/JsonBuilder.js.map +2 -2
  29. package/dist/backend/NextStepCallout.js +4 -4
  30. package/dist/backend/NextStepCallout.js.map +1 -1
  31. package/dist/backend/PerspectiveSidebar.js +1 -1
  32. package/dist/backend/PerspectiveSidebar.js.map +2 -2
  33. package/dist/backend/ProfileDropdown.js +2 -1
  34. package/dist/backend/ProfileDropdown.js.map +2 -2
  35. package/dist/backend/RowActions.js +2 -2
  36. package/dist/backend/RowActions.js.map +2 -2
  37. package/dist/backend/confirm-dialog/ConfirmDialog.js +13 -1
  38. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  39. package/dist/backend/crud/useGroupOrder.js +14 -21
  40. package/dist/backend/crud/useGroupOrder.js.map +2 -2
  41. package/dist/backend/custom-fields/FieldDefinitionsManager.js +1 -1
  42. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +2 -2
  43. package/dist/backend/dashboard/DashboardScreen.js +3 -3
  44. package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
  45. package/dist/backend/detail/AddressTiles.js +4 -5
  46. package/dist/backend/detail/AddressTiles.js.map +2 -2
  47. package/dist/backend/detail/AttachmentDeleteDialog.js +1 -1
  48. package/dist/backend/detail/AttachmentDeleteDialog.js.map +2 -2
  49. package/dist/backend/detail/AttachmentMetadataDialog.js.map +1 -1
  50. package/dist/backend/detail/AttachmentsSection.js +1 -1
  51. package/dist/backend/detail/AttachmentsSection.js.map +2 -2
  52. package/dist/backend/detail/NotesSection.js +1 -1
  53. package/dist/backend/detail/NotesSection.js.map +2 -2
  54. package/dist/backend/detail/TagsSection.js +1 -1
  55. package/dist/backend/detail/TagsSection.js.map +2 -2
  56. package/dist/backend/filters/AdvancedFilterPanel.js +1 -1
  57. package/dist/backend/filters/AdvancedFilterPanel.js.map +2 -2
  58. package/dist/backend/forms/FormHeader.js +1 -2
  59. package/dist/backend/forms/FormHeader.js.map +2 -2
  60. package/dist/backend/icons/lucideRegistry.generated.js +1 -25
  61. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  62. package/dist/backend/icons/lucideRegistry.js +3 -3
  63. package/dist/backend/icons/lucideRegistry.js.map +2 -2
  64. package/dist/backend/icons/lucideRegistryRuntime.js +28 -0
  65. package/dist/backend/icons/lucideRegistryRuntime.js.map +7 -0
  66. package/dist/backend/injection/spotIds.js +23 -18
  67. package/dist/backend/injection/spotIds.js.map +2 -2
  68. package/dist/backend/inputs/ComboboxInput.js.map +1 -1
  69. package/dist/backend/messages/EmailThreadsPanel.js +1 -1
  70. package/dist/backend/messages/EmailThreadsPanel.js.map +2 -2
  71. package/dist/backend/perspectiveState.js +34 -0
  72. package/dist/backend/perspectiveState.js.map +7 -0
  73. package/dist/backend/schedule/ScheduleAgenda.js +3 -3
  74. package/dist/backend/schedule/ScheduleAgenda.js.map +1 -1
  75. package/dist/backend/schedule/ScheduleGrid.js +3 -3
  76. package/dist/backend/schedule/ScheduleGrid.js.map +1 -1
  77. package/dist/backend/schedule/ScheduleToolbar.js +87 -70
  78. package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
  79. package/dist/backend/sidebar/SidebarCustomizationEditor.js +30 -2
  80. package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
  81. package/dist/backend/sidebar/customization-helpers.js +30 -0
  82. package/dist/backend/sidebar/customization-helpers.js.map +2 -2
  83. package/dist/backend/utils/apiCall.js +21 -1
  84. package/dist/backend/utils/apiCall.js.map +2 -2
  85. package/dist/backend/utils/customFieldDefs.js.map +2 -2
  86. package/dist/backend/utils/customFieldForms.js +10 -9
  87. package/dist/backend/utils/customFieldForms.js.map +2 -2
  88. package/dist/backend/utils/fetchAllPages.js +57 -0
  89. package/dist/backend/utils/fetchAllPages.js.map +7 -0
  90. package/dist/backend/utils/nav.js +5 -2
  91. package/dist/backend/utils/nav.js.map +2 -2
  92. package/dist/backend/version-history/VersionHistoryPanel.js +1 -1
  93. package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
  94. package/dist/frontend/LanguageSwitcher.js +2 -1
  95. package/dist/frontend/LanguageSwitcher.js.map +2 -2
  96. package/dist/portal/PortalContext.js +1 -1
  97. package/dist/portal/PortalContext.js.map +2 -2
  98. package/dist/portal/hooks/portalBridgeStatus.js +25 -0
  99. package/dist/portal/hooks/portalBridgeStatus.js.map +7 -0
  100. package/dist/portal/hooks/usePortalEventBridge.js +6 -0
  101. package/dist/portal/hooks/usePortalEventBridge.js.map +2 -2
  102. package/dist/portal/hooks/usePortalNotifications.js +42 -4
  103. package/dist/portal/hooks/usePortalNotifications.js.map +2 -2
  104. package/dist/portal/utils/nav.js +2 -3
  105. package/dist/portal/utils/nav.js.map +2 -2
  106. package/dist/primitives/ErrorNotice.js +1 -1
  107. package/dist/primitives/ErrorNotice.js.map +2 -2
  108. package/dist/primitives/Notice.js +1 -1
  109. package/dist/primitives/Notice.js.map +1 -1
  110. package/dist/primitives/button.js +9 -4
  111. package/dist/primitives/button.js.map +2 -2
  112. package/dist/theme/ThemeProvider.js +1 -1
  113. package/dist/theme/ThemeProvider.js.map +2 -2
  114. package/dist/theme/index.js +3 -0
  115. package/dist/theme/index.js.map +2 -2
  116. package/dist/theme/theme-init-script.js +17 -0
  117. package/dist/theme/theme-init-script.js.map +7 -0
  118. package/figma/alert.figma.tsx +35 -0
  119. package/figma/badge.figma.tsx +33 -0
  120. package/figma/button.figma.tsx +33 -0
  121. package/figma/checkbox.figma.tsx +28 -0
  122. package/figma/drawer.figma.tsx +40 -0
  123. package/figma/input-variants.figma.tsx +58 -0
  124. package/figma/radio.figma.tsx +16 -0
  125. package/figma/select.figma.tsx +24 -0
  126. package/figma/status-badge.figma.tsx +24 -0
  127. package/figma/switch.figma.tsx +28 -0
  128. package/figma/tabs.figma.tsx +22 -0
  129. package/figma/tag.figma.tsx +27 -0
  130. package/figma.config.json +6 -0
  131. package/jest.config.cjs +2 -1
  132. package/package.json +21 -18
  133. package/scripts/lucideRegistrySource.cjs +133 -0
  134. package/src/ai/AiChat.tsx +36 -7
  135. package/src/ai/LoopDisabledBanner.tsx +1 -1
  136. package/src/ai/__tests__/AiChat.test.tsx +31 -0
  137. package/src/ai/parts/ConfirmationCard.tsx +4 -4
  138. package/src/ai/parts/FieldDiffCard.tsx +2 -2
  139. package/src/ai/parts/MutationResultCard.tsx +3 -3
  140. package/src/ai/ui-parts/pending-phase3-placeholder.tsx +2 -2
  141. package/src/backend/AppShell.tsx +55 -4
  142. package/src/backend/AuthSessionGuard.tsx +29 -0
  143. package/src/backend/BackendChromeProvider.tsx +19 -1
  144. package/src/backend/CrudForm.tsx +18 -4
  145. package/src/backend/DataTable.tsx +36 -19
  146. package/src/backend/JsonBuilder.tsx +3 -3
  147. package/src/backend/NextStepCallout.tsx +4 -4
  148. package/src/backend/PerspectiveSidebar.tsx +1 -1
  149. package/src/backend/ProfileDropdown.tsx +1 -0
  150. package/src/backend/RowActions.tsx +2 -2
  151. package/src/backend/__tests__/AppShell.test.tsx +65 -7
  152. package/src/backend/__tests__/AuthSessionGuard.test.tsx +52 -0
  153. package/src/backend/__tests__/BackendChromeProvider.test.tsx +98 -2
  154. package/src/backend/__tests__/CrudForm.fieldsetBinding.test.tsx +158 -0
  155. package/src/backend/__tests__/CrudForm.groupInjectionColumn.test.tsx +98 -0
  156. package/src/backend/__tests__/DataTable.metaHiddenLateColumns.test.tsx +105 -0
  157. package/src/backend/__tests__/DataTable.perspectiveStorage.test.ts +62 -1
  158. package/src/backend/__tests__/nav-utils.test.ts +139 -0
  159. package/src/backend/confirm-dialog/ConfirmDialog.tsx +16 -1
  160. package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +68 -1
  161. package/src/backend/crud/__tests__/useGroupOrder.test.ts +39 -1
  162. package/src/backend/crud/useGroupOrder.ts +30 -25
  163. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +1 -1
  164. package/src/backend/dashboard/DashboardScreen.tsx +3 -3
  165. package/src/backend/detail/AddressTiles.tsx +3 -6
  166. package/src/backend/detail/AttachmentDeleteDialog.tsx +1 -1
  167. package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
  168. package/src/backend/detail/AttachmentsSection.tsx +1 -1
  169. package/src/backend/detail/NotesSection.tsx +1 -1
  170. package/src/backend/detail/TagsSection.tsx +1 -1
  171. package/src/backend/detail/__tests__/AttachmentAssignmentsEditor.test.tsx +21 -0
  172. package/src/backend/filters/AdvancedFilterPanel.tsx +1 -1
  173. package/src/backend/forms/FormHeader.tsx +1 -2
  174. package/src/backend/icons/__tests__/lucideRegistryGenerator.test.ts +177 -0
  175. package/src/backend/icons/lucideRegistry.generated.tsx +3 -37
  176. package/src/backend/icons/lucideRegistry.ts +2 -2
  177. package/src/backend/icons/lucideRegistryRuntime.tsx +36 -0
  178. package/src/backend/injection/__tests__/spotIds.extension-points.test.ts +38 -0
  179. package/src/backend/injection/spotIds.ts +23 -18
  180. package/src/backend/inputs/ComboboxInput.tsx +1 -1
  181. package/src/backend/messages/EmailThreadsPanel.tsx +1 -1
  182. package/src/backend/perspectiveState.ts +44 -0
  183. package/src/backend/schedule/ScheduleAgenda.tsx +3 -3
  184. package/src/backend/schedule/ScheduleGrid.tsx +3 -3
  185. package/src/backend/schedule/ScheduleToolbar.tsx +89 -68
  186. package/src/backend/sidebar/SidebarCustomizationEditor.tsx +42 -3
  187. package/src/backend/sidebar/__tests__/group-visibility.test.ts +131 -0
  188. package/src/backend/sidebar/customization-helpers.ts +47 -0
  189. package/src/backend/utils/__tests__/apiCall.test.ts +61 -0
  190. package/src/backend/utils/__tests__/customFieldForms.optionsPaging.test.ts +95 -0
  191. package/src/backend/utils/__tests__/fetchAllPages.test.ts +103 -0
  192. package/src/backend/utils/apiCall.ts +29 -1
  193. package/src/backend/utils/customFieldDefs.ts +1 -0
  194. package/src/backend/utils/customFieldForms.ts +15 -11
  195. package/src/backend/utils/fetchAllPages.ts +93 -0
  196. package/src/backend/utils/nav.ts +22 -2
  197. package/src/backend/version-history/VersionHistoryPanel.tsx +1 -1
  198. package/src/frontend/LanguageSwitcher.tsx +1 -0
  199. package/src/portal/PortalContext.tsx +1 -1
  200. package/src/portal/hooks/__tests__/usePortalEventBridge.test.tsx +66 -0
  201. package/src/portal/hooks/__tests__/usePortalNotifications.test.tsx +155 -0
  202. package/src/portal/hooks/portalBridgeStatus.ts +29 -0
  203. package/src/portal/hooks/usePortalEventBridge.ts +6 -0
  204. package/src/portal/hooks/usePortalNotifications.ts +55 -12
  205. package/src/portal/utils/__tests__/nav.test.ts +2 -2
  206. package/src/portal/utils/nav.ts +3 -4
  207. package/src/primitives/ErrorNotice.tsx +1 -1
  208. package/src/primitives/Notice.tsx +5 -5
  209. package/src/primitives/__tests__/button.test.tsx +70 -0
  210. package/src/primitives/button.tsx +10 -4
  211. package/src/theme/ThemeProvider.tsx +1 -2
  212. package/src/theme/__tests__/theme-init-script.test.ts +70 -0
  213. package/src/theme/index.ts +1 -0
  214. package/src/theme/theme-init-script.ts +23 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/version-history/VersionHistoryPanel.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ChevronLeft, Clock, Loader2, RotateCcw, Undo2, X } from 'lucide-react'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport type { VersionHistoryEntry } from './types'\nimport { VersionHistoryDetail } from './VersionHistoryDetail'\nimport { formatDate } from '@open-mercato/core/modules/audit_logs/lib/display-helpers'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'\nimport { getVersionHistoryActionLabel, getVersionHistoryStatusLabel } from './labels'\nimport { useAuditPermissions, canUndoEntry, canRedoEntry } from './useAuditPermissions'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { humanizeResourceKind } from './labels'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\nexport type VersionHistoryPanelProps = {\n open: boolean\n onOpenChange: (open: boolean) => void\n entries: VersionHistoryEntry[]\n isLoading: boolean\n error: string | null\n hasMore: boolean\n onLoadMore: () => void\n t: TranslateFn\n /** Explicit override \u2014 when provided, skips auto-check and uses this value directly. */\n canUndoRedo?: boolean\n /** When true (default), auto-checks audit_logs features for the current user. Ignored when canUndoRedo is provided. */\n autoCheckAcl?: boolean\n}\n\nexport function VersionHistoryPanel({\n open,\n onOpenChange,\n entries,\n isLoading,\n error,\n hasMore,\n onLoadMore,\n t,\n canUndoRedo,\n autoCheckAcl = true,\n}: VersionHistoryPanelProps) {\n const shouldAutoCheck = canUndoRedo === undefined && autoCheckAcl\n const permissions = useAuditPermissions(shouldAutoCheck && open)\n\n const visibleEntries = React.useMemo(() => {\n if (canUndoRedo !== undefined || !shouldAutoCheck) return entries\n if (permissions.isLoading) return entries\n if (permissions.canViewTenant) return entries\n if (!permissions.currentUserId) return entries\n return entries.filter((entry) => entry.actorUserId === permissions.currentUserId)\n }, [entries, canUndoRedo, shouldAutoCheck, permissions])\n\n const [selectedEntry, setSelectedEntry] = React.useState<VersionHistoryEntry | null>(null)\n const [undoingToken, setUndoingToken] = React.useState<string | null>(null)\n const [redoingId, setRedoingId] = React.useState<string | null>(null)\n const latestUndoableId = React.useMemo(() => {\n const latest = visibleEntries.find((entry) => entry.undoToken && entry.executionState === 'done')\n return latest?.id ?? null\n }, [visibleEntries])\n const latestUndoneId = React.useMemo(() => {\n const undone = visibleEntries.filter((entry) => entry.executionState === 'undone')\n if (!undone.length) return null\n const sorted = [...undone].sort((a, b) => {\n const aTs = Date.parse(a.updatedAt)\n const bTs = Date.parse(b.updatedAt)\n return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0)\n })\n return sorted[0]?.id ?? null\n }, [visibleEntries])\n\n React.useEffect(() => {\n if (!open) setSelectedEntry(null)\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n const prev = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = prev\n }\n }, [open])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && open) {\n onOpenChange(false)\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [open, onOpenChange])\n\n const handleUndo = React.useCallback(async (token: string | null) => {\n if (!token) return\n setUndoingToken(token)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ undoToken: token }),\n }, { errorMessage: t('audit_logs.error.undo') })\n markUndoSuccess(token)\n if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n logger.error('Undo failed', { err })\n } finally {\n setUndoingToken(null)\n }\n }, [t])\n\n const handleRedo = React.useCallback(async (logId: string | null) => {\n if (!logId) return\n setRedoingId(logId)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/redo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ logId }),\n }, { errorMessage: t('audit_logs.error.redo') })\n markRedoConsumed(logId)\n if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n logger.error('Redo failed', { err })\n } finally {\n setRedoingId(null)\n }\n }, [t])\n\n if (!open) return null\n\n const isEmpty = visibleEntries.length === 0 && !isLoading && !error\n const isInitialLoading = visibleEntries.length === 0 && isLoading\n const isInitialError = visibleEntries.length === 0 && !!error\n\n return (\n <>\n <div\n className=\"fixed inset-0 z-overlay bg-black/20\"\n onClick={() => onOpenChange(false)}\n aria-hidden=\"true\"\n />\n <div\n className=\"fixed right-0 top-0 z-modal h-full w-full max-w-md border-l bg-background shadow-lg\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('audit_logs.version_history.title')}\n >\n <div className=\"flex h-full flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n {selectedEntry ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setSelectedEntry(null)}\n aria-label={t('audit_logs.version_history.detail.back')}\n >\n <ChevronLeft className=\"h-5 w-5\" />\n </Button>\n ) : (\n <Clock className=\"h-5 w-5\" />\n )}\n <h2 className=\"font-semibold\">\n {selectedEntry\n ? t('audit_logs.version_history.detail.title')\n : t('audit_logs.version_history.title')}\n </h2>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => onOpenChange(false)}\n aria-label={t('audit_logs.version_history.close')}\n >\n <X className=\"h-5 w-5\" />\n </Button>\n </div>\n\n <div className=\"flex-1 overflow-y-auto px-4 py-4\">\n {selectedEntry ? (\n <VersionHistoryDetail entry={selectedEntry} t={t} />\n ) : (\n <div className=\"space-y-3\">\n {shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? (\n <Alert variant=\"info\">\n <AlertDescription>\n {t('audit_logs.hint.view_self_only', 'Showing only your own changes. Contact an administrator for broader access.')}\n </AlertDescription>\n </Alert>\n ) : null}\n\n {isInitialLoading ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Loader2 className=\"mb-2 h-8 w-8 animate-spin\" />\n <p>{t('audit_logs.version_history.loading')}</p>\n </div>\n ) : null}\n\n {isInitialError ? (\n <div className=\"flex flex-col items-center justify-center gap-2 py-12 text-muted-foreground\">\n <p>{t('audit_logs.version_history.error')}</p>\n <Button variant=\"ghost\" size=\"sm\" onClick={onLoadMore}>\n {t('audit_logs.common.refresh')}\n </Button>\n </div>\n ) : null}\n\n {isEmpty ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Clock className=\"mb-2 h-8 w-8 opacity-50\" />\n <p>{t('audit_logs.version_history.empty')}</p>\n </div>\n ) : null}\n\n {visibleEntries.length > 0 ? (\n <div className=\"divide-y rounded-lg border\">\n {visibleEntries.map((entry) => {\n const statusLabel = getVersionHistoryStatusLabel(entry.executionState, t)\n const actionLabel = getVersionHistoryActionLabel(entry, t)\n const isRelatedEntry = entry.parentResourceKind != null\n const entryCanUndo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canUndoEntry(permissions, entry.actorUserId) : true)\n const entryCanRedo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canRedoEntry(permissions, entry.actorUserId) : true)\n const canUndo = entryCanUndo\n && Boolean(entry.undoToken)\n && entry.executionState === 'done'\n && entry.id === latestUndoableId\n const showRedo = entryCanRedo && entry.executionState === 'undone'\n const canRedo = showRedo && entry.id === latestUndoneId\n return (\n <div\n key={entry.id}\n className={`flex items-start justify-between gap-3 py-3 transition-colors hover:bg-muted/50 ${isRelatedEntry ? 'pl-8 pr-4 border-l-2 border-l-muted-foreground/20' : 'px-4'}`}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"h-auto min-w-0 flex-1 flex-col items-start justify-start gap-1 whitespace-normal px-0 py-0 text-left hover:bg-transparent\"\n onClick={() => setSelectedEntry(entry)}\n >\n {isRelatedEntry ? (\n <span className=\"text-overline uppercase tracking-wider text-muted-foreground/70 font-medium\">\n {humanizeResourceKind(entry.resourceKind, t)}\n </span>\n ) : null}\n <div className=\"break-words text-sm font-medium\">\n {actionLabel}\n </div>\n <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 text-xs text-muted-foreground\">\n <span>{entry.actorUserName || entry.actorUserId || t('audit_logs.common.none')}</span>\n <span>\u2022</span>\n <span>{formatDate(entry.createdAt)}</span>\n <span>\u2022</span>\n <span>{statusLabel}</span>\n </div>\n </Button>\n <div className=\"flex items-center gap-1 pt-1\">\n {canUndo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.undo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleUndo(entry.undoToken)\n }}\n disabled={undoingToken === entry.undoToken || Boolean(redoingId)}\n >\n <Undo2 className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.undo')}</span>\n </Button>\n ) : null}\n {showRedo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.redo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleRedo(entry.id)\n }}\n disabled={!canRedo || redoingId === entry.id || Boolean(undoingToken)}\n >\n <RotateCcw className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.redo')}</span>\n </Button>\n ) : null}\n </div>\n </div>\n )\n })}\n </div>\n ) : null}\n\n {error && visibleEntries.length > 0 ? (\n <div className=\"text-xs text-red-500\">{error}</div>\n ) : null}\n\n {hasMore ? (\n <div className=\"pt-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={onLoadMore}\n disabled={isLoading}\n >\n {isLoading ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : null}\n {t('audit_logs.version_history.load_more')}\n </Button>\n </div>\n ) : null}\n </div>\n )}\n </div>\n </div>\n </div>\n </>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ChevronLeft, Clock, Loader2, RotateCcw, Undo2, X } from 'lucide-react'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport type { VersionHistoryEntry } from './types'\nimport { VersionHistoryDetail } from './VersionHistoryDetail'\nimport { formatDate } from '@open-mercato/core/modules/audit_logs/lib/display-helpers'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'\nimport { getVersionHistoryActionLabel, getVersionHistoryStatusLabel } from './labels'\nimport { useAuditPermissions, canUndoEntry, canRedoEntry } from './useAuditPermissions'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { humanizeResourceKind } from './labels'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\nexport type VersionHistoryPanelProps = {\n open: boolean\n onOpenChange: (open: boolean) => void\n entries: VersionHistoryEntry[]\n isLoading: boolean\n error: string | null\n hasMore: boolean\n onLoadMore: () => void\n t: TranslateFn\n /** Explicit override \u2014 when provided, skips auto-check and uses this value directly. */\n canUndoRedo?: boolean\n /** When true (default), auto-checks audit_logs features for the current user. Ignored when canUndoRedo is provided. */\n autoCheckAcl?: boolean\n}\n\nexport function VersionHistoryPanel({\n open,\n onOpenChange,\n entries,\n isLoading,\n error,\n hasMore,\n onLoadMore,\n t,\n canUndoRedo,\n autoCheckAcl = true,\n}: VersionHistoryPanelProps) {\n const shouldAutoCheck = canUndoRedo === undefined && autoCheckAcl\n const permissions = useAuditPermissions(shouldAutoCheck && open)\n\n const visibleEntries = React.useMemo(() => {\n if (canUndoRedo !== undefined || !shouldAutoCheck) return entries\n if (permissions.isLoading) return entries\n if (permissions.canViewTenant) return entries\n if (!permissions.currentUserId) return entries\n return entries.filter((entry) => entry.actorUserId === permissions.currentUserId)\n }, [entries, canUndoRedo, shouldAutoCheck, permissions])\n\n const [selectedEntry, setSelectedEntry] = React.useState<VersionHistoryEntry | null>(null)\n const [undoingToken, setUndoingToken] = React.useState<string | null>(null)\n const [redoingId, setRedoingId] = React.useState<string | null>(null)\n const latestUndoableId = React.useMemo(() => {\n const latest = visibleEntries.find((entry) => entry.undoToken && entry.executionState === 'done')\n return latest?.id ?? null\n }, [visibleEntries])\n const latestUndoneId = React.useMemo(() => {\n const undone = visibleEntries.filter((entry) => entry.executionState === 'undone')\n if (!undone.length) return null\n const sorted = [...undone].sort((a, b) => {\n const aTs = Date.parse(a.updatedAt)\n const bTs = Date.parse(b.updatedAt)\n return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0)\n })\n return sorted[0]?.id ?? null\n }, [visibleEntries])\n\n React.useEffect(() => {\n if (!open) setSelectedEntry(null)\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n const prev = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = prev\n }\n }, [open])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && open) {\n onOpenChange(false)\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [open, onOpenChange])\n\n const handleUndo = React.useCallback(async (token: string | null) => {\n if (!token) return\n setUndoingToken(token)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ undoToken: token }),\n }, { errorMessage: t('audit_logs.error.undo') })\n markUndoSuccess(token)\n if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n logger.error('Undo failed', { err })\n } finally {\n setUndoingToken(null)\n }\n }, [t])\n\n const handleRedo = React.useCallback(async (logId: string | null) => {\n if (!logId) return\n setRedoingId(logId)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/redo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ logId }),\n }, { errorMessage: t('audit_logs.error.redo') })\n markRedoConsumed(logId)\n if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n logger.error('Redo failed', { err })\n } finally {\n setRedoingId(null)\n }\n }, [t])\n\n if (!open) return null\n\n const isEmpty = visibleEntries.length === 0 && !isLoading && !error\n const isInitialLoading = visibleEntries.length === 0 && isLoading\n const isInitialError = visibleEntries.length === 0 && !!error\n\n return (\n <>\n <div\n className=\"fixed inset-0 z-overlay bg-black/20\"\n onClick={() => onOpenChange(false)}\n aria-hidden=\"true\"\n />\n <div\n className=\"fixed right-0 top-0 z-modal h-full w-full max-w-md border-l bg-background shadow-lg\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('audit_logs.version_history.title')}\n >\n <div className=\"flex h-full flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n {selectedEntry ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setSelectedEntry(null)}\n aria-label={t('audit_logs.version_history.detail.back')}\n >\n <ChevronLeft className=\"h-5 w-5\" />\n </Button>\n ) : (\n <Clock className=\"h-5 w-5\" />\n )}\n <h2 className=\"font-semibold\">\n {selectedEntry\n ? t('audit_logs.version_history.detail.title')\n : t('audit_logs.version_history.title')}\n </h2>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => onOpenChange(false)}\n aria-label={t('audit_logs.version_history.close')}\n >\n <X className=\"h-5 w-5\" />\n </Button>\n </div>\n\n <div className=\"flex-1 overflow-y-auto px-4 py-4\">\n {selectedEntry ? (\n <VersionHistoryDetail entry={selectedEntry} t={t} />\n ) : (\n <div className=\"space-y-3\">\n {shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? (\n <Alert status=\"information\">\n <AlertDescription>\n {t('audit_logs.hint.view_self_only', 'Showing only your own changes. Contact an administrator for broader access.')}\n </AlertDescription>\n </Alert>\n ) : null}\n\n {isInitialLoading ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Loader2 className=\"mb-2 h-8 w-8 animate-spin\" />\n <p>{t('audit_logs.version_history.loading')}</p>\n </div>\n ) : null}\n\n {isInitialError ? (\n <div className=\"flex flex-col items-center justify-center gap-2 py-12 text-muted-foreground\">\n <p>{t('audit_logs.version_history.error')}</p>\n <Button variant=\"ghost\" size=\"sm\" onClick={onLoadMore}>\n {t('audit_logs.common.refresh')}\n </Button>\n </div>\n ) : null}\n\n {isEmpty ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Clock className=\"mb-2 h-8 w-8 opacity-50\" />\n <p>{t('audit_logs.version_history.empty')}</p>\n </div>\n ) : null}\n\n {visibleEntries.length > 0 ? (\n <div className=\"divide-y rounded-lg border\">\n {visibleEntries.map((entry) => {\n const statusLabel = getVersionHistoryStatusLabel(entry.executionState, t)\n const actionLabel = getVersionHistoryActionLabel(entry, t)\n const isRelatedEntry = entry.parentResourceKind != null\n const entryCanUndo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canUndoEntry(permissions, entry.actorUserId) : true)\n const entryCanRedo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canRedoEntry(permissions, entry.actorUserId) : true)\n const canUndo = entryCanUndo\n && Boolean(entry.undoToken)\n && entry.executionState === 'done'\n && entry.id === latestUndoableId\n const showRedo = entryCanRedo && entry.executionState === 'undone'\n const canRedo = showRedo && entry.id === latestUndoneId\n return (\n <div\n key={entry.id}\n className={`flex items-start justify-between gap-3 py-3 transition-colors hover:bg-muted/50 ${isRelatedEntry ? 'pl-8 pr-4 border-l-2 border-l-muted-foreground/20' : 'px-4'}`}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"h-auto min-w-0 flex-1 flex-col items-start justify-start gap-1 whitespace-normal px-0 py-0 text-left hover:bg-transparent\"\n onClick={() => setSelectedEntry(entry)}\n >\n {isRelatedEntry ? (\n <span className=\"text-overline uppercase tracking-wider text-muted-foreground/70 font-medium\">\n {humanizeResourceKind(entry.resourceKind, t)}\n </span>\n ) : null}\n <div className=\"break-words text-sm font-medium\">\n {actionLabel}\n </div>\n <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 text-xs text-muted-foreground\">\n <span>{entry.actorUserName || entry.actorUserId || t('audit_logs.common.none')}</span>\n <span>\u2022</span>\n <span>{formatDate(entry.createdAt)}</span>\n <span>\u2022</span>\n <span>{statusLabel}</span>\n </div>\n </Button>\n <div className=\"flex items-center gap-1 pt-1\">\n {canUndo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.undo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleUndo(entry.undoToken)\n }}\n disabled={undoingToken === entry.undoToken || Boolean(redoingId)}\n >\n <Undo2 className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.undo')}</span>\n </Button>\n ) : null}\n {showRedo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.redo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleRedo(entry.id)\n }}\n disabled={!canRedo || redoingId === entry.id || Boolean(undoingToken)}\n >\n <RotateCcw className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.redo')}</span>\n </Button>\n ) : null}\n </div>\n </div>\n )\n })}\n </div>\n ) : null}\n\n {error && visibleEntries.length > 0 ? (\n <div className=\"text-xs text-red-500\">{error}</div>\n ) : null}\n\n {hasMore ? (\n <div className=\"pt-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={onLoadMore}\n disabled={isLoading}\n >\n {isLoading ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : null}\n {t('audit_logs.version_history.load_more')}\n </Button>\n </div>\n ) : null}\n </div>\n )}\n </div>\n </div>\n </div>\n </>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -22,7 +22,8 @@ function LanguageSwitcher() {
22
22
  en: t("common.languages.english", "English"),
23
23
  pl: t("common.languages.polish", "Polski"),
24
24
  es: t("common.languages.spanish", "Espa\xF1ol"),
25
- de: t("common.languages.german", "Deutsch")
25
+ de: t("common.languages.german", "Deutsch"),
26
+ ko: t("common.languages.korean", "\uD55C\uAD6D\uC5B4")
26
27
  };
27
28
  async function setLocale(locale) {
28
29
  if (locale === current) return;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/frontend/LanguageSwitcher.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport { useId, useTransition } from 'react'\nimport { useLocale, useLocaleLocked, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useRouter } from 'next/navigation'\nimport { locales, type Locale } from '@open-mercato/shared/lib/i18n/config'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\n\nexport function LanguageSwitcher() {\n const current = useLocale()\n const localeLocked = useLocaleLocked()\n const t = useT()\n const router = useRouter()\n const [pending, startTransition] = useTransition()\n const selectId = useId()\n\n const languageLabels: Record<Locale, string> = {\n en: t('common.languages.english', 'English'),\n pl: t('common.languages.polish', 'Polski'),\n es: t('common.languages.spanish', 'Espa\u00F1ol'),\n de: t('common.languages.german', 'Deutsch'),\n }\n\n async function setLocale(locale: Locale) {\n if (locale === current) return\n try {\n const res = await fetch('/api/auth/locale', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ locale }),\n })\n if (!res.ok) return\n startTransition(() => router.refresh())\n try {\n window.dispatchEvent(new Event('om:refresh-sidebar'))\n } catch {\n // Ignore if window is unavailable\n }\n } catch {\n // Ignore network errors; UX fallback keeps previous locale\n }\n }\n\n // Locale is pinned via OM_FORCE_LOCALE \u2014 switching is a no-op, so hide the control.\n if (localeLocked) return null\n\n return (\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <label htmlFor={selectId}>{t('common.language')}</label>\n <Select\n value={current}\n onValueChange={(value) => setLocale(value as Locale)}\n disabled={pending}\n >\n <SelectTrigger id={selectId} size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {locales.map((locale) => (\n <SelectItem key={locale} value={locale}>\n {languageLabels[locale]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )\n}\n"],
5
- "mappings": ";AAqDM,cACA,YADA;AApDN,SAAS,OAAO,qBAAqB;AACrC,SAAS,WAAW,iBAAiB,YAAY;AACjD,SAAS,iBAAiB;AAC1B,SAAS,eAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,SAAS,mBAAmB;AACjC,QAAM,UAAU,UAAU;AAC1B,QAAM,eAAe,gBAAgB;AACrC,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,SAAS,eAAe,IAAI,cAAc;AACjD,QAAM,WAAW,MAAM;AAEvB,QAAM,iBAAyC;AAAA,IAC7C,IAAI,EAAE,4BAA4B,SAAS;AAAA,IAC3C,IAAI,EAAE,2BAA2B,QAAQ;AAAA,IACzC,IAAI,EAAE,4BAA4B,YAAS;AAAA,IAC3C,IAAI,EAAE,2BAA2B,SAAS;AAAA,EAC5C;AAEA,iBAAe,UAAU,QAAgB;AACvC,QAAI,WAAW,QAAS;AACxB,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,oBAAoB;AAAA,QAC1C,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC;AAAA,MACjC,CAAC;AACD,UAAI,CAAC,IAAI,GAAI;AACb,sBAAgB,MAAM,OAAO,QAAQ,CAAC;AACtC,UAAI;AACF,eAAO,cAAc,IAAI,MAAM,oBAAoB,CAAC;AAAA,MACtD,QAAQ;AAAA,MAER;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,MAAI,aAAc,QAAO;AAEzB,SACE,qBAAC,SAAI,WAAU,yDACb;AAAA,wBAAC,WAAM,SAAS,UAAW,YAAE,iBAAiB,GAAE;AAAA,IAChD;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP,eAAe,CAAC,UAAU,UAAU,KAAe;AAAA,QACnD,UAAU;AAAA,QAEV;AAAA,8BAAC,iBAAc,IAAI,UAAU,MAAK,MAChC,8BAAC,eAAY,GACf;AAAA,UACA,oBAAC,iBACE,kBAAQ,IAAI,CAAC,WACZ,oBAAC,cAAwB,OAAO,QAC7B,yBAAe,MAAM,KADP,MAEjB,CACD,GACH;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\nimport { useId, useTransition } from 'react'\nimport { useLocale, useLocaleLocked, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useRouter } from 'next/navigation'\nimport { locales, type Locale } from '@open-mercato/shared/lib/i18n/config'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\n\nexport function LanguageSwitcher() {\n const current = useLocale()\n const localeLocked = useLocaleLocked()\n const t = useT()\n const router = useRouter()\n const [pending, startTransition] = useTransition()\n const selectId = useId()\n\n const languageLabels: Record<Locale, string> = {\n en: t('common.languages.english', 'English'),\n pl: t('common.languages.polish', 'Polski'),\n es: t('common.languages.spanish', 'Espa\u00F1ol'),\n de: t('common.languages.german', 'Deutsch'),\n ko: t('common.languages.korean', '\uD55C\uAD6D\uC5B4'),\n }\n\n async function setLocale(locale: Locale) {\n if (locale === current) return\n try {\n const res = await fetch('/api/auth/locale', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ locale }),\n })\n if (!res.ok) return\n startTransition(() => router.refresh())\n try {\n window.dispatchEvent(new Event('om:refresh-sidebar'))\n } catch {\n // Ignore if window is unavailable\n }\n } catch {\n // Ignore network errors; UX fallback keeps previous locale\n }\n }\n\n // Locale is pinned via OM_FORCE_LOCALE \u2014 switching is a no-op, so hide the control.\n if (localeLocked) return null\n\n return (\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <label htmlFor={selectId}>{t('common.language')}</label>\n <Select\n value={current}\n onValueChange={(value) => setLocale(value as Locale)}\n disabled={pending}\n >\n <SelectTrigger id={selectId} size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {locales.map((locale) => (\n <SelectItem key={locale} value={locale}>\n {languageLabels[locale]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )\n}\n"],
5
+ "mappings": ";AAsDM,cACA,YADA;AArDN,SAAS,OAAO,qBAAqB;AACrC,SAAS,WAAW,iBAAiB,YAAY;AACjD,SAAS,iBAAiB;AAC1B,SAAS,eAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,SAAS,mBAAmB;AACjC,QAAM,UAAU,UAAU;AAC1B,QAAM,eAAe,gBAAgB;AACrC,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,SAAS,eAAe,IAAI,cAAc;AACjD,QAAM,WAAW,MAAM;AAEvB,QAAM,iBAAyC;AAAA,IAC7C,IAAI,EAAE,4BAA4B,SAAS;AAAA,IAC3C,IAAI,EAAE,2BAA2B,QAAQ;AAAA,IACzC,IAAI,EAAE,4BAA4B,YAAS;AAAA,IAC3C,IAAI,EAAE,2BAA2B,SAAS;AAAA,IAC1C,IAAI,EAAE,2BAA2B,oBAAK;AAAA,EACxC;AAEA,iBAAe,UAAU,QAAgB;AACvC,QAAI,WAAW,QAAS;AACxB,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,oBAAoB;AAAA,QAC1C,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC;AAAA,MACjC,CAAC;AACD,UAAI,CAAC,IAAI,GAAI;AACb,sBAAgB,MAAM,OAAO,QAAQ,CAAC;AACtC,UAAI;AACF,eAAO,cAAc,IAAI,MAAM,oBAAoB,CAAC;AAAA,MACtD,QAAQ;AAAA,MAER;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,MAAI,aAAc,QAAO;AAEzB,SACE,qBAAC,SAAI,WAAU,yDACb;AAAA,wBAAC,WAAM,SAAS,UAAW,YAAE,iBAAiB,GAAE;AAAA,IAChD;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP,eAAe,CAAC,UAAU,UAAU,KAAe;AAAA,QACnD,UAAU;AAAA,QAEV;AAAA,8BAAC,iBAAc,IAAI,UAAU,MAAK,MAChC,8BAAC,eAAY,GACf;AAAA,UACA,oBAAC,iBACE,kBAAQ,IAAI,CAAC,WACZ,oBAAC,cAAwB,OAAO,QAC7B,yBAAe,MAAM,KADP,MAEjB,CACD,GACH;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -33,7 +33,7 @@ function PortalProvider({ orgSlug, children, initialAuth, initialTenant }) {
33
33
  },
34
34
  roles: [],
35
35
  resolvedFeatures: initialAuth.resolvedFeatures,
36
- isPortalAdmin: initialAuth.resolvedFeatures.some((f) => f === "portal.*"),
36
+ isPortalAdmin: initialAuth.isPortalAdmin === true,
37
37
  loading: false,
38
38
  error: null
39
39
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/portal/PortalContext.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport {\n type ReactNode,\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n} from 'react'\nimport type { CustomerAuthResult } from '@open-mercato/shared/modules/customer-auth'\nimport type { CustomerAuthContext } from '@open-mercato/shared/modules/customer-auth'\nimport { apiCall } from '../backend/utils/apiCall'\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\ntype TenantState = {\n tenantId: string | undefined\n organizationId: string | undefined\n organizationName: string | undefined\n loading: boolean\n error: string | null\n}\n\ntype PortalContextValue = {\n auth: CustomerAuthResult & { logout: () => Promise<void> }\n tenant: TenantState\n orgSlug: string\n}\n\nconst PortalCtx = createContext<PortalContextValue | null>(null)\n\n/* ------------------------------------------------------------------ */\n/* Hook */\n/* ------------------------------------------------------------------ */\n\nexport function usePortalContext(): PortalContextValue {\n const ctx = useContext(PortalCtx)\n if (!ctx) throw new Error('usePortalContext must be used inside <PortalProvider>')\n return ctx\n}\n\n/* ------------------------------------------------------------------ */\n/* Provider */\n/* ------------------------------------------------------------------ */\n\ntype InitialTenant = {\n tenantId?: string\n organizationId?: string\n organizationName?: string\n}\n\ntype PortalProviderProps = {\n orgSlug: string\n children: ReactNode\n /** Server-resolved customer auth. When provided, skips client-side fetching. */\n initialAuth?: CustomerAuthContext | null\n /** Server-resolved tenant data. When provided, skips client-side fetching. */\n initialTenant?: InitialTenant\n}\n\n/**\n * Provides auth + tenant state for portal pages.\n *\n * When `initialAuth` and `initialTenant` are provided (from server layout),\n * the context initializes with that data immediately \u2014 no loading state,\n * no client-side fetching, no blink.\n *\n * Falls back to client-side fetching only when initial data is not provided.\n */\nexport function PortalProvider({ orgSlug, children, initialAuth, initialTenant }: PortalProviderProps) {\n const hasServerData = initialAuth !== undefined\n\n /* ---- Auth state ---- */\n const [authState, setAuthState] = useState<CustomerAuthResult>(() => {\n if (hasServerData && initialAuth) {\n // Server-resolved auth \u2014 start fully loaded, no client-side fetch needed\n return {\n user: {\n id: initialAuth.sub,\n email: initialAuth.email,\n displayName: initialAuth.displayName,\n emailVerified: false,\n customerEntityId: initialAuth.customerEntityId ?? null,\n personEntityId: initialAuth.personEntityId ?? null,\n isActive: true,\n lastLoginAt: null,\n createdAt: '',\n },\n roles: [],\n resolvedFeatures: initialAuth.resolvedFeatures,\n isPortalAdmin: initialAuth.resolvedFeatures.some((f) => f === 'portal.*'),\n loading: false,\n error: null,\n }\n }\n if (hasServerData && !initialAuth) {\n // Server confirmed: no auth cookie \u2192 not authenticated\n return { user: null, roles: [], resolvedFeatures: [], isPortalAdmin: false, loading: false, error: null }\n }\n // No server data \u2192 need client-side fetch\n return { user: null, roles: [], resolvedFeatures: [], isPortalAdmin: false, loading: true, error: null }\n })\n\n // Guard against re-fetching when the provider persists across navigations\n // and `initialAuth` arrives as a new object reference on each server render.\n const profileEnrichedRef = useRef(false)\n\n // Client-side profile fetch \u2014 only runs when NO server data was provided,\n // or to enrich server-provided auth with full profile (roles, etc.)\n useEffect(() => {\n if (hasServerData && !initialAuth) return // Server said: not authenticated, nothing to fetch\n if (profileEnrichedRef.current) return\n let cancelled = false\n\n async function fetchProfile() {\n try {\n const { ok, status, result: data } = await apiCall<{ ok: boolean; user: CustomerAuthResult['user']; roles: CustomerAuthResult['roles']; resolvedFeatures: string[]; isPortalAdmin: boolean }>('/api/customer_accounts/portal/profile')\n if (cancelled) return\n if (status === 401) {\n setAuthState((prev) => ({ ...prev, loading: false, user: prev.user }))\n return\n }\n if (!ok || !data?.ok) {\n setAuthState((prev) => ({ ...prev, loading: false }))\n return\n }\n profileEnrichedRef.current = true\n setAuthState({\n user: data!.user,\n roles: data!.roles || [],\n resolvedFeatures: data!.resolvedFeatures || [],\n isPortalAdmin: data!.isPortalAdmin || false,\n loading: false,\n error: null,\n })\n } catch {\n if (!cancelled) {\n setAuthState((prev) => ({ ...prev, loading: false }))\n }\n }\n }\n\n fetchProfile()\n return () => { cancelled = true }\n }, [hasServerData, initialAuth])\n\n const logout = useCallback(async () => {\n try {\n await apiCall('/api/customer_accounts/portal/logout', { method: 'POST' })\n } catch {\n // Best-effort logout \u2014 redirect regardless\n }\n profileEnrichedRef.current = false\n setAuthState({ user: null, roles: [], resolvedFeatures: [], isPortalAdmin: false, loading: false, error: null })\n window.location.assign(`/${orgSlug}/portal/login`)\n }, [orgSlug])\n\n /* ---- Tenant state ---- */\n const [tenantState] = useState<TenantState>(() => {\n if (initialTenant) {\n return {\n tenantId: initialTenant.tenantId,\n organizationId: initialTenant.organizationId,\n organizationName: initialTenant.organizationName,\n loading: false,\n error: null,\n }\n }\n return { tenantId: undefined, organizationId: undefined, organizationName: undefined, loading: true, error: null }\n })\n\n // Client-side tenant fetch \u2014 only when no server data\n useEffect(() => {\n if (initialTenant) return // Server already resolved\n // Fallback: kept for backward compat but shouldn't be hit when layout provides data\n }, [initialTenant])\n\n const value: PortalContextValue = {\n auth: { ...authState, logout },\n tenant: tenantState,\n orgSlug,\n }\n\n return <PortalCtx.Provider value={value}>{children}</PortalCtx.Provider>\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport {\n type ReactNode,\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n} from 'react'\nimport type { CustomerAuthResult } from '@open-mercato/shared/modules/customer-auth'\nimport type { CustomerAuthContext } from '@open-mercato/shared/modules/customer-auth'\nimport { apiCall } from '../backend/utils/apiCall'\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\n\ntype TenantState = {\n tenantId: string | undefined\n organizationId: string | undefined\n organizationName: string | undefined\n loading: boolean\n error: string | null\n}\n\ntype PortalContextValue = {\n auth: CustomerAuthResult & { logout: () => Promise<void> }\n tenant: TenantState\n orgSlug: string\n}\n\nconst PortalCtx = createContext<PortalContextValue | null>(null)\n\n/* ------------------------------------------------------------------ */\n/* Hook */\n/* ------------------------------------------------------------------ */\n\nexport function usePortalContext(): PortalContextValue {\n const ctx = useContext(PortalCtx)\n if (!ctx) throw new Error('usePortalContext must be used inside <PortalProvider>')\n return ctx\n}\n\n/* ------------------------------------------------------------------ */\n/* Provider */\n/* ------------------------------------------------------------------ */\n\ntype InitialTenant = {\n tenantId?: string\n organizationId?: string\n organizationName?: string\n}\n\ntype PortalProviderProps = {\n orgSlug: string\n children: ReactNode\n /** Server-resolved customer auth. When provided, skips client-side fetching. */\n initialAuth?: CustomerAuthContext | null\n /** Server-resolved tenant data. When provided, skips client-side fetching. */\n initialTenant?: InitialTenant\n}\n\n/**\n * Provides auth + tenant state for portal pages.\n *\n * When `initialAuth` and `initialTenant` are provided (from server layout),\n * the context initializes with that data immediately \u2014 no loading state,\n * no client-side fetching, no blink.\n *\n * Falls back to client-side fetching only when initial data is not provided.\n */\nexport function PortalProvider({ orgSlug, children, initialAuth, initialTenant }: PortalProviderProps) {\n const hasServerData = initialAuth !== undefined\n\n /* ---- Auth state ---- */\n const [authState, setAuthState] = useState<CustomerAuthResult>(() => {\n if (hasServerData && initialAuth) {\n // Server-resolved auth \u2014 start fully loaded, no client-side fetch needed\n return {\n user: {\n id: initialAuth.sub,\n email: initialAuth.email,\n displayName: initialAuth.displayName,\n emailVerified: false,\n customerEntityId: initialAuth.customerEntityId ?? null,\n personEntityId: initialAuth.personEntityId ?? null,\n isActive: true,\n lastLoginAt: null,\n createdAt: '',\n },\n roles: [],\n resolvedFeatures: initialAuth.resolvedFeatures,\n isPortalAdmin: initialAuth.isPortalAdmin === true,\n loading: false,\n error: null,\n }\n }\n if (hasServerData && !initialAuth) {\n // Server confirmed: no auth cookie \u2192 not authenticated\n return { user: null, roles: [], resolvedFeatures: [], isPortalAdmin: false, loading: false, error: null }\n }\n // No server data \u2192 need client-side fetch\n return { user: null, roles: [], resolvedFeatures: [], isPortalAdmin: false, loading: true, error: null }\n })\n\n // Guard against re-fetching when the provider persists across navigations\n // and `initialAuth` arrives as a new object reference on each server render.\n const profileEnrichedRef = useRef(false)\n\n // Client-side profile fetch \u2014 only runs when NO server data was provided,\n // or to enrich server-provided auth with full profile (roles, etc.)\n useEffect(() => {\n if (hasServerData && !initialAuth) return // Server said: not authenticated, nothing to fetch\n if (profileEnrichedRef.current) return\n let cancelled = false\n\n async function fetchProfile() {\n try {\n const { ok, status, result: data } = await apiCall<{ ok: boolean; user: CustomerAuthResult['user']; roles: CustomerAuthResult['roles']; resolvedFeatures: string[]; isPortalAdmin: boolean }>('/api/customer_accounts/portal/profile')\n if (cancelled) return\n if (status === 401) {\n setAuthState((prev) => ({ ...prev, loading: false, user: prev.user }))\n return\n }\n if (!ok || !data?.ok) {\n setAuthState((prev) => ({ ...prev, loading: false }))\n return\n }\n profileEnrichedRef.current = true\n setAuthState({\n user: data!.user,\n roles: data!.roles || [],\n resolvedFeatures: data!.resolvedFeatures || [],\n isPortalAdmin: data!.isPortalAdmin || false,\n loading: false,\n error: null,\n })\n } catch {\n if (!cancelled) {\n setAuthState((prev) => ({ ...prev, loading: false }))\n }\n }\n }\n\n fetchProfile()\n return () => { cancelled = true }\n }, [hasServerData, initialAuth])\n\n const logout = useCallback(async () => {\n try {\n await apiCall('/api/customer_accounts/portal/logout', { method: 'POST' })\n } catch {\n // Best-effort logout \u2014 redirect regardless\n }\n profileEnrichedRef.current = false\n setAuthState({ user: null, roles: [], resolvedFeatures: [], isPortalAdmin: false, loading: false, error: null })\n window.location.assign(`/${orgSlug}/portal/login`)\n }, [orgSlug])\n\n /* ---- Tenant state ---- */\n const [tenantState] = useState<TenantState>(() => {\n if (initialTenant) {\n return {\n tenantId: initialTenant.tenantId,\n organizationId: initialTenant.organizationId,\n organizationName: initialTenant.organizationName,\n loading: false,\n error: null,\n }\n }\n return { tenantId: undefined, organizationId: undefined, organizationName: undefined, loading: true, error: null }\n })\n\n // Client-side tenant fetch \u2014 only when no server data\n useEffect(() => {\n if (initialTenant) return // Server already resolved\n // Fallback: kept for backward compat but shouldn't be hit when layout provides data\n }, [initialTenant])\n\n const value: PortalContextValue = {\n auth: { ...authState, logout },\n tenant: tenantState,\n orgSlug,\n }\n\n return <PortalCtx.Provider value={value}>{children}</PortalCtx.Provider>\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,25 @@
1
+ const PORTAL_BRIDGE_STATUS_DOM_NAME = "om:portal-bridge:status";
2
+ function readPortalBridgeHealth() {
3
+ if (typeof window === "undefined") return void 0;
4
+ return window.__portalBridgeHealthy;
5
+ }
6
+ function publishPortalBridgeHealth(healthy) {
7
+ if (typeof window === "undefined") return;
8
+ window.__portalBridgeHealthy = healthy;
9
+ window.dispatchEvent(
10
+ new CustomEvent(PORTAL_BRIDGE_STATUS_DOM_NAME, {
11
+ detail: { healthy }
12
+ })
13
+ );
14
+ }
15
+ function clearPortalBridgeHealth() {
16
+ if (typeof window === "undefined") return;
17
+ delete window.__portalBridgeHealthy;
18
+ }
19
+ export {
20
+ PORTAL_BRIDGE_STATUS_DOM_NAME,
21
+ clearPortalBridgeHealth,
22
+ publishPortalBridgeHealth,
23
+ readPortalBridgeHealth
24
+ };
25
+ //# sourceMappingURL=portalBridgeStatus.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/portal/hooks/portalBridgeStatus.ts"],
4
+ "sourcesContent": ["export const PORTAL_BRIDGE_STATUS_DOM_NAME = 'om:portal-bridge:status'\n\nexport type PortalBridgeStatusDetail = {\n healthy: boolean\n}\n\ntype PortalBridgeWindow = Window & {\n __portalBridgeHealthy?: boolean\n}\n\nexport function readPortalBridgeHealth(): boolean | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as PortalBridgeWindow).__portalBridgeHealthy\n}\n\nexport function publishPortalBridgeHealth(healthy: boolean): void {\n if (typeof window === 'undefined') return\n ;(window as PortalBridgeWindow).__portalBridgeHealthy = healthy\n window.dispatchEvent(\n new CustomEvent<PortalBridgeStatusDetail>(PORTAL_BRIDGE_STATUS_DOM_NAME, {\n detail: { healthy },\n }),\n )\n}\n\nexport function clearPortalBridgeHealth(): void {\n if (typeof window === 'undefined') return\n delete (window as PortalBridgeWindow).__portalBridgeHealthy\n}\n"],
5
+ "mappings": "AAAO,MAAM,gCAAgC;AAUtC,SAAS,yBAA8C;AAC5D,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,SAAQ,OAA8B;AACxC;AAEO,SAAS,0BAA0B,SAAwB;AAChE,MAAI,OAAO,WAAW,YAAa;AAClC,EAAC,OAA8B,wBAAwB;AACxD,SAAO;AAAA,IACL,IAAI,YAAsC,+BAA+B;AAAA,MACvE,QAAQ,EAAE,QAAQ;AAAA,IACpB,CAAC;AAAA,EACH;AACF;AAEO,SAAS,0BAAgC;AAC9C,MAAI,OAAO,WAAW,YAAa;AACnC,SAAQ,OAA8B;AACxC;",
6
+ "names": []
7
+ }
@@ -2,6 +2,7 @@
2
2
  import { useEffect, useRef } from "react";
3
3
  import { PORTAL_EVENT_DOM_NAME } from "./usePortalAppEvent.js";
4
4
  import { createLogger } from "@open-mercato/shared/lib/logger";
5
+ import { publishPortalBridgeHealth } from "./portalBridgeStatus.js";
5
6
  const logger = createLogger("ui").child({ component: "PortalEventBridge" });
6
7
  const PORTAL_SSE_ENDPOINT = "/api/customer_accounts/portal/events/stream";
7
8
  const HEARTBEAT_TIMEOUT = 45e3;
@@ -36,6 +37,7 @@ function usePortalEventBridge() {
36
37
  if (heartbeatTimer.current) clearTimeout(heartbeatTimer.current);
37
38
  heartbeatTimer.current = setTimeout(() => {
38
39
  logger.warn("Heartbeat timeout \u2014 reconnecting");
40
+ publishPortalBridgeHealth(false);
39
41
  disconnect();
40
42
  scheduleReconnect();
41
43
  }, HEARTBEAT_TIMEOUT);
@@ -51,6 +53,7 @@ function usePortalEventBridge() {
51
53
  hasEverConnected.current = true;
52
54
  reconnectPending.current = false;
53
55
  reconnectAttempts.current = 0;
56
+ publishPortalBridgeHealth(true);
54
57
  resetHeartbeatTimer();
55
58
  if (shouldEmitReconnect) {
56
59
  window.dispatchEvent(
@@ -82,6 +85,7 @@ function usePortalEventBridge() {
82
85
  if (hasEverConnected.current) {
83
86
  reconnectPending.current = true;
84
87
  }
88
+ publishPortalBridgeHealth(false);
85
89
  disconnect();
86
90
  if (mounted) scheduleReconnect();
87
91
  };
@@ -89,6 +93,7 @@ function usePortalEventBridge() {
89
93
  if (hasEverConnected.current) {
90
94
  reconnectPending.current = true;
91
95
  }
96
+ publishPortalBridgeHealth(false);
92
97
  if (mounted) scheduleReconnect();
93
98
  }
94
99
  }
@@ -117,6 +122,7 @@ function usePortalEventBridge() {
117
122
  connect();
118
123
  return () => {
119
124
  mounted = false;
125
+ publishPortalBridgeHealth(false);
120
126
  disconnect();
121
127
  if (reconnectTimer.current) {
122
128
  clearTimeout(reconnectTimer.current);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/portal/hooks/usePortalEventBridge.ts"],
4
- "sourcesContent": ["\"use client\"\nimport { useEffect, useRef } from 'react'\nimport type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'\nimport { PORTAL_EVENT_DOM_NAME } from './usePortalAppEvent'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'PortalEventBridge' })\n\nconst PORTAL_SSE_ENDPOINT = '/api/customer_accounts/portal/events/stream'\nconst HEARTBEAT_TIMEOUT = 45_000\nconst RECONNECT_BASE_MS = 1_000\nconst RECONNECT_MAX_MS = 30_000\nconst DEDUP_WINDOW_MS = 500\nconst PORTAL_BRIDGE_RECONNECTED_EVENT_ID = 'om:portal-bridge:reconnected'\n\n/**\n * React hook that establishes a singleton SSE connection to the portal event bridge.\n *\n * Mount once in the portal shell/layout. Receives server-side events with\n * `portalBroadcast: true` and dispatches them as `om:portal-event` CustomEvents\n * on the window object for consumption by `usePortalAppEvent`.\n *\n * Uses customer auth (cookie-based JWT) instead of staff auth.\n *\n * @example\n * ```tsx\n * import { usePortalEventBridge } from '@open-mercato/ui/portal/hooks/usePortalEventBridge'\n *\n * function PortalShell({ children }) {\n * usePortalEventBridge()\n * return <div>{children}</div>\n * }\n * ```\n */\nexport function usePortalEventBridge(): 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 const hasEverConnected = useRef(false)\n const reconnectPending = useRef(false)\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 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 logger.warn('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(PORTAL_SSE_ENDPOINT, { withCredentials: true })\n sourceRef.current = source\n\n source.onopen = () => {\n const shouldEmitReconnect = hasEverConnected.current && reconnectPending.current\n hasEverConnected.current = true\n reconnectPending.current = false\n reconnectAttempts.current = 0\n resetHeartbeatTimer()\n if (shouldEmitReconnect) {\n window.dispatchEvent(\n new CustomEvent(PORTAL_EVENT_DOM_NAME, {\n detail: {\n id: PORTAL_BRIDGE_RECONNECTED_EVENT_ID,\n payload: {},\n timestamp: Date.now(),\n organizationId: '',\n } satisfies AppEventPayload,\n }),\n )\n }\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 if (isDuplicate(parsed)) return\n\n window.dispatchEvent(\n new CustomEvent(PORTAL_EVENT_DOM_NAME, { detail: parsed }),\n )\n } catch {\n // Ignore malformed events\n }\n }\n\n source.onerror = () => {\n if (hasEverConnected.current) {\n reconnectPending.current = true\n }\n disconnect()\n if (mounted) scheduleReconnect()\n }\n } catch {\n if (hasEverConnected.current) {\n reconnectPending.current = true\n }\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": "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",
4
+ "sourcesContent": ["\"use client\"\nimport { useEffect, useRef } from 'react'\nimport type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'\nimport { PORTAL_EVENT_DOM_NAME } from './usePortalAppEvent'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { publishPortalBridgeHealth } from './portalBridgeStatus'\n\nconst logger = createLogger('ui').child({ component: 'PortalEventBridge' })\n\nconst PORTAL_SSE_ENDPOINT = '/api/customer_accounts/portal/events/stream'\nconst HEARTBEAT_TIMEOUT = 45_000\nconst RECONNECT_BASE_MS = 1_000\nconst RECONNECT_MAX_MS = 30_000\nconst DEDUP_WINDOW_MS = 500\nconst PORTAL_BRIDGE_RECONNECTED_EVENT_ID = 'om:portal-bridge:reconnected'\n\n/**\n * React hook that establishes a singleton SSE connection to the portal event bridge.\n *\n * Mount once in the portal shell/layout. Receives server-side events with\n * `portalBroadcast: true` and dispatches them as `om:portal-event` CustomEvents\n * on the window object for consumption by `usePortalAppEvent`.\n *\n * Uses customer auth (cookie-based JWT) instead of staff auth.\n *\n * @example\n * ```tsx\n * import { usePortalEventBridge } from '@open-mercato/ui/portal/hooks/usePortalEventBridge'\n *\n * function PortalShell({ children }) {\n * usePortalEventBridge()\n * return <div>{children}</div>\n * }\n * ```\n */\nexport function usePortalEventBridge(): 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 const hasEverConnected = useRef(false)\n const reconnectPending = useRef(false)\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 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 logger.warn('Heartbeat timeout \u2014 reconnecting')\n publishPortalBridgeHealth(false)\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(PORTAL_SSE_ENDPOINT, { withCredentials: true })\n sourceRef.current = source\n\n source.onopen = () => {\n const shouldEmitReconnect = hasEverConnected.current && reconnectPending.current\n hasEverConnected.current = true\n reconnectPending.current = false\n reconnectAttempts.current = 0\n publishPortalBridgeHealth(true)\n resetHeartbeatTimer()\n if (shouldEmitReconnect) {\n window.dispatchEvent(\n new CustomEvent(PORTAL_EVENT_DOM_NAME, {\n detail: {\n id: PORTAL_BRIDGE_RECONNECTED_EVENT_ID,\n payload: {},\n timestamp: Date.now(),\n organizationId: '',\n } satisfies AppEventPayload,\n }),\n )\n }\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 if (isDuplicate(parsed)) return\n\n window.dispatchEvent(\n new CustomEvent(PORTAL_EVENT_DOM_NAME, { detail: parsed }),\n )\n } catch {\n // Ignore malformed events\n }\n }\n\n source.onerror = () => {\n if (hasEverConnected.current) {\n reconnectPending.current = true\n }\n publishPortalBridgeHealth(false)\n disconnect()\n if (mounted) scheduleReconnect()\n }\n } catch {\n if (hasEverConnected.current) {\n reconnectPending.current = true\n }\n publishPortalBridgeHealth(false)\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 publishPortalBridgeHealth(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,6BAA6B;AACtC,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAE1C,MAAM,SAAS,aAAa,IAAI,EAAE,MAAM,EAAE,WAAW,oBAAoB,CAAC;AAE1E,MAAM,sBAAsB;AAC5B,MAAM,oBAAoB;AAC1B,MAAM,oBAAoB;AAC1B,MAAM,mBAAmB;AACzB,MAAM,kBAAkB;AACxB,MAAM,qCAAqC;AAqBpC,SAAS,uBAA6B;AAC3C,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;AAC1D,QAAM,mBAAmB,OAAO,KAAK;AACrC,QAAM,mBAAmB,OAAO,KAAK;AAErC,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;AACxC,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,eAAO,KAAK,uCAAkC;AAC9C,kCAA0B,KAAK;AAC/B,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,qBAAqB,EAAE,iBAAiB,KAAK,CAAC;AAC7E,kBAAU,UAAU;AAEpB,eAAO,SAAS,MAAM;AACpB,gBAAM,sBAAsB,iBAAiB,WAAW,iBAAiB;AACzE,2BAAiB,UAAU;AAC3B,2BAAiB,UAAU;AAC3B,4BAAkB,UAAU;AAC5B,oCAA0B,IAAI;AAC9B,8BAAoB;AACpB,cAAI,qBAAqB;AACvB,mBAAO;AAAA,cACL,IAAI,YAAY,uBAAuB;AAAA,gBACrC,QAAQ;AAAA,kBACN,IAAI;AAAA,kBACJ,SAAS,CAAC;AAAA,kBACV,WAAW,KAAK,IAAI;AAAA,kBACpB,gBAAgB;AAAA,gBAClB;AAAA,cACF,CAAC;AAAA,YACH;AAAA,UACF;AAAA,QACF;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;AACjD,gBAAI,YAAY,MAAM,EAAG;AAEzB,mBAAO;AAAA,cACL,IAAI,YAAY,uBAAuB,EAAE,QAAQ,OAAO,CAAC;AAAA,YAC3D;AAAA,UACF,QAAQ;AAAA,UAER;AAAA,QACF;AAEA,eAAO,UAAU,MAAM;AACrB,cAAI,iBAAiB,SAAS;AAC5B,6BAAiB,UAAU;AAAA,UAC7B;AACA,oCAA0B,KAAK;AAC/B,qBAAW;AACX,cAAI,QAAS,mBAAkB;AAAA,QACjC;AAAA,MACF,QAAQ;AACN,YAAI,iBAAiB,SAAS;AAC5B,2BAAiB,UAAU;AAAA,QAC7B;AACA,kCAA0B,KAAK;AAC/B,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,gCAA0B,KAAK;AAC/B,iBAAW;AACX,UAAI,eAAe,SAAS;AAC1B,qBAAa,eAAe,OAAO;AACnC,uBAAe,UAAU;AAAA,MAC3B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AACP;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,10 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
3
  import { apiCall } from "../../backend/utils/apiCall.js";
4
+ import {
5
+ PORTAL_BRIDGE_STATUS_DOM_NAME,
6
+ readPortalBridgeHealth
7
+ } from "./portalBridgeStatus.js";
4
8
  const POLL_INTERVAL = 8e3;
5
9
  const BASE = "/api/customer_accounts/portal/notifications";
6
10
  async function fetchJson(url, init) {
@@ -37,21 +41,55 @@ function usePortalNotifications() {
37
41
  }
38
42
  setIsLoading(false);
39
43
  }, []);
44
+ const [usePolling, setUsePolling] = React.useState(() => {
45
+ if (typeof window === "undefined" || !("EventSource" in window)) {
46
+ return true;
47
+ }
48
+ return readPortalBridgeHealth() !== true;
49
+ });
50
+ React.useEffect(() => {
51
+ if (typeof window === "undefined" || !("EventSource" in window)) {
52
+ return;
53
+ }
54
+ const handleStatusChange = (event) => {
55
+ const detail = event.detail;
56
+ if (detail && typeof detail.healthy === "boolean") {
57
+ setUsePolling(!detail.healthy);
58
+ if (!detail.healthy) {
59
+ fetchAll();
60
+ }
61
+ }
62
+ };
63
+ window.addEventListener(PORTAL_BRIDGE_STATUS_DOM_NAME, handleStatusChange);
64
+ return () => {
65
+ window.removeEventListener(PORTAL_BRIDGE_STATUS_DOM_NAME, handleStatusChange);
66
+ };
67
+ }, [fetchAll]);
40
68
  React.useEffect(() => {
41
69
  fetchAll();
70
+ }, [fetchAll]);
71
+ React.useEffect(() => {
72
+ if (!usePolling) return;
42
73
  const interval = setInterval(fetchAll, POLL_INTERVAL);
43
74
  return () => clearInterval(interval);
44
- }, [fetchAll]);
75
+ }, [fetchAll, usePolling]);
45
76
  React.useEffect(() => {
46
- const handler = (e) => {
47
- const detail = e.detail;
48
- if (detail?.id === "notifications.notification.created" || detail?.id === "notifications.notification.batch_created") {
77
+ const handler = (event) => {
78
+ const detail = event.detail;
79
+ if (detail?.id === "notifications.notification.created" || detail?.id === "notifications.notification.batch_created" || detail?.id === "om:portal-bridge:reconnected") {
49
80
  fetchAll();
50
81
  }
51
82
  };
52
83
  window.addEventListener("om:portal-event", handler);
53
84
  return () => window.removeEventListener("om:portal-event", handler);
54
85
  }, [fetchAll]);
86
+ React.useEffect(() => {
87
+ const onFocus = () => {
88
+ fetchAll();
89
+ };
90
+ window.addEventListener("focus", onFocus);
91
+ return () => window.removeEventListener("focus", onFocus);
92
+ }, [fetchAll]);
55
93
  const markAsRead = React.useCallback(async (id) => {
56
94
  await fetchJson(`${BASE}/${id}/read`, { method: "PUT" });
57
95
  setNotifications(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/portal/hooks/usePortalNotifications.ts"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\nimport { apiCall } from '../../backend/utils/apiCall'\n\nexport type UsePortalNotificationsResult = {\n notifications: NotificationDto[]\n unreadCount: number\n hasNew: boolean\n isLoading: boolean\n refresh: () => void\n markAsRead: (id: string) => Promise<void>\n dismiss: (id: string) => Promise<void>\n markAllRead: () => Promise<void>\n}\n\nconst POLL_INTERVAL = 8000\nconst BASE = '/api/customer_accounts/portal/notifications'\n\nasync function fetchJson<T>(url: string, init?: RequestInit): Promise<T | null> {\n try {\n const { ok, result } = await apiCall<T>(url, init)\n if (!ok) return null\n return result\n } catch {\n return null\n }\n}\n\n/**\n * Portal notification hook \u2014 polls customer notification endpoints.\n *\n * Fetches notifications from `/api/customer_accounts/portal/notifications`\n * and unread count from `.../unread-count`. Polls every 8 seconds.\n *\n * Also listens for portal SSE events (`notifications.notification.created`)\n * to trigger immediate refresh.\n */\nexport function usePortalNotifications(): UsePortalNotificationsResult {\n const [notifications, setNotifications] = React.useState<NotificationDto[]>([])\n const [unreadCount, setUnreadCount] = React.useState(0)\n const [hasNew, setHasNew] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState(true)\n const lastIdRef = React.useRef<string | null>(null)\n\n const fetchAll = React.useCallback(async () => {\n const [listData, countData] = await Promise.all([\n fetchJson<{ ok: boolean; items: NotificationDto[] }>(`${BASE}?pageSize=50`),\n fetchJson<{ ok: boolean; unreadCount: number }>(`${BASE}/unread-count`),\n ])\n\n if (listData?.ok && listData.items) {\n const items = listData.items\n if (lastIdRef.current && items.length > 0 && items[0].id !== lastIdRef.current) {\n setHasNew(true)\n setTimeout(() => setHasNew(false), 3000)\n }\n if (items.length > 0) lastIdRef.current = items[0].id\n setNotifications(items)\n }\n\n if (countData?.ok) {\n setUnreadCount(countData.unreadCount)\n }\n\n setIsLoading(false)\n }, [])\n\n // Poll\n React.useEffect(() => {\n fetchAll()\n const interval = setInterval(fetchAll, POLL_INTERVAL)\n return () => clearInterval(interval)\n }, [fetchAll])\n\n // Listen for portal SSE notification events\n React.useEffect(() => {\n const handler = (e: Event) => {\n const detail = (e as CustomEvent).detail\n if (detail?.id === 'notifications.notification.created' || detail?.id === 'notifications.notification.batch_created') {\n fetchAll()\n }\n }\n window.addEventListener('om:portal-event', handler)\n return () => window.removeEventListener('om:portal-event', handler)\n }, [fetchAll])\n\n const markAsRead = React.useCallback(async (id: string) => {\n await fetchJson(`${BASE}/${id}/read`, { method: 'PUT' })\n setNotifications((prev) =>\n prev.map((n) => (n.id === id ? { ...n, status: 'read', readAt: new Date().toISOString() } : n)),\n )\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }, [])\n\n const dismiss = React.useCallback(async (id: string) => {\n await fetchJson(`${BASE}/${id}/dismiss`, { method: 'PUT' })\n setNotifications((prev) => prev.filter((n) => n.id !== id))\n setUnreadCount((prev) => {\n const wasDismissedUnread = notifications.find((n) => n.id === id)?.status === 'unread'\n return wasDismissedUnread ? Math.max(0, prev - 1) : prev\n })\n }, [notifications])\n\n const markAllRead = React.useCallback(async () => {\n await fetchJson(`${BASE}/mark-all-read`, { method: 'PUT' })\n setNotifications((prev) =>\n prev.map((n) => (n.status === 'unread' ? { ...n, status: 'read', readAt: new Date().toISOString() } : n)),\n )\n setUnreadCount(0)\n }, [])\n\n const refresh = React.useCallback(() => { fetchAll() }, [fetchAll])\n\n return { notifications, unreadCount, hasNew, isLoading, refresh, markAsRead, dismiss, markAllRead }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'\nimport { apiCall } from '../../backend/utils/apiCall'\nimport {\n PORTAL_BRIDGE_STATUS_DOM_NAME,\n readPortalBridgeHealth,\n type PortalBridgeStatusDetail,\n} from './portalBridgeStatus'\n\nexport type UsePortalNotificationsResult = {\n notifications: NotificationDto[]\n unreadCount: number\n hasNew: boolean\n isLoading: boolean\n refresh: () => void\n markAsRead: (id: string) => Promise<void>\n dismiss: (id: string) => Promise<void>\n markAllRead: () => Promise<void>\n}\n\nconst POLL_INTERVAL = 8000\nconst BASE = '/api/customer_accounts/portal/notifications'\n\nasync function fetchJson<T>(url: string, init?: RequestInit): Promise<T | null> {\n try {\n const { ok, result } = await apiCall<T>(url, init)\n if (!ok) return null\n return result\n } catch {\n return null\n }\n}\n\n/**\n * Portal notification hook with SSE-first delivery and polling fallback.\n *\n * Performs an initial list/count reconciliation, refreshes on notification,\n * reconnect, and focus events, and polls every 8 seconds until the portal\n * event bridge explicitly reports a healthy connection.\n */\nexport function usePortalNotifications(): UsePortalNotificationsResult {\n const [notifications, setNotifications] = React.useState<NotificationDto[]>([])\n const [unreadCount, setUnreadCount] = React.useState(0)\n const [hasNew, setHasNew] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState(true)\n const lastIdRef = React.useRef<string | null>(null)\n\n const fetchAll = React.useCallback(async () => {\n const [listData, countData] = await Promise.all([\n fetchJson<{ ok: boolean; items: NotificationDto[] }>(`${BASE}?pageSize=50`),\n fetchJson<{ ok: boolean; unreadCount: number }>(`${BASE}/unread-count`),\n ])\n\n if (listData?.ok && listData.items) {\n const items = listData.items\n if (lastIdRef.current && items.length > 0 && items[0].id !== lastIdRef.current) {\n setHasNew(true)\n setTimeout(() => setHasNew(false), 3000)\n }\n if (items.length > 0) lastIdRef.current = items[0].id\n setNotifications(items)\n }\n\n if (countData?.ok) {\n setUnreadCount(countData.unreadCount)\n }\n\n setIsLoading(false)\n }, [])\n\n const [usePolling, setUsePolling] = React.useState(() => {\n if (typeof window === 'undefined' || !('EventSource' in window)) {\n return true\n }\n return readPortalBridgeHealth() !== true\n })\n\n React.useEffect(() => {\n if (typeof window === 'undefined' || !('EventSource' in window)) {\n return\n }\n const handleStatusChange = (event: Event) => {\n const detail = (event as CustomEvent<PortalBridgeStatusDetail>).detail\n if (detail && typeof detail.healthy === 'boolean') {\n setUsePolling(!detail.healthy)\n if (!detail.healthy) {\n fetchAll()\n }\n }\n }\n window.addEventListener(PORTAL_BRIDGE_STATUS_DOM_NAME, handleStatusChange)\n return () => {\n window.removeEventListener(PORTAL_BRIDGE_STATUS_DOM_NAME, handleStatusChange)\n }\n }, [fetchAll])\n\n React.useEffect(() => {\n fetchAll()\n }, [fetchAll])\n\n React.useEffect(() => {\n if (!usePolling) return\n const interval = setInterval(fetchAll, POLL_INTERVAL)\n return () => clearInterval(interval)\n }, [fetchAll, usePolling])\n\n React.useEffect(() => {\n const handler = (event: Event) => {\n const detail = (event as CustomEvent<{ id?: string }>).detail\n if (\n detail?.id === 'notifications.notification.created' ||\n detail?.id === 'notifications.notification.batch_created' ||\n detail?.id === 'om:portal-bridge:reconnected'\n ) {\n fetchAll()\n }\n }\n window.addEventListener('om:portal-event', handler)\n return () => window.removeEventListener('om:portal-event', handler)\n }, [fetchAll])\n\n React.useEffect(() => {\n const onFocus = () => {\n fetchAll()\n }\n window.addEventListener('focus', onFocus)\n return () => window.removeEventListener('focus', onFocus)\n }, [fetchAll])\n\n const markAsRead = React.useCallback(async (id: string) => {\n await fetchJson(`${BASE}/${id}/read`, { method: 'PUT' })\n setNotifications((prev) =>\n prev.map((n) => (n.id === id ? { ...n, status: 'read', readAt: new Date().toISOString() } : n)),\n )\n setUnreadCount((prev) => Math.max(0, prev - 1))\n }, [])\n\n const dismiss = React.useCallback(async (id: string) => {\n await fetchJson(`${BASE}/${id}/dismiss`, { method: 'PUT' })\n setNotifications((prev) => prev.filter((n) => n.id !== id))\n setUnreadCount((prev) => {\n const wasDismissedUnread = notifications.find((n) => n.id === id)?.status === 'unread'\n return wasDismissedUnread ? Math.max(0, prev - 1) : prev\n })\n }, [notifications])\n\n const markAllRead = React.useCallback(async () => {\n await fetchJson(`${BASE}/mark-all-read`, { method: 'PUT' })\n setNotifications((prev) =>\n prev.map((n) => (n.status === 'unread' ? { ...n, status: 'read', readAt: new Date().toISOString() } : n)),\n )\n setUnreadCount(0)\n }, [])\n\n const refresh = React.useCallback(() => { fetchAll() }, [fetchAll])\n\n return { notifications, unreadCount, hasNew, isLoading, refresh, markAsRead, dismiss, markAllRead }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -26,8 +26,7 @@ function pickPreferredRoute(existing, candidate) {
26
26
  function buildPortalNav({
27
27
  routes,
28
28
  orgSlug,
29
- grantedFeatures,
30
- isPortalAdmin = false
29
+ grantedFeatures
31
30
  }) {
32
31
  const mainItems = [];
33
32
  const accountItems = [];
@@ -45,7 +44,7 @@ function buildPortalNav({
45
44
  const pattern = route.pattern ?? route.path;
46
45
  const nav = route.nav;
47
46
  const requireFeatures = route.requireCustomerFeatures ?? [];
48
- if (!isPortalAdmin && requireFeatures.length) {
47
+ if (requireFeatures.length) {
49
48
  if (!hasAllFeatures(grantedFeatures, requireFeatures)) continue;
50
49
  }
51
50
  const href = resolveHref(pattern, orgSlug);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/portal/utils/nav.ts"],
4
- "sourcesContent": ["import type { FrontendRouteManifestEntry } from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\n\nexport type PortalNavGroupId = 'main' | 'account'\n\nexport type PortalNavItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: string\n order: number\n}\n\nexport type PortalNavGroup = {\n id: PortalNavGroupId\n items: PortalNavItem[]\n}\n\nexport type BuildPortalNavOptions = {\n /** Route manifest to inspect (typically `getFrontendRouteManifests()`). */\n routes: readonly FrontendRouteManifestEntry[]\n /** Current customer org slug \u2014 substituted into `[orgSlug]` patterns. */\n orgSlug: string\n /** Feature strings granted to the current customer (may include wildcards). */\n grantedFeatures: readonly string[]\n /** If true, bypass feature checks (portal admin). Defaults to false. */\n isPortalAdmin?: boolean\n}\n\nfunction isPortalPattern(pattern: string | undefined): pattern is string {\n if (!pattern) return false\n return pattern.startsWith('/[orgSlug]/portal/') || pattern === '/[orgSlug]/portal'\n}\n\nfunction hasNoUnresolvedParams(href: string): boolean {\n return !href.includes('[')\n}\n\nfunction resolveHref(pattern: string, orgSlug: string): string {\n return pattern.replace('[orgSlug]', orgSlug)\n}\n\nfunction pickGroup(group: unknown): PortalNavGroupId {\n if (group === 'main' || group === 'account') return group\n return 'main'\n}\n\nfunction pickPreferredRoute(\n existing: FrontendRouteManifestEntry,\n candidate: FrontendRouteManifestEntry,\n): FrontendRouteManifestEntry {\n const existingFeatures = existing.requireCustomerFeatures ?? []\n const candidateFeatures = candidate.requireCustomerFeatures ?? []\n if (existingFeatures.length === 0 && candidateFeatures.length > 0) return candidate\n if (candidateFeatures.length === 0 && existingFeatures.length > 0) return existing\n if (existingFeatures.length !== candidateFeatures.length) {\n return candidateFeatures.length > existingFeatures.length ? candidate : existing\n }\n return existing\n}\n\n/**\n * Build the portal sidebar from the frontend route manifest.\n *\n * Mirrors `buildAdminNav()` for the portal surface: selects routes under\n * `/[orgSlug]/portal/*` that declare a `nav` block, applies\n * `requireCustomerFeatures` against the caller's grants (wildcards honored),\n * and returns ordered sidebar groups.\n *\n * Absence of `nav` on a metadata file means the page is routable but not\n * auto-listed \u2014 useful for detail/create pages.\n */\nexport function buildPortalNav({\n routes,\n orgSlug,\n grantedFeatures,\n isPortalAdmin = false,\n}: BuildPortalNavOptions): PortalNavGroup[] {\n const mainItems: PortalNavItem[] = []\n const accountItems: PortalNavItem[] = []\n\n const dedupedByPattern = new Map<string, FrontendRouteManifestEntry>()\n for (const route of routes) {\n const pattern = route.pattern ?? route.path\n if (!isPortalPattern(pattern)) continue\n if (route.navHidden) continue\n const nav = route.nav\n if (!nav || typeof nav.label !== 'string' || nav.label.length === 0) continue\n const existing = dedupedByPattern.get(pattern)\n dedupedByPattern.set(pattern, existing ? pickPreferredRoute(existing, route) : route)\n }\n\n for (const route of dedupedByPattern.values()) {\n const pattern = (route.pattern ?? route.path) as string\n const nav = route.nav!\n\n const requireFeatures = route.requireCustomerFeatures ?? []\n if (!isPortalAdmin && requireFeatures.length) {\n if (!hasAllFeatures(grantedFeatures as string[], requireFeatures as string[])) continue\n }\n\n const href = resolveHref(pattern, orgSlug)\n if (!hasNoUnresolvedParams(href)) continue\n\n const group = pickGroup(nav.group)\n const item: PortalNavItem = {\n id: `portal-nav:${pattern}`,\n label: nav.label,\n labelKey: nav.labelKey,\n href,\n icon: nav.icon,\n order: typeof nav.order === 'number' ? nav.order : 100,\n }\n if (group === 'account') accountItems.push(item)\n else mainItems.push(item)\n }\n\n const sortItems = (items: PortalNavItem[]) =>\n items.sort((a, b) => {\n if (a.order !== b.order) return a.order - b.order\n return a.label.localeCompare(b.label)\n })\n\n sortItems(mainItems)\n sortItems(accountItems)\n\n const groups: PortalNavGroup[] = []\n if (mainItems.length) groups.push({ id: 'main', items: mainItems })\n if (accountItems.length) groups.push({ id: 'account', items: accountItems })\n return groups\n}\n\n/**\n * Merge sidebar groups from the portal nav endpoint with items contributed via\n * `usePortalInjectedMenuItems`. Auto-discovered entries take precedence \u2014\n * injected items with matching `id` or `href` are dropped as duplicates.\n */\nexport function mergePortalSidebarGroupsWithInjected<TInjected extends { id: string; href?: string }>(\n discovered: readonly PortalNavGroup[],\n injected: {\n main: readonly TInjected[]\n account: readonly TInjected[]\n },\n): {\n main: Array<PortalNavItem | TInjected>\n account: Array<PortalNavItem | TInjected>\n} {\n const mergeGroup = <T extends PortalNavItem | TInjected>(\n base: readonly PortalNavItem[],\n extra: readonly TInjected[],\n ): Array<PortalNavItem | TInjected> => {\n const knownIds = new Set(base.map((item) => item.id))\n const knownHrefs = new Set(base.map((item) => item.href).filter((href): href is string => Boolean(href)))\n const merged: Array<PortalNavItem | TInjected> = [...base]\n for (const item of extra) {\n if (knownIds.has(item.id)) continue\n if (item.href && knownHrefs.has(item.href)) continue\n merged.push(item)\n knownIds.add(item.id)\n if (item.href) knownHrefs.add(item.href)\n }\n return merged\n }\n\n const mainBase = discovered.find((g) => g.id === 'main')?.items ?? []\n const accountBase = discovered.find((g) => g.id === 'account')?.items ?? []\n return {\n main: mergeGroup(mainBase, injected.main),\n account: mergeGroup(accountBase, injected.account),\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { FrontendRouteManifestEntry } from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\n\nexport type PortalNavGroupId = 'main' | 'account'\n\nexport type PortalNavItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: string\n order: number\n}\n\nexport type PortalNavGroup = {\n id: PortalNavGroupId\n items: PortalNavItem[]\n}\n\nexport type BuildPortalNavOptions = {\n /** Route manifest to inspect (typically `getFrontendRouteManifests()`). */\n routes: readonly FrontendRouteManifestEntry[]\n /** Current customer org slug \u2014 substituted into `[orgSlug]` patterns. */\n orgSlug: string\n /** Concrete effective features granted to the current customer. */\n grantedFeatures: readonly string[]\n /** @deprecated Portal admins must receive concrete effective features. */\n isPortalAdmin?: boolean\n}\n\nfunction isPortalPattern(pattern: string | undefined): pattern is string {\n if (!pattern) return false\n return pattern.startsWith('/[orgSlug]/portal/') || pattern === '/[orgSlug]/portal'\n}\n\nfunction hasNoUnresolvedParams(href: string): boolean {\n return !href.includes('[')\n}\n\nfunction resolveHref(pattern: string, orgSlug: string): string {\n return pattern.replace('[orgSlug]', orgSlug)\n}\n\nfunction pickGroup(group: unknown): PortalNavGroupId {\n if (group === 'main' || group === 'account') return group\n return 'main'\n}\n\nfunction pickPreferredRoute(\n existing: FrontendRouteManifestEntry,\n candidate: FrontendRouteManifestEntry,\n): FrontendRouteManifestEntry {\n const existingFeatures = existing.requireCustomerFeatures ?? []\n const candidateFeatures = candidate.requireCustomerFeatures ?? []\n if (existingFeatures.length === 0 && candidateFeatures.length > 0) return candidate\n if (candidateFeatures.length === 0 && existingFeatures.length > 0) return existing\n if (existingFeatures.length !== candidateFeatures.length) {\n return candidateFeatures.length > existingFeatures.length ? candidate : existing\n }\n return existing\n}\n\n/**\n * Build the portal sidebar from the frontend route manifest.\n *\n * Mirrors `buildAdminNav()` for the portal surface: selects routes under\n * `/[orgSlug]/portal/*` that declare a `nav` block, applies\n * `requireCustomerFeatures` against the caller's grants (wildcards honored),\n * and returns ordered sidebar groups.\n *\n * Absence of `nav` on a metadata file means the page is routable but not\n * auto-listed \u2014 useful for detail/create pages.\n */\nexport function buildPortalNav({\n routes,\n orgSlug,\n grantedFeatures,\n}: BuildPortalNavOptions): PortalNavGroup[] {\n const mainItems: PortalNavItem[] = []\n const accountItems: PortalNavItem[] = []\n\n const dedupedByPattern = new Map<string, FrontendRouteManifestEntry>()\n for (const route of routes) {\n const pattern = route.pattern ?? route.path\n if (!isPortalPattern(pattern)) continue\n if (route.navHidden) continue\n const nav = route.nav\n if (!nav || typeof nav.label !== 'string' || nav.label.length === 0) continue\n const existing = dedupedByPattern.get(pattern)\n dedupedByPattern.set(pattern, existing ? pickPreferredRoute(existing, route) : route)\n }\n\n for (const route of dedupedByPattern.values()) {\n const pattern = (route.pattern ?? route.path) as string\n const nav = route.nav!\n\n const requireFeatures = route.requireCustomerFeatures ?? []\n if (requireFeatures.length) {\n if (!hasAllFeatures(grantedFeatures as string[], requireFeatures as string[])) continue\n }\n\n const href = resolveHref(pattern, orgSlug)\n if (!hasNoUnresolvedParams(href)) continue\n\n const group = pickGroup(nav.group)\n const item: PortalNavItem = {\n id: `portal-nav:${pattern}`,\n label: nav.label,\n labelKey: nav.labelKey,\n href,\n icon: nav.icon,\n order: typeof nav.order === 'number' ? nav.order : 100,\n }\n if (group === 'account') accountItems.push(item)\n else mainItems.push(item)\n }\n\n const sortItems = (items: PortalNavItem[]) =>\n items.sort((a, b) => {\n if (a.order !== b.order) return a.order - b.order\n return a.label.localeCompare(b.label)\n })\n\n sortItems(mainItems)\n sortItems(accountItems)\n\n const groups: PortalNavGroup[] = []\n if (mainItems.length) groups.push({ id: 'main', items: mainItems })\n if (accountItems.length) groups.push({ id: 'account', items: accountItems })\n return groups\n}\n\n/**\n * Merge sidebar groups from the portal nav endpoint with items contributed via\n * `usePortalInjectedMenuItems`. Auto-discovered entries take precedence \u2014\n * injected items with matching `id` or `href` are dropped as duplicates.\n */\nexport function mergePortalSidebarGroupsWithInjected<TInjected extends { id: string; href?: string }>(\n discovered: readonly PortalNavGroup[],\n injected: {\n main: readonly TInjected[]\n account: readonly TInjected[]\n },\n): {\n main: Array<PortalNavItem | TInjected>\n account: Array<PortalNavItem | TInjected>\n} {\n const mergeGroup = <T extends PortalNavItem | TInjected>(\n base: readonly PortalNavItem[],\n extra: readonly TInjected[],\n ): Array<PortalNavItem | TInjected> => {\n const knownIds = new Set(base.map((item) => item.id))\n const knownHrefs = new Set(base.map((item) => item.href).filter((href): href is string => Boolean(href)))\n const merged: Array<PortalNavItem | TInjected> = [...base]\n for (const item of extra) {\n if (knownIds.has(item.id)) continue\n if (item.href && knownHrefs.has(item.href)) continue\n merged.push(item)\n knownIds.add(item.id)\n if (item.href) knownHrefs.add(item.href)\n }\n return merged\n }\n\n const mainBase = discovered.find((g) => g.id === 'main')?.items ?? []\n const accountBase = discovered.find((g) => g.id === 'account')?.items ?? []\n return {\n main: mergeGroup(mainBase, injected.main),\n account: mergeGroup(accountBase, injected.account),\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,7 +6,7 @@ function ErrorNotice({ title, message, action }) {
6
6
  const t = useT();
7
7
  const resolvedTitle = title ?? t("ui.errors.defaultTitle", "Something went wrong");
8
8
  const resolvedMessage = message ?? t("ui.errors.defaultMessage", "Unable to load data. Please try again.");
9
- return /* @__PURE__ */ jsxs(Alert, { variant: "destructive", children: [
9
+ return /* @__PURE__ */ jsxs(Alert, { status: "error", children: [
10
10
  /* @__PURE__ */ jsx(AlertTitle, { children: resolvedTitle }),
11
11
  /* @__PURE__ */ jsx(AlertDescription, { children: resolvedMessage }),
12
12
  action ? /* @__PURE__ */ jsx("div", { className: "mt-2", children: action }) : null
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/ErrorNotice.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Alert, AlertDescription, AlertTitle } from './alert'\n\nexport function ErrorNotice({ title, message, action }: {\n title?: string\n message?: string\n action?: React.ReactNode\n}) {\n const t = useT()\n const resolvedTitle = title ?? t('ui.errors.defaultTitle', 'Something went wrong')\n const resolvedMessage = message ?? t('ui.errors.defaultMessage', 'Unable to load data. Please try again.')\n return (\n <Alert variant=\"destructive\">\n <AlertTitle>{resolvedTitle}</AlertTitle>\n <AlertDescription>{resolvedMessage}</AlertDescription>\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </Alert>\n )\n}\n"],
5
- "mappings": ";AAcI,SACE,KADF;AAZJ,SAAS,YAAY;AACrB,SAAS,OAAO,kBAAkB,kBAAkB;AAE7C,SAAS,YAAY,EAAE,OAAO,SAAS,OAAO,GAIlD;AACD,QAAM,IAAI,KAAK;AACf,QAAM,gBAAgB,SAAS,EAAE,0BAA0B,sBAAsB;AACjF,QAAM,kBAAkB,WAAW,EAAE,4BAA4B,wCAAwC;AACzG,SACE,qBAAC,SAAM,SAAQ,eACb;AAAA,wBAAC,cAAY,yBAAc;AAAA,IAC3B,oBAAC,oBAAkB,2BAAgB;AAAA,IAClC,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,KACnD;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Alert, AlertDescription, AlertTitle } from './alert'\n\nexport function ErrorNotice({ title, message, action }: {\n title?: string\n message?: string\n action?: React.ReactNode\n}) {\n const t = useT()\n const resolvedTitle = title ?? t('ui.errors.defaultTitle', 'Something went wrong')\n const resolvedMessage = message ?? t('ui.errors.defaultMessage', 'Unable to load data. Please try again.')\n return (\n <Alert status=\"error\">\n <AlertTitle>{resolvedTitle}</AlertTitle>\n <AlertDescription>{resolvedMessage}</AlertDescription>\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </Alert>\n )\n}\n"],
5
+ "mappings": ";AAcI,SACE,KADF;AAZJ,SAAS,YAAY;AACrB,SAAS,OAAO,kBAAkB,kBAAkB;AAE7C,SAAS,YAAY,EAAE,OAAO,SAAS,OAAO,GAIlD;AACD,QAAM,IAAI,KAAK;AACf,QAAM,gBAAgB,SAAS,EAAE,0BAA0B,sBAAsB;AACjF,QAAM,kBAAkB,WAAW,EAAE,4BAA4B,wCAAwC;AACzG,SACE,qBAAC,SAAM,QAAO,SACZ;AAAA,wBAAC,cAAY,yBAAc;AAAA,IAC3B,oBAAC,oBAAkB,2BAAgB;AAAA,IAClC,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,KACnD;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -27,7 +27,7 @@ function Notice({
27
27
  compact = false
28
28
  }) {
29
29
  if (process.env.NODE_ENV === "development") {
30
- logger.warn('<Notice> is deprecated. Use <Alert variant="destructive|warning|info"> instead. See: docs/design-system/migration-tables.md');
30
+ logger.warn('<Notice> is deprecated. Use <Alert status="error|warning|information"> instead. See: docs/design-system/migration-tables.md');
31
31
  }
32
32
  const styles = variantStyles[variant];
33
33
  if (compact || !title && !action && (children || message)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/Notice.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\n/**\n * @deprecated Use <Alert variant=\"destructive|warning|info\"> instead.\n * Migration guide: docs/design-system/migration-tables.md#j3-component-mapping\n *\n * Notice variant=\"error\" \u2192 Alert variant=\"destructive\"\n * Notice variant=\"warning\" \u2192 Alert variant=\"warning\"\n * Notice variant=\"info\" \u2192 Alert variant=\"info\"\n */\n\nconst variantStyles = {\n error: {\n container: 'border-status-error-border bg-status-error-bg text-status-error-text',\n icon: 'border-status-error-icon',\n },\n info: {\n container: 'border-status-info-border bg-status-info-bg text-status-info-text',\n icon: 'border-status-info-icon',\n },\n warning: {\n container: 'border-status-warning-border bg-status-warning-bg text-status-warning-text',\n icon: 'border-status-warning-icon',\n },\n} as const\n\nexport type NoticeVariant = keyof typeof variantStyles\n\nexport type NoticeProps = {\n variant?: NoticeVariant\n title?: string\n message?: string\n action?: React.ReactNode\n children?: React.ReactNode\n className?: string\n compact?: boolean\n}\n\nexport function Notice({\n variant = 'info',\n title,\n message,\n action,\n children,\n className,\n compact = false,\n}: NoticeProps) {\n if (process.env.NODE_ENV === 'development') {\n logger.warn('<Notice> is deprecated. Use <Alert variant=\"destructive|warning|info\"> instead. See: docs/design-system/migration-tables.md')\n }\n\n const styles = variantStyles[variant]\n\n if (compact || (!title && !action && (children || message))) {\n return (\n <div className={cn('rounded-md border px-3 py-2 text-sm', styles.container, className)}>\n {children ?? message}\n </div>\n )\n }\n\n return (\n <div className={cn('rounded-md border p-4', styles.container, className)}>\n <div className=\"flex items-start gap-3\">\n <span\n className={cn('inline-block mt-0.5 h-4 w-4 rounded-full border-2 shrink-0', styles.icon)}\n aria-hidden\n />\n <div className=\"space-y-1\">\n {title ? <div className=\"text-sm font-medium\">{title}</div> : null}\n {message ? <div className=\"text-sm opacity-90\">{message}</div> : null}\n {children}\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </div>\n </div>\n </div>\n )\n}\n"],
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\n/**\n * @deprecated Use <Alert status=\"error|warning|information\"> instead.\n * Migration guide: docs/design-system/migration-tables.md#j3-component-mapping\n *\n * Notice variant=\"error\" \u2192 Alert status=\"error\"\n * Notice variant=\"warning\" \u2192 Alert status=\"warning\"\n * Notice variant=\"info\" \u2192 Alert status=\"information\"\n */\n\nconst variantStyles = {\n error: {\n container: 'border-status-error-border bg-status-error-bg text-status-error-text',\n icon: 'border-status-error-icon',\n },\n info: {\n container: 'border-status-info-border bg-status-info-bg text-status-info-text',\n icon: 'border-status-info-icon',\n },\n warning: {\n container: 'border-status-warning-border bg-status-warning-bg text-status-warning-text',\n icon: 'border-status-warning-icon',\n },\n} as const\n\nexport type NoticeVariant = keyof typeof variantStyles\n\nexport type NoticeProps = {\n variant?: NoticeVariant\n title?: string\n message?: string\n action?: React.ReactNode\n children?: React.ReactNode\n className?: string\n compact?: boolean\n}\n\nexport function Notice({\n variant = 'info',\n title,\n message,\n action,\n children,\n className,\n compact = false,\n}: NoticeProps) {\n if (process.env.NODE_ENV === 'development') {\n logger.warn('<Notice> is deprecated. Use <Alert status=\"error|warning|information\"> instead. See: docs/design-system/migration-tables.md')\n }\n\n const styles = variantStyles[variant]\n\n if (compact || (!title && !action && (children || message))) {\n return (\n <div className={cn('rounded-md border px-3 py-2 text-sm', styles.container, className)}>\n {children ?? message}\n </div>\n )\n }\n\n return (\n <div className={cn('rounded-md border p-4', styles.container, className)}>\n <div className=\"flex items-start gap-3\">\n <span\n className={cn('inline-block mt-0.5 h-4 w-4 rounded-full border-2 shrink-0', styles.icon)}\n aria-hidden\n />\n <div className=\"space-y-1\">\n {title ? <div className=\"text-sm font-medium\">{title}</div> : null}\n {message ? <div className=\"text-sm opacity-90\">{message}</div> : null}\n {children}\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </div>\n </div>\n </div>\n )\n}\n"],
5
5
  "mappings": ";AA4DM,cAaE,YAbF;AA1DN,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,IAAI;AAWhC,MAAM,gBAAgB;AAAA,EACpB,OAAO;AAAA,IACL,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AAAA,EACA,MAAM;AAAA,IACJ,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AACF;AAcO,SAAS,OAAO;AAAA,EACrB,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AACZ,GAAgB;AACd,MAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,WAAO,KAAK,6HAA6H;AAAA,EAC3I;AAEA,QAAM,SAAS,cAAc,OAAO;AAEpC,MAAI,WAAY,CAAC,SAAS,CAAC,WAAW,YAAY,UAAW;AAC3D,WACE,oBAAC,SAAI,WAAW,GAAG,uCAAuC,OAAO,WAAW,SAAS,GAClF,sBAAY,SACf;AAAA,EAEJ;AAEA,SACE,oBAAC,SAAI,WAAW,GAAG,yBAAyB,OAAO,WAAW,SAAS,GACrE,+BAAC,SAAI,WAAU,0BACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,GAAG,8DAA8D,OAAO,IAAI;AAAA,QACvF,eAAW;AAAA;AAAA,IACb;AAAA,IACA,qBAAC,SAAI,WAAU,aACZ;AAAA,cAAQ,oBAAC,SAAI,WAAU,uBAAuB,iBAAM,IAAS;AAAA,MAC7D,UAAU,oBAAC,SAAI,WAAU,sBAAsB,mBAAQ,IAAS;AAAA,MAChE;AAAA,MACA,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,OACnD;AAAA,KACF,GACF;AAEJ;",
6
6
  "names": []
7
7
  }