@xleddyl/nuxt-cms 0.2.1 → 0.2.3

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 (110) hide show
  1. package/README.md +3 -1
  2. package/dist/module.d.mts +4 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +320 -33
  5. package/dist/runtime/app/blocks/CmsBlocks.d.vue.ts +45 -0
  6. package/dist/runtime/app/blocks/CmsBlocks.vue +36 -0
  7. package/dist/runtime/app/blocks/CmsBlocks.vue.d.ts +45 -0
  8. package/dist/runtime/app/blocks/resolve.d.ts +21 -0
  9. package/dist/runtime/app/blocks/resolve.js +37 -0
  10. package/dist/runtime/app/components/cms/BlockOutline.d.vue.ts +17 -0
  11. package/dist/runtime/app/components/cms/BlockOutline.vue +99 -0
  12. package/dist/runtime/app/components/cms/BlockOutline.vue.d.ts +17 -0
  13. package/dist/runtime/app/components/cms/BlockPalette.d.vue.ts +12 -0
  14. package/dist/runtime/app/components/cms/BlockPalette.vue +29 -0
  15. package/dist/runtime/app/components/cms/BlockPalette.vue.d.ts +12 -0
  16. package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +2 -0
  17. package/dist/runtime/app/components/cms/BlocksField.vue +83 -3
  18. package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +2 -0
  19. package/dist/runtime/app/components/cms/BrandMark.vue +2 -3
  20. package/dist/runtime/app/components/cms/ConfirmModal.vue +14 -1
  21. package/dist/runtime/app/components/cms/EntryForm.vue +9 -4
  22. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +2 -0
  23. package/dist/runtime/app/components/cms/FieldInput.vue +18 -1
  24. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +2 -0
  25. package/dist/runtime/app/components/cms/MediaGallery.vue +89 -11
  26. package/dist/runtime/app/components/cms/Modal.vue +0 -2
  27. package/dist/runtime/app/components/cms/SettingsEntry.vue +3 -2
  28. package/dist/runtime/app/components/cms/SettingsModal.vue +2 -1
  29. package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +2 -0
  30. package/dist/runtime/app/components/cms/StatusBadge.vue +9 -3
  31. package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +2 -0
  32. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +37 -0
  33. package/dist/runtime/app/components/cms/VisualEditor.vue +533 -0
  34. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +37 -0
  35. package/dist/runtime/app/composables/cms-confirm.d.ts +16 -7
  36. package/dist/runtime/app/composables/cms-confirm.js +1 -0
  37. package/dist/runtime/app/composables/cms-entry-disabled.d.ts +7 -1
  38. package/dist/runtime/app/composables/cms-entry-disabled.js +15 -0
  39. package/dist/runtime/app/composables/cms-entry.d.ts +7 -1
  40. package/dist/runtime/app/composables/cms-entry.js +51 -1
  41. package/dist/runtime/app/composables/cms-media-library.d.ts +1 -2
  42. package/dist/runtime/app/composables/cms-media-library.js +12 -19
  43. package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
  44. package/dist/runtime/app/composables/cms-settings.js +8 -2
  45. package/dist/runtime/app/layouts/cms-admin.vue +5 -0
  46. package/dist/runtime/app/layouts/cms-editor.d.vue.ts +13 -0
  47. package/dist/runtime/app/layouts/cms-editor.vue +14 -0
  48. package/dist/runtime/app/layouts/cms-editor.vue.d.ts +13 -0
  49. package/dist/runtime/app/pages/admin-collection.vue +33 -4
  50. package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
  51. package/dist/runtime/app/pages/admin-content.vue +284 -0
  52. package/dist/runtime/app/pages/admin-content.vue.d.ts +3 -0
  53. package/dist/runtime/app/pages/admin-preview.d.vue.ts +3 -0
  54. package/dist/runtime/app/pages/admin-preview.vue +132 -0
  55. package/dist/runtime/app/pages/admin-preview.vue.d.ts +3 -0
  56. package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +15 -0
  57. package/dist/runtime/app/preview/PreviewOverlay.vue +362 -0
  58. package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +15 -0
  59. package/dist/runtime/app/utils/ui.d.ts +0 -2
  60. package/dist/runtime/app/utils/ui.js +1 -0
  61. package/dist/runtime/assets/main.css +1 -1
  62. package/dist/runtime/seed.js +11 -1
  63. package/dist/runtime/server/api/collection.get.js +3 -2
  64. package/dist/runtime/server/api/collection.post.js +24 -15
  65. package/dist/runtime/server/api/collection.put.js +12 -12
  66. package/dist/runtime/server/api/item.delete.js +2 -1
  67. package/dist/runtime/server/api/item.put.js +16 -14
  68. package/dist/runtime/server/api/media-folder.patch.js +10 -16
  69. package/dist/runtime/server/api/media-move.post.js +12 -8
  70. package/dist/runtime/server/api/media.delete.js +1 -1
  71. package/dist/runtime/server/api/media.post.js +2 -1
  72. package/dist/runtime/server/api/media.put.js +2 -1
  73. package/dist/runtime/server/plugins/migrate-sqlite.js +2 -2
  74. package/dist/runtime/server/routes/auth/login.post.js +28 -22
  75. package/dist/runtime/server/utils/db-d1.d.ts +0 -1
  76. package/dist/runtime/server/utils/db-d1.js +3 -4
  77. package/dist/runtime/server/utils/db-libsql.d.ts +0 -1
  78. package/dist/runtime/server/utils/db-libsql.js +3 -4
  79. package/dist/runtime/server/utils/db-postgres.d.ts +0 -1
  80. package/dist/runtime/server/utils/db-postgres.js +1 -4
  81. package/dist/runtime/server/utils/db-sqlite.d.ts +0 -1
  82. package/dist/runtime/server/utils/db-sqlite.js +4 -10
  83. package/dist/runtime/server/utils/graphql.js +25 -9
  84. package/dist/runtime/server/utils/libsql-migrations.d.ts +1 -0
  85. package/dist/runtime/server/utils/libsql-migrations.js +1 -1
  86. package/dist/runtime/server/utils/media-folders.js +14 -23
  87. package/dist/runtime/server/utils/media.js +2 -0
  88. package/dist/runtime/server/utils/migrate.d.ts +1 -0
  89. package/dist/runtime/server/utils/migrate.js +32 -0
  90. package/dist/runtime/server/utils/registry.d.ts +4 -0
  91. package/dist/runtime/server/utils/registry.js +16 -1
  92. package/dist/runtime/server/utils/relations.d.ts +1 -1
  93. package/dist/runtime/server/utils/relations.js +6 -9
  94. package/dist/runtime/server/utils/timestamps.d.ts +12 -0
  95. package/dist/runtime/server/utils/timestamps.js +48 -0
  96. package/dist/runtime/server/utils/users.js +3 -2
  97. package/dist/runtime/server/utils/visibility.d.ts +4 -0
  98. package/dist/runtime/server/utils/visibility.js +12 -0
  99. package/dist/runtime/server/utils/write-statements.d.ts +33 -0
  100. package/dist/runtime/server/utils/write-statements.js +75 -0
  101. package/dist/runtime/shared/graphql-sdl.js +4 -1
  102. package/dist/runtime/shared/index.d.ts +73 -5
  103. package/dist/runtime/shared/index.js +146 -0
  104. package/dist/runtime/shared/preview.d.ts +85 -0
  105. package/dist/runtime/shared/preview.js +219 -0
  106. package/dist/runtime/shared/timestamps.d.ts +4 -0
  107. package/dist/runtime/shared/timestamps.js +24 -0
  108. package/dist/runtime/shared/validation.d.ts +2 -0
  109. package/dist/runtime/shared/validation.js +15 -2
  110. package/package.json +1 -1
@@ -0,0 +1,533 @@
1
+ <template>
2
+ <div class="cms-editor">
3
+ <header class="cms-editor-bar">
4
+ <div class="cms-editor-bar-start">
5
+ <slot name="start" />
6
+ <div class="cms-editor-heading">
7
+ <span class="cms-editor-title" :title="title">{{ title }}</span>
8
+ <slot name="status" />
9
+ </div>
10
+ </div>
11
+ <div class="cms-editor-bar-center">
12
+ <CmsLocaleSwitch
13
+ v-if="i18n.locales.length > 1"
14
+ v-model="locale"
15
+ :value="localeFilled"
16
+ />
17
+ <div class="cms-editor-devices" role="group" aria-label="Preview width">
18
+ <button
19
+ v-for="option in DEVICES"
20
+ :key="option.id"
21
+ type="button"
22
+ class="cms-editor-device"
23
+ :class="{ 'is-active': device === option.id }"
24
+ :aria-pressed="device === option.id"
25
+ :aria-label="option.label"
26
+ :title="option.label"
27
+ @click="device = option.id"
28
+ >
29
+ <CmsIcon :name="option.icon" class="size-4" />
30
+ </button>
31
+ </div>
32
+ </div>
33
+ <div class="cms-editor-bar-end">
34
+ <slot name="actions" />
35
+ </div>
36
+ </header>
37
+
38
+ <div class="cms-editor-body">
39
+ <div class="cms-editor-canvas">
40
+ <div class="cms-editor-frame" :class="`is-${device}`">
41
+ <iframe
42
+ ref="frame"
43
+ :src="previewPath"
44
+ title="Live preview"
45
+ class="cms-editor-iframe"
46
+ @load="sendState"
47
+ />
48
+ </div>
49
+ </div>
50
+
51
+ <aside class="cms-editor-panel" aria-label="Inspector">
52
+ <div v-if="tabList.length > 1" class="cms-editor-tabs">
53
+ <div class="cms-tabs" role="tablist">
54
+ <button
55
+ v-for="tab in tabList"
56
+ :id="`cms-editor-tab-${tab.id}`"
57
+ :key="tab.id"
58
+ type="button"
59
+ role="tab"
60
+ class="cms-tab"
61
+ :class="{ 'is-active': tab.id === activeTab }"
62
+ :aria-selected="tab.id === activeTab"
63
+ :aria-controls="`cms-editor-tabpanel-${tab.id}`"
64
+ @click="activeTab = tab.id"
65
+ >
66
+ {{ tab.label }}
67
+ <span
68
+ v-if="tabHasError(tab.id)"
69
+ class="cms-editor-tab-dot"
70
+ aria-label="has errors"
71
+ />
72
+ </button>
73
+ </div>
74
+ </div>
75
+
76
+ <div
77
+ :id="`cms-editor-tabpanel-${activeTab}`"
78
+ class="cms-editor-panel-body"
79
+ role="tabpanel"
80
+ :aria-labelledby="tabList.length > 1 ? `cms-editor-tab-${activeTab}` : void 0"
81
+ >
82
+ <template v-if="activeTab === 'block'">
83
+ <template v-if="selectedItem && selected !== null">
84
+ <div class="cms-editor-block-head">
85
+ <CmsButton
86
+ icon="arrow-left"
87
+ size="sm"
88
+ variant="ghost"
89
+ color="neutral"
90
+ label="All blocks"
91
+ @click="select(null)"
92
+ />
93
+ <span class="cms-editor-block-position">
94
+ {{ selected + 1 }} / {{ blocks.length }}
95
+ </span>
96
+ </div>
97
+ <div class="cms-editor-block-title">
98
+ <CmsIcon :name="selectedBlock?.icon ?? 'rectangle-stack'" class="size-4" />
99
+ <span>{{ selectedBlock?.label ?? selectedItem.type }}</span>
100
+ <span v-if="isHiddenBlock(selectedItem)" class="cms-badge is-muted">
101
+ Hidden
102
+ </span>
103
+ </div>
104
+ <div class="cms-editor-block-actions">
105
+ <CmsButton
106
+ icon="arrow-up"
107
+ size="sm"
108
+ variant="soft"
109
+ color="neutral"
110
+ aria-label="Move up"
111
+ title="Move up"
112
+ :disabled="selected === 0"
113
+ @click="moveSelected(-1)"
114
+ />
115
+ <CmsButton
116
+ icon="arrow-down"
117
+ size="sm"
118
+ variant="soft"
119
+ color="neutral"
120
+ aria-label="Move down"
121
+ title="Move down"
122
+ :disabled="selected === blocks.length - 1"
123
+ @click="moveSelected(1)"
124
+ />
125
+ <CmsButton
126
+ icon="document-duplicate"
127
+ size="sm"
128
+ variant="soft"
129
+ color="neutral"
130
+ label="Duplicate"
131
+ @click="duplicate(selected)"
132
+ />
133
+ <CmsButton
134
+ v-if="canHideBlock(selectedBlock)"
135
+ :icon="isHiddenBlock(selectedItem) ? 'eye' : 'eye-slash'"
136
+ size="sm"
137
+ variant="soft"
138
+ color="neutral"
139
+ :label="isHiddenBlock(selectedItem) ? 'Show' : 'Hide'"
140
+ @click="toggleHidden(selected)"
141
+ />
142
+ <CmsButton
143
+ icon="trash"
144
+ size="sm"
145
+ variant="soft"
146
+ color="error"
147
+ aria-label="Delete block"
148
+ title="Delete"
149
+ @click="remove(selected)"
150
+ />
151
+ </div>
152
+ <div class="cms-editor-fields">
153
+ <CmsFormField
154
+ v-for="(blockField, blockKey) in selectedBlock?.fields ?? {}"
155
+ :key="`${selected}-${blockKey}`"
156
+ :label="blockField.label"
157
+ :description="blockField.description"
158
+ :icon="fieldIcon(blockField)"
159
+ :required="blockField.required"
160
+ :name="`${fieldKey}.${selected}.${blockKey}`"
161
+ >
162
+ <CmsFieldInput
163
+ :model-value="selectedItem[blockKey]"
164
+ :field="blockField"
165
+ :locale="locale"
166
+ @update:model-value="
167
+ (value) => updateBlockField(String(blockKey), value)
168
+ "
169
+ />
170
+ </CmsFormField>
171
+ </div>
172
+ </template>
173
+
174
+ <template v-else>
175
+ <p v-if="errors[fieldKey]" class="cms-form-error">
176
+ <CmsIcon name="exclamation-circle" class="size-3.5 shrink-0" />
177
+ {{ errors[fieldKey] }}
178
+ </p>
179
+ <div v-if="blocks.length" class="cms-editor-section">
180
+ <span class="cms-label">{{ blocksField.label }}</span>
181
+ <CmsBlockOutline
182
+ :items="blocks"
183
+ :field="blocksField"
184
+ :locale="locale"
185
+ :selected="selected"
186
+ @select="select"
187
+ @move="move"
188
+ />
189
+ </div>
190
+ <div class="cms-editor-section">
191
+ <span class="cms-label">Add a block</span>
192
+ <CmsBlockPalette
193
+ :blocks="blocksField.blocks ?? {}"
194
+ @pick="(type) => insert(blocks.length, type)"
195
+ />
196
+ </div>
197
+ </template>
198
+ </template>
199
+
200
+ <div v-else class="cms-editor-fields">
201
+ <template v-for="section in sections" :key="section.id">
202
+ <div v-if="section.title" class="cms-editor-section-title">
203
+ <span class="cms-label">{{ section.title }}</span>
204
+ <span v-if="section.description" class="cms-form-description">
205
+ {{ section.description }}
206
+ </span>
207
+ </div>
208
+ <CmsFormField
209
+ v-for="key in section.keys"
210
+ :key="key"
211
+ :label="fields[key].label"
212
+ :description="fields[key].description"
213
+ :icon="fieldIcon(fields[key])"
214
+ :name="key"
215
+ :required="fields[key].required"
216
+ >
217
+ <CmsFieldInput
218
+ :model-value="state[key]"
219
+ :field="fields[key]"
220
+ :locale="locale"
221
+ :slug-source="slugSource(fields[key])"
222
+ @update:model-value="(value) => setValue(key, value)"
223
+ />
224
+ </CmsFormField>
225
+ </template>
226
+ </div>
227
+ </div>
228
+ </aside>
229
+ </div>
230
+
231
+ <CmsModal v-model:open="paletteOpen" title="Insert a block">
232
+ <template #body>
233
+ <CmsBlockPalette :blocks="blocksField.blocks ?? {}" @pick="insertFromPalette" />
234
+ </template>
235
+ </CmsModal>
236
+ </div>
237
+ </template>
238
+
239
+ <script setup>
240
+ import { isFieldVisible, isHiddenBlock, isTranslatableField, slugSourceValue } from "#nuxt-cms";
241
+ import {
242
+ computed,
243
+ onBeforeUnmount,
244
+ onMounted,
245
+ provide,
246
+ reactive,
247
+ ref,
248
+ shallowRef,
249
+ watch
250
+ } from "#imports";
251
+ import { buildEntrySchema } from "../../../shared/validation";
252
+ import {
253
+ DEFAULT_CMS_PREVIEW_PATH,
254
+ canHideBlock,
255
+ collectMediaKeys,
256
+ duplicateBlock,
257
+ emptyBlock,
258
+ insertBlock,
259
+ isTrustedPreviewEvent,
260
+ moveBlock,
261
+ parseFrameMessage,
262
+ previewEnvelope,
263
+ previewMedia,
264
+ removeBlock,
265
+ selectionAfterMove,
266
+ selectionAfterRemove,
267
+ toggleBlockHidden
268
+ } from "../../../shared/preview";
269
+ import { useCmsConfirm } from "../../composables/cms-confirm";
270
+ import { useCmsRuntime } from "../../composables/cms-runtime";
271
+ import { entryFormLayout, useCmsSettingsState } from "../../composables/cms-settings";
272
+ import { CMS_FORM_ERRORS, fieldIcon } from "../../utils/ui";
273
+ const SEO_KEYS = ["seoTitle", "seoDescription", "seoImage"];
274
+ const DEVICES = [
275
+ { id: "desktop", label: "Desktop width", icon: "computer-desktop" },
276
+ { id: "mobile", label: "Mobile width", icon: "device-phone-mobile" }
277
+ ];
278
+ const props = defineProps({
279
+ entryName: { type: String, required: true },
280
+ fieldKey: { type: String, required: true },
281
+ fields: { type: Object, required: true },
282
+ title: { type: String, required: true },
283
+ content: { type: Boolean, required: false }
284
+ });
285
+ const state = defineModel("state", { type: Object, ...{ required: true } });
286
+ const runtime = useCmsRuntime();
287
+ const { i18n } = runtime;
288
+ const previewPath = runtime.previewPath ?? DEFAULT_CMS_PREVIEW_PATH;
289
+ const confirmAction = useCmsConfirm();
290
+ const settings = useCmsSettingsState();
291
+ const locale = ref(i18n.defaultLocale);
292
+ const device = ref("desktop");
293
+ const selected = ref(null);
294
+ const paletteOpen = ref(false);
295
+ const paletteIndex = ref(0);
296
+ const frame = ref(null);
297
+ const library = shallowRef(/* @__PURE__ */ new Map());
298
+ const blocksField = computed(() => props.fields[props.fieldKey]);
299
+ const blocks = computed(() => {
300
+ const value = state.value[props.fieldKey];
301
+ return Array.isArray(value) ? value : [];
302
+ });
303
+ const selectedItem = computed(
304
+ () => selected.value === null ? void 0 : blocks.value[selected.value]
305
+ );
306
+ const selectedBlock = computed(
307
+ () => selectedItem.value ? blocksField.value.blocks?.[String(selectedItem.value.type)] : void 0
308
+ );
309
+ const tabList = computed(() => {
310
+ if (!props.content) return [{ id: "block", label: "Block" }];
311
+ const tabs = [
312
+ { id: "content", label: "Content" },
313
+ { id: "block", label: "Block" }
314
+ ];
315
+ if (seoKeys.value.length) tabs.push({ id: "seo", label: "SEO" });
316
+ return tabs;
317
+ });
318
+ const activeTab = ref(props.content ? "content" : "block");
319
+ const seoKeys = computed(() => SEO_KEYS.filter((key) => Object.hasOwn(props.fields, key)));
320
+ const contentFields = computed(
321
+ () => Object.fromEntries(
322
+ Object.entries(props.fields).filter(
323
+ ([key]) => key !== props.fieldKey && !SEO_KEYS.includes(key)
324
+ )
325
+ )
326
+ );
327
+ const sections = computed(() => {
328
+ if (activeTab.value === "seo")
329
+ return [{ id: "seo", title: void 0, description: void 0, keys: seoKeys.value }];
330
+ const layout = entryFormLayout(props.entryName, settings.value, contentFields.value);
331
+ return layout.main.map((section, index) => ({
332
+ id: `${index}-${section.title ?? ""}`,
333
+ title: section.title,
334
+ description: section.description,
335
+ keys: section.rows.flat().filter((key) => {
336
+ const field = props.fields[key];
337
+ return !!field && isFieldVisible(field, state.value);
338
+ })
339
+ })).filter((section) => section.keys.length);
340
+ });
341
+ const localeFilled = computed(() => {
342
+ const title = props.content ? state.value.title : null;
343
+ return title && typeof title === "object" ? title : null;
344
+ });
345
+ function setValue(key, value) {
346
+ state.value = { ...state.value, [key]: value };
347
+ }
348
+ function setBlocks(list) {
349
+ setValue(props.fieldKey, list.length ? list : null);
350
+ }
351
+ function slugSource(field) {
352
+ if (!field.from) return void 0;
353
+ return slugSourceValue(props.fields[field.from], state.value[field.from], i18n.defaultLocale);
354
+ }
355
+ function select(index) {
356
+ selected.value = index;
357
+ if (selected.value !== null) activeTab.value = "block";
358
+ }
359
+ function blockConfigAt(index) {
360
+ return blocksField.value.blocks?.[String(blocks.value[index]?.type)];
361
+ }
362
+ function insert(index, type) {
363
+ const block = blocksField.value.blocks?.[type];
364
+ if (!block) return;
365
+ const at = Math.min(Math.max(index, 0), blocks.value.length);
366
+ setBlocks(insertBlock(blocks.value, at, emptyBlock(type, block)));
367
+ select(at);
368
+ }
369
+ function insertFromPalette(type) {
370
+ paletteOpen.value = false;
371
+ insert(paletteIndex.value, type);
372
+ }
373
+ function openPalette(index) {
374
+ paletteIndex.value = index;
375
+ paletteOpen.value = true;
376
+ }
377
+ function move(from, to) {
378
+ if (to < 0 || to >= blocks.value.length || from === to) return;
379
+ setBlocks(moveBlock(blocks.value, from, to));
380
+ selected.value = selectionAfterMove(selected.value, from, to);
381
+ }
382
+ function moveSelected(delta) {
383
+ if (selected.value === null) return;
384
+ move(selected.value, selected.value + delta);
385
+ }
386
+ function duplicate(index) {
387
+ setBlocks(duplicateBlock(blocks.value, index));
388
+ select(index + 1);
389
+ }
390
+ function toggleHidden(index) {
391
+ setBlocks(toggleBlockHidden(blocks.value, index, blockConfigAt(index)));
392
+ }
393
+ async function remove(index) {
394
+ const label = blockConfigAt(index)?.label ?? "block";
395
+ if (!await confirmAction(`Delete this ${label.toLowerCase()} block?`)) return;
396
+ setBlocks(removeBlock(blocks.value, index));
397
+ selected.value = selectionAfterRemove(selected.value, index);
398
+ }
399
+ function updateBlockField(key, value) {
400
+ const index = selected.value;
401
+ const current = index === null ? void 0 : blocks.value[index];
402
+ if (index === null || !current) return;
403
+ setBlocks(blocks.value.map((item, i) => i === index ? { ...current, [key]: value } : item));
404
+ }
405
+ const media = computed(
406
+ () => Object.fromEntries(
407
+ collectMediaKeys(props.fields, state.value).map((key) => [
408
+ key,
409
+ previewMedia(key, library.value.get(key), runtime.mediaBaseUrl)
410
+ ])
411
+ )
412
+ );
413
+ const previewState = computed(() => ({
414
+ entry: props.entryName,
415
+ field: props.fieldKey,
416
+ locale: locale.value,
417
+ blocks: blocks.value,
418
+ item: props.content ? state.value : null,
419
+ media: media.value,
420
+ selected: selected.value
421
+ }));
422
+ function sendState() {
423
+ const target = frame.value?.contentWindow;
424
+ if (!target) return;
425
+ const snapshot = JSON.parse(JSON.stringify(previewState.value));
426
+ target.postMessage(previewEnvelope({ type: "render", state: snapshot }), window.location.origin);
427
+ }
428
+ watch(previewState, sendState, { deep: true });
429
+ function onFrameMessage(message) {
430
+ switch (message.type) {
431
+ case "ready":
432
+ sendState();
433
+ break;
434
+ case "select":
435
+ select(message.index);
436
+ break;
437
+ case "insert":
438
+ openPalette(message.index);
439
+ break;
440
+ case "move":
441
+ move(message.from, message.to);
442
+ select(message.to);
443
+ break;
444
+ case "duplicate":
445
+ duplicate(message.index);
446
+ break;
447
+ case "toggle-hidden":
448
+ toggleHidden(message.index);
449
+ break;
450
+ case "remove":
451
+ void remove(message.index);
452
+ break;
453
+ }
454
+ }
455
+ function onMessage(event) {
456
+ if (!isTrustedPreviewEvent(event, window.location.origin, frame.value?.contentWindow)) return;
457
+ const message = parseFrameMessage(event.data);
458
+ if (message) onFrameMessage(message);
459
+ }
460
+ async function loadLibrary() {
461
+ try {
462
+ const result = await $fetch("/api/cms/admin/media");
463
+ library.value = new Map(result.items.map((item) => [item.key, item]));
464
+ } catch {
465
+ library.value = /* @__PURE__ */ new Map();
466
+ }
467
+ }
468
+ onMounted(() => {
469
+ window.addEventListener("message", onMessage);
470
+ void loadLibrary();
471
+ });
472
+ onBeforeUnmount(() => {
473
+ window.removeEventListener("message", onMessage);
474
+ });
475
+ const errors = reactive({});
476
+ provide(CMS_FORM_ERRORS, errors);
477
+ const schema = computed(
478
+ () => buildEntrySchema({ fields: props.fields, drafts: props.content }, i18n)
479
+ );
480
+ let submitted = false;
481
+ function collectErrors() {
482
+ for (const key of Object.keys(errors)) delete errors[key];
483
+ const result = schema.value.safeParse(state.value);
484
+ if (result.success) return [];
485
+ const issues = result.error.issues;
486
+ for (const issue of issues) {
487
+ const [key, index, blockKey] = issue.path;
488
+ if (key === void 0) continue;
489
+ errors[String(key)] ??= issue.message;
490
+ if (String(key) === props.fieldKey && typeof index === "number" && blockKey !== void 0)
491
+ errors[`${String(key)}.${index}.${String(blockKey)}`] ??= issue.message;
492
+ }
493
+ return issues;
494
+ }
495
+ function tabOfKey(key) {
496
+ if (key === props.fieldKey) return "block";
497
+ return SEO_KEYS.includes(key) ? "seo" : "content";
498
+ }
499
+ function tabHasError(tab) {
500
+ return Object.keys(errors).some((key) => tabOfKey(key.split(".")[0]) === tab);
501
+ }
502
+ function validate() {
503
+ submitted = true;
504
+ const issues = collectErrors();
505
+ const first = issues[0];
506
+ if (!first) return true;
507
+ const [key, index] = first.path;
508
+ const name = String(key ?? "");
509
+ const field = props.fields[name];
510
+ if (name === props.fieldKey) {
511
+ select(typeof index === "number" ? index : null);
512
+ activeTab.value = "block";
513
+ } else if (field) {
514
+ activeTab.value = tabOfKey(name);
515
+ if (isTranslatableField(field)) locale.value = i18n.defaultLocale;
516
+ }
517
+ return false;
518
+ }
519
+ watch(
520
+ state,
521
+ () => {
522
+ if (submitted) collectErrors();
523
+ },
524
+ { deep: true }
525
+ );
526
+ watch(
527
+ () => blocks.value.length,
528
+ (length) => {
529
+ if (selected.value !== null && selected.value >= length) selected.value = null;
530
+ }
531
+ );
532
+ defineExpose({ validate, locale });
533
+ </script>
@@ -0,0 +1,37 @@
1
+ import type { FieldConfig } from '#nuxt-cms';
2
+ type __VLS_Props = {
3
+ entryName: string;
4
+ fieldKey: string;
5
+ fields: Record<string, FieldConfig>;
6
+ title: string;
7
+ content?: boolean;
8
+ };
9
+ declare function validate(): boolean;
10
+ type __VLS_ModelProps = {
11
+ 'state': Record<string, unknown>;
12
+ };
13
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
14
+ declare var __VLS_1: {}, __VLS_3: {}, __VLS_15: {};
15
+ type __VLS_Slots = {} & {
16
+ start?: (props: typeof __VLS_1) => any;
17
+ } & {
18
+ status?: (props: typeof __VLS_3) => any;
19
+ } & {
20
+ actions?: (props: typeof __VLS_15) => any;
21
+ };
22
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
23
+ validate: typeof validate;
24
+ locale: import("vue").Ref<string, string>;
25
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
26
+ "update:state": (value: Record<string, unknown>) => any;
27
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
28
+ "onUpdate:state"?: ((value: Record<string, unknown>) => any) | undefined;
29
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
31
+ declare const _default: typeof __VLS_export;
32
+ export default _default;
33
+ type __VLS_WithSlots<T, S> = T & {
34
+ new (): {
35
+ $slots: S;
36
+ };
37
+ };
@@ -1,12 +1,21 @@
1
- export interface CmsConfirmState {
2
- open: boolean;
3
- message: string;
1
+ export interface CmsConfirmLink {
2
+ title: string;
3
+ meta: string;
4
+ to: string;
5
+ }
6
+ export interface CmsConfirmGroup {
7
+ title: string;
8
+ links: CmsConfirmLink[];
9
+ }
10
+ export interface CmsConfirmOptions {
4
11
  title?: string;
5
12
  confirmLabel?: string;
13
+ groups?: CmsConfirmGroup[];
14
+ }
15
+ export interface CmsConfirmState extends CmsConfirmOptions {
16
+ open: boolean;
17
+ message: string;
6
18
  resolve?: (value: boolean) => void;
7
19
  }
8
20
  export declare function useCmsConfirmState(): import("vue").Ref<CmsConfirmState, CmsConfirmState>;
9
- export declare function useCmsConfirm(): (message: string, options?: {
10
- title?: string;
11
- confirmLabel?: string;
12
- }) => Promise<boolean>;
21
+ export declare function useCmsConfirm(): (message: string, options?: CmsConfirmOptions) => Promise<boolean>;
@@ -10,6 +10,7 @@ export function useCmsConfirm() {
10
10
  message,
11
11
  title: options?.title,
12
12
  confirmLabel: options?.confirmLabel,
13
+ groups: options?.groups,
13
14
  resolve
14
15
  };
15
16
  });
@@ -1,5 +1,5 @@
1
1
  import type { AsyncData } from 'nuxt/app';
2
- import type { CmsCollectionName, CmsCollectionTypes, CmsPagePath, CmsPageTypes, CmsSingleName, CmsSingleTypes } from '#cms-types';
2
+ import type { CmsCollectionName, CmsCollectionTypes, CmsContentName, CmsContentTypes, CmsPagePath, CmsPageTypes, CmsSingleName, CmsSingleTypes } from '#cms-types';
3
3
  import type { CmsAsyncDataOptions } from './cms-query-disabled.js';
4
4
  export interface CmsSortInput<Entry> {
5
5
  field: Extract<keyof Entry, string>;
@@ -18,6 +18,12 @@ export interface CmsCollectionOptions<ResT, DefaultT, Entry> extends CmsAsyncDat
18
18
  limit?: number;
19
19
  offset?: number;
20
20
  }
21
+ export interface CmsContentOptions<ResT, DefaultT> extends CmsAsyncDataOptions<ResT, DefaultT> {
22
+ locale?: string;
23
+ }
24
+ export type CmsContentsOptions<ResT, DefaultT, Entry> = CmsCollectionOptions<ResT, DefaultT, Entry>;
21
25
  export declare function useCmsSingle<K extends CmsSingleName, DefaultT = null>(name: K, options?: CmsSingleOptions<CmsSingleTypes[K] | null, DefaultT>): AsyncData<CmsSingleTypes[K] | DefaultT | null, Error | undefined>;
22
26
  export declare function useCmsCollection<K extends CmsCollectionName, DefaultT = CmsCollectionTypes[K][]>(name: K, options?: CmsCollectionOptions<CmsCollectionTypes[K][], DefaultT, CmsCollectionTypes[K]>): AsyncData<CmsCollectionTypes[K][] | DefaultT, Error | undefined>;
27
+ export declare function useCmsContents<K extends CmsContentName, DefaultT = CmsContentTypes[K][]>(name: K, options?: CmsContentsOptions<CmsContentTypes[K][], DefaultT, CmsContentTypes[K]>): AsyncData<CmsContentTypes[K][] | DefaultT, Error | undefined>;
28
+ export declare function useCmsContent<K extends CmsContentName, DefaultT = null>(name: K, slug: string, options?: CmsContentOptions<CmsContentTypes[K] | null, DefaultT>): AsyncData<CmsContentTypes[K] | DefaultT | null, Error | undefined>;
23
29
  export declare function useCmsPage<P extends CmsPagePath, DefaultT = null>(path: P, options?: CmsPageOptions<CmsPageTypes[P] | null, DefaultT>): AsyncData<CmsPageTypes[P] | DefaultT | null, Error | undefined>;
@@ -14,6 +14,21 @@ export function useCmsCollection(name, options = {}) {
14
14
  async () => fallback ? fallback() : []
15
15
  );
16
16
  }
17
+ export function useCmsContents(name, options = {}) {
18
+ const { locale, filters, sort, limit, offset, key, default: fallback } = options;
19
+ const variables = { locale, filters, sort, limit, offset };
20
+ return useAsyncData(
21
+ key ?? `cms-contents:${String(name)}:${JSON.stringify(variables)}`,
22
+ async () => fallback ? fallback() : []
23
+ );
24
+ }
25
+ export function useCmsContent(name, slug, options = {}) {
26
+ const { locale, key, default: fallback } = options;
27
+ return useAsyncData(
28
+ key ?? `cms-content:${String(name)}:${slug}:${locale ?? ""}`,
29
+ async () => fallback ? fallback() : null
30
+ );
31
+ }
17
32
  export function useCmsPage(path, options = {}) {
18
33
  const { locale, key, default: fallback } = options;
19
34
  return useAsyncData(
@@ -1,5 +1,5 @@
1
1
  import type { AsyncData } from 'nuxt/app';
2
- import type { CmsCollectionName, CmsCollectionTypes, CmsPagePath, CmsPageTypes, CmsSingleName, CmsSingleTypes } from '#cms-types';
2
+ import type { CmsCollectionName, CmsCollectionTypes, CmsContentName, CmsContentTypes, CmsPagePath, CmsPageTypes, CmsSingleName, CmsSingleTypes } from '#cms-types';
3
3
  import type { CmsAsyncDataOptions } from './cms-query.js';
4
4
  export interface CmsSortInput<Entry> {
5
5
  field: Extract<keyof Entry, string>;
@@ -18,6 +18,12 @@ export interface CmsCollectionOptions<ResT, DefaultT, Entry> extends CmsAsyncDat
18
18
  limit?: number;
19
19
  offset?: number;
20
20
  }
21
+ export interface CmsContentOptions<ResT, DefaultT> extends CmsAsyncDataOptions<ResT, DefaultT> {
22
+ locale?: string;
23
+ }
24
+ export type CmsContentsOptions<ResT, DefaultT, Entry> = CmsCollectionOptions<ResT, DefaultT, Entry>;
21
25
  export declare function useCmsSingle<K extends CmsSingleName, DefaultT = null>(name: K, options?: CmsSingleOptions<CmsSingleTypes[K] | null, DefaultT>): AsyncData<CmsSingleTypes[K] | DefaultT | null, Error | undefined>;
22
26
  export declare function useCmsCollection<K extends CmsCollectionName, DefaultT = CmsCollectionTypes[K][]>(name: K, options?: CmsCollectionOptions<CmsCollectionTypes[K][], DefaultT, CmsCollectionTypes[K]>): AsyncData<CmsCollectionTypes[K][] | DefaultT, Error | undefined>;
27
+ export declare function useCmsContents<K extends CmsContentName, DefaultT = CmsContentTypes[K][]>(name: K, options?: CmsContentsOptions<CmsContentTypes[K][], DefaultT, CmsContentTypes[K]>): AsyncData<CmsContentTypes[K][] | DefaultT, Error | undefined>;
28
+ export declare function useCmsContent<K extends CmsContentName, DefaultT = null>(name: K, slug: string, options?: CmsContentOptions<CmsContentTypes[K] | null, DefaultT>): AsyncData<CmsContentTypes[K] | DefaultT | null, Error | undefined>;
23
29
  export declare function useCmsPage<P extends CmsPagePath, DefaultT = null>(path: P, options?: CmsPageOptions<CmsPageTypes[P] | null, DefaultT>): AsyncData<CmsPageTypes[P] | DefaultT | null, Error | undefined>;