@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.
Files changed (58) hide show
  1. package/dist/components/atoms/background/background.d.ts +13 -27
  2. package/dist/components/atoms/button/button.d.ts +55 -71
  3. package/dist/components/atoms/button/icon-button.d.ts +62 -110
  4. package/dist/components/atoms/input/input-group.d.ts +278 -0
  5. package/dist/components/atoms/input/input.d.ts +121 -0
  6. package/dist/components/atoms/select/select.d.ts +131 -0
  7. package/dist/components/organisms/card/card.d.ts +2 -2
  8. package/dist/components/sections/prose/prose.d.ts +3 -3
  9. package/dist/components/sections/river/river.d.ts +1 -1
  10. package/dist/components/sections/tout/tout.d.ts +1 -1
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.js +11034 -7824
  13. package/dist/index.js.map +1 -1
  14. package/dist/lib/form-control.d.ts +105 -0
  15. package/dist/tokens.css +2132 -17329
  16. package/package.json +1 -1
  17. package/src/components/atoms/background/background.tsx +71 -109
  18. package/src/components/atoms/button/button.stories.tsx +42 -0
  19. package/src/components/atoms/button/button.test.tsx +1 -1
  20. package/src/components/atoms/button/button.tsx +38 -103
  21. package/src/components/atoms/button/button.visual.test.tsx +70 -24
  22. package/src/components/atoms/button/icon-button.tsx +81 -224
  23. package/src/components/atoms/input/index.ts +17 -0
  24. package/src/components/atoms/input/input-group.stories.tsx +650 -0
  25. package/src/components/atoms/input/input-group.test.tsx +376 -0
  26. package/src/components/atoms/input/input-group.tsx +384 -0
  27. package/src/components/atoms/input/input.stories.tsx +232 -0
  28. package/src/components/atoms/input/input.test.tsx +183 -0
  29. package/src/components/atoms/input/input.tsx +97 -0
  30. package/src/components/atoms/select/index.ts +18 -0
  31. package/src/components/atoms/select/select.stories.tsx +455 -0
  32. package/src/components/atoms/select/select.tsx +320 -0
  33. package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +2 -6
  34. package/src/components/foundation/typography/typography.stories.tsx +401 -0
  35. package/src/components/organisms/card/card.stories.tsx +11 -11
  36. package/src/components/organisms/card/card.test.tsx +1 -1
  37. package/src/components/organisms/card/card.tsx +2 -2
  38. package/src/components/organisms/card/card.visual.test.tsx +6 -6
  39. package/src/components/organisms/navbar/navbar.tsx +2 -2
  40. package/src/components/organisms/navbar/navbar.visual.test.tsx +2 -2
  41. package/src/components/sections/card-grid/card-grid.tsx +1 -1
  42. package/src/components/sections/faq-section/faq-section.tsx +2 -2
  43. package/src/components/sections/hero/hero.test.tsx +5 -5
  44. package/src/components/sections/prose/prose.test.tsx +2 -2
  45. package/src/components/sections/prose/prose.tsx +4 -5
  46. package/src/components/sections/river/river.stories.tsx +8 -8
  47. package/src/components/sections/river/river.test.tsx +1 -1
  48. package/src/components/sections/river/river.tsx +2 -4
  49. package/src/components/sections/tout/tout.test.tsx +1 -1
  50. package/src/components/sections/tout/tout.tsx +2 -2
  51. package/src/index.ts +41 -0
  52. package/src/lib/form-control.ts +69 -0
  53. package/src/stories/Introduction.mdx +29 -15
  54. package/src/stories/ThemeProvider.stories.tsx +1 -3
  55. package/src/stories/TokenShowcase.stories.tsx +0 -19
  56. package/src/stories/TokenShowcase.tsx +714 -1366
  57. package/src/styles.css +3 -0
  58. 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";