@flamingo-stack/openframe-frontend-core 0.0.388 → 0.0.389

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 (95) hide show
  1. package/dist/{chunk-B4JRV4FB.cjs → chunk-2JCSORCJ.cjs} +26 -26
  2. package/dist/{chunk-B4JRV4FB.cjs.map → chunk-2JCSORCJ.cjs.map} +1 -1
  3. package/dist/{chunk-HIODHJCJ.cjs → chunk-3JQUNKWY.cjs} +15 -15
  4. package/dist/{chunk-HIODHJCJ.cjs.map → chunk-3JQUNKWY.cjs.map} +1 -1
  5. package/dist/{chunk-SENYQRPF.js → chunk-44Q3IXZM.js} +2 -2
  6. package/dist/{chunk-7CEJO3FE.js → chunk-4OLIDOLO.js} +2 -2
  7. package/dist/{chunk-YKPQINSZ.cjs → chunk-7MKU2EB4.cjs} +26 -26
  8. package/dist/{chunk-YKPQINSZ.cjs.map → chunk-7MKU2EB4.cjs.map} +1 -1
  9. package/dist/{chunk-ATKRYY4U.js → chunk-BJPQJNSC.js} +4 -4
  10. package/dist/{chunk-EQKTBIKH.js → chunk-CQXHHVMR.js} +2 -2
  11. package/dist/{chunk-ZVFDJ35S.cjs → chunk-EHTE3CTT.cjs} +37 -37
  12. package/dist/{chunk-ZVFDJ35S.cjs.map → chunk-EHTE3CTT.cjs.map} +1 -1
  13. package/dist/{chunk-VOPTCR5I.cjs → chunk-GNB5ERZP.cjs} +1011 -735
  14. package/dist/chunk-GNB5ERZP.cjs.map +1 -0
  15. package/dist/{chunk-VCRUAKOJ.js → chunk-IDWKP5H5.js} +521 -245
  16. package/dist/chunk-IDWKP5H5.js.map +1 -0
  17. package/dist/{chunk-UCQWQ7LM.js → chunk-JSFHTIKA.js} +2 -2
  18. package/dist/{chunk-BGYWTU2E.js → chunk-KA4WXCN3.js} +4 -4
  19. package/dist/{chunk-23BJ64AY.js → chunk-LFLFQ422.js} +2 -2
  20. package/dist/{chunk-FS4FVNOU.js → chunk-M6ODI2F5.js} +2 -2
  21. package/dist/{chunk-WIKFWION.cjs → chunk-MIEODAAV.cjs} +9 -9
  22. package/dist/{chunk-WIKFWION.cjs.map → chunk-MIEODAAV.cjs.map} +1 -1
  23. package/dist/{chunk-HQG5AY64.cjs → chunk-MPOP47ME.cjs} +12 -12
  24. package/dist/{chunk-HQG5AY64.cjs.map → chunk-MPOP47ME.cjs.map} +1 -1
  25. package/dist/{chunk-7UQ74ZJN.js → chunk-NULFIYK2.js} +2 -2
  26. package/dist/{chunk-ONXN77KP.cjs → chunk-OBKCQ7LA.cjs} +62 -62
  27. package/dist/{chunk-ONXN77KP.cjs.map → chunk-OBKCQ7LA.cjs.map} +1 -1
  28. package/dist/{chunk-KYN2GYUI.cjs → chunk-Q4YGG3CL.cjs} +31 -31
  29. package/dist/{chunk-KYN2GYUI.cjs.map → chunk-Q4YGG3CL.cjs.map} +1 -1
  30. package/dist/{chunk-SNFSLYVA.cjs → chunk-TQZLC3BQ.cjs} +5 -5
  31. package/dist/{chunk-SNFSLYVA.cjs.map → chunk-TQZLC3BQ.cjs.map} +1 -1
  32. package/dist/{chunk-6ZAQOYW4.cjs → chunk-VXJBE5NX.cjs} +59 -59
  33. package/dist/{chunk-6ZAQOYW4.cjs.map → chunk-VXJBE5NX.cjs.map} +1 -1
  34. package/dist/{chunk-35C3PUUN.js → chunk-WIMXB5EU.js} +4 -4
  35. package/dist/{chunk-DM3JIOFA.js → chunk-XGOX5UQ2.js} +4 -4
  36. package/dist/{chunk-EYQESL6X.cjs → chunk-ZFZAHLI4.cjs} +7 -7
  37. package/dist/{chunk-EYQESL6X.cjs.map → chunk-ZFZAHLI4.cjs.map} +1 -1
  38. package/dist/components/case-studies/index.cjs +8 -8
  39. package/dist/components/case-studies/index.js +2 -2
  40. package/dist/components/chat/index.cjs +2 -2
  41. package/dist/components/chat/index.js +1 -1
  42. package/dist/components/contact/index.cjs +3 -3
  43. package/dist/components/contact/index.js +2 -2
  44. package/dist/components/docs/index.cjs +5 -5
  45. package/dist/components/docs/index.js +4 -4
  46. package/dist/components/embeds/index.cjs +3 -3
  47. package/dist/components/embeds/index.js +2 -2
  48. package/dist/components/faq/index.cjs +3 -3
  49. package/dist/components/faq/index.js +2 -2
  50. package/dist/components/features/index.cjs +4 -2
  51. package/dist/components/features/index.cjs.map +1 -1
  52. package/dist/components/features/index.js +3 -1
  53. package/dist/components/features/video-bites-strip.d.ts +43 -4
  54. package/dist/components/features/video-bites-strip.d.ts.map +1 -1
  55. package/dist/components/features/video.d.ts +6 -0
  56. package/dist/components/features/video.d.ts.map +1 -1
  57. package/dist/components/help-center-pages/index.cjs +20 -20
  58. package/dist/components/help-center-pages/index.js +11 -11
  59. package/dist/components/index.cjs +122 -120
  60. package/dist/components/index.cjs.map +1 -1
  61. package/dist/components/index.js +11 -9
  62. package/dist/components/index.js.map +1 -1
  63. package/dist/components/navigation/index.cjs +2 -2
  64. package/dist/components/navigation/index.js +1 -1
  65. package/dist/components/onboarding-guides/index.cjs +5 -5
  66. package/dist/components/onboarding-guides/index.js +4 -4
  67. package/dist/components/related-content/index.cjs +3 -3
  68. package/dist/components/related-content/index.js +2 -2
  69. package/dist/components/tickets/index.cjs +5 -5
  70. package/dist/components/tickets/index.js +4 -4
  71. package/dist/components/ui/index.cjs +2 -2
  72. package/dist/components/ui/index.js +1 -1
  73. package/dist/index.cjs +4 -2
  74. package/dist/index.cjs.map +1 -1
  75. package/dist/index.js +3 -1
  76. package/dist/types/case-study.d.ts +1 -1
  77. package/dist/types/case-study.d.ts.map +1 -1
  78. package/package.json +1 -1
  79. package/src/components/features/video-bites-strip.tsx +317 -72
  80. package/src/components/features/video.tsx +167 -37
  81. package/src/components/user-display.tsx +2 -2
  82. package/src/types/case-study.ts +1 -1
  83. package/dist/chunk-VCRUAKOJ.js.map +0 -1
  84. package/dist/chunk-VOPTCR5I.cjs.map +0 -1
  85. /package/dist/{chunk-SENYQRPF.js.map → chunk-44Q3IXZM.js.map} +0 -0
  86. /package/dist/{chunk-7CEJO3FE.js.map → chunk-4OLIDOLO.js.map} +0 -0
  87. /package/dist/{chunk-ATKRYY4U.js.map → chunk-BJPQJNSC.js.map} +0 -0
  88. /package/dist/{chunk-EQKTBIKH.js.map → chunk-CQXHHVMR.js.map} +0 -0
  89. /package/dist/{chunk-UCQWQ7LM.js.map → chunk-JSFHTIKA.js.map} +0 -0
  90. /package/dist/{chunk-BGYWTU2E.js.map → chunk-KA4WXCN3.js.map} +0 -0
  91. /package/dist/{chunk-23BJ64AY.js.map → chunk-LFLFQ422.js.map} +0 -0
  92. /package/dist/{chunk-FS4FVNOU.js.map → chunk-M6ODI2F5.js.map} +0 -0
  93. /package/dist/{chunk-7UQ74ZJN.js.map → chunk-NULFIYK2.js.map} +0 -0
  94. /package/dist/{chunk-35C3PUUN.js.map → chunk-WIMXB5EU.js.map} +0 -0
  95. /package/dist/{chunk-DM3JIOFA.js.map → chunk-XGOX5UQ2.js.map} +0 -0
@@ -30,9 +30,10 @@
30
30
  * layout="native" → intrinsic aspect ratio. Bites grid, blog cards.
31
31
  */
32
32
 
33
- import React, { useEffect, useMemo, useRef, useState } from 'react';
33
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
34
34
  import MuxPlayer from '@mux/mux-player-react';
35
35
  import { PlayIcon } from '../icons-v2-generated/media-playback/play-icon';
36
+ import { VolumeXmarkIcon } from '../icons-v2-generated/audio-and-visual/volume-xmark-icon';
36
37
  import { fetchPriorityProp } from '../../utils/fetch-priority';
37
38
 
38
39
  // =============================================================================
@@ -105,6 +106,32 @@ if (typeof window !== 'undefined' && typeof console !== 'undefined') {
105
106
  }
106
107
  }
107
108
 
109
+ // =============================================================================
110
+ // User-activation tracker (module scope) — Chrome's autoplay policy rejects
111
+ // UNMUTED play() until the user has interacted with the page (click/keydown;
112
+ // pointer MOVEMENT does not count). Hover-preview surfaces use this to pick
113
+ // the right first move (sound vs muted) WITHOUT a rejection round-trip, and
114
+ // to unmute a live muted preview the instant the first gesture lands.
115
+ // =============================================================================
116
+
117
+ let userHasInteracted = false;
118
+ const activationWaiters = new Set<() => void>();
119
+ if (typeof window !== 'undefined') {
120
+ const w = window as unknown as { __VIDEO_ACTIVATION_TRACKED__?: boolean };
121
+ if (!w.__VIDEO_ACTIVATION_TRACKED__) {
122
+ w.__VIDEO_ACTIVATION_TRACKED__ = true;
123
+ const markActivated = () => {
124
+ userHasInteracted = true;
125
+ activationWaiters.forEach(fn => { try { fn(); } catch { /* ignore */ } });
126
+ activationWaiters.clear();
127
+ window.removeEventListener('pointerdown', markActivated, true);
128
+ window.removeEventListener('keydown', markActivated, true);
129
+ };
130
+ window.addEventListener('pointerdown', markActivated, true);
131
+ window.addEventListener('keydown', markActivated, true);
132
+ }
133
+ }
134
+
108
135
  // =============================================================================
109
136
  // URL classifiers (private — `<Video>` is the only consumer)
110
137
  // =============================================================================
@@ -218,6 +245,11 @@ interface VideoFileProps extends VideoCommonProps {
218
245
  * sound at 50% volume first (bite-strip behavior); falls back to muted when
219
246
  * the browser's autoplay policy rejects unmuted hover playback. */
220
247
  playOnHover?: boolean;
248
+ /** CONTROLLED variant of playOnHover: the host owns the hover state (e.g.
249
+ * the bite-strip card, whose overlay also counts as "hovering the card").
250
+ * true → start hover playback, false → pause. When provided, the internal
251
+ * pointer handlers are disabled. */
252
+ playWhenHovered?: boolean;
221
253
  /** Hide the bottom control bar, keep only the CENTER play/pause control
222
254
  * (bite-strip cards per Figma). */
223
255
  centerControlsOnly?: boolean;
@@ -239,6 +271,7 @@ interface VideoAutoProps extends VideoCommonProps {
239
271
  loop?: boolean;
240
272
  chromeless?: boolean;
241
273
  playOnHover?: boolean;
274
+ playWhenHovered?: boolean;
242
275
  centerControlsOnly?: boolean;
243
276
  }
244
277
 
@@ -275,6 +308,7 @@ export function Video(props: VideoProps): React.ReactElement | null {
275
308
  loop={'loop' in props ? props.loop : undefined}
276
309
  chromeless={'chromeless' in props ? props.chromeless : undefined}
277
310
  playOnHover={'playOnHover' in props ? props.playOnHover : undefined}
311
+ playWhenHovered={'playWhenHovered' in props ? props.playWhenHovered : undefined}
278
312
  centerControlsOnly={'centerControlsOnly' in props ? props.centerControlsOnly : undefined}
279
313
  className={props.className}
280
314
  />
@@ -353,6 +387,7 @@ interface FilePlayerProps {
353
387
  loop?: boolean;
354
388
  chromeless?: boolean;
355
389
  playOnHover?: boolean;
390
+ playWhenHovered?: boolean;
356
391
  centerControlsOnly?: boolean;
357
392
  className?: string;
358
393
  }
@@ -367,12 +402,18 @@ function FilePlayer({
367
402
  loop,
368
403
  chromeless,
369
404
  playOnHover,
405
+ playWhenHovered,
370
406
  centerControlsOnly,
371
407
  className,
372
408
  }: FilePlayerProps): React.ReactElement {
373
409
  // centerControlsOnly: the center play button shows at REST only — while
374
410
  // playing, ALL chrome hides (no pause sign over the hover-preview).
375
411
  const [isPlaying, setIsPlaying] = useState(false);
412
+ // True while hover playback is running MUTED because the browser's autoplay
413
+ // policy blocked sound (no user activation yet). Drives the center unmute
414
+ // control — the industry pattern (muted autoplay + explicit unmute button)
415
+ // instead of silently waiting for a click somewhere.
416
+ const [hoverMutedFallback, setHoverMutedFallback] = useState(false);
376
417
  // playOnHover drives the underlying mux-player element imperatively — the
377
418
  // element exposes native play()/pause()/muted/volume; the chrome stays as
378
419
  // configured. Sound-first: volume 0.5 unmuted, muted fallback when the
@@ -393,42 +434,102 @@ function FilePlayer({
393
434
  // Per-enter generation token: a LATE NotAllowedError from a previous enter
394
435
  // must not fire the muted fallback into a newer (intended-sound) session.
395
436
  const hoverGenerationRef = useRef(0);
396
- const handleHoverEnter = playOnHover
397
- ? () => {
398
- hoverActiveRef.current = true;
399
- const generation = ++hoverGenerationRef.current;
400
- const el = hoverPlayerRef.current;
401
- if (!el) return;
402
- try {
403
- el.volume = 0.5;
404
- el.muted = false;
405
- const attempt = el.play?.();
406
- if (attempt && typeof (attempt as Promise<void>).catch === 'function') {
407
- (attempt as Promise<void>).catch((err: unknown) => {
408
- const name = (err as { name?: string } | null)?.name;
409
- if (
410
- name === 'NotAllowedError' &&
411
- hoverActiveRef.current &&
412
- generation === hoverGenerationRef.current
413
- ) {
414
- try {
415
- el.muted = true;
416
- // Swallow the retry's own rejection too (a hover-out mid-retry
417
- // aborts it that must not surface as an unhandled rejection).
418
- (el.play?.() as Promise<void> | undefined)?.catch?.(() => {});
419
- } catch { /* give up silently */ }
420
- }
421
- });
437
+ // This instance's pending activation waiter — pruned on hover-leave,
438
+ // re-enter, and unmount so pre-activation hovers don't accumulate stale
439
+ // closures in the module-level set (they'd otherwise setState against
440
+ // unmounted instances when the first user gesture finally lands).
441
+ const activationWaiterRef = useRef<(() => void) | null>(null);
442
+ const clearActivationWaiter = useCallback(() => {
443
+ if (activationWaiterRef.current) {
444
+ activationWaiters.delete(activationWaiterRef.current);
445
+ activationWaiterRef.current = null;
446
+ }
447
+ }, []);
448
+ useEffect(() => clearActivationWaiter, [clearActivationWaiter]);
449
+ const startHoverPlayback = useCallback(() => {
450
+ hoverActiveRef.current = true;
451
+ const generation = ++hoverGenerationRef.current;
452
+ const el = hoverPlayerRef.current;
453
+ if (!el) return;
454
+ try {
455
+ el.volume = 0.5;
456
+ if (userHasInteracted) {
457
+ // Post-activation: unmuted playback is allowed play with sound.
458
+ // The NotAllowedError guard stays as a belt-and-suspenders fallback;
459
+ // a fast hover-out's pause() rejects with AbortError and must not
460
+ // restart playback (name mismatch + cleared hoverActiveRef).
461
+ el.muted = false;
462
+ (el.play?.() as Promise<void> | undefined)?.catch?.((err: unknown) => {
463
+ const name = (err as { name?: string } | null)?.name;
464
+ if (
465
+ name === 'NotAllowedError' &&
466
+ hoverActiveRef.current &&
467
+ generation === hoverGenerationRef.current
468
+ ) {
469
+ try {
470
+ el.muted = true;
471
+ (el.play?.() as Promise<void> | undefined)?.catch?.(() => {});
472
+ setHoverMutedFallback(true);
473
+ } catch { /* give up silently */ }
474
+ }
475
+ });
476
+ } else {
477
+ // Pre-activation: unmuted WOULD be rejected (hover isn't a gesture in
478
+ // Chrome's activation model) — start muted immediately with no
479
+ // rejection round-trip, and UNMUTE LIVE the instant the user's first
480
+ // click/keydown lands anywhere while this hover is still active.
481
+ el.muted = true;
482
+ (el.play?.() as Promise<void> | undefined)?.catch?.(() => {});
483
+ setHoverMutedFallback(true);
484
+ clearActivationWaiter();
485
+ const waiter = () => {
486
+ activationWaiterRef.current = null;
487
+ if (hoverActiveRef.current && generation === hoverGenerationRef.current) {
488
+ try { el.muted = false; el.volume = 0.5; } catch { /* ignore */ }
489
+ setHoverMutedFallback(false);
422
490
  }
423
- } catch { /* ignore */ }
491
+ };
492
+ activationWaiterRef.current = waiter;
493
+ activationWaiters.add(waiter);
424
494
  }
425
- : undefined;
426
- const handleHoverLeave = playOnHover
427
- ? () => {
428
- hoverActiveRef.current = false;
429
- try { hoverPlayerRef.current?.pause?.(); } catch { /* already torn down */ }
495
+ } catch { /* ignore */ }
496
+ }, []);
497
+ const stopHoverPlayback = useCallback(() => {
498
+ hoverActiveRef.current = false;
499
+ setHoverMutedFallback(false);
500
+ clearActivationWaiter();
501
+ try { hoverPlayerRef.current?.pause?.(); } catch { /* already torn down */ }
502
+ }, [clearActivationWaiter]);
503
+
504
+ // Explicit unmute affordance: the click IS the user activation the autoplay
505
+ // policy wants, so unmuting here always succeeds (and the window-level
506
+ // activation listener flips the module flag for every other player too).
507
+ const unmuteNow = useCallback((e: React.MouseEvent) => {
508
+ e.stopPropagation();
509
+ e.preventDefault();
510
+ const el = hoverPlayerRef.current;
511
+ try {
512
+ if (el) {
513
+ el.muted = false;
514
+ el.volume = 0.5;
515
+ (el.play?.() as Promise<void> | undefined)?.catch?.(() => {});
430
516
  }
431
- : undefined;
517
+ } catch { /* ignore */ }
518
+ setHoverMutedFallback(false);
519
+ }, []);
520
+
521
+ // Controlled hover mode (playWhenHovered): the HOST owns hover detection —
522
+ // e.g. the bite-strip card, where the detail overlay is part of the card and
523
+ // must NOT pause playback when the pointer moves onto it.
524
+ const hoverControlled = typeof playWhenHovered === 'boolean';
525
+ useEffect(() => {
526
+ if (!hoverControlled) return;
527
+ if (playWhenHovered) startHoverPlayback();
528
+ else stopHoverPlayback();
529
+ }, [hoverControlled, playWhenHovered, startHoverPlayback, stopHoverPlayback]);
530
+
531
+ const handleHoverEnter = playOnHover && !hoverControlled ? startHoverPlayback : undefined;
532
+ const handleHoverLeave = playOnHover && !hoverControlled ? stopHoverPlayback : undefined;
432
533
  // Raw SRT text is unusable without a custom overlay — and we just deleted
433
534
  // the 900-LOC custom-controls layer that owned that overlay. Consumers
434
535
  // pass `captionsUrl` (the API-side VTT conversion) alongside `srtContent`
@@ -503,16 +604,45 @@ function FilePlayer({
503
604
  </MuxPlayer>
504
605
  );
505
606
 
607
+ // Center unmute control — shown while hover playback runs muted because the
608
+ // autoplay policy blocked sound. Best-practice pattern (Mux / FB / IG):
609
+ // muted autoplay + an explicit unmute affordance, never forced sound.
610
+ // Styled to match media-chrome's center controls exactly (the play glyph in
611
+ // the same slot): plain large white glyph, no circle/border/background,
612
+ // slight dim on hover — so unmute reads as just another center control.
613
+ const unmuteBadge = hoverMutedFallback ? (
614
+ <button
615
+ type="button"
616
+ aria-label="Unmute"
617
+ title="Unmute"
618
+ onClick={unmuteNow}
619
+ className="absolute inset-0 z-10 m-auto flex h-14 w-14 items-center justify-center text-ods-text-primary transition-opacity hover:opacity-75"
620
+ >
621
+ <VolumeXmarkIcon size={56} />
622
+ </button>
623
+ ) : null;
624
+
506
625
  // MuxPlayerProps has no pointer-event props — the hover-play handlers live
507
- // on a full-size wrapper instead (only rendered in playOnHover mode).
508
- if (playOnHover) {
626
+ // on a full-size wrapper instead (only in UNCONTROLLED playOnHover mode;
627
+ // controlled playWhenHovered hosts own their hover detection). Either
628
+ // hover-capable mode gets a relative wrapper so the unmute badge can dock.
629
+ if (playOnHover && !hoverControlled) {
509
630
  return (
510
631
  <div
511
- className="w-full h-full"
632
+ className="relative w-full h-full"
512
633
  onPointerEnter={handleHoverEnter}
513
634
  onPointerLeave={handleHoverLeave}
514
635
  >
515
636
  {player}
637
+ {unmuteBadge}
638
+ </div>
639
+ );
640
+ }
641
+ if (hoverControlled) {
642
+ return (
643
+ <div className="relative w-full h-full">
644
+ {player}
645
+ {unmuteBadge}
516
646
  </div>
517
647
  );
518
648
  }
@@ -29,14 +29,14 @@ export function UserDisplay({ name, avatarUrl, subtitle, size = 32, shape = 'squ
29
29
  <div className="min-w-0 flex-1">
30
30
  <p className={cn(
31
31
  "font-['DM_Sans'] text-ods-text-primary truncate",
32
- compact ? 'text-sm leading-5 font-medium' : 'text-lg leading-[22px]',
32
+ compact ? 'text-xs leading-4 font-medium' : 'text-lg leading-[22px]',
33
33
  )}>
34
34
  {name}
35
35
  </p>
36
36
  {subtitle && (
37
37
  <span className={cn(
38
38
  "font-['DM_Sans'] text-ods-text-secondary truncate block",
39
- compact ? 'text-sm leading-5 font-medium' : 'text-md leading-[16px]',
39
+ compact ? 'text-xs leading-4 font-medium' : 'text-md leading-[16px]',
40
40
  )}>
41
41
  {subtitle}
42
42
  </span>
@@ -106,7 +106,7 @@ export interface CreateCaseStudyData {
106
106
  config?: Record<string, unknown> | null
107
107
  highlight_video_url?: string | null
108
108
  highlight_video_thumbnail?: string | null
109
- highlight_video_source?: string | null
109
+ highlight_video_source?: 'manual' | 'ai_generated' | null
110
110
  highlight_video_duration_ms?: number | null
111
111
  // SEO
112
112
  seo_title?: string