@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.
- package/dist/__tests__/__mocks__/nuxt-imports.d.ts +4 -0
- package/dist/__tests__/__mocks__/nuxt-imports.d.ts.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/useFormFromSchema.test.d.ts +2 -0
- package/dist/__tests__/useFormFromSchema.test.d.ts.map +1 -0
- package/dist/__tests__/useMapoForm.test.d.ts +2 -0
- package/dist/__tests__/useMapoForm.test.d.ts.map +1 -0
- package/dist/index.d.mts +9 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +7 -34
- package/dist/module.d.mts +20 -2
- package/dist/module.d.ts +12 -2
- package/dist/module.d.ts.map +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +167 -13
- package/dist/runtime/components/MapoFocusPortal.vue +113 -0
- package/dist/runtime/components/MapoFocusPortal.vue.d.ts +4 -0
- package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoForm.vue +168 -0
- package/dist/runtime/components/MapoForm.vue.d.ts +47 -0
- package/dist/runtime/components/MapoForm.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoFormField.vue +269 -0
- package/dist/runtime/components/MapoFormField.vue.d.ts +46 -0
- package/dist/runtime/components/MapoFormField.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoFormFlatSection.vue +149 -0
- package/dist/runtime/components/MapoFormFlatSection.vue.d.ts +15 -0
- package/dist/runtime/components/MapoFormFlatSection.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoFormGroup.vue +247 -0
- package/dist/runtime/components/MapoFormGroup.vue.d.ts +44 -0
- package/dist/runtime/components/MapoFormGroup.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoFormTabs.vue +108 -0
- package/dist/runtime/components/MapoFormTabs.vue.d.ts +15 -0
- package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoUnknownField.vue +62 -0
- package/dist/runtime/components/MapoUnknownField.vue.d.ts +16 -0
- package/dist/runtime/components/MapoUnknownField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoDateField.vue +86 -0
- package/dist/runtime/components/fields/MapoDateField.vue.d.ts +17 -0
- package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoDateTimeField.vue +157 -0
- package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts +17 -0
- package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoFileField.vue +97 -0
- package/dist/runtime/components/fields/MapoFileField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoFksField.vue +118 -0
- package/dist/runtime/components/fields/MapoFksField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoMapField.vue +97 -0
- package/dist/runtime/components/fields/MapoMapField.vue.d.ts +22 -0
- package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoMapFieldClient.vue +67 -0
- package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts +20 -0
- package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoRepeater.vue +449 -0
- package/dist/runtime/components/fields/MapoRepeater.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoRepeaterItem.vue +341 -0
- package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts +55 -0
- package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoSeoPreview.vue +118 -0
- package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts +22 -0
- package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoTimeField.vue +44 -0
- package/dist/runtime/components/fields/MapoTimeField.vue.d.ts +17 -0
- package/dist/runtime/components/fields/MapoTimeField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoWygEditor.vue +227 -0
- package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiCheckbox.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiInput.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiSelectMenu.vue +18 -0
- package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiSlider.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiSwitch.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiTextarea.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts +13 -0
- package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/useNuiFieldAttrs.js +19 -0
- package/dist/runtime/composables/defineFormField.d.ts +25 -0
- package/dist/runtime/composables/defineFormField.d.ts.map +1 -0
- package/dist/runtime/composables/defineFormField.js +27 -0
- package/dist/runtime/composables/useCurrentLang.d.ts +6 -0
- package/dist/runtime/composables/useCurrentLang.d.ts.map +1 -0
- package/dist/runtime/composables/useCurrentLang.js +9 -0
- package/dist/runtime/composables/useFocusMode.d.ts +40 -0
- package/dist/runtime/composables/useFocusMode.d.ts.map +1 -0
- package/dist/runtime/composables/useFocusMode.js +31 -0
- package/dist/runtime/composables/useFormDraft.d.ts +60 -0
- package/dist/runtime/composables/useFormDraft.d.ts.map +1 -0
- package/dist/runtime/composables/useFormDraft.js +52 -0
- package/dist/runtime/composables/useFormFromSchema.d.ts +21 -0
- package/dist/runtime/composables/useFormFromSchema.d.ts.map +1 -0
- package/dist/runtime/composables/useFormFromSchema.js +142 -0
- package/dist/runtime/composables/useMapoForm.d.ts +96 -0
- package/dist/runtime/composables/useMapoForm.d.ts.map +1 -0
- package/dist/runtime/composables/useMapoForm.js +323 -0
- package/dist/runtime/composables/useProgressiveDisclosure.d.ts +44 -0
- package/dist/runtime/composables/useProgressiveDisclosure.d.ts.map +1 -0
- package/dist/runtime/composables/useProgressiveDisclosure.js +23 -0
- package/dist/runtime/composables/useUnsavedChangesGuard.d.ts +24 -0
- package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -0
- package/dist/runtime/composables/useUnsavedChangesGuard.js +27 -0
- package/dist/runtime/devtools/DevtoolsFormsPage.vue +235 -0
- package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts +4 -0
- package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts.map +1 -0
- package/dist/runtime/plugins/form-registry.d.ts +18 -0
- package/dist/runtime/plugins/form-registry.d.ts.map +1 -0
- package/dist/runtime/plugins/form-registry.js +19 -0
- package/dist/runtime/registry/defaults.d.ts +3 -0
- package/dist/runtime/registry/defaults.d.ts.map +1 -0
- package/dist/runtime/registry/defaults.js +87 -0
- package/dist/runtime/types/descriptor.d.ts +359 -0
- package/dist/runtime/types/descriptor.d.ts.map +1 -0
- package/dist/runtime/types/descriptor.js +26 -0
- package/dist/runtime/types/index.d.ts +11 -0
- package/dist/runtime/types/index.d.ts.map +1 -0
- package/dist/runtime/types/index.js +6 -0
- package/dist/runtime/types/layout.d.ts +21 -0
- package/dist/runtime/types/layout.d.ts.map +1 -0
- package/dist/runtime/types/layout.js +0 -0
- package/dist/runtime/types/registry.d.ts +62 -0
- package/dist/runtime/types/registry.d.ts.map +1 -0
- package/dist/runtime/types/registry.js +19 -0
- package/dist/runtime/utils/flattenFieldGroups.d.ts +50 -0
- package/dist/runtime/utils/flattenFieldGroups.d.ts.map +1 -0
- package/dist/runtime/utils/flattenFieldGroups.js +24 -0
- package/dist/types.d.mts +6 -10
- package/package.json +4 -4
- package/dist/index.d.cts +0 -1
- 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"}
|