@effect-app/vue-components 4.0.0-beta.14 → 4.0.0-beta.141

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.
Files changed (134) hide show
  1. package/README.md +24 -0
  2. package/dist/reset.css +52 -0
  3. package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +22 -7
  4. package/dist/types/components/OmegaForm/useOmegaForm.d.ts +1 -1
  5. package/dist/types/utils/index.d.ts +6 -6
  6. package/dist/vue-components.es.js +21 -45
  7. package/dist/vue-components10.es.js +5 -0
  8. package/dist/vue-components11.es.js +13 -0
  9. package/dist/vue-components12.es.js +445 -0
  10. package/dist/vue-components13.es.js +4 -0
  11. package/dist/vue-components14.es.js +38 -0
  12. package/dist/vue-components15.es.js +27 -0
  13. package/dist/vue-components16.es.js +28 -0
  14. package/dist/vue-components17.es.js +7 -0
  15. package/dist/vue-components18.es.js +18 -0
  16. package/dist/vue-components19.es.js +36 -0
  17. package/dist/vue-components2.es.js +11 -0
  18. package/dist/vue-components20.es.js +18 -0
  19. package/dist/vue-components21.es.js +21 -0
  20. package/dist/vue-components22.es.js +30 -0
  21. package/dist/vue-components23.es.js +7 -0
  22. package/dist/vue-components24.es.js +9 -0
  23. package/dist/vue-components25.es.js +38 -0
  24. package/dist/vue-components26.es.js +25 -0
  25. package/dist/vue-components27.es.js +128 -0
  26. package/dist/vue-components28.es.js +24 -0
  27. package/dist/vue-components29.es.js +21 -0
  28. package/dist/vue-components3.es.js +54 -0
  29. package/dist/vue-components30.es.js +9 -0
  30. package/dist/vue-components31.es.js +19 -0
  31. package/dist/vue-components32.es.js +5 -0
  32. package/dist/vue-components33.es.js +29 -0
  33. package/dist/vue-components34.es.js +5 -0
  34. package/dist/vue-components35.es.js +29 -0
  35. package/dist/vue-components36.es.js +6 -0
  36. package/dist/vue-components37.es.js +18 -0
  37. package/dist/vue-components38.es.js +56 -0
  38. package/dist/vue-components39.es.js +5 -0
  39. package/dist/vue-components4.es.js +5 -0
  40. package/dist/vue-components40.es.js +44 -0
  41. package/dist/vue-components41.es.js +5 -0
  42. package/dist/vue-components42.es.js +84 -0
  43. package/dist/vue-components44.es.js +8 -0
  44. package/dist/vue-components45.es.js +9 -0
  45. package/dist/vue-components46.es.js +269 -0
  46. package/dist/vue-components48.es.js +8 -0
  47. package/dist/vue-components49.es.js +80 -0
  48. package/dist/vue-components5.es.js +24 -0
  49. package/dist/vue-components50.es.js +5 -0
  50. package/dist/vue-components51.es.js +66 -0
  51. package/dist/vue-components52.es.js +5 -0
  52. package/dist/vue-components53.es.js +24 -0
  53. package/dist/vue-components54.es.js +5 -0
  54. package/dist/vue-components55.es.js +59 -0
  55. package/dist/vue-components56.es.js +5 -0
  56. package/dist/vue-components57.es.js +12 -0
  57. package/dist/vue-components58.es.js +22 -0
  58. package/dist/vue-components6.es.js +13 -0
  59. package/dist/vue-components60.es.js +9 -0
  60. package/dist/vue-components61.es.js +235 -0
  61. package/dist/vue-components62.es.js +33 -0
  62. package/dist/vue-components63.es.js +8 -0
  63. package/dist/vue-components64.es.js +36 -0
  64. package/dist/vue-components7.es.js +28 -0
  65. package/dist/vue-components8.es.js +47 -0
  66. package/dist/vue-components9.es.js +5 -0
  67. package/package.json +32 -27
  68. package/src/components/CommandButton.vue +1 -1
  69. package/src/components/OmegaForm/OmegaFormStuff.ts +265 -115
  70. package/src/components/OmegaForm/OmegaInput.vue +1 -1
  71. package/src/components/OmegaForm/OmegaInputVuetify.vue +4 -1
  72. package/src/components/OmegaForm/OmegaInternalInput.vue +19 -5
  73. package/src/components/OmegaForm/useOmegaForm.ts +30 -7
  74. package/src/reset.css +52 -0
  75. package/src/utils/index.ts +10 -7
  76. package/dist/vue-components.es10.js +0 -239
  77. package/dist/vue-components.es11.js +0 -32
  78. package/dist/vue-components.es12.js +0 -481
  79. package/dist/vue-components.es13.js +0 -49
  80. package/dist/vue-components.es14.js +0 -4
  81. package/dist/vue-components.es15.js +0 -4
  82. package/dist/vue-components.es16.js +0 -6
  83. package/dist/vue-components.es17.js +0 -13
  84. package/dist/vue-components.es18.js +0 -57
  85. package/dist/vue-components.es19.js +0 -56
  86. package/dist/vue-components.es2.js +0 -31
  87. package/dist/vue-components.es20.js +0 -8
  88. package/dist/vue-components.es21.js +0 -8
  89. package/dist/vue-components.es22.js +0 -5
  90. package/dist/vue-components.es23.js +0 -5
  91. package/dist/vue-components.es24.js +0 -4
  92. package/dist/vue-components.es25.js +0 -4
  93. package/dist/vue-components.es26.js +0 -4
  94. package/dist/vue-components.es27.js +0 -4
  95. package/dist/vue-components.es28.js +0 -19
  96. package/dist/vue-components.es29.js +0 -13
  97. package/dist/vue-components.es3.js +0 -17
  98. package/dist/vue-components.es30.js +0 -194
  99. package/dist/vue-components.es32.js +0 -31
  100. package/dist/vue-components.es33.js +0 -6
  101. package/dist/vue-components.es34.js +0 -4
  102. package/dist/vue-components.es35.js +0 -4
  103. package/dist/vue-components.es36.js +0 -113
  104. package/dist/vue-components.es38.js +0 -9
  105. package/dist/vue-components.es39.js +0 -34
  106. package/dist/vue-components.es4.js +0 -52
  107. package/dist/vue-components.es41.js +0 -6
  108. package/dist/vue-components.es42.js +0 -25
  109. package/dist/vue-components.es43.js +0 -7
  110. package/dist/vue-components.es44.js +0 -23
  111. package/dist/vue-components.es45.js +0 -32
  112. package/dist/vue-components.es46.js +0 -24
  113. package/dist/vue-components.es47.js +0 -14
  114. package/dist/vue-components.es48.js +0 -7
  115. package/dist/vue-components.es49.js +0 -21
  116. package/dist/vue-components.es5.js +0 -52
  117. package/dist/vue-components.es50.js +0 -11
  118. package/dist/vue-components.es51.js +0 -33
  119. package/dist/vue-components.es52.js +0 -50
  120. package/dist/vue-components.es53.js +0 -28
  121. package/dist/vue-components.es54.js +0 -13
  122. package/dist/vue-components.es55.js +0 -67
  123. package/dist/vue-components.es56.js +0 -58
  124. package/dist/vue-components.es57.js +0 -19
  125. package/dist/vue-components.es58.js +0 -35
  126. package/dist/vue-components.es59.js +0 -31
  127. package/dist/vue-components.es6.js +0 -69
  128. package/dist/vue-components.es60.js +0 -44
  129. package/dist/vue-components.es61.js +0 -4
  130. package/dist/vue-components.es62.js +0 -46
  131. package/dist/vue-components.es63.js +0 -4
  132. package/dist/vue-components.es7.js +0 -83
  133. package/dist/vue-components.es8.js +0 -63
  134. package/dist/vue-components.es9.js +0 -21
@@ -2,14 +2,29 @@ import { Effect, Option, type Record, S } from "effect-app"
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
  import { type DeepKeys, type DeepValue, type FieldAsyncValidateOrFn, type FieldValidateOrFn, type FormApi, type FormAsyncValidateOrFn, type FormOptions, type FormState, type FormValidateOrFn, type StandardSchemaV1, type VueFormApi } from "@tanstack/vue-form"
4
4
  import { isObject } from "@vueuse/core"
5
- import type * as Fiber from "effect/Fiber"
5
+ import type { Fiber as EffectFiber } from "effect/Fiber"
6
+ import type { Redacted } from "effect/Redacted"
6
7
  import { getTransformationFrom, useIntl } from "../../utils"
7
8
  import { type OmegaFieldInternalApi } from "./InputProps"
8
9
  import { type OF, type OmegaFormReturn } from "./useOmegaForm"
9
10
 
11
+ const legacyTagWarningEmittedFor = new Set<string>()
12
+ type GlobalThisWithOptionalProcess = typeof globalThis & {
13
+ process?: {
14
+ env?: {
15
+ NODE_ENV?: string
16
+ }
17
+ }
18
+ }
19
+
20
+ const isDevelopmentEnvironment = () => {
21
+ const process = (globalThis as GlobalThisWithOptionalProcess).process
22
+ return process?.env?.NODE_ENV !== "production"
23
+ }
24
+
10
25
  export type FieldPath<T> = unknown extends T ? string
11
26
  // technically we cannot have primitive at the root
12
- : T extends string | boolean | number | null | undefined | symbol | bigint ? ""
27
+ : T extends string | boolean | number | null | undefined | symbol | bigint | Redacted<any> ? ""
13
28
  // technically we cannot have array at the root
14
29
  : T extends ReadonlyArray<infer U> ? FieldPath_<U, `[${number}]`>
15
30
  : {
@@ -17,7 +32,7 @@ export type FieldPath<T> = unknown extends T ? string
17
32
  }[keyof T]
18
33
 
19
34
  export type FieldPath_<T, Path extends string> = unknown extends T ? string
20
- : T extends string | boolean | number | null | undefined | symbol | bigint ? Path
35
+ : T extends string | boolean | number | null | undefined | symbol | bigint | Redacted<any> ? Path
21
36
  : T extends ReadonlyArray<infer U> ? FieldPath_<U, `${Path}[${number}]`> | Path
22
37
  : {
23
38
  [K in keyof T]: FieldPath_<T[K], `${Path}.${K & string}`>
@@ -144,7 +159,7 @@ export type FormProps<From, To> =
144
159
  formApi: OmegaFormParams<From, To>
145
160
  meta: any
146
161
  value: To
147
- }) => Promise<any> | Fiber.Fiber<any, any> | Effect.Effect<unknown, any, never>
162
+ }) => Promise<any> | EffectFiber<any, any> | Effect.Effect<unknown, any, never>
148
163
  }
149
164
 
150
165
  export type OmegaFormParams<From, To> = FormApi<
@@ -222,7 +237,13 @@ export type PrefixFromDepth<
222
237
  _TDepth extends any[]
223
238
  > = K
224
239
 
225
- export type NestedKeyOf<T> = DeepKeys<T>
240
+ // Recursively replace Redacted<A> with its inner type so DeepKeys treats it as a leaf
241
+ type StripRedacted<T> = T extends Redacted<any> ? string
242
+ : T extends ReadonlyArray<infer U> ? ReadonlyArray<StripRedacted<U>>
243
+ : T extends Record<string, any> ? { [K in keyof T]: StripRedacted<T[K]> }
244
+ : T
245
+
246
+ export type NestedKeyOf<T> = DeepKeys<StripRedacted<T>>
226
247
 
227
248
  export type FieldValidators<T> = {
228
249
  onChangeAsync?: FieldAsyncValidateOrFn<T, any, any>
@@ -235,6 +256,14 @@ export type FieldValidators<T> = {
235
256
  export type BaseFieldMeta = {
236
257
  required: boolean
237
258
  nullableOrUndefined?: false | "undefined" | "null"
259
+ /**
260
+ * True when the schema property is `S.optionalKey` (AST
261
+ * `context.isOptional`) — i.e. the key should be ABSENT from the submitted
262
+ * object when empty, not present with `undefined`. Distinct from
263
+ * `required: false`, which may also mean "empty string is valid" for
264
+ * unconstrained `S.String` fields.
265
+ */
266
+ isOptionalKey?: boolean
238
267
  }
239
268
 
240
269
  export type StringFieldMeta = BaseFieldMeta & {
@@ -268,6 +297,10 @@ export type BooleanFieldMeta = BaseFieldMeta & {
268
297
  type: "boolean"
269
298
  }
270
299
 
300
+ export type DateFieldMeta = BaseFieldMeta & {
301
+ type: "date"
302
+ }
303
+
271
304
  export type UnknownFieldMeta = BaseFieldMeta & {
272
305
  type: "unknown"
273
306
  }
@@ -278,6 +311,7 @@ export type FieldMeta =
278
311
  | SelectFieldMeta
279
312
  | MultipleFieldMeta
280
313
  | BooleanFieldMeta
314
+ | DateFieldMeta
281
315
  | UnknownFieldMeta
282
316
 
283
317
  export type MetaRecord<T = string> = {
@@ -321,6 +355,17 @@ const unwrapDeclaration = (property: S.AST.AST): S.AST.AST => {
321
355
 
322
356
  const isNullishType = (property: S.AST.AST) => S.AST.isUndefined(property) || S.AST.isNull(property)
323
357
 
358
+ /**
359
+ * Unwrap a single-element Union to its inner type if it's a Literal.
360
+ * After AST.toType, S.Struct({ _tag: S.Literal("X") }) produces Union([Literal("X")])
361
+ * instead of bare Literal("X") like S.TaggedStruct does.
362
+ * TODO: remove after manual _tag deprecation
363
+ */
364
+ const unwrapSingleLiteralUnion = (ast: S.AST.AST): S.AST.AST =>
365
+ S.AST.isUnion(ast) && ast.types.length === 1 && S.AST.isLiteral(ast.types[0]!)
366
+ ? ast.types[0]!
367
+ : ast
368
+
324
369
  const getNullableOrUndefined = (property: S.AST.AST) =>
325
370
  S.AST.isUnion(property)
326
371
  ? property.types.find((_) => isNullishType(_))
@@ -360,13 +405,8 @@ const getJsonSchemaAnnotation = (property: S.AST.AST): Record<string, unknown> =
360
405
  return jsonSchema && typeof jsonSchema === "object" ? jsonSchema as Record<string, unknown> : {}
361
406
  }
362
407
 
363
- const getDefaultFromAst = (property: S.AST.AST) => {
364
- const link = property.context?.defaultValue?.[0] as any
365
-
366
- if (!link?.transformation?.decode?.run) {
367
- return undefined
368
- }
369
-
408
+ const extractDefaultFromLink = (link: any): unknown | undefined => {
409
+ if (!link?.transformation?.decode?.run) return undefined
370
410
  try {
371
411
  const result = Effect.runSync(link.transformation.decode.run(Option.none())) as Option.Option<unknown>
372
412
  return Option.isSome(result) ? result.value : undefined
@@ -375,6 +415,21 @@ const getDefaultFromAst = (property: S.AST.AST) => {
375
415
  }
376
416
  }
377
417
 
418
+ const getDefaultFromAst = (property: S.AST.AST) => {
419
+ // 1. Check withConstructorDefault (stored in context.defaultValue)
420
+ const constructorLink = property.context?.defaultValue?.[0]
421
+ const constructorDefault = extractDefaultFromLink(constructorLink)
422
+ if (constructorDefault !== undefined) return constructorDefault
423
+
424
+ // 2. Check withDecodingDefault (stored in encoding)
425
+ const encodingLink = property.encoding?.[0]
426
+ if (encodingLink && property.context?.isOptional) {
427
+ return extractDefaultFromLink(encodingLink)
428
+ }
429
+
430
+ return undefined
431
+ }
432
+
378
433
  const getCheckMetas = (property: S.AST.AST): Array<Record<string, any>> => {
379
434
  const checks = property.checks ?? []
380
435
 
@@ -426,6 +481,10 @@ const getFieldMetadataFromAst = (property: S.AST.AST) => {
426
481
  case "isLessThanOrEqualTo":
427
482
  base.maximum = check.maximum
428
483
  break
484
+ case "isBetween":
485
+ base.minimum = check.minimum
486
+ base.maximum = check.maximum
487
+ break
429
488
  case "isGreaterThan":
430
489
  base.exclusiveMinimum = check.exclusiveMinimum
431
490
  break
@@ -436,6 +495,11 @@ const getFieldMetadataFromAst = (property: S.AST.AST) => {
436
495
  }
437
496
  } else if (S.AST.isBoolean(property)) {
438
497
  base.type = "boolean"
498
+ } else if (
499
+ S.AST.isDeclaration(property)
500
+ && (property.annotations as any)?.typeConstructor?._tag === "Date"
501
+ ) {
502
+ base.type = "date"
439
503
  } else {
440
504
  base.type = "unknown"
441
505
  }
@@ -463,8 +527,11 @@ export const createMeta = <T = any>(
463
527
  const key = parent ? `${parent}.${p.name.toString()}` : p.name.toString()
464
528
  const nullableOrUndefined = isNullableOrUndefined(p.type)
465
529
 
530
+ const isOptionalKey = (p.type as any).context?.isOptional === true
531
+
466
532
  // Determine if this field should be required:
467
533
  // - For nullable discriminated unions, only _tag should be non-required
534
+ // - optionalKey fields are not required
468
535
  // - All other fields should calculate their required status normally
469
536
  let isRequired: boolean
470
537
  if (meta._isNullableDiscriminatedUnion && p.name.toString() === "_tag") {
@@ -473,6 +540,8 @@ export const createMeta = <T = any>(
473
540
  } else if (meta.required === false) {
474
541
  // Explicitly set to non-required (legacy behavior for backwards compatibility)
475
542
  isRequired = false
543
+ } else if (isOptionalKey) {
544
+ isRequired = false
476
545
  } else {
477
546
  // Calculate from the property itself
478
547
  isRequired = !nullableOrUndefined
@@ -503,14 +572,28 @@ export const createMeta = <T = any>(
503
572
  // - All other fields maintain their normal required status based on their own types
504
573
  const isNullableDiscriminatedUnion = nullableOrUndefined && nonNullTypes.length > 1
505
574
 
506
- Object.assign(
507
- acc,
508
- createMeta<T>({
509
- parent: key,
510
- propertySignatures: nonNullType.propertySignatures,
511
- meta: isNullableDiscriminatedUnion ? { _isNullableDiscriminatedUnion: true } : {}
512
- })
513
- )
575
+ const branchMeta = createMeta<T>({
576
+ parent: key,
577
+ propertySignatures: nonNullType.propertySignatures,
578
+ meta: isNullableDiscriminatedUnion ? { _isNullableDiscriminatedUnion: true } : {}
579
+ })
580
+
581
+ // Merge branch metadata, combining select members for shared discriminator fields
582
+ for (const [metaKey, metaValue] of Object.entries(branchMeta)) {
583
+ const existing = acc[metaKey as NestedKeyOf<T>] as FieldMeta | undefined
584
+ if (
585
+ existing && existing.type === "select" && (metaValue as any)?.type === "select"
586
+ ) {
587
+ existing.members = [
588
+ ...existing.members,
589
+ ...(metaValue as SelectFieldMeta).members.filter(
590
+ (m: any) => !existing.members.includes(m)
591
+ )
592
+ ]
593
+ } else {
594
+ acc[metaKey as NestedKeyOf<T>] = metaValue as FieldMeta
595
+ }
596
+ }
514
597
  }
515
598
  }
516
599
  } else {
@@ -667,8 +750,9 @@ export const createMeta = <T = any>(
667
750
  // an empty string is valid for a S.String field, so we should not mark it as required
668
751
  // TODO: handle this better via the createMeta minLength parsing
669
752
  required: isRequired
670
- && (!S.AST.isString(typeToProcess) || !!getFieldMetadataFromAst(p.type).minLength),
671
- nullableOrUndefined
753
+ && (!S.AST.isString(typeToProcess) || !!getFieldMetadataFromAst(typeToProcess).minLength),
754
+ nullableOrUndefined,
755
+ ...(isOptionalKey ? { isOptionalKey: true } : {})
672
756
  }
673
757
  })
674
758
 
@@ -689,7 +773,20 @@ export const createMeta = <T = any>(
689
773
 
690
774
  if (S.AST.isUnion(property)) {
691
775
  const unwrappedTypes = unwrapNestedUnions(property.types).map(unwrapDeclaration)
692
- const nonNullType = unwrappedTypes.find((t) => !isNullishType(t))!
776
+ const nonNullTypes = unwrappedTypes.filter((t) => !isNullishType(t))
777
+
778
+ // Unwrap single-element unions when the literal is a boolean
779
+ // (effect-app's S.Literal wraps as S.Literals([x]) → Union([Literal(x)]))
780
+ // Don't unwrap string/number literals — they may be discriminator values in a union
781
+ if (
782
+ nonNullTypes.length === 1
783
+ && S.AST.isLiteral(nonNullTypes[0]!)
784
+ && typeof nonNullTypes[0]!.literal === "boolean"
785
+ ) {
786
+ return createMeta<T>({ parent, meta, property: nonNullTypes[0]! })
787
+ }
788
+
789
+ const nonNullType = nonNullTypes[0]!
693
790
 
694
791
  if (S.AST.isObjects(nonNullType)) {
695
792
  return createMeta<T>({
@@ -699,11 +796,13 @@ export const createMeta = <T = any>(
699
796
  })
700
797
  }
701
798
 
702
- if (unwrappedTypes.every((_) => isNullishType(_) || S.AST.isLiteral(_))) {
799
+ // TODO: remove after manual _tag deprecation — unwrap legacy S.Struct({ _tag: S.Literal("X") }) pattern
800
+ const resolvedTypes = unwrappedTypes.map(unwrapSingleLiteralUnion)
801
+ if (resolvedTypes.every((_) => isNullishType(_) || S.AST.isLiteral(_))) {
703
802
  return {
704
803
  ...meta,
705
804
  type: "select",
706
- members: unwrappedTypes.filter(S.AST.isLiteral).map((t) => t.literal)
805
+ members: resolvedTypes.filter(S.AST.isLiteral).map((t) => t.literal)
707
806
  } as FieldMeta
708
807
  }
709
808
 
@@ -726,6 +825,14 @@ export const createMeta = <T = any>(
726
825
  } as FieldMeta
727
826
  }
728
827
 
828
+ if (S.AST.isLiteral(property)) {
829
+ return {
830
+ ...meta,
831
+ type: "select",
832
+ members: [property.literal]
833
+ } as FieldMeta
834
+ }
835
+
729
836
  meta = { ...getJsonSchemaAnnotation(property), ...getFieldMetadataFromAst(property), ...meta }
730
837
 
731
838
  return meta as FieldMeta
@@ -781,9 +888,25 @@ const metadataFromAst = <From, To>(
781
888
  )
782
889
 
783
890
  let tagValue: string | null = null
784
- if (tagProp && S.AST.isLiteral(tagProp.type)) {
785
- tagValue = tagProp.type.literal as string
891
+ // TODO: remove after manual _tag deprecation — unwrap legacy S.Struct({ _tag: S.Literal("X") }) pattern
892
+ const resolvedTagType = tagProp ? unwrapSingleLiteralUnion(tagProp.type) : null
893
+ if (resolvedTagType && S.AST.isLiteral(resolvedTagType)) {
894
+ tagValue = resolvedTagType.literal as string
786
895
  discriminatorValues.push(tagValue)
896
+ // Warn if the tag was wrapped in a single-element Union (legacy pattern)
897
+ if (
898
+ tagProp
899
+ && S.AST.isUnion(tagProp.type)
900
+ && isDevelopmentEnvironment()
901
+ && tagValue != null
902
+ && !legacyTagWarningEmittedFor.has(tagValue)
903
+ ) {
904
+ legacyTagWarningEmittedFor.add(tagValue)
905
+ console.warn(
906
+ `[OmegaForm] Union member with _tag "${tagValue}" uses S.Struct({ _tag: S.Literal("${tagValue}"), ... }). `
907
+ + `Please migrate to S.TaggedStruct("${tagValue}", { ... }) for cleaner AST handling.`
908
+ )
909
+ }
787
910
  }
788
911
 
789
912
  // Create metadata for this member's properties
@@ -843,6 +966,70 @@ const metadataFromAst = <From, To>(
843
966
  return { meta: newMeta, defaultValues, unionMeta }
844
967
  }
845
968
 
969
+ /*
970
+ * Checks if an AST node is a S.Redacted Declaration without encoding.
971
+ * These need to be swapped to S.RedactedFromValue for form usage
972
+ * because S.Redacted expects Redacted objects, not plain strings.
973
+ */
974
+ const isRedactedWithoutEncoding = (ast: S.AST.AST): boolean =>
975
+ S.AST.isDeclaration(ast)
976
+ && (ast.annotations as any)?.typeConstructor?._tag === "effect/Redacted"
977
+ && !ast.encoding
978
+
979
+ /*
980
+ * Creates a form-compatible schema by replacing S.Redacted(X) with
981
+ * S.RedactedFromValue(X). S.Redacted is a Declaration that expects
982
+ * Redacted<A> on both encoded and type sides, so form inputs (which
983
+ * produce plain strings) fail validation. S.RedactedFromValue accepts
984
+ * plain values on the encoded side and wraps them in Redacted on decode.
985
+ */
986
+ export const toFormSchema = <From, To>(
987
+ schema: S.Codec<To, From, never>
988
+ ): S.Codec<To, From, never> => {
989
+ const ast = schema.ast
990
+ const objAst = S.AST.isObjects(ast)
991
+ ? ast
992
+ : S.AST.isDeclaration(ast)
993
+ ? S.AST.toEncoded(ast)
994
+ : null
995
+
996
+ if (!objAst || !("propertySignatures" in objAst)) return schema
997
+
998
+ let hasRedacted = false
999
+ const props: Record<string, S.Struct.Fields[string]> = {}
1000
+
1001
+ for (const p of objAst.propertySignatures) {
1002
+ if (isRedactedWithoutEncoding(p.type)) {
1003
+ hasRedacted = true
1004
+ const innerSchema = S.make((p.type as S.AST.Declaration).typeParameters[0]!)
1005
+ props[p.name as string] = S.RedactedFromValue(innerSchema)
1006
+ } else if (S.AST.isUnion(p.type)) {
1007
+ const types = p.type.types
1008
+ const redactedType = types.find(isRedactedWithoutEncoding)
1009
+ if (redactedType) {
1010
+ hasRedacted = true
1011
+ const innerSchema = S.make((redactedType as S.AST.Declaration).typeParameters[0]!)
1012
+ const hasNull = types.some(S.AST.isNull)
1013
+ const hasUndefined = types.some(S.AST.isUndefined)
1014
+ const base = S.RedactedFromValue(innerSchema)
1015
+ props[p.name as string] = hasNull && hasUndefined
1016
+ ? S.NullishOr(base)
1017
+ : hasNull
1018
+ ? S.NullOr(base)
1019
+ : hasUndefined
1020
+ ? S.UndefinedOr(base)
1021
+ : base
1022
+ } else {
1023
+ props[p.name as string] = S.make(p.type)
1024
+ }
1025
+ } else {
1026
+ props[p.name as string] = S.make(p.type)
1027
+ }
1028
+ }
1029
+
1030
+ return hasRedacted ? S.Struct(props) as unknown as S.Codec<To, From, never> : schema
1031
+ }
1032
+
846
1033
  export const duplicateSchema = <From, To>(
847
1034
  schema: S.Codec<To, From, never>
848
1035
  ) => {
@@ -912,7 +1099,7 @@ export const generateInputStandardSchemaFromFieldMeta = (
912
1099
  message: trans("validation.integer.expected", { actualValue: "NaN" })
913
1100
  }))
914
1101
  } else {
915
- schema = S.Number.annotate({
1102
+ schema = S.Finite.annotate({
916
1103
  message: trans("validation.number.expected", { actualValue: "NaN" })
917
1104
  })
918
1105
 
@@ -979,6 +1166,10 @@ export const generateInputStandardSchemaFromFieldMeta = (
979
1166
  schema = S.Boolean
980
1167
  break
981
1168
 
1169
+ case "date":
1170
+ schema = S.Date
1171
+ break
1172
+
982
1173
  case "unknown":
983
1174
  schema = S.Unknown
984
1175
  break
@@ -996,11 +1187,6 @@ export const generateInputStandardSchemaFromFieldMeta = (
996
1187
  return result
997
1188
  }
998
1189
 
999
- export const nullableInput = <A, I, R>(
1000
- schema: S.Codec<A, I, R>,
1001
- _defaultValue: () => A
1002
- ) => S.NullOr(schema) as any
1003
-
1004
1190
  export type OmegaAutoGenMeta<
1005
1191
  From extends Record<PropertyKey, any>,
1006
1192
  To extends Record<PropertyKey, any>,
@@ -1043,20 +1229,10 @@ export function deepMerge(target: any, source: any) {
1043
1229
  return result
1044
1230
  }
1045
1231
 
1046
- // Type definitions for schemas with fields and members
1047
- type SchemaWithFields = {
1048
- fields: Record<string, S.Schema<any>>
1049
- }
1050
-
1051
1232
  type SchemaWithMembers = {
1052
1233
  members: readonly S.Schema<any>[]
1053
1234
  }
1054
1235
 
1055
- // Type guards to check schema types
1056
- function hasFields(schema: any): schema is SchemaWithFields {
1057
- return schema && "fields" in schema && typeof schema.fields === "object"
1058
- }
1059
-
1060
1236
  function hasMembers(schema: any): schema is SchemaWithMembers {
1061
1237
  return schema && "members" in schema && Array.isArray(schema.members)
1062
1238
  }
@@ -1080,99 +1256,73 @@ export const defaultsValueFromSchema = (
1080
1256
  return undefined
1081
1257
  }
1082
1258
 
1083
- // Check if schema has fields directly
1084
- if (hasFields(schema)) {
1085
- // Process fields and extract default values
1259
+ // Handle structs via AST (covers plain structs, transformed schemas like decodeTo, ExtendedClass, etc.)
1260
+ const objectsAst = S.AST.isObjects(ast)
1261
+ ? ast
1262
+ : S.AST.isDeclaration(ast)
1263
+ ? unwrapDeclaration(ast)
1264
+ : undefined
1265
+ if (objectsAst && S.AST.isObjects(objectsAst)) {
1086
1266
  const result: Record<string, any> = {}
1087
1267
 
1088
- for (const [key, fieldSchema] of Object.entries(schema.fields)) {
1089
- // Check if this field has a defaultValue in its AST
1090
- const fieldAst = (fieldSchema as any)?.ast
1091
- if (fieldAst?.defaultValue) {
1092
- try {
1093
- result[key] = fieldAst.defaultValue()
1094
- continue
1095
- } catch {
1096
- // If defaultValue() throws, fall through to recursive processing
1097
- }
1268
+ for (const prop of objectsAst.propertySignatures) {
1269
+ const key = prop.name.toString()
1270
+ const propType = prop.type
1271
+
1272
+ const propDefault = getDefaultFromAst(propType)
1273
+ if (propDefault !== undefined) {
1274
+ result[key] = propDefault
1275
+ continue
1098
1276
  }
1099
1277
 
1100
- // Recursively process the field
1101
- const fieldValue = defaultsValueFromSchema(fieldSchema as any, record[key] || {})
1102
- if (fieldValue !== undefined) {
1103
- result[key] = fieldValue
1278
+ const propSchema = S.make(propType)
1279
+ const propValue = defaultsValueFromSchema(propSchema, record[key] || {})
1280
+
1281
+ if (propValue !== undefined) {
1282
+ result[key] = propValue
1283
+ } else if (isNullableOrUndefined(propType) === "undefined") {
1284
+ result[key] = undefined
1104
1285
  }
1105
1286
  }
1106
1287
 
1107
1288
  return { ...result, ...record }
1108
1289
  }
1109
1290
 
1110
- // Check if schema has fields in from (for ExtendedClass and similar transformations)
1111
- if ((schema as any)?.from && hasFields((schema as any).from)) {
1112
- return defaultsValueFromSchema((schema as any).from, record)
1113
- }
1291
+ // Handle unions via AST or schema-level .members
1292
+ const unionTypes = S.AST.isUnion(ast)
1293
+ ? ast.types
1294
+ : hasMembers(schema)
1295
+ ? schema.members.map((m) => m.ast)
1296
+ : undefined
1297
+ if (unionTypes) {
1298
+ const mergedFields: Record<string, { ast: S.AST.AST }> = {}
1114
1299
 
1115
- if (hasMembers(schema)) {
1116
- // Merge all member fields, giving precedence to fields with default values
1117
- const mergedMembers = schema.members.reduce((acc, member) => {
1118
- if (hasFields(member)) {
1119
- // Check each field and give precedence to ones with default values
1120
- Object.entries(member.fields).forEach(([key, fieldSchema]) => {
1121
- const fieldAst: any = fieldSchema.ast
1122
- const existingFieldAst: any = acc[key]?.ast
1123
-
1124
- // If field doesn't exist yet, or new field has default and existing doesn't, use new field
1125
- if (!acc[key] || (fieldAst?.defaultValue && !existingFieldAst?.defaultValue)) {
1126
- acc[key] = fieldSchema
1127
- }
1128
- // If both have defaults or neither have defaults, keep the first one (existing)
1129
- })
1130
- return acc
1300
+ for (const memberAstRaw of unionTypes) {
1301
+ const memberAst = unwrapDeclaration(memberAstRaw)
1302
+ if (!S.AST.isObjects(memberAst)) continue
1303
+
1304
+ for (const prop of memberAst.propertySignatures) {
1305
+ const key = prop.name.toString()
1306
+ const fieldDefault = getDefaultFromAst(prop.type)
1307
+ const existingDefault = mergedFields[key] ? getDefaultFromAst(mergedFields[key]!.ast) : undefined
1308
+
1309
+ if (!mergedFields[key] || (fieldDefault !== undefined && existingDefault === undefined)) {
1310
+ mergedFields[key] = { ast: prop.type }
1311
+ }
1131
1312
  }
1132
- return acc
1133
- }, {} as Record<string, any>)
1313
+ }
1134
1314
 
1135
- if (Object.keys(mergedMembers).length === 0) {
1315
+ if (Object.keys(mergedFields).length === 0) {
1136
1316
  return Object.keys(record).length > 0 ? record : undefined
1137
1317
  }
1138
1318
 
1139
- // Use reduce to properly accumulate the merged fields
1140
- return Object.entries(mergedMembers).reduce((acc, [key, value]) => {
1141
- acc[key] = defaultsValueFromSchema(value, record[key] || {})
1319
+ return Object.entries(mergedFields).reduce((acc, [key, { ast: propAst }]) => {
1320
+ acc[key] = defaultsValueFromSchema(S.make(propAst), record[key] || {})
1142
1321
  return acc
1143
1322
  }, record)
1144
1323
  }
1145
1324
 
1146
1325
  if (Object.keys(record).length === 0) {
1147
- if (S.AST.isObjects(ast)) {
1148
- // Process TypeLiteral fields directly to build the result object
1149
- const result: Record<string, any> = { ...record }
1150
-
1151
- for (const prop of ast.propertySignatures) {
1152
- const key = prop.name.toString()
1153
- const propType = prop.type
1154
-
1155
- const propDefault = getDefaultFromAst(propType)
1156
- if (propDefault !== undefined) {
1157
- result[key] = propDefault
1158
- continue
1159
- }
1160
-
1161
- // Create a schema from the property type and get its defaults
1162
- const propSchema = S.make(propType)
1163
-
1164
- // Recursively process the property - don't pas for prop processing
1165
- // to allow proper unwrapping of nested structures
1166
- const propValue = defaultsValueFromSchema(propSchema, record[key] || {})
1167
-
1168
- if (propValue !== undefined) {
1169
- result[key] = propValue
1170
- }
1171
- }
1172
-
1173
- return result
1174
- }
1175
-
1176
1326
  if (S.AST.isString(ast)) {
1177
1327
  return ""
1178
1328
  }
@@ -4,7 +4,7 @@
4
4
  :key="fieldKey"
5
5
  :name="name"
6
6
  :validators="{
7
- onChange: schema,
7
+ onBlur: schema,
8
8
  ...validators
9
9
  }"
10
10
  >
@@ -1,7 +1,10 @@
1
1
  <template>
2
2
  <div
3
3
  class="omega-input"
4
- @focusout="$emit('blur', $event)"
4
+ @focusout="(e) => {
5
+ $emit('blur', e)
6
+ field.handleBlur()
7
+ }"
5
8
  @focusin="$emit('focus', $event)"
6
9
  >
7
10
  <component
@@ -119,6 +119,7 @@ const isFalsyButNotZero = (value: unknown): boolean => {
119
119
  // we remove value and errors when the field is empty and not required
120
120
  // convert nullish value to null or undefined based on schema
121
121
  const handleChange: OmegaFieldInternalApi<From, Name>["handleChange"] = (value) => {
122
+ let fieldDeleted = false
122
123
  if (isFalsyButNotZero(value) && props.meta?.type !== "boolean") {
123
124
  // Only convert to null/undefined if the field is actually nullable or optional
124
125
  if (props.meta?.nullableOrUndefined) {
@@ -128,6 +129,14 @@ const handleChange: OmegaFieldInternalApi<From, Name>["handleChange"] = (value)
128
129
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
130
  : null as any
130
131
  )
132
+ } else if (props.meta?.isOptionalKey) {
133
+ // `S.optionalKey` expects the key to be ABSENT from the submitted
134
+ // object, not present-with-undefined. Remove it from form state
135
+ // rather than setting it to `undefined`. Note: this is distinct
136
+ // from `required: false`, which may also just mean "empty string
137
+ // is valid" for unconstrained `S.String` fields.
138
+ props.field.form.deleteField(props.field.name)
139
+ fieldDeleted = true
131
140
  } else {
132
141
  // Keep the actual value (e.g., empty string for S.String fields)
133
142
  props.field.handleChange(value)
@@ -138,7 +147,10 @@ const handleChange: OmegaFieldInternalApi<From, Name>["handleChange"] = (value)
138
147
 
139
148
  // whenever we change the field, regardless if we set it to null, we should reset onSubmit.
140
149
  // not sure why this is not the case in tanstack form.
141
- props.field.setMeta((m) => ({ ...m, errorMap: { ...m.errorMap, onSubmit: undefined } }))
150
+ // Skip when the field was deleted its meta no longer exists in the form store.
151
+ if (!fieldDeleted) {
152
+ props.field.setMeta((m) => ({ ...m, errorMap: { ...m.errorMap, onSubmit: undefined } }))
153
+ }
142
154
  }
143
155
 
144
156
  // Note: Default value normalization (converting empty strings to null/undefined for nullable fields)
@@ -165,11 +177,13 @@ const inputProps: ComputedRef<InputProps<From, Name>> = computed(() => ({
165
177
  minLength: props.meta?.type === "string" && props.meta?.minLength,
166
178
  maxLength: props.meta?.type === "string" && props.meta?.maxLength,
167
179
  max: (props.meta?.type === "number")
168
- && (props.meta?.maximum
169
- ?? (typeof props.meta?.exclusiveMaximum === "number" && props.meta.exclusiveMaximum - 1)),
180
+ ? (props.meta?.maximum
181
+ ?? (typeof props.meta?.exclusiveMaximum === "number" ? props.meta.exclusiveMaximum - 1 : undefined))
182
+ : undefined,
170
183
  min: (props.meta?.type === "number")
171
- && (props.meta?.minimum
172
- ?? (typeof props.meta?.exclusiveMinimum === "number" && props.meta.exclusiveMinimum + 1)),
184
+ ? (props.meta?.minimum
185
+ ?? (typeof props.meta?.exclusiveMinimum === "number" ? props.meta.exclusiveMinimum + 1 : undefined))
186
+ : undefined,
173
187
  errorMessages: errors.value,
174
188
  error: !!errors.value.length,
175
189
  type: fieldType.value,