@flamingo-stack/openframe-frontend-core 0.0.177 → 0.0.178-snapshot.20260514174355
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-C6ZMI4UB.cjs → chunk-4TM2SBMX.cjs} +884 -1513
- package/dist/chunk-4TM2SBMX.cjs.map +1 -0
- package/dist/{chunk-WX7PT5C7.cjs → chunk-ALW3D72O.cjs} +61 -2
- package/dist/chunk-ALW3D72O.cjs.map +1 -0
- package/dist/{chunk-KB2N44BY.js → chunk-FMWHOUFE.js} +61 -2
- package/dist/chunk-FMWHOUFE.js.map +1 -0
- package/dist/{chunk-6LDN3CIY.js → chunk-ZMQP3UZJ.js} +3701 -4330
- 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 +17 -3
- 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 +22 -8
- 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 +17 -3
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +22 -8
- package/dist/components/media-carousel.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/hooks/index.cjs +4 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +3 -1
- package/dist/hooks/use-near-viewport.d.ts +42 -0
- package/dist/hooks/use-near-viewport.d.ts.map +1 -0
- package/dist/index.cjs +19 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +24 -8
- 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/src/hooks/index.ts +3 -0
- package/src/hooks/use-near-viewport.ts +118 -0
- package/dist/chunk-6LDN3CIY.js.map +0 -1
- package/dist/chunk-C6ZMI4UB.cjs.map +0 -1
- package/dist/chunk-KB2N44BY.js.map +0 -1
- package/dist/chunk-WX7PT5C7.cjs.map +0 -1
- package/dist/components/features/video-player.d.ts +0 -47
- package/dist/components/features/video-player.d.ts.map +0 -1
- package/dist/components/features/youtube-embed.d.ts +0 -17
- 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/__tests__/video-player.test.tsx +0 -142
- package/src/components/features/video-player.tsx +0 -1030
- package/src/components/features/youtube-embed.tsx +0 -275
- package/src/utils/lite-youtube-embed-stub.tsx +0 -21
- package/src/utils/lite-youtube-embed.tsx +0 -46
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useState, useEffect } from 'react';
|
|
4
|
-
import ReactPlayer from 'react-player/youtube';
|
|
5
|
-
|
|
6
|
-
// Simple SVG icon components
|
|
7
|
-
const Play = ({ size = 16, className }: { size?: number; className?: string }) => (
|
|
8
|
-
<svg width={size} height={size} fill="currentColor" viewBox="0 0 24 24" className={className}>
|
|
9
|
-
<polygon points="5,3 19,12 5,21"/>
|
|
10
|
-
</svg>
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
const Loader = ({ size = 16, className }: { size?: number; className?: string }) => (
|
|
14
|
-
<svg width={size} height={size} fill="none" stroke="currentColor" viewBox="0 0 24 24" className={className}>
|
|
15
|
-
<line x1="12" y1="2" x2="12" y2="6"/>
|
|
16
|
-
<line x1="12" y1="18" x2="12" y2="22"/>
|
|
17
|
-
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/>
|
|
18
|
-
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/>
|
|
19
|
-
<line x1="2" y1="12" x2="6" y2="12"/>
|
|
20
|
-
<line x1="18" y1="12" x2="22" y2="12"/>
|
|
21
|
-
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/>
|
|
22
|
-
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/>
|
|
23
|
-
</svg>
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
interface YouTubeEmbedProps {
|
|
27
|
-
videoId: string;
|
|
28
|
-
title?: string;
|
|
29
|
-
className?: string;
|
|
30
|
-
showTitle?: boolean;
|
|
31
|
-
showMeta?: boolean;
|
|
32
|
-
minimalControls?: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const YouTubeEmbed: React.FC<YouTubeEmbedProps> = ({
|
|
36
|
-
videoId,
|
|
37
|
-
title = "YouTube Video",
|
|
38
|
-
className = "",
|
|
39
|
-
showTitle = true,
|
|
40
|
-
showMeta = true,
|
|
41
|
-
minimalControls = false
|
|
42
|
-
}) => {
|
|
43
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
44
|
-
const [hasError, setHasError] = useState(false);
|
|
45
|
-
const [isPlaying, setIsPlaying] = useState(false);
|
|
46
|
-
const [useIframe, setUseIframe] = useState(false);
|
|
47
|
-
const [mounted, setMounted] = useState(false);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
setMounted(true);
|
|
51
|
-
|
|
52
|
-
// Always use iframe to avoid CSP issues with ReactPlayer scripts
|
|
53
|
-
setUseIframe(true);
|
|
54
|
-
setIsLoading(false);
|
|
55
|
-
}, []);
|
|
56
|
-
|
|
57
|
-
const handleReady = () => {
|
|
58
|
-
setIsLoading(false);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const handleError = () => {
|
|
62
|
-
setIsLoading(false);
|
|
63
|
-
setHasError(true);
|
|
64
|
-
// Fallback to iframe on error
|
|
65
|
-
setUseIframe(true);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const handlePlay = () => {
|
|
69
|
-
setIsPlaying(true);
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const handlePause = () => {
|
|
73
|
-
setIsPlaying(false);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
77
|
-
|
|
78
|
-
// Build embed URL with conditional parameters
|
|
79
|
-
const embedParams = new URLSearchParams({
|
|
80
|
-
rel: '0',
|
|
81
|
-
modestbranding: '1',
|
|
82
|
-
playsinline: '1'
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
if (minimalControls) {
|
|
86
|
-
embedParams.set('controls', '0'); // Hide all controls
|
|
87
|
-
embedParams.set('showinfo', '0'); // Hide video info
|
|
88
|
-
embedParams.set('fs', '0'); // Disable fullscreen
|
|
89
|
-
embedParams.set('iv_load_policy', '3'); // Hide annotations
|
|
90
|
-
embedParams.set('cc_load_policy', '0'); // Hide captions
|
|
91
|
-
embedParams.set('disablekb', '1'); // Disable keyboard controls
|
|
92
|
-
embedParams.set('rel', '0'); // Hide related videos (already set above, but ensure it's enforced)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const embedUrl = `https://www.youtube.com/embed/${videoId}?${embedParams.toString()}`;
|
|
96
|
-
|
|
97
|
-
// Don't render until mounted to prevent hydration mismatch
|
|
98
|
-
if (!mounted) {
|
|
99
|
-
return (
|
|
100
|
-
<div className={`youtube-embed-container my-6 ${className}`}>
|
|
101
|
-
<div className="video-wrapper relative w-full" style={{ paddingBottom: '56.25%' }}>
|
|
102
|
-
<div className="loading-overlay absolute inset-0 bg-ods-card border border-ods-border rounded-lg flex items-center justify-center">
|
|
103
|
-
<div className="loading-content flex flex-col items-center gap-3">
|
|
104
|
-
<Loader className="animate-spin text-ods-accent" size={32} />
|
|
105
|
-
<span className="font-sans text-sm text-ods-text-secondary">Loading video...</span>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (hasError) {
|
|
114
|
-
return (
|
|
115
|
-
<div className={`youtube-embed-error ${className}`}>
|
|
116
|
-
<div className="error-state bg-ods-card border border-ods-error rounded-lg p-6 text-center my-6">
|
|
117
|
-
<div className="error-icon flex justify-center mb-4">
|
|
118
|
-
<svg width="48" height="48" fill="currentColor" viewBox="0 0 24 24" className="text-ods-error">
|
|
119
|
-
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
|
120
|
-
</svg>
|
|
121
|
-
</div>
|
|
122
|
-
<div className="error-title font-sans font-semibold text-lg text-ods-error mb-2">
|
|
123
|
-
Video Unavailable
|
|
124
|
-
</div>
|
|
125
|
-
<div className="error-description font-sans text-sm text-ods-text-secondary mb-4">
|
|
126
|
-
Unable to load YouTube video. The video may be private or deleted.
|
|
127
|
-
</div>
|
|
128
|
-
<a
|
|
129
|
-
href={videoUrl}
|
|
130
|
-
target="_blank"
|
|
131
|
-
rel="noopener noreferrer"
|
|
132
|
-
className="error-retry-button bg-ods-error hover:bg-ods-error-hover text-ods-text-on-error border-none rounded px-4 py-2 font-sans font-medium text-sm cursor-pointer transition-colors duration-200"
|
|
133
|
-
>
|
|
134
|
-
Watch on YouTube
|
|
135
|
-
</a>
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<div className={`youtube-embed-container my-6 ${className}`}>
|
|
143
|
-
{/* Video Title */}
|
|
144
|
-
{title && showTitle && (
|
|
145
|
-
<div className="video-title font-sans text-lg font-medium text-ods-text-primary mb-3">
|
|
146
|
-
{title}
|
|
147
|
-
</div>
|
|
148
|
-
)}
|
|
149
|
-
|
|
150
|
-
{/* Video Container with 16:9 Aspect Ratio */}
|
|
151
|
-
<div className="video-wrapper relative w-full" style={{ paddingBottom: '56.25%' }}>
|
|
152
|
-
{/* Loading State */}
|
|
153
|
-
{isLoading && (
|
|
154
|
-
<div className="loading-overlay absolute inset-0 bg-ods-card border border-ods-border rounded-lg flex items-center justify-center">
|
|
155
|
-
<div className="loading-content flex flex-col items-center gap-3">
|
|
156
|
-
<Loader className="animate-spin text-ods-accent" size={32} />
|
|
157
|
-
<span className="font-sans text-sm text-ods-text-secondary">Loading video...</span>
|
|
158
|
-
</div>
|
|
159
|
-
</div>
|
|
160
|
-
)}
|
|
161
|
-
|
|
162
|
-
{/* Video Player */}
|
|
163
|
-
<div className="video-player absolute inset-0 rounded-lg overflow-hidden border border-ods-border bg-ods-bg-inverse">
|
|
164
|
-
{useIframe ? (
|
|
165
|
-
// Iframe fallback for mobile and error cases
|
|
166
|
-
<iframe
|
|
167
|
-
src={embedUrl}
|
|
168
|
-
title={title}
|
|
169
|
-
className="w-full h-full border-0"
|
|
170
|
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
171
|
-
allowFullScreen
|
|
172
|
-
style={{ border: 'none' }}
|
|
173
|
-
/>
|
|
174
|
-
) : (
|
|
175
|
-
<ReactPlayer
|
|
176
|
-
url={videoUrl}
|
|
177
|
-
width="100%"
|
|
178
|
-
height="100%"
|
|
179
|
-
onReady={handleReady}
|
|
180
|
-
onError={handleError}
|
|
181
|
-
onPlay={handlePlay}
|
|
182
|
-
onPause={handlePause}
|
|
183
|
-
config={{
|
|
184
|
-
youtube: {
|
|
185
|
-
playerVars: {
|
|
186
|
-
autoplay: 0,
|
|
187
|
-
controls: 1,
|
|
188
|
-
rel: 0,
|
|
189
|
-
showinfo: 0,
|
|
190
|
-
modestbranding: 1,
|
|
191
|
-
iv_load_policy: 3,
|
|
192
|
-
cc_load_policy: 0,
|
|
193
|
-
playsinline: 1
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
} as any}
|
|
197
|
-
light={false} // Show video immediately
|
|
198
|
-
playing={false} // Don't autoplay
|
|
199
|
-
/>
|
|
200
|
-
)}
|
|
201
|
-
</div>
|
|
202
|
-
|
|
203
|
-
{/* Custom Play Button Overlay (optional enhancement) - only for ReactPlayer */}
|
|
204
|
-
{!useIframe && !isPlaying && !isLoading && (
|
|
205
|
-
<div className="play-overlay absolute inset-0 flex items-center justify-center bg-ods-bg-inverse bg-opacity-20 rounded-lg transition-opacity duration-300 hover:bg-opacity-30">
|
|
206
|
-
<button
|
|
207
|
-
onClick={() => setIsPlaying(true)}
|
|
208
|
-
className="play-button bg-ods-accent hover:bg-ods-accent-hover text-ods-text-on-accent w-16 h-16 rounded-full flex items-center justify-center transition-all duration-200 transform hover:scale-110 shadow-lg"
|
|
209
|
-
aria-label="Play video"
|
|
210
|
-
>
|
|
211
|
-
<Play size={24} className="ml-1" />
|
|
212
|
-
</button>
|
|
213
|
-
</div>
|
|
214
|
-
)}
|
|
215
|
-
</div>
|
|
216
|
-
|
|
217
|
-
{/* Video Meta Information */}
|
|
218
|
-
{showMeta && (
|
|
219
|
-
<div className="video-meta flex items-center justify-between mt-3 text-sm text-ods-text-secondary">
|
|
220
|
-
<div className="video-platform font-sans">
|
|
221
|
-
YouTube
|
|
222
|
-
</div>
|
|
223
|
-
<a
|
|
224
|
-
href={videoUrl}
|
|
225
|
-
target="_blank"
|
|
226
|
-
rel="noopener noreferrer"
|
|
227
|
-
className="video-link font-sans text-ods-accent hover:text-ods-accent-hover transition-colors duration-200"
|
|
228
|
-
>
|
|
229
|
-
Watch on YouTube →
|
|
230
|
-
</a>
|
|
231
|
-
</div>
|
|
232
|
-
)}
|
|
233
|
-
</div>
|
|
234
|
-
);
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// Utility function to extract YouTube video ID from various URL formats
|
|
238
|
-
export const extractYouTubeId = (url: string): string | null => {
|
|
239
|
-
const patterns = [
|
|
240
|
-
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/,
|
|
241
|
-
/youtube\.com\/v\/([^&\n?#]+)/,
|
|
242
|
-
/youtube\.com\/watch\?.*v=([^&\n?#]+)/
|
|
243
|
-
];
|
|
244
|
-
|
|
245
|
-
for (const pattern of patterns) {
|
|
246
|
-
const match = url.match(pattern);
|
|
247
|
-
if (match) {
|
|
248
|
-
return match[1];
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return null;
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
// Component for parsing YouTube URLs in markdown
|
|
256
|
-
export const YouTubeLinkParser: React.FC<{ href: string; children: React.ReactNode }> = ({ href, children }) => {
|
|
257
|
-
const videoId = extractYouTubeId(href);
|
|
258
|
-
|
|
259
|
-
// If it's a YouTube URL, render the embed instead of a link
|
|
260
|
-
if (videoId) {
|
|
261
|
-
return <YouTubeEmbed videoId={videoId} title={typeof children === 'string' ? children : undefined} />;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Otherwise, render as a normal link
|
|
265
|
-
return (
|
|
266
|
-
<a
|
|
267
|
-
href={href}
|
|
268
|
-
target="_blank"
|
|
269
|
-
rel="noopener noreferrer"
|
|
270
|
-
className="text-ods-accent hover:text-ods-accent-hover transition-colors duration-200"
|
|
271
|
-
>
|
|
272
|
-
{children}
|
|
273
|
-
</a>
|
|
274
|
-
);
|
|
275
|
-
};
|
|
@@ -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
|
-
}
|