@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,8 +0,0 @@
|
|
|
1
|
-
export interface LiteYouTubeEmbedProps {
|
|
2
|
-
id: string;
|
|
3
|
-
title?: string;
|
|
4
|
-
poster?: string;
|
|
5
|
-
className?: string;
|
|
6
|
-
}
|
|
7
|
-
export declare function LiteYouTubeEmbed({ id, title, poster, className }: LiteYouTubeEmbedProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
-
//# sourceMappingURL=lite-youtube-embed-stub.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lite-youtube-embed-stub.d.ts","sourceRoot":"","sources":["../../src/utils/lite-youtube-embed-stub.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,qBAAqB,2CAWvF"}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
interface LiteYouTubeEmbedProps {
|
|
2
|
-
id: string;
|
|
3
|
-
title?: string;
|
|
4
|
-
thumbnail?: string;
|
|
5
|
-
className?: string;
|
|
6
|
-
}
|
|
7
|
-
export declare function LiteYouTubeEmbed({ id, title, thumbnail, className }: LiteYouTubeEmbedProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
-
export {};
|
|
9
|
-
//# sourceMappingURL=lite-youtube-embed.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lite-youtube-embed.d.ts","sourceRoot":"","sources":["../../src/utils/lite-youtube-embed.tsx"],"names":[],"mappings":"AAEA,UAAU,qBAAqB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,EAAE,KAAuB,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,qBAAqB,2CAoC5G"}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
<!-- source-hash: ac2c9623eeb0459e6abe9a41a32951c3 -->
|
|
2
|
-
A comprehensive React video player component built on ReactPlayer with custom poster overlay, loading states, and error handling for the OpenFrame platform.
|
|
3
|
-
|
|
4
|
-
## Key Components
|
|
5
|
-
|
|
6
|
-
- **VideoPlayer**: Main component with configurable playback options and responsive design
|
|
7
|
-
- **Play**: SVG icon component for the play button overlay
|
|
8
|
-
- **Loader**: Animated SVG spinner for loading states
|
|
9
|
-
- **VideoPlayerProps**: TypeScript interface defining all configuration options including aspect ratio control, autoplay, and poster image settings
|
|
10
|
-
|
|
11
|
-
## Usage Example
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import { VideoPlayer } from './video-player';
|
|
15
|
-
|
|
16
|
-
// Basic video with poster and title
|
|
17
|
-
<VideoPlayer
|
|
18
|
-
url="https://example.com/video.mp4"
|
|
19
|
-
title="Demo Video"
|
|
20
|
-
poster="https://example.com/thumbnail.jpg"
|
|
21
|
-
showTitle={true}
|
|
22
|
-
controls={true}
|
|
23
|
-
/>
|
|
24
|
-
|
|
25
|
-
// Auto-playing video with native aspect ratio
|
|
26
|
-
<VideoPlayer
|
|
27
|
-
url="https://youtube.com/watch?v=VIDEO_ID"
|
|
28
|
-
autoPlay={true}
|
|
29
|
-
muted={true}
|
|
30
|
-
loop={true}
|
|
31
|
-
useNativeAspectRatio={true}
|
|
32
|
-
className="custom-player"
|
|
33
|
-
/>
|
|
34
|
-
|
|
35
|
-
// Video with custom poster click behavior
|
|
36
|
-
<VideoPlayer
|
|
37
|
-
url="https://vimeo.com/123456789"
|
|
38
|
-
poster="custom-thumbnail.png"
|
|
39
|
-
showTitle={false}
|
|
40
|
-
controls={false}
|
|
41
|
-
/>
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
The component handles client-side hydration, provides comprehensive error states, and integrates with OpenFrame's design system using ODS CSS classes. It supports both 16:9 aspect ratio (default) and native video dimensions through the `useNativeAspectRatio` prop.
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
<!-- source-hash: fc74dea08c8b565aa3e51c72946544d6 -->
|
|
2
|
-
A React component for embedding YouTube videos with responsive design, loading states, and fallback support for the Flamingo documentation system.
|
|
3
|
-
|
|
4
|
-
## Key Components
|
|
5
|
-
|
|
6
|
-
- **`YouTubeEmbed`** - Main YouTube video embed component with configurable options
|
|
7
|
-
- **`Play`** - SVG play button icon component
|
|
8
|
-
- **`Loader`** - Animated loading spinner icon component
|
|
9
|
-
- **`extractYouTubeId`** - Utility function to extract video IDs from various YouTube URL formats
|
|
10
|
-
- **`YouTubeLinkParser`** - Component that automatically converts YouTube links to embedded videos
|
|
11
|
-
|
|
12
|
-
## Usage Example
|
|
13
|
-
|
|
14
|
-
```typescript
|
|
15
|
-
import { YouTubeEmbed, extractYouTubeId } from './youtube-embed';
|
|
16
|
-
|
|
17
|
-
// Basic embed
|
|
18
|
-
<YouTubeEmbed videoId="dQw4w9WgXcQ" />
|
|
19
|
-
|
|
20
|
-
// With custom options
|
|
21
|
-
<YouTubeEmbed
|
|
22
|
-
videoId="dQw4w9WgXcQ"
|
|
23
|
-
title="Custom Video Title"
|
|
24
|
-
showTitle={true}
|
|
25
|
-
showMeta={false}
|
|
26
|
-
minimalControls={true}
|
|
27
|
-
className="my-custom-class"
|
|
28
|
-
/>
|
|
29
|
-
|
|
30
|
-
// Extract video ID from URL
|
|
31
|
-
const videoId = extractYouTubeId("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
|
|
32
|
-
// Returns: "dQw4w9WgXcQ"
|
|
33
|
-
|
|
34
|
-
// Auto-convert YouTube links in markdown
|
|
35
|
-
<YouTubeLinkParser href="https://youtu.be/dQw4w9WgXcQ">
|
|
36
|
-
Video Title
|
|
37
|
-
</YouTubeLinkParser>
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
The component uses iframe embedding by default for better CSP compatibility, includes responsive 16:9 aspect ratio containers, and provides error handling with fallback to YouTube's website. It supports various display options including minimal controls mode and customizable meta information display.
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render } from '@testing-library/react'
|
|
2
|
-
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Regression test for the case-study video performance fix:
|
|
6
|
-
* plan: /Users/michaelassraf/.claude/plans/we-have-terrible-serious-radiant-turing.md
|
|
7
|
-
*
|
|
8
|
-
* Bug: VideoPlayer mounted two competing <video> elements on every render when no
|
|
9
|
-
* `poster` prop was passed — the hidden first-frame extractor (`useVideoFirstFramePoster`)
|
|
10
|
-
* raced ReactPlayer's own metadata fetch on the same URL, doubling bandwidth and
|
|
11
|
-
* stalling click-to-play on big MP4s.
|
|
12
|
-
*
|
|
13
|
-
* Fix: opt-in `lazyMount` prop that:
|
|
14
|
-
* 1. Skips `useVideoFirstFramePoster` (no hidden video)
|
|
15
|
-
* 2. Sets `preload="none"` on ReactPlayer's <video> until user clicks play
|
|
16
|
-
* 3. Synchronously calls .play() in the click handler for iOS user-activation safety
|
|
17
|
-
*
|
|
18
|
-
* Test strategy:
|
|
19
|
-
* - To detect "did the hidden poster <video> get created?", we tag the hook's
|
|
20
|
-
* unique signature — it sets `crossOrigin="anonymous"` on the element. Spy on
|
|
21
|
-
* `document.createElement` and count VIDEO elements that subsequently have
|
|
22
|
-
* `crossOrigin` set. React's own DOM render does NOT set crossOrigin on the
|
|
23
|
-
* mocked <video>, so this isolates the bug surface cleanly.
|
|
24
|
-
* - Mock `react-player` so we don't depend on its internal FilePlayer dynamic
|
|
25
|
-
* import (broken under JSDOM / Vitest's module loader).
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
// Mock react-player as a forwardRef component that exposes the same
|
|
29
|
-
// `getInternalPlayer()` surface used by VideoPlayer (line ~697 lazyMount sync play).
|
|
30
|
-
// Without this, `playerRef.current?.getInternalPlayer()` returns undefined under
|
|
31
|
-
// the test, the `instanceof HTMLVideoElement` guard fails, and the iOS user-
|
|
32
|
-
// activation path is silently never exercised by the test.
|
|
33
|
-
vi.mock('react-player', async () => {
|
|
34
|
-
const React = await import('react')
|
|
35
|
-
const RP = React.forwardRef<
|
|
36
|
-
{ getInternalPlayer: () => HTMLVideoElement | null },
|
|
37
|
-
{ url: string; config?: { file?: { attributes?: Record<string, unknown> } } }
|
|
38
|
-
>(function MockReactPlayer({ url, config }, ref) {
|
|
39
|
-
const videoRef = React.useRef<HTMLVideoElement | null>(null)
|
|
40
|
-
React.useImperativeHandle(ref, () => ({
|
|
41
|
-
getInternalPlayer: () => videoRef.current,
|
|
42
|
-
}))
|
|
43
|
-
const preload = (config?.file?.attributes?.preload as string) ?? 'metadata'
|
|
44
|
-
return <video ref={videoRef} src={url} preload={preload as 'none' | 'metadata' | 'auto'} />
|
|
45
|
-
})
|
|
46
|
-
return { default: RP }
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
beforeAll(() => {
|
|
50
|
-
// JSDOM doesn't implement these — react-player's internals (and the poster hook) call them.
|
|
51
|
-
Object.defineProperty(HTMLMediaElement.prototype, 'canPlayType', {
|
|
52
|
-
value: () => 'maybe',
|
|
53
|
-
writable: true,
|
|
54
|
-
configurable: true,
|
|
55
|
-
})
|
|
56
|
-
Object.defineProperty(HTMLMediaElement.prototype, 'load', {
|
|
57
|
-
value: () => undefined,
|
|
58
|
-
writable: true,
|
|
59
|
-
configurable: true,
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
import { VideoPlayer } from '../video-player'
|
|
64
|
-
|
|
65
|
-
const SAMPLE_MP4 = 'https://example.com/test.mp4'
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Detects the poster hook's signature via the `data-poster-extractor` attribute
|
|
69
|
-
* the hook sets on its hidden <video>. This is more durable than checking
|
|
70
|
-
* `crossOrigin === 'anonymous'` (any future code that happens to set crossOrigin
|
|
71
|
-
* on a freshly-created video would inflate the count and silently flake-pass).
|
|
72
|
-
*/
|
|
73
|
-
function spyForHiddenPosterVideos(): { count: () => number; restore: () => void } {
|
|
74
|
-
const original = document.createElement.bind(document)
|
|
75
|
-
const tracked: HTMLVideoElement[] = []
|
|
76
|
-
document.createElement = ((tag: string, opts?: ElementCreationOptions) => {
|
|
77
|
-
const el = original(tag, opts)
|
|
78
|
-
if (tag.toLowerCase() === 'video') tracked.push(el as HTMLVideoElement)
|
|
79
|
-
return el
|
|
80
|
-
}) as typeof document.createElement
|
|
81
|
-
return {
|
|
82
|
-
count: () => tracked.filter(v => v.getAttribute('data-poster-extractor') === 'true').length,
|
|
83
|
-
restore: () => { document.createElement = original },
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
describe('VideoPlayer lazyMount', () => {
|
|
88
|
-
let videoSpy: ReturnType<typeof spyForHiddenPosterVideos> | null = null
|
|
89
|
-
|
|
90
|
-
afterEach(() => {
|
|
91
|
-
videoSpy?.restore()
|
|
92
|
-
videoSpy = null
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('does NOT instantiate a hidden poster <video> when lazyMount=true (the actual fix)', () => {
|
|
96
|
-
videoSpy = spyForHiddenPosterVideos()
|
|
97
|
-
render(<VideoPlayer url={SAMPLE_MP4} lazyMount />)
|
|
98
|
-
expect(videoSpy.count()).toBe(0)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('renders ReactPlayer with preload="none" pre-click when lazyMount=true', () => {
|
|
102
|
-
const { container } = render(<VideoPlayer url={SAMPLE_MP4} lazyMount />)
|
|
103
|
-
const videos = container.querySelectorAll('video')
|
|
104
|
-
expect(videos.length).toBe(1)
|
|
105
|
-
expect(videos[0].getAttribute('preload')).toBe('none')
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('preserves existing preload="metadata" + poster-hook behavior when lazyMount=false (default)', () => {
|
|
109
|
-
videoSpy = spyForHiddenPosterVideos()
|
|
110
|
-
const { container } = render(<VideoPlayer url={SAMPLE_MP4} />)
|
|
111
|
-
expect(container.querySelectorAll('video')[0].getAttribute('preload')).toBe('metadata')
|
|
112
|
-
// The poster hook DOES create one hidden <video> when no poster is provided.
|
|
113
|
-
// This is the legacy behavior we explicitly opted out of via lazyMount.
|
|
114
|
-
expect(videoSpy.count()).toBeGreaterThanOrEqual(1)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('skips the poster hook when an explicit poster is provided (existing gate, regression check)', () => {
|
|
118
|
-
videoSpy = spyForHiddenPosterVideos()
|
|
119
|
-
render(<VideoPlayer url={SAMPLE_MP4} poster="https://example.com/poster.jpg" />)
|
|
120
|
-
expect(videoSpy.count()).toBe(0)
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('calls play() synchronously inside the click handler when lazyMount=true (iOS user-activation)', () => {
|
|
124
|
-
const playSpy = vi.fn().mockResolvedValue(undefined)
|
|
125
|
-
Object.defineProperty(HTMLMediaElement.prototype, 'play', {
|
|
126
|
-
value: playSpy,
|
|
127
|
-
writable: true,
|
|
128
|
-
configurable: true,
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
const { container } = render(<VideoPlayer url={SAMPLE_MP4} lazyMount />)
|
|
132
|
-
// The play overlay div has `onClick={handlePlayClick}` and uses the
|
|
133
|
-
// 'group cursor-pointer' classes. Find by role/aria isn't reliable since
|
|
134
|
-
// the overlay is a styled div, so query by the unique class combo.
|
|
135
|
-
const overlay = container.querySelector('div.cursor-pointer.group') as HTMLElement | null
|
|
136
|
-
expect(overlay).toBeTruthy()
|
|
137
|
-
fireEvent.click(overlay!)
|
|
138
|
-
// play() must be called synchronously inside the click event task — if it
|
|
139
|
-
// were deferred (e.g., to a useEffect), iOS Safari would block playback.
|
|
140
|
-
expect(playSpy).toHaveBeenCalled()
|
|
141
|
-
})
|
|
142
|
-
})
|