@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
@@ -2,7 +2,7 @@ import type { PgTable } from 'drizzle-orm/pg-core';
2
2
  import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
3
3
  import type { CmsLayoutBlock, CmsListInput } from './layout.js';
4
4
  export type CmsTable = SQLiteTable | PgTable;
5
- export type FieldType = 'text' | 'richtext' | 'number' | 'boolean' | 'date' | 'email' | 'slug' | 'select' | 'json' | 'media' | 'relation' | 'blocks';
5
+ export type FieldType = 'text' | 'richtext' | 'number' | 'boolean' | 'date' | 'datetime' | 'email' | 'slug' | 'select' | 'json' | 'media' | 'relation' | 'blocks';
6
6
  export interface CmsI18n {
7
7
  locales: string[];
8
8
  defaultLocale: string;
@@ -76,6 +76,9 @@ export interface MediaUsageGroup {
76
76
  export declare function mediaUsageGroups(usage: Record<string, MediaUsage[]>, keys: string[]): MediaUsageGroup[];
77
77
  export interface BlockConfig {
78
78
  label: string;
79
+ component?: string;
80
+ icon?: string;
81
+ description?: string;
79
82
  fields: Record<string, FieldConfig>;
80
83
  }
81
84
  export type ConditionValue = string | number | boolean;
@@ -129,10 +132,14 @@ export declare function isMultiSelect(field: FieldConfig): boolean;
129
132
  export declare function translatableFieldKeys(entry: EntryLike): string[];
130
133
  export declare function translatableBlockFieldKeys(block: BlockConfig): string[];
131
134
  export declare function hasTranslatableBlockFields(field: FieldConfig): boolean;
135
+ export declare const BLOCK_HIDDEN_KEY = "hidden";
136
+ export declare function declaresHiddenField(block: BlockConfig | undefined): boolean;
137
+ export declare function isHiddenBlock(item: unknown): boolean;
138
+ export declare function publicBlocks(field: FieldConfig, value: unknown): unknown;
132
139
  export declare function localizeBlock(field: FieldConfig, item: unknown, locale: string, defaultLocale: string): unknown;
133
140
  export declare function localizeBlocks(field: FieldConfig, value: unknown, locale: string, defaultLocale: string): unknown;
134
141
  export declare const CMS_GRAPHQL_BATCH_LIMIT = 20;
135
- export type CmsEntryKind = 'collection' | 'single' | 'page';
142
+ export type CmsEntryKind = 'collection' | 'single' | 'page' | 'content';
136
143
  export interface CmsPageRoute {
137
144
  path: string;
138
145
  key: string;
@@ -158,10 +165,35 @@ export interface CmsEntry {
158
165
  icon?: string;
159
166
  layout?: CmsLayoutBlock[];
160
167
  list?: CmsListInput;
168
+ blocks?: Record<string, BlockConfig>;
169
+ preview?: CmsPreviewOptions;
170
+ declared?: CmsContentDeclared;
171
+ }
172
+ export interface CmsPreviewOptions {
173
+ component?: string;
174
+ }
175
+ export interface CmsContentDeclared {
176
+ fields: string[];
177
+ titleField?: unknown;
178
+ drafts?: unknown;
161
179
  }
162
180
  export declare const MOBILE_MEDIA_SUFFIX = "Mobile";
163
181
  export declare function mobileMediaKey(key: string): string;
164
182
  export declare function expandMobileMedia<T extends Record<string, unknown>>(config: T): T;
183
+ export declare const CONTENT_TITLE_FIELD = "title";
184
+ export declare const CONTENT_SLUG_FIELD = "slug";
185
+ export declare const CONTENT_PUBLISHED_AT_FIELD = "publishedAt";
186
+ export declare const CONTENT_BODY_FIELD = "body";
187
+ export declare const CONTENT_SYSTEM_FIELDS: readonly ["title", "slug", "publishedAt", "excerpt", "cover", "body", "seoTitle", "seoDescription", "seoImage"];
188
+ export type CmsContentSystemField = (typeof CONTENT_SYSTEM_FIELDS)[number];
189
+ export declare const CONTENT_LIST_COLUMNS: string[];
190
+ export declare function isContentEntry(entry: Pick<CmsEntry, 'kind'>): boolean;
191
+ export declare function isCollectionKind(entry: Pick<CmsEntry, 'kind'>): boolean;
192
+ export declare function normalizeContentEntries<T extends Record<string, unknown>>(config: T, i18n?: Pick<CmsI18n, 'locales'>): T;
193
+ export declare function normalizeCmsConfig<T extends Record<string, unknown>>(config: T, i18n?: Pick<CmsI18n, 'locales'>): T;
194
+ export declare function slugSourceValue(source: FieldConfig | undefined, value: unknown, defaultLocale: string): string | null;
195
+ export declare function isScheduledContent(row: Record<string, unknown> | null | undefined, now?: Date): boolean;
196
+ export declare function resolveBlocksMedia(field: FieldConfig, value: unknown, resolve: (key: string) => unknown): unknown;
165
197
  export declare function entryTabs(entry: Pick<CmsEntry, 'tabs'>): CmsTab[];
166
198
  export declare function fieldTab(field: FieldConfig, tabs: CmsTab[]): string | undefined;
167
199
  export declare function fieldsByTab(fields: Record<string, FieldConfig>, tabs: CmsTab[]): Record<string, Record<string, FieldConfig>>;
@@ -214,6 +246,9 @@ export interface BooleanFieldInput extends FieldInputBase {
214
246
  export interface DateFieldInput extends FieldInputBase {
215
247
  type: 'date';
216
248
  }
249
+ export interface DateTimeFieldInput extends FieldInputBase {
250
+ type: 'datetime';
251
+ }
217
252
  export interface EmailFieldInput extends FieldInputBase {
218
253
  type: 'email';
219
254
  }
@@ -243,16 +278,19 @@ export interface RelationFieldInput extends FieldInputBase {
243
278
  onDelete?: 'set null' | 'cascade' | 'restrict';
244
279
  }
245
280
  type BlockField<T> = Omit<T, 'private' | 'showIf' | 'tab'>;
246
- export type BlockFieldInput = BlockField<TextFieldInput> | BlockField<RichtextFieldInput> | BlockField<NumberFieldInput> | BlockField<BooleanFieldInput> | BlockField<DateFieldInput> | BlockField<EmailFieldInput> | BlockField<SelectFieldInput> | BlockField<JsonFieldInput> | Omit<BlockField<MediaFieldInput>, 'mobile'>;
281
+ export type BlockFieldInput = BlockField<TextFieldInput> | BlockField<RichtextFieldInput> | BlockField<NumberFieldInput> | BlockField<BooleanFieldInput> | BlockField<DateFieldInput> | BlockField<DateTimeFieldInput> | BlockField<EmailFieldInput> | BlockField<SelectFieldInput> | BlockField<JsonFieldInput> | Omit<BlockField<MediaFieldInput>, 'mobile'>;
247
282
  export interface BlockInput {
248
283
  label: string;
284
+ component?: string;
285
+ icon?: string;
286
+ description?: string;
249
287
  fields: Record<string, BlockFieldInput>;
250
288
  }
251
289
  export interface BlocksFieldInput extends FieldInputBase {
252
290
  type: 'blocks';
253
291
  blocks: Record<string, BlockInput>;
254
292
  }
255
- export type CmsFieldInput = TextFieldInput | RichtextFieldInput | NumberFieldInput | BooleanFieldInput | DateFieldInput | EmailFieldInput | SlugFieldInput | SelectFieldInput | JsonFieldInput | MediaFieldInput | RelationFieldInput | BlocksFieldInput;
293
+ export type CmsFieldInput = TextFieldInput | RichtextFieldInput | NumberFieldInput | BooleanFieldInput | DateFieldInput | DateTimeFieldInput | EmailFieldInput | SlugFieldInput | SelectFieldInput | JsonFieldInput | MediaFieldInput | RelationFieldInput | BlocksFieldInput;
256
294
  interface CmsEntryInputBase {
257
295
  id: string;
258
296
  label: string;
@@ -283,8 +321,24 @@ export interface CmsPageInput extends CmsEntryInputBase {
283
321
  overrides?: Record<string, Record<string, CmsFieldInput | null>>;
284
322
  columns?: string[];
285
323
  }
286
- export type CmsEntryInput = CmsCollectionInput | CmsSingleInput | CmsPageInput;
324
+ export interface CmsContentInput {
325
+ id: string;
326
+ label: string;
327
+ kind: 'content';
328
+ icon?: string;
329
+ blocks: Record<string, BlockInput>;
330
+ fields?: Record<string, CmsFieldInput>;
331
+ labels?: Partial<Record<CmsContentSystemField, string>>;
332
+ tabs?: CmsTab[];
333
+ layout?: CmsLayoutBlock[];
334
+ list?: CmsListInput;
335
+ preview?: CmsPreviewOptions;
336
+ titleField?: never;
337
+ drafts?: never;
338
+ }
339
+ export type CmsEntryInput = CmsCollectionInput | CmsSingleInput | CmsPageInput | CmsContentInput;
287
340
  export type CmsConfigInput = Record<string, CmsEntryInput>;
288
341
  export declare function defineCmsConfig<T extends CmsConfigInput>(config: T): T;
342
+ export declare function defineCmsBlocks<T extends Record<string, BlockInput>>(blocks: T): T;
289
343
  export * from './layout.js';
290
344
  export * from './users.js';
@@ -219,6 +219,20 @@ export function hasTranslatableBlockFields(field) {
219
219
  (block) => Object.values(block.fields).some(isTranslatableField)
220
220
  );
221
221
  }
222
+ export const BLOCK_HIDDEN_KEY = "hidden";
223
+ export function declaresHiddenField(block) {
224
+ return !!block && Object.hasOwn(block.fields, BLOCK_HIDDEN_KEY);
225
+ }
226
+ export function isHiddenBlock(item) {
227
+ return !!item && typeof item === "object" && item.hidden === true;
228
+ }
229
+ export function publicBlocks(field, value) {
230
+ if (!Array.isArray(value)) return value;
231
+ return value.filter((item) => {
232
+ if (!isHiddenBlock(item)) return true;
233
+ return declaresHiddenField(field.blocks?.[String(item.type)]);
234
+ });
235
+ }
222
236
  export function localizeBlock(field, item, locale, defaultLocale) {
223
237
  if (!item || typeof item !== "object") return item;
224
238
  const block = field.blocks?.[String(item.type)];
@@ -289,6 +303,122 @@ export function expandMobileMedia(config) {
289
303
  }
290
304
  return config;
291
305
  }
306
+ export const CONTENT_TITLE_FIELD = "title";
307
+ export const CONTENT_SLUG_FIELD = "slug";
308
+ export const CONTENT_PUBLISHED_AT_FIELD = "publishedAt";
309
+ export const CONTENT_BODY_FIELD = "body";
310
+ export const CONTENT_SYSTEM_FIELDS = [
311
+ "title",
312
+ "slug",
313
+ "publishedAt",
314
+ "excerpt",
315
+ "cover",
316
+ "body",
317
+ "seoTitle",
318
+ "seoDescription",
319
+ "seoImage"
320
+ ];
321
+ export const CONTENT_LIST_COLUMNS = ["title", "status", "publishedAt"];
322
+ export function isContentEntry(entry) {
323
+ return entry.kind === "content";
324
+ }
325
+ export function isCollectionKind(entry) {
326
+ return entry.kind === "collection" || entry.kind === "content";
327
+ }
328
+ function contentSystemFields(blocks, labels, translatable) {
329
+ const label = (key, fallback) => labels[key] || fallback;
330
+ const localized = translatable ? { translatable: true } : {};
331
+ return {
332
+ title: { label: label("title", "Title"), type: "text", required: true, ...localized },
333
+ slug: {
334
+ label: label("slug", "Slug"),
335
+ type: "slug",
336
+ from: CONTENT_TITLE_FIELD,
337
+ required: true,
338
+ description: "Filled from the title in the default language."
339
+ },
340
+ publishedAt: {
341
+ label: label("publishedAt", "Published at"),
342
+ type: "datetime",
343
+ description: "Empty means now when you publish. A future date schedules the item."
344
+ },
345
+ excerpt: {
346
+ label: label("excerpt", "Excerpt"),
347
+ type: "text",
348
+ textarea: true,
349
+ ...localized
350
+ },
351
+ cover: { label: label("cover", "Cover"), type: "media", mediaType: "image" },
352
+ body: { label: label("body", "Body"), type: "blocks", blocks },
353
+ seoTitle: { label: label("seoTitle", "SEO title"), type: "text", ...localized },
354
+ seoDescription: {
355
+ label: label("seoDescription", "SEO description"),
356
+ type: "text",
357
+ textarea: true,
358
+ ...localized
359
+ },
360
+ seoImage: { label: label("seoImage", "SEO image"), type: "media", mediaType: "image" }
361
+ };
362
+ }
363
+ function normalizeContentEntry(entry, translatable) {
364
+ if (entry.declared) return entry;
365
+ const declared = entry.fields ?? {};
366
+ const system = contentSystemFields(entry.blocks ?? {}, entry.labels ?? {}, translatable);
367
+ const extra = Object.fromEntries(
368
+ Object.entries(declared).filter(([key]) => !Object.hasOwn(system, key))
369
+ );
370
+ const { body, seoTitle, seoDescription, seoImage, ...head } = system;
371
+ return Object.assign(entry, {
372
+ declared: {
373
+ fields: Object.keys(declared),
374
+ ...entry.titleField !== void 0 ? { titleField: entry.titleField } : {},
375
+ ...entry.drafts !== void 0 ? { drafts: entry.drafts } : {}
376
+ },
377
+ titleField: CONTENT_TITLE_FIELD,
378
+ drafts: true,
379
+ fields: { ...head, ...extra, body, seoTitle, seoDescription, seoImage },
380
+ list: entry.list ?? { columns: [...CONTENT_LIST_COLUMNS] }
381
+ });
382
+ }
383
+ export function normalizeContentEntries(config, i18n) {
384
+ const translatable = !!i18n?.locales.length;
385
+ for (const entry of Object.values(config)) {
386
+ if (entry?.kind === "content") normalizeContentEntry(entry, translatable);
387
+ }
388
+ return config;
389
+ }
390
+ export function normalizeCmsConfig(config, i18n) {
391
+ return expandMobileMedia(normalizeContentEntries(config, i18n));
392
+ }
393
+ export function slugSourceValue(source, value, defaultLocale) {
394
+ if (value == null) return null;
395
+ if (source && isTranslatableField(source) && typeof value === "object") {
396
+ return value[defaultLocale] ?? null;
397
+ }
398
+ return typeof value === "string" ? value : null;
399
+ }
400
+ export function isScheduledContent(row, now = /* @__PURE__ */ new Date()) {
401
+ if (!row || row.status !== "published") return false;
402
+ const at = row[CONTENT_PUBLISHED_AT_FIELD];
403
+ if (typeof at !== "string" || !at) return false;
404
+ const time = new Date(at).getTime();
405
+ return !Number.isNaN(time) && time > now.getTime();
406
+ }
407
+ export function resolveBlocksMedia(field, value, resolve) {
408
+ if (!Array.isArray(value)) return value;
409
+ return value.map((item) => {
410
+ if (!item || typeof item !== "object") return item;
411
+ const block = field.blocks?.[String(item.type)];
412
+ if (!block) return item;
413
+ const resolved = { ...item };
414
+ for (const [key, blockField] of Object.entries(block.fields)) {
415
+ if (blockField.type !== "media") continue;
416
+ const media = resolved[key];
417
+ resolved[key] = typeof media === "string" && media ? resolve(media) ?? null : null;
418
+ }
419
+ return resolved;
420
+ });
421
+ }
292
422
  export function entryTabs(entry) {
293
423
  return entry.tabs?.length ? entry.tabs : [];
294
424
  }
@@ -392,5 +522,8 @@ export function blockTypeName(entryName, fieldKey, blockName) {
392
522
  export function defineCmsConfig(config) {
393
523
  return config;
394
524
  }
525
+ export function defineCmsBlocks(blocks) {
526
+ return blocks;
527
+ }
395
528
  export * from "./layout.js";
396
529
  export * from "./users.js";
@@ -0,0 +1,85 @@
1
+ import type { BlockConfig, FieldConfig, MediaItem, MediaType } from './index.js';
2
+ export declare const CMS_PREVIEW_CHANNEL = "nuxt-cms:preview";
3
+ export declare const DEFAULT_CMS_PREVIEW_PATH = "/cms/preview";
4
+ export declare function previewPathErrors(path: string, entryNames: string[]): string[];
5
+ export type CmsBlockValue = Record<string, unknown>;
6
+ export interface CmsPreviewMedia {
7
+ key: string;
8
+ url: string | null;
9
+ type: MediaType;
10
+ alt: string | null;
11
+ folder: string | null;
12
+ mime: string | null;
13
+ size: number | null;
14
+ width: number | null;
15
+ height: number | null;
16
+ }
17
+ export interface CmsPreviewState {
18
+ entry: string;
19
+ field: string;
20
+ locale: string;
21
+ blocks: CmsBlockValue[];
22
+ item: Record<string, unknown> | null;
23
+ media: Record<string, CmsPreviewMedia>;
24
+ selected: number | null;
25
+ }
26
+ export type CmsPreviewHostMessage = {
27
+ type: 'render';
28
+ state: CmsPreviewState;
29
+ };
30
+ export type CmsPreviewFrameMessage = {
31
+ type: 'ready';
32
+ } | {
33
+ type: 'select';
34
+ index: number | null;
35
+ focus?: boolean;
36
+ } | {
37
+ type: 'insert';
38
+ index: number;
39
+ } | {
40
+ type: 'move';
41
+ from: number;
42
+ to: number;
43
+ focus?: boolean;
44
+ } | {
45
+ type: 'duplicate';
46
+ index: number;
47
+ } | {
48
+ type: 'toggle-hidden';
49
+ index: number;
50
+ } | {
51
+ type: 'remove';
52
+ index: number;
53
+ };
54
+ export type CmsPreviewEnvelope<T> = T & {
55
+ channel: typeof CMS_PREVIEW_CHANNEL;
56
+ };
57
+ export declare function previewEnvelope<T extends {
58
+ type: string;
59
+ }>(message: T): CmsPreviewEnvelope<T>;
60
+ export declare function parseFrameMessage(data: unknown): CmsPreviewFrameMessage | null;
61
+ export declare function parseHostMessage(data: unknown): CmsPreviewHostMessage | null;
62
+ export declare function isTrustedPreviewEvent(event: {
63
+ origin: string;
64
+ source: unknown;
65
+ }, origin: string, source: unknown): boolean;
66
+ export declare function emptyBlock(type: string, block: BlockConfig): CmsBlockValue;
67
+ export declare function insertBlock(list: readonly CmsBlockValue[], index: number, item: CmsBlockValue): CmsBlockValue[];
68
+ export declare function moveBlock(list: readonly CmsBlockValue[], from: number, to: number): CmsBlockValue[];
69
+ export declare function duplicateBlock(list: readonly CmsBlockValue[], index: number): CmsBlockValue[];
70
+ export declare function removeBlock(list: readonly CmsBlockValue[], index: number): CmsBlockValue[];
71
+ export declare function canHideBlock(block: BlockConfig | undefined): boolean;
72
+ export declare function toggleBlockHidden(list: readonly CmsBlockValue[], index: number, block: BlockConfig | undefined): CmsBlockValue[];
73
+ export declare function selectionAfterMove(selected: number | null, from: number, to: number): number | null;
74
+ export declare function selectionAfterRemove(selected: number | null, index: number): number | null;
75
+ export declare function moveTargetForInsertion(from: number, insertAt: number): number;
76
+ export declare function insertionIndex(blocks: readonly {
77
+ index: number;
78
+ top: number;
79
+ bottom: number;
80
+ }[], y: number, total: number): number;
81
+ export declare function collectMediaKeys(fields: Record<string, FieldConfig>, values: Record<string, unknown> | null | undefined): string[];
82
+ export declare function previewMedia(key: string, item: MediaItem | undefined, baseUrl: string | null | undefined): CmsPreviewMedia;
83
+ export declare function previewBlocks(field: FieldConfig, value: unknown, locale: string, defaultLocale: string, media: Record<string, CmsPreviewMedia>): CmsBlockValue[];
84
+ export declare function previewItem(fields: Record<string, FieldConfig>, values: Record<string, unknown>, locale: string, defaultLocale: string, media: Record<string, CmsPreviewMedia>): Record<string, unknown>;
85
+ export declare function blockSummary(block: BlockConfig | undefined, item: CmsBlockValue, locale: string, defaultLocale: string, max?: number): string;
@@ -0,0 +1,219 @@
1
+ import {
2
+ BLOCK_HIDDEN_KEY,
3
+ declaresHiddenField,
4
+ decodeTranslatableValue,
5
+ isHiddenBlock,
6
+ isPrivateField,
7
+ isTranslatableField,
8
+ isTranslatableMediaField,
9
+ localizeBlocks,
10
+ mediaPublicUrl,
11
+ mediaTypeForKey,
12
+ pickTranslatedMedia,
13
+ resolveBlocksMedia
14
+ } from "./index.js";
15
+ export const CMS_PREVIEW_CHANNEL = "nuxt-cms:preview";
16
+ export const DEFAULT_CMS_PREVIEW_PATH = "/cms/preview";
17
+ const RESERVED_ADMIN_SEGMENTS = /* @__PURE__ */ new Set(["login", "media"]);
18
+ export function previewPathErrors(path, entryNames) {
19
+ const match = /^\/cms\/([a-z0-9][a-z0-9-]*)$/.exec(path);
20
+ if (!match)
21
+ return [
22
+ `preview.path '${path}' must be a single lowercase segment under /cms, like '${DEFAULT_CMS_PREVIEW_PATH}'`
23
+ ];
24
+ const segment = match[1];
25
+ if (RESERVED_ADMIN_SEGMENTS.has(segment) || entryNames.includes(segment))
26
+ return [`preview.path '${path}' clashes with an admin route, pick another segment`];
27
+ return [];
28
+ }
29
+ export function previewEnvelope(message) {
30
+ return { ...message, channel: CMS_PREVIEW_CHANNEL };
31
+ }
32
+ function isRecord(value) {
33
+ return !!value && typeof value === "object" && !Array.isArray(value);
34
+ }
35
+ function isIndex(value) {
36
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
37
+ }
38
+ function envelope(data) {
39
+ if (!isRecord(data) || data.channel !== CMS_PREVIEW_CHANNEL) return null;
40
+ return typeof data.type === "string" ? data : null;
41
+ }
42
+ export function parseFrameMessage(data) {
43
+ const message = envelope(data);
44
+ if (!message) return null;
45
+ const focus = message.focus === true ? { focus: true } : {};
46
+ switch (message.type) {
47
+ case "ready":
48
+ return { type: "ready" };
49
+ case "select":
50
+ return message.index === null || isIndex(message.index) ? { type: "select", index: message.index, ...focus } : null;
51
+ case "move":
52
+ return isIndex(message.from) && isIndex(message.to) ? { type: "move", from: message.from, to: message.to, ...focus } : null;
53
+ case "insert":
54
+ case "duplicate":
55
+ case "toggle-hidden":
56
+ case "remove":
57
+ return isIndex(message.index) ? { type: message.type, index: message.index } : null;
58
+ default:
59
+ return null;
60
+ }
61
+ }
62
+ export function parseHostMessage(data) {
63
+ const message = envelope(data);
64
+ if (!message || message.type !== "render" || !isRecord(message.state)) return null;
65
+ const state = message.state;
66
+ if (typeof state.entry !== "string" || typeof state.field !== "string") return null;
67
+ if (typeof state.locale !== "string" || !Array.isArray(state.blocks)) return null;
68
+ if (state.item !== null && !isRecord(state.item)) return null;
69
+ if (!isRecord(state.media)) return null;
70
+ if (state.selected !== null && !isIndex(state.selected)) return null;
71
+ return {
72
+ type: "render",
73
+ state: {
74
+ entry: state.entry,
75
+ field: state.field,
76
+ locale: state.locale,
77
+ blocks: state.blocks.filter(isRecord),
78
+ item: state.item,
79
+ media: state.media,
80
+ selected: state.selected
81
+ }
82
+ };
83
+ }
84
+ export function isTrustedPreviewEvent(event, origin, source) {
85
+ return !!source && event.origin === origin && event.source === source;
86
+ }
87
+ function clamp(index, length) {
88
+ return Math.min(Math.max(index, 0), length);
89
+ }
90
+ export function emptyBlock(type, block) {
91
+ return { type, ...Object.fromEntries(Object.keys(block.fields).map((key) => [key, null])) };
92
+ }
93
+ export function insertBlock(list, index, item) {
94
+ const next = [...list];
95
+ next.splice(clamp(index, list.length), 0, item);
96
+ return next;
97
+ }
98
+ export function moveBlock(list, from, to) {
99
+ if (from === to || from < 0 || from >= list.length || to < 0 || to >= list.length)
100
+ return [...list];
101
+ const next = [...list];
102
+ const [item] = next.splice(from, 1);
103
+ next.splice(to, 0, item);
104
+ return next;
105
+ }
106
+ export function duplicateBlock(list, index) {
107
+ const item = list[index];
108
+ if (!item) return [...list];
109
+ return insertBlock(list, index + 1, JSON.parse(JSON.stringify(item)));
110
+ }
111
+ export function removeBlock(list, index) {
112
+ return list.filter((_, i) => i !== index);
113
+ }
114
+ export function canHideBlock(block) {
115
+ return !!block && !declaresHiddenField(block);
116
+ }
117
+ export function toggleBlockHidden(list, index, block) {
118
+ const item = list[index];
119
+ if (!item || !canHideBlock(block)) return [...list];
120
+ const { [BLOCK_HIDDEN_KEY]: _hidden, ...rest } = item;
121
+ const updated = isHiddenBlock(item) ? rest : { ...rest, [BLOCK_HIDDEN_KEY]: true };
122
+ return list.map((current, i) => i === index ? updated : current);
123
+ }
124
+ export function selectionAfterMove(selected, from, to) {
125
+ if (selected === null) return null;
126
+ if (selected === from) return to;
127
+ if (from < selected && to >= selected) return selected - 1;
128
+ if (from > selected && to <= selected) return selected + 1;
129
+ return selected;
130
+ }
131
+ export function selectionAfterRemove(selected, index) {
132
+ if (selected === null || selected === index) return null;
133
+ return selected > index ? selected - 1 : selected;
134
+ }
135
+ export function moveTargetForInsertion(from, insertAt) {
136
+ return insertAt > from ? insertAt - 1 : insertAt;
137
+ }
138
+ export function insertionIndex(blocks, y, total) {
139
+ for (const block of blocks) {
140
+ if (y < (block.top + block.bottom) / 2) return block.index;
141
+ }
142
+ const last = blocks[blocks.length - 1];
143
+ return last ? last.index + 1 : total;
144
+ }
145
+ function pushMediaKeys(keys, field, value) {
146
+ if (field.type !== "media" || value == null) return;
147
+ const values = typeof value === "object" ? Object.values(value) : [value];
148
+ for (const key of values) if (typeof key === "string" && key) keys.add(key);
149
+ }
150
+ export function collectMediaKeys(fields, values) {
151
+ const keys = /* @__PURE__ */ new Set();
152
+ for (const [key, field] of Object.entries(fields)) {
153
+ const value = values?.[key];
154
+ if (field.type === "media") {
155
+ pushMediaKeys(keys, field, value);
156
+ continue;
157
+ }
158
+ if (field.type !== "blocks" || !Array.isArray(value)) continue;
159
+ for (const item of value) {
160
+ if (!isRecord(item)) continue;
161
+ const block = field.blocks?.[String(item.type)];
162
+ for (const [blockKey, blockField] of Object.entries(block?.fields ?? {}))
163
+ pushMediaKeys(keys, blockField, item[blockKey]);
164
+ }
165
+ }
166
+ return [...keys];
167
+ }
168
+ export function previewMedia(key, item, baseUrl) {
169
+ return {
170
+ key,
171
+ url: item?.url ?? mediaPublicUrl(baseUrl, key),
172
+ type: item?.type ?? mediaTypeForKey(key),
173
+ alt: item?.alt ?? null,
174
+ folder: item?.folder ?? null,
175
+ mime: item?.mime ?? null,
176
+ size: item?.size ?? null,
177
+ width: item?.width ?? null,
178
+ height: item?.height ?? null
179
+ };
180
+ }
181
+ export function previewBlocks(field, value, locale, defaultLocale, media) {
182
+ if (!Array.isArray(value)) return [];
183
+ const localized = localizeBlocks(field, value, locale, defaultLocale);
184
+ return resolveBlocksMedia(field, localized, (key) => media[key] ?? null);
185
+ }
186
+ export function previewItem(fields, values, locale, defaultLocale, media) {
187
+ const item = {};
188
+ for (const [key, field] of Object.entries(fields)) {
189
+ if (isPrivateField(field)) continue;
190
+ const value = values[key] ?? null;
191
+ if (field.type === "blocks") {
192
+ item[key] = previewBlocks(field, value, locale, defaultLocale, media);
193
+ } else if (field.type === "media") {
194
+ const mediaKey = isTranslatableMediaField(field) ? pickTranslatedMedia(
195
+ decodeTranslatableValue(value, defaultLocale),
196
+ locale,
197
+ defaultLocale
198
+ ) : value;
199
+ item[key] = typeof mediaKey === "string" && mediaKey ? media[mediaKey] ?? null : null;
200
+ } else if (isTranslatableField(field)) {
201
+ const translations = decodeTranslatableValue(value, defaultLocale);
202
+ item[key] = translations?.[locale] ?? translations?.[defaultLocale] ?? null;
203
+ } else {
204
+ item[key] = value;
205
+ }
206
+ }
207
+ return item;
208
+ }
209
+ export function blockSummary(block, item, locale, defaultLocale, max = 80) {
210
+ for (const [key, field] of Object.entries(block?.fields ?? {})) {
211
+ if (field.type !== "text" && field.type !== "richtext") continue;
212
+ const raw = item[key];
213
+ const translations = isRecord(raw) ? raw : null;
214
+ const value = typeof raw === "string" ? raw : translations?.[locale] || translations?.[defaultLocale] || "";
215
+ const text = value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
216
+ if (text) return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
217
+ }
218
+ return "";
219
+ }
@@ -4,11 +4,13 @@ export declare const objectKeySchema: z.ZodString;
4
4
  export interface ValidationMessages {
5
5
  required: string;
6
6
  invalidDate: string;
7
+ invalidDateTime: string;
7
8
  invalidEmail: string;
8
9
  invalidSlug: string;
9
10
  unknownLocale: string;
10
11
  requiredLocale: (locale: string) => string;
11
12
  }
13
+ export declare function datetimeSchema(message: string): z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
12
14
  export declare function buildEntrySchema(entry: Pick<CmsEntry, 'fields'> & {
13
15
  drafts?: boolean;
14
16
  }, i18n: CmsI18n, messages?: Partial<ValidationMessages>): z.ZodObject<{
@@ -1,14 +1,22 @@
1
1
  import { z } from "zod";
2
- import { isFieldVisible, isTranslatableField } from "./index.js";
2
+ import { BLOCK_HIDDEN_KEY, declaresHiddenField, isFieldVisible, isTranslatableField } from "./index.js";
3
3
  export const objectKeySchema = z.string().min(1).max(1024).refine((k) => !k.includes("..") && !k.startsWith("/"), "Invalid object key");
4
4
  const DEFAULT_MESSAGES = {
5
5
  required: "Required field",
6
6
  invalidDate: "Invalid date (yyyy-mm-dd)",
7
+ invalidDateTime: "Invalid date and time (ISO 8601 with a time zone)",
7
8
  invalidEmail: "Invalid email",
8
9
  invalidSlug: "Invalid slug (lowercase letters, numbers, dashes)",
9
10
  unknownLocale: "Unknown locale key",
10
11
  requiredLocale: (locale) => `Required field (locale '${locale}')`
11
12
  };
13
+ const DATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
14
+ export function datetimeSchema(message) {
15
+ return z.string().trim().refine(
16
+ (value) => DATETIME_PATTERN.test(value) && !Number.isNaN(new Date(value).getTime()),
17
+ message
18
+ ).transform((value) => new Date(value).toISOString());
19
+ }
12
20
  function scalarSchema(field, m) {
13
21
  switch (field.type) {
14
22
  case "number":
@@ -19,6 +27,8 @@ function scalarSchema(field, m) {
19
27
  return z.string().min(1);
20
28
  case "date":
21
29
  return z.string().regex(/^\d{4}-\d{2}-\d{2}$/, m.invalidDate);
30
+ case "datetime":
31
+ return datetimeSchema(m.invalidDateTime);
22
32
  case "email":
23
33
  return z.email(m.invalidEmail);
24
34
  case "slug":
@@ -37,7 +47,9 @@ function optionalize(field, base, m) {
37
47
  }
38
48
  function translatableSchema(field, i18n, m) {
39
49
  const { locales, defaultLocale } = i18n;
40
- const record = z.record(z.string(), field.type === "media" ? objectKeySchema : z.string()).refine((v) => Object.keys(v).every((k) => locales.includes(k)), m.unknownLocale);
50
+ const record = z.record(z.string(), field.type === "media" ? objectKeySchema : z.string(), {
51
+ error: (issue) => issue.input == null ? m.required : void 0
52
+ }).refine((v) => Object.keys(v).every((k) => locales.includes(k)), m.unknownLocale);
41
53
  return field.required ? record.refine((v) => !!v[defaultLocale]?.trim(), m.requiredLocale(defaultLocale)) : record.nullish().transform((v) => v ?? null);
42
54
  }
43
55
  function blockFieldSchema(blockField, i18n, m) {
@@ -49,6 +61,7 @@ function blocksSchema(field, i18n, m) {
49
61
  const variants = Object.entries(field.blocks ?? {}).map(
50
62
  ([type, block]) => z.object({
51
63
  type: z.literal(type),
64
+ ...declaresHiddenField(block) ? {} : { [BLOCK_HIDDEN_KEY]: z.boolean().optional() },
52
65
  ...Object.fromEntries(
53
66
  Object.entries(block.fields).map(([key, blockField]) => [
54
67
  key,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Lightweight CMS that ships with your Nuxt app: runs on the Nitro server, content types defined in code, /cms admin panel, GraphQL API, SQLite or Postgres. No external CMS needed!",
5
5
  "license": "MIT",
6
6
  "author": "Edoardo Alberti (https://github.com/xleddyl)",