@websline/cms-view-utils 0.26.0 → 1.0.0-alpha.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.
@@ -0,0 +1 @@
1
+ (()=>{(function(){var c;if(typeof window>"u")return;let u=150,a=60,t=document.currentScript||[...document.querySelectorAll("script[data-slug]")].pop(),n=(c=t==null?void 0:t.dataset)==null?void 0:c.slug;if(!n){console.warn("[editScroll] Missing data-slug");return}let s=`preview-scroll:${n}`,r=null,l=0,i=()=>{let e=window.scrollY;e!==l&&(l=e,sessionStorage.setItem(s,String(e)))},d=()=>{clearTimeout(r),r=setTimeout(i,u)},w=()=>{let e=sessionStorage.getItem(s);if(!e)return;let o=Number(e);Number.isNaN(o)||requestAnimationFrame(()=>{window.scrollTo(0,o),setTimeout(()=>{window.scrollTo(0,o)},a)})};window.addEventListener("scroll",d,{passive:!0}),window.addEventListener("beforeunload",i),window.addEventListener("load",w)})();})();
@@ -0,0 +1 @@
1
+ (()=>{var e={BLOCK_ADD:"BLOCK_ADD",BLOCK_DELETE:"BLOCK_DELETE",BLOCK_EDIT:"BLOCK_EDIT",BLOCK_SHOW:"BLOCK_SHOW",BLOCK_HIDE:"BLOCK_HIDE",SLUG_CHANGED:"SLUG_CHANGED",SLUG_ON_LOAD:"SLUG_ON_LOAD",IFRAME_DOM_LOADED:"IFRAME_DOM_LOADED",CURRENT_BLOCK_EDIT:"CURRENT_BLOCK_EDIT",SCROLL_TO_BLOCK:"SCROLL_TO_BLOCK"},_=(r,L={})=>{r&&window.parent!==window&&window.parent.postMessage({type:r,payload:L},"*")};(function(){if(window.parent===window||window.__PREVIEW_BRIDGE_INITIALIZED__)return;window.__PREVIEW_BRIDGE_INITIALIZED__=!0;let r=t=>t.metaKey||t.ctrlKey||t.shiftKey,L=t=>{let n=t.target.closest("a[href]");if(!n||n.target==="_blank"||r(t))return;let o=new URL(n.href,window.location.origin);o.origin===window.location.origin&&(t.preventDefault(),t.stopPropagation(),_(e.SLUG_CHANGED,{slug:o.pathname}))};document.addEventListener("click",L,!0),_(e.SLUG_ON_LOAD,{slug:window.location.pathname}),window.addEventListener("load",()=>{_(e.IFRAME_DOM_LOADED)});let c=t=>{let n=`[data-cms-block-id="${t}"]`,o=0,i=10,a=setInterval(()=>{let E=document.querySelector(n);E&&(E.scrollIntoView({behavior:"smooth",block:"center"}),clearInterval(a)),++o>=i&&clearInterval(a)},100)};window.addEventListener("message",t=>{let{type:n,payload:o}=t.data||{};if(n&&n===e.SCROLL_TO_BLOCK){let{draftBlockUuid:i}=o||{};if(!i)return;c(i)}})})();})();
package/package.json CHANGED
@@ -1,19 +1,25 @@
1
1
  {
2
2
  "name": "@websline/cms-view-utils",
3
- "version": "0.26.0",
3
+ "version": "1.0.0-alpha.0",
4
4
  "type": "module",
5
- "exports": {
6
- ".": "./src/index.js"
7
- },
8
5
  "files": [
9
- "src"
6
+ "src",
7
+ "dist"
10
8
  ],
9
+ "exports": {
10
+ "./components/*": "./src/components/*",
11
+ "./utils/*": "./src/utils/*",
12
+ "./preview-bridge": "./dist/previewBridge.global.js",
13
+ "./edit-scroll": "./dist/editScroll.global.js",
14
+ "./dist/*": "./dist/*"
15
+ },
16
+ "dependencies": {
17
+ "jsonwebtoken": "^9.0.3"
18
+ },
11
19
  "peerDependencies": {
12
- "astro": "^5.9.2",
13
- "svelte": "^5.34.1"
20
+ "svelte": "^5.0.0"
14
21
  },
15
22
  "scripts": {
16
- "bump:minor": "pnpm version minor --no-git-tag-version",
17
- "release": "pnpm run bump:minor && pnpm publish --access public --no-git-checks"
23
+ "build": "tsup"
18
24
  }
19
25
  }
@@ -0,0 +1,49 @@
1
+ <script>
2
+ import { EVENTS, notifyCMS } from "../utils/notify.js";
3
+
4
+ let {
5
+ class: className,
6
+ color = "#000fff",
7
+ textColor = "#F1F1F2",
8
+ label = "Ersten Block hinzufügen",
9
+ } = $props();
10
+
11
+ const handleClick = () => {
12
+ notifyCMS(EVENTS.BLOCK_ADD);
13
+ };
14
+ </script>
15
+
16
+ <button
17
+ class={`group relative block min-h-32 w-full cursor-pointer rounded-lg transition-all duration-150 ${className}`}
18
+ style={`--block-color: ${color}; --block-text: ${textColor};`}
19
+ onclick={handleClick}>
20
+ <div
21
+ class="absolute inset-0 rounded-lg border-2 border-dashed transition-all duration-150 group-hover:opacity-60"
22
+ style="border-color: var(--block-color); opacity: 0.25;">
23
+ </div>
24
+
25
+ <div
26
+ class="absolute inset-0 rounded-lg transition-all duration-150 group-hover:opacity-10"
27
+ style="background: var(--block-color); opacity: 0;">
28
+ </div>
29
+
30
+ <div class="relative flex h-full w-full flex-col items-center justify-center gap-2">
31
+ <div
32
+ class="flex h-10 w-10 items-center justify-center rounded-full border transition-all duration-150 group-hover:scale-110"
33
+ style="border-color: var(--block-color); color: var(--block-color);">
34
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
35
+ <path
36
+ d="M12 5V19M5 12H19"
37
+ stroke="currentColor"
38
+ stroke-width="2"
39
+ stroke-linecap="round" />
40
+ </svg>
41
+ </div>
42
+
43
+ <span
44
+ class="text-sm transition-opacity duration-150 group-hover:opacity-100"
45
+ style="color: var(--block-color);">
46
+ {label}
47
+ </span>
48
+ </div>
49
+ </button>
@@ -0,0 +1,51 @@
1
+ <script>
2
+ let { block, children, spacingValues = {} } = $props();
3
+
4
+ const defaultSpacingValues = {
5
+ bottom: {
6
+ none: "pb-0",
7
+ s: "pb-4",
8
+ m: "pb-12",
9
+ l: "pb-24",
10
+ xl: "pb-30",
11
+ },
12
+ top: {
13
+ none: "pt-0",
14
+ s: "pt-4",
15
+ m: "pt-12",
16
+ l: "pt-24",
17
+ xl: "pt-30",
18
+ },
19
+ };
20
+
21
+ const spacingMap = $derived({
22
+ bottom: {
23
+ ...defaultSpacingValues.bottom,
24
+ ...(spacingValues?.bottom ?? {}),
25
+ },
26
+ top: {
27
+ ...defaultSpacingValues.top,
28
+ ...(spacingValues?.top ?? {}),
29
+ },
30
+ });
31
+
32
+ let spacingBottom = $derived(block?.meta?.spacing?.bottom);
33
+ let spacingTop = $derived(block?.meta?.spacing?.top);
34
+
35
+ let spacingClass = $derived(
36
+ [
37
+ spacingMap.top[spacingTop] ?? spacingMap.top.m,
38
+ spacingMap.bottom[spacingBottom] ?? spacingMap.bottom.m,
39
+ ].join(" "),
40
+ );
41
+
42
+ let backgroundColor = $derived(block?.meta?.background);
43
+
44
+ let style = $derived(
45
+ backgroundColor ? `background-color: ${backgroundColor}` : undefined,
46
+ );
47
+ </script>
48
+
49
+ <div data-cms-block-wrapper class={`${spacingClass}`} {style}>
50
+ {@render children?.()}
51
+ </div>
@@ -0,0 +1,284 @@
1
+ <script>
2
+ import { onMount } from "svelte";
3
+ import { EVENTS, notifyCMS } from "../utils/notify.js";
4
+ import { CMS_ACTIONS } from "../utils/cmsActions.js";
5
+ import Text from "./skeleton/Text.svelte";
6
+ import Image from "./skeleton/Image.svelte";
7
+ import Grid from "./skeleton/Grid.svelte";
8
+ import ImageText from "./skeleton/ImageText.svelte";
9
+
10
+ let {
11
+ block,
12
+ children,
13
+ color = "#000fff",
14
+ isValidBlock = true,
15
+ position,
16
+ textColor = "#F1F1F2",
17
+ } = $props();
18
+
19
+ const debounce = (fn, delay = 150) => {
20
+ let timer;
21
+
22
+ const debounced = (...args) => {
23
+ clearTimeout(timer);
24
+ timer = setTimeout(() => fn(...args), delay);
25
+ };
26
+
27
+ debounced.cancel = () => {
28
+ clearTimeout(timer);
29
+ };
30
+
31
+ return debounced;
32
+ };
33
+
34
+ const handleClick = (action) => {
35
+ switch (action) {
36
+ case CMS_ACTIONS.add:
37
+ notifyCMS(EVENTS.BLOCK_ADD, { position });
38
+ break;
39
+ case CMS_ACTIONS.delete:
40
+ notifyCMS(EVENTS.BLOCK_DELETE, {
41
+ draftBlockUuid: block.uuid,
42
+ });
43
+ break;
44
+ case CMS_ACTIONS.edit:
45
+ notifyCMS(EVENTS.BLOCK_EDIT, {
46
+ blockIdentifier: block.identifier,
47
+ draftBlockUuid: block.uuid,
48
+ });
49
+ break;
50
+ case CMS_ACTIONS.hideBlock:
51
+ notifyCMS(EVENTS.BLOCK_HIDE, {
52
+ draftBlockUuid: block.uuid,
53
+ });
54
+ break;
55
+ case CMS_ACTIONS.showBlock:
56
+ notifyCMS(EVENTS.BLOCK_SHOW, {
57
+ draftBlockUuid: block.uuid,
58
+ });
59
+ break;
60
+ default:
61
+ break;
62
+ }
63
+ };
64
+
65
+ let blockVisible = $derived(block?.meta?.visibility?.enabled);
66
+
67
+ let isHovered = $state(false);
68
+ let isActive = $state(false);
69
+ let skeleton = $derived(block?.meta?.emptyOptions?.skeleton || "text");
70
+
71
+ let toolbarVisible = $derived(isHovered || isActive);
72
+
73
+ const handleLeave = () => {
74
+ handleEnterDebounced.cancel();
75
+ isHovered = false;
76
+ };
77
+
78
+ const handleEnterDebounced = debounce(() => {
79
+ isHovered = true;
80
+ }, 50);
81
+
82
+ const handler = (event) => {
83
+ const { type, payload } = event.data || {};
84
+ if (type !== EVENTS.CURRENT_BLOCK_EDIT) return;
85
+
86
+ const { draftBlockUuid } = payload || {};
87
+
88
+ isActive = draftBlockUuid === block.uuid;
89
+ };
90
+
91
+ onMount(() => {
92
+ window.addEventListener("message", handler);
93
+
94
+ return () => {
95
+ window.removeEventListener("message", handler);
96
+ };
97
+ });
98
+ </script>
99
+
100
+ <div
101
+ class="relative min-h-13"
102
+ style={`--block-color: ${color}; --block-text: ${textColor};`}
103
+ data-cms-block-id={block.uuid}
104
+ data-cms-editable-block
105
+ onmouseenter={handleEnterDebounced}
106
+ onmouseleave={handleLeave}
107
+ role="group">
108
+ <div
109
+ class="pointer-events-none absolute inset-0 z-20 border-6 transition-opacity duration-100"
110
+ style="border-color: var(--block-color)"
111
+ class:opacity-100={toolbarVisible}
112
+ class:opacity-0={!toolbarVisible}>
113
+ </div>
114
+
115
+ <div
116
+ class="absolute top-1.5 left-1/2 z-10 flex h-9 -translate-x-1/2 items-center justify-center gap-1.5 rounded-b-lg px-3 pb-1 transition-opacity duration-100 before:absolute before:top-0 before:-left-4 before:h-4 before:w-4 before:rounded-full before:bg-transparent before:shadow-[6px_-6px_0_0_var(--block-color)] before:content-[''] after:absolute after:top-0 after:-right-4 after:h-4 after:w-4 after:rounded-full after:bg-transparent after:shadow-[-6px_-6px_0_0_var(--block-color)] after:content-['']"
117
+ style="background: var(--block-color); color: var(--block-text);"
118
+ class:opacity-100={toolbarVisible}
119
+ class:opacity-0={!toolbarVisible}>
120
+ <button
121
+ aria-label="add block"
122
+ class="cursor-pointer"
123
+ onclick={() => handleClick(CMS_ACTIONS.add)}>
124
+ <svg
125
+ width="24"
126
+ height="24"
127
+ viewBox="0 0 24 24"
128
+ fill="none"
129
+ xmlns="http://www.w3.org/2000/svg">
130
+ <path
131
+ d="M12.0346 4L12.0137 20M4 12H20"
132
+ stroke="var(--block-text)"
133
+ stroke-width="1.5"
134
+ stroke-linecap="round"
135
+ stroke-linejoin="round" />
136
+ </svg>
137
+ </button>
138
+
139
+ <!-- {#if blockVisible}
140
+ <button
141
+ aria-label="hide block"
142
+ class="cursor-pointer"
143
+ onclick={() => handleClick(CMS_ACTIONS.hideBlock)}>
144
+ <svg
145
+ width="24"
146
+ height="24"
147
+ viewBox="0 0 16 16"
148
+ fill="none"
149
+ xmlns="http://www.w3.org/2000/svg">
150
+ <path
151
+ d="M7.99992 12C11.6818 12 14.6666 8 14.6666 8C14.6666 8 11.6818 4 7.99992 4C4.31802 4 1.33325 8 1.33325 8C1.33325 8 4.31802 12 7.99992 12Z"
152
+ stroke="var(--block-text)"
153
+ stroke-width="1"
154
+ stroke-linejoin="round" />
155
+ <path
156
+ d="M7.99992 9.66667C8.92038 9.66667 9.66658 8.92047 9.66658 8C9.66658 7.07953 8.92038 6.33333 7.99992 6.33333C7.07945 6.33333 6.33325 7.07953 6.33325 8C6.33325 8.92047 7.07945 9.66667 7.99992 9.66667Z"
157
+ stroke="var(--block-text)"
158
+ stroke-width="1"
159
+ stroke-linejoin="round" />
160
+ </svg>
161
+ </button>
162
+ {/if}
163
+
164
+ {#if !blockVisible}
165
+ <button
166
+ aria-label="show block"
167
+ class="cursor-pointer"
168
+ onclick={() => handleClick(CMS_ACTIONS.showBlock)}>
169
+ <svg
170
+ width="24"
171
+ height="24"
172
+ viewBox="0 0 16 16"
173
+ fill="none"
174
+ xmlns="http://www.w3.org/2000/svg">
175
+ <path
176
+ d="M7.99992 12C11.6818 12 14.6666 8 14.6666 8C14.6666 8 11.6818 4 7.99992 4C4.31802 4 1.33325 8 1.33325 8C1.33325 8 4.31802 12 7.99992 12Z"
177
+ stroke="var(--block-text)"
178
+ stroke-width="1"
179
+ stroke-linejoin="round" />
180
+ <path
181
+ d="M7.99992 9.66667C8.92038 9.66667 9.66658 8.92047 9.66658 8C9.66658 7.07953 8.92038 6.33333 7.99992 6.33333C7.07945 6.33333 6.33325 7.07953 6.33325 8C6.33325 8.92047 7.07945 9.66667 7.99992 9.66667Z"
182
+ stroke="var(--block-text)"
183
+ stroke-width="1"
184
+ stroke-linejoin="round" />
185
+ <path
186
+ d="M2.66675 13.3334L13.3334 2.66671"
187
+ stroke="var(--block-text)"
188
+ stroke-width="1"
189
+ stroke-linecap="round" />
190
+ </svg>
191
+ </button>
192
+ {/if} -->
193
+
194
+ <button
195
+ aria-label="copy block"
196
+ class="cursor-pointer"
197
+ onclick={() => handleClick(CMS_ACTIONS.copy)}>
198
+ <svg
199
+ width="24"
200
+ height="24"
201
+ viewBox="0 0 28 28"
202
+ fill="none"
203
+ xmlns="http://www.w3.org/2000/svg">
204
+ <path
205
+ d="M0 4C0 1.79086 1.79086 0 4 0H24C26.2091 0 28 1.79086 28 4V24C28 26.2091 26.2091 28 24 28H4C1.79086 28 0 26.2091 0 24V4Z"
206
+ fill="var(--block-color)"></path>
207
+ <path
208
+ d="M10 4.5H22C22.2761 4.50011 22.5 4.72392 22.5 5V19C22.5 19.2761 22.2761 19.4999 22 19.5H18.5V22.999C18.5 23.2731 18.2763 23.4999 17.9932 23.5H6.00684C5.72308 23.5 5.5 23.2754 5.5 22.999L5.50293 9.00098C5.50298 8.72676 5.72665 8.5 6.00977 8.5H9.5V5C9.5 4.72386 9.72386 4.5 10 4.5ZM6.50195 10L6.5 22V22.5H17.5V9.5H6.50293L6.50195 10ZM10.5 8.5H18.5V18.5H21.5V5.5H10.5V8.5Z"
209
+ stroke="var(--block-text)"></path>
210
+ </svg>
211
+ </button>
212
+
213
+ <button
214
+ aria-label="position block"
215
+ class="cursor-pointer"
216
+ onclick={() => handleClick(CMS_ACTIONS.position)}>
217
+ <svg
218
+ width="24"
219
+ height="24"
220
+ viewBox="0 0 24 24"
221
+ fill="none"
222
+ xmlns="http://www.w3.org/2000/svg">
223
+ <path
224
+ d="M12.5 7L8 2.5L3.5 7M7.99585 15.5V2.5M21 17L16.5 21.5L12 17M16.4958 8.5V21.5"
225
+ stroke="var(--block-text)"
226
+ stroke-width="1.5"
227
+ stroke-linecap="round"
228
+ stroke-linejoin="round" />
229
+ </svg>
230
+ </button>
231
+
232
+ <button
233
+ aria-label="delete block"
234
+ class="cursor-pointer"
235
+ onclick={() => handleClick(CMS_ACTIONS.delete)}>
236
+ <svg
237
+ width="24"
238
+ height="24"
239
+ viewBox="0 0 16 16"
240
+ fill="none"
241
+ xmlns="http://www.w3.org/2000/svg">
242
+ <path
243
+ d="M6.66667 6.66659V11.3333M9.33333 6.66659V11.3333M2 3.33325H14M3.33333 3.33325V14.6666H12.6667V3.33325H3.33333ZM5.33333 3.33325L6.42967 1.33325H9.59237L10.6667 3.33325H5.33333Z"
244
+ stroke="var(--block-text)"
245
+ stroke-width="1"
246
+ stroke-linecap="round"
247
+ stroke-linejoin="round" />
248
+ </svg>
249
+ </button>
250
+
251
+ <button
252
+ aria-label="edit block"
253
+ class="cursor-pointer"
254
+ onclick={() => handleClick(CMS_ACTIONS.edit)}>
255
+ <svg
256
+ width="24"
257
+ height="24"
258
+ viewBox="0 0 24 24"
259
+ fill="none"
260
+ xmlns="http://www.w3.org/2000/svg">
261
+ <path
262
+ d="M3 22H21M5 13.5111V17.5556H8.6586L19 6.06006L15.3476 2L5 13.5111Z"
263
+ stroke="var(--block-text)"
264
+ stroke-width="1.5"
265
+ stroke-linecap="round"
266
+ stroke-linejoin="round" />
267
+ </svg>
268
+ </button>
269
+ </div>
270
+
271
+ <div class:opacity-100={blockVisible} class:opacity-25={!blockVisible}>
272
+ {#if isValidBlock}
273
+ {@render children?.()}
274
+ {:else if skeleton === "text"}
275
+ <Text />
276
+ {:else if skeleton === "image"}
277
+ <Image />
278
+ {:else if skeleton === "grid"}
279
+ <Grid />
280
+ {:else if skeleton === "image-text"}
281
+ <ImageText />
282
+ {/if}
283
+ </div>
284
+ </div>
@@ -0,0 +1,87 @@
1
+ <script>
2
+ let {
3
+ className = "",
4
+ decoding = "async",
5
+ image,
6
+ loading = "lazy",
7
+ sources = {},
8
+ } = $props();
9
+
10
+ const data = (() => {
11
+ if (!image || !image.formats) {
12
+ return {
13
+ resolved: [],
14
+ fallbackFormat: null,
15
+ fallbackSrc: "",
16
+ alt: "",
17
+ };
18
+ }
19
+
20
+ const formats = image.formats;
21
+
22
+ const formatMap = Object.create(null);
23
+
24
+ for (let i = 0; i < formats.length; i++) {
25
+ const f = formats[i];
26
+ formatMap[f.identifier] = f;
27
+ }
28
+
29
+ const normalized = [];
30
+
31
+ for (const id in sources) {
32
+ const min = sources[id];
33
+ normalized.push([id, min === true ? 0 : (min ?? 0)]);
34
+ }
35
+
36
+ if (normalized.length > 1) {
37
+ normalized.sort((a, b) => b[1] - a[1]);
38
+ }
39
+
40
+ const resolved = [];
41
+
42
+ for (let i = 0; i < normalized.length; i++) {
43
+ const [id, min] = normalized[i];
44
+ const format = formatMap[id];
45
+
46
+ if (format && format.sources && format.sources.length) {
47
+ resolved.push({ min, format });
48
+ }
49
+ }
50
+
51
+ let fallbackFormat = null;
52
+ let fallbackSrc = "";
53
+
54
+ if (resolved.length) {
55
+ const last = resolved[resolved.length - 1];
56
+
57
+ fallbackFormat = last.format;
58
+ fallbackSrc = fallbackFormat.sources[0]?.url ?? "";
59
+ }
60
+
61
+ const alt = image.alt ?? image.title ?? "";
62
+
63
+ return { resolved, fallbackFormat, fallbackSrc, alt };
64
+ })();
65
+ </script>
66
+
67
+ {#if data.resolved.length}
68
+ <picture>
69
+ {#each data.resolved as r (r.format.identifier + "-" + r.min)}
70
+ {#each r.format.sources as source (source.type + source.url)}
71
+ <source
72
+ srcset={source.url}
73
+ type={source.type}
74
+ media={r.min > 0 ? `(min-width: ${r.min}px)` : undefined} />
75
+ {/each}
76
+ {/each}
77
+
78
+ <img
79
+ src={data.fallbackSrc}
80
+ alt={data.alt}
81
+ width={data.fallbackFormat.width}
82
+ height={data.fallbackFormat.height}
83
+ {loading}
84
+ {decoding}
85
+ class={`h-full w-full object-cover ${className}`} />
86
+ </picture>
87
+ {/if}
@@ -0,0 +1,34 @@
1
+ <script>
2
+ const { columns = 3, gap = 2, items = 6, ratio = "4/3" } = $props();
3
+
4
+ const gridClass = $derived(
5
+ columns === 2 ? "grid-cols-2" : columns === 4 ? "grid-cols-4" : "grid-cols-3",
6
+ );
7
+ </script>
8
+
9
+ <div class={`mx-auto grid max-w-220 gap-${gap} ${gridClass}`}>
10
+ {#each Array.from({ length: items }, (_, idx) => idx) as idx (idx)}
11
+ <div
12
+ class="relative w-full overflow-hidden bg-slate-300/60"
13
+ style={`aspect-ratio: ${ratio};`}>
14
+ <div
15
+ class="animate-shimmer absolute inset-0 bg-linear-to-r from-transparent via-white/60 to-transparent">
16
+ </div>
17
+ </div>
18
+ {/each}
19
+ </div>
20
+
21
+ <style>
22
+ @keyframes shimmer {
23
+ 0% {
24
+ transform: translateX(-120%);
25
+ }
26
+ 100% {
27
+ transform: translateX(120%);
28
+ }
29
+ }
30
+
31
+ .animate-shimmer {
32
+ animation: shimmer 1.4s infinite;
33
+ }
34
+ </style>
@@ -0,0 +1,26 @@
1
+ <script>
2
+ const { ratio = "16/9" } = $props();
3
+ </script>
4
+
5
+ <div
6
+ class="relative mx-auto w-full max-w-220 overflow-hidden bg-slate-300/60"
7
+ style={`aspect-ratio: ${ratio};`}>
8
+ <div
9
+ class="animate-shimmer absolute inset-0 bg-linear-to-r from-transparent via-white/60 to-transparent">
10
+ </div>
11
+ </div>
12
+
13
+ <style>
14
+ @keyframes shimmer {
15
+ 0% {
16
+ transform: translateX(-120%);
17
+ }
18
+ 100% {
19
+ transform: translateX(120%);
20
+ }
21
+ }
22
+
23
+ .animate-shimmer {
24
+ animation: shimmer 1.4s infinite;
25
+ }
26
+ </style>
@@ -0,0 +1,50 @@
1
+ <script>
2
+ const { ratio = "1/1", reverse = false } = $props();
3
+
4
+ const widths = ["w-full", "w-11/12", "w-10/12", "w-8/12"];
5
+ </script>
6
+
7
+ <div
8
+ class={`mx-auto flex max-w-220 items-center gap-6 ${
9
+ reverse ? "flex-row-reverse" : "flex-row"
10
+ }`}>
11
+ <div
12
+ class="relative w-1/2 overflow-hidden bg-slate-300/60"
13
+ style={`aspect-ratio: ${ratio};`}>
14
+ <div
15
+ class="animate-shimmer absolute inset-0 bg-linear-to-r from-transparent via-white/60 to-transparent">
16
+ </div>
17
+ </div>
18
+
19
+ <div class="w-1/2 space-y-2.5">
20
+ {#each Array.from({ length: 6 }, (_, idx) => idx) as idx (idx)}
21
+ <div
22
+ class={`relative overflow-hidden rounded-md ${
23
+ idx === 0
24
+ ? "h-5 w-2/3 bg-slate-300/70"
25
+ : `h-[0.7rem] bg-slate-300/60 ${
26
+ idx === 3 ? "w-2/5" : widths[idx % widths.length]
27
+ }`
28
+ }`}>
29
+ <div
30
+ class="animate-shimmer absolute inset-0 bg-linear-to-r from-transparent via-white/60 to-transparent">
31
+ </div>
32
+ </div>
33
+ {/each}
34
+ </div>
35
+ </div>
36
+
37
+ <style>
38
+ @keyframes shimmer {
39
+ 0% {
40
+ transform: translateX(-120%);
41
+ }
42
+ 100% {
43
+ transform: translateX(120%);
44
+ }
45
+ }
46
+
47
+ .animate-shimmer {
48
+ animation: shimmer 1.4s infinite;
49
+ }
50
+ </style>
@@ -0,0 +1,38 @@
1
+ <script>
2
+ const { lines = 6 } = $props();
3
+
4
+ const widths = ["w-full", "w-11/12", "w-10/12", "w-8/12", "w-9/12"];
5
+ </script>
6
+
7
+ <div class="mx-auto max-w-220 space-y-2.5">
8
+ {#each Array.from({ length: lines }) as value, idx (idx)}
9
+ <div
10
+ class={`relative overflow-hidden rounded-md ${
11
+ idx === 0
12
+ ? "h-5 w-2/3 bg-slate-300/70"
13
+ : `h-[0.7rem] bg-slate-300/60 ${
14
+ idx === lines - 1 ? "w-2/5" : widths[idx % widths.length]
15
+ }`
16
+ }`}
17
+ data-ignore={value}>
18
+ <div
19
+ class="animate-shimmer absolute inset-0 bg-linear-to-r from-transparent via-white/60 to-transparent">
20
+ </div>
21
+ </div>
22
+ {/each}
23
+ </div>
24
+
25
+ <style>
26
+ @keyframes shimmer {
27
+ 0% {
28
+ transform: translateX(-120%);
29
+ }
30
+ 100% {
31
+ transform: translateX(120%);
32
+ }
33
+ }
34
+
35
+ .animate-shimmer {
36
+ animation: shimmer 1.4s infinite;
37
+ }
38
+ </style>
@@ -0,0 +1,9 @@
1
+ const isValid = (schema, block) => {
2
+ const { data, meta: { emptyOptions: { emptyBehavior } = {} } = {} } = block || {};
3
+
4
+ if (emptyBehavior === "show") return true;
5
+
6
+ return schema.safeParse(data).success;
7
+ };
8
+
9
+ export { isValid };
@@ -0,0 +1,12 @@
1
+ const CMS_ACTIONS = {
2
+ add: "add-block",
3
+ copy: "copy-block",
4
+ delete: "delete-block",
5
+ edit: "edit-block",
6
+ hideBlock: "hide-block",
7
+ position: "position-block",
8
+ showBlock: "show-block",
9
+ unlink: "unlink-block",
10
+ };
11
+
12
+ export { CMS_ACTIONS };
@@ -0,0 +1,55 @@
1
+ (function () {
2
+ if (typeof window === "undefined") return;
3
+
4
+ const DEBOUNCE_MS = 150;
5
+ const RESTORE_DELAY = 60;
6
+
7
+ const script =
8
+ document.currentScript ||
9
+ [...document.querySelectorAll("script[data-slug]")].pop();
10
+
11
+ const slug = script?.dataset?.slug;
12
+
13
+ if (!slug) {
14
+ console.warn("[editScroll] Missing data-slug");
15
+ return;
16
+ }
17
+
18
+ const storageKey = `preview-scroll:${slug}`;
19
+
20
+ let timeout = null;
21
+ let lastY = 0;
22
+
23
+ const saveScroll = () => {
24
+ const y = window.scrollY;
25
+ if (y === lastY) return;
26
+
27
+ lastY = y;
28
+ sessionStorage.setItem(storageKey, String(y));
29
+ };
30
+
31
+ const onScroll = () => {
32
+ clearTimeout(timeout);
33
+ timeout = setTimeout(saveScroll, DEBOUNCE_MS);
34
+ };
35
+
36
+ const restoreScroll = () => {
37
+ const raw = sessionStorage.getItem(storageKey);
38
+ if (!raw) return;
39
+
40
+ const y = Number(raw);
41
+ if (Number.isNaN(y)) return;
42
+
43
+ requestAnimationFrame(() => {
44
+ window.scrollTo(0, y);
45
+
46
+ setTimeout(() => {
47
+ window.scrollTo(0, y);
48
+ }, RESTORE_DELAY);
49
+ });
50
+ };
51
+
52
+ window.addEventListener("scroll", onScroll, { passive: true });
53
+ window.addEventListener("beforeunload", saveScroll);
54
+ window.addEventListener("load", restoreScroll);
55
+ })();
@@ -0,0 +1,24 @@
1
+ import jwt from "jsonwebtoken";
2
+ import { HttpError } from "./errors.js";
3
+
4
+ const resolveDraftUuidFromToken = (editorToken) => {
5
+ if (!editorToken) return undefined;
6
+
7
+ try {
8
+ const payload = jwt.verify(editorToken, import.meta.env.CMS_EDITOR_JWT_SECRET);
9
+
10
+ if (payload.type !== "editor") {
11
+ throw new HttpError(401, "Invalid token type");
12
+ }
13
+
14
+ if (!payload.draftUuid) {
15
+ throw new HttpError(401, "Draft UUID missing in token");
16
+ }
17
+
18
+ return payload.draftUuid;
19
+ } catch {
20
+ throw new HttpError(401, "Invalid or expired editor token");
21
+ }
22
+ };
23
+
24
+ export { resolveDraftUuidFromToken };
@@ -0,0 +1,10 @@
1
+ class HttpError extends Error {
2
+ constructor(status, message) {
3
+ super(message);
4
+ this.name = "HttpError";
5
+ this.status = status;
6
+ this.statusCode = status;
7
+ }
8
+ }
9
+
10
+ export { HttpError };
@@ -0,0 +1,51 @@
1
+ import { resolveDraftUuidFromToken } from "./editorToken.js";
2
+ import { buildCmsPageUrl } from "./urlResolver.js";
3
+ import { buildCmsHeaders } from "./headers.js";
4
+ import { HttpError } from "./errors.js";
5
+
6
+ const fetchPage = async (context) => {
7
+ let { slug = "" } = context.params;
8
+ const request = context.request;
9
+ const url = new URL(request.url);
10
+ const isCMSPreviewRoute = url.pathname.startsWith("/preview_");
11
+
12
+ if (isCMSPreviewRoute) {
13
+ slug = slug.replace(/^preview_/, "");
14
+ }
15
+
16
+ const editorToken = url.searchParams.get("editorToken");
17
+ const draftUuid = resolveDraftUuidFromToken(editorToken);
18
+ const isCMSEditRoute = editorToken && draftUuid;
19
+
20
+ const cmsUrl = buildCmsPageUrl({ slug, draftUuid });
21
+
22
+ const response = await fetch(cmsUrl, {
23
+ method: "GET",
24
+ headers: buildCmsHeaders(request),
25
+ });
26
+
27
+ if (response.status === 404) {
28
+ throw new HttpError(404, "Not Found");
29
+ }
30
+
31
+ if (!response.ok) {
32
+ throw new HttpError(response.status, "CMS Error");
33
+ }
34
+
35
+ const data = await response.json();
36
+
37
+ context.locals.content = data?.content ?? [];
38
+ context.locals.navigation = data?.navigation ?? {};
39
+ context.locals.page = data?.page ?? {};
40
+ context.locals.seo = data?.seo ?? {};
41
+ context.locals._cmsRaw = data;
42
+
43
+ if (isCMSPreviewRoute) {
44
+ context.locals._preview = true;
45
+ }
46
+ if (isCMSEditRoute) {
47
+ context.locals._edit = true;
48
+ }
49
+ };
50
+
51
+ export { fetchPage };
@@ -0,0 +1,9 @@
1
+ const buildCmsHeaders = (request) => ({
2
+ "accept-language": request.headers.get("accept-language") ?? "",
3
+ "cms-tenant": import.meta.env.CMS_TENANT,
4
+ "cms-token": import.meta.env.CMS_TOKEN,
5
+ "user-agent": request.headers.get("user-agent") ?? "",
6
+ referer: request.headers.get("referer") ?? "",
7
+ });
8
+
9
+ export { buildCmsHeaders };
@@ -0,0 +1,27 @@
1
+ const EVENTS = {
2
+ BLOCK_ADD: "BLOCK_ADD",
3
+ BLOCK_DELETE: "BLOCK_DELETE",
4
+ BLOCK_EDIT: "BLOCK_EDIT",
5
+ BLOCK_SHOW: "BLOCK_SHOW",
6
+ BLOCK_HIDE: "BLOCK_HIDE",
7
+ SLUG_CHANGED: "SLUG_CHANGED",
8
+ SLUG_ON_LOAD: "SLUG_ON_LOAD",
9
+ IFRAME_DOM_LOADED: "IFRAME_DOM_LOADED",
10
+ CURRENT_BLOCK_EDIT: "CURRENT_BLOCK_EDIT",
11
+ SCROLL_TO_BLOCK: "SCROLL_TO_BLOCK",
12
+ };
13
+
14
+ const notifyCMS = (type, payload = {}) => {
15
+ if (!type) return;
16
+ if (window.parent === window) return;
17
+
18
+ window.parent.postMessage(
19
+ {
20
+ type,
21
+ payload,
22
+ },
23
+ "*",
24
+ );
25
+ };
26
+
27
+ export { EVENTS, notifyCMS };
@@ -0,0 +1,71 @@
1
+ import { EVENTS, notifyCMS } from "./notify.js";
2
+
3
+ (function () {
4
+ if (window.parent === window) return;
5
+ if (window.__PREVIEW_BRIDGE_INITIALIZED__) return;
6
+
7
+ window.__PREVIEW_BRIDGE_INITIALIZED__ = true;
8
+
9
+ const isModifiedClick = (event) => event.metaKey || event.ctrlKey || event.shiftKey;
10
+
11
+ const handleClick = (event) => {
12
+ const link = event.target.closest("a[href]");
13
+
14
+ if (!link) return;
15
+ if (link.target === "_blank") return;
16
+ if (isModifiedClick(event)) return;
17
+
18
+ const url = new URL(link.href, window.location.origin);
19
+ if (url.origin !== window.location.origin) return;
20
+
21
+ event.preventDefault();
22
+ event.stopPropagation();
23
+
24
+ notifyCMS(EVENTS.SLUG_CHANGED, {
25
+ slug: url.pathname,
26
+ });
27
+ };
28
+
29
+ document.addEventListener("click", handleClick, true);
30
+
31
+ notifyCMS(EVENTS.SLUG_ON_LOAD, {
32
+ slug: window.location.pathname,
33
+ });
34
+
35
+ window.addEventListener("load", () => {
36
+ notifyCMS(EVENTS.IFRAME_DOM_LOADED);
37
+ });
38
+
39
+ const scrollToId = (id) => {
40
+ const selector = `[data-cms-block-id="${id}"]`;
41
+
42
+ let tries = 0;
43
+ const max = 10;
44
+
45
+ const interval = setInterval(() => {
46
+ const el = document.querySelector(selector);
47
+
48
+ if (el) {
49
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
50
+ clearInterval(interval);
51
+ }
52
+
53
+ if (++tries >= max) {
54
+ clearInterval(interval);
55
+ }
56
+ }, 100);
57
+ };
58
+
59
+ window.addEventListener("message", (event) => {
60
+ const { type, payload } = event.data || {};
61
+
62
+ if (!type) return;
63
+
64
+ if (type === EVENTS.SCROLL_TO_BLOCK) {
65
+ const { draftBlockUuid } = payload || {};
66
+ if (!draftBlockUuid) return;
67
+
68
+ scrollToId(draftBlockUuid);
69
+ }
70
+ });
71
+ })();
@@ -0,0 +1,13 @@
1
+ const buildCmsPageUrl = ({ slug, draftUuid }) => {
2
+ const base = import.meta.env.CMS_URL;
3
+
4
+ if (draftUuid) {
5
+ return `${base}/api/public/pages/preview/${draftUuid}`;
6
+ }
7
+
8
+ const normalizedSlug = slug.startsWith("/") ? slug : `/${slug}`;
9
+
10
+ return `${base}/api/public/pages${normalizedSlug}`;
11
+ };
12
+
13
+ export { buildCmsPageUrl };
@@ -1,26 +0,0 @@
1
- ---
2
- const editToken = Astro.url.searchParams.get("edit_token") || null;
3
- const isPreviewMode = editToken !== null && editToken !== "";
4
-
5
- const { class: classes, style, ...rest } = Astro.props;
6
- ---
7
-
8
- {
9
- isPreviewMode ? (
10
- <div class={classes} data-edit-block {style} {...rest}>
11
- <div data-edit-block-menu>
12
- <div data-edit-block-item data-edit-block-item-edit>
13
- Bearbeiten...
14
- </div>
15
- <div data-edit-block-item data-edit-block-item-delete>
16
- Löschen...
17
- </div>
18
- </div>
19
- <slot />
20
- </div>
21
- ) : (
22
- <div class={classes} {style}>
23
- <slot />
24
- </div>
25
- )
26
- }
@@ -1,47 +0,0 @@
1
- ---
2
- import { fetchPageData } from "../data/pageData";
3
-
4
- const { locale, slug } = Astro.params;
5
- const editToken = Astro.url.searchParams.get("edit_token") || null;
6
- const isPreviewMode = editToken !== null && editToken !== "";
7
-
8
- await fetchPageData({ editToken, locals: Astro.locals, locale, slug });
9
-
10
- const CMS_URL = import.meta.env.PUBLIC_CMS_URL?.replace(/\/+$/, "");
11
-
12
- const styleURL = `${CMS_URL}/api/static/styles/drag-drop-blocks.css`;
13
- const scriptDragHandlerURL = `${CMS_URL}/api/static/scripts/iframeDragHandler.js`;
14
- const scriptDragDropUtilsURL = `${CMS_URL}/api/static/scripts/dragDropUtils.js`;
15
-
16
- const previewScript = `
17
- import IframeDragHandler from "${scriptDragHandlerURL}";
18
- import {
19
- disableLinks,
20
- enableEditBlocks,
21
- insertDropZones,
22
- } from "${scriptDragDropUtilsURL}";
23
-
24
- new IframeDragHandler();
25
- insertDropZones();
26
- disableLinks();
27
- enableEditBlocks();
28
-
29
- window.CMS_URL = "${CMS_URL}";
30
- `;
31
- ---
32
-
33
- <slot />
34
-
35
- {
36
- isPreviewMode && (
37
- <>
38
- <link
39
- href="https://fonts.googleapis.com/css2?family=Ancizar+Sans:ital,wght@0,100..1000;1,100..1000&display=swap"
40
- rel="stylesheet"
41
- />
42
- <link rel="stylesheet" href={styleURL} />
43
- </>
44
- )
45
- }
46
-
47
- {isPreviewMode && <script type="module" set:html={previewScript} />}
package/src/data/index.js DELETED
@@ -1 +0,0 @@
1
- export * from "./pageData";
@@ -1,46 +0,0 @@
1
- const fetchPageData = async ({ editToken, locals, locale, slug }) => {
2
- const CMS_URL = import.meta.env.PUBLIC_CMS_URL?.replace(/\/+$/, ""); // Remove trailing slashes
3
- if (!CMS_URL) throw new Error("PUBLIC_CMS_URL ist nicht gesetzt.");
4
-
5
- const API_URL = CMS_URL + "/api/public";
6
-
7
- const key = `__pageData__${locale}__${slug}`;
8
-
9
- if (!locals[key]) {
10
- let url = `${API_URL}/${locale}/pages/${slug}`;
11
-
12
- if (editToken) {
13
- url += `?edit_token=${editToken}`;
14
- }
15
- const res = await fetch(url);
16
-
17
- if (!res.ok) {
18
- throw new Error(
19
- `Fehler beim Abrufen der Seite: ${res.status} ${res.statusText}`
20
- );
21
- }
22
-
23
- locals[key] = await res.json();
24
- }
25
-
26
- locals.__currentPageKey = key;
27
- return locals[key];
28
- };
29
-
30
- const getPageData = (locals) => {
31
- const key = locals?.__currentPageKey;
32
-
33
- if (!key) {
34
- throw new Error("locals fehlt");
35
- }
36
-
37
- if (!key || !locals[key]) {
38
- throw new Error(
39
- "getPageData() wurde aufgerufen, bevor fetchPageData() ausgeführt wurde."
40
- );
41
- }
42
-
43
- return locals[key];
44
- };
45
-
46
- export { fetchPageData, getPageData };
package/src/index.js DELETED
@@ -1,3 +0,0 @@
1
- export { default as Block } from "./components/Block.astro";
2
- export { default as PageRenderer } from "./components/PageRenderer.astro";
3
- export * from "./data";