@xleddyl/nuxt-cms 0.2.2 → 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 (69) 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 +319 -32
  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/EntryForm.vue +9 -4
  20. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +2 -0
  21. package/dist/runtime/app/components/cms/FieldInput.vue +18 -1
  22. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +2 -0
  23. package/dist/runtime/app/components/cms/SettingsEntry.vue +3 -2
  24. package/dist/runtime/app/components/cms/SettingsModal.vue +2 -1
  25. package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +2 -0
  26. package/dist/runtime/app/components/cms/StatusBadge.vue +9 -3
  27. package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +2 -0
  28. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +37 -0
  29. package/dist/runtime/app/components/cms/VisualEditor.vue +533 -0
  30. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +37 -0
  31. package/dist/runtime/app/composables/cms-entry-disabled.d.ts +7 -1
  32. package/dist/runtime/app/composables/cms-entry-disabled.js +15 -0
  33. package/dist/runtime/app/composables/cms-entry.d.ts +7 -1
  34. package/dist/runtime/app/composables/cms-entry.js +51 -1
  35. package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
  36. package/dist/runtime/app/composables/cms-settings.js +8 -2
  37. package/dist/runtime/app/layouts/cms-admin.vue +5 -0
  38. package/dist/runtime/app/layouts/cms-editor.d.vue.ts +13 -0
  39. package/dist/runtime/app/layouts/cms-editor.vue +14 -0
  40. package/dist/runtime/app/layouts/cms-editor.vue.d.ts +13 -0
  41. package/dist/runtime/app/pages/admin-collection.vue +33 -4
  42. package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
  43. package/dist/runtime/app/pages/admin-content.vue +284 -0
  44. package/dist/runtime/app/pages/admin-content.vue.d.ts +3 -0
  45. package/dist/runtime/app/pages/admin-preview.d.vue.ts +3 -0
  46. package/dist/runtime/app/pages/admin-preview.vue +132 -0
  47. package/dist/runtime/app/pages/admin-preview.vue.d.ts +3 -0
  48. package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +15 -0
  49. package/dist/runtime/app/preview/PreviewOverlay.vue +362 -0
  50. package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +15 -0
  51. package/dist/runtime/app/utils/ui.js +1 -0
  52. package/dist/runtime/assets/main.css +1 -1
  53. package/dist/runtime/server/api/collection.get.js +3 -2
  54. package/dist/runtime/server/api/collection.post.js +4 -1
  55. package/dist/runtime/server/api/item.delete.js +2 -1
  56. package/dist/runtime/server/api/item.put.js +2 -1
  57. package/dist/runtime/server/utils/graphql.js +23 -8
  58. package/dist/runtime/server/utils/registry.d.ts +1 -0
  59. package/dist/runtime/server/utils/registry.js +6 -0
  60. package/dist/runtime/server/utils/visibility.d.ts +4 -0
  61. package/dist/runtime/server/utils/visibility.js +12 -0
  62. package/dist/runtime/shared/graphql-sdl.js +4 -1
  63. package/dist/runtime/shared/index.d.ts +59 -5
  64. package/dist/runtime/shared/index.js +133 -0
  65. package/dist/runtime/shared/preview.d.ts +85 -0
  66. package/dist/runtime/shared/preview.js +219 -0
  67. package/dist/runtime/shared/validation.d.ts +2 -0
  68. package/dist/runtime/shared/validation.js +15 -2
  69. package/package.json +1 -1
@@ -0,0 +1,45 @@
1
+ import type { PropType } from 'vue';
2
+ import type { CmsBlockEntryName } from '#cms-types';
3
+ import type { CmsBlockItem } from './resolve.js';
4
+ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
5
+ blocks: {
6
+ type: PropType<readonly CmsBlockItem[] | null>;
7
+ default: null;
8
+ };
9
+ entry: {
10
+ type: PropType<CmsBlockEntryName>;
11
+ default: undefined;
12
+ };
13
+ field: {
14
+ type: StringConstructor;
15
+ default: undefined;
16
+ };
17
+ annotate: {
18
+ type: BooleanConstructor;
19
+ default: boolean;
20
+ };
21
+ }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
22
+ blocks: {
23
+ type: PropType<readonly CmsBlockItem[] | null>;
24
+ default: null;
25
+ };
26
+ entry: {
27
+ type: PropType<CmsBlockEntryName>;
28
+ default: undefined;
29
+ };
30
+ field: {
31
+ type: StringConstructor;
32
+ default: undefined;
33
+ };
34
+ annotate: {
35
+ type: BooleanConstructor;
36
+ default: boolean;
37
+ };
38
+ }>> & Readonly<{}>, {
39
+ field: string;
40
+ blocks: readonly CmsBlockItem[] | null;
41
+ entry: string;
42
+ annotate: boolean;
43
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
44
+ declare const _default: typeof __VLS_export;
45
+ export default _default;
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <component
3
+ :is="block.component"
4
+ v-for="block in resolved"
5
+ :key="block.key"
6
+ v-bind="annotate ? { ...block.props, ...annotation(block) } : block.props"
7
+ />
8
+ </template>
9
+
10
+ <script setup>
11
+ import { computed } from "#imports";
12
+ import { cmsBlockTypeName, resolveCmsBlocks } from "./resolve";
13
+ const props = defineProps({
14
+ blocks: { type: Array, default: null },
15
+ entry: { type: String, default: void 0 },
16
+ field: { type: String, default: void 0 },
17
+ annotate: { type: Boolean, default: false }
18
+ });
19
+ function warnMissing(item) {
20
+ if (!import.meta.dev) return;
21
+ const name = cmsBlockTypeName(item, props.entry, props.field) ?? item.type;
22
+ console.warn(
23
+ `[nuxt-cms] <CmsBlocks> has no component for block '${name}': set component on the block in cms.config, or pass entry and field when the data has no __typename`
24
+ );
25
+ }
26
+ const resolved = computed(
27
+ () => resolveCmsBlocks(props.blocks, {
28
+ entry: props.entry,
29
+ field: props.field,
30
+ onMissing: warnMissing
31
+ })
32
+ );
33
+ function annotation(block) {
34
+ return { "data-cms-block": block.index, "data-cms-block-type": block.type };
35
+ }
36
+ </script>
@@ -0,0 +1,45 @@
1
+ import type { PropType } from 'vue';
2
+ import type { CmsBlockEntryName } from '#cms-types';
3
+ import type { CmsBlockItem } from './resolve.js';
4
+ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
5
+ blocks: {
6
+ type: PropType<readonly CmsBlockItem[] | null>;
7
+ default: null;
8
+ };
9
+ entry: {
10
+ type: PropType<CmsBlockEntryName>;
11
+ default: undefined;
12
+ };
13
+ field: {
14
+ type: StringConstructor;
15
+ default: undefined;
16
+ };
17
+ annotate: {
18
+ type: BooleanConstructor;
19
+ default: boolean;
20
+ };
21
+ }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
22
+ blocks: {
23
+ type: PropType<readonly CmsBlockItem[] | null>;
24
+ default: null;
25
+ };
26
+ entry: {
27
+ type: PropType<CmsBlockEntryName>;
28
+ default: undefined;
29
+ };
30
+ field: {
31
+ type: StringConstructor;
32
+ default: undefined;
33
+ };
34
+ annotate: {
35
+ type: BooleanConstructor;
36
+ default: boolean;
37
+ };
38
+ }>> & Readonly<{}>, {
39
+ field: string;
40
+ blocks: readonly CmsBlockItem[] | null;
41
+ entry: string;
42
+ annotate: boolean;
43
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
44
+ declare const _default: typeof __VLS_export;
45
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import type { Component } from 'vue';
2
+ export interface CmsBlockItem {
3
+ type: string;
4
+ __typename?: string;
5
+ }
6
+ export interface ResolvedCmsBlock {
7
+ key: string;
8
+ index: number;
9
+ type: string;
10
+ component: Component;
11
+ props: Record<string, unknown>;
12
+ }
13
+ export declare const isHiddenCmsBlock: (item: CmsBlockItem) => boolean;
14
+ export declare function cmsBlockTypeName(item: CmsBlockItem, entry?: string, field?: string): string | undefined;
15
+ export declare function cmsBlockComponent(item: CmsBlockItem, entry?: string, field?: string): Component | undefined;
16
+ export declare function cmsBlockProps(item: CmsBlockItem): Record<string, unknown>;
17
+ export declare function resolveCmsBlocks(blocks: readonly CmsBlockItem[] | null | undefined, options?: {
18
+ entry?: string;
19
+ field?: string;
20
+ onMissing?: (item: CmsBlockItem) => void;
21
+ }): ResolvedCmsBlock[];
@@ -0,0 +1,37 @@
1
+ import { cmsBlockComponents } from "#cms-blocks";
2
+ import { CONTENT_BODY_FIELD, blockTypeName, isHiddenBlock } from "../../shared/index.js";
3
+ const OMITTED_KEYS = /* @__PURE__ */ new Set(["type", "__typename"]);
4
+ export const isHiddenCmsBlock = isHiddenBlock;
5
+ export function cmsBlockTypeName(item, entry, field = CONTENT_BODY_FIELD) {
6
+ return entry ? blockTypeName(entry, field, item.type) : item.__typename;
7
+ }
8
+ export function cmsBlockComponent(item, entry, field) {
9
+ const name = cmsBlockTypeName(item, entry, field);
10
+ return name && Object.hasOwn(cmsBlockComponents, name) ? cmsBlockComponents[name] : void 0;
11
+ }
12
+ export function cmsBlockProps(item) {
13
+ return Object.fromEntries(
14
+ Object.entries(item).filter(
15
+ ([key]) => !OMITTED_KEYS.has(key)
16
+ )
17
+ );
18
+ }
19
+ export function resolveCmsBlocks(blocks, options = {}) {
20
+ const resolved = [];
21
+ (blocks ?? []).forEach((item, index) => {
22
+ if (!item || typeof item !== "object" || isHiddenCmsBlock(item)) return;
23
+ const component = cmsBlockComponent(item, options.entry, options.field);
24
+ if (!component) {
25
+ options.onMissing?.(item);
26
+ return;
27
+ }
28
+ resolved.push({
29
+ key: `${index}:${item.type}`,
30
+ index,
31
+ type: item.type,
32
+ component,
33
+ props: cmsBlockProps(item)
34
+ });
35
+ });
36
+ return resolved;
37
+ }
@@ -0,0 +1,17 @@
1
+ import type { FieldConfig } from '#nuxt-cms';
2
+ import type { CmsBlockValue } from '../../../shared/preview.js';
3
+ type __VLS_Props = {
4
+ items: CmsBlockValue[];
5
+ field: FieldConfig;
6
+ locale: string;
7
+ selected: number | null;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ select: (index: number) => any;
11
+ move: (from: number, to: number) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
13
+ onSelect?: ((index: number) => any) | undefined;
14
+ onMove?: ((from: number, to: number) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ declare const _default: typeof __VLS_export;
17
+ export default _default;
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <ol class="cms-outline" aria-label="Blocks">
3
+ <li
4
+ v-for="(item, index) in items"
5
+ :key="uidFor(item)"
6
+ class="cms-outline-row"
7
+ :class="{
8
+ 'is-selected': index === selected,
9
+ 'is-hidden': isHiddenBlock(item),
10
+ 'is-dragging': dragIndex === index,
11
+ 'is-drop-target': dropIndex === index && dragIndex !== index
12
+ }"
13
+ draggable="true"
14
+ @dragstart="onDragStart(index, $event)"
15
+ @dragover.prevent="dropIndex = index"
16
+ @dragleave="dropIndex === index && (dropIndex = null)"
17
+ @drop.prevent="onDrop(index)"
18
+ @dragend="onDragEnd"
19
+ >
20
+ <span class="cms-outline-grip" aria-hidden="true">
21
+ <CmsIcon name="bars-2" class="size-3.5" />
22
+ </span>
23
+ <button
24
+ type="button"
25
+ class="cms-outline-main"
26
+ :aria-current="index === selected ? 'true' : void 0"
27
+ :aria-label="`${labelOf(item)}, ${index + 1} of ${items.length}${isHiddenBlock(item) ? ', hidden' : ''}. Alt and the arrow keys move it`"
28
+ aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown"
29
+ @click="emit('select', index)"
30
+ @keydown.alt.up.prevent="index > 0 && emit('move', index, index - 1)"
31
+ @keydown.alt.down.prevent="index < items.length - 1 && emit('move', index, index + 1)"
32
+ >
33
+ <CmsIcon :name="blockOf(item)?.icon ?? 'rectangle-stack'" class="size-4 shrink-0" />
34
+ <span class="cms-outline-text">
35
+ <span class="cms-outline-label">{{ labelOf(item) }}</span>
36
+ <span v-if="summaryOf(item)" class="cms-outline-summary">
37
+ {{ summaryOf(item) }}
38
+ </span>
39
+ </span>
40
+ <CmsIcon
41
+ v-if="isHiddenBlock(item)"
42
+ name="eye-slash"
43
+ class="cms-outline-hidden size-4 shrink-0"
44
+ />
45
+ </button>
46
+ </li>
47
+ </ol>
48
+ </template>
49
+
50
+ <script setup>
51
+ import { isHiddenBlock } from "#nuxt-cms";
52
+ import { ref } from "#imports";
53
+ import { blockSummary } from "../../../shared/preview";
54
+ import { useCmsRuntime } from "../../composables/cms-runtime";
55
+ const props = defineProps({
56
+ items: { type: Array, required: true },
57
+ field: { type: Object, required: true },
58
+ locale: { type: String, required: true },
59
+ selected: { type: [Number, null], required: true }
60
+ });
61
+ const emit = defineEmits(["select", "move"]);
62
+ const { i18n } = useCmsRuntime();
63
+ let uidCounter = 0;
64
+ const uids = /* @__PURE__ */ new WeakMap();
65
+ function uidFor(item) {
66
+ let uid = uids.get(item);
67
+ if (uid === void 0) {
68
+ uid = ++uidCounter;
69
+ uids.set(item, uid);
70
+ }
71
+ return uid;
72
+ }
73
+ function blockOf(item) {
74
+ return props.field.blocks?.[String(item.type)];
75
+ }
76
+ function labelOf(item) {
77
+ return blockOf(item)?.label ?? String(item.type);
78
+ }
79
+ function summaryOf(item) {
80
+ return blockSummary(blockOf(item), item, props.locale, i18n.defaultLocale, 60);
81
+ }
82
+ const dragIndex = ref(null);
83
+ const dropIndex = ref(null);
84
+ function onDragStart(index, event) {
85
+ dragIndex.value = index;
86
+ if (event.dataTransfer) {
87
+ event.dataTransfer.effectAllowed = "move";
88
+ event.dataTransfer.setData("text/plain", String(index));
89
+ }
90
+ }
91
+ function onDrop(index) {
92
+ if (dragIndex.value !== null && dragIndex.value !== index) emit("move", dragIndex.value, index);
93
+ onDragEnd();
94
+ }
95
+ function onDragEnd() {
96
+ dragIndex.value = null;
97
+ dropIndex.value = null;
98
+ }
99
+ </script>
@@ -0,0 +1,17 @@
1
+ import type { FieldConfig } from '#nuxt-cms';
2
+ import type { CmsBlockValue } from '../../../shared/preview.js';
3
+ type __VLS_Props = {
4
+ items: CmsBlockValue[];
5
+ field: FieldConfig;
6
+ locale: string;
7
+ selected: number | null;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ select: (index: number) => any;
11
+ move: (from: number, to: number) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
13
+ onSelect?: ((index: number) => any) | undefined;
14
+ onMove?: ((from: number, to: number) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ declare const _default: typeof __VLS_export;
17
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import type { BlockConfig } from '#nuxt-cms';
2
+ type __VLS_Props = {
3
+ blocks: Record<string, BlockConfig>;
4
+ label?: string;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
+ pick: (type: string) => any;
8
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
9
+ onPick?: ((type: string) => any) | undefined;
10
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ declare const _default: typeof __VLS_export;
12
+ export default _default;
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <div class="cms-palette" role="group" :aria-label="label">
3
+ <button
4
+ v-for="(block, type) in blocks"
5
+ :key="type"
6
+ type="button"
7
+ class="cms-palette-item"
8
+ @click="emit('pick', String(type))"
9
+ >
10
+ <span class="cms-palette-icon">
11
+ <CmsIcon :name="block.icon ?? 'rectangle-stack'" class="size-4" />
12
+ </span>
13
+ <span class="cms-palette-text">
14
+ <span class="cms-palette-label">{{ block.label }}</span>
15
+ <span v-if="block.description" class="cms-palette-description">
16
+ {{ block.description }}
17
+ </span>
18
+ </span>
19
+ </button>
20
+ </div>
21
+ </template>
22
+
23
+ <script setup>
24
+ defineProps({
25
+ blocks: { type: Object, required: true },
26
+ label: { type: String, required: false, default: "Available blocks" }
27
+ });
28
+ const emit = defineEmits(["pick"]);
29
+ </script>
@@ -0,0 +1,12 @@
1
+ import type { BlockConfig } from '#nuxt-cms';
2
+ type __VLS_Props = {
3
+ blocks: Record<string, BlockConfig>;
4
+ label?: string;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
+ pick: (type: string) => any;
8
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
9
+ onPick?: ((type: string) => any) | undefined;
10
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ declare const _default: typeof __VLS_export;
12
+ export default _default;
@@ -2,6 +2,8 @@ import type { FieldConfig } from '#nuxt-cms';
2
2
  type __VLS_Props = {
3
3
  field: FieldConfig;
4
4
  locale?: string;
5
+ entryName?: string;
6
+ fieldKey?: string;
5
7
  };
6
8
  type __VLS_ModelProps = {
7
9
  modelValue: Record<string, unknown>[] | null;
@@ -112,6 +112,45 @@
112
112
  </button>
113
113
  </div>
114
114
 
115
+ <CmsButton
116
+ v-if="visual"
117
+ class="self-start"
118
+ label="Open visual editor"
119
+ icon="window"
120
+ size="sm"
121
+ variant="soft"
122
+ color="neutral"
123
+ @click="visualOpen = true"
124
+ />
125
+
126
+ <Teleport to="body">
127
+ <div v-if="visual && visualOpen" class="cms-scope cms-editor-overlay" :data-theme="theme">
128
+ <CmsVisualEditor
129
+ :state="{ [fieldKey]: model }"
130
+ :entry-name="entryName"
131
+ :field-key="fieldKey"
132
+ :fields="{ [fieldKey]: field }"
133
+ :title="field.label"
134
+ @update:state="onVisualUpdate"
135
+ >
136
+ <template #start>
137
+ <CmsButton
138
+ icon="arrow-left"
139
+ size="sm"
140
+ variant="ghost"
141
+ color="neutral"
142
+ aria-label="Back to the form"
143
+ title="Back to the form"
144
+ @click="visualOpen = false"
145
+ />
146
+ </template>
147
+ <template #actions>
148
+ <CmsButton label="Done" size="sm" @click="visualOpen = false" />
149
+ </template>
150
+ </CmsVisualEditor>
151
+ </div>
152
+ </Teleport>
153
+
115
154
  <CmsModal v-if="mediaOnly" v-model:open="pickerOpen" title="Media" size="lg">
116
155
  <template #body>
117
156
  <CmsMediaGallery
@@ -167,6 +206,14 @@
167
206
  color="neutral"
168
207
  @click="duplicateCurrent"
169
208
  />
209
+ <CmsButton
210
+ v-if="editorItem && !declaresHiddenField(blockOf(editorItem))"
211
+ :label="isHidden(editorItem) ? 'Show' : 'Hide'"
212
+ :icon="isHidden(editorItem) ? 'eye' : 'eye-slash'"
213
+ variant="soft"
214
+ color="neutral"
215
+ @click="toggleHiddenCurrent"
216
+ />
170
217
  </div>
171
218
  <div class="cms-actions">
172
219
  <CmsButton
@@ -187,6 +234,8 @@
187
234
 
188
235
  <script setup>
189
236
  import {
237
+ declaresHiddenField,
238
+ isHiddenBlock,
190
239
  isTranslatableMediaField,
191
240
  mediaFilename,
192
241
  mediaIconFor,
@@ -194,13 +243,17 @@ import {
194
243
  mediaTypeForKey,
195
244
  pickTranslatedMedia
196
245
  } from "#nuxt-cms";
197
- import { computed, ref, useState } from "#imports";
246
+ import { computed, ref, useState, watch } from "#imports";
198
247
  import { useCmsMediaKeys } from "../../composables/cms-media-keys";
248
+ import { useCmsOverlay } from "../../composables/cms-overlay";
199
249
  import { useCmsRuntime } from "../../composables/cms-runtime";
250
+ import { useCmsTheme } from "../../composables/cms-theme";
200
251
  import { fieldIcon } from "../../utils/ui";
201
252
  const props = defineProps({
202
253
  field: { type: Object, required: true },
203
- locale: { type: String, required: false }
254
+ locale: { type: String, required: false },
255
+ entryName: { type: String, required: false },
256
+ fieldKey: { type: String, required: false }
204
257
  });
205
258
  const model = defineModel({ type: [Array, null], ...{ required: true } });
206
259
  const { mediaBaseUrl, i18n } = useCmsRuntime();
@@ -217,6 +270,23 @@ const mediaOnly = computed(() => {
217
270
  if (field.type !== "media" || isTranslatableMediaField(field)) return null;
218
271
  return { type, key, field };
219
272
  });
273
+ const visual = computed(() => {
274
+ const list = Object.values(blocks.value);
275
+ return !mediaOnly.value && !!props.entryName && !!props.fieldKey && !!list.length && list.every((block) => typeof block.component === "string" && !!block.component.trim());
276
+ });
277
+ const visualOpen = ref(false);
278
+ const theme = useCmsTheme();
279
+ const visualOverlay = useCmsOverlay(() => {
280
+ visualOpen.value = false;
281
+ });
282
+ watch(visualOpen, (open) => {
283
+ if (open) visualOverlay.activate();
284
+ else visualOverlay.deactivate();
285
+ });
286
+ function onVisualUpdate(state) {
287
+ const value = props.fieldKey ? state[props.fieldKey] : null;
288
+ model.value = Array.isArray(value) && value.length ? value : null;
289
+ }
220
290
  const addMediaLabel = computed(() => {
221
291
  const types = [mediaOnly.value?.field.mediaType ?? []].flat();
222
292
  return types.length === 1 && types[0] === "image" ? "Add photo" : "Add media";
@@ -293,7 +363,7 @@ function summaryOf(item) {
293
363
  return "";
294
364
  }
295
365
  function isHidden(item) {
296
- return blockOf(item)?.fields.hidden?.type === "boolean" && item.hidden === true;
366
+ return isHiddenBlock(item);
297
367
  }
298
368
  function updateField(index, key, value) {
299
369
  const current = items.value[index];
@@ -406,6 +476,16 @@ function moveCurrent(delta) {
406
476
  move(editorIndex.value, delta);
407
477
  editorIndex.value = next;
408
478
  }
479
+ function toggleHiddenCurrent() {
480
+ if (editorIndex.value === null || !editorItem.value) return;
481
+ const index = editorIndex.value;
482
+ const current = items.value[index];
483
+ if (!current) return;
484
+ const { hidden: _hidden, ...rest } = current;
485
+ const updated = isHidden(current) ? rest : { ...rest, hidden: true };
486
+ uids.set(updated, uidFor(current));
487
+ model.value = items.value.map((item, i) => i === index ? updated : item);
488
+ }
409
489
  function duplicateCurrent() {
410
490
  if (editorIndex.value === null) return;
411
491
  duplicate(editorIndex.value);
@@ -2,6 +2,8 @@ import type { FieldConfig } from '#nuxt-cms';
2
2
  type __VLS_Props = {
3
3
  field: FieldConfig;
4
4
  locale?: string;
5
+ entryName?: string;
6
+ fieldKey?: string;
5
7
  };
6
8
  type __VLS_ModelProps = {
7
9
  modelValue: Record<string, unknown>[] | null;
@@ -70,9 +70,9 @@
70
70
  v-model="state[key]"
71
71
  :field="fields[key]"
72
72
  :locale="localeFor(key)"
73
- :slug-source="
74
- fields[key].from ? state[fields[key].from] : void 0
75
- "
73
+ :slug-source="slugSource(fields[key])"
74
+ :entry-name="entryName"
75
+ :field-key="key"
76
76
  />
77
77
  </CmsFormField>
78
78
  </div>
@@ -99,7 +99,8 @@ import {
99
99
  hasTranslatableBlockFields,
100
100
  isFieldVisible,
101
101
  isTranslatableField,
102
- resolveFormLayout
102
+ resolveFormLayout,
103
+ slugSourceValue
103
104
  } from "#nuxt-cms";
104
105
  import { computed, nextTick, ref, watch } from "#imports";
105
106
  import { buildEntrySchema } from "../../../shared/validation";
@@ -168,6 +169,10 @@ function onError() {
168
169
  if (key) void nextTick(() => revealField(key));
169
170
  emit("error");
170
171
  }
172
+ function slugSource(field) {
173
+ if (!field.from) return void 0;
174
+ return slugSourceValue(props.fields[field.from], state.value[field.from], i18n.defaultLocale);
175
+ }
171
176
  function hasLocaleSwitch(field) {
172
177
  return isTranslatableField(field) || hasTranslatableBlockFields(field);
173
178
  }
@@ -3,6 +3,8 @@ type __VLS_Props = {
3
3
  field: FieldConfig;
4
4
  slugSource?: string | null;
5
5
  locale?: string;
6
+ entryName?: string;
7
+ fieldKey?: string;
6
8
  };
7
9
  type __VLS_ModelProps = {
8
10
  modelValue: unknown;
@@ -10,6 +10,8 @@
10
10
  v-model="blocksValue"
11
11
  :field="field"
12
12
  :locale="locale"
13
+ :entry-name="entryName"
14
+ :field-key="fieldKey"
13
15
  />
14
16
  <CmsRichTextField v-else-if="field.type === 'richtext'" v-model="strOrNull" />
15
17
  <CmsTextarea
@@ -50,6 +52,7 @@
50
52
  :placeholder="field.placeholder"
51
53
  />
52
54
  <CmsInput v-else-if="field.type === 'date'" v-model="str" type="date" />
55
+ <CmsInput v-else-if="field.type === 'datetime'" v-model="localDateTime" type="datetime-local" />
53
56
  <CmsRelationField
54
57
  v-else-if="field.type === 'relation'"
55
58
  v-model="relationValue"
@@ -66,7 +69,9 @@ import { computed } from "#imports";
66
69
  const props = defineProps({
67
70
  field: { type: Object, required: true },
68
71
  slugSource: { type: [String, null], required: false },
69
- locale: { type: String, required: false }
72
+ locale: { type: String, required: false },
73
+ entryName: { type: String, required: false },
74
+ fieldKey: { type: String, required: false }
70
75
  });
71
76
  const model = defineModel({ type: null, ...{ required: true } });
72
77
  const selectItems = computed(() => [
@@ -97,6 +102,18 @@ const translatable = proxy((v) => v);
97
102
  const blocksValue = proxy(
98
103
  (v) => v
99
104
  );
105
+ function toLocalDateTime(value) {
106
+ if (typeof value !== "string" || !value) return "";
107
+ const date = new Date(value);
108
+ if (Number.isNaN(date.getTime())) return "";
109
+ return new Date(date.getTime() - date.getTimezoneOffset() * 6e4).toISOString().slice(0, 16);
110
+ }
111
+ function fromLocalDateTime(value) {
112
+ if (!value) return null;
113
+ const date = new Date(value);
114
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
115
+ }
116
+ const localDateTime = proxy(toLocalDateTime, fromLocalDateTime);
100
117
  const relationValue = proxy((v) => v);
101
118
  const jsonValue = proxy((v) => v);
102
119
  </script>
@@ -3,6 +3,8 @@ type __VLS_Props = {
3
3
  field: FieldConfig;
4
4
  slugSource?: string | null;
5
5
  locale?: string;
6
+ entryName?: string;
7
+ fieldKey?: string;
6
8
  };
7
9
  type __VLS_ModelProps = {
8
10
  modelValue: unknown;
@@ -69,7 +69,8 @@ import { errorMessage } from "../../utils/ui";
69
69
  const KIND_LABELS = {
70
70
  collection: "Collection",
71
71
  single: "Single",
72
- page: "Pages"
72
+ page: "Pages",
73
+ content: "Content"
73
74
  };
74
75
  const STATUS_LABELS = {
75
76
  idle: "Changes save automatically",
@@ -85,7 +86,7 @@ const toast = useCmsToast();
85
86
  const entry = computed(() => cmsConfig[props.name]);
86
87
  const fields = computed(() => entryFields(props.name));
87
88
  const parts = computed(() => [
88
- ...entry.value.kind === "collection" ? [{ id: "columns", label: "Table columns" }] : [],
89
+ ...entry.value.kind === "collection" || entry.value.kind === "content" ? [{ id: "columns", label: "Table columns" }] : [],
89
90
  { id: "layout", label: "Form layout" }
90
91
  ]);
91
92
  const activePart = ref(parts.value[0].id);
@@ -144,7 +144,8 @@ const emit = defineEmits(["logout"]);
144
144
  const KIND_ICONS = {
145
145
  collection: "square-3-stack-3d",
146
146
  single: "document-text",
147
- page: "window"
147
+ page: "window",
148
+ content: "newspaper"
148
149
  };
149
150
  const ENTRY_PREFIX = "entry:";
150
151
  const entries = Object.entries(cmsConfig).map(([name, entry]) => ({