@k3-universe/react-kit 0.0.14 → 0.0.15

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.
@@ -5,12 +5,13 @@ import type {
5
5
  FieldValues,
6
6
  Path,
7
7
  UseFormGetValues,
8
+ UseFormReturn,
8
9
  UseFormSetValue,
9
10
  DefaultValues,
10
11
  } from 'react-hook-form'
11
12
  import type { z } from 'zod'
12
13
  import type { Accept } from 'react-dropzone'
13
- import type { SectionFlexOptions, SectionGridOptions, SectionLayout, SectionNode } from '../section/types'
14
+ import type { SectionFlexOptions, SectionGridOptions, SectionLayout } from '../section/types'
14
15
  import type { AutocompleteFetcher, AutocompleteOption } from '../../components/autocomplete/types'
15
16
  import type { FileRecord, FileUploaderLayout } from '../../components/fileuploader/types'
16
17
 
@@ -192,7 +193,8 @@ export interface FormBuilderProps<TFieldValues extends FieldValues = FieldValues
192
193
  showActions?: boolean
193
194
  customActions?: React.ReactNode
194
195
  showActionsSeparator?: boolean
196
+ form?: UseFormReturn<TFieldValues>
195
197
  }
196
198
 
197
199
  // Re-export for external consumers that build custom section nodes
198
- export type { SectionNode }
200
+ export type { SectionNode } from '../section/types'
@@ -419,7 +419,7 @@ export function Autocomplete<T = unknown>({
419
419
  tabIndex={disabled ? -1 : 0}
420
420
  aria-disabled={disabled || undefined}
421
421
  className={cn(
422
- "w-full inline-flex items-center justify-between rounded-md border bg-background px-3 py-2 text-sm shadow-sm transition-colors",
422
+ "w-full inline-flex items-center justify-between rounded-md border border-border bg-background px-3 py-2 text-sm shadow-sm transition-colors",
423
423
  "hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
424
424
  disabled && "opacity-50 pointer-events-none",
425
425
  className,
@@ -19,7 +19,7 @@
19
19
  --destructive: oklch(0.6368 0.2078 25.3313);
20
20
  --destructive-foreground: oklch(1.0000 0 0);
21
21
  --border: oklch(0.9197 0.0040 286.3202);
22
- --input: oklch(0.9197 0.0040 286.3202);
22
+ --input: oklch(1.0000 0 0);
23
23
  --ring: oklch(0.5234 0.1347 144.1672);
24
24
  --chart-1: oklch(0.5234 0.1347 144.1672);
25
25
  --chart-2: oklch(0.6731 0.1624 144.2083);
@@ -14,7 +14,7 @@ const buttonVariants = cva(
14
14
  destructive:
15
15
  "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16
16
  outline:
17
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ "border border-border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-border dark:hover:bg-input/50",
18
18
  secondary:
19
19
  "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20
20
  ghost:
@@ -67,7 +67,7 @@ function CommandInput({
67
67
  return (
68
68
  <div
69
69
  data-slot="command-input-wrapper"
70
- className="flex h-9 items-center gap-2 border-b px-3"
70
+ className="flex h-9 items-center gap-2 border-b border-border px-3"
71
71
  >
72
72
  <SearchIcon className="size-4 shrink-0 opacity-50" />
73
73
  <CommandPrimitive.Input
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
8
8
  type={type}
9
9
  data-slot="input"
10
10
  className={cn(
11
- "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
11
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-border flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
12
  "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13
13
  "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14
14
  className
@@ -30,7 +30,7 @@ function PopoverContent({
30
30
  align={align}
31
31
  sideOffset={sideOffset}
32
32
  className={cn(
33
- "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 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
33
+ "border-border 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 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
34
34
  className
35
35
  )}
36
36
  {...props}
@@ -37,7 +37,7 @@ function SelectTrigger({
37
37
  data-slot="select-trigger"
38
38
  data-size={size}
39
39
  className={cn(
40
- "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
40
+ "border-border data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
41
41
  className
42
42
  )}
43
43
  {...props}
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
7
7
  <textarea
8
8
  data-slot="textarea"
9
9
  className={cn(
10
- "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
10
+ "border-border placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
11
11
  className
12
12
  )}
13
13
  {...props}
@@ -0,0 +1,335 @@
1
+ import { useMemo } from 'react'
2
+ import type { Meta, StoryObj } from '@storybook/react'
3
+ import { useForm } from 'react-hook-form'
4
+ import { FormBuilder } from '../../../index'
5
+ import type { FormBuilderSectionConfig } from '../../../kit/builder/form/types'
6
+
7
+ interface SplitFormValues {
8
+ firstName: string
9
+ lastName: string
10
+ email: string
11
+ accountType: 'individual' | 'business'
12
+ companyName?: string
13
+ address: {
14
+ street: string
15
+ city: string
16
+ zip: string
17
+ }
18
+ marketingOptIn: boolean
19
+ preferredContactMethod: 'email' | 'phone'
20
+ phoneNumber?: string
21
+ newsletterTopics: string[]
22
+ complianceContactEmail?: string
23
+ eventsWebhookUrl?: string
24
+ smsOptIn: boolean
25
+ smsFrequency?: 'daily' | 'weekly' | 'monthly'
26
+ timezone: string
27
+ }
28
+
29
+ const generalInfoSections: FormBuilderSectionConfig<SplitFormValues>[] = [
30
+ {
31
+ title: 'Profile',
32
+ layout: 'grid',
33
+ grid: { cols: 2, gap: 'gap-4' },
34
+ fields: [
35
+ {
36
+ name: 'firstName',
37
+ label: 'First name',
38
+ type: 'text',
39
+ required: true,
40
+ },
41
+ {
42
+ name: 'lastName',
43
+ label: 'Last name',
44
+ type: 'text',
45
+ required: true,
46
+ },
47
+ {
48
+ name: 'email',
49
+ label: 'Email',
50
+ type: 'email',
51
+ required: true,
52
+ gridCols: 2,
53
+ },
54
+ {
55
+ name: 'accountType',
56
+ label: 'Account type',
57
+ type: 'radio',
58
+ options: [
59
+ { label: 'Individual', value: 'individual' },
60
+ { label: 'Business', value: 'business' },
61
+ ],
62
+ defaultValue: 'individual',
63
+ gridCols: 2,
64
+ description: 'Switch to Business to reveal additional company details.',
65
+ },
66
+ {
67
+ name: 'companyName',
68
+ label: 'Company name',
69
+ type: 'text',
70
+ placeholder: 'Acme Inc.',
71
+ dependencies: [
72
+ {
73
+ field: 'accountType',
74
+ condition: (value) => value === 'business',
75
+ action: 'show',
76
+ },
77
+ ],
78
+ gridCols: 2,
79
+ },
80
+ ],
81
+ },
82
+ {
83
+ title: 'Address',
84
+ layout: 'grid',
85
+ grid: { cols: 3, gap: 'gap-4' },
86
+ fields: [
87
+ {
88
+ name: 'address.street',
89
+ label: 'Street',
90
+ type: 'text',
91
+ required: true,
92
+ gridCols: 3,
93
+ },
94
+ {
95
+ name: 'address.city',
96
+ label: 'City',
97
+ type: 'text',
98
+ required: true,
99
+ gridCols: 2,
100
+ },
101
+ {
102
+ name: 'address.zip',
103
+ label: 'ZIP code',
104
+ type: 'text',
105
+ required: true,
106
+ },
107
+ ],
108
+ },
109
+ ]
110
+
111
+ const preferencesSections: FormBuilderSectionConfig<SplitFormValues>[] = [
112
+ {
113
+ title: 'Preferences',
114
+ layout: 'grid',
115
+ grid: { cols: 2, gap: 'gap-4' },
116
+ fields: [
117
+ {
118
+ name: 'marketingOptIn',
119
+ label: 'Marketing emails',
120
+ type: 'switch',
121
+ defaultValue: true,
122
+ description: 'Receive occasional product updates and tips.',
123
+ },
124
+ {
125
+ name: 'newsletterTopics',
126
+ label: 'Topics of interest',
127
+ type: 'select',
128
+ options: [
129
+ { label: 'Product releases', value: 'product' },
130
+ { label: 'Best practices', value: 'best-practices' },
131
+ { label: 'Events', value: 'events' },
132
+ { label: 'Integrations', value: 'integrations' },
133
+ ],
134
+ multiple: true,
135
+ placeholder: 'Choose one or more topics',
136
+ dependencies: [
137
+ {
138
+ field: 'marketingOptIn',
139
+ condition: (value) => Boolean(value),
140
+ action: 'show',
141
+ },
142
+ {
143
+ field: 'marketingOptIn',
144
+ condition: (value) => !value,
145
+ action: 'setValue',
146
+ value: [],
147
+ },
148
+ ],
149
+ },
150
+ {
151
+ name: 'eventsWebhookUrl',
152
+ label: 'Events webhook URL',
153
+ type: 'text',
154
+ placeholder: 'https://example.com/webhooks/events',
155
+ description: 'Provide a webhook to receive notifications about upcoming events.',
156
+ dependencies: [
157
+ {
158
+ field: 'newsletterTopics',
159
+ condition: (value) => Array.isArray(value) && value.includes('events'),
160
+ action: 'show',
161
+ },
162
+ ],
163
+ },
164
+ {
165
+ name: 'preferredContactMethod',
166
+ label: 'Preferred contact method',
167
+ type: 'radio',
168
+ options: [
169
+ { label: 'Email', value: 'email' },
170
+ { label: 'Phone', value: 'phone' },
171
+ ],
172
+ defaultValue: 'email',
173
+ },
174
+ {
175
+ name: 'phoneNumber',
176
+ label: 'Phone number',
177
+ type: 'text',
178
+ placeholder: '(555) 123-4567',
179
+ dependencies: [
180
+ {
181
+ field: 'preferredContactMethod',
182
+ condition: (value) => value === 'phone',
183
+ action: 'show',
184
+ },
185
+ ],
186
+ },
187
+ {
188
+ name: 'smsOptIn',
189
+ label: 'SMS updates',
190
+ type: 'switch',
191
+ defaultValue: false,
192
+ description: 'Get real-time notifications via SMS.',
193
+ dependencies: [
194
+ {
195
+ field: 'preferredContactMethod',
196
+ condition: (value) => value === 'phone',
197
+ action: 'show',
198
+ },
199
+ ],
200
+ },
201
+ {
202
+ name: 'smsFrequency',
203
+ label: 'SMS frequency',
204
+ type: 'select',
205
+ options: [
206
+ { label: 'Daily recap', value: 'daily' },
207
+ { label: 'Weekly summary', value: 'weekly' },
208
+ { label: 'Monthly digest', value: 'monthly' },
209
+ ],
210
+ placeholder: 'Choose how often we should text you',
211
+ dependencies: [
212
+ {
213
+ field: 'smsOptIn',
214
+ condition: (value) => Boolean(value),
215
+ action: 'show',
216
+ },
217
+ ],
218
+ },
219
+ {
220
+ name: 'complianceContactEmail',
221
+ label: 'Compliance contact email',
222
+ type: 'email',
223
+ placeholder: 'compliance@company.com',
224
+ description: 'Required for business accounts to receive compliance updates.',
225
+ dependencies: [
226
+ {
227
+ field: 'accountType',
228
+ condition: (value) => value === 'business',
229
+ action: 'show',
230
+ },
231
+ ],
232
+ gridCols: 2,
233
+ },
234
+ {
235
+ name: 'timezone',
236
+ label: 'Timezone',
237
+ type: 'select',
238
+ options: [
239
+ { label: 'UTC−08:00 Pacific', value: 'America/Los_Angeles' },
240
+ { label: 'UTC−05:00 Eastern', value: 'America/New_York' },
241
+ { label: 'UTC+00:00 London', value: 'Europe/London' },
242
+ { label: 'UTC+07:00 Jakarta', value: 'Asia/Jakarta' },
243
+ ],
244
+ required: true,
245
+ },
246
+ ],
247
+ },
248
+ ]
249
+
250
+ const SplitFormExample = () => {
251
+ const form = useForm<SplitFormValues>({
252
+ defaultValues: {
253
+ firstName: 'Jane',
254
+ lastName: 'Doe',
255
+ email: 'jane.doe@example.com',
256
+ accountType: 'individual',
257
+ companyName: '',
258
+ address: {
259
+ street: '123 Storybook Way',
260
+ city: 'Componentville',
261
+ zip: '90210',
262
+ },
263
+ marketingOptIn: true,
264
+ newsletterTopics: ['product'],
265
+ eventsWebhookUrl: '',
266
+ preferredContactMethod: 'email',
267
+ phoneNumber: '',
268
+ smsOptIn: false,
269
+ smsFrequency: undefined,
270
+ complianceContactEmail: '',
271
+ timezone: 'Asia/Jakarta',
272
+ },
273
+ mode: 'onSubmit',
274
+ })
275
+
276
+ const logSubmit = useMemo(
277
+ () =>
278
+ form.handleSubmit((values) => {
279
+ console.log('Story submit', values)
280
+ }),
281
+ [form],
282
+ )
283
+
284
+ return (
285
+ <div className="space-y-6">
286
+ <FormBuilder
287
+ form={form}
288
+ sections={generalInfoSections}
289
+ onSubmit={async () => {
290
+ /* handled by explicit button */
291
+ }}
292
+ showActions={false}
293
+ />
294
+
295
+ <FormBuilder
296
+ form={form}
297
+ sections={preferencesSections}
298
+ onSubmit={async () => {
299
+ /* handled by explicit button */
300
+ }}
301
+ customActions={(
302
+ <button
303
+ type="button"
304
+ className="px-4 py-2 text-sm font-medium text-white bg-primary rounded-md"
305
+ onClick={logSubmit}
306
+ >
307
+ Save all sections
308
+ </button>
309
+ )}
310
+ />
311
+ </div>
312
+ )
313
+ }
314
+
315
+ const meta: Meta<typeof SplitFormExample> = {
316
+ title: 'Kit/Builder/Form',
317
+ component: SplitFormExample,
318
+ parameters: {
319
+ docs: {
320
+ description: {
321
+ component:
322
+ 'Demonstrates sharing a single `react-hook-form` instance across multiple `FormBuilder` blocks using the new `form` prop.',
323
+ },
324
+ },
325
+ },
326
+ }
327
+
328
+ export default meta
329
+
330
+ type Story = StoryObj<typeof SplitFormExample>
331
+
332
+ export const SharedFormInstance: Story = {
333
+ name: 'Shared form instance across sections',
334
+ render: () => <SplitFormExample />,
335
+ }