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.
Files changed (107) hide show
  1. package/components.json +21 -0
  2. package/dist/styles.css +3452 -0
  3. package/package.json +103 -0
  4. package/src/layout/app-shell.tsx +115 -0
  5. package/src/layout/app-topbar.stories.tsx +74 -0
  6. package/src/layout/app-topbar.tsx +145 -0
  7. package/src/layout/auth-layout.tsx +45 -0
  8. package/src/layout/box.stories.tsx +86 -0
  9. package/src/layout/box.tsx +34 -0
  10. package/src/layout/center.tsx +24 -0
  11. package/src/layout/container.stories.tsx +44 -0
  12. package/src/layout/container.tsx +39 -0
  13. package/src/layout/divider.tsx +64 -0
  14. package/src/layout/flex.stories.tsx +109 -0
  15. package/src/layout/flex.test.tsx +71 -0
  16. package/src/layout/flex.tsx +112 -0
  17. package/src/layout/grid.stories.tsx +91 -0
  18. package/src/layout/grid.tsx +132 -0
  19. package/src/layout/heading.tsx +53 -0
  20. package/src/layout/index.ts +47 -0
  21. package/src/layout/layout-style.ts +291 -0
  22. package/src/layout/link.stories.tsx +54 -0
  23. package/src/layout/link.test.tsx +75 -0
  24. package/src/layout/link.tsx +100 -0
  25. package/src/layout/navbar.stories.tsx +70 -0
  26. package/src/layout/navbar.tsx +128 -0
  27. package/src/layout/page-header.stories.tsx +80 -0
  28. package/src/layout/page-header.tsx +59 -0
  29. package/src/layout/section.tsx +22 -0
  30. package/src/layout/spacer.tsx +21 -0
  31. package/src/layout/stack.stories.tsx +58 -0
  32. package/src/layout/stack.tsx +19 -0
  33. package/src/layout/text.tsx +139 -0
  34. package/src/layout/typography.stories.tsx +112 -0
  35. package/src/lib/utils.ts +6 -0
  36. package/src/story-types.ts +80 -0
  37. package/src/styles/build.css +33 -0
  38. package/src/styles/globals.css +9 -0
  39. package/src/styles/tokens.css +281 -0
  40. package/src/ui/accordion.stories.tsx +84 -0
  41. package/src/ui/accordion.tsx +57 -0
  42. package/src/ui/alert-dialog.stories.tsx +57 -0
  43. package/src/ui/alert-dialog.tsx +141 -0
  44. package/src/ui/alert.stories.tsx +46 -0
  45. package/src/ui/alert.tsx +59 -0
  46. package/src/ui/avatar.stories.tsx +56 -0
  47. package/src/ui/avatar.tsx +50 -0
  48. package/src/ui/badge.stories.tsx +105 -0
  49. package/src/ui/badge.tsx +57 -0
  50. package/src/ui/breadcrumb.stories.tsx +69 -0
  51. package/src/ui/breadcrumb.tsx +140 -0
  52. package/src/ui/button.stories.tsx +85 -0
  53. package/src/ui/button.tsx +60 -0
  54. package/src/ui/card.stories.tsx +62 -0
  55. package/src/ui/card.tsx +96 -0
  56. package/src/ui/carousel.stories.tsx +55 -0
  57. package/src/ui/carousel.tsx +262 -0
  58. package/src/ui/checkbox.stories.tsx +52 -0
  59. package/src/ui/checkbox.tsx +30 -0
  60. package/src/ui/combobox.stories.tsx +138 -0
  61. package/src/ui/combobox.tsx +217 -0
  62. package/src/ui/command.tsx +184 -0
  63. package/src/ui/dialog.stories.tsx +66 -0
  64. package/src/ui/dialog.tsx +163 -0
  65. package/src/ui/drawer.stories.tsx +80 -0
  66. package/src/ui/drawer.tsx +118 -0
  67. package/src/ui/dropdown-menu.stories.tsx +57 -0
  68. package/src/ui/dropdown-menu.tsx +201 -0
  69. package/src/ui/form.stories.tsx +70 -0
  70. package/src/ui/form.tsx +181 -0
  71. package/src/ui/input-otp.stories.tsx +52 -0
  72. package/src/ui/input-otp.tsx +71 -0
  73. package/src/ui/input.stories.tsx +68 -0
  74. package/src/ui/input.tsx +22 -0
  75. package/src/ui/label.stories.tsx +35 -0
  76. package/src/ui/label.tsx +26 -0
  77. package/src/ui/navigation-menu.stories.tsx +79 -0
  78. package/src/ui/navigation-menu.tsx +128 -0
  79. package/src/ui/pagination.stories.tsx +62 -0
  80. package/src/ui/pagination.tsx +137 -0
  81. package/src/ui/popover.tsx +89 -0
  82. package/src/ui/progress.stories.tsx +33 -0
  83. package/src/ui/progress.tsx +28 -0
  84. package/src/ui/radio-group.stories.tsx +52 -0
  85. package/src/ui/radio-group.tsx +44 -0
  86. package/src/ui/scheduling-chip.stories.tsx +117 -0
  87. package/src/ui/scheduling-chip.tsx +62 -0
  88. package/src/ui/scroll-area.stories.tsx +52 -0
  89. package/src/ui/scroll-area.tsx +48 -0
  90. package/src/ui/select.stories.tsx +56 -0
  91. package/src/ui/select.tsx +159 -0
  92. package/src/ui/sheet.stories.tsx +63 -0
  93. package/src/ui/sheet.tsx +141 -0
  94. package/src/ui/skeleton.stories.tsx +40 -0
  95. package/src/ui/skeleton.tsx +15 -0
  96. package/src/ui/sonner.stories.tsx +62 -0
  97. package/src/ui/sonner.tsx +34 -0
  98. package/src/ui/switch.stories.tsx +41 -0
  99. package/src/ui/switch.tsx +29 -0
  100. package/src/ui/table.stories.tsx +88 -0
  101. package/src/ui/table.tsx +132 -0
  102. package/src/ui/tabs.stories.tsx +63 -0
  103. package/src/ui/tabs.tsx +55 -0
  104. package/src/ui/textarea.stories.tsx +45 -0
  105. package/src/ui/textarea.tsx +22 -0
  106. package/src/ui/tooltip.stories.tsx +48 -0
  107. 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 /> };
@@ -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
+ };
@@ -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 };