@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.
- package/dist/component-registry.md +46 -19
- package/dist/components/atoms/accordion/accordion.d.ts +7 -7
- 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/banner/banner.d.ts +9 -9
- package/dist/components/sections/faq-section/faq-section.d.ts +1 -1
- package/dist/components/sections/hero/hero.d.ts +115 -18
- 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 +9 -9
- package/dist/components/sections/two-column-section/two-column-section.d.ts +7 -21
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11075 -7841
- package/dist/index.js.map +1 -1
- package/dist/lib/form-control.d.ts +105 -0
- package/dist/tokens.css +2144 -17341
- package/package.json +1 -1
- package/src/components/atoms/accordion/accordion.test.tsx +18 -20
- package/src/components/atoms/accordion/accordion.tsx +19 -17
- package/src/components/atoms/background/background.test.tsx +2 -2
- package/src/components/atoms/background/background.tsx +77 -96
- 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 +80 -188
- 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 +5 -3
- 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/banner/banner.stories.tsx +5 -1
- package/src/components/sections/banner/banner.tsx +10 -10
- package/src/components/sections/card-grid/card-grid.tsx +1 -1
- package/src/components/sections/faq-section/faq-section.stories.tsx +7 -7
- package/src/components/sections/faq-section/faq-section.tsx +5 -5
- package/src/components/sections/hero/hero.test.tsx +5 -5
- package/src/components/sections/hero/hero.tsx +33 -51
- 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.stories.tsx +31 -7
- package/src/components/sections/tout/tout.test.tsx +1 -1
- package/src/components/sections/tout/tout.tsx +8 -10
- package/src/components/sections/two-column-section/two-column-section.stories.tsx +11 -11
- package/src/components/sections/two-column-section/two-column-section.tsx +16 -10
- 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
|
@@ -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-strong 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
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useRender } from "@base-ui-components/react/use-render";
|
|
4
|
-
import * as React from "react";
|
|
5
4
|
import { tv, type VariantProps } from "tailwind-variants";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
|
-
* IconButton component based on Figma
|
|
7
|
+
* IconButton component based on Figma Button component (icon-only variant)
|
|
9
8
|
*
|
|
10
9
|
* **IMPORTANT: Accessibility Requirement**
|
|
11
10
|
* Icon-only buttons MUST have an accessible label. Provide one of:
|
|
@@ -21,219 +20,112 @@ import { tv, type VariantProps } from "tailwind-variants";
|
|
|
21
20
|
* </IconButton>
|
|
22
21
|
* ```
|
|
23
22
|
*
|
|
24
|
-
* Variants:
|
|
25
|
-
* -
|
|
26
|
-
* - outline: Outlined button
|
|
27
|
-
* -
|
|
28
|
-
* -
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* - dark: Dark colors for use on light backgrounds (default)
|
|
32
|
-
* - light: Light colors for use on dark backgrounds
|
|
23
|
+
* Variants (matches Figma):
|
|
24
|
+
* - primary: Filled brand button (indigo background)
|
|
25
|
+
* - primary-outline: Outlined brand button (indigo border/text)
|
|
26
|
+
* - secondary: Filled neutral button (white background, for dark backgrounds)
|
|
27
|
+
* - secondary-outline: Outlined neutral button (white border/text, for dark backgrounds)
|
|
28
|
+
* - ghost: Transparent button with subtle hover (for light backgrounds)
|
|
29
|
+
* - ghost-inverse: Transparent button with subtle hover (for dark backgrounds)
|
|
33
30
|
*
|
|
34
31
|
* Sizes:
|
|
35
|
-
* - sm: Small (
|
|
36
|
-
* -
|
|
37
|
-
* - lg: Large (
|
|
32
|
+
* - sm: Small (28x28)
|
|
33
|
+
* - md: Medium (40x40) - default
|
|
34
|
+
* - lg: Large (56x56)
|
|
38
35
|
*
|
|
39
36
|
* Rounded:
|
|
40
|
-
* - default: Standard border radius
|
|
41
|
-
* - sm: Smaller border radius
|
|
37
|
+
* - default: Standard border radius (matches size)
|
|
42
38
|
* - full: Fully circular
|
|
43
39
|
*/
|
|
44
40
|
const iconButtonVariants = tv({
|
|
45
41
|
base: "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
42
|
variants: {
|
|
47
43
|
variant: {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
dark
|
|
55
|
-
|
|
44
|
+
// Primary - filled brand button
|
|
45
|
+
primary:
|
|
46
|
+
"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",
|
|
47
|
+
// Primary Outline - outlined brand button
|
|
48
|
+
"primary-outline":
|
|
49
|
+
"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",
|
|
50
|
+
// Secondary - filled neutral button (for dark backgrounds)
|
|
51
|
+
secondary:
|
|
52
|
+
"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",
|
|
53
|
+
// Secondary Outline - outlined neutral button (for dark backgrounds)
|
|
54
|
+
"secondary-outline":
|
|
55
|
+
"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",
|
|
56
|
+
// Ghost - transparent button (for light backgrounds)
|
|
57
|
+
ghost:
|
|
58
|
+
"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",
|
|
59
|
+
// Ghost Inverse - transparent button (for dark backgrounds)
|
|
60
|
+
"ghost-inverse":
|
|
61
|
+
"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",
|
|
56
62
|
},
|
|
57
63
|
size: {
|
|
58
|
-
sm: "size-
|
|
59
|
-
|
|
60
|
-
lg: "size-
|
|
64
|
+
sm: "size-28 rounded-4",
|
|
65
|
+
md: "size-40 rounded-6",
|
|
66
|
+
lg: "size-56 rounded-10",
|
|
61
67
|
},
|
|
62
68
|
rounded: {
|
|
63
|
-
default: "
|
|
64
|
-
sm: "rounded-radius-10",
|
|
69
|
+
default: "",
|
|
65
70
|
full: "rounded-full",
|
|
66
71
|
},
|
|
67
72
|
},
|
|
68
|
-
compoundVariants: [
|
|
69
|
-
// Solid + Dark (for light backgrounds)
|
|
70
|
-
{
|
|
71
|
-
variant: "solid",
|
|
72
|
-
colorScheme: "dark",
|
|
73
|
-
class:
|
|
74
|
-
"bg-gray-1200 text-gray-100 hover:bg-gray-1100 active:bg-gray-1000 focus-visible:ring-gray-1000",
|
|
75
|
-
},
|
|
76
|
-
// Solid + Light (for dark backgrounds)
|
|
77
|
-
{
|
|
78
|
-
variant: "solid",
|
|
79
|
-
colorScheme: "light",
|
|
80
|
-
class:
|
|
81
|
-
"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",
|
|
82
|
-
},
|
|
83
|
-
// Outline + Dark (for light backgrounds)
|
|
84
|
-
{
|
|
85
|
-
variant: "outline",
|
|
86
|
-
colorScheme: "dark",
|
|
87
|
-
class:
|
|
88
|
-
"border-alpha-black-30 text-gray-1000 hover:bg-alpha-black-5 active:bg-alpha-black-10 focus-visible:ring-gray-1000",
|
|
89
|
-
},
|
|
90
|
-
// Outline + Light (for dark backgrounds)
|
|
91
|
-
{
|
|
92
|
-
variant: "outline",
|
|
93
|
-
colorScheme: "light",
|
|
94
|
-
class:
|
|
95
|
-
"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",
|
|
96
|
-
},
|
|
97
|
-
// Ghost + Dark (for light backgrounds)
|
|
98
|
-
{
|
|
99
|
-
variant: "ghost",
|
|
100
|
-
colorScheme: "dark",
|
|
101
|
-
class:
|
|
102
|
-
"text-gray-700 hover:text-gray-900 hover:bg-alpha-black-5 active:bg-alpha-black-10 focus-visible:ring-gray-1000",
|
|
103
|
-
},
|
|
104
|
-
// Ghost + Light (for dark backgrounds)
|
|
105
|
-
{
|
|
106
|
-
variant: "ghost",
|
|
107
|
-
colorScheme: "light",
|
|
108
|
-
class:
|
|
109
|
-
"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",
|
|
110
|
-
},
|
|
111
|
-
// Subtle + Dark (for light backgrounds)
|
|
112
|
-
{
|
|
113
|
-
variant: "subtle",
|
|
114
|
-
colorScheme: "dark",
|
|
115
|
-
class:
|
|
116
|
-
"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",
|
|
117
|
-
},
|
|
118
|
-
// Subtle + Light (for dark backgrounds)
|
|
119
|
-
{
|
|
120
|
-
variant: "subtle",
|
|
121
|
-
colorScheme: "light",
|
|
122
|
-
class:
|
|
123
|
-
"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",
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
73
|
defaultVariants: {
|
|
127
|
-
variant: "
|
|
128
|
-
|
|
129
|
-
size: "default",
|
|
74
|
+
variant: "primary",
|
|
75
|
+
size: "md",
|
|
130
76
|
rounded: "default",
|
|
131
77
|
},
|
|
132
78
|
});
|
|
133
79
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
* <LinkIcon />
|
|
145
|
-
* </IconButton>
|
|
146
|
-
*
|
|
147
|
-
* // Render with custom element
|
|
148
|
-
* <IconButton render={(props) => <Link {...props} to="/home" />} aria-label="Home">
|
|
149
|
-
* <HomeIcon />
|
|
150
|
-
* </IconButton>
|
|
151
|
-
* ```
|
|
152
|
-
*/
|
|
153
|
-
render?:
|
|
154
|
-
| React.ReactElement
|
|
155
|
-
| ((
|
|
156
|
-
props: React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
157
|
-
) => React.ReactElement);
|
|
158
|
-
/**
|
|
159
|
-
* @deprecated Use `render` prop instead for element composition.
|
|
160
|
-
* @example
|
|
161
|
-
* ```tsx
|
|
162
|
-
* // Old (deprecated)
|
|
163
|
-
* <IconButton asChild><a href="/link">...</a></IconButton>
|
|
164
|
-
*
|
|
165
|
-
* // New (recommended)
|
|
166
|
-
* <IconButton render={<a href="/link" />}>...</IconButton>
|
|
167
|
-
* ```
|
|
168
|
-
*/
|
|
169
|
-
asChild?: boolean;
|
|
80
|
+
interface IconButtonState extends Record<string, unknown> {
|
|
81
|
+
variant:
|
|
82
|
+
| "primary"
|
|
83
|
+
| "primary-outline"
|
|
84
|
+
| "secondary"
|
|
85
|
+
| "secondary-outline"
|
|
86
|
+
| "ghost"
|
|
87
|
+
| "ghost-inverse";
|
|
88
|
+
size: "sm" | "md" | "lg";
|
|
89
|
+
rounded: "default" | "full";
|
|
170
90
|
}
|
|
171
91
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
className,
|
|
176
|
-
variant,
|
|
177
|
-
colorScheme,
|
|
178
|
-
size,
|
|
179
|
-
rounded,
|
|
180
|
-
render,
|
|
181
|
-
asChild,
|
|
182
|
-
...props
|
|
183
|
-
},
|
|
184
|
-
ref,
|
|
185
|
-
) => {
|
|
186
|
-
// Development warnings
|
|
187
|
-
React.useEffect(() => {
|
|
188
|
-
if (import.meta.env?.DEV) {
|
|
189
|
-
// Warn about missing accessible label
|
|
190
|
-
const hasAccessibleLabel =
|
|
191
|
-
props["aria-label"] || props["aria-labelledby"] || props.title;
|
|
192
|
-
if (!hasAccessibleLabel) {
|
|
193
|
-
console.warn(
|
|
194
|
-
"IconButton: Missing accessible label. Icon-only buttons must have an aria-label, aria-labelledby, or title attribute for screen reader users.",
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
// Warn about deprecated asChild prop
|
|
198
|
-
if (asChild !== undefined) {
|
|
199
|
-
console.warn(
|
|
200
|
-
'IconButton: The "asChild" prop is deprecated. Use the "render" prop instead for element composition.\n' +
|
|
201
|
-
'Example: <IconButton render={<a href="/link" />}>...</IconButton>',
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}, [props["aria-label"], props["aria-labelledby"], props.title, asChild]);
|
|
92
|
+
export interface IconButtonProps
|
|
93
|
+
extends useRender.ComponentProps<"button", IconButtonState>,
|
|
94
|
+
VariantProps<typeof iconButtonVariants> {}
|
|
206
95
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
96
|
+
function IconButton(props: IconButtonProps) {
|
|
97
|
+
const {
|
|
98
|
+
className,
|
|
99
|
+
variant = "primary",
|
|
100
|
+
size = "md",
|
|
101
|
+
rounded = "default",
|
|
102
|
+
render,
|
|
103
|
+
...otherProps
|
|
104
|
+
} = props;
|
|
212
105
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
rounded,
|
|
219
|
-
class: className,
|
|
220
|
-
}),
|
|
221
|
-
"data-variant": resolvedVariant,
|
|
222
|
-
"data-color-scheme": resolvedColorScheme,
|
|
223
|
-
"data-size": resolvedSize,
|
|
224
|
-
"data-rounded": resolvedRounded,
|
|
225
|
-
...props,
|
|
226
|
-
};
|
|
106
|
+
const state: IconButtonState = {
|
|
107
|
+
variant,
|
|
108
|
+
size,
|
|
109
|
+
rounded,
|
|
110
|
+
};
|
|
227
111
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
112
|
+
const buttonClassName = iconButtonVariants({
|
|
113
|
+
variant,
|
|
114
|
+
size,
|
|
115
|
+
rounded,
|
|
116
|
+
class: className,
|
|
117
|
+
});
|
|
233
118
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
119
|
+
return useRender<IconButtonState, HTMLButtonElement>({
|
|
120
|
+
render,
|
|
121
|
+
state,
|
|
122
|
+
props: {
|
|
123
|
+
type: "button",
|
|
124
|
+
className: buttonClassName,
|
|
125
|
+
...otherProps,
|
|
126
|
+
},
|
|
127
|
+
defaultTagName: "button",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
238
130
|
|
|
239
131
|
export { IconButton, iconButtonVariants };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { Input, type InputProps, inputVariants } from "./input";
|
|
2
|
+
export {
|
|
3
|
+
InputGroup,
|
|
4
|
+
InputGroupAddon,
|
|
5
|
+
type InputGroupAddonProps,
|
|
6
|
+
InputGroupButton,
|
|
7
|
+
type InputGroupButtonProps,
|
|
8
|
+
InputGroupInput,
|
|
9
|
+
type InputGroupInputProps,
|
|
10
|
+
type InputGroupProps,
|
|
11
|
+
InputGroupText,
|
|
12
|
+
InputGroupTextarea,
|
|
13
|
+
type InputGroupTextareaProps,
|
|
14
|
+
type InputGroupTextProps,
|
|
15
|
+
inputGroupAddonVariants,
|
|
16
|
+
inputGroupVariants,
|
|
17
|
+
} from "./input-group";
|