@mapomodule/form 2.0.0-beta.1 → 2.0.0-beta.11

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 (202) hide show
  1. package/README.md +37 -118
  2. package/dist/__tests__/__mocks__/nuxt-imports.d.ts +4 -0
  3. package/dist/__tests__/__mocks__/nuxt-imports.d.ts.map +1 -0
  4. package/dist/__tests__/camomillaRoundTrip.test.d.ts +2 -0
  5. package/dist/__tests__/camomillaRoundTrip.test.d.ts.map +1 -0
  6. package/dist/__tests__/registry.test.d.ts +2 -0
  7. package/dist/__tests__/registry.test.d.ts.map +1 -0
  8. package/dist/__tests__/tabVisibility.test.d.ts +2 -0
  9. package/dist/__tests__/tabVisibility.test.d.ts.map +1 -0
  10. package/dist/__tests__/useFormFromSchema.test.d.ts +2 -0
  11. package/dist/__tests__/useFormFromSchema.test.d.ts.map +1 -0
  12. package/dist/__tests__/useMapoForm.test.d.ts +2 -0
  13. package/dist/__tests__/useMapoForm.test.d.ts.map +1 -0
  14. package/dist/index.d.mts +9 -1
  15. package/dist/index.d.ts +15 -1
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.mjs +7 -34
  18. package/dist/module.d.mts +20 -2
  19. package/dist/module.d.ts +12 -2
  20. package/dist/module.d.ts.map +1 -0
  21. package/dist/module.json +1 -1
  22. package/dist/module.mjs +187 -13
  23. package/dist/runtime/components/MapoFocusPortal.vue +115 -0
  24. package/dist/runtime/components/MapoFocusPortal.vue.d.ts +4 -0
  25. package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -0
  26. package/dist/runtime/components/MapoForm.vue +177 -0
  27. package/dist/runtime/components/MapoForm.vue.d.ts +47 -0
  28. package/dist/runtime/components/MapoForm.vue.d.ts.map +1 -0
  29. package/dist/runtime/components/MapoFormField.vue +269 -0
  30. package/dist/runtime/components/MapoFormField.vue.d.ts +46 -0
  31. package/dist/runtime/components/MapoFormField.vue.d.ts.map +1 -0
  32. package/dist/runtime/components/MapoFormFlatSection.vue +149 -0
  33. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts +15 -0
  34. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts.map +1 -0
  35. package/dist/runtime/components/MapoFormGroup.vue +247 -0
  36. package/dist/runtime/components/MapoFormGroup.vue.d.ts +44 -0
  37. package/dist/runtime/components/MapoFormGroup.vue.d.ts.map +1 -0
  38. package/dist/runtime/components/MapoFormTabs.vue +122 -0
  39. package/dist/runtime/components/MapoFormTabs.vue.d.ts +15 -0
  40. package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -0
  41. package/dist/runtime/components/MapoUnknownField.vue +62 -0
  42. package/dist/runtime/components/MapoUnknownField.vue.d.ts +16 -0
  43. package/dist/runtime/components/MapoUnknownField.vue.d.ts.map +1 -0
  44. package/dist/runtime/components/fields/MapoDateField.vue +88 -0
  45. package/dist/runtime/components/fields/MapoDateField.vue.d.ts +17 -0
  46. package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -0
  47. package/dist/runtime/components/fields/MapoDateTimeField.vue +159 -0
  48. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts +17 -0
  49. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -0
  50. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue +130 -0
  51. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts +16 -0
  52. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts.map +1 -0
  53. package/dist/runtime/components/fields/MapoFileField.vue +99 -0
  54. package/dist/runtime/components/fields/MapoFileField.vue.d.ts +16 -0
  55. package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -0
  56. package/dist/runtime/components/fields/MapoFksField.vue +122 -0
  57. package/dist/runtime/components/fields/MapoFksField.vue.d.ts +16 -0
  58. package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -0
  59. package/dist/runtime/components/fields/MapoMapField.vue +99 -0
  60. package/dist/runtime/components/fields/MapoMapField.vue.d.ts +22 -0
  61. package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -0
  62. package/dist/runtime/components/fields/MapoMapFieldClient.vue +67 -0
  63. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts +20 -0
  64. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts.map +1 -0
  65. package/dist/runtime/components/fields/MapoMediaField.vue +93 -0
  66. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts +16 -0
  67. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts.map +1 -0
  68. package/dist/runtime/components/fields/MapoMediaM2mField.vue +111 -0
  69. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts +16 -0
  70. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts.map +1 -0
  71. package/dist/runtime/components/fields/MapoMediaUnavailable.vue +15 -0
  72. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts +4 -0
  73. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts.map +1 -0
  74. package/dist/runtime/components/fields/MapoRepeater.vue +459 -0
  75. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts +16 -0
  76. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -0
  77. package/dist/runtime/components/fields/MapoRepeaterItem.vue +346 -0
  78. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts +55 -0
  79. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -0
  80. package/dist/runtime/components/fields/MapoSeoPreview.vue +122 -0
  81. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts +22 -0
  82. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -0
  83. package/dist/runtime/components/fields/MapoTimeField.vue +44 -0
  84. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts +17 -0
  85. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts.map +1 -0
  86. package/dist/runtime/components/fields/MapoWygEditor.vue +258 -0
  87. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts +16 -0
  88. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -0
  89. package/dist/runtime/components/fields/nui/NuiCheckbox.vue +9 -0
  90. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts +4 -0
  91. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts.map +1 -0
  92. package/dist/runtime/components/fields/nui/NuiInput.vue +9 -0
  93. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts +4 -0
  94. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts.map +1 -0
  95. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue +18 -0
  96. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts +4 -0
  97. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts.map +1 -0
  98. package/dist/runtime/components/fields/nui/NuiSlider.vue +9 -0
  99. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts +4 -0
  100. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts.map +1 -0
  101. package/dist/runtime/components/fields/nui/NuiSwitch.vue +9 -0
  102. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts +4 -0
  103. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts.map +1 -0
  104. package/dist/runtime/components/fields/nui/NuiTextarea.vue +9 -0
  105. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts +4 -0
  106. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts.map +1 -0
  107. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts +13 -0
  108. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts.map +1 -0
  109. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.js +19 -0
  110. package/dist/runtime/composables/defineFormField.d.ts +25 -0
  111. package/dist/runtime/composables/defineFormField.d.ts.map +1 -0
  112. package/dist/runtime/composables/defineFormField.js +27 -0
  113. package/dist/runtime/composables/useCurrentLang.d.ts +6 -0
  114. package/dist/runtime/composables/useCurrentLang.d.ts.map +1 -0
  115. package/dist/runtime/composables/useCurrentLang.js +9 -0
  116. package/dist/runtime/composables/useFocusMode.d.ts +40 -0
  117. package/dist/runtime/composables/useFocusMode.d.ts.map +1 -0
  118. package/dist/runtime/composables/useFocusMode.js +31 -0
  119. package/dist/runtime/composables/useFormDraft.d.ts +60 -0
  120. package/dist/runtime/composables/useFormDraft.d.ts.map +1 -0
  121. package/dist/runtime/composables/useFormDraft.js +52 -0
  122. package/dist/runtime/composables/useFormFromSchema.d.ts +21 -0
  123. package/dist/runtime/composables/useFormFromSchema.d.ts.map +1 -0
  124. package/dist/runtime/composables/useFormFromSchema.js +145 -0
  125. package/dist/runtime/composables/useMapoForm.d.ts +96 -0
  126. package/dist/runtime/composables/useMapoForm.d.ts.map +1 -0
  127. package/dist/runtime/composables/useMapoForm.js +325 -0
  128. package/dist/runtime/composables/useMediaManager.d.ts +17 -0
  129. package/dist/runtime/composables/useMediaManager.d.ts.map +1 -0
  130. package/dist/runtime/composables/useMediaManager.js +6 -0
  131. package/dist/runtime/composables/useProgressiveDisclosure.d.ts +44 -0
  132. package/dist/runtime/composables/useProgressiveDisclosure.d.ts.map +1 -0
  133. package/dist/runtime/composables/useProgressiveDisclosure.js +23 -0
  134. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts +24 -0
  135. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -0
  136. package/dist/runtime/composables/useUnsavedChangesGuard.js +28 -0
  137. package/dist/runtime/devtools/DevtoolsFormsPage.vue +235 -0
  138. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts +4 -0
  139. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts.map +1 -0
  140. package/dist/runtime/plugins/form-registry.d.ts +18 -0
  141. package/dist/runtime/plugins/form-registry.d.ts.map +1 -0
  142. package/dist/runtime/plugins/form-registry.js +19 -0
  143. package/dist/runtime/registry/defaults.d.ts +3 -0
  144. package/dist/runtime/registry/defaults.d.ts.map +1 -0
  145. package/dist/runtime/registry/defaults.js +93 -0
  146. package/dist/runtime/types/descriptor.d.ts +373 -0
  147. package/dist/runtime/types/descriptor.d.ts.map +1 -0
  148. package/dist/runtime/types/descriptor.js +26 -0
  149. package/dist/runtime/types/index.d.ts +12 -0
  150. package/dist/runtime/types/index.d.ts.map +1 -0
  151. package/dist/runtime/types/index.js +6 -0
  152. package/dist/runtime/types/layout.d.ts +21 -0
  153. package/dist/runtime/types/layout.d.ts.map +1 -0
  154. package/dist/runtime/types/layout.js +0 -0
  155. package/dist/runtime/types/media.d.ts +43 -0
  156. package/dist/runtime/types/media.d.ts.map +1 -0
  157. package/dist/runtime/types/media.js +0 -0
  158. package/dist/runtime/types/registry.d.ts +62 -0
  159. package/dist/runtime/types/registry.d.ts.map +1 -0
  160. package/dist/runtime/types/registry.js +19 -0
  161. package/dist/runtime/utils/flattenFieldGroups.d.ts +50 -0
  162. package/dist/runtime/utils/flattenFieldGroups.d.ts.map +1 -0
  163. package/dist/runtime/utils/flattenFieldGroups.js +24 -0
  164. package/dist/runtime/utils/tabVisibility.d.ts +18 -0
  165. package/dist/runtime/utils/tabVisibility.d.ts.map +1 -0
  166. package/dist/runtime/utils/tabVisibility.js +18 -0
  167. package/dist/types.d.mts +6 -10
  168. package/package.json +10 -8
  169. package/src/__tests__/article.schema.json +193 -0
  170. package/src/__tests__/camomillaRoundTrip.test.ts +73 -0
  171. package/src/__tests__/tabVisibility.test.ts +102 -0
  172. package/src/__tests__/useFormFromSchema.test.ts +24 -0
  173. package/src/__tests__/useMapoForm.test.ts +5 -0
  174. package/src/index.ts +5 -0
  175. package/src/module.ts +34 -0
  176. package/src/runtime/components/MapoFocusPortal.vue +3 -1
  177. package/src/runtime/components/MapoForm.vue +17 -4
  178. package/src/runtime/components/MapoFormTabs.vue +29 -6
  179. package/src/runtime/components/fields/MapoDateField.vue +3 -1
  180. package/src/runtime/components/fields/MapoDateTimeField.vue +3 -1
  181. package/src/runtime/components/fields/MapoEnhancedMediaField.vue +153 -0
  182. package/src/runtime/components/fields/MapoFileField.vue +3 -1
  183. package/src/runtime/components/fields/MapoFksField.vue +7 -2
  184. package/src/runtime/components/fields/MapoMapField.vue +5 -2
  185. package/src/runtime/components/fields/MapoMediaField.vue +106 -0
  186. package/src/runtime/components/fields/MapoMediaM2mField.vue +131 -0
  187. package/src/runtime/components/fields/MapoMediaUnavailable.vue +19 -0
  188. package/src/runtime/components/fields/MapoRepeater.vue +33 -26
  189. package/src/runtime/components/fields/MapoRepeaterItem.vue +14 -8
  190. package/src/runtime/components/fields/MapoSeoPreview.vue +17 -13
  191. package/src/runtime/components/fields/MapoWygEditor.vue +55 -7
  192. package/src/runtime/composables/useFormFromSchema.ts +4 -1
  193. package/src/runtime/composables/useMapoForm.ts +4 -2
  194. package/src/runtime/composables/useMediaManager.ts +21 -0
  195. package/src/runtime/composables/useUnsavedChangesGuard.ts +3 -3
  196. package/src/runtime/registry/defaults.ts +9 -1
  197. package/src/runtime/types/descriptor.ts +22 -7
  198. package/src/runtime/types/index.ts +4 -0
  199. package/src/runtime/types/media.ts +44 -0
  200. package/src/runtime/utils/tabVisibility.ts +44 -0
  201. package/dist/index.d.cts +0 -1
  202. package/dist/module.d.cts +0 -2
@@ -0,0 +1,149 @@
1
+ <script setup>
2
+ import { ref, computed, provide, onMounted, onUnmounted } from "vue";
3
+ import { injectMapoForm } from "../composables/useMapoForm.js";
4
+ import MapoFormField from "./MapoFormField.vue";
5
+ const props = defineProps({
6
+ fields: { type: Array, required: true }
7
+ });
8
+ defineSlots();
9
+ const form = injectMapoForm();
10
+ const model = computed(() => form?.model.value);
11
+ const currentLang = computed(() => form?.currentLang.value ?? "");
12
+ const expandedFieldKey = ref(null);
13
+ const expandedFieldLabel = computed(() => {
14
+ if (!expandedFieldKey.value) return "";
15
+ const field = props.fields.find(
16
+ (f) => f.key === expandedFieldKey.value
17
+ );
18
+ return field?.label ?? expandedFieldKey.value;
19
+ });
20
+ function expandField(key) {
21
+ expandedFieldKey.value = key;
22
+ document.body.style.overflow = "hidden";
23
+ }
24
+ function collapseField() {
25
+ expandedFieldKey.value = null;
26
+ document.body.style.overflow = "";
27
+ }
28
+ function onKeydown(e) {
29
+ if (e.key === "Escape" && expandedFieldKey.value) collapseField();
30
+ }
31
+ onMounted(() => document.addEventListener("keydown", onKeydown));
32
+ onUnmounted(() => {
33
+ document.removeEventListener("keydown", onKeydown);
34
+ document.body.style.overflow = "";
35
+ });
36
+ provide("mapoGroupExpand", { expandField, collapseField, expandedFieldKey });
37
+ const COL_SPAN = {
38
+ 1: "col-span-1",
39
+ 2: "col-span-2",
40
+ 3: "col-span-3",
41
+ 4: "col-span-4",
42
+ 5: "col-span-5",
43
+ 6: "col-span-6",
44
+ 7: "col-span-7",
45
+ 8: "col-span-8",
46
+ 9: "col-span-9",
47
+ 10: "col-span-10",
48
+ 11: "col-span-11",
49
+ 12: "col-span-12"
50
+ };
51
+ function colClass(cols) {
52
+ if (!cols || typeof cols !== "number") return "col-span-12";
53
+ return COL_SPAN[cols] ?? "col-span-12";
54
+ }
55
+ </script>
56
+
57
+ <template>
58
+ <!-- Expanded panel: teleported to body -->
59
+ <Teleport to="body">
60
+ <Transition name="mapo-expand">
61
+ <div
62
+ v-if="expandedFieldKey"
63
+ class="fixed inset-0 z-50 flex flex-col"
64
+ style="padding: 1rem"
65
+ >
66
+ <div
67
+ class="absolute inset-0 bg-black/50 backdrop-blur-sm"
68
+ @click="collapseField"
69
+ />
70
+ <div
71
+ class="relative z-10 flex h-full flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl"
72
+ >
73
+ <div
74
+ class="flex shrink-0 items-center justify-between border-b border-gray-100 px-5 py-3.5"
75
+ >
76
+ <span class="text-sm font-semibold tracking-wide text-gray-700">{{
77
+ expandedFieldLabel
78
+ }}</span>
79
+ <button
80
+ type="button"
81
+ class="rounded p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
82
+ @click="collapseField"
83
+ >
84
+ <UIcon name="i-lucide-minimize-2" class="h-4 w-4" />
85
+ </button>
86
+ </div>
87
+ <div
88
+ class="mapo-group-expand-body flex min-h-0 flex-1 flex-col p-5"
89
+ style="overflow: hidden"
90
+ >
91
+ <template v-for="field in fields" :key="field.key">
92
+ <div
93
+ v-if="expandedFieldKey === field.key"
94
+ style="
95
+ display: flex;
96
+ flex-direction: column;
97
+ flex: 1;
98
+ min-height: 0;
99
+ height: 100%;
100
+ "
101
+ >
102
+ <slot
103
+ :name="`field.${field.key}`"
104
+ :field="field"
105
+ :model="model"
106
+ :current-lang="currentLang"
107
+ >
108
+ <MapoFormField
109
+ :descriptor="field"
110
+ style="flex: 1; min-height: 0; height: 100%"
111
+ >
112
+ <template
113
+ v-for="(_, slotName) in $slots"
114
+ #[slotName]="slotProps"
115
+ >
116
+ <slot :name="slotName" v-bind="slotProps ?? {}" />
117
+ </template>
118
+ </MapoFormField>
119
+ </slot>
120
+ </div>
121
+ </template>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </Transition>
126
+ </Teleport>
127
+
128
+ <!-- Flat grid -->
129
+ <div class="grid grid-cols-12 gap-5">
130
+ <div
131
+ v-for="field in fields"
132
+ :key="field.key"
133
+ :class="colClass(field.cols)"
134
+ >
135
+ <slot
136
+ :name="`field.${field.key}`"
137
+ :field="field"
138
+ :model="model"
139
+ :current-lang="currentLang"
140
+ >
141
+ <MapoFormField :descriptor="field">
142
+ <template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
143
+ <slot :name="slotName" v-bind="slotProps ?? {}" />
144
+ </template>
145
+ </MapoFormField>
146
+ </slot>
147
+ </div>
148
+ </div>
149
+ </template>
@@ -0,0 +1,15 @@
1
+ import type { AnyFieldDescriptor } from "../types/index.js";
2
+ type __VLS_Props = {
3
+ fields: AnyFieldDescriptor[];
4
+ };
5
+ type __VLS_Slots = Record<string, unknown>;
6
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
7
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
8
+ declare const _default: typeof __VLS_export;
9
+ export default _default;
10
+ type __VLS_WithSlots<T, S> = T & {
11
+ new (): {
12
+ $slots: S;
13
+ };
14
+ };
15
+ //# sourceMappingURL=MapoFormFlatSection.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapoFormFlatSection.vue.d.ts","sourceRoot":"","sources":["../../../src/runtime/components/MapoFormFlatSection.vue"],"names":[],"mappings":"AAuKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAI5D,KAAK,WAAW,GAAG;IAAE,MAAM,EAAE,kBAAkB,EAAE,CAAA;CAAE,CAAC;AAGpD,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAmS3C,QAAA,MAAM,UAAU,kSAEd,CAAC;AACH,QAAA,MAAM,YAAY,EAAS,eAAe,CAAC,OAAO,UAAU,EAAE,WAAW,CAAC,CAAC;wBACtD,OAAO,YAAY;AAAxC,wBAAyC;AACzC,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
@@ -0,0 +1,247 @@
1
+ <script setup>
2
+ import {
3
+ ref,
4
+ computed,
5
+ provide,
6
+ watchEffect,
7
+ onMounted,
8
+ onUnmounted
9
+ } from "vue";
10
+ import { useRuntimeConfig } from "#app";
11
+ import { injectMapoForm } from "../composables/useMapoForm.js";
12
+ import MapoFormField from "./MapoFormField.vue";
13
+ const props = defineProps({
14
+ name: { type: String, required: true },
15
+ label: { type: String, required: false },
16
+ fields: { type: Array, required: true },
17
+ subtabs: { type: Map, required: false },
18
+ initialExpanded: { type: Boolean, required: false }
19
+ });
20
+ const globalExpanded = useRuntimeConfig().public.mapoForm?.groups?.expanded ?? true;
21
+ const expanded = ref(props.initialExpanded ?? globalExpanded);
22
+ const activeSubtab = ref("");
23
+ watchEffect(() => {
24
+ if (props.subtabs && props.subtabs.size > 0 && !activeSubtab.value) {
25
+ activeSubtab.value = [...props.subtabs.keys()][0] ?? "";
26
+ }
27
+ });
28
+ const expandedFieldKey = ref(null);
29
+ function expandField(key) {
30
+ expandedFieldKey.value = key;
31
+ document.body.style.overflow = "hidden";
32
+ }
33
+ function collapseField() {
34
+ expandedFieldKey.value = null;
35
+ document.body.style.overflow = "";
36
+ }
37
+ function onKeydown(e) {
38
+ if (e.key === "Escape" && expandedFieldKey.value) collapseField();
39
+ }
40
+ onMounted(() => document.addEventListener("keydown", onKeydown));
41
+ onUnmounted(() => {
42
+ document.removeEventListener("keydown", onKeydown);
43
+ document.body.style.overflow = "";
44
+ });
45
+ provide("mapoGroupExpand", { expandField, collapseField, expandedFieldKey });
46
+ const form = injectMapoForm();
47
+ const model = computed(() => form?.model.value);
48
+ const currentLang = computed(() => form?.currentLang.value ?? "");
49
+ const COL_SPAN = {
50
+ 1: "col-span-1",
51
+ 2: "col-span-2",
52
+ 3: "col-span-3",
53
+ 4: "col-span-4",
54
+ 5: "col-span-5",
55
+ 6: "col-span-6",
56
+ 7: "col-span-7",
57
+ 8: "col-span-8",
58
+ 9: "col-span-9",
59
+ 10: "col-span-10",
60
+ 11: "col-span-11",
61
+ 12: "col-span-12"
62
+ };
63
+ function colClass(cols) {
64
+ if (!cols || typeof cols !== "number") return "col-span-12";
65
+ return COL_SPAN[cols] ?? "col-span-12";
66
+ }
67
+ </script>
68
+
69
+ <template>
70
+ <!-- Expanded panel: teleported to body to avoid parent overflow/transform interference. -->
71
+ <Teleport to="body">
72
+ <Transition name="mapo-expand">
73
+ <div
74
+ v-if="expandedFieldKey"
75
+ class="fixed inset-0 z-50 flex flex-col"
76
+ style="padding: 1rem"
77
+ >
78
+ <!-- Clickable backdrop -->
79
+ <div
80
+ class="absolute inset-0 bg-black/50 backdrop-blur-sm"
81
+ @click="collapseField"
82
+ />
83
+
84
+ <!-- Panel -->
85
+ <div
86
+ class="relative z-10 flex h-full flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl"
87
+ >
88
+ <!-- Header -->
89
+ <div
90
+ class="flex shrink-0 items-center justify-between border-b border-gray-100 px-5 py-3.5"
91
+ >
92
+ <span class="text-sm font-semibold tracking-wide text-gray-700">{{
93
+ label ?? name
94
+ }}</span>
95
+ <button
96
+ type="button"
97
+ class="rounded p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
98
+ @click="collapseField"
99
+ >
100
+ <UIcon name="i-lucide-minimize-2" class="h-4 w-4" />
101
+ </button>
102
+ </div>
103
+
104
+ <!-- Expanded field -->
105
+ <div
106
+ class="mapo-group-expand-body flex min-h-0 flex-1 flex-col p-5"
107
+ style="overflow: hidden"
108
+ >
109
+ <template v-for="field in fields" :key="field.key">
110
+ <div
111
+ v-if="expandedFieldKey === field.key"
112
+ style="
113
+ display: flex;
114
+ flex-direction: column;
115
+ flex: 1;
116
+ min-height: 0;
117
+ height: 100%;
118
+ "
119
+ >
120
+ <slot
121
+ :name="`field.${field.key}`"
122
+ :field="field"
123
+ :model="model"
124
+ :current-lang="currentLang"
125
+ >
126
+ <MapoFormField
127
+ :descriptor="field"
128
+ style="flex: 1; min-height: 0; height: 100%"
129
+ >
130
+ <template
131
+ v-for="(_, slotName) in $slots"
132
+ #[slotName]="slotProps"
133
+ >
134
+ <slot :name="slotName" v-bind="slotProps ?? {}" />
135
+ </template>
136
+ </MapoFormField>
137
+ </slot>
138
+ </div>
139
+ </template>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </Transition>
144
+ </Teleport>
145
+
146
+ <!-- Regular group (kept in the DOM so the provide stays active) -->
147
+ <div
148
+ class="mapo-form-group relative overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm"
149
+ >
150
+ <!-- Collapsible heading -->
151
+ <button
152
+ type="button"
153
+ class="flex w-full items-center justify-between px-5 py-3.5 text-left transition-colors hover:bg-gray-50"
154
+ @click="expanded = !expanded"
155
+ >
156
+ <span class="text-sm font-semibold tracking-wide text-gray-700">{{
157
+ label ?? name
158
+ }}</span>
159
+ <UIcon
160
+ :name="expanded ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
161
+ class="h-4 w-4 shrink-0 text-gray-400"
162
+ />
163
+ </button>
164
+
165
+ <!-- Body -->
166
+ <div v-if="expanded">
167
+ <!-- Flat fields (no subtab) -->
168
+ <div
169
+ v-if="fields.length"
170
+ class="grid grid-cols-12 gap-5 px-5 pb-5 pt-4"
171
+ :class="{ 'pb-0': subtabs && subtabs.size > 0 }"
172
+ >
173
+ <div
174
+ v-for="field in fields"
175
+ :key="field.key"
176
+ :class="colClass(field.cols)"
177
+ >
178
+ <slot
179
+ :name="`field.${field.key}`"
180
+ :field="field"
181
+ :model="model"
182
+ :current-lang="currentLang"
183
+ >
184
+ <MapoFormField :descriptor="field">
185
+ <template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
186
+ <slot :name="slotName" v-bind="slotProps ?? {}" />
187
+ </template>
188
+ </MapoFormField>
189
+ </slot>
190
+ </div>
191
+ </div>
192
+
193
+ <!-- Sub-tab bar + content (when subtabs are present) -->
194
+ <div v-if="subtabs && subtabs.size > 0" class="px-5 pb-5 pt-4">
195
+ <!-- Sub-tab bar -->
196
+ <div class="flex gap-1 border-b border-gray-200 mb-4">
197
+ <button
198
+ v-for="[subtabName] of subtabs"
199
+ :key="subtabName"
200
+ type="button"
201
+ class="relative px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none"
202
+ :class="[
203
+ activeSubtab === subtabName ? 'text-primary-600 border-b-2 border-primary-600 -mb-px' : 'text-gray-500 hover:text-gray-700'
204
+ ]"
205
+ @click="activeSubtab = subtabName"
206
+ >
207
+ {{ subtabName }}
208
+ </button>
209
+ </div>
210
+
211
+ <!-- Sub-tab fields -->
212
+ <div
213
+ v-for="[subtabName, subtabFields] of subtabs"
214
+ v-show="activeSubtab === subtabName"
215
+ :key="subtabName"
216
+ class="grid grid-cols-12 gap-5"
217
+ >
218
+ <div
219
+ v-for="field in subtabFields"
220
+ :key="field.key"
221
+ :class="colClass(field.cols)"
222
+ >
223
+ <slot
224
+ :name="`field.${field.key}`"
225
+ :field="field"
226
+ :model="model"
227
+ :current-lang="currentLang"
228
+ >
229
+ <MapoFormField :descriptor="field">
230
+ <template
231
+ v-for="(_, slotName) in $slots"
232
+ #[slotName]="slotProps"
233
+ >
234
+ <slot :name="slotName" v-bind="slotProps ?? {}" />
235
+ </template>
236
+ </MapoFormField>
237
+ </slot>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </template>
244
+
245
+ <style>
246
+ .mapo-expand-enter-active,.mapo-expand-leave-active{transition:opacity .2s ease,transform .2s ease}.mapo-expand-enter-from,.mapo-expand-leave-to{opacity:0;transform:scale(.97)}
247
+ </style>
@@ -0,0 +1,44 @@
1
+ import type { AnyFieldDescriptor } from "../types/index.js";
2
+ type __VLS_Props = {
3
+ name: string;
4
+ label?: string;
5
+ fields: AnyFieldDescriptor[];
6
+ subtabs?: Map<string, AnyFieldDescriptor[]>;
7
+ initialExpanded?: boolean;
8
+ };
9
+ declare var __VLS_19: `field.${string}`, __VLS_20: {
10
+ field: AnyFieldDescriptor;
11
+ model: object | undefined;
12
+ currentLang: string;
13
+ }, __VLS_30: string, __VLS_31: any, __VLS_39: `field.${string}`, __VLS_40: {
14
+ field: AnyFieldDescriptor;
15
+ model: object | undefined;
16
+ currentLang: string;
17
+ }, __VLS_50: string, __VLS_51: any, __VLS_54: `field.${string}`, __VLS_55: {
18
+ field: AnyFieldDescriptor;
19
+ model: object | undefined;
20
+ currentLang: string;
21
+ }, __VLS_65: string, __VLS_66: any;
22
+ type __VLS_Slots = {} & {
23
+ [K in NonNullable<typeof __VLS_19>]?: (props: typeof __VLS_20) => any;
24
+ } & {
25
+ [K in NonNullable<typeof __VLS_30>]?: (props: typeof __VLS_31) => any;
26
+ } & {
27
+ [K in NonNullable<typeof __VLS_39>]?: (props: typeof __VLS_40) => any;
28
+ } & {
29
+ [K in NonNullable<typeof __VLS_50>]?: (props: typeof __VLS_51) => any;
30
+ } & {
31
+ [K in NonNullable<typeof __VLS_54>]?: (props: typeof __VLS_55) => any;
32
+ } & {
33
+ [K in NonNullable<typeof __VLS_65>]?: (props: typeof __VLS_66) => any;
34
+ };
35
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
36
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
37
+ declare const _default: typeof __VLS_export;
38
+ export default _default;
39
+ type __VLS_WithSlots<T, S> = T & {
40
+ new (): {
41
+ $slots: S;
42
+ };
43
+ };
44
+ //# sourceMappingURL=MapoFormGroup.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapoFormGroup.vue.d.ts","sourceRoot":"","sources":["../../../src/runtime/components/MapoFormGroup.vue"],"names":[],"mappings":"AAuSA,OAAO,KAAK,EACV,kBAAkB,EAEnB,MAAM,mBAAmB,CAAC;AAI3B,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAC5C,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAidF,QAAA,IAAI,QAAQ,mBAAW,EAAE,QAAQ;;;;CAAW,EAAE,QAAQ,QAAW,EAAE,QAAQ,KAAW,EAAE,QAAQ,mBAAW,EAAE,QAAQ;;;;CAAW,EAAE,QAAQ,QAAW,EAAE,QAAQ,KAAW,EAAE,QAAQ,mBAAW,EAAE,QAAQ;;;;CAAW,EAAE,QAAQ,QAAW,EAAE,QAAQ,KAAY,CAAE;AACjQ,KAAK,WAAW,GAAG,EAAE,GACnB;KAAG,CAAC,IAAI,WAAW,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,QAAQ,KAAK,GAAG;CAAE,GACzE;KAAG,CAAC,IAAI,WAAW,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,QAAQ,KAAK,GAAG;CAAE,GACzE;KAAG,CAAC,IAAI,WAAW,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,QAAQ,KAAK,GAAG;CAAE,GACzE;KAAG,CAAC,IAAI,WAAW,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,QAAQ,KAAK,GAAG;CAAE,GACzE;KAAG,CAAC,IAAI,WAAW,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,QAAQ,KAAK,GAAG;CAAE,GACzE;KAAG,CAAC,IAAI,WAAW,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,QAAQ,KAAK,GAAG;CAAE,CAAC;AAG5E,QAAA,MAAM,UAAU,kSAEd,CAAC;AACH,QAAA,MAAM,YAAY,EAAS,eAAe,CAAC,OAAO,UAAU,EAAE,WAAW,CAAC,CAAC;wBACtD,OAAO,YAAY;AAAxC,wBAAyC;AACzC,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
@@ -0,0 +1,122 @@
1
+ <script setup>
2
+ import { ref, computed } from "vue";
3
+ import { useI18n } from "vue-i18n";
4
+ import { injectMapoForm } from "../composables/useMapoForm.js";
5
+ import { pickActiveTab, tabHasShownFields } from "../utils/tabVisibility.js";
6
+ import MapoFormGroup from "./MapoFormGroup.vue";
7
+ import MapoFormFlatSection from "./MapoFormFlatSection.vue";
8
+ const { t } = useI18n();
9
+ const props = defineProps({
10
+ tabs: { type: Array, required: true }
11
+ });
12
+ const slots = defineSlots();
13
+ const form = injectMapoForm();
14
+ function tabHasContent(tab) {
15
+ const hasPanelSlot = Boolean(slots[`tab.${tab.name}`]);
16
+ const hasBeforeSlot = Boolean(slots[`tab.${tab.name}.before`]);
17
+ const hasAfterSlot = Boolean(slots[`tab.${tab.name}.after`]);
18
+ if (hasPanelSlot || hasBeforeSlot || hasAfterSlot) return true;
19
+ return tabHasShownFields(tab, form.model.value);
20
+ }
21
+ const shownTabs = computed(() => props.tabs.filter(tabHasContent));
22
+ const pickedTab = ref(null);
23
+ const activeTab = computed(
24
+ () => pickActiveTab(shownTabs.value, pickedTab.value)
25
+ );
26
+ function tabHasErrors(tab, errorKeys) {
27
+ const directError = [...tab.groups.values()].some(
28
+ (g) => g.fields.some((f) => errorKeys.has(f.key))
29
+ );
30
+ if (directError) return true;
31
+ return [...tab.children.values()].some(
32
+ (child) => tabHasErrors(child, errorKeys)
33
+ );
34
+ }
35
+ const tabsWithErrors = computed(() => {
36
+ const errorKeys = new Set(Object.keys(form.errors.value));
37
+ return new Set(
38
+ props.tabs.filter((tab) => tabHasErrors(tab, errorKeys)).map((t2) => t2.name)
39
+ );
40
+ });
41
+ </script>
42
+
43
+ <template>
44
+ <div class="mapo-form-tabs">
45
+ <!-- Tab bar -->
46
+ <div class="flex gap-1 border-b border-gray-200 pb-px">
47
+ <button
48
+ v-for="tab in shownTabs"
49
+ :key="tab.name"
50
+ type="button"
51
+ class="relative flex items-center gap-1.5 rounded-t px-4 py-2 text-sm font-medium transition-colors"
52
+ :class="[
53
+ activeTab === tab.name ? 'border-b-2 border-primary-500 text-primary-600' : 'text-gray-500 hover:bg-gray-50 hover:text-gray-700'
54
+ ]"
55
+ @click="pickedTab = tab.name"
56
+ >
57
+ <slot :name="`tab.${tab.name}.label`" :tab="tab">
58
+ {{ tab.label ?? tab.name }}
59
+ </slot>
60
+ <span
61
+ v-if="tabsWithErrors.has(tab.name)"
62
+ class="inline-block h-1.5 w-1.5 rounded-full bg-red-500"
63
+ :aria-label="t('mapo.form.tabErrors')"
64
+ />
65
+ </button>
66
+ <!-- Extra content appended to the tab bar (actions, badges, …). -->
67
+ <slot name="tab-bar-end" />
68
+ </div>
69
+
70
+ <!-- Tab content -->
71
+ <div
72
+ v-for="tab in tabs"
73
+ v-show="activeTab === tab.name"
74
+ :key="tab.name"
75
+ class="space-y-5 pt-5"
76
+ >
77
+ <!-- Full panel override: `tab.{name}` replaces this tab's content entirely. -->
78
+ <slot
79
+ v-if="$slots[`tab.${tab.name}`]"
80
+ :name="`tab.${tab.name}`"
81
+ :tab="tab"
82
+ />
83
+ <template v-else>
84
+ <slot :name="`tab.${tab.name}.before`" :tab="tab" />
85
+ <!-- Direct groups at this level -->
86
+ <template v-for="[groupName, group] of tab.groups" :key="groupName">
87
+ <template v-if="groupName !== '__flat__'">
88
+ <slot :name="`group.${groupName}.before`" />
89
+ <MapoFormGroup
90
+ :name="groupName"
91
+ :label="group.label"
92
+ :fields="group.fields"
93
+ :subtabs="group.subtabs"
94
+ >
95
+ <template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
96
+ <slot :name="slotName" v-bind="slotProps ?? {}" />
97
+ </template>
98
+ </MapoFormGroup>
99
+ <slot :name="`group.${groupName}.after`" />
100
+ </template>
101
+
102
+ <MapoFormFlatSection v-else :fields="group.fields">
103
+ <template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
104
+ <slot :name="slotName" v-bind="slotProps ?? {}" />
105
+ </template>
106
+ </MapoFormFlatSection>
107
+ </template>
108
+
109
+ <!-- Nested sub-tabs (recursive) -->
110
+ <MapoFormTabs
111
+ v-if="tab.children.size > 0"
112
+ :tabs="[...tab.children.values()]"
113
+ >
114
+ <template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
115
+ <slot :name="slotName" v-bind="slotProps ?? {}" />
116
+ </template>
117
+ </MapoFormTabs>
118
+ <slot :name="`tab.${tab.name}.after`" :tab="tab" />
119
+ </template>
120
+ </div>
121
+ </div>
122
+ </template>
@@ -0,0 +1,15 @@
1
+ import type { TabEntry } from "../types/layout.js";
2
+ type __VLS_Props = {
3
+ tabs: TabEntry[];
4
+ };
5
+ type __VLS_Slots = Record<string, unknown>;
6
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
7
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
8
+ declare const _default: typeof __VLS_export;
9
+ export default _default;
10
+ type __VLS_WithSlots<T, S> = T & {
11
+ new (): {
12
+ $slots: S;
13
+ };
14
+ };
15
+ //# sourceMappingURL=MapoFormTabs.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapoFormTabs.vue.d.ts","sourceRoot":"","sources":["../../../src/runtime/components/MapoFormTabs.vue"],"names":[],"mappings":"AAgJA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAQnD,KAAK,WAAW,GAAG;IAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;CAAE,CAAC;AAGxC,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA2P3C,QAAA,MAAM,UAAU,kSAEd,CAAC;AACH,QAAA,MAAM,YAAY,EAAS,eAAe,CAAC,OAAO,UAAU,EAAE,WAAW,CAAC,CAAC;wBACtD,OAAO,YAAY;AAAxC,wBAAyC;AACzC,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
@@ -0,0 +1,62 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ const props = defineProps({
4
+ modelValue: { type: null, required: true },
5
+ descriptor: { type: null, required: true },
6
+ errors: { type: Array, required: false },
7
+ readonly: { type: Boolean, required: false },
8
+ disabled: { type: Boolean, required: false }
9
+ });
10
+ const emit = defineEmits(["update:modelValue"]);
11
+ const jsonValue = computed(() => {
12
+ try {
13
+ return JSON.stringify(props.modelValue, null, 2);
14
+ } catch {
15
+ return String(props.modelValue);
16
+ }
17
+ });
18
+ const draft = ref(jsonValue.value);
19
+ const parseError = ref(null);
20
+ watch(jsonValue, (val) => {
21
+ draft.value = val;
22
+ parseError.value = null;
23
+ });
24
+ function onInput(val) {
25
+ draft.value = String(val);
26
+ try {
27
+ const parsed = JSON.parse(draft.value);
28
+ parseError.value = null;
29
+ emit("update:modelValue", parsed);
30
+ } catch (e) {
31
+ parseError.value = e.message;
32
+ }
33
+ }
34
+ if (import.meta.dev) {
35
+ console.warn(
36
+ `[Mapo] Field type "${props.descriptor.type}" (key: "${props.descriptor.key}") is not in the registry. Rendered as MapoUnknownField.`
37
+ );
38
+ }
39
+ </script>
40
+
41
+ <template>
42
+ <div class="rounded border border-yellow-400 bg-yellow-50 p-3 text-sm">
43
+ <p class="font-mono font-semibold text-yellow-700">
44
+ Unknown field: <code>{{ descriptor.key }}</code>
45
+ <span class="ml-2 text-yellow-500">(type: {{ descriptor.type }})</span>
46
+ </p>
47
+ <UTextarea
48
+ class="mt-2 font-mono text-xs"
49
+ :model-value="draft"
50
+ :rows="4"
51
+ :readonly="readonly || disabled"
52
+ :disabled="disabled"
53
+ @update:model-value="onInput"
54
+ />
55
+ <p v-if="parseError" class="mt-1 text-xs text-yellow-700">
56
+ Invalid JSON — changes are not applied until the value becomes parseable.
57
+ </p>
58
+ <p v-for="err in errors" :key="err" class="mt-1 text-red-500">
59
+ {{ err }}
60
+ </p>
61
+ </div>
62
+ </template>