@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
@@ -1,11 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { Slot } from "@radix-ui/react-slot";
4
- import * as React from "react";
3
+ import { useRender } from "@base-ui-components/react/use-render";
5
4
  import { tv, type VariantProps } from "tailwind-variants";
6
5
 
7
6
  /**
8
- * IconButton component based on Figma BaseKit / Interface / Icon Button
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,254 +20,112 @@ import { tv, type VariantProps } from "tailwind-variants";
21
20
  * </IconButton>
22
21
  * ```
23
22
  *
24
- * Variants:
25
- * - solid: Filled button
26
- * - outline: Outlined button
27
- * - ghost: No background/border, just icon
28
- * - subtle: Subtle outlined button
29
- *
30
- * Color Schemes:
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 (32x32)
36
- * - default: Medium (40x40)
37
- * - lg: Large (48x48)
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
- solid: "",
49
- outline: "border",
50
- ghost: "",
51
- subtle: "border",
52
- },
53
- colorScheme: {
54
- dark: "",
55
- light: "",
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-32",
59
- default: "size-40",
60
- lg: "size-48",
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: "rounded-radius-12",
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: "solid",
128
- colorScheme: "dark",
129
- size: "default",
74
+ variant: "primary",
75
+ size: "md",
130
76
  rounded: "default",
131
77
  },
132
78
  });
133
79
 
134
- export interface IconButtonProps
135
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
136
- VariantProps<typeof iconButtonVariants> {
137
- /**
138
- * Custom render prop for element composition.
139
- * Accepts a React element or render function.
140
- * @example
141
- * ```tsx
142
- * // Render as a link
143
- * <IconButton render={<a href="/contact" />} aria-label="Contact">
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
- const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
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]);
206
-
207
- // Resolve actual values for data attributes
208
- const resolvedVariant = variant ?? "solid";
209
- const resolvedColorScheme = colorScheme ?? "dark";
210
- const resolvedSize = size ?? "default";
211
- const resolvedRounded = rounded ?? "default";
212
-
213
- const buttonClassName = iconButtonVariants({
214
- variant,
215
- colorScheme,
216
- size,
217
- rounded,
218
- class: className,
219
- });
92
+ export interface IconButtonProps
93
+ extends useRender.ComponentProps<"button", IconButtonState>,
94
+ VariantProps<typeof iconButtonVariants> {}
220
95
 
221
- const dataAttributes = {
222
- "data-variant": resolvedVariant,
223
- "data-color-scheme": resolvedColorScheme,
224
- "data-size": resolvedSize,
225
- "data-rounded": resolvedRounded,
226
- };
96
+ function IconButton(props: IconButtonProps) {
97
+ const {
98
+ className,
99
+ variant = "primary",
100
+ size = "md",
101
+ rounded = "default",
102
+ render,
103
+ ...otherProps
104
+ } = props;
227
105
 
228
- // Handle render prop (element or function)
229
- if (render) {
230
- if (typeof render === "function") {
231
- return render({
232
- ref,
233
- className: buttonClassName,
234
- ...dataAttributes,
235
- ...props,
236
- } as React.ButtonHTMLAttributes<HTMLButtonElement>);
237
- }
238
- // Clone the render element with merged props
239
- return React.cloneElement(render, {
240
- ref,
241
- className: buttonClassName,
242
- ...dataAttributes,
243
- ...props,
244
- ...(render.props as Record<string, unknown>),
245
- });
246
- }
106
+ const state: IconButtonState = {
107
+ variant,
108
+ size,
109
+ rounded,
110
+ };
247
111
 
248
- // Handle deprecated asChild prop
249
- if (asChild) {
250
- return (
251
- <Slot
252
- ref={ref}
253
- className={buttonClassName}
254
- {...dataAttributes}
255
- {...props}
256
- />
257
- );
258
- }
112
+ const buttonClassName = iconButtonVariants({
113
+ variant,
114
+ size,
115
+ rounded,
116
+ class: className,
117
+ });
259
118
 
260
- // Default: render as button
261
- return (
262
- <button
263
- ref={ref}
264
- type="button"
265
- className={buttonClassName}
266
- {...dataAttributes}
267
- {...props}
268
- />
269
- );
270
- },
271
- );
272
- IconButton.displayName = "IconButton";
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
+ }
273
130
 
274
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";