@nationaldesignstudio/react 0.5.5 → 0.6.0
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 +1858 -139
- package/dist/index.js +2388 -326
- package/dist/index.js.map +1 -1
- package/dist/tokens.css +680 -0
- package/package.json +22 -2
- 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/components/organisms/card/card.tsx +87 -12
- package/src/components/sections/hero/hero.tsx +35 -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 +2 -2
- package/src/theme/index.ts +4 -0
- package/src/theme/theme-provider.tsx +48 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nationaldesignstudio/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"*.css"
|
|
@@ -45,21 +45,38 @@
|
|
|
45
45
|
"test:visual": "PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:3100 vitest run --config vitest.visual.config.ts",
|
|
46
46
|
"test:visual:update": "PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:3100 vitest run --config vitest.visual.config.ts --update",
|
|
47
47
|
"docker:playwright": "docker compose up playwright",
|
|
48
|
-
"docker:playwright:down": "docker compose down"
|
|
48
|
+
"docker:playwright:down": "docker compose down",
|
|
49
|
+
"release": "release-it"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
52
|
+
"@cloudflare/stream-react": "^1.0.0",
|
|
53
|
+
"hls.js": "^1.5.0",
|
|
54
|
+
"lucide-react": "^0.400.0",
|
|
51
55
|
"react": "^18.0.0 || ^19.0.0",
|
|
52
56
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
53
57
|
"tailwindcss": "^4.0.0"
|
|
54
58
|
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@cloudflare/stream-react": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"hls.js": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"lucide-react": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
55
70
|
"dependencies": {
|
|
56
71
|
"@base-ui-components/react": "^1.0.0-rc.0",
|
|
57
72
|
"@radix-ui/react-slot": "^1.2.4",
|
|
58
73
|
"clsx": "^2.1.1",
|
|
74
|
+
"media-chrome": "^4.0.0",
|
|
59
75
|
"tailwind-variants": "^0.3.1"
|
|
60
76
|
},
|
|
61
77
|
"devDependencies": {
|
|
62
78
|
"@chromatic-com/storybook": "catalog:",
|
|
79
|
+
"@cloudflare/stream-react": "^1.9.1",
|
|
63
80
|
"@figma/code-connect": "^1.3.12",
|
|
64
81
|
"@nds-design-system/tailwind-token-generator": "workspace:*",
|
|
65
82
|
"@nds-design-system/tokens": "workspace:*",
|
|
@@ -81,9 +98,12 @@
|
|
|
81
98
|
"@vitest/browser-playwright": "catalog:",
|
|
82
99
|
"@vitest/coverage-v8": "catalog:",
|
|
83
100
|
"globals": "catalog:",
|
|
101
|
+
"hls.js": "^1.5.17",
|
|
102
|
+
"lucide-react": "^0.511.0",
|
|
84
103
|
"playwright": "catalog:",
|
|
85
104
|
"react": "catalog:",
|
|
86
105
|
"react-dom": "catalog:",
|
|
106
|
+
"release-it": "^19.2.4",
|
|
87
107
|
"storybook": "catalog:",
|
|
88
108
|
"tailwindcss": "catalog:",
|
|
89
109
|
"tsup": "^8.5.1",
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
type BlurIntensity = "low" | "medium" | "high" | "extreme";
|
|
12
|
+
type OverlayType = "none" | "vignette" | "top-bottom";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Variant Definitions
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Blurred video backdrop wrapper variants.
|
|
20
|
+
*
|
|
21
|
+
* The wrapper extends beyond its bounds (inset: -120px) to cover
|
|
22
|
+
* blur artifacts at the edges.
|
|
23
|
+
*/
|
|
24
|
+
const blurredVideoBackdropVariants = tv({
|
|
25
|
+
base: [
|
|
26
|
+
"absolute",
|
|
27
|
+
"pointer-events-none",
|
|
28
|
+
"select-none",
|
|
29
|
+
"will-change-contents",
|
|
30
|
+
"transform-gpu",
|
|
31
|
+
],
|
|
32
|
+
variants: {
|
|
33
|
+
/**
|
|
34
|
+
* Blur intensity level.
|
|
35
|
+
* Higher values provide more diffused backgrounds.
|
|
36
|
+
*/
|
|
37
|
+
blur: {
|
|
38
|
+
low: "",
|
|
39
|
+
medium: "",
|
|
40
|
+
high: "",
|
|
41
|
+
extreme: "",
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* Gradient overlay for visual depth.
|
|
45
|
+
*/
|
|
46
|
+
overlay: {
|
|
47
|
+
none: "",
|
|
48
|
+
vignette: "",
|
|
49
|
+
"top-bottom": "",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
defaultVariants: {
|
|
53
|
+
blur: "high",
|
|
54
|
+
overlay: "none",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Canvas element styles.
|
|
60
|
+
*/
|
|
61
|
+
const canvasVariants = tv({
|
|
62
|
+
base: ["w-full", "h-full", "object-cover"],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Gradient overlay base styles.
|
|
67
|
+
* Gradient backgrounds are applied via inline styles to avoid arbitrary values.
|
|
68
|
+
*/
|
|
69
|
+
const gradientOverlayVariants = tv({
|
|
70
|
+
base: ["absolute", "inset-0", "pointer-events-none"],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gradient overlay background styles.
|
|
75
|
+
* Using inline styles to maintain token compliance (no arbitrary values in Tailwind).
|
|
76
|
+
*/
|
|
77
|
+
const OVERLAY_GRADIENTS: Record<Exclude<OverlayType, "none">, string> = {
|
|
78
|
+
vignette:
|
|
79
|
+
"radial-gradient(ellipse at center, transparent 40%, rgba(0, 0, 0, 0.4) 100%)",
|
|
80
|
+
"top-bottom":
|
|
81
|
+
"linear-gradient(180deg, rgba(0, 0, 0, 0.4) 0%, transparent 30%, transparent 70%, rgba(0, 0, 0, 0.4) 100%)",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Blur amount mapping
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
const BLUR_AMOUNTS: Record<BlurIntensity, number> = {
|
|
89
|
+
low: 40,
|
|
90
|
+
medium: 80,
|
|
91
|
+
high: 100,
|
|
92
|
+
extreme: 120,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// useCanvasBlur Hook
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
interface UseCanvasBlurOptions {
|
|
100
|
+
/** Ref to the source video element */
|
|
101
|
+
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
102
|
+
/** Blur amount in pixels */
|
|
103
|
+
blurAmount: number;
|
|
104
|
+
/** Whether rendering is enabled */
|
|
105
|
+
enabled?: boolean;
|
|
106
|
+
/** Target FPS (lower = better performance, default: 30) */
|
|
107
|
+
targetFps?: number;
|
|
108
|
+
/** Canvas scale factor (lower = better performance, default: 0.5) */
|
|
109
|
+
scale?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface UseCanvasBlurReturn {
|
|
113
|
+
/** Ref to attach to the canvas element */
|
|
114
|
+
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
|
115
|
+
/** Whether the canvas is currently rendering */
|
|
116
|
+
isRendering: boolean;
|
|
117
|
+
/** Performance metrics */
|
|
118
|
+
metrics: {
|
|
119
|
+
fps: number;
|
|
120
|
+
frameTime: number;
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Hook for rendering a blurred video to canvas.
|
|
126
|
+
*
|
|
127
|
+
* Performance optimizations:
|
|
128
|
+
* - Renders at reduced resolution (scale factor)
|
|
129
|
+
* - Throttled to target FPS
|
|
130
|
+
* - Uses CSS scale to fill container
|
|
131
|
+
* - Single video decoder (no sync needed)
|
|
132
|
+
*/
|
|
133
|
+
function useCanvasBlur({
|
|
134
|
+
videoRef,
|
|
135
|
+
blurAmount,
|
|
136
|
+
enabled = true,
|
|
137
|
+
targetFps = 30,
|
|
138
|
+
scale = 0.5,
|
|
139
|
+
}: UseCanvasBlurOptions): UseCanvasBlurReturn {
|
|
140
|
+
const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
|
|
141
|
+
const ctxRef = React.useRef<CanvasRenderingContext2D | null>(null);
|
|
142
|
+
const [isRendering, setIsRendering] = React.useState(false);
|
|
143
|
+
const [metrics, setMetrics] = React.useState({ fps: 0, frameTime: 0 });
|
|
144
|
+
|
|
145
|
+
// Track when video ref is ready (it populates after mount)
|
|
146
|
+
const [videoReady, setVideoReady] = React.useState(false);
|
|
147
|
+
|
|
148
|
+
// Performance tracking refs
|
|
149
|
+
const lastFrameTimeRef = React.useRef(0);
|
|
150
|
+
const frameCountRef = React.useRef(0);
|
|
151
|
+
const fpsIntervalRef = React.useRef(0);
|
|
152
|
+
|
|
153
|
+
// Frame interval based on target FPS
|
|
154
|
+
const frameInterval = 1000 / targetFps;
|
|
155
|
+
|
|
156
|
+
// Poll for video ref to be ready (refs populate after initial render)
|
|
157
|
+
React.useEffect(() => {
|
|
158
|
+
if (!enabled) return;
|
|
159
|
+
|
|
160
|
+
const checkRef = () => {
|
|
161
|
+
if (videoRef.current && canvasRef.current) {
|
|
162
|
+
setVideoReady(true);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Check immediately
|
|
167
|
+
checkRef();
|
|
168
|
+
|
|
169
|
+
// Also check on next frames in case elements mount after this effect
|
|
170
|
+
const frameId = requestAnimationFrame(checkRef);
|
|
171
|
+
const timeoutId = setTimeout(checkRef, 100);
|
|
172
|
+
const timeoutId2 = setTimeout(checkRef, 500);
|
|
173
|
+
|
|
174
|
+
return () => {
|
|
175
|
+
cancelAnimationFrame(frameId);
|
|
176
|
+
clearTimeout(timeoutId);
|
|
177
|
+
clearTimeout(timeoutId2);
|
|
178
|
+
};
|
|
179
|
+
}, [enabled, videoRef]);
|
|
180
|
+
|
|
181
|
+
// Main rendering effect
|
|
182
|
+
React.useEffect(() => {
|
|
183
|
+
if (!enabled || !videoReady) return;
|
|
184
|
+
|
|
185
|
+
const video = videoRef.current;
|
|
186
|
+
const canvas = canvasRef.current;
|
|
187
|
+
|
|
188
|
+
if (!video || !canvas) return;
|
|
189
|
+
|
|
190
|
+
// Initialize canvas context
|
|
191
|
+
const ctx = canvas.getContext("2d", {
|
|
192
|
+
alpha: false,
|
|
193
|
+
desynchronized: true, // Reduces latency
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!ctx) return;
|
|
197
|
+
ctxRef.current = ctx;
|
|
198
|
+
|
|
199
|
+
let animationFrameId: number;
|
|
200
|
+
let isActive = true;
|
|
201
|
+
|
|
202
|
+
// Set canvas size based on video dimensions with scale factor
|
|
203
|
+
const updateCanvasSize = () => {
|
|
204
|
+
if (video.videoWidth && video.videoHeight) {
|
|
205
|
+
canvas.width = Math.floor(video.videoWidth * scale);
|
|
206
|
+
canvas.height = Math.floor(video.videoHeight * scale);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const render = (timestamp: number) => {
|
|
211
|
+
if (!isActive || !video || !ctx) return;
|
|
212
|
+
|
|
213
|
+
// Throttle to target FPS
|
|
214
|
+
const elapsed = timestamp - lastFrameTimeRef.current;
|
|
215
|
+
|
|
216
|
+
if (elapsed >= frameInterval) {
|
|
217
|
+
const frameStart = performance.now();
|
|
218
|
+
|
|
219
|
+
// Update canvas size if needed
|
|
220
|
+
if (canvas.width === 0 || canvas.height === 0) {
|
|
221
|
+
updateCanvasSize();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Only draw if video has data and is playing
|
|
225
|
+
if (video.readyState >= 2 && !video.paused) {
|
|
226
|
+
// Apply blur filter and draw
|
|
227
|
+
ctx.filter = `blur(${blurAmount * scale}px)`;
|
|
228
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
229
|
+
|
|
230
|
+
setIsRendering(true);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Track frame time
|
|
234
|
+
const frameTime = performance.now() - frameStart;
|
|
235
|
+
frameCountRef.current++;
|
|
236
|
+
|
|
237
|
+
// Update FPS every second
|
|
238
|
+
if (timestamp - fpsIntervalRef.current >= 1000) {
|
|
239
|
+
setMetrics({
|
|
240
|
+
fps: frameCountRef.current,
|
|
241
|
+
frameTime: Math.round(frameTime * 100) / 100,
|
|
242
|
+
});
|
|
243
|
+
frameCountRef.current = 0;
|
|
244
|
+
fpsIntervalRef.current = timestamp;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
lastFrameTimeRef.current = timestamp - (elapsed % frameInterval);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
animationFrameId = requestAnimationFrame(render);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Handle video events
|
|
254
|
+
const handleLoadedMetadata = () => {
|
|
255
|
+
updateCanvasSize();
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const handlePlay = () => {
|
|
259
|
+
if (isActive) {
|
|
260
|
+
animationFrameId = requestAnimationFrame(render);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const handlePause = () => {
|
|
265
|
+
setIsRendering(false);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Add event listeners
|
|
269
|
+
video.addEventListener("loadedmetadata", handleLoadedMetadata);
|
|
270
|
+
video.addEventListener("play", handlePlay);
|
|
271
|
+
video.addEventListener("pause", handlePause);
|
|
272
|
+
|
|
273
|
+
// Initialize size if video is already loaded
|
|
274
|
+
if (video.readyState >= 1) {
|
|
275
|
+
updateCanvasSize();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Start render loop if video is playing
|
|
279
|
+
if (!video.paused) {
|
|
280
|
+
animationFrameId = requestAnimationFrame(render);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return () => {
|
|
284
|
+
isActive = false;
|
|
285
|
+
cancelAnimationFrame(animationFrameId);
|
|
286
|
+
video.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
|
287
|
+
video.removeEventListener("play", handlePlay);
|
|
288
|
+
video.removeEventListener("pause", handlePause);
|
|
289
|
+
setIsRendering(false);
|
|
290
|
+
};
|
|
291
|
+
}, [enabled, videoReady, videoRef, blurAmount, frameInterval, scale]);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
canvasRef,
|
|
295
|
+
isRendering,
|
|
296
|
+
metrics,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// BlurredVideoBackdrop Component
|
|
302
|
+
// ============================================================================
|
|
303
|
+
|
|
304
|
+
export interface BlurredVideoBackdropProps
|
|
305
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "children">,
|
|
306
|
+
VariantProps<typeof blurredVideoBackdropVariants> {
|
|
307
|
+
/** Ref to the primary video element to create backdrop from (required) */
|
|
308
|
+
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
309
|
+
/** Opacity of the backdrop (0-1, default: 0.6) */
|
|
310
|
+
opacity?: number;
|
|
311
|
+
/** Extension amount in pixels to cover blur artifacts (default: 120) */
|
|
312
|
+
extension?: number;
|
|
313
|
+
/** Target FPS for canvas rendering (default: 30) */
|
|
314
|
+
targetFps?: number;
|
|
315
|
+
/** Canvas scale factor - lower = better performance (default: 0.5) */
|
|
316
|
+
scale?: number;
|
|
317
|
+
/** Whether to show debug metrics */
|
|
318
|
+
showMetrics?: boolean;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* BlurredVideoBackdrop - A high-performance blurred video backdrop using canvas.
|
|
323
|
+
*
|
|
324
|
+
* Renders a blurred copy of a video element to create an ambient backdrop effect.
|
|
325
|
+
* Uses canvas rendering for optimal performance - no video sync needed.
|
|
326
|
+
*
|
|
327
|
+
* Performance features:
|
|
328
|
+
* - Single video decoder (draws from existing video element)
|
|
329
|
+
* - Reduced resolution rendering (configurable scale)
|
|
330
|
+
* - Throttled frame rate (configurable FPS)
|
|
331
|
+
* - GPU-accelerated canvas scaling
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```tsx
|
|
335
|
+
* const videoRef = useRef<HTMLVideoElement>(null);
|
|
336
|
+
*
|
|
337
|
+
* <div className="relative">
|
|
338
|
+
* <BlurredVideoBackdrop
|
|
339
|
+
* videoRef={videoRef}
|
|
340
|
+
* blur="high"
|
|
341
|
+
* overlay="vignette"
|
|
342
|
+
* />
|
|
343
|
+
* <VideoPlayer videoRef={videoRef} src="/video.mp4" />
|
|
344
|
+
* </div>
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
const BlurredVideoBackdrop = React.forwardRef<
|
|
348
|
+
HTMLDivElement,
|
|
349
|
+
BlurredVideoBackdropProps
|
|
350
|
+
>(
|
|
351
|
+
(
|
|
352
|
+
{
|
|
353
|
+
className,
|
|
354
|
+
videoRef,
|
|
355
|
+
blur = "high",
|
|
356
|
+
overlay = "none",
|
|
357
|
+
opacity = 0.6,
|
|
358
|
+
extension = 120,
|
|
359
|
+
targetFps = 30,
|
|
360
|
+
scale = 0.5,
|
|
361
|
+
showMetrics = false,
|
|
362
|
+
style,
|
|
363
|
+
...props
|
|
364
|
+
},
|
|
365
|
+
ref,
|
|
366
|
+
) => {
|
|
367
|
+
const blurAmount = BLUR_AMOUNTS[blur ?? "high"];
|
|
368
|
+
|
|
369
|
+
const { canvasRef, isRendering, metrics } = useCanvasBlur({
|
|
370
|
+
videoRef,
|
|
371
|
+
blurAmount,
|
|
372
|
+
enabled: true,
|
|
373
|
+
targetFps,
|
|
374
|
+
scale,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<div
|
|
379
|
+
ref={ref}
|
|
380
|
+
className={cn(
|
|
381
|
+
blurredVideoBackdropVariants({ blur, overlay }),
|
|
382
|
+
className,
|
|
383
|
+
)}
|
|
384
|
+
style={{
|
|
385
|
+
inset: `-${extension}px`,
|
|
386
|
+
opacity,
|
|
387
|
+
contain: "layout style paint",
|
|
388
|
+
...style,
|
|
389
|
+
}}
|
|
390
|
+
data-blur={blur ?? "high"}
|
|
391
|
+
data-overlay={overlay ?? "none"}
|
|
392
|
+
data-rendering={isRendering}
|
|
393
|
+
aria-hidden="true"
|
|
394
|
+
{...props}
|
|
395
|
+
>
|
|
396
|
+
<canvas
|
|
397
|
+
ref={canvasRef}
|
|
398
|
+
className={cn(
|
|
399
|
+
canvasVariants(),
|
|
400
|
+
// Scale up the low-res canvas to fill container
|
|
401
|
+
"scale-[2]", // Inverse of 0.5 scale factor
|
|
402
|
+
"origin-center",
|
|
403
|
+
)}
|
|
404
|
+
style={{
|
|
405
|
+
// Additional CSS blur for smoother appearance
|
|
406
|
+
filter: `blur(${blurAmount * 0.3}px)`,
|
|
407
|
+
}}
|
|
408
|
+
/>
|
|
409
|
+
{overlay && overlay !== "none" && (
|
|
410
|
+
<div
|
|
411
|
+
className={gradientOverlayVariants()}
|
|
412
|
+
style={{ background: OVERLAY_GRADIENTS[overlay] }}
|
|
413
|
+
/>
|
|
414
|
+
)}
|
|
415
|
+
{showMetrics && (
|
|
416
|
+
<div className="absolute bottom-8 left-8 z-10 bg-black/80 text-white typography-caption px-8 py-4 rounded-4 font-mono">
|
|
417
|
+
<div>FPS: {metrics.fps}</div>
|
|
418
|
+
<div>Frame: {metrics.frameTime}ms</div>
|
|
419
|
+
<div>Scale: {scale}x</div>
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
},
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
BlurredVideoBackdrop.displayName = "BlurredVideoBackdrop";
|
|
428
|
+
|
|
429
|
+
// ============================================================================
|
|
430
|
+
// Exports
|
|
431
|
+
// ============================================================================
|
|
432
|
+
|
|
433
|
+
export {
|
|
434
|
+
BlurredVideoBackdrop,
|
|
435
|
+
blurredVideoBackdropVariants,
|
|
436
|
+
canvasVariants,
|
|
437
|
+
gradientOverlayVariants,
|
|
438
|
+
useCanvasBlur,
|
|
439
|
+
BLUR_AMOUNTS,
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
export type {
|
|
443
|
+
UseCanvasBlurOptions,
|
|
444
|
+
UseCanvasBlurReturn,
|
|
445
|
+
BlurIntensity,
|
|
446
|
+
OverlayType,
|
|
447
|
+
};
|
|
@@ -38,7 +38,7 @@ import { tv, type VariantProps } from "tailwind-variants";
|
|
|
38
38
|
* - full: Fully circular
|
|
39
39
|
*/
|
|
40
40
|
const iconButtonVariants = tv({
|
|
41
|
-
base: "inline-flex items-center justify-center whitespace-nowrap transition-colors duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
41
|
+
base: "inline-flex items-center justify-center whitespace-nowrap transition-colors duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&>svg]:shrink-0",
|
|
42
42
|
variants: {
|
|
43
43
|
variant: {
|
|
44
44
|
// Primary - filled brand button
|
|
@@ -61,15 +61,21 @@ const iconButtonVariants = tv({
|
|
|
61
61
|
"bg-button-ghost-inverse-bg text-button-ghost-inverse-text hover:bg-button-ghost-inverse-bg-hover hover:text-button-ghost-inverse-text-hover border-transparent focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
|
|
62
62
|
},
|
|
63
63
|
size: {
|
|
64
|
-
sm: "size-28
|
|
65
|
-
md: "size-40
|
|
66
|
-
lg: "size-56
|
|
64
|
+
sm: "size-28",
|
|
65
|
+
md: "size-40",
|
|
66
|
+
lg: "size-56",
|
|
67
67
|
},
|
|
68
68
|
rounded: {
|
|
69
69
|
default: "",
|
|
70
70
|
full: "rounded-full",
|
|
71
71
|
},
|
|
72
72
|
},
|
|
73
|
+
compoundVariants: [
|
|
74
|
+
// Apply size-specific rounded only when rounded is "default"
|
|
75
|
+
{ size: "sm", rounded: "default", class: "rounded-surface-button-small" },
|
|
76
|
+
{ size: "md", rounded: "default", class: "rounded-surface-button-medium" },
|
|
77
|
+
{ size: "lg", rounded: "default", class: "rounded-surface-button-large" },
|
|
78
|
+
],
|
|
73
79
|
defaultVariants: {
|
|
74
80
|
variant: "primary",
|
|
75
81
|
size: "md",
|