@nationaldesignstudio/react 0.2.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.
- package/dist/component-registry.md +1310 -127
- package/dist/components/atoms/background/background.d.ts +13 -27
- package/dist/components/atoms/button/button.d.ts +64 -72
- package/dist/components/atoms/button/button.figma.d.ts +1 -0
- package/dist/components/atoms/button/icon-button.d.ts +62 -110
- package/dist/components/atoms/input/input-group.d.ts +278 -0
- package/dist/components/atoms/input/input.d.ts +121 -0
- package/dist/components/atoms/popover/popover.d.ts +195 -0
- package/dist/components/atoms/select/select.d.ts +131 -0
- package/dist/components/atoms/tooltip/tooltip.d.ts +161 -0
- package/dist/components/organisms/card/card.d.ts +3 -3
- package/dist/components/sections/hero/hero.d.ts +2 -2
- package/dist/components/sections/prose/prose.d.ts +3 -3
- package/dist/components/sections/river/river.d.ts +1 -1
- package/dist/components/sections/tout/tout.d.ts +4 -4
- package/dist/components/shared/floating-arrow.d.ts +34 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +13935 -7622
- package/dist/index.js.map +1 -1
- package/dist/lib/form-control.d.ts +106 -0
- package/dist/tokens.css +4725 -19065
- package/package.json +2 -1
- package/src/components/atoms/accordion/accordion.stories.tsx +1 -1
- package/src/components/atoms/accordion/accordion.tsx +2 -2
- package/src/components/atoms/background/background.tsx +71 -109
- package/src/components/atoms/button/button.figma.tsx +37 -0
- package/src/components/atoms/button/button.stories.tsx +253 -115
- package/src/components/atoms/button/button.test.tsx +289 -5
- package/src/components/atoms/button/button.tsx +40 -101
- package/src/components/atoms/button/button.visual.test.tsx +28 -32
- package/src/components/atoms/button/icon-button.stories.tsx +44 -101
- package/src/components/atoms/button/icon-button.test.tsx +26 -94
- package/src/components/atoms/button/icon-button.tsx +81 -224
- package/src/components/atoms/input/index.ts +17 -0
- package/src/components/atoms/input/input-group.stories.tsx +646 -0
- package/src/components/atoms/input/input-group.test.tsx +362 -0
- package/src/components/atoms/input/input-group.tsx +409 -0
- package/src/components/atoms/input/input.stories.tsx +228 -0
- package/src/components/atoms/input/input.test.tsx +167 -0
- package/src/components/atoms/input/input.tsx +104 -0
- package/src/components/atoms/pager-control/pager-control.stories.tsx +6 -8
- package/src/components/atoms/pager-control/pager-control.tsx +12 -12
- package/src/components/atoms/popover/index.ts +30 -0
- package/src/components/atoms/popover/popover.stories.tsx +531 -0
- package/src/components/atoms/popover/popover.test.tsx +486 -0
- package/src/components/atoms/popover/popover.tsx +488 -0
- package/src/components/atoms/select/index.ts +18 -0
- package/src/components/atoms/select/select.stories.tsx +455 -0
- package/src/components/atoms/select/select.tsx +324 -0
- package/src/components/atoms/tooltip/index.ts +24 -0
- package/src/components/atoms/tooltip/tooltip.stories.tsx +348 -0
- package/src/components/atoms/tooltip/tooltip.test.tsx +363 -0
- package/src/components/atoms/tooltip/tooltip.tsx +347 -0
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +8 -17
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +3 -3
- package/src/components/foundation/typography/typography.stories.tsx +401 -0
- package/src/components/organisms/card/card.stories.tsx +19 -19
- package/src/components/organisms/card/card.test.tsx +1 -1
- package/src/components/organisms/card/card.tsx +3 -3
- package/src/components/organisms/card/card.visual.test.tsx +11 -11
- package/src/components/organisms/navbar/navbar.tsx +2 -2
- package/src/components/organisms/navbar/navbar.visual.test.tsx +2 -2
- package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +2 -2
- package/src/components/sections/banner/banner.stories.tsx +1 -5
- package/src/components/sections/banner/banner.test.tsx +2 -2
- package/src/components/sections/banner/banner.tsx +6 -6
- package/src/components/sections/card-grid/card-grid.tsx +5 -5
- package/src/components/sections/faq-section/faq-section.tsx +2 -2
- package/src/components/sections/hero/hero.stories.tsx +7 -7
- package/src/components/sections/hero/hero.test.tsx +5 -5
- package/src/components/sections/hero/hero.tsx +10 -11
- package/src/components/sections/prose/prose.test.tsx +2 -2
- package/src/components/sections/prose/prose.tsx +6 -7
- package/src/components/sections/river/river.stories.tsx +8 -8
- package/src/components/sections/river/river.test.tsx +4 -4
- package/src/components/sections/river/river.tsx +8 -16
- package/src/components/sections/tout/tout.stories.tsx +7 -31
- package/src/components/sections/tout/tout.test.tsx +1 -1
- package/src/components/sections/tout/tout.tsx +11 -11
- package/src/components/sections/two-column-section/two-column-section.tsx +7 -9
- package/src/components/shared/floating-arrow.tsx +78 -0
- package/src/components/shared/index.ts +5 -0
- package/src/index.ts +98 -0
- package/src/lib/form-control.ts +71 -0
- package/src/stories/grid-system.stories.tsx +309 -0
- package/src/stories/{Introduction.mdx → introduction.mdx} +29 -15
- package/src/stories/{ThemeProvider.stories.tsx → theme-provider.stories.tsx} +8 -22
- package/src/stories/{TokenShowcase.stories.tsx → token-showcase.stories.tsx} +1 -20
- package/src/stories/token-showcase.tsx +777 -0
- package/src/styles.css +3 -0
- package/src/tests/token-resolution.test.tsx +298 -0
- package/src/theme/hooks.ts +1 -1
- package/src/theme/index.ts +1 -1
- package/src/theme/theme-provider.test.tsx +270 -0
- package/src/theme/{ThemeProvider.tsx → theme-provider.tsx} +18 -2
- package/src/stories/GridSystem.stories.tsx +0 -84
- package/src/stories/TokenShowcase.tsx +0 -1429
package/src/styles.css
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/* Google Fonts - must be first to comply with CSS @import ordering rules */
|
|
2
|
+
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Inter+Tight:wght@100..900&display=swap");
|
|
3
|
+
|
|
1
4
|
@import "tailwindcss";
|
|
2
5
|
@import "../../tailwind-token-generator/dist/tokens.css";
|
|
3
6
|
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Resolution Tests
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that CSS classes using design tokens resolve to the correct
|
|
5
|
+
* computed styles. This ensures that:
|
|
6
|
+
* 1. Semantic color tokens resolve to actual colors
|
|
7
|
+
* 2. Typography tokens apply correct font properties
|
|
8
|
+
* 3. Spacing tokens resolve to correct pixel values
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test } from "vitest";
|
|
12
|
+
import { page } from "vitest/browser";
|
|
13
|
+
import { render } from "vitest-browser-react";
|
|
14
|
+
import { Button } from "../components/atoms/button/button";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Helper to get computed style of an element
|
|
18
|
+
*/
|
|
19
|
+
function getComputedStyleValue(element: Element, property: string): string {
|
|
20
|
+
return window.getComputedStyle(element).getPropertyValue(property);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("Token Resolution", () => {
|
|
24
|
+
describe("Semantic Color Tokens", () => {
|
|
25
|
+
test("Button primary variant uses semantic bg token", async () => {
|
|
26
|
+
render(<Button data-testid="btn">Primary Button</Button>);
|
|
27
|
+
const button = page.getByTestId("btn");
|
|
28
|
+
await expect.element(button).toBeInTheDocument();
|
|
29
|
+
|
|
30
|
+
const element = button.element();
|
|
31
|
+
const bgColor = getComputedStyleValue(element, "background-color");
|
|
32
|
+
|
|
33
|
+
// The button should have a non-transparent background
|
|
34
|
+
expect(bgColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
35
|
+
expect(bgColor).not.toBe("transparent");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("Button text color resolves from semantic token", async () => {
|
|
39
|
+
render(<Button data-testid="btn">Text Color</Button>);
|
|
40
|
+
const button = page.getByTestId("btn");
|
|
41
|
+
await expect.element(button).toBeInTheDocument();
|
|
42
|
+
|
|
43
|
+
const element = button.element();
|
|
44
|
+
const textColor = getComputedStyleValue(element, "color");
|
|
45
|
+
|
|
46
|
+
// Should have a defined text color
|
|
47
|
+
expect(textColor).toBeTruthy();
|
|
48
|
+
expect(textColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("Background page token resolves correctly", async () => {
|
|
52
|
+
render(
|
|
53
|
+
<div data-testid="bg-test" className="bg-bg-page p-16">
|
|
54
|
+
Background Test
|
|
55
|
+
</div>,
|
|
56
|
+
);
|
|
57
|
+
const element = page.getByTestId("bg-test").element();
|
|
58
|
+
const bgColor = getComputedStyleValue(element, "background-color");
|
|
59
|
+
|
|
60
|
+
// bg-page should resolve to a non-transparent color
|
|
61
|
+
expect(bgColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("Text primary token resolves correctly", async () => {
|
|
65
|
+
render(
|
|
66
|
+
<p data-testid="text-test" className="text-text-primary">
|
|
67
|
+
Primary Text
|
|
68
|
+
</p>,
|
|
69
|
+
);
|
|
70
|
+
const element = page.getByTestId("text-test").element();
|
|
71
|
+
const textColor = getComputedStyleValue(element, "color");
|
|
72
|
+
|
|
73
|
+
// text-primary should be a dark color
|
|
74
|
+
expect(textColor).toBeTruthy();
|
|
75
|
+
expect(textColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("Primitive Color Tokens", () => {
|
|
80
|
+
test("Gray scale colors resolve", async () => {
|
|
81
|
+
render(
|
|
82
|
+
<div>
|
|
83
|
+
<div data-testid="gray-100" className="bg-gray-100">
|
|
84
|
+
Gray 100
|
|
85
|
+
</div>
|
|
86
|
+
<div data-testid="gray-500" className="bg-gray-500">
|
|
87
|
+
Gray 500
|
|
88
|
+
</div>
|
|
89
|
+
<div data-testid="gray-900" className="bg-gray-900">
|
|
90
|
+
Gray 900
|
|
91
|
+
</div>
|
|
92
|
+
</div>,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const gray100 = page.getByTestId("gray-100").element();
|
|
96
|
+
const gray500 = page.getByTestId("gray-500").element();
|
|
97
|
+
const gray900 = page.getByTestId("gray-900").element();
|
|
98
|
+
|
|
99
|
+
const bg100 = getComputedStyleValue(gray100, "background-color");
|
|
100
|
+
const bg500 = getComputedStyleValue(gray500, "background-color");
|
|
101
|
+
const bg900 = getComputedStyleValue(gray900, "background-color");
|
|
102
|
+
|
|
103
|
+
// All should have resolved background colors
|
|
104
|
+
expect(bg100).not.toBe("rgba(0, 0, 0, 0)");
|
|
105
|
+
expect(bg500).not.toBe("rgba(0, 0, 0, 0)");
|
|
106
|
+
expect(bg900).not.toBe("rgba(0, 0, 0, 0)");
|
|
107
|
+
|
|
108
|
+
// Gray 100 should be lighter than Gray 900
|
|
109
|
+
// (comparing luminance would be ideal, but we'll just verify they're different)
|
|
110
|
+
expect(bg100).not.toBe(bg900);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("Indigo accent colors resolve", async () => {
|
|
114
|
+
render(
|
|
115
|
+
<div data-testid="indigo" className="bg-indigo-600">
|
|
116
|
+
Indigo 600
|
|
117
|
+
</div>,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const element = page.getByTestId("indigo").element();
|
|
121
|
+
const bgColor = getComputedStyleValue(element, "background-color");
|
|
122
|
+
|
|
123
|
+
// Should resolve to a blue-ish color (indigo)
|
|
124
|
+
expect(bgColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("Typography Tokens", () => {
|
|
129
|
+
test("Typography h1 class applies correct styles", async () => {
|
|
130
|
+
render(
|
|
131
|
+
<h1 data-testid="h1" className="typography-h1">
|
|
132
|
+
Heading 1
|
|
133
|
+
</h1>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const element = page.getByTestId("h1").element();
|
|
137
|
+
const fontSize = getComputedStyleValue(element, "font-size");
|
|
138
|
+
const fontFamily = getComputedStyleValue(element, "font-family");
|
|
139
|
+
|
|
140
|
+
const sizeNum = Number.parseFloat(fontSize);
|
|
141
|
+
expect(sizeNum).toBeGreaterThan(0);
|
|
142
|
+
|
|
143
|
+
expect(fontFamily).toBeTruthy();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("Typography body-medium class applies correct styles", async () => {
|
|
147
|
+
render(
|
|
148
|
+
<p data-testid="body" className="typography-body-medium">
|
|
149
|
+
Body text
|
|
150
|
+
</p>,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const element = page.getByTestId("body").element();
|
|
154
|
+
const fontSize = getComputedStyleValue(element, "font-size");
|
|
155
|
+
const lineHeight = getComputedStyleValue(element, "line-height");
|
|
156
|
+
|
|
157
|
+
// Body medium should be around 14-18px depending on breakpoint
|
|
158
|
+
const sizeNum = Number.parseFloat(fontSize);
|
|
159
|
+
expect(sizeNum).toBeGreaterThanOrEqual(14);
|
|
160
|
+
expect(sizeNum).toBeLessThanOrEqual(18);
|
|
161
|
+
|
|
162
|
+
// Should have a line height set
|
|
163
|
+
expect(lineHeight).toBeTruthy();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("Typography button class applies correct weight", async () => {
|
|
167
|
+
render(
|
|
168
|
+
<span data-testid="btn-text" className="typography-ui-button-medium">
|
|
169
|
+
Button Text
|
|
170
|
+
</span>,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const element = page.getByTestId("btn-text").element();
|
|
174
|
+
const fontWeight = getComputedStyleValue(element, "font-weight");
|
|
175
|
+
|
|
176
|
+
const weightNum = Number.parseInt(fontWeight, 10);
|
|
177
|
+
expect(weightNum).toBeGreaterThan(0);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("Spacing Tokens", () => {
|
|
182
|
+
test("Padding tokens resolve to correct pixel values", async () => {
|
|
183
|
+
render(
|
|
184
|
+
<div data-testid="padded" className="p-16">
|
|
185
|
+
Padded content
|
|
186
|
+
</div>,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const element = page.getByTestId("padded").element();
|
|
190
|
+
const padding = getComputedStyleValue(element, "padding");
|
|
191
|
+
|
|
192
|
+
// p-16 should resolve to 16px on all sides
|
|
193
|
+
expect(padding).toBe("16px");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("Margin tokens resolve correctly", async () => {
|
|
197
|
+
render(
|
|
198
|
+
<div data-testid="margin" className="m-24">
|
|
199
|
+
Margin content
|
|
200
|
+
</div>,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const element = page.getByTestId("margin").element();
|
|
204
|
+
const margin = getComputedStyleValue(element, "margin");
|
|
205
|
+
|
|
206
|
+
// m-24 should resolve to 24px on all sides
|
|
207
|
+
expect(margin).toBe("24px");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("Gap tokens resolve correctly", async () => {
|
|
211
|
+
render(
|
|
212
|
+
<div data-testid="gap" className="flex gap-8">
|
|
213
|
+
<span>Item 1</span>
|
|
214
|
+
<span>Item 2</span>
|
|
215
|
+
</div>,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const element = page.getByTestId("gap").element();
|
|
219
|
+
const gap = getComputedStyleValue(element, "gap");
|
|
220
|
+
|
|
221
|
+
// gap-8 should resolve to 8px
|
|
222
|
+
expect(gap).toBe("8px");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("Width spacing tokens resolve", async () => {
|
|
226
|
+
render(
|
|
227
|
+
<div data-testid="width" className="w-64 h-8 bg-gray-500">
|
|
228
|
+
Width test
|
|
229
|
+
</div>,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const element = page.getByTestId("width").element();
|
|
233
|
+
const width = getComputedStyleValue(element, "width");
|
|
234
|
+
|
|
235
|
+
// w-64 should be 64px
|
|
236
|
+
expect(width).toBe("64px");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("Border Radius Tokens", () => {
|
|
241
|
+
test("Radius tokens resolve correctly", async () => {
|
|
242
|
+
render(
|
|
243
|
+
<div data-testid="rounded" className="rounded-8 bg-gray-200 p-16">
|
|
244
|
+
Rounded corners
|
|
245
|
+
</div>,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const element = page.getByTestId("rounded").element();
|
|
249
|
+
const radius = getComputedStyleValue(element, "border-radius");
|
|
250
|
+
|
|
251
|
+
// rounded-8 should be 8px
|
|
252
|
+
expect(radius).toBe("8px");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("Component Token Integration", () => {
|
|
257
|
+
test("Button has defined height from spacing tokens", async () => {
|
|
258
|
+
render(
|
|
259
|
+
<Button data-testid="btn" size="md">
|
|
260
|
+
Medium Button
|
|
261
|
+
</Button>,
|
|
262
|
+
);
|
|
263
|
+
const button = page.getByTestId("btn");
|
|
264
|
+
await expect.element(button).toBeInTheDocument();
|
|
265
|
+
|
|
266
|
+
const element = button.element();
|
|
267
|
+
const height = getComputedStyleValue(element, "height");
|
|
268
|
+
|
|
269
|
+
// Button should have a defined height (not auto)
|
|
270
|
+
const heightNum = Number.parseFloat(height);
|
|
271
|
+
expect(heightNum).toBeGreaterThan(0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("Button sizes use different padding tokens", async () => {
|
|
275
|
+
render(
|
|
276
|
+
<div>
|
|
277
|
+
<Button data-testid="btn-sm" size="sm">
|
|
278
|
+
Small
|
|
279
|
+
</Button>
|
|
280
|
+
<Button data-testid="btn-lg" size="lg">
|
|
281
|
+
Large
|
|
282
|
+
</Button>
|
|
283
|
+
</div>,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const smallBtn = page.getByTestId("btn-sm").element();
|
|
287
|
+
const largeBtn = page.getByTestId("btn-lg").element();
|
|
288
|
+
|
|
289
|
+
const smallPadding = getComputedStyleValue(smallBtn, "padding-left");
|
|
290
|
+
const largePadding = getComputedStyleValue(largeBtn, "padding-left");
|
|
291
|
+
|
|
292
|
+
// Large should have more padding than small
|
|
293
|
+
const smallPaddingNum = Number.parseFloat(smallPadding);
|
|
294
|
+
const largePaddingNum = Number.parseFloat(largePadding);
|
|
295
|
+
expect(largePaddingNum).toBeGreaterThan(smallPaddingNum);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
package/src/theme/hooks.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
NestedStringRecord,
|
|
10
10
|
} from "@nds-design-system/tokens";
|
|
11
11
|
import { useContext } from "react";
|
|
12
|
-
import { ThemeContext, type ThemeContextValue } from "./
|
|
12
|
+
import { ThemeContext, type ThemeContextValue } from "./theme-provider";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Hook to access the theme context
|
package/src/theme/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
{
|
|
304
|
+
{applyStyles ? (
|
|
305
|
+
<div style={cssVars} className={className}>
|
|
306
|
+
{children}
|
|
307
|
+
</div>
|
|
308
|
+
) : (
|
|
309
|
+
children
|
|
310
|
+
)}
|
|
295
311
|
</ThemeContext.Provider>
|
|
296
312
|
);
|
|
297
313
|
}
|