@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.
Files changed (53) hide show
  1. package/dist/{chunk-L4T24AN4.cjs → chunk-4TM2SBMX.cjs} +855 -1325
  2. package/dist/chunk-4TM2SBMX.cjs.map +1 -0
  3. package/dist/{chunk-AAX27BCR.js → chunk-ZMQP3UZJ.js} +3690 -4160
  4. package/dist/chunk-ZMQP3UZJ.js.map +1 -0
  5. package/dist/components/features/entity-video-section.d.ts +54 -0
  6. package/dist/components/features/entity-video-section.d.ts.map +1 -0
  7. package/dist/components/features/index.cjs +18 -2
  8. package/dist/components/features/index.cjs.map +1 -1
  9. package/dist/components/features/index.d.ts +4 -2
  10. package/dist/components/features/index.d.ts.map +1 -1
  11. package/dist/components/features/index.js +21 -5
  12. package/dist/components/features/video-bites-display.d.ts +38 -0
  13. package/dist/components/features/video-bites-display.d.ts.map +1 -0
  14. package/dist/components/features/video-ratio-tabs.d.ts +62 -0
  15. package/dist/components/features/video-ratio-tabs.d.ts.map +1 -0
  16. package/dist/components/features/video.d.ts +94 -0
  17. package/dist/components/features/video.d.ts.map +1 -0
  18. package/dist/components/index.cjs +18 -2
  19. package/dist/components/index.cjs.map +1 -1
  20. package/dist/components/index.js +21 -5
  21. package/dist/components/media-carousel.d.ts.map +1 -1
  22. package/dist/components/navigation/index.cjs +2 -2
  23. package/dist/components/navigation/index.js +1 -1
  24. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  25. package/dist/components/ui/index.cjs +2 -2
  26. package/dist/components/ui/index.js +1 -1
  27. package/dist/index.cjs +18 -2
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.js +21 -5
  30. package/package.json +2 -2
  31. package/src/components/features/entity-video-section.tsx +175 -0
  32. package/src/components/features/index.ts +9 -2
  33. package/src/components/features/video-bites-display.tsx +216 -0
  34. package/src/components/features/video-ratio-tabs.tsx +174 -0
  35. package/src/components/features/video.tsx +474 -0
  36. package/src/components/media-carousel.tsx +43 -236
  37. package/src/components/shared/product-release/release-detail-page.tsx +26 -19
  38. package/dist/chunk-AAX27BCR.js.map +0 -1
  39. package/dist/chunk-L4T24AN4.cjs.map +0 -1
  40. package/dist/components/features/video-player.d.ts +0 -44
  41. package/dist/components/features/video-player.d.ts.map +0 -1
  42. package/dist/components/features/youtube-embed.d.ts +0 -31
  43. package/dist/components/features/youtube-embed.d.ts.map +0 -1
  44. package/dist/utils/lite-youtube-embed-stub.d.ts +0 -8
  45. package/dist/utils/lite-youtube-embed-stub.d.ts.map +0 -1
  46. package/dist/utils/lite-youtube-embed.d.ts +0 -9
  47. package/dist/utils/lite-youtube-embed.d.ts.map +0 -1
  48. package/src/components/features/.video-player.md +0 -44
  49. package/src/components/features/.youtube-embed.md +0 -40
  50. package/src/components/features/video-player.tsx +0 -893
  51. package/src/components/features/youtube-embed.tsx +0 -158
  52. package/src/utils/lite-youtube-embed-stub.tsx +0 -21
  53. 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
- }