@nationaldesignstudio/react 0.5.4 → 0.5.6
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/component-registry.md +1286 -40
- package/dist/index.d.ts +1572 -133
- package/dist/index.js +2245 -257
- package/dist/index.js.map +1 -1
- package/dist/tokens.css +680 -0
- package/package.json +20 -1
- package/src/components/atoms/blurred-video-backdrop/blurred-video-backdrop.tsx +447 -0
- package/src/components/atoms/button/icon-button.tsx +10 -4
- package/src/components/atoms/select/select.tsx +202 -49
- package/src/components/atoms/video-player/caption-overlay.tsx +107 -0
- package/src/components/atoms/video-player/video-player.tsx +811 -0
- package/src/components/molecules/dialog/dialog.tsx +526 -0
- package/src/components/molecules/video-dialog/video-dialog.tsx +272 -0
- package/src/components/molecules/video-with-backdrop/video-with-backdrop.tsx +383 -0
- package/src/hooks/index.ts +16 -0
- package/src/hooks/use-breakpoint.ts +145 -0
- package/src/hooks/use-captions.ts +247 -0
- package/src/hooks/use-video-keyboard.ts +230 -0
- package/src/lib/utils.ts +8 -1
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Dialog as BaseDialog } from "@base-ui-components/react/dialog";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { tv } from "tailwind-variants";
|
|
6
|
+
import {
|
|
7
|
+
BlurredVideoBackdrop,
|
|
8
|
+
type BlurredVideoBackdropProps,
|
|
9
|
+
} from "@/components/atoms/blurred-video-backdrop";
|
|
10
|
+
import {
|
|
11
|
+
VideoPlayer,
|
|
12
|
+
type VideoPlayerProps,
|
|
13
|
+
} from "@/components/atoms/video-player";
|
|
14
|
+
import { cn } from "@/lib/utils";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Variant Definitions
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Video dialog container variants.
|
|
22
|
+
* Uses fixed positioning to cover the viewport.
|
|
23
|
+
*/
|
|
24
|
+
const videoDialogVariants = tv({
|
|
25
|
+
base: [
|
|
26
|
+
// Fixed positioning covering viewport
|
|
27
|
+
"fixed inset-0",
|
|
28
|
+
// Dark background base
|
|
29
|
+
"bg-black",
|
|
30
|
+
// Flex centering for the video
|
|
31
|
+
"flex items-center justify-center",
|
|
32
|
+
// Animation
|
|
33
|
+
"transition-opacity duration-300",
|
|
34
|
+
"data-[starting-style]:opacity-0",
|
|
35
|
+
"data-[ending-style]:opacity-0",
|
|
36
|
+
// Focus outline
|
|
37
|
+
"outline-none",
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Close button variants.
|
|
43
|
+
*/
|
|
44
|
+
const closeButtonVariants = tv({
|
|
45
|
+
base: [
|
|
46
|
+
// Positioning
|
|
47
|
+
"absolute z-50",
|
|
48
|
+
// Size and shape
|
|
49
|
+
"w-48 h-48 rounded-full",
|
|
50
|
+
// Colors
|
|
51
|
+
"bg-black/60 text-white",
|
|
52
|
+
"hover:bg-black/80",
|
|
53
|
+
// Transition
|
|
54
|
+
"transition-all duration-150",
|
|
55
|
+
// Focus
|
|
56
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-white/50",
|
|
57
|
+
// Flex centering for icon
|
|
58
|
+
"flex items-center justify-center",
|
|
59
|
+
// Cursor
|
|
60
|
+
"cursor-pointer",
|
|
61
|
+
],
|
|
62
|
+
variants: {
|
|
63
|
+
position: {
|
|
64
|
+
"top-right": "top-24 right-24",
|
|
65
|
+
"top-left": "top-24 left-24",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
defaultVariants: {
|
|
69
|
+
position: "top-right",
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Video container variants.
|
|
75
|
+
* Expands to fill viewport, constrained by height (maintains aspect ratio).
|
|
76
|
+
*/
|
|
77
|
+
const videoContainerVariants = tv({
|
|
78
|
+
base: [
|
|
79
|
+
// Relative for z-index
|
|
80
|
+
"relative z-10",
|
|
81
|
+
// Fill available space
|
|
82
|
+
"w-full h-full",
|
|
83
|
+
// Flex to center the video
|
|
84
|
+
"flex items-center justify-center",
|
|
85
|
+
// Padding from viewport edges
|
|
86
|
+
"p-16 sm:p-24 lg:p-32",
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// VideoDialog Component
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
export interface VideoDialogProps
|
|
95
|
+
extends Omit<VideoPlayerProps, "videoRef" | "className"> {
|
|
96
|
+
/** Trigger element that opens the dialog */
|
|
97
|
+
trigger: React.ReactNode;
|
|
98
|
+
/** Blur intensity for backdrop (default: high) */
|
|
99
|
+
blur?: BlurredVideoBackdropProps["blur"];
|
|
100
|
+
/** Gradient overlay for backdrop (default: vignette) */
|
|
101
|
+
overlay?: BlurredVideoBackdropProps["overlay"];
|
|
102
|
+
/** Backdrop opacity (default: 0.6) */
|
|
103
|
+
backdropOpacity?: number;
|
|
104
|
+
/** Whether to show close button (default: true) */
|
|
105
|
+
showClose?: boolean;
|
|
106
|
+
/** Close button position (default: top-right) */
|
|
107
|
+
closePosition?: "top-right" | "top-left";
|
|
108
|
+
/** Video player rounded corners (default: lg) */
|
|
109
|
+
rounded?: VideoPlayerProps["rounded"];
|
|
110
|
+
/** Controlled open state */
|
|
111
|
+
open?: boolean;
|
|
112
|
+
/** Default open state */
|
|
113
|
+
defaultOpen?: boolean;
|
|
114
|
+
/** Callback when open state changes */
|
|
115
|
+
onOpenChange?: (open: boolean) => void;
|
|
116
|
+
/** Additional className for the dialog container */
|
|
117
|
+
className?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* VideoDialog - Fullscreen video modal with blurred video backdrop.
|
|
122
|
+
*
|
|
123
|
+
* Creates an immersive video viewing experience where the blurred video
|
|
124
|
+
* serves as the modal backdrop, with the main video centered on top.
|
|
125
|
+
* Based on the DGA modal pattern.
|
|
126
|
+
*
|
|
127
|
+
* Features:
|
|
128
|
+
* - Blurred video backdrop that syncs with main video
|
|
129
|
+
* - Configurable blur intensity and gradient overlays
|
|
130
|
+
* - Automatic play/pause when dialog opens/closes
|
|
131
|
+
* - HLS streaming support via Cloudflare Stream
|
|
132
|
+
* - Accessible dialog with focus trap and escape key handling
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```tsx
|
|
136
|
+
* <VideoDialog
|
|
137
|
+
* trigger={<Button>Watch Video</Button>}
|
|
138
|
+
* cloudflare={{ videoId: "abc123", customerCode: "xyz789" }}
|
|
139
|
+
* blur="high"
|
|
140
|
+
* overlay="vignette"
|
|
141
|
+
* />
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
const VideoDialog = React.forwardRef<HTMLDivElement, VideoDialogProps>(
|
|
145
|
+
(
|
|
146
|
+
{
|
|
147
|
+
trigger,
|
|
148
|
+
src,
|
|
149
|
+
cloudflare,
|
|
150
|
+
blur = "high",
|
|
151
|
+
overlay = "vignette",
|
|
152
|
+
backdropOpacity = 0.6,
|
|
153
|
+
showClose = true,
|
|
154
|
+
closePosition = "top-right",
|
|
155
|
+
rounded = "lg",
|
|
156
|
+
open: controlledOpen,
|
|
157
|
+
defaultOpen,
|
|
158
|
+
onOpenChange,
|
|
159
|
+
className,
|
|
160
|
+
autoPlay,
|
|
161
|
+
...videoProps
|
|
162
|
+
},
|
|
163
|
+
ref,
|
|
164
|
+
) => {
|
|
165
|
+
// Internal open state for autoplay control
|
|
166
|
+
const [internalOpen, setInternalOpen] = React.useState(
|
|
167
|
+
defaultOpen ?? false,
|
|
168
|
+
);
|
|
169
|
+
const isControlled = controlledOpen !== undefined;
|
|
170
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
171
|
+
|
|
172
|
+
const handleOpenChange = React.useCallback(
|
|
173
|
+
(newOpen: boolean) => {
|
|
174
|
+
if (!isControlled) {
|
|
175
|
+
setInternalOpen(newOpen);
|
|
176
|
+
}
|
|
177
|
+
onOpenChange?.(newOpen);
|
|
178
|
+
},
|
|
179
|
+
[isControlled, onOpenChange],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Primary video ref for sync
|
|
183
|
+
const primaryVideoRef = React.useRef<HTMLVideoElement | null>(null);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<BaseDialog.Root
|
|
187
|
+
open={open}
|
|
188
|
+
defaultOpen={defaultOpen}
|
|
189
|
+
onOpenChange={handleOpenChange}
|
|
190
|
+
>
|
|
191
|
+
<BaseDialog.Trigger
|
|
192
|
+
render={
|
|
193
|
+
React.isValidElement(trigger)
|
|
194
|
+
? (trigger as React.ReactElement<Record<string, unknown>>)
|
|
195
|
+
: undefined
|
|
196
|
+
}
|
|
197
|
+
>
|
|
198
|
+
{!React.isValidElement(trigger) ? trigger : undefined}
|
|
199
|
+
</BaseDialog.Trigger>
|
|
200
|
+
<BaseDialog.Portal>
|
|
201
|
+
<BaseDialog.Popup
|
|
202
|
+
ref={ref}
|
|
203
|
+
className={cn(videoDialogVariants(), className)}
|
|
204
|
+
data-component="video-dialog"
|
|
205
|
+
>
|
|
206
|
+
{/* Blur Video Backdrop - covers entire viewport */}
|
|
207
|
+
<BlurredVideoBackdrop
|
|
208
|
+
videoRef={primaryVideoRef}
|
|
209
|
+
blur={blur}
|
|
210
|
+
overlay={overlay}
|
|
211
|
+
opacity={backdropOpacity}
|
|
212
|
+
extension={120}
|
|
213
|
+
/>
|
|
214
|
+
|
|
215
|
+
{/* Close Button */}
|
|
216
|
+
{showClose && (
|
|
217
|
+
<BaseDialog.Close
|
|
218
|
+
className={closeButtonVariants({ position: closePosition })}
|
|
219
|
+
>
|
|
220
|
+
<svg
|
|
221
|
+
width="20"
|
|
222
|
+
height="20"
|
|
223
|
+
viewBox="0 0 20 20"
|
|
224
|
+
fill="none"
|
|
225
|
+
aria-hidden="true"
|
|
226
|
+
>
|
|
227
|
+
<path
|
|
228
|
+
d="M4 4L16 16M4 16L16 4"
|
|
229
|
+
stroke="currentColor"
|
|
230
|
+
strokeWidth="2"
|
|
231
|
+
strokeLinecap="round"
|
|
232
|
+
/>
|
|
233
|
+
</svg>
|
|
234
|
+
<span className="sr-only">Close</span>
|
|
235
|
+
</BaseDialog.Close>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{/* Video Container - fills viewport, video centered within */}
|
|
239
|
+
<div className={videoContainerVariants()}>
|
|
240
|
+
<VideoPlayer
|
|
241
|
+
src={src}
|
|
242
|
+
cloudflare={cloudflare}
|
|
243
|
+
videoRef={primaryVideoRef}
|
|
244
|
+
rounded={rounded}
|
|
245
|
+
autoPlay={autoPlay ?? open}
|
|
246
|
+
aspectRatio="16/9"
|
|
247
|
+
style={{
|
|
248
|
+
width: "min(100%, calc((100vh - 64px) * 16 / 9))",
|
|
249
|
+
maxHeight: "calc(100vh - 64px)",
|
|
250
|
+
}}
|
|
251
|
+
{...videoProps}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
</BaseDialog.Popup>
|
|
255
|
+
</BaseDialog.Portal>
|
|
256
|
+
</BaseDialog.Root>
|
|
257
|
+
);
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
VideoDialog.displayName = "VideoDialog";
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// Exports
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
export {
|
|
268
|
+
VideoDialog,
|
|
269
|
+
videoDialogVariants,
|
|
270
|
+
closeButtonVariants,
|
|
271
|
+
videoContainerVariants,
|
|
272
|
+
};
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
5
|
+
import {
|
|
6
|
+
BlurredVideoBackdrop,
|
|
7
|
+
type BlurredVideoBackdropProps,
|
|
8
|
+
} from "@/components/atoms/blurred-video-backdrop";
|
|
9
|
+
import {
|
|
10
|
+
VideoPlayer,
|
|
11
|
+
type VideoPlayerProps,
|
|
12
|
+
} from "@/components/atoms/video-player";
|
|
13
|
+
import { cn } from "@/lib/utils";
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Context
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
interface VideoWithBackdropContextValue {
|
|
20
|
+
/** Ref to primary video element */
|
|
21
|
+
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
22
|
+
/** Video source URL */
|
|
23
|
+
src?: string;
|
|
24
|
+
/** Cloudflare config */
|
|
25
|
+
cloudflare?: CloudflareConfig;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const VideoWithBackdropContext =
|
|
29
|
+
React.createContext<VideoWithBackdropContextValue | null>(null);
|
|
30
|
+
|
|
31
|
+
function useVideoWithBackdropContext() {
|
|
32
|
+
const context = React.useContext(VideoWithBackdropContext);
|
|
33
|
+
if (!context) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"VideoWithBackdrop compound components must be used within VideoWithBackdrop.Root",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return context;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Types
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/** Cloudflare Stream configuration */
|
|
46
|
+
interface CloudflareConfig {
|
|
47
|
+
/** Cloudflare Stream video ID */
|
|
48
|
+
videoId: string;
|
|
49
|
+
/** Cloudflare customer code/subdomain */
|
|
50
|
+
customerCode: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Variant Definitions
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Root container variants.
|
|
59
|
+
*/
|
|
60
|
+
const videoWithBackdropVariants = tv({
|
|
61
|
+
base: ["relative", "overflow-hidden", "bg-black"],
|
|
62
|
+
variants: {
|
|
63
|
+
/**
|
|
64
|
+
* Full-height mode for dialogs.
|
|
65
|
+
*/
|
|
66
|
+
fullHeight: {
|
|
67
|
+
true: "h-full w-full",
|
|
68
|
+
false: "",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
defaultVariants: {
|
|
72
|
+
fullHeight: false,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Content container variants.
|
|
78
|
+
*/
|
|
79
|
+
const contentVariants = tv({
|
|
80
|
+
base: ["relative", "z-10", "flex", "items-center", "justify-center"],
|
|
81
|
+
variants: {
|
|
82
|
+
fullHeight: {
|
|
83
|
+
true: "h-full w-full",
|
|
84
|
+
false: "",
|
|
85
|
+
},
|
|
86
|
+
padding: {
|
|
87
|
+
none: "",
|
|
88
|
+
sm: "p-16",
|
|
89
|
+
md: "p-24",
|
|
90
|
+
lg: "p-48",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
defaultVariants: {
|
|
94
|
+
fullHeight: false,
|
|
95
|
+
padding: "md",
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Root Component
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
export interface VideoWithBackdropRootProps
|
|
104
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
105
|
+
VariantProps<typeof videoWithBackdropVariants> {
|
|
106
|
+
/** Video source URL (HLS .m3u8 or regular video file) */
|
|
107
|
+
src?: string;
|
|
108
|
+
/** Cloudflare Stream configuration (takes precedence over src) */
|
|
109
|
+
cloudflare?: CloudflareConfig;
|
|
110
|
+
/** Children to render */
|
|
111
|
+
children: React.ReactNode;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* VideoWithBackdrop Root
|
|
116
|
+
*
|
|
117
|
+
* Container that provides video context to child components.
|
|
118
|
+
* Use with VideoWithBackdrop.Backdrop and VideoWithBackdrop.Content.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```tsx
|
|
122
|
+
* <VideoWithBackdrop.Root cloudflare={config}>
|
|
123
|
+
* <VideoWithBackdrop.Backdrop blur="high" overlay="vignette" />
|
|
124
|
+
* <VideoWithBackdrop.Content>
|
|
125
|
+
* <VideoWithBackdrop.Video />
|
|
126
|
+
* </VideoWithBackdrop.Content>
|
|
127
|
+
* </VideoWithBackdrop.Root>
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
const VideoWithBackdropRoot = React.forwardRef<
|
|
131
|
+
HTMLDivElement,
|
|
132
|
+
VideoWithBackdropRootProps
|
|
133
|
+
>(({ className, src, cloudflare, fullHeight, children, ...props }, ref) => {
|
|
134
|
+
const videoRef = React.useRef<HTMLVideoElement | null>(null);
|
|
135
|
+
|
|
136
|
+
const contextValue = React.useMemo(
|
|
137
|
+
() => ({ videoRef, src, cloudflare }),
|
|
138
|
+
[src, cloudflare],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<VideoWithBackdropContext.Provider value={contextValue}>
|
|
143
|
+
<div
|
|
144
|
+
ref={ref}
|
|
145
|
+
className={cn(videoWithBackdropVariants({ fullHeight }), className)}
|
|
146
|
+
data-full-height={fullHeight ?? false}
|
|
147
|
+
{...props}
|
|
148
|
+
>
|
|
149
|
+
{children}
|
|
150
|
+
</div>
|
|
151
|
+
</VideoWithBackdropContext.Provider>
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
VideoWithBackdropRoot.displayName = "VideoWithBackdropRoot";
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Backdrop Component
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
export interface VideoWithBackdropBackdropProps
|
|
161
|
+
extends Omit<BlurredVideoBackdropProps, "videoRef"> {}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* VideoWithBackdrop Backdrop
|
|
165
|
+
*
|
|
166
|
+
* Renders the blurred video backdrop layer using canvas.
|
|
167
|
+
* Automatically draws from the video element in context.
|
|
168
|
+
*/
|
|
169
|
+
const VideoWithBackdropBackdrop = React.forwardRef<
|
|
170
|
+
HTMLDivElement,
|
|
171
|
+
VideoWithBackdropBackdropProps
|
|
172
|
+
>(({ ...props }, ref) => {
|
|
173
|
+
const { videoRef } = useVideoWithBackdropContext();
|
|
174
|
+
|
|
175
|
+
return <BlurredVideoBackdrop ref={ref} videoRef={videoRef} {...props} />;
|
|
176
|
+
});
|
|
177
|
+
VideoWithBackdropBackdrop.displayName = "VideoWithBackdropBackdrop";
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// Content Component
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
export interface VideoWithBackdropContentProps
|
|
184
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
185
|
+
VariantProps<typeof contentVariants> {}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* VideoWithBackdrop Content
|
|
189
|
+
*
|
|
190
|
+
* Container for the main video player and any additional content.
|
|
191
|
+
* Positioned above the backdrop with z-index.
|
|
192
|
+
*/
|
|
193
|
+
const VideoWithBackdropContent = React.forwardRef<
|
|
194
|
+
HTMLDivElement,
|
|
195
|
+
VideoWithBackdropContentProps
|
|
196
|
+
>(({ className, fullHeight, padding, children, ...props }, ref) => {
|
|
197
|
+
return (
|
|
198
|
+
<div
|
|
199
|
+
ref={ref}
|
|
200
|
+
className={cn(contentVariants({ fullHeight, padding }), className)}
|
|
201
|
+
{...props}
|
|
202
|
+
>
|
|
203
|
+
{children}
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
VideoWithBackdropContent.displayName = "VideoWithBackdropContent";
|
|
208
|
+
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Video Component (convenience wrapper)
|
|
211
|
+
// ============================================================================
|
|
212
|
+
|
|
213
|
+
export interface VideoWithBackdropVideoProps
|
|
214
|
+
extends Omit<VideoPlayerProps, "videoRef"> {
|
|
215
|
+
/** Max width of the video player container */
|
|
216
|
+
maxWidth?: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* VideoWithBackdrop Video
|
|
221
|
+
*
|
|
222
|
+
* Convenience wrapper for VideoPlayer that automatically connects
|
|
223
|
+
* to the backdrop via context.
|
|
224
|
+
*/
|
|
225
|
+
const VideoWithBackdropVideo = React.forwardRef<
|
|
226
|
+
HTMLDivElement,
|
|
227
|
+
VideoWithBackdropVideoProps
|
|
228
|
+
>(({ className, maxWidth = "960px", cloudflare, src, ...props }, ref) => {
|
|
229
|
+
const context = useVideoWithBackdropContext();
|
|
230
|
+
|
|
231
|
+
// Use context values if not explicitly provided
|
|
232
|
+
const videoCloudflare = cloudflare ?? context.cloudflare;
|
|
233
|
+
const videoSrc = src ?? context.src;
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div ref={ref} className={cn("w-full", className)} style={{ maxWidth }}>
|
|
237
|
+
<VideoPlayer
|
|
238
|
+
cloudflare={videoCloudflare}
|
|
239
|
+
src={videoSrc}
|
|
240
|
+
videoRef={context.videoRef}
|
|
241
|
+
{...props}
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
VideoWithBackdropVideo.displayName = "VideoWithBackdropVideo";
|
|
247
|
+
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// Simple Pre-composed Component
|
|
250
|
+
// ============================================================================
|
|
251
|
+
|
|
252
|
+
export interface VideoWithBackdropProps
|
|
253
|
+
extends Omit<VideoPlayerProps, "videoRef" | "className" | "aspectRatio"> {
|
|
254
|
+
/** Blur intensity (default: high) */
|
|
255
|
+
blur?: BlurredVideoBackdropProps["blur"];
|
|
256
|
+
/** Gradient overlay (default: vignette) */
|
|
257
|
+
overlay?: BlurredVideoBackdropProps["overlay"];
|
|
258
|
+
/** Backdrop opacity (default: 0.6) */
|
|
259
|
+
backdropOpacity?: number;
|
|
260
|
+
/** Max width of video player (default: 960px) */
|
|
261
|
+
maxWidth?: string;
|
|
262
|
+
/** Content padding (default: md) */
|
|
263
|
+
padding?: "none" | "sm" | "md" | "lg";
|
|
264
|
+
/** Video player rounded corners */
|
|
265
|
+
rounded?: VideoPlayerProps["rounded"];
|
|
266
|
+
/** Additional className for root container */
|
|
267
|
+
className?: string;
|
|
268
|
+
/** Target FPS for backdrop canvas (default: 30) */
|
|
269
|
+
targetFps?: number;
|
|
270
|
+
/** Canvas scale factor for backdrop (default: 0.5) */
|
|
271
|
+
scale?: number;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* VideoWithBackdrop - Pre-composed video player with blurred backdrop.
|
|
276
|
+
*
|
|
277
|
+
* A simple, ready-to-use component that combines VideoPlayer with
|
|
278
|
+
* BlurredVideoBackdrop for modal video experiences. Uses canvas rendering
|
|
279
|
+
* for optimal performance.
|
|
280
|
+
*
|
|
281
|
+
* For custom layouts, use the compound components:
|
|
282
|
+
* - VideoWithBackdrop.Root
|
|
283
|
+
* - VideoWithBackdrop.Backdrop
|
|
284
|
+
* - VideoWithBackdrop.Content
|
|
285
|
+
* - VideoWithBackdrop.Video
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```tsx
|
|
289
|
+
* // Simple usage (in a full-height container like Dialog)
|
|
290
|
+
* <VideoWithBackdrop
|
|
291
|
+
* cloudflare={{ videoId: "...", customerCode: "..." }}
|
|
292
|
+
* autoPlay
|
|
293
|
+
* blur="high"
|
|
294
|
+
* overlay="vignette"
|
|
295
|
+
* />
|
|
296
|
+
*
|
|
297
|
+
* // With Dialog
|
|
298
|
+
* <Dialog size="full" variant="minimal">
|
|
299
|
+
* <VideoWithBackdrop cloudflare={config} autoPlay />
|
|
300
|
+
* </Dialog>
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
const VideoWithBackdrop = React.forwardRef<
|
|
304
|
+
HTMLDivElement,
|
|
305
|
+
VideoWithBackdropProps
|
|
306
|
+
>(
|
|
307
|
+
(
|
|
308
|
+
{
|
|
309
|
+
src,
|
|
310
|
+
cloudflare,
|
|
311
|
+
blur = "high",
|
|
312
|
+
overlay = "vignette",
|
|
313
|
+
backdropOpacity = 0.6,
|
|
314
|
+
maxWidth = "960px",
|
|
315
|
+
padding = "md",
|
|
316
|
+
rounded = "lg",
|
|
317
|
+
className,
|
|
318
|
+
targetFps = 30,
|
|
319
|
+
scale = 0.5,
|
|
320
|
+
...videoProps
|
|
321
|
+
},
|
|
322
|
+
ref,
|
|
323
|
+
) => {
|
|
324
|
+
const videoRef = React.useRef<HTMLVideoElement | null>(null);
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<div
|
|
328
|
+
ref={ref}
|
|
329
|
+
className={cn(
|
|
330
|
+
videoWithBackdropVariants({ fullHeight: true }),
|
|
331
|
+
className,
|
|
332
|
+
)}
|
|
333
|
+
data-component="video-with-backdrop"
|
|
334
|
+
>
|
|
335
|
+
<BlurredVideoBackdrop
|
|
336
|
+
videoRef={videoRef}
|
|
337
|
+
blur={blur}
|
|
338
|
+
overlay={overlay}
|
|
339
|
+
opacity={backdropOpacity}
|
|
340
|
+
targetFps={targetFps}
|
|
341
|
+
scale={scale}
|
|
342
|
+
/>
|
|
343
|
+
<div className={cn(contentVariants({ fullHeight: true, padding }))}>
|
|
344
|
+
<div className="w-full" style={{ maxWidth }}>
|
|
345
|
+
<VideoPlayer
|
|
346
|
+
src={src}
|
|
347
|
+
cloudflare={cloudflare}
|
|
348
|
+
videoRef={videoRef}
|
|
349
|
+
rounded={rounded}
|
|
350
|
+
{...videoProps}
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
VideoWithBackdrop.displayName = "VideoWithBackdrop";
|
|
359
|
+
|
|
360
|
+
// ============================================================================
|
|
361
|
+
// Compound Component Export
|
|
362
|
+
// ============================================================================
|
|
363
|
+
|
|
364
|
+
export const VideoWithBackdropParts = Object.assign(VideoWithBackdropRoot, {
|
|
365
|
+
Root: VideoWithBackdropRoot,
|
|
366
|
+
Backdrop: VideoWithBackdropBackdrop,
|
|
367
|
+
Content: VideoWithBackdropContent,
|
|
368
|
+
Video: VideoWithBackdropVideo,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ============================================================================
|
|
372
|
+
// Exports
|
|
373
|
+
// ============================================================================
|
|
374
|
+
|
|
375
|
+
export {
|
|
376
|
+
VideoWithBackdrop,
|
|
377
|
+
VideoWithBackdropRoot,
|
|
378
|
+
VideoWithBackdropBackdrop,
|
|
379
|
+
VideoWithBackdropContent,
|
|
380
|
+
VideoWithBackdropVideo,
|
|
381
|
+
videoWithBackdropVariants,
|
|
382
|
+
contentVariants,
|
|
383
|
+
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -1 +1,17 @@
|
|
|
1
|
+
// Utility hooks
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
BREAKPOINTS,
|
|
5
|
+
type Breakpoint,
|
|
6
|
+
useBreakpoint,
|
|
7
|
+
useMaxBreakpoint,
|
|
8
|
+
useMinBreakpoint,
|
|
9
|
+
} from "./use-breakpoint";
|
|
10
|
+
// Video player hooks
|
|
11
|
+
export { type CaptionCue, useCaptions } from "./use-captions";
|
|
1
12
|
export { useEventListener } from "./use-event-listener";
|
|
13
|
+
export {
|
|
14
|
+
type UseVideoKeyboardOptions,
|
|
15
|
+
type UseVideoKeyboardReturn,
|
|
16
|
+
useVideoKeyboard,
|
|
17
|
+
} from "./use-video-keyboard";
|