@xleddyl/nuxt-cms 0.2.2 → 0.3.0

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 (92) hide show
  1. package/README.md +4 -2
  2. package/dist/module.d.mts +4 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +329 -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.d.vue.ts +2 -0
  20. package/dist/runtime/app/components/cms/EntryForm.vue +43 -7
  21. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +2 -0
  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/MediaDetail.vue +25 -13
  26. package/dist/runtime/app/components/cms/Search.d.vue.ts +7 -0
  27. package/dist/runtime/app/components/cms/Search.vue +347 -0
  28. package/dist/runtime/app/components/cms/Search.vue.d.ts +7 -0
  29. package/dist/runtime/app/components/cms/SettingsEntry.vue +3 -2
  30. package/dist/runtime/app/components/cms/SettingsModal.vue +2 -1
  31. package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +2 -0
  32. package/dist/runtime/app/components/cms/StatusBadge.vue +9 -3
  33. package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +2 -0
  34. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +39 -0
  35. package/dist/runtime/app/components/cms/VisualEditor.vue +553 -0
  36. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +39 -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-field-focus.d.ts +4 -0
  42. package/dist/runtime/app/composables/cms-field-focus.js +20 -0
  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.d.vue.ts +2 -2
  46. package/dist/runtime/app/layouts/cms-admin.vue +7 -0
  47. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  48. package/dist/runtime/app/layouts/cms-editor.d.vue.ts +13 -0
  49. package/dist/runtime/app/layouts/cms-editor.vue +14 -0
  50. package/dist/runtime/app/layouts/cms-editor.vue.d.ts +13 -0
  51. package/dist/runtime/app/pages/admin-collection.vue +36 -4
  52. package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
  53. package/dist/runtime/app/pages/admin-content.vue +287 -0
  54. package/dist/runtime/app/pages/admin-content.vue.d.ts +3 -0
  55. package/dist/runtime/app/pages/admin-entry.vue +3 -0
  56. package/dist/runtime/app/pages/admin-preview.d.vue.ts +3 -0
  57. package/dist/runtime/app/pages/admin-preview.vue +132 -0
  58. package/dist/runtime/app/pages/admin-preview.vue.d.ts +3 -0
  59. package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +15 -0
  60. package/dist/runtime/app/preview/PreviewOverlay.vue +362 -0
  61. package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +15 -0
  62. package/dist/runtime/app/utils/ui.js +1 -0
  63. package/dist/runtime/assets/main.css +1 -1
  64. package/dist/runtime/server/api/collection.get.js +4 -5
  65. package/dist/runtime/server/api/collection.post.js +4 -1
  66. package/dist/runtime/server/api/item.delete.js +2 -1
  67. package/dist/runtime/server/api/item.put.js +2 -1
  68. package/dist/runtime/server/api/media-download.get.d.ts +2 -0
  69. package/dist/runtime/server/api/media-download.get.js +90 -0
  70. package/dist/runtime/server/api/search.get.d.ts +3 -0
  71. package/dist/runtime/server/api/search.get.js +24 -0
  72. package/dist/runtime/server/utils/graphql.js +23 -8
  73. package/dist/runtime/server/utils/media-download.d.ts +1 -0
  74. package/dist/runtime/server/utils/media-download.js +8 -0
  75. package/dist/runtime/server/utils/media.d.ts +1 -0
  76. package/dist/runtime/server/utils/media.js +18 -3
  77. package/dist/runtime/server/utils/registry.d.ts +1 -0
  78. package/dist/runtime/server/utils/registry.js +6 -0
  79. package/dist/runtime/server/utils/search.d.ts +20 -0
  80. package/dist/runtime/server/utils/search.js +233 -0
  81. package/dist/runtime/server/utils/visibility.d.ts +4 -0
  82. package/dist/runtime/server/utils/visibility.js +12 -0
  83. package/dist/runtime/shared/graphql-sdl.js +4 -1
  84. package/dist/runtime/shared/index.d.ts +59 -5
  85. package/dist/runtime/shared/index.js +133 -0
  86. package/dist/runtime/shared/preview.d.ts +85 -0
  87. package/dist/runtime/shared/preview.js +219 -0
  88. package/dist/runtime/shared/search.d.ts +52 -0
  89. package/dist/runtime/shared/search.js +207 -0
  90. package/dist/runtime/shared/validation.d.ts +2 -0
  91. package/dist/runtime/shared/validation.js +15 -2
  92. package/package.json +2 -2
@@ -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
+ }
@@ -0,0 +1,52 @@
1
+ import type { CmsConfig, CmsEntryKind } from './index.js';
2
+ export declare const CMS_SEARCH_MIN_LENGTH = 2;
3
+ export declare const CMS_SEARCH_MAX_LENGTH = 200;
4
+ export declare const CMS_SEARCH_LABEL_LIMIT = 20;
5
+ export declare const CMS_SEARCH_VALUE_LIMIT = 50;
6
+ export declare const CMS_SEARCH_ROWS_PER_ENTRY = 10;
7
+ export declare const CMS_SEARCH_HITS_PER_ROW = 5;
8
+ export declare const CMS_SEARCH_PATH_SEPARATOR = " \u203A ";
9
+ export type CmsSearchHitType = 'entry' | 'page' | 'tab' | 'field' | 'block' | 'value' | 'media';
10
+ export type CmsSearchKind = CmsEntryKind | 'media';
11
+ export interface CmsSearchSnippet {
12
+ before: string;
13
+ match: string;
14
+ after: string;
15
+ }
16
+ export interface CmsSearchHit {
17
+ type: CmsSearchHitType;
18
+ collection: string | null;
19
+ entryLabel: string;
20
+ kind: CmsSearchKind;
21
+ id: string | null;
22
+ title: string | null;
23
+ field: string | null;
24
+ fieldLabel: string | null;
25
+ locale: string | null;
26
+ snippet: CmsSearchSnippet;
27
+ to: string;
28
+ }
29
+ export interface CmsSearchResponse {
30
+ query: string;
31
+ hits: CmsSearchHit[];
32
+ truncated: boolean;
33
+ }
34
+ export interface CmsSearchFocus {
35
+ field?: string;
36
+ locale?: string | null;
37
+ block?: number | null;
38
+ tab?: string;
39
+ }
40
+ export declare function normalizeSearchQuery(value: string): string;
41
+ export declare function isSearchableQuery(value: string): boolean;
42
+ export declare function searchLikePattern(term: string): string;
43
+ export declare function decodeHtmlEntities(value: string): string;
44
+ export declare function stripMarkup(html: string): string;
45
+ export declare function searchSnippet(text: string, term: string): CmsSearchSnippet | null;
46
+ export declare function searchFieldPath(...labels: string[]): string;
47
+ export declare function cmsSearchLink(path: string, focus?: CmsSearchFocus): string;
48
+ export declare function readSearchFocus(query: Record<string, unknown>): CmsSearchFocus | null;
49
+ export declare function searchCmsLabels(config: CmsConfig, query: string, limit?: number): {
50
+ hits: CmsSearchHit[];
51
+ truncated: boolean;
52
+ };