vinta-schedule-design-system 0.1.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/components.json +21 -0
- package/dist/styles.css +3452 -0
- package/package.json +103 -0
- package/src/layout/app-shell.tsx +115 -0
- package/src/layout/app-topbar.stories.tsx +74 -0
- package/src/layout/app-topbar.tsx +145 -0
- package/src/layout/auth-layout.tsx +45 -0
- package/src/layout/box.stories.tsx +86 -0
- package/src/layout/box.tsx +34 -0
- package/src/layout/center.tsx +24 -0
- package/src/layout/container.stories.tsx +44 -0
- package/src/layout/container.tsx +39 -0
- package/src/layout/divider.tsx +64 -0
- package/src/layout/flex.stories.tsx +109 -0
- package/src/layout/flex.test.tsx +71 -0
- package/src/layout/flex.tsx +112 -0
- package/src/layout/grid.stories.tsx +91 -0
- package/src/layout/grid.tsx +132 -0
- package/src/layout/heading.tsx +53 -0
- package/src/layout/index.ts +47 -0
- package/src/layout/layout-style.ts +291 -0
- package/src/layout/link.stories.tsx +54 -0
- package/src/layout/link.test.tsx +75 -0
- package/src/layout/link.tsx +100 -0
- package/src/layout/navbar.stories.tsx +70 -0
- package/src/layout/navbar.tsx +128 -0
- package/src/layout/page-header.stories.tsx +80 -0
- package/src/layout/page-header.tsx +59 -0
- package/src/layout/section.tsx +22 -0
- package/src/layout/spacer.tsx +21 -0
- package/src/layout/stack.stories.tsx +58 -0
- package/src/layout/stack.tsx +19 -0
- package/src/layout/text.tsx +139 -0
- package/src/layout/typography.stories.tsx +112 -0
- package/src/lib/utils.ts +6 -0
- package/src/story-types.ts +80 -0
- package/src/styles/build.css +33 -0
- package/src/styles/globals.css +9 -0
- package/src/styles/tokens.css +281 -0
- package/src/ui/accordion.stories.tsx +84 -0
- package/src/ui/accordion.tsx +57 -0
- package/src/ui/alert-dialog.stories.tsx +57 -0
- package/src/ui/alert-dialog.tsx +141 -0
- package/src/ui/alert.stories.tsx +46 -0
- package/src/ui/alert.tsx +59 -0
- package/src/ui/avatar.stories.tsx +56 -0
- package/src/ui/avatar.tsx +50 -0
- package/src/ui/badge.stories.tsx +105 -0
- package/src/ui/badge.tsx +57 -0
- package/src/ui/breadcrumb.stories.tsx +69 -0
- package/src/ui/breadcrumb.tsx +140 -0
- package/src/ui/button.stories.tsx +85 -0
- package/src/ui/button.tsx +60 -0
- package/src/ui/card.stories.tsx +62 -0
- package/src/ui/card.tsx +96 -0
- package/src/ui/carousel.stories.tsx +55 -0
- package/src/ui/carousel.tsx +262 -0
- package/src/ui/checkbox.stories.tsx +52 -0
- package/src/ui/checkbox.tsx +30 -0
- package/src/ui/combobox.stories.tsx +138 -0
- package/src/ui/combobox.tsx +217 -0
- package/src/ui/command.tsx +184 -0
- package/src/ui/dialog.stories.tsx +66 -0
- package/src/ui/dialog.tsx +163 -0
- package/src/ui/drawer.stories.tsx +80 -0
- package/src/ui/drawer.tsx +118 -0
- package/src/ui/dropdown-menu.stories.tsx +57 -0
- package/src/ui/dropdown-menu.tsx +201 -0
- package/src/ui/form.stories.tsx +70 -0
- package/src/ui/form.tsx +181 -0
- package/src/ui/input-otp.stories.tsx +52 -0
- package/src/ui/input-otp.tsx +71 -0
- package/src/ui/input.stories.tsx +68 -0
- package/src/ui/input.tsx +22 -0
- package/src/ui/label.stories.tsx +35 -0
- package/src/ui/label.tsx +26 -0
- package/src/ui/navigation-menu.stories.tsx +79 -0
- package/src/ui/navigation-menu.tsx +128 -0
- package/src/ui/pagination.stories.tsx +62 -0
- package/src/ui/pagination.tsx +137 -0
- package/src/ui/popover.tsx +89 -0
- package/src/ui/progress.stories.tsx +33 -0
- package/src/ui/progress.tsx +28 -0
- package/src/ui/radio-group.stories.tsx +52 -0
- package/src/ui/radio-group.tsx +44 -0
- package/src/ui/scheduling-chip.stories.tsx +117 -0
- package/src/ui/scheduling-chip.tsx +62 -0
- package/src/ui/scroll-area.stories.tsx +52 -0
- package/src/ui/scroll-area.tsx +48 -0
- package/src/ui/select.stories.tsx +56 -0
- package/src/ui/select.tsx +159 -0
- package/src/ui/sheet.stories.tsx +63 -0
- package/src/ui/sheet.tsx +141 -0
- package/src/ui/skeleton.stories.tsx +40 -0
- package/src/ui/skeleton.tsx +15 -0
- package/src/ui/sonner.stories.tsx +62 -0
- package/src/ui/sonner.tsx +34 -0
- package/src/ui/switch.stories.tsx +41 -0
- package/src/ui/switch.tsx +29 -0
- package/src/ui/table.stories.tsx +88 -0
- package/src/ui/table.tsx +132 -0
- package/src/ui/tabs.stories.tsx +63 -0
- package/src/ui/tabs.tsx +55 -0
- package/src/ui/textarea.stories.tsx +45 -0
- package/src/ui/textarea.tsx +22 -0
- package/src/ui/tooltip.stories.tsx +48 -0
- package/src/ui/tooltip.tsx +32 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
|
5
|
+
import { Check, ChevronRight, Circle } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/utils';
|
|
8
|
+
|
|
9
|
+
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
10
|
+
|
|
11
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
12
|
+
|
|
13
|
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
|
14
|
+
|
|
15
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
16
|
+
|
|
17
|
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
18
|
+
|
|
19
|
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
20
|
+
|
|
21
|
+
const DropdownMenuSubTrigger = React.forwardRef<
|
|
22
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
23
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
24
|
+
inset?: boolean;
|
|
25
|
+
}
|
|
26
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
27
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn(
|
|
30
|
+
'focus:bg-accent data-[state=open]:bg-accent flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
31
|
+
inset && 'pl-8',
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
<ChevronRight className='ml-auto' />
|
|
38
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
39
|
+
));
|
|
40
|
+
DropdownMenuSubTrigger.displayName =
|
|
41
|
+
DropdownMenuPrimitive.SubTrigger.displayName;
|
|
42
|
+
|
|
43
|
+
const DropdownMenuSubContent = React.forwardRef<
|
|
44
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
45
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
46
|
+
>(({ className, ...props }, ref) => (
|
|
47
|
+
<DropdownMenuPrimitive.SubContent
|
|
48
|
+
ref={ref}
|
|
49
|
+
className={cn(
|
|
50
|
+
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[--radix-dropdown-menu-content-transform-origin] overflow-hidden rounded-md border p-1 shadow-lg',
|
|
51
|
+
className
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
));
|
|
56
|
+
DropdownMenuSubContent.displayName =
|
|
57
|
+
DropdownMenuPrimitive.SubContent.displayName;
|
|
58
|
+
|
|
59
|
+
const DropdownMenuContent = React.forwardRef<
|
|
60
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
61
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
62
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
63
|
+
<DropdownMenuPrimitive.Portal>
|
|
64
|
+
<DropdownMenuPrimitive.Content
|
|
65
|
+
ref={ref}
|
|
66
|
+
sideOffset={sideOffset}
|
|
67
|
+
className={cn(
|
|
68
|
+
'bg-popover text-popover-foreground z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
|
|
69
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]',
|
|
70
|
+
className
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
</DropdownMenuPrimitive.Portal>
|
|
75
|
+
));
|
|
76
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
|
77
|
+
|
|
78
|
+
const DropdownMenuItem = React.forwardRef<
|
|
79
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
80
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
81
|
+
inset?: boolean;
|
|
82
|
+
}
|
|
83
|
+
>(({ className, inset, ...props }, ref) => (
|
|
84
|
+
<DropdownMenuPrimitive.Item
|
|
85
|
+
ref={ref}
|
|
86
|
+
className={cn(
|
|
87
|
+
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
|
|
88
|
+
inset && 'pl-8',
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
));
|
|
94
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
|
95
|
+
|
|
96
|
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
97
|
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
98
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
99
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
100
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
101
|
+
ref={ref}
|
|
102
|
+
className={cn(
|
|
103
|
+
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
104
|
+
className
|
|
105
|
+
)}
|
|
106
|
+
checked={checked}
|
|
107
|
+
{...props}
|
|
108
|
+
>
|
|
109
|
+
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
|
110
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
111
|
+
<Check className='h-4 w-4' />
|
|
112
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
113
|
+
</span>
|
|
114
|
+
{children}
|
|
115
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
116
|
+
));
|
|
117
|
+
DropdownMenuCheckboxItem.displayName =
|
|
118
|
+
DropdownMenuPrimitive.CheckboxItem.displayName;
|
|
119
|
+
|
|
120
|
+
const DropdownMenuRadioItem = React.forwardRef<
|
|
121
|
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
122
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
123
|
+
>(({ className, children, ...props }, ref) => (
|
|
124
|
+
<DropdownMenuPrimitive.RadioItem
|
|
125
|
+
ref={ref}
|
|
126
|
+
className={cn(
|
|
127
|
+
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
128
|
+
className
|
|
129
|
+
)}
|
|
130
|
+
{...props}
|
|
131
|
+
>
|
|
132
|
+
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
|
133
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
134
|
+
<Circle className='h-2 w-2 fill-current' />
|
|
135
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
136
|
+
</span>
|
|
137
|
+
{children}
|
|
138
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
139
|
+
));
|
|
140
|
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
|
141
|
+
|
|
142
|
+
const DropdownMenuLabel = React.forwardRef<
|
|
143
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
144
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
145
|
+
inset?: boolean;
|
|
146
|
+
}
|
|
147
|
+
>(({ className, inset, ...props }, ref) => (
|
|
148
|
+
<DropdownMenuPrimitive.Label
|
|
149
|
+
ref={ref}
|
|
150
|
+
className={cn(
|
|
151
|
+
'px-2 py-1.5 text-sm font-semibold',
|
|
152
|
+
inset && 'pl-8',
|
|
153
|
+
className
|
|
154
|
+
)}
|
|
155
|
+
{...props}
|
|
156
|
+
/>
|
|
157
|
+
));
|
|
158
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
|
159
|
+
|
|
160
|
+
const DropdownMenuSeparator = React.forwardRef<
|
|
161
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
162
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
163
|
+
>(({ className, ...props }, ref) => (
|
|
164
|
+
<DropdownMenuPrimitive.Separator
|
|
165
|
+
ref={ref}
|
|
166
|
+
className={cn('bg-muted -mx-1 my-1 h-px', className)}
|
|
167
|
+
{...props}
|
|
168
|
+
/>
|
|
169
|
+
));
|
|
170
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
|
171
|
+
|
|
172
|
+
const DropdownMenuShortcut = ({
|
|
173
|
+
className,
|
|
174
|
+
...props
|
|
175
|
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
176
|
+
return (
|
|
177
|
+
<span
|
|
178
|
+
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
|
179
|
+
{...props}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
|
|
184
|
+
|
|
185
|
+
export {
|
|
186
|
+
DropdownMenu,
|
|
187
|
+
DropdownMenuTrigger,
|
|
188
|
+
DropdownMenuContent,
|
|
189
|
+
DropdownMenuItem,
|
|
190
|
+
DropdownMenuCheckboxItem,
|
|
191
|
+
DropdownMenuRadioItem,
|
|
192
|
+
DropdownMenuLabel,
|
|
193
|
+
DropdownMenuSeparator,
|
|
194
|
+
DropdownMenuShortcut,
|
|
195
|
+
DropdownMenuGroup,
|
|
196
|
+
DropdownMenuPortal,
|
|
197
|
+
DropdownMenuSub,
|
|
198
|
+
DropdownMenuSubContent,
|
|
199
|
+
DropdownMenuSubTrigger,
|
|
200
|
+
DropdownMenuRadioGroup,
|
|
201
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '../story-types';
|
|
2
|
+
import { useForm } from 'react-hook-form';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Form,
|
|
6
|
+
FormControl,
|
|
7
|
+
FormDescription,
|
|
8
|
+
FormField,
|
|
9
|
+
FormItem,
|
|
10
|
+
FormLabel,
|
|
11
|
+
FormMessage,
|
|
12
|
+
} from './form';
|
|
13
|
+
import { Input } from './input';
|
|
14
|
+
import { Button } from './button';
|
|
15
|
+
|
|
16
|
+
const meta = {
|
|
17
|
+
title: 'Components/Form',
|
|
18
|
+
// `Form` is react-hook-form's `FormProvider`: every one of its props is a
|
|
19
|
+
// `useForm()` return value (control, register, formState, handleSubmit, …)
|
|
20
|
+
// plus `children`. It has NO designer-editable scalar prop and cannot be
|
|
21
|
+
// driven by Puck controls, so the single argTypes key below is declared with
|
|
22
|
+
// a disabled control — it satisfies the emptiness check without fabricating a
|
|
23
|
+
// prop that doesn't exist. `component` is intentionally omitted: FormProvider's
|
|
24
|
+
// function name is `FormProvider`, which would not match the title leaf (§7).
|
|
25
|
+
tags: ['autodocs'],
|
|
26
|
+
argTypes: {
|
|
27
|
+
children: {
|
|
28
|
+
control: { type: 'text', disable: true },
|
|
29
|
+
description:
|
|
30
|
+
'Fields rendered inside the react-hook-form context. Supplied in code, not editable.',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
} satisfies Meta<typeof Form>;
|
|
34
|
+
|
|
35
|
+
export default meta;
|
|
36
|
+
// Form (FormProvider) has many required props, so type stories loosely (render-only).
|
|
37
|
+
type Story = StoryObj;
|
|
38
|
+
|
|
39
|
+
type Values = { email: string };
|
|
40
|
+
|
|
41
|
+
const BookingForm = () => {
|
|
42
|
+
const form = useForm<Values>({ defaultValues: { email: '' } });
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Form {...form}>
|
|
46
|
+
<form onSubmit={form.handleSubmit(() => {})} className='w-80 space-y-4'>
|
|
47
|
+
<FormField
|
|
48
|
+
control={form.control}
|
|
49
|
+
name='email'
|
|
50
|
+
rules={{ required: 'Email is required' }}
|
|
51
|
+
render={({ field }) => (
|
|
52
|
+
<FormItem>
|
|
53
|
+
<FormLabel>Email</FormLabel>
|
|
54
|
+
<FormControl>
|
|
55
|
+
<Input placeholder='you@clinic.com' {...field} />
|
|
56
|
+
</FormControl>
|
|
57
|
+
<FormDescription>
|
|
58
|
+
We’ll send the booking confirmation here.
|
|
59
|
+
</FormDescription>
|
|
60
|
+
<FormMessage />
|
|
61
|
+
</FormItem>
|
|
62
|
+
)}
|
|
63
|
+
/>
|
|
64
|
+
<Button type='submit'>Confirm booking</Button>
|
|
65
|
+
</form>
|
|
66
|
+
</Form>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const Default: Story = { render: () => <BookingForm /> };
|
package/src/ui/form.tsx
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as LabelPrimitive from '@radix-ui/react-label';
|
|
5
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
6
|
+
import {
|
|
7
|
+
Controller,
|
|
8
|
+
FormProvider,
|
|
9
|
+
useFormContext,
|
|
10
|
+
type ControllerProps,
|
|
11
|
+
type FieldPath,
|
|
12
|
+
type FieldValues,
|
|
13
|
+
} from 'react-hook-form';
|
|
14
|
+
|
|
15
|
+
import { cn } from '../lib/utils';
|
|
16
|
+
import { Label } from './label';
|
|
17
|
+
|
|
18
|
+
const Form = FormProvider;
|
|
19
|
+
|
|
20
|
+
type FormFieldContextValue<
|
|
21
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
22
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
23
|
+
> = {
|
|
24
|
+
name: TName;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const FormFieldContext = React.createContext<FormFieldContextValue | null>(
|
|
28
|
+
null
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const FormField = <
|
|
32
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
33
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
34
|
+
>({
|
|
35
|
+
...props
|
|
36
|
+
}: ControllerProps<TFieldValues, TName>) => {
|
|
37
|
+
return (
|
|
38
|
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
|
39
|
+
<Controller {...props} />
|
|
40
|
+
</FormFieldContext.Provider>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const useFormField = () => {
|
|
45
|
+
const fieldContext = React.useContext(FormFieldContext);
|
|
46
|
+
const itemContext = React.useContext(FormItemContext);
|
|
47
|
+
const { getFieldState, formState } = useFormContext();
|
|
48
|
+
|
|
49
|
+
if (!fieldContext) {
|
|
50
|
+
throw new Error('useFormField should be used within <FormField>');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!itemContext) {
|
|
54
|
+
throw new Error('useFormField should be used within <FormItem>');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fieldState = getFieldState(fieldContext.name, formState);
|
|
58
|
+
|
|
59
|
+
const { id } = itemContext;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
id,
|
|
63
|
+
name: fieldContext.name,
|
|
64
|
+
formItemId: `${id}-form-item`,
|
|
65
|
+
formDescriptionId: `${id}-form-item-description`,
|
|
66
|
+
formMessageId: `${id}-form-item-message`,
|
|
67
|
+
...fieldState,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type FormItemContextValue = {
|
|
72
|
+
id: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const FormItemContext = React.createContext<FormItemContextValue | null>(null);
|
|
76
|
+
|
|
77
|
+
const FormItem = React.forwardRef<
|
|
78
|
+
HTMLDivElement,
|
|
79
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
80
|
+
>(({ className, ...props }, ref) => {
|
|
81
|
+
const id = React.useId();
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<FormItemContext.Provider value={{ id }}>
|
|
85
|
+
<div ref={ref} className={cn('space-y-2', className)} {...props} />
|
|
86
|
+
</FormItemContext.Provider>
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
FormItem.displayName = 'FormItem';
|
|
90
|
+
|
|
91
|
+
const FormLabel = React.forwardRef<
|
|
92
|
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
93
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
|
94
|
+
>(({ className, ...props }, ref) => {
|
|
95
|
+
const { error, formItemId } = useFormField();
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Label
|
|
99
|
+
ref={ref}
|
|
100
|
+
className={cn(error && 'text-destructive', className)}
|
|
101
|
+
htmlFor={formItemId}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
FormLabel.displayName = 'FormLabel';
|
|
107
|
+
|
|
108
|
+
const FormControl = React.forwardRef<
|
|
109
|
+
React.ElementRef<typeof Slot>,
|
|
110
|
+
React.ComponentPropsWithoutRef<typeof Slot>
|
|
111
|
+
>(({ ...props }, ref) => {
|
|
112
|
+
const { error, formItemId, formDescriptionId, formMessageId } =
|
|
113
|
+
useFormField();
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Slot
|
|
117
|
+
ref={ref}
|
|
118
|
+
id={formItemId}
|
|
119
|
+
aria-describedby={
|
|
120
|
+
!error
|
|
121
|
+
? `${formDescriptionId}`
|
|
122
|
+
: `${formDescriptionId} ${formMessageId}`
|
|
123
|
+
}
|
|
124
|
+
aria-invalid={!!error}
|
|
125
|
+
{...props}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
FormControl.displayName = 'FormControl';
|
|
130
|
+
|
|
131
|
+
const FormDescription = React.forwardRef<
|
|
132
|
+
HTMLParagraphElement,
|
|
133
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
134
|
+
>(({ className, ...props }, ref) => {
|
|
135
|
+
const { formDescriptionId } = useFormField();
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<p
|
|
139
|
+
ref={ref}
|
|
140
|
+
id={formDescriptionId}
|
|
141
|
+
className={cn('text-muted-foreground text-[0.8rem]', className)}
|
|
142
|
+
{...props}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
FormDescription.displayName = 'FormDescription';
|
|
147
|
+
|
|
148
|
+
const FormMessage = React.forwardRef<
|
|
149
|
+
HTMLParagraphElement,
|
|
150
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
151
|
+
>(({ className, children, ...props }, ref) => {
|
|
152
|
+
const { error, formMessageId } = useFormField();
|
|
153
|
+
const body = error ? String(error?.message ?? '') : children;
|
|
154
|
+
|
|
155
|
+
if (!body) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<p
|
|
161
|
+
ref={ref}
|
|
162
|
+
id={formMessageId}
|
|
163
|
+
className={cn('text-destructive text-[0.8rem] font-medium', className)}
|
|
164
|
+
{...props}
|
|
165
|
+
>
|
|
166
|
+
{body}
|
|
167
|
+
</p>
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
FormMessage.displayName = 'FormMessage';
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
useFormField,
|
|
174
|
+
Form,
|
|
175
|
+
FormItem,
|
|
176
|
+
FormLabel,
|
|
177
|
+
FormControl,
|
|
178
|
+
FormDescription,
|
|
179
|
+
FormMessage,
|
|
180
|
+
FormField,
|
|
181
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '../story-types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
InputOTP,
|
|
5
|
+
InputOTPGroup,
|
|
6
|
+
InputOTPSeparator,
|
|
7
|
+
InputOTPSlot,
|
|
8
|
+
} from './input-otp';
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: 'Components/InputOTP',
|
|
12
|
+
component: InputOTP,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
// Container: InputOTP renders composed children (the groups/slots/separator),
|
|
15
|
+
// so `children` is a slot and must not also be an argType (§5). argTypes are
|
|
16
|
+
// real `OTPInput` props (§6: containerClassName/className stay unexposed).
|
|
17
|
+
argTypes: {
|
|
18
|
+
maxLength: {
|
|
19
|
+
control: 'number',
|
|
20
|
+
description: 'Number of characters in the code',
|
|
21
|
+
},
|
|
22
|
+
textAlign: {
|
|
23
|
+
control: 'inline-radio',
|
|
24
|
+
options: ['left', 'center', 'right'],
|
|
25
|
+
},
|
|
26
|
+
disabled: { control: 'boolean' },
|
|
27
|
+
autoFocus: { control: 'boolean' },
|
|
28
|
+
},
|
|
29
|
+
args: { maxLength: 6 },
|
|
30
|
+
parameters: { puck: { slots: ['children'] } },
|
|
31
|
+
} satisfies Meta<typeof InputOTP>;
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof meta>;
|
|
35
|
+
|
|
36
|
+
export const Default: Story = {
|
|
37
|
+
render: (args) => (
|
|
38
|
+
<InputOTP maxLength={6} {...args}>
|
|
39
|
+
<InputOTPGroup>
|
|
40
|
+
<InputOTPSlot index={0} />
|
|
41
|
+
<InputOTPSlot index={1} />
|
|
42
|
+
<InputOTPSlot index={2} />
|
|
43
|
+
</InputOTPGroup>
|
|
44
|
+
<InputOTPSeparator />
|
|
45
|
+
<InputOTPGroup>
|
|
46
|
+
<InputOTPSlot index={3} />
|
|
47
|
+
<InputOTPSlot index={4} />
|
|
48
|
+
<InputOTPSlot index={5} />
|
|
49
|
+
</InputOTPGroup>
|
|
50
|
+
</InputOTP>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { OTPInput, OTPInputContext } from 'input-otp';
|
|
5
|
+
import { Minus } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/utils';
|
|
8
|
+
|
|
9
|
+
const InputOTP = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof OTPInput>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof OTPInput>
|
|
12
|
+
>(({ className, containerClassName, ...props }, ref) => (
|
|
13
|
+
<OTPInput
|
|
14
|
+
ref={ref}
|
|
15
|
+
containerClassName={cn(
|
|
16
|
+
'flex items-center gap-2 has-[:disabled]:opacity-50',
|
|
17
|
+
containerClassName
|
|
18
|
+
)}
|
|
19
|
+
className={cn('disabled:cursor-not-allowed', className)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
));
|
|
23
|
+
InputOTP.displayName = 'InputOTP';
|
|
24
|
+
|
|
25
|
+
const InputOTPGroup = React.forwardRef<
|
|
26
|
+
React.ElementRef<'div'>,
|
|
27
|
+
React.ComponentPropsWithoutRef<'div'>
|
|
28
|
+
>(({ className, ...props }, ref) => (
|
|
29
|
+
<div ref={ref} className={cn('flex items-center', className)} {...props} />
|
|
30
|
+
));
|
|
31
|
+
InputOTPGroup.displayName = 'InputOTPGroup';
|
|
32
|
+
|
|
33
|
+
const InputOTPSlot = React.forwardRef<
|
|
34
|
+
React.ElementRef<'div'>,
|
|
35
|
+
React.ComponentPropsWithoutRef<'div'> & { index: number }
|
|
36
|
+
>(({ index, className, ...props }, ref) => {
|
|
37
|
+
const inputOTPContext = React.useContext(OTPInputContext);
|
|
38
|
+
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={cn(
|
|
44
|
+
'border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
|
|
45
|
+
isActive && 'ring-ring z-10 ring-1',
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{char}
|
|
51
|
+
{hasFakeCaret && (
|
|
52
|
+
<div className='pointer-events-none absolute inset-0 flex items-center justify-center'>
|
|
53
|
+
<div className='animate-caret-blink bg-foreground h-4 w-px duration-1000' />
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
InputOTPSlot.displayName = 'InputOTPSlot';
|
|
60
|
+
|
|
61
|
+
const InputOTPSeparator = React.forwardRef<
|
|
62
|
+
React.ElementRef<'div'>,
|
|
63
|
+
React.ComponentPropsWithoutRef<'div'>
|
|
64
|
+
>(({ ...props }, ref) => (
|
|
65
|
+
<div ref={ref} role='separator' {...props}>
|
|
66
|
+
<Minus />
|
|
67
|
+
</div>
|
|
68
|
+
));
|
|
69
|
+
InputOTPSeparator.displayName = 'InputOTPSeparator';
|
|
70
|
+
|
|
71
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '../story-types';
|
|
2
|
+
|
|
3
|
+
import { Input } from './input';
|
|
4
|
+
import { Label } from './label';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Components/Input',
|
|
8
|
+
component: Input,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
// Leaf form control — self-closing, so NO slot. Props below are the real
|
|
11
|
+
// `React.ComponentProps<'input'>` members worth exposing to a designer.
|
|
12
|
+
// `className`/`style` are deliberately not exposed (§6).
|
|
13
|
+
argTypes: {
|
|
14
|
+
type: {
|
|
15
|
+
control: 'select',
|
|
16
|
+
options: [
|
|
17
|
+
'text',
|
|
18
|
+
'email',
|
|
19
|
+
'password',
|
|
20
|
+
'number',
|
|
21
|
+
'tel',
|
|
22
|
+
'url',
|
|
23
|
+
'search',
|
|
24
|
+
'date',
|
|
25
|
+
'time',
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
placeholder: { control: 'text' },
|
|
29
|
+
defaultValue: {
|
|
30
|
+
control: 'text',
|
|
31
|
+
description: 'Initial (uncontrolled) value',
|
|
32
|
+
},
|
|
33
|
+
disabled: { control: 'boolean' },
|
|
34
|
+
required: { control: 'boolean' },
|
|
35
|
+
readOnly: { control: 'boolean' },
|
|
36
|
+
},
|
|
37
|
+
args: { type: 'email', placeholder: 'you@clinic.com' },
|
|
38
|
+
} satisfies Meta<typeof Input>;
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
type Story = StoryObj<typeof meta>;
|
|
42
|
+
|
|
43
|
+
export const Default: Story = {};
|
|
44
|
+
|
|
45
|
+
export const WithLabel: Story = {
|
|
46
|
+
render: (args) => (
|
|
47
|
+
<div className='grid w-72 gap-2'>
|
|
48
|
+
<Label htmlFor='email'>Email</Label>
|
|
49
|
+
<Input id='email' type='email' {...args} />
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Disabled: Story = {
|
|
55
|
+
args: { disabled: true, defaultValue: 'Locked' },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const Types: Story = {
|
|
59
|
+
render: () => (
|
|
60
|
+
<div className='grid w-72 gap-3'>
|
|
61
|
+
<Input type='text' placeholder='Text' />
|
|
62
|
+
<Input type='password' placeholder='Password' />
|
|
63
|
+
<Input type='number' placeholder='Number' />
|
|
64
|
+
<Input type='date' />
|
|
65
|
+
<Input type='file' />
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
};
|
package/src/ui/input.tsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../lib/utils';
|
|
4
|
+
|
|
5
|
+
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
|
6
|
+
({ className, type, ...props }, ref) => {
|
|
7
|
+
return (
|
|
8
|
+
<input
|
|
9
|
+
type={type}
|
|
10
|
+
className={cn(
|
|
11
|
+
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
ref={ref}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
Input.displayName = 'Input';
|
|
21
|
+
|
|
22
|
+
export { Input };
|