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

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 (194) hide show
  1. package/README.md +2 -1
  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__/useFormFromSchema.test.d.ts +2 -0
  9. package/dist/__tests__/useFormFromSchema.test.d.ts.map +1 -0
  10. package/dist/__tests__/useMapoForm.test.d.ts +2 -0
  11. package/dist/__tests__/useMapoForm.test.d.ts.map +1 -0
  12. package/dist/index.d.mts +9 -1
  13. package/dist/index.d.ts +15 -1
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.mjs +7 -34
  16. package/dist/module.d.mts +20 -2
  17. package/dist/module.d.ts +12 -2
  18. package/dist/module.d.ts.map +1 -0
  19. package/dist/module.json +1 -1
  20. package/dist/module.mjs +187 -13
  21. package/dist/runtime/components/MapoFocusPortal.vue +115 -0
  22. package/dist/runtime/components/MapoFocusPortal.vue.d.ts +4 -0
  23. package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -0
  24. package/dist/runtime/components/MapoForm.vue +168 -0
  25. package/dist/runtime/components/MapoForm.vue.d.ts +47 -0
  26. package/dist/runtime/components/MapoForm.vue.d.ts.map +1 -0
  27. package/dist/runtime/components/MapoFormField.vue +269 -0
  28. package/dist/runtime/components/MapoFormField.vue.d.ts +46 -0
  29. package/dist/runtime/components/MapoFormField.vue.d.ts.map +1 -0
  30. package/dist/runtime/components/MapoFormFlatSection.vue +149 -0
  31. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts +15 -0
  32. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts.map +1 -0
  33. package/dist/runtime/components/MapoFormGroup.vue +247 -0
  34. package/dist/runtime/components/MapoFormGroup.vue.d.ts +44 -0
  35. package/dist/runtime/components/MapoFormGroup.vue.d.ts.map +1 -0
  36. package/dist/runtime/components/MapoFormTabs.vue +110 -0
  37. package/dist/runtime/components/MapoFormTabs.vue.d.ts +15 -0
  38. package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -0
  39. package/dist/runtime/components/MapoUnknownField.vue +62 -0
  40. package/dist/runtime/components/MapoUnknownField.vue.d.ts +16 -0
  41. package/dist/runtime/components/MapoUnknownField.vue.d.ts.map +1 -0
  42. package/dist/runtime/components/fields/MapoDateField.vue +88 -0
  43. package/dist/runtime/components/fields/MapoDateField.vue.d.ts +17 -0
  44. package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -0
  45. package/dist/runtime/components/fields/MapoDateTimeField.vue +159 -0
  46. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts +17 -0
  47. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -0
  48. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue +130 -0
  49. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts +16 -0
  50. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts.map +1 -0
  51. package/dist/runtime/components/fields/MapoFileField.vue +99 -0
  52. package/dist/runtime/components/fields/MapoFileField.vue.d.ts +16 -0
  53. package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -0
  54. package/dist/runtime/components/fields/MapoFksField.vue +122 -0
  55. package/dist/runtime/components/fields/MapoFksField.vue.d.ts +16 -0
  56. package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -0
  57. package/dist/runtime/components/fields/MapoMapField.vue +99 -0
  58. package/dist/runtime/components/fields/MapoMapField.vue.d.ts +22 -0
  59. package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -0
  60. package/dist/runtime/components/fields/MapoMapFieldClient.vue +67 -0
  61. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts +20 -0
  62. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts.map +1 -0
  63. package/dist/runtime/components/fields/MapoMediaField.vue +91 -0
  64. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts +16 -0
  65. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts.map +1 -0
  66. package/dist/runtime/components/fields/MapoMediaM2mField.vue +110 -0
  67. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts +16 -0
  68. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts.map +1 -0
  69. package/dist/runtime/components/fields/MapoMediaUnavailable.vue +15 -0
  70. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts +4 -0
  71. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts.map +1 -0
  72. package/dist/runtime/components/fields/MapoRepeater.vue +459 -0
  73. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts +16 -0
  74. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -0
  75. package/dist/runtime/components/fields/MapoRepeaterItem.vue +346 -0
  76. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts +55 -0
  77. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -0
  78. package/dist/runtime/components/fields/MapoSeoPreview.vue +122 -0
  79. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts +22 -0
  80. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -0
  81. package/dist/runtime/components/fields/MapoTimeField.vue +44 -0
  82. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts +17 -0
  83. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts.map +1 -0
  84. package/dist/runtime/components/fields/MapoWygEditor.vue +252 -0
  85. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts +16 -0
  86. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -0
  87. package/dist/runtime/components/fields/nui/NuiCheckbox.vue +9 -0
  88. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts +4 -0
  89. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts.map +1 -0
  90. package/dist/runtime/components/fields/nui/NuiInput.vue +9 -0
  91. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts +4 -0
  92. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts.map +1 -0
  93. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue +18 -0
  94. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts +4 -0
  95. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts.map +1 -0
  96. package/dist/runtime/components/fields/nui/NuiSlider.vue +9 -0
  97. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts +4 -0
  98. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts.map +1 -0
  99. package/dist/runtime/components/fields/nui/NuiSwitch.vue +9 -0
  100. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts +4 -0
  101. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts.map +1 -0
  102. package/dist/runtime/components/fields/nui/NuiTextarea.vue +9 -0
  103. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts +4 -0
  104. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts.map +1 -0
  105. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts +13 -0
  106. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts.map +1 -0
  107. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.js +19 -0
  108. package/dist/runtime/composables/defineFormField.d.ts +25 -0
  109. package/dist/runtime/composables/defineFormField.d.ts.map +1 -0
  110. package/dist/runtime/composables/defineFormField.js +27 -0
  111. package/dist/runtime/composables/useCurrentLang.d.ts +6 -0
  112. package/dist/runtime/composables/useCurrentLang.d.ts.map +1 -0
  113. package/dist/runtime/composables/useCurrentLang.js +9 -0
  114. package/dist/runtime/composables/useFocusMode.d.ts +40 -0
  115. package/dist/runtime/composables/useFocusMode.d.ts.map +1 -0
  116. package/dist/runtime/composables/useFocusMode.js +31 -0
  117. package/dist/runtime/composables/useFormDraft.d.ts +60 -0
  118. package/dist/runtime/composables/useFormDraft.d.ts.map +1 -0
  119. package/dist/runtime/composables/useFormDraft.js +52 -0
  120. package/dist/runtime/composables/useFormFromSchema.d.ts +21 -0
  121. package/dist/runtime/composables/useFormFromSchema.d.ts.map +1 -0
  122. package/dist/runtime/composables/useFormFromSchema.js +145 -0
  123. package/dist/runtime/composables/useMapoForm.d.ts +96 -0
  124. package/dist/runtime/composables/useMapoForm.d.ts.map +1 -0
  125. package/dist/runtime/composables/useMapoForm.js +325 -0
  126. package/dist/runtime/composables/useMediaManager.d.ts +17 -0
  127. package/dist/runtime/composables/useMediaManager.d.ts.map +1 -0
  128. package/dist/runtime/composables/useMediaManager.js +6 -0
  129. package/dist/runtime/composables/useProgressiveDisclosure.d.ts +44 -0
  130. package/dist/runtime/composables/useProgressiveDisclosure.d.ts.map +1 -0
  131. package/dist/runtime/composables/useProgressiveDisclosure.js +23 -0
  132. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts +24 -0
  133. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -0
  134. package/dist/runtime/composables/useUnsavedChangesGuard.js +28 -0
  135. package/dist/runtime/devtools/DevtoolsFormsPage.vue +235 -0
  136. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts +4 -0
  137. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts.map +1 -0
  138. package/dist/runtime/plugins/form-registry.d.ts +18 -0
  139. package/dist/runtime/plugins/form-registry.d.ts.map +1 -0
  140. package/dist/runtime/plugins/form-registry.js +19 -0
  141. package/dist/runtime/registry/defaults.d.ts +3 -0
  142. package/dist/runtime/registry/defaults.d.ts.map +1 -0
  143. package/dist/runtime/registry/defaults.js +93 -0
  144. package/dist/runtime/types/descriptor.d.ts +373 -0
  145. package/dist/runtime/types/descriptor.d.ts.map +1 -0
  146. package/dist/runtime/types/descriptor.js +26 -0
  147. package/dist/runtime/types/index.d.ts +12 -0
  148. package/dist/runtime/types/index.d.ts.map +1 -0
  149. package/dist/runtime/types/index.js +6 -0
  150. package/dist/runtime/types/layout.d.ts +21 -0
  151. package/dist/runtime/types/layout.d.ts.map +1 -0
  152. package/dist/runtime/types/layout.js +0 -0
  153. package/dist/runtime/types/media.d.ts +43 -0
  154. package/dist/runtime/types/media.d.ts.map +1 -0
  155. package/dist/runtime/types/media.js +0 -0
  156. package/dist/runtime/types/registry.d.ts +62 -0
  157. package/dist/runtime/types/registry.d.ts.map +1 -0
  158. package/dist/runtime/types/registry.js +19 -0
  159. package/dist/runtime/utils/flattenFieldGroups.d.ts +50 -0
  160. package/dist/runtime/utils/flattenFieldGroups.d.ts.map +1 -0
  161. package/dist/runtime/utils/flattenFieldGroups.js +24 -0
  162. package/dist/types.d.mts +6 -10
  163. package/package.json +10 -8
  164. package/src/__tests__/article.schema.json +193 -0
  165. package/src/__tests__/camomillaRoundTrip.test.ts +73 -0
  166. package/src/__tests__/useFormFromSchema.test.ts +24 -0
  167. package/src/__tests__/useMapoForm.test.ts +5 -0
  168. package/src/index.ts +5 -0
  169. package/src/module.ts +34 -0
  170. package/src/runtime/components/MapoFocusPortal.vue +3 -1
  171. package/src/runtime/components/MapoFormTabs.vue +4 -1
  172. package/src/runtime/components/fields/MapoDateField.vue +3 -1
  173. package/src/runtime/components/fields/MapoDateTimeField.vue +3 -1
  174. package/src/runtime/components/fields/MapoEnhancedMediaField.vue +153 -0
  175. package/src/runtime/components/fields/MapoFileField.vue +3 -1
  176. package/src/runtime/components/fields/MapoFksField.vue +7 -2
  177. package/src/runtime/components/fields/MapoMapField.vue +5 -2
  178. package/src/runtime/components/fields/MapoMediaField.vue +104 -0
  179. package/src/runtime/components/fields/MapoMediaM2mField.vue +130 -0
  180. package/src/runtime/components/fields/MapoMediaUnavailable.vue +19 -0
  181. package/src/runtime/components/fields/MapoRepeater.vue +33 -26
  182. package/src/runtime/components/fields/MapoRepeaterItem.vue +14 -8
  183. package/src/runtime/components/fields/MapoSeoPreview.vue +17 -13
  184. package/src/runtime/components/fields/MapoWygEditor.vue +47 -5
  185. package/src/runtime/composables/useFormFromSchema.ts +4 -1
  186. package/src/runtime/composables/useMapoForm.ts +4 -2
  187. package/src/runtime/composables/useMediaManager.ts +21 -0
  188. package/src/runtime/composables/useUnsavedChangesGuard.ts +3 -3
  189. package/src/runtime/registry/defaults.ts +9 -1
  190. package/src/runtime/types/descriptor.ts +22 -7
  191. package/src/runtime/types/index.ts +4 -0
  192. package/src/runtime/types/media.ts +44 -0
  193. package/dist/index.d.cts +0 -1
  194. package/dist/module.d.cts +0 -2
@@ -0,0 +1,346 @@
1
+ <script setup>
2
+ import { ref, computed, watch, watchEffect } from "vue";
3
+ import { useI18n } from "vue-i18n";
4
+ import { useMapoForm, injectMapoForm } from "../../composables/useMapoForm.js";
5
+ import { useFocusMode } from "../../composables/useFocusMode.js";
6
+ import MapoFormField from "../MapoFormField.vue";
7
+ const { t } = useI18n();
8
+ const props = defineProps({
9
+ uid: { type: String, required: false },
10
+ item: { type: Object, required: true },
11
+ fields: { type: Array, required: true },
12
+ index: { type: Number, required: true },
13
+ errorPrefix: { type: String, required: true },
14
+ parentErrors: { type: Object, required: true },
15
+ languages: { type: Array, required: true },
16
+ currentLang: { type: String, required: true },
17
+ readonly: { type: Boolean, required: true },
18
+ registry: { type: Object, required: true },
19
+ previewLabel: { type: Function, required: false },
20
+ defaultExpanded: { type: Boolean, required: true },
21
+ allowDuplicate: { type: Boolean, required: false },
22
+ repeaterDescriptor: { type: Object, required: false },
23
+ totalItems: { type: Number, required: false },
24
+ selected: { type: Boolean, required: false },
25
+ selectionMode: { type: Boolean, required: false }
26
+ });
27
+ const emit = defineEmits(["update:item", "delete", "duplicate", "move-up", "move-down", "move-to", "toggle-select"]);
28
+ function toggleSelectFromEvent(e) {
29
+ emit("toggle-select", e.shiftKey === true);
30
+ }
31
+ const expanded = ref(props.defaultExpanded);
32
+ const localModel = ref({ ...props.item });
33
+ watch(
34
+ () => props.item,
35
+ (next) => {
36
+ if (next === localModel.value) return;
37
+ localModel.value = { ...next };
38
+ }
39
+ );
40
+ watch(
41
+ localModel,
42
+ (val) => {
43
+ emit("update:item", val);
44
+ },
45
+ { deep: true }
46
+ );
47
+ const itemErrors = computed(() => {
48
+ const result = {};
49
+ const prefix = `${props.errorPrefix}.`;
50
+ for (const [k, v] of Object.entries(props.parentErrors)) {
51
+ if (k.startsWith(prefix)) result[k.slice(prefix.length)] = v;
52
+ }
53
+ return result;
54
+ });
55
+ const hasErrors = computed(() => Object.keys(itemErrors.value).length > 0);
56
+ const currentLangRef = computed(() => props.currentLang);
57
+ const readonly = computed(() => props.readonly);
58
+ const parentCtx = injectMapoForm();
59
+ const form = useMapoForm({
60
+ model: localModel,
61
+ fields: computed(() => props.fields),
62
+ errors: itemErrors,
63
+ languages: props.languages,
64
+ currentLang: currentLangRef,
65
+ immediate: parentCtx?.immediate ?? false,
66
+ debounce: parentCtx?.debounce ?? 300,
67
+ notifyErrors: false,
68
+ registry: props.registry
69
+ });
70
+ watchEffect(() => {
71
+ form.readonly.value = props.readonly;
72
+ if (parentCtx) form.ctx.submitted.value = parentCtx.submitted.value;
73
+ });
74
+ const label = computed(
75
+ () => props.previewLabel?.(localModel.value, props.index) ?? `Item ${props.index + 1}`
76
+ );
77
+ const miniCardAttrs = computed(() => props.repeaterDescriptor?.attrs);
78
+ const isCompressed = computed(() => {
79
+ if (expanded.value) return false;
80
+ const threshold = miniCardAttrs.value?.compressThreshold ?? 3;
81
+ const hasMiniCard = !!miniCardAttrs.value?.miniCard;
82
+ return hasMiniCard && (props.totalItems ?? 0) >= threshold;
83
+ });
84
+ const miniCard = computed(
85
+ () => miniCardAttrs.value?.miniCard?.(localModel.value, props.index)
86
+ );
87
+ const statusColorClass = computed(
88
+ () => ({
89
+ success: "bg-green-500",
90
+ info: "bg-blue-500",
91
+ warning: "bg-yellow-500",
92
+ error: "bg-red-500",
93
+ neutral: "bg-gray-400"
94
+ })[miniCard.value?.statusColor ?? "neutral"]
95
+ );
96
+ const focusMode = useFocusMode();
97
+ function enterFocusMode() {
98
+ focusMode.enter({
99
+ descriptor: props.repeaterDescriptor,
100
+ fields: props.fields,
101
+ model: { ...localModel.value },
102
+ errors: itemErrors,
103
+ onUpdate: (val) => {
104
+ localModel.value = { ...val };
105
+ emit("update:item", localModel.value);
106
+ },
107
+ registry: props.registry,
108
+ languages: props.languages,
109
+ currentLang: props.currentLang,
110
+ breadcrumb: [
111
+ props.repeaterDescriptor?.label ?? t("mapo.repeater.repeater"),
112
+ label.value
113
+ ]
114
+ });
115
+ }
116
+ </script>
117
+
118
+ <template>
119
+ <div
120
+ class="mapo-repeater-item rounded border transition-all"
121
+ :class="[
122
+ hasErrors ? 'border-red-300' : 'border-gray-200',
123
+ selected && selectionMode ? 'ring-2 ring-primary-500 ring-offset-1' : ''
124
+ ]"
125
+ >
126
+ <!-- ── Mini-card (contextual scaling) ──────────────────────────────────── -->
127
+ <div
128
+ v-if="isCompressed && miniCard"
129
+ class="flex cursor-pointer items-center gap-3 px-3 py-2 transition-colors hover:bg-gray-50"
130
+ @click="selectionMode ? toggleSelectFromEvent($event) : expanded = true"
131
+ >
132
+ <!-- Bulk selection checkbox -->
133
+ <UCheckbox
134
+ v-if="selectionMode"
135
+ :model-value="selected"
136
+ @update:model-value="emit('toggle-select', false)"
137
+ @click.stop
138
+ />
139
+
140
+ <!-- Optional thumbnail -->
141
+ <img
142
+ v-if="miniCard.thumbnail"
143
+ :src="miniCard.thumbnail"
144
+ class="h-9 w-9 rounded object-cover"
145
+ alt=""
146
+ />
147
+
148
+ <!-- Status dot -->
149
+ <span
150
+ v-if="miniCard.statusColor"
151
+ class="h-2 w-2 shrink-0 rounded-full"
152
+ :class="statusColorClass"
153
+ />
154
+
155
+ <!-- Drag handle (mousedown.stop prevents the parent click handler from firing on drag start) -->
156
+ <UIcon
157
+ name="i-lucide-grip-vertical"
158
+ class="drag-handle h-4 w-4 shrink-0 cursor-grab text-gray-300 active:cursor-grabbing"
159
+ @mousedown.stop
160
+ @click.stop
161
+ />
162
+
163
+ <!-- Contenuto testuale -->
164
+ <div class="min-w-0 flex-1">
165
+ <p class="truncate text-sm font-medium text-gray-700">
166
+ {{ miniCard.title }}
167
+ </p>
168
+ <p v-if="miniCard.subtitle" class="truncate text-xs text-gray-400">
169
+ {{ miniCard.subtitle }}
170
+ </p>
171
+ </div>
172
+
173
+ <!-- Error badge -->
174
+ <UBadge v-if="hasErrors" color="error" variant="soft" size="xs">
175
+ errors
176
+ </UBadge>
177
+
178
+ <!-- Quick actions (delete only in mini mode) -->
179
+ <div class="flex items-center gap-1">
180
+ <UButton
181
+ size="xs"
182
+ variant="ghost"
183
+ color="error"
184
+ icon="i-lucide-trash-2"
185
+ :disabled="readonly"
186
+ :title="t('mapo.delete')"
187
+ @click.stop="$emit('delete')"
188
+ />
189
+ <UIcon name="i-lucide-chevron-down" class="h-4 w-4 text-gray-400" />
190
+ </div>
191
+ </div>
192
+
193
+ <!-- ── Item header (expanded mode) ─────────────────────────────────────── -->
194
+ <div
195
+ v-else
196
+ class="flex items-center gap-2 bg-gray-50 px-3 py-2"
197
+ :class="selectionMode ? 'cursor-pointer hover:bg-gray-100' : ''"
198
+ @click="selectionMode && toggleSelectFromEvent($event)"
199
+ >
200
+ <!-- Bulk selection checkbox -->
201
+ <UCheckbox
202
+ v-if="selectionMode"
203
+ :model-value="selected"
204
+ @update:model-value="emit('toggle-select', false)"
205
+ @click.stop
206
+ />
207
+
208
+ <!-- Drag handle (mousedown.stop avoids triggering row-click selection on drag start) -->
209
+ <UIcon
210
+ name="i-lucide-grip-vertical"
211
+ class="drag-handle h-4 w-4 cursor-grab text-gray-400 active:cursor-grabbing"
212
+ @mousedown.stop
213
+ @click.stop
214
+ />
215
+
216
+ <!-- Collapsed label / error badge. In selection mode the row click handles selection,
217
+ so this button only toggles expansion when not selecting. -->
218
+ <button
219
+ type="button"
220
+ class="flex flex-1 items-center gap-2 text-left text-sm font-medium text-gray-700"
221
+ @click.stop="
222
+ selectionMode ? toggleSelectFromEvent($event) : expanded = !expanded
223
+ "
224
+ >
225
+ <span class="truncate">{{ label }}</span>
226
+ <UBadge v-if="hasErrors" color="error" variant="soft" size="xs">
227
+ errors
228
+ </UBadge>
229
+ </button>
230
+
231
+ <!-- Action buttons. All buttons stop propagation so the row-click selection handler
232
+ does not also toggle selection when the user clicks an action. -->
233
+ <div class="flex items-center gap-1" @click.stop>
234
+ <slot name="actions" />
235
+
236
+ <!-- Numeric position field (enabled via repeaterDescriptor.attrs.showPositionField) -->
237
+ <template
238
+ v-if="repeaterDescriptor?.attrs?.showPositionField && !readonly"
239
+ >
240
+ <UInput
241
+ :model-value="index + 1"
242
+ type="number"
243
+ :min="1"
244
+ :max="totalItems"
245
+ size="xs"
246
+ class="w-14 text-center"
247
+ :title="t('mapo.repeater.position')"
248
+ @change="
249
+ $emit(
250
+ 'move-to',
251
+ Math.max(
252
+ 0,
253
+ Math.min(
254
+ totalItems - 1,
255
+ Number($event.target.value) - 1
256
+ )
257
+ )
258
+ )
259
+ "
260
+ />
261
+ </template>
262
+
263
+ <!-- Focus mode -->
264
+ <UButton
265
+ size="xs"
266
+ variant="ghost"
267
+ color="neutral"
268
+ icon="i-lucide-maximize-2"
269
+ :title="t('mapo.repeater.editFullScreen')"
270
+ @click="enterFocusMode"
271
+ />
272
+
273
+ <UButton
274
+ size="xs"
275
+ variant="ghost"
276
+ color="neutral"
277
+ icon="i-lucide-chevron-up"
278
+ :disabled="readonly"
279
+ :title="t('mapo.repeater.moveUp')"
280
+ @click="$emit('move-up')"
281
+ />
282
+ <UButton
283
+ size="xs"
284
+ variant="ghost"
285
+ color="neutral"
286
+ icon="i-lucide-chevron-down"
287
+ :disabled="readonly"
288
+ :title="t('mapo.repeater.moveDown')"
289
+ @click="$emit('move-down')"
290
+ />
291
+
292
+ <UButton
293
+ v-if="allowDuplicate"
294
+ size="xs"
295
+ variant="ghost"
296
+ color="neutral"
297
+ icon="i-lucide-copy"
298
+ :disabled="readonly"
299
+ :title="t('mapo.duplicate')"
300
+ @click="$emit('duplicate')"
301
+ />
302
+
303
+ <UButton
304
+ size="xs"
305
+ variant="ghost"
306
+ color="error"
307
+ icon="i-lucide-trash-2"
308
+ :disabled="readonly"
309
+ :title="t('mapo.delete')"
310
+ @click="$emit('delete')"
311
+ />
312
+
313
+ <UIcon
314
+ :name="expanded ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
315
+ class="h-4 w-4 text-gray-400"
316
+ @click="expanded = !expanded"
317
+ />
318
+ </div>
319
+ </div>
320
+
321
+ <!-- ── Item body (fields) ──────────────────────────────────────────────── -->
322
+ <!-- COL_SPAN: Tailwind v4 requires static classes — do not use dynamic template literals. -->
323
+ <div v-if="expanded && !isCompressed" class="grid grid-cols-12 gap-5 p-4">
324
+ <div
325
+ v-for="field in fields"
326
+ :key="field.key"
327
+ :class="{
328
+ 'col-span-1': field.cols === 1,
329
+ 'col-span-2': field.cols === 2,
330
+ 'col-span-3': field.cols === 3,
331
+ 'col-span-4': field.cols === 4,
332
+ 'col-span-5': field.cols === 5,
333
+ 'col-span-6': field.cols === 6,
334
+ 'col-span-7': field.cols === 7,
335
+ 'col-span-8': field.cols === 8,
336
+ 'col-span-9': field.cols === 9,
337
+ 'col-span-10': field.cols === 10,
338
+ 'col-span-11': field.cols === 11,
339
+ 'col-span-12': !field.cols || field.cols === 12
340
+ }"
341
+ >
342
+ <MapoFormField :descriptor="field" />
343
+ </div>
344
+ </div>
345
+ </div>
346
+ </template>
@@ -0,0 +1,55 @@
1
+ import type { AnyFieldDescriptor, FieldRegistry, RepeaterDescriptor } from "../../types/index.js";
2
+ type __VLS_Props = {
3
+ /** Stable identity-based UID assigned by the parent repeater. */
4
+ uid?: string;
5
+ item: Record<string, unknown>;
6
+ fields: AnyFieldDescriptor[];
7
+ index: number;
8
+ errorPrefix: string;
9
+ parentErrors: Record<string, string[]>;
10
+ languages: string[];
11
+ currentLang: string;
12
+ readonly: boolean;
13
+ registry: FieldRegistry;
14
+ previewLabel?: (item: unknown, index: number) => string;
15
+ defaultExpanded: boolean;
16
+ allowDuplicate?: boolean;
17
+ /** Descriptor of the parent repeater (for miniCard and other attrs). */
18
+ repeaterDescriptor?: RepeaterDescriptor;
19
+ /** Total number of items in the repeater (used to compute contextual scaling). */
20
+ totalItems?: number;
21
+ /** true = this item is selected in bulk mode. */
22
+ selected?: boolean;
23
+ /** true = bulk selection mode is active in the parent repeater. */
24
+ selectionMode?: boolean;
25
+ };
26
+ declare var __VLS_53: {};
27
+ type __VLS_Slots = {} & {
28
+ actions?: (props: typeof __VLS_53) => any;
29
+ };
30
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
31
+ delete: () => any;
32
+ "update:item": (value: Record<string, unknown>) => any;
33
+ duplicate: () => any;
34
+ "move-up": () => any;
35
+ "move-down": () => any;
36
+ "move-to": (index: number) => any;
37
+ "toggle-select": (withShift: boolean) => any;
38
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
39
+ onDelete?: (() => any) | undefined;
40
+ "onUpdate:item"?: ((value: Record<string, unknown>) => any) | undefined;
41
+ onDuplicate?: (() => any) | undefined;
42
+ "onMove-up"?: (() => any) | undefined;
43
+ "onMove-down"?: (() => any) | undefined;
44
+ "onMove-to"?: ((index: number) => any) | undefined;
45
+ "onToggle-select"?: ((withShift: boolean) => any) | undefined;
46
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
47
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
48
+ declare const _default: typeof __VLS_export;
49
+ export default _default;
50
+ type __VLS_WithSlots<T, S> = T & {
51
+ new (): {
52
+ $slots: S;
53
+ };
54
+ };
55
+ //# sourceMappingURL=MapoRepeaterItem.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapoRepeaterItem.vue.d.ts","sourceRoot":"","sources":["../../../../src/runtime/components/fields/MapoRepeaterItem.vue"],"names":[],"mappings":"AAoaA,OAAO,KAAK,EACV,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAO9B,KAAK,WAAW,GAAG;IACjB,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,aAAa,CAAC;IACxB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACxD,eAAe,EAAE,OAAO,CAAC;IACzB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mEAAmE;IACnE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAszBF,QAAA,IAAI,QAAQ,IAAY,CAAE;AAC1B,KAAK,WAAW,GAAG,EAAE,GACnB;IAAE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,QAAQ,KAAK,GAAG,CAAA;CAAE,CAAC;AAKhD,QAAA,MAAM,UAAU;;;;;;;;;;;;;;;;kFAGd,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 { computed } from "vue";
3
+ import { useI18n } from "vue-i18n";
4
+ import { useRequestURL } from "#app";
5
+ const { t } = useI18n();
6
+ const props = defineProps({
7
+ modelValue: { type: null, required: true },
8
+ descriptor: { type: Object, required: true },
9
+ errors: { type: Array, required: false },
10
+ readonly: { type: Boolean, required: false },
11
+ disabled: { type: Boolean, required: false }
12
+ });
13
+ const emit = defineEmits(["update:modelValue"]);
14
+ const value = computed(() => {
15
+ const v = props.modelValue;
16
+ if (!v || typeof v !== "object")
17
+ return { title: "", description: "", permalink: "" };
18
+ return v;
19
+ });
20
+ function update(field, val) {
21
+ emit("update:modelValue", { ...value.value, [field]: val });
22
+ }
23
+ const titleLen = computed(() => (value.value.title ?? "").length);
24
+ const descLen = computed(() => (value.value.description ?? "").length);
25
+ const titleColor = computed(() => {
26
+ if (titleLen.value > 60) return "text-red-500";
27
+ if (titleLen.value > 50) return "text-amber-500";
28
+ return "text-green-600";
29
+ });
30
+ const descColor = computed(() => {
31
+ if (descLen.value > 155) return "text-red-500";
32
+ if (descLen.value > 130) return "text-amber-500";
33
+ return "text-green-600";
34
+ });
35
+ const requestUrl = useRequestURL();
36
+ const displayUrl = computed(() => value.value.permalink || requestUrl.host);
37
+ </script>
38
+
39
+ <template>
40
+ <div class="mapo-seo-preview space-y-4">
41
+ <!-- Input fields -->
42
+ <div class="space-y-3">
43
+ <div>
44
+ <div class="mb-1 flex items-center justify-between">
45
+ <label class="text-sm font-medium text-gray-700">{{
46
+ t("mapo.url")
47
+ }}</label>
48
+ </div>
49
+ <UInput
50
+ :model-value="value.permalink"
51
+ :readonly="readonly"
52
+ :disabled="disabled"
53
+ placeholder="https://example.com/slug"
54
+ class="w-full"
55
+ @update:model-value="update('permalink', $event)"
56
+ />
57
+ </div>
58
+
59
+ <div>
60
+ <div class="mb-1 flex items-center justify-between">
61
+ <label class="text-sm font-medium text-gray-700">{{
62
+ t("mapo.seoPreview.seoTitle")
63
+ }}</label>
64
+ <span class="text-xs" :class="titleColor">{{ titleLen }} / 60</span>
65
+ </div>
66
+ <UInput
67
+ :model-value="value.title"
68
+ :readonly="readonly"
69
+ :disabled="disabled"
70
+ :placeholder="t('mapo.seoPreview.titlePlaceholder')"
71
+ class="w-full"
72
+ @update:model-value="update('title', $event)"
73
+ />
74
+ </div>
75
+
76
+ <div>
77
+ <div class="mb-1 flex items-center justify-between">
78
+ <label class="text-sm font-medium text-gray-700">{{
79
+ t("mapo.seoPreview.metaDescription")
80
+ }}</label>
81
+ <span class="text-xs" :class="descColor">{{ descLen }} / 155</span>
82
+ </div>
83
+ <UTextarea
84
+ :model-value="value.description"
85
+ :rows="3"
86
+ :readonly="readonly"
87
+ :disabled="disabled"
88
+ :placeholder="t('mapo.seoPreview.descriptionPlaceholder')"
89
+ class="w-full"
90
+ @update:model-value="update('description', $event)"
91
+ />
92
+ </div>
93
+ </div>
94
+
95
+ <!-- SERP preview -->
96
+ <div class="rounded border border-gray-200 bg-gray-50 p-4">
97
+ <p class="mb-1 text-xs font-medium uppercase tracking-wide text-gray-400">
98
+ {{ t("mapo.seoPreview.googlePreview") }}
99
+ </p>
100
+ <div class="max-w-lg">
101
+ <p class="truncate text-sm text-green-700">
102
+ {{ displayUrl }}
103
+ </p>
104
+ <p class="truncate text-lg text-blue-700 underline">
105
+ {{ value.title || t("mapo.seoPreview.defaultTitle") }}
106
+ </p>
107
+ <p class="mt-1 line-clamp-2 text-sm text-gray-600">
108
+ {{ value.description || t("mapo.seoPreview.defaultDescription") }}
109
+ </p>
110
+ </div>
111
+ </div>
112
+
113
+ <p
114
+ v-for="err in errors"
115
+ :key="err"
116
+ class="text-sm text-red-500"
117
+ role="alert"
118
+ >
119
+ {{ err }}
120
+ </p>
121
+ </div>
122
+ </template>
@@ -0,0 +1,22 @@
1
+ import type { SeoDescriptor } from "../../types/index.js";
2
+ interface SeoValue {
3
+ title?: string;
4
+ description?: string;
5
+ /** Matches v1 key. `url` is accepted as alias via the registry accessor. */
6
+ permalink?: string;
7
+ }
8
+ type __VLS_Props = {
9
+ modelValue: unknown;
10
+ descriptor: SeoDescriptor;
11
+ errors?: string[];
12
+ readonly?: boolean;
13
+ disabled?: boolean;
14
+ };
15
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
16
+ "update:modelValue": (value: SeoValue) => any;
17
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
18
+ "onUpdate:modelValue"?: ((value: SeoValue) => any) | undefined;
19
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
+ declare const _default: typeof __VLS_export;
21
+ export default _default;
22
+ //# sourceMappingURL=MapoSeoPreview.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapoSeoPreview.vue.d.ts","sourceRoot":"","sources":["../../../../src/runtime/components/fields/MapoSeoPreview.vue"],"names":[],"mappings":"AAmJA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,UAAU,QAAQ;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID,KAAK,WAAW,GAAG;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,aAAa,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAySF,QAAA,MAAM,YAAY;;;;kFAGhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
@@ -0,0 +1,44 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { parseDateTime, CalendarDateTime } from "@internationalized/date";
4
+ const props = defineProps({
5
+ modelValue: { type: null, required: true },
6
+ descriptor: { type: Object, required: true },
7
+ errors: { type: Array, required: false },
8
+ readonly: { type: Boolean, required: false },
9
+ disabled: { type: Boolean, required: false }
10
+ });
11
+ const emit = defineEmits(["update:modelValue"]);
12
+ const calendarValue = computed(() => {
13
+ const v = props.modelValue;
14
+ if (!v) return void 0;
15
+ const normalized = v.includes("T") ? v : `1970-01-01T${v}`;
16
+ try {
17
+ return parseDateTime(normalized);
18
+ } catch {
19
+ return void 0;
20
+ }
21
+ });
22
+ function onUpdate(val) {
23
+ const time = val;
24
+ if (!time || typeof time.hour !== "number" || typeof time.minute !== "number") {
25
+ emit("update:modelValue", null);
26
+ return;
27
+ }
28
+ const h = String(time.hour).padStart(2, "0");
29
+ const m = String(time.minute).padStart(2, "0");
30
+ emit("update:modelValue", `${h}:${m}`);
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <UInputDate
36
+ :model-value="calendarValue"
37
+ granularity="minute"
38
+ hide-time-zone
39
+ :readonly="readonly"
40
+ :disabled="disabled"
41
+ :highlight="!!errors?.length"
42
+ @update:model-value="onUpdate"
43
+ />
44
+ </template>
@@ -0,0 +1,17 @@
1
+ import type { DateDescriptor } from "../../types/index.js";
2
+ /** Time-only field backed by `UInputDate` with minute granularity. */
3
+ type __VLS_Props = {
4
+ modelValue: unknown;
5
+ descriptor: DateDescriptor;
6
+ errors?: string[];
7
+ readonly?: boolean;
8
+ disabled?: boolean;
9
+ };
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ "update:modelValue": (value: string | null) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
13
+ "onUpdate:modelValue"?: ((value: string | null) => any) | undefined;
14
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
17
+ //# sourceMappingURL=MapoTimeField.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapoTimeField.vue.d.ts","sourceRoot":"","sources":["../../../../src/runtime/components/fields/MapoTimeField.vue"],"names":[],"mappings":"AAgEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,sEAAsE;AACtE,KAAK,WAAW,GAAG;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,cAAc,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AA2FF,QAAA,MAAM,YAAY;;;;kFAGhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}