@nationaldesignstudio/react 0.5.6 → 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/index.d.ts +290 -7
- package/dist/index.js +95 -17
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
- package/src/components/organisms/card/card.tsx +87 -12
- package/src/components/sections/hero/hero.tsx +35 -0
- 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,7 +45,8 @@
|
|
|
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": {
|
|
51
52
|
"@cloudflare/stream-react": "^1.0.0",
|
|
@@ -74,11 +75,9 @@
|
|
|
74
75
|
"tailwind-variants": "^0.3.1"
|
|
75
76
|
},
|
|
76
77
|
"devDependencies": {
|
|
77
|
-
"@cloudflare/stream-react": "^1.9.1",
|
|
78
78
|
"@chromatic-com/storybook": "catalog:",
|
|
79
|
+
"@cloudflare/stream-react": "^1.9.1",
|
|
79
80
|
"@figma/code-connect": "^1.3.12",
|
|
80
|
-
"hls.js": "^1.5.17",
|
|
81
|
-
"lucide-react": "^0.511.0",
|
|
82
81
|
"@nds-design-system/tailwind-token-generator": "workspace:*",
|
|
83
82
|
"@nds-design-system/tokens": "workspace:*",
|
|
84
83
|
"@nds-design-system/tools": "workspace:*",
|
|
@@ -99,9 +98,12 @@
|
|
|
99
98
|
"@vitest/browser-playwright": "catalog:",
|
|
100
99
|
"@vitest/coverage-v8": "catalog:",
|
|
101
100
|
"globals": "catalog:",
|
|
101
|
+
"hls.js": "^1.5.17",
|
|
102
|
+
"lucide-react": "^0.511.0",
|
|
102
103
|
"playwright": "catalog:",
|
|
103
104
|
"react": "catalog:",
|
|
104
105
|
"react-dom": "catalog:",
|
|
106
|
+
"release-it": "^19.2.4",
|
|
105
107
|
"storybook": "catalog:",
|
|
106
108
|
"tailwindcss": "catalog:",
|
|
107
109
|
"tsup": "^8.5.1",
|
|
@@ -12,6 +12,16 @@ const cardVariants = tv({
|
|
|
12
12
|
* Use with Background components for images/gradients.
|
|
13
13
|
*/
|
|
14
14
|
overlay: "w-full flex-col",
|
|
15
|
+
/**
|
|
16
|
+
* Profile layout - square image with stacked content below.
|
|
17
|
+
* Ideal for team member cards, user profiles, testimonials.
|
|
18
|
+
*/
|
|
19
|
+
profile: "w-full flex-col",
|
|
20
|
+
/**
|
|
21
|
+
* Compact layout - small thumbnail with condensed horizontal content.
|
|
22
|
+
* Ideal for news items, article previews, resource lists.
|
|
23
|
+
*/
|
|
24
|
+
compact: "w-full flex-row items-center gap-16",
|
|
15
25
|
},
|
|
16
26
|
},
|
|
17
27
|
defaultVariants: {
|
|
@@ -48,16 +58,26 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
|
48
58
|
Card.displayName = "Card";
|
|
49
59
|
|
|
50
60
|
const cardImageVariants = tv({
|
|
51
|
-
base:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
base: "relative shrink-0 bg-bg-muted",
|
|
62
|
+
variants: {
|
|
63
|
+
layout: {
|
|
64
|
+
vertical: "aspect-video w-full",
|
|
65
|
+
horizontal: "aspect-auto w-2/5 self-stretch",
|
|
66
|
+
overlay: "aspect-video w-full",
|
|
67
|
+
/** Profile: square aspect ratio for headshots/avatars */
|
|
68
|
+
profile: "aspect-square w-full",
|
|
69
|
+
/** Compact: fixed small size for thumbnails */
|
|
70
|
+
compact: "size-80 rounded-8",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
defaultVariants: {
|
|
74
|
+
layout: "vertical",
|
|
75
|
+
},
|
|
58
76
|
});
|
|
59
77
|
|
|
60
|
-
export interface CardImageProps
|
|
78
|
+
export interface CardImageProps
|
|
79
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
80
|
+
VariantProps<typeof cardImageVariants> {
|
|
61
81
|
/**
|
|
62
82
|
* The image source URL
|
|
63
83
|
*/
|
|
@@ -69,15 +89,18 @@ export interface CardImageProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
69
89
|
}
|
|
70
90
|
|
|
71
91
|
/**
|
|
72
|
-
* Card image area
|
|
73
|
-
*
|
|
92
|
+
* Card image area with layout-specific styling.
|
|
93
|
+
* - vertical: 16:9 aspect ratio, full width
|
|
94
|
+
* - horizontal: ~40% width, stretches to content height
|
|
95
|
+
* - profile: square aspect ratio for headshots/avatars
|
|
96
|
+
* - compact: fixed small size for thumbnails
|
|
74
97
|
*/
|
|
75
98
|
const CardImage = React.forwardRef<HTMLDivElement, CardImageProps>(
|
|
76
|
-
({ className, src, alt = "", ...props }, ref) => {
|
|
99
|
+
({ className, src, alt = "", layout, ...props }, ref) => {
|
|
77
100
|
return (
|
|
78
101
|
<div
|
|
79
102
|
ref={ref}
|
|
80
|
-
className={cardImageVariants({ class: className })}
|
|
103
|
+
className={cardImageVariants({ layout, class: className })}
|
|
81
104
|
{...props}
|
|
82
105
|
>
|
|
83
106
|
{src && (
|
|
@@ -263,6 +286,56 @@ const CardActions = React.forwardRef<HTMLDivElement, CardActionsProps>(
|
|
|
263
286
|
);
|
|
264
287
|
CardActions.displayName = "CardActions";
|
|
265
288
|
|
|
289
|
+
const cardLinkVariants = tv({
|
|
290
|
+
base: "group/link flex items-center gap-4 text-text-muted transition-colors duration-200 hover:text-text-primary",
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
export interface CardLinkProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
294
|
+
/**
|
|
295
|
+
* Whether to show the arrow indicator
|
|
296
|
+
* @default true
|
|
297
|
+
*/
|
|
298
|
+
showArrow?: boolean;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Inline link element for cards with optional animated arrow.
|
|
303
|
+
* Commonly used for "Read More →" or "Learn More →" patterns.
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```tsx
|
|
307
|
+
* <Card layout="compact">
|
|
308
|
+
* <CardImage src="/thumb.jpg" layout="compact" />
|
|
309
|
+
* <CardContent>
|
|
310
|
+
* <CardTitle>Article Title</CardTitle>
|
|
311
|
+
* <CardLink>Read More</CardLink>
|
|
312
|
+
* </CardContent>
|
|
313
|
+
* </Card>
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
const CardLink = React.forwardRef<HTMLSpanElement, CardLinkProps>(
|
|
317
|
+
({ className, children, showArrow = true, ...props }, ref) => {
|
|
318
|
+
return (
|
|
319
|
+
<span
|
|
320
|
+
ref={ref}
|
|
321
|
+
className={cardLinkVariants({ class: className })}
|
|
322
|
+
{...props}
|
|
323
|
+
>
|
|
324
|
+
<span>{children}</span>
|
|
325
|
+
{showArrow && (
|
|
326
|
+
<span
|
|
327
|
+
aria-hidden="true"
|
|
328
|
+
className="inline-block transition-transform duration-200 ease-out group-hover/link:translate-x-4"
|
|
329
|
+
>
|
|
330
|
+
→
|
|
331
|
+
</span>
|
|
332
|
+
)}
|
|
333
|
+
</span>
|
|
334
|
+
);
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
CardLink.displayName = "CardLink";
|
|
338
|
+
|
|
266
339
|
export {
|
|
267
340
|
Card,
|
|
268
341
|
cardVariants,
|
|
@@ -280,4 +353,6 @@ export {
|
|
|
280
353
|
cardBodyVariants,
|
|
281
354
|
CardActions,
|
|
282
355
|
cardActionsVariants,
|
|
356
|
+
CardLink,
|
|
357
|
+
cardLinkVariants,
|
|
283
358
|
};
|
|
@@ -30,6 +30,7 @@ const heroVariants = tv({
|
|
|
30
30
|
"md:p-56",
|
|
31
31
|
],
|
|
32
32
|
title: DEFAULT_TITLE_TYPOGRAPHY,
|
|
33
|
+
indicator: "absolute inset-x-0 bottom-0 z-10 flex justify-center pb-24",
|
|
33
34
|
},
|
|
34
35
|
variants: {
|
|
35
36
|
variant: {
|
|
@@ -46,6 +47,21 @@ const heroVariants = tv({
|
|
|
46
47
|
content: ["items-center justify-center", "lg:p-64"],
|
|
47
48
|
},
|
|
48
49
|
},
|
|
50
|
+
/**
|
|
51
|
+
* Vertical alignment of content within the hero.
|
|
52
|
+
* Provides a simpler API than variant for basic alignment needs.
|
|
53
|
+
*/
|
|
54
|
+
contentAlign: {
|
|
55
|
+
top: {
|
|
56
|
+
content: "justify-start",
|
|
57
|
+
},
|
|
58
|
+
center: {
|
|
59
|
+
content: "items-center justify-center",
|
|
60
|
+
},
|
|
61
|
+
bottom: {
|
|
62
|
+
content: "justify-end",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
49
65
|
colorScheme: {
|
|
50
66
|
dark: {
|
|
51
67
|
root: "bg-bg-page",
|
|
@@ -144,11 +160,24 @@ export interface HeroProps
|
|
|
144
160
|
* - light: Light text for use on dark backgrounds
|
|
145
161
|
*/
|
|
146
162
|
colorScheme?: "dark" | "light";
|
|
163
|
+
// Note: contentAlign is provided by VariantProps<typeof heroVariants>
|
|
164
|
+
// Options: "top" | "center" | "bottom"
|
|
147
165
|
/**
|
|
148
166
|
* Content for the top slot (full-width, no padding).
|
|
149
167
|
* Use for USGovBanner, Navigation, etc.
|
|
150
168
|
*/
|
|
151
169
|
top?: React.ReactNode;
|
|
170
|
+
/**
|
|
171
|
+
* Indicator slot for scroll hints, arrows, or other visual cues.
|
|
172
|
+
* Rendered at the bottom of the hero, below the main content.
|
|
173
|
+
* @example
|
|
174
|
+
* ```tsx
|
|
175
|
+
* <Hero indicator={<ChevronDown className="animate-bounce" />}>
|
|
176
|
+
* <h1>Welcome</h1>
|
|
177
|
+
* </Hero>
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
indicator?: React.ReactNode;
|
|
152
181
|
/**
|
|
153
182
|
* Background for the hero. Can be:
|
|
154
183
|
* - A color string (hex, rgb, etc.) for solid backgrounds
|
|
@@ -235,7 +264,9 @@ const Hero = React.forwardRef<HTMLElement, HeroProps>(
|
|
|
235
264
|
title,
|
|
236
265
|
titleClassName,
|
|
237
266
|
colorScheme = "dark",
|
|
267
|
+
contentAlign,
|
|
238
268
|
top,
|
|
269
|
+
indicator,
|
|
239
270
|
variant,
|
|
240
271
|
background,
|
|
241
272
|
overlayOpacity = 0,
|
|
@@ -251,6 +282,7 @@ const Hero = React.forwardRef<HTMLElement, HeroProps>(
|
|
|
251
282
|
const hasMediaBackground = background && !isColor;
|
|
252
283
|
const styles = heroVariants({
|
|
253
284
|
variant,
|
|
285
|
+
contentAlign,
|
|
254
286
|
colorScheme,
|
|
255
287
|
hasBackground: !!background,
|
|
256
288
|
});
|
|
@@ -295,6 +327,9 @@ const Hero = React.forwardRef<HTMLElement, HeroProps>(
|
|
|
295
327
|
{/* Children - always render if provided */}
|
|
296
328
|
{children}
|
|
297
329
|
</div>
|
|
330
|
+
|
|
331
|
+
{/* Indicator slot - scroll hints, arrows, etc. */}
|
|
332
|
+
{indicator && <div className={styles.indicator()}>{indicator}</div>}
|
|
298
333
|
</section>
|
|
299
334
|
);
|
|
300
335
|
},
|
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
|
|