includio-cms 0.5.2 → 0.5.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 (93) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/ROADMAP.md +13 -0
  3. package/dist/admin/client/entry/entry-form.svelte +1 -0
  4. package/dist/admin/client/entry/entry.svelte +130 -123
  5. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +92 -9
  6. package/dist/admin/components/fields/blocks-field.svelte +142 -112
  7. package/dist/admin/components/fields/blocks-field.svelte.d.ts +10 -30
  8. package/dist/admin/components/fields/boolean-field.svelte +28 -38
  9. package/dist/admin/components/fields/boolean-field.svelte.d.ts +5 -27
  10. package/dist/admin/components/fields/checkboxes-field.svelte +12 -24
  11. package/dist/admin/components/fields/checkboxes-field.svelte.d.ts +5 -27
  12. package/dist/admin/components/fields/content-field.svelte +4 -17
  13. package/dist/admin/components/fields/content-field.svelte.d.ts +5 -27
  14. package/dist/admin/components/fields/date-field.svelte +8 -21
  15. package/dist/admin/components/fields/date-field.svelte.d.ts +5 -27
  16. package/dist/admin/components/fields/datetime-field.svelte +8 -21
  17. package/dist/admin/components/fields/datetime-field.svelte.d.ts +5 -27
  18. package/dist/admin/components/fields/field-renderer.svelte +32 -19
  19. package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -1
  20. package/dist/admin/components/fields/field-value-bridge.svelte +21 -0
  21. package/dist/admin/components/fields/field-value-bridge.svelte.d.ts +31 -0
  22. package/dist/admin/components/fields/fields-form.svelte +13 -10
  23. package/dist/admin/components/fields/file-field.svelte +12 -27
  24. package/dist/admin/components/fields/file-field.svelte.d.ts +5 -27
  25. package/dist/admin/components/fields/image-field.svelte +13 -28
  26. package/dist/admin/components/fields/image-field.svelte.d.ts +5 -27
  27. package/dist/admin/components/fields/media-field.svelte +15 -30
  28. package/dist/admin/components/fields/media-field.svelte.d.ts +5 -27
  29. package/dist/admin/components/fields/number-field.svelte +6 -20
  30. package/dist/admin/components/fields/number-field.svelte.d.ts +5 -27
  31. package/dist/admin/components/fields/object-field.svelte +26 -29
  32. package/dist/admin/components/fields/object-field.svelte.d.ts +11 -31
  33. package/dist/admin/components/fields/radio-field.svelte +8 -20
  34. package/dist/admin/components/fields/radio-field.svelte.d.ts +5 -27
  35. package/dist/admin/components/fields/relation-field.svelte +15 -30
  36. package/dist/admin/components/fields/relation-field.svelte.d.ts +5 -27
  37. package/dist/admin/components/fields/richtext-field.svelte +4 -17
  38. package/dist/admin/components/fields/richtext-field.svelte.d.ts +5 -27
  39. package/dist/admin/components/fields/select-field.svelte +14 -28
  40. package/dist/admin/components/fields/select-field.svelte.d.ts +5 -27
  41. package/dist/admin/components/fields/seo-field.svelte +5 -12
  42. package/dist/admin/components/fields/seo-field.svelte.d.ts +8 -28
  43. package/dist/admin/components/fields/simple-array-field.svelte +29 -42
  44. package/dist/admin/components/fields/simple-array-field.svelte.d.ts +5 -27
  45. package/dist/admin/components/fields/slug-field.svelte +6 -11
  46. package/dist/admin/components/fields/slug-field.svelte.d.ts +6 -26
  47. package/dist/admin/components/fields/text-field-wrapper.svelte +22 -40
  48. package/dist/admin/components/fields/text-field.svelte +7 -19
  49. package/dist/admin/components/fields/text-field.svelte.d.ts +5 -27
  50. package/dist/admin/components/fields/url-field-wrapper.svelte +8 -3
  51. package/dist/admin/components/fields/url-field.svelte +294 -128
  52. package/dist/admin/components/fields/url-field.svelte.d.ts +5 -27
  53. package/dist/admin/components/layout/layout-renderer.svelte +8 -6
  54. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +221 -31
  55. package/dist/admin/components/tiptap/content-editor.svelte +13 -2
  56. package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
  57. package/dist/admin/components/tiptap/inline-block-node.js +18 -1
  58. package/dist/admin/components/tiptap/slash-command.js +2 -3
  59. package/dist/admin/components/tiptap/standalone-form.d.ts +7 -0
  60. package/dist/admin/components/tiptap/standalone-form.js +31 -0
  61. package/dist/admin/components/tiptap/tiptap-editor.svelte +7 -0
  62. package/dist/admin/remote/entry.remote.js +16 -0
  63. package/dist/admin/styles/admin.css +10 -0
  64. package/dist/admin/utils/fieldCondition.d.ts +6 -0
  65. package/dist/admin/utils/fieldCondition.js +20 -0
  66. package/dist/components/ui/switch/index.d.ts +2 -0
  67. package/dist/components/ui/switch/index.js +4 -0
  68. package/dist/components/ui/switch/switch.svelte +26 -0
  69. package/dist/components/ui/switch/switch.svelte.d.ts +4 -0
  70. package/dist/core/fields/fieldSchemaToTs.js +15 -3
  71. package/dist/core/fields/formFieldSchemaToTs.js +22 -6
  72. package/dist/core/fields/urlUtils.d.ts +14 -0
  73. package/dist/core/fields/urlUtils.js +21 -0
  74. package/dist/core/server/fields/populateEntry.js +43 -0
  75. package/dist/core/server/fields/resolveImageFields.js +33 -1
  76. package/dist/core/server/fields/resolveRelationFields.js +46 -0
  77. package/dist/core/server/fields/resolveRichtextLinks.js +15 -1
  78. package/dist/core/server/fields/resolveUrlFields.js +65 -0
  79. package/dist/core/server/generator/formFieldSchemaToString.js +40 -9
  80. package/dist/core/server/generator/formFields.js +2 -0
  81. package/dist/core/server/generator/generator.js +25 -1
  82. package/dist/schemas/field/url.d.ts +2 -0
  83. package/dist/schemas/field/url.js +4 -2
  84. package/dist/types/fields.d.ts +9 -0
  85. package/dist/types/formFields.d.ts +15 -2
  86. package/dist/types/index.d.ts +1 -0
  87. package/dist/types/index.js +1 -0
  88. package/dist/updates/0.5.3/index.d.ts +2 -0
  89. package/dist/updates/0.5.3/index.js +19 -0
  90. package/dist/updates/index.js +2 -1
  91. package/package.json +2 -1
  92. package/dist/admin/components/fields/standalone-field-renderer.svelte +0 -148
  93. package/dist/admin/components/fields/standalone-field-renderer.svelte.d.ts +0 -9
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to includio-cms are documented here.
4
4
  Generated from `src/lib/updates/` — do not edit manually.
5
5
 
6
+ ## 0.5.3 — 2026-03-04
7
+
8
+ Form fields, inline blocks, hybrid preview, field components refactor
9
+
10
+ ### Added
11
+ - Form fields: select type, minLength/maxLength, errorMessage, remote commands
12
+ - Conditional field visibility (showWhen)
13
+ - Hybrid preview: device frames, container queries
14
+ - TipTap placeholder extension + Notion-style styles
15
+ - Server-side field resolution for inline blocks
16
+ - Inline blocks: standalone form, full field rendering, collapse UI
17
+ - URL field: rel attribute, external auto-detect, UI redesign
18
+ - Switch UI component (bits-ui)
19
+ - Boolean field: Switch toggle zamiast checkbox
20
+ - Storybook stories for all field types
21
+
22
+ ### Fixed
23
+ - Hybrid preview layout fix
24
+
6
25
  ## 0.5.2 — 2026-02-25
7
26
 
8
27
  Update system: split migration into sql + notes
package/ROADMAP.md CHANGED
@@ -76,6 +76,19 @@
76
76
 
77
77
  - [x] `[fix]` `[P1]` Split `migration` field into `sql` + `notes` — fixes CLI crash on text descriptions
78
78
 
79
+ ## 0.5.3 — Form fields, inline blocks, hybrid preview
80
+
81
+ - [x] `[feature]` `[P1]` Form fields: select type, minLength/maxLength, errorMessage, remote commands
82
+ - [x] `[feature]` `[P1]` Conditional field visibility (showWhen)
83
+ - [x] `[feature]` `[P1]` Hybrid preview: device frames, container queries
84
+ - [x] `[feature]` `[P2]` TipTap placeholder extension + Notion-style styles
85
+ - [x] `[feature]` `[P1]` Server-side field resolution for inline blocks
86
+ - [x] `[feature]` `[P1]` Inline blocks: standalone form, field rendering, collapse UI
87
+ - [x] `[feature]` `[P2]` URL field: rel attribute, external auto-detect, UI redesign
88
+ - [x] `[feature]` `[P2]` Switch UI component + boolean field toggle
89
+ - [x] `[chore]` `[P2]` Storybook stories for all field types
90
+ - [x] `[chore]` `[P1]` Field components refactor: bindable value props
91
+
79
92
  ## 0.6.0 — Plugin system _(deferred from 0.2.0)_
80
93
 
81
94
  - [ ] `[feature]` `[P0]` Wire plugin hooks into CRUD operations (before/afterCreate, Update, Delete) <!-- files: src/lib/types/plugins.ts, src/lib/core/server/entries/operations/ -->
@@ -112,6 +112,7 @@
112
112
 
113
113
  <style>
114
114
  .layout-entry-form {
115
+ container-type: inline-size;
115
116
  max-width: 1100px;
116
117
  margin: 0 auto;
117
118
  padding: 24px;
@@ -13,6 +13,7 @@
13
13
  import ArchiveIcon from '@tabler/icons-svelte/icons/archive';
14
14
  import XIcon from '@tabler/icons-svelte/icons/x';
15
15
  import Button from '../../../components/ui/button/button.svelte';
16
+ import { cn } from '../../../utils.js';
16
17
  import { ElementSize, useDebounce } from 'runed';
17
18
  import { defaults, superForm, type SuperForm } from 'sveltekit-superforms';
18
19
  import { zod4, zod4Client } from 'sveltekit-superforms/adapters';
@@ -326,9 +327,7 @@
326
327
  validationErrors = errors;
327
328
 
328
329
  // Scroll to first errored field
329
- const firstErrorKey = Object.keys(validatedForm.errors).find(
330
- (k) => k !== '_errors'
331
- );
330
+ const firstErrorKey = Object.keys(validatedForm.errors).find((k) => k !== '_errors');
332
331
  if (firstErrorKey) {
333
332
  scrollToIssue(firstErrorKey);
334
333
  }
@@ -540,137 +539,145 @@
540
539
  });
541
540
 
542
541
  const t = $derived(lang[interfaceLanguage.current]);
542
+ const isHybrid = $derived(hybridContext.mode === 'hybrid' && !!collection.previewUrl);
543
543
  </script>
544
544
 
545
- <EntryHeader
546
- {entry}
547
- version={editingEntry}
548
- {onSave}
549
- onSaveDraft={performAutosave}
550
- {onArchive}
551
- {saveStatus}
552
- {isArchived}
553
- fields={getFieldsFromConfig(collection)}
554
- getFormData={() => get(form.form)}
555
- onScrollToIssue={scrollToIssue}
556
- {translationStatus}
557
- />
558
-
559
- {#if validationErrors.length > 0}
560
- <div
561
- role="alert"
562
- aria-live="assertive"
563
- class="flex items-start gap-3 border-b border-[var(--error)]/20 bg-[#FDF0F0] px-6 py-3 text-sm text-[var(--error)]"
564
- >
565
- <AlertCircle class="mt-0.5 size-4 shrink-0" />
566
- <div class="min-w-0 flex-1">
567
- <p class="font-semibold">{t.cannotPublish}</p>
568
- <p class="text-xs opacity-80">{t.validationHint}</p>
569
- <ul class="mt-1 text-xs">
570
- {#each validationErrors.slice(0, 5) as error}
571
- <li>— {error}</li>
572
- {/each}
573
- </ul>
574
- </div>
575
- <button
576
- type="button"
577
- onclick={() => (validationErrors = [])}
578
- class="shrink-0 rounded p-0.5 opacity-60 hover:opacity-100"
579
- aria-label="Zamknij"
545
+ <div class={isHybrid ? 'flex h-full flex-col overflow-hidden' : ''}>
546
+ <EntryHeader
547
+ {entry}
548
+ version={editingEntry}
549
+ {onSave}
550
+ onSaveDraft={performAutosave}
551
+ {onArchive}
552
+ {saveStatus}
553
+ {isArchived}
554
+ fields={getFieldsFromConfig(collection)}
555
+ getFormData={() => get(form.form)}
556
+ onScrollToIssue={scrollToIssue}
557
+ {translationStatus}
558
+ />
559
+
560
+ {#if validationErrors.length > 0}
561
+ <div
562
+ role="alert"
563
+ aria-live="assertive"
564
+ class="flex shrink-0 items-start gap-3 border-b border-[var(--error)]/20 bg-[#FDF0F0] px-6 py-3 text-sm text-[var(--error)]"
580
565
  >
581
- <XIcon class="size-4" />
582
- </button>
583
- </div>
584
- {/if}
566
+ <AlertCircle class="mt-0.5 size-4 shrink-0" />
567
+ <div class="min-w-0 flex-1">
568
+ <p class="font-semibold">{t.cannotPublish}</p>
569
+ <p class="text-xs opacity-80">{t.validationHint}</p>
570
+ <ul class="mt-1 text-xs">
571
+ {#each validationErrors.slice(0, 5) as error}
572
+ <li>— {error}</li>
573
+ {/each}
574
+ </ul>
575
+ </div>
576
+ <button
577
+ type="button"
578
+ onclick={() => (validationErrors = [])}
579
+ class="shrink-0 rounded p-0.5 opacity-60 hover:opacity-100"
580
+ aria-label="Zamknij"
581
+ >
582
+ <XIcon class="size-4" />
583
+ </button>
584
+ </div>
585
+ {/if}
585
586
 
586
- {#if isArchived}
587
- <div
588
- role="alert"
589
- class="flex items-center justify-between gap-3 border-b border-[var(--warning)]/20 bg-[#FDF6EC] px-6 py-3 text-sm"
590
- >
591
- <div class="flex items-center gap-2 text-[var(--warning)]">
592
- <ArchiveIcon class="size-4 shrink-0" />
593
- <span>{t.archivedBanner}</span>
587
+ {#if isArchived}
588
+ <div
589
+ role="alert"
590
+ class="flex shrink-0 items-center justify-between gap-3 border-b border-[var(--warning)]/20 bg-[#FDF6EC] px-6 py-3 text-sm"
591
+ >
592
+ <div class="flex items-center gap-2 text-[var(--warning)]">
593
+ <ArchiveIcon class="size-4 shrink-0" />
594
+ <span>{t.archivedBanner}</span>
595
+ </div>
596
+ <Button size="sm" onclick={onRestore}>{t.restore}</Button>
594
597
  </div>
595
- <Button size="sm" onclick={onRestore}>{t.restore}</Button>
596
- </div>
597
- {/if}
598
+ {/if}
598
599
 
599
- {#if showDraftBanner}
600
- <div
601
- class="flex items-center justify-between border-b bg-[var(--lavender-lighter)] px-6 py-2 text-sm"
602
- >
603
- <span class="text-[var(--text-secondary)]">{lang[interfaceLanguage.current].newerDraft}</span>
604
- <button
605
- type="button"
606
- class="font-semibold text-[var(--primary)] hover:underline"
607
- onclick={() => goto(`?version=${draftVersionId}`)}
600
+ {#if showDraftBanner}
601
+ <div
602
+ class="flex shrink-0 items-center justify-between border-b bg-[var(--lavender-lighter)] px-6 py-2 text-sm"
608
603
  >
609
- {lang[interfaceLanguage.current].switchToDraft}
610
- </button>
611
- </div>
612
- {:else if showPublishedBanner}
604
+ <span class="text-[var(--text-secondary)]">{lang[interfaceLanguage.current].newerDraft}</span>
605
+ <button
606
+ type="button"
607
+ class="font-semibold text-[var(--primary)] hover:underline"
608
+ onclick={() => goto(`?version=${draftVersionId}`)}
609
+ >
610
+ {lang[interfaceLanguage.current].switchToDraft}
611
+ </button>
612
+ </div>
613
+ {:else if showPublishedBanner}
614
+ <div
615
+ class="flex shrink-0 items-center justify-between border-b bg-[var(--lavender-lighter)] px-6 py-2 text-sm"
616
+ >
617
+ <span class="text-[var(--text-secondary)]"
618
+ >{lang[interfaceLanguage.current].editingDraft}</span
619
+ >
620
+ <button
621
+ type="button"
622
+ class="font-semibold text-[var(--primary)] hover:underline"
623
+ onclick={() => goto(`?version=${entry.publishedVersion!.id}`)}
624
+ >
625
+ {lang[interfaceLanguage.current].switchToPublished}
626
+ </button>
627
+ </div>
628
+ {/if}
629
+
613
630
  <div
614
- class="flex items-center justify-between border-b bg-[var(--lavender-lighter)] px-6 py-2 text-sm"
631
+ class={cn(
632
+ isArchived && 'pointer-events-none opacity-60',
633
+ isHybrid && 'flex min-h-0 flex-1 overflow-hidden'
634
+ )}
615
635
  >
616
- <span class="text-[var(--text-secondary)]">{lang[interfaceLanguage.current].editingDraft}</span>
617
- <button
618
- type="button"
619
- class="font-semibold text-[var(--primary)] hover:underline"
620
- onclick={() => goto(`?version=${entry.publishedVersion!.id}`)}
621
- >
622
- {lang[interfaceLanguage.current].switchToPublished}
623
- </button>
624
- </div>
625
- {/if}
626
-
627
- <div class={isArchived ? 'pointer-events-none opacity-60' : ''}>
628
- {#if hybridContext.mode === 'hybrid' && collection.previewUrl}
629
- <div class="flex min-h-0 flex-1 overflow-hidden">
630
- {#await import('./hybrid/hybrid-layout.svelte')}
631
- <div class="bg-accent h-full animate-pulse rounded-md"></div>
632
- {:then { default: HybridLayout }}
633
- <HybridLayout>
634
- {#snippet preview()}
635
- {#await import('./hybrid/hybrid-preview.svelte')}
636
- <div class="bg-accent h-full animate-pulse rounded-md"></div>
637
- {:then { default: HybridPreview }}
638
- <HybridPreview
639
- {collection}
640
- {editingEntry}
641
- bind:previewIframe
642
- bind:sizePreset
643
- {size}
644
- bind:el
636
+ {#if isHybrid}
637
+ {#await import('./hybrid/hybrid-layout.svelte')}
638
+ <div class="bg-accent h-full animate-pulse rounded-md"></div>
639
+ {:then { default: HybridLayout }}
640
+ <HybridLayout>
641
+ {#snippet preview()}
642
+ {#await import('./hybrid/hybrid-preview.svelte')}
643
+ <div class="bg-accent h-full animate-pulse rounded-md"></div>
644
+ {:then { default: HybridPreview }}
645
+ <HybridPreview
646
+ {collection}
647
+ {editingEntry}
648
+ bind:previewIframe
649
+ bind:sizePreset
650
+ {size}
651
+ bind:el
652
+ />
653
+ {:catch}
654
+ <p class="text-destructive p-4 text-sm">Failed to load preview</p>
655
+ {/await}
656
+ {/snippet}
657
+ {#snippet formPanel()}
658
+ <EntryForm
659
+ {form}
660
+ {entry}
661
+ focusedPath={hybridContext.focusedPath}
662
+ onPathSelect={(path) => (hybridContext.focusedPath = path)}
645
663
  />
646
- {:catch}
647
- <p class="text-destructive p-4 text-sm">Failed to load preview</p>
648
- {/await}
649
- {/snippet}
650
- {#snippet formPanel()}
651
- <EntryForm
652
- {form}
653
- {entry}
654
- focusedPath={hybridContext.focusedPath}
655
- onPathSelect={(path) => (hybridContext.focusedPath = path)}
656
- />
657
- {/snippet}
658
- </HybridLayout>
659
- {:catch}
660
- <p class="text-destructive p-4 text-sm">Failed to load layout</p>
661
- {/await}
662
- </div>
663
- {:else if hasLayout(collection)}
664
- <div class="overflow-y-auto" style="scroll-padding-top: 48px;">
665
- <EntryForm {form} {entry} />
666
- </div>
667
- {:else}
668
- <div class="flex items-stretch justify-center" style="scroll-padding-top: 48px;">
669
- <div class="max-w-2xl grow p-4 lg:p-6">
670
- <div class="bg-card rounded-2xl border p-4 shadow-sm lg:p-6">
664
+ {/snippet}
665
+ </HybridLayout>
666
+ {:catch}
667
+ <p class="text-destructive p-4 text-sm">Failed to load layout</p>
668
+ {/await}
669
+ {:else if hasLayout(collection)}
670
+ <div class="overflow-y-auto" style="scroll-padding-top: 48px;">
671
671
  <EntryForm {form} {entry} />
672
672
  </div>
673
- </div>
673
+ {:else}
674
+ <div class="flex items-stretch justify-center" style="scroll-padding-top: 48px;">
675
+ <div class="max-w-2xl grow p-4 lg:p-6">
676
+ <div class="bg-card rounded-2xl border p-4 shadow-sm lg:p-6">
677
+ <EntryForm {form} {entry} />
678
+ </div>
679
+ </div>
680
+ </div>
681
+ {/if}
674
682
  </div>
675
- {/if}
676
683
  </div>
@@ -28,6 +28,36 @@
28
28
  tablet: [768, 1024],
29
29
  mobile: [375, 667]
30
30
  } as const;
31
+
32
+ const deviceFrames = {
33
+ mobile: { padX: 16, padTop: 40, padBottom: 24, radius: 32, notch: 40 },
34
+ tablet: { padX: 20, padTop: 32, padBottom: 24, radius: 24, notch: 48 }
35
+ } as const;
36
+
37
+ const frameSize = $derived.by(() => {
38
+ if (sizePreset !== 'mobile' && sizePreset !== 'tablet') return null;
39
+ const frame = deviceFrames[sizePreset];
40
+ const [iw, ih] = sizePresets[sizePreset];
41
+ return {
42
+ w: iw + frame.padX * 2 + 16,
43
+ h: ih + frame.padTop + frame.padBottom + 16
44
+ };
45
+ });
46
+
47
+ const scale = $derived.by(() => {
48
+ if (sizePreset === 'responsive') return 1;
49
+ const cw = size.width;
50
+ const ch = size.height;
51
+ if (!cw || !ch) return 1;
52
+
53
+ if (sizePreset === 'desktop') {
54
+ const [iw, ih] = sizePresets.desktop;
55
+ return Math.min(cw / iw, ch / ih, 1);
56
+ }
57
+
58
+ if (!frameSize) return 1;
59
+ return Math.min(cw / frameSize.w, ch / frameSize.h, 1) * 0.95;
60
+ });
31
61
  </script>
32
62
 
33
63
  <div class="flex h-full flex-col overflow-hidden">
@@ -56,14 +86,67 @@
56
86
  </Button>
57
87
  </div>
58
88
  <div class="relative flex-1 overflow-hidden" bind:this={el}>
59
- <iframe
60
- bind:this={previewIframe}
61
- style={sizePreset !== 'responsive' ? `transform: scale(${size.width / sizePresets[sizePreset][0]});` : ''}
62
- class="{sizePreset === 'responsive' ? 'h-full w-full' : 'absolute top-0 left-0 origin-top-left'} border-0"
63
- width={sizePreset !== 'responsive' ? sizePresets[sizePreset][0] : undefined}
64
- height={sizePreset !== 'responsive' ? sizePresets[sizePreset][1] : undefined}
65
- src="{collection.previewUrl}?preview={editingEntry.id}"
66
- title="preview"
67
- ></iframe>
89
+ {#if sizePreset === 'responsive'}
90
+ <iframe
91
+ bind:this={previewIframe}
92
+ class="h-full w-full border-0"
93
+ src="{collection.previewUrl}?preview={editingEntry.id}"
94
+ title="preview"
95
+ ></iframe>
96
+ {:else if sizePreset === 'desktop'}
97
+ <div class="flex h-full w-full items-center justify-center">
98
+ <iframe
99
+ bind:this={previewIframe}
100
+ style="transform: scale({scale}); transform-origin: center center;"
101
+ class="shrink-0 border-0"
102
+ width={sizePresets.desktop[0]}
103
+ height={sizePresets.desktop[1]}
104
+ src="{collection.previewUrl}?preview={editingEntry.id}"
105
+ title="preview"
106
+ ></iframe>
107
+ </div>
108
+ {:else}
109
+ {@const frame = deviceFrames[sizePreset]}
110
+ {@const [iw, ih] = sizePresets[sizePreset]}
111
+ <div class="flex h-full w-full items-center justify-center">
112
+ <div
113
+ class="relative shrink-0 overflow-hidden"
114
+ style="
115
+ width: {iw + frame.padX * 2 + 16}px;
116
+ height: {ih + frame.padTop + frame.padBottom + 16}px;
117
+ border: 8px solid #1a1a2e;
118
+ border-radius: {frame.radius}px;
119
+ background: #000;
120
+ transform: scale({scale});
121
+ transform-origin: center center;
122
+ "
123
+ >
124
+ <!-- notch -->
125
+ <div
126
+ class="absolute left-1/2 -translate-x-1/2"
127
+ style="
128
+ top: {(frame.padTop - 6) / 2}px;
129
+ width: {frame.notch}px;
130
+ height: 6px;
131
+ background: #333;
132
+ border-radius: 3px;
133
+ "
134
+ ></div>
135
+ <!-- iframe -->
136
+ <iframe
137
+ bind:this={previewIframe}
138
+ class="absolute border-0 bg-white"
139
+ style="
140
+ top: {frame.padTop}px;
141
+ left: {frame.padX}px;
142
+ "
143
+ width={iw}
144
+ height={ih}
145
+ src="{collection.previewUrl}?preview={editingEntry.id}"
146
+ title="preview"
147
+ ></iframe>
148
+ </div>
149
+ </div>
150
+ {/if}
68
151
  </div>
69
152
  </div>