@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.
- package/dist/{chunk-4BUX2F5P.cjs → chunk-2R5NM3FC.cjs} +40 -40
- package/dist/{chunk-4BUX2F5P.cjs.map → chunk-2R5NM3FC.cjs.map} +1 -1
- package/dist/{chunk-BY6TZ6KA.js → chunk-3NNCBZEM.js} +6 -6
- package/dist/{chunk-WD7SE532.cjs → chunk-464NJS2W.cjs} +73 -73
- package/dist/{chunk-WD7SE532.cjs.map → chunk-464NJS2W.cjs.map} +1 -1
- package/dist/{chunk-KZZCBQDU.cjs → chunk-5TQQ3CPE.cjs} +29 -29
- package/dist/{chunk-KZZCBQDU.cjs.map → chunk-5TQQ3CPE.cjs.map} +1 -1
- package/dist/{chunk-ONK4D4LG.cjs → chunk-6KD6KVAI.cjs} +5 -5
- package/dist/{chunk-ONK4D4LG.cjs.map → chunk-6KD6KVAI.cjs.map} +1 -1
- package/dist/{chunk-73CH4CZL.cjs → chunk-7VBGYCXG.cjs} +7 -7
- package/dist/{chunk-73CH4CZL.cjs.map → chunk-7VBGYCXG.cjs.map} +1 -1
- package/dist/{chunk-FPOHUGEK.js → chunk-AGX565ZT.js} +2 -2
- package/dist/{chunk-RIEAXUJX.cjs → chunk-BOPLMNSN.cjs} +135 -174
- package/dist/chunk-BOPLMNSN.cjs.map +1 -0
- package/dist/{chunk-PJYGEXWM.js → chunk-FBUIZTAI.js} +2 -2
- package/dist/{chunk-HQL5GJ35.cjs → chunk-FM5KWLKU.cjs} +3 -3
- package/dist/{chunk-HQL5GJ35.cjs.map → chunk-FM5KWLKU.cjs.map} +1 -1
- package/dist/{chunk-YTLINJRR.cjs → chunk-GELW7JOQ.cjs} +32 -32
- package/dist/{chunk-YTLINJRR.cjs.map → chunk-GELW7JOQ.cjs.map} +1 -1
- package/dist/{chunk-TVMARS74.js → chunk-IIOHHNMI.js} +54 -93
- package/dist/chunk-IIOHHNMI.js.map +1 -0
- package/dist/{chunk-XNJ2LZH2.js → chunk-K52S6LZ5.js} +3 -3
- package/dist/{chunk-JO7XJQ6G.cjs → chunk-KPVKZ5ML.cjs} +15 -15
- package/dist/{chunk-JO7XJQ6G.cjs.map → chunk-KPVKZ5ML.cjs.map} +1 -1
- package/dist/{chunk-DWQ4L3IB.cjs → chunk-KXKTXAGN.cjs} +40 -40
- package/dist/{chunk-DWQ4L3IB.cjs.map → chunk-KXKTXAGN.cjs.map} +1 -1
- package/dist/{chunk-VC4FOIY7.cjs → chunk-LFXUFFDZ.cjs} +73 -73
- package/dist/{chunk-VC4FOIY7.cjs.map → chunk-LFXUFFDZ.cjs.map} +1 -1
- package/dist/{chunk-ZPUT2BB4.js → chunk-OKRUL7YV.js} +4 -4
- package/dist/{chunk-U5FNZ2TL.cjs → chunk-PFD2VRZI.cjs} +18 -18
- package/dist/{chunk-U5FNZ2TL.cjs.map → chunk-PFD2VRZI.cjs.map} +1 -1
- package/dist/{chunk-W6RDDT3S.js → chunk-PTFSF3UP.js} +6 -6
- package/dist/{chunk-74ZFCTRZ.js → chunk-R4OGP4OV.js} +3 -3
- package/dist/{chunk-7D5KOJMF.js → chunk-RIPWRVA3.js} +5 -5
- package/dist/{chunk-EK2VQXUN.js → chunk-S6IB3BZK.js} +2 -2
- package/dist/{chunk-OUHLWOPJ.js → chunk-SAH4FR36.js} +5 -5
- package/dist/{chunk-TONMD7QX.cjs → chunk-VCOL4C2W.cjs} +86 -20
- package/dist/chunk-VCOL4C2W.cjs.map +1 -0
- package/dist/{chunk-XDBNMB5K.js → chunk-VNDAMVMI.js} +2 -2
- package/dist/{chunk-CUBNUQPZ.cjs → chunk-YJWISFIK.cjs} +12 -12
- package/dist/{chunk-CUBNUQPZ.cjs.map → chunk-YJWISFIK.cjs.map} +1 -1
- package/dist/{chunk-NY4WLKNU.js → chunk-Z4LAZ4ZZ.js} +3 -3
- package/dist/{chunk-OTV4A6NA.js → chunk-ZPUZPJDV.js} +80 -14
- package/dist/chunk-ZPUZPJDV.js.map +1 -0
- package/dist/components/announcement-bar.d.ts +1 -1
- package/dist/components/announcement-bar.d.ts.map +1 -1
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +174 -162
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +49 -37
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/navigation/mobile-burger-menu.d.ts.map +1 -1
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/announcement-bar-view.d.ts +26 -24
- package/dist/components/ui/announcement-bar-view.d.ts.map +1 -1
- package/dist/components/ui/entity-image.d.ts.map +1 -1
- package/dist/components/ui/image-uploader.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
- package/dist/components/ui/square-avatar.d.ts.map +1 -1
- package/dist/hooks/index.cjs +6 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +5 -1
- package/dist/hooks/use-authed-image-src.d.ts +10 -0
- package/dist/hooks/use-authed-image-src.d.ts.map +1 -0
- package/dist/index.cjs +7 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -2
- package/dist/types/announcement.cjs.map +1 -1
- package/dist/types/announcement.d.ts +5 -0
- package/dist/types/announcement.d.ts.map +1 -1
- package/dist/types/announcement.js.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/embed-authed-fetch.d.ts +15 -0
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/announcement-bar.tsx +58 -42
- package/src/components/navigation/mobile-burger-menu.tsx +1 -0
- package/src/components/ui/announcement-bar-view.tsx +42 -66
- package/src/components/ui/entity-image.tsx +5 -3
- package/src/components/ui/image-uploader.tsx +21 -12
- package/src/components/ui/simple-markdown-renderer.tsx +57 -58
- package/src/components/ui/square-avatar.tsx +5 -3
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-authed-image-src.ts +93 -0
- package/src/stories/AnnouncementBar.stories.tsx +20 -0
- package/src/types/announcement.ts +5 -0
- package/src/utils/embed-authed-fetch.ts +30 -0
- package/dist/chunk-OTV4A6NA.js.map +0 -1
- package/dist/chunk-RIEAXUJX.cjs.map +0 -1
- package/dist/chunk-TONMD7QX.cjs.map +0 -1
- package/dist/chunk-TVMARS74.js.map +0 -1
- /package/dist/{chunk-BY6TZ6KA.js.map → chunk-3NNCBZEM.js.map} +0 -0
- /package/dist/{chunk-FPOHUGEK.js.map → chunk-AGX565ZT.js.map} +0 -0
- /package/dist/{chunk-PJYGEXWM.js.map → chunk-FBUIZTAI.js.map} +0 -0
- /package/dist/{chunk-XNJ2LZH2.js.map → chunk-K52S6LZ5.js.map} +0 -0
- /package/dist/{chunk-ZPUT2BB4.js.map → chunk-OKRUL7YV.js.map} +0 -0
- /package/dist/{chunk-W6RDDT3S.js.map → chunk-PTFSF3UP.js.map} +0 -0
- /package/dist/{chunk-74ZFCTRZ.js.map → chunk-R4OGP4OV.js.map} +0 -0
- /package/dist/{chunk-7D5KOJMF.js.map → chunk-RIPWRVA3.js.map} +0 -0
- /package/dist/{chunk-EK2VQXUN.js.map → chunk-S6IB3BZK.js.map} +0 -0
- /package/dist/{chunk-OUHLWOPJ.js.map → chunk-SAH4FR36.js.map} +0 -0
- /package/dist/{chunk-XDBNMB5K.js.map → chunk-VNDAMVMI.js.map} +0 -0
- /package/dist/{chunk-NY4WLKNU.js.map → chunk-Z4LAZ4ZZ.js.map} +0 -0
package/package.json
CHANGED
|
@@ -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({
|
|
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:
|
|
188
|
-
|
|
189
|
-
and
|
|
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
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
className="relative !size-
|
|
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
|
-
/*
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
237
|
-
`md
|
|
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
|
|
241
|
-
|
|
242
|
-
|
|
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="
|
|
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={
|
|
261
|
-
className="w-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
19
|
-
*
|
|
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
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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)
|
|
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
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
-
{
|
|
94
|
-
|
|
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
|
-
{/*
|
|
105
|
-
|
|
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
|
-
}, [
|
|
22
|
+
}, [resolvedSrc])
|
|
21
23
|
|
|
22
|
-
const showFallback = imageFailed || !
|
|
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={
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
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
|
-
|
|
71
|
+
resolvedSrc && 'hidden'
|
|
70
72
|
)}>
|
|
71
73
|
{getFirstLastInitials(fallback || alt) || '?'}
|
|
72
74
|
</div>
|
|
73
|
-
{
|
|
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={
|
|
78
|
+
src={resolvedSrc}
|
|
77
79
|
alt={alt || ''}
|
|
78
80
|
width={sizePx ?? sizePxBySize[size]}
|
|
79
81
|
height={sizePx ?? sizePxBySize[size]}
|
package/src/hooks/index.ts
CHANGED