@flamingo-stack/openframe-frontend-core 0.0.854 → 0.0.855

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 (133) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{case-study-card-WP5Z234Q.cjs → case-study-card-6VAMNHLU.cjs} +8 -8
  3. package/dist/{chunk-FBANHIAG.cjs → chunk-2JCA7BRK.cjs} +19 -19
  4. package/dist/{chunk-7MOBD44H.cjs → chunk-2MCPK6AX.cjs} +28 -28
  5. package/dist/{chunk-GVHCQRUE.cjs → chunk-2P72KAG5.cjs} +23 -23
  6. package/dist/{chunk-YY3WP2YQ.cjs → chunk-3GJ75OUL.cjs} +183 -183
  7. package/dist/{chunk-FJXSKMQJ.cjs → chunk-4H3J57AN.cjs} +25 -25
  8. package/dist/{chunk-6GWRPW6B.cjs → chunk-5NK3FUHY.cjs} +4 -4
  9. package/dist/{chunk-DLTPSP5F.cjs → chunk-5TYG5JNB.cjs} +2 -2
  10. package/dist/{chunk-63ZV3UGG.cjs → chunk-65NMKAPQ.cjs} +12 -12
  11. package/dist/{chunk-IGVAPIEW.cjs → chunk-6C4OR4MZ.cjs} +2 -2
  12. package/dist/{chunk-GG466XFQ.cjs → chunk-6DMH4GXN.cjs} +30 -5
  13. package/dist/{chunk-S5P6CF5R.cjs → chunk-6VVQJU76.cjs} +54 -54
  14. package/dist/{chunk-2IJMHBOK.cjs → chunk-77GX573I.cjs} +9 -9
  15. package/dist/{chunk-D2XGRWPW.cjs → chunk-AOOHJLXS.cjs} +90 -90
  16. package/dist/{chunk-O3EKOXFZ.cjs → chunk-AYMY2VNB.cjs} +37 -43
  17. package/dist/{chunk-RMI6O3AE.cjs → chunk-BSPIGZCC.cjs} +36 -36
  18. package/dist/{chunk-JKBNK5U6.cjs → chunk-D6GEMVHD.cjs} +3 -3
  19. package/dist/{chunk-7J5NSWJG.cjs → chunk-DPNFI5XM.cjs} +100 -100
  20. package/dist/{chunk-TMPLHUUU.cjs → chunk-DTZ6KASI.cjs} +6 -6
  21. package/dist/{chunk-SSKMGH5O.cjs → chunk-DXVGGWLG.cjs} +10 -10
  22. package/dist/{chunk-5TXE7BXT.cjs → chunk-EOUEUYCQ.cjs} +54 -54
  23. package/dist/{chunk-EIHMPQRQ.cjs → chunk-ESGE7CUX.cjs} +14 -14
  24. package/dist/{chunk-ETZFBB7O.cjs → chunk-EXQSIX2I.cjs} +4 -4
  25. package/dist/{chunk-3WXHPD6N.cjs → chunk-F33EAPD2.cjs} +124 -124
  26. package/dist/{chunk-4SSJ3T3N.cjs → chunk-GK577ZLJ.cjs} +3 -13
  27. package/dist/{chunk-BAA3YCLT.cjs → chunk-GZYRDLJW.cjs} +103 -103
  28. package/dist/{chunk-VMAOZA2V.cjs → chunk-HML3JUXF.cjs} +324 -322
  29. package/dist/{chunk-4O5N5CGK.cjs → chunk-J3CFRGZ2.cjs} +17 -17
  30. package/dist/{chunk-LQDAB5V5.cjs → chunk-KZAQPRTA.cjs} +9 -11
  31. package/dist/{chunk-6DCUGIGU.cjs → chunk-LSLRJJFP.cjs} +2 -2
  32. package/dist/{chunk-ETQNMR2K.cjs → chunk-MINFN3IG.cjs} +8 -8
  33. package/dist/{chunk-ONYANAMA.cjs → chunk-MX52LZ6M.cjs} +6 -6
  34. package/dist/{chunk-OH5TXFRR.cjs → chunk-O4CPT3RK.cjs} +7 -7
  35. package/dist/{chunk-ZDPPNHA2.cjs → chunk-OTHQ2KQW.cjs} +3 -3
  36. package/dist/{chunk-5HMENXSR.cjs → chunk-PCQ2HKU3.cjs} +5 -5
  37. package/dist/{chunk-6XU3RCK3.cjs → chunk-QPHXYE5P.cjs} +27 -27
  38. package/dist/{chunk-UPQ4MAR7.cjs → chunk-QZXCUY36.cjs} +53 -53
  39. package/dist/{chunk-5OENPMRU.cjs → chunk-RIJU4HZ2.cjs} +51 -55
  40. package/dist/{chunk-KKFDRCQF.cjs → chunk-RY7INTYU.cjs} +14 -1
  41. package/dist/{chunk-GI3MMBP3.cjs → chunk-SQ7YHWEP.cjs} +228 -15
  42. package/dist/{chunk-ACPOJE32.cjs → chunk-STV6VJ3U.cjs} +21 -21
  43. package/dist/{chunk-QD727QFI.cjs → chunk-T54I5ZZY.cjs} +7 -7
  44. package/dist/{chunk-4LTLEUY5.cjs → chunk-TW5Y4D5I.cjs} +7 -7
  45. package/dist/{chunk-FX35L4Y6.cjs → chunk-XCQ5I27Z.cjs} +3 -3
  46. package/dist/{chunk-AZEO7HBB.cjs → chunk-Y44EF4JW.cjs} +2 -2
  47. package/dist/{chunk-KA3L6D5N.cjs → chunk-YSXVRTGT.cjs} +9 -12
  48. package/dist/{chunk-PRJZMZ3X.cjs → chunk-ZUGJLCYP.cjs} +678 -678
  49. package/dist/{chunk-H3GBPYPS.cjs → chunk-ZYAVIRQV.cjs} +25 -25
  50. package/dist/{cloud-tenants-5RNK4ZAT.cjs → cloud-tenants-2XFIDDGU.cjs} +28 -29
  51. package/dist/components/case-studies/index.cjs +44 -45
  52. package/dist/components/chat/chat-message-row.d.ts +1 -1
  53. package/dist/components/chat/chat-message-row.d.ts.map +1 -1
  54. package/dist/components/chat/chat-message-row.js +4 -4
  55. package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts +7 -13
  56. package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts.map +1 -1
  57. package/dist/components/chat/entity-cards/use-cover-image-fallback.js +2 -8
  58. package/dist/components/chat/hooks/use-chat-attachment-image-gallery.d.ts.map +1 -1
  59. package/dist/components/chat/hooks/use-chat-attachment-image-gallery.js +8 -4
  60. package/dist/components/chat/index.cjs +22 -23
  61. package/dist/components/contact/index.cjs +29 -30
  62. package/dist/components/docs/index.cjs +32 -33
  63. package/dist/components/embeds/index.cjs +29 -30
  64. package/dist/components/faq/index.cjs +30 -31
  65. package/dist/components/features/index.cjs +28 -29
  66. package/dist/components/help-center-pages/index.cjs +134 -135
  67. package/dist/components/index.cjs +112 -113
  68. package/dist/components/meeting-scheduler/index.cjs +102 -103
  69. package/dist/components/navigation/index.cjs +31 -32
  70. package/dist/components/onboarding-guides/index.cjs +31 -32
  71. package/dist/components/product-demo/index.cjs +39 -40
  72. package/dist/components/related-content/index.cjs +16 -17
  73. package/dist/components/tickets/index.cjs +35 -36
  74. package/dist/components/ui/entity-image.d.ts +1 -2
  75. package/dist/components/ui/entity-image.d.ts.map +1 -1
  76. package/dist/components/ui/entity-image.js +4 -8
  77. package/dist/components/ui/index.cjs +28 -29
  78. package/dist/components/ui/markdown/base-components.d.ts +0 -13
  79. package/dist/components/ui/markdown/base-components.d.ts.map +1 -1
  80. package/dist/components/ui/markdown/base-components.js +7 -7
  81. package/dist/components/ui/square-avatar.d.ts.map +1 -1
  82. package/dist/components/ui/square-avatar.js +3 -8
  83. package/dist/{devices-44O5PZOW.cjs → devices-O3DYGIZB.cjs} +29 -30
  84. package/dist/embed-shims/index.cjs +3 -2
  85. package/dist/embed-shims/next-image.cjs +3 -2
  86. package/dist/embed-shims/next-image.d.ts +2 -0
  87. package/dist/embed-shims/next-image.d.ts.map +1 -1
  88. package/dist/embed-shims/next-image.js +14 -4
  89. package/dist/{embeddable-chat-W3CLMGC3.cjs → embeddable-chat-QD7NFD2H.cjs} +100 -101
  90. package/dist/hooks/index.cjs +23 -16
  91. package/dist/hooks/index.d.ts +1 -0
  92. package/dist/hooks/index.d.ts.map +1 -1
  93. package/dist/hooks/index.js +1 -0
  94. package/dist/hooks/use-authed-asset-src.d.ts +77 -2
  95. package/dist/hooks/use-authed-asset-src.d.ts.map +1 -1
  96. package/dist/hooks/use-authed-asset-src.js +185 -73
  97. package/dist/hooks/use-authed-image-src.d.ts +11 -4
  98. package/dist/hooks/use-authed-image-src.d.ts.map +1 -1
  99. package/dist/hooks/use-authed-image-src.js +6 -2
  100. package/dist/hooks/use-image-fallback.d.ts +16 -0
  101. package/dist/hooks/use-image-fallback.d.ts.map +1 -0
  102. package/dist/hooks/use-image-fallback.js +14 -0
  103. package/dist/index.cjs +40 -36
  104. package/dist/{knowledge-UUU3WVO3.cjs → knowledge-K7AFNZNO.cjs} +29 -30
  105. package/dist/{logs-DFHNSODA.cjs → logs-MZ3BR2FR.cjs} +35 -36
  106. package/dist/{policies-RGFUGDBP.cjs → policies-XVKQ2W7S.cjs} +32 -33
  107. package/dist/{remote-session-U3H77HKR.cjs → remote-session-VOZJXDM4.cjs} +13 -14
  108. package/dist/{scripts-KOVO5DHW.cjs → scripts-IBPMBS6G.cjs} +35 -36
  109. package/dist/{software-update-4EKHVV5W.cjs → software-update-PXTANMAO.cjs} +32 -33
  110. package/dist/{tickets-board-YIFOVMDX.cjs → tickets-board-POYZS7BW.cjs} +16 -17
  111. package/dist/utils/app-origins.d.ts +3 -6
  112. package/dist/utils/app-origins.d.ts.map +1 -1
  113. package/dist/utils/app-origins.js +8 -1
  114. package/dist/utils/embed-authed-fetch.d.ts +46 -26
  115. package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
  116. package/dist/utils/embed-authed-fetch.js +24 -8
  117. package/dist/utils/index.cjs +4 -1
  118. package/dist/{video-file-player-XMOB234V.cjs → video-file-player-RIOT24NT.cjs} +3 -4
  119. package/package.json +1 -1
  120. package/src/components/chat/chat-message-row.tsx +8 -7
  121. package/src/components/chat/entity-cards/use-cover-image-fallback.ts +8 -28
  122. package/src/components/chat/hooks/use-chat-attachment-image-gallery.tsx +19 -8
  123. package/src/components/ui/entity-image.tsx +4 -12
  124. package/src/components/ui/markdown/base-components.tsx +15 -20
  125. package/src/components/ui/square-avatar.tsx +5 -8
  126. package/src/embed-shims/next-image.tsx +34 -3
  127. package/src/hooks/index.ts +1 -0
  128. package/src/hooks/use-authed-asset-src.ts +323 -107
  129. package/src/hooks/use-authed-image-src.ts +15 -5
  130. package/src/hooks/use-image-fallback.ts +31 -0
  131. package/src/utils/app-origins.ts +12 -7
  132. package/src/utils/embed-authed-fetch.ts +71 -32
  133. package/dist/chunk-YSDB6HGQ.cjs +0 -102
@@ -11,8 +11,9 @@
11
11
  * while closed.
12
12
  *
13
13
  * Behavior on click:
14
- * 1. Filter for `<img>` whose `src` starts with the runtime's
15
- * `attachmentViewUrlPrefix` (so non-chat images don't intercept).
14
+ * 1. Filter for `<img>` whose address (`assetSrcOf`) starts with the
15
+ * runtime's `attachmentViewUrlPrefix` (so non-chat images don't
16
+ * intercept).
16
17
  * 2. Walk up to the nearest `[data-message-role]` ancestor — the
17
18
  * boundary the chat-shell wraps each message bubble with.
18
19
  * 3. Collect ALL chat-attachment `<img>`s inside that boundary in
@@ -51,32 +52,32 @@ export function useChatAttachmentImageGallery() {
51
52
  const onClick = (ev: Event) => {
52
53
  const target = ev.target as HTMLElement | null;
53
54
  if (!target) return;
54
- // Match an <img> whose `src` starts with the proxy URL prefix.
55
+ // Match an <img> whose address starts with the proxy URL prefix.
55
56
  const img = target.closest('img');
56
- if (!img || !img.src.startsWith(absolutize(viewUrlPrefix))) return;
57
+ if (!img || !assetSrcOf(img).startsWith(absolutize(viewUrlPrefix))) return;
57
58
 
58
59
  // Walk up to the message-bubble boundary.
59
60
  const bubble = img.closest('[data-message-role]');
60
61
  if (!bubble) {
61
62
  // Fallback: open the modal with just the clicked image.
62
63
  ev.preventDefault();
63
- setState({ isOpen: true, images: [img.src], initialIndex: 0 });
64
+ setState({ isOpen: true, images: [assetSrcOf(img)], initialIndex: 0 });
64
65
  return;
65
66
  }
66
67
 
67
68
  // Collect all chat-attachment images inside this bubble in DOM
68
- // order. The `closest` lookup matched img.src.startsWith already,
69
+ // order. The `closest` lookup matched the clicked one's address already,
69
70
  // but we re-check below because `querySelectorAll` returns ALL
70
71
  // <img>s in the bubble (avatar images etc.).
71
72
  const allImgs = Array.from(bubble.querySelectorAll('img'));
72
- const matching = allImgs.filter(el => el.src.startsWith(absolutize(viewUrlPrefix)));
73
+ const matching = allImgs.filter(el => assetSrcOf(el).startsWith(absolutize(viewUrlPrefix)));
73
74
  if (matching.length === 0) return;
74
75
 
75
76
  const idx = Math.max(0, matching.indexOf(img));
76
77
  ev.preventDefault();
77
78
  setState({
78
79
  isOpen: true,
79
- images: matching.map(el => el.src),
80
+ images: matching.map(assetSrcOf),
80
81
  initialIndex: idx,
81
82
  });
82
83
  };
@@ -112,3 +113,13 @@ function absolutize(prefix: string): string {
112
113
  if (typeof window === 'undefined') return prefix;
113
114
  return `${window.location.origin}${prefix.startsWith('/') ? '' : '/'}${prefix}`;
114
115
  }
116
+
117
+ /**
118
+ * The address an `<img>` was asked to draw. Where auth rides in a header, the boxed `Image` draws a blob fetched
119
+ * with it and keeps the address in `data-asset-src`: `.src` would be `blob:…`, matching no prefix, and the gallery
120
+ * would never open in the native shells. Resolved against the page, as `.src` is: on dev-ticket web it is relative.
121
+ */
122
+ function assetSrcOf(img: HTMLImageElement): string {
123
+ const declared = img.dataset.assetSrc;
124
+ return declared ? new URL(declared, window.location.href).href : img.src;
125
+ }
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
4
3
  import Image from '../../embed-shims/next-image';
5
4
  import { useAuthedImageSrc } from '../../hooks/use-authed-image-src';
5
+ import { useImageFallback } from '../../hooks/use-image-fallback';
6
6
  import { TILE_SIZE } from '../../styles/component-sizes';
7
7
  import { IMAGE_FIXED_PX } from '../../styles/image-sizes';
8
8
  import { cn } from '../../utils/cn';
@@ -37,20 +37,12 @@ export interface EntityImageProps {
37
37
  export function EntityImage({ src, alt, fallbackText, sizeClassName, className }: EntityImageProps) {
38
38
  // In a loading scope the image is its skeleton, and it is not fetched.
39
39
  const held = useLoadingScope();
40
- const authedSrc = useAuthedImageSrc(held ? undefined : src);
41
- const resolvedSrc = authedSrc;
42
- // Which src failed, rather than a bare "failed" flag reset from an effect on
43
- // every src change. The reset is then implicit — a new src simply is not the
44
- // one that failed — so the fallback initials never survive into the render
45
- // that swapped the image, and a new src costs no second render pass.
46
- const [failedSrc, setFailedSrc] = React.useState<string | null>(null);
47
- const imageFailed = resolvedSrc !== undefined && failedSrc === resolvedSrc;
40
+ const { src: resolvedSrc, onError } = useImageFallback(useAuthedImageSrc(held ? undefined : src));
48
41
 
49
42
  if (held) return <EntityImageSkeleton sizeClassName={sizeClassName} className={className} />;
50
- const showFallback = imageFailed || !resolvedSrc;
51
43
  const initials = personInitials(fallbackText ?? alt);
52
44
 
53
- if (showFallback) {
45
+ if (!resolvedSrc) {
54
46
  return (
55
47
  <div
56
48
  aria-label={alt}
@@ -70,7 +62,7 @@ export function EntityImage({ src, alt, fallbackText, sizeClassName, className }
70
62
  <Image
71
63
  src={resolvedSrc}
72
64
  alt={alt ?? ''}
73
- onError={() => setFailedSrc(resolvedSrc)}
65
+ onError={onError}
74
66
  className={cn(
75
67
  'shrink-0 border border-ods-border object-contain',
76
68
  ENTITY_IMAGE_RADIUS_CLASS,
@@ -54,19 +54,6 @@ export interface StandardLeafRenderers {
54
54
  * the rich composition's `img` / `video` overrides so the empty-`![]()`
55
55
  * guard has exactly one definition.
56
56
  */
57
- /**
58
- * Inline content image (blog / docs / chat attachments). A standalone
59
- * component — NOT inline JSX in the `img` renderer — because it must call
60
- * `useAuthedImageSrc`: in bearer-mode native shells (`capacitor://`,
61
- * `tauri://`) a gateway-hosted image can't load through a plain `<img>` (no
62
- * Authorization header on native asset loads), so the hook swaps in an authed
63
- * blob URL; everywhere else it returns `src` untouched. While the blob fetch
64
- * is in flight `resolvedSrc` is null and the component renders nothing — same
65
- * as the no-src case. Ported from #1548 into the unified engine (the original
66
- * lived in the now-deleted `ui/simple-markdown-renderer.tsx`). Sizing matches
67
- * the engine's cap: 400x400 intrinsic, `w-auto h-auto max-h-[400px]` drives
68
- * the actual rendered box; click-to-expand surfaces provide full resolution.
69
- */
70
57
  /**
71
58
  * The page title, a section heading and a paragraph of rendered markdown (before the text size): read by the
72
59
  * renderer and by every skeleton that stands in for a markdown body.
@@ -76,16 +63,23 @@ export const MARKDOWN_H2_CLASS =
76
63
  'mb-4 mt-8 border-b border-ods-border pb-2 font-sans font-semibold text-ods-text-primary';
77
64
  export const MARKDOWN_PARAGRAPH_CLASS = 'mb-4 leading-relaxed text-ods-text-primary first:mt-0 last:mb-0';
78
65
 
66
+ /**
67
+ * Inline content image (blog / docs / chat attachments). The boxed `Image` resolves and authenticates it
68
+ * (`useAuthedAsset`); this component reads the same store only to draw nothing while there is nothing to draw (the
69
+ * authed fetch in flight or failed, as with no src), where `Image` would draw its box or the failing URL. Ported from
70
+ * #1548 into the unified engine (the original lived in the now-deleted `ui/simple-markdown-renderer.tsx`). Sizing
71
+ * matches the engine's cap: 400x400 intrinsic, `w-auto h-auto max-h-[400px]` drives the actual rendered box;
72
+ * click-to-expand surfaces provide full resolution.
73
+ */
79
74
  const MarkdownContentImage: React.FC<{ src: string; alt?: string }> = ({ src, alt }) => {
80
75
  // In a loading scope the image is a box and nothing is fetched: the authed fetch is skipped, and the `Image` shim
81
76
  // draws its skeleton box (`HeldImage`) without reading `src`. `src` is passed only so the box is drawn at all.
82
77
  const inScope = useLoadingScope();
83
- const authedSrc = useAuthedImageSrc(inScope ? undefined : src);
84
- const resolvedSrc = inScope ? src : authedSrc;
85
- if (!resolvedSrc) return null;
78
+ const drawable = useAuthedImageSrc(inScope ? undefined : src);
79
+ if (!inScope && !drawable) return null;
86
80
  return (
87
81
  <Image
88
- src={resolvedSrc}
82
+ src={src}
89
83
  alt={alt ?? 'No image available'}
90
84
  width={400}
91
85
  height={400}
@@ -363,9 +357,10 @@ export function buildBaseComponents({
363
357
 
364
358
  // --- images ---
365
359
  // Inline content image renderer (blog, docs, chat attachments) —
366
- // delegates to `MarkdownContentImage` (above), which handles bearer-mode
367
- // authed loading for native shells and the 400x400 cap with click-to-
368
- // expand for full resolution.
360
+ // delegates to `MarkdownContentImage` (above), which draws nothing until
361
+ // there is a picture to draw and applies the 400x400 cap with click-to-
362
+ // expand for full resolution; the boxed `Image` it renders resolves and
363
+ // authenticates it.
369
364
  //
370
365
  // A third-party `src` never reaches here unless the composition set
371
366
  // `allowRemoteMedia`: `rehypeStripUnsafe` moves it to `data-blocked-src`,
@@ -3,6 +3,7 @@
3
3
  import { type HTMLAttributes, memo, forwardRef } from 'react';
4
4
  import Image from '../../embed-shims/next-image';
5
5
  import { useAuthedImageSrc } from '../../hooks/use-authed-image-src';
6
+ import { useImageFallback } from '../../hooks/use-image-fallback';
6
7
  import { AVATAR_SIZE, AVATAR_SIZE_PX, type AvatarSize } from '../../styles/component-sizes';
7
8
  import { cn } from '../../utils/cn';
8
9
  import { personInitials } from '../../utils/format';
@@ -66,7 +67,9 @@ const SquareAvatar = memo(
66
67
  // is not fetched.
67
68
  const inScope = useLoadingScope();
68
69
  const held = inScope || !!loading;
69
- const authedSrc = useAuthedImageSrc(held ? undefined : src);
70
+ // The initials fallback used to be written into the DOM from `onError`, and the hidden picture stayed hidden
71
+ // for the avatar's lifetime, even once a src that loads arrived.
72
+ const { src: resolvedSrc, onError } = useImageFallback(useAuthedImageSrc(held ? undefined : src));
70
73
  if (held) {
71
74
  return (
72
75
  <SquareAvatarSkeleton
@@ -78,7 +81,6 @@ const SquareAvatar = memo(
78
81
  />
79
82
  );
80
83
  }
81
- const resolvedSrc = authedSrc;
82
84
  const sizeClasses = AVATAR_SIZE;
83
85
  const sizePxBySize = AVATAR_SIZE_PX;
84
86
 
@@ -137,12 +139,7 @@ const SquareAvatar = memo(
137
139
  alt={alt || ''}
138
140
  width={sizePx ?? sizePxBySize[size]}
139
141
  height={sizePx ?? sizePxBySize[size]}
140
- onError={e => {
141
- const img = e.currentTarget;
142
- img.style.display = 'none';
143
- const el = img.previousElementSibling as HTMLElement;
144
- if (el) el.classList.remove('hidden');
145
- }}
142
+ onError={onError}
146
143
  />
147
144
  )}
148
145
  </div>
@@ -24,6 +24,8 @@ import { forwardRef, type ComponentType, type CSSProperties, type ImgHTMLAttribu
24
24
 
25
25
  import { UnifiedSkeleton } from '../components/loading/unified-skeleton';
26
26
  import { useLoadingScope } from '../components/ui/loading-scope';
27
+ import type { AuthedAsset } from '../hooks/use-authed-asset-src';
28
+ import { useAuthedImage } from '../hooks/use-authed-image-src';
27
29
  import { cn } from '../utils/cn';
28
30
  import { fetchPriorityProp } from '../utils/fetch-priority';
29
31
 
@@ -49,6 +51,8 @@ type ImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'placeholder
49
51
  optimizedWidth?: number;
50
52
  onLoadingComplete?: (img: HTMLImageElement) => void;
51
53
  loader?: unknown;
54
+ /** The picture's address when `src` is the blob fetched from it (set by the shim; see `drawnAsset`). */
55
+ 'data-asset-src'?: string;
52
56
  };
53
57
 
54
58
  /** What the shim renders once a host has registered `next/image`. */
@@ -122,12 +126,22 @@ function HeldImage({ className, style, width, height, fill }: ImageProps) {
122
126
  const Image = forwardRef<HTMLImageElement, ImageProps>(function NextImageShim({ optimizedWidth, ...given }, ref) {
123
127
  // In a loading scope the picture is a box of its own size, and it is not fetched.
124
128
  const held = useLoadingScope();
125
- if (held) return <HeldImage {...given} />;
126
129
  // A picture asked for at a width is the host resizer's URL, drawn as it comes.
127
- const props: ImageProps =
130
+ const sized: ImageProps =
128
131
  optimizedWidth && resizer && typeof given.src === 'string'
129
132
  ? { ...given, src: resizer(given.src, optimizedWidth), unoptimized: true }
130
133
  : given;
134
+ // Every picture is drawn from where it lives and with the credential it needs: the host's `resolveAssetUrl`
135
+ // sends a backend path to its gateway, and a bearer-auth host's picture is fetched with the bearer and drawn
136
+ // from a blob (`useAuthedAsset`). Cookie-auth web and public pictures pass through unchanged.
137
+ const asset = useAuthedImage(!held && typeof sized.src === 'string' ? sized.src : undefined);
138
+ // Waiting for the credential or for the fetch: the picture's box, with nothing requested.
139
+ if (held || asset.status === 'pending') return <HeldImage {...given} />;
140
+ const props: ImageProps = typeof sized.src !== 'string' ? sized : drawnAsset(sized, asset);
141
+ // The element that failed is not the one a retry draws into: a caller that hid it in `onError` would keep it
142
+ // hidden. Only that transition remounts: a native in-place src swap keeps the old picture until the new one loads
143
+ // (a bearer swap draws the box while the new blob is fetched).
144
+ const key = asset.status === 'failed' ? 'failed' : undefined;
131
145
  // `priority` means "this is the picture the page is waiting for", so its
132
146
  // request is ranked high on EVERY host. Stated here, once: since Next 16
133
147
  // `priority` only preloads and no longer ranks the request, and the plain
@@ -139,7 +153,7 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function NextImageShim({
139
153
  // (priority, placeholder, sizes, loader…) reaches the real component intact.
140
154
  if (impl) {
141
155
  const Real = impl;
142
- return <Real ref={ref} {...props} fetchPriority={fetchPriority} />;
156
+ return <Real key={key} ref={ref} {...props} fetchPriority={fetchPriority} />;
143
157
  }
144
158
 
145
159
  // Fallback path — plain <img>. Drops Next-only props and reduces
@@ -183,6 +197,7 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function NextImageShim({
183
197
  : style;
184
198
  return (
185
199
  <img
200
+ key={key}
186
201
  ref={ref}
187
202
  src={srcStr}
188
203
  alt={alt ?? ''}
@@ -197,4 +212,20 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function NextImageShim({
197
212
  );
198
213
  });
199
214
 
215
+ /**
216
+ * The props drawn for a string `src` once `useAuthedAsset` answered:
217
+ *
218
+ * - `native` / `ready`: its `src` (the URL, or the fetched blob). A blob goes to no optimizer.
219
+ * - `failed`: the URL itself, so the element fails the way a plain `<img>` would and the caller's `onError`
220
+ * fallback runs. It used to stay blank, with nothing to tell the caller.
221
+ *
222
+ * `data-asset-src` keeps the URL on the element when the drawn `src` is a blob, for code that reads a picture's
223
+ * address off the DOM (the chat's click-to-expand gallery).
224
+ */
225
+ function drawnAsset(props: ImageProps, asset: AuthedAsset): ImageProps {
226
+ const src = asset.status === 'failed' ? asset.url : asset.src;
227
+ if (asset.status !== 'ready') return { ...props, src };
228
+ return { ...props, src, unoptimized: true, 'data-asset-src': asset.url };
229
+ }
230
+
200
231
  export default Image;
@@ -12,6 +12,7 @@ export * from './use-batch-images';
12
12
  export * from './use-authenticated-image';
13
13
  export * from './use-authed-asset-src';
14
14
  export * from './use-authed-image-src';
15
+ export * from './use-image-fallback';
15
16
 
16
17
  // URL State Management hooks
17
18
  export * from './state';