@formwright/schema 0.1.0 → 0.2.2

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/README.md CHANGED
@@ -1,20 +1,53 @@
1
1
  # @formwright/schema
2
2
 
3
- Schema types and a dependency-free runtime validator for [Formwright](../../README.md).
3
+ > Schema types and a dependency-free runtime validator for [Formwright](https://github.com/aliarsalan177/formwright).
4
4
 
5
- The schema is Formwright's public contract: plain, serializable data that both the
6
- runtime renderer and the codegen compiler consume. This package gives you the
7
- TypeScript types plus a validator that produces precise, path-addressed issues —
8
- ideal for repairing LLM-emitted schemas before they reach the runtime.
5
+ The schema is Formwright's public contract: plain, serializable data that both the runtime
6
+ renderer and the codegen compiler consume. This package gives you the TypeScript types plus
7
+ a validator that produces precise, path-addressed issues — ideal for repairing LLM-emitted
8
+ schemas before they reach the runtime.
9
+
10
+ ```bash
11
+ npm i @formwright/schema
12
+ ```
9
13
 
10
14
  ```ts
11
15
  import { validateSchema, parseSchema } from "@formwright/schema";
12
16
 
17
+ // Non-throwing: inspect issues
13
18
  const result = validateSchema(unknownInput);
14
19
  if (!result.ok) {
15
20
  for (const issue of result.issues) console.warn(issue.path, issue.message);
16
21
  }
17
22
 
18
- // or throw on failure:
23
+ // Or throw a single aggregated error on failure:
19
24
  const schema = parseSchema(unknownInput);
20
25
  ```
26
+
27
+ ## Types
28
+
29
+ `FormSchema`, `FieldSchema`, `FieldType`, `FieldOption`, `ValidationSchema`, `Condition`,
30
+ `SubmitSchema`, and more — the full, serializable shape of a form, including nested `group`
31
+ and `collection` fields and the JSONLogic-style `Condition` algebra.
32
+
33
+ ```ts
34
+ import type { FormSchema } from "@formwright/schema";
35
+
36
+ const schema: FormSchema = {
37
+ id: "signup",
38
+ version: "1.0",
39
+ fields: [
40
+ { id: "email", type: "email", validation: { kind: "string", format: "email", required: true } },
41
+ ],
42
+ };
43
+ ```
44
+
45
+ ## Why a runtime validator
46
+
47
+ LLM-emitted schemas can be malformed. `validateSchema` checks the structure (ids present and
48
+ unique, container fields declared, etc.) and returns issues addressed by path
49
+ (`fields[2].type`), so you can repair or reject input before constructing a `Form`.
50
+
51
+ ## License
52
+
53
+ MIT
package/dist/index.cjs CHANGED
@@ -11,9 +11,16 @@ var BUILTIN_TYPES = /* @__PURE__ */ new Set([
11
11
  "checkbox",
12
12
  "radio",
13
13
  "group",
14
- "collection"
14
+ "collection",
15
+ "steps",
16
+ "step"
17
+ ]);
18
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set([
19
+ "group",
20
+ "collection",
21
+ "steps",
22
+ "step"
15
23
  ]);
16
- var CONTAINER_TYPES = /* @__PURE__ */ new Set(["group", "collection"]);
17
24
  function isRecord(v) {
18
25
  return typeof v === "object" && v !== null && !Array.isArray(v);
19
26
  }
@@ -49,7 +56,18 @@ function validateField(field, path, seenIds, c) {
49
56
  c.add(`${path}.fields`, `field of type "${type}" must declare a non-empty "fields" array`);
50
57
  } else {
51
58
  const childIds = /* @__PURE__ */ new Set();
52
- nested.forEach((f, i) => validateField(f, `${path}.fields[${i}]`, childIds, c));
59
+ nested.forEach((f, i) => {
60
+ if (type === "steps") {
61
+ const childType = isRecord(f) ? f["type"] : void 0;
62
+ if (childType !== "step") {
63
+ c.add(
64
+ `${path}.fields[${i}].type`,
65
+ `field of type "steps" must contain "step" children (got "${String(childType)}")`
66
+ );
67
+ }
68
+ }
69
+ validateField(f, `${path}.fields[${i}]`, childIds, c);
70
+ });
53
71
  }
54
72
  }
55
73
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/validate.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Runtime validation of an unknown value into a typed {@link FormSchema}.\n *\n * Dependency-free and structural — its job is to give precise, path-addressed\n * errors so an LLM-emitted schema can be repaired (or rejected) before it ever\n * reaches the runtime. This validates the *schema shape*, not user form input\n * (that is the job of the per-field {@link ValidationSchema}).\n */\nimport type { FieldSchema, FieldType, FormSchema } from \"./types.js\";\n\nexport interface ValidationIssue {\n /** JSON-pointer-ish path to the offending node, e.g. `fields[2].type`. */\n readonly path: string;\n readonly message: string;\n}\n\nexport type ValidationResult =\n | { readonly ok: true; readonly value: FormSchema }\n | { readonly ok: false; readonly issues: readonly ValidationIssue[] };\n\nconst BUILTIN_TYPES: ReadonlySet<string> = new Set<FieldType>([\n \"text\",\n \"number\",\n \"email\",\n \"password\",\n \"textarea\",\n \"select\",\n \"checkbox\",\n \"radio\",\n \"group\",\n \"collection\",\n]);\n\nconst CONTAINER_TYPES: ReadonlySet<string> = new Set<FieldType>([\"group\", \"collection\"]);\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nclass Collector {\n readonly issues: ValidationIssue[] = [];\n add(path: string, message: string): void {\n this.issues.push({ path, message });\n }\n}\n\nfunction validateField(field: unknown, path: string, seenIds: Set<string>, c: Collector): void {\n if (!isRecord(field)) {\n c.add(path, \"field must be an object\");\n return;\n }\n\n const id = field[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) {\n c.add(`${path}.id`, \"field.id must be a non-empty string\");\n } else if (seenIds.has(id)) {\n c.add(`${path}.id`, `duplicate field id \"${id}\"`);\n } else {\n seenIds.add(id);\n }\n\n const type = field[\"type\"];\n if (typeof type !== \"string\" || type.length === 0) {\n c.add(`${path}.type`, \"field.type must be a non-empty string\");\n } else if (!BUILTIN_TYPES.has(type)) {\n // Unknown types are allowed (custom widgets) but flagged as a hint, not an error.\n // We intentionally do not push an issue here.\n }\n\n if ((type === \"select\" || type === \"radio\") && field[\"options\"] === undefined) {\n c.add(`${path}.options`, `field of type \"${type}\" should declare options`);\n }\n\n // Containers (group/collection) must declare nested fields; recurse so nested\n // problems surface with a precise path (e.g. `fields[3].fields[0].id`).\n if (typeof type === \"string\" && CONTAINER_TYPES.has(type)) {\n const nested = field[\"fields\"];\n if (!Array.isArray(nested) || nested.length === 0) {\n c.add(`${path}.fields`, `field of type \"${type}\" must declare a non-empty \"fields\" array`);\n } else {\n // A container opens a new naming scope: child ids are unique within it, not globally.\n const childIds = new Set<string>();\n nested.forEach((f, i) => validateField(f, `${path}.fields[${i}]`, childIds, c));\n }\n }\n}\n\n/** Validate an unknown value as a {@link FormSchema}. Never throws. */\nexport function validateSchema(input: unknown): ValidationResult {\n const c = new Collector();\n\n if (!isRecord(input)) {\n c.add(\"$\", \"schema must be an object\");\n return { ok: false, issues: c.issues };\n }\n\n if (typeof input[\"id\"] !== \"string\" || (input[\"id\"] as string).length === 0) {\n c.add(\"id\", \"schema.id must be a non-empty string\");\n }\n if (typeof input[\"version\"] !== \"string\" || (input[\"version\"] as string).length === 0) {\n c.add(\"version\", \"schema.version must be a non-empty string\");\n }\n\n const fields = input[\"fields\"];\n if (!Array.isArray(fields)) {\n c.add(\"fields\", \"schema.fields must be an array\");\n } else if (fields.length === 0) {\n c.add(\"fields\", \"schema.fields must contain at least one field\");\n } else {\n const seenIds = new Set<string>();\n fields.forEach((f, i) => validateField(f, `fields[${i}]`, seenIds, c));\n }\n\n const providers = input[\"providers\"];\n if (providers !== undefined && !isRecord(providers)) {\n c.add(\"providers\", \"schema.providers must be an object when present\");\n }\n\n const submit = input[\"submit\"];\n if (submit !== undefined && !isRecord(submit)) {\n c.add(\"submit\", \"schema.submit must be an object when present\");\n }\n\n if (c.issues.length > 0) {\n return { ok: false, issues: c.issues };\n }\n return { ok: true, value: input as unknown as FormSchema };\n}\n\n/** Validate and throw a single aggregated error on failure. Convenience for trusted input. */\nexport function parseSchema(input: unknown): FormSchema {\n const result = validateSchema(input);\n if (!result.ok) {\n const detail = result.issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\");\n throw new SchemaValidationError(`Invalid Formwright schema:\\n${detail}`, result.issues);\n }\n return result.value;\n}\n\nexport class SchemaValidationError extends Error {\n readonly issues: readonly ValidationIssue[];\n constructor(message: string, issues: readonly ValidationIssue[]) {\n super(message);\n this.name = \"SchemaValidationError\";\n this.issues = issues;\n }\n}\n\n/** Type guard form of {@link validateSchema}. */\nexport function isFormSchema(input: unknown): input is FormSchema {\n return validateSchema(input).ok;\n}\n\n/** Convenience: list the field ids declared by a schema, in order. */\nexport function fieldIds(schema: FormSchema): string[] {\n return schema.fields.map((f: FieldSchema) => f.id);\n}\n"]}
1
+ {"version":3,"sources":["../src/validate.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Runtime validation of an unknown value into a typed {@link FormSchema}.\n *\n * Dependency-free and structural — its job is to give precise, path-addressed\n * errors so an LLM-emitted schema can be repaired (or rejected) before it ever\n * reaches the runtime. This validates the *schema shape*, not user form input\n * (that is the job of the per-field {@link ValidationSchema}).\n */\nimport type { FieldSchema, FieldType, FormSchema } from \"./types.js\";\n\nexport interface ValidationIssue {\n /** JSON-pointer-ish path to the offending node, e.g. `fields[2].type`. */\n readonly path: string;\n readonly message: string;\n}\n\nexport type ValidationResult =\n | { readonly ok: true; readonly value: FormSchema }\n | { readonly ok: false; readonly issues: readonly ValidationIssue[] };\n\nconst BUILTIN_TYPES: ReadonlySet<string> = new Set<FieldType>([\n \"text\",\n \"number\",\n \"email\",\n \"password\",\n \"textarea\",\n \"select\",\n \"checkbox\",\n \"radio\",\n \"group\",\n \"collection\",\n \"steps\",\n \"step\",\n]);\n\nconst CONTAINER_TYPES: ReadonlySet<string> = new Set<FieldType>([\n \"group\",\n \"collection\",\n \"steps\",\n \"step\",\n]);\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nclass Collector {\n readonly issues: ValidationIssue[] = [];\n add(path: string, message: string): void {\n this.issues.push({ path, message });\n }\n}\n\nfunction validateField(field: unknown, path: string, seenIds: Set<string>, c: Collector): void {\n if (!isRecord(field)) {\n c.add(path, \"field must be an object\");\n return;\n }\n\n const id = field[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) {\n c.add(`${path}.id`, \"field.id must be a non-empty string\");\n } else if (seenIds.has(id)) {\n c.add(`${path}.id`, `duplicate field id \"${id}\"`);\n } else {\n seenIds.add(id);\n }\n\n const type = field[\"type\"];\n if (typeof type !== \"string\" || type.length === 0) {\n c.add(`${path}.type`, \"field.type must be a non-empty string\");\n } else if (!BUILTIN_TYPES.has(type)) {\n // Unknown types are allowed (custom widgets) but flagged as a hint, not an error.\n // We intentionally do not push an issue here.\n }\n\n if ((type === \"select\" || type === \"radio\") && field[\"options\"] === undefined) {\n c.add(`${path}.options`, `field of type \"${type}\" should declare options`);\n }\n\n // Containers must declare nested fields; recurse so nested problems surface with a\n // precise path (e.g. `fields[3].fields[0].id`).\n if (typeof type === \"string\" && CONTAINER_TYPES.has(type)) {\n const nested = field[\"fields\"];\n if (!Array.isArray(nested) || nested.length === 0) {\n c.add(`${path}.fields`, `field of type \"${type}\" must declare a non-empty \"fields\" array`);\n } else {\n const childIds = new Set<string>();\n nested.forEach((f, i) => {\n if (type === \"steps\") {\n const childType = isRecord(f) ? f[\"type\"] : undefined;\n if (childType !== \"step\") {\n c.add(\n `${path}.fields[${i}].type`,\n `field of type \"steps\" must contain \"step\" children (got \"${String(childType)}\")`,\n );\n }\n }\n validateField(f, `${path}.fields[${i}]`, childIds, c);\n });\n }\n }\n}\n\n/** Validate an unknown value as a {@link FormSchema}. Never throws. */\nexport function validateSchema(input: unknown): ValidationResult {\n const c = new Collector();\n\n if (!isRecord(input)) {\n c.add(\"$\", \"schema must be an object\");\n return { ok: false, issues: c.issues };\n }\n\n if (typeof input[\"id\"] !== \"string\" || (input[\"id\"] as string).length === 0) {\n c.add(\"id\", \"schema.id must be a non-empty string\");\n }\n if (typeof input[\"version\"] !== \"string\" || (input[\"version\"] as string).length === 0) {\n c.add(\"version\", \"schema.version must be a non-empty string\");\n }\n\n const fields = input[\"fields\"];\n if (!Array.isArray(fields)) {\n c.add(\"fields\", \"schema.fields must be an array\");\n } else if (fields.length === 0) {\n c.add(\"fields\", \"schema.fields must contain at least one field\");\n } else {\n const seenIds = new Set<string>();\n fields.forEach((f, i) => validateField(f, `fields[${i}]`, seenIds, c));\n }\n\n const providers = input[\"providers\"];\n if (providers !== undefined && !isRecord(providers)) {\n c.add(\"providers\", \"schema.providers must be an object when present\");\n }\n\n const submit = input[\"submit\"];\n if (submit !== undefined && !isRecord(submit)) {\n c.add(\"submit\", \"schema.submit must be an object when present\");\n }\n\n if (c.issues.length > 0) {\n return { ok: false, issues: c.issues };\n }\n return { ok: true, value: input as unknown as FormSchema };\n}\n\n/** Validate and throw a single aggregated error on failure. Convenience for trusted input. */\nexport function parseSchema(input: unknown): FormSchema {\n const result = validateSchema(input);\n if (!result.ok) {\n const detail = result.issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\");\n throw new SchemaValidationError(`Invalid Formwright schema:\\n${detail}`, result.issues);\n }\n return result.value;\n}\n\nexport class SchemaValidationError extends Error {\n readonly issues: readonly ValidationIssue[];\n constructor(message: string, issues: readonly ValidationIssue[]) {\n super(message);\n this.name = \"SchemaValidationError\";\n this.issues = issues;\n }\n}\n\n/** Type guard form of {@link validateSchema}. */\nexport function isFormSchema(input: unknown): input is FormSchema {\n return validateSchema(input).ok;\n}\n\n/** Convenience: list the field ids declared by a schema, in order. */\nexport function fieldIds(schema: FormSchema): string[] {\n return schema.fields.map((f: FieldSchema) => f.id);\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -48,7 +48,25 @@ type Condition = boolean | {
48
48
  readonly or: readonly Condition[];
49
49
  } | FieldValue;
50
50
  /** Built-in field widget kinds. Extensible: any string maps to a registered widget. */
51
- type FieldType = "text" | "number" | "email" | "password" | "textarea" | "select" | "checkbox" | "radio" | "group" | "collection" | (string & {});
51
+ type FieldType = "text" | "number" | "email" | "password" | "textarea" | "select" | "checkbox" | "radio" | "date" | "time" | "datetime" | "daterange" | "color" | "range" | "file" | "group" | "collection" | "steps" | "step" | "heading" | "separator" | "paragraph" | (string & {});
52
+ /**
53
+ * Map a field to your own UI — a custom element, native tag, or a widget you
54
+ * registered by name. The serializable bits (tag/component/valueProp/event/attrs)
55
+ * live here; code-level transforms and framework `mount` functions are attached
56
+ * via `registerWidget` in the renderer.
57
+ */
58
+ type WidgetRef = string | {
59
+ /** A registered widget name (takes precedence over `tag`). */
60
+ readonly component?: string;
61
+ /** A custom element / native tag to render, e.g. "s-select". */
62
+ readonly tag?: string;
63
+ /** Property the value is written to / read from (default "value"). */
64
+ readonly valueProp?: string;
65
+ /** DOM event that signals a change, e.g. "value-change" (default "input"). */
66
+ readonly event?: string;
67
+ /** Static attributes to set on the element. */
68
+ readonly attrs?: Record<string, string>;
69
+ };
52
70
  /** Validation descriptor — declarative, mapped to a Standard Schema validator at runtime. */
53
71
  interface ValidationSchema {
54
72
  readonly kind: "string" | "number" | "boolean" | "array";
@@ -59,20 +77,85 @@ interface ValidationSchema {
59
77
  readonly maxLength?: number;
60
78
  readonly pattern?: string;
61
79
  readonly format?: "email" | "url" | "uuid";
80
+ /** Catch-all override for every rule's message. */
62
81
  readonly message?: Resolvable<string>;
82
+ /** Per-rule message overrides (take precedence over `message` and the defaults). */
83
+ readonly messages?: {
84
+ readonly required?: string;
85
+ readonly min?: string;
86
+ readonly max?: string;
87
+ readonly minLength?: string;
88
+ readonly maxLength?: string;
89
+ readonly pattern?: string;
90
+ readonly format?: string;
91
+ readonly type?: string;
92
+ };
63
93
  }
64
94
  /** A selectable option for `select` / `radio` fields. */
65
95
  interface FieldOption {
66
96
  readonly label: Resolvable<string>;
67
97
  readonly value: FieldValue;
68
98
  }
99
+ /**
100
+ * Per-part class overrides — drop in your own CSS classes or Tailwind utilities
101
+ * to restyle any part of a field without touching the renderer.
102
+ */
103
+ interface FieldClasses {
104
+ readonly field?: string;
105
+ readonly label?: string;
106
+ readonly control?: string;
107
+ readonly help?: string;
108
+ readonly description?: string;
109
+ readonly error?: string;
110
+ }
111
+ /**
112
+ * A slot rendered at the start/end of an input — either decorative text/icon
113
+ * (a string like "$" or "🔍") or a nested field (e.g. a currency `select`) whose
114
+ * value is added to the payload as a sibling key.
115
+ */
116
+ type FieldSlot = string | FieldSchema;
69
117
  /** A single field in the form. Resolved to a widget by `type`, keyed by `id`. */
70
118
  interface FieldSchema {
71
119
  readonly id: string;
72
120
  readonly type: FieldType;
73
121
  readonly label?: Resolvable<string>;
74
122
  readonly placeholder?: Resolvable<string>;
123
+ /** Native `autocomplete` token for the input (e.g. "email", "name", "off"). */
124
+ readonly autocomplete?: string;
75
125
  readonly help?: Resolvable<string>;
126
+ /** Static body text — for `paragraph`/`heading` presentational fields. */
127
+ readonly content?: Resolvable<string>;
128
+ /** An info tooltip shown next to the field's label. */
129
+ readonly tooltip?: Resolvable<string>;
130
+ /**
131
+ * Capture a value per locale → payload `{ en: "...", ar: "..." }`. Requires
132
+ * the form's `locales`. Renders as one input with a language switcher; reshape to
133
+ * `translations: { en: {...} }` in submit if needed.
134
+ */
135
+ readonly localized?: boolean;
136
+ /** The locale shown first for a `localized` field (default: the form's first locale). */
137
+ readonly defaultLocale?: string;
138
+ /** Longer descriptive text, positioned by {@link descriptionPosition}. */
139
+ readonly description?: Resolvable<string>;
140
+ /** Where to render `description` (default `"below-label"`). */
141
+ readonly descriptionPosition?: "below-label" | "below-field";
142
+ /** For check-like fields (checkbox/toggle): label `"start"` (with control at the end) or `"end"` (default). */
143
+ readonly labelPosition?: "start" | "end";
144
+ /** Render content (icon/text or a value-bearing field) before/after the input. */
145
+ readonly slots?: {
146
+ readonly start?: FieldSlot;
147
+ readonly end?: FieldSlot;
148
+ };
149
+ /** Exclude this field's value from the submitted payload (UI-only control). */
150
+ readonly omit?: boolean;
151
+ /** Render this field with your own component/element instead of the built-in for `type`. */
152
+ readonly widget?: WidgetRef;
153
+ /** Columns the field spans in the form's 12-column grid (e.g. 6 = half width, two side by side). */
154
+ readonly colSpan?: number;
155
+ /** Extra class(es) on the field wrapper (e.g. Tailwind utilities). */
156
+ readonly class?: string;
157
+ /** Per-part class overrides (wrapper, label, control, help, description, error). */
158
+ readonly classes?: FieldClasses;
76
159
  readonly defaultValue?: FieldValue;
77
160
  readonly options?: Resolvable<readonly FieldOption[]>;
78
161
  readonly validation?: ValidationSchema;
@@ -93,8 +176,19 @@ interface FieldSchema {
93
176
  * Container layout:
94
177
  * - `group`: `"fieldset"` (default) or `"accordion"` (collapsible section).
95
178
  * - `collection`: `"list"` (default), `"cards"`, or `"accordion"` (each row collapsible).
179
+ * - `steps`: `"bar"` (default), `"tabs"`, or `"numbers"` progress indicator.
96
180
  */
97
- readonly layout?: "fieldset" | "accordion" | "list" | "cards";
181
+ readonly layout?: "fieldset" | "accordion" | "list" | "cards" | "bar" | "tabs" | "numbers";
182
+ /** `steps` only: show a progress indicator (default `true`). */
183
+ readonly showProgress?: boolean;
184
+ /** `steps` only: validate the current step before advancing (default `true`). */
185
+ readonly validateOnNext?: boolean;
186
+ /** `steps` only: label for the Next button (default `"Next"`). */
187
+ readonly nextLabel?: Resolvable<string>;
188
+ /** `steps` only: label for the Back button (default `"Back"`). */
189
+ readonly prevLabel?: Resolvable<string>;
190
+ /** `steps` only: label for Submit on the last step (default `"Submit"`). */
191
+ readonly submitLabel?: Resolvable<string>;
98
192
  /** `collection` only: label for each row, e.g. "Contact". */
99
193
  readonly itemLabel?: Resolvable<string>;
100
194
  /** `collection` only: text for the add-row button (default: "Add"). */
@@ -110,6 +204,18 @@ interface ProviderDecl {
110
204
  readonly type: "i18n" | "tanstack-query" | "theme" | (string & {});
111
205
  readonly [key: string]: unknown;
112
206
  }
207
+ /** A form-level action button (submit, reset, or a named custom action like "delete"). */
208
+ interface FormAction {
209
+ readonly name: string;
210
+ readonly label?: Resolvable<string>;
211
+ /** `"submit"` triggers submission, `"reset"` resets, `"button"` (default) emits an action event. */
212
+ readonly role?: "submit" | "reset" | "button";
213
+ readonly variant?: "primary" | "secondary" | "danger";
214
+ /** Render this button full-width (stretches to fill the action bar). */
215
+ readonly fullWidth?: boolean;
216
+ /** Name of a handler in `options.handlers`, called with the form on click. */
217
+ readonly handler?: string;
218
+ }
113
219
  /** How the form submits: transform the payload, send it, handle success/error. */
114
220
  interface SubmitSchema {
115
221
  /** Name of a registered transform applied to values before sending. */
@@ -131,6 +237,14 @@ interface FormSchema {
131
237
  readonly providers?: Record<string, ProviderDecl>;
132
238
  readonly fields: readonly FieldSchema[];
133
239
  readonly submit?: SubmitSchema;
240
+ /** Locales for `localized` fields — each captures a value per locale. */
241
+ readonly locales?: readonly string[];
242
+ /** Locales rendered right-to-left (defaults to the common RTL set: ar, he, fa, ur, …). */
243
+ readonly rtlLocales?: readonly string[];
244
+ /** Action buttons rendered at the bottom of the form (defaults to a single Submit). */
245
+ readonly actions?: readonly FormAction[];
246
+ /** How action buttons are aligned: `"start"` (default), `"end"`, or `"between"`. */
247
+ readonly actionsAlign?: "start" | "end" | "between";
134
248
  }
135
249
 
136
250
  /**
@@ -167,4 +281,4 @@ declare function isFormSchema(input: unknown): input is FormSchema;
167
281
  /** Convenience: list the field ids declared by a schema, in order. */
168
282
  declare function fieldIds(schema: FormSchema): string[];
169
283
 
170
- export { type Condition, type FieldOption, type FieldSchema, type FieldType, type FieldValue, type FormSchema, type ProviderDecl, type ProviderRef, type Resolvable, SchemaValidationError, type SubmitSchema, type ValidationIssue, type ValidationResult, type ValidationSchema, fieldIds, isFormSchema, parseSchema, validateSchema };
284
+ export { type Condition, type FieldClasses, type FieldOption, type FieldSchema, type FieldSlot, type FieldType, type FieldValue, type FormAction, type FormSchema, type ProviderDecl, type ProviderRef, type Resolvable, SchemaValidationError, type SubmitSchema, type ValidationIssue, type ValidationResult, type ValidationSchema, type WidgetRef, fieldIds, isFormSchema, parseSchema, validateSchema };
package/dist/index.d.ts CHANGED
@@ -48,7 +48,25 @@ type Condition = boolean | {
48
48
  readonly or: readonly Condition[];
49
49
  } | FieldValue;
50
50
  /** Built-in field widget kinds. Extensible: any string maps to a registered widget. */
51
- type FieldType = "text" | "number" | "email" | "password" | "textarea" | "select" | "checkbox" | "radio" | "group" | "collection" | (string & {});
51
+ type FieldType = "text" | "number" | "email" | "password" | "textarea" | "select" | "checkbox" | "radio" | "date" | "time" | "datetime" | "daterange" | "color" | "range" | "file" | "group" | "collection" | "steps" | "step" | "heading" | "separator" | "paragraph" | (string & {});
52
+ /**
53
+ * Map a field to your own UI — a custom element, native tag, or a widget you
54
+ * registered by name. The serializable bits (tag/component/valueProp/event/attrs)
55
+ * live here; code-level transforms and framework `mount` functions are attached
56
+ * via `registerWidget` in the renderer.
57
+ */
58
+ type WidgetRef = string | {
59
+ /** A registered widget name (takes precedence over `tag`). */
60
+ readonly component?: string;
61
+ /** A custom element / native tag to render, e.g. "s-select". */
62
+ readonly tag?: string;
63
+ /** Property the value is written to / read from (default "value"). */
64
+ readonly valueProp?: string;
65
+ /** DOM event that signals a change, e.g. "value-change" (default "input"). */
66
+ readonly event?: string;
67
+ /** Static attributes to set on the element. */
68
+ readonly attrs?: Record<string, string>;
69
+ };
52
70
  /** Validation descriptor — declarative, mapped to a Standard Schema validator at runtime. */
53
71
  interface ValidationSchema {
54
72
  readonly kind: "string" | "number" | "boolean" | "array";
@@ -59,20 +77,85 @@ interface ValidationSchema {
59
77
  readonly maxLength?: number;
60
78
  readonly pattern?: string;
61
79
  readonly format?: "email" | "url" | "uuid";
80
+ /** Catch-all override for every rule's message. */
62
81
  readonly message?: Resolvable<string>;
82
+ /** Per-rule message overrides (take precedence over `message` and the defaults). */
83
+ readonly messages?: {
84
+ readonly required?: string;
85
+ readonly min?: string;
86
+ readonly max?: string;
87
+ readonly minLength?: string;
88
+ readonly maxLength?: string;
89
+ readonly pattern?: string;
90
+ readonly format?: string;
91
+ readonly type?: string;
92
+ };
63
93
  }
64
94
  /** A selectable option for `select` / `radio` fields. */
65
95
  interface FieldOption {
66
96
  readonly label: Resolvable<string>;
67
97
  readonly value: FieldValue;
68
98
  }
99
+ /**
100
+ * Per-part class overrides — drop in your own CSS classes or Tailwind utilities
101
+ * to restyle any part of a field without touching the renderer.
102
+ */
103
+ interface FieldClasses {
104
+ readonly field?: string;
105
+ readonly label?: string;
106
+ readonly control?: string;
107
+ readonly help?: string;
108
+ readonly description?: string;
109
+ readonly error?: string;
110
+ }
111
+ /**
112
+ * A slot rendered at the start/end of an input — either decorative text/icon
113
+ * (a string like "$" or "🔍") or a nested field (e.g. a currency `select`) whose
114
+ * value is added to the payload as a sibling key.
115
+ */
116
+ type FieldSlot = string | FieldSchema;
69
117
  /** A single field in the form. Resolved to a widget by `type`, keyed by `id`. */
70
118
  interface FieldSchema {
71
119
  readonly id: string;
72
120
  readonly type: FieldType;
73
121
  readonly label?: Resolvable<string>;
74
122
  readonly placeholder?: Resolvable<string>;
123
+ /** Native `autocomplete` token for the input (e.g. "email", "name", "off"). */
124
+ readonly autocomplete?: string;
75
125
  readonly help?: Resolvable<string>;
126
+ /** Static body text — for `paragraph`/`heading` presentational fields. */
127
+ readonly content?: Resolvable<string>;
128
+ /** An info tooltip shown next to the field's label. */
129
+ readonly tooltip?: Resolvable<string>;
130
+ /**
131
+ * Capture a value per locale → payload `{ en: "...", ar: "..." }`. Requires
132
+ * the form's `locales`. Renders as one input with a language switcher; reshape to
133
+ * `translations: { en: {...} }` in submit if needed.
134
+ */
135
+ readonly localized?: boolean;
136
+ /** The locale shown first for a `localized` field (default: the form's first locale). */
137
+ readonly defaultLocale?: string;
138
+ /** Longer descriptive text, positioned by {@link descriptionPosition}. */
139
+ readonly description?: Resolvable<string>;
140
+ /** Where to render `description` (default `"below-label"`). */
141
+ readonly descriptionPosition?: "below-label" | "below-field";
142
+ /** For check-like fields (checkbox/toggle): label `"start"` (with control at the end) or `"end"` (default). */
143
+ readonly labelPosition?: "start" | "end";
144
+ /** Render content (icon/text or a value-bearing field) before/after the input. */
145
+ readonly slots?: {
146
+ readonly start?: FieldSlot;
147
+ readonly end?: FieldSlot;
148
+ };
149
+ /** Exclude this field's value from the submitted payload (UI-only control). */
150
+ readonly omit?: boolean;
151
+ /** Render this field with your own component/element instead of the built-in for `type`. */
152
+ readonly widget?: WidgetRef;
153
+ /** Columns the field spans in the form's 12-column grid (e.g. 6 = half width, two side by side). */
154
+ readonly colSpan?: number;
155
+ /** Extra class(es) on the field wrapper (e.g. Tailwind utilities). */
156
+ readonly class?: string;
157
+ /** Per-part class overrides (wrapper, label, control, help, description, error). */
158
+ readonly classes?: FieldClasses;
76
159
  readonly defaultValue?: FieldValue;
77
160
  readonly options?: Resolvable<readonly FieldOption[]>;
78
161
  readonly validation?: ValidationSchema;
@@ -93,8 +176,19 @@ interface FieldSchema {
93
176
  * Container layout:
94
177
  * - `group`: `"fieldset"` (default) or `"accordion"` (collapsible section).
95
178
  * - `collection`: `"list"` (default), `"cards"`, or `"accordion"` (each row collapsible).
179
+ * - `steps`: `"bar"` (default), `"tabs"`, or `"numbers"` progress indicator.
96
180
  */
97
- readonly layout?: "fieldset" | "accordion" | "list" | "cards";
181
+ readonly layout?: "fieldset" | "accordion" | "list" | "cards" | "bar" | "tabs" | "numbers";
182
+ /** `steps` only: show a progress indicator (default `true`). */
183
+ readonly showProgress?: boolean;
184
+ /** `steps` only: validate the current step before advancing (default `true`). */
185
+ readonly validateOnNext?: boolean;
186
+ /** `steps` only: label for the Next button (default `"Next"`). */
187
+ readonly nextLabel?: Resolvable<string>;
188
+ /** `steps` only: label for the Back button (default `"Back"`). */
189
+ readonly prevLabel?: Resolvable<string>;
190
+ /** `steps` only: label for Submit on the last step (default `"Submit"`). */
191
+ readonly submitLabel?: Resolvable<string>;
98
192
  /** `collection` only: label for each row, e.g. "Contact". */
99
193
  readonly itemLabel?: Resolvable<string>;
100
194
  /** `collection` only: text for the add-row button (default: "Add"). */
@@ -110,6 +204,18 @@ interface ProviderDecl {
110
204
  readonly type: "i18n" | "tanstack-query" | "theme" | (string & {});
111
205
  readonly [key: string]: unknown;
112
206
  }
207
+ /** A form-level action button (submit, reset, or a named custom action like "delete"). */
208
+ interface FormAction {
209
+ readonly name: string;
210
+ readonly label?: Resolvable<string>;
211
+ /** `"submit"` triggers submission, `"reset"` resets, `"button"` (default) emits an action event. */
212
+ readonly role?: "submit" | "reset" | "button";
213
+ readonly variant?: "primary" | "secondary" | "danger";
214
+ /** Render this button full-width (stretches to fill the action bar). */
215
+ readonly fullWidth?: boolean;
216
+ /** Name of a handler in `options.handlers`, called with the form on click. */
217
+ readonly handler?: string;
218
+ }
113
219
  /** How the form submits: transform the payload, send it, handle success/error. */
114
220
  interface SubmitSchema {
115
221
  /** Name of a registered transform applied to values before sending. */
@@ -131,6 +237,14 @@ interface FormSchema {
131
237
  readonly providers?: Record<string, ProviderDecl>;
132
238
  readonly fields: readonly FieldSchema[];
133
239
  readonly submit?: SubmitSchema;
240
+ /** Locales for `localized` fields — each captures a value per locale. */
241
+ readonly locales?: readonly string[];
242
+ /** Locales rendered right-to-left (defaults to the common RTL set: ar, he, fa, ur, …). */
243
+ readonly rtlLocales?: readonly string[];
244
+ /** Action buttons rendered at the bottom of the form (defaults to a single Submit). */
245
+ readonly actions?: readonly FormAction[];
246
+ /** How action buttons are aligned: `"start"` (default), `"end"`, or `"between"`. */
247
+ readonly actionsAlign?: "start" | "end" | "between";
134
248
  }
135
249
 
136
250
  /**
@@ -167,4 +281,4 @@ declare function isFormSchema(input: unknown): input is FormSchema;
167
281
  /** Convenience: list the field ids declared by a schema, in order. */
168
282
  declare function fieldIds(schema: FormSchema): string[];
169
283
 
170
- export { type Condition, type FieldOption, type FieldSchema, type FieldType, type FieldValue, type FormSchema, type ProviderDecl, type ProviderRef, type Resolvable, SchemaValidationError, type SubmitSchema, type ValidationIssue, type ValidationResult, type ValidationSchema, fieldIds, isFormSchema, parseSchema, validateSchema };
284
+ export { type Condition, type FieldClasses, type FieldOption, type FieldSchema, type FieldSlot, type FieldType, type FieldValue, type FormAction, type FormSchema, type ProviderDecl, type ProviderRef, type Resolvable, SchemaValidationError, type SubmitSchema, type ValidationIssue, type ValidationResult, type ValidationSchema, type WidgetRef, fieldIds, isFormSchema, parseSchema, validateSchema };
package/dist/index.js CHANGED
@@ -9,9 +9,16 @@ var BUILTIN_TYPES = /* @__PURE__ */ new Set([
9
9
  "checkbox",
10
10
  "radio",
11
11
  "group",
12
- "collection"
12
+ "collection",
13
+ "steps",
14
+ "step"
15
+ ]);
16
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set([
17
+ "group",
18
+ "collection",
19
+ "steps",
20
+ "step"
13
21
  ]);
14
- var CONTAINER_TYPES = /* @__PURE__ */ new Set(["group", "collection"]);
15
22
  function isRecord(v) {
16
23
  return typeof v === "object" && v !== null && !Array.isArray(v);
17
24
  }
@@ -47,7 +54,18 @@ function validateField(field, path, seenIds, c) {
47
54
  c.add(`${path}.fields`, `field of type "${type}" must declare a non-empty "fields" array`);
48
55
  } else {
49
56
  const childIds = /* @__PURE__ */ new Set();
50
- nested.forEach((f, i) => validateField(f, `${path}.fields[${i}]`, childIds, c));
57
+ nested.forEach((f, i) => {
58
+ if (type === "steps") {
59
+ const childType = isRecord(f) ? f["type"] : void 0;
60
+ if (childType !== "step") {
61
+ c.add(
62
+ `${path}.fields[${i}].type`,
63
+ `field of type "steps" must contain "step" children (got "${String(childType)}")`
64
+ );
65
+ }
66
+ }
67
+ validateField(f, `${path}.fields[${i}]`, childIds, c);
68
+ });
51
69
  }
52
70
  }
53
71
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/validate.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Runtime validation of an unknown value into a typed {@link FormSchema}.\n *\n * Dependency-free and structural — its job is to give precise, path-addressed\n * errors so an LLM-emitted schema can be repaired (or rejected) before it ever\n * reaches the runtime. This validates the *schema shape*, not user form input\n * (that is the job of the per-field {@link ValidationSchema}).\n */\nimport type { FieldSchema, FieldType, FormSchema } from \"./types.js\";\n\nexport interface ValidationIssue {\n /** JSON-pointer-ish path to the offending node, e.g. `fields[2].type`. */\n readonly path: string;\n readonly message: string;\n}\n\nexport type ValidationResult =\n | { readonly ok: true; readonly value: FormSchema }\n | { readonly ok: false; readonly issues: readonly ValidationIssue[] };\n\nconst BUILTIN_TYPES: ReadonlySet<string> = new Set<FieldType>([\n \"text\",\n \"number\",\n \"email\",\n \"password\",\n \"textarea\",\n \"select\",\n \"checkbox\",\n \"radio\",\n \"group\",\n \"collection\",\n]);\n\nconst CONTAINER_TYPES: ReadonlySet<string> = new Set<FieldType>([\"group\", \"collection\"]);\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nclass Collector {\n readonly issues: ValidationIssue[] = [];\n add(path: string, message: string): void {\n this.issues.push({ path, message });\n }\n}\n\nfunction validateField(field: unknown, path: string, seenIds: Set<string>, c: Collector): void {\n if (!isRecord(field)) {\n c.add(path, \"field must be an object\");\n return;\n }\n\n const id = field[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) {\n c.add(`${path}.id`, \"field.id must be a non-empty string\");\n } else if (seenIds.has(id)) {\n c.add(`${path}.id`, `duplicate field id \"${id}\"`);\n } else {\n seenIds.add(id);\n }\n\n const type = field[\"type\"];\n if (typeof type !== \"string\" || type.length === 0) {\n c.add(`${path}.type`, \"field.type must be a non-empty string\");\n } else if (!BUILTIN_TYPES.has(type)) {\n // Unknown types are allowed (custom widgets) but flagged as a hint, not an error.\n // We intentionally do not push an issue here.\n }\n\n if ((type === \"select\" || type === \"radio\") && field[\"options\"] === undefined) {\n c.add(`${path}.options`, `field of type \"${type}\" should declare options`);\n }\n\n // Containers (group/collection) must declare nested fields; recurse so nested\n // problems surface with a precise path (e.g. `fields[3].fields[0].id`).\n if (typeof type === \"string\" && CONTAINER_TYPES.has(type)) {\n const nested = field[\"fields\"];\n if (!Array.isArray(nested) || nested.length === 0) {\n c.add(`${path}.fields`, `field of type \"${type}\" must declare a non-empty \"fields\" array`);\n } else {\n // A container opens a new naming scope: child ids are unique within it, not globally.\n const childIds = new Set<string>();\n nested.forEach((f, i) => validateField(f, `${path}.fields[${i}]`, childIds, c));\n }\n }\n}\n\n/** Validate an unknown value as a {@link FormSchema}. Never throws. */\nexport function validateSchema(input: unknown): ValidationResult {\n const c = new Collector();\n\n if (!isRecord(input)) {\n c.add(\"$\", \"schema must be an object\");\n return { ok: false, issues: c.issues };\n }\n\n if (typeof input[\"id\"] !== \"string\" || (input[\"id\"] as string).length === 0) {\n c.add(\"id\", \"schema.id must be a non-empty string\");\n }\n if (typeof input[\"version\"] !== \"string\" || (input[\"version\"] as string).length === 0) {\n c.add(\"version\", \"schema.version must be a non-empty string\");\n }\n\n const fields = input[\"fields\"];\n if (!Array.isArray(fields)) {\n c.add(\"fields\", \"schema.fields must be an array\");\n } else if (fields.length === 0) {\n c.add(\"fields\", \"schema.fields must contain at least one field\");\n } else {\n const seenIds = new Set<string>();\n fields.forEach((f, i) => validateField(f, `fields[${i}]`, seenIds, c));\n }\n\n const providers = input[\"providers\"];\n if (providers !== undefined && !isRecord(providers)) {\n c.add(\"providers\", \"schema.providers must be an object when present\");\n }\n\n const submit = input[\"submit\"];\n if (submit !== undefined && !isRecord(submit)) {\n c.add(\"submit\", \"schema.submit must be an object when present\");\n }\n\n if (c.issues.length > 0) {\n return { ok: false, issues: c.issues };\n }\n return { ok: true, value: input as unknown as FormSchema };\n}\n\n/** Validate and throw a single aggregated error on failure. Convenience for trusted input. */\nexport function parseSchema(input: unknown): FormSchema {\n const result = validateSchema(input);\n if (!result.ok) {\n const detail = result.issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\");\n throw new SchemaValidationError(`Invalid Formwright schema:\\n${detail}`, result.issues);\n }\n return result.value;\n}\n\nexport class SchemaValidationError extends Error {\n readonly issues: readonly ValidationIssue[];\n constructor(message: string, issues: readonly ValidationIssue[]) {\n super(message);\n this.name = \"SchemaValidationError\";\n this.issues = issues;\n }\n}\n\n/** Type guard form of {@link validateSchema}. */\nexport function isFormSchema(input: unknown): input is FormSchema {\n return validateSchema(input).ok;\n}\n\n/** Convenience: list the field ids declared by a schema, in order. */\nexport function fieldIds(schema: FormSchema): string[] {\n return schema.fields.map((f: FieldSchema) => f.id);\n}\n"]}
1
+ {"version":3,"sources":["../src/validate.ts"],"names":[],"mappings":";AAoBA,IAAM,aAAA,uBAAyC,GAAA,CAAe;AAAA,EAC5D,MAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA,UAAA;AAAA,EACA,UAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA;AAAA,EACA,OAAA;AAAA,EACA,OAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAC,CAAA;AAED,IAAM,eAAA,uBAA2C,GAAA,CAAe;AAAA,EAC9D,OAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAC,CAAA;AAED,SAAS,SAAS,CAAA,EAA0C;AAC1D,EAAA,OAAO,OAAO,MAAM,QAAA,IAAY,CAAA,KAAM,QAAQ,CAAC,KAAA,CAAM,QAAQ,CAAC,CAAA;AAChE;AAEA,IAAM,YAAN,MAAgB;AAAA,EACL,SAA4B,EAAC;AAAA,EACtC,GAAA,CAAI,MAAc,OAAA,EAAuB;AACvC,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,EAAE,IAAA,EAAM,SAAS,CAAA;AAAA,EACpC;AACF,CAAA;AAEA,SAAS,aAAA,CAAc,KAAA,EAAgB,IAAA,EAAc,OAAA,EAAsB,CAAA,EAAoB;AAC7F,EAAA,IAAI,CAAC,QAAA,CAAS,KAAK,CAAA,EAAG;AACpB,IAAA,CAAA,CAAE,GAAA,CAAI,MAAM,yBAAyB,CAAA;AACrC,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,EAAA,GAAK,MAAM,IAAI,CAAA;AACrB,EAAA,IAAI,OAAO,EAAA,KAAO,QAAA,IAAY,EAAA,CAAG,WAAW,CAAA,EAAG;AAC7C,IAAA,CAAA,CAAE,GAAA,CAAI,CAAA,EAAG,IAAI,CAAA,GAAA,CAAA,EAAO,qCAAqC,CAAA;AAAA,EAC3D,CAAA,MAAA,IAAW,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA,EAAG;AAC1B,IAAA,CAAA,CAAE,IAAI,CAAA,EAAG,IAAI,CAAA,GAAA,CAAA,EAAO,CAAA,oBAAA,EAAuB,EAAE,CAAA,CAAA,CAAG,CAAA;AAAA,EAClD,CAAA,MAAO;AACL,IAAA,OAAA,CAAQ,IAAI,EAAE,CAAA;AAAA,EAChB;AAEA,EAAA,MAAM,IAAA,GAAO,MAAM,MAAM,CAAA;AACzB,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,IAAA,CAAK,WAAW,CAAA,EAAG;AACjD,IAAA,CAAA,CAAE,GAAA,CAAI,CAAA,EAAG,IAAI,CAAA,KAAA,CAAA,EAAS,uCAAuC,CAAA;AAAA,EAC/D,CAAA,MAAA,IAAW,CAAC,aAAA,CAAc,GAAA,CAAI,IAAI,CAAA,EAAG;AAKrC,EAAA,IAAA,CAAK,SAAS,QAAA,IAAY,IAAA,KAAS,YAAY,KAAA,CAAM,SAAS,MAAM,MAAA,EAAW;AAC7E,IAAA,CAAA,CAAE,IAAI,CAAA,EAAG,IAAI,CAAA,QAAA,CAAA,EAAY,CAAA,eAAA,EAAkB,IAAI,CAAA,wBAAA,CAA0B,CAAA;AAAA,EAC3E;AAIA,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,eAAA,CAAgB,GAAA,CAAI,IAAI,CAAA,EAAG;AACzD,IAAA,MAAM,MAAA,GAAS,MAAM,QAAQ,CAAA;AAC7B,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,IAAK,MAAA,CAAO,WAAW,CAAA,EAAG;AACjD,MAAA,CAAA,CAAE,IAAI,CAAA,EAAG,IAAI,CAAA,OAAA,CAAA,EAAW,CAAA,eAAA,EAAkB,IAAI,CAAA,yCAAA,CAA2C,CAAA;AAAA,IAC3F,CAAA,MAAO;AACL,MAAA,MAAM,QAAA,uBAAe,GAAA,EAAY;AACjC,MAAA,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAA,EAAG,CAAA,KAAM;AACvB,QAAA,IAAI,SAAS,OAAA,EAAS;AACpB,UAAA,MAAM,YAAY,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA,CAAE,MAAM,CAAA,GAAI,MAAA;AAC5C,UAAA,IAAI,cAAc,MAAA,EAAQ;AACxB,YAAA,CAAA,CAAE,GAAA;AAAA,cACA,CAAA,EAAG,IAAI,CAAA,QAAA,EAAW,CAAC,CAAA,MAAA,CAAA;AAAA,cACnB,CAAA,yDAAA,EAA4D,MAAA,CAAO,SAAS,CAAC,CAAA,EAAA;AAAA,aAC/E;AAAA,UACF;AAAA,QACF;AACA,QAAA,aAAA,CAAc,GAAG,CAAA,EAAG,IAAI,WAAW,CAAC,CAAA,CAAA,CAAA,EAAK,UAAU,CAAC,CAAA;AAAA,MACtD,CAAC,CAAA;AAAA,IACH;AAAA,EACF;AACF;AAGO,SAAS,eAAe,KAAA,EAAkC;AAC/D,EAAA,MAAM,CAAA,GAAI,IAAI,SAAA,EAAU;AAExB,EAAA,IAAI,CAAC,QAAA,CAAS,KAAK,CAAA,EAAG;AACpB,IAAA,CAAA,CAAE,GAAA,CAAI,KAAK,0BAA0B,CAAA;AACrC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,EAAE,MAAA,EAAO;AAAA,EACvC;AAEA,EAAA,IAAI,OAAO,MAAM,IAAI,CAAA,KAAM,YAAa,KAAA,CAAM,IAAI,CAAA,CAAa,MAAA,KAAW,CAAA,EAAG;AAC3E,IAAA,CAAA,CAAE,GAAA,CAAI,MAAM,sCAAsC,CAAA;AAAA,EACpD;AACA,EAAA,IAAI,OAAO,MAAM,SAAS,CAAA,KAAM,YAAa,KAAA,CAAM,SAAS,CAAA,CAAa,MAAA,KAAW,CAAA,EAAG;AACrF,IAAA,CAAA,CAAE,GAAA,CAAI,WAAW,2CAA2C,CAAA;AAAA,EAC9D;AAEA,EAAA,MAAM,MAAA,GAAS,MAAM,QAAQ,CAAA;AAC7B,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,EAAG;AAC1B,IAAA,CAAA,CAAE,GAAA,CAAI,UAAU,gCAAgC,CAAA;AAAA,EAClD,CAAA,MAAA,IAAW,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG;AAC9B,IAAA,CAAA,CAAE,GAAA,CAAI,UAAU,+CAA+C,CAAA;AAAA,EACjE,CAAA,MAAO;AACL,IAAA,MAAM,OAAA,uBAAc,GAAA,EAAY;AAChC,IAAA,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAA,EAAG,CAAA,KAAM,aAAA,CAAc,CAAA,EAAG,CAAA,OAAA,EAAU,CAAC,CAAA,CAAA,CAAA,EAAK,OAAA,EAAS,CAAC,CAAC,CAAA;AAAA,EACvE;AAEA,EAAA,MAAM,SAAA,GAAY,MAAM,WAAW,CAAA;AACnC,EAAA,IAAI,SAAA,KAAc,MAAA,IAAa,CAAC,QAAA,CAAS,SAAS,CAAA,EAAG;AACnD,IAAA,CAAA,CAAE,GAAA,CAAI,aAAa,iDAAiD,CAAA;AAAA,EACtE;AAEA,EAAA,MAAM,MAAA,GAAS,MAAM,QAAQ,CAAA;AAC7B,EAAA,IAAI,MAAA,KAAW,MAAA,IAAa,CAAC,QAAA,CAAS,MAAM,CAAA,EAAG;AAC7C,IAAA,CAAA,CAAE,GAAA,CAAI,UAAU,8CAA8C,CAAA;AAAA,EAChE;AAEA,EAAA,IAAI,CAAA,CAAE,MAAA,CAAO,MAAA,GAAS,CAAA,EAAG;AACvB,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,EAAE,MAAA,EAAO;AAAA,EACvC;AACA,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,KAAA,EAA+B;AAC3D;AAGO,SAAS,YAAY,KAAA,EAA4B;AACtD,EAAA,MAAM,MAAA,GAAS,eAAe,KAAK,CAAA;AACnC,EAAA,IAAI,CAAC,OAAO,EAAA,EAAI;AACd,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,IAAA,EAAO,CAAA,CAAE,IAAI,KAAK,CAAA,CAAE,OAAO,CAAA,CAAE,CAAA,CAAE,KAAK,IAAI,CAAA;AAChF,IAAA,MAAM,IAAI,qBAAA,CAAsB,CAAA;AAAA,EAA+B,MAAM,CAAA,CAAA,EAAI,MAAA,CAAO,MAAM,CAAA;AAAA,EACxF;AACA,EAAA,OAAO,MAAA,CAAO,KAAA;AAChB;AAEO,IAAM,qBAAA,GAAN,cAAoC,KAAA,CAAM;AAAA,EACtC,MAAA;AAAA,EACT,WAAA,CAAY,SAAiB,MAAA,EAAoC;AAC/D,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,uBAAA;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AACF;AAGO,SAAS,aAAa,KAAA,EAAqC;AAChE,EAAA,OAAO,cAAA,CAAe,KAAK,CAAA,CAAE,EAAA;AAC/B;AAGO,SAAS,SAAS,MAAA,EAA8B;AACrD,EAAA,OAAO,OAAO,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAmB,EAAE,EAAE,CAAA;AACnD","file":"index.js","sourcesContent":["/**\n * Runtime validation of an unknown value into a typed {@link FormSchema}.\n *\n * Dependency-free and structural — its job is to give precise, path-addressed\n * errors so an LLM-emitted schema can be repaired (or rejected) before it ever\n * reaches the runtime. This validates the *schema shape*, not user form input\n * (that is the job of the per-field {@link ValidationSchema}).\n */\nimport type { FieldSchema, FieldType, FormSchema } from \"./types.js\";\n\nexport interface ValidationIssue {\n /** JSON-pointer-ish path to the offending node, e.g. `fields[2].type`. */\n readonly path: string;\n readonly message: string;\n}\n\nexport type ValidationResult =\n | { readonly ok: true; readonly value: FormSchema }\n | { readonly ok: false; readonly issues: readonly ValidationIssue[] };\n\nconst BUILTIN_TYPES: ReadonlySet<string> = new Set<FieldType>([\n \"text\",\n \"number\",\n \"email\",\n \"password\",\n \"textarea\",\n \"select\",\n \"checkbox\",\n \"radio\",\n \"group\",\n \"collection\",\n \"steps\",\n \"step\",\n]);\n\nconst CONTAINER_TYPES: ReadonlySet<string> = new Set<FieldType>([\n \"group\",\n \"collection\",\n \"steps\",\n \"step\",\n]);\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nclass Collector {\n readonly issues: ValidationIssue[] = [];\n add(path: string, message: string): void {\n this.issues.push({ path, message });\n }\n}\n\nfunction validateField(field: unknown, path: string, seenIds: Set<string>, c: Collector): void {\n if (!isRecord(field)) {\n c.add(path, \"field must be an object\");\n return;\n }\n\n const id = field[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) {\n c.add(`${path}.id`, \"field.id must be a non-empty string\");\n } else if (seenIds.has(id)) {\n c.add(`${path}.id`, `duplicate field id \"${id}\"`);\n } else {\n seenIds.add(id);\n }\n\n const type = field[\"type\"];\n if (typeof type !== \"string\" || type.length === 0) {\n c.add(`${path}.type`, \"field.type must be a non-empty string\");\n } else if (!BUILTIN_TYPES.has(type)) {\n // Unknown types are allowed (custom widgets) but flagged as a hint, not an error.\n // We intentionally do not push an issue here.\n }\n\n if ((type === \"select\" || type === \"radio\") && field[\"options\"] === undefined) {\n c.add(`${path}.options`, `field of type \"${type}\" should declare options`);\n }\n\n // Containers must declare nested fields; recurse so nested problems surface with a\n // precise path (e.g. `fields[3].fields[0].id`).\n if (typeof type === \"string\" && CONTAINER_TYPES.has(type)) {\n const nested = field[\"fields\"];\n if (!Array.isArray(nested) || nested.length === 0) {\n c.add(`${path}.fields`, `field of type \"${type}\" must declare a non-empty \"fields\" array`);\n } else {\n const childIds = new Set<string>();\n nested.forEach((f, i) => {\n if (type === \"steps\") {\n const childType = isRecord(f) ? f[\"type\"] : undefined;\n if (childType !== \"step\") {\n c.add(\n `${path}.fields[${i}].type`,\n `field of type \"steps\" must contain \"step\" children (got \"${String(childType)}\")`,\n );\n }\n }\n validateField(f, `${path}.fields[${i}]`, childIds, c);\n });\n }\n }\n}\n\n/** Validate an unknown value as a {@link FormSchema}. Never throws. */\nexport function validateSchema(input: unknown): ValidationResult {\n const c = new Collector();\n\n if (!isRecord(input)) {\n c.add(\"$\", \"schema must be an object\");\n return { ok: false, issues: c.issues };\n }\n\n if (typeof input[\"id\"] !== \"string\" || (input[\"id\"] as string).length === 0) {\n c.add(\"id\", \"schema.id must be a non-empty string\");\n }\n if (typeof input[\"version\"] !== \"string\" || (input[\"version\"] as string).length === 0) {\n c.add(\"version\", \"schema.version must be a non-empty string\");\n }\n\n const fields = input[\"fields\"];\n if (!Array.isArray(fields)) {\n c.add(\"fields\", \"schema.fields must be an array\");\n } else if (fields.length === 0) {\n c.add(\"fields\", \"schema.fields must contain at least one field\");\n } else {\n const seenIds = new Set<string>();\n fields.forEach((f, i) => validateField(f, `fields[${i}]`, seenIds, c));\n }\n\n const providers = input[\"providers\"];\n if (providers !== undefined && !isRecord(providers)) {\n c.add(\"providers\", \"schema.providers must be an object when present\");\n }\n\n const submit = input[\"submit\"];\n if (submit !== undefined && !isRecord(submit)) {\n c.add(\"submit\", \"schema.submit must be an object when present\");\n }\n\n if (c.issues.length > 0) {\n return { ok: false, issues: c.issues };\n }\n return { ok: true, value: input as unknown as FormSchema };\n}\n\n/** Validate and throw a single aggregated error on failure. Convenience for trusted input. */\nexport function parseSchema(input: unknown): FormSchema {\n const result = validateSchema(input);\n if (!result.ok) {\n const detail = result.issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\");\n throw new SchemaValidationError(`Invalid Formwright schema:\\n${detail}`, result.issues);\n }\n return result.value;\n}\n\nexport class SchemaValidationError extends Error {\n readonly issues: readonly ValidationIssue[];\n constructor(message: string, issues: readonly ValidationIssue[]) {\n super(message);\n this.name = \"SchemaValidationError\";\n this.issues = issues;\n }\n}\n\n/** Type guard form of {@link validateSchema}. */\nexport function isFormSchema(input: unknown): input is FormSchema {\n return validateSchema(input).ok;\n}\n\n/** Convenience: list the field ids declared by a schema, in order. */\nexport function fieldIds(schema: FormSchema): string[] {\n return schema.fields.map((f: FieldSchema) => f.id);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formwright/schema",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
4
  "description": "Schema types and runtime validator for Formwright.",
5
5
  "license": "MIT",
6
6
  "repository": {