@effect-app/vue-components 4.0.0-beta.32 → 4.0.0-beta.321

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 (211) hide show
  1. package/README.md +13 -9
  2. package/dist/reset.css +39 -38
  3. package/dist/types/components/CommandButton.vue.d.ts +24 -5
  4. package/dist/types/components/Dialog.vue.d.ts +4 -2
  5. package/dist/types/components/FixedNuxtErrorBoundary.vue.d.ts +39 -0
  6. package/dist/types/components/OmegaForm/InputProps.d.ts +2 -1
  7. package/dist/types/components/OmegaForm/OmegaArray.vue.d.ts +1 -1
  8. package/dist/types/components/OmegaForm/OmegaAutoGen.vue.d.ts +2 -2
  9. package/dist/types/components/OmegaForm/OmegaErrorsInternal.vue.d.ts +2 -3
  10. package/dist/types/components/OmegaForm/OmegaFormInput.vue.d.ts +1 -1
  11. package/dist/types/components/OmegaForm/OmegaInput.vue.d.ts +1 -1
  12. package/dist/types/components/OmegaForm/OmegaInputVuetify.vue.d.ts +12 -0
  13. package/dist/types/components/OmegaForm/OmegaInternalInput.vue.d.ts +7 -9
  14. package/dist/types/components/OmegaForm/OmegaTaggedUnion.vue.d.ts +2 -2
  15. package/dist/types/components/OmegaForm/OmegaTaggedUnionInternal.vue.d.ts +3 -3
  16. package/dist/types/components/OmegaForm/OmegaWrapper.vue.d.ts +1 -1
  17. package/dist/types/components/OmegaForm/createUseFormWithCustomInput.d.ts +4 -2
  18. package/dist/types/components/OmegaForm/decimalSeparatorInput.d.ts +13 -0
  19. package/dist/types/components/OmegaForm/errors.d.ts +33 -0
  20. package/dist/types/components/OmegaForm/getOmegaStore.d.ts +1 -1
  21. package/dist/types/components/OmegaForm/hocs.d.ts +3 -0
  22. package/dist/types/components/OmegaForm/index.d.ts +14 -3
  23. package/dist/types/components/OmegaForm/inputs.d.ts +4 -0
  24. package/dist/types/components/OmegaForm/meta/checks.d.ts +4 -0
  25. package/dist/types/components/OmegaForm/meta/createMeta.d.ts +33 -0
  26. package/dist/types/components/OmegaForm/meta/defaults.d.ts +21 -0
  27. package/dist/types/components/OmegaForm/meta/redacted.d.ts +2 -0
  28. package/dist/types/components/OmegaForm/meta/types.d.ts +56 -0
  29. package/dist/types/components/OmegaForm/meta/walker.d.ts +18 -0
  30. package/dist/types/components/OmegaForm/persistency.d.ts +58 -0
  31. package/dist/types/components/OmegaForm/submit.d.ts +60 -0
  32. package/dist/types/components/OmegaForm/types.d.ts +312 -0
  33. package/dist/types/components/OmegaForm/useOmegaForm.d.ts +9 -213
  34. package/dist/types/components/OmegaForm/validation/localized.d.ts +10 -0
  35. package/dist/types/components/index.d.ts +1 -0
  36. package/dist/types/index.d.ts +0 -1
  37. package/dist/types/utils/index.d.ts +8 -8
  38. package/dist/vue-components.es.js +30 -44
  39. package/dist/vue-components10.es.js +5 -0
  40. package/dist/vue-components11.es.js +56 -0
  41. package/dist/vue-components12.es.js +5 -0
  42. package/dist/vue-components13.es.js +20 -0
  43. package/dist/vue-components14.es.js +50 -0
  44. package/dist/vue-components15.es.js +128 -0
  45. package/dist/vue-components16.es.js +65 -0
  46. package/dist/vue-components17.es.js +114 -0
  47. package/dist/vue-components18.es.js +26 -0
  48. package/dist/vue-components19.es.js +5 -0
  49. package/dist/vue-components2.es.js +11 -0
  50. package/dist/vue-components20.es.js +80 -0
  51. package/dist/vue-components21.es.js +115 -0
  52. package/dist/vue-components22.es.js +74 -0
  53. package/dist/vue-components23.es.js +12 -0
  54. package/dist/vue-components24.es.js +54 -0
  55. package/dist/vue-components25.es.js +5 -0
  56. package/dist/vue-components26.es.js +44 -0
  57. package/dist/vue-components27.es.js +5 -0
  58. package/dist/vue-components28.es.js +84 -0
  59. package/dist/vue-components3.es.js +98 -0
  60. package/dist/vue-components30.es.js +8 -0
  61. package/dist/vue-components31.es.js +9 -0
  62. package/dist/vue-components32.es.js +15 -0
  63. package/dist/vue-components33.es.js +24 -0
  64. package/dist/vue-components34.es.js +344 -0
  65. package/dist/vue-components36.es.js +8 -0
  66. package/dist/vue-components37.es.js +74 -0
  67. package/dist/vue-components38.es.js +5 -0
  68. package/dist/vue-components39.es.js +64 -0
  69. package/dist/vue-components4.es.js +5 -0
  70. package/dist/vue-components40.es.js +5 -0
  71. package/dist/vue-components41.es.js +24 -0
  72. package/dist/vue-components42.es.js +5 -0
  73. package/dist/vue-components43.es.js +59 -0
  74. package/dist/vue-components44.es.js +5 -0
  75. package/dist/vue-components45.es.js +12 -0
  76. package/dist/vue-components46.es.js +22 -0
  77. package/dist/vue-components48.es.js +9 -0
  78. package/dist/vue-components49.es.js +4 -0
  79. package/dist/vue-components5.es.js +24 -0
  80. package/dist/vue-components50.es.js +38 -0
  81. package/dist/vue-components51.es.js +27 -0
  82. package/dist/vue-components52.es.js +28 -0
  83. package/dist/vue-components53.es.js +7 -0
  84. package/dist/vue-components54.es.js +18 -0
  85. package/dist/vue-components55.es.js +36 -0
  86. package/dist/vue-components56.es.js +18 -0
  87. package/dist/vue-components57.es.js +21 -0
  88. package/dist/vue-components58.es.js +30 -0
  89. package/dist/vue-components59.es.js +7 -0
  90. package/dist/vue-components6.es.js +13 -0
  91. package/dist/vue-components60.es.js +9 -0
  92. package/dist/vue-components61.es.js +38 -0
  93. package/dist/vue-components62.es.js +25 -0
  94. package/dist/vue-components63.es.js +128 -0
  95. package/dist/vue-components64.es.js +24 -0
  96. package/dist/vue-components65.es.js +21 -0
  97. package/dist/vue-components66.es.js +9 -0
  98. package/dist/vue-components67.es.js +19 -0
  99. package/dist/vue-components68.es.js +5 -0
  100. package/dist/vue-components69.es.js +29 -0
  101. package/dist/vue-components7.es.js +13 -0
  102. package/dist/vue-components70.es.js +5 -0
  103. package/dist/vue-components71.es.js +43 -0
  104. package/dist/vue-components72.es.js +101 -0
  105. package/dist/vue-components73.es.js +36 -0
  106. package/dist/vue-components74.es.js +19 -0
  107. package/dist/vue-components75.es.js +50 -0
  108. package/dist/vue-components8.es.js +35 -0
  109. package/dist/vue-components9.es.js +47 -0
  110. package/package.json +39 -33
  111. package/src/components/CommandButton.vue +96 -16
  112. package/src/components/Dialog.vue +0 -1
  113. package/src/components/FixedNuxtErrorBoundary.vue +112 -0
  114. package/src/components/OmegaForm/InputProps.ts +2 -1
  115. package/src/components/OmegaForm/OmegaArray.vue +9 -23
  116. package/src/components/OmegaForm/OmegaAutoGen.vue +3 -2
  117. package/src/components/OmegaForm/OmegaErrorsInternal.vue +20 -17
  118. package/src/components/OmegaForm/OmegaFormInput.vue +1 -1
  119. package/src/components/OmegaForm/OmegaInput.vue +29 -43
  120. package/src/components/OmegaForm/OmegaInputVuetify.vue +122 -16
  121. package/src/components/OmegaForm/OmegaInternalInput.vue +33 -13
  122. package/src/components/OmegaForm/OmegaTaggedUnion.vue +10 -3
  123. package/src/components/OmegaForm/OmegaTaggedUnionInternal.vue +6 -6
  124. package/src/components/OmegaForm/OmegaWrapper.vue +1 -1
  125. package/src/components/OmegaForm/blockDialog.ts +18 -6
  126. package/src/components/OmegaForm/createUseFormWithCustomInput.ts +16 -14
  127. package/src/components/OmegaForm/decimalSeparatorInput.ts +38 -0
  128. package/src/components/OmegaForm/errors.ts +139 -0
  129. package/src/components/OmegaForm/getOmegaStore.ts +1 -1
  130. package/src/components/OmegaForm/hocs.ts +19 -0
  131. package/src/components/OmegaForm/index.ts +17 -4
  132. package/src/components/OmegaForm/inputs.ts +22 -0
  133. package/src/components/OmegaForm/meta/checks.ts +107 -0
  134. package/src/components/OmegaForm/meta/createMeta.ts +140 -0
  135. package/src/components/OmegaForm/meta/defaults.ts +263 -0
  136. package/src/components/OmegaForm/meta/redacted.ts +71 -0
  137. package/src/components/OmegaForm/meta/types.ts +78 -0
  138. package/src/components/OmegaForm/meta/walker.ts +248 -0
  139. package/src/components/OmegaForm/persistency.ts +247 -0
  140. package/src/components/OmegaForm/submit.ts +131 -0
  141. package/src/components/OmegaForm/types.ts +801 -0
  142. package/src/components/OmegaForm/useOmegaForm.ts +103 -896
  143. package/src/components/OmegaForm/useRegisterField.ts +1 -1
  144. package/src/components/OmegaForm/validation/localized.ts +242 -0
  145. package/src/components/index.ts +1 -0
  146. package/src/index.ts +0 -1
  147. package/src/reset.css +39 -38
  148. package/src/utils/index.ts +11 -8
  149. package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +0 -159
  150. package/dist/types/constants/index.d.ts +0 -1
  151. package/dist/vue-components.es10.js +0 -239
  152. package/dist/vue-components.es11.js +0 -32
  153. package/dist/vue-components.es12.js +0 -503
  154. package/dist/vue-components.es13.js +0 -49
  155. package/dist/vue-components.es14.js +0 -4
  156. package/dist/vue-components.es15.js +0 -4
  157. package/dist/vue-components.es16.js +0 -6
  158. package/dist/vue-components.es17.js +0 -13
  159. package/dist/vue-components.es18.js +0 -57
  160. package/dist/vue-components.es19.js +0 -56
  161. package/dist/vue-components.es2.js +0 -30
  162. package/dist/vue-components.es20.js +0 -8
  163. package/dist/vue-components.es21.js +0 -8
  164. package/dist/vue-components.es22.js +0 -5
  165. package/dist/vue-components.es23.js +0 -5
  166. package/dist/vue-components.es24.js +0 -4
  167. package/dist/vue-components.es25.js +0 -4
  168. package/dist/vue-components.es26.js +0 -4
  169. package/dist/vue-components.es27.js +0 -4
  170. package/dist/vue-components.es28.js +0 -19
  171. package/dist/vue-components.es29.js +0 -13
  172. package/dist/vue-components.es3.js +0 -17
  173. package/dist/vue-components.es30.js +0 -31
  174. package/dist/vue-components.es31.js +0 -6
  175. package/dist/vue-components.es32.js +0 -4
  176. package/dist/vue-components.es33.js +0 -4
  177. package/dist/vue-components.es34.js +0 -113
  178. package/dist/vue-components.es36.js +0 -9
  179. package/dist/vue-components.es37.js +0 -34
  180. package/dist/vue-components.es39.js +0 -194
  181. package/dist/vue-components.es4.js +0 -52
  182. package/dist/vue-components.es41.js +0 -6
  183. package/dist/vue-components.es42.js +0 -25
  184. package/dist/vue-components.es43.js +0 -7
  185. package/dist/vue-components.es44.js +0 -23
  186. package/dist/vue-components.es45.js +0 -32
  187. package/dist/vue-components.es46.js +0 -24
  188. package/dist/vue-components.es47.js +0 -14
  189. package/dist/vue-components.es48.js +0 -7
  190. package/dist/vue-components.es49.js +0 -21
  191. package/dist/vue-components.es5.js +0 -52
  192. package/dist/vue-components.es50.js +0 -11
  193. package/dist/vue-components.es51.js +0 -33
  194. package/dist/vue-components.es52.js +0 -50
  195. package/dist/vue-components.es53.js +0 -28
  196. package/dist/vue-components.es54.js +0 -13
  197. package/dist/vue-components.es55.js +0 -67
  198. package/dist/vue-components.es56.js +0 -58
  199. package/dist/vue-components.es57.js +0 -19
  200. package/dist/vue-components.es58.js +0 -35
  201. package/dist/vue-components.es59.js +0 -31
  202. package/dist/vue-components.es6.js +0 -69
  203. package/dist/vue-components.es60.js +0 -44
  204. package/dist/vue-components.es61.js +0 -4
  205. package/dist/vue-components.es62.js +0 -46
  206. package/dist/vue-components.es63.js +0 -4
  207. package/dist/vue-components.es7.js +0 -83
  208. package/dist/vue-components.es8.js +0 -63
  209. package/dist/vue-components.es9.js +0 -21
  210. package/src/components/OmegaForm/OmegaFormStuff.ts +0 -1276
  211. package/src/constants/index.ts +0 -1
@@ -1,19 +1,21 @@
1
1
  <template>
2
2
  <component
3
3
  :is="form.Field"
4
- :key="fieldKey"
5
4
  :name="name"
6
- :validators="{
7
- onChange: schema,
8
- ...validators
9
- }"
5
+ :validators="validators"
10
6
  >
11
- <template #default="{ field, state }">
7
+ <!--
8
+ Read `state` from `field.state`, not the Field slot's `state` prop.
9
+ Since @tanstack/vue-form 1.32 the slot `state` is a one-time snapshot
10
+ taken when the field mounts and never updates, whereas `field.state`
11
+ is a reactive getter backed by the field store.
12
+ -->
13
+ <template #default="{ field }">
12
14
  <OmegaInternalInput
13
15
  v-if="meta"
14
- v-bind="{ ...$attrs, ...$props, inputClass: computedClass }"
15
- :field="field"
16
- :state="state"
16
+ v-bind="{ ...$attrs, ...internalInputProps, inputClass: computedClass }"
17
+ :field="field as any"
18
+ :state="field.state"
17
19
  :register="form.registerField"
18
20
  :label="label ?? errori18n(propsName)"
19
21
  :meta="meta"
@@ -40,14 +42,24 @@
40
42
  lang="ts"
41
43
  generic="From extends Record<PropertyKey, any>, To extends Record<PropertyKey, any>, Name extends DeepKeys<From>"
42
44
  >
45
+ /* eslint-disable @typescript-eslint/no-explicit-any -- TanStack Form Field generic interop and slot prop typing */
43
46
  import { type DeepKeys } from "@tanstack/vue-form"
44
47
  import { computed, inject, type Ref, useAttrs } from "vue"
45
- import { useIntl } from "../../utils"
46
- import { type FieldMeta, generateInputStandardSchemaFromFieldMeta, type OmegaInputPropsBase } from "./OmegaFormStuff"
48
+ import { useErrorLabel } from "./errors"
49
+ import { type FieldMeta } from "./meta/types"
47
50
  import OmegaInternalInput from "./OmegaInternalInput.vue"
48
- import { useErrorLabel } from "./useOmegaForm"
51
+ import { type OmegaInputPropsBase } from "./types"
49
52
 
50
- const props = defineProps<OmegaInputPropsBase<From, To, Name>>()
53
+ const props = withDefaults(defineProps<OmegaInputPropsBase<From, To, Name>>(), {
54
+ required: undefined
55
+ })
56
+ const internalInputProps = computed(() => ({
57
+ label: props.label,
58
+ required: props.required,
59
+ type: props.type,
60
+ options: props.options,
61
+ inputs: props.form.inputs
62
+ }))
51
63
 
52
64
  // downgrade to *as* DeepKeys<From> to avoid useless and possible infinite recursion in TS
53
65
  const propsName = computed(() => props.name as DeepKeys<From>)
@@ -57,13 +69,10 @@ defineSlots<{
57
69
  default?: (props: any) => any
58
70
  }>()
59
71
 
60
- defineOptions({
61
- inheritAttrs: false
62
- })
72
+ defineOptions({ inheritAttrs: false })
63
73
 
64
74
  const attrs = useAttrs()
65
75
 
66
- // Compute the class to use based on inputClass prop
67
76
  const computedClass = computed(() => {
68
77
  if (props.inputClass === null) return undefined
69
78
  if (props.inputClass !== undefined) return props.inputClass
@@ -76,33 +85,10 @@ const getMetaFromArray = inject<Ref<(name: string) => FieldMeta | null> | null>(
76
85
  )
77
86
 
78
87
  const meta = computed(() => {
79
- if (getMetaFromArray?.value && getMetaFromArray.value(props.name as DeepKeys<From>)) {
80
- return getMetaFromArray.value(propsName.value)
81
- }
88
+ const fromArray = getMetaFromArray?.value?.(propsName.value)
89
+ if (fromArray) return fromArray
82
90
  return props.form.meta[propsName.value]
83
- })
84
-
85
- // Key to force Field re-mount when meta type changes (for TaggedUnion support)
86
- const fieldKey = computed(() => {
87
- const m = meta.value
88
- if (!m) return propsName.value
89
- // Include type and key constraints in the key so Field re-mounts when validation rules change
90
- // Cast to any since not all FieldMeta variants have these properties
91
- const fm = m as any
92
- return `${propsName.value}-${fm.type}-${fm.minLength ?? ""}-${fm.maxLength ?? ""}-${fm.minimum ?? ""}-${
93
- fm.maximum ?? ""
94
- }`
95
- })
96
-
97
- // Call useIntl during setup to avoid issues when computed re-evaluates
98
- const { trans } = useIntl()
99
-
100
- const schema = computed(() => {
101
- if (!meta.value) {
102
- console.log(props.name, Object.keys(props.form.meta), props.form.meta)
103
- throw new Error("Meta is undefined")
104
- }
105
- return generateInputStandardSchemaFromFieldMeta(meta.value, trans)
91
+ ?? props.form.meta[propsName.value.replace(/\[\d+\]/g, "") as DeepKeys<From>]
106
92
  })
107
93
 
108
94
  const errori18n = useErrorLabel(props.form)
@@ -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
@@ -31,7 +34,12 @@
31
34
  v-if="inputProps.type === 'email'
32
35
  || inputProps.type === 'string'
33
36
  || inputProps.type === 'password'
34
- || inputProps.type === 'date'"
37
+ || inputProps.type === 'date'
38
+ || inputProps.type === 'tel'
39
+ || inputProps.type === 'url'
40
+ || inputProps.type === 'color'
41
+ || inputProps.type === 'time'
42
+ || inputProps.type === 'search'"
35
43
  :id="inputProps.id"
36
44
  :required="inputProps.required"
37
45
  :min-length="inputProps.minLength"
@@ -43,7 +51,7 @@
43
51
  :error="inputProps.error"
44
52
  v-bind="$attrs"
45
53
  :model-value="state.value"
46
- @update:model-value="field.handleChange"
54
+ @update:model-value="(v: any) => field.handleChange(v)"
47
55
  >
48
56
  <template
49
57
  v-if="$slots.label"
@@ -67,7 +75,7 @@
67
75
  :error="inputProps.error"
68
76
  v-bind="$attrs"
69
77
  :model-value="state.value"
70
- @update:model-value="field.handleChange"
78
+ @update:model-value="(v: any) => field.handleChange(v)"
71
79
  >
72
80
  <template
73
81
  v-if="$slots.label"
@@ -79,20 +87,54 @@
79
87
  />
80
88
  </template>
81
89
  </v-textarea>
82
- <component
83
- :is="inputProps.type === 'range' ? 'v-slider' : 'v-text-field'"
84
- v-if="inputProps.type === 'number' || inputProps.type === 'range'"
90
+ <!-- min/max are deliberately NOT passed as VNumberInput props: the component
91
+ silently withholds out-of-range values from the model (so schema errors
92
+ would never show) and clamps on blur. Validation stays schema-driven; the
93
+ bounds are exposed to AT via the spinbutton ARIA attrs, which fall through
94
+ to the native input. Same reasoning for precision: Vuetify's default (0)
95
+ would silently block decimals, so it's forced to null and typing 1.5 into
96
+ an int field shows the schema error instead. -->
97
+ <v-number-input
98
+ v-if="inputProps.type === 'number'"
85
99
  :id="inputProps.id"
86
100
  :required="inputProps.required"
87
- :min="inputProps.min"
88
- :max="inputProps.max"
89
- :type="inputProps.type"
101
+ role="spinbutton"
102
+ :aria-valuemin="typeof inputProps.min === 'number' ? inputProps.min : undefined"
103
+ :aria-valuemax="typeof inputProps.max === 'number' ? inputProps.max : undefined"
104
+ :aria-valuenow="typeof state.value === 'number' ? state.value : undefined"
90
105
  :name="field.name"
91
106
  :label="inputProps.label"
92
107
  :error-messages="inputProps.errorMessages"
93
108
  :error="inputProps.error"
109
+ :precision="null"
110
+ control-variant="stacked"
94
111
  v-bind="$attrs"
95
- :model-value="state.value"
112
+ :model-value="state.value as any"
113
+ @beforeinput.capture="onNumberBeforeinput"
114
+ @update:model-value="(v: number | null) => field.handleChange((v ?? undefined) as any)"
115
+ >
116
+ <template
117
+ v-if="$slots.label"
118
+ #label
119
+ >
120
+ <slot
121
+ name="label"
122
+ v-bind="{ required: inputProps.required, id: inputProps.id, label: inputProps.label }"
123
+ />
124
+ </template>
125
+ </v-number-input>
126
+ <v-slider
127
+ v-if="inputProps.type === 'range'"
128
+ :id="inputProps.id"
129
+ :required="inputProps.required"
130
+ :min="typeof inputProps.min === 'number' ? inputProps.min : undefined"
131
+ :max="typeof inputProps.max === 'number' ? inputProps.max : undefined"
132
+ :name="field.name"
133
+ :label="inputProps.label"
134
+ :error-messages="inputProps.errorMessages"
135
+ :error="inputProps.error"
136
+ v-bind="$attrs"
137
+ :model-value="state.value as any"
96
138
  @update:model-value="(e: any) => {
97
139
  if (e || e === 0) {
98
140
  field.handleChange(Number(e) as any)
@@ -110,7 +152,7 @@
110
152
  v-bind="{ required: inputProps.required, id: inputProps.id, label: inputProps.label }"
111
153
  />
112
154
  </template>
113
- </component>
155
+ </v-slider>
114
156
  <template v-if="inputProps.type === 'radio'">
115
157
  <v-radio-group
116
158
  :id="inputProps.id"
@@ -120,7 +162,7 @@
120
162
  :error="inputProps.error"
121
163
  v-bind="$attrs"
122
164
  :model-value="state.value"
123
- @update:model-value="field.handleChange"
165
+ @update:model-value="(v: any) => field.handleChange(v)"
124
166
  >
125
167
  <template
126
168
  v-if="$slots.label"
@@ -133,7 +175,7 @@
133
175
  </template>
134
176
  <v-radio
135
177
  v-for="option in inputProps.options"
136
- :key="option.value"
178
+ :key="option.value as PropertyKey"
137
179
  :label="option.title"
138
180
  :value="option.value"
139
181
  />
@@ -195,6 +237,33 @@
195
237
  />
196
238
  </template>
197
239
  </v-autocomplete>
240
+
241
+ <!-- Fallback for any type not handled by a dedicated branch above (e.g. an
242
+ unrecognized schema type resolved to "unknown", or a custom `type` with
243
+ no renderer). Renders an editable text input instead of nothing. -->
244
+ <v-text-field
245
+ v-if="isUnhandledType"
246
+ :id="inputProps.id"
247
+ :required="inputProps.required"
248
+ :type="getInputType(inputProps.type)"
249
+ :name="field.name"
250
+ :label="inputProps.label"
251
+ :error-messages="inputProps.errorMessages"
252
+ :error="inputProps.error"
253
+ v-bind="$attrs"
254
+ :model-value="state.value"
255
+ @update:model-value="(v: any) => field.handleChange(v)"
256
+ >
257
+ <template
258
+ v-if="$slots.label"
259
+ #label
260
+ >
261
+ <slot
262
+ name="label"
263
+ v-bind="{ required: inputProps.required, id: inputProps.id, label: inputProps.label }"
264
+ />
265
+ </template>
266
+ </v-text-field>
198
267
  </div>
199
268
  </template>
200
269
 
@@ -204,10 +273,14 @@
204
273
  generic="From extends Record<PropertyKey, any>, Name extends DeepKeys<From>"
205
274
  >
206
275
  import { type DeepKeys } from "@tanstack/vue-form"
207
- import { getInputType } from "../OmegaForm/OmegaFormStuff"
276
+ import { computed, useAttrs, watchEffect } from "vue"
277
+ import { useLocale } from "vuetify"
278
+ import { handleDecimalSeparatorBeforeinput } from "./decimalSeparatorInput"
208
279
  import type { VuetifyInputProps } from "./InputProps"
280
+ import { getInputType } from "./inputs"
281
+ import { typeOverrides } from "./types"
209
282
 
210
- defineProps<VuetifyInputProps<From, Name>>()
283
+ const props = defineProps<VuetifyInputProps<From, Name>>()
211
284
 
212
285
  defineEmits<{
213
286
  (e: "focus", event: Event): void
@@ -217,6 +290,39 @@ defineEmits<{
217
290
  defineOptions({
218
291
  inheritAttrs: false
219
292
  })
293
+
294
+ const attrs = useAttrs()
295
+ // useLocale throws outside a Vuetify app (e.g. tests mounting with stubbed
296
+ // vuetify components); numbers then fall back to the "." separator.
297
+ const localeDecimalSeparator = (() => {
298
+ try {
299
+ return useLocale().decimalSeparator
300
+ } catch {
301
+ return undefined
302
+ }
303
+ })()
304
+
305
+ // Mirrors VNumberInput's own resolution: an explicit decimal-separator attr
306
+ // wins, otherwise the locale decides.
307
+ const decimalSeparator = computed(() => {
308
+ const explicit = (attrs["decimal-separator"] ?? attrs["decimalSeparator"]) as string | undefined
309
+ return explicit?.[0] || localeDecimalSeparator?.value || "."
310
+ })
311
+
312
+ const onNumberBeforeinput = (e: Event) => handleDecimalSeparatorBeforeinput(e as InputEvent, decimalSeparator.value)
313
+
314
+ // True when no dedicated branch handles `inputProps.type` (it's outside the
315
+ // built-in `typeOverrides`): the fallback text input renders and we warn.
316
+ const isUnhandledType = computed(() => !(typeOverrides as readonly string[]).includes(props.inputProps.type))
317
+
318
+ watchEffect(() => {
319
+ if (isUnhandledType.value) {
320
+ // eslint-disable-next-line no-console
321
+ console.warn(
322
+ `[OmegaForm] Unhandled input type "${props.inputProps.type}", falling back to a text input.`
323
+ )
324
+ }
325
+ })
220
326
  </script>
221
327
 
222
328
  <style>
@@ -1,8 +1,9 @@
1
1
  <template>
2
2
  <slot v-bind="{ ...inputProps.inputProps, field: inputProps.field, state: inputProps.state }">
3
3
  <div :class="rootClass">
4
- <OmegaInputVuetify
5
- v-if="vuetified"
4
+ <component
5
+ :is="renderComponent"
6
+ v-if="renderComponent"
6
7
  v-bind="{ ...attrsWithoutClass, ...inputProps, class: props.inputClass }"
7
8
  >
8
9
  <template
@@ -14,7 +15,7 @@
14
15
  v-bind="labelProps"
15
16
  />
16
17
  </template>
17
- </OmegaInputVuetify>
18
+ </component>
18
19
  </div>
19
20
  </slot>
20
21
  </template>
@@ -24,11 +25,13 @@
24
25
  lang="ts"
25
26
  generic="From extends Record<PropertyKey, any>, Name extends DeepKeys<From>"
26
27
  >
28
+ /* eslint-disable @typescript-eslint/no-explicit-any -- TanStack Form / Vue attrs interop */
27
29
  import { type DeepKeys, useStore } from "@tanstack/vue-form"
28
- import { computed, type ComputedRef, getCurrentInstance, useAttrs, useId, useSlots } from "vue"
30
+ import { type Component, computed, type ComputedRef, getCurrentInstance, useAttrs, useId, useSlots } from "vue"
29
31
  import type { InputProps, OmegaFieldInternalApi } from "./InputProps"
30
- import type { FieldValidators, MetaRecord, NestedKeyOf, TypeOverride } from "./OmegaFormStuff"
32
+ import type { MetaRecord, NestedKeyOf } from "./meta/types"
31
33
  import OmegaInputVuetify from "./OmegaInputVuetify.vue"
34
+ import type { TypeOverride } from "./types"
32
35
 
33
36
  defineOptions({
34
37
  inheritAttrs: false
@@ -41,7 +44,6 @@ const props = withDefaults(
41
44
  meta: MetaRecord<From>[NestedKeyOf<From>]
42
45
  label: string
43
46
  type?: TypeOverride
44
- validators?: FieldValidators<From>
45
47
  required?: boolean
46
48
  inputClass?: string | null
47
49
 
@@ -54,14 +56,16 @@ const props = withDefaults(
54
56
  ) => void
55
57
 
56
58
  // TODO: these should really be optional, depending on the input type (and the custom input type for custom inputs :s)
57
- options?: { title: string; value: string }[]
59
+ options?: { title: string; value: unknown }[]
60
+
61
+ inputs?: Record<string, Component>
58
62
  }>(),
59
63
  {
60
64
  required: undefined,
61
65
  type: undefined,
62
66
  options: undefined,
63
- validators: undefined,
64
- inputClass: undefined
67
+ inputClass: undefined,
68
+ inputs: undefined
65
69
  }
66
70
  )
67
71
 
@@ -84,7 +88,8 @@ const id = useId()
84
88
 
85
89
  const fieldApi = props.field
86
90
 
87
- const fieldState = useStore(fieldApi.store, (state) => state)
91
+ // Subscribed for side-effect: keeps component reactive to fieldApi.store changes
92
+ useStore(fieldApi.store, (state) => state)
88
93
 
89
94
  // Get errors from form-level fieldMeta (persists across Field re-mounts)
90
95
  const formFieldMeta = useStore(fieldApi.form.store, (state) => state.fieldMeta)
@@ -98,6 +103,11 @@ const fieldType = computed(() => {
98
103
  return props.meta?.type || "unknown"
99
104
  })
100
105
 
106
+ const renderComponent = computed(() =>
107
+ props.inputs?.[fieldType.value]
108
+ ?? (vuetified ? OmegaInputVuetify : undefined)
109
+ )
110
+
101
111
  props.register(computed(() => ({ name: props.field.name, label: props.label, id })))
102
112
 
103
113
  // Get errors from form-level fieldMeta instead of field-level state
@@ -128,6 +138,13 @@ const handleChange: OmegaFieldInternalApi<From, Name>["handleChange"] = (value)
128
138
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
139
  : null as any
130
140
  )
141
+ } else if (props.meta?.isOptionalKey) {
142
+ // `S.optionalKey` expects the key to be ABSENT from the submitted
143
+ // object, not present-with-undefined. Remove it from form state
144
+ // rather than setting it to `undefined`. Note: this is distinct
145
+ // from `required: false`, which may also just mean "empty string
146
+ // is valid" for unconstrained `S.String` fields.
147
+ props.field.form.deleteField(props.field.name)
131
148
  } else {
132
149
  // Keep the actual value (e.g., empty string for S.String fields)
133
150
  props.field.handleChange(value)
@@ -136,9 +153,11 @@ const handleChange: OmegaFieldInternalApi<From, Name>["handleChange"] = (value)
136
153
  props.field.handleChange(value)
137
154
  }
138
155
 
139
- // whenever we change the field, regardless if we set it to null, we should reset onSubmit.
140
- // not sure why this is not the case in tanstack form.
141
- props.field.setMeta((m) => ({ ...m, errorMap: { ...m.errorMap, onSubmit: undefined } }))
156
+ // A change here may have materialised a nullable struct (this field is one
157
+ // of its children); backfill the untouched siblings so the live `values`
158
+ // matches what validation and decoding see.
159
+ const form = props.field.form as { normalizeNullableStructs?: () => void }
160
+ form.normalizeNullableStructs?.()
142
161
  }
143
162
 
144
163
  // Note: Default value normalization (converting empty strings to null/undefined for nullable fields)
@@ -172,6 +191,7 @@ const inputProps: ComputedRef<InputProps<From, Name>> = computed(() => ({
172
191
  ? (props.meta?.minimum
173
192
  ?? (typeof props.meta?.exclusiveMinimum === "number" ? props.meta.exclusiveMinimum + 1 : undefined))
174
193
  : undefined,
194
+ refinement: props.meta?.type === "number" ? props.meta?.refinement : undefined,
175
195
  errorMessages: errors.value,
176
196
  error: !!errors.value.length,
177
197
  type: fieldType.value,
@@ -8,8 +8,8 @@
8
8
  import { type DeepKeys } from "@tanstack/vue-form"
9
9
  import { computed, provide, ref, watch } from "vue"
10
10
  import { type TaggedUnionOption } from "./InputProps"
11
- import { type FieldPath } from "./OmegaFormStuff"
12
11
  import OmegaTaggedUnionInternal from "./OmegaTaggedUnionInternal.vue"
12
+ import { type FieldPath } from "./types"
13
13
  import { type useOmegaForm } from "./useOmegaForm"
14
14
 
15
15
  const props = defineProps<{
@@ -30,6 +30,7 @@ watch(
30
30
  () => {
31
31
  const path = tagPath.value
32
32
  // Navigate to the nested value
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- traversing arbitrary nested values
33
34
  return path.split(".").reduce((acc: any, key) => acc?.[key], formValues.value) as string | null
34
35
  },
35
36
  (newTag) => {
@@ -74,9 +75,15 @@ provide("getMetaFromArray", getMetaFromArray)
74
75
  />
75
76
  </slot>
76
77
  <slot />
78
+ <!--
79
+ Drive the branch off `currentTag`: the tag is read from the form store
80
+ (the canonical state), the same single source of truth that feeds
81
+ `getMetaFromArray` above — rather than the `Field` slot's `state`,
82
+ which is only a projection of it.
83
+ -->
77
84
  <OmegaTaggedUnionInternal
78
85
  :field="inputProps.field as any"
79
- :state="inputProps.state.value"
86
+ :tag="currentTag"
80
87
  :name="name"
81
88
  :form="form"
82
89
  >
@@ -91,7 +98,7 @@ provide("getMetaFromArray", getMetaFromArray)
91
98
  </template>
92
99
  </OmegaTaggedUnionInternal>
93
100
  <slot
94
- v-if="inputProps.state.value"
101
+ v-if="currentTag"
95
102
  name="OmegaCommon"
96
103
  />
97
104
  </template>
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <slot
3
- v-if="state"
4
- :name="`${name ? `${name}.` : ''}${state}`"
5
- v-bind="{ field, state }"
3
+ v-if="tag"
4
+ :name="`${name ? `${name}.` : ''}${tag}`"
5
+ v-bind="{ field, tag }"
6
6
  />
7
7
  </template>
8
8
 
@@ -17,7 +17,7 @@ import { type OmegaFieldInternalApi } from "./InputProps"
17
17
  import { type useOmegaForm } from "./useOmegaForm"
18
18
 
19
19
  const props = defineProps<{
20
- state: DeepValue<From, Name>
20
+ tag: string | null
21
21
  field: OmegaFieldInternalApi<From, Name>
22
22
  name?: DeepKeys<From>
23
23
  form: ReturnType<typeof useOmegaForm<From, To>>
@@ -26,7 +26,7 @@ const props = defineProps<{
26
26
  const values = props.form.useStore(({ values }) => values)
27
27
 
28
28
  // Watch for _tag changes
29
- watch(() => props.state, (newTag, oldTag) => {
29
+ watch(() => props.tag, (newTag, oldTag) => {
30
30
  if (newTag === null) {
31
31
  props.field.setValue(null as DeepValue<From, Name>)
32
32
  }
@@ -34,7 +34,7 @@ watch(() => props.state, (newTag, oldTag) => {
34
34
  if (newTag !== oldTag) {
35
35
  props.form.reset(values.value)
36
36
  setTimeout(() => {
37
- props.field.validate("change")
37
+ void props.field.validate("change")
38
38
  }, 0)
39
39
  }
40
40
  }, { immediate: true })
@@ -30,7 +30,7 @@
30
30
  import { useStore } from "@tanstack/vue-form"
31
31
  import { usePreventClose } from "./blockDialog"
32
32
  import { getOmegaStore } from "./getOmegaStore"
33
- import { type DefaultTypeProps, type OmegaFormApi, type OmegaFormState } from "./OmegaFormStuff"
33
+ import { type DefaultTypeProps, type OmegaFormApi, type OmegaFormState } from "./types"
34
34
  import { type OmegaFormReturn } from "./useOmegaForm"
35
35
 
36
36
  type OmegaWrapperProps = {
@@ -1,5 +1,6 @@
1
1
  import mitt from "mitt"
2
2
  import { inject, type InjectionKey, provide, type Ref } from "vue"
3
+ import { useIntl } from "../../utils"
3
4
  import { onMountedWithCleanup } from "./onMountedWithCleanup"
4
5
 
5
6
  export type DialogClosing = { prevent?: boolean | Promise<boolean> }
@@ -19,11 +20,19 @@ export const usePreventClose = (mkIsDirty: () => Ref<boolean>) => {
19
20
  if (!bus) {
20
21
  return
21
22
  }
23
+ const { formatMessage, trans } = useIntl()
22
24
  const isDirty = mkIsDirty()
25
+ const defaultMessage = "There are unsaved changes. Are you sure you want to close?"
23
26
  onMountedWithCleanup(() => {
24
27
  const onDialogClosing = (evt: DialogClosing) => {
25
28
  if (isDirty.value) {
26
- if (!confirm("Es sind ungespeicherte Änderungen vorhanden. Wirklich schließen?")) {
29
+ // Mirror the guard pattern in errors.ts: a custom `useIntl` mock may
30
+ // only provide `trans`, so fall back through trans → defaultMessage.
31
+ const message = formatMessage
32
+ ? formatMessage({ id: "form.unsaved_changes_confirm", defaultMessage })
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- key may not be registered in the locale catalog
34
+ : trans?.("form.unsaved_changes_confirm") ?? defaultMessage
35
+ if (!confirm(message)) {
27
36
  evt.prevent = true
28
37
  }
29
38
  }
@@ -46,11 +55,14 @@ export const useOnClose = (close: () => void) => {
46
55
  bus.emit("dialog-closing", evt)
47
56
  if (evt.prevent) {
48
57
  if (typeof evt.prevent === "object" && "then" in evt.prevent) {
49
- evt.prevent.then((r) => {
50
- if (r !== false) {
51
- close()
52
- }
53
- })
58
+ evt
59
+ .prevent
60
+ .then((r) => {
61
+ if (r) {
62
+ close()
63
+ }
64
+ })
65
+ .catch(console.error)
54
66
  }
55
67
  } else {
56
68
  close()
@@ -1,21 +1,21 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any -- TanStack Form / Vue render-fn slot interop */
1
2
  import type { DeepKeys } from "@tanstack/vue-form"
2
3
  import { type Component, h } from "vue"
3
4
  import type { MergedInputProps } from "./InputProps"
4
- import { type DefaultTypeProps } from "./OmegaFormStuff"
5
5
  import OmegaInput from "./OmegaInput.vue"
6
+ import type { OmegaConfig } from "./types"
6
7
  import { useOmegaForm } from "./useOmegaForm"
7
8
 
8
- export const createUseFormWithCustomInput = <
9
- TypeProps = DefaultTypeProps
10
- >(CustomInputComponent: Component) => {
9
+ export const createUseFormWithCustomInput = (CustomInputComponent: Component) => {
11
10
  return <
12
11
  From extends Record<PropertyKey, any>,
13
- To extends Record<PropertyKey, any>
12
+ To extends Record<PropertyKey, any>,
13
+ const Cfg extends OmegaConfig<To> = OmegaConfig<To>
14
14
  >(
15
- ...args: Parameters<typeof useOmegaForm<From, To>>
15
+ schema: Parameters<typeof useOmegaForm<From, To>>[0],
16
+ tanstackFormOptions?: Parameters<typeof useOmegaForm<From, To>>[1],
17
+ omegaConfig?: Cfg
16
18
  ) => {
17
- const [schema, tanstackFormOptions, omegaConfig] = args
18
-
19
19
  // Create a wrapper that extends OmegaInput and overrides its default slot
20
20
  const WrappedInput = {
21
21
  name: "WrappedInput",
@@ -35,7 +35,10 @@ export const createUseFormWithCustomInput = <
35
35
  && key !== "form"
36
36
  )
37
37
  )
38
- return h(CustomInputComponent, { ...filteredAttrs, field, state, inputProps }, {
38
+ // A per-type registration (omegaConfig.inputs) wins over the
39
+ // universal CustomInputComponent; otherwise fall back to it.
40
+ const LeafComponent = omegaConfig?.inputs?.[inputProps.type] ?? CustomInputComponent
41
+ return h(LeafComponent, { ...filteredAttrs, field, state, inputProps }, {
39
42
  // Pass through label slot if it exists
40
43
  ...(slots.label && {
41
44
  label: (labelProps: any) => slots.label(labelProps)
@@ -54,13 +57,12 @@ export const createUseFormWithCustomInput = <
54
57
  }
55
58
  }
56
59
 
57
- const form = useOmegaForm<From, To, TypeProps>(
60
+ // Thread `Cfg` so `omegaConfig.inputs` keys are inferred into the `type`
61
+ // union (`<form.Input type="...">`), exactly like a bare `useOmegaForm`.
62
+ const form = useOmegaForm<From, To, Cfg>(
58
63
  schema,
59
64
  tanstackFormOptions,
60
- {
61
- ...omegaConfig,
62
- input: WrappedInput
63
- }
65
+ { ...omegaConfig, input: WrappedInput } as Cfg
64
66
  )
65
67
 
66
68
  return form