@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,384 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { Button, type ButtonProps } from "../button";
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// InputGroup Variants
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
const inputGroupVariants = tv({
|
|
13
|
+
base: [
|
|
14
|
+
"group/input-group relative flex w-full items-center",
|
|
15
|
+
"border border-solid border-ui-color-border rounded-radius-6",
|
|
16
|
+
"bg-ui-control-background shadow-xs",
|
|
17
|
+
"transition-[color,box-shadow,border-color] duration-150",
|
|
18
|
+
"h-36 min-w-0",
|
|
19
|
+
"has-[>textarea]:h-auto",
|
|
20
|
+
"has-[>[data-align=inline-start]]:[&>input]:pl-6",
|
|
21
|
+
"has-[>[data-align=inline-end]]:[&>input]:pr-6",
|
|
22
|
+
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-10",
|
|
23
|
+
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-10",
|
|
24
|
+
"has-[[data-slot=input-group-control]:focus-visible]:border-ui-accent-base has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ui-color-focus",
|
|
25
|
+
"has-[[data-slot][aria-invalid=true]]:border-ui-error-color has-[[data-slot][aria-invalid=true]]:ring-ui-error-color/20",
|
|
26
|
+
"data-[disabled=true]:bg-ui-control-background-disabled data-[disabled=true]:opacity-50 data-[disabled=true]:cursor-not-allowed",
|
|
27
|
+
],
|
|
28
|
+
variants: {
|
|
29
|
+
size: {
|
|
30
|
+
sm: "h-32",
|
|
31
|
+
default: "h-36",
|
|
32
|
+
lg: "h-48",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: {
|
|
36
|
+
size: "default",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// InputGroupAddon Variants
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
const inputGroupAddonVariants = tv({
|
|
45
|
+
base: [
|
|
46
|
+
"flex items-center justify-center gap-6",
|
|
47
|
+
"text-12 font-medium text-text-muted",
|
|
48
|
+
"select-none cursor-text",
|
|
49
|
+
"[&>svg:not([class*='size-'])]:size-14",
|
|
50
|
+
"[&_button]:text-[unset] [&_button]:cursor-pointer",
|
|
51
|
+
"group-data-[disabled=true]/input-group:opacity-50",
|
|
52
|
+
],
|
|
53
|
+
variants: {
|
|
54
|
+
align: {
|
|
55
|
+
"inline-start": ["order-first h-full pl-10", "has-[>button]:ml-[-6px]"],
|
|
56
|
+
"inline-end": ["order-last h-full pr-10", "has-[>button]:mr-[-6px]"],
|
|
57
|
+
"block-start": [
|
|
58
|
+
"order-first h-auto w-full justify-start px-12 pt-10",
|
|
59
|
+
"[.border-b]:pb-10",
|
|
60
|
+
],
|
|
61
|
+
"block-end": [
|
|
62
|
+
"order-last h-auto w-full justify-start px-12 pb-10",
|
|
63
|
+
"[.border-t]:pt-10",
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
defaultVariants: {
|
|
68
|
+
align: "inline-start",
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// InputGroup Component
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
export interface InputGroupProps
|
|
77
|
+
extends React.FieldsetHTMLAttributes<HTMLFieldSetElement>,
|
|
78
|
+
VariantProps<typeof inputGroupVariants> {}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* InputGroup component for combining inputs with addons, buttons, and text.
|
|
82
|
+
*
|
|
83
|
+
* A container that groups an input with prefix/suffix addons, icons, or buttons.
|
|
84
|
+
* Supports inline (left/right) and block (top/bottom) addon positioning.
|
|
85
|
+
*
|
|
86
|
+
* Uses semantic UI tokens for theming support.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* // With prefix icon
|
|
91
|
+
* <InputGroup>
|
|
92
|
+
* <InputGroupAddon>
|
|
93
|
+
* <SearchIcon />
|
|
94
|
+
* </InputGroupAddon>
|
|
95
|
+
* <InputGroupInput placeholder="Search..." />
|
|
96
|
+
* </InputGroup>
|
|
97
|
+
*
|
|
98
|
+
* // With suffix button
|
|
99
|
+
* <InputGroup>
|
|
100
|
+
* <InputGroupInput placeholder="Enter email" />
|
|
101
|
+
* <InputGroupAddon align="inline-end">
|
|
102
|
+
* <InputGroupButton>Subscribe</InputGroupButton>
|
|
103
|
+
* </InputGroupAddon>
|
|
104
|
+
* </InputGroup>
|
|
105
|
+
*
|
|
106
|
+
* // With text prefix
|
|
107
|
+
* <InputGroup>
|
|
108
|
+
* <InputGroupAddon>
|
|
109
|
+
* <InputGroupText>https://</InputGroupText>
|
|
110
|
+
* </InputGroupAddon>
|
|
111
|
+
* <InputGroupInput placeholder="example.com" />
|
|
112
|
+
* </InputGroup>
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
function InputGroup({ className, size, disabled, ...props }: InputGroupProps) {
|
|
116
|
+
return (
|
|
117
|
+
<fieldset
|
|
118
|
+
data-slot="input-group"
|
|
119
|
+
data-disabled={disabled || undefined}
|
|
120
|
+
disabled={disabled}
|
|
121
|
+
className={cn(
|
|
122
|
+
"border-0 p-0 m-0 min-w-0",
|
|
123
|
+
inputGroupVariants({ size, class: className }),
|
|
124
|
+
)}
|
|
125
|
+
{...props}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// =============================================================================
|
|
131
|
+
// InputGroupAddon Component
|
|
132
|
+
// =============================================================================
|
|
133
|
+
|
|
134
|
+
export interface InputGroupAddonProps
|
|
135
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
136
|
+
VariantProps<typeof inputGroupAddonVariants> {}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* InputGroupAddon component for positioning addons within an InputGroup.
|
|
140
|
+
*
|
|
141
|
+
* Can contain icons, text, or buttons. Clicking the addon focuses the input.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```tsx
|
|
145
|
+
* // Inline start (default - left side)
|
|
146
|
+
* <InputGroupAddon>
|
|
147
|
+
* <SearchIcon />
|
|
148
|
+
* </InputGroupAddon>
|
|
149
|
+
*
|
|
150
|
+
* // Inline end (right side)
|
|
151
|
+
* <InputGroupAddon align="inline-end">
|
|
152
|
+
* <InputGroupButton>Submit</InputGroupButton>
|
|
153
|
+
* </InputGroupAddon>
|
|
154
|
+
*
|
|
155
|
+
* // Block positions (top/bottom)
|
|
156
|
+
* <InputGroupAddon align="block-start">
|
|
157
|
+
* <label>Email Address</label>
|
|
158
|
+
* </InputGroupAddon>
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
function InputGroupAddon({
|
|
162
|
+
className,
|
|
163
|
+
align = "inline-start",
|
|
164
|
+
onClick,
|
|
165
|
+
onKeyDown,
|
|
166
|
+
...props
|
|
167
|
+
}: InputGroupAddonProps) {
|
|
168
|
+
const focusInput = (element: HTMLElement) => {
|
|
169
|
+
element.parentElement?.querySelector("input")?.focus();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
173
|
+
// Don't focus input if clicking a button inside the addon
|
|
174
|
+
if ((e.target as HTMLElement).closest("button")) {
|
|
175
|
+
onClick?.(e);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Focus the input when clicking the addon
|
|
179
|
+
focusInput(e.currentTarget);
|
|
180
|
+
onClick?.(e);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
184
|
+
// Focus input on Enter or Space (unless inside a button)
|
|
185
|
+
if (
|
|
186
|
+
(e.key === "Enter" || e.key === " ") &&
|
|
187
|
+
!(e.target as HTMLElement).closest("button")
|
|
188
|
+
) {
|
|
189
|
+
focusInput(e.currentTarget);
|
|
190
|
+
}
|
|
191
|
+
onKeyDown?.(e);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: Click-to-focus is a convenience UX pattern; primary interaction is via the input itself
|
|
196
|
+
<div
|
|
197
|
+
data-slot="input-group-addon"
|
|
198
|
+
data-align={align}
|
|
199
|
+
className={cn(inputGroupAddonVariants({ align, class: className }))}
|
|
200
|
+
onClick={handleClick}
|
|
201
|
+
onKeyDown={handleKeyDown}
|
|
202
|
+
{...props}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// =============================================================================
|
|
208
|
+
// InputGroupButton Component
|
|
209
|
+
// =============================================================================
|
|
210
|
+
|
|
211
|
+
const inputGroupButtonVariants = tv({
|
|
212
|
+
base: [
|
|
213
|
+
"text-14 shadow-none flex gap-6 items-center",
|
|
214
|
+
"focus-visible:ring-1 focus-visible:ring-offset-0 focus-visible:ring-ui-color-border",
|
|
215
|
+
"transition-opacity duration-150",
|
|
216
|
+
],
|
|
217
|
+
variants: {
|
|
218
|
+
size: {
|
|
219
|
+
xs: "!h-24 gap-4 px-8 rounded-radius-4 [&>svg:not([class*='size-'])]:size-14 has-[>svg]:px-6",
|
|
220
|
+
sm: "!h-28 px-10 gap-6 rounded-radius-6 has-[>svg]:px-8",
|
|
221
|
+
"icon-xs": "!size-24 rounded-radius-4 p-0 has-[>svg]:p-0",
|
|
222
|
+
"icon-sm": "!size-28 rounded-radius-6 p-0 has-[>svg]:p-0",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
defaultVariants: {
|
|
226
|
+
size: "xs",
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
export interface InputGroupButtonProps
|
|
231
|
+
extends Omit<ButtonProps, "size">,
|
|
232
|
+
VariantProps<typeof inputGroupButtonVariants> {}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* InputGroupButton component for inline buttons within an InputGroup.
|
|
236
|
+
*
|
|
237
|
+
* A small button variant designed to fit inside input groups.
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```tsx
|
|
241
|
+
* <InputGroupAddon align="inline-end">
|
|
242
|
+
* <InputGroupButton>Submit</InputGroupButton>
|
|
243
|
+
* </InputGroupAddon>
|
|
244
|
+
*
|
|
245
|
+
* // Icon button
|
|
246
|
+
* <InputGroupAddon align="inline-end">
|
|
247
|
+
* <InputGroupButton size="icon-xs">
|
|
248
|
+
* <ClearIcon />
|
|
249
|
+
* </InputGroupButton>
|
|
250
|
+
* </InputGroupAddon>
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
function InputGroupButton({
|
|
254
|
+
className,
|
|
255
|
+
type = "button",
|
|
256
|
+
variant = "ghost",
|
|
257
|
+
size = "xs",
|
|
258
|
+
...props
|
|
259
|
+
}: InputGroupButtonProps) {
|
|
260
|
+
return (
|
|
261
|
+
<Button
|
|
262
|
+
type={type}
|
|
263
|
+
data-size={size}
|
|
264
|
+
variant={variant}
|
|
265
|
+
className={cn(inputGroupButtonVariants({ size, class: className }))}
|
|
266
|
+
{...props}
|
|
267
|
+
/>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// =============================================================================
|
|
272
|
+
// InputGroupText Component
|
|
273
|
+
// =============================================================================
|
|
274
|
+
|
|
275
|
+
export interface InputGroupTextProps
|
|
276
|
+
extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* InputGroupText component for static text within an InputGroup.
|
|
280
|
+
*
|
|
281
|
+
* Use for prefixes like "https://" or suffixes like ".com"
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```tsx
|
|
285
|
+
* <InputGroupAddon>
|
|
286
|
+
* <InputGroupText>https://</InputGroupText>
|
|
287
|
+
* </InputGroupAddon>
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
function InputGroupText({ className, ...props }: InputGroupTextProps) {
|
|
291
|
+
return (
|
|
292
|
+
<span
|
|
293
|
+
className={cn(
|
|
294
|
+
"flex items-center gap-6 text-14 text-text-muted",
|
|
295
|
+
"[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-14",
|
|
296
|
+
className,
|
|
297
|
+
)}
|
|
298
|
+
{...props}
|
|
299
|
+
/>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// =============================================================================
|
|
304
|
+
// InputGroupInput Component
|
|
305
|
+
// =============================================================================
|
|
306
|
+
|
|
307
|
+
export interface InputGroupInputProps
|
|
308
|
+
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* InputGroupInput component - the input element within an InputGroup.
|
|
312
|
+
*
|
|
313
|
+
* Styled to integrate seamlessly with the InputGroup container.
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```tsx
|
|
317
|
+
* <InputGroup>
|
|
318
|
+
* <InputGroupInput placeholder="Enter text..." />
|
|
319
|
+
* </InputGroup>
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
function InputGroupInput({ className, ...props }: InputGroupInputProps) {
|
|
323
|
+
return (
|
|
324
|
+
<input
|
|
325
|
+
data-slot="input-group-control"
|
|
326
|
+
className={cn(
|
|
327
|
+
"flex-1 min-w-0 h-full",
|
|
328
|
+
"border-0 bg-transparent shadow-none outline-none",
|
|
329
|
+
"text-14 font-medium placeholder:text-text-muted",
|
|
330
|
+
"px-12",
|
|
331
|
+
"focus-visible:ring-0",
|
|
332
|
+
className,
|
|
333
|
+
)}
|
|
334
|
+
{...props}
|
|
335
|
+
/>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// =============================================================================
|
|
340
|
+
// InputGroupTextarea Component
|
|
341
|
+
// =============================================================================
|
|
342
|
+
|
|
343
|
+
export interface InputGroupTextareaProps
|
|
344
|
+
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* InputGroupTextarea component - a textarea element within an InputGroup.
|
|
348
|
+
*
|
|
349
|
+
* Styled to integrate seamlessly with the InputGroup container.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```tsx
|
|
353
|
+
* <InputGroup>
|
|
354
|
+
* <InputGroupTextarea placeholder="Enter long text..." rows={4} />
|
|
355
|
+
* </InputGroup>
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
function InputGroupTextarea({ className, ...props }: InputGroupTextareaProps) {
|
|
359
|
+
return (
|
|
360
|
+
<textarea
|
|
361
|
+
data-slot="input-group-control"
|
|
362
|
+
className={cn(
|
|
363
|
+
"flex-1 min-w-0 w-full resize-none",
|
|
364
|
+
"border-0 bg-transparent shadow-none outline-none",
|
|
365
|
+
"text-14 font-medium placeholder:text-text-muted",
|
|
366
|
+
"px-12 py-10",
|
|
367
|
+
"focus-visible:ring-0",
|
|
368
|
+
className,
|
|
369
|
+
)}
|
|
370
|
+
{...props}
|
|
371
|
+
/>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export {
|
|
376
|
+
InputGroup,
|
|
377
|
+
InputGroupAddon,
|
|
378
|
+
InputGroupButton,
|
|
379
|
+
InputGroupText,
|
|
380
|
+
InputGroupInput,
|
|
381
|
+
InputGroupTextarea,
|
|
382
|
+
inputGroupVariants,
|
|
383
|
+
inputGroupAddonVariants,
|
|
384
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Input } from ".";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Input> = {
|
|
5
|
+
title: "Atoms/Input",
|
|
6
|
+
component: Input,
|
|
7
|
+
} as Meta<typeof Input>;
|
|
8
|
+
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof Input>;
|
|
11
|
+
|
|
12
|
+
export const Playground: Story = {
|
|
13
|
+
render: (args) => <Input {...args} />,
|
|
14
|
+
};
|
|
15
|
+
Playground.argTypes = {
|
|
16
|
+
size: {
|
|
17
|
+
control: {
|
|
18
|
+
type: "radio",
|
|
19
|
+
},
|
|
20
|
+
options: ["sm", "default", "lg"],
|
|
21
|
+
},
|
|
22
|
+
disabled: {
|
|
23
|
+
control: {
|
|
24
|
+
type: "boolean",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
error: {
|
|
28
|
+
control: {
|
|
29
|
+
type: "boolean",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
placeholder: {
|
|
33
|
+
control: {
|
|
34
|
+
type: "text",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
Playground.args = {
|
|
39
|
+
size: "default",
|
|
40
|
+
disabled: false,
|
|
41
|
+
error: false,
|
|
42
|
+
placeholder: "Enter text...",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// States
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Default state - white background with subtle border
|
|
51
|
+
*/
|
|
52
|
+
export const Default = () => <Input placeholder="Enter text..." />;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Filled state - shows how a filled input looks
|
|
56
|
+
*/
|
|
57
|
+
export const Filled = () => <Input defaultValue="Filled content" />;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Error state - red border and text
|
|
61
|
+
*/
|
|
62
|
+
export const ErrorState = () => <Input error placeholder="Invalid input" />;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Disabled state - grayed out and not interactive
|
|
66
|
+
*/
|
|
67
|
+
export const Disabled = () => <Input disabled placeholder="Disabled input" />;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* All states comparison
|
|
71
|
+
*/
|
|
72
|
+
export const AllStates = () => (
|
|
73
|
+
<div className="flex flex-col gap-spacing-16 max-w-[320px]">
|
|
74
|
+
<div>
|
|
75
|
+
<p className="mb-spacing-8 text-12 text-text-muted">Default</p>
|
|
76
|
+
<Input placeholder="Enter text..." />
|
|
77
|
+
</div>
|
|
78
|
+
<div>
|
|
79
|
+
<p className="mb-spacing-8 text-12 text-text-muted">
|
|
80
|
+
Hover (hover the input)
|
|
81
|
+
</p>
|
|
82
|
+
<Input placeholder="Hover me..." />
|
|
83
|
+
</div>
|
|
84
|
+
<div>
|
|
85
|
+
<p className="mb-spacing-8 text-12 text-text-muted">
|
|
86
|
+
Focus (click the input)
|
|
87
|
+
</p>
|
|
88
|
+
<Input placeholder="Click to focus..." />
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<p className="mb-spacing-8 text-12 text-text-muted">Filled</p>
|
|
92
|
+
<Input defaultValue="Filled content" />
|
|
93
|
+
</div>
|
|
94
|
+
<div>
|
|
95
|
+
<p className="mb-spacing-8 text-12 text-text-muted">Error</p>
|
|
96
|
+
<Input error placeholder="Invalid input" />
|
|
97
|
+
</div>
|
|
98
|
+
<div>
|
|
99
|
+
<p className="mb-spacing-8 text-12 text-text-muted">Disabled</p>
|
|
100
|
+
<Input disabled placeholder="Disabled input" />
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// =============================================================================
|
|
106
|
+
// Sizes
|
|
107
|
+
// =============================================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Small size - compact input for tight layouts
|
|
111
|
+
*/
|
|
112
|
+
export const Small = () => <Input size="sm" placeholder="Small input" />;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Default size - standard 48px height
|
|
116
|
+
*/
|
|
117
|
+
export const Medium = () => (
|
|
118
|
+
<Input size="default" placeholder="Default input" />
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Large size - more prominent input
|
|
123
|
+
*/
|
|
124
|
+
export const Large = () => <Input size="lg" placeholder="Large input" />;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* All sizes comparison
|
|
128
|
+
*/
|
|
129
|
+
export const AllSizes = () => (
|
|
130
|
+
<div className="flex flex-col gap-spacing-16 max-w-[320px]">
|
|
131
|
+
<div>
|
|
132
|
+
<p className="mb-spacing-8 text-12 text-text-muted">Small (36px)</p>
|
|
133
|
+
<Input size="sm" placeholder="Small input" />
|
|
134
|
+
</div>
|
|
135
|
+
<div>
|
|
136
|
+
<p className="mb-spacing-8 text-12 text-text-muted">Default (48px)</p>
|
|
137
|
+
<Input size="default" placeholder="Default input" />
|
|
138
|
+
</div>
|
|
139
|
+
<div>
|
|
140
|
+
<p className="mb-spacing-8 text-12 text-text-muted">Large (56px)</p>
|
|
141
|
+
<Input size="lg" placeholder="Large input" />
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// =============================================================================
|
|
147
|
+
// Types
|
|
148
|
+
// =============================================================================
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Email input type
|
|
152
|
+
*/
|
|
153
|
+
export const EmailType = () => (
|
|
154
|
+
<Input type="email" placeholder="Enter your email" />
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Password input type
|
|
159
|
+
*/
|
|
160
|
+
export const PasswordType = () => (
|
|
161
|
+
<Input type="password" placeholder="Enter your password" />
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Number input type
|
|
166
|
+
*/
|
|
167
|
+
export const NumberType = () => (
|
|
168
|
+
<Input type="number" placeholder="Enter a number" />
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Search input type
|
|
173
|
+
*/
|
|
174
|
+
export const SearchType = () => <Input type="search" placeholder="Search..." />;
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Examples
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Form field example with label
|
|
182
|
+
*/
|
|
183
|
+
export const WithLabel = () => (
|
|
184
|
+
<div className="flex flex-col gap-spacing-8 max-w-[320px]">
|
|
185
|
+
<label
|
|
186
|
+
htmlFor="email-input"
|
|
187
|
+
className="text-14 font-medium text-text-primary"
|
|
188
|
+
>
|
|
189
|
+
Email Address
|
|
190
|
+
</label>
|
|
191
|
+
<Input id="email-input" type="email" placeholder="you@example.com" />
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Form field with error message
|
|
197
|
+
*/
|
|
198
|
+
export const WithErrorMessage = () => (
|
|
199
|
+
<div className="flex flex-col gap-spacing-8 max-w-[320px]">
|
|
200
|
+
<label
|
|
201
|
+
htmlFor="email-error"
|
|
202
|
+
className="text-14 font-medium text-text-primary"
|
|
203
|
+
>
|
|
204
|
+
Email Address
|
|
205
|
+
</label>
|
|
206
|
+
<Input
|
|
207
|
+
id="email-error"
|
|
208
|
+
type="email"
|
|
209
|
+
error
|
|
210
|
+
placeholder="you@example.com"
|
|
211
|
+
defaultValue="invalid-email"
|
|
212
|
+
/>
|
|
213
|
+
<p className="text-12 text-ui-error-color">
|
|
214
|
+
Please enter a valid email address
|
|
215
|
+
</p>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Required field indicator
|
|
221
|
+
*/
|
|
222
|
+
export const RequiredField = () => (
|
|
223
|
+
<div className="flex flex-col gap-spacing-8 max-w-[320px]">
|
|
224
|
+
<label
|
|
225
|
+
htmlFor="required-input"
|
|
226
|
+
className="text-14 font-medium text-text-primary"
|
|
227
|
+
>
|
|
228
|
+
Full Name <span className="text-ui-error-color">*</span>
|
|
229
|
+
</label>
|
|
230
|
+
<Input id="required-input" required placeholder="John Doe" />
|
|
231
|
+
</div>
|
|
232
|
+
);
|