@mapomodule/form 2.0.0-beta.0 → 2.0.0-beta.2

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 (144) hide show
  1. package/dist/__tests__/__mocks__/nuxt-imports.d.ts +4 -0
  2. package/dist/__tests__/__mocks__/nuxt-imports.d.ts.map +1 -0
  3. package/dist/__tests__/registry.test.d.ts +2 -0
  4. package/dist/__tests__/registry.test.d.ts.map +1 -0
  5. package/dist/__tests__/useFormFromSchema.test.d.ts +2 -0
  6. package/dist/__tests__/useFormFromSchema.test.d.ts.map +1 -0
  7. package/dist/__tests__/useMapoForm.test.d.ts +2 -0
  8. package/dist/__tests__/useMapoForm.test.d.ts.map +1 -0
  9. package/dist/index.d.mts +9 -1
  10. package/dist/index.d.ts +15 -1
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.mjs +7 -34
  13. package/dist/module.d.mts +20 -2
  14. package/dist/module.d.ts +12 -2
  15. package/dist/module.d.ts.map +1 -0
  16. package/dist/module.json +1 -1
  17. package/dist/module.mjs +167 -13
  18. package/dist/runtime/components/MapoFocusPortal.vue +113 -0
  19. package/dist/runtime/components/MapoFocusPortal.vue.d.ts +4 -0
  20. package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -0
  21. package/dist/runtime/components/MapoForm.vue +168 -0
  22. package/dist/runtime/components/MapoForm.vue.d.ts +47 -0
  23. package/dist/runtime/components/MapoForm.vue.d.ts.map +1 -0
  24. package/dist/runtime/components/MapoFormField.vue +269 -0
  25. package/dist/runtime/components/MapoFormField.vue.d.ts +46 -0
  26. package/dist/runtime/components/MapoFormField.vue.d.ts.map +1 -0
  27. package/dist/runtime/components/MapoFormFlatSection.vue +149 -0
  28. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts +15 -0
  29. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts.map +1 -0
  30. package/dist/runtime/components/MapoFormGroup.vue +247 -0
  31. package/dist/runtime/components/MapoFormGroup.vue.d.ts +44 -0
  32. package/dist/runtime/components/MapoFormGroup.vue.d.ts.map +1 -0
  33. package/dist/runtime/components/MapoFormTabs.vue +108 -0
  34. package/dist/runtime/components/MapoFormTabs.vue.d.ts +15 -0
  35. package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -0
  36. package/dist/runtime/components/MapoUnknownField.vue +62 -0
  37. package/dist/runtime/components/MapoUnknownField.vue.d.ts +16 -0
  38. package/dist/runtime/components/MapoUnknownField.vue.d.ts.map +1 -0
  39. package/dist/runtime/components/fields/MapoDateField.vue +86 -0
  40. package/dist/runtime/components/fields/MapoDateField.vue.d.ts +17 -0
  41. package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -0
  42. package/dist/runtime/components/fields/MapoDateTimeField.vue +157 -0
  43. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts +17 -0
  44. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -0
  45. package/dist/runtime/components/fields/MapoFileField.vue +97 -0
  46. package/dist/runtime/components/fields/MapoFileField.vue.d.ts +16 -0
  47. package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -0
  48. package/dist/runtime/components/fields/MapoFksField.vue +118 -0
  49. package/dist/runtime/components/fields/MapoFksField.vue.d.ts +16 -0
  50. package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -0
  51. package/dist/runtime/components/fields/MapoMapField.vue +97 -0
  52. package/dist/runtime/components/fields/MapoMapField.vue.d.ts +22 -0
  53. package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -0
  54. package/dist/runtime/components/fields/MapoMapFieldClient.vue +67 -0
  55. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts +20 -0
  56. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts.map +1 -0
  57. package/dist/runtime/components/fields/MapoRepeater.vue +449 -0
  58. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts +16 -0
  59. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -0
  60. package/dist/runtime/components/fields/MapoRepeaterItem.vue +341 -0
  61. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts +55 -0
  62. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -0
  63. package/dist/runtime/components/fields/MapoSeoPreview.vue +118 -0
  64. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts +22 -0
  65. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -0
  66. package/dist/runtime/components/fields/MapoTimeField.vue +44 -0
  67. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts +17 -0
  68. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts.map +1 -0
  69. package/dist/runtime/components/fields/MapoWygEditor.vue +227 -0
  70. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts +16 -0
  71. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -0
  72. package/dist/runtime/components/fields/nui/NuiCheckbox.vue +9 -0
  73. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts +4 -0
  74. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts.map +1 -0
  75. package/dist/runtime/components/fields/nui/NuiInput.vue +9 -0
  76. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts +4 -0
  77. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts.map +1 -0
  78. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue +18 -0
  79. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts +4 -0
  80. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts.map +1 -0
  81. package/dist/runtime/components/fields/nui/NuiSlider.vue +9 -0
  82. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts +4 -0
  83. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts.map +1 -0
  84. package/dist/runtime/components/fields/nui/NuiSwitch.vue +9 -0
  85. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts +4 -0
  86. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts.map +1 -0
  87. package/dist/runtime/components/fields/nui/NuiTextarea.vue +9 -0
  88. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts +4 -0
  89. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts.map +1 -0
  90. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts +13 -0
  91. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts.map +1 -0
  92. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.js +19 -0
  93. package/dist/runtime/composables/defineFormField.d.ts +25 -0
  94. package/dist/runtime/composables/defineFormField.d.ts.map +1 -0
  95. package/dist/runtime/composables/defineFormField.js +27 -0
  96. package/dist/runtime/composables/useCurrentLang.d.ts +6 -0
  97. package/dist/runtime/composables/useCurrentLang.d.ts.map +1 -0
  98. package/dist/runtime/composables/useCurrentLang.js +9 -0
  99. package/dist/runtime/composables/useFocusMode.d.ts +40 -0
  100. package/dist/runtime/composables/useFocusMode.d.ts.map +1 -0
  101. package/dist/runtime/composables/useFocusMode.js +31 -0
  102. package/dist/runtime/composables/useFormDraft.d.ts +60 -0
  103. package/dist/runtime/composables/useFormDraft.d.ts.map +1 -0
  104. package/dist/runtime/composables/useFormDraft.js +52 -0
  105. package/dist/runtime/composables/useFormFromSchema.d.ts +21 -0
  106. package/dist/runtime/composables/useFormFromSchema.d.ts.map +1 -0
  107. package/dist/runtime/composables/useFormFromSchema.js +142 -0
  108. package/dist/runtime/composables/useMapoForm.d.ts +96 -0
  109. package/dist/runtime/composables/useMapoForm.d.ts.map +1 -0
  110. package/dist/runtime/composables/useMapoForm.js +323 -0
  111. package/dist/runtime/composables/useProgressiveDisclosure.d.ts +44 -0
  112. package/dist/runtime/composables/useProgressiveDisclosure.d.ts.map +1 -0
  113. package/dist/runtime/composables/useProgressiveDisclosure.js +23 -0
  114. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts +24 -0
  115. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -0
  116. package/dist/runtime/composables/useUnsavedChangesGuard.js +27 -0
  117. package/dist/runtime/devtools/DevtoolsFormsPage.vue +235 -0
  118. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts +4 -0
  119. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts.map +1 -0
  120. package/dist/runtime/plugins/form-registry.d.ts +18 -0
  121. package/dist/runtime/plugins/form-registry.d.ts.map +1 -0
  122. package/dist/runtime/plugins/form-registry.js +19 -0
  123. package/dist/runtime/registry/defaults.d.ts +3 -0
  124. package/dist/runtime/registry/defaults.d.ts.map +1 -0
  125. package/dist/runtime/registry/defaults.js +87 -0
  126. package/dist/runtime/types/descriptor.d.ts +359 -0
  127. package/dist/runtime/types/descriptor.d.ts.map +1 -0
  128. package/dist/runtime/types/descriptor.js +26 -0
  129. package/dist/runtime/types/index.d.ts +11 -0
  130. package/dist/runtime/types/index.d.ts.map +1 -0
  131. package/dist/runtime/types/index.js +6 -0
  132. package/dist/runtime/types/layout.d.ts +21 -0
  133. package/dist/runtime/types/layout.d.ts.map +1 -0
  134. package/dist/runtime/types/layout.js +0 -0
  135. package/dist/runtime/types/registry.d.ts +62 -0
  136. package/dist/runtime/types/registry.d.ts.map +1 -0
  137. package/dist/runtime/types/registry.js +19 -0
  138. package/dist/runtime/utils/flattenFieldGroups.d.ts +50 -0
  139. package/dist/runtime/utils/flattenFieldGroups.d.ts.map +1 -0
  140. package/dist/runtime/utils/flattenFieldGroups.js +24 -0
  141. package/dist/types.d.mts +6 -10
  142. package/package.json +4 -4
  143. package/dist/index.d.cts +0 -1
  144. package/dist/module.d.cts +0 -2
@@ -0,0 +1,341 @@
1
+ <script setup>
2
+ import { ref, computed, watch, watchEffect } from "vue";
3
+ import { useMapoForm, injectMapoForm } from "../../composables/useMapoForm.js";
4
+ import { useFocusMode } from "../../composables/useFocusMode.js";
5
+ import MapoFormField from "../MapoFormField.vue";
6
+ const props = defineProps({
7
+ uid: { type: String, required: false },
8
+ item: { type: Object, required: true },
9
+ fields: { type: Array, required: true },
10
+ index: { type: Number, required: true },
11
+ errorPrefix: { type: String, required: true },
12
+ parentErrors: { type: Object, required: true },
13
+ languages: { type: Array, required: true },
14
+ currentLang: { type: String, required: true },
15
+ readonly: { type: Boolean, required: true },
16
+ registry: { type: Object, required: true },
17
+ previewLabel: { type: Function, required: false },
18
+ defaultExpanded: { type: Boolean, required: true },
19
+ allowDuplicate: { type: Boolean, required: false },
20
+ repeaterDescriptor: { type: Object, required: false },
21
+ totalItems: { type: Number, required: false },
22
+ selected: { type: Boolean, required: false },
23
+ selectionMode: { type: Boolean, required: false }
24
+ });
25
+ const emit = defineEmits(["update:item", "delete", "duplicate", "move-up", "move-down", "move-to", "toggle-select"]);
26
+ function toggleSelectFromEvent(e) {
27
+ emit("toggle-select", e.shiftKey === true);
28
+ }
29
+ const expanded = ref(props.defaultExpanded);
30
+ const localModel = ref({ ...props.item });
31
+ watch(
32
+ () => props.item,
33
+ (next) => {
34
+ if (next === localModel.value) return;
35
+ localModel.value = { ...next };
36
+ }
37
+ );
38
+ watch(
39
+ localModel,
40
+ (val) => {
41
+ emit("update:item", val);
42
+ },
43
+ { deep: true }
44
+ );
45
+ const itemErrors = computed(() => {
46
+ const result = {};
47
+ const prefix = `${props.errorPrefix}.`;
48
+ for (const [k, v] of Object.entries(props.parentErrors)) {
49
+ if (k.startsWith(prefix)) result[k.slice(prefix.length)] = v;
50
+ }
51
+ return result;
52
+ });
53
+ const hasErrors = computed(() => Object.keys(itemErrors.value).length > 0);
54
+ const currentLangRef = computed(() => props.currentLang);
55
+ const readonly = computed(() => props.readonly);
56
+ const parentCtx = injectMapoForm();
57
+ const form = useMapoForm({
58
+ model: localModel,
59
+ fields: computed(() => props.fields),
60
+ errors: itemErrors,
61
+ languages: props.languages,
62
+ currentLang: currentLangRef,
63
+ immediate: parentCtx?.immediate ?? false,
64
+ debounce: parentCtx?.debounce ?? 300,
65
+ notifyErrors: false,
66
+ registry: props.registry
67
+ });
68
+ watchEffect(() => {
69
+ form.readonly.value = props.readonly;
70
+ if (parentCtx) form.ctx.submitted.value = parentCtx.submitted.value;
71
+ });
72
+ const label = computed(
73
+ () => props.previewLabel?.(localModel.value, props.index) ?? `Item ${props.index + 1}`
74
+ );
75
+ const miniCardAttrs = computed(() => props.repeaterDescriptor?.attrs);
76
+ const isCompressed = computed(() => {
77
+ if (expanded.value) return false;
78
+ const threshold = miniCardAttrs.value?.compressThreshold ?? 3;
79
+ const hasMiniCard = !!miniCardAttrs.value?.miniCard;
80
+ return hasMiniCard && (props.totalItems ?? 0) >= threshold;
81
+ });
82
+ const miniCard = computed(
83
+ () => miniCardAttrs.value?.miniCard?.(localModel.value, props.index)
84
+ );
85
+ const statusColorClass = computed(
86
+ () => ({
87
+ success: "bg-green-500",
88
+ info: "bg-blue-500",
89
+ warning: "bg-yellow-500",
90
+ error: "bg-red-500",
91
+ neutral: "bg-gray-400"
92
+ })[miniCard.value?.statusColor ?? "neutral"]
93
+ );
94
+ const focusMode = useFocusMode();
95
+ function enterFocusMode() {
96
+ focusMode.enter({
97
+ descriptor: props.repeaterDescriptor,
98
+ fields: props.fields,
99
+ model: { ...localModel.value },
100
+ errors: itemErrors,
101
+ onUpdate: (val) => {
102
+ localModel.value = { ...val };
103
+ emit("update:item", localModel.value);
104
+ },
105
+ registry: props.registry,
106
+ languages: props.languages,
107
+ currentLang: props.currentLang,
108
+ breadcrumb: [props.repeaterDescriptor?.label ?? "Repeater", label.value]
109
+ });
110
+ }
111
+ </script>
112
+
113
+ <template>
114
+ <div
115
+ class="mapo-repeater-item rounded border transition-all"
116
+ :class="[
117
+ hasErrors ? 'border-red-300' : 'border-gray-200',
118
+ selected && selectionMode ? 'ring-2 ring-primary-500 ring-offset-1' : ''
119
+ ]"
120
+ >
121
+ <!-- ── Mini-card (contextual scaling) ──────────────────────────────────── -->
122
+ <div
123
+ v-if="isCompressed && miniCard"
124
+ class="flex cursor-pointer items-center gap-3 px-3 py-2 transition-colors hover:bg-gray-50"
125
+ @click="selectionMode ? toggleSelectFromEvent($event) : expanded = true"
126
+ >
127
+ <!-- Bulk selection checkbox -->
128
+ <UCheckbox
129
+ v-if="selectionMode"
130
+ :model-value="selected"
131
+ @update:model-value="emit('toggle-select', false)"
132
+ @click.stop
133
+ />
134
+
135
+ <!-- Optional thumbnail -->
136
+ <img
137
+ v-if="miniCard.thumbnail"
138
+ :src="miniCard.thumbnail"
139
+ class="h-9 w-9 rounded object-cover"
140
+ alt=""
141
+ />
142
+
143
+ <!-- Status dot -->
144
+ <span
145
+ v-if="miniCard.statusColor"
146
+ class="h-2 w-2 shrink-0 rounded-full"
147
+ :class="statusColorClass"
148
+ />
149
+
150
+ <!-- Drag handle (mousedown.stop prevents the parent click handler from firing on drag start) -->
151
+ <UIcon
152
+ name="i-lucide-grip-vertical"
153
+ class="drag-handle h-4 w-4 shrink-0 cursor-grab text-gray-300 active:cursor-grabbing"
154
+ @mousedown.stop
155
+ @click.stop
156
+ />
157
+
158
+ <!-- Contenuto testuale -->
159
+ <div class="min-w-0 flex-1">
160
+ <p class="truncate text-sm font-medium text-gray-700">
161
+ {{ miniCard.title }}
162
+ </p>
163
+ <p v-if="miniCard.subtitle" class="truncate text-xs text-gray-400">
164
+ {{ miniCard.subtitle }}
165
+ </p>
166
+ </div>
167
+
168
+ <!-- Error badge -->
169
+ <UBadge v-if="hasErrors" color="error" variant="soft" size="xs">
170
+ errors
171
+ </UBadge>
172
+
173
+ <!-- Quick actions (delete only in mini mode) -->
174
+ <div class="flex items-center gap-1">
175
+ <UButton
176
+ size="xs"
177
+ variant="ghost"
178
+ color="error"
179
+ icon="i-lucide-trash-2"
180
+ :disabled="readonly"
181
+ title="Delete"
182
+ @click.stop="$emit('delete')"
183
+ />
184
+ <UIcon name="i-lucide-chevron-down" class="h-4 w-4 text-gray-400" />
185
+ </div>
186
+ </div>
187
+
188
+ <!-- ── Item header (expanded mode) ─────────────────────────────────────── -->
189
+ <div
190
+ v-else
191
+ class="flex items-center gap-2 bg-gray-50 px-3 py-2"
192
+ :class="selectionMode ? 'cursor-pointer hover:bg-gray-100' : ''"
193
+ @click="selectionMode && toggleSelectFromEvent($event)"
194
+ >
195
+ <!-- Bulk selection checkbox -->
196
+ <UCheckbox
197
+ v-if="selectionMode"
198
+ :model-value="selected"
199
+ @update:model-value="emit('toggle-select', false)"
200
+ @click.stop
201
+ />
202
+
203
+ <!-- Drag handle (mousedown.stop avoids triggering row-click selection on drag start) -->
204
+ <UIcon
205
+ name="i-lucide-grip-vertical"
206
+ class="drag-handle h-4 w-4 cursor-grab text-gray-400 active:cursor-grabbing"
207
+ @mousedown.stop
208
+ @click.stop
209
+ />
210
+
211
+ <!-- Collapsed label / error badge. In selection mode the row click handles selection,
212
+ so this button only toggles expansion when not selecting. -->
213
+ <button
214
+ type="button"
215
+ class="flex flex-1 items-center gap-2 text-left text-sm font-medium text-gray-700"
216
+ @click.stop="
217
+ selectionMode ? toggleSelectFromEvent($event) : expanded = !expanded
218
+ "
219
+ >
220
+ <span class="truncate">{{ label }}</span>
221
+ <UBadge v-if="hasErrors" color="error" variant="soft" size="xs">
222
+ errors
223
+ </UBadge>
224
+ </button>
225
+
226
+ <!-- Action buttons. All buttons stop propagation so the row-click selection handler
227
+ does not also toggle selection when the user clicks an action. -->
228
+ <div class="flex items-center gap-1" @click.stop>
229
+ <slot name="actions" />
230
+
231
+ <!-- Numeric position field (enabled via repeaterDescriptor.attrs.showPositionField) -->
232
+ <template
233
+ v-if="repeaterDescriptor?.attrs?.showPositionField && !readonly"
234
+ >
235
+ <UInput
236
+ :model-value="index + 1"
237
+ type="number"
238
+ :min="1"
239
+ :max="totalItems"
240
+ size="xs"
241
+ class="w-14 text-center"
242
+ title="Position"
243
+ @change="
244
+ $emit(
245
+ 'move-to',
246
+ Math.max(
247
+ 0,
248
+ Math.min(
249
+ totalItems - 1,
250
+ Number($event.target.value) - 1
251
+ )
252
+ )
253
+ )
254
+ "
255
+ />
256
+ </template>
257
+
258
+ <!-- Focus mode -->
259
+ <UButton
260
+ size="xs"
261
+ variant="ghost"
262
+ color="neutral"
263
+ icon="i-lucide-maximize-2"
264
+ title="Edit full screen"
265
+ @click="enterFocusMode"
266
+ />
267
+
268
+ <UButton
269
+ size="xs"
270
+ variant="ghost"
271
+ color="neutral"
272
+ icon="i-lucide-chevron-up"
273
+ :disabled="readonly"
274
+ title="Move up"
275
+ @click="$emit('move-up')"
276
+ />
277
+ <UButton
278
+ size="xs"
279
+ variant="ghost"
280
+ color="neutral"
281
+ icon="i-lucide-chevron-down"
282
+ :disabled="readonly"
283
+ title="Move down"
284
+ @click="$emit('move-down')"
285
+ />
286
+
287
+ <UButton
288
+ v-if="allowDuplicate"
289
+ size="xs"
290
+ variant="ghost"
291
+ color="neutral"
292
+ icon="i-lucide-copy"
293
+ :disabled="readonly"
294
+ title="Duplicate"
295
+ @click="$emit('duplicate')"
296
+ />
297
+
298
+ <UButton
299
+ size="xs"
300
+ variant="ghost"
301
+ color="error"
302
+ icon="i-lucide-trash-2"
303
+ :disabled="readonly"
304
+ title="Delete"
305
+ @click="$emit('delete')"
306
+ />
307
+
308
+ <UIcon
309
+ :name="expanded ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
310
+ class="h-4 w-4 text-gray-400"
311
+ @click="expanded = !expanded"
312
+ />
313
+ </div>
314
+ </div>
315
+
316
+ <!-- ── Item body (fields) ──────────────────────────────────────────────── -->
317
+ <!-- COL_SPAN: Tailwind v4 requires static classes — do not use dynamic template literals. -->
318
+ <div v-if="expanded && !isCompressed" class="grid grid-cols-12 gap-5 p-4">
319
+ <div
320
+ v-for="field in fields"
321
+ :key="field.key"
322
+ :class="{
323
+ 'col-span-1': field.cols === 1,
324
+ 'col-span-2': field.cols === 2,
325
+ 'col-span-3': field.cols === 3,
326
+ 'col-span-4': field.cols === 4,
327
+ 'col-span-5': field.cols === 5,
328
+ 'col-span-6': field.cols === 6,
329
+ 'col-span-7': field.cols === 7,
330
+ 'col-span-8': field.cols === 8,
331
+ 'col-span-9': field.cols === 9,
332
+ 'col-span-10': field.cols === 10,
333
+ 'col-span-11': field.cols === 11,
334
+ 'col-span-12': !field.cols || field.cols === 12
335
+ }"
336
+ >
337
+ <MapoFormField :descriptor="field" />
338
+ </div>
339
+ </div>
340
+ </div>
341
+ </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":"AA6ZA,OAAO,KAAK,EACV,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAK9B,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;AAkzBF,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,118 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { useRequestURL } from "#app";
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 value = computed(() => {
13
+ const v = props.modelValue;
14
+ if (!v || typeof v !== "object")
15
+ return { title: "", description: "", permalink: "" };
16
+ return v;
17
+ });
18
+ function update(field, val) {
19
+ emit("update:modelValue", { ...value.value, [field]: val });
20
+ }
21
+ const titleLen = computed(() => (value.value.title ?? "").length);
22
+ const descLen = computed(() => (value.value.description ?? "").length);
23
+ const titleColor = computed(() => {
24
+ if (titleLen.value > 60) return "text-red-500";
25
+ if (titleLen.value > 50) return "text-amber-500";
26
+ return "text-green-600";
27
+ });
28
+ const descColor = computed(() => {
29
+ if (descLen.value > 155) return "text-red-500";
30
+ if (descLen.value > 130) return "text-amber-500";
31
+ return "text-green-600";
32
+ });
33
+ const requestUrl = useRequestURL();
34
+ const displayUrl = computed(() => value.value.permalink || requestUrl.host);
35
+ </script>
36
+
37
+ <template>
38
+ <div class="mapo-seo-preview space-y-4">
39
+ <!-- Input fields -->
40
+ <div class="space-y-3">
41
+ <div>
42
+ <div class="mb-1 flex items-center justify-between">
43
+ <label class="text-sm font-medium text-gray-700">URL</label>
44
+ </div>
45
+ <UInput
46
+ :model-value="value.permalink"
47
+ :readonly="readonly"
48
+ :disabled="disabled"
49
+ placeholder="https://example.com/slug"
50
+ class="w-full"
51
+ @update:model-value="update('permalink', $event)"
52
+ />
53
+ </div>
54
+
55
+ <div>
56
+ <div class="mb-1 flex items-center justify-between">
57
+ <label class="text-sm font-medium text-gray-700">SEO title</label>
58
+ <span class="text-xs" :class="titleColor">{{ titleLen }} / 60</span>
59
+ </div>
60
+ <UInput
61
+ :model-value="value.title"
62
+ :readonly="readonly"
63
+ :disabled="disabled"
64
+ placeholder="Page title..."
65
+ class="w-full"
66
+ @update:model-value="update('title', $event)"
67
+ />
68
+ </div>
69
+
70
+ <div>
71
+ <div class="mb-1 flex items-center justify-between">
72
+ <label class="text-sm font-medium text-gray-700"
73
+ >Meta description</label
74
+ >
75
+ <span class="text-xs" :class="descColor">{{ descLen }} / 155</span>
76
+ </div>
77
+ <UTextarea
78
+ :model-value="value.description"
79
+ :rows="3"
80
+ :readonly="readonly"
81
+ :disabled="disabled"
82
+ placeholder="Page description..."
83
+ class="w-full"
84
+ @update:model-value="update('description', $event)"
85
+ />
86
+ </div>
87
+ </div>
88
+
89
+ <!-- SERP preview -->
90
+ <div class="rounded border border-gray-200 bg-gray-50 p-4">
91
+ <p class="mb-1 text-xs font-medium uppercase tracking-wide text-gray-400">
92
+ Google preview
93
+ </p>
94
+ <div class="max-w-lg">
95
+ <p class="truncate text-sm text-green-700">
96
+ {{ displayUrl }}
97
+ </p>
98
+ <p class="truncate text-lg text-blue-700 underline">
99
+ {{ value.title || "Page title" }}
100
+ </p>
101
+ <p class="mt-1 line-clamp-2 text-sm text-gray-600">
102
+ {{
103
+ value.description || "Page description that will appear in Google search results."
104
+ }}
105
+ </p>
106
+ </div>
107
+ </div>
108
+
109
+ <p
110
+ v-for="err in errors"
111
+ :key="err"
112
+ class="text-sm text-red-500"
113
+ role="alert"
114
+ >
115
+ {{ err }}
116
+ </p>
117
+ </div>
118
+ </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":"AA8IA,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;AAED,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;AAiSF,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"}