@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nationaldesignstudio/react",
3
- "version": "0.5.6",
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
- "relative shrink-0 bg-bg-muted",
53
- // Vertical: full width with aspect ratio
54
- "aspect-video w-full",
55
- // When in horizontal card (parent has flex-row), override
56
- "[.flex-row>&]:aspect-auto [.flex-row>&]:w-2/5 [.flex-row>&]:self-stretch",
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 extends React.HTMLAttributes<HTMLDivElement> {
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. For vertical layout, displays with 16:9 aspect ratio.
73
- * For horizontal layout, takes up ~40% width and stretches to content height.
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
  },
@@ -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
- * // Mix and match
243
- * <ThemeProvider color="institution" surface="soft">
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 (merge with base if not base)
280
+ // Get base color theme
259
281
  const baseColorModule = colorThemes.base;
260
- const colorModule = colorThemes[color];
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
- color === "base"
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
- const cssVariables = flatToCSSVars(flatTokens);
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