@flamingo-stack/openframe-frontend-core 0.0.178 → 0.0.179
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-L4T24AN4.cjs → chunk-4TM2SBMX.cjs} +855 -1325
- package/dist/chunk-4TM2SBMX.cjs.map +1 -0
- package/dist/{chunk-AAX27BCR.js → chunk-ZMQP3UZJ.js} +3690 -4160
- package/dist/chunk-ZMQP3UZJ.js.map +1 -0
- package/dist/components/features/entity-video-section.d.ts +54 -0
- package/dist/components/features/entity-video-section.d.ts.map +1 -0
- package/dist/components/features/index.cjs +18 -2
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.d.ts +4 -2
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +21 -5
- package/dist/components/features/video-bites-display.d.ts +38 -0
- package/dist/components/features/video-bites-display.d.ts.map +1 -0
- package/dist/components/features/video-ratio-tabs.d.ts +62 -0
- package/dist/components/features/video-ratio-tabs.d.ts.map +1 -0
- package/dist/components/features/video.d.ts +94 -0
- package/dist/components/features/video.d.ts.map +1 -0
- package/dist/components/index.cjs +18 -2
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +21 -5
- package/dist/components/media-carousel.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/index.cjs +18 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +21 -5
- package/package.json +2 -2
- package/src/components/features/entity-video-section.tsx +175 -0
- package/src/components/features/index.ts +9 -2
- package/src/components/features/video-bites-display.tsx +216 -0
- package/src/components/features/video-ratio-tabs.tsx +174 -0
- package/src/components/features/video.tsx +474 -0
- package/src/components/media-carousel.tsx +43 -236
- package/src/components/shared/product-release/release-detail-page.tsx +26 -19
- package/dist/chunk-AAX27BCR.js.map +0 -1
- package/dist/chunk-L4T24AN4.cjs.map +0 -1
- package/dist/components/features/video-player.d.ts +0 -44
- package/dist/components/features/video-player.d.ts.map +0 -1
- package/dist/components/features/youtube-embed.d.ts +0 -31
- package/dist/components/features/youtube-embed.d.ts.map +0 -1
- package/dist/utils/lite-youtube-embed-stub.d.ts +0 -8
- package/dist/utils/lite-youtube-embed-stub.d.ts.map +0 -1
- package/dist/utils/lite-youtube-embed.d.ts +0 -9
- package/dist/utils/lite-youtube-embed.d.ts.map +0 -1
- package/src/components/features/.video-player.md +0 -44
- package/src/components/features/.youtube-embed.md +0 -40
- package/src/components/features/video-player.tsx +0 -893
- package/src/components/features/youtube-embed.tsx +0 -158
- package/src/utils/lite-youtube-embed-stub.tsx +0 -21
- package/src/utils/lite-youtube-embed.tsx +0 -46
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useRef, useState } from 'react';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* YouTube facade (lite-youtube-embed pattern). Renders poster + play
|
|
7
|
-
* button until clicked; on click, a real `<iframe>` is created via
|
|
8
|
-
* `document.createElement` synchronously so the user-activation chain
|
|
9
|
-
* holds and `autoplay=1` plays on Chrome / Safari / Firefox.
|
|
10
|
-
*
|
|
11
|
-
* Uses `youtube-nocookie.com` (GDPR-friendly) and `mqdefault` posters
|
|
12
|
-
* (`maxresdefault` returns a gray 200 placeholder when missing, which
|
|
13
|
-
* defeats `onError` fallback).
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
interface YouTubeEmbedProps {
|
|
17
|
-
videoId: string;
|
|
18
|
-
title?: string;
|
|
19
|
-
className?: string;
|
|
20
|
-
showTitle?: boolean;
|
|
21
|
-
showMeta?: boolean;
|
|
22
|
-
minimalControls?: boolean;
|
|
23
|
-
/** When true, the poster img gets `fetchpriority="high"` for LCP. Default false (below-the-fold). */
|
|
24
|
-
aboveTheFold?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const YouTubeEmbed: React.FC<YouTubeEmbedProps> = ({
|
|
28
|
-
videoId,
|
|
29
|
-
title = 'YouTube Video',
|
|
30
|
-
className = '',
|
|
31
|
-
showTitle = true,
|
|
32
|
-
showMeta = true,
|
|
33
|
-
minimalControls = false,
|
|
34
|
-
aboveTheFold = false,
|
|
35
|
-
}) => {
|
|
36
|
-
const [activated, setActivated] = useState(false);
|
|
37
|
-
const iframeSlotRef = useRef<HTMLDivElement | null>(null);
|
|
38
|
-
|
|
39
|
-
const embedParams = new URLSearchParams({ autoplay: '1', rel: '0', modestbranding: '1', playsinline: '1' });
|
|
40
|
-
if (minimalControls) {
|
|
41
|
-
embedParams.set('controls', '0');
|
|
42
|
-
embedParams.set('showinfo', '0');
|
|
43
|
-
embedParams.set('fs', '0');
|
|
44
|
-
embedParams.set('iv_load_policy', '3');
|
|
45
|
-
embedParams.set('cc_load_policy', '0');
|
|
46
|
-
embedParams.set('disablekb', '1');
|
|
47
|
-
}
|
|
48
|
-
const embedUrl = `https://www.youtube-nocookie.com/embed/${videoId}?${embedParams.toString()}`;
|
|
49
|
-
const watchUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
50
|
-
const posterJpg = `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`;
|
|
51
|
-
const posterWebp = `https://i.ytimg.com/vi_webp/${videoId}/mqdefault.webp`;
|
|
52
|
-
|
|
53
|
-
// `iframeSlotRef` is a JSX-empty div React owns but never reconciles
|
|
54
|
-
// children into; the `<button>` overlay is a SIBLING of that slot, not
|
|
55
|
-
// a child. Without that split, React would yank the button on
|
|
56
|
-
// re-render and trip `removeChild ... is not a child of this node`.
|
|
57
|
-
const handleActivate = () => {
|
|
58
|
-
const slot = iframeSlotRef.current;
|
|
59
|
-
if (!slot || activated) return;
|
|
60
|
-
const iframe = document.createElement('iframe');
|
|
61
|
-
iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share');
|
|
62
|
-
iframe.setAttribute('allowfullscreen', '');
|
|
63
|
-
iframe.setAttribute('title', title);
|
|
64
|
-
iframe.className = 'absolute inset-0 w-full h-full border-0';
|
|
65
|
-
iframe.src = embedUrl;
|
|
66
|
-
slot.appendChild(iframe);
|
|
67
|
-
setActivated(true);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<div className={`youtube-embed-container my-6 ${className}`}>
|
|
72
|
-
{title && showTitle && (
|
|
73
|
-
<div className="video-title font-sans text-lg font-medium text-ods-text-primary mb-3">
|
|
74
|
-
{title}
|
|
75
|
-
</div>
|
|
76
|
-
)}
|
|
77
|
-
|
|
78
|
-
<div className="video-wrapper relative w-full" style={{ paddingBottom: '56.25%' }}>
|
|
79
|
-
<div className="absolute inset-0 rounded-lg overflow-hidden border border-ods-border bg-ods-card">
|
|
80
|
-
<div ref={iframeSlotRef} className="absolute inset-0" aria-hidden={!activated} />
|
|
81
|
-
{!activated && (
|
|
82
|
-
<button
|
|
83
|
-
type="button"
|
|
84
|
-
aria-label={`Play: ${title}`}
|
|
85
|
-
onClick={handleActivate}
|
|
86
|
-
className="group absolute inset-0 p-0 m-0 border-0 cursor-pointer bg-transparent"
|
|
87
|
-
>
|
|
88
|
-
<picture>
|
|
89
|
-
<source type="image/webp" srcSet={posterWebp} />
|
|
90
|
-
<img
|
|
91
|
-
src={posterJpg}
|
|
92
|
-
alt={title}
|
|
93
|
-
loading="lazy"
|
|
94
|
-
fetchPriority={aboveTheFold ? 'high' : 'low'}
|
|
95
|
-
decoding={aboveTheFold ? 'sync' : 'async'}
|
|
96
|
-
className="absolute inset-0 w-full h-full object-cover"
|
|
97
|
-
/>
|
|
98
|
-
</picture>
|
|
99
|
-
<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">
|
|
100
|
-
<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">
|
|
101
|
-
<svg width={24} height={24} fill="currentColor" viewBox="0 0 24 24" className="ml-1">
|
|
102
|
-
<polygon points="5,3 19,12 5,21" />
|
|
103
|
-
</svg>
|
|
104
|
-
</span>
|
|
105
|
-
</div>
|
|
106
|
-
</button>
|
|
107
|
-
)}
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
|
|
111
|
-
{showMeta && (
|
|
112
|
-
<div className="video-meta flex items-center justify-between mt-3 text-sm text-ods-text-secondary">
|
|
113
|
-
<div className="video-platform font-sans">YouTube</div>
|
|
114
|
-
<a
|
|
115
|
-
href={watchUrl}
|
|
116
|
-
target="_blank"
|
|
117
|
-
rel="noopener noreferrer"
|
|
118
|
-
className="video-link font-sans text-ods-accent hover:text-ods-accent-hover transition-colors duration-200"
|
|
119
|
-
>
|
|
120
|
-
Watch on YouTube →
|
|
121
|
-
</a>
|
|
122
|
-
</div>
|
|
123
|
-
)}
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const YT_HOSTS = new Set([
|
|
129
|
-
'youtube.com', 'www.youtube.com', 'm.youtube.com',
|
|
130
|
-
'youtu.be',
|
|
131
|
-
'youtube-nocookie.com', 'www.youtube-nocookie.com',
|
|
132
|
-
]);
|
|
133
|
-
|
|
134
|
-
// `youtube.com/(embed|v|shorts)/<id>` — anchored, no `.*`, ReDoS-safe.
|
|
135
|
-
const YT_PATH_RE = /^\/(?:embed|v|shorts)\/([^/]+)\/?$/;
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Extract the YouTube video id from any common URL shape. Uses `URL`
|
|
139
|
-
* parsing + a strict, anchored pathname regex — NOT the previous
|
|
140
|
-
* `.*v=` pattern that CodeQL flagged for polynomial-time backtracking
|
|
141
|
-
* on adversarial input like `youtube.com/watch?` repeated N times.
|
|
142
|
-
*/
|
|
143
|
-
export const extractYouTubeId = (url: string): string | null => {
|
|
144
|
-
let u: URL;
|
|
145
|
-
try { u = new URL(url); } catch { return null; }
|
|
146
|
-
if (!YT_HOSTS.has(u.hostname.toLowerCase())) return null;
|
|
147
|
-
// `youtu.be/<id>` — id is the first path segment.
|
|
148
|
-
if (u.hostname.toLowerCase().endsWith('youtu.be')) {
|
|
149
|
-
return u.pathname.split('/').filter(Boolean)[0] ?? null;
|
|
150
|
-
}
|
|
151
|
-
// `youtube.com/watch?v=<id>` — query parameter.
|
|
152
|
-
const v = u.searchParams.get('v');
|
|
153
|
-
if (v) return v;
|
|
154
|
-
// `youtube.com/(embed|v|shorts)/<id>` — anchored pathname match.
|
|
155
|
-
const m = u.pathname.match(YT_PATH_RE);
|
|
156
|
-
return m ? m[1] : null;
|
|
157
|
-
};
|
|
158
|
-
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
export interface LiteYouTubeEmbedProps {
|
|
4
|
-
id: string;
|
|
5
|
-
title?: string;
|
|
6
|
-
poster?: string;
|
|
7
|
-
className?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function LiteYouTubeEmbed({ id, title, poster, className }: LiteYouTubeEmbedProps) {
|
|
11
|
-
return (
|
|
12
|
-
<div className={className}>
|
|
13
|
-
<iframe
|
|
14
|
-
src={`https://www.youtube.com/embed/${id}`}
|
|
15
|
-
title={title}
|
|
16
|
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
17
|
-
allowFullScreen
|
|
18
|
-
/>
|
|
19
|
-
</div>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
interface LiteYouTubeEmbedProps {
|
|
4
|
-
id: string;
|
|
5
|
-
title?: string;
|
|
6
|
-
thumbnail?: string;
|
|
7
|
-
className?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function LiteYouTubeEmbed({ id, title = 'YouTube video', thumbnail, className }: LiteYouTubeEmbedProps) {
|
|
11
|
-
const [isLoaded, setIsLoaded] = React.useState(false);
|
|
12
|
-
|
|
13
|
-
const handleLoad = () => {
|
|
14
|
-
setIsLoaded(true);
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
if (isLoaded) {
|
|
18
|
-
return (
|
|
19
|
-
<iframe
|
|
20
|
-
src={`https://www.youtube.com/embed/${id}`}
|
|
21
|
-
title={title}
|
|
22
|
-
className={className}
|
|
23
|
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
24
|
-
allowFullScreen
|
|
25
|
-
/>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div
|
|
31
|
-
className={`cursor-pointer relative ${className}`}
|
|
32
|
-
onClick={handleLoad}
|
|
33
|
-
>
|
|
34
|
-
<img
|
|
35
|
-
src={thumbnail || `https://img.youtube.com/vi/${id}/maxresdefault.jpg`}
|
|
36
|
-
alt={title}
|
|
37
|
-
className="w-full h-full object-cover"
|
|
38
|
-
/>
|
|
39
|
-
<div className="absolute inset-0 flex items-center justify-center">
|
|
40
|
-
<div className="w-16 h-16 bg-red-600 rounded-full flex items-center justify-center">
|
|
41
|
-
<div className="w-0 h-0 border-l-8 border-l-white border-t-4 border-t-transparent border-b-4 border-b-transparent ml-1" />
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
);
|
|
46
|
-
}
|