@ereo/forms 0.1.23 → 0.1.24

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
@@ -219,6 +219,7 @@ function flattenToPaths(obj, prefix = "") {
219
219
  var PROXY_MARKER = Symbol("ereo-form-proxy");
220
220
  function createValuesProxy(store, basePath = "", proxyCache) {
221
221
  const cache = proxyCache ?? new Map;
222
+ const s = store;
222
223
  if (basePath && cache.has(basePath)) {
223
224
  return cache.get(basePath);
224
225
  }
@@ -229,7 +230,7 @@ function createValuesProxy(store, basePath = "", proxyCache) {
229
230
  if (typeof prop === "symbol")
230
231
  return;
231
232
  const fullPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
232
- const value = store.getValue(fullPath);
233
+ const value = s.getValue(fullPath);
233
234
  if (value !== null && typeof value === "object") {
234
235
  return createValuesProxy(store, fullPath, cache);
235
236
  }
@@ -239,7 +240,7 @@ function createValuesProxy(store, basePath = "", proxyCache) {
239
240
  if (typeof prop === "symbol")
240
241
  return true;
241
242
  const fullPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
242
- store.setValue(fullPath, value);
243
+ s.setValue(fullPath, value);
243
244
  return true;
244
245
  },
245
246
  has(_target, prop) {
@@ -247,14 +248,14 @@ function createValuesProxy(store, basePath = "", proxyCache) {
247
248
  return true;
248
249
  if (typeof prop === "symbol")
249
250
  return false;
250
- const obj = basePath ? store.getValue(basePath) : store.getValues();
251
+ const obj = basePath ? s.getValue(basePath) : s.getValues();
251
252
  if (obj !== null && typeof obj === "object") {
252
253
  return String(prop) in obj;
253
254
  }
254
255
  return false;
255
256
  },
256
257
  ownKeys() {
257
- const obj = basePath ? store.getValue(basePath) : store.getValues();
258
+ const obj = basePath ? s.getValue(basePath) : s.getValues();
258
259
  if (obj !== null && typeof obj === "object") {
259
260
  return Object.keys(obj);
260
261
  }
@@ -263,14 +264,14 @@ function createValuesProxy(store, basePath = "", proxyCache) {
263
264
  getOwnPropertyDescriptor(_target, prop) {
264
265
  if (typeof prop === "symbol")
265
266
  return;
266
- const obj = basePath ? store.getValue(basePath) : store.getValues();
267
+ const obj = basePath ? s.getValue(basePath) : s.getValues();
267
268
  if (obj !== null && typeof obj === "object" && String(prop) in obj) {
268
269
  const fullPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
269
270
  return {
270
271
  configurable: true,
271
272
  enumerable: true,
272
273
  writable: true,
273
- value: store.getValue(fullPath)
274
+ value: s.getValue(fullPath)
274
275
  };
275
276
  }
276
277
  return;
@@ -285,6 +286,267 @@ function createValuesProxy(store, basePath = "", proxyCache) {
285
286
  // src/validation-engine.ts
286
287
  import { signal } from "@ereo/state";
287
288
 
289
+ // src/schema.ts
290
+ function isStandardSchema(value) {
291
+ return value !== null && typeof value === "object" && "~standard" in value;
292
+ }
293
+ function normalizePath(path) {
294
+ if (!path)
295
+ return [];
296
+ return path.map((segment) => {
297
+ if (typeof segment === "object" && segment !== null && "key" in segment) {
298
+ return typeof segment.key === "number" ? segment.key : String(segment.key);
299
+ }
300
+ return typeof segment === "number" ? segment : String(segment);
301
+ });
302
+ }
303
+ function standardSchemaAdapter(schema) {
304
+ return {
305
+ parse: (data) => {
306
+ const result = schema["~standard"].validate(data);
307
+ if (result instanceof Promise) {
308
+ throw new Error("Async Standard Schema validation not supported in parse()");
309
+ }
310
+ if ("issues" in result && result.issues) {
311
+ const msgs = result.issues.map((i) => i.message).join(", ");
312
+ throw new Error(msgs);
313
+ }
314
+ return result.value;
315
+ },
316
+ safeParse: (data) => {
317
+ const result = schema["~standard"].validate(data);
318
+ if (result instanceof Promise) {
319
+ throw new Error("Async Standard Schema validation not supported in safeParse()");
320
+ }
321
+ if ("issues" in result && result.issues) {
322
+ return {
323
+ success: false,
324
+ error: {
325
+ issues: result.issues.map((issue) => ({
326
+ path: normalizePath(issue.path),
327
+ message: issue.message
328
+ }))
329
+ }
330
+ };
331
+ }
332
+ return { success: true, data: result.value };
333
+ }
334
+ };
335
+ }
336
+ function zodAdapter(zodSchema) {
337
+ return {
338
+ parse: (data) => zodSchema.parse(data),
339
+ safeParse: (data) => {
340
+ const result = zodSchema.safeParse(data);
341
+ if (result.success) {
342
+ return { success: true, data: result.data };
343
+ }
344
+ return {
345
+ success: false,
346
+ error: {
347
+ issues: result.error.issues.map((issue) => ({
348
+ path: issue.path,
349
+ message: issue.message
350
+ }))
351
+ }
352
+ };
353
+ }
354
+ };
355
+ }
356
+ function valibotAdapter(schema, parse, safeParse) {
357
+ return {
358
+ parse: (data) => parse(schema, data),
359
+ safeParse: (data) => {
360
+ const result = safeParse(schema, data);
361
+ if (result.success) {
362
+ return { success: true, data: result.output };
363
+ }
364
+ return {
365
+ success: false,
366
+ error: {
367
+ issues: (result.issues || []).map((issue) => ({
368
+ path: (issue.path || []).map((p) => p.key),
369
+ message: issue.message
370
+ }))
371
+ }
372
+ };
373
+ }
374
+ };
375
+ }
376
+ function createSchemaValidator(opts) {
377
+ return {
378
+ parse: (data) => {
379
+ const result = opts.validate(data);
380
+ if (result.success)
381
+ return result.data;
382
+ throw new Error("Validation failed");
383
+ },
384
+ safeParse: (data) => {
385
+ const result = opts.validate(data);
386
+ if (result.success) {
387
+ return { success: true, data: result.data };
388
+ }
389
+ return {
390
+ success: false,
391
+ error: {
392
+ issues: Object.entries(result.errors).flatMap(([path, messages]) => messages.map((message) => ({
393
+ path: path.split("."),
394
+ message
395
+ })))
396
+ }
397
+ };
398
+ }
399
+ };
400
+ }
401
+ var EREO_SCHEMA_MARKER = Symbol("ereo-schema");
402
+ function ereoSchema(definition) {
403
+ const schema = {
404
+ [EREO_SCHEMA_MARKER]: true,
405
+ parse: (data) => {
406
+ const result = schema.safeParse(data);
407
+ if (result.success)
408
+ return result.data;
409
+ const messages = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
410
+ throw new Error(`Validation failed:
411
+ ${messages.join(`
412
+ `)}`);
413
+ },
414
+ safeParse: (data) => {
415
+ const context = {
416
+ getValue: (path) => getPath(data, path),
417
+ getValues: () => data
418
+ };
419
+ const errors = validateDefinition(definition, data, "", context);
420
+ if (errors.length === 0) {
421
+ return { success: true, data };
422
+ }
423
+ return {
424
+ success: false,
425
+ error: { issues: errors }
426
+ };
427
+ }
428
+ };
429
+ return schema;
430
+ }
431
+ function validateDefinition(definition, data, basePath, context) {
432
+ const issues = [];
433
+ for (const [key, rule] of Object.entries(definition)) {
434
+ const path = basePath ? `${basePath}.${key}` : key;
435
+ const value = data?.[key];
436
+ if (typeof rule === "function") {
437
+ if (rule._isAsync)
438
+ continue;
439
+ const result = rule(value, context);
440
+ if (typeof result === "string") {
441
+ issues.push({ path: path.split("."), message: result });
442
+ }
443
+ } else if (Array.isArray(rule)) {
444
+ for (const validator of rule) {
445
+ if (typeof validator === "function") {
446
+ if (validator._isAsync)
447
+ continue;
448
+ const result = validator(value, context);
449
+ if (typeof result === "string") {
450
+ issues.push({ path: path.split("."), message: result });
451
+ break;
452
+ }
453
+ }
454
+ }
455
+ } else if (typeof rule === "object" && rule !== null) {
456
+ const nested = validateDefinition(rule, value, path, context);
457
+ issues.push(...nested);
458
+ }
459
+ }
460
+ return issues;
461
+ }
462
+ function isEreoSchema(value) {
463
+ return value !== null && typeof value === "object" && EREO_SCHEMA_MARKER in value;
464
+ }
465
+ function formDataToObject(formData, opts) {
466
+ const result = {};
467
+ const arrayFields = new Set(opts?.arrays ?? []);
468
+ for (const [key, value] of formData.entries()) {
469
+ const isArray = key.endsWith("[]") || arrayFields.has(key);
470
+ const cleanKey = key.replace(/\[\]$/, "");
471
+ const coerced = opts?.coerce !== false ? coerceValue(value) : value;
472
+ if (isArray) {
473
+ if (!result[cleanKey])
474
+ result[cleanKey] = [];
475
+ result[cleanKey].push(coerced);
476
+ } else if (cleanKey.includes(".") || cleanKey.includes("[")) {
477
+ setNestedValue(result, cleanKey, coerced);
478
+ } else {
479
+ result[cleanKey] = coerced;
480
+ }
481
+ }
482
+ return result;
483
+ }
484
+ function coerceValue(value) {
485
+ if (value instanceof File)
486
+ return value;
487
+ const str = String(value);
488
+ if (str === "true")
489
+ return true;
490
+ if (str === "false")
491
+ return false;
492
+ if (str === "null")
493
+ return null;
494
+ if (str === "")
495
+ return "";
496
+ const trimmed = str.trim();
497
+ if (trimmed !== "" && !/^0\d/.test(trimmed)) {
498
+ const num = Number(trimmed);
499
+ if (!isNaN(num))
500
+ return num;
501
+ }
502
+ if (/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/.test(str)) {
503
+ const d = new Date(str);
504
+ if (!isNaN(d.getTime()))
505
+ return d.toISOString();
506
+ }
507
+ return str;
508
+ }
509
+ function setNestedValue(obj, path, value) {
510
+ const segments = [];
511
+ let current = "";
512
+ for (let i = 0;i < path.length; i++) {
513
+ const char = path[i];
514
+ if (char === ".") {
515
+ if (current) {
516
+ segments.push(current);
517
+ current = "";
518
+ }
519
+ } else if (char === "[") {
520
+ if (current) {
521
+ segments.push(current);
522
+ current = "";
523
+ }
524
+ const close = path.indexOf("]", i);
525
+ if (close !== -1) {
526
+ const idx = path.slice(i + 1, close);
527
+ const num = parseInt(idx, 10);
528
+ segments.push(!isNaN(num) ? num : idx);
529
+ i = close;
530
+ }
531
+ } else {
532
+ current += char;
533
+ }
534
+ }
535
+ if (current)
536
+ segments.push(current);
537
+ let target = obj;
538
+ for (let i = 0;i < segments.length - 1; i++) {
539
+ const seg = segments[i];
540
+ const nextSeg = segments[i + 1];
541
+ if (target[seg] === undefined) {
542
+ target[seg] = typeof nextSeg === "number" ? [] : {};
543
+ }
544
+ target = target[seg];
545
+ }
546
+ target[segments[segments.length - 1]] = value;
547
+ }
548
+
549
+ // src/validation-engine.ts
288
550
  class ValidationEngine {
289
551
  _store;
290
552
  _fieldValidations = new Map;
@@ -294,6 +556,8 @@ class ValidationEngine {
294
556
  _validatingSignals = new Map;
295
557
  _fieldGenerations = new Map;
296
558
  _validateAllController = null;
559
+ _dependents = new Map;
560
+ _validatingDependents = new Set;
297
561
  constructor(store) {
298
562
  this._store = store;
299
563
  const { validators } = store.config;
@@ -305,14 +569,36 @@ class ValidationEngine {
305
569
  }
306
570
  }
307
571
  }
572
+ const { dependencies } = store.config;
573
+ if (dependencies) {
574
+ for (const [dependent, sources] of Object.entries(dependencies)) {
575
+ if (sources) {
576
+ const sourceArr = Array.isArray(sources) ? sources : [sources];
577
+ for (const source of sourceArr) {
578
+ this._registerDependency(dependent, source);
579
+ }
580
+ }
581
+ }
582
+ }
308
583
  }
309
- registerFieldValidators(path, validators, explicitTrigger) {
584
+ registerFieldValidators(path, validators, explicitTrigger, dependsOn) {
310
585
  const derivedTrigger = explicitTrigger ?? this._deriveValidateOn(validators);
311
586
  this._fieldValidations.set(path, {
312
587
  validators,
313
588
  validateOn: explicitTrigger,
314
589
  derivedTrigger
315
590
  });
591
+ for (const v of validators) {
592
+ if (v._dependsOnField) {
593
+ this._registerDependency(path, v._dependsOnField);
594
+ }
595
+ }
596
+ if (dependsOn) {
597
+ const deps = Array.isArray(dependsOn) ? dependsOn : [dependsOn];
598
+ for (const dep of deps) {
599
+ this._registerDependency(path, dep);
600
+ }
601
+ }
316
602
  }
317
603
  _deriveValidateOn(validators) {
318
604
  const hasAsync = validators.some((v) => v._isAsync);
@@ -325,17 +611,18 @@ class ValidationEngine {
325
611
  }
326
612
  onFieldChange(path) {
327
613
  const validation = this._fieldValidations.get(path);
328
- if (!validation)
329
- return;
330
- const trigger = validation.validateOn ?? validation.derivedTrigger;
331
- if (trigger !== "change")
332
- return;
333
- const debounceMs = this._getDebounceMs(validation.validators);
334
- if (debounceMs > 0) {
335
- this._debounceValidation(path, debounceMs);
336
- } else {
337
- this._runFieldValidation(path);
614
+ if (validation) {
615
+ const trigger = validation.validateOn ?? validation.derivedTrigger;
616
+ if (trigger === "change") {
617
+ const debounceMs = this._getDebounceMs(validation.validators);
618
+ if (debounceMs > 0) {
619
+ this._debounceValidation(path, debounceMs);
620
+ } else {
621
+ this._runFieldValidation(path);
622
+ }
623
+ }
338
624
  }
625
+ this._triggerDependents(path);
339
626
  }
340
627
  onFieldBlur(path) {
341
628
  const validation = this._fieldValidations.get(path);
@@ -382,16 +669,27 @@ class ValidationEngine {
382
669
  const generation = (this._fieldGenerations.get(path) ?? 0) + 1;
383
670
  this._fieldGenerations.set(path, generation);
384
671
  this._setFieldValidating(path, true);
385
- const errors = [];
672
+ const syncErrors = [];
673
+ const asyncErrors = [];
386
674
  const value = this._store.getValue(path);
387
675
  const context = this._createContext(controller.signal);
388
676
  try {
389
- for (const validator of validation.validators) {
677
+ const syncValidators = validation.validators.filter((v) => !v._isAsync);
678
+ const asyncValidators = validation.validators.filter((v) => v._isAsync);
679
+ for (const validator of syncValidators) {
390
680
  if (controller.signal.aborted)
391
681
  break;
392
682
  const result = await validator(value, context);
393
- if (result) {
394
- errors.push(result);
683
+ if (result)
684
+ syncErrors.push(result);
685
+ }
686
+ if (syncErrors.length === 0 && !controller.signal.aborted) {
687
+ for (const validator of asyncValidators) {
688
+ if (controller.signal.aborted)
689
+ break;
690
+ const result = await validator(value, context);
691
+ if (result)
692
+ asyncErrors.push(result);
395
693
  }
396
694
  }
397
695
  } finally {
@@ -400,9 +698,21 @@ class ValidationEngine {
400
698
  this._abortControllers.delete(path);
401
699
  }
402
700
  }
701
+ const allErrors = [...syncErrors, ...asyncErrors];
403
702
  if (!controller.signal.aborted && this._fieldGenerations.get(path) === generation) {
404
- this._store.setErrors(path, errors);
405
- return errors;
703
+ this._store.clearErrorsBySource(path, "sync");
704
+ this._store.clearErrorsBySource(path, "async");
705
+ if (syncErrors.length > 0) {
706
+ this._store.setErrorsWithSource(path, syncErrors, "sync");
707
+ }
708
+ if (asyncErrors.length > 0) {
709
+ this._store.setErrorsWithSource(path, asyncErrors, "async");
710
+ }
711
+ if (allErrors.length === 0) {
712
+ this._store.clearErrorsBySource(path, "sync");
713
+ this._store.clearErrorsBySource(path, "async");
714
+ }
715
+ return allErrors;
406
716
  }
407
717
  return [];
408
718
  }
@@ -426,9 +736,10 @@ class ValidationEngine {
426
736
  this._validateAllController = controller;
427
737
  const allErrors = {};
428
738
  let hasErrors = false;
739
+ let schemaResult = null;
429
740
  const schema = this._store.config.schema;
430
741
  if (schema) {
431
- const schemaResult = await this._validateSchema(schema);
742
+ schemaResult = await this._validateSchema(schema);
432
743
  if (controller.signal.aborted)
433
744
  return { success: true };
434
745
  if (!schemaResult.success && schemaResult.errors) {
@@ -448,13 +759,24 @@ class ValidationEngine {
448
759
  const value = this._store.getValue(path);
449
760
  const context = this._createContext(controller.signal);
450
761
  const errors = [];
451
- for (const validator of validation.validators) {
762
+ const syncValidators = validation.validators.filter((v) => !v._isAsync);
763
+ const asyncValidators = validation.validators.filter((v) => v._isAsync);
764
+ for (const validator of syncValidators) {
452
765
  if (controller.signal.aborted)
453
766
  break;
454
767
  const result = await validator(value, context);
455
768
  if (result)
456
769
  errors.push(result);
457
770
  }
771
+ if (errors.length === 0 && !controller.signal.aborted) {
772
+ for (const validator of asyncValidators) {
773
+ if (controller.signal.aborted)
774
+ break;
775
+ const result = await validator(value, context);
776
+ if (result)
777
+ errors.push(result);
778
+ }
779
+ }
458
780
  if (errors.length > 0 && !controller.signal.aborted) {
459
781
  if (allErrors[path]) {
460
782
  allErrors[path] = [...allErrors[path], ...errors];
@@ -470,15 +792,51 @@ class ValidationEngine {
470
792
  this._validateAllController = null;
471
793
  }
472
794
  this._store.clearErrors();
795
+ if (schema) {
796
+ const schemaErrors = schemaResult?.errors ?? {};
797
+ for (const [path, errors] of Object.entries(schemaErrors)) {
798
+ this._store.setErrorsWithSource(path, errors, "schema");
799
+ }
800
+ }
473
801
  for (const [path, errors] of Object.entries(allErrors)) {
474
- this._store.setErrors(path, errors);
802
+ const schemaErrors = schemaResult?.errors ?? {};
803
+ if (schemaErrors[path]) {
804
+ const validation = this._fieldValidations.get(path);
805
+ const hasAsync = validation?.validators.some((v) => v._isAsync);
806
+ const source = hasAsync && errors.length > 0 ? "sync" : "sync";
807
+ const fieldOnlyErrors = errors.filter((e) => !schemaErrors[path]?.includes(e));
808
+ if (fieldOnlyErrors.length > 0) {
809
+ this._store.setErrorsWithSource(path, fieldOnlyErrors, source);
810
+ }
811
+ } else {
812
+ const validation = this._fieldValidations.get(path);
813
+ if (validation) {
814
+ const syncValidators = validation.validators.filter((v) => !v._isAsync);
815
+ const asyncValidators = validation.validators.filter((v) => v._isAsync);
816
+ if (syncValidators.length > 0 && errors.length > 0) {
817
+ this._store.setErrorsWithSource(path, errors, "sync");
818
+ } else if (asyncValidators.length > 0 && errors.length > 0) {
819
+ this._store.setErrorsWithSource(path, errors, "async");
820
+ } else {
821
+ this._store.setErrors(path, errors);
822
+ }
823
+ } else {
824
+ this._store.setErrors(path, errors);
825
+ }
826
+ }
475
827
  }
476
828
  return { success: !hasErrors, errors: hasErrors ? allErrors : undefined };
477
829
  }
478
830
  async _validateSchema(schema) {
831
+ let resolvedSchema;
832
+ if (isStandardSchema(schema)) {
833
+ resolvedSchema = standardSchemaAdapter(schema);
834
+ } else {
835
+ resolvedSchema = schema;
836
+ }
479
837
  const values = this._store._getCurrentValues();
480
- if (schema.safeParse) {
481
- const result = schema.safeParse(values);
838
+ if (resolvedSchema.safeParse) {
839
+ const result = resolvedSchema.safeParse(values);
482
840
  if (result.success) {
483
841
  return { success: true };
484
842
  }
@@ -494,7 +852,7 @@ class ValidationEngine {
494
852
  return { success: false, errors };
495
853
  }
496
854
  try {
497
- schema.parse(values);
855
+ resolvedSchema.parse(values);
498
856
  return { success: true };
499
857
  } catch (e) {
500
858
  if (e?.issues) {
@@ -513,11 +871,48 @@ class ValidationEngine {
513
871
  };
514
872
  }
515
873
  }
874
+ getRegisteredPaths() {
875
+ return this._fieldValidations.keys();
876
+ }
877
+ _registerDependency(dependentField, sourceField) {
878
+ let set = this._dependents.get(sourceField);
879
+ if (!set) {
880
+ set = new Set;
881
+ this._dependents.set(sourceField, set);
882
+ }
883
+ set.add(dependentField);
884
+ }
885
+ _triggerDependents(sourcePath) {
886
+ const dependents = this._dependents.get(sourcePath);
887
+ if (!dependents)
888
+ return;
889
+ for (const dep of dependents) {
890
+ if (this._validatingDependents.has(dep))
891
+ continue;
892
+ const validation = this._fieldValidations.get(dep);
893
+ if (!validation)
894
+ continue;
895
+ if (!this._store.getTouched(dep))
896
+ continue;
897
+ this._validatingDependents.add(dep);
898
+ this._runFieldValidation(dep);
899
+ Promise.resolve().then(() => {
900
+ this._validatingDependents.delete(dep);
901
+ });
902
+ }
903
+ }
904
+ getDependents(sourcePath) {
905
+ return this._dependents.get(sourcePath);
906
+ }
516
907
  unregisterField(path) {
517
908
  this._cancelFieldValidation(path);
518
909
  this._fieldValidations.delete(path);
519
910
  this._validatingSignals.delete(path);
520
911
  this._fieldGenerations.delete(path);
912
+ this._dependents.delete(path);
913
+ for (const [, set] of this._dependents) {
914
+ set.delete(path);
915
+ }
521
916
  }
522
917
  dispose() {
523
918
  for (const timer of this._debounceTimers.values()) {
@@ -531,6 +926,8 @@ class ValidationEngine {
531
926
  this._validatingFields.clear();
532
927
  this._validatingSignals.clear();
533
928
  this._fieldGenerations.clear();
929
+ this._dependents.clear();
930
+ this._validatingDependents.clear();
534
931
  if (this._validateAllController) {
535
932
  this._validateAllController.abort();
536
933
  this._validateAllController = null;
@@ -579,11 +976,203 @@ class ValidationEngine {
579
976
  }
580
977
  }
581
978
 
979
+ // src/a11y.ts
980
+ var idCounter = 0;
981
+ function generateA11yId(prefix = "ereo") {
982
+ return `${prefix}-${++idCounter}`;
983
+ }
984
+ function getFieldA11y(name, state) {
985
+ const attrs = {};
986
+ if (state.errors.length > 0 && state.touched) {
987
+ attrs["aria-invalid"] = true;
988
+ attrs["aria-describedby"] = `${name}-error`;
989
+ }
990
+ return attrs;
991
+ }
992
+ function getErrorA11y(name) {
993
+ return {
994
+ id: `${name}-error`,
995
+ role: "alert",
996
+ "aria-live": "polite"
997
+ };
998
+ }
999
+ function getLabelA11y(name, opts) {
1000
+ return {
1001
+ htmlFor: name,
1002
+ id: opts?.id ?? `${name}-label`
1003
+ };
1004
+ }
1005
+ function getDescriptionA11y(name) {
1006
+ return {
1007
+ id: `${name}-description`
1008
+ };
1009
+ }
1010
+ function getFieldsetA11y(name, _legend) {
1011
+ return {
1012
+ role: "group",
1013
+ "aria-labelledby": `${name}-legend`
1014
+ };
1015
+ }
1016
+ function getFieldWrapperA11y(name, state) {
1017
+ const attrs = {};
1018
+ attrs["data-field"] = name;
1019
+ if (state.errors.length > 0 && state.touched) {
1020
+ attrs["data-invalid"] = true;
1021
+ }
1022
+ return attrs;
1023
+ }
1024
+ function getFormA11y(id, opts) {
1025
+ const attrs = {
1026
+ id,
1027
+ role: "form"
1028
+ };
1029
+ if (opts?.isSubmitting) {
1030
+ attrs["aria-busy"] = true;
1031
+ }
1032
+ return attrs;
1033
+ }
1034
+ function getErrorSummaryA11y(formId) {
1035
+ return {
1036
+ role: "alert",
1037
+ "aria-labelledby": `${formId}-error-summary`
1038
+ };
1039
+ }
1040
+ function focusFirstError(form) {
1041
+ if (typeof document === "undefined")
1042
+ return;
1043
+ const scrollBehavior = prefersReducedMotion() ? "auto" : "smooth";
1044
+ const formStore = form;
1045
+ if (formStore._fieldRefs) {
1046
+ for (const [path, el] of formStore._fieldRefs) {
1047
+ if (!el)
1048
+ continue;
1049
+ const errors = form.getErrors(path).get();
1050
+ if (errors.length > 0) {
1051
+ el.focus();
1052
+ el.scrollIntoView({ behavior: scrollBehavior, block: "center" });
1053
+ return;
1054
+ }
1055
+ }
1056
+ }
1057
+ const elements = document.querySelectorAll('[aria-invalid="true"]');
1058
+ const first = elements[0];
1059
+ if (first) {
1060
+ first.focus();
1061
+ first.scrollIntoView({ behavior: scrollBehavior, block: "center" });
1062
+ }
1063
+ }
1064
+ function focusField(name) {
1065
+ if (typeof document === "undefined")
1066
+ return;
1067
+ const scrollBehavior = prefersReducedMotion() ? "auto" : "smooth";
1068
+ const el = document.querySelector(`[name="${name}"]`);
1069
+ if (el) {
1070
+ el.focus();
1071
+ el.scrollIntoView({ behavior: scrollBehavior, block: "center" });
1072
+ }
1073
+ }
1074
+ function trapFocus(container) {
1075
+ if (typeof document === "undefined")
1076
+ return () => {};
1077
+ const focusableSelector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
1078
+ const handleKeydown = (e) => {
1079
+ if (e.key !== "Tab")
1080
+ return;
1081
+ const focusable = container.querySelectorAll(focusableSelector);
1082
+ if (focusable.length === 0)
1083
+ return;
1084
+ const first = focusable[0];
1085
+ const last = focusable[focusable.length - 1];
1086
+ if (e.shiftKey) {
1087
+ if (document.activeElement === first) {
1088
+ e.preventDefault();
1089
+ last.focus();
1090
+ }
1091
+ } else {
1092
+ if (document.activeElement === last) {
1093
+ e.preventDefault();
1094
+ first.focus();
1095
+ }
1096
+ }
1097
+ };
1098
+ container.addEventListener("keydown", handleKeydown);
1099
+ return () => container.removeEventListener("keydown", handleKeydown);
1100
+ }
1101
+ var liveRegion = null;
1102
+ function getOrCreateLiveRegion() {
1103
+ if (typeof document === "undefined") {
1104
+ return { textContent: "" };
1105
+ }
1106
+ if (!liveRegion) {
1107
+ liveRegion = document.createElement("div");
1108
+ liveRegion.setAttribute("role", "status");
1109
+ liveRegion.setAttribute("aria-live", "polite");
1110
+ liveRegion.setAttribute("aria-atomic", "true");
1111
+ liveRegion.style.position = "absolute";
1112
+ liveRegion.style.width = "1px";
1113
+ liveRegion.style.height = "1px";
1114
+ liveRegion.style.padding = "0";
1115
+ liveRegion.style.margin = "-1px";
1116
+ liveRegion.style.overflow = "hidden";
1117
+ liveRegion.style.clip = "rect(0, 0, 0, 0)";
1118
+ liveRegion.style.whiteSpace = "nowrap";
1119
+ liveRegion.style.borderWidth = "0";
1120
+ document.body.appendChild(liveRegion);
1121
+ }
1122
+ return liveRegion;
1123
+ }
1124
+ function announce(message, priority = "polite") {
1125
+ const region = getOrCreateLiveRegion();
1126
+ region.setAttribute("aria-live", priority);
1127
+ region.textContent = "";
1128
+ requestAnimationFrame(() => {
1129
+ region.textContent = message;
1130
+ });
1131
+ }
1132
+ function cleanupLiveRegion() {
1133
+ if (liveRegion && typeof document !== "undefined") {
1134
+ liveRegion.remove();
1135
+ liveRegion = null;
1136
+ }
1137
+ }
1138
+ function announceErrors(errors, opts) {
1139
+ const errorEntries = Object.entries(errors).filter(([_, msgs]) => msgs.length > 0);
1140
+ if (errorEntries.length === 0)
1141
+ return;
1142
+ const prefix = opts?.prefix ?? "Form has errors:";
1143
+ const messages = errorEntries.map(([field, msgs]) => `${field}: ${msgs[0]}`).join(". ");
1144
+ announce(`${prefix} ${messages}`, "assertive");
1145
+ }
1146
+ function announceSubmitStatus(status, opts) {
1147
+ switch (status) {
1148
+ case "submitting":
1149
+ announce(opts?.submittingMessage ?? "Submitting form...", "polite");
1150
+ break;
1151
+ case "success":
1152
+ announce(opts?.successMessage ?? "Form submitted successfully.", "polite");
1153
+ break;
1154
+ case "error":
1155
+ announce(opts?.errorMessage ?? "Form submission failed. Please check for errors.", "assertive");
1156
+ break;
1157
+ }
1158
+ }
1159
+ function prefersReducedMotion() {
1160
+ if (typeof window === "undefined")
1161
+ return false;
1162
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1163
+ }
1164
+ function isScreenReaderActive() {
1165
+ if (typeof window === "undefined")
1166
+ return false;
1167
+ return document.querySelector('[role="application"]') !== null || window.navigator.userAgent.includes("NVDA") || window.navigator.userAgent.includes("JAWS");
1168
+ }
1169
+
582
1170
  // src/store.ts
583
1171
  class FormStore {
584
1172
  config;
585
1173
  _signals = new Map;
586
1174
  _errorSignals = new Map;
1175
+ _errorMapSignals = new Map;
587
1176
  _formErrors;
588
1177
  _baseline;
589
1178
  _touchedSet = new Set;
@@ -815,6 +1404,11 @@ class FormStore {
815
1404
  }
816
1405
  setErrors(path, errors) {
817
1406
  this.getErrors(path).set(errors);
1407
+ const mapSig = this._errorMapSignals.get(path);
1408
+ if (mapSig) {
1409
+ const current = mapSig.get();
1410
+ mapSig.set({ ...current, manual: errors });
1411
+ }
818
1412
  this._updateIsValid();
819
1413
  this._notifySubscribers();
820
1414
  }
@@ -823,10 +1417,16 @@ class FormStore {
823
1417
  const sig = this._errorSignals.get(path);
824
1418
  if (sig)
825
1419
  sig.set([]);
1420
+ const mapSig = this._errorMapSignals.get(path);
1421
+ if (mapSig)
1422
+ mapSig.set(this._emptyErrorMap());
826
1423
  } else {
827
1424
  for (const sig of this._errorSignals.values()) {
828
1425
  sig.set([]);
829
1426
  }
1427
+ for (const sig of this._errorMapSignals.values()) {
1428
+ sig.set(this._emptyErrorMap());
1429
+ }
830
1430
  this._formErrors.set([]);
831
1431
  }
832
1432
  this._updateIsValid();
@@ -837,6 +1437,44 @@ class FormStore {
837
1437
  this._updateIsValid();
838
1438
  this._notifySubscribers();
839
1439
  }
1440
+ _emptyErrorMap() {
1441
+ return { sync: [], async: [], schema: [], server: [], manual: [] };
1442
+ }
1443
+ getErrorMap(path) {
1444
+ let sig = this._errorMapSignals.get(path);
1445
+ if (!sig) {
1446
+ sig = signal2(this._emptyErrorMap());
1447
+ this._errorMapSignals.set(path, sig);
1448
+ }
1449
+ return sig;
1450
+ }
1451
+ setErrorsWithSource(path, errors, source) {
1452
+ const mapSig = this.getErrorMap(path);
1453
+ const current = mapSig.get();
1454
+ const updated = { ...current, [source]: errors };
1455
+ mapSig.set(updated);
1456
+ this._rebuildFlatErrors(path, updated);
1457
+ }
1458
+ clearErrorsBySource(path, source) {
1459
+ const mapSig = this._errorMapSignals.get(path);
1460
+ if (!mapSig)
1461
+ return;
1462
+ const current = mapSig.get();
1463
+ if (current[source].length === 0)
1464
+ return;
1465
+ const updated = { ...current, [source]: [] };
1466
+ mapSig.set(updated);
1467
+ this._rebuildFlatErrors(path, updated);
1468
+ }
1469
+ _rebuildFlatErrors(path, errorMap) {
1470
+ const flat = [];
1471
+ for (const source of ["sync", "async", "schema", "server", "manual"]) {
1472
+ flat.push(...errorMap[source]);
1473
+ }
1474
+ this.getErrors(path).set(flat);
1475
+ this._updateIsValid();
1476
+ this._notifySubscribers();
1477
+ }
840
1478
  _updateIsValid() {
841
1479
  let valid = this._formErrors.get().length === 0;
842
1480
  if (valid) {
@@ -870,7 +1508,7 @@ class FormStore {
870
1508
  if (options) {
871
1509
  this._fieldOptions.set(path, options);
872
1510
  if (options.validate) {
873
- this._validationEngine.registerFieldValidators(path, Array.isArray(options.validate) ? options.validate : [options.validate], options.validateOn);
1511
+ this._validationEngine.registerFieldValidators(path, Array.isArray(options.validate) ? options.validate : [options.validate], options.validateOn, options.dependsOn);
874
1512
  }
875
1513
  }
876
1514
  const value = this.getValue(path);
@@ -936,7 +1574,10 @@ class FormStore {
936
1574
  if (e)
937
1575
  e.preventDefault?.();
938
1576
  if (!this.config.onSubmit) {
939
- await this._validationEngine.validateAll();
1577
+ const result = await this._validationEngine.validateAll();
1578
+ if (!result.success && this.config.focusOnError !== false) {
1579
+ focusFirstError(this);
1580
+ }
940
1581
  return;
941
1582
  }
942
1583
  await this.submitWith(this.config.onSubmit);
@@ -970,6 +1611,9 @@ class FormStore {
970
1611
  this.isSubmitting.set(false);
971
1612
  this.submitState.set("error");
972
1613
  });
1614
+ if (this.config.focusOnError !== false) {
1615
+ focusFirstError(this);
1616
+ }
973
1617
  return;
974
1618
  }
975
1619
  const values = this._getCurrentValues();
@@ -1003,6 +1647,32 @@ class FormStore {
1003
1647
  const result = await this._validationEngine.validateAll();
1004
1648
  return result.success;
1005
1649
  }
1650
+ resetField(path) {
1651
+ batch(() => {
1652
+ const baseline = getPath(this._baseline, path);
1653
+ this.setValue(path, baseline);
1654
+ this.clearErrors(path);
1655
+ this.setTouched(path, false);
1656
+ this._dirtySet.delete(path);
1657
+ this.isDirty.set(this._dirtySet.size > 0);
1658
+ });
1659
+ }
1660
+ async trigger(path) {
1661
+ if (path) {
1662
+ this.setTouched(path, true);
1663
+ const errors = await this._validationEngine.validateField(path);
1664
+ return errors.length === 0;
1665
+ }
1666
+ for (const p of this._fieldOptions.keys()) {
1667
+ this._touchedSet.add(p);
1668
+ }
1669
+ for (const p of this._validationEngine.getRegisteredPaths()) {
1670
+ this._touchedSet.add(p);
1671
+ }
1672
+ this._notifySubscribers();
1673
+ const result = await this._validationEngine.validateAll();
1674
+ return result.success;
1675
+ }
1006
1676
  reset() {
1007
1677
  this.resetTo(deepClone(this.config.defaultValues));
1008
1678
  }
@@ -1112,6 +1782,7 @@ class FormStore {
1112
1782
  this._watchers.clear();
1113
1783
  this._fieldRefs.clear();
1114
1784
  this._fieldOptions.clear();
1785
+ this._errorMapSignals.clear();
1115
1786
  if (this._submitAbort) {
1116
1787
  this._submitAbort.abort();
1117
1788
  this._submitAbort = null;
@@ -1199,38 +1870,41 @@ function useForm(config) {
1199
1870
  return storeRef.current;
1200
1871
  }
1201
1872
  function useField(form, name, opts) {
1873
+ const f = form;
1202
1874
  const optsRef = useRef(opts);
1203
1875
  const registered = useRef(false);
1204
1876
  if (!registered.current || optsRef.current !== opts) {
1205
1877
  optsRef.current = opts;
1206
1878
  if (opts) {
1207
- form.register(name, opts);
1879
+ f.register(name, opts);
1208
1880
  }
1209
1881
  registered.current = true;
1210
1882
  }
1211
1883
  useEffect(() => {
1212
1884
  return () => {
1213
- form.unregister(name);
1885
+ f.unregister(name);
1214
1886
  };
1215
- }, [form, name]);
1216
- const valueSig = form.getSignal(name);
1887
+ }, [f, name]);
1888
+ const valueSig = f.getSignal(name);
1217
1889
  const value = useSignal(valueSig);
1218
- const errorsSig = form.getErrors(name);
1890
+ const errorsSig = f.getErrors(name);
1219
1891
  const errors = useSignal(errorsSig);
1220
- const touched = useSyncExternalStore(useCallback((cb) => form.subscribe(cb), [form]), () => form.getTouched(name), () => form.getTouched(name));
1221
- const dirty = useSyncExternalStore(useCallback((cb) => form.subscribe(cb), [form]), () => form.getDirty(name), () => form.getDirty(name));
1222
- const validatingSig = form.getFieldValidating(name);
1892
+ const touched = useSyncExternalStore(useCallback((cb) => f.subscribe(cb), [f]), () => f.getTouched(name), () => f.getTouched(name));
1893
+ const dirty = useSyncExternalStore(useCallback((cb) => f.subscribe(cb), [f]), () => f.getDirty(name), () => f.getDirty(name));
1894
+ const validatingSig = f.getFieldValidating(name);
1223
1895
  const validating = useSignal(validatingSig);
1224
- const setValue = useCallback((v) => form.setValue(name, v), [form, name]);
1225
- const setError = useCallback((errs) => form.setErrors(name, errs), [form, name]);
1226
- const clearErrors = useCallback(() => form.clearErrors(name), [form, name]);
1227
- const setTouched = useCallback((t) => form.setTouched(name, t), [form, name]);
1896
+ const errorMapSig = f.getErrorMap(name);
1897
+ const errorMap = useSignal(errorMapSig);
1898
+ const setValue = useCallback((v) => f.setValue(name, v), [f, name]);
1899
+ const setError = useCallback((errs) => f.setErrors(name, errs), [f, name]);
1900
+ const clearErrors = useCallback(() => f.clearErrors(name), [f, name]);
1901
+ const setTouched = useCallback((t) => f.setTouched(name, t), [f, name]);
1228
1902
  const reset = useCallback(() => {
1229
1903
  const baseline = getPath(form.getBaseline(), name);
1230
- form.setValue(name, baseline);
1231
- form.clearErrors(name);
1232
- form.setTouched(name, false);
1233
- }, [form, name]);
1904
+ f.setValue(name, baseline);
1905
+ f.clearErrors(name);
1906
+ f.setTouched(name, false);
1907
+ }, [f, form, name]);
1234
1908
  const onChange = useCallback((e) => {
1235
1909
  let newValue;
1236
1910
  if (opts?.parse) {
@@ -1244,12 +1918,12 @@ function useField(form, name, opts) {
1244
1918
  if (opts?.transform) {
1245
1919
  newValue = opts.transform(newValue);
1246
1920
  }
1247
- form.setValue(name, newValue);
1248
- }, [form, name, opts]);
1921
+ f.setValue(name, newValue);
1922
+ }, [f, name, opts]);
1249
1923
  const onBlur = useCallback(() => {
1250
- form.setTouched(name);
1251
- form.triggerBlurValidation(name);
1252
- }, [form, name]);
1924
+ f.setTouched(name);
1925
+ f.triggerBlurValidation(name);
1926
+ }, [f, name]);
1253
1927
  const refCallback = useCallback((el) => {
1254
1928
  if (form.setFieldRef) {
1255
1929
  form.setFieldRef(name, el);
@@ -1273,6 +1947,7 @@ function useField(form, name, opts) {
1273
1947
  touched,
1274
1948
  dirty,
1275
1949
  validating,
1950
+ errorMap,
1276
1951
  setValue,
1277
1952
  setError,
1278
1953
  clearErrors,
@@ -1281,14 +1956,14 @@ function useField(form, name, opts) {
1281
1956
  };
1282
1957
  }
1283
1958
  function useFieldArray(form, name) {
1284
- const idCounter = useRef(0);
1959
+ const idCounter2 = useRef(0);
1285
1960
  const idsRef = useRef([]);
1286
1961
  const arraySig = form.getSignal(name);
1287
1962
  const rawArray = useSignal(arraySig);
1288
1963
  const items = rawArray ?? [];
1289
1964
  if (idsRef.current.length < items.length) {
1290
1965
  for (let i = idsRef.current.length;i < items.length; i++) {
1291
- idsRef.current.push(`${name}-${idCounter.current++}`);
1966
+ idsRef.current.push(`${name}-${idCounter2.current++}`);
1292
1967
  }
1293
1968
  } else if (idsRef.current.length > items.length) {
1294
1969
  idsRef.current.length = items.length;
@@ -1300,12 +1975,12 @@ function useFieldArray(form, name) {
1300
1975
  })), [items, name]);
1301
1976
  const append = useCallback((value) => {
1302
1977
  const current = form.getValue(name) ?? [];
1303
- idsRef.current = [...idsRef.current, `${name}-${idCounter.current++}`];
1978
+ idsRef.current = [...idsRef.current, `${name}-${idCounter2.current++}`];
1304
1979
  form.setValue(name, [...current, value]);
1305
1980
  }, [form, name]);
1306
1981
  const prepend = useCallback((value) => {
1307
1982
  const current = form.getValue(name) ?? [];
1308
- idsRef.current = [`${name}-${idCounter.current++}`, ...idsRef.current];
1983
+ idsRef.current = [`${name}-${idCounter2.current++}`, ...idsRef.current];
1309
1984
  form.setValue(name, [value, ...current]);
1310
1985
  }, [form, name]);
1311
1986
  const insert = useCallback((index, value) => {
@@ -1313,7 +1988,7 @@ function useFieldArray(form, name) {
1313
1988
  const next = [...current];
1314
1989
  next.splice(index, 0, value);
1315
1990
  const ids = [...idsRef.current];
1316
- ids.splice(index, 0, `${name}-${idCounter.current++}`);
1991
+ ids.splice(index, 0, `${name}-${idCounter2.current++}`);
1317
1992
  idsRef.current = ids;
1318
1993
  form.setValue(name, next);
1319
1994
  }, [form, name]);
@@ -1353,7 +2028,7 @@ function useFieldArray(form, name) {
1353
2028
  form.setValue(name, next);
1354
2029
  }, [form, name]);
1355
2030
  const replaceAll = useCallback((values) => {
1356
- idsRef.current = values.map(() => `${name}-${idCounter.current++}`);
2031
+ idsRef.current = values.map(() => `${name}-${idCounter2.current++}`);
1357
2032
  form.setValue(name, values);
1358
2033
  }, [form, name]);
1359
2034
  const clone = useCallback((index) => {
@@ -1362,7 +2037,7 @@ function useFieldArray(form, name) {
1362
2037
  const cloned = deepClone(current[index]);
1363
2038
  next.splice(index + 1, 0, cloned);
1364
2039
  const ids = [...idsRef.current];
1365
- ids.splice(index + 1, 0, `${name}-${idCounter.current++}`);
2040
+ ids.splice(index + 1, 0, `${name}-${idCounter2.current++}`);
1366
2041
  idsRef.current = ids;
1367
2042
  form.setValue(name, next);
1368
2043
  }, [form, name]);
@@ -1375,225 +2050,53 @@ function useFieldArray(form, name) {
1375
2050
  swap,
1376
2051
  move,
1377
2052
  replace,
1378
- replaceAll,
1379
- clone
1380
- };
1381
- }
1382
- function useFormStatus(form) {
1383
- const isSubmitting = useSignal(form.isSubmitting);
1384
- const submitState = useSignal(form.submitState);
1385
- const isValid = useSignal(form.isValid);
1386
- const isDirty = useSignal(form.isDirty);
1387
- const submitCount = useSignal(form.submitCount);
1388
- return { isSubmitting, submitState, isValid, isDirty, submitCount };
1389
- }
1390
- // src/context.ts
1391
- import { createContext, useContext, createElement } from "react";
1392
- var FormContext = createContext(null);
1393
- function FormProvider({
1394
- form,
1395
- children
1396
- }) {
1397
- return createElement(FormContext.Provider, { value: form }, children);
1398
- }
1399
- function useFormContext() {
1400
- return useContext(FormContext);
1401
- }
1402
- // src/components.ts
1403
- import { createElement as createElement2 } from "react";
1404
-
1405
- // src/a11y.ts
1406
- var idCounter = 0;
1407
- function generateA11yId(prefix = "ereo") {
1408
- return `${prefix}-${++idCounter}`;
1409
- }
1410
- function getFieldA11y(name, state) {
1411
- const attrs = {};
1412
- if (state.errors.length > 0 && state.touched) {
1413
- attrs["aria-invalid"] = true;
1414
- attrs["aria-describedby"] = `${name}-error`;
1415
- }
1416
- return attrs;
1417
- }
1418
- function getErrorA11y(name) {
1419
- return {
1420
- id: `${name}-error`,
1421
- role: "alert",
1422
- "aria-live": "polite"
1423
- };
1424
- }
1425
- function getLabelA11y(name, opts) {
1426
- return {
1427
- htmlFor: name,
1428
- id: opts?.id ?? `${name}-label`
1429
- };
1430
- }
1431
- function getDescriptionA11y(name) {
1432
- return {
1433
- id: `${name}-description`
1434
- };
1435
- }
1436
- function getFieldsetA11y(name, _legend) {
1437
- return {
1438
- role: "group",
1439
- "aria-labelledby": `${name}-legend`
1440
- };
1441
- }
1442
- function getFieldWrapperA11y(name, state) {
1443
- const attrs = {};
1444
- attrs["data-field"] = name;
1445
- if (state.errors.length > 0 && state.touched) {
1446
- attrs["data-invalid"] = true;
1447
- }
1448
- return attrs;
1449
- }
1450
- function getFormA11y(id, opts) {
1451
- const attrs = {
1452
- id,
1453
- role: "form"
1454
- };
1455
- if (opts?.isSubmitting) {
1456
- attrs["aria-busy"] = true;
1457
- }
1458
- return attrs;
1459
- }
1460
- function getErrorSummaryA11y(formId) {
1461
- return {
1462
- role: "alert",
1463
- "aria-labelledby": `${formId}-error-summary`
1464
- };
1465
- }
1466
- function focusFirstError(form) {
1467
- if (typeof document === "undefined")
1468
- return;
1469
- const scrollBehavior = prefersReducedMotion() ? "auto" : "smooth";
1470
- const formStore = form;
1471
- if (formStore._fieldRefs) {
1472
- for (const [path, el] of formStore._fieldRefs) {
1473
- if (!el)
1474
- continue;
1475
- const errors = form.getErrors(path).get();
1476
- if (errors.length > 0) {
1477
- el.focus();
1478
- el.scrollIntoView({ behavior: scrollBehavior, block: "center" });
1479
- return;
1480
- }
1481
- }
1482
- }
1483
- const elements = document.querySelectorAll('[aria-invalid="true"]');
1484
- const first = elements[0];
1485
- if (first) {
1486
- first.focus();
1487
- first.scrollIntoView({ behavior: scrollBehavior, block: "center" });
1488
- }
1489
- }
1490
- function focusField(name) {
1491
- if (typeof document === "undefined")
1492
- return;
1493
- const scrollBehavior = prefersReducedMotion() ? "auto" : "smooth";
1494
- const el = document.querySelector(`[name="${name}"]`);
1495
- if (el) {
1496
- el.focus();
1497
- el.scrollIntoView({ behavior: scrollBehavior, block: "center" });
1498
- }
1499
- }
1500
- function trapFocus(container) {
1501
- if (typeof document === "undefined")
1502
- return () => {};
1503
- const focusableSelector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
1504
- const handleKeydown = (e) => {
1505
- if (e.key !== "Tab")
1506
- return;
1507
- const focusable = container.querySelectorAll(focusableSelector);
1508
- if (focusable.length === 0)
1509
- return;
1510
- const first = focusable[0];
1511
- const last = focusable[focusable.length - 1];
1512
- if (e.shiftKey) {
1513
- if (document.activeElement === first) {
1514
- e.preventDefault();
1515
- last.focus();
1516
- }
1517
- } else {
1518
- if (document.activeElement === last) {
1519
- e.preventDefault();
1520
- first.focus();
1521
- }
1522
- }
1523
- };
1524
- container.addEventListener("keydown", handleKeydown);
1525
- return () => container.removeEventListener("keydown", handleKeydown);
1526
- }
1527
- var liveRegion = null;
1528
- function getOrCreateLiveRegion() {
1529
- if (typeof document === "undefined") {
1530
- return { textContent: "" };
1531
- }
1532
- if (!liveRegion) {
1533
- liveRegion = document.createElement("div");
1534
- liveRegion.setAttribute("role", "status");
1535
- liveRegion.setAttribute("aria-live", "polite");
1536
- liveRegion.setAttribute("aria-atomic", "true");
1537
- liveRegion.style.position = "absolute";
1538
- liveRegion.style.width = "1px";
1539
- liveRegion.style.height = "1px";
1540
- liveRegion.style.padding = "0";
1541
- liveRegion.style.margin = "-1px";
1542
- liveRegion.style.overflow = "hidden";
1543
- liveRegion.style.clip = "rect(0, 0, 0, 0)";
1544
- liveRegion.style.whiteSpace = "nowrap";
1545
- liveRegion.style.borderWidth = "0";
1546
- document.body.appendChild(liveRegion);
1547
- }
1548
- return liveRegion;
1549
- }
1550
- function announce(message, priority = "polite") {
1551
- const region = getOrCreateLiveRegion();
1552
- region.setAttribute("aria-live", priority);
1553
- region.textContent = "";
1554
- requestAnimationFrame(() => {
1555
- region.textContent = message;
1556
- });
1557
- }
1558
- function cleanupLiveRegion() {
1559
- if (liveRegion && typeof document !== "undefined") {
1560
- liveRegion.remove();
1561
- liveRegion = null;
1562
- }
1563
- }
1564
- function announceErrors(errors, opts) {
1565
- const errorEntries = Object.entries(errors).filter(([_, msgs]) => msgs.length > 0);
1566
- if (errorEntries.length === 0)
1567
- return;
1568
- const prefix = opts?.prefix ?? "Form has errors:";
1569
- const messages = errorEntries.map(([field, msgs]) => `${field}: ${msgs[0]}`).join(". ");
1570
- announce(`${prefix} ${messages}`, "assertive");
2053
+ replaceAll,
2054
+ clone
2055
+ };
1571
2056
  }
1572
- function announceSubmitStatus(status, opts) {
1573
- switch (status) {
1574
- case "submitting":
1575
- announce(opts?.submittingMessage ?? "Submitting form...", "polite");
1576
- break;
1577
- case "success":
1578
- announce(opts?.successMessage ?? "Form submitted successfully.", "polite");
1579
- break;
1580
- case "error":
1581
- announce(opts?.errorMessage ?? "Form submission failed. Please check for errors.", "assertive");
1582
- break;
1583
- }
2057
+ function useWatch(form, pathOrPaths) {
2058
+ const f = form;
2059
+ if (typeof pathOrPaths === "string") {
2060
+ const sig = f.getSignal(pathOrPaths);
2061
+ return useSignal(sig);
2062
+ }
2063
+ const prevRef = useRef([]);
2064
+ const subscribe = useCallback((cb) => {
2065
+ const unsubs = pathOrPaths.map((p) => f.getSignal(p).subscribe(cb));
2066
+ return () => unsubs.forEach((u) => u());
2067
+ }, [f, ...pathOrPaths]);
2068
+ const getSnapshot = useCallback(() => {
2069
+ const next = pathOrPaths.map((p) => f.getValue(p));
2070
+ if (next.length === prevRef.current.length && next.every((v, i) => v === prevRef.current[i])) {
2071
+ return prevRef.current;
2072
+ }
2073
+ prevRef.current = next;
2074
+ return next;
2075
+ }, [f, ...pathOrPaths]);
2076
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
1584
2077
  }
1585
- function prefersReducedMotion() {
1586
- if (typeof window === "undefined")
1587
- return false;
1588
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2078
+ function useFormStatus(form) {
2079
+ const isSubmitting = useSignal(form.isSubmitting);
2080
+ const submitState = useSignal(form.submitState);
2081
+ const isValid = useSignal(form.isValid);
2082
+ const isDirty = useSignal(form.isDirty);
2083
+ const submitCount = useSignal(form.submitCount);
2084
+ return { isSubmitting, submitState, isValid, isDirty, submitCount };
1589
2085
  }
1590
- function isScreenReaderActive() {
1591
- if (typeof window === "undefined")
1592
- return false;
1593
- return document.querySelector('[role="application"]') !== null || window.navigator.userAgent.includes("NVDA") || window.navigator.userAgent.includes("JAWS");
2086
+ // src/context.ts
2087
+ import { createContext, useContext, createElement } from "react";
2088
+ var FormContext = createContext(null);
2089
+ function FormProvider({
2090
+ form,
2091
+ children
2092
+ }) {
2093
+ return createElement(FormContext.Provider, { value: form }, children);
2094
+ }
2095
+ function useFormContext() {
2096
+ return useContext(FormContext);
1594
2097
  }
1595
-
1596
2098
  // src/components.ts
2099
+ import { createElement as createElement2 } from "react";
1597
2100
  function inferInputType(name, explicitType) {
1598
2101
  if (explicitType)
1599
2102
  return explicitType;
@@ -1855,6 +2358,7 @@ function matches(otherField, msg) {
1855
2358
  return value === other ? undefined : msg ?? `Must match ${otherField}`;
1856
2359
  };
1857
2360
  validator._crossField = true;
2361
+ validator._dependsOnField = otherField;
1858
2362
  return validator;
1859
2363
  }
1860
2364
  function oneOf(values, msg) {
@@ -1954,219 +2458,6 @@ var v = {
1954
2458
  compose,
1955
2459
  when
1956
2460
  };
1957
- // src/schema.ts
1958
- function zodAdapter(zodSchema) {
1959
- return {
1960
- parse: (data) => zodSchema.parse(data),
1961
- safeParse: (data) => {
1962
- const result = zodSchema.safeParse(data);
1963
- if (result.success) {
1964
- return { success: true, data: result.data };
1965
- }
1966
- return {
1967
- success: false,
1968
- error: {
1969
- issues: result.error.issues.map((issue) => ({
1970
- path: issue.path,
1971
- message: issue.message
1972
- }))
1973
- }
1974
- };
1975
- }
1976
- };
1977
- }
1978
- function valibotAdapter(schema, parse, safeParse) {
1979
- return {
1980
- parse: (data) => parse(schema, data),
1981
- safeParse: (data) => {
1982
- const result = safeParse(schema, data);
1983
- if (result.success) {
1984
- return { success: true, data: result.output };
1985
- }
1986
- return {
1987
- success: false,
1988
- error: {
1989
- issues: (result.issues || []).map((issue) => ({
1990
- path: (issue.path || []).map((p) => p.key),
1991
- message: issue.message
1992
- }))
1993
- }
1994
- };
1995
- }
1996
- };
1997
- }
1998
- function createSchemaValidator(opts) {
1999
- return {
2000
- parse: (data) => {
2001
- const result = opts.validate(data);
2002
- if (result.success)
2003
- return result.data;
2004
- throw new Error("Validation failed");
2005
- },
2006
- safeParse: (data) => {
2007
- const result = opts.validate(data);
2008
- if (result.success) {
2009
- return { success: true, data: result.data };
2010
- }
2011
- return {
2012
- success: false,
2013
- error: {
2014
- issues: Object.entries(result.errors).flatMap(([path, messages]) => messages.map((message) => ({
2015
- path: path.split("."),
2016
- message
2017
- })))
2018
- }
2019
- };
2020
- }
2021
- };
2022
- }
2023
- var EREO_SCHEMA_MARKER = Symbol("ereo-schema");
2024
- function ereoSchema(definition) {
2025
- const schema = {
2026
- [EREO_SCHEMA_MARKER]: true,
2027
- parse: (data) => {
2028
- const result = schema.safeParse(data);
2029
- if (result.success)
2030
- return result.data;
2031
- const messages = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
2032
- throw new Error(`Validation failed:
2033
- ${messages.join(`
2034
- `)}`);
2035
- },
2036
- safeParse: (data) => {
2037
- const context = {
2038
- getValue: (path) => getPath(data, path),
2039
- getValues: () => data
2040
- };
2041
- const errors = validateDefinition(definition, data, "", context);
2042
- if (errors.length === 0) {
2043
- return { success: true, data };
2044
- }
2045
- return {
2046
- success: false,
2047
- error: { issues: errors }
2048
- };
2049
- }
2050
- };
2051
- return schema;
2052
- }
2053
- function validateDefinition(definition, data, basePath, context) {
2054
- const issues = [];
2055
- for (const [key, rule] of Object.entries(definition)) {
2056
- const path = basePath ? `${basePath}.${key}` : key;
2057
- const value = data?.[key];
2058
- if (typeof rule === "function") {
2059
- if (rule._isAsync)
2060
- continue;
2061
- const result = rule(value, context);
2062
- if (typeof result === "string") {
2063
- issues.push({ path: path.split("."), message: result });
2064
- }
2065
- } else if (Array.isArray(rule)) {
2066
- for (const validator of rule) {
2067
- if (typeof validator === "function") {
2068
- if (validator._isAsync)
2069
- continue;
2070
- const result = validator(value, context);
2071
- if (typeof result === "string") {
2072
- issues.push({ path: path.split("."), message: result });
2073
- break;
2074
- }
2075
- }
2076
- }
2077
- } else if (typeof rule === "object" && rule !== null) {
2078
- const nested = validateDefinition(rule, value, path, context);
2079
- issues.push(...nested);
2080
- }
2081
- }
2082
- return issues;
2083
- }
2084
- function isEreoSchema(value) {
2085
- return value !== null && typeof value === "object" && EREO_SCHEMA_MARKER in value;
2086
- }
2087
- function formDataToObject(formData, opts) {
2088
- const result = {};
2089
- const arrayFields = new Set(opts?.arrays ?? []);
2090
- for (const [key, value] of formData.entries()) {
2091
- const isArray = key.endsWith("[]") || arrayFields.has(key);
2092
- const cleanKey = key.replace(/\[\]$/, "");
2093
- const coerced = opts?.coerce !== false ? coerceValue(value) : value;
2094
- if (isArray) {
2095
- if (!result[cleanKey])
2096
- result[cleanKey] = [];
2097
- result[cleanKey].push(coerced);
2098
- } else if (cleanKey.includes(".") || cleanKey.includes("[")) {
2099
- setNestedValue(result, cleanKey, coerced);
2100
- } else {
2101
- result[cleanKey] = coerced;
2102
- }
2103
- }
2104
- return result;
2105
- }
2106
- function coerceValue(value) {
2107
- if (value instanceof File)
2108
- return value;
2109
- const str = String(value);
2110
- if (str === "true")
2111
- return true;
2112
- if (str === "false")
2113
- return false;
2114
- if (str === "null")
2115
- return null;
2116
- if (str === "")
2117
- return "";
2118
- const trimmed = str.trim();
2119
- if (trimmed !== "" && !/^0\d/.test(trimmed)) {
2120
- const num = Number(trimmed);
2121
- if (!isNaN(num))
2122
- return num;
2123
- }
2124
- if (/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/.test(str)) {
2125
- const d = new Date(str);
2126
- if (!isNaN(d.getTime()))
2127
- return d.toISOString();
2128
- }
2129
- return str;
2130
- }
2131
- function setNestedValue(obj, path, value) {
2132
- const segments = [];
2133
- let current = "";
2134
- for (let i = 0;i < path.length; i++) {
2135
- const char = path[i];
2136
- if (char === ".") {
2137
- if (current) {
2138
- segments.push(current);
2139
- current = "";
2140
- }
2141
- } else if (char === "[") {
2142
- if (current) {
2143
- segments.push(current);
2144
- current = "";
2145
- }
2146
- const close = path.indexOf("]", i);
2147
- if (close !== -1) {
2148
- const idx = path.slice(i + 1, close);
2149
- const num = parseInt(idx, 10);
2150
- segments.push(!isNaN(num) ? num : idx);
2151
- i = close;
2152
- }
2153
- } else {
2154
- current += char;
2155
- }
2156
- }
2157
- if (current)
2158
- segments.push(current);
2159
- let target = obj;
2160
- for (let i = 0;i < segments.length - 1; i++) {
2161
- const seg = segments[i];
2162
- const nextSeg = segments[i + 1];
2163
- if (target[seg] === undefined) {
2164
- target[seg] = typeof nextSeg === "number" ? [] : {};
2165
- }
2166
- target = target[seg];
2167
- }
2168
- target[segments[segments.length - 1]] = value;
2169
- }
2170
2461
  // src/action.ts
2171
2462
  import { createElement as createElement3, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect2, useState } from "react";
2172
2463
  import { batch as batch2 } from "@ereo/state";
@@ -2308,7 +2599,7 @@ function ActionForm(props) {
2308
2599
  if (result.errors) {
2309
2600
  for (const [path, errors] of Object.entries(result.errors)) {
2310
2601
  if (path) {
2311
- form.setErrors(path, errors);
2602
+ form.setErrorsWithSource(path, errors, "server");
2312
2603
  } else {
2313
2604
  form.setFormErrors(errors);
2314
2605
  }
@@ -2868,6 +3159,7 @@ export {
2868
3159
  v,
2869
3160
  useWizardContext,
2870
3161
  useWizard,
3162
+ useWatch,
2871
3163
  useFormStatus,
2872
3164
  useFormContext,
2873
3165
  useFormAction,
@@ -2876,6 +3168,7 @@ export {
2876
3168
  useField,
2877
3169
  url,
2878
3170
  trapFocus,
3171
+ standardSchemaAdapter,
2879
3172
  setPath,
2880
3173
  required,
2881
3174
  prefersReducedMotion,
@@ -2893,6 +3186,7 @@ export {
2893
3186
  maxLength,
2894
3187
  max,
2895
3188
  matches,
3189
+ isStandardSchema,
2896
3190
  isScreenReaderActive,
2897
3191
  isEreoSchema,
2898
3192
  integer,