@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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { page, userEvent } from "vitest/browser";
|
|
3
|
+
import { render } from "vitest-browser-react";
|
|
4
|
+
import { Input } from "./input";
|
|
5
|
+
|
|
6
|
+
describe("Input", () => {
|
|
7
|
+
describe("Accessibility", () => {
|
|
8
|
+
test("has correct textbox role", async () => {
|
|
9
|
+
render(<Input placeholder="Enter text" />);
|
|
10
|
+
await expect.element(page.getByRole("textbox")).toBeInTheDocument();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("is focusable via keyboard", async () => {
|
|
14
|
+
render(<Input placeholder="Focusable" />);
|
|
15
|
+
await userEvent.keyboard("{Tab}");
|
|
16
|
+
await expect.element(page.getByRole("textbox")).toHaveFocus();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("disabled input has disabled attribute", async () => {
|
|
20
|
+
render(<Input disabled placeholder="Disabled" />);
|
|
21
|
+
await expect.element(page.getByRole("textbox")).toBeDisabled();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("disabled input is not focusable", async () => {
|
|
25
|
+
render(
|
|
26
|
+
<>
|
|
27
|
+
<Input disabled placeholder="Disabled" />
|
|
28
|
+
<Input placeholder="After" />
|
|
29
|
+
</>,
|
|
30
|
+
);
|
|
31
|
+
await userEvent.keyboard("{Tab}");
|
|
32
|
+
// Focus should skip the disabled input and go to the next one
|
|
33
|
+
await expect.element(page.getByPlaceholder("After")).toHaveFocus();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("input with aria-label has accessible name", async () => {
|
|
37
|
+
render(<Input aria-label="Search query" />);
|
|
38
|
+
await expect
|
|
39
|
+
.element(page.getByRole("textbox", { name: "Search query" }))
|
|
40
|
+
.toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("error state sets aria-invalid", async () => {
|
|
44
|
+
render(<Input error placeholder="Error input" />);
|
|
45
|
+
await expect
|
|
46
|
+
.element(page.getByRole("textbox"))
|
|
47
|
+
.toHaveAttribute("aria-invalid", "true");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("non-error state does not have aria-invalid", async () => {
|
|
51
|
+
render(<Input placeholder="Normal input" />);
|
|
52
|
+
const input = page.getByRole("textbox");
|
|
53
|
+
await expect.element(input).not.toHaveAttribute("aria-invalid");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("Interactions", () => {
|
|
58
|
+
test("accepts text input", async () => {
|
|
59
|
+
render(<Input placeholder="Type here" />);
|
|
60
|
+
const input = page.getByRole("textbox");
|
|
61
|
+
await input.fill("Hello World");
|
|
62
|
+
await expect.element(input).toHaveValue("Hello World");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("calls onChange when text is entered", async () => {
|
|
66
|
+
const handleChange = vi.fn();
|
|
67
|
+
render(<Input onChange={handleChange} placeholder="Type here" />);
|
|
68
|
+
await page.getByRole("textbox").fill("Test");
|
|
69
|
+
expect(handleChange).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("calls onFocus when focused", async () => {
|
|
73
|
+
const handleFocus = vi.fn();
|
|
74
|
+
render(<Input onFocus={handleFocus} placeholder="Focus me" />);
|
|
75
|
+
await page.getByRole("textbox").click();
|
|
76
|
+
expect(handleFocus).toHaveBeenCalledOnce();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("calls onBlur when focus is lost", async () => {
|
|
80
|
+
const handleBlur = vi.fn();
|
|
81
|
+
render(
|
|
82
|
+
<>
|
|
83
|
+
<Input onBlur={handleBlur} placeholder="Blur me" />
|
|
84
|
+
<button type="button">Other</button>
|
|
85
|
+
</>,
|
|
86
|
+
);
|
|
87
|
+
await page.getByRole("textbox").click();
|
|
88
|
+
await page.getByRole("button").click();
|
|
89
|
+
expect(handleBlur).toHaveBeenCalledOnce();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("respects maxLength attribute", async () => {
|
|
93
|
+
render(<Input maxLength={5} placeholder="Max 5 chars" />);
|
|
94
|
+
const input = page.getByRole("textbox");
|
|
95
|
+
await input.fill("1234567890");
|
|
96
|
+
await expect.element(input).toHaveValue("12345");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("respects readonly attribute", async () => {
|
|
100
|
+
render(<Input readOnly defaultValue="Read only" />);
|
|
101
|
+
const input = page.getByRole("textbox");
|
|
102
|
+
await expect.element(input).toHaveAttribute("readonly", "");
|
|
103
|
+
await expect.element(input).toHaveValue("Read only");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("Variants", () => {
|
|
108
|
+
test("applies default size classes", async () => {
|
|
109
|
+
render(<Input placeholder="Default" />);
|
|
110
|
+
const input = page.getByRole("textbox");
|
|
111
|
+
await expect.element(input).toHaveAttribute("data-size", "default");
|
|
112
|
+
await expect.element(input).toHaveClass(/h-48/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("applies small size classes", async () => {
|
|
116
|
+
render(<Input size="sm" placeholder="Small" />);
|
|
117
|
+
const input = page.getByRole("textbox");
|
|
118
|
+
await expect.element(input).toHaveAttribute("data-size", "sm");
|
|
119
|
+
await expect.element(input).toHaveClass(/h-36/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("applies large size classes", async () => {
|
|
123
|
+
render(<Input size="lg" placeholder="Large" />);
|
|
124
|
+
const input = page.getByRole("textbox");
|
|
125
|
+
await expect.element(input).toHaveAttribute("data-size", "lg");
|
|
126
|
+
await expect.element(input).toHaveClass(/h-56/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("applies error state classes", async () => {
|
|
130
|
+
render(<Input error placeholder="Error" />);
|
|
131
|
+
const input = page.getByRole("textbox");
|
|
132
|
+
await expect.element(input).toHaveAttribute("data-error", "true");
|
|
133
|
+
await expect.element(input).toHaveClass(/border-ui-error-color/);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("applies semantic token classes for background", async () => {
|
|
137
|
+
render(<Input placeholder="Default" />);
|
|
138
|
+
const input = page.getByRole("textbox");
|
|
139
|
+
await expect.element(input).toHaveClass(/bg-ui-control-background/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("applies semantic token classes for border", async () => {
|
|
143
|
+
render(<Input placeholder="Default" />);
|
|
144
|
+
const input = page.getByRole("textbox");
|
|
145
|
+
await expect.element(input).toHaveClass(/border-ui-color-border/);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("Input Types", () => {
|
|
150
|
+
test("renders email type", async () => {
|
|
151
|
+
render(<Input type="email" placeholder="Email" />);
|
|
152
|
+
await expect
|
|
153
|
+
.element(page.getByRole("textbox"))
|
|
154
|
+
.toHaveAttribute("type", "email");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("renders password type", async () => {
|
|
158
|
+
render(<Input type="password" placeholder="Password" />);
|
|
159
|
+
// Password inputs don't have textbox role
|
|
160
|
+
const input = page.getByPlaceholder("Password");
|
|
161
|
+
await expect.element(input).toHaveAttribute("type", "password");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("renders number type", async () => {
|
|
165
|
+
render(<Input type="number" placeholder="Number" />);
|
|
166
|
+
const input = page.getByRole("spinbutton");
|
|
167
|
+
await expect.element(input).toHaveAttribute("type", "number");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("renders search type", async () => {
|
|
171
|
+
render(<Input type="search" placeholder="Search" />);
|
|
172
|
+
const input = page.getByRole("searchbox");
|
|
173
|
+
await expect.element(input).toHaveAttribute("type", "search");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("defaults to text type", async () => {
|
|
177
|
+
render(<Input placeholder="Default type" />);
|
|
178
|
+
await expect
|
|
179
|
+
.element(page.getByRole("textbox"))
|
|
180
|
+
.toHaveAttribute("type", "text");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
5
|
+
import {
|
|
6
|
+
formControlBase,
|
|
7
|
+
formControlError,
|
|
8
|
+
formControlSizes,
|
|
9
|
+
} from "@/lib/form-control";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Input variants for styling based on Figma BaseKit / Interface / Input
|
|
14
|
+
*
|
|
15
|
+
* States (handled via CSS pseudo-classes and props):
|
|
16
|
+
* - Default: White background, subtle border
|
|
17
|
+
* - Hover: Light gray background (via :hover)
|
|
18
|
+
* - Focus: Accent border with focus ring (via :focus-visible)
|
|
19
|
+
* - Error: Error border color (via error prop)
|
|
20
|
+
* - Disabled: Disabled background (via :disabled)
|
|
21
|
+
*
|
|
22
|
+
* Sizes:
|
|
23
|
+
* - sm: Smaller height and padding
|
|
24
|
+
* - default: Standard 48px height
|
|
25
|
+
* - lg: Larger height and padding
|
|
26
|
+
*/
|
|
27
|
+
const inputVariants = tv({
|
|
28
|
+
base: [
|
|
29
|
+
...formControlBase,
|
|
30
|
+
// Input-specific: Placeholder styling
|
|
31
|
+
"placeholder:text-text-muted",
|
|
32
|
+
],
|
|
33
|
+
variants: {
|
|
34
|
+
size: formControlSizes,
|
|
35
|
+
error: {
|
|
36
|
+
...formControlError,
|
|
37
|
+
true: `${formControlError.true} placeholder:text-ui-error-color/60`,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
defaultVariants: {
|
|
41
|
+
size: "default",
|
|
42
|
+
error: false,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export interface InputProps
|
|
47
|
+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
|
|
48
|
+
VariantProps<typeof inputVariants> {
|
|
49
|
+
/**
|
|
50
|
+
* Whether the input is in an error state
|
|
51
|
+
*/
|
|
52
|
+
error?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Input component based on Figma BaseKit / Interface / Input
|
|
57
|
+
*
|
|
58
|
+
* A styled text input with support for various states:
|
|
59
|
+
* - Default, hover, focus, error, and disabled states
|
|
60
|
+
* - Three size variants: sm, default, lg
|
|
61
|
+
*
|
|
62
|
+
* Uses semantic UI tokens for theming support.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* // Basic usage
|
|
67
|
+
* <Input placeholder="Enter your email" />
|
|
68
|
+
*
|
|
69
|
+
* // With error state
|
|
70
|
+
* <Input error placeholder="Invalid input" />
|
|
71
|
+
*
|
|
72
|
+
* // Different sizes
|
|
73
|
+
* <Input size="sm" placeholder="Small" />
|
|
74
|
+
* <Input size="lg" placeholder="Large" />
|
|
75
|
+
*
|
|
76
|
+
* // Disabled
|
|
77
|
+
* <Input disabled placeholder="Disabled input" />
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
81
|
+
({ className, size, error, type = "text", ...props }, ref) => {
|
|
82
|
+
return (
|
|
83
|
+
<input
|
|
84
|
+
ref={ref}
|
|
85
|
+
type={type}
|
|
86
|
+
aria-invalid={error || undefined}
|
|
87
|
+
className={cn(inputVariants({ size, error, class: className }))}
|
|
88
|
+
data-size={size ?? "default"}
|
|
89
|
+
data-error={error ?? false}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
Input.displayName = "Input";
|
|
96
|
+
|
|
97
|
+
export { Input, inputVariants };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Select,
|
|
3
|
+
SelectGroup,
|
|
4
|
+
SelectGroupLabel,
|
|
5
|
+
type SelectGroupLabelProps,
|
|
6
|
+
type SelectGroupProps,
|
|
7
|
+
SelectOption,
|
|
8
|
+
type SelectOptionProps,
|
|
9
|
+
SelectPopup,
|
|
10
|
+
type SelectPopupProps,
|
|
11
|
+
type SelectProps,
|
|
12
|
+
SelectRoot,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
type SelectTriggerProps,
|
|
15
|
+
selectOptionVariants,
|
|
16
|
+
selectPopupVariants,
|
|
17
|
+
selectTriggerVariants,
|
|
18
|
+
} from "./select";
|