@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.
- package/dist/component-registry.md +1310 -127
- package/dist/components/atoms/button/button.d.ts +55 -47
- package/dist/components/atoms/button/button.figma.d.ts +1 -0
- package/dist/components/atoms/input/input.d.ts +24 -24
- package/dist/components/atoms/popover/popover.d.ts +195 -0
- package/dist/components/atoms/select/select.d.ts +24 -24
- package/dist/components/atoms/tooltip/tooltip.d.ts +161 -0
- package/dist/components/organisms/card/card.d.ts +1 -1
- package/dist/components/sections/hero/hero.d.ts +2 -2
- package/dist/components/sections/tout/tout.d.ts +3 -3
- package/dist/components/shared/floating-arrow.d.ts +34 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +11602 -8499
- package/dist/index.js.map +1 -1
- package/dist/lib/form-control.d.ts +25 -24
- package/dist/tokens.css +4797 -3940
- 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/button/button.figma.tsx +37 -0
- package/src/components/atoms/button/button.stories.tsx +236 -140
- package/src/components/atoms/button/button.test.tsx +289 -5
- package/src/components/atoms/button/button.tsx +37 -33
- package/src/components/atoms/button/button.visual.test.tsx +26 -76
- 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 +3 -3
- package/src/components/atoms/input/input-group.stories.tsx +4 -8
- package/src/components/atoms/input/input-group.test.tsx +14 -28
- package/src/components/atoms/input/input-group.tsx +57 -32
- package/src/components/atoms/input/input.stories.tsx +14 -18
- package/src/components/atoms/input/input.test.tsx +4 -20
- package/src/components/atoms/input/input.tsx +16 -9
- 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/select.tsx +12 -8
- 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 -13
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +3 -3
- package/src/components/organisms/card/card.stories.tsx +19 -19
- package/src/components/organisms/card/card.tsx +1 -1
- package/src/components/organisms/card/card.visual.test.tsx +11 -11
- 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 +4 -4
- package/src/components/sections/hero/hero.stories.tsx +7 -7
- package/src/components/sections/hero/hero.tsx +10 -11
- package/src/components/sections/prose/prose.tsx +2 -2
- package/src/components/sections/river/river.test.tsx +3 -3
- package/src/components/sections/river/river.tsx +6 -12
- package/src/components/sections/tout/tout.stories.tsx +7 -31
- package/src/components/sections/tout/tout.tsx +9 -9
- 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 +57 -0
- package/src/lib/form-control.ts +8 -6
- package/src/stories/grid-system.stories.tsx +309 -0
- package/src/stories/{ThemeProvider.stories.tsx → theme-provider.stories.tsx} +7 -19
- package/src/stories/{TokenShowcase.stories.tsx → token-showcase.stories.tsx} +1 -1
- package/src/stories/{TokenShowcase.tsx → token-showcase.tsx} +34 -34
- package/src/styles.css +3 -3
- package/src/tests/token-resolution.test.tsx +6 -9
- 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/{Introduction.mdx → introduction.mdx} +0 -0
|
@@ -3,6 +3,28 @@ import { page, userEvent } from "vitest/browser";
|
|
|
3
3
|
import { render } from "vitest-browser-react";
|
|
4
4
|
import { Button } from "./button";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Helper to get computed styles of an element
|
|
8
|
+
*/
|
|
9
|
+
function getStyles(element: HTMLElement) {
|
|
10
|
+
return window.getComputedStyle(element);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Helper to convert rgb/rgba to hex for easier comparison
|
|
15
|
+
*/
|
|
16
|
+
function _rgbToHex(rgb: string): string {
|
|
17
|
+
// Handle rgba format
|
|
18
|
+
const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/);
|
|
19
|
+
if (!match) return rgb;
|
|
20
|
+
|
|
21
|
+
const r = parseInt(match[1], 10);
|
|
22
|
+
const g = parseInt(match[2], 10);
|
|
23
|
+
const b = parseInt(match[3], 10);
|
|
24
|
+
|
|
25
|
+
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
6
28
|
describe("Button", () => {
|
|
7
29
|
describe("Accessibility", () => {
|
|
8
30
|
test("has correct button role", async () => {
|
|
@@ -124,12 +146,274 @@ describe("Button", () => {
|
|
|
124
146
|
});
|
|
125
147
|
});
|
|
126
148
|
|
|
127
|
-
describe("
|
|
128
|
-
test("
|
|
129
|
-
render(<Button>Default</Button>);
|
|
149
|
+
describe("Variant Styles", () => {
|
|
150
|
+
test("default variant has dark background", async () => {
|
|
151
|
+
render(<Button variant="default">Default</Button>);
|
|
130
152
|
const button = page.getByRole("button", { name: "Default" });
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
await expect.element(button).toBeInTheDocument();
|
|
154
|
+
|
|
155
|
+
const element = button.element();
|
|
156
|
+
const styles = getStyles(element);
|
|
157
|
+
|
|
158
|
+
// Default variant should have a dark background (gray-1200)
|
|
159
|
+
// The actual color depends on CSS variables, but it should not be transparent
|
|
160
|
+
expect(styles.backgroundColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
161
|
+
expect(styles.backgroundColor).not.toBe("transparent");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("primary variant has blue background", async () => {
|
|
165
|
+
render(<Button variant="primary">Primary</Button>);
|
|
166
|
+
const button = page.getByRole("button", { name: "Primary" });
|
|
167
|
+
await expect.element(button).toBeInTheDocument();
|
|
168
|
+
|
|
169
|
+
const element = button.element();
|
|
170
|
+
const styles = getStyles(element);
|
|
171
|
+
|
|
172
|
+
// Primary variant should have a visible background
|
|
173
|
+
expect(styles.backgroundColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
174
|
+
expect(styles.backgroundColor).not.toBe("transparent");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("destructive variant has red-tinted background", async () => {
|
|
178
|
+
render(<Button variant="destructive">Delete</Button>);
|
|
179
|
+
const button = page.getByRole("button", { name: "Delete" });
|
|
180
|
+
await expect.element(button).toBeInTheDocument();
|
|
181
|
+
|
|
182
|
+
const element = button.element();
|
|
183
|
+
const styles = getStyles(element);
|
|
184
|
+
|
|
185
|
+
// Destructive variant should have a visible background
|
|
186
|
+
expect(styles.backgroundColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
187
|
+
expect(styles.backgroundColor).not.toBe("transparent");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("ghost variant has transparent background", async () => {
|
|
191
|
+
render(<Button variant="ghost">Ghost</Button>);
|
|
192
|
+
const button = page.getByRole("button", { name: "Ghost" });
|
|
193
|
+
await expect.element(button).toBeInTheDocument();
|
|
194
|
+
|
|
195
|
+
const element = button.element();
|
|
196
|
+
const styles = getStyles(element);
|
|
197
|
+
|
|
198
|
+
// Ghost variant should have transparent background
|
|
199
|
+
expect(
|
|
200
|
+
styles.backgroundColor === "rgba(0, 0, 0, 0)" ||
|
|
201
|
+
styles.backgroundColor === "transparent",
|
|
202
|
+
).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("link variant has no background and underline on hover behavior", async () => {
|
|
206
|
+
render(<Button variant="link">Link</Button>);
|
|
207
|
+
const button = page.getByRole("button", { name: "Link" });
|
|
208
|
+
await expect.element(button).toBeInTheDocument();
|
|
209
|
+
|
|
210
|
+
const element = button.element();
|
|
211
|
+
const styles = getStyles(element);
|
|
212
|
+
|
|
213
|
+
// Link variant should have transparent background
|
|
214
|
+
expect(
|
|
215
|
+
styles.backgroundColor === "rgba(0, 0, 0, 0)" ||
|
|
216
|
+
styles.backgroundColor === "transparent",
|
|
217
|
+
).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("outline variant has visible border", async () => {
|
|
221
|
+
render(<Button variant="outline">Outline</Button>);
|
|
222
|
+
const button = page.getByRole("button", { name: "Outline" });
|
|
223
|
+
await expect.element(button).toBeInTheDocument();
|
|
224
|
+
|
|
225
|
+
const element = button.element();
|
|
226
|
+
const styles = getStyles(element);
|
|
227
|
+
|
|
228
|
+
// Outline variant should have a visible border
|
|
229
|
+
const borderWidth = parseFloat(styles.borderWidth) || 0;
|
|
230
|
+
expect(borderWidth).toBeGreaterThan(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("secondary variant has light gray background with border", async () => {
|
|
234
|
+
render(<Button variant="secondary">Secondary</Button>);
|
|
235
|
+
const button = page.getByRole("button", { name: "Secondary" });
|
|
236
|
+
await expect.element(button).toBeInTheDocument();
|
|
237
|
+
|
|
238
|
+
const element = button.element();
|
|
239
|
+
const styles = getStyles(element);
|
|
240
|
+
|
|
241
|
+
// Secondary variant should have a visible background
|
|
242
|
+
expect(styles.backgroundColor).not.toBe("rgba(0, 0, 0, 0)");
|
|
243
|
+
expect(styles.backgroundColor).not.toBe("transparent");
|
|
244
|
+
|
|
245
|
+
// Secondary should also have a border
|
|
246
|
+
const borderWidth = parseFloat(styles.borderWidth) || 0;
|
|
247
|
+
expect(borderWidth).toBeGreaterThan(0);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("Size Styles", () => {
|
|
252
|
+
test("small size has 32px height", async () => {
|
|
253
|
+
render(<Button size="sm">Small</Button>);
|
|
254
|
+
const button = page.getByRole("button", { name: "Small" });
|
|
255
|
+
await expect.element(button).toBeInTheDocument();
|
|
256
|
+
|
|
257
|
+
const element = button.element();
|
|
258
|
+
const styles = getStyles(element);
|
|
259
|
+
|
|
260
|
+
// Small button should be 32px height
|
|
261
|
+
expect(styles.height).toBe("32px");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("default size has 36px height", async () => {
|
|
265
|
+
render(<Button size="default">Default</Button>);
|
|
266
|
+
const button = page.getByRole("button", { name: "Default" });
|
|
267
|
+
await expect.element(button).toBeInTheDocument();
|
|
268
|
+
|
|
269
|
+
const element = button.element();
|
|
270
|
+
const styles = getStyles(element);
|
|
271
|
+
|
|
272
|
+
// Default button should be 36px height
|
|
273
|
+
expect(styles.height).toBe("36px");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("large size has 40px height", async () => {
|
|
277
|
+
render(<Button size="lg">Large</Button>);
|
|
278
|
+
const button = page.getByRole("button", { name: "Large" });
|
|
279
|
+
await expect.element(button).toBeInTheDocument();
|
|
280
|
+
|
|
281
|
+
const element = button.element();
|
|
282
|
+
const styles = getStyles(element);
|
|
283
|
+
|
|
284
|
+
// Large button should be 40px height
|
|
285
|
+
expect(styles.height).toBe("40px");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("size affects padding appropriately", async () => {
|
|
289
|
+
render(
|
|
290
|
+
<>
|
|
291
|
+
<Button size="sm">Small</Button>
|
|
292
|
+
<Button size="lg">Large</Button>
|
|
293
|
+
</>,
|
|
294
|
+
);
|
|
295
|
+
const smallButton = page.getByRole("button", { name: "Small" });
|
|
296
|
+
const largeButton = page.getByRole("button", { name: "Large" });
|
|
297
|
+
|
|
298
|
+
const smallStyles = getStyles(smallButton.element());
|
|
299
|
+
const largeStyles = getStyles(largeButton.element());
|
|
300
|
+
|
|
301
|
+
const smallPaddingLeft = parseFloat(smallStyles.paddingLeft);
|
|
302
|
+
const largePaddingLeft = parseFloat(largeStyles.paddingLeft);
|
|
303
|
+
|
|
304
|
+
// Large should have more padding than small
|
|
305
|
+
expect(largePaddingLeft).toBeGreaterThan(smallPaddingLeft);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("size affects border radius appropriately", async () => {
|
|
309
|
+
render(
|
|
310
|
+
<>
|
|
311
|
+
<Button size="sm">Small</Button>
|
|
312
|
+
<Button size="lg">Large</Button>
|
|
313
|
+
</>,
|
|
314
|
+
);
|
|
315
|
+
const smallButton = page.getByRole("button", { name: "Small" });
|
|
316
|
+
const largeButton = page.getByRole("button", { name: "Large" });
|
|
317
|
+
|
|
318
|
+
const smallStyles = getStyles(smallButton.element());
|
|
319
|
+
const largeStyles = getStyles(largeButton.element());
|
|
320
|
+
|
|
321
|
+
const smallRadius = parseFloat(smallStyles.borderRadius);
|
|
322
|
+
const largeRadius = parseFloat(largeStyles.borderRadius);
|
|
323
|
+
|
|
324
|
+
// Large should have more border radius than small
|
|
325
|
+
// Figma: sm=4px, default=6px, lg=10px
|
|
326
|
+
expect(largeRadius).toBeGreaterThan(smallRadius);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("Disabled State Styles", () => {
|
|
331
|
+
test("disabled button has reduced opacity", async () => {
|
|
332
|
+
render(<Button disabled>Disabled</Button>);
|
|
333
|
+
const button = page.getByRole("button", { name: "Disabled" });
|
|
334
|
+
await expect.element(button).toBeInTheDocument();
|
|
335
|
+
|
|
336
|
+
const element = button.element();
|
|
337
|
+
const styles = getStyles(element);
|
|
338
|
+
|
|
339
|
+
// Disabled button should have opacity of 0.5
|
|
340
|
+
expect(parseFloat(styles.opacity)).toBe(0.5);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("disabled button has pointer-events none", async () => {
|
|
344
|
+
render(<Button disabled>Disabled</Button>);
|
|
345
|
+
const button = page.getByRole("button", { name: "Disabled" });
|
|
346
|
+
await expect.element(button).toBeInTheDocument();
|
|
347
|
+
|
|
348
|
+
const element = button.element();
|
|
349
|
+
const styles = getStyles(element);
|
|
350
|
+
|
|
351
|
+
expect(styles.pointerEvents).toBe("none");
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("Data Attributes", () => {
|
|
356
|
+
test("button has correct data-variant attribute", async () => {
|
|
357
|
+
render(<Button variant="destructive">Test</Button>);
|
|
358
|
+
const button = page.getByRole("button", { name: "Test" });
|
|
359
|
+
|
|
360
|
+
await expect
|
|
361
|
+
.element(button)
|
|
362
|
+
.toHaveAttribute("data-variant", "destructive");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("button has correct data-size attribute", async () => {
|
|
366
|
+
render(<Button size="lg">Test</Button>);
|
|
367
|
+
const button = page.getByRole("button", { name: "Test" });
|
|
368
|
+
|
|
369
|
+
await expect.element(button).toHaveAttribute("data-size", "lg");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("default variant and size are reflected in data attributes", async () => {
|
|
373
|
+
render(<Button>Test</Button>);
|
|
374
|
+
const button = page.getByRole("button", { name: "Test" });
|
|
375
|
+
|
|
376
|
+
await expect.element(button).toHaveAttribute("data-variant", "default");
|
|
377
|
+
await expect.element(button).toHaveAttribute("data-size", "default");
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe("Layout Styles", () => {
|
|
382
|
+
test("button uses flexbox for content alignment", async () => {
|
|
383
|
+
render(<Button>Test</Button>);
|
|
384
|
+
const button = page.getByRole("button", { name: "Test" });
|
|
385
|
+
await expect.element(button).toBeInTheDocument();
|
|
386
|
+
|
|
387
|
+
const element = button.element();
|
|
388
|
+
const styles = getStyles(element);
|
|
389
|
+
|
|
390
|
+
expect(styles.display).toBe("inline-flex");
|
|
391
|
+
expect(styles.alignItems).toBe("center");
|
|
392
|
+
expect(styles.justifyContent).toBe("center");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("button has cursor pointer", async () => {
|
|
396
|
+
render(<Button>Test</Button>);
|
|
397
|
+
const button = page.getByRole("button", { name: "Test" });
|
|
398
|
+
await expect.element(button).toBeInTheDocument();
|
|
399
|
+
|
|
400
|
+
const element = button.element();
|
|
401
|
+
const styles = getStyles(element);
|
|
402
|
+
|
|
403
|
+
expect(styles.cursor).toBe("pointer");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("button has gap for icon-text spacing", async () => {
|
|
407
|
+
render(<Button>Test</Button>);
|
|
408
|
+
const button = page.getByRole("button", { name: "Test" });
|
|
409
|
+
await expect.element(button).toBeInTheDocument();
|
|
410
|
+
|
|
411
|
+
const element = button.element();
|
|
412
|
+
const styles = getStyles(element);
|
|
413
|
+
|
|
414
|
+
// Gap should be set (6px from Figma)
|
|
415
|
+
const gap = parseFloat(styles.gap);
|
|
416
|
+
expect(gap).toBeGreaterThan(0);
|
|
133
417
|
});
|
|
134
418
|
});
|
|
135
419
|
});
|
|
@@ -12,17 +12,18 @@ import { type ButtonTheme, buttonThemeToStyleVars } from "../../../lib/theme";
|
|
|
12
12
|
* Button component based on Figma Button component
|
|
13
13
|
*
|
|
14
14
|
* Variants (matches Figma):
|
|
15
|
-
* - primary:
|
|
16
|
-
* -
|
|
17
|
-
* - secondary:
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
20
|
-
* - ghost
|
|
15
|
+
* - primary: Blue filled button for primary actions
|
|
16
|
+
* - default: Dark filled button for secondary prominence
|
|
17
|
+
* - secondary: Light gray filled button with subtle border
|
|
18
|
+
* - destructive: Red filled button for destructive actions
|
|
19
|
+
* - outline: Bordered button with transparent background
|
|
20
|
+
* - ghost: Transparent button with subtle hover
|
|
21
|
+
* - link: Text-only button with underline on hover
|
|
21
22
|
*
|
|
22
|
-
* Sizes:
|
|
23
|
-
* -
|
|
24
|
-
* -
|
|
25
|
-
* -
|
|
23
|
+
* Sizes (matches Figma):
|
|
24
|
+
* - sm: Small buttons (32px height)
|
|
25
|
+
* - default: Default buttons (36px height)
|
|
26
|
+
* - lg: Large buttons (40px height)
|
|
26
27
|
*
|
|
27
28
|
* For icon-only buttons, use the IconButton component instead.
|
|
28
29
|
*
|
|
@@ -30,40 +31,43 @@ import { type ButtonTheme, buttonThemeToStyleVars } from "../../../lib/theme";
|
|
|
30
31
|
* Pass a `theme` prop to override default colors via CSS custom properties.
|
|
31
32
|
*/
|
|
32
33
|
const buttonVariants = tv({
|
|
33
|
-
base: "inline-flex items-center justify-center gap-
|
|
34
|
+
base: "inline-flex items-center justify-center gap-spatial-ui-button-gap-icon-text whitespace-nowrap transition-colors duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50",
|
|
34
35
|
variants: {
|
|
35
36
|
variant: {
|
|
36
|
-
// Primary - filled
|
|
37
|
+
// Primary - blue filled button
|
|
37
38
|
primary:
|
|
38
|
-
"bg-button-primary-bg text-button-primary-text hover:bg-button-primary-bg-hover
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
"bg-button-
|
|
42
|
-
// Secondary -
|
|
39
|
+
"bg-button-primary-bg text-button-primary-text hover:bg-button-primary-bg-hover active:bg-button-primary-bg-active border-transparent",
|
|
40
|
+
// Default - dark filled button
|
|
41
|
+
default:
|
|
42
|
+
"bg-button-default-bg text-button-default-text hover:bg-button-default-bg-hover active:bg-button-default-bg-active border-transparent",
|
|
43
|
+
// Secondary - light gray filled with subtle border
|
|
43
44
|
secondary:
|
|
44
|
-
"bg-button-secondary-bg text-button-secondary-text hover:bg-button-secondary-bg-hover
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
"bg-button-
|
|
48
|
-
//
|
|
45
|
+
"bg-button-secondary-bg text-button-secondary-text hover:bg-button-secondary-bg-hover active:bg-button-secondary-bg-active border border-button-secondary-border",
|
|
46
|
+
// Destructive - red filled button
|
|
47
|
+
destructive:
|
|
48
|
+
"bg-button-destructive-bg text-button-destructive-text hover:bg-button-destructive-bg-hover active:bg-button-destructive-bg-active border-transparent",
|
|
49
|
+
// Outline - bordered with transparent background
|
|
50
|
+
outline:
|
|
51
|
+
"bg-button-outline-bg text-button-outline-text hover:bg-button-outline-bg-hover active:bg-button-outline-bg-active border border-button-outline-border hover:border-button-outline-border-hover",
|
|
52
|
+
// Ghost - transparent with subtle hover
|
|
49
53
|
ghost:
|
|
50
|
-
"bg-button-ghost-bg text-button-ghost-text hover:bg-button-ghost-bg-hover
|
|
51
|
-
//
|
|
52
|
-
"
|
|
53
|
-
"bg-button-ghost-inverse-bg text-button-ghost-inverse-text hover:bg-button-ghost-inverse-bg-hover hover:text-button-ghost-inverse-text-hover border-transparent focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
|
|
54
|
+
"bg-button-ghost-bg text-button-ghost-text hover:bg-button-ghost-bg-hover active:bg-button-ghost-bg-active border-transparent",
|
|
55
|
+
// Link - text only with underline on hover
|
|
56
|
+
link: "bg-transparent text-button-link-text hover:text-button-link-text-hover hover:underline active:text-button-link-text-hover border-transparent underline-offset-4",
|
|
54
57
|
// Themed - uses CSS custom properties for styling
|
|
55
58
|
themed:
|
|
56
59
|
"[background:var(--btn-bg)] [color:var(--btn-text)] [border-color:var(--btn-border-color,transparent)] hover:[background:var(--btn-bg-hover,var(--btn-bg))] active:[background:var(--btn-bg-active,var(--btn-bg-hover,var(--btn-bg)))]",
|
|
57
60
|
},
|
|
58
61
|
size: {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
sm: "typography-ui-text-xs h-spatial-ui-button-height-small px-spatial-ui-button-padding-x-small py-spatial-ui-button-padding-y-small rounded-surface-button-small",
|
|
63
|
+
default:
|
|
64
|
+
"typography-ui-text-sm h-spatial-ui-button-height-medium px-spatial-ui-button-padding-x-medium py-spatial-ui-button-padding-y-medium rounded-surface-button-medium",
|
|
65
|
+
lg: "typography-ui-text-md h-spatial-ui-button-height-large px-spatial-ui-button-padding-x-large py-spatial-ui-button-padding-y-large rounded-surface-button-large",
|
|
62
66
|
},
|
|
63
67
|
},
|
|
64
68
|
defaultVariants: {
|
|
65
|
-
variant: "
|
|
66
|
-
size: "
|
|
69
|
+
variant: "default",
|
|
70
|
+
size: "default",
|
|
67
71
|
},
|
|
68
72
|
});
|
|
69
73
|
|
|
@@ -100,8 +104,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
100
104
|
const combinedStyles = hasTheme ? { ...themeStyles, ...style } : style;
|
|
101
105
|
|
|
102
106
|
// Resolve actual values for data attributes
|
|
103
|
-
const resolvedVariant = effectiveVariant ?? "
|
|
104
|
-
const resolvedSize = size ?? "
|
|
107
|
+
const resolvedVariant = effectiveVariant ?? "default";
|
|
108
|
+
const resolvedSize = size ?? "default";
|
|
105
109
|
|
|
106
110
|
return (
|
|
107
111
|
<BaseButton
|
|
@@ -4,115 +4,66 @@ import { page } from "vitest/browser";
|
|
|
4
4
|
import { Button } from "./button";
|
|
5
5
|
|
|
6
6
|
describe("Button Visual Regression", () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
render(
|
|
10
|
-
<Button variant="solid" colorScheme="dark">
|
|
11
|
-
Solid Dark Button
|
|
12
|
-
</Button>,
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
await expect(
|
|
16
|
-
page.getByRole("button", { name: "Solid Dark Button" }),
|
|
17
|
-
).toMatchScreenshot("button-solid-dark");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test("solid light variant renders correctly", async () => {
|
|
21
|
-
render(
|
|
22
|
-
<div style={{ background: "#1a1a1a", padding: "20px" }}>
|
|
23
|
-
<Button variant="solid" colorScheme="light">
|
|
24
|
-
Solid Light Button
|
|
25
|
-
</Button>
|
|
26
|
-
</div>,
|
|
27
|
-
);
|
|
7
|
+
test("primary variant renders correctly", async () => {
|
|
8
|
+
render(<Button variant="primary">Primary Button</Button>);
|
|
28
9
|
|
|
29
10
|
await expect(
|
|
30
|
-
page.getByRole("button", { name: "
|
|
31
|
-
).toMatchScreenshot("button-
|
|
11
|
+
page.getByRole("button", { name: "Primary Button" }),
|
|
12
|
+
).toMatchScreenshot("button-primary");
|
|
32
13
|
});
|
|
33
14
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
render(
|
|
37
|
-
<Button variant="outline" colorScheme="dark">
|
|
38
|
-
Outline Dark Button
|
|
39
|
-
</Button>,
|
|
40
|
-
);
|
|
15
|
+
test("primary-outline variant renders correctly", async () => {
|
|
16
|
+
render(<Button variant="primary-outline">Primary Outline Button</Button>);
|
|
41
17
|
|
|
42
18
|
await expect(
|
|
43
|
-
page.getByRole("button", { name: "Outline
|
|
44
|
-
).toMatchScreenshot("button-outline
|
|
19
|
+
page.getByRole("button", { name: "Primary Outline Button" }),
|
|
20
|
+
).toMatchScreenshot("button-primary-outline");
|
|
45
21
|
});
|
|
46
22
|
|
|
47
|
-
test("
|
|
23
|
+
test("secondary variant renders correctly", async () => {
|
|
48
24
|
render(
|
|
49
25
|
<div style={{ background: "#1a1a1a", padding: "20px" }}>
|
|
50
|
-
<Button variant="
|
|
51
|
-
Outline Light Button
|
|
52
|
-
</Button>
|
|
26
|
+
<Button variant="secondary">Secondary Button</Button>
|
|
53
27
|
</div>,
|
|
54
28
|
);
|
|
55
29
|
|
|
56
30
|
await expect(
|
|
57
|
-
page.getByRole("button", { name: "
|
|
58
|
-
).toMatchScreenshot("button-
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// Ghost variants
|
|
62
|
-
test("ghost dark variant renders correctly", async () => {
|
|
63
|
-
render(
|
|
64
|
-
<Button variant="ghost" colorScheme="dark">
|
|
65
|
-
Ghost Dark Button
|
|
66
|
-
</Button>,
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
await expect(
|
|
70
|
-
page.getByRole("button", { name: "Ghost Dark Button" }),
|
|
71
|
-
).toMatchScreenshot("button-ghost-dark");
|
|
31
|
+
page.getByRole("button", { name: "Secondary Button" }),
|
|
32
|
+
).toMatchScreenshot("button-secondary");
|
|
72
33
|
});
|
|
73
34
|
|
|
74
|
-
test("
|
|
35
|
+
test("secondary-outline variant renders correctly", async () => {
|
|
75
36
|
render(
|
|
76
37
|
<div style={{ background: "#1a1a1a", padding: "20px" }}>
|
|
77
|
-
<Button variant="
|
|
78
|
-
Ghost Light Button
|
|
79
|
-
</Button>
|
|
38
|
+
<Button variant="secondary-outline">Secondary Outline Button</Button>
|
|
80
39
|
</div>,
|
|
81
40
|
);
|
|
82
41
|
|
|
83
42
|
await expect(
|
|
84
|
-
page.getByRole("button", { name: "
|
|
85
|
-
).toMatchScreenshot("button-
|
|
43
|
+
page.getByRole("button", { name: "Secondary Outline Button" }),
|
|
44
|
+
).toMatchScreenshot("button-secondary-outline");
|
|
86
45
|
});
|
|
87
46
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
render(
|
|
91
|
-
<Button variant="subtle" colorScheme="dark">
|
|
92
|
-
Subtle Dark Button
|
|
93
|
-
</Button>,
|
|
94
|
-
);
|
|
47
|
+
test("ghost variant renders correctly", async () => {
|
|
48
|
+
render(<Button variant="ghost">Ghost Button</Button>);
|
|
95
49
|
|
|
96
50
|
await expect(
|
|
97
|
-
page.getByRole("button", { name: "
|
|
98
|
-
).toMatchScreenshot("button-
|
|
51
|
+
page.getByRole("button", { name: "Ghost Button" }),
|
|
52
|
+
).toMatchScreenshot("button-ghost");
|
|
99
53
|
});
|
|
100
54
|
|
|
101
|
-
test("
|
|
55
|
+
test("ghost-inverse variant renders correctly", async () => {
|
|
102
56
|
render(
|
|
103
57
|
<div style={{ background: "#1a1a1a", padding: "20px" }}>
|
|
104
|
-
<Button variant="
|
|
105
|
-
Subtle Light Button
|
|
106
|
-
</Button>
|
|
58
|
+
<Button variant="ghost-inverse">Ghost Inverse Button</Button>
|
|
107
59
|
</div>,
|
|
108
60
|
);
|
|
109
61
|
|
|
110
62
|
await expect(
|
|
111
|
-
page.getByRole("button", { name: "
|
|
112
|
-
).toMatchScreenshot("button-
|
|
63
|
+
page.getByRole("button", { name: "Ghost Inverse Button" }),
|
|
64
|
+
).toMatchScreenshot("button-ghost-inverse");
|
|
113
65
|
});
|
|
114
66
|
|
|
115
|
-
// Size variants
|
|
116
67
|
test("small size renders correctly", async () => {
|
|
117
68
|
render(<Button size="sm">Small Button</Button>);
|
|
118
69
|
|
|
@@ -121,8 +72,8 @@ describe("Button Visual Regression", () => {
|
|
|
121
72
|
).toMatchScreenshot("button-size-small");
|
|
122
73
|
});
|
|
123
74
|
|
|
124
|
-
test("medium
|
|
125
|
-
render(<Button size="
|
|
75
|
+
test("medium size renders correctly", async () => {
|
|
76
|
+
render(<Button size="md">Medium Button</Button>);
|
|
126
77
|
|
|
127
78
|
await expect(
|
|
128
79
|
page.getByRole("button", { name: "Medium Button" }),
|
|
@@ -137,7 +88,6 @@ describe("Button Visual Regression", () => {
|
|
|
137
88
|
).toMatchScreenshot("button-size-large");
|
|
138
89
|
});
|
|
139
90
|
|
|
140
|
-
// Disabled state
|
|
141
91
|
test("disabled state renders correctly", async () => {
|
|
142
92
|
render(<Button disabled>Disabled Button</Button>);
|
|
143
93
|
|