@nationaldesignstudio/react 0.0.15 → 0.0.16

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.
Files changed (164) hide show
  1. package/package.json +3 -2
  2. package/src/App.css +0 -0
  3. package/src/App.tsx +7 -0
  4. package/src/assets/fonts/PPNeueMontreal-Variable.woff2 +0 -0
  5. package/src/assets/react.svg +1 -0
  6. package/src/components/atoms/accordion/accordion.stories.tsx +228 -0
  7. package/src/components/atoms/accordion/accordion.tsx +219 -0
  8. package/src/components/atoms/accordion/index.ts +6 -0
  9. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-darwin.png +0 -0
  10. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-linux.png +0 -0
  11. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-darwin.png +0 -0
  12. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-linux.png +0 -0
  13. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-darwin.png +0 -0
  14. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-linux.png +0 -0
  15. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-darwin.png +0 -0
  16. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-linux.png +0 -0
  17. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-darwin.png +0 -0
  18. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-linux.png +0 -0
  19. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-darwin.png +0 -0
  20. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-linux.png +0 -0
  21. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-darwin.png +0 -0
  22. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-linux.png +0 -0
  23. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-darwin.png +0 -0
  24. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-linux.png +0 -0
  25. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-darwin.png +0 -0
  26. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-linux.png +0 -0
  27. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-darwin.png +0 -0
  28. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-linux.png +0 -0
  29. package/src/components/atoms/button/button.stories.tsx +102 -0
  30. package/src/components/atoms/button/button.test.tsx +135 -0
  31. package/src/components/atoms/button/button.tsx +139 -0
  32. package/src/components/atoms/button/button.visual.test.tsx +102 -0
  33. package/src/components/atoms/button/icon-button.stories.tsx +166 -0
  34. package/src/components/atoms/button/icon-button.tsx +120 -0
  35. package/src/components/atoms/button/index.ts +6 -0
  36. package/src/components/atoms/ndstudio-footer/index.ts +1 -0
  37. package/src/components/atoms/ndstudio-footer/ndstudio-footer.tsx +55 -0
  38. package/src/components/atoms/pager-control/index.ts +5 -0
  39. package/src/components/atoms/pager-control/pager-control.stories.tsx +209 -0
  40. package/src/components/atoms/pager-control/pager-control.test.tsx +130 -0
  41. package/src/components/atoms/pager-control/pager-control.tsx +329 -0
  42. package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +82 -0
  43. package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +196 -0
  44. package/src/components/dev-tools/dev-toolbar/index.ts +1 -0
  45. package/src/components/dev-tools/grid-overlay/grid-overlay.tsx +41 -0
  46. package/src/components/dev-tools/grid-overlay/index.ts +1 -0
  47. package/src/components/dev-tools/index.ts +2 -0
  48. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-darwin.png +0 -0
  49. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-linux.png +0 -0
  50. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-darwin.png +0 -0
  51. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-linux.png +0 -0
  52. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-darwin.png +0 -0
  53. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-linux.png +0 -0
  54. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-darwin.png +0 -0
  55. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-linux.png +0 -0
  56. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-darwin.png +0 -0
  57. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-linux.png +0 -0
  58. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-darwin.png +0 -0
  59. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-linux.png +0 -0
  60. package/src/components/organisms/card/card.stories.tsx +293 -0
  61. package/src/components/organisms/card/card.test.tsx +245 -0
  62. package/src/components/organisms/card/card.tsx +225 -0
  63. package/src/components/organisms/card/card.visual.test.tsx +197 -0
  64. package/src/components/organisms/card/index.ts +19 -0
  65. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-darwin.png +0 -0
  66. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-linux.png +0 -0
  67. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-darwin.png +0 -0
  68. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-linux.png +0 -0
  69. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-darwin.png +0 -0
  70. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-linux.png +0 -0
  71. package/src/components/organisms/navbar/index.ts +18 -0
  72. package/src/components/organisms/navbar/navbar.stories.tsx +313 -0
  73. package/src/components/organisms/navbar/navbar.test.tsx +190 -0
  74. package/src/components/organisms/navbar/navbar.tsx +323 -0
  75. package/src/components/organisms/navbar/navbar.visual.test.tsx +85 -0
  76. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-darwin.png +0 -0
  77. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-linux.png +0 -0
  78. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-darwin.png +0 -0
  79. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-linux.png +0 -0
  80. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-darwin.png +0 -0
  81. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-linux.png +0 -0
  82. package/src/components/organisms/us-gov-banner/index.ts +1 -0
  83. package/src/components/organisms/us-gov-banner/us-gov-banner.stories.tsx +35 -0
  84. package/src/components/organisms/us-gov-banner/us-gov-banner.test.tsx +107 -0
  85. package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +73 -0
  86. package/src/components/organisms/us-gov-banner/us-gov-banner.visual.test.tsx +46 -0
  87. package/src/components/sections/banner/banner.stories.tsx +150 -0
  88. package/src/components/sections/banner/banner.test.tsx +185 -0
  89. package/src/components/sections/banner/banner.tsx +130 -0
  90. package/src/components/sections/banner/index.ts +2 -0
  91. package/src/components/sections/card-grid/card-grid.stories.tsx +351 -0
  92. package/src/components/sections/card-grid/card-grid.tsx +116 -0
  93. package/src/components/sections/card-grid/index.ts +1 -0
  94. package/src/components/sections/faq-section/faq-section.stories.tsx +453 -0
  95. package/src/components/sections/faq-section/faq-section.tsx +84 -0
  96. package/src/components/sections/faq-section/index.ts +2 -0
  97. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-darwin.png +0 -0
  98. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-linux.png +0 -0
  99. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-darwin.png +0 -0
  100. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-linux.png +0 -0
  101. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-darwin.png +0 -0
  102. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-linux.png +0 -0
  103. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-darwin.png +0 -0
  104. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-linux.png +0 -0
  105. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-darwin.png +0 -0
  106. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-linux.png +0 -0
  107. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-darwin.png +0 -0
  108. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-linux.png +0 -0
  109. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-darwin.png +0 -0
  110. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-linux.png +0 -0
  111. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-darwin.png +0 -0
  112. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-linux.png +0 -0
  113. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-darwin.png +0 -0
  114. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-linux.png +0 -0
  115. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-darwin.png +0 -0
  116. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-linux.png +0 -0
  117. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-default-chromium-linux.png +0 -0
  118. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-darwin.png +0 -0
  119. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-linux.png +0 -0
  120. package/src/components/sections/hero/hero.stories.tsx +274 -0
  121. package/src/components/sections/hero/hero.test.tsx +135 -0
  122. package/src/components/sections/hero/hero.tsx +453 -0
  123. package/src/components/sections/hero/hero.visual.test.tsx +140 -0
  124. package/src/components/sections/hero/index.ts +10 -0
  125. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-darwin.png +0 -0
  126. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-linux.png +0 -0
  127. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-darwin.png +0 -0
  128. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-linux.png +0 -0
  129. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-darwin.png +0 -0
  130. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-linux.png +0 -0
  131. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-darwin.png +0 -0
  132. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-linux.png +0 -0
  133. package/src/components/sections/prose/index.ts +6 -0
  134. package/src/components/sections/prose/prose.stories.tsx +144 -0
  135. package/src/components/sections/prose/prose.test.tsx +178 -0
  136. package/src/components/sections/prose/prose.tsx +88 -0
  137. package/src/components/sections/prose/prose.visual.test.tsx +105 -0
  138. package/src/components/sections/river/index.ts +1 -0
  139. package/src/components/sections/river/river.stories.tsx +237 -0
  140. package/src/components/sections/river/river.test.tsx +268 -0
  141. package/src/components/sections/river/river.tsx +173 -0
  142. package/src/components/sections/tout/index.ts +1 -0
  143. package/src/components/sections/tout/tout.stories.tsx +171 -0
  144. package/src/components/sections/tout/tout.test.tsx +242 -0
  145. package/src/components/sections/tout/tout.tsx +270 -0
  146. package/src/components/sections/two-column-section/index.ts +5 -0
  147. package/src/components/sections/two-column-section/two-column-section.stories.tsx +285 -0
  148. package/src/components/sections/two-column-section/two-column-section.tsx +162 -0
  149. package/src/hooks/index.ts +1 -0
  150. package/src/hooks/use-event-listener.ts +73 -0
  151. package/src/index.ts +155 -0
  152. package/src/lib/theme.ts +1000 -0
  153. package/src/lib/utils.ts +6 -0
  154. package/src/main.tsx +13 -0
  155. package/src/stories/GridSystem.stories.tsx +84 -0
  156. package/src/stories/Introduction.mdx +114 -0
  157. package/src/stories/ThemeProvider.stories.tsx +357 -0
  158. package/src/stories/TokenShowcase.stories.tsx +92 -0
  159. package/src/stories/TokenShowcase.tsx +1429 -0
  160. package/src/styles.css +11 -0
  161. package/src/theme/ThemeProvider.tsx +297 -0
  162. package/src/theme/hooks.ts +40 -0
  163. package/src/theme/index.ts +43 -0
  164. package/src/theme/utils.ts +104 -0
package/src/styles.css ADDED
@@ -0,0 +1,11 @@
1
+ @import "tailwindcss";
2
+ @import "../../tailwind-token-generator/dist/tokens.css";
3
+
4
+ @font-face {
5
+ font-family: "PP Neue Montreal";
6
+ src: url("./assets/fonts/PPNeueMontreal-Variable.woff2")
7
+ format("woff2-variations");
8
+ font-weight: 100 900;
9
+ font-style: normal;
10
+ font-display: swap;
11
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * ThemeProvider
3
+ *
4
+ * React context provider for applying theme tokens as CSS variables.
5
+ * Use string theme names for type-safe selection, with "base" as default.
6
+ */
7
+
8
+ import type {
9
+ ColorThemeName,
10
+ CSSVariableMap,
11
+ NestedStringRecord,
12
+ SurfaceThemeName,
13
+ TokenModule,
14
+ } from "@nds-design-system/tokens";
15
+ import {
16
+ colorThemes,
17
+ flatToCSSVars,
18
+ flatToNested,
19
+ isColorValue,
20
+ isDimensionValue,
21
+ surfaceThemes,
22
+ } from "@nds-design-system/tokens";
23
+ import { createContext, type ReactNode, useMemo } from "react";
24
+
25
+ /**
26
+ * Theme context value
27
+ */
28
+ export interface ThemeContextValue {
29
+ /** CSS variables map for inline styles */
30
+ cssVars: CSSVariableMap;
31
+ /** Resolved nested tokens */
32
+ tokens: NestedStringRecord;
33
+ /** Current color theme name */
34
+ colorTheme: ColorThemeName;
35
+ /** Current surface theme name */
36
+ surfaceTheme: SurfaceThemeName;
37
+ }
38
+
39
+ export const ThemeContext = createContext<ThemeContextValue | null>(null);
40
+
41
+ /**
42
+ * Props for ThemeProvider
43
+ */
44
+ export interface ThemeProviderProps {
45
+ /** Color theme name (defaults to "base") */
46
+ color?: ColorThemeName;
47
+ /** Surface theme name (defaults to "base") */
48
+ surface?: SurfaceThemeName;
49
+ /** Children to render */
50
+ children: ReactNode;
51
+ }
52
+
53
+ /**
54
+ * Convert a color value to CSS string
55
+ */
56
+ function colorToCSS(color: { components: number[]; alpha: number }): string {
57
+ const [r, g, b] = color.components;
58
+ if (color.alpha < 1) {
59
+ return `color(srgb ${r} ${g} ${b} / ${color.alpha})`;
60
+ }
61
+ return `color(srgb ${r} ${g} ${b})`;
62
+ }
63
+
64
+ /**
65
+ * Convert a token value to CSS string
66
+ */
67
+ function tokenValueToCSS(value: unknown): string {
68
+ if (isColorValue(value)) {
69
+ return colorToCSS(value);
70
+ }
71
+ if (isDimensionValue(value)) {
72
+ return `${value.value}${value.unit}`;
73
+ }
74
+ if (typeof value === "string") {
75
+ return value;
76
+ }
77
+ if (typeof value === "number") {
78
+ return String(value);
79
+ }
80
+ return String(value);
81
+ }
82
+
83
+ /**
84
+ * Process color tokens to flat key-value pairs (without -- prefix)
85
+ */
86
+ function processColorTokens(
87
+ colorObj: Record<string, unknown>,
88
+ path: string[] = [],
89
+ ): Record<string, string> {
90
+ const result: Record<string, string> = {};
91
+
92
+ for (const [key, value] of Object.entries(colorObj)) {
93
+ if (key.startsWith("$")) continue;
94
+
95
+ const currentPath = [...path, key];
96
+
97
+ if (typeof value === "object" && value !== null) {
98
+ const record = value as Record<string, unknown>;
99
+
100
+ if ("$value" in record) {
101
+ // This is a token - key without -- prefix
102
+ const varName = `color-${currentPath.join("-")}`;
103
+ const tokenValue = record.$value;
104
+
105
+ if (typeof tokenValue === "string" && tokenValue.startsWith("{")) {
106
+ // Alias reference - resolve to CSS var
107
+ let refPath = tokenValue.slice(1, -1).replace(/\./g, "-");
108
+ refPath = refPath.replace(/^semantic-color-/, "color-");
109
+ result[varName] = `var(--${refPath})`;
110
+ } else {
111
+ result[varName] = tokenValueToCSS(tokenValue);
112
+ }
113
+ } else {
114
+ // Nested object - recurse
115
+ Object.assign(result, processColorTokens(record, currentPath));
116
+ }
117
+ }
118
+ }
119
+
120
+ return result;
121
+ }
122
+
123
+ /**
124
+ * Process surface tokens to flat key-value pairs (without -- prefix)
125
+ */
126
+ function processSurfaceTokens(
127
+ surfaceObj: Record<string, unknown>,
128
+ path: string[] = [],
129
+ ): Record<string, string> {
130
+ const result: Record<string, string> = {};
131
+
132
+ for (const [key, value] of Object.entries(surfaceObj)) {
133
+ if (key.startsWith("$")) continue;
134
+
135
+ const currentPath = [...path, key];
136
+
137
+ if (typeof value === "object" && value !== null) {
138
+ const record = value as Record<string, unknown>;
139
+
140
+ if ("$value" in record) {
141
+ // This is a token
142
+ const tokenValue = record.$value;
143
+ let varName: string;
144
+ let cssValue: string;
145
+
146
+ // Determine variable name based on token type (without -- prefix)
147
+ if (currentPath[currentPath.length - 1] === "radius") {
148
+ // e.g., button.radius -> radius-surface-button
149
+ const componentName = currentPath[0];
150
+ varName = `radius-surface-${componentName}`;
151
+ } else if (currentPath[currentPath.length - 1] === "stroke") {
152
+ // e.g., button.stroke -> surface-button-stroke
153
+ const componentName = currentPath[0];
154
+ varName = `surface-${componentName}-stroke`;
155
+ } else {
156
+ varName = `surface-${currentPath.join("-")}`;
157
+ }
158
+
159
+ // Resolve value
160
+ if (typeof tokenValue === "string" && tokenValue.startsWith("{")) {
161
+ // Alias reference
162
+ let refPath = tokenValue.slice(1, -1).replace(/\./g, "-");
163
+ refPath = refPath.replace(/^primitive-radii-/, "radius-");
164
+ cssValue = `var(--${refPath})`;
165
+ } else {
166
+ cssValue = tokenValueToCSS(tokenValue);
167
+ }
168
+
169
+ result[varName] = cssValue;
170
+ } else {
171
+ // Nested object - recurse
172
+ Object.assign(result, processSurfaceTokens(record, currentPath));
173
+ }
174
+ }
175
+ }
176
+
177
+ return result;
178
+ }
179
+
180
+ /**
181
+ * Deep merge two token modules
182
+ */
183
+ function mergeTokenModules(
184
+ base: TokenModule,
185
+ override: TokenModule,
186
+ ): Record<string, unknown> {
187
+ const result: Record<string, unknown> = JSON.parse(JSON.stringify(base));
188
+ deepMerge(result, override as Record<string, unknown>);
189
+ return result;
190
+ }
191
+
192
+ /**
193
+ * Deep merge objects
194
+ */
195
+ function deepMerge(
196
+ target: Record<string, unknown>,
197
+ source: Record<string, unknown>,
198
+ ): void {
199
+ for (const [key, value] of Object.entries(source)) {
200
+ if (
201
+ typeof value === "object" &&
202
+ value !== null &&
203
+ !Array.isArray(value) &&
204
+ typeof target[key] === "object" &&
205
+ target[key] !== null
206
+ ) {
207
+ deepMerge(
208
+ target[key] as Record<string, unknown>,
209
+ value as Record<string, unknown>,
210
+ );
211
+ } else {
212
+ target[key] = value;
213
+ }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * ThemeProvider component
219
+ *
220
+ * Applies theme tokens as CSS variables using type-safe theme names.
221
+ *
222
+ * @example
223
+ * ```tsx
224
+ * // Use default base themes
225
+ * <ThemeProvider>
226
+ * <App />
227
+ * </ThemeProvider>
228
+ *
229
+ * // Use specific themes by name
230
+ * <ThemeProvider color="civic" surface="sharp">
231
+ * <App />
232
+ * </ThemeProvider>
233
+ *
234
+ * // Mix and match
235
+ * <ThemeProvider color="institution" surface="soft">
236
+ * <App />
237
+ * </ThemeProvider>
238
+ * ```
239
+ */
240
+ export function ThemeProvider({
241
+ color = "base",
242
+ surface = "base",
243
+ children,
244
+ }: ThemeProviderProps) {
245
+ const { tokens, cssVars } = useMemo(() => {
246
+ const flatTokens: Record<string, string> = {};
247
+
248
+ // Get color theme (merge with base if not base)
249
+ const baseColorModule = colorThemes.base;
250
+ const colorModule = colorThemes[color];
251
+
252
+ const mergedColor =
253
+ color === "base"
254
+ ? (baseColorModule as Record<string, unknown>)
255
+ : mergeTokenModules(baseColorModule, colorModule);
256
+
257
+ // Navigate to semantic.color
258
+ const colorTokens =
259
+ (mergedColor as { semantic?: { color?: Record<string, unknown> } })
260
+ ?.semantic?.color ?? mergedColor;
261
+ Object.assign(flatTokens, processColorTokens(colorTokens));
262
+
263
+ // Get surface theme (merge with base if not base)
264
+ const baseSurfaceModule = surfaceThemes.base;
265
+ const surfaceModule = surfaceThemes[surface];
266
+
267
+ const mergedSurface =
268
+ surface === "base"
269
+ ? (baseSurfaceModule as Record<string, unknown>)
270
+ : mergeTokenModules(baseSurfaceModule, surfaceModule);
271
+
272
+ // Navigate to semantic.surface
273
+ const surfaceTokens =
274
+ (mergedSurface as { semantic?: { surface?: Record<string, unknown> } })
275
+ ?.semantic?.surface ?? mergedSurface;
276
+ Object.assign(flatTokens, processSurfaceTokens(surfaceTokens));
277
+
278
+ // Use shared utilities from tokens package
279
+ const nestedTokens = flatToNested(flatTokens);
280
+ const cssVariables = flatToCSSVars(flatTokens);
281
+
282
+ return { tokens: nestedTokens, cssVars: cssVariables };
283
+ }, [color, surface]);
284
+
285
+ const contextValue: ThemeContextValue = {
286
+ cssVars,
287
+ tokens,
288
+ colorTheme: color,
289
+ surfaceTheme: surface,
290
+ };
291
+
292
+ return (
293
+ <ThemeContext.Provider value={contextValue}>
294
+ {children}
295
+ </ThemeContext.Provider>
296
+ );
297
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Theme Hooks
3
+ *
4
+ * React hooks for accessing theme context.
5
+ */
6
+
7
+ import type {
8
+ CSSVariableMap,
9
+ NestedStringRecord,
10
+ } from "@nds-design-system/tokens";
11
+ import { useContext } from "react";
12
+ import { ThemeContext, type ThemeContextValue } from "./ThemeProvider";
13
+
14
+ /**
15
+ * Hook to access the theme context
16
+ * @throws Error if used outside of ThemeProvider
17
+ */
18
+ export function useTheme(): ThemeContextValue {
19
+ const context = useContext(ThemeContext);
20
+ if (!context) {
21
+ throw new Error("useTheme must be used within a ThemeProvider");
22
+ }
23
+ return context;
24
+ }
25
+
26
+ /**
27
+ * Hook to get just the nested tokens
28
+ */
29
+ export function useThemeTokens(): NestedStringRecord {
30
+ const { tokens } = useTheme();
31
+ return tokens;
32
+ }
33
+
34
+ /**
35
+ * Hook to get CSS variables for inline styles
36
+ */
37
+ export function useCSSVars(): CSSVariableMap {
38
+ const { cssVars } = useTheme();
39
+ return cssVars;
40
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Theme Module
3
+ *
4
+ * React components and utilities for theme composition and CSS variable application.
5
+ */
6
+
7
+ // Re-export types from tokens package for convenience
8
+ export type {
9
+ APICollection,
10
+ CollectionDefinition,
11
+ ColorThemeName,
12
+ CSSVariableMap,
13
+ NestedStringRecord,
14
+ SurfaceThemeName,
15
+ ThemeComposition,
16
+ } from "@nds-design-system/tokens";
17
+ // Re-export theme registries for programmatic access
18
+ export {
19
+ colorThemeNames,
20
+ colorThemes,
21
+ surfaceThemeNames,
22
+ surfaceThemes,
23
+ } from "@nds-design-system/tokens";
24
+
25
+ // Hooks
26
+ export { useCSSVars, useTheme, useThemeTokens } from "./hooks";
27
+ // Provider
28
+ export {
29
+ ThemeContext,
30
+ type ThemeContextValue,
31
+ ThemeProvider,
32
+ type ThemeProviderProps,
33
+ } from "./ThemeProvider";
34
+ // Utilities
35
+ export {
36
+ applyTheme,
37
+ createThemeStyle,
38
+ filterCSSVars,
39
+ getToken,
40
+ mergeCSSVars,
41
+ removeTheme,
42
+ toCSSVars,
43
+ } from "./utils";
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Theme Utilities
3
+ *
4
+ * Helper functions for working with theme tokens and CSS variables.
5
+ */
6
+
7
+ import type {
8
+ CSSVariableMap,
9
+ NestedStringRecord,
10
+ } from "@nds-design-system/tokens";
11
+ import { nestedToCSSVars } from "@nds-design-system/tokens";
12
+
13
+ /**
14
+ * Convert nested token object to CSS variable map
15
+ * @example
16
+ * toCSSVars({ color: { bg: { page: '#fff' } } })
17
+ * // Returns: { '--color-bg-page': '#fff' }
18
+ */
19
+ export function toCSSVars(tokens: NestedStringRecord): CSSVariableMap {
20
+ return nestedToCSSVars(tokens);
21
+ }
22
+
23
+ /**
24
+ * Apply CSS variables to an element
25
+ * @example
26
+ * applyTheme(document.body, { '--color-bg-page': '#fff' })
27
+ */
28
+ export function applyTheme(
29
+ element: HTMLElement,
30
+ cssVars: CSSVariableMap,
31
+ ): void {
32
+ for (const [name, value] of Object.entries(cssVars)) {
33
+ element.style.setProperty(name, value);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Remove CSS variables from an element
39
+ */
40
+ export function removeTheme(
41
+ element: HTMLElement,
42
+ cssVars: CSSVariableMap,
43
+ ): void {
44
+ for (const name of Object.keys(cssVars)) {
45
+ element.style.removeProperty(name);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get a specific token value from nested tokens using dot notation
51
+ * @example
52
+ * getToken({ color: { bg: { page: '#fff' } } }, 'color.bg.page')
53
+ * // Returns: '#fff'
54
+ */
55
+ export function getToken(
56
+ tokens: NestedStringRecord,
57
+ path: string,
58
+ ): string | undefined {
59
+ const parts = path.split(".");
60
+ let current: NestedStringRecord | string = tokens;
61
+
62
+ for (const part of parts) {
63
+ if (typeof current === "string") return undefined;
64
+ current = current[part];
65
+ if (current === undefined) return undefined;
66
+ }
67
+
68
+ return typeof current === "string" ? current : undefined;
69
+ }
70
+
71
+ /**
72
+ * Create a style object with CSS variables for React inline styles
73
+ * @example
74
+ * <div style={createThemeStyle(cssVars)}>...</div>
75
+ */
76
+ export function createThemeStyle(cssVars: CSSVariableMap): React.CSSProperties {
77
+ return cssVars as React.CSSProperties;
78
+ }
79
+
80
+ /**
81
+ * Merge multiple CSS variable maps
82
+ */
83
+ export function mergeCSSVars(...maps: CSSVariableMap[]): CSSVariableMap {
84
+ return Object.assign({}, ...maps);
85
+ }
86
+
87
+ /**
88
+ * Filter CSS variables by prefix
89
+ * @example
90
+ * filterCSSVars(cssVars, '--color-')
91
+ * // Returns only variables starting with --color-
92
+ */
93
+ export function filterCSSVars(
94
+ cssVars: CSSVariableMap,
95
+ prefix: string,
96
+ ): CSSVariableMap {
97
+ const result: CSSVariableMap = {};
98
+ for (const [name, value] of Object.entries(cssVars)) {
99
+ if (name.startsWith(prefix)) {
100
+ result[name] = value;
101
+ }
102
+ }
103
+ return result;
104
+ }