@flamingo-stack/openframe-frontend-core 0.0.397 → 0.0.398
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-T5F54MN4.cjs → chunk-2DGA5LHT.cjs} +19 -19
- package/dist/{chunk-T5F54MN4.cjs.map → chunk-2DGA5LHT.cjs.map} +1 -1
- package/dist/{chunk-K5FRWPWF.js → chunk-2TBXIW7L.js} +3 -3
- package/dist/{chunk-XNAYZHPK.cjs → chunk-4BVRLCCA.cjs} +73 -73
- package/dist/{chunk-XNAYZHPK.cjs.map → chunk-4BVRLCCA.cjs.map} +1 -1
- package/dist/{chunk-WWHPTP4B.js → chunk-4KWIQEZT.js} +2 -2
- package/dist/{chunk-DB6CUWTL.cjs → chunk-7Q2DZEUA.cjs} +32 -32
- package/dist/{chunk-DB6CUWTL.cjs.map → chunk-7Q2DZEUA.cjs.map} +1 -1
- package/dist/{chunk-CK5UZLIN.js → chunk-B2K5COVE.js} +3305 -3189
- package/dist/chunk-B2K5COVE.js.map +1 -0
- package/dist/{chunk-OAENYOKC.cjs → chunk-C4DHYDXB.cjs} +12 -12
- package/dist/{chunk-OAENYOKC.cjs.map → chunk-C4DHYDXB.cjs.map} +1 -1
- package/dist/{chunk-HPQKX5IW.cjs → chunk-FZIMFLJG.cjs} +15 -15
- package/dist/{chunk-HPQKX5IW.cjs.map → chunk-FZIMFLJG.cjs.map} +1 -1
- package/dist/{chunk-ZFK4HHRX.js → chunk-G42EXM23.js} +2 -2
- package/dist/{chunk-WCDM226S.js → chunk-GKRJOS6S.js} +3 -3
- package/dist/{chunk-Z6PAD2UN.cjs → chunk-HMTOQGYU.cjs} +7 -7
- package/dist/{chunk-Z6PAD2UN.cjs.map → chunk-HMTOQGYU.cjs.map} +1 -1
- package/dist/{chunk-743FCO64.cjs → chunk-JWBRDNYH.cjs} +31 -31
- package/dist/{chunk-743FCO64.cjs.map → chunk-JWBRDNYH.cjs.map} +1 -1
- package/dist/{chunk-BB7TZ6KS.cjs → chunk-LBDKAMOH.cjs} +75 -75
- package/dist/{chunk-BB7TZ6KS.cjs.map → chunk-LBDKAMOH.cjs.map} +1 -1
- package/dist/{chunk-LWNPMLIH.cjs → chunk-LPH2JDOW.cjs} +56 -2
- package/dist/chunk-LPH2JDOW.cjs.map +1 -0
- package/dist/{chunk-3E4BDAHJ.js → chunk-MGJZQ3EE.js} +2 -2
- package/dist/{chunk-2OOZASQE.js → chunk-MNQVBME4.js} +3 -3
- package/dist/{chunk-VTR4QDL2.js → chunk-MQXUIYYN.js} +4 -4
- package/dist/{chunk-IVXOR2IA.js → chunk-N4R7TRF3.js} +2 -2
- package/dist/{chunk-LQHMXPOJ.js → chunk-N7YBNHVT.js} +563 -509
- package/dist/chunk-N7YBNHVT.js.map +1 -0
- package/dist/{chunk-55SNCMFS.cjs → chunk-PE37UDPF.cjs} +40 -40
- package/dist/{chunk-55SNCMFS.cjs.map → chunk-PE37UDPF.cjs.map} +1 -1
- package/dist/{chunk-J6TAK6HE.js → chunk-URODM26E.js} +6 -6
- package/dist/{chunk-JNU3YPLP.js → chunk-VDDNJ6XC.js} +5 -5
- package/dist/{chunk-ZUKBC2ZX.cjs → chunk-VVO7AZKZ.cjs} +306 -190
- package/dist/chunk-VVO7AZKZ.cjs.map +1 -0
- package/dist/{chunk-2U3YPWHG.cjs → chunk-WBLRUO3F.cjs} +3 -3
- package/dist/{chunk-2U3YPWHG.cjs.map → chunk-WBLRUO3F.cjs.map} +1 -1
- package/dist/{chunk-43C5ECNT.cjs → chunk-WJSUUOPU.cjs} +40 -40
- package/dist/{chunk-43C5ECNT.cjs.map → chunk-WJSUUOPU.cjs.map} +1 -1
- package/dist/{chunk-D4F7ZLJG.js → chunk-WXOIEYXW.js} +5 -5
- package/dist/{chunk-SBWTMJPQ.cjs → chunk-XYT5PIKZ.cjs} +5 -5
- package/dist/{chunk-SBWTMJPQ.cjs.map → chunk-XYT5PIKZ.cjs.map} +1 -1
- package/dist/{chunk-TTHTO5H5.js → chunk-Y3FREU4B.js} +7 -7
- package/dist/{chunk-M6NPDNVB.cjs → chunk-YEWGFWNM.cjs} +29 -29
- package/dist/{chunk-M6NPDNVB.cjs.map → chunk-YEWGFWNM.cjs.map} +1 -1
- package/dist/{chunk-KQESCGVG.js → chunk-ZSZRJWY3.js} +2 -2
- package/dist/components/case-studies/index.cjs +14 -14
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +8 -4
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.d.ts +1 -0
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +7 -3
- package/dist/components/features/video-bites-strip.d.ts.map +1 -1
- package/dist/components/features/video-center-badge.d.ts +40 -0
- package/dist/components/features/video-center-badge.d.ts.map +1 -0
- package/dist/components/features/video.d.ts +7 -4
- package/dist/components/features/video.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/icons/index.cjs +6 -2
- package/dist/components/icons/index.cjs.map +1 -1
- package/dist/components/icons/index.d.ts +2 -0
- package/dist/components/icons/index.d.ts.map +1 -1
- package/dist/components/icons/index.js +5 -1
- package/dist/components/icons/play-filled-icon.d.ts +15 -0
- package/dist/components/icons/play-filled-icon.d.ts.map +1 -0
- package/dist/components/icons/volume-off-filled-icon.d.ts +14 -0
- package/dist/components/icons/volume-off-filled-icon.d.ts.map +1 -0
- package/dist/components/index.cjs +152 -142
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +22 -12
- package/dist/components/index.js.map +1 -1
- package/dist/components/media-carousel.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/crossfade-panels.d.ts +52 -0
- package/dist/components/ui/crossfade-panels.d.ts.map +1 -0
- package/dist/components/ui/index.cjs +6 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +5 -3
- package/dist/components/ui/tab-navigation.d.ts +6 -2
- package/dist/components/ui/tab-navigation.d.ts.map +1 -1
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +14 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -3
- package/package.json +4 -4
- package/src/components/features/index.ts +1 -0
- package/src/components/features/video-bites-strip.tsx +73 -15
- package/src/components/features/video-center-badge.tsx +77 -0
- package/src/components/features/video.tsx +51 -28
- package/src/components/icons/index.ts +2 -0
- package/src/components/icons/play-filled-icon.tsx +36 -0
- package/src/components/icons/volume-off-filled-icon.tsx +38 -0
- package/src/components/media-carousel.tsx +2 -4
- package/src/components/ui/crossfade-panels.tsx +88 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/tab-navigation.tsx +31 -14
- package/src/styles/app-globals.css +23 -0
- package/dist/chunk-CK5UZLIN.js.map +0 -1
- package/dist/chunk-LQHMXPOJ.js.map +0 -1
- package/dist/chunk-LWNPMLIH.cjs.map +0 -1
- package/dist/chunk-ZUKBC2ZX.cjs.map +0 -1
- /package/dist/{chunk-K5FRWPWF.js.map → chunk-2TBXIW7L.js.map} +0 -0
- /package/dist/{chunk-WWHPTP4B.js.map → chunk-4KWIQEZT.js.map} +0 -0
- /package/dist/{chunk-ZFK4HHRX.js.map → chunk-G42EXM23.js.map} +0 -0
- /package/dist/{chunk-WCDM226S.js.map → chunk-GKRJOS6S.js.map} +0 -0
- /package/dist/{chunk-3E4BDAHJ.js.map → chunk-MGJZQ3EE.js.map} +0 -0
- /package/dist/{chunk-2OOZASQE.js.map → chunk-MNQVBME4.js.map} +0 -0
- /package/dist/{chunk-VTR4QDL2.js.map → chunk-MQXUIYYN.js.map} +0 -0
- /package/dist/{chunk-IVXOR2IA.js.map → chunk-N4R7TRF3.js.map} +0 -0
- /package/dist/{chunk-J6TAK6HE.js.map → chunk-URODM26E.js.map} +0 -0
- /package/dist/{chunk-JNU3YPLP.js.map → chunk-VDDNJ6XC.js.map} +0 -0
- /package/dist/{chunk-D4F7ZLJG.js.map → chunk-WXOIEYXW.js.map} +0 -0
- /package/dist/{chunk-TTHTO5H5.js.map → chunk-Y3FREU4B.js.map} +0 -0
- /package/dist/{chunk-KQESCGVG.js.map → chunk-ZSZRJWY3.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -186,6 +186,7 @@ import {
|
|
|
186
186
|
ContextItemsSkeleton,
|
|
187
187
|
ContextMenuRow,
|
|
188
188
|
CreateOrganizationForm,
|
|
189
|
+
CrossfadePanels,
|
|
189
190
|
CursorPagination,
|
|
190
191
|
CursorPaginationSimple,
|
|
191
192
|
CustomerInterviewCard,
|
|
@@ -606,7 +607,9 @@ import {
|
|
|
606
607
|
VideoBitesDisplay,
|
|
607
608
|
VideoBitesStrip,
|
|
608
609
|
VideoClipsSection,
|
|
610
|
+
VideoPlayBadge,
|
|
609
611
|
VideoSourceSelector,
|
|
612
|
+
VideoUnmuteGlyph,
|
|
610
613
|
ViewToggle,
|
|
611
614
|
WaitlistForm,
|
|
612
615
|
WarningBlock,
|
|
@@ -913,7 +916,7 @@ import {
|
|
|
913
916
|
validateEmailDomain,
|
|
914
917
|
validateEmailDomainList,
|
|
915
918
|
validatePhoneNumber
|
|
916
|
-
} from "./chunk-
|
|
919
|
+
} from "./chunk-B2K5COVE.js";
|
|
917
920
|
import {
|
|
918
921
|
CommandApprovalToast,
|
|
919
922
|
DEFAULT_MIN_FILL_MS,
|
|
@@ -1024,7 +1027,7 @@ import {
|
|
|
1024
1027
|
validateSchema,
|
|
1025
1028
|
validateVariables,
|
|
1026
1029
|
variablesToUrlParams
|
|
1027
|
-
} from "./chunk-
|
|
1030
|
+
} from "./chunk-ZSZRJWY3.js";
|
|
1028
1031
|
import {
|
|
1029
1032
|
PLAY_ICON_PATH
|
|
1030
1033
|
} from "./chunk-OYGFWWLQ.js";
|
|
@@ -1167,6 +1170,7 @@ import {
|
|
|
1167
1170
|
PackageIcon,
|
|
1168
1171
|
PatchPolicyIcon,
|
|
1169
1172
|
PilotIcon,
|
|
1173
|
+
PlayFilledIcon,
|
|
1170
1174
|
PlusCircleIcon,
|
|
1171
1175
|
PodbeanIcon,
|
|
1172
1176
|
PoliciesIcon,
|
|
@@ -1209,6 +1213,7 @@ import {
|
|
|
1209
1213
|
VendorDirectoryIcon,
|
|
1210
1214
|
VendorShowcaseMainIcon,
|
|
1211
1215
|
VendorsIcon,
|
|
1216
|
+
VolumeOffFilledIcon,
|
|
1212
1217
|
WhatsAppIcon,
|
|
1213
1218
|
WindowsIcon,
|
|
1214
1219
|
XIcon,
|
|
@@ -1216,7 +1221,7 @@ import {
|
|
|
1216
1221
|
YouTubeIcon,
|
|
1217
1222
|
getDeviceTypeIcon,
|
|
1218
1223
|
renderSvgIcon
|
|
1219
|
-
} from "./chunk-
|
|
1224
|
+
} from "./chunk-N7YBNHVT.js";
|
|
1220
1225
|
import "./chunk-W72U7OU7.js";
|
|
1221
1226
|
import {
|
|
1222
1227
|
cn,
|
|
@@ -1439,6 +1444,7 @@ export {
|
|
|
1439
1444
|
CopyIcon,
|
|
1440
1445
|
CpuIcon,
|
|
1441
1446
|
CreateOrganizationForm,
|
|
1447
|
+
CrossfadePanels,
|
|
1442
1448
|
CursorPagination,
|
|
1443
1449
|
CursorPaginationSimple,
|
|
1444
1450
|
CustomExternalLinkIcon,
|
|
@@ -1794,6 +1800,7 @@ export {
|
|
|
1794
1800
|
PilotIcon,
|
|
1795
1801
|
PlatformBadge,
|
|
1796
1802
|
PlatformFilterComponent,
|
|
1803
|
+
PlayFilledIcon,
|
|
1797
1804
|
PlusCircleIcon,
|
|
1798
1805
|
PodbeanIcon,
|
|
1799
1806
|
PoliciesIcon,
|
|
@@ -2018,8 +2025,11 @@ export {
|
|
|
2018
2025
|
VideoBitesDisplay,
|
|
2019
2026
|
VideoBitesStrip,
|
|
2020
2027
|
VideoClipsSection,
|
|
2028
|
+
VideoPlayBadge,
|
|
2021
2029
|
VideoSourceSelector,
|
|
2030
|
+
VideoUnmuteGlyph,
|
|
2022
2031
|
ViewToggle,
|
|
2032
|
+
VolumeOffFilledIcon,
|
|
2023
2033
|
WaitlistForm,
|
|
2024
2034
|
WarningBlock,
|
|
2025
2035
|
WhatIShippedCard,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flamingo-stack/openframe-frontend-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.398",
|
|
4
4
|
"description": "Shared design system and components for all Flamingo platforms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -257,9 +257,9 @@
|
|
|
257
257
|
"test:ui": "vitest --ui",
|
|
258
258
|
"test:run": "vitest run",
|
|
259
259
|
"test:coverage": "vitest run --coverage",
|
|
260
|
-
"yalc:watch": "NODE_OPTIONS='--max-old-space-size=4096' nodemon --watch src --watch tsconfig.json --watch tsup.config.ts --watch tailwind.config.ts --watch package.json -e ts,tsx,json,css --ignore dist --ignore node_modules --ignore '*.lock' --ignore .yalc --ignore yalc.lock --exec 'rm -rf dist && tsup && tsc -p tsconfig.declarations.json && yalc push --changed'",
|
|
261
|
-
"yalc:push": "yalc push",
|
|
262
|
-
"yalc:publish": "yalc publish",
|
|
260
|
+
"yalc:watch": "NODE_OPTIONS='--max-old-space-size=4096' nodemon --watch src --watch tsconfig.json --watch tsup.config.ts --watch tailwind.config.ts --watch package.json -e ts,tsx,json,css --ignore dist --ignore node_modules --ignore '*.lock' --ignore .yalc --ignore yalc.lock --exec 'rm -rf dist && tsup && tsc -p tsconfig.declarations.json && yalc push --changed ${YALC_STORE_FOLDER:+--store-folder \"$YALC_STORE_FOLDER\"}'",
|
|
261
|
+
"yalc:push": "yalc push ${YALC_STORE_FOLDER:+--store-folder \"$YALC_STORE_FOLDER\"}",
|
|
262
|
+
"yalc:publish": "yalc publish ${YALC_STORE_FOLDER:+--store-folder \"$YALC_STORE_FOLDER\"}",
|
|
263
263
|
"generate:icons": "node scripts/generate-icons.mjs",
|
|
264
264
|
"storybook": "storybook dev -p 6006",
|
|
265
265
|
"build-storybook": "storybook build"
|
|
@@ -60,6 +60,7 @@ export * from './warning-block'
|
|
|
60
60
|
// `video-bites-shared` is the SERVER-SAFE leaf (also its own published subpath).
|
|
61
61
|
export * from './cards-strip'
|
|
62
62
|
export * from './video'
|
|
63
|
+
export * from './video-center-badge'
|
|
63
64
|
export * from './video-ratio-tabs'
|
|
64
65
|
export * from './video-bites-shared'
|
|
65
66
|
export * from './video-bites-strip'
|
|
@@ -36,9 +36,12 @@ import {
|
|
|
36
36
|
sortBitesByCreatedAtDesc,
|
|
37
37
|
type VideoBiteStripProfile,
|
|
38
38
|
} from './video-bites-shared';
|
|
39
|
+
import Image from '../../embed-shims/next-image';
|
|
39
40
|
import { CardsStrip, STRIP_CELL_MAX_WIDTH } from './cards-strip';
|
|
41
|
+
import { useCoverImageFallback } from '../chat/entity-cards/use-cover-image-fallback';
|
|
40
42
|
import { UserDisplay } from '../user-display';
|
|
41
43
|
import { Chevron02RightIcon } from '../icons-v2-generated/arrows/chevron-02-right-icon';
|
|
44
|
+
import { VideoPlayBadge } from './video-center-badge';
|
|
42
45
|
|
|
43
46
|
// NOTE: the title constant / profile adapter / sort comparator live in the
|
|
44
47
|
// server-safe leaf `video-bites-shared.ts` (its own published subpath). The
|
|
@@ -252,7 +255,23 @@ export function VideoBiteCard({
|
|
|
252
255
|
io.observe(el);
|
|
253
256
|
return () => io.disconnect();
|
|
254
257
|
}, [gateControlled]);
|
|
255
|
-
const
|
|
258
|
+
const showMedia = gateControlled ? playerMounted : isNear;
|
|
259
|
+
|
|
260
|
+
// Player mounts at NEAR-VIEWPORT (not on first hover) so hover playback is
|
|
261
|
+
// INSTANT for any card the user can reach — mounting on activation made
|
|
262
|
+
// freshly-scrolled cards stall for seconds while MuxPlayer initialized and
|
|
263
|
+
// buffered. Always-mounted players are visually safe now that every bite
|
|
264
|
+
// carries a poster: the player renders with a transparent media background
|
|
265
|
+
// over the card's poster layer, so a not-yet-decoded player never shows as
|
|
266
|
+
// a black rectangle (the original wrap-seam flash came from POSTERLESS
|
|
267
|
+
// mounted players, not from mounting itself).
|
|
268
|
+
const showPlayer = showMedia;
|
|
269
|
+
|
|
270
|
+
// Poster resolution — THE shared entity-card cover fallback chain
|
|
271
|
+
// (use-cover-image-fallback): the bite's real thumbnail_url, dropped on
|
|
272
|
+
// load error. When it resolves to null (no thumbnail yet, or it failed)
|
|
273
|
+
// the media zone falls back to the first-frame <video> facade below.
|
|
274
|
+
const { src: posterSrc, onError: onPosterError } = useCoverImageFallback(bite.thumbnail_url);
|
|
256
275
|
|
|
257
276
|
const navigate = () => {
|
|
258
277
|
if (bite.onNavigate) bite.onNavigate();
|
|
@@ -342,26 +361,65 @@ export function VideoBiteCard({
|
|
|
342
361
|
// same as the old single-box layout). Editor mode: width-driven.
|
|
343
362
|
style={{ aspectRatio: cssAspect, ...(height !== undefined ? { height, maxWidth: STRIP_CELL_MAX_WIDTH } : {}) }}
|
|
344
363
|
>
|
|
345
|
-
{
|
|
364
|
+
{showMedia ? (
|
|
346
365
|
// Clones: `inert` (not just the wrapper's aria-hidden) — the player's
|
|
347
366
|
// shadow-DOM center control is otherwise still tab-reachable inside a
|
|
348
367
|
// hidden subtree (axe aria-hidden-focus). Hover preview on clones
|
|
349
368
|
// keeps working: playback is driven imperatively from the CARD's own
|
|
350
369
|
// pointer handlers, never from focus/clicks on the player chrome.
|
|
351
370
|
<div className="absolute inset-0" inert={isClone || undefined}>
|
|
352
|
-
{/*
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
371
|
+
{/* Poster layer. Preferred: the bite's REAL thumbnail (generated at
|
|
372
|
+
Vizard-ingestion time by vizard-persistence-utils via Shotstack
|
|
373
|
+
capture — the provider sends no cover), resolved through the
|
|
374
|
+
shared cover fallback chain. Fallback when it's absent or fails
|
|
375
|
+
to load: the <Video> component's `firstFrameOnly` facade paints
|
|
376
|
+
the `#t=0.1` frame. Either way the box is never a black
|
|
377
|
+
rectangle, and original + clone paint the IDENTICAL image so the
|
|
378
|
+
marquee's wrap seam stays pixel-invisible. */}
|
|
379
|
+
{posterSrc ? (
|
|
380
|
+
<Image
|
|
381
|
+
src={posterSrc}
|
|
382
|
+
alt=""
|
|
383
|
+
fill
|
|
384
|
+
unoptimized
|
|
385
|
+
onError={onPosterError}
|
|
386
|
+
className="object-cover"
|
|
387
|
+
/>
|
|
388
|
+
) : (
|
|
389
|
+
<Video kind="file" url={bite.url} firstFrameOnly layout="fill" />
|
|
390
|
+
)}
|
|
391
|
+
|
|
392
|
+
{/* Play affordance for every non-playing state (resting poster,
|
|
393
|
+
facade, mounted-but-paused): <VideoPlayBadge> — a pixel replica
|
|
394
|
+
of MuxPlayer's pre-play center button (media-chrome's own PAUSED
|
|
395
|
+
center control is a different, smaller bare glyph, so the
|
|
396
|
+
mounted player runs chromeless and the replica keeps all states
|
|
397
|
+
identical to the mux button). Hidden while playing. */}
|
|
398
|
+
{!isActive && <VideoPlayBadge className="absolute inset-0 z-10 m-auto" />}
|
|
399
|
+
{showPlayer && (
|
|
400
|
+
// `--media-background-color: transparent` (inherited into
|
|
401
|
+
// media-chrome) — the freshly-mounted player must not blank the
|
|
402
|
+
// facade behind it with its default black fill while it loads.
|
|
403
|
+
<div
|
|
404
|
+
className="absolute inset-0"
|
|
405
|
+
style={{ '--media-background-color': 'transparent' } as React.CSSProperties}
|
|
406
|
+
>
|
|
407
|
+
{/* CONTROLLED hover playback keyed to CARD hover (`isActive`): the
|
|
408
|
+
detail overlay is part of the card, so moving the pointer onto
|
|
409
|
+
it keeps playing. Sound at 50% (pre-activation: muted start +
|
|
410
|
+
live unmute on the user's first gesture); CHROMELESS — the
|
|
411
|
+
card's mux-replica badge above is the play affordance for
|
|
412
|
+
every non-playing state. */}
|
|
413
|
+
<Video
|
|
414
|
+
kind="file"
|
|
415
|
+
url={bite.url}
|
|
416
|
+
poster={bite.thumbnail_url}
|
|
417
|
+
playWhenHovered={isActive}
|
|
418
|
+
chromeless
|
|
419
|
+
layout="fill"
|
|
420
|
+
/>
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
365
423
|
</div>
|
|
366
424
|
) : (
|
|
367
425
|
// Aspect-matched placeholder until the card nears the viewport (CLS-free).
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* THE center media glyphs — the single visual identity for every play/unmute
|
|
5
|
+
* affordance over video, across the whole project: a SOLID WHITE GLYPH
|
|
6
|
+
* (`text-ods-text-primary`), NO background disc, with a soft drop-shadow for
|
|
7
|
+
* legibility over arbitrary frames. Accent color appears ONLY while the
|
|
8
|
+
* icon's own control is hovered (hosts pass `group-hover:text-ods-accent`;
|
|
9
|
+
* mux-player controls get the same via `::part(button):hover` in
|
|
10
|
+
* styles/app-globals.css).
|
|
11
|
+
*
|
|
12
|
+
* Glyphs come from THE icon system — never inline SVG paths here:
|
|
13
|
+
* PlayFilledIcon / VolumeOffFilledIcon (components/icons — the FILLED
|
|
14
|
+
* media-chrome-matching glyphs, so custom affordances are pixel-identical to
|
|
15
|
+
* MuxPlayer's native center button on real players).
|
|
16
|
+
*
|
|
17
|
+
* Consumers:
|
|
18
|
+
* - <VideoBiteCard> resting/paused posters (md)
|
|
19
|
+
* - <YouTubeFacade> poster (lg)
|
|
20
|
+
* - <MediaCarousel> video thumbnails (sm)
|
|
21
|
+
* - <FilePlayer>'s unmute chip (VideoUnmuteGlyph, md)
|
|
22
|
+
* - MuxPlayer's native center pre-play button is stripped to the same bare
|
|
23
|
+
* glyph language via CSS (see styles/app-globals.css).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import React from 'react'
|
|
27
|
+
import { PlayFilledIcon } from '../icons/play-filled-icon'
|
|
28
|
+
import { VolumeOffFilledIcon } from '../icons/volume-off-filled-icon'
|
|
29
|
+
import { cn } from '../../utils/cn'
|
|
30
|
+
|
|
31
|
+
export type VideoCenterBadgeSize = 'sm' | 'md' | 'lg'
|
|
32
|
+
|
|
33
|
+
const SIZES: Record<VideoCenterBadgeSize, number> = {
|
|
34
|
+
sm: 20, // gallery thumbnails
|
|
35
|
+
md: 56, // strip cards + unmute chip
|
|
36
|
+
lg: 64, // hero facades
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Soft legibility shadow — NOT a background (per design: bare glyphs). */
|
|
40
|
+
const GLYPH_SHADOW = 'drop-shadow(0 1px 4px rgba(0,0,0,0.5))'
|
|
41
|
+
|
|
42
|
+
export interface VideoPlayBadgeProps {
|
|
43
|
+
size?: VideoCenterBadgeSize
|
|
44
|
+
className?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Bare play glyph (the icon system's PlayIcon). Decorative
|
|
48
|
+
* (`pointer-events-none`) — the host surface owns activation. */
|
|
49
|
+
export function VideoPlayBadge({ size = 'md', className }: VideoPlayBadgeProps): React.ReactElement {
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={cn('pointer-events-none flex items-center justify-center text-ods-text-primary transition-colors', className)}
|
|
53
|
+
style={{ filter: GLYPH_SHADOW }}
|
|
54
|
+
>
|
|
55
|
+
<PlayFilledIcon size={SIZES[size]} color="currentColor" />
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface VideoUnmuteGlyphProps {
|
|
61
|
+
size?: VideoCenterBadgeSize
|
|
62
|
+
className?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Bare muted-volume glyph (the icon system's VolumeOffFilledIcon). Rendered
|
|
66
|
+
* by FilePlayer's unmute button; exported here so play + unmute share one
|
|
67
|
+
* identity. */
|
|
68
|
+
export function VideoUnmuteGlyph({ size = 'md', className }: VideoUnmuteGlyphProps): React.ReactElement {
|
|
69
|
+
return (
|
|
70
|
+
<VolumeOffFilledIcon
|
|
71
|
+
size={SIZES[size]}
|
|
72
|
+
color="currentColor"
|
|
73
|
+
className={className}
|
|
74
|
+
style={{ filter: GLYPH_SHADOW }}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -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 {
|
|
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
|
// =============================================================================
|
|
@@ -250,9 +249,12 @@ interface VideoFileProps extends VideoCommonProps {
|
|
|
250
249
|
* true → start hover playback, false → pause. When provided, the internal
|
|
251
250
|
* pointer handlers are disabled. */
|
|
252
251
|
playWhenHovered?: boolean;
|
|
253
|
-
/**
|
|
254
|
-
* (
|
|
255
|
-
|
|
252
|
+
/** Render ONLY a lightweight first-frame preview — a metadata-only element
|
|
253
|
+
* seeked to `#t=0.1` (media-fragment trick; paints on iOS Safari where a
|
|
254
|
+
* fragmentless metadata load stays blank). No chrome, no playback, no
|
|
255
|
+
* MuxPlayer cost — the resting facade layer under strip cards when no
|
|
256
|
+
* poster asset exists. All player props are ignored. */
|
|
257
|
+
firstFrameOnly?: boolean;
|
|
256
258
|
}
|
|
257
259
|
|
|
258
260
|
interface VideoYouTubeProps extends VideoCommonProps {
|
|
@@ -272,7 +274,7 @@ interface VideoAutoProps extends VideoCommonProps {
|
|
|
272
274
|
chromeless?: boolean;
|
|
273
275
|
playOnHover?: boolean;
|
|
274
276
|
playWhenHovered?: boolean;
|
|
275
|
-
|
|
277
|
+
firstFrameOnly?: boolean;
|
|
276
278
|
}
|
|
277
279
|
|
|
278
280
|
export type VideoProps = VideoFileProps | VideoYouTubeProps | VideoAutoProps;
|
|
@@ -297,6 +299,8 @@ export function Video(props: VideoProps): React.ReactElement | null {
|
|
|
297
299
|
className={props.className}
|
|
298
300
|
minimalControls={props.minimalControls}
|
|
299
301
|
/>
|
|
302
|
+
) : 'firstFrameOnly' in props && props.firstFrameOnly ? (
|
|
303
|
+
<FirstFramePreview url={url} className={props.className} />
|
|
300
304
|
) : (
|
|
301
305
|
<FilePlayer
|
|
302
306
|
url={url}
|
|
@@ -309,7 +313,6 @@ export function Video(props: VideoProps): React.ReactElement | null {
|
|
|
309
313
|
chromeless={'chromeless' in props ? props.chromeless : undefined}
|
|
310
314
|
playOnHover={'playOnHover' in props ? props.playOnHover : undefined}
|
|
311
315
|
playWhenHovered={'playWhenHovered' in props ? props.playWhenHovered : undefined}
|
|
312
|
-
centerControlsOnly={'centerControlsOnly' in props ? props.centerControlsOnly : undefined}
|
|
313
316
|
className={props.className}
|
|
314
317
|
/>
|
|
315
318
|
);
|
|
@@ -373,6 +376,35 @@ function wrapWithLayout(
|
|
|
373
376
|
}
|
|
374
377
|
}
|
|
375
378
|
|
|
379
|
+
// -----------------------------------------------------------------------------
|
|
380
|
+
// First-frame preview — the `firstFrameOnly` facade branch
|
|
381
|
+
// -----------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
/** First-frame paint through the SAME FilePlayer/MuxPlayer pipeline as
|
|
384
|
+
* playback (one rendering path for every file source — HLS included, which a
|
|
385
|
+
* plain `<video>` couldn't decode in Chromium): a chromeless, muted,
|
|
386
|
+
* metadata-only instance whose `#t=0.1` media fragment seeks-and-paints the
|
|
387
|
+
* first frame (also fixes iOS Safari, which stays blank on a fragmentless
|
|
388
|
+
* metadata load). The transparent media background keeps the box showing the
|
|
389
|
+
* card surface (never black) until the frame decodes. */
|
|
390
|
+
function FirstFramePreview({ url, className }: { url: string; className?: string }): React.ReactElement {
|
|
391
|
+
return (
|
|
392
|
+
<div
|
|
393
|
+
aria-hidden
|
|
394
|
+
className="h-full w-full"
|
|
395
|
+
style={{ '--media-background-color': 'transparent' } as React.CSSProperties}
|
|
396
|
+
>
|
|
397
|
+
<FilePlayer
|
|
398
|
+
url={`${url}#t=0.1`}
|
|
399
|
+
preload="metadata"
|
|
400
|
+
muted
|
|
401
|
+
chromeless
|
|
402
|
+
className={className}
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
376
408
|
// -----------------------------------------------------------------------------
|
|
377
409
|
// File branch — MuxPlayer (handles both .m3u8 HLS and plain .mp4)
|
|
378
410
|
// -----------------------------------------------------------------------------
|
|
@@ -388,7 +420,8 @@ interface FilePlayerProps {
|
|
|
388
420
|
chromeless?: boolean;
|
|
389
421
|
playOnHover?: boolean;
|
|
390
422
|
playWhenHovered?: boolean;
|
|
391
|
-
|
|
423
|
+
/** Media preload hint — the firstFrameOnly facade passes 'metadata'. */
|
|
424
|
+
preload?: 'none' | 'metadata' | 'auto';
|
|
392
425
|
className?: string;
|
|
393
426
|
}
|
|
394
427
|
|
|
@@ -403,12 +436,9 @@ function FilePlayer({
|
|
|
403
436
|
chromeless,
|
|
404
437
|
playOnHover,
|
|
405
438
|
playWhenHovered,
|
|
406
|
-
|
|
439
|
+
preload,
|
|
407
440
|
className,
|
|
408
441
|
}: FilePlayerProps): React.ReactElement {
|
|
409
|
-
// centerControlsOnly: the center play button shows at REST only — while
|
|
410
|
-
// playing, ALL chrome hides (no pause sign over the hover-preview).
|
|
411
|
-
const [isPlaying, setIsPlaying] = useState(false);
|
|
412
442
|
// True while hover playback is running MUTED because the browser's autoplay
|
|
413
443
|
// policy blocked sound (no user activation yet). Drives the center unmute
|
|
414
444
|
// control — the industry pattern (muted autoplay + explicit unmute button)
|
|
@@ -546,11 +576,10 @@ function FilePlayer({
|
|
|
546
576
|
const player = (
|
|
547
577
|
<MuxPlayer
|
|
548
578
|
ref={hoverPlayerRef as React.Ref<never>}
|
|
549
|
-
onPlay={centerControlsOnly ? () => setIsPlaying(true) : undefined}
|
|
550
|
-
onPause={centerControlsOnly ? () => setIsPlaying(false) : undefined}
|
|
551
579
|
src={url}
|
|
552
580
|
poster={poster || undefined}
|
|
553
581
|
streamType="on-demand"
|
|
582
|
+
preload={preload}
|
|
554
583
|
playsInline
|
|
555
584
|
muted={muted}
|
|
556
585
|
preferCmcd="header"
|
|
@@ -581,15 +610,6 @@ function FilePlayer({
|
|
|
581
610
|
width: '100%',
|
|
582
611
|
height: '100%',
|
|
583
612
|
...(chromeless ? ({ '--controls': 'none' } as React.CSSProperties) : {}),
|
|
584
|
-
...(centerControlsOnly && !chromeless
|
|
585
|
-
? ({
|
|
586
|
-
'--bottom-controls': 'none',
|
|
587
|
-
'--top-controls': 'none',
|
|
588
|
-
// While playing, hide the center control too — no pause sign
|
|
589
|
-
// over the hover-preview; it reappears when playback pauses.
|
|
590
|
-
...(isPlaying ? { '--center-controls': 'none' } : {}),
|
|
591
|
-
} as React.CSSProperties)
|
|
592
|
-
: {}),
|
|
593
613
|
}}
|
|
594
614
|
>
|
|
595
615
|
{captionsUrl ? (
|
|
@@ -616,9 +636,11 @@ function FilePlayer({
|
|
|
616
636
|
aria-label="Unmute"
|
|
617
637
|
title="Unmute"
|
|
618
638
|
onClick={unmuteNow}
|
|
619
|
-
|
|
639
|
+
// White at rest, ACCENT while the icon itself is hovered — the same
|
|
640
|
+
// hover language as every mux control icon (see app-globals.css).
|
|
641
|
+
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
642
|
>
|
|
621
|
-
<
|
|
643
|
+
<VideoUnmuteGlyph />
|
|
622
644
|
</button>
|
|
623
645
|
) : null;
|
|
624
646
|
|
|
@@ -882,9 +904,10 @@ function YouTubeFacadeInner({
|
|
|
882
904
|
/>
|
|
883
905
|
</picture>
|
|
884
906
|
<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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
907
|
+
{/* THE shared center play badge (video-center-badge.tsx) — same disc
|
|
908
|
+
as strip cards / carousel thumbs / unmute chip; hero size + the
|
|
909
|
+
facade's hover-scale affordance. */}
|
|
910
|
+
<VideoPlayBadge size="lg" className="transition-transform duration-200 group-hover:scale-110 group-hover:text-ods-accent" />
|
|
888
911
|
</div>
|
|
889
912
|
</button>
|
|
890
913
|
</div>
|
|
@@ -35,6 +35,8 @@ export { VendorDirectoryIcon } from './vendor-directory-icon';
|
|
|
35
35
|
export { OpenSourceIcon } from './open-source-icon';
|
|
36
36
|
export { CommunityHubIcon } from './community-hub-icon';
|
|
37
37
|
export { AboutIcon } from './about-icon';
|
|
38
|
+
export { PlayFilledIcon } from './play-filled-icon';
|
|
39
|
+
export { VolumeOffFilledIcon } from './volume-off-filled-icon';
|
|
38
40
|
export { UserIcon } from './user-icon';
|
|
39
41
|
export { HamburgerIcon } from './hamburger-icon';
|
|
40
42
|
export { MenuIcon } from './menu-icon';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SVGProps } from "react";
|
|
2
|
+
|
|
3
|
+
export interface PlayFilledIconProps
|
|
4
|
+
extends Omit<SVGProps<SVGSVGElement>, "width" | "height"> {
|
|
5
|
+
className?: string;
|
|
6
|
+
size?: number;
|
|
7
|
+
color?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Solid play triangle — the SAME glyph media-chrome/MuxPlayer renders in its
|
|
12
|
+
* native center play button, so custom video affordances (strip cards,
|
|
13
|
+
* facades, carousels) stay pixel-consistent with real player chrome.
|
|
14
|
+
* Hand-maintained (not in the Figma-generated set, which has only the
|
|
15
|
+
* outline `PlayIcon`).
|
|
16
|
+
*/
|
|
17
|
+
export function PlayFilledIcon({
|
|
18
|
+
className = "",
|
|
19
|
+
size = 24,
|
|
20
|
+
color = "currentColor",
|
|
21
|
+
...props
|
|
22
|
+
}: PlayFilledIconProps) {
|
|
23
|
+
return (
|
|
24
|
+
<svg
|
|
25
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
26
|
+
width={size}
|
|
27
|
+
height={size}
|
|
28
|
+
fill="none"
|
|
29
|
+
viewBox="0 0 24 24"
|
|
30
|
+
className={className}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
<path fill={color} d="m6 21 15-9L6 3v18Z" />
|
|
34
|
+
</svg>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SVGProps } from "react";
|
|
2
|
+
|
|
3
|
+
export interface VolumeOffFilledIconProps
|
|
4
|
+
extends Omit<SVGProps<SVGSVGElement>, "width" | "height"> {
|
|
5
|
+
className?: string;
|
|
6
|
+
size?: number;
|
|
7
|
+
color?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Solid muted-volume glyph (speaker + slash) — the filled companion to
|
|
12
|
+
* `PlayFilledIcon`, matching media-chrome's icon family so video unmute
|
|
13
|
+
* affordances read identically to real player chrome. Hand-maintained (the
|
|
14
|
+
* Figma-generated set has only outline volume icons).
|
|
15
|
+
*/
|
|
16
|
+
export function VolumeOffFilledIcon({
|
|
17
|
+
className = "",
|
|
18
|
+
size = 24,
|
|
19
|
+
color = "currentColor",
|
|
20
|
+
...props
|
|
21
|
+
}: VolumeOffFilledIconProps) {
|
|
22
|
+
return (
|
|
23
|
+
<svg
|
|
24
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
25
|
+
width={size}
|
|
26
|
+
height={size}
|
|
27
|
+
fill="none"
|
|
28
|
+
viewBox="0 0 24 24"
|
|
29
|
+
className={className}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
<path
|
|
33
|
+
fill={color}
|
|
34
|
+
d="M16.5 12A4.5 4.5 0 0 0 14 7.97v2.21l2.45 2.45c.03-.2.05-.41.05-.63Zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51A8.796 8.796 0 0 0 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71ZM4.27 3 3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a8.99 8.99 0 0 0 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3ZM12 4 9.91 6.09 12 8.18V4Z"
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -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 {
|
|
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
|
-
<
|
|
219
|
-
<PlayIcon size={12} color="white" />
|
|
220
|
-
</div>
|
|
218
|
+
<VideoPlayBadge size="sm" />
|
|
221
219
|
</div>
|
|
222
220
|
)}
|
|
223
221
|
|