@nationaldesignstudio/react 0.3.0 → 0.5.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.
Files changed (79) hide show
  1. package/dist/component-registry.md +1310 -127
  2. package/dist/components/atoms/button/button.d.ts +55 -47
  3. package/dist/components/atoms/button/button.figma.d.ts +1 -0
  4. package/dist/components/atoms/input/input.d.ts +24 -24
  5. package/dist/components/atoms/popover/popover.d.ts +195 -0
  6. package/dist/components/atoms/select/select.d.ts +24 -24
  7. package/dist/components/atoms/tooltip/tooltip.d.ts +161 -0
  8. package/dist/components/organisms/card/card.d.ts +1 -1
  9. package/dist/components/sections/hero/hero.d.ts +2 -2
  10. package/dist/components/sections/tout/tout.d.ts +3 -3
  11. package/dist/components/shared/floating-arrow.d.ts +34 -0
  12. package/dist/index.d.ts +8 -0
  13. package/dist/index.js +11602 -8499
  14. package/dist/index.js.map +1 -1
  15. package/dist/lib/form-control.d.ts +25 -24
  16. package/dist/tokens.css +4797 -3940
  17. package/package.json +2 -1
  18. package/src/components/atoms/accordion/accordion.stories.tsx +1 -1
  19. package/src/components/atoms/accordion/accordion.tsx +2 -2
  20. package/src/components/atoms/button/button.figma.tsx +37 -0
  21. package/src/components/atoms/button/button.stories.tsx +236 -140
  22. package/src/components/atoms/button/button.test.tsx +289 -5
  23. package/src/components/atoms/button/button.tsx +37 -33
  24. package/src/components/atoms/button/button.visual.test.tsx +26 -76
  25. package/src/components/atoms/button/icon-button.stories.tsx +44 -101
  26. package/src/components/atoms/button/icon-button.test.tsx +26 -94
  27. package/src/components/atoms/button/icon-button.tsx +3 -3
  28. package/src/components/atoms/input/input-group.stories.tsx +4 -8
  29. package/src/components/atoms/input/input-group.test.tsx +14 -28
  30. package/src/components/atoms/input/input-group.tsx +57 -32
  31. package/src/components/atoms/input/input.stories.tsx +14 -18
  32. package/src/components/atoms/input/input.test.tsx +4 -20
  33. package/src/components/atoms/input/input.tsx +16 -9
  34. package/src/components/atoms/pager-control/pager-control.stories.tsx +6 -8
  35. package/src/components/atoms/pager-control/pager-control.tsx +12 -12
  36. package/src/components/atoms/popover/index.ts +30 -0
  37. package/src/components/atoms/popover/popover.stories.tsx +531 -0
  38. package/src/components/atoms/popover/popover.test.tsx +486 -0
  39. package/src/components/atoms/popover/popover.tsx +488 -0
  40. package/src/components/atoms/select/select.tsx +12 -8
  41. package/src/components/atoms/tooltip/index.ts +24 -0
  42. package/src/components/atoms/tooltip/tooltip.stories.tsx +348 -0
  43. package/src/components/atoms/tooltip/tooltip.test.tsx +363 -0
  44. package/src/components/atoms/tooltip/tooltip.tsx +347 -0
  45. package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +8 -13
  46. package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +3 -3
  47. package/src/components/organisms/card/card.stories.tsx +19 -19
  48. package/src/components/organisms/card/card.tsx +1 -1
  49. package/src/components/organisms/card/card.visual.test.tsx +11 -11
  50. package/src/components/organisms/navbar/navbar.visual.test.tsx +2 -2
  51. package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +2 -2
  52. package/src/components/sections/banner/banner.stories.tsx +1 -5
  53. package/src/components/sections/banner/banner.test.tsx +2 -2
  54. package/src/components/sections/banner/banner.tsx +6 -6
  55. package/src/components/sections/card-grid/card-grid.tsx +4 -4
  56. package/src/components/sections/hero/hero.stories.tsx +7 -7
  57. package/src/components/sections/hero/hero.tsx +10 -11
  58. package/src/components/sections/prose/prose.tsx +2 -2
  59. package/src/components/sections/river/river.test.tsx +3 -3
  60. package/src/components/sections/river/river.tsx +6 -12
  61. package/src/components/sections/tout/tout.stories.tsx +7 -31
  62. package/src/components/sections/tout/tout.tsx +9 -9
  63. package/src/components/sections/two-column-section/two-column-section.tsx +7 -9
  64. package/src/components/shared/floating-arrow.tsx +78 -0
  65. package/src/components/shared/index.ts +5 -0
  66. package/src/index.ts +57 -0
  67. package/src/lib/form-control.ts +8 -6
  68. package/src/stories/grid-system.stories.tsx +309 -0
  69. package/src/stories/{ThemeProvider.stories.tsx → theme-provider.stories.tsx} +7 -19
  70. package/src/stories/{TokenShowcase.stories.tsx → token-showcase.stories.tsx} +1 -1
  71. package/src/stories/{TokenShowcase.tsx → token-showcase.tsx} +34 -34
  72. package/src/styles.css +3 -3
  73. package/src/tests/token-resolution.test.tsx +6 -9
  74. package/src/theme/hooks.ts +1 -1
  75. package/src/theme/index.ts +1 -1
  76. package/src/theme/theme-provider.test.tsx +270 -0
  77. package/src/theme/{ThemeProvider.tsx → theme-provider.tsx} +18 -2
  78. package/src/stories/GridSystem.stories.tsx +0 -84
  79. /package/src/stories/{Introduction.mdx → introduction.mdx} +0 -0
@@ -0,0 +1,270 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { page } from "vitest/browser";
3
+ import { render } from "vitest-browser-react";
4
+ import { Button } from "../components/atoms/button/button";
5
+ import { useCSSVars, useTheme } from "./hooks";
6
+ import { ThemeProvider } from "./theme-provider";
7
+
8
+ function getInlineStyleVar(element: Element, varName: string): string | null {
9
+ const style = element.getAttribute("style") || "";
10
+ const regex = new RegExp(`${varName}:\\s*([^;]+)`);
11
+ const match = style.match(regex);
12
+ return match ? match[1].trim() : null;
13
+ }
14
+
15
+ describe("ThemeProvider", () => {
16
+ describe("CSS Variable Application", () => {
17
+ test("renders a wrapper div with style attribute", async () => {
18
+ render(
19
+ <ThemeProvider>
20
+ <div data-testid="content">Content</div>
21
+ </ThemeProvider>,
22
+ );
23
+
24
+ const content = page.getByTestId("content");
25
+ await expect.element(content).toBeInTheDocument();
26
+
27
+ const wrapper = content.element().parentElement;
28
+ expect(wrapper).toBeTruthy();
29
+ expect(wrapper?.tagName).toBe("DIV");
30
+ expect(wrapper?.hasAttribute("style")).toBe(true);
31
+
32
+ const styleAttr = wrapper?.getAttribute("style") || "";
33
+ expect(styleAttr).toContain("--color-bg-page");
34
+ });
35
+
36
+ test("applies className to wrapper div", async () => {
37
+ render(
38
+ <ThemeProvider className="custom-class">
39
+ <div data-testid="content">Content</div>
40
+ </ThemeProvider>,
41
+ );
42
+
43
+ const content = page.getByTestId("content");
44
+ const wrapper = content.element().parentElement;
45
+ expect(wrapper?.classList.contains("custom-class")).toBe(true);
46
+ });
47
+
48
+ test("applyStyles=false skips wrapper div", async () => {
49
+ function TestContent() {
50
+ const theme = useTheme();
51
+ return <div data-testid="content">{theme.colorTheme}</div>;
52
+ }
53
+
54
+ render(
55
+ <ThemeProvider applyStyles={false}>
56
+ <TestContent />
57
+ </ThemeProvider>,
58
+ );
59
+
60
+ const content = page.getByTestId("content");
61
+ await expect.element(content).toBeInTheDocument();
62
+ expect(content.element().textContent).toBe("base");
63
+ });
64
+ });
65
+
66
+ describe("Color Theme Overrides", () => {
67
+ test("base theme applies CSS variables for buttons", async () => {
68
+ render(
69
+ <ThemeProvider color="base">
70
+ <Button data-testid="btn" variant="primary">
71
+ Primary
72
+ </Button>
73
+ </ThemeProvider>,
74
+ );
75
+
76
+ const button = page.getByTestId("btn");
77
+ await expect.element(button).toBeInTheDocument();
78
+
79
+ const wrapper = button.element().parentElement;
80
+ const styleAttr = wrapper?.getAttribute("style") || "";
81
+ expect(styleAttr).toContain("--color-button-primary-bg");
82
+ });
83
+
84
+ test("civic theme overrides button primary color", async () => {
85
+ render(
86
+ <ThemeProvider color="civic">
87
+ <Button data-testid="btn" variant="primary">
88
+ Primary
89
+ </Button>
90
+ </ThemeProvider>,
91
+ );
92
+
93
+ const button = page.getByTestId("btn");
94
+ await expect.element(button).toBeInTheDocument();
95
+
96
+ const wrapper = button.element().parentElement;
97
+ expect(wrapper).toBeTruthy();
98
+
99
+ const styleAttr = wrapper?.getAttribute("style") || "";
100
+ expect(styleAttr).toContain("--color-button-primary-bg");
101
+ expect(styleAttr).toContain("red");
102
+ });
103
+
104
+ test("dark theme applies dark mode colors", async () => {
105
+ render(
106
+ <ThemeProvider color="dark">
107
+ <div data-testid="bg" className="bg-bg-page p-16">
108
+ Dark Theme
109
+ </div>
110
+ </ThemeProvider>,
111
+ );
112
+
113
+ const bgElement = page.getByTestId("bg");
114
+ await expect.element(bgElement).toBeInTheDocument();
115
+
116
+ const wrapper = bgElement.element().parentElement;
117
+ const styleAttr = wrapper?.getAttribute("style") || "";
118
+ expect(styleAttr).toContain("--color-bg-page");
119
+ });
120
+
121
+ test("institution theme uses green button colors", async () => {
122
+ render(
123
+ <ThemeProvider color="institution">
124
+ <Button data-testid="btn" variant="primary">
125
+ Institution
126
+ </Button>
127
+ </ThemeProvider>,
128
+ );
129
+
130
+ const button = page.getByTestId("btn");
131
+ await expect.element(button).toBeInTheDocument();
132
+
133
+ const wrapper = button.element().parentElement;
134
+ const styleAttr = wrapper?.getAttribute("style") || "";
135
+ expect(styleAttr).toContain("--color-button-primary-bg");
136
+ expect(styleAttr).toContain("green");
137
+ });
138
+ });
139
+
140
+ describe("Theme Context", () => {
141
+ test("useTheme returns current theme names", async () => {
142
+ function ThemeInfo() {
143
+ const { colorTheme, surfaceTheme } = useTheme();
144
+ return (
145
+ <div data-testid="info">
146
+ {colorTheme}-{surfaceTheme}
147
+ </div>
148
+ );
149
+ }
150
+
151
+ render(
152
+ <ThemeProvider color="civic" surface="sharp">
153
+ <ThemeInfo />
154
+ </ThemeProvider>,
155
+ );
156
+
157
+ const info = page.getByTestId("info");
158
+ await expect.element(info).toBeInTheDocument();
159
+ expect(info.element().textContent).toBe("civic-sharp");
160
+ });
161
+
162
+ test("useCSSVars returns CSS variable map", async () => {
163
+ function CSSVarsTest() {
164
+ const cssVars = useCSSVars();
165
+ const hasVars = Object.keys(cssVars).length > 0;
166
+ return (
167
+ <div data-testid="result">{hasVars ? "has-vars" : "no-vars"}</div>
168
+ );
169
+ }
170
+
171
+ render(
172
+ <ThemeProvider>
173
+ <CSSVarsTest />
174
+ </ThemeProvider>,
175
+ );
176
+
177
+ const result = page.getByTestId("result");
178
+ await expect.element(result).toBeInTheDocument();
179
+ expect(result.element().textContent).toBe("has-vars");
180
+ });
181
+ });
182
+
183
+ describe("Nested ThemeProviders", () => {
184
+ test("nested ThemeProvider overrides parent theme", async () => {
185
+ function ThemeInfo() {
186
+ const { colorTheme } = useTheme();
187
+ return <span data-testid="theme">{colorTheme}</span>;
188
+ }
189
+
190
+ render(
191
+ <ThemeProvider color="base">
192
+ <div>
193
+ <ThemeInfo />
194
+ <ThemeProvider color="civic">
195
+ <ThemeInfo />
196
+ </ThemeProvider>
197
+ </div>
198
+ </ThemeProvider>,
199
+ );
200
+
201
+ const themes = page.getByTestId("theme");
202
+ await expect.element(themes.first()).toBeInTheDocument();
203
+
204
+ const themeElements = document.querySelectorAll('[data-testid="theme"]');
205
+ expect(themeElements[0].textContent).toBe("base");
206
+ expect(themeElements[1].textContent).toBe("civic");
207
+ });
208
+ });
209
+
210
+ describe("Button Theming Integration", () => {
211
+ test("button primary-outline inherits theme colors", async () => {
212
+ render(
213
+ <ThemeProvider color="civic">
214
+ <Button data-testid="btn" variant="primary-outline">
215
+ Outline
216
+ </Button>
217
+ </ThemeProvider>,
218
+ );
219
+
220
+ const button = page.getByTestId("btn");
221
+ await expect.element(button).toBeInTheDocument();
222
+
223
+ const wrapper = button.element().parentElement;
224
+ const styleAttr = wrapper?.getAttribute("style") || "";
225
+ expect(styleAttr).toContain("--color-button-primary-outline");
226
+ });
227
+
228
+ test("different themes produce different CSS variables", async () => {
229
+ render(
230
+ <div className="flex gap-16">
231
+ <ThemeProvider color="base">
232
+ <Button data-testid="base-btn" variant="primary">
233
+ Base
234
+ </Button>
235
+ </ThemeProvider>
236
+ <ThemeProvider color="civic">
237
+ <Button data-testid="civic-btn" variant="primary">
238
+ Civic
239
+ </Button>
240
+ </ThemeProvider>
241
+ </div>,
242
+ );
243
+
244
+ const baseBtn = page.getByTestId("base-btn");
245
+ const civicBtn = page.getByTestId("civic-btn");
246
+
247
+ await expect.element(baseBtn).toBeInTheDocument();
248
+ await expect.element(civicBtn).toBeInTheDocument();
249
+
250
+ const baseWrapper = baseBtn.element().parentElement;
251
+ const civicWrapper = civicBtn.element().parentElement;
252
+
253
+ expect(baseWrapper).toBeTruthy();
254
+ expect(civicWrapper).toBeTruthy();
255
+
256
+ const baseBgVar = getInlineStyleVar(
257
+ baseWrapper as Element,
258
+ "--color-button-primary-bg",
259
+ );
260
+ const civicBgVar = getInlineStyleVar(
261
+ civicWrapper as Element,
262
+ "--color-button-primary-bg",
263
+ );
264
+
265
+ expect(baseBgVar).toBeTruthy();
266
+ expect(civicBgVar).toBeTruthy();
267
+ expect(baseBgVar).not.toBe(civicBgVar);
268
+ });
269
+ });
270
+ });
@@ -48,6 +48,10 @@ export interface ThemeProviderProps {
48
48
  surface?: SurfaceThemeName;
49
49
  /** Children to render */
50
50
  children: ReactNode;
51
+ /** Optional className for the wrapper div */
52
+ className?: string;
53
+ /** Whether to render a wrapper div with CSS variables applied (defaults to true) */
54
+ applyStyles?: boolean;
51
55
  }
52
56
 
53
57
  /**
@@ -105,6 +109,8 @@ function processColorTokens(
105
109
  if (typeof tokenValue === "string" && tokenValue.startsWith("{")) {
106
110
  // Alias reference - resolve to CSS var
107
111
  let refPath = tokenValue.slice(1, -1).replace(/\./g, "-");
112
+ // Strip prefixes that aren't used in CSS variable names
113
+ refPath = refPath.replace(/^primitive-/, "");
108
114
  refPath = refPath.replace(/^semantic-color-/, "color-");
109
115
  result[varName] = `var(--${refPath})`;
110
116
  } else {
@@ -160,7 +166,9 @@ function processSurfaceTokens(
160
166
  if (typeof tokenValue === "string" && tokenValue.startsWith("{")) {
161
167
  // Alias reference
162
168
  let refPath = tokenValue.slice(1, -1).replace(/\./g, "-");
163
- refPath = refPath.replace(/^primitive-radii-/, "radius-");
169
+ // Strip prefixes that aren't used in CSS variable names
170
+ refPath = refPath.replace(/^primitive-/, "");
171
+ refPath = refPath.replace(/^radii-/, "radius-");
164
172
  cssValue = `var(--${refPath})`;
165
173
  } else {
166
174
  cssValue = tokenValueToCSS(tokenValue);
@@ -241,6 +249,8 @@ export function ThemeProvider({
241
249
  color = "base",
242
250
  surface = "base",
243
251
  children,
252
+ className,
253
+ applyStyles = true,
244
254
  }: ThemeProviderProps) {
245
255
  const { tokens, cssVars } = useMemo(() => {
246
256
  const flatTokens: Record<string, string> = {};
@@ -291,7 +301,13 @@ export function ThemeProvider({
291
301
 
292
302
  return (
293
303
  <ThemeContext.Provider value={contextValue}>
294
- {children}
304
+ {applyStyles ? (
305
+ <div style={cssVars} className={className}>
306
+ {children}
307
+ </div>
308
+ ) : (
309
+ children
310
+ )}
295
311
  </ThemeContext.Provider>
296
312
  );
297
313
  }
@@ -1,84 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- /**
4
- * Spatial Grid System
5
- *
6
- * Responsive grid tokens that adapt across breakpoints:
7
- * - Large (lg): 1440px+ - 24 columns, 72px margin, 24px gutter
8
- * - Medium (md): 768px+ - 12 columns, 56px margin, 20px gutter
9
- * - Small (sm): 320px+ - 4 columns, 24px margin, 12px gutter
10
- */
11
-
12
- const GridColumn = ({ index }: { index: number }) => (
13
- <div className="h-12 bg-brand-primary/20 border border-brand-primary/40 rounded flex items-center justify-center">
14
- <span className="text-xs font-mono text-brand-primary">{index + 1}</span>
15
- </div>
16
- );
17
-
18
- const GridVisualization = () => (
19
- <div className="w-full bg-gray-100 py-8">
20
- <div className="w-full max-w-[90rem] mx-auto px-[var(--spatial-grid-small-margin)] md:px-[var(--spatial-grid-medium-margin)] lg:px-[var(--spatial-grid-large-margin)]">
21
- <div className="grid grid-cols-4 md:grid-cols-12 lg:grid-cols-24 gap-[var(--spatial-grid-small-gutter)] md:gap-[var(--spatial-grid-medium-gutter)] lg:gap-[var(--spatial-grid-large-gutter)]">
22
- {Array.from({ length: 24 }).map((_, i) => (
23
- // biome-ignore lint/suspicious/noArrayIndexKey: Static grid columns never reorder
24
- <GridColumn key={i} index={i} />
25
- ))}
26
- </div>
27
- </div>
28
- </div>
29
- );
30
-
31
- const meta = {
32
- title: "Design System/Grid System",
33
- component: GridVisualization,
34
- parameters: {
35
- layout: "fullscreen",
36
- },
37
- } satisfies Meta<typeof GridVisualization>;
38
-
39
- export default meta;
40
- type Story = StoryObj<typeof meta>;
41
-
42
- export const Default: Story = {};
43
-
44
- export const Desktop: Story = {
45
- globals: { viewport: { value: "lg" } },
46
- };
47
-
48
- export const Tablet: Story = {
49
- globals: { viewport: { value: "md" } },
50
- };
51
-
52
- export const Mobile: Story = {
53
- globals: { viewport: { value: "sm" } },
54
- };
55
-
56
- const ColumnSpans = () => (
57
- <div className="w-full py-8 px-[var(--spatial-grid-large-margin)]">
58
- <div className="max-w-[90rem] mx-auto grid grid-cols-24 gap-[var(--spatial-grid-large-gutter)]">
59
- <div className="col-span-full h-12 bg-gray-200 rounded flex items-center px-4">
60
- <code className="text-sm font-mono">col-span-full</code>
61
- </div>
62
- <div className="col-start-4 col-end-22 h-12 bg-gray-200 rounded flex items-center px-4">
63
- <code className="text-sm font-mono">
64
- col-start-4 col-end-22 (18 cols)
65
- </code>
66
- </div>
67
- <div className="col-start-7 col-end-19 h-12 bg-gray-200 rounded flex items-center px-4">
68
- <code className="text-sm font-mono">
69
- col-start-7 col-end-19 (12 cols)
70
- </code>
71
- </div>
72
- <div className="col-start-9 col-end-17 h-12 bg-gray-200 rounded flex items-center px-4">
73
- <code className="text-sm font-mono">
74
- col-start-9 col-end-17 (8 cols)
75
- </code>
76
- </div>
77
- </div>
78
- </div>
79
- );
80
-
81
- export const Spans: Story = {
82
- render: () => <ColumnSpans />,
83
- globals: { viewport: { value: "lg" } },
84
- };