@flamingo-stack/openframe-frontend-core 0.0.445 → 0.0.446-snapshot.20260717214810
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-R6WGCJBB.cjs → chunk-2PNWAAU4.cjs} +73 -73
- package/dist/{chunk-R6WGCJBB.cjs.map → chunk-2PNWAAU4.cjs.map} +1 -1
- package/dist/{chunk-H352AZJN.js → chunk-3GBLPPSW.js} +6 -6
- package/dist/{chunk-TCEBFMLQ.js → chunk-4ONE3HQP.js} +2 -2
- package/dist/{chunk-K4RASNQM.js → chunk-5BJJRPDH.js} +3 -3
- package/dist/{chunk-H2DYCT6M.js → chunk-6TAIXSF5.js} +2 -2
- package/dist/{chunk-VIJARDBM.cjs → chunk-AN7MCJTV.cjs} +5 -3
- package/dist/chunk-AN7MCJTV.cjs.map +1 -0
- package/dist/{chunk-7R256DKK.js → chunk-D2NLNBLH.js} +2 -2
- package/dist/{chunk-N64VTHZ5.cjs → chunk-E6HKLQ3Q.cjs} +7 -7
- package/dist/{chunk-N64VTHZ5.cjs.map → chunk-E6HKLQ3Q.cjs.map} +1 -1
- package/dist/{chunk-DTLNCOSG.cjs → chunk-EZMM2OMP.cjs} +32 -32
- package/dist/{chunk-DTLNCOSG.cjs.map → chunk-EZMM2OMP.cjs.map} +1 -1
- package/dist/{chunk-5FUQTSTT.cjs → chunk-FR6NVZVM.cjs} +18 -18
- package/dist/{chunk-5FUQTSTT.cjs.map → chunk-FR6NVZVM.cjs.map} +1 -1
- package/dist/{chunk-Z4N7XLFG.js → chunk-G7QSLGMI.js} +3 -3
- package/dist/{chunk-5DWPXK37.cjs → chunk-ITJMUH4Q.cjs} +40 -40
- package/dist/{chunk-5DWPXK37.cjs.map → chunk-ITJMUH4Q.cjs.map} +1 -1
- package/dist/{chunk-P3FN4NAE.cjs → chunk-KYLOPUIH.cjs} +29 -29
- package/dist/{chunk-P3FN4NAE.cjs.map → chunk-KYLOPUIH.cjs.map} +1 -1
- package/dist/{chunk-YCLSV2H6.cjs → chunk-PF6CKZIH.cjs} +5 -5
- package/dist/{chunk-YCLSV2H6.cjs.map → chunk-PF6CKZIH.cjs.map} +1 -1
- package/dist/{chunk-QL2EI56X.cjs → chunk-RNXMP7GB.cjs} +1014 -988
- package/dist/chunk-RNXMP7GB.cjs.map +1 -0
- package/dist/{chunk-PTT4J5GF.cjs → chunk-ROCJMSJI.cjs} +3 -3
- package/dist/{chunk-PTT4J5GF.cjs.map → chunk-ROCJMSJI.cjs.map} +1 -1
- package/dist/{chunk-RXLMN6B5.cjs → chunk-S7VMUDUB.cjs} +12 -12
- package/dist/{chunk-RXLMN6B5.cjs.map → chunk-S7VMUDUB.cjs.map} +1 -1
- package/dist/{chunk-22NSWMI6.js → chunk-SHC5NFOH.js} +3 -3
- package/dist/{chunk-5NYXGWBN.js → chunk-TKGCTDQY.js} +4 -2
- package/dist/{chunk-5NYXGWBN.js.map → chunk-TKGCTDQY.js.map} +1 -1
- package/dist/{chunk-3DQ5BFPM.js → chunk-TMZYFYSS.js} +5 -5
- package/dist/{chunk-KISQUH33.cjs → chunk-WLU37UZO.cjs} +73 -73
- package/dist/{chunk-KISQUH33.cjs.map → chunk-WLU37UZO.cjs.map} +1 -1
- package/dist/{chunk-FIAXFNAA.js → chunk-WMOF3WQT.js} +5 -5
- package/dist/{chunk-MPSA6YAM.cjs → chunk-X4XHW3OV.cjs} +40 -40
- package/dist/{chunk-MPSA6YAM.cjs.map → chunk-X4XHW3OV.cjs.map} +1 -1
- package/dist/{chunk-3HYSD7NR.cjs → chunk-XUS75SQW.cjs} +15 -15
- package/dist/{chunk-3HYSD7NR.cjs.map → chunk-XUS75SQW.cjs.map} +1 -1
- package/dist/{chunk-3BHV2QAH.js → chunk-YCSNDUDE.js} +510 -484
- package/dist/chunk-YCSNDUDE.js.map +1 -0
- package/dist/{chunk-G2KXNMFQ.js → chunk-YHNBJ3F3.js} +2 -2
- package/dist/{chunk-MQUS6BFR.js → chunk-ZQVEB6KT.js} +6 -6
- package/dist/{chunk-XPZLIONZ.js → chunk-ZYB3BYBA.js} +4 -4
- 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/cards-strip.d.ts.map +1 -1
- package/dist/components/features/highlight-video-section.d.ts.map +1 -1
- package/dist/components/features/index.cjs +5 -3
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +4 -2
- package/dist/components/features/media-gallery-manager.d.ts.map +1 -1
- package/dist/components/features/mux-origins.cjs +14 -0
- package/dist/components/features/mux-origins.cjs.map +1 -1
- package/dist/components/features/mux-origins.d.ts +26 -2
- package/dist/components/features/mux-origins.d.ts.map +1 -1
- package/dist/components/features/mux-origins.js +14 -1
- package/dist/components/features/mux-origins.js.map +1 -1
- package/dist/components/features/policy-configuration-panel.d.ts.map +1 -1
- package/dist/components/features/release-media-manager.d.ts.map +1 -1
- package/dist/components/features/use-video-warmup.d.ts +12 -0
- package/dist/components/features/use-video-warmup.d.ts.map +1 -1
- package/dist/components/features/video-bites-strip.d.ts.map +1 -1
- package/dist/components/features/video-source-selector.d.ts +1 -11
- package/dist/components/features/video-source-selector.d.ts.map +1 -1
- package/dist/components/features/video.d.ts +14 -1
- package/dist/components/features/video.d.ts.map +1 -1
- 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 +137 -135
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +13 -11
- 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/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/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/hooks/index.cjs +4 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +3 -1
- package/dist/hooks/use-near-viewport.d.ts +7 -0
- package/dist/hooks/use-near-viewport.d.ts.map +1 -1
- package/dist/index.cjs +7 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -2
- package/package.json +1 -1
- package/src/components/features/cards-strip.tsx +2 -1
- package/src/components/features/highlight-video-section.tsx +10 -7
- package/src/components/features/media-gallery-manager.tsx +6 -8
- package/src/components/features/mux-origins.ts +40 -2
- package/src/components/features/policy-configuration-panel.tsx +34 -49
- package/src/components/features/release-media-manager.tsx +6 -8
- package/src/components/features/use-video-warmup.ts +19 -3
- package/src/components/features/video-bites-strip.tsx +24 -4
- package/src/components/features/video-source-selector.tsx +16 -56
- package/src/components/features/video.tsx +103 -8
- package/src/components/ui/rich-markdown-renderer.tsx +20 -0
- package/src/components/ui/simple-markdown-renderer.tsx +9 -5
- package/src/hooks/use-near-viewport.ts +9 -1
- package/src/stories/PolicyConfigurationPanel.stories.tsx +8 -6
- package/dist/chunk-3BHV2QAH.js.map +0 -1
- package/dist/chunk-QL2EI56X.cjs.map +0 -1
- package/dist/chunk-VIJARDBM.cjs.map +0 -1
- /package/dist/{chunk-H352AZJN.js.map → chunk-3GBLPPSW.js.map} +0 -0
- /package/dist/{chunk-TCEBFMLQ.js.map → chunk-4ONE3HQP.js.map} +0 -0
- /package/dist/{chunk-K4RASNQM.js.map → chunk-5BJJRPDH.js.map} +0 -0
- /package/dist/{chunk-H2DYCT6M.js.map → chunk-6TAIXSF5.js.map} +0 -0
- /package/dist/{chunk-7R256DKK.js.map → chunk-D2NLNBLH.js.map} +0 -0
- /package/dist/{chunk-Z4N7XLFG.js.map → chunk-G7QSLGMI.js.map} +0 -0
- /package/dist/{chunk-22NSWMI6.js.map → chunk-SHC5NFOH.js.map} +0 -0
- /package/dist/{chunk-3DQ5BFPM.js.map → chunk-TMZYFYSS.js.map} +0 -0
- /package/dist/{chunk-FIAXFNAA.js.map → chunk-WMOF3WQT.js.map} +0 -0
- /package/dist/{chunk-G2KXNMFQ.js.map → chunk-YHNBJ3F3.js.map} +0 -0
- /package/dist/{chunk-MQUS6BFR.js.map → chunk-ZQVEB6KT.js.map} +0 -0
- /package/dist/{chunk-XPZLIONZ.js.map → chunk-ZYB3BYBA.js.map} +0 -0
|
@@ -34,6 +34,21 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
34
34
|
import MuxPlayer from '@mux/mux-player-react';
|
|
35
35
|
import { VideoPlayBadge, VideoUnmuteGlyph } from './video-center-badge';
|
|
36
36
|
import { fetchPriorityProp } from '../../utils/fetch-priority';
|
|
37
|
+
import { saveDataEnabled } from './use-video-warmup';
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Dev-only hover→playing latency instrumentation gate. Always on in dev
|
|
41
|
+
// builds; opt-in in production via `localStorage.VIDEO_PERF_DEBUG` so an
|
|
42
|
+
// "instant hover" regression can be measured on a live deployment.
|
|
43
|
+
// =============================================================================
|
|
44
|
+
function videoPerfDebugEnabled(): boolean {
|
|
45
|
+
if (process.env.NODE_ENV !== 'production') return true;
|
|
46
|
+
try {
|
|
47
|
+
return typeof localStorage !== 'undefined' && localStorage.getItem('VIDEO_PERF_DEBUG') !== null;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
37
52
|
|
|
38
53
|
// =============================================================================
|
|
39
54
|
// Suppress Google Cast SDK loading (CSP-friendly)
|
|
@@ -253,8 +268,19 @@ interface VideoFileProps extends VideoCommonProps {
|
|
|
253
268
|
* seeked to `#t=0.1` (media-fragment trick; paints on iOS Safari where a
|
|
254
269
|
* fragmentless metadata load stays blank). No chrome, no playback, no
|
|
255
270
|
* MuxPlayer cost — the resting facade layer under strip cards when no
|
|
256
|
-
* poster asset exists.
|
|
271
|
+
* poster asset exists. `poster`/`fit`/`className` are honored; all other
|
|
272
|
+
* player props are ignored. */
|
|
257
273
|
firstFrameOnly?: boolean;
|
|
274
|
+
/** Media preload hint. When omitted, the SSOT default applies:
|
|
275
|
+
* `'metadata'` (manifest + ~1 segment buffered on mount — instant
|
|
276
|
+
* hover/click start, bounded by playback-core's maxBufferLength=1 clamp),
|
|
277
|
+
* downgraded to `'none'` on Save-Data connections. Pass a value only to
|
|
278
|
+
* override the policy deliberately. */
|
|
279
|
+
preload?: 'none' | 'metadata' | 'auto';
|
|
280
|
+
/** Object-fit for the media inside the player box. Default `'contain'`
|
|
281
|
+
* (MuxPlayer default); `'cover'` crops to fill — aspect-cropped grid
|
|
282
|
+
* cells and social-post mockups. */
|
|
283
|
+
fit?: 'contain' | 'cover';
|
|
258
284
|
}
|
|
259
285
|
|
|
260
286
|
interface VideoYouTubeProps extends VideoCommonProps {
|
|
@@ -275,6 +301,8 @@ interface VideoAutoProps extends VideoCommonProps {
|
|
|
275
301
|
playOnHover?: boolean;
|
|
276
302
|
playWhenHovered?: boolean;
|
|
277
303
|
firstFrameOnly?: boolean;
|
|
304
|
+
preload?: 'none' | 'metadata' | 'auto';
|
|
305
|
+
fit?: 'contain' | 'cover';
|
|
278
306
|
}
|
|
279
307
|
|
|
280
308
|
export type VideoProps = VideoFileProps | VideoYouTubeProps | VideoAutoProps;
|
|
@@ -300,7 +328,12 @@ export function Video(props: VideoProps): React.ReactElement | null {
|
|
|
300
328
|
minimalControls={props.minimalControls}
|
|
301
329
|
/>
|
|
302
330
|
) : 'firstFrameOnly' in props && props.firstFrameOnly ? (
|
|
303
|
-
<FirstFramePreview
|
|
331
|
+
<FirstFramePreview
|
|
332
|
+
url={url}
|
|
333
|
+
poster={props.poster}
|
|
334
|
+
fit={'fit' in props ? props.fit : undefined}
|
|
335
|
+
className={props.className}
|
|
336
|
+
/>
|
|
304
337
|
) : (
|
|
305
338
|
<FilePlayer
|
|
306
339
|
url={url}
|
|
@@ -313,6 +346,8 @@ export function Video(props: VideoProps): React.ReactElement | null {
|
|
|
313
346
|
chromeless={'chromeless' in props ? props.chromeless : undefined}
|
|
314
347
|
playOnHover={'playOnHover' in props ? props.playOnHover : undefined}
|
|
315
348
|
playWhenHovered={'playWhenHovered' in props ? props.playWhenHovered : undefined}
|
|
349
|
+
preload={'preload' in props ? props.preload : undefined}
|
|
350
|
+
fit={'fit' in props ? props.fit : undefined}
|
|
316
351
|
className={props.className}
|
|
317
352
|
/>
|
|
318
353
|
);
|
|
@@ -387,16 +422,30 @@ function wrapWithLayout(
|
|
|
387
422
|
* first frame (also fixes iOS Safari, which stays blank on a fragmentless
|
|
388
423
|
* metadata load). The transparent media background keeps the box showing the
|
|
389
424
|
* card surface (never black) until the frame decodes. */
|
|
390
|
-
function FirstFramePreview({
|
|
425
|
+
function FirstFramePreview({
|
|
426
|
+
url,
|
|
427
|
+
poster,
|
|
428
|
+
fit,
|
|
429
|
+
className,
|
|
430
|
+
}: {
|
|
431
|
+
url: string;
|
|
432
|
+
poster?: string | null;
|
|
433
|
+
fit?: 'contain' | 'cover';
|
|
434
|
+
className?: string;
|
|
435
|
+
}): React.ReactElement {
|
|
391
436
|
return (
|
|
392
437
|
<div
|
|
393
438
|
aria-hidden
|
|
394
439
|
className="h-full w-full"
|
|
395
440
|
style={{ '--media-background-color': 'transparent' } as React.CSSProperties}
|
|
396
441
|
>
|
|
442
|
+
{/* No explicit `preload` — inherits the SSOT default ('metadata',
|
|
443
|
+
'none' under Save-Data) so posterless facade cards honor the
|
|
444
|
+
Save-Data policy like every other surface. */}
|
|
397
445
|
<FilePlayer
|
|
398
446
|
url={`${url}#t=0.1`}
|
|
399
|
-
|
|
447
|
+
poster={poster}
|
|
448
|
+
fit={fit}
|
|
400
449
|
muted
|
|
401
450
|
chromeless
|
|
402
451
|
className={className}
|
|
@@ -420,8 +469,11 @@ interface FilePlayerProps {
|
|
|
420
469
|
chromeless?: boolean;
|
|
421
470
|
playOnHover?: boolean;
|
|
422
471
|
playWhenHovered?: boolean;
|
|
423
|
-
/** Media preload hint — the
|
|
472
|
+
/** Media preload hint — see the public `VideoFileProps.preload` JSDoc.
|
|
473
|
+
* When omitted, defaults to 'metadata' ('none' under Save-Data). */
|
|
424
474
|
preload?: 'none' | 'metadata' | 'auto';
|
|
475
|
+
/** Object-fit — 'cover' maps to media-chrome's `--media-object-fit`. */
|
|
476
|
+
fit?: 'contain' | 'cover';
|
|
425
477
|
className?: string;
|
|
426
478
|
}
|
|
427
479
|
|
|
@@ -437,8 +489,21 @@ function FilePlayer({
|
|
|
437
489
|
playOnHover,
|
|
438
490
|
playWhenHovered,
|
|
439
491
|
preload,
|
|
492
|
+
fit,
|
|
440
493
|
className,
|
|
441
494
|
}: FilePlayerProps): React.ReactElement {
|
|
495
|
+
// Explicit preload policy — never rely on the browser/MuxPlayer implicit
|
|
496
|
+
// default. 'metadata' makes playback-core load the manifest immediately
|
|
497
|
+
// and clamp buffering to maxBufferLength=1/maxBufferSize=1 (manifest +
|
|
498
|
+
// ~1 segment, then idle); play() restores full buffering automatically.
|
|
499
|
+
// That bounded prefetch is what makes hover/click start instant. Save-Data
|
|
500
|
+
// connections get 'none' (nothing until play()). SSR note: the server pass
|
|
501
|
+
// always emits 'metadata' (saveDataEnabled() is false server-side); for
|
|
502
|
+
// Save-Data users React reconciles the property to 'none' during hydration.
|
|
503
|
+
// The mux-player custom element upgrades from client JS, so the window for
|
|
504
|
+
// an early manifest fetch is at most one bounded manifest+segment — the
|
|
505
|
+
// Save-Data verification gate covers it (see plan A2.4).
|
|
506
|
+
const effectivePreload = preload ?? (saveDataEnabled() ? 'none' : 'metadata');
|
|
442
507
|
// True while hover playback is running MUTED because the browser's autoplay
|
|
443
508
|
// policy blocked sound (no user activation yet). Drives the center unmute
|
|
444
509
|
// control — the industry pattern (muted autoplay + explicit unmute button)
|
|
@@ -454,7 +519,22 @@ function FilePlayer({
|
|
|
454
519
|
pause?: () => void;
|
|
455
520
|
muted?: boolean;
|
|
456
521
|
volume?: number;
|
|
522
|
+
addEventListener?: (type: string, listener: () => void) => void;
|
|
523
|
+
removeEventListener?: (type: string, listener: () => void) => void;
|
|
457
524
|
} | null>(null);
|
|
525
|
+
// Dev/opt-in hover→'playing' latency metric (see videoPerfDebugEnabled).
|
|
526
|
+
// One listener at a time — re-entering hover replaces it; hover-leave and
|
|
527
|
+
// unmount clear it so no stale listener survives across generations.
|
|
528
|
+
const perfListenerRef = useRef<(() => void) | null>(null);
|
|
529
|
+
const clearPerfListener = useCallback(() => {
|
|
530
|
+
if (perfListenerRef.current) {
|
|
531
|
+
try {
|
|
532
|
+
hoverPlayerRef.current?.removeEventListener?.('playing', perfListenerRef.current);
|
|
533
|
+
} catch { /* element already torn down */ }
|
|
534
|
+
perfListenerRef.current = null;
|
|
535
|
+
}
|
|
536
|
+
}, []);
|
|
537
|
+
useEffect(() => clearPerfListener, [clearPerfListener]);
|
|
458
538
|
// Tracks whether the pointer is STILL over the player. A fast hover-out
|
|
459
539
|
// pauses the in-flight play(), which rejects it with AbortError — that must
|
|
460
540
|
// NOT trigger the muted retry (it would restart playback after the pointer
|
|
@@ -481,6 +561,17 @@ function FilePlayer({
|
|
|
481
561
|
const generation = ++hoverGenerationRef.current;
|
|
482
562
|
const el = hoverPlayerRef.current;
|
|
483
563
|
if (!el) return;
|
|
564
|
+
if (videoPerfDebugEnabled()) {
|
|
565
|
+
clearPerfListener();
|
|
566
|
+
const startedAt = performance.now();
|
|
567
|
+
const onPlaying = () => {
|
|
568
|
+
clearPerfListener();
|
|
569
|
+
// eslint-disable-next-line no-console
|
|
570
|
+
console.debug('[Video] hover→playing %dms', Math.round(performance.now() - startedAt));
|
|
571
|
+
};
|
|
572
|
+
perfListenerRef.current = onPlaying;
|
|
573
|
+
try { el.addEventListener?.('playing', onPlaying); } catch { /* ignore */ }
|
|
574
|
+
}
|
|
484
575
|
try {
|
|
485
576
|
el.volume = 0.5;
|
|
486
577
|
if (userHasInteracted) {
|
|
@@ -523,13 +614,14 @@ function FilePlayer({
|
|
|
523
614
|
activationWaiters.add(waiter);
|
|
524
615
|
}
|
|
525
616
|
} catch { /* ignore */ }
|
|
526
|
-
}, []);
|
|
617
|
+
}, [clearActivationWaiter, clearPerfListener]);
|
|
527
618
|
const stopHoverPlayback = useCallback(() => {
|
|
528
619
|
hoverActiveRef.current = false;
|
|
529
620
|
setHoverMutedFallback(false);
|
|
530
621
|
clearActivationWaiter();
|
|
622
|
+
clearPerfListener();
|
|
531
623
|
try { hoverPlayerRef.current?.pause?.(); } catch { /* already torn down */ }
|
|
532
|
-
}, [clearActivationWaiter]);
|
|
624
|
+
}, [clearActivationWaiter, clearPerfListener]);
|
|
533
625
|
|
|
534
626
|
// Explicit unmute affordance: the click IS the user activation the autoplay
|
|
535
627
|
// policy wants, so unmuting here always succeeds (and the window-level
|
|
@@ -579,7 +671,7 @@ function FilePlayer({
|
|
|
579
671
|
src={url}
|
|
580
672
|
poster={poster || undefined}
|
|
581
673
|
streamType="on-demand"
|
|
582
|
-
preload={
|
|
674
|
+
preload={effectivePreload}
|
|
583
675
|
playsInline
|
|
584
676
|
muted={muted}
|
|
585
677
|
preferCmcd="header"
|
|
@@ -606,10 +698,13 @@ function FilePlayer({
|
|
|
606
698
|
// hides only the bottom bar (center play/pause stays) — the
|
|
607
699
|
// bite-strip card look per Figma. Merged (never replacing) into the
|
|
608
700
|
// sizing style; custom properties need the CSSProperties cast.
|
|
701
|
+
// `--media-object-fit: cover` is media-chrome's documented object-fit
|
|
702
|
+
// var — the `fit="cover"` crop for grid cells / mockups.
|
|
609
703
|
style={{
|
|
610
704
|
width: '100%',
|
|
611
705
|
height: '100%',
|
|
612
706
|
...(chromeless ? ({ '--controls': 'none' } as React.CSSProperties) : {}),
|
|
707
|
+
...(fit === 'cover' ? ({ '--media-object-fit': 'cover' } as React.CSSProperties) : {}),
|
|
613
708
|
}}
|
|
614
709
|
>
|
|
615
710
|
{captionsUrl ? (
|
|
@@ -1100,6 +1100,26 @@ const RichMarkdownInner: React.FC<InnerProps> = ({
|
|
|
1100
1100
|
return <MarkdownImage src={src.trim()} alt={alt} />;
|
|
1101
1101
|
},
|
|
1102
1102
|
|
|
1103
|
+
// Raw <video> tags in stored content (e.g. the hub's blog publisher
|
|
1104
|
+
// injects `<video src class="w-full my-8 rounded-lg" controls>` via
|
|
1105
|
+
// injectVideoInBlogContent) pass through rehypeRaw — route them to the
|
|
1106
|
+
// <Video> SSOT (MuxPlayer) instead of a native element so Mux HLS
|
|
1107
|
+
// manifests play on every browser and playback UX is unified. The
|
|
1108
|
+
// incoming className is forwarded so existing embeds keep their
|
|
1109
|
+
// width/margin/rounding.
|
|
1110
|
+
video: ({ src, poster, className }: any) => {
|
|
1111
|
+
if (!src || typeof src !== 'string' || src.trim() === '') {
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
return (
|
|
1115
|
+
<div className={`overflow-hidden ${className || 'w-full my-8 rounded-lg'}`}>
|
|
1116
|
+
<div className="w-full aspect-video">
|
|
1117
|
+
<Video kind="file" url={src.trim()} poster={typeof poster === 'string' ? poster : undefined} />
|
|
1118
|
+
</div>
|
|
1119
|
+
</div>
|
|
1120
|
+
);
|
|
1121
|
+
},
|
|
1122
|
+
|
|
1103
1123
|
// Style lists
|
|
1104
1124
|
ul: ({ children }: any) => (
|
|
1105
1125
|
<ul className={`list-disc list-outside my-4 ml-8 space-y-2 ${
|
|
@@ -200,7 +200,11 @@ function cardAwareUrlTransform(url: string, key: string): string {
|
|
|
200
200
|
* 18 logs "tag is unrecognized in this browser" for every unknown tag;
|
|
201
201
|
* React 19 throws on tags with reserved kebab-case forms. We pre-escape
|
|
202
202
|
* to keep the renderer pristine without losing legitimate inline HTML
|
|
203
|
-
* (details/summary,
|
|
203
|
+
* (details/summary, iframe, kbd, mark, etc.). `video` is deliberately
|
|
204
|
+
* NOT allow-listed: chat strips <video> tags server-side, and all video
|
|
205
|
+
* playback goes through the <Video> SSOT — a stray tag renders as
|
|
206
|
+
* escaped literal text (visible + debuggable) rather than a native
|
|
207
|
+
* player outside the unified pipeline.
|
|
204
208
|
*
|
|
205
209
|
* The allow-list mirrors HTML5 + the elements the chat shell wires
|
|
206
210
|
* component overrides for. Kept lower-case; matched case-insensitively.
|
|
@@ -215,8 +219,8 @@ const SAFE_HTML_TAGS = new Set([
|
|
|
215
219
|
'ruby', 's', 'samp', 'section', 'small', 'span', 'strong', 'sub', 'summary',
|
|
216
220
|
'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'u',
|
|
217
221
|
'ul', 'var', 'wbr',
|
|
218
|
-
// Media
|
|
219
|
-
'img', 'picture', 'source', 'audio', '
|
|
222
|
+
// Media ('video' intentionally excluded — see the header comment)
|
|
223
|
+
'img', 'picture', 'source', 'audio', 'iframe', 'track',
|
|
220
224
|
// Forms (rehype-raw allows them; mostly harmless for chat output)
|
|
221
225
|
'button', 'input', 'label', 'select', 'option', 'optgroup', 'textarea', 'form', 'fieldset', 'legend',
|
|
222
226
|
])
|
|
@@ -652,8 +656,8 @@ const SimpleMarkdownRendererImpl: React.FC<SimpleMarkdownRendererProps> = ({
|
|
|
652
656
|
// logs a noisy "tag is unrecognized in this browser" warning AND can
|
|
653
657
|
// crash the SimpleMarkdownRenderer when the tag has a kebab-case
|
|
654
658
|
// form React rejects outright). Allow-listed tags pass through
|
|
655
|
-
// unchanged so legitimate inline HTML (details/summary,
|
|
656
|
-
//
|
|
659
|
+
// unchanged so legitimate inline HTML (details/summary, iframe,
|
|
660
|
+
// etc.) still renders ('video' is excluded — see SAFE_HTML_TAGS).
|
|
657
661
|
const processedContent = useMemo(
|
|
658
662
|
() => escapeUnknownHtmlTags(preprocessContent ? preprocessContent(content) : content),
|
|
659
663
|
[preprocessContent, content],
|
|
@@ -58,6 +58,14 @@ function getObserverFor(rootMargin: string): IntersectionObserver {
|
|
|
58
58
|
return io;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Default near-viewport lookahead — the SSOT for "mount media on approach".
|
|
63
|
+
* Shared by this hook's default AND the raw two-way IntersectionObservers in
|
|
64
|
+
* `cards-strip.tsx` / `video-bites-strip.tsx` (which need mount+unmount, not
|
|
65
|
+
* this hook's fire-once semantics, but must agree on the distance).
|
|
66
|
+
*/
|
|
67
|
+
export const NEAR_VIEWPORT_ROOT_MARGIN = '500px';
|
|
68
|
+
|
|
61
69
|
export interface UseNearViewportResult<T extends Element = HTMLElement> {
|
|
62
70
|
/** Ref to attach to the element you want to gate on visibility. */
|
|
63
71
|
ref: (node: T | null) => void;
|
|
@@ -72,7 +80,7 @@ export interface UseNearViewportResult<T extends Element = HTMLElement> {
|
|
|
72
80
|
* '0px' = strict on-screen detection.
|
|
73
81
|
*/
|
|
74
82
|
export function useNearViewport<T extends Element = HTMLElement>(
|
|
75
|
-
rootMargin: string =
|
|
83
|
+
rootMargin: string = NEAR_VIEWPORT_ROOT_MARGIN
|
|
76
84
|
): UseNearViewportResult<T> {
|
|
77
85
|
const [isNear, setIsNear] = useState(false);
|
|
78
86
|
const elRef = useRef<T | null>(null);
|
|
@@ -239,12 +239,14 @@ export const GroupedPolicySections: Story = {
|
|
|
239
239
|
<PolicyConfigurationPanel
|
|
240
240
|
categories={categories}
|
|
241
241
|
editMode={args.editMode}
|
|
242
|
-
onPolicyPermissionChange={(categoryId, policyId, level) =>
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
242
|
+
onPolicyPermissionChange={(categoryId, policyId, level) => {
|
|
243
|
+
args.onPolicyPermissionChange(categoryId, policyId, level)
|
|
244
|
+
updateGroup(groupName, prev => applyPolicyLevel(prev, categoryId, policyId, level))
|
|
245
|
+
}}
|
|
246
|
+
onCategoryPermissionChange={(categoryId, level) => {
|
|
247
|
+
args.onCategoryPermissionChange(categoryId, level)
|
|
248
|
+
updateGroup(groupName, prev => applyCategoryLevel(prev, categoryId, level))
|
|
249
|
+
}}
|
|
248
250
|
/>
|
|
249
251
|
</div>
|
|
250
252
|
))}
|