@flamingo-stack/openframe-frontend-core 0.0.841 → 0.0.843

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 (104) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{case-study-card-TK7ZCNLM.cjs → case-study-card-VN3JBNF5.cjs} +3 -3
  3. package/dist/{chunk-RSJYTQEA.cjs → chunk-23DVVS4I.cjs} +104 -104
  4. package/dist/{chunk-IELPVUPR.cjs → chunk-2H5D2PUC.cjs} +23 -23
  5. package/dist/{chunk-275AQ6LC.cjs → chunk-2JZFXGHE.cjs} +4 -4
  6. package/dist/{chunk-DYDQLS2R.cjs → chunk-2WGA2IRN.cjs} +370 -298
  7. package/dist/{chunk-VBH2RCFM.cjs → chunk-3IXW2EKO.cjs} +78 -78
  8. package/dist/{chunk-DGY5KEPY.cjs → chunk-4ASUUB3A.cjs} +83 -83
  9. package/dist/{chunk-WCOSGDGA.cjs → chunk-4R4HAFXI.cjs} +2 -2
  10. package/dist/{chunk-INBFKUZA.cjs → chunk-5CPEMPNP.cjs} +15 -15
  11. package/dist/{chunk-CCRXBFNL.cjs → chunk-6HFTSVVC.cjs} +8 -8
  12. package/dist/{chunk-FHI4I3TQ.cjs → chunk-6M764K5Z.cjs} +12 -12
  13. package/dist/{chunk-SOCCHWDK.cjs → chunk-BHOVF4KP.cjs} +2 -2
  14. package/dist/{chunk-WVNUCKB7.cjs → chunk-CNMXP77I.cjs} +4 -4
  15. package/dist/{chunk-HXTZZE3R.cjs → chunk-DUB2UVYS.cjs} +61 -72
  16. package/dist/{chunk-TD4CC4KV.cjs → chunk-EUEPGHY4.cjs} +6 -6
  17. package/dist/{chunk-Q6WUHYOG.cjs → chunk-HLMA6CDT.cjs} +85 -85
  18. package/dist/{chunk-ZAO5SYBJ.cjs → chunk-MWQOM2EI.cjs} +4 -4
  19. package/dist/{chunk-WI7EXAZT.cjs → chunk-NMLNZCK2.cjs} +2 -2
  20. package/dist/{chunk-GUKFMMDZ.cjs → chunk-OGINOWMT.cjs} +3 -3
  21. package/dist/{chunk-APNONOHY.cjs → chunk-PZNWIJRZ.cjs} +13 -10
  22. package/dist/{chunk-GMK4JGC2.cjs → chunk-QBZ73IK7.cjs} +44 -44
  23. package/dist/{chunk-Z35PL3UR.cjs → chunk-QJRZVIQP.cjs} +34 -34
  24. package/dist/{chunk-SMZWXY4X.cjs → chunk-QKZQ5Q6E.cjs} +6 -1
  25. package/dist/{chunk-MYAPG3FQ.cjs → chunk-QPJUQ5YU.cjs} +21 -21
  26. package/dist/{chunk-JUEAZCD6.cjs → chunk-RSZZTB5T.cjs} +54 -54
  27. package/dist/{chunk-7WKRMJI6.cjs → chunk-SC36S7DJ.cjs} +2 -2
  28. package/dist/{chunk-AT6D7BAZ.cjs → chunk-SOQVKIWM.cjs} +3 -3
  29. package/dist/{chunk-UZPYYHF5.cjs → chunk-SOTTMZ6U.cjs} +19 -19
  30. package/dist/{chunk-E7M54JWW.cjs → chunk-SSSI2RBV.cjs} +14 -14
  31. package/dist/{chunk-EV6BPF47.cjs → chunk-WPMJUYW3.cjs} +143 -143
  32. package/dist/{chunk-FRL2ZN4O.cjs → chunk-XCVH5BCZ.cjs} +6 -6
  33. package/dist/{chunk-JM2XA7VY.cjs → chunk-ZXEWBJUE.cjs} +15 -15
  34. package/dist/{cloud-tenants-PV63RZ73.cjs → cloud-tenants-VPYROTKH.cjs} +11 -11
  35. package/dist/components/case-studies/index.cjs +26 -26
  36. package/dist/components/chat/index.cjs +8 -8
  37. package/dist/components/contact/index.cjs +12 -12
  38. package/dist/components/docs/index.cjs +15 -15
  39. package/dist/components/embeds/index.cjs +12 -12
  40. package/dist/components/faq/index.cjs +13 -13
  41. package/dist/components/features/devices/device-unread-indicator.d.ts +6 -0
  42. package/dist/components/features/devices/device-unread-indicator.d.ts.map +1 -0
  43. package/dist/components/features/devices/device-unread-indicator.js +12 -0
  44. package/dist/components/features/devices/devices-grid.d.ts.map +1 -1
  45. package/dist/components/features/devices/devices-grid.js +5 -1
  46. package/dist/components/features/devices/devices-table-columns.d.ts.map +1 -1
  47. package/dist/components/features/devices/devices-table-columns.js +3 -1
  48. package/dist/components/features/devices/devices-view-config.d.ts +6 -4
  49. package/dist/components/features/devices/devices-view-config.d.ts.map +1 -1
  50. package/dist/components/features/devices/index.d.ts +1 -0
  51. package/dist/components/features/devices/index.d.ts.map +1 -1
  52. package/dist/components/features/devices/index.js +2 -0
  53. package/dist/components/features/devices/types.d.ts +2 -0
  54. package/dist/components/features/devices/types.d.ts.map +1 -1
  55. package/dist/components/features/index.cjs +13 -11
  56. package/dist/components/help-center-pages/index.cjs +105 -105
  57. package/dist/components/index.cjs +73 -65
  58. package/dist/components/meeting-scheduler/index.cjs +50 -50
  59. package/dist/components/navigation/count-pill.d.ts +10 -0
  60. package/dist/components/navigation/count-pill.d.ts.map +1 -0
  61. package/dist/components/navigation/count-pill.js +18 -0
  62. package/dist/components/navigation/index.cjs +20 -14
  63. package/dist/components/navigation/index.d.ts +4 -0
  64. package/dist/components/navigation/index.d.ts.map +1 -1
  65. package/dist/components/navigation/index.js +5 -0
  66. package/dist/components/navigation/navigation-sidebar-grouped.d.ts.map +1 -1
  67. package/dist/components/navigation/navigation-sidebar-grouped.js +1 -12
  68. package/dist/components/navigation/unread-notifications-indicator.d.ts +19 -0
  69. package/dist/components/navigation/unread-notifications-indicator.d.ts.map +1 -0
  70. package/dist/components/navigation/unread-notifications-indicator.js +45 -0
  71. package/dist/components/onboarding-guides/index.cjs +14 -14
  72. package/dist/components/product-demo/fixtures/devices.d.ts.map +1 -1
  73. package/dist/components/product-demo/fixtures/devices.js +6 -1
  74. package/dist/components/product-demo/index.cjs +20 -20
  75. package/dist/components/related-content/index.cjs +7 -7
  76. package/dist/components/tickets/index.cjs +18 -18
  77. package/dist/components/ui/index.cjs +11 -11
  78. package/dist/{devices-VECG5JNC.cjs → devices-W76BAABE.cjs} +14 -14
  79. package/dist/{embeddable-chat-VRYAQQF6.cjs → embeddable-chat-C6DUH2JL.cjs} +78 -78
  80. package/dist/hooks/index.cjs +4 -4
  81. package/dist/index.cjs +23 -15
  82. package/dist/{knowledge-YJSDJIXT.cjs → knowledge-ILAIWOSY.cjs} +12 -12
  83. package/dist/{logs-42JG3NZ5.cjs → logs-VCPRFJGB.cjs} +18 -18
  84. package/dist/platform-domains.cjs +14 -10
  85. package/dist/platform-domains.d.ts +12 -7
  86. package/dist/platform-domains.d.ts.map +1 -1
  87. package/dist/platform-domains.js +14 -10
  88. package/dist/{policies-D4QNVJ26.cjs → policies-3BNAW4M5.cjs} +15 -15
  89. package/dist/{scripts-4VFYBXBE.cjs → scripts-VFFSJDYV.cjs} +16 -16
  90. package/dist/{software-update-OSDZQ5CP.cjs → software-update-VTBTLCZO.cjs} +16 -16
  91. package/dist/utils/index.cjs +13 -10
  92. package/package.json +1 -1
  93. package/src/components/features/devices/device-unread-indicator.tsx +14 -0
  94. package/src/components/features/devices/devices-grid.tsx +9 -1
  95. package/src/components/features/devices/devices-table-columns.tsx +3 -0
  96. package/src/components/features/devices/devices-view-config.tsx +6 -4
  97. package/src/components/features/devices/index.ts +1 -0
  98. package/src/components/features/devices/types.ts +2 -0
  99. package/src/components/navigation/count-pill.tsx +25 -0
  100. package/src/components/navigation/index.ts +4 -0
  101. package/src/components/navigation/navigation-sidebar-grouped.tsx +1 -14
  102. package/src/components/navigation/unread-notifications-indicator.tsx +69 -0
  103. package/src/components/product-demo/fixtures/devices.ts +7 -0
  104. package/src/platform-domains.ts +28 -20
@@ -80150,16 +80150,7 @@ function getPlatformByHostname(hostname) {
80150
80150
  function isPreviewHost(hostname) {
80151
80151
  return hostname.endsWith(".vercel.app");
80152
80152
  }
80153
- function getAllPlatformBaseDomains() {
80154
- if (typeof window === "undefined") return [];
80155
- const hostname = window.location.hostname;
80156
- if (hostname === "localhost" || hostname === "127.0.0.1" || hostname.startsWith("127.")) {
80157
- return [];
80158
- }
80159
- const previewEnv = process.env.VERCEL_ENV === "preview" || process.env.NEXT_PUBLIC_VERCEL_ENV === "preview" || isPreviewHost(hostname);
80160
- if (previewEnv) {
80161
- return [".vercel.app", "vercel.app"];
80162
- }
80153
+ function getPlatformBaseDomains() {
80163
80154
  const baseDomains = /* @__PURE__ */ new Set();
80164
80155
  for (const entry of PLATFORM_DOMAINS) {
80165
80156
  if (entry.pseudo) continue;
@@ -80173,6 +80164,18 @@ function getAllPlatformBaseDomains() {
80173
80164
  }
80174
80165
  return Array.from(baseDomains);
80175
80166
  }
80167
+ function getAllPlatformBaseDomains() {
80168
+ if (typeof window === "undefined") return [];
80169
+ const hostname = window.location.hostname;
80170
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname.startsWith("127.")) {
80171
+ return [];
80172
+ }
80173
+ const previewEnv = process.env.VERCEL_ENV === "preview" || process.env.NEXT_PUBLIC_VERCEL_ENV === "preview" || isPreviewHost(hostname);
80174
+ if (previewEnv) {
80175
+ return [".vercel.app", "vercel.app"];
80176
+ }
80177
+ return getPlatformBaseDomains();
80178
+ }
80176
80179
  var _orderIdx = (k) => PLATFORM_DOMAINS.findIndex((e) => e.key === k);
80177
80180
  if (_orderIdx("flamingo") > _orderIdx("universal") || getPlatformByHostname("flamingo.cx") !== "flamingo" || getPlatformByHostname("hub.openframe.ai") !== "openframe") {
80178
80181
  console.error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.841",
3
+ "version": "0.0.843",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+
3
+ import { UnreadNotificationsIndicator } from '../../navigation/unread-notifications-indicator';
4
+ import { useDevicesViewConfig } from './devices-view-config';
5
+ import type { DeviceRow } from './types';
6
+
7
+ /** The viewer's unread notifications about a device, opening them where the host lists them. Nothing at 0. */
8
+ export function DeviceUnreadIndicator({ device }: { device: DeviceRow }) {
9
+ const { getDeviceNotificationsHref } = useDevicesViewConfig();
10
+ const count = device.unreadNotificationCount ?? 0;
11
+ return (
12
+ <UnreadNotificationsIndicator count={count} href={count > 0 ? getDeviceNotificationsHref?.(device) : undefined} />
13
+ );
14
+ }
@@ -7,6 +7,7 @@ import { DeviceCard } from '../../ui';
7
7
  import { LoadingItems, useLoadingScope } from '../../ui/loading-scope';
8
8
  import { getDeviceName } from './device-name';
9
9
  import { getDeviceOperatingSystem, getDeviceStatusConfig } from './device-status';
10
+ import { DeviceUnreadIndicator } from './device-unread-indicator';
10
11
  import { useDevicesViewConfig } from './devices-view-config';
11
12
  import type { DeviceRow } from './types';
12
13
 
@@ -102,7 +103,14 @@ export function DevicesGrid<T extends DeviceRow = DeviceRow>({
102
103
  : undefined
103
104
  }
104
105
  onDeviceClick={() => handleDeviceClick(device)}
105
- actions={GRID_CARD_ACTIONS}
106
+ actions={
107
+ device.unreadNotificationCount
108
+ ? {
109
+ ...GRID_CARD_ACTIONS,
110
+ detailsButton: { component: <DeviceUnreadIndicator device={device} /> },
111
+ }
112
+ : GRID_CARD_ACTIONS
113
+ }
106
114
  className="h-full"
107
115
  // One card of a read list: how many there are is the record's (the height gate counts them).
108
116
  data-loading-item
@@ -27,6 +27,7 @@ import { getDeviceName } from './device-name';
27
27
  import { getDeviceStatusConfig } from './device-status';
28
28
  import { DEFAULT_VISIBLE_STATUSES } from './device-statuses';
29
29
  import { DeviceTypeTile } from './device-type-tile';
30
+ import { DeviceUnreadIndicator } from './device-unread-indicator';
30
31
  import { useDeviceImageUrl, useDevicesViewConfig } from './devices-view-config';
31
32
  import type { DeviceFilters, DeviceRow } from './types';
32
33
 
@@ -341,6 +342,8 @@ export function getDeviceTableColumns<T extends DeviceRow = DeviceRow>(
341
342
  <div className="min-w-0 flex-1">
342
343
  <TruncateText>{getDeviceName(device)}</TruncateText>
343
344
  </div>
345
+ {/* In the name's cell, not the open column: that one is dropped on a narrow page. */}
346
+ <DeviceUnreadIndicator device={device} />
344
347
  </div>
345
348
  );
346
349
  },
@@ -5,14 +5,16 @@ import type { DeviceRow } from './types';
5
5
 
6
6
  /**
7
7
  * What the device views cannot know by themselves, because it belongs to the
8
- * host: where a device's page lives, where images are served from, and whether
9
- * the page runs inside the mobile shell. The product provides it once, above
10
- * every page; without a provider the views render as a picture (no links,
11
- * image URLs used as given).
8
+ * host: where a device's page and its notifications live, where images are
9
+ * served from, and whether the page runs inside the mobile shell. The product
10
+ * provides it once, above every page; without a provider the views render as a
11
+ * picture (no links, image URLs used as given).
12
12
  */
13
13
  export interface DevicesViewConfig {
14
14
  /** The device's page. Absent: rows and cards are not links. */
15
15
  getDeviceHref?: (device: DeviceRow) => string | undefined;
16
+ /** The viewer's notifications about the device. Absent: the unread indicator is a counter, not a button. */
17
+ getDeviceNotificationsHref?: (device: DeviceRow) => string | undefined;
16
18
  /** A customer image's full URL from its stored path and content hash. */
17
19
  resolveImageUrl?: (imageUrl: string | null | undefined, hash?: string | null) => string | undefined;
18
20
  /** The page runs inside the mobile shell: options nobody can pick there are dropped. */
@@ -38,6 +38,7 @@ export {
38
38
  export { DeviceTagsFilterButton } from './device-tags-filter-button';
39
39
  export { renderDeviceTypeIcon } from './device-type-icon';
40
40
  export { DeviceTypeTile } from './device-type-tile';
41
+ export { DeviceUnreadIndicator } from './device-unread-indicator';
41
42
  export { DevicesFilterToolbar, type DevicesFilterToolbarProps } from './devices-filter-toolbar';
42
43
  export { DevicesGrid, type DevicesGridProps } from './devices-grid';
43
44
  export { DevicesGridFilters, type DevicesGridFiltersProps } from './devices-grid-filters';
@@ -34,6 +34,8 @@ export interface DeviceRow extends DeviceNameSource {
34
34
  organizationImageUrl?: string | null;
35
35
  organizationImageHash?: string | null;
36
36
  tags?: DeviceTag[];
37
+ /** The viewer's unread notifications about this device. Absent or 0: no indicator. */
38
+ unreadNotificationCount?: number;
37
39
  }
38
40
 
39
41
  export interface DeviceFilterValue {
@@ -0,0 +1,25 @@
1
+ 'use client';
2
+
3
+ import { cn } from '../../utils/cn';
4
+
5
+ export interface CountPillProps {
6
+ count: number;
7
+ className?: string;
8
+ }
9
+
10
+ /**
11
+ * The navigation's unread count: a 24px accent pill, widening for "99+". The grouped sidebar ends its rows with it,
12
+ * and a list row that counts the same notifications draws the same pill, so the two read alike.
13
+ */
14
+ export function CountPill({ count, className }: CountPillProps) {
15
+ return (
16
+ <span
17
+ className={cn(
18
+ 'flex h-6 min-w-6 shrink-0 items-center justify-center rounded-md bg-ods-accent px-[var(--spacing-system-xxs)]',
19
+ className,
20
+ )}
21
+ >
22
+ <span className="text-ods-text-on-accent text-h5">{count > 99 ? '99+' : count}</span>
23
+ </span>
24
+ );
25
+ }
@@ -90,6 +90,10 @@ export { HeaderButton } from './header-button';
90
90
  export type { HeaderButtonProps } from './header-button';
91
91
  export { UnreadDot, UnreadCountBadge } from './unread-dot';
92
92
  export type { UnreadDotProps, UnreadCountBadgeProps } from './unread-dot';
93
+ export { CountPill } from './count-pill';
94
+ export type { CountPillProps } from './count-pill';
95
+ export { UnreadNotificationsIndicator, unreadNotificationsLabel } from './unread-notifications-indicator';
96
+ export type { UnreadNotificationsIndicatorProps } from './unread-notifications-indicator';
93
97
  export { NavigationItemBadge } from './navigation-item-badge';
94
98
  export type { NavigationItemBadgeProps } from './navigation-item-badge';
95
99
  export { TicketAlertsButton } from './ticket-alerts-button';
@@ -7,6 +7,7 @@ import type { NavigationSidebarItem } from '../../types/navigation';
7
7
  import { cn } from '../../utils';
8
8
  import { UnifiedSkeleton } from '../loading/unified-skeleton';
9
9
  import { FloatingTooltip } from '../ui/floating-tooltip';
10
+ import { CountPill } from './count-pill';
10
11
  import { NavigationItemBadge } from './navigation-item-badge';
11
12
  import { UnreadDot } from './unread-dot';
12
13
 
@@ -133,20 +134,6 @@ interface RowStateProps {
133
134
  disabled: boolean;
134
135
  }
135
136
 
136
- /** The count pill: 24px, widening for "99+". */
137
- function CountPill({ count, className }: { count: number; className?: string }) {
138
- return (
139
- <span
140
- className={cn(
141
- 'flex h-6 min-w-6 shrink-0 items-center justify-center rounded-md bg-ods-accent px-[var(--spacing-system-xxs)]',
142
- className,
143
- )}
144
- >
145
- <span className="text-ods-text-on-accent text-h5">{count > 99 ? '99+' : count}</span>
146
- </span>
147
- );
148
- }
149
-
150
137
  /** The row's end, flush right where every row's count sits. */
151
138
  function RowEnd({ count }: { count: number }) {
152
139
  return count > 0 ? <CountPill count={count} className="ml-auto" /> : null;
@@ -0,0 +1,69 @@
1
+ 'use client';
2
+
3
+ import type { MouseEvent } from 'react';
4
+ import { useChatRuntime } from '../../contexts/chat-runtime-context';
5
+ import { cn } from '../../utils/cn';
6
+ import { isModifierClick } from '../chat/utils/chat-nav-resolution';
7
+ import { executeNavigationImperative, executeNewTabNavigation } from '../chat/utils/execute-navigation';
8
+ import { CountPill } from './count-pill';
9
+
10
+ /** What the indicator says to a screen reader: the count, in words. */
11
+ export function unreadNotificationsLabel(count: number): string {
12
+ return `${count} unread ${count === 1 ? 'notification' : 'notifications'}`;
13
+ }
14
+
15
+ export interface UnreadNotificationsIndicatorProps {
16
+ /** The viewer's unread notifications about one item. Nothing is drawn at 0. */
17
+ count: number;
18
+ /** Where those notifications are listed. Absent: the count alone, not a button. */
19
+ href?: string;
20
+ className?: string;
21
+ }
22
+
23
+ /**
24
+ * The viewer's unread notifications about one item of a list (a device, an incident), drawn with the sidebar's own
25
+ * count pill so a row and its section's badge read alike.
26
+ *
27
+ * A button, not a link: the row or card it sits in is usually a link itself, and a link cannot hold another
28
+ * (`data-no-row-click` keeps a table row from following its own). It goes through the lib's navigation executor,
29
+ * so the host decides how `href` opens, and a modifier click asks for a new tab as it would on a link.
30
+ */
31
+ export function UnreadNotificationsIndicator({ count, href, className }: UnreadNotificationsIndicatorProps) {
32
+ const runtime = useChatRuntime();
33
+ if (count <= 0) return null;
34
+
35
+ const label = unreadNotificationsLabel(count);
36
+
37
+ if (!href) {
38
+ return (
39
+ <span data-loading-optional role="img" aria-label={label} className={cn('inline-flex shrink-0', className)}>
40
+ <CountPill count={count} />
41
+ </span>
42
+ );
43
+ }
44
+
45
+ const open = (event: MouseEvent) => {
46
+ event.preventDefault();
47
+ event.stopPropagation();
48
+ if (isModifierClick(event)) executeNewTabNavigation({ runtime, href });
49
+ else executeNavigationImperative({ runtime, href });
50
+ };
51
+
52
+ return (
53
+ <button
54
+ type="button"
55
+ data-loading-optional
56
+ data-no-row-click
57
+ onClick={open}
58
+ aria-label={label}
59
+ title={label}
60
+ className={cn(
61
+ 'pointer-events-auto inline-flex shrink-0 rounded-md transition-opacity hover:opacity-80',
62
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus',
63
+ className,
64
+ )}
65
+ >
66
+ <CountPill count={count} />
67
+ </button>
68
+ );
69
+ }
@@ -4,6 +4,12 @@ import { type DEMO_DEVICES, DEMO_FLEET, DEMO_FLEET_SEARCH_OS, demoMinutesAgo } f
4
4
 
5
5
  type DemoDevice = (typeof DEMO_DEVICES)[keyof typeof DEMO_DEVICES];
6
6
 
7
+ /** The viewer's unread notifications per device: the offline reception desktop's recordings failed overnight. */
8
+ const DEMO_UNREAD_NOTIFICATIONS: Partial<Record<DemoDevice['id'], number>> = {
9
+ 'device-reception': 2,
10
+ 'device-leo': 1,
11
+ };
12
+
7
13
  function device(
8
14
  cast: DemoCast,
9
15
  demo: DemoDevice,
@@ -24,6 +30,7 @@ function device(
24
30
  organizationId: organization.id,
25
31
  organization: organization.name,
26
32
  organizationImageUrl: organization.logoUrl,
33
+ unreadNotificationCount: DEMO_UNREAD_NOTIFICATIONS[demo.id],
27
34
  };
28
35
  }
29
36
 
@@ -309,18 +309,37 @@ export function isPreviewHost(hostname: string): boolean {
309
309
  }
310
310
 
311
311
  /**
312
- * ALL unique cookie base domains (the cross-subdomain SSO mechanism).
312
+ * The registrable base of every site in the registry, each as `.base` and bare `base`: for each non-pseudo platform,
313
+ * the base of its resolved host. It reads the registry only, so it answers the same on a server and in a browser;
314
+ * a server's cookie rule reads it directly.
315
+ *
316
+ * Because `getPlatformProductionUrl` always yields a host (override OR default),
317
+ * `.flamingo.so` / `.flamingo.run` / `.openmsp.ai` / `.tmcg.miami` / `.openframe.ai`
318
+ * are ALWAYS present → cross-hub SSO is byte-identical to today.
319
+ */
320
+ export function getPlatformBaseDomains(): string[] {
321
+ const baseDomains = new Set<string>();
322
+ for (const entry of PLATFORM_DOMAINS) {
323
+ if (entry.pseudo) continue;
324
+ const host = hostOf(getPlatformProductionUrl(entry.key));
325
+ if (!host) continue;
326
+ const base = toRegistrableBaseDomain(host);
327
+ if (base) {
328
+ baseDomains.add(`.${base}`);
329
+ baseDomains.add(base);
330
+ }
331
+ }
332
+ return Array.from(baseDomains);
333
+ }
334
+
335
+ /**
336
+ * ALL unique cookie base domains for the PAGE this runs on (the cross-subdomain SSO mechanism).
313
337
  *
314
338
  * NON-PURE (the sole such export): reads `typeof window` + `process.env` to
315
339
  * preserve byte-identical cookie behavior. Keeps the original three branches:
316
340
  * 1. localhost / private IP → [] (host-only cookies)
317
341
  * 2. Vercel preview (env OR host) → ['.vercel.app','vercel.app']
318
- * 3. production → for each non-pseudo platform, registrable base of its resolved
319
- * host, emitted as both `.base` and bare `base`.
320
- *
321
- * Because `getPlatformProductionUrl` always yields a host (override OR default),
322
- * `.flamingo.so` / `.flamingo.run` / `.openmsp.ai` / `.tmcg.miami` / `.openframe.ai`
323
- * are ALWAYS present → cross-hub SSO is byte-identical to today.
342
+ * 3. production → `getPlatformBaseDomains`.
324
343
  */
325
344
  export function getAllPlatformBaseDomains(): string[] {
326
345
  if (typeof window === 'undefined') return [];
@@ -339,19 +358,8 @@ export function getAllPlatformBaseDomains(): string[] {
339
358
  return ['.vercel.app', 'vercel.app'];
340
359
  }
341
360
 
342
- // Case 3: production — registrable base of every non-pseudo platform's resolved host
343
- const baseDomains = new Set<string>();
344
- for (const entry of PLATFORM_DOMAINS) {
345
- if (entry.pseudo) continue;
346
- const host = hostOf(getPlatformProductionUrl(entry.key));
347
- if (!host) continue;
348
- const base = toRegistrableBaseDomain(host);
349
- if (base) {
350
- baseDomains.add(`.${base}`);
351
- baseDomains.add(base);
352
- }
353
- }
354
- return Array.from(baseDomains);
361
+ // Case 3: production
362
+ return getPlatformBaseDomains();
355
363
  }
356
364
 
357
365
  // ── Cookie-domain guard + match (single owner; shared by the client cookie-domain.ts +