@nationaldesignstudio/react 0.2.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.
- package/dist/components/atoms/background/background.d.ts +13 -27
- package/dist/components/atoms/button/button.d.ts +55 -71
- package/dist/components/atoms/button/icon-button.d.ts +62 -110
- package/dist/components/atoms/input/input-group.d.ts +278 -0
- package/dist/components/atoms/input/input.d.ts +121 -0
- package/dist/components/atoms/select/select.d.ts +131 -0
- package/dist/components/organisms/card/card.d.ts +2 -2
- package/dist/components/sections/prose/prose.d.ts +3 -3
- package/dist/components/sections/river/river.d.ts +1 -1
- package/dist/components/sections/tout/tout.d.ts +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11034 -7824
- package/dist/index.js.map +1 -1
- package/dist/lib/form-control.d.ts +105 -0
- package/dist/tokens.css +2132 -17329
- package/package.json +1 -1
- package/src/components/atoms/background/background.tsx +71 -109
- package/src/components/atoms/button/button.stories.tsx +42 -0
- package/src/components/atoms/button/button.test.tsx +1 -1
- package/src/components/atoms/button/button.tsx +38 -103
- package/src/components/atoms/button/button.visual.test.tsx +70 -24
- package/src/components/atoms/button/icon-button.tsx +81 -224
- package/src/components/atoms/input/index.ts +17 -0
- package/src/components/atoms/input/input-group.stories.tsx +650 -0
- package/src/components/atoms/input/input-group.test.tsx +376 -0
- package/src/components/atoms/input/input-group.tsx +384 -0
- package/src/components/atoms/input/input.stories.tsx +232 -0
- package/src/components/atoms/input/input.test.tsx +183 -0
- package/src/components/atoms/input/input.tsx +97 -0
- package/src/components/atoms/select/index.ts +18 -0
- package/src/components/atoms/select/select.stories.tsx +455 -0
- package/src/components/atoms/select/select.tsx +320 -0
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +2 -6
- package/src/components/foundation/typography/typography.stories.tsx +401 -0
- package/src/components/organisms/card/card.stories.tsx +11 -11
- package/src/components/organisms/card/card.test.tsx +1 -1
- package/src/components/organisms/card/card.tsx +2 -2
- package/src/components/organisms/card/card.visual.test.tsx +6 -6
- package/src/components/organisms/navbar/navbar.tsx +2 -2
- package/src/components/organisms/navbar/navbar.visual.test.tsx +2 -2
- package/src/components/sections/card-grid/card-grid.tsx +1 -1
- package/src/components/sections/faq-section/faq-section.tsx +2 -2
- package/src/components/sections/hero/hero.test.tsx +5 -5
- package/src/components/sections/prose/prose.test.tsx +2 -2
- package/src/components/sections/prose/prose.tsx +4 -5
- package/src/components/sections/river/river.stories.tsx +8 -8
- package/src/components/sections/river/river.test.tsx +1 -1
- package/src/components/sections/river/river.tsx +2 -4
- package/src/components/sections/tout/tout.test.tsx +1 -1
- package/src/components/sections/tout/tout.tsx +2 -2
- package/src/index.ts +41 -0
- package/src/lib/form-control.ts +69 -0
- package/src/stories/Introduction.mdx +29 -15
- package/src/stories/ThemeProvider.stories.tsx +1 -3
- package/src/stories/TokenShowcase.stories.tsx +0 -19
- package/src/stories/TokenShowcase.tsx +714 -1366
- package/src/styles.css +3 -0
- package/src/tests/token-resolution.test.tsx +301 -0
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useRender } from "@base-ui-components/react/use-render";
|
|
3
4
|
import * as React from "react";
|
|
4
5
|
import { tv } from "tailwind-variants";
|
|
5
6
|
|
|
@@ -49,7 +50,8 @@ const backgroundImageVariants = tv({
|
|
|
49
50
|
});
|
|
50
51
|
|
|
51
52
|
export interface BackgroundImageProps
|
|
52
|
-
extends
|
|
53
|
+
extends useRender.ComponentProps<"img">,
|
|
54
|
+
Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "render"> {
|
|
53
55
|
/**
|
|
54
56
|
* URL for the background image
|
|
55
57
|
*/
|
|
@@ -58,23 +60,6 @@ export interface BackgroundImageProps
|
|
|
58
60
|
* Object position (default: "center")
|
|
59
61
|
*/
|
|
60
62
|
position?: string;
|
|
61
|
-
/**
|
|
62
|
-
* Custom render prop for element composition.
|
|
63
|
-
* Accepts a React element or render function.
|
|
64
|
-
* @example
|
|
65
|
-
* ```tsx
|
|
66
|
-
* // Element pattern
|
|
67
|
-
* <BackgroundImage render={<img className="custom" />} src="/bg.jpg" />
|
|
68
|
-
*
|
|
69
|
-
* // Callback pattern
|
|
70
|
-
* <BackgroundImage render={(props) => <img {...props} />} src="/bg.jpg" />
|
|
71
|
-
* ```
|
|
72
|
-
*/
|
|
73
|
-
render?:
|
|
74
|
-
| React.ReactElement
|
|
75
|
-
| ((
|
|
76
|
-
props: React.ImgHTMLAttributes<HTMLImageElement>,
|
|
77
|
-
) => React.ReactElement);
|
|
78
63
|
}
|
|
79
64
|
|
|
80
65
|
/**
|
|
@@ -82,45 +67,32 @@ export interface BackgroundImageProps
|
|
|
82
67
|
* Supports native lazy loading, srcset, and better accessibility.
|
|
83
68
|
* Supports render prop for element composition.
|
|
84
69
|
*/
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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: {
|
|
96
87
|
src,
|
|
97
88
|
alt,
|
|
98
89
|
className: imgClassName,
|
|
99
90
|
style: imgStyle,
|
|
100
|
-
...
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (typeof render === "function") {
|
|
106
|
-
return render({
|
|
107
|
-
ref,
|
|
108
|
-
...imgProps,
|
|
109
|
-
} as React.ImgHTMLAttributes<HTMLImageElement>);
|
|
110
|
-
}
|
|
111
|
-
// Clone the render element with merged props
|
|
112
|
-
return React.cloneElement(render, {
|
|
113
|
-
ref,
|
|
114
|
-
...imgProps,
|
|
115
|
-
...(render.props as Record<string, unknown>),
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Default: render as img
|
|
120
|
-
// biome-ignore lint/a11y/useAltText: alt is provided via imgProps spread
|
|
121
|
-
return <img ref={ref} {...imgProps} />;
|
|
122
|
-
},
|
|
123
|
-
);
|
|
91
|
+
...otherProps,
|
|
92
|
+
},
|
|
93
|
+
defaultTagName: "img",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
124
96
|
BackgroundImage.displayName = "Background.Image";
|
|
125
97
|
|
|
126
98
|
// =============================================================================
|
|
@@ -132,7 +104,11 @@ const backgroundVideoVariants = tv({
|
|
|
132
104
|
});
|
|
133
105
|
|
|
134
106
|
export interface BackgroundVideoProps
|
|
135
|
-
extends
|
|
107
|
+
extends useRender.ComponentProps<"video">,
|
|
108
|
+
Omit<
|
|
109
|
+
React.VideoHTMLAttributes<HTMLVideoElement>,
|
|
110
|
+
"children" | "render" | "src"
|
|
111
|
+
> {
|
|
136
112
|
/**
|
|
137
113
|
* URL for the video source
|
|
138
114
|
*/
|
|
@@ -145,77 +121,63 @@ export interface BackgroundVideoProps
|
|
|
145
121
|
* Poster image URL shown before video loads
|
|
146
122
|
*/
|
|
147
123
|
poster?: string;
|
|
148
|
-
/**
|
|
149
|
-
* Custom render prop for element composition.
|
|
150
|
-
* @example
|
|
151
|
-
* ```tsx
|
|
152
|
-
* <BackgroundVideo render={<video className="custom" />} src="/bg.mp4" />
|
|
153
|
-
* ```
|
|
154
|
-
*/
|
|
155
|
-
render?:
|
|
156
|
-
| React.ReactElement
|
|
157
|
-
| ((
|
|
158
|
-
props: React.VideoHTMLAttributes<HTMLVideoElement>,
|
|
159
|
-
) => React.ReactElement);
|
|
160
124
|
}
|
|
161
125
|
|
|
162
126
|
/**
|
|
163
127
|
* Background video layer using HTML5 video element.
|
|
164
128
|
* Supports render prop for element composition.
|
|
165
129
|
*/
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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: {
|
|
187
151
|
autoPlay,
|
|
188
152
|
loop,
|
|
189
153
|
muted,
|
|
190
154
|
playsInline,
|
|
191
155
|
poster,
|
|
192
156
|
className: videoClassName,
|
|
193
|
-
...
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
ref,
|
|
201
|
-
...videoProps,
|
|
202
|
-
} as React.VideoHTMLAttributes<HTMLVideoElement>);
|
|
203
|
-
}
|
|
204
|
-
// Clone the render element with merged props
|
|
205
|
-
return React.cloneElement(render, {
|
|
206
|
-
...videoProps,
|
|
207
|
-
...(render.props as Record<string, unknown>),
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Default: render as video with source
|
|
157
|
+
...otherProps,
|
|
158
|
+
},
|
|
159
|
+
defaultTagName: "video",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// If no render prop, return video with source child
|
|
163
|
+
if (!render) {
|
|
212
164
|
return (
|
|
213
|
-
<video
|
|
165
|
+
<video
|
|
166
|
+
autoPlay={autoPlay}
|
|
167
|
+
loop={loop}
|
|
168
|
+
muted={muted}
|
|
169
|
+
playsInline={playsInline}
|
|
170
|
+
poster={poster}
|
|
171
|
+
className={videoClassName}
|
|
172
|
+
{...otherProps}
|
|
173
|
+
>
|
|
214
174
|
<source src={src} type={type} />
|
|
215
175
|
</video>
|
|
216
176
|
);
|
|
217
|
-
}
|
|
218
|
-
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return rendered;
|
|
180
|
+
}
|
|
219
181
|
BackgroundVideo.displayName = "Background.Video";
|
|
220
182
|
|
|
221
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
|
});
|
|
@@ -9,22 +9,20 @@ import { tv, type VariantProps } from "tailwind-variants";
|
|
|
9
9
|
import { type ButtonTheme, buttonThemeToStyleVars } from "../../../lib/theme";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Button component based on Figma
|
|
12
|
+
* Button component based on Figma Button component
|
|
13
13
|
*
|
|
14
|
-
* Variants:
|
|
15
|
-
* -
|
|
16
|
-
* - outline: Outlined button
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* - dark: Dark colors for use on light backgrounds (default)
|
|
22
|
-
* - light: Light colors for use on dark backgrounds
|
|
14
|
+
* Variants (matches Figma):
|
|
15
|
+
* - primary: Filled brand button (indigo background)
|
|
16
|
+
* - primary-outline: Outlined brand button (indigo border/text)
|
|
17
|
+
* - secondary: Filled neutral button (white background, for dark backgrounds)
|
|
18
|
+
* - secondary-outline: Outlined neutral button (white border/text, for dark backgrounds)
|
|
19
|
+
* - ghost: Transparent button with subtle hover (for light backgrounds)
|
|
20
|
+
* - ghost-inverse: Transparent button with subtle hover (for dark backgrounds)
|
|
23
21
|
*
|
|
24
22
|
* Sizes:
|
|
25
|
-
* - lg: Large buttons
|
|
26
|
-
* -
|
|
27
|
-
* - sm: Small buttons
|
|
23
|
+
* - lg: Large buttons (56px height)
|
|
24
|
+
* - md: Medium buttons (40px height) - default
|
|
25
|
+
* - sm: Small buttons (28px height)
|
|
28
26
|
*
|
|
29
27
|
* For icon-only buttons, use the IconButton component instead.
|
|
30
28
|
*
|
|
@@ -32,90 +30,40 @@ import { type ButtonTheme, buttonThemeToStyleVars } from "../../../lib/theme";
|
|
|
32
30
|
* Pass a `theme` prop to override default colors via CSS custom properties.
|
|
33
31
|
*/
|
|
34
32
|
const buttonVariants = tv({
|
|
35
|
-
base: "inline-flex items-center justify-center gap-
|
|
33
|
+
base: "inline-flex items-center justify-center gap-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 border-solid",
|
|
36
34
|
variants: {
|
|
37
35
|
variant: {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
// Primary - filled brand button
|
|
37
|
+
primary:
|
|
38
|
+
"bg-button-primary-bg text-button-primary-text hover:bg-button-primary-bg-hover hover:text-button-primary-text-hover border-transparent focus-visible:ring-button-primary-bg",
|
|
39
|
+
// Primary Outline - outlined brand button
|
|
40
|
+
"primary-outline":
|
|
41
|
+
"bg-button-primary-outline-bg text-button-primary-outline-text border border-button-primary-outline-border hover:bg-button-primary-outline-bg-hover hover:text-button-primary-outline-text-hover hover:border-button-primary-outline-border-hover focus-visible:ring-button-primary-outline-border",
|
|
42
|
+
// Secondary - filled neutral button (for dark backgrounds)
|
|
43
|
+
secondary:
|
|
44
|
+
"bg-button-secondary-bg text-button-secondary-text hover:bg-button-secondary-bg-hover hover:text-button-secondary-text-hover border-transparent focus-visible:ring-button-secondary-bg focus-visible:ring-offset-gray-1000",
|
|
45
|
+
// Secondary Outline - outlined neutral button (for dark backgrounds)
|
|
46
|
+
"secondary-outline":
|
|
47
|
+
"bg-button-secondary-outline-bg text-button-secondary-outline-text border border-button-secondary-outline-border hover:bg-button-secondary-outline-bg-hover hover:text-button-secondary-outline-text-hover hover:border-button-secondary-outline-border-hover focus-visible:ring-button-secondary-outline-border focus-visible:ring-offset-gray-1000",
|
|
48
|
+
// Ghost - transparent button (for light backgrounds)
|
|
49
|
+
ghost:
|
|
50
|
+
"bg-button-ghost-bg text-button-ghost-text hover:bg-button-ghost-bg-hover hover:text-button-ghost-text-hover border-transparent focus-visible:ring-gray-1000",
|
|
51
|
+
// Ghost Inverse - transparent button (for dark backgrounds)
|
|
52
|
+
"ghost-inverse":
|
|
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",
|
|
42
54
|
// Themed - uses CSS custom properties for styling
|
|
43
55
|
themed:
|
|
44
56
|
"[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)))]",
|
|
45
57
|
},
|
|
46
|
-
colorScheme: {
|
|
47
|
-
dark: "",
|
|
48
|
-
light: "",
|
|
49
|
-
},
|
|
50
58
|
size: {
|
|
51
|
-
lg: "px-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
sm: "px-spacing-16 py-spacing-8 typography-small-button-small h-spacing-32",
|
|
59
|
+
lg: "px-32 py-20 h-56 rounded-10 typography-large-button-large",
|
|
60
|
+
md: "px-20 py-12 h-40 rounded-6 typography-medium-button-medium",
|
|
61
|
+
sm: "px-12 py-8 h-28 rounded-4 typography-small-button-small",
|
|
55
62
|
},
|
|
56
63
|
},
|
|
57
|
-
compoundVariants: [
|
|
58
|
-
// Solid + Dark (for light backgrounds) - uses semantic button tokens
|
|
59
|
-
{
|
|
60
|
-
variant: "solid",
|
|
61
|
-
colorScheme: "dark",
|
|
62
|
-
class:
|
|
63
|
-
"bg-button-primary-bg text-text-inverted hover:bg-button-primary-bg-hover active:bg-button-primary-bg-hover border-transparent focus-visible:ring-button-primary-bg",
|
|
64
|
-
},
|
|
65
|
-
// Solid + Light (for dark backgrounds)
|
|
66
|
-
{
|
|
67
|
-
variant: "solid",
|
|
68
|
-
colorScheme: "light",
|
|
69
|
-
class:
|
|
70
|
-
"bg-button-secondary-bg text-text-primary hover:bg-button-secondary-bg-hover active:bg-gray-200 border-transparent focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
|
|
71
|
-
},
|
|
72
|
-
// Outline + Dark (for light backgrounds)
|
|
73
|
-
{
|
|
74
|
-
variant: "outline",
|
|
75
|
-
colorScheme: "dark",
|
|
76
|
-
class:
|
|
77
|
-
"border-border-subtle text-gray-1000 hover:bg-alpha-black-5 active:bg-alpha-black-10 focus-visible:ring-gray-1000",
|
|
78
|
-
},
|
|
79
|
-
// Outline + Light (for dark backgrounds)
|
|
80
|
-
{
|
|
81
|
-
variant: "outline",
|
|
82
|
-
colorScheme: "light",
|
|
83
|
-
class:
|
|
84
|
-
"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",
|
|
85
|
-
},
|
|
86
|
-
// Ghost + Dark (for light backgrounds)
|
|
87
|
-
{
|
|
88
|
-
variant: "ghost",
|
|
89
|
-
colorScheme: "dark",
|
|
90
|
-
class:
|
|
91
|
-
"text-gray-700 hover:text-gray-900 hover:bg-alpha-black-5 active:bg-alpha-black-10 focus-visible:ring-gray-1000",
|
|
92
|
-
},
|
|
93
|
-
// Ghost + Light (for dark backgrounds)
|
|
94
|
-
{
|
|
95
|
-
variant: "ghost",
|
|
96
|
-
colorScheme: "light",
|
|
97
|
-
class:
|
|
98
|
-
"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",
|
|
99
|
-
},
|
|
100
|
-
// Subtle + Dark (for light backgrounds)
|
|
101
|
-
{
|
|
102
|
-
variant: "subtle",
|
|
103
|
-
colorScheme: "dark",
|
|
104
|
-
class:
|
|
105
|
-
"border-border-subtle text-alpha-black-60 hover:border-border-strong hover:text-alpha-black-80 active:bg-alpha-black-5 focus-visible:ring-gray-1000",
|
|
106
|
-
},
|
|
107
|
-
// Subtle + Light (for dark backgrounds)
|
|
108
|
-
{
|
|
109
|
-
variant: "subtle",
|
|
110
|
-
colorScheme: "light",
|
|
111
|
-
class:
|
|
112
|
-
"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",
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
64
|
defaultVariants: {
|
|
116
|
-
variant: "
|
|
117
|
-
|
|
118
|
-
size: "default",
|
|
65
|
+
variant: "primary",
|
|
66
|
+
size: "md",
|
|
119
67
|
},
|
|
120
68
|
});
|
|
121
69
|
|
|
@@ -139,17 +87,7 @@ function hasThemeValues(theme: ButtonTheme | undefined): boolean {
|
|
|
139
87
|
|
|
140
88
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
141
89
|
(
|
|
142
|
-
{
|
|
143
|
-
className,
|
|
144
|
-
variant,
|
|
145
|
-
colorScheme,
|
|
146
|
-
size,
|
|
147
|
-
render,
|
|
148
|
-
nativeButton,
|
|
149
|
-
theme,
|
|
150
|
-
style,
|
|
151
|
-
...props
|
|
152
|
-
},
|
|
90
|
+
{ className, variant, size, render, nativeButton, theme, style, ...props },
|
|
153
91
|
ref,
|
|
154
92
|
) => {
|
|
155
93
|
// When render prop is provided, default nativeButton to false to suppress warnings
|
|
@@ -162,15 +100,13 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
162
100
|
const combinedStyles = hasTheme ? { ...themeStyles, ...style } : style;
|
|
163
101
|
|
|
164
102
|
// Resolve actual values for data attributes
|
|
165
|
-
const resolvedVariant = effectiveVariant ?? "
|
|
166
|
-
const
|
|
167
|
-
const resolvedSize = size ?? "default";
|
|
103
|
+
const resolvedVariant = effectiveVariant ?? "primary";
|
|
104
|
+
const resolvedSize = size ?? "md";
|
|
168
105
|
|
|
169
106
|
return (
|
|
170
107
|
<BaseButton
|
|
171
108
|
className={buttonVariants({
|
|
172
109
|
variant: effectiveVariant,
|
|
173
|
-
colorScheme,
|
|
174
110
|
size,
|
|
175
111
|
class: className,
|
|
176
112
|
})}
|
|
@@ -179,7 +115,6 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
179
115
|
nativeButton={isNativeButton}
|
|
180
116
|
style={combinedStyles}
|
|
181
117
|
data-variant={resolvedVariant}
|
|
182
|
-
data-color-scheme={resolvedColorScheme}
|
|
183
118
|
data-size={resolvedSize}
|
|
184
119
|
{...props}
|
|
185
120
|
/>
|
|
@@ -4,66 +4,112 @@ import { page } from "vitest/browser";
|
|
|
4
4
|
import { Button } from "./button";
|
|
5
5
|
|
|
6
6
|
describe("Button Visual Regression", () => {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// Solid variants
|
|
8
|
+
test("solid dark variant renders correctly", async () => {
|
|
9
|
+
render(
|
|
10
|
+
<Button variant="solid" colorScheme="dark">
|
|
11
|
+
Solid Dark Button
|
|
12
|
+
</Button>,
|
|
13
|
+
);
|
|
9
14
|
|
|
10
15
|
await expect(
|
|
11
|
-
page.getByRole("button", { name: "
|
|
12
|
-
).toMatchScreenshot("button-
|
|
16
|
+
page.getByRole("button", { name: "Solid Dark Button" }),
|
|
17
|
+
).toMatchScreenshot("button-solid-dark");
|
|
13
18
|
});
|
|
14
19
|
|
|
15
|
-
test("
|
|
16
|
-
render(
|
|
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
|
+
);
|
|
17
28
|
|
|
18
29
|
await expect(
|
|
19
|
-
page.getByRole("button", { name: "
|
|
20
|
-
).toMatchScreenshot("button-
|
|
30
|
+
page.getByRole("button", { name: "Solid Light Button" }),
|
|
31
|
+
).toMatchScreenshot("button-solid-light");
|
|
21
32
|
});
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
// Outline variants
|
|
35
|
+
test("outline dark variant renders correctly", async () => {
|
|
24
36
|
render(
|
|
25
|
-
<Button variant="
|
|
37
|
+
<Button variant="outline" colorScheme="dark">
|
|
38
|
+
Outline Dark Button
|
|
39
|
+
</Button>,
|
|
26
40
|
);
|
|
27
41
|
|
|
28
42
|
await expect(
|
|
29
|
-
page.getByRole("button", { name: "
|
|
30
|
-
).toMatchScreenshot("button-
|
|
43
|
+
page.getByRole("button", { name: "Outline Dark Button" }),
|
|
44
|
+
).toMatchScreenshot("button-outline-dark");
|
|
31
45
|
});
|
|
32
46
|
|
|
33
|
-
test("
|
|
47
|
+
test("outline light variant renders correctly", async () => {
|
|
34
48
|
render(
|
|
35
49
|
<div style={{ background: "#1a1a1a", padding: "20px" }}>
|
|
36
|
-
<Button variant="
|
|
50
|
+
<Button variant="outline" colorScheme="light">
|
|
51
|
+
Outline Light Button
|
|
52
|
+
</Button>
|
|
37
53
|
</div>,
|
|
38
54
|
);
|
|
39
55
|
|
|
40
56
|
await expect(
|
|
41
|
-
page.getByRole("button", { name: "
|
|
42
|
-
).toMatchScreenshot("button-
|
|
57
|
+
page.getByRole("button", { name: "Outline Light Button" }),
|
|
58
|
+
).toMatchScreenshot("button-outline-light");
|
|
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");
|
|
43
72
|
});
|
|
44
73
|
|
|
45
|
-
test("
|
|
74
|
+
test("ghost light variant renders correctly", async () => {
|
|
46
75
|
render(
|
|
47
76
|
<div style={{ background: "#1a1a1a", padding: "20px" }}>
|
|
48
|
-
<Button variant="
|
|
77
|
+
<Button variant="ghost" colorScheme="light">
|
|
78
|
+
Ghost Light Button
|
|
79
|
+
</Button>
|
|
49
80
|
</div>,
|
|
50
81
|
);
|
|
51
82
|
|
|
52
83
|
await expect(
|
|
53
|
-
page.getByRole("button", { name: "
|
|
54
|
-
).toMatchScreenshot("button-
|
|
84
|
+
page.getByRole("button", { name: "Ghost Light Button" }),
|
|
85
|
+
).toMatchScreenshot("button-ghost-light");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Subtle variants
|
|
89
|
+
test("subtle dark variant renders correctly", async () => {
|
|
90
|
+
render(
|
|
91
|
+
<Button variant="subtle" colorScheme="dark">
|
|
92
|
+
Subtle Dark Button
|
|
93
|
+
</Button>,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await expect(
|
|
97
|
+
page.getByRole("button", { name: "Subtle Dark Button" }),
|
|
98
|
+
).toMatchScreenshot("button-subtle-dark");
|
|
55
99
|
});
|
|
56
100
|
|
|
57
|
-
test("
|
|
101
|
+
test("subtle light variant renders correctly", async () => {
|
|
58
102
|
render(
|
|
59
103
|
<div style={{ background: "#1a1a1a", padding: "20px" }}>
|
|
60
|
-
<Button variant="
|
|
104
|
+
<Button variant="subtle" colorScheme="light">
|
|
105
|
+
Subtle Light Button
|
|
106
|
+
</Button>
|
|
61
107
|
</div>,
|
|
62
108
|
);
|
|
63
109
|
|
|
64
110
|
await expect(
|
|
65
|
-
page.getByRole("button", { name: "
|
|
66
|
-
).toMatchScreenshot("button-
|
|
111
|
+
page.getByRole("button", { name: "Subtle Light Button" }),
|
|
112
|
+
).toMatchScreenshot("button-subtle-light");
|
|
67
113
|
});
|
|
68
114
|
|
|
69
115
|
// Size variants
|