@flamingo-stack/openframe-frontend-core 0.0.466 → 0.0.467

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 (97) hide show
  1. package/dist/{chunk-XJH5ZT5Y.js → chunk-25YYEVSE.js} +2 -2
  2. package/dist/{chunk-CC73CSLI.js → chunk-3L44PPMF.js} +2 -2
  3. package/dist/{chunk-GRB7LPJV.js → chunk-4DFPQKKJ.js} +2 -2
  4. package/dist/{chunk-3WF7INYU.cjs → chunk-6DZ5QOUM.cjs} +11 -11
  5. package/dist/{chunk-3WF7INYU.cjs.map → chunk-6DZ5QOUM.cjs.map} +1 -1
  6. package/dist/{chunk-BVNAYWLM.cjs → chunk-72NEN4G4.cjs} +15 -15
  7. package/dist/{chunk-BVNAYWLM.cjs.map → chunk-72NEN4G4.cjs.map} +1 -1
  8. package/dist/{chunk-FHLJQI4F.js → chunk-ABU37QSG.js} +4 -4
  9. package/dist/{chunk-KX2TBXKN.js → chunk-BIDOIPIT.js} +2 -2
  10. package/dist/{chunk-IZCOYFCN.js → chunk-CZFPPNYV.js} +3921 -3876
  11. package/dist/chunk-CZFPPNYV.js.map +1 -0
  12. package/dist/{chunk-FGL4ZNR7.cjs → chunk-G2H4AKL3.cjs} +62 -62
  13. package/dist/{chunk-FGL4ZNR7.cjs.map → chunk-G2H4AKL3.cjs.map} +1 -1
  14. package/dist/{chunk-YN77BNI7.cjs → chunk-G5D7NRH5.cjs} +26 -26
  15. package/dist/{chunk-YN77BNI7.cjs.map → chunk-G5D7NRH5.cjs.map} +1 -1
  16. package/dist/{chunk-72HPXAMZ.js → chunk-GXXLJHQX.js} +2 -2
  17. package/dist/{chunk-RQZOCLTI.js → chunk-ILZ66CS6.js} +4 -4
  18. package/dist/{chunk-GHZMNDPN.js → chunk-LOWJWKM5.js} +4 -4
  19. package/dist/{chunk-7DULN25L.js → chunk-NYOQNYSI.js} +2 -2
  20. package/dist/{chunk-KLBBOLDK.cjs → chunk-O7LQYYUA.cjs} +5 -5
  21. package/dist/{chunk-KLBBOLDK.cjs.map → chunk-O7LQYYUA.cjs.map} +1 -1
  22. package/dist/{chunk-HDP5VRPL.cjs → chunk-QITN43HN.cjs} +7 -7
  23. package/dist/{chunk-HDP5VRPL.cjs.map → chunk-QITN43HN.cjs.map} +1 -1
  24. package/dist/{chunk-OO7DO4DI.js → chunk-QJ7NIL6A.js} +4 -4
  25. package/dist/{chunk-NHNKMR22.cjs → chunk-SGDXS7VT.cjs} +59 -59
  26. package/dist/{chunk-NHNKMR22.cjs.map → chunk-SGDXS7VT.cjs.map} +1 -1
  27. package/dist/{chunk-WAO34PFO.js → chunk-TZSPLSDU.js} +2 -2
  28. package/dist/{chunk-MQQ7VIG6.cjs → chunk-UWW4PQQS.cjs} +26 -26
  29. package/dist/{chunk-MQQ7VIG6.cjs.map → chunk-UWW4PQQS.cjs.map} +1 -1
  30. package/dist/{chunk-5H3HACZZ.cjs → chunk-VERK32MO.cjs} +37 -37
  31. package/dist/{chunk-5H3HACZZ.cjs.map → chunk-VERK32MO.cjs.map} +1 -1
  32. package/dist/{chunk-VLLFRPNP.cjs → chunk-WWC75D3J.cjs} +57 -12
  33. package/dist/chunk-WWC75D3J.cjs.map +1 -0
  34. package/dist/{chunk-2VCSDMPV.cjs → chunk-YGW7T57C.cjs} +31 -31
  35. package/dist/{chunk-2VCSDMPV.cjs.map → chunk-YGW7T57C.cjs.map} +1 -1
  36. package/dist/{chunk-YHCLDRMU.cjs → chunk-ZYPHZ3R3.cjs} +9 -9
  37. package/dist/{chunk-YHCLDRMU.cjs.map → chunk-ZYPHZ3R3.cjs.map} +1 -1
  38. package/dist/components/announcement-bar.d.ts +1 -1
  39. package/dist/components/announcement-bar.d.ts.map +1 -1
  40. package/dist/components/case-studies/index.cjs +8 -8
  41. package/dist/components/case-studies/index.js +2 -2
  42. package/dist/components/chat/index.cjs +2 -2
  43. package/dist/components/chat/index.js +1 -1
  44. package/dist/components/contact/index.cjs +3 -3
  45. package/dist/components/contact/index.js +2 -2
  46. package/dist/components/docs/index.cjs +5 -5
  47. package/dist/components/docs/index.js +4 -4
  48. package/dist/components/embeds/index.cjs +3 -3
  49. package/dist/components/embeds/index.js +2 -2
  50. package/dist/components/faq/index.cjs +3 -3
  51. package/dist/components/faq/index.js +2 -2
  52. package/dist/components/features/index.cjs +2 -2
  53. package/dist/components/features/index.js +1 -1
  54. package/dist/components/form.d.ts +1 -0
  55. package/dist/components/form.d.ts.map +1 -1
  56. package/dist/components/help-center-pages/index.cjs +20 -20
  57. package/dist/components/help-center-pages/index.js +11 -11
  58. package/dist/components/index.cjs +232 -206
  59. package/dist/components/index.cjs.map +1 -1
  60. package/dist/components/index.js +117 -91
  61. package/dist/components/index.js.map +1 -1
  62. package/dist/components/navigation/index.cjs +2 -2
  63. package/dist/components/navigation/index.js +1 -1
  64. package/dist/components/onboarding-guides/index.cjs +5 -5
  65. package/dist/components/onboarding-guides/index.js +4 -4
  66. package/dist/components/related-content/index.cjs +3 -3
  67. package/dist/components/related-content/index.js +2 -2
  68. package/dist/components/tickets/index.cjs +5 -5
  69. package/dist/components/tickets/index.js +4 -4
  70. package/dist/components/ui/announcement-bar-view.d.ts +49 -0
  71. package/dist/components/ui/announcement-bar-view.d.ts.map +1 -0
  72. package/dist/components/ui/index.cjs +4 -2
  73. package/dist/components/ui/index.cjs.map +1 -1
  74. package/dist/components/ui/index.d.ts +1 -0
  75. package/dist/components/ui/index.d.ts.map +1 -1
  76. package/dist/components/ui/index.js +3 -1
  77. package/dist/index.cjs +4 -2
  78. package/dist/index.cjs.map +1 -1
  79. package/dist/index.js +3 -1
  80. package/package.json +1 -1
  81. package/src/components/announcement-bar.tsx +102 -110
  82. package/src/components/ui/announcement-bar-view.tsx +94 -0
  83. package/src/components/ui/index.ts +1 -0
  84. package/src/styles/ods-colors.css +16 -0
  85. package/dist/chunk-IZCOYFCN.js.map +0 -1
  86. package/dist/chunk-VLLFRPNP.cjs.map +0 -1
  87. /package/dist/{chunk-XJH5ZT5Y.js.map → chunk-25YYEVSE.js.map} +0 -0
  88. /package/dist/{chunk-CC73CSLI.js.map → chunk-3L44PPMF.js.map} +0 -0
  89. /package/dist/{chunk-GRB7LPJV.js.map → chunk-4DFPQKKJ.js.map} +0 -0
  90. /package/dist/{chunk-FHLJQI4F.js.map → chunk-ABU37QSG.js.map} +0 -0
  91. /package/dist/{chunk-KX2TBXKN.js.map → chunk-BIDOIPIT.js.map} +0 -0
  92. /package/dist/{chunk-72HPXAMZ.js.map → chunk-GXXLJHQX.js.map} +0 -0
  93. /package/dist/{chunk-RQZOCLTI.js.map → chunk-ILZ66CS6.js.map} +0 -0
  94. /package/dist/{chunk-GHZMNDPN.js.map → chunk-LOWJWKM5.js.map} +0 -0
  95. /package/dist/{chunk-7DULN25L.js.map → chunk-NYOQNYSI.js.map} +0 -0
  96. /package/dist/{chunk-OO7DO4DI.js.map → chunk-QJ7NIL6A.js.map} +0 -0
  97. /package/dist/{chunk-WAO34PFO.js.map → chunk-TZSPLSDU.js.map} +0 -0
@@ -1,20 +1,21 @@
1
- "use client";
1
+ 'use client';
2
2
 
3
- import { useEffect, useMemo, useRef, useState } from 'react';
4
3
  import { X } from 'lucide-react';
5
- import { Button } from './ui/button';
6
- import { EntityIcon } from './icon-display';
4
+ import { useEffect, useMemo, useRef, useState } from 'react';
5
+ import { useEndpointsRuntime } from '../contexts/endpoints-runtime-context';
6
+ import { useSelfFetch } from '../hooks/use-self-fetch';
7
+ import type { Announcement, AnnouncementBarProps, AnnouncementResponse } from '../types/announcement';
8
+ import { ANNOUNCEMENT_CTA_DEFAULTS } from '../types/announcement';
7
9
  import {
10
+ clearLegacyAnnouncementCache,
8
11
  dismissAnnouncement,
9
12
  isAnnouncementDismissed,
10
- clearLegacyAnnouncementCache,
11
13
  } from '../utils/announcement-storage';
12
- import { ANNOUNCEMENT_CTA_DEFAULTS } from '../types/announcement';
13
- import type { Announcement, AnnouncementBarProps, AnnouncementResponse } from '../types/announcement';
14
14
  import { getAppType } from '../utils/app-config';
15
- import { useEndpointsRuntime } from '../contexts/endpoints-runtime-context';
16
- import { useSelfFetch } from '../hooks/use-self-fetch';
17
15
  import { pickReadableTextColor } from '../utils/color-analysis';
16
+ import { EntityIcon } from './icon-display';
17
+ import { AnnouncementBarView } from './ui/announcement-bar-view';
18
+ import { Button } from './ui/button';
18
19
 
19
20
  /**
20
21
  * Platform announcement bar — DUAL MODE, works with or without SSR.
@@ -41,11 +42,7 @@ import { pickReadableTextColor } from '../utils/color-analysis';
41
42
  * storage reads happen ONLY in effects (a render-time read would desync
42
43
  * hydration in SSR mode).
43
44
  */
44
- export function AnnouncementBar({
45
- initialAnnouncement,
46
- previewMode = false,
47
- className,
48
- }: AnnouncementBarProps = {}) {
45
+ export function AnnouncementBar({ initialAnnouncement, previewMode = false, className }: AnnouncementBarProps = {}) {
49
46
  // Namespace for the dismissal cookie/legacy keys. Next hosts inline
50
47
  // NEXT_PUBLIC_APP_TYPE (matching the server's currentPlatform(), which the
51
48
  // hub layout uses for the SSR cookie read); platform-agnostic embeds get
@@ -55,7 +52,7 @@ export function AnnouncementBar({
55
52
 
56
53
  // Optional endpoint runtime: no provider → no URL → fetching disabled.
57
54
  const endpoints = useEndpointsRuntime();
58
- const url = previewMode ? null : endpoints?.announcementsUrl ?? null;
55
+ const url = previewMode ? null : (endpoints?.announcementsUrl ?? null);
59
56
 
60
57
  // MUST be memoized (the hook re-syncs on [initialData] identity — an inline
61
58
  // literal per render would setData-loop) and strict-undefined-mapped:
@@ -157,18 +154,20 @@ export function AnnouncementBar({
157
154
  // dark-toned needs the dark theme's). `.theme-light` / `.theme-dark` are
158
155
  // the design system's own scoping classes (ods-colors.css) and flip every
159
156
  // Tier-1 `--ods-*` primitive for descendants.
160
- const themeScope = pickReadableTextColor(displayAnnouncement.background_color) === 'dark' ? 'theme-light' : 'theme-dark';
157
+ const themeScope =
158
+ pickReadableTextColor(displayAnnouncement.background_color) === 'dark' ? 'theme-light' : 'theme-dark';
161
159
 
162
- // Inside the scope, colors reference Tier-1 theme primitives directly
163
- // (Tier-2 `--color-*` aliases resolve once at :root and do NOT re-resolve
164
- // in a nested theme scope — verified). `--ods-system-greys-white` is the
165
- // theme's primary-text primitive; `--ods-system-greys-black-hover/-action`
166
- // are its quiet hover/active surfaces. Every value comes from the active
167
- // ODS theme; nothing is hardcoded.
160
+ // Inside the scope, colors reference Tier-2 semantic aliases. Aliases
161
+ // declared only at :root would NOT re-resolve in a nested theme scope
162
+ // (they compute at :root), so ods-colors.css re-anchors these three inside
163
+ // `.theme-light` / `.theme-dark` specifically for theme islands like this
164
+ // bar. Every value comes from the active ODS theme; nothing is hardcoded.
168
165
  const barButtonClasses =
169
- 'text-[color:var(--ods-system-greys-white)] hover:bg-[var(--ods-system-greys-black-hover)] active:bg-[var(--ods-system-greys-black-action)]';
166
+ 'text-[color:var(--color-text-primary)] hover:bg-[var(--color-bg-hover)] active:bg-[var(--color-bg-active)]';
170
167
 
171
- const hasCta = Boolean(displayAnnouncement.cta_enabled && displayAnnouncement.cta_url && displayAnnouncement.cta_text);
168
+ const hasCta = Boolean(
169
+ displayAnnouncement.cta_enabled && displayAnnouncement.cta_url && displayAnnouncement.cta_text,
170
+ );
172
171
 
173
172
  return (
174
173
  <div
@@ -184,32 +183,23 @@ export function AnnouncementBar({
184
183
  style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
185
184
  >
186
185
  {/*
187
- Bar anatomy follows the announcement-bar industry standard: ONE line of
188
- text at 13-14px inside a 44px-tall strip (guides converge on 40-60px
189
- with a single sentence; two stacked 18px rows blew past that), title +
190
- description merged inline (description hidden on small screens), ONE
191
- compact CTA on the right, and a ghost-icon dismiss (design-system
192
- icon-sm: 32px target, >= the 24px WCAG 2.2 SC 2.5.8 AA floor).
186
+ Markup is the shared pure view (AnnouncementBarView, Figma
187
+ 9364-40603 / 9418-43969 / 9418-44006): one row from `md` (800px) up,
188
+ stacked below it with the CTA stretched full-width next to the
189
+ content-width dismiss. The CTA Button is now rendered on EVERY
190
+ breakpoint (it replaced the old whole-bar mobile tap target, which
191
+ also retires the 768px window.innerWidth check that disagreed with
192
+ the md:800px breakpoint).
193
193
  */}
194
- <div className={`min-h-0 overflow-hidden ${themeScope}`} style={{ backgroundColor: displayAnnouncement.background_color }}>
195
- <div className="flex items-center w-full max-w-full min-h-11 text-[color:var(--ods-system-greys-white)]">
196
- {/* Mobile: whole-bar tap target (touch-first by design; the CTA
197
- Button below is the keyboard/AT path and is CSS-hidden < md —
198
- known tradeoff carried from the original bar). */}
199
- <div
200
- className={`flex flex-row gap-2 md:gap-3 items-center pl-4 md:pl-6 py-1.5 flex-1 min-w-0 ${
201
- hasCta ? 'md:cursor-default cursor-pointer' : ''
202
- }`}
203
- onClick={(e) => {
204
- // Only handle click on mobile (< 768px) and if CTA is enabled
205
- if (window.innerWidth < 768 && hasCta) {
206
- e.preventDefault();
207
- handleCtaClick();
208
- }
209
- }}
210
- >
211
- {/* ONE unified icon path (shared with the chat): uploaded image URL
212
- wins, else a library glyph by name (+ props), via <EntityIcon>. */}
194
+ <div
195
+ className={`min-h-0 overflow-hidden ${themeScope}`}
196
+ style={{ backgroundColor: displayAnnouncement.background_color }}
197
+ >
198
+ <AnnouncementBarView
199
+ className="text-[color:var(--color-text-primary)]"
200
+ startAdornment={
201
+ /* ONE unified icon path (shared with the chat): uploaded image URL
202
+ wins, else a library glyph by name (+ props), via <EntityIcon>. */
213
203
  <EntityIcon
214
204
  icon={{
215
205
  name: displayAnnouncement.icon_name || 'openframe-logo',
@@ -217,72 +207,74 @@ export function AnnouncementBar({
217
207
  props: displayAnnouncement.icon_props,
218
208
  }}
219
209
  size={24}
220
- className="relative shrink-0 w-5 h-5 md:w-6 md:h-6"
210
+ // `!` is required: logo glyphs (LogoOpenframeIcon / sizedLogo in
211
+ // icon-library) drive their size via an inline style, which beats
212
+ // a plain class — author !important beats the inline style, so
213
+ // the responsive token (16px < md, 24px from md) wins everywhere.
214
+ className="relative !size-[var(--icon-size-icon-size)] shrink-0"
221
215
  />
222
-
223
- {/* Single-line message: bold title + regular description inline,
224
- truncating as one unit. Separator is a middot (house rule: no
225
- en/em dashes in copy). */}
226
- <p className="flex-1 min-w-0 max-w-full text-h6 truncate mb-0">
227
- <span className="font-semibold">{displayAnnouncement.title}</span>
216
+ }
217
+ title={
218
+ /* Single-line message: title + description inline, truncating as
219
+ one unit. Type per the ODS mockup (2862-8391): the "h3 body @
220
+ 500" Figma style maps to the code's `.text-h4` utility (the
221
+ `.text-h3` utility is 700; h3/h4 body share the same size
222
+ tokens) — the same treatment string titles get from the view.
223
+ Description keeps opacity-80 for hierarchy. Separator is a
224
+ middot (house rule: no en/em dashes in copy). */
225
+ <p className="min-w-0 max-w-full text-h4 truncate mb-0">
226
+ <span>{displayAnnouncement.title}</span>
228
227
  {displayAnnouncement.description && (
229
228
  <span className="hidden sm:inline opacity-80"> · {displayAnnouncement.description}</span>
230
229
  )}
231
230
  </p>
231
+ }
232
+ actionBlock={
233
+ /* CTA - the common Button carrying the ADMIN-CONFIGURED colors
234
+ (cta_button_background_color / cta_button_text_color are an
235
+ admin FEATURE, defaults from ANNOUNCEMENT_CTA_DEFAULTS —
236
+ announcement colors are data, not token surfaces). Inline
237
+ styles win over the variant's hover classes on every state,
238
+ so hover feedback is opacity (the bar's original treatment);
239
+ nothing can render dark-on-dark.
232
240
 
233
- {/* CTA - the common Button carrying the ADMIN-CONFIGURED colors
234
- (cta_button_background_color / cta_button_text_color are an
235
- admin FEATURE, defaults from ANNOUNCEMENT_CTA_DEFAULTS
236
- announcement colors are data, not token surfaces). Inline
237
- styles win over the variant's hover classes on every state,
238
- so hover feedback is opacity (the bar's original treatment);
239
- nothing can render dark-on-dark. Hidden on mobile, where the
240
- whole bar is the tap target.
241
-
242
- Geometry + type come from the design system's size="compact"
243
- (24px caption-scale pill for slim strips — rationale documented
244
- on the variant in button.tsx). */}
245
- {hasCta && displayAnnouncement.cta_text && (
246
- <div className="hidden md:flex flex-shrink-0 ml-2 md:ml-4">
247
- <Button
248
- onClick={handleCtaClick}
249
- variant="outline"
250
- size="compact"
251
- className="transition-opacity hover:opacity-90"
252
- style={{
253
- backgroundColor: displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,
254
- color: displayAnnouncement.cta_button_text_color || ANNOUNCEMENT_CTA_DEFAULTS.text,
255
- borderColor: displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,
256
- }}
257
- tabIndex={expanded ? 0 : -1}
258
- leftIcon={
259
- displayAnnouncement.cta_show_icon && displayAnnouncement.cta_icon_name
260
- ? (
261
- <EntityIcon
262
- icon={{ name: displayAnnouncement.cta_icon_name, props: displayAnnouncement.cta_icon_props }}
263
- size={14}
264
- className="w-3.5 h-3.5"
265
- />
266
- )
267
- : undefined
268
- }
269
- >
270
- {displayAnnouncement.cta_text}
271
- </Button>
272
- </div>
273
- )}
274
- </div>
275
-
276
- {/* Dismiss - the common Button in its ghost-icon treatment
277
- (size="icon-sm": 32px target, >= the 24px WCAG 2.5.8 AA floor,
278
- 16px glyph) with the bar's quiet tint hover. Inert in
279
- previewMode. */}
280
- <div className="flex-shrink-0 ml-1 md:ml-2 mr-2 md:mr-4">
241
+ Geometry + type come from the design system's size="compact"
242
+ (24px caption-scale pill for slim strips — rationale documented
243
+ on the variant in button.tsx). */
244
+ hasCta && displayAnnouncement.cta_text ? (
245
+ <Button
246
+ onClick={handleCtaClick}
247
+ variant="outline"
248
+ size="compact"
249
+ className="transition-opacity hover:opacity-90"
250
+ style={{
251
+ backgroundColor:
252
+ displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,
253
+ color: displayAnnouncement.cta_button_text_color || ANNOUNCEMENT_CTA_DEFAULTS.text,
254
+ borderColor: displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,
255
+ }}
256
+ tabIndex={expanded ? 0 : -1}
257
+ leftIcon={
258
+ displayAnnouncement.cta_show_icon && displayAnnouncement.cta_icon_name ? (
259
+ <EntityIcon
260
+ icon={{ name: displayAnnouncement.cta_icon_name, props: displayAnnouncement.cta_icon_props }}
261
+ size={14}
262
+ className="w-3.5 h-3.5"
263
+ />
264
+ ) : undefined
265
+ }
266
+ >
267
+ {displayAnnouncement.cta_text}
268
+ </Button>
269
+ ) : undefined
270
+ }
271
+ endAdornment={
272
+ /* Dismiss - the common Button in its ghost-icon treatment
273
+ (size="icon-sm": 32px target, >= the 24px WCAG 2.5.8 AA floor,
274
+ 16px glyph) with the bar's quiet tint hover. Inert in
275
+ previewMode. */
281
276
  <Button
282
- onClick={(e) => {
283
- e.stopPropagation(); // Prevent triggering the mobile CTA click
284
- handleDismiss();
285
- }}
277
+ onClick={handleDismiss}
286
278
  variant="transparent"
287
279
  size="icon-sm"
288
280
  className={barButtonClasses}
@@ -292,8 +284,8 @@ export function AnnouncementBar({
292
284
  >
293
285
  <X strokeWidth={2} />
294
286
  </Button>
295
- </div>
296
- </div>
287
+ }
288
+ />
297
289
  </div>
298
290
  </div>
299
291
  );
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+
3
+ import type { CSSProperties, ReactElement, ReactNode } from 'react';
4
+ import { cn } from '../../utils/cn';
5
+
6
+ export interface AnnouncementBarViewProps {
7
+ /**
8
+ * Leading slot — typically an icon. Rendered before the title. Per the
9
+ * mockup icons are 16px below `md` and 24px from `md` up — size them with
10
+ * the responsive token: `size-[var(--icon-size-icon-size)]`.
11
+ */
12
+ startAdornment?: ReactNode;
13
+ /**
14
+ * Bar message. A plain string gets the mockup's text treatment (`text-h4`,
15
+ * single line from `md` up, wrapping below); pass a ReactElement to fully
16
+ * own the markup/typography (e.g. bold title + inline description).
17
+ */
18
+ title?: string | ReactElement;
19
+ /**
20
+ * Primary CTA. From `md` up it sits at content width in one trailing
21
+ * container with `endAdornment`; below `md` it drops to its own
22
+ * full-width second row (Figma ODS 2862-8391). Omit it entirely to
23
+ * collapse that row — the bar height then follows the remaining content.
24
+ */
25
+ actionBlock?: ReactElement;
26
+ /**
27
+ * Trailing slot (e.g. a dismiss button). Always keeps its content width:
28
+ * inline after the title below `md` (and on every breakpoint when there is
29
+ * no `actionBlock`), after the action from `md` up. NOTE: with an
30
+ * `actionBlock` present the element is mounted twice (one copy per
31
+ * breakpoint, the inactive one `display:none`) — keep it stateless.
32
+ */
33
+ endAdornment?: ReactElement;
34
+ /** Surface styling (background, text color, theme scope) is the consumer's — pass it here. */
35
+ className?: string;
36
+ style?: CSSProperties;
37
+ }
38
+
39
+ /**
40
+ * Pure presentational announcement/notification bar (Figma 9364-40603 desktop,
41
+ * 9418-43969 tablet, ODS 2862-8391 mobile). No data fetching, storage, or
42
+ * navigation — consumers own state and pass content through slots.
43
+ *
44
+ * Layout: `md` (800px) and up is a single row — `startAdornment` + title
45
+ * (flex-1, truncating) + trailing container (`actionBlock` + `endAdornment`)
46
+ * at content width. Below `md`: `startAdornment` + wrapping title +
47
+ * `endAdornment` share the first row (top-aligned so a two-line title keeps
48
+ * the adornments on line one, each centered in a line-height box), and the
49
+ * action alone stretches across its own second row. No action → no second
50
+ * row; the bar height follows the content. Spacing/typography use responsive
51
+ * ODS tokens, so paddings and font sizes track the breakpoint automatically.
52
+ */
53
+ export function AnnouncementBarView({
54
+ startAdornment,
55
+ title,
56
+ actionBlock,
57
+ endAdornment,
58
+ className,
59
+ style,
60
+ }: AnnouncementBarViewProps) {
61
+ // Below `md` adornments center inside a box matching the title's first
62
+ // text line (20px), so `items-start` keeps them on line one of a wrapped
63
+ // title without hugging the row's top edge. From `md` up the row is
64
+ // single-line and center-aligned, so the boxes dissolve (`h-auto`).
65
+ const adornmentBox = 'flex h-5 shrink-0 items-center md:h-auto';
66
+
67
+ return (
68
+ <div
69
+ className={cn(
70
+ 'flex w-full flex-col gap-[var(--spacing-system-s)] px-[var(--spacing-system-l)] py-[var(--spacing-system-sf)] md:flex-row md:items-center',
71
+ className,
72
+ )}
73
+ style={style}
74
+ >
75
+ <div className="flex min-w-0 flex-1 items-start gap-[var(--spacing-system-s)] md:items-center">
76
+ {startAdornment && <div className={adornmentBox}>{startAdornment}</div>}
77
+ {typeof title === 'string' ? (
78
+ <p className="min-w-0 flex-1 break-words text-h4 md:truncate">{title}</p>
79
+ ) : (
80
+ title != null && <div className="min-w-0 flex-1">{title}</div>
81
+ )}
82
+ {/* Mobile home of the trailing adornment — and its only home when
83
+ there is no action row to share from `md` up. */}
84
+ {endAdornment && <div className={cn(adornmentBox, actionBlock && 'md:hidden')}>{endAdornment}</div>}
85
+ </div>
86
+ {actionBlock && (
87
+ <div className="flex w-full shrink-0 items-center gap-[var(--spacing-system-s)] md:w-auto">
88
+ <div className="flex min-w-0 flex-1 md:flex-none [&>*]:w-full md:[&>*]:w-auto">{actionBlock}</div>
89
+ {endAdornment && <div className="hidden shrink-0 md:flex">{endAdornment}</div>}
90
+ </div>
91
+ )}
92
+ </div>
93
+ );
94
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  // UI Components exports
4
4
  export * from './allowed-domains-input'
5
+ export * from './announcement-bar-view'
5
6
  export * from './autocomplete'
6
7
  export * from './button'
7
8
  export * from './card'
@@ -336,6 +336,22 @@
336
336
  --ods-accent: var(--ods-open-yellow-base);
337
337
  }
338
338
 
339
+ /* Tier-2 re-anchor for NESTED theme scopes. Tier-2 aliases declared at
340
+ * `:root` compute there (custom properties substitute var() where the
341
+ * property is declared), so descendants of a nested `.theme-light` /
342
+ * `.theme-dark` island inherit the ROOT-theme value — the island's Tier-1
343
+ * flips never reach them. Re-declaring an alias inside the scope classes
344
+ * recomputes it against the scoped Tier-1 primitives, letting components
345
+ * inside theme islands (e.g. AnnouncementBar) use semantic tokens instead
346
+ * of reaching for Tier-1 directly. Values are identical to the `:root`
347
+ * declarations above — keep them in sync. */
348
+ .theme-dark,
349
+ .theme-light {
350
+ --color-text-primary: var(--ods-system-greys-white);
351
+ --color-bg-hover: var(--ods-system-greys-black-hover);
352
+ --color-bg-active: var(--ods-system-greys-black-action);
353
+ }
354
+
339
355
  /* Alpha-based semantic tokens that need a light-theme tweak (the only
340
356
  * Tier-2 values not derived from a flipping primitive). */
341
357
  :root[data-theme="light"],