@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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Types
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export interface UseVideoKeyboardOptions {
|
|
8
|
+
/** Ref to the video element */
|
|
9
|
+
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
10
|
+
/** Whether keyboard handling is enabled (default: true) */
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
/** Seek amount in seconds for arrow keys (default: 5) */
|
|
13
|
+
seekAmount?: number;
|
|
14
|
+
/** Volume change amount for arrow keys (default: 0.1) */
|
|
15
|
+
volumeStep?: number;
|
|
16
|
+
/** Callback when play/pause is toggled */
|
|
17
|
+
onTogglePlay?: () => void;
|
|
18
|
+
/** Callback when fullscreen is toggled */
|
|
19
|
+
onToggleFullscreen?: () => void;
|
|
20
|
+
/** Callback when captions are toggled */
|
|
21
|
+
onToggleCaptions?: () => void;
|
|
22
|
+
/** Callback when controls should be shown */
|
|
23
|
+
onShowControls?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UseVideoKeyboardReturn {
|
|
27
|
+
/** Key down handler to attach to container element */
|
|
28
|
+
handleKeyDown: (e: React.KeyboardEvent) => void;
|
|
29
|
+
/** Props to spread on the container element */
|
|
30
|
+
containerProps: {
|
|
31
|
+
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
32
|
+
tabIndex: number;
|
|
33
|
+
role: string;
|
|
34
|
+
"aria-label": string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Hook
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Hook for handling keyboard shortcuts in a video player.
|
|
44
|
+
*
|
|
45
|
+
* Supported shortcuts:
|
|
46
|
+
* - Space: Play/pause
|
|
47
|
+
* - Left Arrow: Seek backward
|
|
48
|
+
* - Right Arrow: Seek forward
|
|
49
|
+
* - Up Arrow: Volume up
|
|
50
|
+
* - Down Arrow: Volume down
|
|
51
|
+
* - M: Toggle mute
|
|
52
|
+
* - F: Toggle fullscreen
|
|
53
|
+
* - C: Toggle captions
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* const { containerProps } = useVideoKeyboard({
|
|
58
|
+
* videoRef,
|
|
59
|
+
* onTogglePlay: () => video.paused ? video.play() : video.pause(),
|
|
60
|
+
* onToggleFullscreen: () => toggleFullscreen(),
|
|
61
|
+
* onShowControls: () => setControlsVisible(true),
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* return <div {...containerProps}>...</div>;
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function useVideoKeyboard({
|
|
68
|
+
videoRef,
|
|
69
|
+
enabled = true,
|
|
70
|
+
seekAmount = 5,
|
|
71
|
+
volumeStep = 0.1,
|
|
72
|
+
onTogglePlay,
|
|
73
|
+
onToggleFullscreen,
|
|
74
|
+
onToggleCaptions,
|
|
75
|
+
onShowControls,
|
|
76
|
+
}: UseVideoKeyboardOptions): UseVideoKeyboardReturn {
|
|
77
|
+
const handleKeyDown = React.useCallback(
|
|
78
|
+
(e: React.KeyboardEvent) => {
|
|
79
|
+
if (!enabled) return;
|
|
80
|
+
|
|
81
|
+
const video = videoRef.current;
|
|
82
|
+
if (!video) return;
|
|
83
|
+
|
|
84
|
+
// Don't handle if focus is on interactive elements
|
|
85
|
+
const target = e.target as HTMLElement;
|
|
86
|
+
if (
|
|
87
|
+
target.tagName === "BUTTON" ||
|
|
88
|
+
target.tagName === "INPUT" ||
|
|
89
|
+
target.tagName === "TEXTAREA" ||
|
|
90
|
+
target.closest("button") ||
|
|
91
|
+
target.closest("input") ||
|
|
92
|
+
target.closest("[role='slider']")
|
|
93
|
+
) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let handled = false;
|
|
98
|
+
|
|
99
|
+
switch (e.key) {
|
|
100
|
+
// Play/Pause
|
|
101
|
+
case " ":
|
|
102
|
+
case "Spacebar":
|
|
103
|
+
case "k": // YouTube-style shortcut
|
|
104
|
+
if (onTogglePlay) {
|
|
105
|
+
onTogglePlay();
|
|
106
|
+
} else {
|
|
107
|
+
if (video.paused) {
|
|
108
|
+
video.play().catch(() => {});
|
|
109
|
+
} else {
|
|
110
|
+
video.pause();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
handled = true;
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
// Seek backward
|
|
117
|
+
case "ArrowLeft":
|
|
118
|
+
case "j": // YouTube-style shortcut
|
|
119
|
+
video.currentTime = Math.max(0, video.currentTime - seekAmount);
|
|
120
|
+
handled = true;
|
|
121
|
+
break;
|
|
122
|
+
|
|
123
|
+
// Seek forward
|
|
124
|
+
case "ArrowRight":
|
|
125
|
+
case "l": // YouTube-style shortcut
|
|
126
|
+
video.currentTime = Math.min(
|
|
127
|
+
video.duration || 0,
|
|
128
|
+
video.currentTime + seekAmount,
|
|
129
|
+
);
|
|
130
|
+
handled = true;
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
// Volume up
|
|
134
|
+
case "ArrowUp":
|
|
135
|
+
video.volume = Math.min(1, video.volume + volumeStep);
|
|
136
|
+
video.muted = false;
|
|
137
|
+
handled = true;
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
// Volume down
|
|
141
|
+
case "ArrowDown":
|
|
142
|
+
video.volume = Math.max(0, video.volume - volumeStep);
|
|
143
|
+
handled = true;
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
// Toggle mute
|
|
147
|
+
case "m":
|
|
148
|
+
case "M":
|
|
149
|
+
video.muted = !video.muted;
|
|
150
|
+
handled = true;
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
// Toggle fullscreen
|
|
154
|
+
case "f":
|
|
155
|
+
case "F":
|
|
156
|
+
onToggleFullscreen?.();
|
|
157
|
+
handled = true;
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
// Toggle captions
|
|
161
|
+
case "c":
|
|
162
|
+
case "C":
|
|
163
|
+
onToggleCaptions?.();
|
|
164
|
+
handled = true;
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
// Jump to start
|
|
168
|
+
case "Home":
|
|
169
|
+
video.currentTime = 0;
|
|
170
|
+
handled = true;
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
// Jump to end
|
|
174
|
+
case "End":
|
|
175
|
+
video.currentTime = video.duration || 0;
|
|
176
|
+
handled = true;
|
|
177
|
+
break;
|
|
178
|
+
|
|
179
|
+
// Number keys for percentage seeking (0-9)
|
|
180
|
+
case "0":
|
|
181
|
+
case "1":
|
|
182
|
+
case "2":
|
|
183
|
+
case "3":
|
|
184
|
+
case "4":
|
|
185
|
+
case "5":
|
|
186
|
+
case "6":
|
|
187
|
+
case "7":
|
|
188
|
+
case "8":
|
|
189
|
+
case "9":
|
|
190
|
+
if (video.duration) {
|
|
191
|
+
const percent = parseInt(e.key, 10) / 10;
|
|
192
|
+
video.currentTime = video.duration * percent;
|
|
193
|
+
handled = true;
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (handled) {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
e.stopPropagation();
|
|
201
|
+
onShowControls?.();
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
[
|
|
205
|
+
enabled,
|
|
206
|
+
videoRef,
|
|
207
|
+
seekAmount,
|
|
208
|
+
volumeStep,
|
|
209
|
+
onTogglePlay,
|
|
210
|
+
onToggleFullscreen,
|
|
211
|
+
onToggleCaptions,
|
|
212
|
+
onShowControls,
|
|
213
|
+
],
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const containerProps = React.useMemo(
|
|
217
|
+
() => ({
|
|
218
|
+
onKeyDown: handleKeyDown,
|
|
219
|
+
tabIndex: 0,
|
|
220
|
+
role: "application" as const,
|
|
221
|
+
"aria-label": "Video player, press space to play or pause",
|
|
222
|
+
}),
|
|
223
|
+
[handleKeyDown],
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
handleKeyDown,
|
|
228
|
+
containerProps,
|
|
229
|
+
};
|
|
230
|
+
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ClassValue, clsx } from "clsx";
|
|
2
2
|
import { cnBase as twMerge } from "tailwind-variants";
|
|
3
3
|
|
|
4
4
|
export { twMerge };
|
|
5
5
|
|
|
6
6
|
export function cn(...inputs: ClassValue[]) {
|
|
7
|
-
|
|
7
|
+
return twMerge(clsx(inputs));
|
|
8
8
|
}
|
package/src/theme/index.ts
CHANGED
|
@@ -11,13 +11,17 @@ export type {
|
|
|
11
11
|
ColorThemeName,
|
|
12
12
|
CSSVariableMap,
|
|
13
13
|
NestedStringRecord,
|
|
14
|
+
ResolvedProjectTheme,
|
|
14
15
|
SurfaceThemeName,
|
|
15
16
|
ThemeComposition,
|
|
16
17
|
} from "@nds-design-system/tokens";
|
|
18
|
+
// Re-export project theme utilities for convenience
|
|
17
19
|
// Re-export theme registries for programmatic access
|
|
18
20
|
export {
|
|
19
21
|
colorThemeNames,
|
|
20
22
|
colorThemes,
|
|
23
|
+
defineTheme,
|
|
24
|
+
generateThemeCSS,
|
|
21
25
|
surfaceThemeNames,
|
|
22
26
|
surfaceThemes,
|
|
23
27
|
} from "@nds-design-system/tokens";
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
ColorThemeName,
|
|
10
10
|
CSSVariableMap,
|
|
11
11
|
NestedStringRecord,
|
|
12
|
+
ResolvedProjectTheme,
|
|
12
13
|
SurfaceThemeName,
|
|
13
14
|
TokenModule,
|
|
14
15
|
} from "@nds-design-system/tokens";
|
|
@@ -46,6 +47,11 @@ export interface ThemeProviderProps {
|
|
|
46
47
|
color?: ColorThemeName;
|
|
47
48
|
/** Surface theme name (defaults to "base") */
|
|
48
49
|
surface?: SurfaceThemeName;
|
|
50
|
+
/**
|
|
51
|
+
* Custom project theme created with defineTheme()
|
|
52
|
+
* When provided, overrides color/surface props with custom theme tokens
|
|
53
|
+
*/
|
|
54
|
+
customTheme?: ResolvedProjectTheme;
|
|
49
55
|
/** Children to render */
|
|
50
56
|
children: ReactNode;
|
|
51
57
|
/** Optional className for the wrapper div */
|
|
@@ -239,8 +245,23 @@ function deepMerge(
|
|
|
239
245
|
* <App />
|
|
240
246
|
* </ThemeProvider>
|
|
241
247
|
*
|
|
242
|
-
* //
|
|
243
|
-
*
|
|
248
|
+
* // Use a custom project theme
|
|
249
|
+
* import { defineTheme, srgb } from "@nds-design-system/tokens";
|
|
250
|
+
*
|
|
251
|
+
* const myTheme = defineTheme({
|
|
252
|
+
* name: "my-project",
|
|
253
|
+
* extends: "base",
|
|
254
|
+
* tokens: {
|
|
255
|
+
* semantic: {
|
|
256
|
+
* color: {
|
|
257
|
+
* bg: { page: srgb("#FEFDF9") },
|
|
258
|
+
* accent: { brand: srgb("#A68B5E") },
|
|
259
|
+
* },
|
|
260
|
+
* },
|
|
261
|
+
* },
|
|
262
|
+
* });
|
|
263
|
+
*
|
|
264
|
+
* <ThemeProvider customTheme={myTheme}>
|
|
244
265
|
* <App />
|
|
245
266
|
* </ThemeProvider>
|
|
246
267
|
* ```
|
|
@@ -248,6 +269,7 @@ function deepMerge(
|
|
|
248
269
|
export function ThemeProvider({
|
|
249
270
|
color = "base",
|
|
250
271
|
surface = "base",
|
|
272
|
+
customTheme,
|
|
251
273
|
children,
|
|
252
274
|
className,
|
|
253
275
|
applyStyles = true,
|
|
@@ -255,12 +277,17 @@ export function ThemeProvider({
|
|
|
255
277
|
const { tokens, cssVars } = useMemo(() => {
|
|
256
278
|
const flatTokens: Record<string, string> = {};
|
|
257
279
|
|
|
258
|
-
// Get color theme
|
|
280
|
+
// Get base color theme
|
|
259
281
|
const baseColorModule = colorThemes.base;
|
|
260
|
-
|
|
282
|
+
|
|
283
|
+
// If customTheme is provided, use its base theme; otherwise use the color prop
|
|
284
|
+
const effectiveColor = customTheme ? customTheme.extends : color;
|
|
285
|
+
const colorModule =
|
|
286
|
+
colorThemes[effectiveColor as keyof typeof colorThemes] ??
|
|
287
|
+
colorThemes.base;
|
|
261
288
|
|
|
262
289
|
const mergedColor =
|
|
263
|
-
|
|
290
|
+
effectiveColor === "base"
|
|
264
291
|
? (baseColorModule as Record<string, unknown>)
|
|
265
292
|
: mergeTokenModules(baseColorModule, colorModule);
|
|
266
293
|
|
|
@@ -287,15 +314,28 @@ export function ThemeProvider({
|
|
|
287
314
|
|
|
288
315
|
// Use shared utilities from tokens package
|
|
289
316
|
const nestedTokens = flatToNested(flatTokens);
|
|
290
|
-
|
|
317
|
+
let cssVariables = flatToCSSVars(flatTokens);
|
|
318
|
+
|
|
319
|
+
// If customTheme provided, merge its CSS variables (overriding base values)
|
|
320
|
+
if (customTheme) {
|
|
321
|
+
cssVariables = {
|
|
322
|
+
...cssVariables,
|
|
323
|
+
...Object.fromEntries(
|
|
324
|
+
Object.entries(customTheme.cssVars).map(([key, value]) => [
|
|
325
|
+
key.startsWith("--") ? key : `--${key}`,
|
|
326
|
+
value,
|
|
327
|
+
]),
|
|
328
|
+
),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
291
331
|
|
|
292
332
|
return { tokens: nestedTokens, cssVars: cssVariables };
|
|
293
|
-
}, [color, surface]);
|
|
333
|
+
}, [color, surface, customTheme]);
|
|
294
334
|
|
|
295
335
|
const contextValue: ThemeContextValue = {
|
|
296
336
|
cssVars,
|
|
297
337
|
tokens,
|
|
298
|
-
colorTheme: color,
|
|
338
|
+
colorTheme: customTheme ? (customTheme.extends as ColorThemeName) : color,
|
|
299
339
|
surfaceTheme: surface,
|
|
300
340
|
};
|
|
301
341
|
|