includio-cms 0.0.67 → 0.0.69

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 (41) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/ROADMAP.md +73 -0
  3. package/dist/admin/client/entry/entry-form.svelte +1 -1
  4. package/dist/admin/client/entry/entry-form.svelte.d.ts +1 -1
  5. package/dist/admin/client/entry/entry.svelte +17 -6
  6. package/dist/admin/client/entry/hybrid/hybrid-context.svelte.d.ts +1 -0
  7. package/dist/admin/components/fields/array-field.svelte +126 -71
  8. package/dist/admin/components/fields/relation-field.svelte +6 -10
  9. package/dist/admin/components/layout/nav-search.svelte +43 -31
  10. package/dist/admin/remote/media.remote.js +3 -3
  11. package/dist/admin/utils/arrayMove.d.ts +5 -0
  12. package/dist/admin/utils/arrayMove.js +12 -0
  13. package/dist/cms/runtime/types.d.ts +8 -0
  14. package/dist/core/server/generator/fields.js +9 -3
  15. package/dist/core/server/generator/generator.js +4 -7
  16. package/dist/core/server/generator/utils.d.ts +1 -0
  17. package/dist/core/server/generator/utils.js +6 -0
  18. package/dist/core/server/media/styles/operations/generateDefaultStyles.d.ts +1 -0
  19. package/dist/core/server/media/styles/operations/generateDefaultStyles.js +17 -16
  20. package/dist/core/server/media/styles/sharp/generateImageStyle.js +15 -5
  21. package/dist/core/server/utils/sanitizeRichText.d.ts +1 -0
  22. package/dist/core/server/utils/sanitizeRichText.js +67 -0
  23. package/dist/sveltekit/components/image.svelte +11 -2
  24. package/dist/sveltekit/components/preview.svelte +22 -14
  25. package/dist/sveltekit/components/preview.svelte.d.ts +38 -8
  26. package/dist/sveltekit/index.d.ts +1 -0
  27. package/dist/sveltekit/index.js +1 -0
  28. package/dist/sveltekit/utils/getLink.d.ts +7 -0
  29. package/dist/sveltekit/utils/getLink.js +32 -0
  30. package/dist/sveltekit/utils/index.d.ts +2 -0
  31. package/dist/sveltekit/utils/index.js +2 -0
  32. package/dist/sveltekit/utils/media.d.ts +3 -0
  33. package/dist/sveltekit/utils/media.js +6 -0
  34. package/dist/types/index.d.ts +8 -1
  35. package/dist/types/index.js +7 -0
  36. package/dist/updates/0.0.68/index.d.ts +2 -0
  37. package/dist/updates/0.0.68/index.js +21 -0
  38. package/dist/updates/0.0.69/index.d.ts +2 -0
  39. package/dist/updates/0.0.69/index.js +12 -0
  40. package/dist/updates/index.js +3 -1
  41. package/package.json +7 -2
@@ -28,34 +28,49 @@
28
28
  open = false;
29
29
  goto(url);
30
30
  }
31
+
32
+ async function getData() {
33
+ const [singles, collections, forms] = await Promise.all([
34
+ remotes.getSingles(),
35
+ remotes.getCollections(),
36
+ remotes.getForms()
37
+ ]);
38
+
39
+ return { singles, collections, forms };
40
+ }
31
41
  </script>
32
42
 
33
43
  <svelte:window {onkeydown} />
34
44
 
35
- <button
36
- onclick={() => (open = true)}
37
- class="text-muted-foreground mx-2 flex w-[calc(100%-1rem)] items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm transition-colors hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/10"
38
- >
39
- <SearchIcon class="size-4 shrink-0" />
40
- <span class="flex-1 truncate text-left">{sidebarLang[interfaceLanguage.current].search.placeholder}</span>
41
- <kbd class="bg-muted text-muted-foreground shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium">⌘K</kbd>
42
- </button>
45
+ {#await getData() then { singles, collections, forms }}
46
+ <button
47
+ onclick={() => (open = true)}
48
+ class="text-muted-foreground mx-2 flex w-[calc(100%-1rem)] items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm transition-colors hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/10"
49
+ >
50
+ <SearchIcon class="size-4 shrink-0" />
51
+ <span class="flex-1 truncate text-left"
52
+ >{sidebarLang[interfaceLanguage.current].search.placeholder}</span
53
+ >
54
+ <kbd
55
+ class="bg-muted text-muted-foreground shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium"
56
+ >⌘K</kbd
57
+ >
58
+ </button>
43
59
 
44
- <Command.Dialog bind:open title={sidebarLang[interfaceLanguage.current].search.placeholder}>
45
- <Command.Input placeholder={sidebarLang[interfaceLanguage.current].search.placeholder} />
46
- <Command.List>
47
- <Command.Empty>{sidebarLang[interfaceLanguage.current].search.noResults}</Command.Empty>
60
+ <Command.Dialog bind:open title={sidebarLang[interfaceLanguage.current].search.placeholder}>
61
+ <Command.Input placeholder={sidebarLang[interfaceLanguage.current].search.placeholder} />
62
+ <Command.List>
63
+ <Command.Empty>{sidebarLang[interfaceLanguage.current].search.noResults}</Command.Empty>
48
64
 
49
- <Command.Group heading={sidebarLang[interfaceLanguage.current].main.platform}>
50
- <Command.Item onSelect={() => navigate('/admin')}>
51
- <DashboardIcon class="mr-2 size-4" />
52
- {sidebarLang[interfaceLanguage.current].main.dashboard}
53
- </Command.Item>
54
- <Command.Item onSelect={() => navigate('/admin/media')}>
55
- <CameraIcon class="mr-2 size-4" />
56
- {sidebarLang[interfaceLanguage.current].main.media}
57
- </Command.Item>
58
- {#await remotes.getSingles() then singles}
65
+ <Command.Group heading={sidebarLang[interfaceLanguage.current].main.platform}>
66
+ <Command.Item onSelect={() => navigate('/admin')}>
67
+ <DashboardIcon class="mr-2 size-4" />
68
+ {sidebarLang[interfaceLanguage.current].main.dashboard}
69
+ </Command.Item>
70
+ <Command.Item onSelect={() => navigate('/admin/media')}>
71
+ <CameraIcon class="mr-2 size-4" />
72
+ {sidebarLang[interfaceLanguage.current].main.media}
73
+ </Command.Item>
59
74
  {#each singles as item (item.slug)}
60
75
  {@const name = getLocalizedLabel(item.label, interfaceLanguage.current) ?? item.slug}
61
76
  <Command.Item onSelect={() => navigate(`/admin/entries/${item.slug}`)}>
@@ -67,14 +82,13 @@
67
82
  {name}
68
83
  </Command.Item>
69
84
  {/each}
70
- {/await}
71
- </Command.Group>
85
+ </Command.Group>
72
86
 
73
- {#await remotes.getCollections() then collections}
74
87
  {#if collections.length > 0}
75
88
  <Command.Group heading={sidebarLang[interfaceLanguage.current].collections.title}>
76
89
  {#each collections as item (item.slug)}
77
- {@const name = getLocalizedLabel(item.labels?.plural, interfaceLanguage.current) ?? item.slug}
90
+ {@const name =
91
+ getLocalizedLabel(item.labels?.plural, interfaceLanguage.current) ?? item.slug}
78
92
  <Command.Item onSelect={() => navigate(`/admin/collections/${item.slug}`)}>
79
93
  {#if item.sidebarIcon}
80
94
  <item.sidebarIcon class="mr-2 size-4" />
@@ -86,9 +100,7 @@
86
100
  {/each}
87
101
  </Command.Group>
88
102
  {/if}
89
- {/await}
90
103
 
91
- {#await remotes.getForms() then forms}
92
104
  {#if forms.length > 0}
93
105
  <Command.Group heading={sidebarLang[interfaceLanguage.current].forms.title}>
94
106
  {#each forms as item (item.slug)}
@@ -100,6 +112,6 @@
100
112
  {/each}
101
113
  </Command.Group>
102
114
  {/if}
103
- {/await}
104
- </Command.List>
105
- </Command.Dialog>
115
+ </Command.List>
116
+ </Command.Dialog>
117
+ {/await}
@@ -100,11 +100,11 @@ export const setFocalPoint = command(z.object({
100
100
  await cms.filesAdapter.deleteFile(fileName).catch((e) => console.warn('Style file cleanup failed:', e));
101
101
  }
102
102
  }
103
- // Re-generate styles in background with new focal point
103
+ // Re-generate styles with new focal point (awaited so client gets fresh URLs)
104
104
  const file = await cms.databaseAdapter.getMediaFile({ data: { id: fileId } });
105
105
  if (file) {
106
- const { generateDefaultStylesInBackground } = await import('../../core/server/media/styles/operations/generateDefaultStyles.js');
107
- generateDefaultStylesInBackground(file);
106
+ const { generateDefaultStyles } = await import('../../core/server/media/styles/operations/generateDefaultStyles.js');
107
+ await generateDefaultStyles(file);
108
108
  }
109
109
  });
110
110
  export const renameMediaFile = command(z.object({
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Move an element from one index to another via splice, returning a new array.
3
+ * Returns the original array reference (unchanged) when `from === to`.
4
+ */
5
+ export declare function arrayMove<T>(arr: T[], from: number, to: number): T[];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Move an element from one index to another via splice, returning a new array.
3
+ * Returns the original array reference (unchanged) when `from === to`.
4
+ */
5
+ export function arrayMove(arr, from, to) {
6
+ if (from === to)
7
+ return arr;
8
+ const copy = [...arr];
9
+ const [moved] = copy.splice(from, 1);
10
+ copy.splice(to, 0, moved);
11
+ return copy;
12
+ }
@@ -1,6 +1,8 @@
1
1
  import type { ImageFieldData } from 'includio-cms/types';
2
2
  export type SingleSlug = "settings" | "image-showcase";
3
3
  export interface Settings {
4
+ id: string;
5
+ slug: string;
4
6
  data: {
5
7
  siteName: string;
6
8
  description?: string;
@@ -17,6 +19,8 @@ export interface Settings {
17
19
  publishedAt: Date | null;
18
20
  }
19
21
  export interface ImageShowcase {
22
+ id: string;
23
+ slug: string;
20
24
  data: {
21
25
  photo?: ImageFieldData | null;
22
26
  };
@@ -27,6 +31,8 @@ export type SingleEntryMap = {
27
31
  };
28
32
  export type CollectionSlug = "blog-post" | "project";
29
33
  export interface BlogPost {
34
+ id: string;
35
+ slug: string;
30
36
  data: {
31
37
  title: string;
32
38
  slug?: string;
@@ -48,6 +54,8 @@ export interface BlogPost {
48
54
  publishedAt: Date | null;
49
55
  }
50
56
  export interface Project {
57
+ id: string;
58
+ slug: string;
51
59
  data: {
52
60
  title: string;
53
61
  description?: string;
@@ -1,3 +1,4 @@
1
+ import { toPascalCase } from './utils.js';
1
2
  function getFieldTypeAsString(field) {
2
3
  switch (field.type) {
3
4
  case 'text':
@@ -11,14 +12,19 @@ function getFieldTypeAsString(field) {
11
12
  const base = field.multiple ? 'ImageFieldData[]' : 'ImageFieldData';
12
13
  return base + (field.required ? '' : ' | null');
13
14
  }
15
+ case 'media': {
16
+ const base = field.multiple
17
+ ? '(ImageFieldData | VideoFieldData)[]'
18
+ : 'ImageFieldData | VideoFieldData';
19
+ return base + (field.required ? '' : ' | null');
20
+ }
14
21
  case 'file': {
15
22
  const base = field.multiple ? 'MediaFile[]' : 'MediaFile';
16
23
  return base + (field.required ? '' : ' | null');
17
24
  }
18
25
  case 'relation': {
19
- return field.multiple
20
- ? `${field.collection.charAt(0).toUpperCase() + field.collection.slice(1)}[]`
21
- : field.collection.charAt(0).toUpperCase() + field.collection.slice(1);
26
+ const name = toPascalCase(field.collection);
27
+ return field.multiple ? `${name}[]` : name;
22
28
  }
23
29
  case 'select':
24
30
  return field.multiple ? 'string[]' : 'string';
@@ -3,12 +3,7 @@ import { join } from 'node:path';
3
3
  import { generateTsTypeFromFields } from './fields.js';
4
4
  import { generateTsTypeFromFormFields } from './formFields.js';
5
5
  import { generateZodSchemaStringFromFormFieldsAsString } from './formFieldSchemaToString.js';
6
- function toPascalCase(slug) {
7
- return slug
8
- .split('-')
9
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
10
- .join('');
11
- }
6
+ import { toPascalCase } from './utils.js';
12
7
  function createCmsRuntimeDir() {
13
8
  const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
14
9
  mkdirSync(cmsDir, { recursive: true });
@@ -22,6 +17,8 @@ function generateTypesStringForRecords(type, records) {
22
17
  .map((single) => {
23
18
  return `
24
19
  export interface ${toPascalCase(single.slug)} {
20
+ id: string;
21
+ slug: string;
25
22
  data: ${generateTsTypeFromFields(single.fields)}
26
23
  publishedAt: Date | null;
27
24
  };
@@ -71,7 +68,7 @@ function generateTypes(config) {
71
68
  const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
72
69
  const filePath = join(cmsDir, 'types.ts');
73
70
  let code = `// This file is auto-generated. Do not edit directly.\n\n`;
74
- code += `import type { MediaFile, ImageFieldData } from 'includio-cms/types';\n\n`;
71
+ code += `import type { MediaFile, ImageFieldData, VideoFieldData } from 'includio-cms/types';\n\n`;
75
72
  if (config.singles && config.singles.length > 0) {
76
73
  code += generateTypesStringForRecords('single', Object.values(config.singles));
77
74
  }
@@ -0,0 +1 @@
1
+ export declare function toPascalCase(slug: string): string;
@@ -0,0 +1,6 @@
1
+ export function toPascalCase(slug) {
2
+ return slug
3
+ .split('-')
4
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
5
+ .join('');
6
+ }
@@ -1,2 +1,3 @@
1
1
  import type { MediaFile } from '../../../../../types/media.js';
2
+ export declare function generateDefaultStyles(mediaFile: MediaFile): Promise<void>;
2
3
  export declare function generateDefaultStylesInBackground(mediaFile: MediaFile): void;
@@ -1,25 +1,26 @@
1
1
  import { defaultStyles, isProcessableImage, expandStyleFormats, getOriginalFormat } from '../../../fields/utils/imageStyles.js';
2
2
  import { getImageStyle } from './getImageStyle.js';
3
- export function generateDefaultStylesInBackground(mediaFile) {
3
+ export async function generateDefaultStyles(mediaFile) {
4
4
  if (!isProcessableImage(mediaFile))
5
5
  return;
6
6
  const origFormat = getOriginalFormat(mediaFile);
7
7
  const expanded = expandStyleFormats(defaultStyles, origFormat);
8
- (async () => {
9
- for (const style of expanded) {
10
- await getImageStyle(mediaFile.id, style);
11
- if (style.srcset && mediaFile.width) {
12
- const widths = style.srcset.filter((w) => w <= mediaFile.width);
13
- for (const w of widths) {
14
- await getImageStyle(mediaFile.id, {
15
- ...style,
16
- name: `${style.name}_${w}w`,
17
- width: w,
18
- srcset: undefined,
19
- sizes: undefined
20
- });
21
- }
8
+ for (const style of expanded) {
9
+ await getImageStyle(mediaFile.id, style);
10
+ if (style.srcset && mediaFile.width) {
11
+ const widths = style.srcset.filter((w) => w <= mediaFile.width);
12
+ for (const w of widths) {
13
+ await getImageStyle(mediaFile.id, {
14
+ ...style,
15
+ name: `${style.name}_${w}w`,
16
+ width: w,
17
+ srcset: undefined,
18
+ sizes: undefined
19
+ });
22
20
  }
23
21
  }
24
- })().catch((e) => console.warn('Background style generation failed:', e));
22
+ }
23
+ }
24
+ export function generateDefaultStylesInBackground(mediaFile) {
25
+ generateDefaultStyles(mediaFile).catch((e) => console.warn('Background style generation failed:', e));
25
26
  }
@@ -15,8 +15,18 @@ export async function generateImageStyle(mediaFileId, style) {
15
15
  throw new Error('Media file not found');
16
16
  }
17
17
  const imageBuffer = await file.arrayBuffer();
18
- let sharpInstance = sharp(Buffer.from(imageBuffer));
19
- const width = style.width ?? mediaFile.width ?? undefined;
18
+ const buf = Buffer.from(imageBuffer);
19
+ // Read EXIF orientation before processing
20
+ const metadata = await sharp(buf).metadata();
21
+ // .rotate() applies EXIF orientation to pixels AND strips the tag from output.
22
+ // Prevents double-rotation in WebP/JPEG where EXIF orientation tag may persist.
23
+ let sharpInstance = sharp(buf).rotate();
24
+ // DB stores raw (pre-EXIF) dimensions. After .rotate(), pixels are oriented,
25
+ // so we need oriented dimensions for crop calculation.
26
+ const needsSwap = metadata.orientation != null && metadata.orientation >= 5;
27
+ const imgWidth = needsSwap ? metadata.height : metadata.width;
28
+ const imgHeight = needsSwap ? metadata.width : metadata.height;
29
+ const width = style.width ?? imgWidth ?? mediaFile.width ?? undefined;
20
30
  const height = style.height ?? undefined;
21
31
  if (
22
32
  // Focal point crop
@@ -25,9 +35,9 @@ export async function generateImageStyle(mediaFileId, style) {
25
35
  height &&
26
36
  mediaFile.focalX != null &&
27
37
  mediaFile.focalY != null &&
28
- mediaFile.width &&
29
- mediaFile.height) {
30
- const region = calculateFocalCropRegion(mediaFile.width, mediaFile.height, mediaFile.focalX, mediaFile.focalY, width, height);
38
+ imgWidth &&
39
+ imgHeight) {
40
+ const region = calculateFocalCropRegion(imgWidth, imgHeight, mediaFile.focalX, mediaFile.focalY, width, height);
31
41
  sharpInstance = sharpInstance
32
42
  .extract(region)
33
43
  .resize(width, height);
@@ -0,0 +1 @@
1
+ export declare function sanitizeRichText(dirty: string): string;
@@ -0,0 +1,67 @@
1
+ import DOMPurify from 'isomorphic-dompurify';
2
+ DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
3
+ const tag = node.tagName.toLowerCase();
4
+ if (data.attrName === 'src' && !['img', 'video'].includes(tag)) {
5
+ data.keepAttr = false;
6
+ }
7
+ if (data.attrName === 'style') {
8
+ const value = node.getAttribute('style') || '';
9
+ const match = value.match(/text-align\s*:\s*(left|center|right|justify)/);
10
+ node.setAttribute('style', match ? `text-align: ${match[1]}` : '');
11
+ }
12
+ });
13
+ export function sanitizeRichText(dirty) {
14
+ return DOMPurify.sanitize(dirty, {
15
+ ALLOWED_TAGS: [
16
+ 'p',
17
+ 'h1',
18
+ 'h2',
19
+ 'h3',
20
+ 'h4',
21
+ 'h5',
22
+ 'h6',
23
+ 'strong',
24
+ 'em',
25
+ 's',
26
+ 'code',
27
+ 'ul',
28
+ 'ol',
29
+ 'li',
30
+ 'blockquote',
31
+ 'br',
32
+ 'hr',
33
+ 'a',
34
+ 'img',
35
+ 'table',
36
+ 'thead',
37
+ 'tbody',
38
+ 'tr',
39
+ 'th',
40
+ 'td',
41
+ 'mark',
42
+ 'u',
43
+ 'pre',
44
+ 'video',
45
+ 'figure',
46
+ 'figcaption'
47
+ ],
48
+ ALLOWED_ATTR: [
49
+ 'href',
50
+ 'target',
51
+ 'rel',
52
+ 'src',
53
+ 'alt',
54
+ 'title',
55
+ 'class',
56
+ 'colspan',
57
+ 'rowspan',
58
+ 'colwidth',
59
+ 'style',
60
+ 'poster',
61
+ 'data-media-id',
62
+ 'controls',
63
+ 'width',
64
+ 'height'
65
+ ]
66
+ });
67
+ }
@@ -24,6 +24,14 @@
24
24
  const blurUrl = $derived(
25
25
  data && isProperImageObject(data) ? data.blurDataUrl : null
26
26
  );
27
+
28
+ let loaded = $state(false);
29
+
30
+ // Reset loaded when image source changes
31
+ $effect(() => {
32
+ if (data && isProperImageObject(data)) data.data?.url;
33
+ loaded = false;
34
+ });
27
35
  </script>
28
36
 
29
37
  <style>
@@ -52,8 +60,9 @@
52
60
  alt={data.data.alt}
53
61
  width={data.data.width}
54
62
  height={data.data.height}
55
- class="{className} {blurUrl ? 'includio-blur-placeholder' : ''}"
56
- style={blurUrl ? `background-image:url(${blurUrl})` : undefined}
63
+ class="{className} {blurUrl && !loaded ? 'includio-blur-placeholder' : ''}"
64
+ style={blurUrl && !loaded ? `background-image:url(${blurUrl})` : undefined}
65
+ onload={() => (loaded = true)}
57
66
  {loading}
58
67
  {...restProps}
59
68
  />
@@ -1,17 +1,18 @@
1
- <script lang="ts">
2
- import type { Entry, PopulatedEntryData } from '../../types/entries.js';
1
+ <script lang="ts" generics="T extends { data: PopulatedEntryData }">
2
+ import type { PopulatedEntryData } from '../../types/entries.js';
3
3
  import { onMount, type Snippet } from 'svelte';
4
4
 
5
5
  type Props = {
6
- entry: Entry;
7
- child?: Snippet<[{ entry: Entry }]>;
6
+ entry: T;
7
+ child?: Snippet<[{ entry: T }]>;
8
8
  };
9
9
 
10
10
  let { entry, child }: Props = $props();
11
11
 
12
- let previewData: Entry = $state(entry);
12
+ let previewData: T = $state(entry);
13
13
  let hybridModeEnabled = $state(false);
14
14
  let highlightedPath = $state<string | null>(null);
15
+ let parentOrigin: string | null = null;
15
16
 
16
17
  /**
17
18
  * Checks if a value looks like a UUID reference
@@ -53,18 +54,18 @@
53
54
  }
54
55
 
55
56
  /**
56
- * Checks if a value is a simple resolved reference (has id and url at top level)
57
- * e.g., { id: "...", url: "...", width: 800, ... }
57
+ * Checks if a value is a simple resolved reference (has id UUID at top level)
58
+ * e.g., { id: "...", slug: "...", type: "collection" } or { id: "...", url: "...", width: 800 }
58
59
  */
59
60
  function isResolvedReference(
60
61
  value: unknown
61
- ): value is { id: string; url: string; [key: string]: unknown } {
62
+ ): value is { id: string; [key: string]: unknown } {
62
63
  return (
63
64
  typeof value === 'object' &&
64
65
  value !== null &&
65
66
  'id' in value &&
66
- 'url' in value &&
67
- typeof (value as Record<string, unknown>).id === 'string'
67
+ typeof (value as Record<string, unknown>).id === 'string' &&
68
+ isUUID((value as Record<string, unknown>).id)
68
69
  );
69
70
  }
70
71
 
@@ -232,12 +233,19 @@
232
233
 
233
234
  onMount(() => {
234
235
  function handleMessage(e: MessageEvent) {
236
+ // Accept first message from any origin (handshake), then lock to that origin
237
+ if (!parentOrigin) {
238
+ parentOrigin = e.origin;
239
+ } else if (e.origin !== parentOrigin) {
240
+ return;
241
+ }
242
+
235
243
  if (e.data.type === 'preview-update') {
236
244
  // Merge form data with current preview data, preserving resolved references
237
245
  previewData = {
238
246
  ...previewData,
239
- data: deepMergeWithReferences(previewData.data, e.data.data)
240
- };
247
+ data: deepMergeWithReferences(previewData.data as PopulatedEntryData, e.data.data)
248
+ } as T;
241
249
  }
242
250
 
243
251
  if (e.data.type === 'hybrid-mode-enable') {
@@ -257,7 +265,7 @@
257
265
  document.addEventListener('click', handleLinkClick, true);
258
266
 
259
267
  // Signal to parent that preview is ready to receive data
260
- window.parent.postMessage({ type: 'preview-ready' }, '*');
268
+ window.parent.postMessage({ type: 'preview-ready' }, parentOrigin || '*');
261
269
 
262
270
  return () => {
263
271
  window.removeEventListener('message', handleMessage);
@@ -319,7 +327,7 @@
319
327
  if (path) {
320
328
  // Normalize to dot notation (hero[0].data → hero.0.data)
321
329
  const normalized = path.replace(/\[(\d+)\]/g, '.$1');
322
- window.parent.postMessage({ type: 'hybrid-focus', path: normalized }, '*');
330
+ window.parent.postMessage({ type: 'hybrid-focus', path: normalized }, parentOrigin || '*');
323
331
  }
324
332
  }
325
333
 
@@ -1,11 +1,41 @@
1
- import type { Entry } from '../../types/entries.js';
1
+ import type { PopulatedEntryData } from '../../types/entries.js';
2
2
  import { type Snippet } from 'svelte';
3
- type Props = {
4
- entry: Entry;
5
- child?: Snippet<[{
6
- entry: Entry;
7
- }]>;
3
+ declare function $$render<T extends {
4
+ data: PopulatedEntryData;
5
+ }>(): {
6
+ props: {
7
+ entry: T;
8
+ child?: Snippet<[{
9
+ entry: T;
10
+ }]>;
11
+ };
12
+ exports: {};
13
+ bindings: "";
14
+ slots: {};
15
+ events: {};
8
16
  };
9
- declare const Preview: import("svelte").Component<Props, {}, "">;
10
- type Preview = ReturnType<typeof Preview>;
17
+ declare class __sveltets_Render<T extends {
18
+ data: PopulatedEntryData;
19
+ }> {
20
+ props(): ReturnType<typeof $$render<T>>['props'];
21
+ events(): ReturnType<typeof $$render<T>>['events'];
22
+ slots(): ReturnType<typeof $$render<T>>['slots'];
23
+ bindings(): "";
24
+ exports(): {};
25
+ }
26
+ interface $$IsomorphicComponent {
27
+ new <T extends {
28
+ data: PopulatedEntryData;
29
+ }>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
30
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
31
+ } & ReturnType<__sveltets_Render<T>['exports']>;
32
+ <T extends {
33
+ data: PopulatedEntryData;
34
+ }>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
35
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
36
+ }
37
+ declare const Preview: $$IsomorphicComponent;
38
+ type Preview<T extends {
39
+ data: PopulatedEntryData;
40
+ }> = InstanceType<typeof Preview<T>>;
11
41
  export default Preview;
@@ -5,3 +5,4 @@ export { default as HybridTarget } from './components/hybrid-target.svelte';
5
5
  export { default as Image } from './components/image.svelte';
6
6
  export { default as Video } from './components/video.svelte';
7
7
  export { default as Media } from './components/media.svelte';
8
+ export { getLink, isImageFieldData, isVideoFieldData } from './utils/index.js';
@@ -5,3 +5,4 @@ export { default as HybridTarget } from './components/hybrid-target.svelte';
5
5
  export { default as Image } from './components/image.svelte';
6
6
  export { default as Video } from './components/video.svelte';
7
7
  export { default as Media } from './components/media.svelte';
8
+ export { getLink, isImageFieldData, isVideoFieldData } from './utils/index.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Resolves a link value to a URL string.
3
+ * Handles: string, { url: string }, { url: Record<string, string> } (UrlFieldData), null/undefined.
4
+ */
5
+ export declare function getLink(link: string | {
6
+ url: string | Record<string, string>;
7
+ } | null | undefined, language?: string): string;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Resolves a link value to a URL string.
3
+ * Handles: string, { url: string }, { url: Record<string, string> } (UrlFieldData), null/undefined.
4
+ */
5
+ export function getLink(link, language) {
6
+ if (!link)
7
+ return '';
8
+ if (typeof link === 'object' && 'url' in link) {
9
+ const url = link.url;
10
+ if (typeof url === 'object') {
11
+ // UrlFieldData — localized Record<string, string>
12
+ if (language && url[language])
13
+ return normalizeLink(url[language]);
14
+ // Fallback to first available locale
15
+ const first = Object.values(url)[0];
16
+ return first ? normalizeLink(first) : '';
17
+ }
18
+ return normalizeLink(url);
19
+ }
20
+ if (typeof link !== 'string')
21
+ return '';
22
+ return normalizeLink(link);
23
+ }
24
+ function normalizeLink(link) {
25
+ if (link.startsWith('#') ||
26
+ link.startsWith('http://') ||
27
+ link.startsWith('https://') ||
28
+ link.startsWith('/')) {
29
+ return link;
30
+ }
31
+ return `/${link}`;
32
+ }
@@ -0,0 +1,2 @@
1
+ export { getLink } from './getLink.js';
2
+ export { isImageFieldData, isVideoFieldData } from './media.js';
@@ -0,0 +1,2 @@
1
+ export { getLink } from './getLink.js';
2
+ export { isImageFieldData, isVideoFieldData } from './media.js';
@@ -0,0 +1,3 @@
1
+ import type { ImageFieldData, VideoFieldData, MediaFieldData } from '../../types/fields.js';
2
+ export declare function isImageFieldData(media: MediaFieldData): media is ImageFieldData;
3
+ export declare function isVideoFieldData(media: MediaFieldData): media is VideoFieldData;
@@ -0,0 +1,6 @@
1
+ export function isImageFieldData(media) {
2
+ return 'styles' in media;
3
+ }
4
+ export function isVideoFieldData(media) {
5
+ return !('styles' in media);
6
+ }