@nationaldesignstudio/react 0.2.0 → 0.5.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 (97) hide show
  1. package/dist/component-registry.md +1310 -127
  2. package/dist/components/atoms/background/background.d.ts +13 -27
  3. package/dist/components/atoms/button/button.d.ts +64 -72
  4. package/dist/components/atoms/button/button.figma.d.ts +1 -0
  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/popover/popover.d.ts +195 -0
  9. package/dist/components/atoms/select/select.d.ts +131 -0
  10. package/dist/components/atoms/tooltip/tooltip.d.ts +161 -0
  11. package/dist/components/organisms/card/card.d.ts +3 -3
  12. package/dist/components/sections/hero/hero.d.ts +2 -2
  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 +4 -4
  16. package/dist/components/shared/floating-arrow.d.ts +34 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.js +13935 -7622
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/form-control.d.ts +106 -0
  21. package/dist/tokens.css +4725 -19065
  22. package/package.json +2 -1
  23. package/src/components/atoms/accordion/accordion.stories.tsx +1 -1
  24. package/src/components/atoms/accordion/accordion.tsx +2 -2
  25. package/src/components/atoms/background/background.tsx +71 -109
  26. package/src/components/atoms/button/button.figma.tsx +37 -0
  27. package/src/components/atoms/button/button.stories.tsx +253 -115
  28. package/src/components/atoms/button/button.test.tsx +289 -5
  29. package/src/components/atoms/button/button.tsx +40 -101
  30. package/src/components/atoms/button/button.visual.test.tsx +28 -32
  31. package/src/components/atoms/button/icon-button.stories.tsx +44 -101
  32. package/src/components/atoms/button/icon-button.test.tsx +26 -94
  33. package/src/components/atoms/button/icon-button.tsx +81 -224
  34. package/src/components/atoms/input/index.ts +17 -0
  35. package/src/components/atoms/input/input-group.stories.tsx +646 -0
  36. package/src/components/atoms/input/input-group.test.tsx +362 -0
  37. package/src/components/atoms/input/input-group.tsx +409 -0
  38. package/src/components/atoms/input/input.stories.tsx +228 -0
  39. package/src/components/atoms/input/input.test.tsx +167 -0
  40. package/src/components/atoms/input/input.tsx +104 -0
  41. package/src/components/atoms/pager-control/pager-control.stories.tsx +6 -8
  42. package/src/components/atoms/pager-control/pager-control.tsx +12 -12
  43. package/src/components/atoms/popover/index.ts +30 -0
  44. package/src/components/atoms/popover/popover.stories.tsx +531 -0
  45. package/src/components/atoms/popover/popover.test.tsx +486 -0
  46. package/src/components/atoms/popover/popover.tsx +488 -0
  47. package/src/components/atoms/select/index.ts +18 -0
  48. package/src/components/atoms/select/select.stories.tsx +455 -0
  49. package/src/components/atoms/select/select.tsx +324 -0
  50. package/src/components/atoms/tooltip/index.ts +24 -0
  51. package/src/components/atoms/tooltip/tooltip.stories.tsx +348 -0
  52. package/src/components/atoms/tooltip/tooltip.test.tsx +363 -0
  53. package/src/components/atoms/tooltip/tooltip.tsx +347 -0
  54. package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +8 -17
  55. package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +3 -3
  56. package/src/components/foundation/typography/typography.stories.tsx +401 -0
  57. package/src/components/organisms/card/card.stories.tsx +19 -19
  58. package/src/components/organisms/card/card.test.tsx +1 -1
  59. package/src/components/organisms/card/card.tsx +3 -3
  60. package/src/components/organisms/card/card.visual.test.tsx +11 -11
  61. package/src/components/organisms/navbar/navbar.tsx +2 -2
  62. package/src/components/organisms/navbar/navbar.visual.test.tsx +2 -2
  63. package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +2 -2
  64. package/src/components/sections/banner/banner.stories.tsx +1 -5
  65. package/src/components/sections/banner/banner.test.tsx +2 -2
  66. package/src/components/sections/banner/banner.tsx +6 -6
  67. package/src/components/sections/card-grid/card-grid.tsx +5 -5
  68. package/src/components/sections/faq-section/faq-section.tsx +2 -2
  69. package/src/components/sections/hero/hero.stories.tsx +7 -7
  70. package/src/components/sections/hero/hero.test.tsx +5 -5
  71. package/src/components/sections/hero/hero.tsx +10 -11
  72. package/src/components/sections/prose/prose.test.tsx +2 -2
  73. package/src/components/sections/prose/prose.tsx +6 -7
  74. package/src/components/sections/river/river.stories.tsx +8 -8
  75. package/src/components/sections/river/river.test.tsx +4 -4
  76. package/src/components/sections/river/river.tsx +8 -16
  77. package/src/components/sections/tout/tout.stories.tsx +7 -31
  78. package/src/components/sections/tout/tout.test.tsx +1 -1
  79. package/src/components/sections/tout/tout.tsx +11 -11
  80. package/src/components/sections/two-column-section/two-column-section.tsx +7 -9
  81. package/src/components/shared/floating-arrow.tsx +78 -0
  82. package/src/components/shared/index.ts +5 -0
  83. package/src/index.ts +98 -0
  84. package/src/lib/form-control.ts +71 -0
  85. package/src/stories/grid-system.stories.tsx +309 -0
  86. package/src/stories/{Introduction.mdx → introduction.mdx} +29 -15
  87. package/src/stories/{ThemeProvider.stories.tsx → theme-provider.stories.tsx} +8 -22
  88. package/src/stories/{TokenShowcase.stories.tsx → token-showcase.stories.tsx} +1 -20
  89. package/src/stories/token-showcase.tsx +777 -0
  90. package/src/styles.css +3 -0
  91. package/src/tests/token-resolution.test.tsx +298 -0
  92. package/src/theme/hooks.ts +1 -1
  93. package/src/theme/index.ts +1 -1
  94. package/src/theme/theme-provider.test.tsx +270 -0
  95. package/src/theme/{ThemeProvider.tsx → theme-provider.tsx} +18 -2
  96. package/src/stories/GridSystem.stories.tsx +0 -84
  97. package/src/stories/TokenShowcase.tsx +0 -1429
@@ -0,0 +1,167 @@
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", async () => {
109
+ render(<Input placeholder="Default" />);
110
+ const input = page.getByRole("textbox");
111
+ await expect.element(input).toHaveAttribute("data-size", "default");
112
+ });
113
+
114
+ test("applies small size", async () => {
115
+ render(<Input size="sm" placeholder="Small" />);
116
+ const input = page.getByRole("textbox");
117
+ await expect.element(input).toHaveAttribute("data-size", "sm");
118
+ });
119
+
120
+ test("applies large size", async () => {
121
+ render(<Input size="lg" placeholder="Large" />);
122
+ const input = page.getByRole("textbox");
123
+ await expect.element(input).toHaveAttribute("data-size", "lg");
124
+ });
125
+
126
+ test("applies error state", async () => {
127
+ render(<Input error placeholder="Error" />);
128
+ const input = page.getByRole("textbox");
129
+ await expect.element(input).toHaveAttribute("data-error", "true");
130
+ });
131
+ });
132
+
133
+ describe("Input Types", () => {
134
+ test("renders email type", async () => {
135
+ render(<Input type="email" placeholder="Email" />);
136
+ await expect
137
+ .element(page.getByRole("textbox"))
138
+ .toHaveAttribute("type", "email");
139
+ });
140
+
141
+ test("renders password type", async () => {
142
+ render(<Input type="password" placeholder="Password" />);
143
+ // Password inputs don't have textbox role
144
+ const input = page.getByPlaceholder("Password");
145
+ await expect.element(input).toHaveAttribute("type", "password");
146
+ });
147
+
148
+ test("renders number type", async () => {
149
+ render(<Input type="number" placeholder="Number" />);
150
+ const input = page.getByRole("spinbutton");
151
+ await expect.element(input).toHaveAttribute("type", "number");
152
+ });
153
+
154
+ test("renders search type", async () => {
155
+ render(<Input type="search" placeholder="Search" />);
156
+ const input = page.getByRole("searchbox");
157
+ await expect.element(input).toHaveAttribute("type", "search");
158
+ });
159
+
160
+ test("defaults to text type", async () => {
161
+ render(<Input placeholder="Default type" />);
162
+ await expect
163
+ .element(page.getByRole("textbox"))
164
+ .toHaveAttribute("type", "text");
165
+ });
166
+ });
167
+ });
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { tv, type VariantProps } from "tailwind-variants";
5
+ import { formControlBase, formControlError } from "@/lib/form-control";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ /**
9
+ * Input-specific size variants using semantic input tokens.
10
+ * These tokens allow input sizing to be customized independently from other controls.
11
+ */
12
+ const inputSizes = {
13
+ sm: "h-spatial-ui-input-height-small px-spatial-ui-input-padding-x-small py-spatial-ui-input-padding-y-small typography-body-sm-md",
14
+ default:
15
+ "h-spatial-ui-input-height-medium px-spatial-ui-input-padding-x-medium py-spatial-ui-input-padding-y-medium typography-body-md-md",
16
+ lg: "h-spatial-ui-input-height-large px-spatial-ui-input-padding-x-large py-spatial-ui-input-padding-y-large typography-body-md-lg",
17
+ } as const;
18
+
19
+ /**
20
+ * Input variants for styling based on Figma BaseKit / Interface / Input
21
+ *
22
+ * States (handled via CSS pseudo-classes and props):
23
+ * - Default: White background, subtle border
24
+ * - Hover: Light gray background (via :hover)
25
+ * - Focus: Accent border with focus ring (via :focus-visible)
26
+ * - Error: Error border color (via error prop)
27
+ * - Disabled: Disabled background (via :disabled)
28
+ *
29
+ * Sizes:
30
+ * - sm: Smaller height and padding (36px)
31
+ * - default: Standard height (48px)
32
+ * - lg: Larger height and padding (56px)
33
+ */
34
+ const inputVariants = tv({
35
+ base: [
36
+ ...formControlBase,
37
+ // Input-specific: Placeholder styling
38
+ "placeholder:text-text-muted",
39
+ ],
40
+ variants: {
41
+ size: inputSizes,
42
+ error: {
43
+ ...formControlError,
44
+ true: `${formControlError.true} placeholder:text-ui-error-color/60`,
45
+ },
46
+ },
47
+ defaultVariants: {
48
+ size: "default",
49
+ error: false,
50
+ },
51
+ });
52
+
53
+ export interface InputProps
54
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
55
+ VariantProps<typeof inputVariants> {
56
+ /**
57
+ * Whether the input is in an error state
58
+ */
59
+ error?: boolean;
60
+ }
61
+
62
+ /**
63
+ * Input component based on Figma BaseKit / Interface / Input
64
+ *
65
+ * A styled text input with support for various states:
66
+ * - Default, hover, focus, error, and disabled states
67
+ * - Three size variants: sm, default, lg
68
+ *
69
+ * Uses semantic UI tokens for theming support.
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * // Basic usage
74
+ * <Input placeholder="Enter your email" />
75
+ *
76
+ * // With error state
77
+ * <Input error placeholder="Invalid input" />
78
+ *
79
+ * // Different sizes
80
+ * <Input size="sm" placeholder="Small" />
81
+ * <Input size="lg" placeholder="Large" />
82
+ *
83
+ * // Disabled
84
+ * <Input disabled placeholder="Disabled input" />
85
+ * ```
86
+ */
87
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
88
+ ({ className, size, error, type = "text", ...props }, ref) => {
89
+ return (
90
+ <input
91
+ ref={ref}
92
+ type={type}
93
+ aria-invalid={error || undefined}
94
+ className={cn(inputVariants({ size, error, class: className }))}
95
+ data-size={size ?? "default"}
96
+ data-error={error ?? false}
97
+ {...props}
98
+ />
99
+ );
100
+ },
101
+ );
102
+ Input.displayName = "Input";
103
+
104
+ export { Input, inputVariants };
@@ -130,28 +130,26 @@ export const Controlled = () => {
130
130
  const [activeIndex, setActiveIndex] = React.useState(0);
131
131
 
132
132
  return (
133
- <div className="flex flex-col items-center gap-spacing-24">
133
+ <div className="flex flex-col items-center gap-24">
134
134
  <PagerControl
135
135
  count={4}
136
136
  activeIndex={activeIndex}
137
137
  onChange={setActiveIndex}
138
138
  autoPlay={false}
139
139
  />
140
- <div className="flex gap-spacing-10">
140
+ <div className="flex gap-10">
141
141
  <button
142
142
  type="button"
143
143
  onClick={() => setActiveIndex((prev) => Math.max(0, prev - 1))}
144
- className="rounded bg-gray-200 px-spacing-12 py-spacing-6"
144
+ className="rounded bg-gray-200 px-12 py-6"
145
145
  >
146
146
  Previous
147
147
  </button>
148
- <span className="px-spacing-12 py-spacing-6">
149
- Page {activeIndex + 1} of 4
150
- </span>
148
+ <span className="px-12 py-6">Page {activeIndex + 1} of 4</span>
151
149
  <button
152
150
  type="button"
153
151
  onClick={() => setActiveIndex((prev) => Math.min(3, prev + 1))}
154
- className="rounded bg-gray-200 px-spacing-12 py-spacing-6"
152
+ className="rounded bg-gray-200 px-12 py-6"
155
153
  >
156
154
  Next
157
155
  </button>
@@ -174,7 +172,7 @@ export const WithCarousel = () => {
174
172
  ];
175
173
 
176
174
  return (
177
- <div className="flex w-[400px] flex-col gap-spacing-16">
175
+ <div className="flex w-[400px] flex-col gap-16">
178
176
  <div className="relative h-[200px] overflow-hidden rounded-radius-12">
179
177
  {slides.map((slide, index) => (
180
178
  <div
@@ -9,9 +9,9 @@ const pagerControlVariants = tv({
9
9
  variants: {
10
10
  size: {
11
11
  // Uses primitive spacing tokens
12
- sm: "gap-spacing-2",
13
- default: "gap-spacing-2",
14
- lg: "gap-spacing-4",
12
+ sm: "gap-2",
13
+ default: "gap-2",
14
+ lg: "gap-4",
15
15
  },
16
16
  },
17
17
  defaultVariants: {
@@ -24,9 +24,9 @@ const dotBaseVariants = tv({
24
24
  variants: {
25
25
  size: {
26
26
  // Uses primitive spacing tokens
27
- sm: "h-spacing-6",
28
- default: "h-spacing-10",
29
- lg: "h-spacing-16",
27
+ sm: "h-6",
28
+ default: "h-10",
29
+ lg: "h-16",
30
30
  },
31
31
  variant: {
32
32
  charcoal: "",
@@ -245,20 +245,20 @@ const PagerControl = React.forwardRef<HTMLDivElement, PagerControlProps>(
245
245
  if (isActive) {
246
246
  switch (size) {
247
247
  case "sm":
248
- return "w-spacing-16";
248
+ return "w-16";
249
249
  case "lg":
250
- return "w-spacing-36";
250
+ return "w-36";
251
251
  default:
252
- return "w-spacing-28";
252
+ return "w-28";
253
253
  }
254
254
  }
255
255
  switch (size) {
256
256
  case "sm":
257
- return "w-spacing-6";
257
+ return "w-6";
258
258
  case "lg":
259
- return "w-spacing-16";
259
+ return "w-16";
260
260
  default:
261
- return "w-spacing-10";
261
+ return "w-10";
262
262
  }
263
263
  };
264
264
 
@@ -0,0 +1,30 @@
1
+ export type {
2
+ PopoverArrowProps,
3
+ PopoverBackdropProps,
4
+ PopoverCloseProps,
5
+ PopoverDescriptionProps,
6
+ PopoverPopupProps,
7
+ PopoverPortalProps,
8
+ PopoverPositionerProps,
9
+ PopoverProps,
10
+ PopoverRootProps,
11
+ PopoverTitleProps,
12
+ PopoverTriggerProps,
13
+ } from "./popover";
14
+
15
+ export {
16
+ Popover,
17
+ PopoverArrow,
18
+ PopoverBackdrop,
19
+ PopoverClose,
20
+ PopoverDescription,
21
+ PopoverParts,
22
+ PopoverPopup,
23
+ PopoverPortal,
24
+ PopoverPositioner,
25
+ PopoverRoot,
26
+ PopoverTitle,
27
+ PopoverTrigger,
28
+ popoverArrowVariants,
29
+ popoverPopupVariants,
30
+ } from "./popover";