@classytic/formkit 1.3.0 → 1.4.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/server.mjs CHANGED
@@ -85,6 +85,14 @@ function toRules(condition) {
85
85
  logic: "and"
86
86
  };
87
87
  }
88
+ function resolveRuleObject(rule, defaultMessage) {
89
+ if (rule !== null && typeof rule === "object" && "value" in rule && "message" in rule) return rule;
90
+ const v = rule;
91
+ return {
92
+ value: v,
93
+ message: defaultMessage(v)
94
+ };
95
+ }
88
96
  /**
89
97
  * Evaluates a conditional rule, array of rules, or a ConditionConfig against form values.
90
98
  * Supports AND (default) and OR logic via ConditionConfig.
@@ -95,7 +103,12 @@ function toRules(condition) {
95
103
  */
96
104
  function evaluateCondition(condition, formValues) {
97
105
  if (!condition) return true;
98
- if (typeof condition === "function") return condition(formValues);
106
+ if (typeof condition === "function") try {
107
+ return condition(formValues);
108
+ } catch (err) {
109
+ console.warn("[FormKit] Condition function threw:", err);
110
+ return false;
111
+ }
99
112
  const { rules, logic } = toRules(condition);
100
113
  const evalFn = (rule) => evaluateRule(rule, formValues);
101
114
  return logic === "or" ? rules.some(evalFn) : rules.every(evalFn);
@@ -130,6 +143,7 @@ function defineSection(section) {
130
143
  /**
131
144
  * Extracts default values from a form schema.
132
145
  * Walks all sections and fields, respecting nameSpace prefixes and group nesting.
146
+ * Array fields default to `[]` when no explicit `defaultValue` is provided.
133
147
  *
134
148
  * @example
135
149
  * ```ts
@@ -143,9 +157,11 @@ function extractDefaultValues(schema) {
143
157
  const prefix = section.nameSpace ? `${section.nameSpace}.` : "";
144
158
  if (!section.fields) continue;
145
159
  for (const field of section.fields) {
146
- if (field.defaultValue !== void 0) defaults[`${prefix}${field.name}`] = field.defaultValue;
160
+ const key = `${prefix}${field.name}`;
161
+ if (field.defaultValue !== void 0) defaults[key] = field.defaultValue;
162
+ else if (field.type === "array") defaults[key] = [];
147
163
  if (field.itemFields && field.type !== "array") {
148
- for (const sub of field.itemFields) if (sub.defaultValue !== void 0) defaults[`${prefix}${field.name}.${sub.name}`] = sub.defaultValue;
164
+ for (const sub of field.itemFields) if (sub.defaultValue !== void 0) defaults[`${key}.${sub.name}`] = sub.defaultValue;
149
165
  }
150
166
  }
151
167
  }
@@ -156,6 +172,9 @@ function extractDefaultValues(schema) {
156
172
  * from a field's schema props. Maps `required`, `min`, `max`, `minLength`,
157
173
  * `maxLength`, `pattern`, and `validate` to RHF rules.
158
174
  *
175
+ * Supports both shorthand scalars and `{ value, message }` objects for all
176
+ * numeric/length rules, and `{ regex, message }` for pattern.
177
+ *
159
178
  * @example
160
179
  * ```tsx
161
180
  * import { buildValidationRules } from '@classytic/formkit';
@@ -168,29 +187,190 @@ function extractDefaultValues(schema) {
168
187
  */
169
188
  function buildValidationRules(field) {
170
189
  const rules = {};
171
- if (field.required) rules.required = `${field.label || field.name} is required`;
172
- if (field.minLength !== void 0) rules.minLength = {
173
- value: field.minLength,
174
- message: `At least ${field.minLength} characters`
175
- };
176
- if (field.maxLength !== void 0) rules.maxLength = {
177
- value: field.maxLength,
178
- message: `At most ${field.maxLength} characters`
190
+ if (field.required) rules.required = {
191
+ value: true,
192
+ message: `${field.label || field.name} is required`
179
193
  };
180
- if (field.min !== void 0) rules.min = {
181
- value: field.min,
182
- message: `Must be at least ${field.min}`
194
+ if (field.minLength !== void 0) {
195
+ const { value, message } = resolveRuleObject(field.minLength, (v) => `At least ${v} characters`);
196
+ rules.minLength = {
197
+ value,
198
+ message
199
+ };
200
+ }
201
+ if (field.maxLength !== void 0) {
202
+ const { value, message } = resolveRuleObject(field.maxLength, (v) => `At most ${v} characters`);
203
+ rules.maxLength = {
204
+ value,
205
+ message
206
+ };
207
+ }
208
+ if (field.min !== void 0) {
209
+ const { value, message } = resolveRuleObject(field.min, (v) => `Must be at least ${v}`);
210
+ rules.min = {
211
+ value,
212
+ message
213
+ };
214
+ }
215
+ if (field.max !== void 0) {
216
+ const { value, message } = resolveRuleObject(field.max, (v) => `Must be at most ${v}`);
217
+ rules.max = {
218
+ value,
219
+ message
220
+ };
221
+ }
222
+ if (field.pattern) {
223
+ const isObject = typeof field.pattern === "object";
224
+ const regexStr = isObject ? field.pattern.regex : field.pattern;
225
+ const message = isObject ? field.pattern.message : "Invalid format";
226
+ try {
227
+ rules.pattern = {
228
+ value: new RegExp(regexStr),
229
+ message
230
+ };
231
+ } catch {
232
+ console.warn(`[FormKit] Invalid regex pattern "${regexStr}" in field "${field.name}", skipping.`);
233
+ }
234
+ }
235
+ if (field.validate) rules.validate = field.validate;
236
+ return rules;
237
+ }
238
+ /** Returns true for fields that carry an `options` array (select, radio, etc.) */
239
+ function isChoiceField(field) {
240
+ return [
241
+ "select",
242
+ "combobox",
243
+ "multiselect",
244
+ "dependentSelect",
245
+ "radio",
246
+ "checkbox"
247
+ ].includes(field.type);
248
+ }
249
+ /** Returns true for free-text input fields */
250
+ function isTextField(field) {
251
+ return [
252
+ "text",
253
+ "email",
254
+ "password",
255
+ "tel",
256
+ "phone",
257
+ "url",
258
+ "slug",
259
+ "textarea",
260
+ "rich-text"
261
+ ].includes(field.type);
262
+ }
263
+ /** Returns true for numeric input fields */
264
+ function isNumericField(field) {
265
+ return ["number", "rating"].includes(field.type);
266
+ }
267
+ /** Returns true for date / time fields */
268
+ function isDateField(field) {
269
+ return [
270
+ "date",
271
+ "time",
272
+ "datetime"
273
+ ].includes(field.type);
274
+ }
275
+ /** Returns true for structural fields that contain sub-fields (`itemFields`) */
276
+ function isContainerField(field) {
277
+ return ["group", "array"].includes(field.type);
278
+ }
279
+ /** Returns true for array fields that render a repeatable list */
280
+ function isArrayField(field) {
281
+ return field.type === "array";
282
+ }
283
+ /** Returns true for fields that load options asynchronously */
284
+ function isDynamicField(field) {
285
+ return !!field.loadOptions;
286
+ }
287
+ /** Returns true for fields with conditional rendering */
288
+ function isConditionalField(field) {
289
+ return field.condition !== void 0;
290
+ }
291
+ /**
292
+ * Merge two or more schemas into one, concatenating their sections.
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * const full = mergeSchemas(personalSchema, addressSchema, billingSchema);
297
+ * ```
298
+ */
299
+ function mergeSchemas(...schemas) {
300
+ return { sections: schemas.flatMap((s) => s.sections) };
301
+ }
302
+ /**
303
+ * Add fields to a section identified by `sectionId`.
304
+ * Returns a new schema — the original is not mutated.
305
+ *
306
+ * @example
307
+ * ```ts
308
+ * const extended = extendSection(schema, "personal", [
309
+ * field.text("middleName", "Middle Name"),
310
+ * ]);
311
+ * ```
312
+ */
313
+ function extendSection(schema, sectionId, fields, position = "end") {
314
+ return {
315
+ ...schema,
316
+ sections: schema.sections.map((section) => {
317
+ if (section.id !== sectionId) return section;
318
+ const existing = section.fields ?? [];
319
+ return {
320
+ ...section,
321
+ fields: position === "start" ? [...fields, ...existing] : [...existing, ...fields]
322
+ };
323
+ })
183
324
  };
184
- if (field.max !== void 0) rules.max = {
185
- value: field.max,
186
- message: `Must be at most ${field.max}`
325
+ }
326
+ /**
327
+ * Create a new schema that includes only the named fields.
328
+ *
329
+ * @example
330
+ * ```ts
331
+ * const slim = pickFields(schema, ["email", "password"]);
332
+ * ```
333
+ */
334
+ function pickFields(schema, names) {
335
+ const nameSet = new Set(names);
336
+ return {
337
+ ...schema,
338
+ sections: schema.sections.map((section) => ({
339
+ ...section,
340
+ fields: (section.fields ?? []).filter((f) => nameSet.has(f.name))
341
+ })).filter((section) => (section.fields?.length ?? 0) > 0)
187
342
  };
188
- if (field.pattern) rules.pattern = {
189
- value: new RegExp(field.pattern),
190
- message: "Invalid format"
343
+ }
344
+ /**
345
+ * Create a new schema that excludes the named fields.
346
+ *
347
+ * @example
348
+ * ```ts
349
+ * const withoutInternal = omitFields(schema, ["__id", "__createdAt"]);
350
+ * ```
351
+ */
352
+ function omitFields(schema, names) {
353
+ const nameSet = new Set(names);
354
+ return {
355
+ ...schema,
356
+ sections: schema.sections.map((section) => ({
357
+ ...section,
358
+ fields: (section.fields ?? []).filter((f) => !nameSet.has(f.name))
359
+ }))
191
360
  };
192
- if (field.validate) rules.validate = field.validate;
193
- return rules;
361
+ }
362
+ /**
363
+ * Collect every field from every section into a flat array.
364
+ * Useful for validation, documentation, and AI schema introspection.
365
+ *
366
+ * @example
367
+ * ```ts
368
+ * const allFields = flattenSchema(schema);
369
+ * const required = allFields.filter(f => f.required);
370
+ * ```
371
+ */
372
+ function flattenSchema(schema) {
373
+ return schema.sections.flatMap((s) => s.fields ?? []);
194
374
  }
195
375
 
196
376
  //#endregion
@@ -198,226 +378,314 @@ function buildValidationRules(field) {
198
378
  /**
199
379
  * Type-safe field builder helpers for schema-driven forms.
200
380
  *
201
- * Provides shorthand methods for common field types with sensible defaults,
202
- * reducing boilerplate while maintaining full type safety.
381
+ * All methods are generic over TFieldValues, defaulting to FieldValues (any string)
382
+ * when no type argument is provided. Specify the generic to enforce that field
383
+ * names are valid paths in your form values type.
384
+ *
385
+ * For fully-typed schemas where every field name is checked, prefer
386
+ * `field.for<MyForm>()` which fixes the generic once for the whole schema:
203
387
  *
204
388
  * @example
205
389
  * ```ts
206
- * import { field, section } from '@classytic/formkit';
390
+ * // Untyped any string accepted (backwards compatible)
391
+ * field.text("email", "Email")
207
392
  *
208
- * const schema = {
209
- * sections: [
210
- * section("personal", "Personal Info", [
211
- * field.text("firstName", "First Name", { required: true }),
212
- * field.email("email", "Email"),
213
- * field.select("role", "Role", [
214
- * { label: "Admin", value: "admin" },
215
- * { label: "User", value: "user" },
216
- * ]),
217
- * ], { cols: 2 }),
218
- * ],
219
- * };
393
+ * // Per-call generic — name is checked against MyForm
394
+ * field.text<MyForm>("email", "Email")
395
+ *
396
+ * // Typed factory name checked on every call without repeating the generic
397
+ * const f = field.for<MyForm>()
398
+ * f.text("email", "Email") //
399
+ * f.text("typo", "Email") // ✗ TypeScript error
220
400
  * ```
221
401
  */
222
402
  const field = {
223
- text: (name, label, props = {}) => ({
403
+ /** Text input field. */
404
+ text: (name, label, props) => ({
224
405
  type: "text",
225
406
  name,
226
407
  label,
227
- ...props
408
+ ...props ?? {}
228
409
  }),
229
- email: (name, label, props = {}) => ({
410
+ /** Email input field with default placeholder. */
411
+ email: (name, label, props) => ({
230
412
  type: "email",
231
413
  name,
232
414
  label,
233
415
  placeholder: "example@email.com",
234
- ...props
416
+ ...props ?? {}
235
417
  }),
236
- url: (name, label, props = {}) => ({
418
+ /** URL input field with default placeholder. */
419
+ url: (name, label, props) => ({
237
420
  type: "url",
238
421
  name,
239
422
  label,
240
423
  placeholder: "https://example.com",
241
- ...props
424
+ ...props ?? {}
242
425
  }),
243
- tel: (name, label, props = {}) => ({
426
+ /** Phone/tel input field with default placeholder. */
427
+ tel: (name, label, props) => ({
244
428
  type: "tel",
245
429
  name,
246
430
  label,
247
431
  placeholder: "+1 (555) 000-0000",
248
- ...props
432
+ ...props ?? {}
249
433
  }),
250
- password: (name, label, props = {}) => ({
434
+ /** Password input field. */
435
+ password: (name, label, props) => ({
251
436
  type: "password",
252
437
  name,
253
438
  label,
254
- ...props
439
+ ...props ?? {}
255
440
  }),
256
- number: (name, label, props = {}) => ({
441
+ /** Number input field with min: 0 default (overrideable via props). */
442
+ number: (name, label, props) => ({
257
443
  type: "number",
258
444
  name,
259
445
  label,
260
446
  min: 0,
261
- ...props
447
+ ...props ?? {}
262
448
  }),
263
- textarea: (name, label, props = {}) => ({
449
+ /** Textarea field with default 3 rows. */
450
+ textarea: (name, label, props) => ({
264
451
  type: "textarea",
265
452
  name,
266
453
  label,
267
454
  rows: 3,
268
- ...props
455
+ ...props ?? {}
269
456
  }),
270
- select: (name, label, options, props = {}) => ({
457
+ /** Select dropdown field. */
458
+ select: (name, label, options, props) => ({
271
459
  type: "select",
272
460
  name,
273
461
  label,
274
462
  options,
275
- ...props
463
+ ...props ?? {}
276
464
  }),
277
- combobox: (name, label, options, props = {}) => ({
465
+ /** Searchable combobox field. */
466
+ combobox: (name, label, options, props) => ({
278
467
  type: "combobox",
279
468
  name,
280
469
  label,
281
470
  options,
282
- ...props
471
+ ...props ?? {}
283
472
  }),
284
- multiselect: (name, label, options, props = {}) => ({
473
+ /** Multi-select field. */
474
+ multiselect: (name, label, options, props) => ({
285
475
  type: "multiselect",
286
476
  name,
287
477
  label,
288
478
  options,
289
479
  placeholder: "Select options...",
290
- ...props
291
- }),
292
- tagChoice: (name, label, options, props = {}) => ({
293
- type: "tagChoice",
294
- name,
295
- label,
296
- options,
297
- ...props
480
+ ...props ?? {}
298
481
  }),
299
- dependentSelect: (name, label, props = {}) => ({
482
+ /** Dependent select field that reacts to parent field changes. */
483
+ dependentSelect: (name, label, props) => ({
300
484
  type: "dependentSelect",
301
485
  name,
302
486
  label,
303
- ...props
487
+ ...props ?? {}
304
488
  }),
305
- switch: (name, label, props = {}) => ({
489
+ /** Switch/toggle field. */
490
+ switch: (name, label, props) => ({
306
491
  type: "switch",
307
492
  name,
308
493
  label,
309
- ...props
494
+ ...props ?? {}
310
495
  }),
311
- boolean: (name, label, props = {}) => ({
496
+ /** Boolean field (alias for switch). */
497
+ boolean: (name, label, props) => ({
312
498
  type: "switch",
313
499
  name,
314
500
  label,
315
- ...props
501
+ ...props ?? {}
316
502
  }),
317
- checkbox: (name, label, props = {}) => ({
503
+ /** Checkbox field. */
504
+ checkbox: (name, label, props) => ({
318
505
  type: "checkbox",
319
506
  name,
320
507
  label,
321
- ...props
508
+ ...props ?? {}
322
509
  }),
323
- radio: (name, label, options, props = {}) => ({
510
+ /** Radio button group field. */
511
+ radio: (name, label, options, props) => ({
324
512
  type: "radio",
325
513
  name,
326
514
  label,
327
515
  options,
328
- ...props
516
+ ...props ?? {}
329
517
  }),
330
- date: (name, label, props = {}) => ({
518
+ /** Date picker field. */
519
+ date: (name, label, props) => ({
331
520
  type: "date",
332
521
  name,
333
522
  label,
334
- ...props
523
+ ...props ?? {}
335
524
  }),
336
- tags: (name, label, props = {}) => ({
525
+ /** Tag input field. */
526
+ tags: (name, label, props) => ({
337
527
  type: "tags",
338
528
  name,
339
529
  label,
340
530
  placeholder: "Add tags...",
341
- ...props
531
+ ...props ?? {}
342
532
  }),
343
- slug: (name, label, props = {}) => ({
533
+ /** Slug field. */
534
+ slug: (name, label, props) => ({
344
535
  type: "slug",
345
536
  name,
346
537
  label,
347
538
  placeholder: "my-page-slug",
348
- ...props
539
+ ...props ?? {}
349
540
  }),
350
- file: (name, label, props = {}) => ({
541
+ /** File upload field. */
542
+ file: (name, label, props) => ({
351
543
  type: "file",
352
544
  name,
353
545
  label,
354
- ...props
355
- }),
356
- otp: (name, label, props = {}) => ({
357
- type: "otp",
358
- name,
359
- label,
360
- ...props
361
- }),
362
- asyncCombobox: (name, label, props = {}) => ({
363
- type: "asyncCombobox",
364
- name,
365
- label,
366
- ...props
367
- }),
368
- asyncMultiselect: (name, label, props = {}) => ({
369
- type: "asyncMultiselect",
370
- name,
371
- label,
372
- ...props
373
- }),
374
- dateTime: (name, label, props = {}) => ({
375
- type: "dateTime",
376
- name,
377
- label,
378
- ...props
546
+ ...props ?? {}
379
547
  }),
380
- hidden: (name, props = {}) => ({
548
+ /** Hidden field (no UI). */
549
+ hidden: (name, props) => ({
381
550
  type: "hidden",
382
551
  name,
383
- ...props
552
+ ...props ?? {}
384
553
  }),
385
- group: (name, label, itemFields, props = {}) => ({
554
+ /**
555
+ * Group field for nested objects.
556
+ * Renders itemFields as a sub-grid. Child names are relative (e.g. "street"),
557
+ * FormGenerator prefixes them with the group name at render time.
558
+ *
559
+ * @example
560
+ * ```ts
561
+ * field.group("address", "Address", [
562
+ * field.text("street", "Street"),
563
+ * field.text("city", "City"),
564
+ * ], { cols: 2 })
565
+ * ```
566
+ */
567
+ group: (name, label, itemFields, props) => ({
386
568
  type: "group",
387
569
  name,
388
570
  label,
389
571
  itemFields,
390
- ...props
572
+ ...props ?? {}
391
573
  }),
392
- array: (name, label, itemFields, props = {}) => ({
574
+ /**
575
+ * Array/repeatable field backed by react-hook-form's useFieldArray.
576
+ *
577
+ * @example
578
+ * ```ts
579
+ * field.array("contacts", "Contacts", [
580
+ * field.text("name", "Name"),
581
+ * field.email("email", "Email"),
582
+ * ])
583
+ * ```
584
+ */
585
+ array: (name, label, itemFields, props) => ({
393
586
  type: "array",
394
587
  name,
395
588
  label,
396
589
  itemFields,
397
- ...props
590
+ ...props ?? {}
398
591
  }),
399
- custom: (name, label, render, props = {}) => ({
592
+ /**
593
+ * Custom field with a render function.
594
+ * Bypasses the component registry — full control over rendering.
595
+ *
596
+ * The render callback receives the complete `FieldComponentProps` including
597
+ * `fieldId`, `errorId`, `shouldShowError`, `error`, `rules`, and `control`.
598
+ *
599
+ * Use `shouldShowError` (not `!!error`) to drive `aria-invalid` and error
600
+ * visibility so timing mirrors the CSS `:user-invalid` pseudo-class.
601
+ *
602
+ * @example
603
+ * ```tsx
604
+ * field.custom("skills", "Skills", ({ control, shouldShowError, errorId, error, fieldId }) => (
605
+ * <div>
606
+ * <SkillSelector
607
+ * id={fieldId}
608
+ * control={control}
609
+ * aria-invalid={shouldShowError || undefined}
610
+ * aria-errormessage={shouldShowError ? errorId : undefined}
611
+ * />
612
+ * {shouldShowError && (
613
+ * <p id={errorId} role="alert" className="text-sm text-destructive">
614
+ * {error?.message}
615
+ * </p>
616
+ * )}
617
+ * </div>
618
+ * ))
619
+ * ```
620
+ */
621
+ custom: (name, label, render, props) => ({
400
622
  type: "custom",
401
623
  name,
402
624
  label,
403
625
  render,
404
- ...props
626
+ ...props ?? {}
627
+ }),
628
+ /**
629
+ * Returns a typed field builder with `TFieldValues` fixed.
630
+ * Every field name is validated against `Path<TFieldValues>` at the call site —
631
+ * no need to repeat the generic on each individual builder call.
632
+ *
633
+ * @example
634
+ * ```ts
635
+ * interface ContactForm {
636
+ * firstName: string;
637
+ * email: string;
638
+ * address: { street: string; city: string };
639
+ * }
640
+ *
641
+ * const f = field.for<ContactForm>()
642
+ *
643
+ * const schema = defineSchema<ContactForm>({
644
+ * sections: [{
645
+ * fields: [
646
+ * f.text("firstName", "First Name"), // ✓
647
+ * f.email("email", "Email"), // ✓
648
+ * f.text("typo", "Label"), // ✗ TypeScript error
649
+ * ],
650
+ * }],
651
+ * })
652
+ * ```
653
+ */
654
+ for: () => ({
655
+ text: (name, label, props) => field.text(name, label, props),
656
+ email: (name, label, props) => field.email(name, label, props),
657
+ url: (name, label, props) => field.url(name, label, props),
658
+ tel: (name, label, props) => field.tel(name, label, props),
659
+ password: (name, label, props) => field.password(name, label, props),
660
+ number: (name, label, props) => field.number(name, label, props),
661
+ textarea: (name, label, props) => field.textarea(name, label, props),
662
+ select: (name, label, options, props) => field.select(name, label, options, props),
663
+ combobox: (name, label, options, props) => field.combobox(name, label, options, props),
664
+ multiselect: (name, label, options, props) => field.multiselect(name, label, options, props),
665
+ dependentSelect: (name, label, props) => field.dependentSelect(name, label, props),
666
+ switch: (name, label, props) => field.switch(name, label, props),
667
+ boolean: (name, label, props) => field.boolean(name, label, props),
668
+ checkbox: (name, label, props) => field.checkbox(name, label, props),
669
+ radio: (name, label, options, props) => field.radio(name, label, options, props),
670
+ date: (name, label, props) => field.date(name, label, props),
671
+ tags: (name, label, props) => field.tags(name, label, props),
672
+ slug: (name, label, props) => field.slug(name, label, props),
673
+ file: (name, label, props) => field.file(name, label, props),
674
+ hidden: (name, props) => field.hidden(name, props),
675
+ group: (name, label, itemFields, props) => field.group(name, label, itemFields, props),
676
+ array: (name, label, itemFields, props) => field.array(name, label, itemFields, props),
677
+ custom: (name, label, render, builderProps) => field.custom(name, label, render, builderProps)
405
678
  })
406
679
  };
407
680
  /**
408
681
  * Create a section definition with sensible defaults.
409
682
  *
410
- * @param id - Unique section identifier
411
- * @param title - Section title
412
- * @param fields - Array of field definitions
413
- * @param props - Additional section configuration
414
- *
415
683
  * @example
416
684
  * ```ts
417
685
  * section("personal", "Personal Info", [
418
686
  * field.text("name", "Name", { required: true }),
419
687
  * field.email("email", "Email"),
420
- * ], { cols: 2, variant: "card" })
688
+ * ], { cols: 2 })
421
689
  * ```
422
690
  */
423
691
  function section(id, title, fields, props = {}) {
@@ -433,6 +701,9 @@ function section(id, title, fields, props = {}) {
433
701
  /**
434
702
  * Create a section without a title (transparent section).
435
703
  * Useful for grouping fields without visual separation.
704
+ *
705
+ * Accepts `BaseField[]` (no generic) so mixed-type field arrays don't trigger
706
+ * conflicting type inference across different field name generics.
436
707
  */
437
708
  function sectionUntitled(fields, props = {}) {
438
709
  const { cols = 1, ...rest } = props;
@@ -445,4 +716,4 @@ function sectionUntitled(fields, props = {}) {
445
716
  }
446
717
 
447
718
  //#endregion
448
- export { buildValidationRules, cn, defineField, defineSchema, defineSection, evaluateCondition, extractDefaultValues, extractWatchNames, field, section, sectionUntitled };
719
+ export { buildValidationRules, cn, defineField, defineSchema, defineSection, evaluateCondition, extendSection, extractDefaultValues, extractWatchNames, field, flattenSchema, isArrayField, isChoiceField, isConditionalField, isContainerField, isDateField, isDynamicField, isNumericField, isTextField, mergeSchemas, omitFields, pickFields, section, sectionUntitled };