@formwright/core 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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { signal, computed, untrack, batch } from './chunk-EZUHEI5F.js';
2
- export { batch, computed, effect, isTracking, signal, untrack } from './chunk-EZUHEI5F.js';
1
+ import { signal, computed, effect, untrack, batch } from './chunk-OWEG6VGP.js';
2
+ export { batch, computed, effect, isTracking, signal, untrack } from './chunk-OWEG6VGP.js';
3
3
  import { parseSchema } from '@formwright/schema';
4
4
 
5
5
  // src/conditions.ts
@@ -90,35 +90,42 @@ function isEmpty(value) {
90
90
  return value === void 0 || value === null || value === "";
91
91
  }
92
92
  function compileValidator(schema) {
93
+ const overrides = schema.messages ?? {};
94
+ const msg = (rule, fallback) => overrides[rule] ?? (typeof schema.message === "string" ? schema.message : fallback);
93
95
  return (value) => {
94
- const msg = (fallback) => typeof schema.message === "string" ? schema.message : fallback;
95
96
  if (isEmpty(value)) {
96
- return schema.required ? msg("This field is required") : null;
97
+ return schema.required ? msg("required", "This field is required") : null;
97
98
  }
98
99
  if (schema.kind === "string" || schema.format) {
99
100
  const str = String(value);
100
101
  if (schema.minLength !== void 0 && str.length < schema.minLength) {
101
- return msg(`Must be at least ${schema.minLength} characters`);
102
+ return msg("minLength", `Must be at least ${schema.minLength} characters`);
102
103
  }
103
104
  if (schema.maxLength !== void 0 && str.length > schema.maxLength) {
104
- return msg(`Must be at most ${schema.maxLength} characters`);
105
+ return msg("maxLength", `Must be at most ${schema.maxLength} characters`);
105
106
  }
106
107
  if (schema.pattern !== void 0 && !new RegExp(schema.pattern).test(str)) {
107
- return msg("Invalid format");
108
+ return msg("pattern", "Invalid format");
108
109
  }
109
- if (schema.format === "email" && !EMAIL.test(str)) return msg("Enter a valid email");
110
- if (schema.format === "url" && !URL.test(str)) return msg("Enter a valid URL");
111
- if (schema.format === "uuid" && !UUID.test(str)) return msg("Enter a valid UUID");
110
+ if (schema.format === "email" && !EMAIL.test(str))
111
+ return msg("format", "Enter a valid email");
112
+ if (schema.format === "url" && !URL.test(str)) return msg("format", "Enter a valid URL");
113
+ if (schema.format === "uuid" && !UUID.test(str)) return msg("format", "Enter a valid UUID");
112
114
  }
113
115
  if (schema.kind === "number") {
114
116
  const num = Number(value);
115
- if (Number.isNaN(num)) return msg("Must be a number");
116
- if (schema.min !== void 0 && num < schema.min) return msg(`Must be \u2265 ${schema.min}`);
117
- if (schema.max !== void 0 && num > schema.max) return msg(`Must be \u2264 ${schema.max}`);
117
+ if (Number.isNaN(num)) return msg("type", "Must be a number");
118
+ if (schema.min !== void 0 && num < schema.min)
119
+ return msg("min", `Must be \u2265 ${schema.min}`);
120
+ if (schema.max !== void 0 && num > schema.max)
121
+ return msg("max", `Must be \u2264 ${schema.max}`);
118
122
  }
119
123
  return null;
120
124
  };
121
125
  }
126
+ function requiredMessage(schema) {
127
+ return schema?.messages?.required ?? (typeof schema?.message === "string" ? schema.message : "This field is required");
128
+ }
122
129
 
123
130
  // src/providers.ts
124
131
  function isProviderRef(value) {
@@ -158,10 +165,14 @@ function defaultValueFor(type) {
158
165
  return "";
159
166
  }
160
167
  }
168
+ var uidSeq = 0;
161
169
  var FieldState = class {
162
170
  /** Discriminant for the {@link FieldNode} union (leaf vs group/collection). */
163
171
  kind = "field";
164
172
  id;
173
+ /** A globally-unique DOM id for the control (collection rows reuse `id`, so this must be unique). */
174
+ domId = `fw-${(uidSeq++).toString(36)}`;
175
+ /** The field's schema — mutable at runtime via {@link patchSchema}. */
165
176
  schema;
166
177
  value;
167
178
  error;
@@ -169,32 +180,55 @@ var FieldState = class {
169
180
  visible;
170
181
  enabled;
171
182
  required;
183
+ /** Bumps whenever the schema is patched — renderers re-render the field on change. */
184
+ revision;
172
185
  validator;
173
- constructor(schema, initial, getValue) {
186
+ rev = signal(0);
187
+ stepActive;
188
+ constructor(schema, initial, getValue, stepActive) {
174
189
  this.id = schema.id;
175
190
  this.schema = schema;
191
+ this.stepActive = stepActive;
176
192
  this.value = signal(initial);
177
193
  this.error = signal(null);
178
194
  this.touched = signal(false);
179
195
  this.validator = schema.validation ? compileValidator(schema.validation) : null;
180
- this.visible = computed(() => evaluateCondition(schema.visibleWhen, getValue, true));
181
- this.enabled = computed(() => evaluateCondition(schema.enabledWhen, getValue, true));
196
+ this.revision = this.rev;
197
+ this.visible = computed(() => {
198
+ this.rev.get();
199
+ return evaluateCondition(this.schema.visibleWhen, getValue, true);
200
+ });
201
+ this.enabled = computed(() => {
202
+ this.rev.get();
203
+ return evaluateCondition(this.schema.enabledWhen, getValue, true);
204
+ });
182
205
  this.required = computed(() => {
183
- if (schema.requiredWhen !== void 0) {
184
- return evaluateCondition(schema.requiredWhen, getValue, false);
206
+ this.rev.get();
207
+ if (this.schema.requiredWhen !== void 0) {
208
+ return evaluateCondition(this.schema.requiredWhen, getValue, false);
185
209
  }
186
- return schema.validation?.required ?? false;
210
+ return this.schema.validation?.required ?? false;
187
211
  });
188
212
  }
189
- /** Run validation, store and return the error (or null). Hidden fields never error. */
190
- validate() {
213
+ /** Merge a partial schema in at runtime (change type, label, options, validation, …). */
214
+ patchSchema(partial) {
215
+ this.schema = { ...this.schema, ...partial };
216
+ this.validator = this.schema.validation ? compileValidator(this.schema.validation) : null;
217
+ this.rev.update((n) => n + 1);
218
+ }
219
+ /** Run validation, store and return the error (or null). Hidden / inactive-step fields never error. */
220
+ validate(options) {
191
221
  if (!this.visible.peek()) {
192
222
  this.error.set(null);
193
223
  return null;
194
224
  }
225
+ if (!options?.allSteps && this.stepActive && !this.stepActive.peek()) {
226
+ this.error.set(null);
227
+ return null;
228
+ }
195
229
  let result = null;
196
230
  if (this.required.peek() && isEmpty2(this.value.peek())) {
197
- result = "This field is required";
231
+ result = requiredMessage(this.schema.validation);
198
232
  } else if (this.validator) {
199
233
  result = this.validator(this.value.peek());
200
234
  }
@@ -212,26 +246,40 @@ function isEmpty2(value) {
212
246
  }
213
247
 
214
248
  // src/nodes.ts
249
+ var PRESENTATIONAL = /* @__PURE__ */ new Set(["heading", "separator", "paragraph"]);
250
+ function isPresentational(type) {
251
+ return PRESENTATIONAL.has(type);
252
+ }
215
253
  function nodeValue(node) {
216
254
  return node.value.get();
217
255
  }
218
256
  function collectValues(nodes) {
219
257
  const out = {};
220
- for (const node of nodes) out[node.id] = nodeValue(node);
258
+ for (const node of nodes) {
259
+ if (node.schema.omit || isPresentational(node.schema.type)) continue;
260
+ if (!node.visible.get()) continue;
261
+ out[node.id] = nodeValue(node);
262
+ }
221
263
  return out;
222
264
  }
223
- function buildNodes(schemas, scope, initial) {
265
+ function buildNodes(schemas, scope, initial, stepActive) {
224
266
  const nodes = [];
225
267
  const byName = /* @__PURE__ */ new Map();
226
268
  for (const schema of schemas) {
227
269
  let node;
228
270
  if (schema.type === "group") {
229
- node = new GroupNode(schema, scope, asDict(initial[schema.id]));
271
+ node = new GroupNode(schema, scope, asDict(initial[schema.id]), stepActive);
272
+ } else if (schema.type === "step") {
273
+ if (!stepActive)
274
+ throw new Error('Field type "step" must be nested inside a "steps" container');
275
+ node = new StepNode(schema, scope, asDict(initial[schema.id]), stepActive);
230
276
  } else if (schema.type === "collection") {
231
277
  node = new CollectionNode(schema, scope, asArray(initial[schema.id]));
278
+ } else if (schema.type === "steps") {
279
+ node = new StepsNode(schema, scope, asDict(initial[schema.id]));
232
280
  } else {
233
281
  const init = initial[schema.id] ?? schema.defaultValue ?? defaultValueFor(schema.type);
234
- node = new FieldState(schema, init, scope);
282
+ node = new FieldState(schema, init, scope, stepActive);
235
283
  }
236
284
  nodes.push(node);
237
285
  byName.set(schema.id, node);
@@ -249,7 +297,9 @@ function resetNodes(nodes, initial) {
249
297
  if (node.kind === "field") {
250
298
  const init = initial[node.id] ?? node.schema.defaultValue ?? defaultValueFor(node.schema.type);
251
299
  node.reset(init);
252
- } else if (node.kind === "group") {
300
+ } else if (node.kind === "group" || node.kind === "step") {
301
+ node.reset(asDict(initial[node.id]));
302
+ } else if (node.kind === "steps") {
253
303
  node.reset(asDict(initial[node.id]));
254
304
  } else {
255
305
  node.reset(asArray(initial[node.id]));
@@ -267,14 +317,14 @@ var GroupNode = class {
267
317
  enabled;
268
318
  /** The scope a child uses: resolve a name among siblings, else delegate upward. */
269
319
  scope;
270
- constructor(schema, parentScope, initial) {
320
+ constructor(schema, parentScope, initial, stepActive) {
271
321
  this.id = schema.id;
272
322
  this.schema = schema;
273
323
  this.scope = (name) => {
274
324
  const child = this.byName.get(name);
275
325
  return child ? nodeValue(child) : parentScope(name);
276
326
  };
277
- const built = buildNodes(schema.fields ?? [], this.scope, initial);
327
+ const built = buildNodes(schema.fields ?? [], this.scope, initial, stepActive);
278
328
  this.children = built.nodes;
279
329
  this.byName = built.byName;
280
330
  this.value = computed(() => collectValues(this.children));
@@ -337,6 +387,109 @@ var CollectionNode = class {
337
387
  this.rows.set(this.seedRows(initial));
338
388
  }
339
389
  };
390
+ var StepNode = class {
391
+ kind = "step";
392
+ id;
393
+ schema;
394
+ children;
395
+ byName;
396
+ value;
397
+ visible;
398
+ enabled;
399
+ scope;
400
+ /** True when this step is the active step in its parent wizard. */
401
+ active;
402
+ constructor(schema, parentScope, initial, active) {
403
+ this.id = schema.id;
404
+ this.schema = schema;
405
+ this.active = active;
406
+ this.scope = (name) => {
407
+ const child = this.byName.get(name);
408
+ return child ? nodeValue(child) : parentScope(name);
409
+ };
410
+ const built = buildNodes(schema.fields ?? [], this.scope, initial, active);
411
+ this.children = built.nodes;
412
+ this.byName = built.byName;
413
+ this.value = computed(() => collectValues(this.children));
414
+ this.visible = computed(() => evaluateCondition(schema.visibleWhen, parentScope, true));
415
+ this.enabled = computed(() => evaluateCondition(schema.enabledWhen, parentScope, true));
416
+ }
417
+ reset(initial) {
418
+ resetNodes(this.children, initial);
419
+ }
420
+ };
421
+ var StepsNode = class {
422
+ kind = "steps";
423
+ id;
424
+ schema;
425
+ steps;
426
+ byName;
427
+ value;
428
+ visible;
429
+ enabled;
430
+ currentStep;
431
+ scope;
432
+ constructor(schema, parentScope, initial) {
433
+ this.id = schema.id;
434
+ this.schema = schema;
435
+ this.currentStep = signal(0);
436
+ this.byName = /* @__PURE__ */ new Map();
437
+ this.scope = (name) => {
438
+ const step = this.byName.get(name);
439
+ return step ? step.value.get() : parentScope(name);
440
+ };
441
+ const stepSchemas = schema.fields ?? [];
442
+ const steps = stepSchemas.map((stepSchema, index) => {
443
+ const active = computed(() => this.currentStep.get() === index);
444
+ const stepInitial = asDict(initial[stepSchema.id]);
445
+ const step = new StepNode(stepSchema, this.scope, stepInitial, active);
446
+ this.byName.set(stepSchema.id, step);
447
+ return step;
448
+ });
449
+ this.steps = steps;
450
+ this.value = computed(() => {
451
+ const out = {};
452
+ for (const step of this.steps) out[step.id] = step.value.get();
453
+ return out;
454
+ });
455
+ this.visible = computed(() => evaluateCondition(schema.visibleWhen, parentScope, true));
456
+ this.enabled = computed(() => evaluateCondition(schema.enabledWhen, parentScope, true));
457
+ }
458
+ /** Validate every leaf in the step at `index` (defaults to the current step). */
459
+ validateStep(index) {
460
+ const i = index ?? this.currentStep.peek();
461
+ const step = this.steps[i];
462
+ if (!step) return true;
463
+ let ok = true;
464
+ eachLeaf([step], (leaf) => {
465
+ if (leaf.validate() !== null) ok = false;
466
+ });
467
+ return ok;
468
+ }
469
+ /** Advance to the next step after optionally validating the current one. Returns false if blocked. */
470
+ next() {
471
+ const validate = this.schema.validateOnNext !== false;
472
+ if (validate && !this.validateStep()) return false;
473
+ const cur = this.currentStep.peek();
474
+ if (cur < this.steps.length - 1) {
475
+ this.currentStep.set(cur + 1);
476
+ return true;
477
+ }
478
+ return false;
479
+ }
480
+ /** Go back one step (no validation). */
481
+ prev() {
482
+ this.currentStep.update((i) => Math.max(0, i - 1));
483
+ }
484
+ /** Jump to a step by index (does not validate). */
485
+ goTo(index) {
486
+ if (index >= 0 && index < this.steps.length) this.currentStep.set(index);
487
+ }
488
+ reset(initial) {
489
+ this.currentStep.set(0);
490
+ for (const step of this.steps) step.reset(asDict(initial[step.id]));
491
+ }
492
+ };
340
493
  function buildTree(schemas, initial) {
341
494
  let byName;
342
495
  const scope = (name) => {
@@ -350,8 +503,10 @@ function buildTree(schemas, initial) {
350
503
  function eachLeaf(nodes, visit) {
351
504
  for (const node of nodes) {
352
505
  if (node.kind === "field") visit(node);
353
- else if (node.kind === "group") eachLeaf(node.children, visit);
354
- else for (const row of node.items.peek()) eachLeaf(row.group.children, visit);
506
+ else if (node.kind === "group" || node.kind === "step") eachLeaf(node.children, visit);
507
+ else if (node.kind === "steps") {
508
+ for (const step of node.steps) eachLeaf(step.children, visit);
509
+ } else for (const row of node.items.peek()) eachLeaf(row.group.children, visit);
355
510
  }
356
511
  }
357
512
  var defaultRenderer = null;
@@ -376,15 +531,28 @@ var Form = class {
376
531
  rootByName;
377
532
  listeners = /* @__PURE__ */ new Map();
378
533
  disposeRenderer = null;
534
+ disposePersist = null;
379
535
  constructor(schema, initialValues = {}, options = {}) {
380
536
  this.schema = parseSchema(schema);
381
537
  this.options = options;
382
538
  this.initialValues = initialValues;
383
- const tree = buildTree(this.schema.fields, initialValues);
539
+ const fields = this.schema.locales?.length ? expandLocalized(this.schema.fields, this.schema.locales) : this.schema.fields;
540
+ const seed = loadPersisted(options.persistKey, initialValues);
541
+ const tree = buildTree(fields, seed);
384
542
  this.tree = tree.nodes;
385
543
  this.rootByName = tree.byName;
386
- this.order = this.schema.fields.map((f) => f.id);
544
+ this.order = fields.map((f) => f.id);
387
545
  this.values = computed(() => collectTree(this.tree));
546
+ if (options.persistKey) {
547
+ const key = options.persistKey;
548
+ this.disposePersist = effect(() => {
549
+ const v = this.values.get();
550
+ try {
551
+ localStorage.setItem(key, JSON.stringify(v));
552
+ } catch {
553
+ }
554
+ });
555
+ }
388
556
  this.initialSnapshot = JSON.stringify(untrack(() => this.values.peek()));
389
557
  this.isDirty = computed(() => JSON.stringify(this.values.get()) !== this.initialSnapshot);
390
558
  this.isValid = computed(() => {
@@ -415,12 +583,22 @@ var Form = class {
415
583
  setFieldValue(field, value) {
416
584
  field.value.set(value);
417
585
  field.touched.set(true);
418
- if (field.error.peek() !== null) field.validate();
586
+ field.validate();
419
587
  this.emit("change", { id: field.id, value });
420
588
  }
421
589
  setError(id, error) {
422
590
  this.field(id)?.error.set(error);
423
591
  }
592
+ /** Patch one field's schema at runtime (change type, label, options, validation, …). */
593
+ setFieldSchema(path, partial) {
594
+ this.field(path)?.patchSchema(partial);
595
+ }
596
+ /** Patch many fields' schemas at once: `form.patch({ state: { type: "text" }, … })`. */
597
+ patch(updates) {
598
+ batch(() => {
599
+ for (const [path, partial] of Object.entries(updates)) this.setFieldSchema(path, partial);
600
+ });
601
+ }
424
602
  setErrors(errors) {
425
603
  batch(() => {
426
604
  for (const [id, error] of Object.entries(errors)) this.setError(id, error);
@@ -431,38 +609,54 @@ var Form = class {
431
609
  }
432
610
  // ---- lifecycle ----------------------------------------------------------
433
611
  /** Validate every (visible) leaf field; returns true when the whole form is valid. */
434
- validate() {
612
+ validate(options) {
613
+ const allSteps = options?.allSteps ?? false;
435
614
  return untrack(() => {
436
615
  let ok = true;
437
616
  batch(() => {
438
617
  eachLeaf(this.tree, (leaf) => {
439
- if (leaf.validate() !== null) ok = false;
618
+ if (leaf.validate({ allSteps }) !== null) ok = false;
440
619
  });
441
620
  });
442
621
  return ok;
443
622
  });
444
623
  }
445
- /** Run the submission pipeline: validate transform send onSuccess/onError. */
446
- async submit() {
447
- if (!this.validate()) {
448
- const error = new FormValidationError(this.collectErrors());
624
+ /** Find the first `steps` container in the field tree (if any). */
625
+ findSteps() {
626
+ return findSteps(this.tree);
627
+ }
628
+ /**
629
+ * Run the submission pipeline: validate → transform → send → onSuccess/onError.
630
+ * Pass an inline `transform` to shape the final payload, e.g.
631
+ * `form.submit((values) => ({ ...values, source: "web" }))`.
632
+ *
633
+ * Always **resolves** with a {@link SubmitResult} — never throws — so you can
634
+ * handle both outcomes from the API in one place:
635
+ * `const res = await form.submit(); res.ok ? res.data : res.error`.
636
+ */
637
+ async submit(transform) {
638
+ if (!this.validate({ allSteps: true })) {
639
+ const errors = this.collectErrors();
640
+ const error = new FormValidationError(errors);
449
641
  this.runErrorHandler(error);
450
642
  this.emit("error", error);
451
- throw error;
643
+ return { ok: false, error, errors };
452
644
  }
453
645
  this.submitting.set(true);
454
646
  const values = untrack(() => this.values.peek());
455
- const payload = this.applyTransform(values);
647
+ const named = this.applyTransform(values);
648
+ const payload = transform ? transform(named, this) : named;
456
649
  this.emit("submit", payload);
457
650
  try {
458
- const result = await this.send(payload);
459
- this.runSuccessHandler(result);
460
- this.emit("success", result);
461
- return result;
651
+ const data = await this.send(payload);
652
+ this.clearPersisted();
653
+ this.runSuccessHandler(data);
654
+ this.emit("success", data);
655
+ return { ok: true, data };
462
656
  } catch (error) {
463
657
  this.runErrorHandler(error);
464
658
  this.emit("error", error);
465
- throw error;
659
+ return { ok: false, error };
466
660
  } finally {
467
661
  this.submitting.set(false);
468
662
  }
@@ -472,6 +666,13 @@ var Form = class {
472
666
  resetNodes(this.tree, values);
473
667
  });
474
668
  }
669
+ /** Trigger a named form action: runs its handler (from options) and emits "action". */
670
+ action(name) {
671
+ const def = this.schema.actions?.find((a) => a.name === name);
672
+ const handler = def?.handler ? this.options.handlers?.[def.handler] : void 0;
673
+ handler?.(this);
674
+ this.emit("action", { name });
675
+ }
475
676
  /** Mount into a host element using the given renderer (or the registered default). */
476
677
  mount(host, renderer = defaultRenderer) {
477
678
  if (!renderer) {
@@ -486,8 +687,19 @@ var Form = class {
486
687
  destroy() {
487
688
  this.disposeRenderer?.();
488
689
  this.disposeRenderer = null;
690
+ this.disposePersist?.();
691
+ this.disposePersist = null;
489
692
  this.listeners.clear();
490
693
  }
694
+ /** Remove the cached draft from `localStorage` (called on a successful submit). */
695
+ clearPersisted() {
696
+ const key = this.options.persistKey;
697
+ if (!key || typeof localStorage === "undefined") return;
698
+ try {
699
+ localStorage.removeItem(key);
700
+ } catch {
701
+ }
702
+ }
491
703
  // ---- events -------------------------------------------------------------
492
704
  on(event, listener) {
493
705
  let set = this.listeners.get(event);
@@ -549,14 +761,64 @@ var FormValidationError = class extends Error {
549
761
  function collectTree(tree) {
550
762
  return collectValues(tree);
551
763
  }
764
+ function loadPersisted(key, initial) {
765
+ if (!key || typeof localStorage === "undefined") return initial;
766
+ try {
767
+ const saved = localStorage.getItem(key);
768
+ if (saved) return { ...initial, ...JSON.parse(saved) };
769
+ } catch {
770
+ }
771
+ return initial;
772
+ }
773
+ function expandLocalized(fields, locales) {
774
+ return fields.map((f) => {
775
+ if (f.localized) {
776
+ const leafType = f.type === "group" || f.type === "collection" ? "text" : f.type;
777
+ const child = (loc) => {
778
+ const c = { id: loc, type: leafType, label: loc };
779
+ if (f.placeholder !== void 0) c["placeholder"] = f.placeholder;
780
+ if (f.validation !== void 0) c["validation"] = f.validation;
781
+ if (f.options !== void 0) c["options"] = f.options;
782
+ if (f.widget !== void 0) c["widget"] = f.widget;
783
+ if (f.tooltip !== void 0) c["tooltip"] = f.tooltip;
784
+ return c;
785
+ };
786
+ const group = {
787
+ id: f.id,
788
+ type: "group",
789
+ // Keep the `localized` flag so the renderer shows ONE input + a language
790
+ // switcher (instead of one input per locale). Value stays `{ en, ar }`.
791
+ localized: true,
792
+ fields: locales.map(child)
793
+ };
794
+ if (f.defaultLocale !== void 0) group["defaultLocale"] = f.defaultLocale;
795
+ for (const key of [
796
+ "label",
797
+ "visibleWhen",
798
+ "enabledWhen",
799
+ "class",
800
+ "classes",
801
+ "help",
802
+ "tooltip"
803
+ ]) {
804
+ if (f[key] !== void 0) group[key] = f[key];
805
+ }
806
+ return group;
807
+ }
808
+ if (f.fields) return { ...f, fields: expandLocalized(f.fields, locales) };
809
+ return f;
810
+ });
811
+ }
552
812
  function collectLeaves(tree) {
553
813
  const out = /* @__PURE__ */ new Map();
554
814
  const walk = (nodes, prefix) => {
555
815
  for (const node of nodes) {
556
816
  const path = prefix ? `${prefix}.${node.id}` : node.id;
557
817
  if (node.kind === "field") out.set(path, node);
558
- else if (node.kind === "group") walk(node.children, path);
559
- else {
818
+ else if (node.kind === "group" || node.kind === "step") walk(node.children, path);
819
+ else if (node.kind === "steps") {
820
+ for (const step of node.steps) walk(step.children, `${path}.${step.id}`);
821
+ } else {
560
822
  node.items.peek().forEach((row, i) => walk(row.group.children, `${path}.${i}`));
561
823
  }
562
824
  }
@@ -569,7 +831,9 @@ function resolveLeaf(tree, rootByName, path) {
569
831
  let node = rootByName.get(parts[0]);
570
832
  for (let i = 1; i < parts.length && node; i++) {
571
833
  const part = parts[i];
572
- if (node instanceof GroupNode) {
834
+ if (node instanceof GroupNode || node instanceof StepNode) {
835
+ node = node.byName.get(part);
836
+ } else if (node instanceof StepsNode) {
573
837
  node = node.byName.get(part);
574
838
  } else if (node instanceof CollectionNode) {
575
839
  node = node.items.peek()[Number(part)]?.group;
@@ -579,7 +843,22 @@ function resolveLeaf(tree, rootByName, path) {
579
843
  }
580
844
  return node && node.kind === "field" ? node : void 0;
581
845
  }
846
+ function findSteps(tree) {
847
+ for (const node of tree) {
848
+ if (node.kind === "steps") return node;
849
+ if (node.kind === "group" || node.kind === "step") {
850
+ const found = findSteps(node.children);
851
+ if (found) return found;
852
+ } else if (node.kind === "collection") {
853
+ for (const row of node.items.peek()) {
854
+ const found = findSteps(row.group.children);
855
+ if (found) return found;
856
+ }
857
+ }
858
+ }
859
+ return void 0;
860
+ }
582
861
 
583
- export { CollectionNode, FieldState, Form, FormValidationError, GroupNode, buildTree, compileValidator, defaultValueFor, eachLeaf, evaluateCondition, isProviderRef, referencedFields, resolve, resolveQuery, setDefaultRenderer };
862
+ export { CollectionNode, FieldState, Form, FormValidationError, GroupNode, StepNode, StepsNode, buildTree, compileValidator, defaultValueFor, eachLeaf, evaluateCondition, isPresentational, isProviderRef, referencedFields, resolve, resolveQuery, setDefaultRenderer };
584
863
  //# sourceMappingURL=index.js.map
585
864
  //# sourceMappingURL=index.js.map