@nationaldesignstudio/react 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/component-registry.md +46 -19
  2. package/dist/components/atoms/accordion/accordion.d.ts +7 -7
  3. package/dist/components/atoms/background/background.d.ts +13 -27
  4. package/dist/components/atoms/button/button.d.ts +55 -71
  5. package/dist/components/atoms/button/icon-button.d.ts +62 -110
  6. package/dist/components/atoms/input/input-group.d.ts +278 -0
  7. package/dist/components/atoms/input/input.d.ts +121 -0
  8. package/dist/components/atoms/select/select.d.ts +131 -0
  9. package/dist/components/organisms/card/card.d.ts +2 -2
  10. package/dist/components/sections/banner/banner.d.ts +9 -9
  11. package/dist/components/sections/faq-section/faq-section.d.ts +1 -1
  12. package/dist/components/sections/hero/hero.d.ts +115 -18
  13. package/dist/components/sections/prose/prose.d.ts +3 -3
  14. package/dist/components/sections/river/river.d.ts +1 -1
  15. package/dist/components/sections/tout/tout.d.ts +9 -9
  16. package/dist/components/sections/two-column-section/two-column-section.d.ts +7 -21
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.js +11075 -7841
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/form-control.d.ts +105 -0
  21. package/dist/tokens.css +2144 -17341
  22. package/package.json +1 -1
  23. package/src/components/atoms/accordion/accordion.test.tsx +18 -20
  24. package/src/components/atoms/accordion/accordion.tsx +19 -17
  25. package/src/components/atoms/background/background.test.tsx +2 -2
  26. package/src/components/atoms/background/background.tsx +77 -96
  27. package/src/components/atoms/button/button.stories.tsx +42 -0
  28. package/src/components/atoms/button/button.test.tsx +1 -1
  29. package/src/components/atoms/button/button.tsx +38 -103
  30. package/src/components/atoms/button/button.visual.test.tsx +70 -24
  31. package/src/components/atoms/button/icon-button.tsx +80 -188
  32. package/src/components/atoms/input/index.ts +17 -0
  33. package/src/components/atoms/input/input-group.stories.tsx +650 -0
  34. package/src/components/atoms/input/input-group.test.tsx +376 -0
  35. package/src/components/atoms/input/input-group.tsx +384 -0
  36. package/src/components/atoms/input/input.stories.tsx +232 -0
  37. package/src/components/atoms/input/input.test.tsx +183 -0
  38. package/src/components/atoms/input/input.tsx +97 -0
  39. package/src/components/atoms/select/index.ts +18 -0
  40. package/src/components/atoms/select/select.stories.tsx +455 -0
  41. package/src/components/atoms/select/select.tsx +320 -0
  42. package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +2 -6
  43. package/src/components/foundation/typography/typography.stories.tsx +401 -0
  44. package/src/components/organisms/card/card.stories.tsx +11 -11
  45. package/src/components/organisms/card/card.test.tsx +5 -3
  46. package/src/components/organisms/card/card.tsx +2 -2
  47. package/src/components/organisms/card/card.visual.test.tsx +6 -6
  48. package/src/components/organisms/navbar/navbar.tsx +2 -2
  49. package/src/components/organisms/navbar/navbar.visual.test.tsx +2 -2
  50. package/src/components/sections/banner/banner.stories.tsx +5 -1
  51. package/src/components/sections/banner/banner.tsx +10 -10
  52. package/src/components/sections/card-grid/card-grid.tsx +1 -1
  53. package/src/components/sections/faq-section/faq-section.stories.tsx +7 -7
  54. package/src/components/sections/faq-section/faq-section.tsx +5 -5
  55. package/src/components/sections/hero/hero.test.tsx +5 -5
  56. package/src/components/sections/hero/hero.tsx +33 -51
  57. package/src/components/sections/prose/prose.test.tsx +2 -2
  58. package/src/components/sections/prose/prose.tsx +4 -5
  59. package/src/components/sections/river/river.stories.tsx +8 -8
  60. package/src/components/sections/river/river.test.tsx +1 -1
  61. package/src/components/sections/river/river.tsx +2 -4
  62. package/src/components/sections/tout/tout.stories.tsx +31 -7
  63. package/src/components/sections/tout/tout.test.tsx +1 -1
  64. package/src/components/sections/tout/tout.tsx +8 -10
  65. package/src/components/sections/two-column-section/two-column-section.stories.tsx +11 -11
  66. package/src/components/sections/two-column-section/two-column-section.tsx +16 -10
  67. package/src/index.ts +41 -0
  68. package/src/lib/form-control.ts +69 -0
  69. package/src/stories/Introduction.mdx +29 -15
  70. package/src/stories/ThemeProvider.stories.tsx +1 -3
  71. package/src/stories/TokenShowcase.stories.tsx +0 -19
  72. package/src/stories/TokenShowcase.tsx +714 -1366
  73. package/src/styles.css +3 -0
  74. package/src/tests/token-resolution.test.tsx +301 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nationaldesignstudio/react",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "*.css"
@@ -38,7 +38,7 @@ describe("Accordion", () => {
38
38
  // The first item should be expanded (has data-open attribute)
39
39
  await expect
40
40
  .element(page.getByRole("button", { name: "Question 1" }))
41
- .toHaveAttribute("data-open");
41
+ .toHaveAttribute("aria-expanded", "true");
42
42
  });
43
43
 
44
44
  test("renders with multiple default expanded items", async () => {
@@ -54,10 +54,10 @@ describe("Accordion", () => {
54
54
  );
55
55
  await expect
56
56
  .element(page.getByRole("button", { name: "Question 1" }))
57
- .toHaveAttribute("data-open");
57
+ .toHaveAttribute("aria-expanded", "true");
58
58
  await expect
59
59
  .element(page.getByRole("button", { name: "Question 2" }))
60
- .toHaveAttribute("data-open");
60
+ .toHaveAttribute("aria-expanded", "true");
61
61
  });
62
62
  });
63
63
 
@@ -90,7 +90,7 @@ describe("Accordion", () => {
90
90
  const trigger = page.getByRole("button", { name: "Question 1" });
91
91
  trigger.element().focus();
92
92
  await userEvent.keyboard("{Enter}");
93
- await expect.element(trigger).toHaveAttribute("data-open");
93
+ await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
94
94
  });
95
95
 
96
96
  test("Space key expands/collapses item", async () => {
@@ -104,7 +104,7 @@ describe("Accordion", () => {
104
104
  const trigger = page.getByRole("button", { name: "Question 1" });
105
105
  trigger.element().focus();
106
106
  await userEvent.keyboard(" ");
107
- await expect.element(trigger).toHaveAttribute("data-open");
107
+ await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
108
108
  });
109
109
  });
110
110
 
@@ -119,7 +119,7 @@ describe("Accordion", () => {
119
119
  );
120
120
  const trigger = page.getByRole("button", { name: "Question 1" });
121
121
  await trigger.click();
122
- await expect.element(trigger).toHaveAttribute("data-open");
122
+ await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
123
123
  });
124
124
 
125
125
  test("clicking expanded trigger collapses item", async () => {
@@ -131,9 +131,9 @@ describe("Accordion", () => {
131
131
  </Accordion>,
132
132
  );
133
133
  const trigger = page.getByRole("button", { name: "Question 1" });
134
- await expect.element(trigger).toHaveAttribute("data-open");
134
+ await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
135
135
  await trigger.click();
136
- await expect.element(trigger).not.toHaveAttribute("data-open");
136
+ await expect.element(trigger).toHaveAttribute("aria-expanded", "false");
137
137
  });
138
138
 
139
139
  test("single mode collapses other items when opening new one", async () => {
@@ -150,10 +150,10 @@ describe("Accordion", () => {
150
150
  const trigger1 = page.getByRole("button", { name: "Question 1" });
151
151
  const trigger2 = page.getByRole("button", { name: "Question 2" });
152
152
 
153
- await expect.element(trigger1).toHaveAttribute("data-open");
153
+ await expect.element(trigger1).toHaveAttribute("aria-expanded", "true");
154
154
  await trigger2.click();
155
- await expect.element(trigger2).toHaveAttribute("data-open");
156
- await expect.element(trigger1).not.toHaveAttribute("data-open");
155
+ await expect.element(trigger2).toHaveAttribute("aria-expanded", "true");
156
+ await expect.element(trigger1).toHaveAttribute("aria-expanded", "false");
157
157
  });
158
158
 
159
159
  test("multiple mode allows multiple items open", async () => {
@@ -170,15 +170,15 @@ describe("Accordion", () => {
170
170
  const trigger1 = page.getByRole("button", { name: "Question 1" });
171
171
  const trigger2 = page.getByRole("button", { name: "Question 2" });
172
172
 
173
- await expect.element(trigger1).toHaveAttribute("data-open");
173
+ await expect.element(trigger1).toHaveAttribute("aria-expanded", "true");
174
174
  await trigger2.click();
175
- await expect.element(trigger1).toHaveAttribute("data-open");
176
- await expect.element(trigger2).toHaveAttribute("data-open");
175
+ await expect.element(trigger1).toHaveAttribute("aria-expanded", "true");
176
+ await expect.element(trigger2).toHaveAttribute("aria-expanded", "true");
177
177
  });
178
178
  });
179
179
 
180
- describe("Variants", () => {
181
- test("applies light variant by default", async () => {
180
+ describe("Color Schemes", () => {
181
+ test("applies light colorScheme by default", async () => {
182
182
  render(
183
183
  <Accordion>
184
184
  <AccordionItem id="item-1" title="Question 1">
@@ -187,20 +187,18 @@ describe("Accordion", () => {
187
187
  </Accordion>,
188
188
  );
189
189
  const trigger = page.getByRole("button", { name: "Question 1" });
190
- // Light variant has gray-800 text
191
190
  await expect.element(trigger).toHaveClass(/text-gray-800/);
192
191
  });
193
192
 
194
- test("applies dark variant classes", async () => {
193
+ test("applies dark colorScheme classes", async () => {
195
194
  render(
196
- <Accordion variant="dark">
195
+ <Accordion colorScheme="dark">
197
196
  <AccordionItem id="item-1" title="Question 1">
198
197
  Answer 1
199
198
  </AccordionItem>
200
199
  </Accordion>,
201
200
  );
202
201
  const trigger = page.getByRole("button", { name: "Question 1" });
203
- // Dark variant has gray-100 text
204
202
  await expect.element(trigger).toHaveClass(/text-gray-100/);
205
203
  });
206
204
  });
@@ -12,26 +12,26 @@ import { cn } from "@/lib/utils";
12
12
  const accordionVariants = tv({
13
13
  base: "flex flex-col",
14
14
  variants: {
15
- variant: {
15
+ colorScheme: {
16
16
  dark: "",
17
17
  light: "",
18
18
  },
19
19
  },
20
20
  defaultVariants: {
21
- variant: "light",
21
+ colorScheme: "light",
22
22
  },
23
23
  });
24
24
 
25
25
  const accordionItemVariants = tv({
26
26
  base: "border-b overflow-hidden",
27
27
  variants: {
28
- variant: {
28
+ colorScheme: {
29
29
  dark: "border-gray-300",
30
30
  light: "border-gray-500",
31
31
  },
32
32
  },
33
33
  defaultVariants: {
34
- variant: "light",
34
+ colorScheme: "light",
35
35
  },
36
36
  });
37
37
 
@@ -42,13 +42,13 @@ const accordionTriggerVariants = tv({
42
42
  "typography-body-large transition-colors cursor-pointer",
43
43
  ],
44
44
  variants: {
45
- variant: {
45
+ colorScheme: {
46
46
  dark: "text-gray-100 data-[open]:text-white",
47
47
  light: "text-gray-800 data-[open]:text-gray-900",
48
48
  },
49
49
  },
50
50
  defaultVariants: {
51
- variant: "light",
51
+ colorScheme: "light",
52
52
  },
53
53
  });
54
54
 
@@ -56,13 +56,13 @@ const accordionPanelVariants = tv({
56
56
  // Uses primitive spacing tokens
57
57
  base: "typography-body-large pb-spacing-24",
58
58
  variants: {
59
- variant: {
59
+ colorScheme: {
60
60
  dark: "text-gray-100",
61
61
  light: "text-gray-800",
62
62
  },
63
63
  },
64
64
  defaultVariants: {
65
- variant: "light",
65
+ colorScheme: "light",
66
66
  },
67
67
  });
68
68
 
@@ -71,9 +71,9 @@ const accordionPanelVariants = tv({
71
71
  // =============================================================================
72
72
 
73
73
  const AccordionContext = React.createContext<{
74
- variant: "dark" | "light";
74
+ colorScheme: "dark" | "light";
75
75
  }>({
76
- variant: "light",
76
+ colorScheme: "light",
77
77
  });
78
78
 
79
79
  // =============================================================================
@@ -121,7 +121,7 @@ const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
121
121
  className,
122
122
  allowMultiple = false,
123
123
  defaultExpanded,
124
- variant = "light",
124
+ colorScheme = "light",
125
125
  children,
126
126
  },
127
127
  ref,
@@ -134,10 +134,10 @@ const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
134
134
  }, [defaultExpanded]);
135
135
 
136
136
  return (
137
- <AccordionContext.Provider value={{ variant }}>
137
+ <AccordionContext.Provider value={{ colorScheme }}>
138
138
  <BaseAccordion.Root
139
139
  ref={ref}
140
- className={accordionVariants({ variant, class: className })}
140
+ className={accordionVariants({ colorScheme, class: className })}
141
141
  defaultValue={defaultValue}
142
142
  multiple={allowMultiple}
143
143
  >
@@ -175,18 +175,18 @@ export interface AccordionItemProps
175
175
  */
176
176
  const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
177
177
  ({ className, id, title, children }, ref) => {
178
- const { variant } = React.useContext(AccordionContext);
178
+ const { colorScheme } = React.useContext(AccordionContext);
179
179
 
180
180
  return (
181
181
  <BaseAccordion.Item
182
182
  ref={ref}
183
183
  value={id}
184
- className={accordionItemVariants({ variant, class: className })}
184
+ className={accordionItemVariants({ colorScheme, class: className })}
185
185
  >
186
186
  {/* Header - always visible */}
187
187
  <BaseAccordion.Header>
188
188
  <BaseAccordion.Trigger
189
- className={accordionTriggerVariants({ variant })}
189
+ className={accordionTriggerVariants({ colorScheme })}
190
190
  >
191
191
  <span>{title}</span>
192
192
  <span
@@ -208,7 +208,9 @@ const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
208
208
  "[&[data-starting-style]]:h-0 [&[data-ending-style]]:h-0",
209
209
  )}
210
210
  >
211
- <div className={accordionPanelVariants({ variant })}>{children}</div>
211
+ <div className={accordionPanelVariants({ colorScheme })}>
212
+ {children}
213
+ </div>
212
214
  </BaseAccordion.Panel>
213
215
  </BaseAccordion.Item>
214
216
  );
@@ -102,12 +102,12 @@ describe("Background", () => {
102
102
  await expect.element(video).toBeInTheDocument();
103
103
  });
104
104
 
105
- test("has autoplay, loop, and muted by default", async () => {
105
+ test("has autoplay and loop by default", async () => {
106
106
  render(<BackgroundVideo src="/test.mp4" data-testid="bg-video" />);
107
107
  const video = page.getByTestId("bg-video");
108
108
  await expect.element(video).toHaveAttribute("autoplay");
109
109
  await expect.element(video).toHaveAttribute("loop");
110
- await expect.element(video).toHaveAttribute("muted");
110
+ // Note: muted is a boolean prop in React that may not render as an HTML attribute
111
111
  });
112
112
 
113
113
  test("has object-cover class", async () => {
@@ -50,7 +50,8 @@ const backgroundImageVariants = tv({
50
50
  });
51
51
 
52
52
  export interface BackgroundImageProps
53
- extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src"> {
53
+ extends useRender.ComponentProps<"img">,
54
+ Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "render"> {
54
55
  /**
55
56
  * URL for the background image
56
57
  */
@@ -59,23 +60,6 @@ export interface BackgroundImageProps
59
60
  * Object position (default: "center")
60
61
  */
61
62
  position?: string;
62
- /**
63
- * Custom render prop for element composition.
64
- * Accepts a React element or render function.
65
- * @example
66
- * ```tsx
67
- * // Element pattern
68
- * <BackgroundImage render={<img className="custom" />} src="/bg.jpg" />
69
- *
70
- * // Callback pattern
71
- * <BackgroundImage render={(props) => <img {...props} />} src="/bg.jpg" />
72
- * ```
73
- */
74
- render?:
75
- | React.ReactElement
76
- | ((
77
- props: React.ImgHTMLAttributes<HTMLImageElement>,
78
- ) => React.ReactElement);
79
63
  }
80
64
 
81
65
  /**
@@ -83,35 +67,32 @@ export interface BackgroundImageProps
83
67
  * Supports native lazy loading, srcset, and better accessibility.
84
68
  * Supports render prop for element composition.
85
69
  */
86
- const BackgroundImage = React.forwardRef<
87
- HTMLImageElement,
88
- BackgroundImageProps
89
- >(
90
- (
91
- { className, src, position = "center", alt = "", style, render, ...props },
92
- ref,
93
- ) => {
94
- const mergedProps = {
70
+ function BackgroundImage(props: BackgroundImageProps) {
71
+ const {
72
+ className,
73
+ src,
74
+ position = "center",
75
+ alt = "",
76
+ style,
77
+ render,
78
+ ...otherProps
79
+ } = props;
80
+
81
+ const imgClassName = backgroundImageVariants({ class: className });
82
+ const imgStyle = { objectPosition: position, ...style };
83
+
84
+ return useRender({
85
+ render,
86
+ props: {
95
87
  src,
96
88
  alt,
97
- className: backgroundImageVariants({ class: className }),
98
- style: {
99
- objectPosition: position,
100
- ...style,
101
- },
102
- ...props,
103
- };
104
-
105
- const element = useRender({
106
- // biome-ignore lint/a11y/useAltText: alt is provided via mergedProps
107
- render: render ?? <img />,
108
- ref,
109
- props: mergedProps,
110
- });
111
-
112
- return element;
113
- },
114
- );
89
+ className: imgClassName,
90
+ style: imgStyle,
91
+ ...otherProps,
92
+ },
93
+ defaultTagName: "img",
94
+ });
95
+ }
115
96
  BackgroundImage.displayName = "Background.Image";
116
97
 
117
98
  // =============================================================================
@@ -123,7 +104,11 @@ const backgroundVideoVariants = tv({
123
104
  });
124
105
 
125
106
  export interface BackgroundVideoProps
126
- extends Omit<React.VideoHTMLAttributes<HTMLVideoElement>, "children"> {
107
+ extends useRender.ComponentProps<"video">,
108
+ Omit<
109
+ React.VideoHTMLAttributes<HTMLVideoElement>,
110
+ "children" | "render" | "src"
111
+ > {
127
112
  /**
128
113
  * URL for the video source
129
114
  */
@@ -136,67 +121,63 @@ export interface BackgroundVideoProps
136
121
  * Poster image URL shown before video loads
137
122
  */
138
123
  poster?: string;
139
- /**
140
- * Custom render prop for element composition.
141
- * @example
142
- * ```tsx
143
- * <BackgroundVideo render={<video className="custom" />} src="/bg.mp4" />
144
- * ```
145
- */
146
- render?:
147
- | React.ReactElement
148
- | ((
149
- props: React.VideoHTMLAttributes<HTMLVideoElement>,
150
- ) => React.ReactElement);
151
124
  }
152
125
 
153
126
  /**
154
127
  * Background video layer using HTML5 video element.
155
128
  * Supports render prop for element composition.
156
129
  */
157
- const BackgroundVideo = React.forwardRef<
158
- HTMLVideoElement,
159
- BackgroundVideoProps
160
- >(
161
- (
162
- {
163
- className,
164
- src,
165
- type,
166
- poster,
167
- autoPlay = true,
168
- loop = true,
169
- muted = true,
170
- playsInline = true,
171
- render,
172
- ...props
173
- },
174
- ref,
175
- ) => {
176
- const mergedProps = {
130
+ function BackgroundVideo(props: BackgroundVideoProps) {
131
+ const {
132
+ className,
133
+ src,
134
+ type,
135
+ poster,
136
+ autoPlay = true,
137
+ loop = true,
138
+ muted = true,
139
+ playsInline = true,
140
+ render,
141
+ children,
142
+ ...otherProps
143
+ } = props;
144
+
145
+ const videoClassName = backgroundVideoVariants({ class: className });
146
+
147
+ // useRender must be called unconditionally
148
+ const rendered = useRender({
149
+ render,
150
+ props: {
177
151
  autoPlay,
178
152
  loop,
179
153
  muted,
180
154
  playsInline,
181
155
  poster,
182
- className: backgroundVideoVariants({ class: className }),
183
- ...props,
184
- };
185
-
186
- const element = useRender({
187
- render: render ?? (
188
- // biome-ignore lint/a11y/useMediaCaption: Background videos are decorative and shouldn't have captions
189
- <video>
190
- <source src={src} type={type} />
191
- </video>
192
- ),
193
- ref,
194
- props: mergedProps,
195
- });
196
-
197
- return element;
198
- },
199
- );
156
+ className: videoClassName,
157
+ ...otherProps,
158
+ },
159
+ defaultTagName: "video",
160
+ });
161
+
162
+ // If no render prop, return video with source child
163
+ if (!render) {
164
+ return (
165
+ <video
166
+ autoPlay={autoPlay}
167
+ loop={loop}
168
+ muted={muted}
169
+ playsInline={playsInline}
170
+ poster={poster}
171
+ className={videoClassName}
172
+ {...otherProps}
173
+ >
174
+ <source src={src} type={type} />
175
+ </video>
176
+ );
177
+ }
178
+
179
+ return rendered;
180
+ }
200
181
  BackgroundVideo.displayName = "Background.Video";
201
182
 
202
183
  // =============================================================================
@@ -45,24 +45,51 @@ Playground.args = {
45
45
 
46
46
  // =============================================================================
47
47
  // Variants (Dark Color Scheme - for light backgrounds)
48
+ // Figma naming: Charcoal = solid+dark, Charcoal Outline = outline+dark
48
49
  // =============================================================================
49
50
 
51
+ /**
52
+ * Solid Dark (Figma: "Charcoal" / "Black")
53
+ * Primary filled button with ui-button-primary-bg background and inverted text.
54
+ * Use on light backgrounds for primary actions.
55
+ */
50
56
  export const Solid = () => <Button variant="solid">Solid</Button>;
51
57
 
58
+ /**
59
+ * Outline Dark (Figma: "Charcoal Outline")
60
+ * Primary-colored border and text on transparent background.
61
+ * Use on light backgrounds for secondary actions.
62
+ */
52
63
  export const Outline = () => <Button variant="outline">Outline</Button>;
53
64
 
65
+ /**
66
+ * Ghost Dark
67
+ * Primary-colored text with no border or background.
68
+ * Use for tertiary actions on light backgrounds.
69
+ */
54
70
  export const Ghost = () => <Button variant="ghost">Ghost</Button>;
55
71
 
72
+ /**
73
+ * Subtle Dark
74
+ * Subtle border with primary-colored text.
75
+ * Use for less prominent actions on light backgrounds.
76
+ */
56
77
  export const Subtle = () => <Button variant="subtle">Subtle</Button>;
57
78
 
58
79
  // =============================================================================
59
80
  // Variants (Light Color Scheme - for dark backgrounds)
81
+ // Figma naming: Ivory = solid+light, Ivory Outline = outline+light
60
82
  // =============================================================================
61
83
 
62
84
  const DarkBackground = ({ children }: { children: React.ReactNode }) => (
63
85
  <div className="rounded-radius-12 bg-gray-1200 p-spacing-32">{children}</div>
64
86
  );
65
87
 
88
+ /**
89
+ * Solid Light (Figma: "Ivory")
90
+ * White filled button with primary-colored text.
91
+ * Use on dark backgrounds for primary actions.
92
+ */
66
93
  export const SolidLight = () => (
67
94
  <DarkBackground>
68
95
  <Button variant="solid" colorScheme="light">
@@ -71,6 +98,11 @@ export const SolidLight = () => (
71
98
  </DarkBackground>
72
99
  );
73
100
 
101
+ /**
102
+ * Outline Light (Figma: "Ivory Outline")
103
+ * White border and text on transparent background.
104
+ * Use on dark backgrounds for secondary actions.
105
+ */
74
106
  export const OutlineLight = () => (
75
107
  <DarkBackground>
76
108
  <Button variant="outline" colorScheme="light">
@@ -79,6 +111,11 @@ export const OutlineLight = () => (
79
111
  </DarkBackground>
80
112
  );
81
113
 
114
+ /**
115
+ * Ghost Light
116
+ * White text with no border or background.
117
+ * Use for tertiary actions on dark backgrounds.
118
+ */
82
119
  export const GhostLight = () => (
83
120
  <DarkBackground>
84
121
  <Button variant="ghost" colorScheme="light">
@@ -87,6 +124,11 @@ export const GhostLight = () => (
87
124
  </DarkBackground>
88
125
  );
89
126
 
127
+ /**
128
+ * Subtle Light
129
+ * Alpha-white border with white text.
130
+ * Use for less prominent actions on dark backgrounds.
131
+ */
90
132
  export const SubtleLight = () => (
91
133
  <DarkBackground>
92
134
  <Button variant="subtle" colorScheme="light">
@@ -129,7 +129,7 @@ describe("Button", () => {
129
129
  render(<Button>Default</Button>);
130
130
  const button = page.getByRole("button", { name: "Default" });
131
131
  // Button uses semantic token classes
132
- await expect.element(button).toHaveClass(/bg-button-primary-bg/);
132
+ await expect.element(button).toHaveClass(/bg-ui-button-primary-bg/);
133
133
  });
134
134
  });
135
135
  });