@flamingo-stack/openframe-frontend-core 0.0.402 → 0.0.404-snapshot.20260711053526

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 (153) hide show
  1. package/dist/{chunk-NHFQ2LCC.js → chunk-3XCNFTFV.js} +3 -3
  2. package/dist/{chunk-F63VMX4S.js → chunk-5XZEY34X.js} +3 -3
  3. package/dist/{chunk-EDOLCPBS.cjs → chunk-6OQ7YOX5.cjs} +33 -33
  4. package/dist/{chunk-EDOLCPBS.cjs.map → chunk-6OQ7YOX5.cjs.map} +1 -1
  5. package/dist/{chunk-A4RDD2PS.cjs → chunk-ACJ4CM6H.cjs} +93 -93
  6. package/dist/{chunk-A4RDD2PS.cjs.map → chunk-ACJ4CM6H.cjs.map} +1 -1
  7. package/dist/{chunk-WM72XFOD.js → chunk-BK7QCB5J.js} +6 -6
  8. package/dist/{chunk-ONAWSVFK.cjs → chunk-BS6O4HLJ.cjs} +40 -40
  9. package/dist/{chunk-ONAWSVFK.cjs.map → chunk-BS6O4HLJ.cjs.map} +1 -1
  10. package/dist/{chunk-GA5AN24T.cjs → chunk-BX3R44MR.cjs} +2 -2
  11. package/dist/chunk-BX3R44MR.cjs.map +1 -0
  12. package/dist/{chunk-J4XR6YYZ.cjs → chunk-CCIBZ7TO.cjs} +10 -6
  13. package/dist/chunk-CCIBZ7TO.cjs.map +1 -0
  14. package/dist/{chunk-DYV47UA6.cjs → chunk-DHIYBC5Z.cjs} +32 -32
  15. package/dist/{chunk-DYV47UA6.cjs.map → chunk-DHIYBC5Z.cjs.map} +1 -1
  16. package/dist/{chunk-DH6HTYOV.js → chunk-E6YHDT3D.js} +6 -2
  17. package/dist/chunk-E6YHDT3D.js.map +1 -0
  18. package/dist/{chunk-CWLKGOU4.cjs → chunk-ELLTTMPE.cjs} +5 -5
  19. package/dist/{chunk-CWLKGOU4.cjs.map → chunk-ELLTTMPE.cjs.map} +1 -1
  20. package/dist/{chunk-6VAJDGZK.js → chunk-F3YLM5PO.js} +3 -3
  21. package/dist/{chunk-MEU2JEXL.cjs → chunk-FFYEDIAG.cjs} +511 -447
  22. package/dist/chunk-FFYEDIAG.cjs.map +1 -0
  23. package/dist/{chunk-D3HPV6YE.cjs → chunk-FOJRR647.cjs} +73 -73
  24. package/dist/{chunk-D3HPV6YE.cjs.map → chunk-FOJRR647.cjs.map} +1 -1
  25. package/dist/{chunk-6FIVVZO2.js → chunk-GT7AJXKH.js} +2 -2
  26. package/dist/{chunk-RZIFF62P.js → chunk-ICJRSNBM.js} +73 -9
  27. package/dist/chunk-ICJRSNBM.js.map +1 -0
  28. package/dist/{chunk-32K5CRL2.cjs → chunk-LREN3G7R.cjs} +12 -12
  29. package/dist/{chunk-32K5CRL2.cjs.map → chunk-LREN3G7R.cjs.map} +1 -1
  30. package/dist/{chunk-HU5HWEQ5.cjs → chunk-M6BGJVDY.cjs} +19 -19
  31. package/dist/{chunk-HU5HWEQ5.cjs.map → chunk-M6BGJVDY.cjs.map} +1 -1
  32. package/dist/{chunk-MTBFE3JF.js → chunk-PNJ5LMVK.js} +16 -2
  33. package/dist/chunk-PNJ5LMVK.js.map +1 -0
  34. package/dist/{chunk-VMJTNXOH.js → chunk-Q2KKEE6W.js} +6 -6
  35. package/dist/{chunk-Q3AO2QWK.cjs → chunk-QAFRLO5N.cjs} +18 -4
  36. package/dist/chunk-QAFRLO5N.cjs.map +1 -0
  37. package/dist/{chunk-O5QU5KAC.js → chunk-QYTGHN2T.js} +24 -21
  38. package/dist/chunk-QYTGHN2T.js.map +1 -0
  39. package/dist/{chunk-COGPHD7D.js → chunk-RC35ITSR.js} +6 -6
  40. package/dist/{chunk-3TCCW7B4.js → chunk-RIGB7LP7.js} +4 -4
  41. package/dist/{chunk-YPNI74ZP.js → chunk-UIAQXIA6.js} +5 -5
  42. package/dist/{chunk-LAFLYVZ4.js → chunk-UN2BJ7XW.js} +3 -3
  43. package/dist/{chunk-YIJOSOSA.cjs → chunk-V4MOB3AD.cjs} +12 -12
  44. package/dist/{chunk-YIJOSOSA.cjs.map → chunk-V4MOB3AD.cjs.map} +1 -1
  45. package/dist/{chunk-JXSWDXUA.cjs → chunk-VFWEFR3Q.cjs} +40 -40
  46. package/dist/{chunk-JXSWDXUA.cjs.map → chunk-VFWEFR3Q.cjs.map} +1 -1
  47. package/dist/{chunk-OPCGDJU7.cjs → chunk-WEZIVLCO.cjs} +37 -34
  48. package/dist/chunk-WEZIVLCO.cjs.map +1 -0
  49. package/dist/{chunk-HGKWTKW7.js → chunk-WJRPMULP.js} +2 -2
  50. package/dist/chunk-WJRPMULP.js.map +1 -0
  51. package/dist/components/announcement-bar.d.ts +27 -1
  52. package/dist/components/announcement-bar.d.ts.map +1 -1
  53. package/dist/components/case-studies/index.cjs +10 -10
  54. package/dist/components/case-studies/index.js +4 -4
  55. package/dist/components/chat/index.cjs +4 -4
  56. package/dist/components/chat/index.js +3 -3
  57. package/dist/components/contact/index.cjs +5 -5
  58. package/dist/components/contact/index.js +4 -4
  59. package/dist/components/docs/index.cjs +7 -7
  60. package/dist/components/docs/index.js +6 -6
  61. package/dist/components/embeds/index.cjs +5 -5
  62. package/dist/components/embeds/index.js +4 -4
  63. package/dist/components/faq/index.cjs +6 -6
  64. package/dist/components/faq/index.js +5 -5
  65. package/dist/components/faq-accordion.d.ts.map +1 -1
  66. package/dist/components/features/index.cjs +4 -4
  67. package/dist/components/features/index.js +3 -3
  68. package/dist/components/help-center-pages/index.cjs +23 -23
  69. package/dist/components/help-center-pages/index.js +14 -14
  70. package/dist/components/index.cjs +274 -299
  71. package/dist/components/index.cjs.map +1 -1
  72. package/dist/components/index.js +110 -135
  73. package/dist/components/index.js.map +1 -1
  74. package/dist/components/navigation/index.cjs +4 -4
  75. package/dist/components/navigation/index.js +3 -3
  76. package/dist/components/onboarding-guides/index.cjs +8 -8
  77. package/dist/components/onboarding-guides/index.js +7 -7
  78. package/dist/components/related-content/index.cjs +6 -6
  79. package/dist/components/related-content/index.js +5 -5
  80. package/dist/components/tickets/index.cjs +7 -7
  81. package/dist/components/tickets/index.js +6 -6
  82. package/dist/components/ui/file-manager/index.cjs +50 -50
  83. package/dist/components/ui/file-manager/index.js +1 -1
  84. package/dist/components/ui/index.cjs +4 -4
  85. package/dist/components/ui/index.js +3 -3
  86. package/dist/hooks/api-hooks.d.ts +0 -4
  87. package/dist/hooks/api-hooks.d.ts.map +1 -1
  88. package/dist/hooks/index.cjs +2 -2
  89. package/dist/hooks/index.js +1 -1
  90. package/dist/hooks/use-self-fetch.d.ts +7 -0
  91. package/dist/hooks/use-self-fetch.d.ts.map +1 -1
  92. package/dist/index.cjs +20 -4
  93. package/dist/index.cjs.map +1 -1
  94. package/dist/index.js +19 -3
  95. package/dist/types/announcement.cjs +5 -0
  96. package/dist/types/announcement.cjs.map +1 -1
  97. package/dist/types/announcement.d.ts +37 -7
  98. package/dist/types/announcement.d.ts.map +1 -1
  99. package/dist/types/announcement.js +5 -1
  100. package/dist/types/announcement.js.map +1 -1
  101. package/dist/types/index.cjs +5 -0
  102. package/dist/types/index.cjs.map +1 -1
  103. package/dist/types/index.js +5 -1
  104. package/dist/types/index.js.map +1 -1
  105. package/dist/utils/announcement-storage.d.ts +39 -7
  106. package/dist/utils/announcement-storage.d.ts.map +1 -1
  107. package/dist/utils/app-config.d.ts.map +1 -1
  108. package/dist/utils/color-analysis.d.ts +13 -0
  109. package/dist/utils/color-analysis.d.ts.map +1 -1
  110. package/dist/utils/index.cjs +58 -1
  111. package/dist/utils/index.cjs.map +1 -1
  112. package/dist/utils/index.d.ts +1 -0
  113. package/dist/utils/index.d.ts.map +1 -1
  114. package/dist/utils/index.js +52 -2
  115. package/dist/utils/index.js.map +1 -1
  116. package/package.json +1 -1
  117. package/src/components/.announcement-bar.md +32 -30
  118. package/src/components/announcement-bar.tsx +227 -182
  119. package/src/components/faq/faq-section.tsx +3 -3
  120. package/src/components/faq-accordion.tsx +17 -20
  121. package/src/components/features/video.tsx +26 -6
  122. package/src/components/ui/button/button.tsx +1 -1
  123. package/src/hooks/.api-hooks.md +1 -4
  124. package/src/hooks/api-hooks.ts +0 -7
  125. package/src/hooks/use-self-fetch.ts +29 -1
  126. package/src/stories/AnnouncementBar.stories.tsx +188 -152
  127. package/src/types/.announcement.md +2 -2
  128. package/src/types/announcement.ts +37 -7
  129. package/src/utils/.announcement-storage.md +19 -24
  130. package/src/utils/announcement-storage.ts +74 -28
  131. package/src/utils/app-config.ts +10 -1
  132. package/src/utils/color-analysis.ts +16 -1
  133. package/src/utils/index.ts +3 -0
  134. package/dist/chunk-DH6HTYOV.js.map +0 -1
  135. package/dist/chunk-GA5AN24T.cjs.map +0 -1
  136. package/dist/chunk-HGKWTKW7.js.map +0 -1
  137. package/dist/chunk-J4XR6YYZ.cjs.map +0 -1
  138. package/dist/chunk-MEU2JEXL.cjs.map +0 -1
  139. package/dist/chunk-MTBFE3JF.js.map +0 -1
  140. package/dist/chunk-O5QU5KAC.js.map +0 -1
  141. package/dist/chunk-OPCGDJU7.cjs.map +0 -1
  142. package/dist/chunk-Q3AO2QWK.cjs.map +0 -1
  143. package/dist/chunk-RZIFF62P.js.map +0 -1
  144. /package/dist/{chunk-NHFQ2LCC.js.map → chunk-3XCNFTFV.js.map} +0 -0
  145. /package/dist/{chunk-F63VMX4S.js.map → chunk-5XZEY34X.js.map} +0 -0
  146. /package/dist/{chunk-WM72XFOD.js.map → chunk-BK7QCB5J.js.map} +0 -0
  147. /package/dist/{chunk-6VAJDGZK.js.map → chunk-F3YLM5PO.js.map} +0 -0
  148. /package/dist/{chunk-6FIVVZO2.js.map → chunk-GT7AJXKH.js.map} +0 -0
  149. /package/dist/{chunk-VMJTNXOH.js.map → chunk-Q2KKEE6W.js.map} +0 -0
  150. /package/dist/{chunk-COGPHD7D.js.map → chunk-RC35ITSR.js.map} +0 -0
  151. /package/dist/{chunk-3TCCW7B4.js.map → chunk-RIGB7LP7.js.map} +0 -0
  152. /package/dist/{chunk-YPNI74ZP.js.map → chunk-UIAQXIA6.js.map} +0 -0
  153. /package/dist/{chunk-LAFLYVZ4.js.map → chunk-UN2BJ7XW.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  // Announcement system TypeScript types
2
- import type { LegacyPlatform, PlatformFilter, PlatformRecord } from './platform';
2
+ import type { LegacyPlatform, PlatformFilter } from './platform';
3
+ import type { EntityPlatformAssoc } from './entity-platform';
3
4
 
4
- // For backward compatibility, maintain the old Platform type temporarily
5
5
  // Database Models
6
6
  export interface Announcement {
7
7
  id: string;
@@ -12,8 +12,9 @@ export interface Announcement {
12
12
  icon_url?: string;
13
13
  /** Extra props for the main bar/icon */
14
14
  icon_props?: Record<string, any>;
15
- platform_id: string; // Foreign key to platforms table
16
- platform?: PlatformRecord; // Joined platform data
15
+ /** Hydrated entity_platforms associations (featured-first) what the hub
16
+ * DAL's attachPlatformsToRow(s) emits. No singular platform/platform_id. */
17
+ announcement_platforms?: EntityPlatformAssoc[];
17
18
  is_active: boolean;
18
19
  // CTA (Call-To-Action) fields
19
20
  cta_enabled?: boolean;
@@ -109,10 +110,25 @@ export interface AnnouncementListResponse {
109
110
  };
110
111
  }
111
112
 
113
+ /**
114
+ * Wire shape of GET /api/announcements/active — the single typed home for the
115
+ * `{ announcement }` envelope (hub route return + AnnouncementBar's
116
+ * useSelfFetch generic). `null` when no active announcement exists.
117
+ */
112
118
  export interface AnnouncementResponse {
113
- announcement: Announcement;
119
+ announcement: Announcement | null;
114
120
  }
115
121
 
122
+ /**
123
+ * Default CTA button colors — one home for the pair the DAL create/update
124
+ * defaults and the admin form both reference. Announcement colors are
125
+ * admin-chosen data (not ODS-token surfaces) by design.
126
+ */
127
+ export const ANNOUNCEMENT_CTA_DEFAULTS = {
128
+ background: '#1A1A1A',
129
+ text: '#FAFAFA',
130
+ } as const;
131
+
116
132
  // Form Data Types
117
133
  export interface AnnouncementFormData {
118
134
  title: string;
@@ -214,8 +230,22 @@ export interface AnnouncementStats {
214
230
 
215
231
  // Component Props Types
216
232
  export interface AnnouncementBarProps {
217
- announcement?: Announcement;
218
- onDismiss?: (announcementId: string) => void;
233
+ /**
234
+ * SSR mode seed. `null` = the server resolved "nothing to show" (no active
235
+ * announcement, or dismissed via cookie) — seeds the fetch hook and skips
236
+ * the mount fetch. Absent (`undefined`) = client-only mode: the bar
237
+ * self-fetches on mount from the EndpointsRuntime provider's
238
+ * `announcementsUrl` (see react-embedding-example — no per-component URL
239
+ * or platform knobs; the server resolves its own platform via
240
+ * currentPlatform()).
241
+ */
242
+ initialAnnouncement?: Announcement | null;
243
+ /**
244
+ * Render-only mode for the admin live preview: no fetch/revalidation, inert
245
+ * dismiss button, and no storage side effects (never writes dismissal
246
+ * cookies or touches localStorage).
247
+ */
248
+ previewMode?: boolean;
219
249
  className?: string;
220
250
  }
221
251
 
@@ -1,36 +1,31 @@
1
- <!-- source-hash: eb9e1e114c43007debee28971d977a02 -->
2
- A utility module for managing announcement data persistence in browser localStorage with safe server-side rendering support.
1
+ <!-- source-hash: pending-regeneration -->
2
+ Announcement dismissal store. The dismissal COOKIE is the single source of truth the hub's root layout reads it server-side to decide whether to SSR-seed the AnnouncementBar (no flash for dismissed users); the legacy per-id localStorage key is read-only legacy consulted only when no cookie exists (migration trigger).
3
3
 
4
4
  ## Key Components
5
5
 
6
- - **`AnnouncementStorageOptions`** - Interface defining storage configuration with key and optional default value
7
- - **`getStoredAnnouncement(key)`** - Retrieves and parses announcement data from localStorage
8
- - **`setStoredAnnouncement(key, value)`** - Stores announcement data as JSON in localStorage
9
- - **`clearStoredAnnouncement(key?)`** - Removes announcement data from localStorage (defaults to 'announcement' key)
6
+ - **`announcementDismissCookieName(platform)`** - The ONE home for the cookie-name encoding (`<platform>-announcement-dismissed`), shared by the client writer and the hub's SSR reader
7
+ - **`isDismissedCookieValue(cookieValue, id)`** - THE dismissal match rule (id-match, never mere cookie presence), pure and isomorphic shared across the server/client boundary
8
+ - **`dismissAnnouncement(platform, id)`** - Persists a dismissal (cookie only, 1 year, SameSite=Lax); localStorage is intentionally not written
9
+ - **`isAnnouncementDismissed(platform, id)`** - Client-side check, cookie-first with legacy-localStorage fallback; reads browser storage so it must be called from effects, never during render
10
+ - **`clearLegacyAnnouncementCache(platform)`** - One-time cleanup of the pre-refactor localStorage "instant paint" cache blob
10
11
 
11
12
  ## Usage Example
12
13
 
13
14
  ```typescript
14
- import {
15
- getStoredAnnouncement,
16
- setStoredAnnouncement,
17
- clearStoredAnnouncement
15
+ import {
16
+ dismissAnnouncement,
17
+ isAnnouncementDismissed,
18
+ announcementDismissCookieName,
19
+ isDismissedCookieValue,
18
20
  } from './announcement-storage';
19
21
 
20
- // Store announcement dismissal state
21
- setStoredAnnouncement('banner-dismissed', {
22
- dismissed: true,
23
- timestamp: Date.now()
24
- });
22
+ // Client (in an effect):
23
+ if (isAnnouncementDismissed('openframe', announcement.id)) { /* collapse */ }
24
+ dismissAnnouncement('openframe', announcement.id);
25
25
 
26
- // Check if announcement was dismissed
27
- const dismissalState = getStoredAnnouncement('banner-dismissed');
28
- if (dismissalState?.dismissed) {
29
- console.log('Banner was dismissed');
30
- }
31
-
32
- // Clear announcement data
33
- clearStoredAnnouncement('banner-dismissed');
26
+ // Server (hub root layout):
27
+ const cookieValue = (await cookies()).get(announcementDismissCookieName(platform))?.value;
28
+ const dismissed = isDismissedCookieValue(cookieValue, announcement?.id);
34
29
  ```
35
30
 
36
- All functions include browser environment checks and error handling to prevent issues during server-side rendering or when localStorage is unavailable.
31
+ No `"use client"` directive on purpose the name/match helpers are imported by the hub's server layout; DOM-touching helpers guard on `typeof document`.
@@ -1,36 +1,82 @@
1
- // Stub for announcement storage
2
- export interface AnnouncementStorageOptions {
3
- key: string;
4
- defaultValue?: any;
1
+ /**
2
+ * Announcement dismissal store.
3
+ *
4
+ * The dismissal COOKIE is the single source of truth: the hub's root layout
5
+ * reads it server-side to decide whether to SSR-seed the bar (no flash for
6
+ * dismissed users), and the client reads it via `isAnnouncementDismissed`.
7
+ * One cookie per platform holding the LAST dismissed announcement id —
8
+ * dismissal is an id-match, never mere cookie presence, so a new announcement
9
+ * always shows after an old one was dismissed, and re-activating an old
10
+ * announcement intentionally re-shows it.
11
+ *
12
+ * The legacy per-id localStorage key (`<platform>-announcement-<id>-dismissed`)
13
+ * is READ-ONLY legacy: consulted only when no cookie exists for the platform,
14
+ * purely as the migration trigger (the bar's mount effect backfills the
15
+ * cookie). New dismissals write the cookie only, so the second store dies out.
16
+ *
17
+ * No `"use client"` directive on purpose — `announcementDismissCookieName` and
18
+ * `isDismissedCookieValue` are pure/isomorphic and imported by the hub's
19
+ * server layout; the DOM-touching helpers guard on `typeof document`.
20
+ */
21
+
22
+ /** Cookie name for a platform's dismissed-announcement id — the ONE home for
23
+ * the encoding, shared by the client writer and the hub's SSR reader. */
24
+ export function announcementDismissCookieName(platform: string): string {
25
+ return `${platform}-announcement-dismissed`
5
26
  }
6
27
 
7
- export function getStoredAnnouncement(key: string): any {
8
- if (typeof window !== 'undefined') {
9
- try {
10
- return JSON.parse(localStorage.getItem(key) || 'null');
11
- } catch {
12
- return null;
13
- }
14
- }
15
- return null;
28
+ /** THE dismissal match rule (id-match, not presence), shared across the
29
+ * server/client boundary. `undefined` id (no active announcement) → false. */
30
+ export function isDismissedCookieValue(
31
+ cookieValue: string | undefined,
32
+ id: string | undefined,
33
+ ): boolean {
34
+ return !!id && cookieValue === id
35
+ }
36
+
37
+ function readCookie(name: string): string | undefined {
38
+ if (typeof document === 'undefined') return undefined
39
+ const match = document.cookie
40
+ .split('; ')
41
+ .find((row) => row.startsWith(`${name}=`))
42
+ return match ? decodeURIComponent(match.slice(name.length + 1)) : undefined
16
43
  }
17
44
 
18
- export function setStoredAnnouncement(key: string, value: any): void {
19
- if (typeof window !== 'undefined') {
20
- try {
21
- localStorage.setItem(key, JSON.stringify(value));
22
- } catch {
23
- // Ignore storage errors
24
- }
45
+ const legacyDismissKey = (platform: string, id: string) =>
46
+ `${platform}-announcement-${id}-dismissed`
47
+
48
+ /** Persist a dismissal: cookie only (1 year). The SSR layout sees it on the
49
+ * next request; localStorage is intentionally NOT written (read-only legacy). */
50
+ export function dismissAnnouncement(platform: string, id: string): void {
51
+ if (typeof document === 'undefined') return
52
+ const name = announcementDismissCookieName(platform)
53
+ document.cookie = `${name}=${encodeURIComponent(id)}; path=/; max-age=31536000; SameSite=Lax`
54
+ }
55
+
56
+ /**
57
+ * Client-side dismissal check — cookie-first: the legacy localStorage key is
58
+ * consulted ONLY when no cookie exists for the platform. Reads browser
59
+ * storage, so callers must invoke it from effects, never during render
60
+ * (a render-time read would desync hydration from the SSR HTML).
61
+ */
62
+ export function isAnnouncementDismissed(platform: string, id: string): boolean {
63
+ if (typeof document === 'undefined') return false
64
+ const cookieValue = readCookie(announcementDismissCookieName(platform))
65
+ if (cookieValue !== undefined) return isDismissedCookieValue(cookieValue, id)
66
+ try {
67
+ return localStorage.getItem(legacyDismissKey(platform, id)) !== null
68
+ } catch {
69
+ return false
25
70
  }
26
71
  }
27
72
 
28
- export function clearStoredAnnouncement(key: string = 'announcement'): void {
29
- if (typeof window !== 'undefined') {
30
- try {
31
- localStorage.removeItem(key);
32
- } catch {
33
- // Ignore storage errors
34
- }
73
+ /** One-time cleanup of pre-refactor storage: the orphaned announcement cache
74
+ * (the old "instant paint" blob) — called from the bar's mount effect. */
75
+ export function clearLegacyAnnouncementCache(platform: string): void {
76
+ if (typeof window === 'undefined') return
77
+ try {
78
+ localStorage.removeItem(`${platform}-announcement-cache`)
79
+ } catch {
80
+ // ignore storage errors
35
81
  }
36
- }
82
+ }
@@ -16,5 +16,14 @@ export function getAppConfig() {
16
16
  }
17
17
 
18
18
  export function getAppType() {
19
- return process.env.NEXT_PUBLIC_APP_TYPE || 'openmsp';
19
+ // The exact `process.env.NEXT_PUBLIC_APP_TYPE` member expression is kept so
20
+ // Next/webpack can statically inline it. The try/catch makes the call safe
21
+ // in non-Next React hosts (Vite/CRA embeds) where `process` does not exist
22
+ // at runtime — those hosts fall back to 'openmsp' or pass an explicit
23
+ // platform where it matters (e.g. AnnouncementBar's `platform` prop).
24
+ try {
25
+ return process.env.NEXT_PUBLIC_APP_TYPE || 'openmsp';
26
+ } catch {
27
+ return 'openmsp';
28
+ }
20
29
  }
@@ -26,7 +26,7 @@ export function hexToRgb(hex: string): [number, number, number] {
26
26
  /**
27
27
  * Calculate relative luminance for contrast calculations
28
28
  */
29
- function getLuminance(rgb: [number, number, number]): number {
29
+ export function getLuminance(rgb: [number, number, number]): number {
30
30
  const [r, g, b] = rgb.map(c => {
31
31
  c = c / 255;
32
32
  return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
@@ -45,6 +45,21 @@ export function getContrastRatio(color1: [number, number, number], color2: [numb
45
45
  return (brightest + 0.05) / (darkest + 0.05);
46
46
  }
47
47
 
48
+ /**
49
+ * Pick the readable foreground shade for an arbitrary background hex — 'dark'
50
+ * when dark text has the better WCAG contrast on it, 'light' otherwise.
51
+ * Callers map the result to an ODS THEME, not to color literals (e.g. the
52
+ * announcement bar scopes its content with .theme-light when the background
53
+ * needs dark-on-light, .theme-dark otherwise — every color then resolves
54
+ * from the active theme's own tokens).
55
+ */
56
+ export function pickReadableTextColor(bgHex: string): 'dark' | 'light' {
57
+ const bg = hexToRgb(bgHex);
58
+ const darkContrast = getContrastRatio(bg, [26, 26, 26]);
59
+ const lightContrast = getContrastRatio(bg, [250, 250, 250]);
60
+ return darkContrast >= lightContrast ? 'dark' : 'light';
61
+ }
62
+
48
63
  /**
49
64
  * Extract dominant color from image canvas
50
65
  */
@@ -85,6 +85,9 @@ export * from './generic-domain-utils'
85
85
  // Color analysis (canvas-based image color extraction)
86
86
  export * from './color-analysis'
87
87
 
88
+ // Announcement dismissal store (cookie SSOT; server-safe name/match helpers)
89
+ export * from './announcement-storage'
90
+
88
91
  // Image-proxy URL builder (pure, runtime-configurable)
89
92
  export {
90
93
  type GetProxiedImageUrlOptions,