@open-mercato/ui 0.6.7-develop.6860.1.f796b22e80 → 0.6.7

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 (167) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +2 -4
  3. package/agentic/standalone-guide.md +1 -1
  4. package/build.mjs +54 -2
  5. package/dist/ai/AiChat.js +2 -21
  6. package/dist/ai/AiChat.js.map +2 -2
  7. package/dist/backend/AppShell.js +4 -43
  8. package/dist/backend/AppShell.js.map +2 -2
  9. package/dist/backend/AuthSessionGuard.js +0 -14
  10. package/dist/backend/AuthSessionGuard.js.map +2 -2
  11. package/dist/backend/BackendChromeProvider.js +1 -6
  12. package/dist/backend/BackendChromeProvider.js.map +2 -2
  13. package/dist/backend/CrudForm.js +4 -6
  14. package/dist/backend/CrudForm.js.map +2 -2
  15. package/dist/backend/DataTable.js +18 -20
  16. package/dist/backend/DataTable.js.map +2 -2
  17. package/dist/backend/JsonBuilder.js +3 -3
  18. package/dist/backend/JsonBuilder.js.map +2 -2
  19. package/dist/backend/NextStepCallout.js +4 -4
  20. package/dist/backend/NextStepCallout.js.map +1 -1
  21. package/dist/backend/ProfileDropdown.js +1 -2
  22. package/dist/backend/ProfileDropdown.js.map +2 -2
  23. package/dist/backend/RowActions.js +2 -2
  24. package/dist/backend/RowActions.js.map +2 -2
  25. package/dist/backend/confirm-dialog/ConfirmDialog.js +1 -13
  26. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  27. package/dist/backend/crud/useGroupOrder.js +21 -14
  28. package/dist/backend/crud/useGroupOrder.js.map +2 -2
  29. package/dist/backend/detail/AddressTiles.js +5 -4
  30. package/dist/backend/detail/AddressTiles.js.map +2 -2
  31. package/dist/backend/detail/AttachmentDeleteDialog.js +1 -1
  32. package/dist/backend/detail/AttachmentDeleteDialog.js.map +2 -2
  33. package/dist/backend/detail/AttachmentMetadataDialog.js.map +1 -1
  34. package/dist/backend/detail/AttachmentsSection.js +1 -1
  35. package/dist/backend/detail/AttachmentsSection.js.map +2 -2
  36. package/dist/backend/detail/NotesSection.js +1 -1
  37. package/dist/backend/detail/NotesSection.js.map +2 -2
  38. package/dist/backend/detail/TagsSection.js +1 -1
  39. package/dist/backend/detail/TagsSection.js.map +2 -2
  40. package/dist/backend/forms/FormHeader.js +2 -1
  41. package/dist/backend/forms/FormHeader.js.map +2 -2
  42. package/dist/backend/icons/lucideRegistry.generated.js +25 -1
  43. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  44. package/dist/backend/icons/lucideRegistry.js +3 -3
  45. package/dist/backend/icons/lucideRegistry.js.map +2 -2
  46. package/dist/backend/injection/spotIds.js +18 -23
  47. package/dist/backend/injection/spotIds.js.map +2 -2
  48. package/dist/backend/schedule/ScheduleAgenda.js +3 -3
  49. package/dist/backend/schedule/ScheduleAgenda.js.map +1 -1
  50. package/dist/backend/schedule/ScheduleGrid.js +3 -3
  51. package/dist/backend/schedule/ScheduleGrid.js.map +1 -1
  52. package/dist/backend/schedule/ScheduleToolbar.js +70 -87
  53. package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
  54. package/dist/backend/sidebar/SidebarCustomizationEditor.js +2 -30
  55. package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
  56. package/dist/backend/sidebar/customization-helpers.js +0 -30
  57. package/dist/backend/sidebar/customization-helpers.js.map +2 -2
  58. package/dist/backend/utils/api.js +13 -3
  59. package/dist/backend/utils/api.js.map +2 -2
  60. package/dist/backend/utils/apiCall.js +1 -21
  61. package/dist/backend/utils/apiCall.js.map +2 -2
  62. package/dist/backend/utils/customFieldDefs.js.map +2 -2
  63. package/dist/backend/utils/customFieldForms.js +9 -10
  64. package/dist/backend/utils/customFieldForms.js.map +2 -2
  65. package/dist/backend/utils/nav.js +2 -5
  66. package/dist/backend/utils/nav.js.map +2 -2
  67. package/dist/frontend/LanguageSwitcher.js +1 -2
  68. package/dist/frontend/LanguageSwitcher.js.map +2 -2
  69. package/dist/portal/PortalContext.js +1 -1
  70. package/dist/portal/PortalContext.js.map +2 -2
  71. package/dist/portal/hooks/usePortalEventBridge.js +0 -6
  72. package/dist/portal/hooks/usePortalEventBridge.js.map +2 -2
  73. package/dist/portal/hooks/usePortalNotifications.js +4 -42
  74. package/dist/portal/hooks/usePortalNotifications.js.map +2 -2
  75. package/dist/portal/utils/nav.js +3 -2
  76. package/dist/portal/utils/nav.js.map +2 -2
  77. package/dist/primitives/button.js +4 -9
  78. package/dist/primitives/button.js.map +2 -2
  79. package/jest.config.cjs +1 -2
  80. package/package.json +18 -21
  81. package/src/ai/AiChat.tsx +1 -30
  82. package/src/ai/__tests__/AiChat.test.tsx +0 -31
  83. package/src/backend/AppShell.tsx +4 -55
  84. package/src/backend/AuthSessionGuard.tsx +0 -29
  85. package/src/backend/BackendChromeProvider.tsx +1 -19
  86. package/src/backend/CrudForm.tsx +4 -18
  87. package/src/backend/DataTable.tsx +19 -36
  88. package/src/backend/JsonBuilder.tsx +3 -3
  89. package/src/backend/NextStepCallout.tsx +4 -4
  90. package/src/backend/ProfileDropdown.tsx +0 -1
  91. package/src/backend/RowActions.tsx +2 -2
  92. package/src/backend/__tests__/AppShell.test.tsx +7 -65
  93. package/src/backend/__tests__/AuthSessionGuard.test.tsx +0 -52
  94. package/src/backend/__tests__/BackendChromeProvider.test.tsx +2 -98
  95. package/src/backend/__tests__/DataTable.perspectiveStorage.test.ts +1 -62
  96. package/src/backend/__tests__/nav-utils.test.ts +0 -139
  97. package/src/backend/confirm-dialog/ConfirmDialog.tsx +1 -16
  98. package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +1 -68
  99. package/src/backend/crud/__tests__/useGroupOrder.test.ts +1 -39
  100. package/src/backend/crud/useGroupOrder.ts +25 -30
  101. package/src/backend/detail/AddressTiles.tsx +6 -3
  102. package/src/backend/detail/AttachmentDeleteDialog.tsx +1 -1
  103. package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
  104. package/src/backend/detail/AttachmentsSection.tsx +1 -1
  105. package/src/backend/detail/NotesSection.tsx +1 -1
  106. package/src/backend/detail/TagsSection.tsx +1 -1
  107. package/src/backend/detail/__tests__/AttachmentAssignmentsEditor.test.tsx +0 -21
  108. package/src/backend/forms/FormHeader.tsx +2 -1
  109. package/src/backend/icons/lucideRegistry.generated.tsx +37 -3
  110. package/src/backend/icons/lucideRegistry.ts +2 -2
  111. package/src/backend/injection/spotIds.ts +18 -23
  112. package/src/backend/schedule/ScheduleAgenda.tsx +3 -3
  113. package/src/backend/schedule/ScheduleGrid.tsx +3 -3
  114. package/src/backend/schedule/ScheduleToolbar.tsx +68 -89
  115. package/src/backend/sidebar/SidebarCustomizationEditor.tsx +3 -42
  116. package/src/backend/sidebar/customization-helpers.ts +0 -47
  117. package/src/backend/utils/__tests__/api.test.ts +27 -0
  118. package/src/backend/utils/__tests__/apiCall.test.ts +0 -61
  119. package/src/backend/utils/api.ts +20 -3
  120. package/src/backend/utils/apiCall.ts +1 -29
  121. package/src/backend/utils/customFieldDefs.ts +0 -1
  122. package/src/backend/utils/customFieldForms.ts +11 -15
  123. package/src/backend/utils/nav.ts +2 -22
  124. package/src/frontend/LanguageSwitcher.tsx +0 -1
  125. package/src/portal/PortalContext.tsx +1 -1
  126. package/src/portal/hooks/usePortalEventBridge.ts +0 -6
  127. package/src/portal/hooks/usePortalNotifications.ts +12 -55
  128. package/src/portal/utils/__tests__/nav.test.ts +2 -2
  129. package/src/portal/utils/nav.ts +4 -3
  130. package/src/primitives/button.tsx +4 -10
  131. package/dist/backend/icons/lucideRegistryRuntime.js +0 -28
  132. package/dist/backend/icons/lucideRegistryRuntime.js.map +0 -7
  133. package/dist/backend/perspectiveState.js +0 -34
  134. package/dist/backend/perspectiveState.js.map +0 -7
  135. package/dist/backend/utils/fetchAllPages.js +0 -57
  136. package/dist/backend/utils/fetchAllPages.js.map +0 -7
  137. package/dist/portal/hooks/portalBridgeStatus.js +0 -25
  138. package/dist/portal/hooks/portalBridgeStatus.js.map +0 -7
  139. package/figma/alert.figma.tsx +0 -35
  140. package/figma/badge.figma.tsx +0 -33
  141. package/figma/button.figma.tsx +0 -33
  142. package/figma/checkbox.figma.tsx +0 -28
  143. package/figma/drawer.figma.tsx +0 -40
  144. package/figma/input-variants.figma.tsx +0 -58
  145. package/figma/radio.figma.tsx +0 -16
  146. package/figma/select.figma.tsx +0 -24
  147. package/figma/status-badge.figma.tsx +0 -24
  148. package/figma/switch.figma.tsx +0 -28
  149. package/figma/tabs.figma.tsx +0 -22
  150. package/figma/tag.figma.tsx +0 -27
  151. package/figma.config.json +0 -6
  152. package/scripts/lucideRegistrySource.cjs +0 -133
  153. package/src/backend/__tests__/CrudForm.fieldsetBinding.test.tsx +0 -158
  154. package/src/backend/__tests__/CrudForm.groupInjectionColumn.test.tsx +0 -98
  155. package/src/backend/__tests__/DataTable.metaHiddenLateColumns.test.tsx +0 -105
  156. package/src/backend/icons/__tests__/lucideRegistryGenerator.test.ts +0 -177
  157. package/src/backend/icons/lucideRegistryRuntime.tsx +0 -36
  158. package/src/backend/injection/__tests__/spotIds.extension-points.test.ts +0 -38
  159. package/src/backend/perspectiveState.ts +0 -44
  160. package/src/backend/sidebar/__tests__/group-visibility.test.ts +0 -131
  161. package/src/backend/utils/__tests__/customFieldForms.optionsPaging.test.ts +0 -95
  162. package/src/backend/utils/__tests__/fetchAllPages.test.ts +0 -103
  163. package/src/backend/utils/fetchAllPages.ts +0 -93
  164. package/src/portal/hooks/__tests__/usePortalEventBridge.test.tsx +0 -66
  165. package/src/portal/hooks/__tests__/usePortalNotifications.test.tsx +0 -155
  166. package/src/portal/hooks/portalBridgeStatus.ts +0 -29
  167. package/src/primitives/__tests__/button.test.tsx +0 -70
@@ -47,6 +47,18 @@ export async function withScopedApiHeaders<T>(headers: Record<string, string>, r
47
47
  return scopedHeaders.withScopedHeaders(headers, run)
48
48
  }
49
49
 
50
+ function readPathname(): string {
51
+ return typeof window !== 'undefined' ? window.location?.pathname ?? '' : ''
52
+ }
53
+
54
+ function isLoginPathname(pathname: string): boolean {
55
+ return pathname.startsWith('/login')
56
+ }
57
+
58
+ function isPortalPathname(pathname: string): boolean {
59
+ return /\/[^/]+\/portal(\/|$)/.test(pathname)
60
+ }
61
+
50
62
  export class UnauthorizedError extends Error {
51
63
  readonly status = 401
52
64
  constructor(message = 'Unauthorized') {
@@ -166,10 +178,15 @@ export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Pr
166
178
  const requestHeaders = new Headers(mergedInit?.headers)
167
179
  const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, 'x-om-unauthorized-redirect')
168
180
  const disableForbiddenRedirect = readRedirectOverride(requestHeaders, 'x-om-forbidden-redirect')
181
+ // Snapshot the pathname BEFORE the request is sent. A 401 for a request that
182
+ // started on the login page must stay silent even when the response lands after
183
+ // the post-login client-side navigation to /backend — otherwise the stale
184
+ // pre-auth 401 raises a bogus "Session expired" banner right after signing in.
185
+ const requestPathname = readPathname()
169
186
  const res = await baseFetch(input, mergedInit)
170
- const pathname = typeof window !== 'undefined' ? window.location.pathname : ''
171
- const onLoginPage = pathname.startsWith('/login')
172
- const onPortalRoute = /\/[^/]+\/portal(\/|$)/.test(pathname)
187
+ const responsePathname = readPathname()
188
+ const onLoginPage = isLoginPathname(requestPathname) || isLoginPathname(responsePathname)
189
+ const onPortalRoute = isPortalPathname(requestPathname) || isPortalPathname(responsePathname)
173
190
  if (res.status === 401) {
174
191
  // Trigger same redirect flow as protected pages
175
192
  // Skip for staff login page and all portal routes (portal has its own auth)
@@ -19,27 +19,6 @@ export type ApiCallResult<TReturn> = {
19
19
 
20
20
  const scopedRequestHeaders = createScopedHeaderStack()
21
21
 
22
- function isAbortError(error: unknown): boolean {
23
- return typeof error === 'object'
24
- && error !== null
25
- && (error as { name?: unknown }).name === 'AbortError'
26
- }
27
-
28
- function createAbortError(): Error {
29
- if (typeof DOMException === 'function') {
30
- return new DOMException('[internal] The operation was aborted.', 'AbortError')
31
- }
32
- const error = new Error('[internal] The operation was aborted.')
33
- error.name = 'AbortError'
34
- return error
35
- }
36
-
37
- function resolveAbortSignal(input: RequestInfo | URL, init?: RequestInit): AbortSignal | null {
38
- if (init?.signal) return init.signal
39
- if (typeof Request !== 'undefined' && input instanceof Request) return input.signal ?? null
40
- return null
41
- }
42
-
43
22
  function mergeHeaders(base: HeadersInit | undefined, extra: Record<string, string>): HeadersInit {
44
23
  if (!base) return extra
45
24
  if (typeof Headers !== 'undefined' && base instanceof Headers) {
@@ -84,16 +63,9 @@ export async function apiCall<TReturn = Record<string, unknown>>(
84
63
  : response
85
64
  if (parser) result = await parser(source)
86
65
  else result = await readJsonSafe<TReturn>(source, fallback)
87
- } catch (err) {
88
- if (isAbortError(err)) throw err
66
+ } catch {
89
67
  result = fallback
90
68
  }
91
- // `readJsonSafe` swallows the abort that cancels an in-flight body read, so a
92
- // request aborted after its response headers arrived would otherwise look like
93
- // a successful call that returned an empty payload.
94
- if (result == null && resolveAbortSignal(input, init)?.aborted) {
95
- throw createAbortError()
96
- }
97
69
  return {
98
70
  ok: response.ok,
99
71
  status: response.status,
@@ -39,7 +39,6 @@ export type CustomFieldDefDto = {
39
39
  dictionaryId?: string
40
40
  dictionaryInlineCreate?: boolean
41
41
  defaultValue?: string | number | boolean | null
42
- relatedEntityId?: string
43
42
  }
44
43
 
45
44
  export type CustomFieldsetGroupDto = {
@@ -9,7 +9,7 @@ import {
9
9
  fetchCustomFieldDefinitionsPayload,
10
10
  } from './customFieldDefs'
11
11
  import { FieldRegistry, loadGeneratedFieldRegistrations } from '../fields/registry'
12
- import { fetchAllOffsetPages } from './fetchAllPages'
12
+ import { apiCall } from './apiCall'
13
13
  import { normalizeCustomFieldOptions } from '@open-mercato/shared/modules/entities/options'
14
14
  import { CURRENCY_OPTIONS_URL } from '@open-mercato/shared/modules/entities/kinds'
15
15
 
@@ -42,22 +42,18 @@ function buildOptionsUrl(base: string, query?: string): string {
42
42
  }
43
43
  }
44
44
 
45
+ type OptionsResponse = { items?: unknown[] }
46
+
45
47
  async function loadRemoteOptions(url: string): Promise<Array<{ value: string; label: string }>> {
46
48
  try {
47
- // Options routes bound how many rows one response carries (dictionary
48
- // entries at 500), so walk every page instead of reading a single
49
- // response a stored value past the first page must stay selectable and
50
- // resolvable to its label. Routes without `hasMore` still resolve in one
51
- // request.
52
- const pages = await fetchAllOffsetPages(url)
53
- if (!pages.ok) return []
54
- return pages.items.map((item) => {
55
- const record = item && typeof item === 'object' ? (item as Record<string, unknown>) : null
56
- return {
57
- value: String(record?.value ?? item),
58
- label: String(record?.label ?? record?.value ?? item),
59
- }
60
- })
49
+ const call = await apiCall<OptionsResponse>(url, undefined, { fallback: { items: [] } })
50
+ if (!call.ok) return []
51
+ const payload = call.result ?? { items: [] }
52
+ const items = Array.isArray(payload?.items) ? payload.items : []
53
+ return items.map((it: any) => ({
54
+ value: String(it?.value ?? it),
55
+ label: String(it?.label ?? it?.value ?? it),
56
+ }))
61
57
  } catch {
62
58
  return []
63
59
  }
@@ -100,26 +100,6 @@ export type SettingsSectionItem = {
100
100
  children?: SettingsSectionItem[]
101
101
  }
102
102
 
103
- /**
104
- * Slug of a rendered group label, used as the pre-#4843 settings section id.
105
- *
106
- * `sectionOrder` used to be keyed by these slugs, which made ordering depend on the active locale.
107
- * Kept only as a lookup fallback so third-party callers passing a legacy map keep their weights.
108
- *
109
- * @deprecated Key `sectionOrder` by the untranslated group id (`AdminNavItem.groupId`) instead.
110
- */
111
- function legacySettingsSectionSlug(groupLabel: string): string {
112
- return groupLabel.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')
113
- }
114
-
115
- /**
116
- * Groups the settings-context nav entries into ordered sections.
117
- *
118
- * `sectionOrder` is keyed by the untranslated group id (`AdminNavItem.groupId`, i.e. the page's
119
- * `pageGroupKey` when it declares one) — the same convention the main sidebar's `defaultGroupOrder`
120
- * follows. Keying off the rendered label instead made every non-English deployment miss its weights
121
- * and fall back to the catch-all bucket (#4843).
122
- */
123
103
  export function buildSettingsSections(
124
104
  entries: AdminNavItem[],
125
105
  sectionOrder: Record<string, number>
@@ -143,8 +123,8 @@ export function buildSettingsSections(
143
123
  }
144
124
 
145
125
  for (const item of settingsItems) {
146
- const sectionId = item.groupId
147
- const order = sectionOrder[sectionId] ?? sectionOrder[legacySettingsSectionSlug(item.group)] ?? 999
126
+ const sectionId = item.group.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')
127
+ const order = sectionOrder[sectionId] ?? 999
148
128
 
149
129
  if (!sectionMap.has(sectionId)) {
150
130
  sectionMap.set(sectionId, {
@@ -24,7 +24,6 @@ export function LanguageSwitcher() {
24
24
  pl: t('common.languages.polish', 'Polski'),
25
25
  es: t('common.languages.spanish', 'Español'),
26
26
  de: t('common.languages.german', 'Deutsch'),
27
- ko: t('common.languages.korean', '한국어'),
28
27
  }
29
28
 
30
29
  async function setLocale(locale: Locale) {
@@ -91,7 +91,7 @@ export function PortalProvider({ orgSlug, children, initialAuth, initialTenant }
91
91
  },
92
92
  roles: [],
93
93
  resolvedFeatures: initialAuth.resolvedFeatures,
94
- isPortalAdmin: initialAuth.isPortalAdmin === true,
94
+ isPortalAdmin: initialAuth.resolvedFeatures.some((f) => f === 'portal.*'),
95
95
  loading: false,
96
96
  error: null,
97
97
  }
@@ -3,7 +3,6 @@ import { useEffect, useRef } from 'react'
3
3
  import type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'
4
4
  import { PORTAL_EVENT_DOM_NAME } from './usePortalAppEvent'
5
5
  import { createLogger } from '@open-mercato/shared/lib/logger'
6
- import { publishPortalBridgeHealth } from './portalBridgeStatus'
7
6
 
8
7
  const logger = createLogger('ui').child({ component: 'PortalEventBridge' })
9
8
 
@@ -63,7 +62,6 @@ export function usePortalEventBridge(): void {
63
62
  if (heartbeatTimer.current) clearTimeout(heartbeatTimer.current)
64
63
  heartbeatTimer.current = setTimeout(() => {
65
64
  logger.warn('Heartbeat timeout — reconnecting')
66
- publishPortalBridgeHealth(false)
67
65
  disconnect()
68
66
  scheduleReconnect()
69
67
  }, HEARTBEAT_TIMEOUT)
@@ -82,7 +80,6 @@ export function usePortalEventBridge(): void {
82
80
  hasEverConnected.current = true
83
81
  reconnectPending.current = false
84
82
  reconnectAttempts.current = 0
85
- publishPortalBridgeHealth(true)
86
83
  resetHeartbeatTimer()
87
84
  if (shouldEmitReconnect) {
88
85
  window.dispatchEvent(
@@ -119,7 +116,6 @@ export function usePortalEventBridge(): void {
119
116
  if (hasEverConnected.current) {
120
117
  reconnectPending.current = true
121
118
  }
122
- publishPortalBridgeHealth(false)
123
119
  disconnect()
124
120
  if (mounted) scheduleReconnect()
125
121
  }
@@ -127,7 +123,6 @@ export function usePortalEventBridge(): void {
127
123
  if (hasEverConnected.current) {
128
124
  reconnectPending.current = true
129
125
  }
130
- publishPortalBridgeHealth(false)
131
126
  if (mounted) scheduleReconnect()
132
127
  }
133
128
  }
@@ -160,7 +155,6 @@ export function usePortalEventBridge(): void {
160
155
 
161
156
  return () => {
162
157
  mounted = false
163
- publishPortalBridgeHealth(false)
164
158
  disconnect()
165
159
  if (reconnectTimer.current) {
166
160
  clearTimeout(reconnectTimer.current)
@@ -2,11 +2,6 @@
2
2
  import * as React from 'react'
3
3
  import type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'
4
4
  import { apiCall } from '../../backend/utils/apiCall'
5
- import {
6
- PORTAL_BRIDGE_STATUS_DOM_NAME,
7
- readPortalBridgeHealth,
8
- type PortalBridgeStatusDetail,
9
- } from './portalBridgeStatus'
10
5
 
11
6
  export type UsePortalNotificationsResult = {
12
7
  notifications: NotificationDto[]
@@ -33,11 +28,13 @@ async function fetchJson<T>(url: string, init?: RequestInit): Promise<T | null>
33
28
  }
34
29
 
35
30
  /**
36
- * Portal notification hook with SSE-first delivery and polling fallback.
31
+ * Portal notification hook polls customer notification endpoints.
37
32
  *
38
- * Performs an initial list/count reconciliation, refreshes on notification,
39
- * reconnect, and focus events, and polls every 8 seconds until the portal
40
- * event bridge explicitly reports a healthy connection.
33
+ * Fetches notifications from `/api/customer_accounts/portal/notifications`
34
+ * and unread count from `.../unread-count`. Polls every 8 seconds.
35
+ *
36
+ * Also listens for portal SSE events (`notifications.notification.created`)
37
+ * to trigger immediate refresh.
41
38
  */
42
39
  export function usePortalNotifications(): UsePortalNotificationsResult {
43
40
  const [notifications, setNotifications] = React.useState<NotificationDto[]>([])
@@ -69,50 +66,18 @@ export function usePortalNotifications(): UsePortalNotificationsResult {
69
66
  setIsLoading(false)
70
67
  }, [])
71
68
 
72
- const [usePolling, setUsePolling] = React.useState(() => {
73
- if (typeof window === 'undefined' || !('EventSource' in window)) {
74
- return true
75
- }
76
- return readPortalBridgeHealth() !== true
77
- })
78
-
79
- React.useEffect(() => {
80
- if (typeof window === 'undefined' || !('EventSource' in window)) {
81
- return
82
- }
83
- const handleStatusChange = (event: Event) => {
84
- const detail = (event as CustomEvent<PortalBridgeStatusDetail>).detail
85
- if (detail && typeof detail.healthy === 'boolean') {
86
- setUsePolling(!detail.healthy)
87
- if (!detail.healthy) {
88
- fetchAll()
89
- }
90
- }
91
- }
92
- window.addEventListener(PORTAL_BRIDGE_STATUS_DOM_NAME, handleStatusChange)
93
- return () => {
94
- window.removeEventListener(PORTAL_BRIDGE_STATUS_DOM_NAME, handleStatusChange)
95
- }
96
- }, [fetchAll])
97
-
69
+ // Poll
98
70
  React.useEffect(() => {
99
71
  fetchAll()
100
- }, [fetchAll])
101
-
102
- React.useEffect(() => {
103
- if (!usePolling) return
104
72
  const interval = setInterval(fetchAll, POLL_INTERVAL)
105
73
  return () => clearInterval(interval)
106
- }, [fetchAll, usePolling])
74
+ }, [fetchAll])
107
75
 
76
+ // Listen for portal SSE notification events
108
77
  React.useEffect(() => {
109
- const handler = (event: Event) => {
110
- const detail = (event as CustomEvent<{ id?: string }>).detail
111
- if (
112
- detail?.id === 'notifications.notification.created' ||
113
- detail?.id === 'notifications.notification.batch_created' ||
114
- detail?.id === 'om:portal-bridge:reconnected'
115
- ) {
78
+ const handler = (e: Event) => {
79
+ const detail = (e as CustomEvent).detail
80
+ if (detail?.id === 'notifications.notification.created' || detail?.id === 'notifications.notification.batch_created') {
116
81
  fetchAll()
117
82
  }
118
83
  }
@@ -120,14 +85,6 @@ export function usePortalNotifications(): UsePortalNotificationsResult {
120
85
  return () => window.removeEventListener('om:portal-event', handler)
121
86
  }, [fetchAll])
122
87
 
123
- React.useEffect(() => {
124
- const onFocus = () => {
125
- fetchAll()
126
- }
127
- window.addEventListener('focus', onFocus)
128
- return () => window.removeEventListener('focus', onFocus)
129
- }, [fetchAll])
130
-
131
88
  const markAsRead = React.useCallback(async (id: string) => {
132
89
  await fetchJson(`${BASE}/${id}/read`, { method: 'PUT' })
133
90
  setNotifications((prev) =>
@@ -97,7 +97,7 @@ describe('buildPortalNav', () => {
97
97
  expect(groups).toEqual([{ id: 'main', items: [expect.objectContaining({ label: 'Orders' })] }])
98
98
  })
99
99
 
100
- it('does not let the deprecated portal-admin flag bypass effective features', () => {
100
+ it('bypasses feature checks when isPortalAdmin is true', () => {
101
101
  const routes: FrontendRouteManifestEntry[] = [
102
102
  makeRoute({
103
103
  pattern: '/[orgSlug]/portal/orders',
@@ -107,7 +107,7 @@ describe('buildPortalNav', () => {
107
107
  ]
108
108
 
109
109
  const groups = buildPortalNav({ routes, orgSlug: 'my-org', grantedFeatures: [], isPortalAdmin: true })
110
- expect(groups).toEqual([])
110
+ expect(groups[0].items[0].label).toBe('Orders')
111
111
  })
112
112
 
113
113
  it('ignores navHidden pages even when nav is declared', () => {
@@ -22,9 +22,9 @@ export type BuildPortalNavOptions = {
22
22
  routes: readonly FrontendRouteManifestEntry[]
23
23
  /** Current customer org slug — substituted into `[orgSlug]` patterns. */
24
24
  orgSlug: string
25
- /** Concrete effective features granted to the current customer. */
25
+ /** Feature strings granted to the current customer (may include wildcards). */
26
26
  grantedFeatures: readonly string[]
27
- /** @deprecated Portal admins must receive concrete effective features. */
27
+ /** If true, bypass feature checks (portal admin). Defaults to false. */
28
28
  isPortalAdmin?: boolean
29
29
  }
30
30
 
@@ -75,6 +75,7 @@ export function buildPortalNav({
75
75
  routes,
76
76
  orgSlug,
77
77
  grantedFeatures,
78
+ isPortalAdmin = false,
78
79
  }: BuildPortalNavOptions): PortalNavGroup[] {
79
80
  const mainItems: PortalNavItem[] = []
80
81
  const accountItems: PortalNavItem[] = []
@@ -95,7 +96,7 @@ export function buildPortalNav({
95
96
  const nav = route.nav!
96
97
 
97
98
  const requireFeatures = route.requireCustomerFeatures ?? []
98
- if (requireFeatures.length) {
99
+ if (!isPortalAdmin && requireFeatures.length) {
99
100
  if (!hasAllFeatures(grantedFeatures as string[], requireFeatures as string[])) continue
100
101
  }
101
102
 
@@ -9,20 +9,14 @@ const buttonVariants = cva(
9
9
  variants: {
10
10
  variant: {
11
11
  default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary-hover',
12
- /* Destructive is quiet by design: red text on a calm surface. A wall
13
- of solid red buttons trains users to ignore red. The filled form is
14
- `destructive-solid`, reserved for the single point-of-no-return
15
- confirmation inside a dialog — never for the button that opens it. */
16
12
  destructive:
17
- 'border border-destructive bg-background text-destructive shadow-xs hover:bg-destructive/10 aria-invalid:ring-destructive dark:aria-invalid:ring-destructive',
18
- 'destructive-solid':
19
- 'bg-destructive text-white shadow-xs hover:bg-destructive/90 aria-invalid:ring-destructive dark:aria-invalid:ring-destructive',
13
+ 'bg-destructive text-white shadow-xs hover:bg-destructive/90 aria-invalid:ring-destructive dark:aria-invalid:ring-destructive dark:bg-destructive/10',
20
14
  'destructive-outline':
21
- 'border border-destructive bg-background text-destructive shadow-xs hover:bg-destructive/10',
15
+ 'border border-destructive/30 bg-background text-destructive shadow-xs hover:bg-destructive/10 dark:border-destructive/40 dark:hover:bg-destructive/15',
22
16
  'destructive-soft':
23
- 'bg-destructive/10 text-destructive hover:bg-destructive/15',
17
+ 'bg-destructive/10 text-destructive hover:bg-destructive/15 dark:bg-destructive/15 dark:hover:bg-destructive/20',
24
18
  'destructive-ghost':
25
- 'text-destructive hover:bg-destructive/10',
19
+ 'text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/15',
26
20
  outline:
27
21
  'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
28
22
  secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
@@ -1,28 +0,0 @@
1
- import { jsx } from "react/jsx-runtime";
2
- import { LUCIDE_ICON_REGISTRY } from "./lucideRegistry.generated.js";
3
- function normalizeKebabIconName(input) {
4
- const trimmed = input.trim();
5
- if (!trimmed) return "";
6
- const withoutPrefix = trimmed.startsWith("lucide:") ? trimmed.slice("lucide:".length) : trimmed;
7
- if (!withoutPrefix) return "";
8
- if (!withoutPrefix.includes("-") && !withoutPrefix.includes("_") && !withoutPrefix.includes(" ") && /[A-Z]/.test(withoutPrefix)) {
9
- return withoutPrefix.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
10
- }
11
- return withoutPrefix.replace(/[_\s]+/g, "-").replace(/-+/g, "-").toLowerCase();
12
- }
13
- function resolveRegisteredLucideIcon(name) {
14
- if (!name) return null;
15
- const normalized = normalizeKebabIconName(name);
16
- if (!normalized) return null;
17
- return LUCIDE_ICON_REGISTRY[normalized] ?? null;
18
- }
19
- function resolveRegisteredLucideIconNode(name, className) {
20
- const Icon = resolveRegisteredLucideIcon(name);
21
- if (!Icon) return null;
22
- return /* @__PURE__ */ jsx(Icon, { className });
23
- }
24
- export {
25
- resolveRegisteredLucideIcon,
26
- resolveRegisteredLucideIconNode
27
- };
28
- //# sourceMappingURL=lucideRegistryRuntime.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/backend/icons/lucideRegistryRuntime.tsx"],
4
- "sourcesContent": ["import type * as React from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'\n\nfunction normalizeKebabIconName(input: string): string {\n const trimmed = input.trim()\n if (!trimmed) return ''\n const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed\n if (!withoutPrefix) return ''\n if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {\n return withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')\n .toLowerCase()\n }\n return withoutPrefix\n .replace(/[_\\s]+/g, '-')\n .replace(/-+/g, '-')\n .toLowerCase()\n}\n\nexport function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {\n if (!name) return null\n const normalized = normalizeKebabIconName(name)\n if (!normalized) return null\n return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null\n}\n\nexport function resolveRegisteredLucideIconNode(\n name: string | undefined,\n className: string\n): React.ReactNode | null {\n const Icon = resolveRegisteredLucideIcon(name)\n if (!Icon) return null\n return <Icon className={className} />\n}\n"],
5
- "mappings": "AAkCS;AAhCT,SAAS,4BAA4B;AAErC,SAAS,uBAAuB,OAAuB;AACrD,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,gBAAgB,QAAQ,WAAW,SAAS,IAAI,QAAQ,MAAM,UAAU,MAAM,IAAI;AACxF,MAAI,CAAC,cAAe,QAAO;AAC3B,MAAI,CAAC,cAAc,SAAS,GAAG,KAAK,CAAC,cAAc,SAAS,GAAG,KAAK,CAAC,cAAc,SAAS,GAAG,KAAK,QAAQ,KAAK,aAAa,GAAG;AAC/H,WAAO,cACJ,QAAQ,sBAAsB,OAAO,EACrC,QAAQ,wBAAwB,OAAO,EACvC,YAAY;AAAA,EACjB;AACA,SAAO,cACJ,QAAQ,WAAW,GAAG,EACtB,QAAQ,OAAO,GAAG,EAClB,YAAY;AACjB;AAEO,SAAS,4BAA4B,MAA6C;AACvF,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,aAAa,uBAAuB,IAAI;AAC9C,MAAI,CAAC,WAAY,QAAO;AACxB,SAAQ,qBAAgE,UAAU,KAAK;AACzF;AAEO,SAAS,gCACd,MACA,WACwB;AACxB,QAAM,OAAO,4BAA4B,IAAI;AAC7C,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,oBAAC,QAAK,WAAsB;AACrC;",
6
- "names": []
7
- }
@@ -1,34 +0,0 @@
1
- const PERSPECTIVE_COOKIE_PREFIX = "om_table_perspective";
2
- const PERSPECTIVE_STORAGE_PREFIX = "om_table_perspective_snapshot";
3
- function clearAllPerspectiveState() {
4
- if (typeof window !== "undefined") {
5
- try {
6
- const storage = window.localStorage;
7
- const staleKeys = [];
8
- for (let index = 0; index < storage.length; index += 1) {
9
- const key = storage.key(index);
10
- if (key && key.startsWith(`${PERSPECTIVE_STORAGE_PREFIX}:`)) staleKeys.push(key);
11
- }
12
- for (const key of staleKeys) storage.removeItem(key);
13
- } catch {
14
- }
15
- }
16
- if (typeof document !== "undefined") {
17
- try {
18
- const cookies = document.cookie ? document.cookie.split(";") : [];
19
- for (const cookie of cookies) {
20
- const name = cookie.split("=")[0]?.trim();
21
- if (name && name.startsWith(`${PERSPECTIVE_COOKIE_PREFIX}:`)) {
22
- document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`;
23
- }
24
- }
25
- } catch {
26
- }
27
- }
28
- }
29
- export {
30
- PERSPECTIVE_COOKIE_PREFIX,
31
- PERSPECTIVE_STORAGE_PREFIX,
32
- clearAllPerspectiveState
33
- };
34
- //# sourceMappingURL=perspectiveState.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/backend/perspectiveState.ts"],
4
- "sourcesContent": ["export const PERSPECTIVE_COOKIE_PREFIX = 'om_table_perspective'\nexport const PERSPECTIVE_STORAGE_PREFIX = 'om_table_perspective_snapshot'\n\n/**\n * Purge every browser-local DataTable perspective snapshot and active-view cookie\n * across all tables in this origin. Snapshots persist the unsaved/live table state\n * (column widths, order, visibility) keyed only by `tableId`, so they are shared by\n * every account that signs in on the same browser profile. Call this at the auth\n * identity boundary so one user's unsaved layout never carries over to a different\n * user \u2014 including a different tenant \u2014 reusing the same browser tab (#4185).\n *\n * Lives here rather than in DataTable so the backend shell can call it without\n * pulling the whole table implementation into the bundle of every backend page.\n * `@open-mercato/ui/backend/DataTable` re-exports it, which is the published\n * import path and must stay stable.\n */\nexport function clearAllPerspectiveState(): void {\n if (typeof window !== 'undefined') {\n try {\n const storage = window.localStorage\n const staleKeys: string[] = []\n for (let index = 0; index < storage.length; index += 1) {\n const key = storage.key(index)\n if (key && key.startsWith(`${PERSPECTIVE_STORAGE_PREFIX}:`)) staleKeys.push(key)\n }\n for (const key of staleKeys) storage.removeItem(key)\n } catch {\n // private mode / quota errors are non-fatal\n }\n }\n if (typeof document !== 'undefined') {\n try {\n const cookies = document.cookie ? document.cookie.split(';') : []\n for (const cookie of cookies) {\n const name = cookie.split('=')[0]?.trim()\n if (name && name.startsWith(`${PERSPECTIVE_COOKIE_PREFIX}:`)) {\n document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`\n }\n }\n } catch {\n // ignore \u2014 cookie access can throw in sandboxed contexts\n }\n }\n}\n"],
5
- "mappings": "AAAO,MAAM,4BAA4B;AAClC,MAAM,6BAA6B;AAenC,SAAS,2BAAiC;AAC/C,MAAI,OAAO,WAAW,aAAa;AACjC,QAAI;AACF,YAAM,UAAU,OAAO;AACvB,YAAM,YAAsB,CAAC;AAC7B,eAAS,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;AACtD,cAAM,MAAM,QAAQ,IAAI,KAAK;AAC7B,YAAI,OAAO,IAAI,WAAW,GAAG,0BAA0B,GAAG,EAAG,WAAU,KAAK,GAAG;AAAA,MACjF;AACA,iBAAW,OAAO,UAAW,SAAQ,WAAW,GAAG;AAAA,IACrD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,MAAI,OAAO,aAAa,aAAa;AACnC,QAAI;AACF,YAAM,UAAU,SAAS,SAAS,SAAS,OAAO,MAAM,GAAG,IAAI,CAAC;AAChE,iBAAW,UAAU,SAAS;AAC5B,cAAM,OAAO,OAAO,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AACxC,YAAI,QAAQ,KAAK,WAAW,GAAG,yBAAyB,GAAG,GAAG;AAC5D,mBAAS,SAAS,GAAG,IAAI;AAAA,QAC3B;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACF;",
6
- "names": []
7
- }
@@ -1,57 +0,0 @@
1
- "use client";
2
- import { apiCall } from "./apiCall.js";
3
- const MAX_OFFSET_PAGES = 50;
4
- const ABSOLUTE_URL_PATTERN = /^([a-z][a-z\d+\-.]*:)?\/\//i;
5
- function parseUrl(value) {
6
- try {
7
- const absolute = ABSOLUTE_URL_PATTERN.test(value);
8
- const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
9
- return { url: absolute ? new URL(value) : new URL(value, origin), absolute };
10
- } catch {
11
- return null;
12
- }
13
- }
14
- function formatUrl(parsed) {
15
- return parsed.absolute ? parsed.url.toString() : `${parsed.url.pathname}${parsed.url.search}`;
16
- }
17
- function readOffset(value) {
18
- const parsed = parseUrl(value);
19
- if (!parsed) return 0;
20
- const offset = Number.parseInt(parsed.url.searchParams.get("offset") ?? "", 10);
21
- return Number.isFinite(offset) && offset > 0 ? offset : 0;
22
- }
23
- function withOffset(value, offset) {
24
- const parsed = parseUrl(value);
25
- if (!parsed) {
26
- const separator = value.includes("?") ? "&" : "?";
27
- return `${value}${separator}offset=${offset}`;
28
- }
29
- parsed.url.searchParams.set("offset", String(offset));
30
- return formatUrl(parsed);
31
- }
32
- async function fetchAllOffsetPages(url, options = {}) {
33
- const maxPages = options.maxPages ?? MAX_OFFSET_PAGES;
34
- const baseOffset = readOffset(url);
35
- const items = [];
36
- let firstPayload = {};
37
- let pageCount = 0;
38
- while (pageCount < maxPages) {
39
- const pageUrl = pageCount === 0 ? url : withOffset(url, baseOffset + items.length);
40
- const call = await apiCall(pageUrl, void 0, { fallback: {} });
41
- const payload = call.result ?? {};
42
- if (!call.ok) {
43
- return { ok: false, items, pageCount, firstPayload, errorPayload: payload };
44
- }
45
- pageCount += 1;
46
- if (pageCount === 1) firstPayload = payload;
47
- const pageItems = Array.isArray(payload.items) ? payload.items : [];
48
- items.push(...pageItems);
49
- if (payload.hasMore !== true || pageItems.length === 0) break;
50
- }
51
- return { ok: true, items, pageCount, firstPayload, errorPayload: null };
52
- }
53
- export {
54
- MAX_OFFSET_PAGES,
55
- fetchAllOffsetPages
56
- };
57
- //# sourceMappingURL=fetchAllPages.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/backend/utils/fetchAllPages.ts"],
4
- "sourcesContent": ["\"use client\"\n\nimport { apiCall } from './apiCall'\n\n// Offset-paged list routes cap how many rows one response may carry, so a\n// caller that needs the whole collection has to walk the pages. Bound the walk\n// so a route reporting hasMore forever cannot hang the caller; at the 500-row\n// dictionary ceiling this still covers 25k rows.\nexport const MAX_OFFSET_PAGES = 50\n\nexport type OffsetPagedPayload = Record<string, unknown>\n\nexport type OffsetPagedResult = {\n ok: boolean\n items: unknown[]\n pageCount: number\n firstPayload: OffsetPagedPayload\n errorPayload: OffsetPagedPayload | null\n}\n\nconst ABSOLUTE_URL_PATTERN = /^([a-z][a-z\\d+\\-.]*:)?\\/\\//i\n\ntype ParsedUrl = { url: URL; absolute: boolean }\n\nfunction parseUrl(value: string): ParsedUrl | null {\n try {\n const absolute = ABSOLUTE_URL_PATTERN.test(value)\n const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'\n return { url: absolute ? new URL(value) : new URL(value, origin), absolute }\n } catch {\n return null\n }\n}\n\nfunction formatUrl(parsed: ParsedUrl): string {\n return parsed.absolute ? parsed.url.toString() : `${parsed.url.pathname}${parsed.url.search}`\n}\n\nfunction readOffset(value: string): number {\n const parsed = parseUrl(value)\n if (!parsed) return 0\n const offset = Number.parseInt(parsed.url.searchParams.get('offset') ?? '', 10)\n return Number.isFinite(offset) && offset > 0 ? offset : 0\n}\n\nfunction withOffset(value: string, offset: number): string {\n const parsed = parseUrl(value)\n if (!parsed) {\n const separator = value.includes('?') ? '&' : '?'\n return `${value}${separator}offset=${offset}`\n }\n parsed.url.searchParams.set('offset', String(offset))\n return formatUrl(parsed)\n}\n\n/**\n * Walk every page of an offset-paged list route and return the assembled items.\n *\n * A route that does not report `hasMore` resolves after a single request, so\n * this is a drop-in replacement for a one-shot `apiCall` against any list\n * endpoint. Ordering is preserved as the route returned it, page after page \u2014\n * callers that need a global order across page boundaries must re-sort.\n */\nexport async function fetchAllOffsetPages(\n url: string,\n options: { maxPages?: number } = {},\n): Promise<OffsetPagedResult> {\n const maxPages = options.maxPages ?? MAX_OFFSET_PAGES\n const baseOffset = readOffset(url)\n const items: unknown[] = []\n let firstPayload: OffsetPagedPayload = {}\n let pageCount = 0\n\n while (pageCount < maxPages) {\n // The first request goes out untouched so a route that caches only its\n // default page still serves that page from cache.\n const pageUrl = pageCount === 0 ? url : withOffset(url, baseOffset + items.length)\n const call = await apiCall<OffsetPagedPayload>(pageUrl, undefined, { fallback: {} })\n const payload = call.result ?? {}\n if (!call.ok) {\n return { ok: false, items, pageCount, firstPayload, errorPayload: payload }\n }\n pageCount += 1\n if (pageCount === 1) firstPayload = payload\n const pageItems = Array.isArray(payload.items) ? payload.items : []\n items.push(...pageItems)\n // An empty page also stops the walk: without it a route that keeps\n // reporting hasMore while returning nothing would spin to the page cap.\n if (payload.hasMore !== true || pageItems.length === 0) break\n }\n\n return { ok: true, items, pageCount, firstPayload, errorPayload: null }\n}\n"],
5
- "mappings": ";AAEA,SAAS,eAAe;AAMjB,MAAM,mBAAmB;AAYhC,MAAM,uBAAuB;AAI7B,SAAS,SAAS,OAAiC;AACjD,MAAI;AACF,UAAM,WAAW,qBAAqB,KAAK,KAAK;AAChD,UAAM,SAAS,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS;AACxE,WAAO,EAAE,KAAK,WAAW,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,OAAO,MAAM,GAAG,SAAS;AAAA,EAC7E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,UAAU,QAA2B;AAC5C,SAAO,OAAO,WAAW,OAAO,IAAI,SAAS,IAAI,GAAG,OAAO,IAAI,QAAQ,GAAG,OAAO,IAAI,MAAM;AAC7F;AAEA,SAAS,WAAW,OAAuB;AACzC,QAAM,SAAS,SAAS,KAAK;AAC7B,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,OAAO,SAAS,OAAO,IAAI,aAAa,IAAI,QAAQ,KAAK,IAAI,EAAE;AAC9E,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAEA,SAAS,WAAW,OAAe,QAAwB;AACzD,QAAM,SAAS,SAAS,KAAK;AAC7B,MAAI,CAAC,QAAQ;AACX,UAAM,YAAY,MAAM,SAAS,GAAG,IAAI,MAAM;AAC9C,WAAO,GAAG,KAAK,GAAG,SAAS,UAAU,MAAM;AAAA,EAC7C;AACA,SAAO,IAAI,aAAa,IAAI,UAAU,OAAO,MAAM,CAAC;AACpD,SAAO,UAAU,MAAM;AACzB;AAUA,eAAsB,oBACpB,KACA,UAAiC,CAAC,GACN;AAC5B,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,aAAa,WAAW,GAAG;AACjC,QAAM,QAAmB,CAAC;AAC1B,MAAI,eAAmC,CAAC;AACxC,MAAI,YAAY;AAEhB,SAAO,YAAY,UAAU;AAG3B,UAAM,UAAU,cAAc,IAAI,MAAM,WAAW,KAAK,aAAa,MAAM,MAAM;AACjF,UAAM,OAAO,MAAM,QAA4B,SAAS,QAAW,EAAE,UAAU,CAAC,EAAE,CAAC;AACnF,UAAM,UAAU,KAAK,UAAU,CAAC;AAChC,QAAI,CAAC,KAAK,IAAI;AACZ,aAAO,EAAE,IAAI,OAAO,OAAO,WAAW,cAAc,cAAc,QAAQ;AAAA,IAC5E;AACA,iBAAa;AACb,QAAI,cAAc,EAAG,gBAAe;AACpC,UAAM,YAAY,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAClE,UAAM,KAAK,GAAG,SAAS;AAGvB,QAAI,QAAQ,YAAY,QAAQ,UAAU,WAAW,EAAG;AAAA,EAC1D;AAEA,SAAO,EAAE,IAAI,MAAM,OAAO,WAAW,cAAc,cAAc,KAAK;AACxE;",
6
- "names": []
7
- }
@@ -1,25 +0,0 @@
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
@@ -1,7 +0,0 @@
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
- }