@flamingo-stack/openframe-frontend-core 0.0.396 → 0.0.397-snapshot.20260710042522

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 (142) hide show
  1. package/dist/{chunk-EKYE2VJU.js → chunk-3MMYFUWX.js} +4 -4
  2. package/dist/{chunk-HWJUWOXK.cjs → chunk-5IZDYGPU.cjs} +12 -12
  3. package/dist/{chunk-HWJUWOXK.cjs.map → chunk-5IZDYGPU.cjs.map} +1 -1
  4. package/dist/{chunk-OP3H7VB2.js → chunk-5SGIFYSA.js} +2 -2
  5. package/dist/{chunk-AODSKL5I.cjs → chunk-BIQH2W35.cjs} +9 -9
  6. package/dist/{chunk-AODSKL5I.cjs.map → chunk-BIQH2W35.cjs.map} +1 -1
  7. package/dist/{chunk-H4WIS6RJ.js → chunk-D3EAVXAQ.js} +2 -2
  8. package/dist/{chunk-XQILJ7FF.cjs → chunk-I4D6KJLI.cjs} +5 -5
  9. package/dist/{chunk-XQILJ7FF.cjs.map → chunk-I4D6KJLI.cjs.map} +1 -1
  10. package/dist/{chunk-TIC3UE6L.js → chunk-JAO2PKOU.js} +2 -2
  11. package/dist/{chunk-2PTEK3D6.js → chunk-KOIH637Y.js} +2 -2
  12. package/dist/{chunk-GA3R77TB.js → chunk-LHICE5MK.js} +2 -2
  13. package/dist/{chunk-CCV6Z3CL.cjs → chunk-MBWSBKSG.cjs} +26 -26
  14. package/dist/{chunk-CCV6Z3CL.cjs.map → chunk-MBWSBKSG.cjs.map} +1 -1
  15. package/dist/{chunk-PQXUY6SB.js → chunk-MH5U4ZMV.js} +2 -2
  16. package/dist/{chunk-SV7IJPIH.js → chunk-NLYOIYUB.js} +5456 -5015
  17. package/dist/chunk-NLYOIYUB.js.map +1 -0
  18. package/dist/{chunk-STEM5CPV.js → chunk-PKRHMZBN.js} +4 -4
  19. package/dist/{chunk-Y36KVLBX.cjs → chunk-PZIZOFPY.cjs} +1260 -819
  20. package/dist/chunk-PZIZOFPY.cjs.map +1 -0
  21. package/dist/{chunk-J2UHW4UM.js → chunk-QCUHIPG3.js} +2 -2
  22. package/dist/{chunk-KRMGVMIO.cjs → chunk-QOMEGWOM.cjs} +31 -31
  23. package/dist/{chunk-KRMGVMIO.cjs.map → chunk-QOMEGWOM.cjs.map} +1 -1
  24. package/dist/{chunk-HNXZFKDQ.cjs → chunk-THE2AFUN.cjs} +62 -62
  25. package/dist/{chunk-HNXZFKDQ.cjs.map → chunk-THE2AFUN.cjs.map} +1 -1
  26. package/dist/{chunk-NFRHBR7W.js → chunk-WR74UUVM.js} +4 -4
  27. package/dist/{chunk-RUYZ5QMK.cjs → chunk-WRS7OVIT.cjs} +37 -37
  28. package/dist/{chunk-RUYZ5QMK.cjs.map → chunk-WRS7OVIT.cjs.map} +1 -1
  29. package/dist/{chunk-FAUC7EA3.js → chunk-WX3MHQRH.js} +4 -4
  30. package/dist/{chunk-6DGQPKVA.cjs → chunk-XFOSQQKD.cjs} +26 -26
  31. package/dist/{chunk-6DGQPKVA.cjs.map → chunk-XFOSQQKD.cjs.map} +1 -1
  32. package/dist/{chunk-HEH47UTC.cjs → chunk-XQ35TKEM.cjs} +15 -15
  33. package/dist/{chunk-HEH47UTC.cjs.map → chunk-XQ35TKEM.cjs.map} +1 -1
  34. package/dist/{chunk-NAFW3LKI.cjs → chunk-Y2QAEMGF.cjs} +7 -7
  35. package/dist/{chunk-NAFW3LKI.cjs.map → chunk-Y2QAEMGF.cjs.map} +1 -1
  36. package/dist/{chunk-VSPNWVIE.cjs → chunk-YSI6FXZH.cjs} +59 -59
  37. package/dist/{chunk-VSPNWVIE.cjs.map → chunk-YSI6FXZH.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/entity-cards/blog-card.d.ts +6 -3
  41. package/dist/components/chat/entity-cards/blog-card.d.ts.map +1 -1
  42. package/dist/components/chat/entity-cards/case-study-card.d.ts +7 -3
  43. package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
  44. package/dist/components/chat/entity-cards/customer-interview-card.d.ts +6 -3
  45. package/dist/components/chat/entity-cards/customer-interview-card.d.ts.map +1 -1
  46. package/dist/components/chat/entity-cards/entity-portrait-card.d.ts +59 -0
  47. package/dist/components/chat/entity-cards/entity-portrait-card.d.ts.map +1 -0
  48. package/dist/components/chat/entity-cards/index.d.ts +3 -0
  49. package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
  50. package/dist/components/chat/entity-cards/investor-update-card.d.ts +6 -3
  51. package/dist/components/chat/entity-cards/investor-update-card.d.ts.map +1 -1
  52. package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts +4 -2
  53. package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts.map +1 -1
  54. package/dist/components/chat/entity-cards/program-card.d.ts +17 -5
  55. package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
  56. package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts +26 -0
  57. package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts.map +1 -0
  58. package/dist/components/chat/index.cjs +10 -2
  59. package/dist/components/chat/index.cjs.map +1 -1
  60. package/dist/components/chat/index.js +9 -1
  61. package/dist/components/chat/types/entities/program-types.d.ts +23 -0
  62. package/dist/components/chat/types/entities/program-types.d.ts.map +1 -1
  63. package/dist/components/contact/index.cjs +3 -3
  64. package/dist/components/contact/index.js +2 -2
  65. package/dist/components/docs/index.cjs +5 -5
  66. package/dist/components/docs/index.js +4 -4
  67. package/dist/components/embeds/index.cjs +3 -3
  68. package/dist/components/embeds/index.js +2 -2
  69. package/dist/components/faq/index.cjs +3 -3
  70. package/dist/components/faq/index.js +2 -2
  71. package/dist/components/features/cards-strip.d.ts.map +1 -1
  72. package/dist/components/features/index.cjs +6 -2
  73. package/dist/components/features/index.cjs.map +1 -1
  74. package/dist/components/features/index.d.ts +1 -0
  75. package/dist/components/features/index.d.ts.map +1 -1
  76. package/dist/components/features/index.js +5 -1
  77. package/dist/components/features/video-bites-strip.d.ts.map +1 -1
  78. package/dist/components/features/video-center-badge.d.ts +36 -0
  79. package/dist/components/features/video-center-badge.d.ts.map +1 -0
  80. package/dist/components/features/video.d.ts +7 -0
  81. package/dist/components/features/video.d.ts.map +1 -1
  82. package/dist/components/help-center-pages/index.cjs +20 -20
  83. package/dist/components/help-center-pages/index.js +11 -11
  84. package/dist/components/index.cjs +134 -120
  85. package/dist/components/index.cjs.map +1 -1
  86. package/dist/components/index.js +23 -9
  87. package/dist/components/index.js.map +1 -1
  88. package/dist/components/media-carousel.d.ts.map +1 -1
  89. package/dist/components/navigation/index.cjs +2 -2
  90. package/dist/components/navigation/index.js +1 -1
  91. package/dist/components/onboarding-guides/index.cjs +5 -5
  92. package/dist/components/onboarding-guides/index.js +4 -4
  93. package/dist/components/related-content/index.cjs +3 -3
  94. package/dist/components/related-content/index.js +2 -2
  95. package/dist/components/tickets/index.cjs +5 -5
  96. package/dist/components/tickets/index.js +4 -4
  97. package/dist/components/ui/crossfade-panels.d.ts +52 -0
  98. package/dist/components/ui/crossfade-panels.d.ts.map +1 -0
  99. package/dist/components/ui/index.cjs +12 -2
  100. package/dist/components/ui/index.cjs.map +1 -1
  101. package/dist/components/ui/index.d.ts +1 -0
  102. package/dist/components/ui/index.d.ts.map +1 -1
  103. package/dist/components/ui/index.js +11 -1
  104. package/dist/components/ui/tab-navigation.d.ts +6 -2
  105. package/dist/components/ui/tab-navigation.d.ts.map +1 -1
  106. package/dist/index.cjs +16 -2
  107. package/dist/index.cjs.map +1 -1
  108. package/dist/index.js +15 -1
  109. package/package.json +4 -4
  110. package/src/components/chat/entity-cards/blog-card.tsx +40 -7
  111. package/src/components/chat/entity-cards/case-study-card.tsx +32 -6
  112. package/src/components/chat/entity-cards/customer-interview-card.tsx +31 -6
  113. package/src/components/chat/entity-cards/entity-portrait-card.tsx +201 -0
  114. package/src/components/chat/entity-cards/index.ts +18 -0
  115. package/src/components/chat/entity-cards/investor-update-card.tsx +28 -2
  116. package/src/components/chat/entity-cards/onboarding-guide-card.tsx +30 -1
  117. package/src/components/chat/entity-cards/program-card.tsx +69 -17
  118. package/src/components/chat/entity-cards/use-cover-image-fallback.ts +50 -0
  119. package/src/components/chat/types/entities/program-types.ts +34 -0
  120. package/src/components/features/cards-strip.tsx +122 -8
  121. package/src/components/features/index.ts +1 -0
  122. package/src/components/features/video-bites-strip.tsx +77 -15
  123. package/src/components/features/video-center-badge.tsx +76 -0
  124. package/src/components/features/video.tsx +51 -7
  125. package/src/components/media-carousel.tsx +2 -4
  126. package/src/components/ui/crossfade-panels.tsx +88 -0
  127. package/src/components/ui/index.ts +1 -0
  128. package/src/components/ui/tab-navigation.tsx +31 -14
  129. package/src/styles/app-globals.css +23 -0
  130. package/dist/chunk-SV7IJPIH.js.map +0 -1
  131. package/dist/chunk-Y36KVLBX.cjs.map +0 -1
  132. /package/dist/{chunk-EKYE2VJU.js.map → chunk-3MMYFUWX.js.map} +0 -0
  133. /package/dist/{chunk-OP3H7VB2.js.map → chunk-5SGIFYSA.js.map} +0 -0
  134. /package/dist/{chunk-H4WIS6RJ.js.map → chunk-D3EAVXAQ.js.map} +0 -0
  135. /package/dist/{chunk-TIC3UE6L.js.map → chunk-JAO2PKOU.js.map} +0 -0
  136. /package/dist/{chunk-2PTEK3D6.js.map → chunk-KOIH637Y.js.map} +0 -0
  137. /package/dist/{chunk-GA3R77TB.js.map → chunk-LHICE5MK.js.map} +0 -0
  138. /package/dist/{chunk-PQXUY6SB.js.map → chunk-MH5U4ZMV.js.map} +0 -0
  139. /package/dist/{chunk-STEM5CPV.js.map → chunk-PKRHMZBN.js.map} +0 -0
  140. /package/dist/{chunk-J2UHW4UM.js.map → chunk-QCUHIPG3.js.map} +0 -0
  141. /package/dist/{chunk-NFRHBR7W.js.map → chunk-WR74UUVM.js.map} +0 -0
  142. /package/dist/{chunk-FAUC7EA3.js.map → chunk-WX3MHQRH.js.map} +0 -0
@@ -32,8 +32,7 @@
32
32
 
33
33
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
34
34
  import MuxPlayer from '@mux/mux-player-react';
35
- import { PlayIcon } from '../icons-v2-generated/media-playback/play-icon';
36
- import { VolumeXmarkIcon } from '../icons-v2-generated/audio-and-visual/volume-xmark-icon';
35
+ import { VideoPlayBadge, VideoUnmuteGlyph } from './video-center-badge';
37
36
  import { fetchPriorityProp } from '../../utils/fetch-priority';
38
37
 
39
38
  // =============================================================================
@@ -253,6 +252,12 @@ interface VideoFileProps extends VideoCommonProps {
253
252
  /** Hide the bottom control bar, keep only the CENTER play/pause control
254
253
  * (bite-strip cards per Figma). */
255
254
  centerControlsOnly?: boolean;
255
+ /** Render ONLY a lightweight first-frame preview — a metadata-only element
256
+ * seeked to `#t=0.1` (media-fragment trick; paints on iOS Safari where a
257
+ * fragmentless metadata load stays blank). No chrome, no playback, no
258
+ * MuxPlayer cost — the resting facade layer under strip cards when no
259
+ * poster asset exists. All player props are ignored. */
260
+ firstFrameOnly?: boolean;
256
261
  }
257
262
 
258
263
  interface VideoYouTubeProps extends VideoCommonProps {
@@ -273,6 +278,7 @@ interface VideoAutoProps extends VideoCommonProps {
273
278
  playOnHover?: boolean;
274
279
  playWhenHovered?: boolean;
275
280
  centerControlsOnly?: boolean;
281
+ firstFrameOnly?: boolean;
276
282
  }
277
283
 
278
284
  export type VideoProps = VideoFileProps | VideoYouTubeProps | VideoAutoProps;
@@ -297,6 +303,8 @@ export function Video(props: VideoProps): React.ReactElement | null {
297
303
  className={props.className}
298
304
  minimalControls={props.minimalControls}
299
305
  />
306
+ ) : 'firstFrameOnly' in props && props.firstFrameOnly ? (
307
+ <FirstFramePreview url={url} className={props.className} />
300
308
  ) : (
301
309
  <FilePlayer
302
310
  url={url}
@@ -373,6 +381,35 @@ function wrapWithLayout(
373
381
  }
374
382
  }
375
383
 
384
+ // -----------------------------------------------------------------------------
385
+ // First-frame preview — the `firstFrameOnly` facade branch
386
+ // -----------------------------------------------------------------------------
387
+
388
+ /** First-frame paint through the SAME FilePlayer/MuxPlayer pipeline as
389
+ * playback (one rendering path for every file source — HLS included, which a
390
+ * plain `<video>` couldn't decode in Chromium): a chromeless, muted,
391
+ * metadata-only instance whose `#t=0.1` media fragment seeks-and-paints the
392
+ * first frame (also fixes iOS Safari, which stays blank on a fragmentless
393
+ * metadata load). The transparent media background keeps the box showing the
394
+ * card surface (never black) until the frame decodes. */
395
+ function FirstFramePreview({ url, className }: { url: string; className?: string }): React.ReactElement {
396
+ return (
397
+ <div
398
+ aria-hidden
399
+ className="h-full w-full"
400
+ style={{ '--media-background-color': 'transparent' } as React.CSSProperties}
401
+ >
402
+ <FilePlayer
403
+ url={`${url}#t=0.1`}
404
+ preload="metadata"
405
+ muted
406
+ chromeless
407
+ className={className}
408
+ />
409
+ </div>
410
+ );
411
+ }
412
+
376
413
  // -----------------------------------------------------------------------------
377
414
  // File branch — MuxPlayer (handles both .m3u8 HLS and plain .mp4)
378
415
  // -----------------------------------------------------------------------------
@@ -389,6 +426,8 @@ interface FilePlayerProps {
389
426
  playOnHover?: boolean;
390
427
  playWhenHovered?: boolean;
391
428
  centerControlsOnly?: boolean;
429
+ /** Media preload hint — the firstFrameOnly facade passes 'metadata'. */
430
+ preload?: 'none' | 'metadata' | 'auto';
392
431
  className?: string;
393
432
  }
394
433
 
@@ -404,6 +443,7 @@ function FilePlayer({
404
443
  playOnHover,
405
444
  playWhenHovered,
406
445
  centerControlsOnly,
446
+ preload,
407
447
  className,
408
448
  }: FilePlayerProps): React.ReactElement {
409
449
  // centerControlsOnly: the center play button shows at REST only — while
@@ -551,6 +591,7 @@ function FilePlayer({
551
591
  src={url}
552
592
  poster={poster || undefined}
553
593
  streamType="on-demand"
594
+ preload={preload}
554
595
  playsInline
555
596
  muted={muted}
556
597
  preferCmcd="header"
@@ -616,9 +657,11 @@ function FilePlayer({
616
657
  aria-label="Unmute"
617
658
  title="Unmute"
618
659
  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"
660
+ // White at rest, ACCENT while the icon itself is hovered — the same
661
+ // hover language as every mux control icon (see app-globals.css).
662
+ className="absolute inset-0 z-10 m-auto flex h-14 w-14 items-center justify-center text-ods-text-primary transition-colors hover:text-ods-accent"
620
663
  >
621
- <VolumeXmarkIcon size={56} />
664
+ <VideoUnmuteGlyph />
622
665
  </button>
623
666
  ) : null;
624
667
 
@@ -882,9 +925,10 @@ function YouTubeFacadeInner({
882
925
  />
883
926
  </picture>
884
927
  <div className="absolute inset-0 flex items-center justify-center bg-ods-bg-inverse bg-opacity-20 transition-opacity duration-200 group-hover:bg-opacity-30">
885
- <span className="flex items-center justify-center w-16 h-16 rounded-full bg-ods-accent text-ods-text-on-accent shadow-lg transition-transform duration-200 group-hover:scale-110">
886
- <PlayIcon size={24} color="currentColor" className="ml-1" />
887
- </span>
928
+ {/* THE shared center play badge (video-center-badge.tsx) same disc
929
+ as strip cards / carousel thumbs / unmute chip; hero size + the
930
+ facade's hover-scale affordance. */}
931
+ <VideoPlayBadge size="lg" className="transition-transform duration-200 group-hover:scale-110 group-hover:text-ods-accent" />
888
932
  </div>
889
933
  </button>
890
934
  </div>
@@ -4,7 +4,7 @@ import { useState, useRef, useEffect, memo, useCallback } from 'react';
4
4
  import { cn } from "../utils/cn";
5
5
  import { MediaItem } from '../utils/media-carousel-utils-stub';
6
6
  import { Video, extractYouTubeId } from './features/video';
7
- import { PlayIcon } from './icons-v2-generated/media-playback/play-icon';
7
+ import { VideoPlayBadge } from './features/video-center-badge';
8
8
 
9
9
  // Navigation icons
10
10
  const ChevronLeftIcon = () => (
@@ -215,9 +215,7 @@ export const MediaCarousel = memo(function MediaCarousel({
215
215
  {/* Play icon overlay for videos */}
216
216
  {(item.type === 'video' || item.type === 'youtube') && (
217
217
  <div className="absolute inset-0 flex items-center justify-center bg-black/30">
218
- <div className="bg-black/70 rounded-full p-1">
219
- <PlayIcon size={12} color="white" />
220
- </div>
218
+ <VideoPlayBadge size="sm" />
221
219
  </div>
222
220
  )}
223
221
 
@@ -0,0 +1,88 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * <CrossfadePanels> — THE keep-mounted tab-panel stack: every panel stays in
5
+ * the DOM, stacked in the SAME grid cell, and switching is an opacity
6
+ * crossfade. Use it under any tab control (TabNavigation, TabSelector, …)
7
+ * whenever panels are content surfaces where height stability and a soft
8
+ * switch matter (strips, media rails, dashboards-in-view).
9
+ *
10
+ * Invariants encoded HERE exactly once (so no consumer re-derives them):
11
+ * - HEIGHT LOCK: grid items in one cell size the row to max(panel heights) —
12
+ * switching never shifts the page (no CLS), and surrounding layout treats
13
+ * the stack as one constant-height block.
14
+ * - FADE-THEN-HIDE: `visibility` transitions alongside `opacity`; per the CSS
15
+ * transitions spec's special visibility interpolation the outgoing panel
16
+ * stays paintable until the fade completes, THEN becomes `hidden` — no
17
+ * pop-out, and the settled hidden panel is fully removed from paint.
18
+ * - DEAD WHILE HIDDEN: `inert` + `aria-hidden` + `pointer-events-none` on
19
+ * inactive panels — no tab-stops, no screen-reader content, no hover/click
20
+ * leaks from the invisible layer (WCAG aria-hidden-focus).
21
+ * - MOTION RESPECT: `prefers-reduced-motion` collapses the fade to an
22
+ * instant swap.
23
+ * - The ACTIVE panel takes `z-[1]` so the incoming layer composites on top
24
+ * during the crossfade regardless of DOM order.
25
+ *
26
+ * This deliberately complements (not replaces) <TabContent>, which MOUNTS one
27
+ * panel at a time — right for heavy admin surfaces that should unmount when
28
+ * hidden. CrossfadePanels is for surfaces whose panels are cheap enough to
29
+ * keep alive and whose switch must be seamless.
30
+ *
31
+ * NOTE for marquee/animated panel content: hidden panels stay mounted and
32
+ * their animations keep running unless the content self-pauses. CardsStrip
33
+ * panels are fine (its rAF gates on near-viewport + visibility), but a
34
+ * consumer embedding heavier always-on content should pause it via its own
35
+ * visibility signal.
36
+ */
37
+
38
+ import React from 'react'
39
+ import { cn } from '../../utils/cn'
40
+
41
+ export interface CrossfadePanel {
42
+ id: string
43
+ content: React.ReactNode
44
+ }
45
+
46
+ export interface CrossfadePanelsProps {
47
+ /** Which panel is shown. Unknown id → all panels hidden (renders the
48
+ * stack's reserved height, nothing visible). */
49
+ activeId: string | undefined
50
+ panels: ReadonlyArray<CrossfadePanel>
51
+ /** Crossfade duration in ms. Default 300. */
52
+ durationMs?: number
53
+ className?: string
54
+ /** Extra classes for every panel wrapper (e.g. `min-w-0` is built in). */
55
+ panelClassName?: string
56
+ }
57
+
58
+ export function CrossfadePanels({
59
+ activeId,
60
+ panels,
61
+ durationMs = 300,
62
+ className,
63
+ panelClassName,
64
+ }: CrossfadePanelsProps) {
65
+ if (panels.length === 0) return null
66
+ return (
67
+ <div className={cn('grid w-full', className)}>
68
+ {panels.map(p => {
69
+ const active = p.id === activeId
70
+ return (
71
+ <div
72
+ key={p.id}
73
+ className={cn(
74
+ 'col-start-1 row-start-1 min-w-0 transition-[opacity,visibility] ease-out motion-reduce:transition-none',
75
+ active ? 'visible opacity-100 z-[1]' : 'invisible opacity-0 pointer-events-none',
76
+ panelClassName,
77
+ )}
78
+ style={{ transitionDuration: `${durationMs}ms` }}
79
+ aria-hidden={!active || undefined}
80
+ inert={!active || undefined}
81
+ >
82
+ {p.content}
83
+ </div>
84
+ )
85
+ })}
86
+ </div>
87
+ )
88
+ }
@@ -47,6 +47,7 @@ export * from './breadcrumb'
47
47
  export * from './dropdown-menu'
48
48
  export * from './menubar'
49
49
  export * from './navigation-menu'
50
+ export * from './crossfade-panels'
50
51
  export * from './tab-content'
51
52
  export * from './tab-navigation'
52
53
  // Feedback components
@@ -7,7 +7,8 @@ import { cn } from '../../utils/cn'
7
7
  export interface TabItem {
8
8
  id: string
9
9
  label: string
10
- icon: React.ComponentType<React.SVGProps<SVGSVGElement>>
10
+ /** Optional — text-only tabs (e.g. the homepage content-strip switcher) omit it. */
11
+ icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>
11
12
  component?: React.ComponentType<any>
12
13
  indicator?: 'success' | 'warning' | 'error'
13
14
  }
@@ -29,6 +30,9 @@ interface TabNavigationProps {
29
30
  showRightGradient?: boolean
30
31
  /** Force the left-edge gradient to always render, independent of scroll state. */
31
32
  showLeftGradient?: boolean
33
+ /** Tabs grow to share the bar's full width equally (Figma segmented-underline
34
+ * look, e.g. the 480px homepage strip switcher). Default: natural width. */
35
+ stretchTabs?: boolean
32
36
 
33
37
  // URL sync mode
34
38
  urlSync?: boolean | TabNavigationUrlSyncOptions
@@ -46,6 +50,7 @@ export function TabNavigation({
46
50
  shadowClassName,
47
51
  showRightGradient = false,
48
52
  showLeftGradient = false,
53
+ stretchTabs = false,
49
54
  urlSync = false,
50
55
  defaultTab,
51
56
  children
@@ -222,24 +227,36 @@ export function TabNavigation({
222
227
  className={cn(
223
228
  "flex gap-[var(--spacing-system-xxs)] items-center justify-center p-[var(--spacing-system-m)] relative shrink-0 cursor-pointer",
224
229
  "transition-all duration-200 bg-transparent border-none outline-none",
230
+ stretchTabs && 'flex-1',
225
231
  isActive
226
232
  ? 'bg-gradient-to-b from-[rgba(255,255,255,0)] to-[rgba(255,255,255,0.1)]'
227
233
  : 'hover:bg-gradient-to-b hover:from-[rgba(255,255,255,0)] hover:to-[rgba(255,255,255,0.1)]'
228
234
  )}
229
235
  >
230
- <div className="relative flex items-center justify-center">
231
- <tab.icon
232
- className={cn("h-4 w-4 md:h-6 md:w-6 transition-colors", isActive ? 'text-ods-accent' : 'text-ods-text-secondary')}
233
- />
234
- {tab.indicator && (
235
- <div className={cn(
236
- "absolute right-0 top-[-3px] w-3 h-3 rounded-full border-2 border-ods-bg",
237
- tab.indicator === 'error' && 'bg-ods-error',
238
- tab.indicator === 'warning' && 'bg-ods-accent',
239
- tab.indicator === 'success' && 'bg-green-500'
240
- )} />
241
- )}
242
- </div>
236
+ {tab.icon ? (
237
+ <div className="relative flex items-center justify-center">
238
+ <tab.icon
239
+ className={cn("h-4 w-4 md:h-6 md:w-6 transition-colors", isActive ? 'text-ods-accent' : 'text-ods-text-secondary')}
240
+ />
241
+ {tab.indicator && (
242
+ <div className={cn(
243
+ "absolute right-0 top-[-3px] w-3 h-3 rounded-full border-2 border-ods-bg",
244
+ tab.indicator === 'error' && 'bg-ods-error',
245
+ tab.indicator === 'warning' && 'bg-ods-accent',
246
+ tab.indicator === 'success' && 'bg-green-500'
247
+ )} />
248
+ )}
249
+ </div>
250
+ ) : tab.indicator ? (
251
+ // Text-only tabs keep their status badge — inline dot before
252
+ // the label (there's no icon corner to anchor to).
253
+ <div className={cn(
254
+ "w-3 h-3 shrink-0 rounded-full border-2 border-ods-bg",
255
+ tab.indicator === 'error' && 'bg-ods-error',
256
+ tab.indicator === 'warning' && 'bg-ods-accent',
257
+ tab.indicator === 'success' && 'bg-green-500'
258
+ )} />
259
+ ) : null}
243
260
 
244
261
  <span className={cn(
245
262
  "text-h4 whitespace-nowrap transition-colors",
@@ -182,3 +182,26 @@ body {
182
182
  --ring: 180 100% 69%;
183
183
  --radius: 0.5rem;
184
184
  }
185
+
186
+ /* ============================================================================
187
+ Video center-control identity — ONE look across every video surface:
188
+ a bare solid-white glyph, NO background disc (see
189
+ components/features/video-center-badge.tsx for the React consumers).
190
+ This strips media-chrome's default dark circle off MuxPlayer's big
191
+ center pre-play button so full players match the strip/facade glyphs.
192
+ ========================================================================== */
193
+ /* REAL players (any mounted MuxPlayer): icons are white at rest and turn
194
+ the platform ACCENT color while the icon's control is hovered — every
195
+ media-chrome control carries the `button` part token, so this covers
196
+ center play, bar play/mute/fullscreen/etc. uniformly. */
197
+ mux-player::part(button):hover {
198
+ --media-icon-color: var(--ods-accent, var(--color-accent-primary));
199
+ }
200
+ mux-player::part(center play button) {
201
+ background: transparent;
202
+ filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.5));
203
+ }
204
+ mux-player::part(center play button):hover {
205
+ background: transparent;
206
+ opacity: 0.85;
207
+ }