@classytic/formkit 1.4.0 → 1.5.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/CHANGELOG.md +61 -0
- package/README.md +136 -9
- package/dist/index.d.mts +132 -5
- package/dist/index.mjs +513 -162
- package/dist/server.d.mts +81 -3
- package/dist/server.mjs +214 -12
- package/package.json +2 -1
package/dist/server.d.mts
CHANGED
|
@@ -161,6 +161,13 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
|
|
|
161
161
|
variant?: Variant;
|
|
162
162
|
/** Whether field should span full width in grid */
|
|
163
163
|
fullWidth?: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Span N columns of the section grid (e.g. `2` of a 3-col section). `1` (or
|
|
166
|
+
* unset) is the default single cell; `fullWidth` still means "span the whole
|
|
167
|
+
* row" and takes precedence. Applied as an inline `grid-column: span N` so it
|
|
168
|
+
* works regardless of the host's Tailwind content scan.
|
|
169
|
+
*/
|
|
170
|
+
colSpan?: number;
|
|
164
171
|
/** Custom CSS class name */
|
|
165
172
|
className?: string;
|
|
166
173
|
/**
|
|
@@ -222,13 +229,33 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
|
|
|
222
229
|
/**
|
|
223
230
|
* Dynamic options loaded based on current form values.
|
|
224
231
|
* Useful for dependent selects (e.g., state depends on country).
|
|
232
|
+
*
|
|
233
|
+
* Receives an `AbortSignal` in the second arg — forward it to `fetch` (or
|
|
234
|
+
* abort your own request on it) so a superseded / unmounted load is cancelled
|
|
235
|
+
* instead of racing to completion:
|
|
236
|
+
*
|
|
237
|
+
* ```ts
|
|
238
|
+
* loadOptions: (values, { signal }) =>
|
|
239
|
+
* fetch(`/api/cities?country=${values.country}`, { signal }).then(r => r.json())
|
|
240
|
+
* ```
|
|
225
241
|
*/
|
|
226
|
-
loadOptions?: (formValues: Partial<TFieldValues
|
|
242
|
+
loadOptions?: (formValues: Partial<TFieldValues>, options?: {
|
|
243
|
+
signal: AbortSignal;
|
|
244
|
+
}) => Promise<(FieldOption | FieldOptionGroup)[]> | (FieldOption | FieldOptionGroup)[];
|
|
227
245
|
/**
|
|
228
246
|
* Error callback for loadOptions failures.
|
|
229
247
|
* Called when loadOptions rejects. Defaults to console.error.
|
|
230
248
|
*/
|
|
231
249
|
onLoadError?: (error: unknown) => void;
|
|
250
|
+
/**
|
|
251
|
+
* Memoize async `loadOptions` results per set of watched values, so toggling
|
|
252
|
+
* a dependency back and forth (e.g. re-selecting a country) reuses the last
|
|
253
|
+
* fetch instead of hitting the API again. Off by default — enable only when
|
|
254
|
+
* the options are stable for a given input, since the cache lives for the
|
|
255
|
+
* field's lifetime and won't see server-side changes. Bounded (LRU-ish) so it
|
|
256
|
+
* can't grow without limit.
|
|
257
|
+
*/
|
|
258
|
+
cacheOptions?: boolean;
|
|
232
259
|
/**
|
|
233
260
|
* Sub-fields for `group` and `array` field types.
|
|
234
261
|
*
|
|
@@ -591,6 +618,54 @@ declare function defineSection<TFieldValues extends FieldValues = FieldValues>(s
|
|
|
591
618
|
* ```
|
|
592
619
|
*/
|
|
593
620
|
declare function extractDefaultValues<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>): Partial<TFieldValues>;
|
|
621
|
+
interface SchemaIssue {
|
|
622
|
+
/** Where the issue is, e.g. `sections[0].fields[2]` or `sections[1]`. */
|
|
623
|
+
path: string;
|
|
624
|
+
code: "missing-name" | "missing-type" | "duplicate-name" | "itemfields-on-noncontainer" | "empty-container" | "unknown-operator";
|
|
625
|
+
/** `error` = will misbehave at runtime; `warning` = suspicious but tolerated. */
|
|
626
|
+
severity: "error" | "warning";
|
|
627
|
+
message: string;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Structurally validate a form schema and return a list of issues (empty ⇒ OK).
|
|
631
|
+
* Server-safe (no hooks/DOM), so you can run it when a schema is loaded from a
|
|
632
|
+
* DB, in a test, or in a dev boot check. It validates SHAPE, not your component
|
|
633
|
+
* registry — an unknown field `type` is a registry concern, not a schema error.
|
|
634
|
+
*
|
|
635
|
+
* Checks: missing `name`/`type`, duplicate names (namespace-aware), `itemFields`
|
|
636
|
+
* on a non-container type, containers with no `itemFields`, and unknown DSL
|
|
637
|
+
* condition operators.
|
|
638
|
+
*/
|
|
639
|
+
declare function validateSchema(schema: FormSchema): SchemaIssue[];
|
|
640
|
+
/**
|
|
641
|
+
* Build a default-value object for a flat list of fields — used to seed a new
|
|
642
|
+
* array item (or a group) from its `itemFields`.
|
|
643
|
+
*
|
|
644
|
+
* Recurses into nested `group` children (→ nested object) and seeds nested
|
|
645
|
+
* `array` children as `[]`, so appending an item never leaves a deep sub-field
|
|
646
|
+
* `undefined`. That matters because a missing deep field can trip a resolver
|
|
647
|
+
* (zod et al.) into a spurious "required" error the moment the row is added.
|
|
648
|
+
* Leaf fields without an explicit `defaultValue` seed to `""` (a controlled
|
|
649
|
+
* empty value RHF is happy with).
|
|
650
|
+
*/
|
|
651
|
+
declare function buildFieldDefaults(fields: BaseField[] | undefined): Record<string, unknown>;
|
|
652
|
+
/**
|
|
653
|
+
* Deep-merge `override` onto `base` with default-values semantics: nested
|
|
654
|
+
* plain objects merge recursively; arrays, primitives, and class instances
|
|
655
|
+
* (Date, File, …) from `override` replace wholesale.
|
|
656
|
+
*
|
|
657
|
+
* This is the merge `useFormKit` applies between schema-extracted defaults and
|
|
658
|
+
* caller-provided `defaultValues`. Exported so wrappers that re-seed a form at
|
|
659
|
+
* runtime (e.g. an edit sheet swapping entities) can reproduce the exact same
|
|
660
|
+
* merge for `form.reset(...)`:
|
|
661
|
+
*
|
|
662
|
+
* @example
|
|
663
|
+
* ```ts
|
|
664
|
+
* const merged = mergeDefaultValues(extractDefaultValues(schema), entity);
|
|
665
|
+
* form.reset(merged);
|
|
666
|
+
* ```
|
|
667
|
+
*/
|
|
668
|
+
declare function mergeDefaultValues(base: Record<string, unknown>, override: Record<string, unknown>): Record<string, unknown>;
|
|
594
669
|
/**
|
|
595
670
|
* Generates react-hook-form `RegisterOptions`-compatible validation rules
|
|
596
671
|
* from a field's schema props. Maps `required`, `min`, `max`, `minLength`,
|
|
@@ -719,7 +794,10 @@ declare const field: {
|
|
|
719
794
|
email: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** URL input field with default placeholder. */
|
|
720
795
|
url: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Phone/tel input field with default placeholder. */
|
|
721
796
|
tel: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Password input field. */
|
|
722
|
-
password: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
797
|
+
password: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
798
|
+
/** Number input field. No implicit `min` — pass `{ min }` to add one, so the
|
|
799
|
+
* builder never injects validation the author didn't write (signed
|
|
800
|
+
* quantities like deltas / temperatures stay valid). */
|
|
723
801
|
number: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Textarea field with default 3 rows. */
|
|
724
802
|
textarea: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Select dropdown field. */
|
|
725
803
|
select: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Searchable combobox field. */
|
|
@@ -864,4 +942,4 @@ declare function section<TFieldValues extends FieldValues = FieldValues>(id: str
|
|
|
864
942
|
*/
|
|
865
943
|
declare function sectionUntitled(fields: BaseField[], props?: Omit<SectionProps, "variant">): Section;
|
|
866
944
|
//#endregion
|
|
867
|
-
export { type BaseField, type ClassValue, type Condition, type ConditionConfig, type ConditionRule, type DefaultLayoutProps, type DefineField, type FieldMeta, type FieldOption, type FieldOptionGroup, type FieldType, type FormSchema, type GridLayoutProps, type InferSchemaValues, type LayoutComponentProps, type LayoutType, type PatternRuleObject, type SchemaFieldNames, type Section, type SectionLayoutProps, type SectionRenderProps, type ValidationRuleObject, type Variant, buildValidationRules, cn, defineField, defineSchema, defineSection, evaluateCondition, extendSection, extractDefaultValues, extractWatchNames, field, flattenSchema, isArrayField, isChoiceField, isConditionalField, isContainerField, isDateField, isDynamicField, isNumericField, isTextField, mergeSchemas, omitFields, pickFields, section, sectionUntitled };
|
|
945
|
+
export { type BaseField, type ClassValue, type Condition, type ConditionConfig, type ConditionRule, type DefaultLayoutProps, type DefineField, type FieldMeta, type FieldOption, type FieldOptionGroup, type FieldType, type FormSchema, type GridLayoutProps, type InferSchemaValues, type LayoutComponentProps, type LayoutType, type PatternRuleObject, type SchemaFieldNames, type SchemaIssue, type Section, type SectionLayoutProps, type SectionRenderProps, type ValidationRuleObject, type Variant, buildFieldDefaults, buildValidationRules, cn, defineField, defineSchema, defineSection, evaluateCondition, extendSection, extractDefaultValues, extractWatchNames, field, flattenSchema, isArrayField, isChoiceField, isConditionalField, isContainerField, isDateField, isDynamicField, isNumericField, isTextField, mergeDefaultValues, mergeSchemas, omitFields, pickFields, section, sectionUntitled, validateSchema };
|
package/dist/server.mjs
CHANGED
|
@@ -154,20 +154,221 @@ function defineSection(section) {
|
|
|
154
154
|
function extractDefaultValues(schema) {
|
|
155
155
|
const defaults = {};
|
|
156
156
|
for (const section of schema.sections) {
|
|
157
|
-
const prefix = section.nameSpace ? `${section.nameSpace}.` : "";
|
|
158
157
|
if (!section.fields) continue;
|
|
159
|
-
|
|
160
|
-
const key = `${prefix}${field.name}`;
|
|
161
|
-
if (field.defaultValue !== void 0) defaults[key] = field.defaultValue;
|
|
162
|
-
else if (field.type === "array") defaults[key] = [];
|
|
163
|
-
if (field.itemFields && field.type !== "array") {
|
|
164
|
-
for (const sub of field.itemFields) if (sub.defaultValue !== void 0) defaults[`${key}.${sub.name}`] = sub.defaultValue;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
158
|
+
collectExplicitDefaults(section.fields, section.nameSpace ?? "", defaults);
|
|
167
159
|
}
|
|
168
160
|
return defaults;
|
|
169
161
|
}
|
|
170
162
|
/**
|
|
163
|
+
* Recursively assign a field list's EXPLICIT defaults (sparse — only fields
|
|
164
|
+
* that declare a `defaultValue`, plus `array` fields seeded to `[]`) into
|
|
165
|
+
* `target` at the given dot-path prefix. Recurses into nested `group` children
|
|
166
|
+
* so deeply-nested defaults aren't dropped; `array` children are left dynamic.
|
|
167
|
+
*/
|
|
168
|
+
function collectExplicitDefaults(fields, prefix, target) {
|
|
169
|
+
for (const field of fields) {
|
|
170
|
+
const name = field.name;
|
|
171
|
+
const key = prefix ? `${prefix}.${name}` : name;
|
|
172
|
+
if (field.defaultValue !== void 0) assignPath(target, key, field.defaultValue);
|
|
173
|
+
else if (field.type === "array") assignPath(target, key, []);
|
|
174
|
+
else if (field.itemFields && field.itemFields.length > 0) collectExplicitDefaults(field.itemFields, key, target);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const VALID_OPERATORS = new Set([
|
|
178
|
+
"===",
|
|
179
|
+
"!==",
|
|
180
|
+
"in",
|
|
181
|
+
"not-in",
|
|
182
|
+
"truthy",
|
|
183
|
+
"falsy"
|
|
184
|
+
]);
|
|
185
|
+
const CONTAINER_TYPES = new Set(["group", "array"]);
|
|
186
|
+
/**
|
|
187
|
+
* Structurally validate a form schema and return a list of issues (empty ⇒ OK).
|
|
188
|
+
* Server-safe (no hooks/DOM), so you can run it when a schema is loaded from a
|
|
189
|
+
* DB, in a test, or in a dev boot check. It validates SHAPE, not your component
|
|
190
|
+
* registry — an unknown field `type` is a registry concern, not a schema error.
|
|
191
|
+
*
|
|
192
|
+
* Checks: missing `name`/`type`, duplicate names (namespace-aware), `itemFields`
|
|
193
|
+
* on a non-container type, containers with no `itemFields`, and unknown DSL
|
|
194
|
+
* condition operators.
|
|
195
|
+
*/
|
|
196
|
+
function validateSchema(schema) {
|
|
197
|
+
const issues = [];
|
|
198
|
+
const seen = /* @__PURE__ */ new Map();
|
|
199
|
+
const checkCondition = (condition, path) => {
|
|
200
|
+
if (!condition || typeof condition === "function") return;
|
|
201
|
+
const asObj = condition;
|
|
202
|
+
const rules = Array.isArray(condition) ? condition : Array.isArray(asObj.rules) ? asObj.rules : [condition];
|
|
203
|
+
for (const rule of rules) {
|
|
204
|
+
const op = rule?.operator;
|
|
205
|
+
if (op && !VALID_OPERATORS.has(op)) issues.push({
|
|
206
|
+
path,
|
|
207
|
+
code: "unknown-operator",
|
|
208
|
+
severity: "error",
|
|
209
|
+
message: `Unknown condition operator "${op}". Valid: ${[...VALID_OPERATORS].join(", ")}.`
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const walkFields = (fields, prefix, locPath, dedup) => {
|
|
214
|
+
fields.forEach((field, i) => {
|
|
215
|
+
const loc = `${locPath}.fields[${i}]`;
|
|
216
|
+
const name = field.name;
|
|
217
|
+
if (!name) issues.push({
|
|
218
|
+
path: loc,
|
|
219
|
+
code: "missing-name",
|
|
220
|
+
severity: "error",
|
|
221
|
+
message: "Field is missing a `name`."
|
|
222
|
+
});
|
|
223
|
+
if (!field.type) issues.push({
|
|
224
|
+
path: loc,
|
|
225
|
+
code: "missing-type",
|
|
226
|
+
severity: "error",
|
|
227
|
+
message: `Field "${name ?? "?"}" is missing a \`type\`.`
|
|
228
|
+
});
|
|
229
|
+
if (name && dedup) {
|
|
230
|
+
const full = prefix ? `${prefix}.${name}` : name;
|
|
231
|
+
const prior = seen.get(full);
|
|
232
|
+
if (prior) issues.push({
|
|
233
|
+
path: loc,
|
|
234
|
+
code: "duplicate-name",
|
|
235
|
+
severity: "error",
|
|
236
|
+
message: `Duplicate field name "${full}" (also at ${prior}).`
|
|
237
|
+
});
|
|
238
|
+
else seen.set(full, loc);
|
|
239
|
+
}
|
|
240
|
+
const hasItems = !!field.itemFields && field.itemFields.length > 0;
|
|
241
|
+
const isContainer = CONTAINER_TYPES.has(field.type);
|
|
242
|
+
if (hasItems && !isContainer) issues.push({
|
|
243
|
+
path: loc,
|
|
244
|
+
code: "itemfields-on-noncontainer",
|
|
245
|
+
severity: "error",
|
|
246
|
+
message: `Field "${name}" has \`itemFields\` but type "${field.type}" is not a container (group/array).`
|
|
247
|
+
});
|
|
248
|
+
if (isContainer && !hasItems) issues.push({
|
|
249
|
+
path: loc,
|
|
250
|
+
code: "empty-container",
|
|
251
|
+
severity: "warning",
|
|
252
|
+
message: `Container field "${name}" (${field.type}) has no \`itemFields\`.`
|
|
253
|
+
});
|
|
254
|
+
checkCondition(field.condition, loc);
|
|
255
|
+
if (hasItems) {
|
|
256
|
+
const childDedup = field.type !== "array" && dedup;
|
|
257
|
+
const childPrefix = field.type === "array" ? "" : name ? prefix ? `${prefix}.${name}` : name : prefix;
|
|
258
|
+
walkFields(field.itemFields, childPrefix, loc, childDedup);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
schema.sections.forEach((section, i) => {
|
|
263
|
+
const loc = `sections[${i}]`;
|
|
264
|
+
checkCondition(section.condition, loc);
|
|
265
|
+
if (section.fields) walkFields(section.fields, section.nameSpace ?? "", loc, true);
|
|
266
|
+
});
|
|
267
|
+
return issues;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Build a default-value object for a flat list of fields — used to seed a new
|
|
271
|
+
* array item (or a group) from its `itemFields`.
|
|
272
|
+
*
|
|
273
|
+
* Recurses into nested `group` children (→ nested object) and seeds nested
|
|
274
|
+
* `array` children as `[]`, so appending an item never leaves a deep sub-field
|
|
275
|
+
* `undefined`. That matters because a missing deep field can trip a resolver
|
|
276
|
+
* (zod et al.) into a spurious "required" error the moment the row is added.
|
|
277
|
+
* Leaf fields without an explicit `defaultValue` seed to `""` (a controlled
|
|
278
|
+
* empty value RHF is happy with).
|
|
279
|
+
*/
|
|
280
|
+
function buildFieldDefaults(fields) {
|
|
281
|
+
const item = {};
|
|
282
|
+
if (!fields) return item;
|
|
283
|
+
for (const f of fields) {
|
|
284
|
+
const name = f.name;
|
|
285
|
+
if (f.defaultValue !== void 0) item[name] = f.defaultValue;
|
|
286
|
+
else if (f.type === "array") item[name] = [];
|
|
287
|
+
else if (f.itemFields && f.itemFields.length > 0) item[name] = buildFieldDefaults(f.itemFields);
|
|
288
|
+
else item[name] = emptyForType(f.type);
|
|
289
|
+
}
|
|
290
|
+
return item;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Type-appropriate "empty" seed for a leaf field that declares no
|
|
294
|
+
* `defaultValue`. A blanket `""` is wrong for non-text fields — a checkbox
|
|
295
|
+
* seeded to `""` is neither on nor off, and a numeric field coerces `""` to
|
|
296
|
+
* `NaN`. Text-like fields fall through to `""`.
|
|
297
|
+
*/
|
|
298
|
+
function emptyForType(type) {
|
|
299
|
+
switch (type) {
|
|
300
|
+
case "checkbox":
|
|
301
|
+
case "switch":
|
|
302
|
+
case "boolean": return false;
|
|
303
|
+
case "number":
|
|
304
|
+
case "money": return;
|
|
305
|
+
case "multiselect":
|
|
306
|
+
case "tags": return [];
|
|
307
|
+
default: return "";
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Assign a possibly dot-notated key into a NESTED object shape:
|
|
312
|
+
* `assignPath(obj, "address.city", "NYC")` → `obj.address.city = "NYC"`.
|
|
313
|
+
*
|
|
314
|
+
* Nested (not flat `{"address.city": ...}`) is required for react-hook-form:
|
|
315
|
+
* its `get(defaultValues, "address.city")` resolves through the object graph,
|
|
316
|
+
* so a flat dotted key would never be found and the field's default would
|
|
317
|
+
* silently not apply. Top-level keys (no dot) are assigned directly.
|
|
318
|
+
*/
|
|
319
|
+
function assignPath(target, path, value) {
|
|
320
|
+
if (!path.includes(".")) {
|
|
321
|
+
target[path] = value;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const parts = path.split(".");
|
|
325
|
+
const last = parts.length - 1;
|
|
326
|
+
let node = target;
|
|
327
|
+
for (let i = 0; i < last; i++) {
|
|
328
|
+
const key = parts[i];
|
|
329
|
+
const next = node[key];
|
|
330
|
+
if (typeof next !== "object" || next === null || Array.isArray(next)) node[key] = {};
|
|
331
|
+
node = node[key];
|
|
332
|
+
}
|
|
333
|
+
node[parts[last]] = value;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* True for objects that are safe to spread-merge: plain `{}` records (or
|
|
337
|
+
* `Object.create(null)`). A Date / File / class instance is `typeof "object"`
|
|
338
|
+
* but must be REPLACED wholesale, never spread-merged — `{ ...new Date() }` is
|
|
339
|
+
* `{}`, which would silently destroy the value.
|
|
340
|
+
*/
|
|
341
|
+
function isPlainObject(v) {
|
|
342
|
+
if (typeof v !== "object" || v === null) return false;
|
|
343
|
+
const proto = Object.getPrototypeOf(v);
|
|
344
|
+
return proto === Object.prototype || proto === null;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Deep-merge `override` onto `base` with default-values semantics: nested
|
|
348
|
+
* plain objects merge recursively; arrays, primitives, and class instances
|
|
349
|
+
* (Date, File, …) from `override` replace wholesale.
|
|
350
|
+
*
|
|
351
|
+
* This is the merge `useFormKit` applies between schema-extracted defaults and
|
|
352
|
+
* caller-provided `defaultValues`. Exported so wrappers that re-seed a form at
|
|
353
|
+
* runtime (e.g. an edit sheet swapping entities) can reproduce the exact same
|
|
354
|
+
* merge for `form.reset(...)`:
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* ```ts
|
|
358
|
+
* const merged = mergeDefaultValues(extractDefaultValues(schema), entity);
|
|
359
|
+
* form.reset(merged);
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
function mergeDefaultValues(base, override) {
|
|
363
|
+
const out = { ...base };
|
|
364
|
+
for (const key of Object.keys(override)) {
|
|
365
|
+
const b = out[key];
|
|
366
|
+
const o = override[key];
|
|
367
|
+
out[key] = isPlainObject(b) && isPlainObject(o) ? mergeDefaultValues(b, o) : o;
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
171
372
|
* Generates react-hook-form `RegisterOptions`-compatible validation rules
|
|
172
373
|
* from a field's schema props. Maps `required`, `min`, `max`, `minLength`,
|
|
173
374
|
* `maxLength`, `pattern`, and `validate` to RHF rules.
|
|
@@ -438,12 +639,13 @@ const field = {
|
|
|
438
639
|
label,
|
|
439
640
|
...props ?? {}
|
|
440
641
|
}),
|
|
441
|
-
/** Number input field
|
|
642
|
+
/** Number input field. No implicit `min` — pass `{ min }` to add one, so the
|
|
643
|
+
* builder never injects validation the author didn't write (signed
|
|
644
|
+
* quantities like deltas / temperatures stay valid). */
|
|
442
645
|
number: (name, label, props) => ({
|
|
443
646
|
type: "number",
|
|
444
647
|
name,
|
|
445
648
|
label,
|
|
446
|
-
min: 0,
|
|
447
649
|
...props ?? {}
|
|
448
650
|
}),
|
|
449
651
|
/** Textarea field with default 3 rows. */
|
|
@@ -716,4 +918,4 @@ function sectionUntitled(fields, props = {}) {
|
|
|
716
918
|
}
|
|
717
919
|
|
|
718
920
|
//#endregion
|
|
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 };
|
|
921
|
+
export { buildFieldDefaults, buildValidationRules, cn, defineField, defineSchema, defineSection, evaluateCondition, extendSection, extractDefaultValues, extractWatchNames, field, flattenSchema, isArrayField, isChoiceField, isConditionalField, isContainerField, isDateField, isDynamicField, isNumericField, isTextField, mergeDefaultValues, mergeSchemas, omitFields, pickFields, section, sectionUntitled, validateSchema };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/formkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Headless, type-safe form generation engine for React 19. Schema-driven with full TypeScript support.",
|
|
5
5
|
"author": "Classytic",
|
|
6
6
|
"license": "MIT",
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
"@vitejs/plugin-react": "^5.2.0",
|
|
97
97
|
"clsx": "^2.1.1",
|
|
98
98
|
"eslint": "^9.39.4",
|
|
99
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
99
100
|
"happy-dom": "^20.9.0",
|
|
100
101
|
"react": "^19.2.6",
|
|
101
102
|
"react-dom": "^19.2.6",
|