@nationaldesignstudio/react 0.0.10 → 0.0.11

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 (169) hide show
  1. package/dist/component-registry.md +2405 -0
  2. package/dist/components/atoms/accordion/accordion.d.ts +44 -3
  3. package/dist/components/atoms/button/button.d.ts +155 -11
  4. package/dist/components/atoms/button/icon-button.d.ts +114 -5
  5. package/dist/components/atoms/ndstudio-footer/ndstudio-footer.d.ts +30 -0
  6. package/dist/components/atoms/pager-control/pager-control.d.ts +116 -9
  7. package/dist/components/dev-tools/dev-toolbar/dev-toolbar.d.ts +4 -0
  8. package/dist/components/dev-tools/grid-overlay/grid-overlay.d.ts +6 -0
  9. package/dist/components/organisms/card/card.d.ts +40 -4
  10. package/dist/components/sections/banner/banner.d.ts +39 -6
  11. package/dist/components/sections/card-grid/card-grid.d.ts +37 -4
  12. package/dist/components/sections/faq-section/faq-section.d.ts +2 -2
  13. package/dist/components/sections/hero/hero.d.ts +167 -16
  14. package/dist/components/sections/river/river.d.ts +37 -4
  15. package/dist/components/sections/tout/tout.d.ts +86 -6
  16. package/dist/components/sections/two-column-section/two-column-section.d.ts +80 -6
  17. package/dist/hooks/index.d.ts +1 -0
  18. package/dist/hooks/use-event-listener.d.ts +24 -0
  19. package/dist/index.d.ts +9 -2
  20. package/dist/index.js +12034 -5934
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/theme.d.ts +330 -0
  23. package/dist/tokens.css +13650 -6129
  24. package/package.json +11 -21
  25. package/src/App.css +0 -0
  26. package/src/App.tsx +0 -7
  27. package/src/assets/fonts/PPNeueMontreal-Variable.woff2 +0 -0
  28. package/src/assets/react.svg +0 -1
  29. package/src/components/atoms/accordion/accordion.stories.tsx +0 -228
  30. package/src/components/atoms/accordion/accordion.tsx +0 -137
  31. package/src/components/atoms/accordion/index.ts +0 -6
  32. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-darwin.png +0 -0
  33. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-linux.png +0 -0
  34. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-darwin.png +0 -0
  35. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-linux.png +0 -0
  36. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-darwin.png +0 -0
  37. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-linux.png +0 -0
  38. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-darwin.png +0 -0
  39. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-linux.png +0 -0
  40. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-darwin.png +0 -0
  41. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-linux.png +0 -0
  42. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-darwin.png +0 -0
  43. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-linux.png +0 -0
  44. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-darwin.png +0 -0
  45. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-linux.png +0 -0
  46. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-darwin.png +0 -0
  47. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-linux.png +0 -0
  48. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-darwin.png +0 -0
  49. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-linux.png +0 -0
  50. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-darwin.png +0 -0
  51. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-linux.png +0 -0
  52. package/src/components/atoms/button/button.stories.tsx +0 -84
  53. package/src/components/atoms/button/button.test.tsx +0 -141
  54. package/src/components/atoms/button/button.tsx +0 -95
  55. package/src/components/atoms/button/button.visual.test.tsx +0 -102
  56. package/src/components/atoms/button/icon-button.stories.tsx +0 -166
  57. package/src/components/atoms/button/icon-button.tsx +0 -125
  58. package/src/components/atoms/button/index.ts +0 -6
  59. package/src/components/atoms/pager-control/index.ts +0 -5
  60. package/src/components/atoms/pager-control/pager-control.stories.tsx +0 -209
  61. package/src/components/atoms/pager-control/pager-control.test.tsx +0 -149
  62. package/src/components/atoms/pager-control/pager-control.tsx +0 -328
  63. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-darwin.png +0 -0
  64. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-linux.png +0 -0
  65. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-darwin.png +0 -0
  66. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-linux.png +0 -0
  67. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-darwin.png +0 -0
  68. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-linux.png +0 -0
  69. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-darwin.png +0 -0
  70. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-linux.png +0 -0
  71. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-darwin.png +0 -0
  72. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-linux.png +0 -0
  73. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-darwin.png +0 -0
  74. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-linux.png +0 -0
  75. package/src/components/organisms/card/card.stories.tsx +0 -293
  76. package/src/components/organisms/card/card.test.tsx +0 -245
  77. package/src/components/organisms/card/card.tsx +0 -227
  78. package/src/components/organisms/card/card.visual.test.tsx +0 -197
  79. package/src/components/organisms/card/index.ts +0 -19
  80. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-darwin.png +0 -0
  81. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-linux.png +0 -0
  82. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-darwin.png +0 -0
  83. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-linux.png +0 -0
  84. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-darwin.png +0 -0
  85. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-linux.png +0 -0
  86. package/src/components/organisms/navbar/index.ts +0 -18
  87. package/src/components/organisms/navbar/navbar.stories.tsx +0 -313
  88. package/src/components/organisms/navbar/navbar.test.tsx +0 -190
  89. package/src/components/organisms/navbar/navbar.tsx +0 -317
  90. package/src/components/organisms/navbar/navbar.visual.test.tsx +0 -85
  91. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-darwin.png +0 -0
  92. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-linux.png +0 -0
  93. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-darwin.png +0 -0
  94. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-linux.png +0 -0
  95. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-darwin.png +0 -0
  96. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-linux.png +0 -0
  97. package/src/components/organisms/us-gov-banner/index.ts +0 -1
  98. package/src/components/organisms/us-gov-banner/us-gov-banner.stories.tsx +0 -35
  99. package/src/components/organisms/us-gov-banner/us-gov-banner.test.tsx +0 -107
  100. package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +0 -73
  101. package/src/components/organisms/us-gov-banner/us-gov-banner.visual.test.tsx +0 -46
  102. package/src/components/sections/banner/banner.stories.tsx +0 -150
  103. package/src/components/sections/banner/banner.test.tsx +0 -185
  104. package/src/components/sections/banner/banner.tsx +0 -130
  105. package/src/components/sections/banner/index.ts +0 -2
  106. package/src/components/sections/card-grid/card-grid.stories.tsx +0 -351
  107. package/src/components/sections/card-grid/card-grid.tsx +0 -118
  108. package/src/components/sections/card-grid/index.ts +0 -1
  109. package/src/components/sections/faq-section/faq-section.tsx +0 -77
  110. package/src/components/sections/faq-section/index.ts +0 -2
  111. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-darwin.png +0 -0
  112. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-linux.png +0 -0
  113. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-darwin.png +0 -0
  114. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-linux.png +0 -0
  115. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-darwin.png +0 -0
  116. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-linux.png +0 -0
  117. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-darwin.png +0 -0
  118. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-linux.png +0 -0
  119. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-darwin.png +0 -0
  120. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-linux.png +0 -0
  121. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-darwin.png +0 -0
  122. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-linux.png +0 -0
  123. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-darwin.png +0 -0
  124. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-linux.png +0 -0
  125. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-darwin.png +0 -0
  126. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-linux.png +0 -0
  127. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-darwin.png +0 -0
  128. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-linux.png +0 -0
  129. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-darwin.png +0 -0
  130. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-linux.png +0 -0
  131. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-default-chromium-linux.png +0 -0
  132. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-darwin.png +0 -0
  133. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-linux.png +0 -0
  134. package/src/components/sections/hero/hero.stories.tsx +0 -145
  135. package/src/components/sections/hero/hero.test.tsx +0 -135
  136. package/src/components/sections/hero/hero.tsx +0 -191
  137. package/src/components/sections/hero/hero.visual.test.tsx +0 -140
  138. package/src/components/sections/hero/index.ts +0 -1
  139. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-darwin.png +0 -0
  140. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-linux.png +0 -0
  141. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-darwin.png +0 -0
  142. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-linux.png +0 -0
  143. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-darwin.png +0 -0
  144. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-linux.png +0 -0
  145. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-darwin.png +0 -0
  146. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-linux.png +0 -0
  147. package/src/components/sections/prose/index.ts +0 -6
  148. package/src/components/sections/prose/prose.stories.tsx +0 -144
  149. package/src/components/sections/prose/prose.test.tsx +0 -178
  150. package/src/components/sections/prose/prose.tsx +0 -88
  151. package/src/components/sections/prose/prose.visual.test.tsx +0 -105
  152. package/src/components/sections/river/index.ts +0 -1
  153. package/src/components/sections/river/river.stories.tsx +0 -237
  154. package/src/components/sections/river/river.test.tsx +0 -268
  155. package/src/components/sections/river/river.tsx +0 -175
  156. package/src/components/sections/tout/index.ts +0 -1
  157. package/src/components/sections/tout/tout.stories.tsx +0 -154
  158. package/src/components/sections/tout/tout.test.tsx +0 -242
  159. package/src/components/sections/tout/tout.tsx +0 -206
  160. package/src/components/sections/two-column-section/index.ts +0 -5
  161. package/src/components/sections/two-column-section/two-column-section.stories.tsx +0 -285
  162. package/src/components/sections/two-column-section/two-column-section.tsx +0 -152
  163. package/src/index.ts +0 -98
  164. package/src/lib/utils.ts +0 -6
  165. package/src/main.tsx +0 -13
  166. package/src/stories/Introduction.mdx +0 -114
  167. package/src/stories/TokenShowcase.stories.tsx +0 -92
  168. package/src/stories/TokenShowcase.tsx +0 -1352
  169. package/src/styles.css +0 -11
@@ -1,141 +0,0 @@
1
- import { describe, expect, test, vi } from "vitest";
2
- import { page, userEvent } from "vitest/browser";
3
- import { render } from "vitest-browser-react";
4
- import { Button } from "./button";
5
-
6
- describe("Button", () => {
7
- describe("Accessibility", () => {
8
- test("has correct button role", async () => {
9
- render(<Button>Click me</Button>);
10
- await expect
11
- .element(page.getByRole("button", { name: "Click me" }))
12
- .toBeInTheDocument();
13
- });
14
-
15
- test("is focusable via keyboard", async () => {
16
- render(<Button>Focusable</Button>);
17
- await userEvent.keyboard("{Tab}");
18
- await expect
19
- .element(page.getByRole("button", { name: "Focusable" }))
20
- .toHaveFocus();
21
- });
22
-
23
- test("disabled button has disabled attribute", async () => {
24
- render(<Button disabled>Disabled</Button>);
25
- await expect
26
- .element(page.getByRole("button", { name: "Disabled" }))
27
- .toBeDisabled();
28
- });
29
-
30
- test("disabled button is not focusable", async () => {
31
- render(
32
- <>
33
- <Button disabled>Disabled</Button>
34
- <Button>After</Button>
35
- </>,
36
- );
37
- await userEvent.keyboard("{Tab}");
38
- // Focus should skip the disabled button and go to the next one
39
- await expect
40
- .element(page.getByRole("button", { name: "After" }))
41
- .toHaveFocus();
42
- });
43
-
44
- test("button with aria-label has accessible name", async () => {
45
- render(<Button aria-label="Close dialog">×</Button>);
46
- await expect
47
- .element(page.getByRole("button", { name: "Close dialog" }))
48
- .toBeInTheDocument();
49
- });
50
-
51
- test("button type defaults to button (not submit)", async () => {
52
- render(<Button>Click me</Button>);
53
- await expect
54
- .element(page.getByRole("button", { name: "Click me" }))
55
- .toHaveAttribute("type", "button");
56
- });
57
-
58
- test("button type can be set explicitly to submit", async () => {
59
- render(<Button type="submit">Submit</Button>);
60
- await expect
61
- .element(page.getByRole("button", { name: "Submit" }))
62
- .toHaveAttribute("type", "submit");
63
- });
64
- });
65
-
66
- describe("Interactions", () => {
67
- test("calls onClick when clicked", async () => {
68
- const handleClick = vi.fn();
69
- render(<Button onClick={handleClick}>Click me</Button>);
70
- await page.getByRole("button", { name: "Click me" }).click();
71
- expect(handleClick).toHaveBeenCalledOnce();
72
- });
73
-
74
- test("responds to Enter key when focused", async () => {
75
- const handleClick = vi.fn();
76
- render(<Button onClick={handleClick}>Enter key</Button>);
77
- page.getByRole("button", { name: "Enter key" }).element().focus();
78
- await userEvent.keyboard("{Enter}");
79
- expect(handleClick).toHaveBeenCalledOnce();
80
- });
81
-
82
- test("responds to Space key when focused", async () => {
83
- const handleClick = vi.fn();
84
- render(<Button onClick={handleClick}>Space key</Button>);
85
- page.getByRole("button", { name: "Space key" }).element().focus();
86
- await userEvent.keyboard(" ");
87
- expect(handleClick).toHaveBeenCalledOnce();
88
- });
89
-
90
- test("does not fire onClick when disabled", async () => {
91
- const handleClick = vi.fn();
92
- render(
93
- <Button disabled onClick={handleClick}>
94
- Disabled
95
- </Button>,
96
- );
97
- await page
98
- .getByRole("button", { name: "Disabled" })
99
- .click({ force: true });
100
- expect(handleClick).not.toHaveBeenCalled();
101
- });
102
-
103
- test("supports multiple clicks", async () => {
104
- const handleClick = vi.fn();
105
- render(<Button onClick={handleClick}>Multi click</Button>);
106
- const button = page.getByRole("button", { name: "Multi click" });
107
- await button.click();
108
- await button.click();
109
- await button.click();
110
- expect(handleClick).toHaveBeenCalledTimes(3);
111
- });
112
- });
113
-
114
- describe("render prop", () => {
115
- test("renders as anchor element with button role when render prop is used", async () => {
116
- render(
117
- // biome-ignore lint/a11y/useAnchorContent: Content provided via Button children
118
- <Button render={<a href="/test" />}>Link Button</Button>,
119
- );
120
- // Base UI keeps role="button" for accessibility when rendering as another element
121
- const button = page.getByRole("button", { name: "Link Button" });
122
- await expect.element(button).toBeInTheDocument();
123
- await expect.element(button).toHaveAttribute("href", "/test");
124
- });
125
- });
126
-
127
- describe("Variants", () => {
128
- test("applies charcoal variant classes by default", async () => {
129
- render(<Button>Default</Button>);
130
- const button = page.getByRole("button", { name: "Default" });
131
- await expect.element(button).toHaveClass(/bg-gray-1200/);
132
- });
133
-
134
- test("applies size classes correctly", async () => {
135
- render(<Button size="lg">Large</Button>);
136
- const button = page.getByRole("button", { name: "Large" });
137
- // Large buttons have h-[46px]
138
- await expect.element(button).toHaveClass(/h-\[46px\]/);
139
- });
140
- });
141
- });
@@ -1,95 +0,0 @@
1
- import {
2
- Button as BaseButton,
3
- type ButtonProps as BaseButtonProps,
4
- } from "@base-ui-components/react/button";
5
- import { cva, type VariantProps } from "class-variance-authority";
6
- import * as React from "react";
7
- import { cn } from "@/lib/utils";
8
-
9
- /**
10
- * Button component based on Figma BaseKit / Interface / Buttons
11
- *
12
- * Variants:
13
- * - charcoal: Dark filled button (for light backgrounds)
14
- * - charcoalOutline: Dark outlined button (for light backgrounds)
15
- * - charcoalOutlineQuiet: Subtle dark outlined button (for light backgrounds)
16
- * - ivory: Light filled button (for dark backgrounds)
17
- * - ivoryOutline: Light outlined button (for dark backgrounds)
18
- * - ivoryOutlineQuiet: Subtle light outlined button (for dark backgrounds)
19
- * - secondary: Gray filled button (for dark backgrounds)
20
- *
21
- * Sizes:
22
- * - lg: Large buttons (46px height)
23
- * - default: Medium buttons (38px height)
24
- * - sm: Small buttons (33px height)
25
- *
26
- * For icon-only buttons, use the IconButton component instead.
27
- */
28
- const buttonVariants = cva(
29
- "inline-flex items-center justify-center gap-spacing-6 whitespace-nowrap transition-colors duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
30
- {
31
- variants: {
32
- variant: {
33
- // Charcoal (dark filled) - primary dark
34
- charcoal:
35
- "bg-gray-1200 text-gray-100 hover:bg-gray-1100 active:bg-gray-1000 focus-visible:ring-gray-1000",
36
- // Charcoal Outline - outlined dark (for light backgrounds)
37
- charcoalOutline:
38
- "border border-alpha-black-30 text-gray-1000 hover:bg-alpha-black-5 active:bg-alpha-black-10 focus-visible:ring-gray-1000",
39
- // Charcoal Outline Quiet - subtle outlined dark (for light backgrounds)
40
- charcoalOutlineQuiet:
41
- "border border-alpha-black-20 text-alpha-black-60 hover:border-alpha-black-30 hover:text-alpha-black-80 active:bg-alpha-black-5 focus-visible:ring-gray-1000",
42
- // Ivory (light filled) - primary light (for dark backgrounds)
43
- ivory:
44
- "bg-gray-50 text-gray-1000 hover:bg-gray-100 active:bg-gray-200 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
45
- // Ivory Outline - outlined light (for dark backgrounds)
46
- ivoryOutline:
47
- "border border-gray-50 text-gray-50 hover:bg-alpha-white-10 active:bg-alpha-white-20 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
48
- // Ivory Outline Quiet - subtle light outline (for dark backgrounds)
49
- ivoryOutlineQuiet:
50
- "border border-alpha-white-20 text-alpha-white-60 hover:border-alpha-white-30 hover:text-alpha-white-80 active:bg-alpha-white-5 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
51
- // Secondary - gray filled button (for dark backgrounds)
52
- gray:
53
- "bg-gray-800 text-gray-100 hover:bg-gray-700 active:bg-gray-600 focus-visible:ring-gray-700 focus-visible:ring-offset-gray-1000",
54
- },
55
- size: {
56
- // Large - uses brand-large component tokens
57
- lg: "px-spacing-brand-large-component-button-padding-x-large py-spacing-brand-large-component-button-padding-y-large rounded-[var(--spacing-brand-large-component-button-radius-large)] typography-brand-large-button-large h-spacing-brand-large-component-button-height-large",
58
- // Medium (default) - uses brand-medium component tokens
59
- default:
60
- "px-spacing-brand-medium-component-button-padding-x-medium py-spacing-brand-medium-component-button-padding-y-medium rounded-[var(--spacing-brand-medium-component-button-radius-medium)] typography-brand-medium-button-small h-spacing-brand-medium-component-button-height-medium",
61
- // Small - uses brand-small component tokens
62
- sm: "px-spacing-brand-small-component-button-padding-x-small py-spacing-brand-small-component-button-padding-y-small rounded-[var(--spacing-brand-small-component-button-radius-small)] typography-brand-small-button-small h-spacing-brand-small-component-button-height-small",
63
- },
64
- },
65
- defaultVariants: {
66
- variant: "charcoal",
67
- size: "default",
68
- },
69
- },
70
- );
71
-
72
- export type HTMLButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
73
- export type ButtonProps = BaseButtonProps &
74
- VariantProps<typeof buttonVariants> &
75
- HTMLButtonProps;
76
-
77
- const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
78
- ({ className, variant, size, render, nativeButton, ...props }, ref) => {
79
- // When render prop is provided, default nativeButton to false to suppress warnings
80
- const isNativeButton = nativeButton ?? render === undefined;
81
-
82
- return (
83
- <BaseButton
84
- className={cn(buttonVariants({ variant, size, className }))}
85
- ref={ref}
86
- render={render}
87
- nativeButton={isNativeButton}
88
- {...props}
89
- />
90
- );
91
- },
92
- );
93
- Button.displayName = "Button";
94
-
95
- export { Button, buttonVariants };
@@ -1,102 +0,0 @@
1
- import { render } from "@testing-library/react";
2
- import { describe, expect, test } from "vitest";
3
- import { page } from "vitest/browser";
4
- import { Button } from "./button";
5
-
6
- describe("Button Visual Regression", () => {
7
- test("charcoal variant renders correctly", async () => {
8
- render(<Button variant="charcoal">Charcoal Button</Button>);
9
-
10
- await expect(
11
- page.getByRole("button", { name: "Charcoal Button" }),
12
- ).toMatchScreenshot("button-charcoal");
13
- });
14
-
15
- test("charcoalOutline variant renders correctly", async () => {
16
- render(<Button variant="charcoalOutline">Charcoal Outline Button</Button>);
17
-
18
- await expect(
19
- page.getByRole("button", { name: "Charcoal Outline Button" }),
20
- ).toMatchScreenshot("button-charcoal-outline");
21
- });
22
-
23
- test("charcoalOutlineQuiet variant renders correctly", async () => {
24
- render(
25
- <Button variant="charcoalOutlineQuiet">Charcoal Outline Quiet</Button>,
26
- );
27
-
28
- await expect(
29
- page.getByRole("button", { name: "Charcoal Outline Quiet" }),
30
- ).toMatchScreenshot("button-charcoal-outline-quiet");
31
- });
32
-
33
- test("ivory variant renders correctly", async () => {
34
- render(
35
- <div style={{ background: "#1a1a1a", padding: "20px" }}>
36
- <Button variant="ivory">Ivory Button</Button>
37
- </div>,
38
- );
39
-
40
- await expect(
41
- page.getByRole("button", { name: "Ivory Button" }),
42
- ).toMatchScreenshot("button-ivory");
43
- });
44
-
45
- test("ivoryOutline variant renders correctly", async () => {
46
- render(
47
- <div style={{ background: "#1a1a1a", padding: "20px" }}>
48
- <Button variant="ivoryOutline">Ivory Outline Button</Button>
49
- </div>,
50
- );
51
-
52
- await expect(
53
- page.getByRole("button", { name: "Ivory Outline Button" }),
54
- ).toMatchScreenshot("button-ivory-outline");
55
- });
56
-
57
- test("ivoryOutlineQuiet variant renders correctly", async () => {
58
- render(
59
- <div style={{ background: "#1a1a1a", padding: "20px" }}>
60
- <Button variant="ivoryOutlineQuiet">Ivory Outline Quiet</Button>
61
- </div>,
62
- );
63
-
64
- await expect(
65
- page.getByRole("button", { name: "Ivory Outline Quiet" }),
66
- ).toMatchScreenshot("button-ivory-outline-quiet");
67
- });
68
-
69
- // Size variants
70
- test("small size renders correctly", async () => {
71
- render(<Button size="sm">Small Button</Button>);
72
-
73
- await expect(
74
- page.getByRole("button", { name: "Small Button" }),
75
- ).toMatchScreenshot("button-size-small");
76
- });
77
-
78
- test("medium (default) size renders correctly", async () => {
79
- render(<Button size="default">Medium Button</Button>);
80
-
81
- await expect(
82
- page.getByRole("button", { name: "Medium Button" }),
83
- ).toMatchScreenshot("button-size-medium");
84
- });
85
-
86
- test("large size renders correctly", async () => {
87
- render(<Button size="lg">Large Button</Button>);
88
-
89
- await expect(
90
- page.getByRole("button", { name: "Large Button" }),
91
- ).toMatchScreenshot("button-size-large");
92
- });
93
-
94
- // Disabled state
95
- test("disabled state renders correctly", async () => {
96
- render(<Button disabled>Disabled Button</Button>);
97
-
98
- await expect(
99
- page.getByRole("button", { name: "Disabled Button" }),
100
- ).toMatchScreenshot("button-disabled");
101
- });
102
- });
@@ -1,166 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { IconButton } from ".";
3
-
4
- // Simple placeholder icons for stories
5
- const SearchIcon = () => (
6
- <svg
7
- width="16"
8
- height="16"
9
- viewBox="0 0 16 16"
10
- fill="none"
11
- xmlns="http://www.w3.org/2000/svg"
12
- aria-hidden="true"
13
- >
14
- <path
15
- d="M7.33333 12.6667C10.2789 12.6667 12.6667 10.2789 12.6667 7.33333C12.6667 4.38781 10.2789 2 7.33333 2C4.38781 2 2 4.38781 2 7.33333C2 10.2789 4.38781 12.6667 7.33333 12.6667Z"
16
- stroke="currentColor"
17
- strokeWidth="1.5"
18
- strokeLinecap="round"
19
- strokeLinejoin="round"
20
- />
21
- <path
22
- d="M14 14L11.1 11.1"
23
- stroke="currentColor"
24
- strokeWidth="1.5"
25
- strokeLinecap="round"
26
- strokeLinejoin="round"
27
- />
28
- </svg>
29
- );
30
-
31
- const ArrowRightIcon = () => (
32
- <svg
33
- width="12"
34
- height="12"
35
- viewBox="0 0 12 12"
36
- fill="none"
37
- xmlns="http://www.w3.org/2000/svg"
38
- aria-hidden="true"
39
- >
40
- <path
41
- d="M2.5 6H9.5M9.5 6L6 2.5M9.5 6L6 9.5"
42
- stroke="currentColor"
43
- strokeWidth="1.5"
44
- strokeLinecap="round"
45
- strokeLinejoin="round"
46
- />
47
- </svg>
48
- );
49
-
50
- const meta: Meta<typeof IconButton> = {
51
- title: "Atoms/IconButton",
52
- } as Meta<typeof IconButton>;
53
-
54
- export default meta;
55
- type Story = StoryObj<typeof IconButton>;
56
-
57
- export const Playground: Story = {
58
- render: (args) => (
59
- <IconButton {...args}>
60
- <SearchIcon />
61
- </IconButton>
62
- ),
63
- };
64
- Playground.argTypes = {
65
- size: {
66
- control: {
67
- type: "radio",
68
- },
69
- options: ["sm", "default", "lg"],
70
- },
71
- disabled: {
72
- control: {
73
- type: "boolean",
74
- },
75
- },
76
- variant: {
77
- control: {
78
- type: "radio",
79
- },
80
- options: [
81
- "charcoal",
82
- "charcoalOutline",
83
- "charcoalOutlineQuiet",
84
- "ivory",
85
- "ivoryOutline",
86
- "ivoryOutlineQuiet",
87
- ],
88
- },
89
- };
90
- Playground.args = {
91
- size: "default",
92
- disabled: false,
93
- variant: "charcoal",
94
- };
95
-
96
- // =============================================================================
97
- // Variants
98
- // =============================================================================
99
-
100
- export const Charcoal = () => (
101
- <IconButton variant="charcoal">
102
- <SearchIcon />
103
- </IconButton>
104
- );
105
-
106
- export const CharcoalOutline = () => (
107
- <IconButton variant="charcoalOutline">
108
- <SearchIcon />
109
- </IconButton>
110
- );
111
-
112
- export const CharcoalOutlineQuiet = () => (
113
- <IconButton variant="charcoalOutlineQuiet">
114
- <SearchIcon />
115
- </IconButton>
116
- );
117
-
118
- export const Ivory = () => (
119
- <IconButton variant="ivory">
120
- <SearchIcon />
121
- </IconButton>
122
- );
123
-
124
- export const IvoryOutline = () => (
125
- <IconButton variant="ivoryOutline">
126
- <ArrowRightIcon />
127
- </IconButton>
128
- );
129
-
130
- export const IvoryOutlineQuiet = () => (
131
- <IconButton variant="ivoryOutlineQuiet">
132
- <ArrowRightIcon />
133
- </IconButton>
134
- );
135
-
136
- // =============================================================================
137
- // Sizes
138
- // =============================================================================
139
-
140
- export const Small = () => (
141
- <IconButton size="sm">
142
- <ArrowRightIcon />
143
- </IconButton>
144
- );
145
-
146
- export const Medium = () => (
147
- <IconButton size="default">
148
- <SearchIcon />
149
- </IconButton>
150
- );
151
-
152
- export const Large = () => (
153
- <IconButton size="lg">
154
- <SearchIcon />
155
- </IconButton>
156
- );
157
-
158
- // =============================================================================
159
- // States
160
- // =============================================================================
161
-
162
- export const Disabled = () => (
163
- <IconButton disabled>
164
- <SearchIcon />
165
- </IconButton>
166
- );
@@ -1,125 +0,0 @@
1
- import { Slot } from "@radix-ui/react-slot";
2
- import { cva, type VariantProps } from "class-variance-authority";
3
- import * as React from "react";
4
- import { cn } from "@/lib/utils";
5
-
6
- /**
7
- * IconButton component based on Figma BaseKit / Interface / Icon Button
8
- *
9
- * **IMPORTANT: Accessibility Requirement**
10
- * Icon-only buttons MUST have an accessible label. Provide one of:
11
- * - `aria-label`: A text description of the button's action (recommended)
12
- * - `aria-labelledby`: Reference to an element containing the label
13
- * - `title`: Tooltip text (less preferred, but provides a label)
14
- *
15
- * @example
16
- * ```tsx
17
- * // Correct usage with aria-label
18
- * <IconButton aria-label="Close menu">
19
- * <CloseIcon />
20
- * </IconButton>
21
- *
22
- * // Correct usage with aria-labelledby
23
- * <IconButton aria-labelledby="close-label">
24
- * <CloseIcon />
25
- * </IconButton>
26
- * <span id="close-label" className="sr-only">Close menu</span>
27
- * ```
28
- *
29
- * Variants:
30
- * - charcoal: Dark filled button (for light backgrounds)
31
- * - charcoalOutline: Dark outlined button (for light backgrounds)
32
- * - charcoalOutlineQuiet: Subtle dark outlined button (for light backgrounds)
33
- * - ghost: No background/border, just icon (for light backgrounds)
34
- * - ghostDark: No background/border, just icon (for dark backgrounds)
35
- * - ivory: Light filled button (for dark backgrounds)
36
- * - ivoryOutline: Light outlined button (for dark backgrounds)
37
- * - ivoryOutlineQuiet: Subtle light outlined button (for dark backgrounds)
38
- *
39
- * Sizes:
40
- * - lg: Large (46x46)
41
- * - default: Medium (36x36)
42
- * - sm: Small (29x29)
43
- */
44
- const iconButtonVariants = cva(
45
- "inline-flex items-center justify-center whitespace-nowrap transition-colors duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
46
- {
47
- variants: {
48
- variant: {
49
- // Charcoal (dark filled) - primary dark
50
- charcoal:
51
- "bg-gray-1200 text-gray-100 hover:bg-gray-1100 active:bg-gray-1000 focus-visible:ring-gray-1000",
52
- // Charcoal Outline - outlined dark (for light backgrounds)
53
- charcoalOutline:
54
- "border border-alpha-black-30 text-gray-1000 hover:bg-alpha-black-5 active:bg-alpha-black-10 focus-visible:ring-gray-1000",
55
- // Charcoal Outline Quiet - subtle outlined dark (for light backgrounds)
56
- charcoalOutlineQuiet:
57
- "border border-alpha-black-20 text-alpha-black-60 hover:border-alpha-black-30 hover:text-alpha-black-80 active:bg-alpha-black-5 focus-visible:ring-gray-1000",
58
- // Ghost - no background/border (for light backgrounds)
59
- ghost:
60
- "text-gray-700 hover:text-gray-900 hover:bg-alpha-black-5 active:bg-alpha-black-10 focus-visible:ring-gray-1000",
61
- // Ghost Dark - no background/border (for dark backgrounds)
62
- ghostDark:
63
- "text-gray-300 hover:text-gray-100 hover:bg-alpha-white-10 active:bg-alpha-white-20 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
64
- // Ivory (light filled) - primary light (for dark backgrounds)
65
- ivory:
66
- "bg-gray-50 text-gray-1000 hover:bg-gray-100 active:bg-gray-200 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
67
- // Ivory Outline - outlined light (for dark backgrounds)
68
- ivoryOutline:
69
- "border border-gray-50 text-gray-50 hover:bg-alpha-white-10 active:bg-alpha-white-20 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
70
- // Ivory Outline Quiet - subtle light outline (for dark backgrounds)
71
- ivoryOutlineQuiet:
72
- "border border-alpha-white-20 text-alpha-white-60 hover:border-alpha-white-30 hover:text-alpha-white-80 active:bg-alpha-white-5 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
73
- },
74
- size: {
75
- // Large (46x46)
76
- lg: "rounded-[12px] size-[46px]",
77
- // Medium (36x36) - default
78
- default: "rounded-[12px] size-[36px]",
79
- // Small (29x29)
80
- sm: "rounded-[10px] size-[29px]",
81
- },
82
- },
83
- defaultVariants: {
84
- variant: "charcoal",
85
- size: "default",
86
- },
87
- },
88
- );
89
-
90
- export interface IconButtonProps
91
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
92
- VariantProps<typeof iconButtonVariants> {
93
- asChild?: boolean;
94
- }
95
-
96
- const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
97
- ({ className, variant, size, asChild = false, ...props }, ref) => {
98
- // Development warning for missing accessible label
99
- React.useEffect(() => {
100
- if (import.meta.env?.DEV) {
101
- const hasAccessibleLabel =
102
- props["aria-label"] ||
103
- props["aria-labelledby"] ||
104
- props.title;
105
- if (!hasAccessibleLabel) {
106
- console.warn(
107
- "IconButton: Missing accessible label. Icon-only buttons must have an aria-label, aria-labelledby, or title attribute for screen reader users.",
108
- );
109
- }
110
- }
111
- }, [props["aria-label"], props["aria-labelledby"], props.title]);
112
-
113
- const Comp = asChild ? Slot : "button";
114
- return (
115
- <Comp
116
- className={cn(iconButtonVariants({ variant, size, className }))}
117
- ref={ref}
118
- {...props}
119
- />
120
- );
121
- },
122
- );
123
- IconButton.displayName = "IconButton";
124
-
125
- export { IconButton, iconButtonVariants };
@@ -1,6 +0,0 @@
1
- export { Button, type ButtonProps, buttonVariants } from "./button";
2
- export {
3
- IconButton,
4
- type IconButtonProps,
5
- iconButtonVariants,
6
- } from "./icon-button";
@@ -1,5 +0,0 @@
1
- export {
2
- PagerControl,
3
- type PagerControlProps,
4
- pagerControlVariants,
5
- } from "./pager-control";