@effect-app/vue-components 4.0.0-beta.3 → 4.0.0-beta.300

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 (245) hide show
  1. package/README.md +36 -8
  2. package/dist/reset.css +52 -0
  3. package/dist/types/components/CommandButton.vue.d.ts +24 -5
  4. package/dist/types/components/Dialog.vue.d.ts +3 -1
  5. package/dist/types/components/FixedNuxtErrorBoundary.vue.d.ts +39 -0
  6. package/dist/types/components/OmegaForm/InputProps.d.ts +1 -1
  7. package/dist/types/components/OmegaForm/OmegaArray.vue.d.ts +1 -1
  8. package/dist/types/components/OmegaForm/OmegaAutoGen.vue.d.ts +1 -1
  9. package/dist/types/components/OmegaForm/OmegaErrorsInternal.vue.d.ts +1 -1
  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 +6 -0
  13. package/dist/types/components/OmegaForm/OmegaInternalInput.vue.d.ts +6 -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/errors.d.ts +33 -0
  19. package/dist/types/components/OmegaForm/getOmegaStore.d.ts +1 -1
  20. package/dist/types/components/OmegaForm/hocs.d.ts +3 -0
  21. package/dist/types/components/OmegaForm/index.d.ts +14 -3
  22. package/dist/types/components/OmegaForm/inputs.d.ts +4 -0
  23. package/dist/types/components/OmegaForm/meta/checks.d.ts +4 -0
  24. package/dist/types/components/OmegaForm/meta/createMeta.d.ts +33 -0
  25. package/dist/types/components/OmegaForm/meta/defaults.d.ts +21 -0
  26. package/dist/types/components/OmegaForm/meta/redacted.d.ts +2 -0
  27. package/dist/types/components/OmegaForm/meta/types.d.ts +56 -0
  28. package/dist/types/components/OmegaForm/meta/walker.d.ts +18 -0
  29. package/dist/types/components/OmegaForm/persistency.d.ts +58 -0
  30. package/dist/types/components/OmegaForm/submit.d.ts +60 -0
  31. package/dist/types/components/OmegaForm/types.d.ts +312 -0
  32. package/dist/types/components/OmegaForm/useOmegaForm.d.ts +9 -215
  33. package/dist/types/components/OmegaForm/validation/localized.d.ts +10 -0
  34. package/dist/types/components/index.d.ts +1 -0
  35. package/dist/types/index.d.ts +0 -1
  36. package/dist/types/utils/index.d.ts +8 -9
  37. package/dist/vue-components.es.js +30 -44
  38. package/dist/vue-components10.es.js +5 -0
  39. package/dist/vue-components11.es.js +56 -0
  40. package/dist/vue-components12.es.js +5 -0
  41. package/dist/vue-components13.es.js +20 -0
  42. package/dist/vue-components14.es.js +49 -0
  43. package/dist/vue-components15.es.js +128 -0
  44. package/dist/vue-components16.es.js +65 -0
  45. package/dist/vue-components17.es.js +114 -0
  46. package/dist/vue-components18.es.js +22 -0
  47. package/dist/vue-components19.es.js +5 -0
  48. package/dist/vue-components2.es.js +11 -0
  49. package/dist/vue-components20.es.js +80 -0
  50. package/dist/vue-components21.es.js +93 -0
  51. package/dist/vue-components22.es.js +73 -0
  52. package/dist/vue-components23.es.js +12 -0
  53. package/dist/vue-components24.es.js +57 -0
  54. package/dist/vue-components25.es.js +5 -0
  55. package/dist/vue-components26.es.js +44 -0
  56. package/dist/vue-components27.es.js +5 -0
  57. package/dist/vue-components28.es.js +84 -0
  58. package/dist/vue-components3.es.js +98 -0
  59. package/dist/vue-components30.es.js +8 -0
  60. package/dist/vue-components31.es.js +9 -0
  61. package/dist/vue-components32.es.js +24 -0
  62. package/dist/vue-components33.es.js +300 -0
  63. package/dist/vue-components35.es.js +8 -0
  64. package/dist/vue-components36.es.js +73 -0
  65. package/dist/vue-components37.es.js +5 -0
  66. package/dist/vue-components38.es.js +64 -0
  67. package/dist/vue-components39.es.js +5 -0
  68. package/dist/vue-components4.es.js +5 -0
  69. package/dist/vue-components40.es.js +24 -0
  70. package/dist/vue-components41.es.js +5 -0
  71. package/dist/vue-components42.es.js +59 -0
  72. package/dist/vue-components43.es.js +5 -0
  73. package/dist/vue-components44.es.js +12 -0
  74. package/dist/vue-components45.es.js +22 -0
  75. package/dist/vue-components47.es.js +9 -0
  76. package/dist/vue-components48.es.js +4 -0
  77. package/dist/vue-components49.es.js +38 -0
  78. package/dist/vue-components5.es.js +24 -0
  79. package/dist/vue-components50.es.js +27 -0
  80. package/dist/vue-components51.es.js +28 -0
  81. package/dist/vue-components52.es.js +7 -0
  82. package/dist/vue-components53.es.js +18 -0
  83. package/dist/vue-components54.es.js +36 -0
  84. package/dist/vue-components55.es.js +18 -0
  85. package/dist/vue-components56.es.js +21 -0
  86. package/dist/vue-components57.es.js +30 -0
  87. package/dist/vue-components58.es.js +7 -0
  88. package/dist/vue-components59.es.js +9 -0
  89. package/dist/vue-components6.es.js +13 -0
  90. package/dist/vue-components60.es.js +38 -0
  91. package/dist/vue-components61.es.js +25 -0
  92. package/dist/vue-components62.es.js +128 -0
  93. package/dist/vue-components63.es.js +24 -0
  94. package/dist/vue-components64.es.js +21 -0
  95. package/dist/vue-components65.es.js +9 -0
  96. package/dist/vue-components66.es.js +19 -0
  97. package/dist/vue-components67.es.js +5 -0
  98. package/dist/vue-components68.es.js +29 -0
  99. package/dist/vue-components69.es.js +5 -0
  100. package/dist/vue-components7.es.js +13 -0
  101. package/dist/vue-components70.es.js +43 -0
  102. package/dist/vue-components71.es.js +101 -0
  103. package/dist/vue-components72.es.js +36 -0
  104. package/dist/vue-components73.es.js +19 -0
  105. package/dist/vue-components74.es.js +50 -0
  106. package/dist/vue-components8.es.js +35 -0
  107. package/dist/vue-components9.es.js +47 -0
  108. package/package.json +44 -34
  109. package/src/components/CommandButton.vue +96 -16
  110. package/src/components/Dialog.vue +0 -1
  111. package/src/components/FixedNuxtErrorBoundary.vue +112 -0
  112. package/src/components/OmegaForm/InputProps.ts +1 -1
  113. package/src/components/OmegaForm/OmegaArray.vue +8 -9
  114. package/src/components/OmegaForm/OmegaAutoGen.vue +26 -30
  115. package/src/components/OmegaForm/OmegaErrorsInternal.vue +3 -4
  116. package/src/components/OmegaForm/OmegaFormInput.vue +1 -1
  117. package/src/components/OmegaForm/OmegaInput.vue +26 -40
  118. package/src/components/OmegaForm/OmegaInputVuetify.vue +58 -8
  119. package/src/components/OmegaForm/OmegaInternalInput.vue +41 -18
  120. package/src/components/OmegaForm/OmegaTaggedUnion.vue +10 -3
  121. package/src/components/OmegaForm/OmegaTaggedUnionInternal.vue +6 -6
  122. package/src/components/OmegaForm/OmegaWrapper.vue +1 -1
  123. package/src/components/OmegaForm/blockDialog.ts +18 -6
  124. package/src/components/OmegaForm/createUseFormWithCustomInput.ts +16 -14
  125. package/src/components/OmegaForm/errors.ts +136 -0
  126. package/src/components/OmegaForm/getOmegaStore.ts +1 -1
  127. package/src/components/OmegaForm/hocs.ts +19 -0
  128. package/src/components/OmegaForm/index.ts +17 -4
  129. package/src/components/OmegaForm/inputs.ts +22 -0
  130. package/src/components/OmegaForm/meta/checks.ts +82 -0
  131. package/src/components/OmegaForm/meta/createMeta.ts +140 -0
  132. package/src/components/OmegaForm/meta/defaults.ts +261 -0
  133. package/src/components/OmegaForm/meta/redacted.ts +66 -0
  134. package/src/components/OmegaForm/meta/types.ts +78 -0
  135. package/src/components/OmegaForm/meta/walker.ts +248 -0
  136. package/src/components/OmegaForm/persistency.ts +247 -0
  137. package/src/components/OmegaForm/submit.ts +131 -0
  138. package/src/components/OmegaForm/types.ts +801 -0
  139. package/src/components/OmegaForm/useOmegaForm.ts +103 -898
  140. package/src/components/OmegaForm/useRegisterField.ts +1 -1
  141. package/src/components/OmegaForm/validation/localized.ts +203 -0
  142. package/src/components/index.ts +1 -0
  143. package/src/index.ts +0 -1
  144. package/src/reset.css +52 -0
  145. package/src/utils/index.ts +10 -11
  146. package/dist/types/components/OmegaForm/OmegaFormStuff.d.ts +0 -157
  147. package/dist/types/constants/index.d.ts +0 -1
  148. package/dist/vue-components.es10.js +0 -237
  149. package/dist/vue-components.es100.js +0 -4
  150. package/dist/vue-components.es11.js +0 -32
  151. package/dist/vue-components.es12.js +0 -439
  152. package/dist/vue-components.es13.js +0 -49
  153. package/dist/vue-components.es14.js +0 -4
  154. package/dist/vue-components.es15.js +0 -4
  155. package/dist/vue-components.es16.js +0 -725
  156. package/dist/vue-components.es17.js +0 -143
  157. package/dist/vue-components.es18.js +0 -6
  158. package/dist/vue-components.es19.js +0 -13
  159. package/dist/vue-components.es2.js +0 -30
  160. package/dist/vue-components.es20.js +0 -5
  161. package/dist/vue-components.es21.js +0 -26
  162. package/dist/vue-components.es22.js +0 -6
  163. package/dist/vue-components.es23.js +0 -10
  164. package/dist/vue-components.es24.js +0 -57
  165. package/dist/vue-components.es25.js +0 -71
  166. package/dist/vue-components.es26.js +0 -8
  167. package/dist/vue-components.es27.js +0 -8
  168. package/dist/vue-components.es28.js +0 -5
  169. package/dist/vue-components.es29.js +0 -5
  170. package/dist/vue-components.es3.js +0 -16
  171. package/dist/vue-components.es30.js +0 -4
  172. package/dist/vue-components.es31.js +0 -4
  173. package/dist/vue-components.es32.js +0 -4
  174. package/dist/vue-components.es33.js +0 -4
  175. package/dist/vue-components.es34.js +0 -19
  176. package/dist/vue-components.es35.js +0 -13
  177. package/dist/vue-components.es36.js +0 -40
  178. package/dist/vue-components.es37.js +0 -6
  179. package/dist/vue-components.es38.js +0 -85
  180. package/dist/vue-components.es39.js +0 -54
  181. package/dist/vue-components.es4.js +0 -52
  182. package/dist/vue-components.es40.js +0 -563
  183. package/dist/vue-components.es41.js +0 -43
  184. package/dist/vue-components.es42.js +0 -29
  185. package/dist/vue-components.es43.js +0 -7
  186. package/dist/vue-components.es44.js +0 -42
  187. package/dist/vue-components.es45.js +0 -316
  188. package/dist/vue-components.es46.js +0 -33
  189. package/dist/vue-components.es47.js +0 -6
  190. package/dist/vue-components.es48.js +0 -26
  191. package/dist/vue-components.es49.js +0 -77
  192. package/dist/vue-components.es5.js +0 -52
  193. package/dist/vue-components.es50.js +0 -101
  194. package/dist/vue-components.es51.js +0 -4
  195. package/dist/vue-components.es52.js +0 -320
  196. package/dist/vue-components.es53.js +0 -66
  197. package/dist/vue-components.es54.js +0 -4
  198. package/dist/vue-components.es55.js +0 -4
  199. package/dist/vue-components.es56.js +0 -113
  200. package/dist/vue-components.es58.js +0 -9
  201. package/dist/vue-components.es59.js +0 -34
  202. package/dist/vue-components.es6.js +0 -69
  203. package/dist/vue-components.es61.js +0 -194
  204. package/dist/vue-components.es63.js +0 -6
  205. package/dist/vue-components.es64.js +0 -103
  206. package/dist/vue-components.es65.js +0 -84
  207. package/dist/vue-components.es66.js +0 -17
  208. package/dist/vue-components.es67.js +0 -34
  209. package/dist/vue-components.es68.js +0 -4
  210. package/dist/vue-components.es69.js +0 -23
  211. package/dist/vue-components.es7.js +0 -83
  212. package/dist/vue-components.es70.js +0 -14
  213. package/dist/vue-components.es71.js +0 -115
  214. package/dist/vue-components.es72.js +0 -5
  215. package/dist/vue-components.es73.js +0 -4
  216. package/dist/vue-components.es74.js +0 -4
  217. package/dist/vue-components.es75.js +0 -17
  218. package/dist/vue-components.es76.js +0 -72
  219. package/dist/vue-components.es77.js +0 -25
  220. package/dist/vue-components.es78.js +0 -7
  221. package/dist/vue-components.es79.js +0 -23
  222. package/dist/vue-components.es8.js +0 -63
  223. package/dist/vue-components.es80.js +0 -32
  224. package/dist/vue-components.es81.js +0 -24
  225. package/dist/vue-components.es82.js +0 -14
  226. package/dist/vue-components.es83.js +0 -7
  227. package/dist/vue-components.es84.js +0 -21
  228. package/dist/vue-components.es85.js +0 -11
  229. package/dist/vue-components.es86.js +0 -33
  230. package/dist/vue-components.es87.js +0 -50
  231. package/dist/vue-components.es88.js +0 -28
  232. package/dist/vue-components.es89.js +0 -18
  233. package/dist/vue-components.es9.js +0 -21
  234. package/dist/vue-components.es90.js +0 -10
  235. package/dist/vue-components.es91.js +0 -13
  236. package/dist/vue-components.es92.js +0 -67
  237. package/dist/vue-components.es93.js +0 -58
  238. package/dist/vue-components.es94.js +0 -19
  239. package/dist/vue-components.es95.js +0 -35
  240. package/dist/vue-components.es96.js +0 -31
  241. package/dist/vue-components.es97.js +0 -44
  242. package/dist/vue-components.es98.js +0 -4
  243. package/dist/vue-components.es99.js +0 -46
  244. package/src/components/OmegaForm/OmegaFormStuff.ts +0 -1174
  245. package/src/constants/index.ts +0 -1
@@ -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"
@@ -120,7 +128,7 @@
120
128
  :error="inputProps.error"
121
129
  v-bind="$attrs"
122
130
  :model-value="state.value"
123
- @update:model-value="field.handleChange"
131
+ @update:model-value="(v: any) => field.handleChange(v)"
124
132
  >
125
133
  <template
126
134
  v-if="$slots.label"
@@ -133,7 +141,7 @@
133
141
  </template>
134
142
  <v-radio
135
143
  v-for="option in inputProps.options"
136
- :key="option.value"
144
+ :key="option.value as PropertyKey"
137
145
  :label="option.title"
138
146
  :value="option.value"
139
147
  />
@@ -195,6 +203,33 @@
195
203
  />
196
204
  </template>
197
205
  </v-autocomplete>
206
+
207
+ <!-- Fallback for any type not handled by a dedicated branch above (e.g. an
208
+ unrecognized schema type resolved to "unknown", or a custom `type` with
209
+ no renderer). Renders an editable text input instead of nothing. -->
210
+ <v-text-field
211
+ v-if="isUnhandledType"
212
+ :id="inputProps.id"
213
+ :required="inputProps.required"
214
+ :type="getInputType(inputProps.type)"
215
+ :name="field.name"
216
+ :label="inputProps.label"
217
+ :error-messages="inputProps.errorMessages"
218
+ :error="inputProps.error"
219
+ v-bind="$attrs"
220
+ :model-value="state.value"
221
+ @update:model-value="(v: any) => field.handleChange(v)"
222
+ >
223
+ <template
224
+ v-if="$slots.label"
225
+ #label
226
+ >
227
+ <slot
228
+ name="label"
229
+ v-bind="{ required: inputProps.required, id: inputProps.id, label: inputProps.label }"
230
+ />
231
+ </template>
232
+ </v-text-field>
198
233
  </div>
199
234
  </template>
200
235
 
@@ -204,10 +239,12 @@
204
239
  generic="From extends Record<PropertyKey, any>, Name extends DeepKeys<From>"
205
240
  >
206
241
  import { type DeepKeys } from "@tanstack/vue-form"
207
- import { getInputType } from "../OmegaForm/OmegaFormStuff"
242
+ import { computed, watchEffect } from "vue"
208
243
  import type { VuetifyInputProps } from "./InputProps"
244
+ import { getInputType } from "./inputs"
245
+ import { typeOverrides } from "./types"
209
246
 
210
- defineProps<VuetifyInputProps<From, Name>>()
247
+ const props = defineProps<VuetifyInputProps<From, Name>>()
211
248
 
212
249
  defineEmits<{
213
250
  (e: "focus", event: Event): void
@@ -217,6 +254,19 @@ defineEmits<{
217
254
  defineOptions({
218
255
  inheritAttrs: false
219
256
  })
257
+
258
+ // True when no dedicated branch handles `inputProps.type` (it's outside the
259
+ // built-in `typeOverrides`): the fallback text input renders and we warn.
260
+ const isUnhandledType = computed(() => !(typeOverrides as readonly string[]).includes(props.inputProps.type))
261
+
262
+ watchEffect(() => {
263
+ if (isUnhandledType.value) {
264
+ // eslint-disable-next-line no-console
265
+ console.warn(
266
+ `[OmegaForm] Unhandled input type "${props.inputProps.type}", falling back to a text input.`
267
+ )
268
+ }
269
+ })
220
270
  </script>
221
271
 
222
272
  <style>
@@ -1,8 +1,9 @@
1
1
  <template>
2
2
  <slot v-bind="{ ...inputProps.inputProps, field: inputProps.field, state: inputProps.state }">
3
- <div :class="$attrs.class as any">
4
- <OmegaInputVuetify
5
- v-if="vuetified"
3
+ <div :class="rootClass">
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
 
@@ -78,11 +82,14 @@ const attrsWithoutClass = computed(() => {
78
82
  return rest
79
83
  })
80
84
 
85
+ const rootClass = computed(() => attrs.class as any)
86
+
81
87
  const id = useId()
82
88
 
83
89
  const fieldApi = props.field
84
90
 
85
- 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)
86
93
 
87
94
  // Get errors from form-level fieldMeta (persists across Field re-mounts)
88
95
  const formFieldMeta = useStore(fieldApi.form.store, (state) => state.fieldMeta)
@@ -96,6 +103,11 @@ const fieldType = computed(() => {
96
103
  return props.meta?.type || "unknown"
97
104
  })
98
105
 
106
+ const renderComponent = computed(() =>
107
+ props.inputs?.[fieldType.value]
108
+ ?? (vuetified ? OmegaInputVuetify : undefined)
109
+ )
110
+
99
111
  props.register(computed(() => ({ name: props.field.name, label: props.label, id })))
100
112
 
101
113
  // Get errors from form-level fieldMeta instead of field-level state
@@ -126,6 +138,13 @@ const handleChange: OmegaFieldInternalApi<From, Name>["handleChange"] = (value)
126
138
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
127
139
  : null as any
128
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)
129
148
  } else {
130
149
  // Keep the actual value (e.g., empty string for S.String fields)
131
150
  props.field.handleChange(value)
@@ -134,9 +153,11 @@ const handleChange: OmegaFieldInternalApi<From, Name>["handleChange"] = (value)
134
153
  props.field.handleChange(value)
135
154
  }
136
155
 
137
- // whenever we change the field, regardless if we set it to null, we should reset onSubmit.
138
- // not sure why this is not the case in tanstack form.
139
- 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?.()
140
161
  }
141
162
 
142
163
  // Note: Default value normalization (converting empty strings to null/undefined for nullable fields)
@@ -163,11 +184,13 @@ const inputProps: ComputedRef<InputProps<From, Name>> = computed(() => ({
163
184
  minLength: props.meta?.type === "string" && props.meta?.minLength,
164
185
  maxLength: props.meta?.type === "string" && props.meta?.maxLength,
165
186
  max: (props.meta?.type === "number")
166
- && (props.meta?.maximum
167
- ?? (typeof props.meta?.exclusiveMaximum === "number" && props.meta.exclusiveMaximum - 1)),
187
+ ? (props.meta?.maximum
188
+ ?? (typeof props.meta?.exclusiveMaximum === "number" ? props.meta.exclusiveMaximum - 1 : undefined))
189
+ : undefined,
168
190
  min: (props.meta?.type === "number")
169
- && (props.meta?.minimum
170
- ?? (typeof props.meta?.exclusiveMinimum === "number" && props.meta.exclusiveMinimum + 1)),
191
+ ? (props.meta?.minimum
192
+ ?? (typeof props.meta?.exclusiveMinimum === "number" ? props.meta.exclusiveMinimum + 1 : undefined))
193
+ : undefined,
171
194
  errorMessages: errors.value,
172
195
  error: !!errors.value.length,
173
196
  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" as any) ?? 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
@@ -0,0 +1,136 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import { type Component, computed, type ComputedRef, type ConcreteComponent, h, onUnmounted, type Ref, ref, watch } from "vue"
4
+ import { useIntl } from "../../utils"
5
+ import type { OmegaError } from "./types"
6
+ import type { OF } from "./useOmegaForm"
7
+
8
+ export const useErrorLabel = (form: OF<any, any>) => {
9
+ const { formatMessage } = useIntl()
10
+ const humanize = (str: string) => {
11
+ return str
12
+ .replace(/([A-Z])/g, " $1") // Add space before capital letters
13
+ .replace(/^./, (char) => char.toUpperCase()) // Capitalize the first letter
14
+ .trim() // Remove leading/trailing spaces
15
+ }
16
+ const fallback = (propsName: string) =>
17
+ formatMessage
18
+ ? formatMessage({ id: `general.fields.${propsName}`, defaultMessage: humanize(propsName) })
19
+ : humanize(propsName)
20
+ const i18n = (propsName: string) =>
21
+ form.i18nNamespace
22
+ ? formatMessage({ id: `${form.i18nNamespace}.fields.${propsName}`, defaultMessage: fallback(propsName) })
23
+ : fallback(propsName)
24
+
25
+ return i18n
26
+ }
27
+
28
+ export const eHoc = (errorProps: {
29
+ form: OF<any, any>
30
+ fieldMap: Ref<Map<string, { id: string; label: string }>>
31
+ }) => {
32
+ return function FormHoc<P>(
33
+ WrappedComponent: Component<P>
34
+ ): ConcreteComponent<P> {
35
+ return {
36
+ setup() {
37
+ const { fieldMap, form } = errorProps
38
+ const generalErrors = form.useStore((state) => state.errors)
39
+ const fieldMeta = form.useStore((state) => state.fieldMeta)
40
+ const errorMap = form.useStore((state) => state.errorMap)
41
+
42
+ const errorLabel = useErrorLabel(form)
43
+
44
+ const errors = computed(() => {
45
+ // Collect errors from fieldMeta (field-level errors for registered fields)
46
+ const fieldErrors = Object.entries(fieldMeta.value).reduce<OmegaError[]>((acc, [key, m]) => {
47
+ const fieldErrors = (m as { errors?: Array<{ message?: string }> } | undefined)?.errors ?? []
48
+ if (!fieldErrors.length) {
49
+ return acc
50
+ }
51
+
52
+ const fieldInfo = fieldMap.value.get(key)
53
+ if (!fieldInfo) {
54
+ return acc
55
+ }
56
+
57
+ acc.push({
58
+ label: fieldInfo.label,
59
+ inputId: fieldInfo.id,
60
+ errors: [fieldErrors[0]?.message].filter(Boolean) as string[]
61
+ })
62
+
63
+ return acc
64
+ }, [])
65
+
66
+ // Collect errors from errorMap.onDynamic / errorMap.onSubmit ONLY for fields that are NOT registered
67
+ // (registered fields already have their errors in fieldMeta).
68
+ // Our localized standard schema writes to onDynamic via validationLogic: revalidateLogic();
69
+ // caller-provided validators.onSubmit (via tanstackFormOptions spread) writes to onSubmit.
70
+ const submitErrors: OmegaError[] = []
71
+ const submitIssueMaps = [errorMap.value.onDynamic, errorMap.value.onSubmit].filter(
72
+ Boolean
73
+ ) as unknown as Array<
74
+ Record<string, unknown>
75
+ >
76
+ const seenPaths = new Set<string>()
77
+ for (const issuesByPath of submitIssueMaps) {
78
+ for (const [_, issues] of Object.entries(issuesByPath)) {
79
+ if (Array.isArray(issues) && issues.length) {
80
+ for (const issue of issues) {
81
+ const issAny: any = issue
82
+ if (issAny?.path && Array.isArray(issAny.path) && issAny.path.length) {
83
+ const fieldPath = issAny.path.join(".")
84
+ if (!fieldMap.value.has(fieldPath) && !seenPaths.has(fieldPath)) {
85
+ seenPaths.add(fieldPath)
86
+ submitErrors.push({
87
+ label: errorLabel(fieldPath),
88
+ inputId: fieldPath,
89
+ errors: [issAny.message].filter(Boolean)
90
+ })
91
+ break
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ // Combine both error sources (no need to check for duplicates since they're mutually exclusive)
100
+ return [...fieldErrors, ...submitErrors]
101
+ })
102
+
103
+ return {
104
+ generalErrors,
105
+ errors
106
+ }
107
+ },
108
+ render({ errors, generalErrors }: any) {
109
+ return h(WrappedComponent, {
110
+ errors,
111
+ generalErrors,
112
+ ...this.$attrs
113
+ }, this.$slots)
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ export const makeFieldMap = () => {
120
+ const fieldMap = ref(new Map<string, { label: string; id: string }>())
121
+ const registerField = (field: ComputedRef<{ name: string; label: string; id: string }>) => {
122
+ watch(field, (f) => {
123
+ fieldMap.value.set(f.name, { label: f.label, id: f.id })
124
+ }, { immediate: true })
125
+ onUnmounted(() => {
126
+ // Only delete if we still own this entry (id matches)
127
+ // This prevents old components from deleting entries registered by new components
128
+ // during re-mount transitions (e.g., when :key changes)
129
+ const currentEntry = fieldMap.value.get(field.value.name)
130
+ if (currentEntry?.id === field.value.id) {
131
+ fieldMap.value.delete(field.value.name)
132
+ }
133
+ })
134
+ }
135
+ return { fieldMap, registerField }
136
+ }
@@ -1,6 +1,6 @@
1
1
  import { useStore } from "@tanstack/vue-form"
2
2
  import { computed, type Ref } from "vue"
3
- import type { OmegaFormApi, OmegaFormState } from "./OmegaFormStuff"
3
+ import type { OmegaFormApi, OmegaFormState } from "./types"
4
4
 
5
5
  export function getOmegaStore<
6
6
  To,
@@ -0,0 +1,19 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import { type Component, type ConcreteComponent, h } from "vue"
4
+ import type { OF } from "./useOmegaForm"
5
+
6
+ export const fHoc = (form: OF<any, any>) => {
7
+ return function FormHoc<P>(
8
+ WrappedComponent: Component<P>
9
+ ): ConcreteComponent<P> {
10
+ return {
11
+ render() {
12
+ return h(WrappedComponent, {
13
+ form,
14
+ ...this.$attrs
15
+ }, this.$slots)
16
+ }
17
+ }
18
+ }
19
+ }
@@ -1,7 +1,20 @@
1
- export * from "./OmegaFormStuff"
2
- export { type OmegaConfig, type OmegaFormReturn, useOmegaForm } from "./useOmegaForm"
1
+ import type * as S from "effect-app/Schema"
2
+
3
+ export { getInputType, type SupportedInputs } from "./inputs"
4
+ export { createMeta, generateMetaFromSchema, isNullableOrUndefined, metadataFromAst } from "./meta/createMeta"
5
+ export type { CreateMeta, FilterItems } from "./meta/createMeta"
6
+ export { defaultsValueFromSchema } from "./meta/defaults"
7
+ export { toFormSchema } from "./meta/redacted"
8
+ export type { BaseFieldMeta, BooleanFieldMeta, DateFieldMeta, FieldMeta, MetaRecord, MultipleFieldMeta, NestedKeyOf, NumberFieldMeta, SelectFieldMeta, StringFieldMeta, UnknownFieldMeta } from "./meta/types"
9
+ export { deepMerge } from "./persistency"
10
+ export type { BaseProps, DefaultTypeProps, FieldPath, FieldPath_, FieldValidators, FormComponent, FormProps, FormType, OmegaArrayProps, OmegaAutoGenMeta, OmegaError, OmegaFormApi, OmegaFormParams, OmegaFormState, OmegaInputProps, OmegaInputPropsBase, PrefixFromDepth, TypeOverride, TypesWithOptions } from "./types"
11
+ export { makeStandardSchemaV1Hooks, toLocalizedStandardSchemaV1 } from "./validation/localized"
12
+
13
+ export { FormErrors, OmegaFormKey, useErrorLabel, useOmegaForm } from "./useOmegaForm"
14
+ export type { defaultValuesPriorityUnion, OF, OmegaConfig, OmegaFormReturn, Policies } from "./useOmegaForm"
3
15
 
4
16
  export { type ExtractTagValue, type ExtractUnionBranch, type InputProps, type MergedInputProps, type TaggedUnionOption, type TaggedUnionOptionsArray, type TaggedUnionProps } from "./InputProps"
17
+ export type { VuetifyInputProps, VuetifyInputProps as OmegaRendererProps } from "./InputProps"
5
18
  export { default as OmegaInput } from "./OmegaInput.vue"
6
19
  export { default as OmegaVuetifyInput } from "./OmegaInternalInput.vue"
7
20
  export { default as OmegaTaggedUnion } from "./OmegaTaggedUnion.vue"
@@ -9,6 +22,6 @@ export { default as OmegaTaggedUnionInternal } from "./OmegaTaggedUnionInternal.
9
22
 
10
23
  export { useOnClose, usePreventClose } from "./blockDialog"
11
24
 
12
- export { getInputType } from "./OmegaFormStuff"
13
-
14
25
  export { createUseFormWithCustomInput } from "./createUseFormWithCustomInput"
26
+
27
+ export const duplicateSchema = <From, To>(schema: S.Codec<To, From>) => schema