@flamingo-stack/openframe-frontend-core 0.0.472 → 0.0.473-snapshot.20260724114839

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 (132) hide show
  1. package/dist/{chunk-4BUX2F5P.cjs → chunk-2R5NM3FC.cjs} +40 -40
  2. package/dist/{chunk-4BUX2F5P.cjs.map → chunk-2R5NM3FC.cjs.map} +1 -1
  3. package/dist/{chunk-BY6TZ6KA.js → chunk-3NNCBZEM.js} +6 -6
  4. package/dist/{chunk-WD7SE532.cjs → chunk-464NJS2W.cjs} +73 -73
  5. package/dist/{chunk-WD7SE532.cjs.map → chunk-464NJS2W.cjs.map} +1 -1
  6. package/dist/{chunk-KZZCBQDU.cjs → chunk-5TQQ3CPE.cjs} +29 -29
  7. package/dist/{chunk-KZZCBQDU.cjs.map → chunk-5TQQ3CPE.cjs.map} +1 -1
  8. package/dist/{chunk-ONK4D4LG.cjs → chunk-6KD6KVAI.cjs} +5 -5
  9. package/dist/{chunk-ONK4D4LG.cjs.map → chunk-6KD6KVAI.cjs.map} +1 -1
  10. package/dist/{chunk-73CH4CZL.cjs → chunk-7VBGYCXG.cjs} +7 -7
  11. package/dist/{chunk-73CH4CZL.cjs.map → chunk-7VBGYCXG.cjs.map} +1 -1
  12. package/dist/{chunk-FPOHUGEK.js → chunk-AGX565ZT.js} +2 -2
  13. package/dist/{chunk-RIEAXUJX.cjs → chunk-BOPLMNSN.cjs} +135 -174
  14. package/dist/chunk-BOPLMNSN.cjs.map +1 -0
  15. package/dist/{chunk-PJYGEXWM.js → chunk-FBUIZTAI.js} +2 -2
  16. package/dist/{chunk-HQL5GJ35.cjs → chunk-FM5KWLKU.cjs} +3 -3
  17. package/dist/{chunk-HQL5GJ35.cjs.map → chunk-FM5KWLKU.cjs.map} +1 -1
  18. package/dist/{chunk-YTLINJRR.cjs → chunk-GELW7JOQ.cjs} +32 -32
  19. package/dist/{chunk-YTLINJRR.cjs.map → chunk-GELW7JOQ.cjs.map} +1 -1
  20. package/dist/{chunk-TVMARS74.js → chunk-IIOHHNMI.js} +54 -93
  21. package/dist/chunk-IIOHHNMI.js.map +1 -0
  22. package/dist/{chunk-XNJ2LZH2.js → chunk-K52S6LZ5.js} +3 -3
  23. package/dist/{chunk-JO7XJQ6G.cjs → chunk-KPVKZ5ML.cjs} +15 -15
  24. package/dist/{chunk-JO7XJQ6G.cjs.map → chunk-KPVKZ5ML.cjs.map} +1 -1
  25. package/dist/{chunk-DWQ4L3IB.cjs → chunk-KXKTXAGN.cjs} +40 -40
  26. package/dist/{chunk-DWQ4L3IB.cjs.map → chunk-KXKTXAGN.cjs.map} +1 -1
  27. package/dist/{chunk-VC4FOIY7.cjs → chunk-LFXUFFDZ.cjs} +73 -73
  28. package/dist/{chunk-VC4FOIY7.cjs.map → chunk-LFXUFFDZ.cjs.map} +1 -1
  29. package/dist/{chunk-ZPUT2BB4.js → chunk-OKRUL7YV.js} +4 -4
  30. package/dist/{chunk-U5FNZ2TL.cjs → chunk-PFD2VRZI.cjs} +18 -18
  31. package/dist/{chunk-U5FNZ2TL.cjs.map → chunk-PFD2VRZI.cjs.map} +1 -1
  32. package/dist/{chunk-W6RDDT3S.js → chunk-PTFSF3UP.js} +6 -6
  33. package/dist/{chunk-74ZFCTRZ.js → chunk-R4OGP4OV.js} +3 -3
  34. package/dist/{chunk-7D5KOJMF.js → chunk-RIPWRVA3.js} +5 -5
  35. package/dist/{chunk-EK2VQXUN.js → chunk-S6IB3BZK.js} +2 -2
  36. package/dist/{chunk-OUHLWOPJ.js → chunk-SAH4FR36.js} +5 -5
  37. package/dist/{chunk-TONMD7QX.cjs → chunk-VCOL4C2W.cjs} +86 -20
  38. package/dist/chunk-VCOL4C2W.cjs.map +1 -0
  39. package/dist/{chunk-XDBNMB5K.js → chunk-VNDAMVMI.js} +2 -2
  40. package/dist/{chunk-CUBNUQPZ.cjs → chunk-YJWISFIK.cjs} +12 -12
  41. package/dist/{chunk-CUBNUQPZ.cjs.map → chunk-YJWISFIK.cjs.map} +1 -1
  42. package/dist/{chunk-NY4WLKNU.js → chunk-Z4LAZ4ZZ.js} +3 -3
  43. package/dist/{chunk-OTV4A6NA.js → chunk-ZPUZPJDV.js} +80 -14
  44. package/dist/chunk-ZPUZPJDV.js.map +1 -0
  45. package/dist/components/announcement-bar.d.ts +1 -1
  46. package/dist/components/announcement-bar.d.ts.map +1 -1
  47. package/dist/components/case-studies/index.cjs +9 -9
  48. package/dist/components/case-studies/index.js +3 -3
  49. package/dist/components/chat/index.cjs +3 -3
  50. package/dist/components/chat/index.js +2 -2
  51. package/dist/components/contact/index.cjs +4 -4
  52. package/dist/components/contact/index.js +3 -3
  53. package/dist/components/docs/index.cjs +6 -6
  54. package/dist/components/docs/index.js +5 -5
  55. package/dist/components/embeds/index.cjs +4 -4
  56. package/dist/components/embeds/index.js +3 -3
  57. package/dist/components/faq/index.cjs +5 -5
  58. package/dist/components/faq/index.js +4 -4
  59. package/dist/components/features/index.cjs +3 -3
  60. package/dist/components/features/index.js +2 -2
  61. package/dist/components/help-center-pages/index.cjs +22 -22
  62. package/dist/components/help-center-pages/index.js +13 -13
  63. package/dist/components/index.cjs +174 -162
  64. package/dist/components/index.cjs.map +1 -1
  65. package/dist/components/index.js +49 -37
  66. package/dist/components/index.js.map +1 -1
  67. package/dist/components/navigation/index.cjs +3 -3
  68. package/dist/components/navigation/index.js +2 -2
  69. package/dist/components/navigation/mobile-burger-menu.d.ts.map +1 -1
  70. package/dist/components/onboarding-guides/index.cjs +7 -7
  71. package/dist/components/onboarding-guides/index.js +6 -6
  72. package/dist/components/related-content/index.cjs +5 -5
  73. package/dist/components/related-content/index.js +4 -4
  74. package/dist/components/tickets/index.cjs +6 -6
  75. package/dist/components/tickets/index.js +5 -5
  76. package/dist/components/ui/announcement-bar-view.d.ts +26 -24
  77. package/dist/components/ui/announcement-bar-view.d.ts.map +1 -1
  78. package/dist/components/ui/entity-image.d.ts.map +1 -1
  79. package/dist/components/ui/image-uploader.d.ts.map +1 -1
  80. package/dist/components/ui/index.cjs +3 -3
  81. package/dist/components/ui/index.js +2 -2
  82. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
  83. package/dist/components/ui/square-avatar.d.ts.map +1 -1
  84. package/dist/hooks/index.cjs +6 -2
  85. package/dist/hooks/index.cjs.map +1 -1
  86. package/dist/hooks/index.d.ts +1 -0
  87. package/dist/hooks/index.d.ts.map +1 -1
  88. package/dist/hooks/index.js +5 -1
  89. package/dist/hooks/use-authed-image-src.d.ts +10 -0
  90. package/dist/hooks/use-authed-image-src.d.ts.map +1 -0
  91. package/dist/index.cjs +7 -3
  92. package/dist/index.cjs.map +1 -1
  93. package/dist/index.js +6 -2
  94. package/dist/types/announcement.cjs.map +1 -1
  95. package/dist/types/announcement.d.ts +5 -0
  96. package/dist/types/announcement.d.ts.map +1 -1
  97. package/dist/types/announcement.js.map +1 -1
  98. package/dist/types/index.cjs.map +1 -1
  99. package/dist/types/index.js.map +1 -1
  100. package/dist/utils/embed-authed-fetch.d.ts +15 -0
  101. package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
  102. package/dist/utils/index.cjs.map +1 -1
  103. package/dist/utils/index.js.map +1 -1
  104. package/package.json +1 -1
  105. package/src/components/announcement-bar.tsx +58 -42
  106. package/src/components/navigation/mobile-burger-menu.tsx +1 -0
  107. package/src/components/ui/announcement-bar-view.tsx +42 -66
  108. package/src/components/ui/entity-image.tsx +5 -3
  109. package/src/components/ui/image-uploader.tsx +21 -12
  110. package/src/components/ui/simple-markdown-renderer.tsx +57 -58
  111. package/src/components/ui/square-avatar.tsx +5 -3
  112. package/src/hooks/index.ts +1 -0
  113. package/src/hooks/use-authed-image-src.ts +93 -0
  114. package/src/stories/AnnouncementBar.stories.tsx +20 -0
  115. package/src/types/announcement.ts +5 -0
  116. package/src/utils/embed-authed-fetch.ts +30 -0
  117. package/dist/chunk-OTV4A6NA.js.map +0 -1
  118. package/dist/chunk-RIEAXUJX.cjs.map +0 -1
  119. package/dist/chunk-TONMD7QX.cjs.map +0 -1
  120. package/dist/chunk-TVMARS74.js.map +0 -1
  121. /package/dist/{chunk-BY6TZ6KA.js.map → chunk-3NNCBZEM.js.map} +0 -0
  122. /package/dist/{chunk-FPOHUGEK.js.map → chunk-AGX565ZT.js.map} +0 -0
  123. /package/dist/{chunk-PJYGEXWM.js.map → chunk-FBUIZTAI.js.map} +0 -0
  124. /package/dist/{chunk-XNJ2LZH2.js.map → chunk-K52S6LZ5.js.map} +0 -0
  125. /package/dist/{chunk-ZPUT2BB4.js.map → chunk-OKRUL7YV.js.map} +0 -0
  126. /package/dist/{chunk-W6RDDT3S.js.map → chunk-PTFSF3UP.js.map} +0 -0
  127. /package/dist/{chunk-74ZFCTRZ.js.map → chunk-R4OGP4OV.js.map} +0 -0
  128. /package/dist/{chunk-7D5KOJMF.js.map → chunk-RIPWRVA3.js.map} +0 -0
  129. /package/dist/{chunk-EK2VQXUN.js.map → chunk-S6IB3BZK.js.map} +0 -0
  130. /package/dist/{chunk-OUHLWOPJ.js.map → chunk-SAH4FR36.js.map} +0 -0
  131. /package/dist/{chunk-XDBNMB5K.js.map → chunk-VNDAMVMI.js.map} +0 -0
  132. /package/dist/{chunk-NY4WLKNU.js.map → chunk-Z4LAZ4ZZ.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.472",
3
+ "version": "0.0.473-snapshot.20260724114839",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,6 +12,7 @@ import {
12
12
  isAnnouncementDismissed,
13
13
  } from '../utils/announcement-storage';
14
14
  import { getAppType } from '../utils/app-config';
15
+ import { cn } from '../utils/cn';
15
16
  import { pickReadableTextColor } from '../utils/color-analysis';
16
17
  import { EntityIcon } from './icon-display';
17
18
  import { AnnouncementBarView } from './ui/announcement-bar-view';
@@ -42,7 +43,12 @@ import { Button } from './ui/button';
42
43
  * storage reads happen ONLY in effects (a render-time read would desync
43
44
  * hydration in SSR mode).
44
45
  */
45
- export function AnnouncementBar({ initialAnnouncement, previewMode = false, className }: AnnouncementBarProps = {}) {
46
+ export function AnnouncementBar({
47
+ initialAnnouncement,
48
+ previewMode = false,
49
+ dismissible = true,
50
+ className,
51
+ }: AnnouncementBarProps = {}) {
46
52
  // Namespace for the dismissal cookie/legacy keys. Next hosts inline
47
53
  // NEXT_PUBLIC_APP_TYPE (matching the server's currentPlatform(), which the
48
54
  // hub layout uses for the SSR cookie read); platform-agnostic embeds get
@@ -184,9 +190,11 @@ export function AnnouncementBar({ initialAnnouncement, previewMode = false, clas
184
190
  >
185
191
  {/*
186
192
  Markup is the shared pure view (AnnouncementBarView), which carries the
187
- bar's anatomy: ONE line of text inside a 44px strip, ONE compact CTA on
188
- the right (hidden below `md`, where the content row is the tap target),
189
- and a ghost-icon dismiss. Surface + content treatment stay here.
193
+ bar's anatomy: one 56px row from `md` up with the compact CTA inline;
194
+ below `md` a stacked layout with the CTA VISIBLE full-width on its own
195
+ row and the dismiss up in the content row (Figma 2862-8391 — this
196
+ replaced the old whole-bar mobile tap target). Surface + content
197
+ treatment stay here.
190
198
  */}
191
199
  <div
192
200
  className={`min-h-0 overflow-hidden ${themeScope}`}
@@ -194,8 +202,6 @@ export function AnnouncementBar({ initialAnnouncement, previewMode = false, clas
194
202
  >
195
203
  <AnnouncementBarView
196
204
  className="text-[color:var(--color-text-primary)]"
197
- contentClassName={hasCta ? 'cursor-pointer md:cursor-default' : undefined}
198
- onContentClick={hasCta ? handleCtaClick : undefined}
199
205
  startAdornment={
200
206
  /* ONE unified icon path (shared with the chat): uploaded image URL
201
207
  wins, else a library glyph by name (+ props), via <EntityIcon>. */
@@ -206,20 +212,22 @@ export function AnnouncementBar({ initialAnnouncement, previewMode = false, clas
206
212
  props: displayAnnouncement.icon_props,
207
213
  }}
208
214
  size={24}
209
- // 20px below `md`, 24px from `md` up. `!` is required: logo
210
- // glyphs (LogoOpenframeIcon / sizedLogo in icon-library) drive
211
- // their size via an inline style, which a plain class loses to.
212
- // Not `--icon-size-icon-size` — that token is 16/24 and would
213
- // shrink the mobile glyph.
214
- className="relative !size-5 shrink-0 md:!size-6"
215
+ // 16px below `md` (the mockup's mobile glyph — it must not
216
+ // exceed the 20px title row, Figma 9418-52387 / 2862-8391),
217
+ // 24px from `md` up. `!` is required: logo glyphs
218
+ // (LogoOpenframeIcon / sizedLogo in icon-library) drive their
219
+ // size via an inline style, which a plain class loses to.
220
+ className="relative !size-4 shrink-0 md:!size-6"
215
221
  />
216
222
  }
217
223
  title={
218
- /* Single-line message: bold title + regular description inline,
219
- truncating as one unit, at the strip's caption scale (`text-h6`
220
- = 13/14px — the same treatment string titles get from the view).
221
- Separator is a middot (house rule: no en/em dashes in copy). */
222
- <p className="min-w-0 max-w-full text-h6 truncate mb-0">
224
+ /* Bold title + regular description inline at the mockup's body
225
+ scale (`text-h4` = DM Sans 500, 14/20 below `md`, 18/24 from
226
+ `md` up) — wrapping below `md`, truncating as one unit from
227
+ `md` up (the same responsive pair string titles get from the
228
+ view). Separator is a middot (house rule: no en/em dashes in
229
+ copy). */
230
+ <p className="min-w-0 max-w-full break-words text-h4 md:truncate mb-0">
223
231
  <span className="font-[number:var(--font-weight-semibold)]">{displayAnnouncement.title}</span>
224
232
  {displayAnnouncement.description && (
225
233
  <span className="hidden sm:inline opacity-80"> · {displayAnnouncement.description}</span>
@@ -233,19 +241,20 @@ export function AnnouncementBar({ initialAnnouncement, previewMode = false, clas
233
241
  announcement colors are data, not token surfaces). Inline
234
242
  styles win over the variant's hover classes on every state,
235
243
  so hover feedback is opacity (the bar's original treatment);
236
- nothing can render dark-on-dark. The view CSS-hides it below
237
- `md`, where the whole content row is the tap target — this
238
- Button stays the keyboard/AT path (known tradeoff).
244
+ nothing can render dark-on-dark. The view renders it inline
245
+ from `md` up and as a visible full-width row below `md`.
239
246
 
240
- Geometry + type come from the design system's size="compact"
241
- (24px caption-scale pill for slim strips — rationale documented
242
- on the variant in button.tsx). */
247
+ Geometry + type follow the mockup's "button-full" (Figma
248
+ 9418-52494 / 2862-8391): the design system's size="small"
249
+ (uppercase text-h5 label, 16px glyph) pinned to h-8 — the
250
+ mockup keeps the 32px height on EVERY breakpoint, while
251
+ "small" alone drops to 24px below `md`. */
243
252
  hasCta && displayAnnouncement.cta_text ? (
244
253
  <Button
245
254
  onClick={handleCtaClick}
246
255
  variant="outline"
247
- size="compact"
248
- className="transition-opacity hover:opacity-90"
256
+ size="small"
257
+ className="h-8 transition-opacity hover:opacity-90"
249
258
  style={{
250
259
  backgroundColor:
251
260
  displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,
@@ -257,8 +266,8 @@ export function AnnouncementBar({ initialAnnouncement, previewMode = false, clas
257
266
  displayAnnouncement.cta_show_icon && displayAnnouncement.cta_icon_name ? (
258
267
  <EntityIcon
259
268
  icon={{ name: displayAnnouncement.cta_icon_name, props: displayAnnouncement.cta_icon_props }}
260
- size={14}
261
- className="w-3.5 h-3.5"
269
+ size={16}
270
+ className="w-4 h-4"
262
271
  />
263
272
  ) : undefined
264
273
  }
@@ -268,21 +277,28 @@ export function AnnouncementBar({ initialAnnouncement, previewMode = false, clas
268
277
  ) : undefined
269
278
  }
270
279
  endAdornment={
271
- /* Dismiss - the common Button in its ghost-icon treatment
272
- (size="icon-sm": 32px target, >= the 24px WCAG 2.5.8 AA floor,
273
- 16px glyph) with the bar's quiet tint hover. Inert in
274
- previewMode. */
275
- <Button
276
- onClick={handleDismiss}
277
- variant="transparent"
278
- size="icon-sm"
279
- className={barButtonClasses}
280
- aria-label="Dismiss announcement"
281
- type="button"
282
- tabIndex={expanded ? 0 : -1}
283
- >
284
- <X strokeWidth={2} />
285
- </Button>
280
+ /* Dismiss - the common Button in its ghost-icon treatment with
281
+ the bar's quiet tint hover. Below `md` the mockup pins the
282
+ close to a 20px box with a 16px glyph so the top row stays
283
+ 20px tall and the stacked bar lands on 76px total (Figma
284
+ 2862-8391 — a deliberate exception to the 24px WCAG 2.5.8
285
+ target floor); from `md` up it grows back to the standard
286
+ icon-sm 32px target. Inert in previewMode; omitted entirely
287
+ when the host opts out via `dismissible={false}` (the bar
288
+ then only disappears by deactivating the announcement). */
289
+ dismissible ? (
290
+ <Button
291
+ onClick={handleDismiss}
292
+ variant="transparent"
293
+ size="icon-sm"
294
+ className={cn('h-5 w-5 p-0 md:h-8 md:w-8', barButtonClasses)}
295
+ aria-label="Dismiss announcement"
296
+ type="button"
297
+ tabIndex={expanded ? 0 : -1}
298
+ >
299
+ <X strokeWidth={2} />
300
+ </Button>
301
+ ) : undefined
286
302
  }
287
303
  />
288
304
  </div>
@@ -79,6 +79,7 @@ export const MobileBurgerMenu = React.memo(function MobileBurgerMenu({
79
79
  disabled={disabled}
80
80
  className={cn(
81
81
  "flex items-center gap-1 p-3 relative",
82
+ "focus:outline-none focus-visible:outline-none",
82
83
  "transition-colors duration-200",
83
84
  "bg-ods-card border border-ods-border rounded-md",
84
85
  !disabled && "hover:bg-ods-bg-hover",
@@ -3,44 +3,29 @@
3
3
  import type { CSSProperties, ReactElement, ReactNode } from 'react';
4
4
  import { cn } from '../../utils/cn';
5
5
 
6
- /**
7
- * Mirrors `screens.md` in tailwind.config.ts (800px), which is also where the
8
- * ODS responsive tokens step up (ods-responsive-tokens.css). ONE breakpoint
9
- * for the whole bar: the `md:` variants below and the `onContentClick` guard
10
- * must never disagree (they did before — the guard read a stale 768px while
11
- * the CSS switched at 800px).
12
- */
13
- const MD_QUERY = '(min-width: 800px)';
14
-
15
6
  export interface AnnouncementBarViewProps {
16
7
  /**
17
8
  * Leading slot — typically an icon, at content width before the title. Size
18
- * it in the slot: the bar's icon runs 20px below `md` and 24px from `md` up,
19
- * and no ODS icon token carries that pair (`--icon-size-icon-size` is 16/24).
9
+ * it in the slot: the bar's icon is 16px below `md` (must not exceed the
10
+ * 20px title row, Figma 9418-52387) and 24px from `md` up.
20
11
  */
21
12
  startAdornment?: ReactNode;
22
13
  /**
23
- * Bar message, always ONE line. A plain string gets the strip's text
24
- * treatment (`text-h6`, truncating); pass a ReactElement to own the markup
25
- * (e.g. bold title + inline description truncating as one unit).
14
+ * Bar message. A plain string gets the mockup's text treatment (`text-h4` =
15
+ * DM Sans 500, 14/20 below `md` and 18/24 from `md` up), wrapping below `md`
16
+ * and truncating to one line from `md` up. Pass a ReactElement to own the
17
+ * markup (e.g. bold title + inline description) — mirror that responsive
18
+ * wrap/truncate pair in your element.
26
19
  */
27
20
  title?: string | ReactElement;
28
21
  /**
29
- * Primary CTA, at content width after the title. Hidden below `md`, where
30
- * the content row becomes the tap target instead (`onContentClick`) — the
31
- * banner pattern's touch-first tradeoff.
22
+ * Primary CTA. From `md` up it sits inline at content width between the
23
+ * title and `endAdornment`; below `md` it moves to its own full-width row
24
+ * under the content (Figma 2862-8391) and the bar's height grows to fit.
32
25
  */
33
26
  actionBlock?: ReactElement;
34
- /** Trailing slot (e.g. a dismiss button). Outside the tap target, every breakpoint. */
27
+ /** Trailing slot (e.g. a dismiss button) — stays in the top/content row on every breakpoint. */
35
28
  endAdornment?: ReactElement;
36
- /**
37
- * Below `md` only: fires when the content row is tapped, standing in for the
38
- * `actionBlock` that is CSS-hidden at that width. No-op from `md` up, where
39
- * the CTA is visible and owns the interaction.
40
- */
41
- onContentClick?: () => void;
42
- /** Extra classes for the padded content row (e.g. a cursor affordance). */
43
- contentClassName?: string;
44
29
  /** Surface styling (background, text color, theme scope) is the consumer's — pass it here. */
45
30
  className?: string;
46
31
  style?: CSSProperties;
@@ -50,62 +35,53 @@ export interface AnnouncementBarViewProps {
50
35
  * Pure presentational announcement/notification bar. No data fetching,
51
36
  * storage, or navigation — consumers own state and pass content through slots.
52
37
  *
53
- * Anatomy follows the announcement-bar industry standard: ONE line of text at
54
- * 13-14px inside a 44px-tall strip (guides converge on 40-60px with a single
55
- * sentence; two stacked 18px rows blow past that), ONE compact CTA on the
56
- * right, and a trailing dismiss slot. Spacing is the ODS responsive tokens,
57
- * which step at the same 800px as `md`: `l` = 16/24px edge padding, `s` =
58
- * 8/12px gap, `m` = CTA offset, `xs` = 4/8px.
38
+ * Layout (Figma 9418-52494 desktop/tablet, 2862-8391 mobile):
39
+ * - `md` (800px) and up: ONE row inside a strip with a RESERVED 56px minimum
40
+ * height (`md:min-h-14`, the mockup's strip height) — held unconditionally,
41
+ * so toggling or removing the action never resizes the bar on
42
+ * desktop/tablet. Title truncates to one line.
43
+ * - Below `md`: the container stacks (`px` 16 / `py` 8 / gap 8). Top row =
44
+ * `startAdornment` + wrapping title + `endAdornment`; when an `actionBlock`
45
+ * is present it renders as a VISIBLE full-width button on its own second
46
+ * row and the bar's height adapts to content (76px with a single-line
47
+ * title and CTA per Figma 9418-52387: 8 + 20 + 8 + 32 + 8).
48
+ *
49
+ * The `actionBlock` element is mounted in both positions (inline `md`-up slot
50
+ * and the mobile full-width row); exactly one is `display:none` at any width,
51
+ * so keyboard/AT always reach exactly one control.
59
52
  */
60
53
  export function AnnouncementBarView({
61
54
  startAdornment,
62
55
  title,
63
56
  actionBlock,
64
57
  endAdornment,
65
- onContentClick,
66
- contentClassName,
67
58
  className,
68
59
  style,
69
60
  }: AnnouncementBarViewProps) {
70
- const handleContentClick = onContentClick
71
- ? () => {
72
- if (!window.matchMedia(MD_QUERY).matches) onContentClick();
73
- }
74
- : undefined;
75
-
76
61
  return (
77
- <div className={cn('flex w-full max-w-full min-h-11 items-center', className)} style={style}>
78
- {/* Content row — the tap target below `md`, where the CTA is hidden.
79
- Its vertical padding never drives the height; the 44px strip does. */}
80
- <div
81
- className={cn(
82
- 'flex min-w-0 flex-1 flex-row items-center gap-[var(--spacing-system-s)] py-[var(--spacing-system-xs)] pl-[var(--spacing-system-l)]',
83
- contentClassName,
84
- )}
85
- onClick={handleContentClick}
86
- >
62
+ <div
63
+ className={cn(
64
+ 'flex w-full max-w-full flex-col gap-[var(--spacing-system-s)] px-[var(--spacing-system-l)] py-[var(--spacing-system-s)] md:min-h-14 md:flex-row md:items-center md:py-0',
65
+ className,
66
+ )}
67
+ style={style}
68
+ >
69
+ {/* Content row: leading icon + title (+ inline CTA from `md`) + trailing
70
+ slot. `items-start` below `md` keeps the adornments pinned to the
71
+ first text line when the title wraps; `md:items-center` re-centers
72
+ everything in the fixed strip. */}
73
+ <div className="flex w-full min-w-0 flex-1 items-start gap-[var(--spacing-system-s)] md:w-auto md:items-center">
87
74
  {startAdornment && <div className="flex shrink-0 items-center">{startAdornment}</div>}
88
75
  {typeof title === 'string' ? (
89
- <p className="mb-0 min-w-0 max-w-full flex-1 truncate text-h6">{title}</p>
76
+ <p className="mb-0 min-w-0 max-w-full flex-1 break-words text-h4 md:truncate">{title}</p>
90
77
  ) : (
91
78
  title != null && <div className="min-w-0 max-w-full flex-1">{title}</div>
92
79
  )}
93
- {/* Below `md` the CTA is visually replaced by the row-wide tap target,
94
- but a `display:none` button is unreachable by keyboard and AT. So
95
- it is only visually hidden there (`sr-only`) and reveals itself on
96
- focus — the row stays the touch affordance while Tab still reaches
97
- a real, labelled control. `md:` restores the normal inline CTA. */}
98
- {actionBlock && (
99
- <div className="ml-[var(--spacing-system-m)] sr-only shrink-0 focus-within:not-sr-only md:not-sr-only md:flex">
100
- {actionBlock}
101
- </div>
102
- )}
80
+ {actionBlock && <div className="hidden shrink-0 md:flex">{actionBlock}</div>}
81
+ {endAdornment && <div className="flex shrink-0 items-center">{endAdornment}</div>}
103
82
  </div>
104
- {/* Trailing slot. Its own 32px hit box optically re-centers the glyph
105
- against the 16/24px left edge. */}
106
- {endAdornment && (
107
- <div className="ml-[var(--spacing-system-xs)] mr-[var(--spacing-system-m)] shrink-0">{endAdornment}</div>
108
- )}
83
+ {/* Mobile-only action row — the CTA stretched to the bar's full width. */}
84
+ {actionBlock && <div className="flex w-full md:hidden [&>*]:w-full">{actionBlock}</div>}
109
85
  </div>
110
86
  );
111
87
  }
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import React from 'react'
4
+ import { useAuthedImageSrc } from '../../hooks/use-authed-image-src'
4
5
  import { cn } from '../../utils/cn'
5
6
  import { getFirstLastInitials } from '../../utils/format'
6
7
 
@@ -13,13 +14,14 @@ export interface EntityImageProps {
13
14
  }
14
15
 
15
16
  export function EntityImage({ src, alt, fallbackText, className }: EntityImageProps) {
17
+ const resolvedSrc = useAuthedImageSrc(src)
16
18
  const [imageFailed, setImageFailed] = React.useState(false)
17
19
 
18
20
  React.useEffect(() => {
19
21
  setImageFailed(false)
20
- }, [src])
22
+ }, [resolvedSrc])
21
23
 
22
- const showFallback = imageFailed || !src
24
+ const showFallback = imageFailed || !resolvedSrc
23
25
  const initials = getFirstLastInitials(fallbackText ?? alt)
24
26
 
25
27
  if (showFallback) {
@@ -38,7 +40,7 @@ export function EntityImage({ src, alt, fallbackText, className }: EntityImagePr
38
40
 
39
41
  return (
40
42
  <img
41
- src={src ?? undefined}
43
+ src={resolvedSrc}
42
44
  alt={alt ?? ''}
43
45
  onError={() => setImageFailed(true)}
44
46
  className={cn(
@@ -5,6 +5,7 @@ import { Loader2 } from "lucide-react"
5
5
  import { ImagePlusIcon } from "../icons-v2-generated/audio-and-visual/image-plus-icon"
6
6
  import { Refresh02VrIcon } from "../icons-v2-generated/media-playback/refresh-02-vr-icon"
7
7
  import { TrashIcon } from "../icons-v2-generated/interface/trash-icon"
8
+ import { useAuthedImageSrc } from "../../hooks/use-authed-image-src"
8
9
  import { cn } from "../../utils/cn"
9
10
  import { FieldWrapper } from "./field-wrapper"
10
11
 
@@ -98,6 +99,12 @@ export function ImageUploader({
98
99
  const [validationError, setValidationError] = React.useState<string | null>(null)
99
100
 
100
101
  const interactive = !disabled && !loading
102
+ // Bearer-mode shells: swap a gateway URL for an authed blob URL (native
103
+ // <img> loads can't carry Authorization); pass-through everywhere else,
104
+ // including local blob/data preview URLs. `hasImage` stays keyed on the
105
+ // caller's `value` so the preview chrome doesn't flash to the empty
106
+ // state while the blob fetch is in flight.
107
+ const resolvedValue = useAuthedImageSrc(value)
101
108
  const hasImage = Boolean(value)
102
109
  const displayError = error || validationError || undefined
103
110
  const resolvedHint = hint === undefined ? `Max. size: ${formatSize(maxSize)}` : hint
@@ -196,18 +203,20 @@ export function ImageUploader({
196
203
  {hasImage ? (
197
204
  <>
198
205
  <div className="relative min-h-0 w-full flex-1 overflow-hidden rounded-md">
199
- <img
200
- src={value}
201
- alt={alt}
202
- className={cn(
203
- "pointer-events-none absolute inset-0 size-full",
204
- objectFit === "cover" && "object-cover",
205
- objectFit === "contain" && "object-contain",
206
- objectFit === "fill" && "object-fill",
207
- objectFit === "none" && "object-none",
208
- objectFit === "scale-down" && "object-scale-down",
209
- )}
210
- />
206
+ {resolvedValue && (
207
+ <img
208
+ src={resolvedValue}
209
+ alt={alt}
210
+ className={cn(
211
+ "pointer-events-none absolute inset-0 size-full",
212
+ objectFit === "cover" && "object-cover",
213
+ objectFit === "contain" && "object-contain",
214
+ objectFit === "fill" && "object-fill",
215
+ objectFit === "none" && "object-none",
216
+ objectFit === "scale-down" && "object-scale-down",
217
+ )}
218
+ />
219
+ )}
211
220
  </div>
212
221
 
213
222
  <div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center gap-4">
@@ -9,6 +9,7 @@ import rehypeHighlight from 'rehype-highlight';
9
9
  import rehypeRaw from 'rehype-raw';
10
10
  import { visit } from 'unist-util-visit';
11
11
  import Image from '../../embed-shims/next-image';
12
+ import { useAuthedImageSrc } from '../../hooks/use-authed-image-src';
12
13
  import { AlertCircleIcon } from '../icons-v2-generated';
13
14
  import { cn } from '../../utils/cn';
14
15
  import type { ResolveLinkResult } from '../../types/doc-source';
@@ -453,6 +454,58 @@ const MermaidDiagram: React.FC<{ chart: string }> = ({ chart }) => {
453
454
  );
454
455
  };
455
456
 
457
+ // ---------------------------------------------------------------------------
458
+ // MarkdownContentImage
459
+ // ---------------------------------------------------------------------------
460
+ // Inline content image renderer. Used by blog posts, docs, AND chat
461
+ // messages (where users may attach screenshots / photos via the +
462
+ // attachment button). A standalone component (rather than inline JSX in
463
+ // the `img` renderer) because it needs `useAuthedImageSrc`: in
464
+ // bearer-mode native shells a gateway-hosted image can't load via a
465
+ // plain `<img>` (no Authorization header on native asset loads), so the
466
+ // hook swaps in an authed blob URL; everywhere else it passes `src`
467
+ // through untouched. While the blob fetch is in flight the component
468
+ // renders nothing — same as the no-src case.
469
+ //
470
+ // Sizing rules (2025-2026 best practice — Claude.ai, ChatGPT,
471
+ // iMessage, Slack, Discord inline image patterns):
472
+ //
473
+ // - CAP max-width at 400px so inline images don't blow out the
474
+ // message column on wide panels. Click-to-expand opens a
475
+ // full-resolution modal for users who need detail.
476
+ // - CAP max-height at 400px so portrait-orientation images
477
+ // don't dominate vertical space (a 1000x3000 phone screenshot
478
+ // would otherwise push the next message off-screen).
479
+ // - Small images render at NATURAL pixel size — a 64x64
480
+ // thumbnail stays 64x64, not stretched to fill the column.
481
+ // - `object-contain` preserves aspect ratio when both dimensions
482
+ // are constrained (long landscape, tall portrait).
483
+ //
484
+ // Implementation: Next.js `<Image>` — the project's canonical
485
+ // image primitive (WebP/AVIF conversion, responsive `srcset`, lazy
486
+ // loading, async decode). `width={400} height={400}` are REQUIRED by
487
+ // Next.js `<Image>` (non-`fill` mode throws without them) but they're
488
+ // effectively a CEILING here, not the display size — the CSS overrides
489
+ // (`w-auto h-auto max-w-full max-h-[400px]`) drive the actual rendered
490
+ // size, and the inline `style={{ width: 'auto', height: 'auto' }}` is
491
+ // belt-and-suspenders against the rendered `<img>`'s HTML width/height
492
+ // attributes.
493
+ const MarkdownContentImage: React.FC<{ src: string; alt?: string }> = ({ src, alt }) => {
494
+ const resolvedSrc = useAuthedImageSrc(src);
495
+ if (!resolvedSrc) return null;
496
+ return (
497
+ <Image
498
+ src={resolvedSrc}
499
+ alt={alt ?? 'No image available'}
500
+ width={400}
501
+ height={400}
502
+ sizes="(max-width: 400px) 100vw, 400px"
503
+ className="max-w-full max-h-[400px] w-auto h-auto rounded-lg object-contain"
504
+ style={{ width: 'auto', height: 'auto' }}
505
+ />
506
+ );
507
+ };
508
+
456
509
  // ---------------------------------------------------------------------------
457
510
  // Utility: extract plain text from React children
458
511
  // ---------------------------------------------------------------------------
@@ -820,66 +873,12 @@ const SimpleMarkdownRendererImpl: React.FC<SimpleMarkdownRendererProps> = ({
820
873
  },
821
874
 
822
875
  // --- images ---
823
- // Inline content image renderer. Used by blog posts, docs, AND chat
824
- // messages (where users may attach screenshots / photos via the +
825
- // attachment button).
826
- //
827
- // Sizing rules (2025-2026 best practice — Claude.ai, ChatGPT,
828
- // iMessage, Slack, Discord inline image patterns):
829
- //
830
- // - CAP max-width at 400px so inline images don't blow out the
831
- // message column on wide panels. Click-to-expand opens a
832
- // full-resolution modal for users who need detail.
833
- // - CAP max-height at 400px so portrait-orientation images
834
- // don't dominate vertical space (a 1000x3000 phone screenshot
835
- // would otherwise push the next message off-screen).
836
- // - Small images render at NATURAL pixel size — a 64x64
837
- // thumbnail stays 64x64, not stretched to fill the column.
838
- // - `object-contain` preserves aspect ratio when both dimensions
839
- // are constrained (long landscape, tall portrait).
840
- //
841
- // Implementation: Next.js `<Image>` — the project's canonical
842
- // image primitive. Gives us:
843
- // - WebP/AVIF format conversion for modern browsers (smaller
844
- // bytes for the same visual quality).
845
- // - Responsive `srcset` via the `sizes` prop (browser picks the
846
- // right variant for the viewport).
847
- // - Automatic lazy-loading (`loading="lazy"` by default, mid-
848
- // page images skipped until they near the viewport).
849
- // - Automatic `decoding="async"` so image decode doesn't block
850
- // paint.
851
- //
852
- // `width={400} height={400}` props are REQUIRED by Next.js
853
- // `<Image>` (non-`fill` mode throws without them) but they're
854
- // effectively a CEILING here, not the display size — the CSS
855
- // overrides (`w-auto h-auto max-w-full max-h-[400px]`) drive
856
- // the actual rendered size. The inline `style={{ width: 'auto',
857
- // height: 'auto' }}` is belt-and-suspenders: Next.js Image sets
858
- // matching HTML `width`/`height` attributes on the rendered
859
- // `<img>` and inline style wins over both HTML attributes AND
860
- // utility classes regardless of CSS-specificity surprises.
861
- //
862
- // Layout reservation trade-off: until image bytes arrive, the
863
- // browser may reserve a placeholder box up to 400x400 (the props'
864
- // intrinsic-ratio hint). Once loaded, the box collapses to the
865
- // natural size if smaller. This is the standard Next.js Image
866
- // behavior across the codebase — accepted for the optimizer +
867
- // responsive-srcset benefits. Chat attachments hosted in side
868
- // panels see this only on first render of a fresh attachment
869
- // (cached re-renders pop in without a perceptible shift).
876
+ // Delegates to MarkdownContentImage (module scope, above) — sizing
877
+ // rules, Next.js <Image> rationale, and the bearer-mode authed-src
878
+ // handling are documented there.
870
879
  img: ({ src, alt }: any) => {
871
880
  if (!src || typeof src !== 'string' || src.trim() === '') return null;
872
- return (
873
- <Image
874
- src={src}
875
- alt={alt ?? 'No image available'}
876
- width={400}
877
- height={400}
878
- sizes="(max-width: 400px) 100vw, 400px"
879
- className="max-w-full max-h-[400px] w-auto h-auto rounded-lg object-contain"
880
- style={{ width: 'auto', height: 'auto' }}
881
- />
882
- );
881
+ return <MarkdownContentImage src={src} alt={alt} />;
883
882
  },
884
883
 
885
884
  // --- lists ---
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import Image from "../../embed-shims/next-image";
5
+ import { useAuthedImageSrc } from "../../hooks/use-authed-image-src";
5
6
  import { cn } from "../../utils/cn";
6
7
  import { getFirstLastInitials } from "../../utils/format";
7
8
 
@@ -22,6 +23,7 @@ interface SquareAvatarProps extends React.HTMLAttributes<HTMLDivElement> {
22
23
 
23
24
  const SquareAvatar = React.memo(React.forwardRef<HTMLDivElement, SquareAvatarProps>(
24
25
  ({ className, src, alt, size = 'md', sizePx, fallback, variant = 'square', initialsClassName, style, ...props }, ref) => {
26
+ const resolvedSrc = useAuthedImageSrc(src)
25
27
  const sizeClasses = {
26
28
  sm: 'h-8 w-8',
27
29
  md: 'h-10 w-10',
@@ -66,14 +68,14 @@ const SquareAvatar = React.memo(React.forwardRef<HTMLDivElement, SquareAvatarPro
66
68
  // keeps the later class).
67
69
  'flex items-center justify-center text-xs font-medium text-[color:var(--ods-avatar-initials,var(--color-text-primary))]',
68
70
  initialsClassName,
69
- src && 'hidden'
71
+ resolvedSrc && 'hidden'
70
72
  )}>
71
73
  {getFirstLastInitials(fallback || alt) || '?'}
72
74
  </div>
73
- {src && (
75
+ {resolvedSrc && (
74
76
  <Image
75
77
  className="absolute -inset-px h-[calc(100%+2px)] w-[calc(100%+2px)] max-w-none object-cover"
76
- src={src}
78
+ src={resolvedSrc}
77
79
  alt={alt || ''}
78
80
  width={sizePx ?? sizePxBySize[size]}
79
81
  height={sizePx ?? sizePxBySize[size]}
@@ -8,6 +8,7 @@ export * from './use-copy-to-clipboard'
8
8
  // Image authentication hooks
9
9
  export * from './use-batch-images'
10
10
  export * from './use-authenticated-image'
11
+ export * from './use-authed-image-src'
11
12
 
12
13
  // URL State Management hooks
13
14
  export * from './state'