@flamingo-stack/openframe-frontend-core 0.0.387 → 0.0.388
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-DU2X2MH2.js → chunk-23BJ64AY.js} +2 -2
- package/dist/{chunk-OGNKGDHT.js → chunk-35C3PUUN.js} +4 -4
- package/dist/{chunk-RUHE3RJK.cjs → chunk-6ZAQOYW4.cjs} +62 -67
- package/dist/chunk-6ZAQOYW4.cjs.map +1 -0
- package/dist/{chunk-MVY4ZKQR.js → chunk-7CEJO3FE.js} +2 -2
- package/dist/{chunk-EHVRW3WB.js → chunk-7UQ74ZJN.js} +2 -2
- package/dist/{chunk-EULRZYQH.js → chunk-ATKRYY4U.js} +9 -14
- package/dist/chunk-ATKRYY4U.js.map +1 -0
- package/dist/{chunk-SOAWTQXO.cjs → chunk-B4JRV4FB.cjs} +31 -28
- package/dist/chunk-B4JRV4FB.cjs.map +1 -0
- package/dist/{chunk-GPTS2NXW.js → chunk-BGYWTU2E.js} +12 -9
- package/dist/chunk-BGYWTU2E.js.map +1 -0
- package/dist/{chunk-3BPVTHOW.js → chunk-DM3JIOFA.js} +4 -4
- package/dist/{chunk-XGLZK25V.js → chunk-EQKTBIKH.js} +2 -2
- package/dist/{chunk-SGULN3RX.cjs → chunk-EYQESL6X.cjs} +7 -7
- package/dist/{chunk-SGULN3RX.cjs.map → chunk-EYQESL6X.cjs.map} +1 -1
- package/dist/{chunk-N2LW5NHS.js → chunk-FS4FVNOU.js} +2 -2
- package/dist/{chunk-OLCNA32C.cjs → chunk-HIODHJCJ.cjs} +15 -15
- package/dist/{chunk-OLCNA32C.cjs.map → chunk-HIODHJCJ.cjs.map} +1 -1
- package/dist/{chunk-44ENRZZJ.cjs → chunk-HQG5AY64.cjs} +12 -12
- package/dist/{chunk-44ENRZZJ.cjs.map → chunk-HQG5AY64.cjs.map} +1 -1
- package/dist/{chunk-3L4B7MBD.cjs → chunk-KYN2GYUI.cjs} +31 -31
- package/dist/{chunk-3L4B7MBD.cjs.map → chunk-KYN2GYUI.cjs.map} +1 -1
- package/dist/{chunk-CIH2LZI7.cjs → chunk-ONXN77KP.cjs} +62 -62
- package/dist/{chunk-CIH2LZI7.cjs.map → chunk-ONXN77KP.cjs.map} +1 -1
- package/dist/{chunk-OX75ZFN6.js → chunk-SENYQRPF.js} +2 -2
- package/dist/{chunk-SECF2KWQ.cjs → chunk-SNFSLYVA.cjs} +5 -5
- package/dist/{chunk-SECF2KWQ.cjs.map → chunk-SNFSLYVA.cjs.map} +1 -1
- package/dist/{chunk-3KA5L7LC.js → chunk-UCQWQ7LM.js} +2 -2
- package/dist/{chunk-ANPV7GZT.js → chunk-VCRUAKOJ.js} +5701 -5281
- package/dist/chunk-VCRUAKOJ.js.map +1 -0
- package/dist/{chunk-DKN425XR.cjs → chunk-VOPTCR5I.cjs} +1604 -1184
- package/dist/chunk-VOPTCR5I.cjs.map +1 -0
- package/dist/{chunk-DO7LOWFE.cjs → chunk-WIKFWION.cjs} +9 -9
- package/dist/{chunk-DO7LOWFE.cjs.map → chunk-WIKFWION.cjs.map} +1 -1
- package/dist/{chunk-ADEP6E3Z.cjs → chunk-YKPQINSZ.cjs} +26 -26
- package/dist/{chunk-ADEP6E3Z.cjs.map → chunk-YKPQINSZ.cjs.map} +1 -1
- package/dist/{chunk-POJRURML.cjs → chunk-ZVFDJ35S.cjs} +37 -37
- package/dist/{chunk-POJRURML.cjs.map → chunk-ZVFDJ35S.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/entity-video-section.d.ts +15 -3
- package/dist/components/features/entity-video-section.d.ts.map +1 -1
- package/dist/components/features/index.cjs +8 -2
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.d.ts +2 -0
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +11 -5
- package/dist/components/features/video-bites-display.d.ts +12 -26
- package/dist/components/features/video-bites-display.d.ts.map +1 -1
- package/dist/components/features/video-bites-shared.cjs +24 -0
- package/dist/components/features/video-bites-shared.cjs.map +1 -0
- package/dist/components/features/video-bites-shared.d.ts +47 -0
- package/dist/components/features/video-bites-shared.d.ts.map +1 -0
- package/dist/components/features/video-bites-shared.js +20 -0
- package/dist/components/features/video-bites-shared.js.map +1 -0
- package/dist/components/features/video-bites-strip.d.ts +75 -0
- package/dist/components/features/video-bites-strip.d.ts.map +1 -0
- package/dist/components/features/video-ratio-tabs.d.ts +6 -9
- package/dist/components/features/video-ratio-tabs.d.ts.map +1 -1
- package/dist/components/features/video.d.ts +19 -0
- package/dist/components/features/video.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +128 -165
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +333 -370
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/shared/product-release/release-detail-page.d.ts +6 -7
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/square-avatar.d.ts +3 -1
- package/dist/components/square-avatar.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/components/user-display.d.ts +5 -1
- package/dist/components/user-display.d.ts.map +1 -1
- package/dist/index.cjs +8 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +11 -5
- package/dist/types/case-study.d.ts +18 -0
- package/dist/types/case-study.d.ts.map +1 -1
- package/dist/types/video-processing.d.ts +11 -0
- package/dist/types/video-processing.d.ts.map +1 -1
- package/package.json +7 -1
- package/src/components/features/entity-video-section.tsx +26 -5
- package/src/components/features/index.ts +6 -3
- package/src/components/features/video-bites-display.tsx +17 -189
- package/src/components/features/video-bites-shared.ts +62 -0
- package/src/components/features/video-bites-strip.tsx +494 -0
- package/src/components/features/video-ratio-tabs.tsx +15 -19
- package/src/components/features/video.tsx +134 -3
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +10 -9
- package/src/components/shared/product-release/release-detail-page.tsx +12 -18
- package/src/components/square-avatar.tsx +5 -2
- package/src/components/user-display.tsx +14 -4
- package/src/stories/VideoBitesStrip.stories.tsx +82 -0
- package/src/types/case-study.ts +23 -0
- package/src/types/video-processing.ts +11 -0
- package/dist/chunk-ANPV7GZT.js.map +0 -1
- package/dist/chunk-DKN425XR.cjs.map +0 -1
- package/dist/chunk-EULRZYQH.js.map +0 -1
- package/dist/chunk-GPTS2NXW.js.map +0 -1
- package/dist/chunk-RUHE3RJK.cjs.map +0 -1
- package/dist/chunk-SOAWTQXO.cjs.map +0 -1
- /package/dist/{chunk-DU2X2MH2.js.map → chunk-23BJ64AY.js.map} +0 -0
- /package/dist/{chunk-OGNKGDHT.js.map → chunk-35C3PUUN.js.map} +0 -0
- /package/dist/{chunk-MVY4ZKQR.js.map → chunk-7CEJO3FE.js.map} +0 -0
- /package/dist/{chunk-EHVRW3WB.js.map → chunk-7UQ74ZJN.js.map} +0 -0
- /package/dist/{chunk-3BPVTHOW.js.map → chunk-DM3JIOFA.js.map} +0 -0
- /package/dist/{chunk-XGLZK25V.js.map → chunk-EQKTBIKH.js.map} +0 -0
- /package/dist/{chunk-N2LW5NHS.js.map → chunk-FS4FVNOU.js.map} +0 -0
- /package/dist/{chunk-OX75ZFN6.js.map → chunk-SENYQRPF.js.map} +0 -0
- /package/dist/{chunk-3KA5L7LC.js.map → chunk-UCQWQ7LM.js.map} +0 -0
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.388",
|
|
4
4
|
"description": "Shared design system and components for all Flamingo platforms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -120,6 +120,12 @@
|
|
|
120
120
|
"require": "./dist/components/features/mux-origins.cjs",
|
|
121
121
|
"default": "./dist/components/features/mux-origins.js"
|
|
122
122
|
},
|
|
123
|
+
"./components/features/video-bites-shared": {
|
|
124
|
+
"types": "./dist/components/features/video-bites-shared.d.ts",
|
|
125
|
+
"import": "./dist/components/features/video-bites-shared.js",
|
|
126
|
+
"require": "./dist/components/features/video-bites-shared.cjs",
|
|
127
|
+
"default": "./dist/components/features/video-bites-shared.js"
|
|
128
|
+
},
|
|
123
129
|
"./components/toast": {
|
|
124
130
|
"types": "./dist/components/toast/index.d.ts",
|
|
125
131
|
"import": "./dist/components/toast/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { ComponentType } from 'react';
|
|
3
|
+
import React, { ComponentType } from 'react';
|
|
4
4
|
import {
|
|
5
5
|
Tabs,
|
|
6
6
|
TabsList,
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
} from '../ui/tabs';
|
|
10
10
|
import type { VideoTeaser } from '../../types/video-processing';
|
|
11
11
|
import { Video } from './video';
|
|
12
|
-
import {
|
|
12
|
+
import { VideoBitesStrip, type VideoBiteStripItem } from './video-bites-strip';
|
|
13
|
+
import { DEFAULT_VIDEO_BITES_TITLE, type VideoBiteStripProfile } from './video-bites-shared';
|
|
13
14
|
import { SECTION_HEADING_CLASS } from '../layout/page-heading';
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -47,10 +48,20 @@ export interface EntityVideoSectionProps {
|
|
|
47
48
|
videoSummary?: string | null;
|
|
48
49
|
/** Video bites/teasers array. */
|
|
49
50
|
videoBites?: VideoTeaser[];
|
|
50
|
-
/** Title for the video bites section. */
|
|
51
|
+
/** Title for the video bites section. Default: 'Key Moments' (unified). */
|
|
51
52
|
bitesTitle?: string;
|
|
52
53
|
/** Whether to filter bites to published only. */
|
|
53
54
|
filterPublishedBites?: boolean;
|
|
55
|
+
/** Section-level profile shown in the strip's hover overlay. */
|
|
56
|
+
bitesProfile?: VideoBiteStripProfile | null;
|
|
57
|
+
/** Navigation target for the strip's overlay footer (the origin entity). */
|
|
58
|
+
bitesHref?: string;
|
|
59
|
+
/** Custom node between the bites heading and the strip (description slot). */
|
|
60
|
+
bitesHeaderSlot?: React.ReactNode;
|
|
61
|
+
/** Navigation fallback for bites without their own href/onNavigate. */
|
|
62
|
+
onBiteNavigate?: (bite: VideoBiteStripItem, index: number) => void;
|
|
63
|
+
/** Marquee auto-scroll for the bites strip. Default true. */
|
|
64
|
+
bitesAutoScroll?: boolean;
|
|
54
65
|
/** Markdown renderer component injected by the host app. */
|
|
55
66
|
MarkdownRenderer?: ComponentType<MarkdownRendererProps>;
|
|
56
67
|
/**
|
|
@@ -73,8 +84,13 @@ export function EntityVideoSection({
|
|
|
73
84
|
title = 'Video',
|
|
74
85
|
videoSummary,
|
|
75
86
|
videoBites,
|
|
76
|
-
bitesTitle =
|
|
87
|
+
bitesTitle = DEFAULT_VIDEO_BITES_TITLE,
|
|
77
88
|
filterPublishedBites = true,
|
|
89
|
+
bitesProfile = null,
|
|
90
|
+
bitesHref,
|
|
91
|
+
bitesHeaderSlot,
|
|
92
|
+
onBiteNavigate,
|
|
93
|
+
bitesAutoScroll = true,
|
|
78
94
|
MarkdownRenderer,
|
|
79
95
|
srtContent,
|
|
80
96
|
captionsUrl,
|
|
@@ -165,10 +181,15 @@ export function EntityVideoSection({
|
|
|
165
181
|
)}
|
|
166
182
|
|
|
167
183
|
{videoBites && videoBites.length > 0 && (
|
|
168
|
-
<
|
|
184
|
+
<VideoBitesStrip
|
|
169
185
|
bites={videoBites}
|
|
170
186
|
title={bitesTitle}
|
|
171
187
|
filterPublished={filterPublishedBites}
|
|
188
|
+
profile={bitesProfile}
|
|
189
|
+
href={bitesHref}
|
|
190
|
+
headerSlot={bitesHeaderSlot}
|
|
191
|
+
onBiteNavigate={onBiteNavigate}
|
|
192
|
+
autoScroll={bitesAutoScroll}
|
|
172
193
|
/>
|
|
173
194
|
)}
|
|
174
195
|
</>
|
|
@@ -52,11 +52,14 @@ export * from './tags-selector'
|
|
|
52
52
|
export * from './warning-block'
|
|
53
53
|
// Unified video primitives — single source of truth across all platforms.
|
|
54
54
|
// `<Video>` replaces the deleted `<VideoPlayer>` + `<YouTubeEmbed>` pair;
|
|
55
|
-
// `<EntityVideoSection>` + `<
|
|
56
|
-
// wrappers
|
|
57
|
-
//
|
|
55
|
+
// `<EntityVideoSection>` + `<VideoBitesStrip>` are the public detail-page
|
|
56
|
+
// wrappers (`<VideoBitesDisplay>` is a deprecated shim over the strip);
|
|
57
|
+
// `video-ratio-tabs` keeps the aspect-ratio primitives (tabs/grid = admin-only);
|
|
58
|
+
// `video-bites-shared` is the SERVER-SAFE leaf (also its own published subpath).
|
|
58
59
|
export * from './video'
|
|
59
60
|
export * from './video-ratio-tabs'
|
|
61
|
+
export * from './video-bites-shared'
|
|
62
|
+
export * from './video-bites-strip'
|
|
60
63
|
export * from './video-bites-display'
|
|
61
64
|
export * from './entity-video-section'
|
|
62
65
|
export * from './use-video-warmup'
|
|
@@ -1,77 +1,22 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React
|
|
4
|
-
import { Card } from '../ui/card';
|
|
5
|
-
import { useNearViewport } from '../../hooks/use-near-viewport';
|
|
3
|
+
import React from 'react';
|
|
6
4
|
import type { VideoTeaser } from '../../types/video-processing';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
RatioTabs,
|
|
11
|
-
groupByAspectRatio,
|
|
12
|
-
detectAspectRatio,
|
|
13
|
-
ratioToCategory,
|
|
14
|
-
RATIO_DISPLAY_GRID_CLASS,
|
|
15
|
-
type VideoTeaserWithRatio,
|
|
16
|
-
type RatioCategory,
|
|
17
|
-
} from './video-ratio-tabs';
|
|
5
|
+
import { VideoBitesStrip } from './video-bites-strip';
|
|
6
|
+
import { DEFAULT_VIDEO_BITES_TITLE } from './video-bites-shared';
|
|
18
7
|
|
|
19
8
|
/**
|
|
20
|
-
*
|
|
21
|
-
* aspect ratio.
|
|
9
|
+
* @deprecated Transitional shim — use `<VideoBitesStrip>` directly.
|
|
22
10
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* lazily — for a plain MP4 source it's a thin shell around `<video>`,
|
|
28
|
-
* so the carve-out costs more in complexity than it saves in bytes.
|
|
11
|
+
* The old aspect-ratio GRID rendered here was replaced by the unified
|
|
12
|
+
* strip (`video-bites-strip.tsx`, Figma node 4033-90364). This wrapper
|
|
13
|
+
* keeps the historical 4-prop signature compiling for hub call sites
|
|
14
|
+
* between the lib version bump and the hub-side migration.
|
|
29
15
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* `useNearViewport` singleton used everywhere else in the lib.
|
|
16
|
+
* Scheduled deletion: next lib minor after the hub migration's grep gate
|
|
17
|
+
* confirms zero consumers. Do NOT add features here.
|
|
33
18
|
*/
|
|
34
19
|
|
|
35
|
-
// =============================================================================
|
|
36
|
-
// LazyBite — viewport-gated wrapper for video bite cards
|
|
37
|
-
// =============================================================================
|
|
38
|
-
|
|
39
|
-
/** Aspect-ratio-aware placeholder prevents CLS across the grid. */
|
|
40
|
-
const RATIO_TO_CSS_ASPECT: Record<RatioCategory, string> = {
|
|
41
|
-
portrait: '9 / 16',
|
|
42
|
-
square: '1 / 1',
|
|
43
|
-
landscape: '16 / 9',
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
interface LazyBiteProps {
|
|
47
|
-
/** Aspect ratio of the wrapped bite — placeholder keeps layout stable. */
|
|
48
|
-
ratio: RatioCategory;
|
|
49
|
-
/** Card rendered once the wrapper enters (within `500px` of) the viewport. */
|
|
50
|
-
children: React.ReactNode;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Defers mounting an off-screen video bite until it scrolls within
|
|
55
|
-
* ~500px of the viewport. Renders an aspect-ratio-matched placeholder
|
|
56
|
-
* beforehand so layout stays stable.
|
|
57
|
-
*
|
|
58
|
-
* Placeholder bg matches the wrapped `<Card>` background (`bg-ods-card`)
|
|
59
|
-
* so the swap-in is visually seamless and avoids a flash on hydration.
|
|
60
|
-
*/
|
|
61
|
-
function LazyBite({ ratio, children }: LazyBiteProps) {
|
|
62
|
-
const { ref, isNear } = useNearViewport<HTMLDivElement>('500px');
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<div ref={ref} style={{ aspectRatio: RATIO_TO_CSS_ASPECT[ratio] }}>
|
|
66
|
-
{isNear ? children : <div className="w-full h-full bg-ods-card rounded-md" />}
|
|
67
|
-
</div>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// =============================================================================
|
|
72
|
-
// Public component
|
|
73
|
-
// =============================================================================
|
|
74
|
-
|
|
75
20
|
export interface VideoBitesDisplayProps {
|
|
76
21
|
/** Array of video bites/teasers to display. */
|
|
77
22
|
bites: VideoTeaser[];
|
|
@@ -83,135 +28,18 @@ export interface VideoBitesDisplayProps {
|
|
|
83
28
|
showTitle?: boolean;
|
|
84
29
|
}
|
|
85
30
|
|
|
86
|
-
/**
|
|
87
|
-
* Unified video-bites grid.
|
|
88
|
-
*
|
|
89
|
-
* Groups by aspect ratio when multiple ratios are present, otherwise
|
|
90
|
-
* renders a flat grid. Each bite mounts lazily via `LazyBite` so
|
|
91
|
-
* off-screen players don't cost the page.
|
|
92
|
-
*/
|
|
93
31
|
export function VideoBitesDisplay({
|
|
94
32
|
bites,
|
|
95
|
-
title =
|
|
33
|
+
title = DEFAULT_VIDEO_BITES_TITLE,
|
|
96
34
|
filterPublished = true,
|
|
97
35
|
showTitle = true,
|
|
98
36
|
}: VideoBitesDisplayProps) {
|
|
99
|
-
const grouped = useMemo(() => {
|
|
100
|
-
const filtered = filterPublished ? bites.filter(b => b.published) : bites;
|
|
101
|
-
|
|
102
|
-
const sorted = [...filtered].sort((a, b) => {
|
|
103
|
-
if (!a.created_at && !b.created_at) return 0;
|
|
104
|
-
if (!a.created_at) return 1;
|
|
105
|
-
if (!b.created_at) return -1;
|
|
106
|
-
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
return groupByAspectRatio(sorted, b =>
|
|
110
|
-
detectAspectRatio((b as VideoTeaserWithRatio).aspect_ratio),
|
|
111
|
-
);
|
|
112
|
-
}, [bites, filterPublished]);
|
|
113
|
-
|
|
114
|
-
const totalCount = grouped.portrait.length + grouped.square.length + grouped.landscape.length;
|
|
115
|
-
if (totalCount === 0) return null;
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<div className="flex flex-col gap-6 w-full min-w-0">
|
|
119
|
-
{showTitle && (
|
|
120
|
-
<h2 className={`${SECTION_HEADING_CLASS} break-words`}>
|
|
121
|
-
{title}
|
|
122
|
-
</h2>
|
|
123
|
-
)}
|
|
124
|
-
|
|
125
|
-
{grouped.hasMultiple ? (
|
|
126
|
-
<RatioTabs
|
|
127
|
-
groups={{
|
|
128
|
-
portrait: {
|
|
129
|
-
count: grouped.portrait.length,
|
|
130
|
-
render: () => <BiteGrid bites={grouped.portrait} ratio="portrait" />,
|
|
131
|
-
},
|
|
132
|
-
square: {
|
|
133
|
-
count: grouped.square.length,
|
|
134
|
-
render: () => <BiteGrid bites={grouped.square} ratio="square" />,
|
|
135
|
-
},
|
|
136
|
-
landscape: {
|
|
137
|
-
count: grouped.landscape.length,
|
|
138
|
-
render: () => <BiteGrid bites={grouped.landscape} ratio="landscape" />,
|
|
139
|
-
},
|
|
140
|
-
}}
|
|
141
|
-
/>
|
|
142
|
-
) : (
|
|
143
|
-
<BiteGrid
|
|
144
|
-
bites={
|
|
145
|
-
grouped.portrait.length > 0
|
|
146
|
-
? grouped.portrait
|
|
147
|
-
: grouped.square.length > 0
|
|
148
|
-
? grouped.square
|
|
149
|
-
: grouped.landscape
|
|
150
|
-
}
|
|
151
|
-
ratio={
|
|
152
|
-
grouped.portrait.length > 0
|
|
153
|
-
? 'portrait'
|
|
154
|
-
: grouped.square.length > 0
|
|
155
|
-
? 'square'
|
|
156
|
-
: 'landscape'
|
|
157
|
-
}
|
|
158
|
-
/>
|
|
159
|
-
)}
|
|
160
|
-
</div>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// =============================================================================
|
|
165
|
-
// Internals
|
|
166
|
-
// =============================================================================
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Renders a grid of bite cards with ratio-appropriate column layout.
|
|
170
|
-
* Each card is wrapped in `LazyBite` so off-screen bites don't mount
|
|
171
|
-
* their player until they scroll near the viewport.
|
|
172
|
-
*/
|
|
173
|
-
function BiteGrid({ bites, ratio }: { bites: VideoTeaser[]; ratio: RatioCategory }) {
|
|
174
37
|
return (
|
|
175
|
-
<
|
|
176
|
-
{bites
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
</div>
|
|
38
|
+
<VideoBitesStrip
|
|
39
|
+
bites={bites}
|
|
40
|
+
title={title}
|
|
41
|
+
filterPublished={filterPublished}
|
|
42
|
+
showTitle={showTitle}
|
|
43
|
+
/>
|
|
182
44
|
);
|
|
183
45
|
}
|
|
184
|
-
|
|
185
|
-
interface VideoBiteCardProps {
|
|
186
|
-
url: string;
|
|
187
|
-
title?: string | null;
|
|
188
|
-
thumbnailUrl?: string | null;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Individual bite card — routes through `<Video>` so the SSoT player
|
|
193
|
-
* is the only video primitive in the lib.
|
|
194
|
-
*
|
|
195
|
-
* Layout: `LazyBite` sets the OUTER `aspectRatio` (portrait/square/landscape),
|
|
196
|
-
* but `<Card>` between LazyBite and `<Video>` has no intrinsic height, so
|
|
197
|
-
* we wrap `<Video>` in `layout="fill"` + an explicit `relative` parent so
|
|
198
|
-
* the player fills the bite's aspect box from first paint. Otherwise
|
|
199
|
-
* MuxPlayer renders at its intrinsic default size and grows once metadata
|
|
200
|
-
* loads — the same CLS that hits the centered layout.
|
|
201
|
-
*/
|
|
202
|
-
function VideoBiteCard({ url, title, thumbnailUrl }: VideoBiteCardProps) {
|
|
203
|
-
return (
|
|
204
|
-
<Card className="overflow-hidden border border-ods-border bg-ods-card hover:border-ods-accent transition-colors flex flex-col h-full">
|
|
205
|
-
<div className="relative flex-1 min-h-0">
|
|
206
|
-
<Video url={url} poster={thumbnailUrl || undefined} layout="fill" />
|
|
207
|
-
</div>
|
|
208
|
-
{title && (
|
|
209
|
-
<div className="p-4">
|
|
210
|
-
<p className="text-h4 text-ods-text-primary line-clamp-2" title={title}>{title}</p>
|
|
211
|
-
</div>
|
|
212
|
-
)}
|
|
213
|
-
</Card>
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export { VideoBiteCard };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* video-bites-shared — SERVER-SAFE leaf for the video-bites surface.
|
|
3
|
+
*
|
|
4
|
+
* No React imports, no 'use client'. Published as its own subpath
|
|
5
|
+
* (`@flamingo-stack/openframe-frontend-core/components/features/video-bites-shared`)
|
|
6
|
+
* so server-side consumers (the hub's featured-video-bites DAL) can import the
|
|
7
|
+
* title constant, profile adapter, and sort comparator WITHOUT pulling the
|
|
8
|
+
* 'use client' strip/MuxPlayer module graph into a server bundle — same
|
|
9
|
+
* pattern (and reason) as `mux-origins.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Client components (`<VideoBitesStrip>`, the features barrel) re-export from
|
|
12
|
+
* here; there must never be a second definition of any of these.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { VideoTeaser } from '../../types/video-processing'
|
|
16
|
+
|
|
17
|
+
/** Single exported home for the unified public bites-section title. */
|
|
18
|
+
export const DEFAULT_VIDEO_BITES_TITLE = 'Key Moments'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Profile rendered in the strip's hover overlay. Structurally identical to
|
|
22
|
+
* `UserDisplayProps` — the overlay renders it via `<UserDisplay>` 1:1.
|
|
23
|
+
*/
|
|
24
|
+
export interface VideoBiteStripProfile {
|
|
25
|
+
name: string
|
|
26
|
+
avatarUrl?: string | null
|
|
27
|
+
subtitle?: string | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Adapter SSoT: builds a strip profile from the repo-wide author/user shape
|
|
32
|
+
* (`profiles` rows and hydrated authors all expose full_name / avatar_url /
|
|
33
|
+
* job_title). ALL strip call sites and the featured DAL's mapProfile route
|
|
34
|
+
* through this — never hand-roll the mapping.
|
|
35
|
+
*
|
|
36
|
+
* Returns NULL (never `{name: ''}`) when `full_name` is null/empty: callers
|
|
37
|
+
* rely on `profile ?? fallback` chains, and some producers (e.g. the release
|
|
38
|
+
* kit) pass null-field author stubs.
|
|
39
|
+
*/
|
|
40
|
+
export function toStripProfile(
|
|
41
|
+
p?: { full_name?: string | null; avatar_url?: string | null; job_title?: string | null } | null,
|
|
42
|
+
fallbackSubtitle?: string | null,
|
|
43
|
+
): VideoBiteStripProfile | null {
|
|
44
|
+
if (!p?.full_name) return null
|
|
45
|
+
return {
|
|
46
|
+
name: p.full_name,
|
|
47
|
+
avatarUrl: p.avatar_url ?? null,
|
|
48
|
+
subtitle: p.job_title ?? fallbackSubtitle ?? null,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Null-safe created_at-desc COMPARATOR (missing dates sort last).
|
|
54
|
+
* Call as `[...arr].sort(sortBitesByCreatedAtDesc)` — shared by the strip,
|
|
55
|
+
* the hub admin editor, and the hub featured DAL.
|
|
56
|
+
*/
|
|
57
|
+
export function sortBitesByCreatedAtDesc(a: VideoTeaser, b: VideoTeaser): number {
|
|
58
|
+
if (!a.created_at && !b.created_at) return 0
|
|
59
|
+
if (!a.created_at) return 1
|
|
60
|
+
if (!b.created_at) return -1
|
|
61
|
+
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
|
62
|
+
}
|