@setzkasten-cms/ui 0.4.2

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.
package/dist/index.js ADDED
@@ -0,0 +1,2936 @@
1
+ // src/providers/setzkasten-provider.tsx
2
+ import { createContext, useContext, useMemo } from "react";
3
+ import { createEventBus } from "@setzkasten-cms/core";
4
+ import { jsx } from "react/jsx-runtime";
5
+ var Context = createContext(null);
6
+ function SetzKastenProvider({
7
+ config,
8
+ repository,
9
+ auth,
10
+ assets,
11
+ children
12
+ }) {
13
+ const value = useMemo(
14
+ () => ({
15
+ config,
16
+ repository,
17
+ auth,
18
+ assets,
19
+ eventBus: createEventBus()
20
+ }),
21
+ [config, repository, auth, assets]
22
+ );
23
+ return /* @__PURE__ */ jsx(Context, { value, children });
24
+ }
25
+ function useSetzKasten() {
26
+ const context = useContext(Context);
27
+ if (!context) {
28
+ throw new Error("useSetzKasten must be used within a <SetzKastenProvider>");
29
+ }
30
+ return context;
31
+ }
32
+ function useRepository() {
33
+ return useSetzKasten().repository;
34
+ }
35
+ function useAuth() {
36
+ return useSetzKasten().auth;
37
+ }
38
+ function useAssets() {
39
+ return useSetzKasten().assets;
40
+ }
41
+ function useEventBus() {
42
+ return useSetzKasten().eventBus;
43
+ }
44
+ function useConfig() {
45
+ return useSetzKasten().config;
46
+ }
47
+
48
+ // src/stores/form-store.ts
49
+ import { produce, enableMapSet } from "immer";
50
+ import { createStore } from "zustand/vanilla";
51
+ import { createCommandHistory } from "@setzkasten-cms/core";
52
+ enableMapSet();
53
+ function getIn(obj, path) {
54
+ let current = obj;
55
+ for (const key of path) {
56
+ if (current === null || current === void 0) return void 0;
57
+ if (typeof current === "object") {
58
+ current = current[key];
59
+ } else {
60
+ return void 0;
61
+ }
62
+ }
63
+ return current;
64
+ }
65
+ function setIn(obj, path, value) {
66
+ if (path.length === 0) return;
67
+ let current = obj;
68
+ for (let i = 0; i < path.length - 1; i++) {
69
+ const key = path[i];
70
+ const next = current[key];
71
+ if (next === null || next === void 0 || typeof next !== "object") {
72
+ const nextKey = path[i + 1];
73
+ current[key] = typeof nextKey === "number" ? [] : {};
74
+ }
75
+ current = current[key];
76
+ }
77
+ const lastKey = path[path.length - 1];
78
+ current[lastKey] = value;
79
+ }
80
+ function pathToKey(path) {
81
+ return path.join(".");
82
+ }
83
+ function createFormStore() {
84
+ const history = createCommandHistory();
85
+ let snapshotTimer = null;
86
+ let pendingSnapshot = null;
87
+ let isRestoring = false;
88
+ function flushSnapshot(store2) {
89
+ if (!pendingSnapshot) return;
90
+ const before = pendingSnapshot;
91
+ const after = structuredClone(store2.getState().values);
92
+ pendingSnapshot = null;
93
+ if (JSON.stringify(before) === JSON.stringify(after)) return;
94
+ const command = {
95
+ execute() {
96
+ isRestoring = true;
97
+ store2.setState({ values: structuredClone(after) });
98
+ isRestoring = false;
99
+ },
100
+ undo() {
101
+ isRestoring = true;
102
+ store2.setState({ values: structuredClone(before) });
103
+ isRestoring = false;
104
+ },
105
+ describe() {
106
+ return "Field change";
107
+ }
108
+ };
109
+ history.execute(command);
110
+ }
111
+ const store = createStore((set, get) => ({
112
+ // State
113
+ schema: null,
114
+ values: {},
115
+ initialValues: {},
116
+ errors: {},
117
+ touched: /* @__PURE__ */ new Set(),
118
+ status: "idle",
119
+ // Actions
120
+ init(schema, values, draftValues) {
121
+ history.clear();
122
+ pendingSnapshot = null;
123
+ if (snapshotTimer) clearTimeout(snapshotTimer);
124
+ set({
125
+ schema,
126
+ values: structuredClone(draftValues ?? values),
127
+ initialValues: structuredClone(values),
128
+ errors: {},
129
+ touched: /* @__PURE__ */ new Set(),
130
+ status: "idle",
131
+ errorMessage: void 0
132
+ });
133
+ },
134
+ setFieldValue(path, value) {
135
+ if (!isRestoring) {
136
+ if (!pendingSnapshot) {
137
+ pendingSnapshot = structuredClone(get().values);
138
+ }
139
+ if (snapshotTimer) clearTimeout(snapshotTimer);
140
+ snapshotTimer = setTimeout(() => flushSnapshot(store), 500);
141
+ }
142
+ set(
143
+ produce((state) => {
144
+ setIn(state.values, path, value);
145
+ })
146
+ );
147
+ },
148
+ getFieldValue(path) {
149
+ return getIn(get().values, path);
150
+ },
151
+ touchField(path) {
152
+ set(
153
+ produce((state) => {
154
+ state.touched.add(pathToKey(path));
155
+ })
156
+ );
157
+ },
158
+ isFieldDirty(path) {
159
+ const current = getIn(get().values, path);
160
+ const initial = getIn(get().initialValues, path);
161
+ return JSON.stringify(current) !== JSON.stringify(initial);
162
+ },
163
+ isDirty() {
164
+ const { values, initialValues } = get();
165
+ return JSON.stringify(values) !== JSON.stringify(initialValues);
166
+ },
167
+ setFieldErrors(path, errors) {
168
+ set(
169
+ produce((state) => {
170
+ const key = pathToKey(path);
171
+ if (errors.length > 0) {
172
+ state.errors[key] = errors;
173
+ } else {
174
+ delete state.errors[key];
175
+ }
176
+ })
177
+ );
178
+ },
179
+ clearErrors() {
180
+ set({ errors: {} });
181
+ },
182
+ setStatus(status, errorMessage) {
183
+ set({ status, errorMessage });
184
+ },
185
+ reset() {
186
+ const { initialValues } = get();
187
+ set({
188
+ values: structuredClone(initialValues),
189
+ errors: {},
190
+ touched: /* @__PURE__ */ new Set(),
191
+ status: "idle",
192
+ errorMessage: void 0
193
+ });
194
+ },
195
+ undo() {
196
+ flushSnapshot(store);
197
+ history.undo();
198
+ },
199
+ redo() {
200
+ history.redo();
201
+ },
202
+ canUndo() {
203
+ return history.canUndo() || pendingSnapshot !== null;
204
+ },
205
+ canRedo() {
206
+ return history.canRedo();
207
+ },
208
+ resetToInitial() {
209
+ const { initialValues } = get();
210
+ history.clear();
211
+ pendingSnapshot = null;
212
+ if (snapshotTimer) clearTimeout(snapshotTimer);
213
+ isRestoring = true;
214
+ set({
215
+ values: structuredClone(initialValues),
216
+ errors: {},
217
+ touched: /* @__PURE__ */ new Set(),
218
+ status: "idle",
219
+ errorMessage: void 0
220
+ });
221
+ isRestoring = false;
222
+ }
223
+ }));
224
+ return store;
225
+ }
226
+
227
+ // src/stores/app-store.ts
228
+ import { createStore as createStore2 } from "zustand/vanilla";
229
+ var MAX_RECENT = 10;
230
+ function createAppStore() {
231
+ return createStore2((set, get) => ({
232
+ currentCollection: null,
233
+ currentSlug: null,
234
+ sidebarOpen: true,
235
+ theme: "system",
236
+ recentEntries: [],
237
+ navigate(collection, slug) {
238
+ set({ currentCollection: collection, currentSlug: slug });
239
+ get().addRecentEntry({ collection, slug, label: `${collection}/${slug}` });
240
+ },
241
+ toggleSidebar() {
242
+ set((state) => ({ sidebarOpen: !state.sidebarOpen }));
243
+ },
244
+ setTheme(theme) {
245
+ set({ theme });
246
+ },
247
+ addRecentEntry(entry) {
248
+ set((state) => {
249
+ const filtered = state.recentEntries.filter(
250
+ (e) => !(e.collection === entry.collection && e.slug === entry.slug)
251
+ );
252
+ return {
253
+ recentEntries: [{ ...entry, timestamp: Date.now() }, ...filtered].slice(0, MAX_RECENT)
254
+ };
255
+ });
256
+ }
257
+ }));
258
+ }
259
+
260
+ // src/fields/field-renderer.tsx
261
+ import { memo as memo10 } from "react";
262
+
263
+ // src/fields/text-field-renderer.tsx
264
+ import { memo, useCallback as useCallback2, useEffect, useRef as useRef2, useState } from "react";
265
+
266
+ // src/hooks/use-field.ts
267
+ import { useCallback, useMemo as useMemo2 } from "react";
268
+ import { useStore } from "zustand";
269
+ var EMPTY_ERRORS = [];
270
+ function getIn2(obj, path) {
271
+ let current = obj;
272
+ for (const key of path) {
273
+ if (current === null || current === void 0) return void 0;
274
+ if (typeof current === "object") {
275
+ current = current[key];
276
+ } else {
277
+ return void 0;
278
+ }
279
+ }
280
+ return current;
281
+ }
282
+ function useField(store, path) {
283
+ const pathKey = path.join(".");
284
+ const value = useStore(store, useCallback(
285
+ (s) => getIn2(s.values, path),
286
+ [pathKey]
287
+ ));
288
+ const errors = useStore(store, useCallback(
289
+ (s) => {
290
+ const fieldErrors = s.errors[pathKey];
291
+ return fieldErrors && fieldErrors.length > 0 ? fieldErrors : EMPTY_ERRORS;
292
+ },
293
+ [pathKey]
294
+ ));
295
+ const isTouched = useStore(store, useCallback(
296
+ (s) => s.touched.has(pathKey),
297
+ [pathKey]
298
+ ));
299
+ const initialValues = useStore(store, useCallback(
300
+ (s) => getIn2(s.initialValues, path),
301
+ [pathKey]
302
+ ));
303
+ const isDirty = JSON.stringify(value) !== JSON.stringify(initialValues);
304
+ const setValue = useCallback(
305
+ (newValue) => {
306
+ store.getState().setFieldValue(path, newValue);
307
+ },
308
+ [store, pathKey]
309
+ );
310
+ const touch = useCallback(() => {
311
+ store.getState().touchField(path);
312
+ }, [store, pathKey]);
313
+ return useMemo2(
314
+ () => ({
315
+ value,
316
+ errors,
317
+ isTouched,
318
+ isDirty,
319
+ setValue,
320
+ touch,
321
+ path
322
+ }),
323
+ [value, errors, isTouched, isDirty, setValue, touch, path]
324
+ );
325
+ }
326
+
327
+ // src/fields/text-field-renderer.tsx
328
+ import { useEditor, EditorContent } from "@tiptap/react";
329
+ import StarterKit from "@tiptap/starter-kit";
330
+ import Placeholder from "@tiptap/extension-placeholder";
331
+ import Link from "@tiptap/extension-link";
332
+ import TextAlign from "@tiptap/extension-text-align";
333
+ import {
334
+ Type,
335
+ AlignLeft,
336
+ AlignCenter,
337
+ AlignRight,
338
+ AlignJustify
339
+ } from "lucide-react";
340
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
341
+ var STORAGE_KEY = "sk-formatting-fields";
342
+ function getFormattingState(fieldPath) {
343
+ try {
344
+ const stored = localStorage.getItem(STORAGE_KEY);
345
+ if (!stored) return false;
346
+ const map = JSON.parse(stored);
347
+ return map[fieldPath] ?? false;
348
+ } catch {
349
+ return false;
350
+ }
351
+ }
352
+ function setFormattingState(fieldPath, enabled) {
353
+ try {
354
+ const stored = localStorage.getItem(STORAGE_KEY);
355
+ const map = stored ? JSON.parse(stored) : {};
356
+ if (enabled) {
357
+ map[fieldPath] = true;
358
+ } else {
359
+ delete map[fieldPath];
360
+ }
361
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(map));
362
+ } catch {
363
+ }
364
+ }
365
+ var InlineToolbarButton = memo(function InlineToolbarButton2({
366
+ label,
367
+ isActive,
368
+ onClick,
369
+ title,
370
+ children
371
+ }) {
372
+ return /* @__PURE__ */ jsx2(
373
+ "button",
374
+ {
375
+ type: "button",
376
+ className: `sk-rte__btn${isActive ? " sk-rte__btn--active" : ""}`,
377
+ onClick,
378
+ title: title ?? label,
379
+ children: children ?? label
380
+ }
381
+ );
382
+ });
383
+ var FormattedTextInput = memo(function FormattedTextInput2({
384
+ field,
385
+ path,
386
+ store
387
+ }) {
388
+ const textField = field;
389
+ const { value, errors, isDirty, setValue, touch } = useField(store, path);
390
+ const isUpdatingRef = useRef2(false);
391
+ const editor = useEditor({
392
+ extensions: [
393
+ StarterKit.configure({
394
+ heading: false,
395
+ horizontalRule: false,
396
+ codeBlock: false
397
+ }),
398
+ Placeholder.configure({ placeholder: textField.placeholder ?? "Schreibe hier..." }),
399
+ Link.configure({
400
+ openOnClick: false,
401
+ HTMLAttributes: { rel: "noopener noreferrer", target: "_blank" }
402
+ }),
403
+ TextAlign.configure({ types: ["paragraph"] })
404
+ ],
405
+ content: value || "",
406
+ onUpdate: ({ editor: ed }) => {
407
+ isUpdatingRef.current = true;
408
+ setValue(ed.getHTML());
409
+ isUpdatingRef.current = false;
410
+ },
411
+ onBlur: () => touch()
412
+ });
413
+ useEffect(() => {
414
+ if (!editor || isUpdatingRef.current) return;
415
+ const current = editor.getHTML();
416
+ if (current !== value && typeof value === "string") {
417
+ editor.commands.setContent(value, { emitUpdate: false });
418
+ }
419
+ }, [editor, value]);
420
+ const handleLink = useCallback2(() => {
421
+ if (!editor) return;
422
+ if (editor.isActive("link")) {
423
+ editor.chain().focus().unsetLink().run();
424
+ return;
425
+ }
426
+ const url = window.prompt("URL eingeben:");
427
+ if (!url) return;
428
+ editor.chain().focus().setLink({ href: url }).run();
429
+ }, [editor]);
430
+ if (!editor) return null;
431
+ return /* @__PURE__ */ jsxs("div", { className: "sk-rte sk-rte--inline", children: [
432
+ /* @__PURE__ */ jsxs("div", { className: "sk-rte__toolbar", children: [
433
+ /* @__PURE__ */ jsx2(
434
+ InlineToolbarButton,
435
+ {
436
+ label: "B",
437
+ isActive: editor.isActive("bold"),
438
+ onClick: () => {
439
+ editor.chain().focus().toggleBold().run();
440
+ }
441
+ }
442
+ ),
443
+ /* @__PURE__ */ jsx2(
444
+ InlineToolbarButton,
445
+ {
446
+ label: "I",
447
+ isActive: editor.isActive("italic"),
448
+ onClick: () => {
449
+ editor.chain().focus().toggleItalic().run();
450
+ }
451
+ }
452
+ ),
453
+ /* @__PURE__ */ jsx2(
454
+ InlineToolbarButton,
455
+ {
456
+ label: "S",
457
+ isActive: editor.isActive("strike"),
458
+ onClick: () => {
459
+ editor.chain().focus().toggleStrike().run();
460
+ }
461
+ }
462
+ ),
463
+ /* @__PURE__ */ jsx2(
464
+ InlineToolbarButton,
465
+ {
466
+ label: "\u{1F517}",
467
+ isActive: editor.isActive("link"),
468
+ onClick: handleLink,
469
+ title: "Link"
470
+ }
471
+ ),
472
+ /* @__PURE__ */ jsx2("span", { className: "sk-rte__sep" }),
473
+ /* @__PURE__ */ jsx2(
474
+ InlineToolbarButton,
475
+ {
476
+ label: "\u2022",
477
+ isActive: editor.isActive("bulletList"),
478
+ onClick: () => {
479
+ editor.chain().focus().toggleBulletList().run();
480
+ }
481
+ }
482
+ ),
483
+ /* @__PURE__ */ jsx2(
484
+ InlineToolbarButton,
485
+ {
486
+ label: "1.",
487
+ isActive: editor.isActive("orderedList"),
488
+ onClick: () => {
489
+ editor.chain().focus().toggleOrderedList().run();
490
+ }
491
+ }
492
+ ),
493
+ /* @__PURE__ */ jsx2(
494
+ InlineToolbarButton,
495
+ {
496
+ label: "\u201C",
497
+ isActive: editor.isActive("blockquote"),
498
+ onClick: () => {
499
+ editor.chain().focus().toggleBlockquote().run();
500
+ }
501
+ }
502
+ ),
503
+ /* @__PURE__ */ jsx2("span", { className: "sk-rte__sep" }),
504
+ /* @__PURE__ */ jsx2(
505
+ InlineToolbarButton,
506
+ {
507
+ isActive: editor.isActive({ textAlign: "left" }),
508
+ onClick: () => {
509
+ editor.chain().focus().setTextAlign("left").run();
510
+ },
511
+ title: "Linksb\xFCndig",
512
+ children: /* @__PURE__ */ jsx2(AlignLeft, { size: 14 })
513
+ }
514
+ ),
515
+ /* @__PURE__ */ jsx2(
516
+ InlineToolbarButton,
517
+ {
518
+ isActive: editor.isActive({ textAlign: "center" }),
519
+ onClick: () => {
520
+ editor.chain().focus().setTextAlign("center").run();
521
+ },
522
+ title: "Zentriert",
523
+ children: /* @__PURE__ */ jsx2(AlignCenter, { size: 14 })
524
+ }
525
+ ),
526
+ /* @__PURE__ */ jsx2(
527
+ InlineToolbarButton,
528
+ {
529
+ isActive: editor.isActive({ textAlign: "right" }),
530
+ onClick: () => {
531
+ editor.chain().focus().setTextAlign("right").run();
532
+ },
533
+ title: "Rechtsb\xFCndig",
534
+ children: /* @__PURE__ */ jsx2(AlignRight, { size: 14 })
535
+ }
536
+ ),
537
+ /* @__PURE__ */ jsx2(
538
+ InlineToolbarButton,
539
+ {
540
+ isActive: editor.isActive({ textAlign: "justify" }),
541
+ onClick: () => {
542
+ editor.chain().focus().setTextAlign("justify").run();
543
+ },
544
+ title: "Blocksatz",
545
+ children: /* @__PURE__ */ jsx2(AlignJustify, { size: 14 })
546
+ }
547
+ )
548
+ ] }),
549
+ /* @__PURE__ */ jsx2(EditorContent, { editor, className: "sk-rte__content sk-rte__content--inline" })
550
+ ] });
551
+ });
552
+ var PlainTextInput = memo(function PlainTextInput2({
553
+ field,
554
+ path,
555
+ store
556
+ }) {
557
+ const textField = field;
558
+ const { value, errors, setValue, touch } = useField(store, path);
559
+ const handleChange = (e) => {
560
+ setValue(e.target.value);
561
+ };
562
+ const inputProps = {
563
+ value: value ?? "",
564
+ onChange: handleChange,
565
+ onBlur: touch,
566
+ placeholder: textField.placeholder,
567
+ maxLength: textField.maxLength,
568
+ "aria-label": textField.label,
569
+ "aria-invalid": errors.length > 0
570
+ };
571
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
572
+ textField.multiline ? /* @__PURE__ */ jsx2("textarea", { className: "sk-field__input sk-field__textarea", rows: 4, ...inputProps }) : /* @__PURE__ */ jsx2("input", { className: "sk-field__input", type: "text", ...inputProps }),
573
+ textField.maxLength && /* @__PURE__ */ jsxs("span", { className: "sk-field__counter", children: [
574
+ (value ?? "").length,
575
+ "/",
576
+ textField.maxLength
577
+ ] })
578
+ ] });
579
+ });
580
+ var TextFieldRenderer = memo(function TextFieldRenderer2(props) {
581
+ const textField = props.field;
582
+ const { errors, isDirty } = useField(props.store, props.path);
583
+ const fieldPath = props.path.join(".");
584
+ const [formatting, setFormatting] = useState(
585
+ () => textField.formatting || getFormattingState(fieldPath)
586
+ );
587
+ const toggleFormatting = useCallback2(() => {
588
+ setFormatting((prev) => {
589
+ const next = !prev;
590
+ setFormattingState(fieldPath, next);
591
+ return next;
592
+ });
593
+ }, [fieldPath]);
594
+ return /* @__PURE__ */ jsxs(
595
+ "div",
596
+ {
597
+ className: `sk-field sk-field--text${formatting ? " sk-field--formatted" : ""}`,
598
+ "data-dirty": isDirty || void 0,
599
+ children: [
600
+ /* @__PURE__ */ jsxs("div", { className: "sk-field__header", children: [
601
+ /* @__PURE__ */ jsxs("label", { className: "sk-field__label", children: [
602
+ textField.label,
603
+ textField.required && /* @__PURE__ */ jsx2("span", { className: "sk-field__required", children: "*" })
604
+ ] }),
605
+ /* @__PURE__ */ jsx2(
606
+ "button",
607
+ {
608
+ type: "button",
609
+ className: `sk-field__format-toggle${formatting ? " sk-field__format-toggle--active" : ""}`,
610
+ onClick: toggleFormatting,
611
+ title: formatting ? "Formatierung ausschalten" : "Formatierung einschalten",
612
+ children: /* @__PURE__ */ jsx2(Type, { size: 14 })
613
+ }
614
+ )
615
+ ] }),
616
+ textField.description && /* @__PURE__ */ jsx2("p", { className: "sk-field__description", children: textField.description }),
617
+ formatting ? /* @__PURE__ */ jsx2(FormattedTextInput, { ...props }) : /* @__PURE__ */ jsx2(PlainTextInput, { ...props }),
618
+ errors.length > 0 && /* @__PURE__ */ jsx2("div", { className: "sk-field__errors", children: errors.map((err3, i) => /* @__PURE__ */ jsx2("p", { className: "sk-field__error", children: err3 }, i)) })
619
+ ]
620
+ }
621
+ );
622
+ });
623
+
624
+ // src/fields/number-field-renderer.tsx
625
+ import { memo as memo2 } from "react";
626
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
627
+ var NumberFieldRenderer = memo2(function NumberFieldRenderer2({
628
+ field,
629
+ path,
630
+ store
631
+ }) {
632
+ const numField = field;
633
+ const { value, errors, setValue, touch } = useField(store, path);
634
+ return /* @__PURE__ */ jsxs2("div", { className: "sk-field sk-field--number", children: [
635
+ /* @__PURE__ */ jsx3("label", { className: "sk-field__label", children: numField.label }),
636
+ numField.description && /* @__PURE__ */ jsx3("p", { className: "sk-field__description", children: numField.description }),
637
+ /* @__PURE__ */ jsx3(
638
+ "input",
639
+ {
640
+ className: "sk-field__input",
641
+ type: "number",
642
+ value: value ?? 0,
643
+ onChange: (e) => setValue(Number(e.target.value)),
644
+ onBlur: touch,
645
+ min: numField.min,
646
+ max: numField.max,
647
+ step: numField.step,
648
+ "aria-label": numField.label
649
+ }
650
+ ),
651
+ errors.length > 0 && /* @__PURE__ */ jsx3("div", { className: "sk-field__errors", children: errors.map((err3, i) => /* @__PURE__ */ jsx3("p", { className: "sk-field__error", children: err3 }, i)) })
652
+ ] });
653
+ });
654
+
655
+ // src/fields/boolean-field-renderer.tsx
656
+ import { memo as memo3 } from "react";
657
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
658
+ var BooleanFieldRenderer = memo3(function BooleanFieldRenderer2({
659
+ field,
660
+ path,
661
+ store
662
+ }) {
663
+ const boolField = field;
664
+ const { value, setValue } = useField(store, path);
665
+ return /* @__PURE__ */ jsxs3("div", { className: "sk-field sk-field--boolean", children: [
666
+ /* @__PURE__ */ jsxs3("label", { className: "sk-field__toggle", children: [
667
+ /* @__PURE__ */ jsx4(
668
+ "input",
669
+ {
670
+ type: "checkbox",
671
+ checked: Boolean(value),
672
+ onChange: (e) => setValue(e.target.checked),
673
+ "aria-label": boolField.label
674
+ }
675
+ ),
676
+ /* @__PURE__ */ jsx4("span", { className: "sk-field__toggle-label", children: boolField.label })
677
+ ] }),
678
+ boolField.description && /* @__PURE__ */ jsx4("p", { className: "sk-field__description", children: boolField.description })
679
+ ] });
680
+ });
681
+
682
+ // src/fields/select-field-renderer.tsx
683
+ import { memo as memo4 } from "react";
684
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
685
+ var SelectFieldRenderer = memo4(function SelectFieldRenderer2({
686
+ field,
687
+ path,
688
+ store
689
+ }) {
690
+ const selectField = field;
691
+ const { value, errors, setValue, touch } = useField(store, path);
692
+ return /* @__PURE__ */ jsxs4("div", { className: "sk-field sk-field--select", children: [
693
+ /* @__PURE__ */ jsx5("label", { className: "sk-field__label", children: selectField.label }),
694
+ selectField.description && /* @__PURE__ */ jsx5("p", { className: "sk-field__description", children: selectField.description }),
695
+ /* @__PURE__ */ jsx5(
696
+ "select",
697
+ {
698
+ className: "sk-field__input sk-field__select",
699
+ value: value ?? "",
700
+ onChange: (e) => setValue(e.target.value),
701
+ onBlur: touch,
702
+ "aria-label": selectField.label,
703
+ children: selectField.options.map((opt) => /* @__PURE__ */ jsx5("option", { value: opt.value, children: opt.label }, opt.value))
704
+ }
705
+ ),
706
+ errors.length > 0 && /* @__PURE__ */ jsx5("div", { className: "sk-field__errors", children: errors.map((err3, i) => /* @__PURE__ */ jsx5("p", { className: "sk-field__error", children: err3 }, i)) })
707
+ ] });
708
+ });
709
+
710
+ // src/fields/icon-field-renderer.tsx
711
+ import { memo as memo5, useState as useState2, useMemo as useMemo3, useCallback as useCallback3, useRef as useRef3, useEffect as useEffect2 } from "react";
712
+ import { icons } from "lucide-react";
713
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
714
+ function toKebab(name) {
715
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
716
+ }
717
+ function toPascal(name) {
718
+ return name.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase());
719
+ }
720
+ var ALL_ICONS = Object.entries(icons).map(
721
+ ([pascal, comp]) => [toKebab(pascal), pascal, comp]
722
+ );
723
+ var MAX_VISIBLE = 120;
724
+ var IconFieldRenderer = memo5(function IconFieldRenderer2({
725
+ field,
726
+ path,
727
+ store
728
+ }) {
729
+ const { value, errors, setValue } = useField(store, path);
730
+ const [open, setOpen] = useState2(false);
731
+ const [search, setSearch] = useState2("");
732
+ const searchRef = useRef3(null);
733
+ const panelRef = useRef3(null);
734
+ const currentValue = value ?? "";
735
+ const CurrentIcon = currentValue ? icons[toPascal(currentValue)] ?? null : null;
736
+ const filtered = useMemo3(() => {
737
+ if (!search) return ALL_ICONS.slice(0, MAX_VISIBLE);
738
+ const q = search.toLowerCase();
739
+ return ALL_ICONS.filter(([kebab]) => kebab.includes(q)).slice(0, MAX_VISIBLE);
740
+ }, [search]);
741
+ const selectIcon = useCallback3(
742
+ (kebab) => {
743
+ setValue(kebab);
744
+ setOpen(false);
745
+ setSearch("");
746
+ },
747
+ [setValue]
748
+ );
749
+ useEffect2(() => {
750
+ if (open) searchRef.current?.focus();
751
+ }, [open]);
752
+ useEffect2(() => {
753
+ if (!open) return;
754
+ const handler = (e) => {
755
+ if (panelRef.current && !panelRef.current.contains(e.target)) {
756
+ setOpen(false);
757
+ setSearch("");
758
+ }
759
+ };
760
+ document.addEventListener("mousedown", handler);
761
+ return () => document.removeEventListener("mousedown", handler);
762
+ }, [open]);
763
+ return /* @__PURE__ */ jsxs5("div", { className: "sk-field sk-field--icon", children: [
764
+ /* @__PURE__ */ jsx6("label", { className: "sk-field__label", children: field.label }),
765
+ field.description && /* @__PURE__ */ jsx6("p", { className: "sk-field__description", children: field.description }),
766
+ /* @__PURE__ */ jsx6(
767
+ "button",
768
+ {
769
+ type: "button",
770
+ className: "sk-icon-picker__trigger",
771
+ onClick: () => setOpen(!open),
772
+ children: CurrentIcon ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
773
+ /* @__PURE__ */ jsx6(CurrentIcon, { size: 20 }),
774
+ /* @__PURE__ */ jsx6("span", { children: currentValue })
775
+ ] }) : /* @__PURE__ */ jsx6("span", { className: "sk-icon-picker__placeholder", children: "Icon w\xE4hlen..." })
776
+ }
777
+ ),
778
+ open && /* @__PURE__ */ jsxs5("div", { className: "sk-icon-picker__panel", ref: panelRef, children: [
779
+ /* @__PURE__ */ jsx6(
780
+ "input",
781
+ {
782
+ ref: searchRef,
783
+ type: "text",
784
+ className: "sk-icon-picker__search",
785
+ placeholder: "Icon suchen...",
786
+ value: search,
787
+ onChange: (e) => setSearch(e.target.value)
788
+ }
789
+ ),
790
+ /* @__PURE__ */ jsxs5("div", { className: "sk-icon-picker__grid", children: [
791
+ filtered.map(([kebab, , Icon]) => /* @__PURE__ */ jsx6(
792
+ "button",
793
+ {
794
+ type: "button",
795
+ className: `sk-icon-picker__item${kebab === currentValue ? " sk-icon-picker__item--active" : ""}`,
796
+ onClick: () => selectIcon(kebab),
797
+ title: kebab,
798
+ children: /* @__PURE__ */ jsx6(Icon, { size: 20 })
799
+ },
800
+ kebab
801
+ )),
802
+ filtered.length === 0 && /* @__PURE__ */ jsx6("p", { className: "sk-icon-picker__empty", children: "Kein Icon gefunden" })
803
+ ] })
804
+ ] }),
805
+ errors.length > 0 && /* @__PURE__ */ jsx6("div", { className: "sk-field__errors", children: errors.map((err3, i) => /* @__PURE__ */ jsx6("p", { className: "sk-field__error", children: err3 }, i)) })
806
+ ] });
807
+ });
808
+
809
+ // src/fields/array-field-renderer.tsx
810
+ import { memo as memo6, useCallback as useCallback4 } from "react";
811
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
812
+ var ArrayFieldRenderer = memo6(function ArrayFieldRenderer2({
813
+ field,
814
+ path,
815
+ store
816
+ }) {
817
+ const arrayField = field;
818
+ const { value, setValue } = useField(store, path);
819
+ const items = value ?? [];
820
+ const addItem = useCallback4(() => {
821
+ const defaultValue = arrayField.itemField.defaultValue ?? (arrayField.itemField.type === "object" ? {} : arrayField.itemField.type === "text" ? "" : arrayField.itemField.type === "number" ? 0 : arrayField.itemField.type === "boolean" ? false : null);
822
+ setValue([...items, defaultValue]);
823
+ }, [items, arrayField.itemField, setValue]);
824
+ const removeItem = useCallback4(
825
+ (index) => {
826
+ setValue(items.filter((_, i) => i !== index));
827
+ },
828
+ [items, setValue]
829
+ );
830
+ const moveItem = useCallback4(
831
+ (from, to) => {
832
+ const newItems = [...items];
833
+ const [moved] = newItems.splice(from, 1);
834
+ newItems.splice(to, 0, moved);
835
+ setValue(newItems);
836
+ },
837
+ [items, setValue]
838
+ );
839
+ const canAdd = arrayField.maxItems === void 0 || items.length < arrayField.maxItems;
840
+ const canRemove = arrayField.minItems === void 0 || items.length > arrayField.minItems;
841
+ return /* @__PURE__ */ jsxs6("div", { className: "sk-field sk-field--array", children: [
842
+ /* @__PURE__ */ jsx7("div", { className: "sk-array__header", children: /* @__PURE__ */ jsxs6("label", { className: "sk-field__label", children: [
843
+ arrayField.label,
844
+ /* @__PURE__ */ jsxs6("span", { className: "sk-array__count", children: [
845
+ "(",
846
+ items.length,
847
+ ")"
848
+ ] })
849
+ ] }) }),
850
+ /* @__PURE__ */ jsx7("div", { className: "sk-array__items", children: items.map((_, index) => {
851
+ const itemLabel = arrayField.itemLabel ? arrayField.itemLabel(items[index], index) : `#${index + 1}`;
852
+ return /* @__PURE__ */ jsxs6("div", { className: "sk-array__item", children: [
853
+ /* @__PURE__ */ jsxs6("div", { className: "sk-array__item-header", children: [
854
+ /* @__PURE__ */ jsx7("span", { className: "sk-array__item-label", children: itemLabel }),
855
+ /* @__PURE__ */ jsxs6("div", { className: "sk-array__item-actions", children: [
856
+ index > 0 && /* @__PURE__ */ jsx7("button", { type: "button", className: "sk-button sk-button--sm", onClick: () => moveItem(index, index - 1), title: "Nach oben", children: "\u2191" }),
857
+ index < items.length - 1 && /* @__PURE__ */ jsx7("button", { type: "button", className: "sk-button sk-button--sm", onClick: () => moveItem(index, index + 1), title: "Nach unten", children: "\u2193" }),
858
+ canRemove && /* @__PURE__ */ jsx7("button", { type: "button", className: "sk-button sk-button--sm sk-button--danger", onClick: () => removeItem(index), title: "Entfernen", children: "\xD7" })
859
+ ] })
860
+ ] }),
861
+ /* @__PURE__ */ jsx7(
862
+ FieldRenderer,
863
+ {
864
+ field: arrayField.itemField,
865
+ path: [...path, index],
866
+ store
867
+ }
868
+ )
869
+ ] }, index);
870
+ }) }),
871
+ canAdd && /* @__PURE__ */ jsx7("button", { type: "button", className: "sk-button sk-button--sm sk-array__add", onClick: addItem, children: "+ Hinzuf\xFCgen" })
872
+ ] });
873
+ });
874
+
875
+ // src/fields/object-field-renderer.tsx
876
+ import { memo as memo7, useState as useState3 } from "react";
877
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
878
+ var ObjectFieldRenderer = memo7(function ObjectFieldRenderer2({
879
+ field,
880
+ path,
881
+ store
882
+ }) {
883
+ const objField = field;
884
+ const [collapsed, setCollapsed] = useState3(false);
885
+ return /* @__PURE__ */ jsxs7("div", { className: "sk-field sk-field--object", children: [
886
+ /* @__PURE__ */ jsxs7("div", { className: "sk-field__object-header", children: [
887
+ /* @__PURE__ */ jsx8("label", { className: "sk-field__label", children: objField.label }),
888
+ objField.collapsible && /* @__PURE__ */ jsx8(
889
+ "button",
890
+ {
891
+ type: "button",
892
+ className: "sk-field__object-toggle",
893
+ onClick: () => setCollapsed(!collapsed),
894
+ children: collapsed ? "\u25B8" : "\u25BE"
895
+ }
896
+ )
897
+ ] }),
898
+ !collapsed && /* @__PURE__ */ jsx8("div", { className: "sk-field__object-fields", children: Object.entries(objField.fields).map(([key, childField]) => /* @__PURE__ */ jsx8(
899
+ FieldRenderer,
900
+ {
901
+ field: childField,
902
+ path: [...path, key],
903
+ store
904
+ },
905
+ key
906
+ )) })
907
+ ] });
908
+ });
909
+
910
+ // src/fields/image-field-renderer.tsx
911
+ import { memo as memo8, useCallback as useCallback5, useRef as useRef4, useState as useState4 } from "react";
912
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
913
+ function previewUrl(assets, path) {
914
+ return assets.getPreviewUrl?.(path) ?? assets.getUrl(path);
915
+ }
916
+ async function compressToWebP(file, quality = 0.85) {
917
+ const skipTypes = ["image/svg+xml", "image/gif", "image/webp", "image/avif"];
918
+ if (skipTypes.includes(file.type)) {
919
+ return { bytes: new Uint8Array(await file.arrayBuffer()), filename: file.name, mimeType: file.type };
920
+ }
921
+ const bitmap = await createImageBitmap(file);
922
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
923
+ const ctx = canvas.getContext("2d");
924
+ ctx.drawImage(bitmap, 0, 0);
925
+ bitmap.close();
926
+ const blob = await canvas.convertToBlob({ type: "image/webp", quality });
927
+ const webpName = file.name.replace(/\.(jpe?g|png|bmp|tiff?)$/i, ".webp");
928
+ return { bytes: new Uint8Array(await blob.arrayBuffer()), filename: webpName, mimeType: "image/webp" };
929
+ }
930
+ var ImageFieldRenderer = memo8(function ImageFieldRenderer2({
931
+ field,
932
+ path,
933
+ store
934
+ }) {
935
+ const imgField = field;
936
+ const { value, errors, setValue, touch } = useField(store, path);
937
+ const assets = useAssets();
938
+ const fileInputRef = useRef4(null);
939
+ const [uploading, setUploading] = useState4(false);
940
+ const [dragOver, setDragOver] = useState4(false);
941
+ const [showBrowser, setShowBrowser] = useState4(false);
942
+ const [browserScope, setBrowserScope] = useState4("directory");
943
+ const [existing, setExisting] = useState4([]);
944
+ const [loadingExisting, setLoadingExisting] = useState4(false);
945
+ const imageValue = value;
946
+ const acceptStr = imgField.accept.map((ext) => `.${ext}`).join(",");
947
+ const selectedThumbUrl = imageValue?.path && assets.getPreviewUrl ? assets.getPreviewUrl(imageValue.path) : imageValue?.path || void 0;
948
+ const uploadFile = useCallback5(
949
+ async (file) => {
950
+ setUploading(true);
951
+ try {
952
+ const { bytes, filename, mimeType } = await compressToWebP(file);
953
+ const result = await assets.upload(imgField.directory, filename, bytes, mimeType);
954
+ if (result.ok) {
955
+ const publicUrl = assets.getUrl(result.value.path);
956
+ setValue({ path: publicUrl, alt: imageValue?.alt ?? "" });
957
+ } else {
958
+ const dir = imgField.directory.replace(/^\/+|\/+$/g, "");
959
+ const publicUrl = assets.getUrl(`public/images/${dir}/${filename}`);
960
+ setValue({ path: publicUrl, alt: imageValue?.alt ?? "" });
961
+ }
962
+ } catch {
963
+ const dir = imgField.directory.replace(/^\/+|\/+$/g, "");
964
+ const publicUrl = assets.getUrl(`public/images/${dir}/${file.name}`);
965
+ setValue({ path: publicUrl, alt: imageValue?.alt ?? "" });
966
+ }
967
+ setUploading(false);
968
+ touch();
969
+ },
970
+ [assets, imgField.directory, imageValue?.alt, setValue, touch]
971
+ );
972
+ const loadExisting = useCallback5(async (scope) => {
973
+ setLoadingExisting(true);
974
+ setBrowserScope(scope);
975
+ try {
976
+ const dir = scope === "all" ? "" : imgField.directory;
977
+ const result = await assets.list(dir);
978
+ if (result.ok) {
979
+ setExisting(result.value);
980
+ }
981
+ } catch {
982
+ }
983
+ setLoadingExisting(false);
984
+ }, [assets, imgField.directory]);
985
+ const handleBrowse = useCallback5(() => {
986
+ setShowBrowser(true);
987
+ loadExisting("directory");
988
+ }, [loadExisting]);
989
+ const handleSelectExisting = useCallback5(
990
+ (asset) => {
991
+ const publicUrl = assets.getUrl(asset.path);
992
+ setValue({ path: publicUrl, alt: imageValue?.alt ?? "" });
993
+ setShowBrowser(false);
994
+ },
995
+ [assets, imageValue?.alt, setValue]
996
+ );
997
+ const handleFileSelect = useCallback5(
998
+ async (e) => {
999
+ const file = e.target.files?.[0];
1000
+ if (!file) return;
1001
+ await uploadFile(file);
1002
+ },
1003
+ [uploadFile]
1004
+ );
1005
+ const handleDrop = useCallback5(
1006
+ async (e) => {
1007
+ e.preventDefault();
1008
+ setDragOver(false);
1009
+ const file = e.dataTransfer.files[0];
1010
+ if (file) await uploadFile(file);
1011
+ },
1012
+ [uploadFile]
1013
+ );
1014
+ const handleAltChange = useCallback5(
1015
+ (e) => {
1016
+ setValue({ path: imageValue?.path ?? "", alt: e.target.value });
1017
+ },
1018
+ [imageValue?.path, setValue]
1019
+ );
1020
+ const handleRemove = useCallback5(() => {
1021
+ setValue({ path: "", alt: "" });
1022
+ if (fileInputRef.current) {
1023
+ fileInputRef.current.value = "";
1024
+ }
1025
+ }, [setValue]);
1026
+ return /* @__PURE__ */ jsxs8("div", { className: "sk-field sk-field--image", children: [
1027
+ /* @__PURE__ */ jsxs8("label", { className: "sk-field__label", children: [
1028
+ imgField.label,
1029
+ imgField.required && /* @__PURE__ */ jsx9("span", { className: "sk-field__required", children: "*" })
1030
+ ] }),
1031
+ imgField.description && /* @__PURE__ */ jsx9("p", { className: "sk-field__description", children: imgField.description }),
1032
+ /* @__PURE__ */ jsxs8("div", { className: "sk-field__image-zone", children: [
1033
+ uploading ? /* @__PURE__ */ jsx9("div", { className: "sk-field__image-upload", style: { borderColor: "var(--sk-accent)" }, children: "Wird hochgeladen..." }) : imageValue?.path ? /* @__PURE__ */ jsxs8("div", { className: "sk-field__image-preview", children: [
1034
+ selectedThumbUrl && /* @__PURE__ */ jsx9("img", { src: selectedThumbUrl, alt: imageValue.alt ?? "", className: "sk-field__image-thumb" }),
1035
+ /* @__PURE__ */ jsx9("p", { className: "sk-field__image-path", children: imageValue.path }),
1036
+ /* @__PURE__ */ jsxs8("div", { className: "sk-field__image-actions", children: [
1037
+ /* @__PURE__ */ jsx9("button", { type: "button", className: "sk-button sk-button--sm", onClick: () => fileInputRef.current?.click(), children: "Neu hochladen" }),
1038
+ /* @__PURE__ */ jsx9("button", { type: "button", className: "sk-button sk-button--sm", onClick: handleBrowse, children: "Aus Bibliothek" }),
1039
+ /* @__PURE__ */ jsx9("button", { type: "button", className: "sk-button sk-button--sm sk-button--danger", onClick: handleRemove, children: "Entfernen" })
1040
+ ] })
1041
+ ] }) : /* @__PURE__ */ jsxs8("div", { className: "sk-field__image-empty", children: [
1042
+ /* @__PURE__ */ jsx9(
1043
+ "button",
1044
+ {
1045
+ type: "button",
1046
+ className: `sk-field__image-upload${dragOver ? " sk-field__image-upload--drag" : ""}`,
1047
+ onClick: () => fileInputRef.current?.click(),
1048
+ onDragOver: (e) => {
1049
+ e.preventDefault();
1050
+ setDragOver(true);
1051
+ },
1052
+ onDragLeave: () => setDragOver(false),
1053
+ onDrop: handleDrop,
1054
+ children: "Bild hochladen oder hierher ziehen"
1055
+ }
1056
+ ),
1057
+ /* @__PURE__ */ jsx9("button", { type: "button", className: "sk-button sk-button--sm sk-field__image-browse", onClick: handleBrowse, children: "Aus Bibliothek w\xE4hlen" })
1058
+ ] }),
1059
+ /* @__PURE__ */ jsx9(
1060
+ "input",
1061
+ {
1062
+ ref: fileInputRef,
1063
+ type: "file",
1064
+ accept: acceptStr,
1065
+ onChange: handleFileSelect,
1066
+ style: { display: "none" }
1067
+ }
1068
+ )
1069
+ ] }),
1070
+ showBrowser && /* @__PURE__ */ jsxs8("div", { className: "sk-field__image-browser", children: [
1071
+ /* @__PURE__ */ jsxs8("div", { className: "sk-field__image-browser-header", children: [
1072
+ /* @__PURE__ */ jsx9("span", { className: "sk-field__image-browser-title", children: browserScope === "all" ? "Alle Bilder" : `Bilder in ${imgField.directory || "Bibliothek"}` }),
1073
+ /* @__PURE__ */ jsxs8("div", { className: "sk-field__image-browser-controls", children: [
1074
+ browserScope === "directory" ? /* @__PURE__ */ jsx9("button", { type: "button", className: "sk-button sk-button--sm", onClick: () => loadExisting("all"), children: "Alle Bilder" }) : /* @__PURE__ */ jsxs8("button", { type: "button", className: "sk-button sk-button--sm", onClick: () => loadExisting("directory"), children: [
1075
+ "Nur ",
1076
+ imgField.directory || "Ordner"
1077
+ ] }),
1078
+ /* @__PURE__ */ jsx9("button", { type: "button", className: "sk-button sk-button--sm", onClick: () => setShowBrowser(false), children: "Schlie\xDFen" })
1079
+ ] })
1080
+ ] }),
1081
+ loadingExisting ? /* @__PURE__ */ jsx9("p", { className: "sk-field__image-browser-loading", children: "Lade Bilder..." }) : existing.length === 0 ? /* @__PURE__ */ jsx9("p", { className: "sk-field__image-browser-empty", children: "Keine Bilder vorhanden" }) : /* @__PURE__ */ jsx9("div", { className: "sk-field__image-browser-grid", children: existing.map((asset) => {
1082
+ const thumbUrl = previewUrl(assets, asset.path);
1083
+ return /* @__PURE__ */ jsxs8(
1084
+ "button",
1085
+ {
1086
+ type: "button",
1087
+ className: "sk-field__image-browser-item",
1088
+ onClick: () => handleSelectExisting(asset),
1089
+ title: asset.path.split("/").pop(),
1090
+ children: [
1091
+ /* @__PURE__ */ jsx9("img", { src: thumbUrl, alt: "", className: "sk-field__image-browser-thumb" }),
1092
+ /* @__PURE__ */ jsx9("span", { className: "sk-field__image-browser-name", children: asset.path.split("/").pop() })
1093
+ ]
1094
+ },
1095
+ asset.path
1096
+ );
1097
+ }) })
1098
+ ] }),
1099
+ imageValue?.path && /* @__PURE__ */ jsx9(
1100
+ "input",
1101
+ {
1102
+ className: "sk-field__input",
1103
+ type: "text",
1104
+ value: imageValue.alt ?? "",
1105
+ onChange: handleAltChange,
1106
+ placeholder: "Alt-Text (f\xFCr SEO)",
1107
+ "aria-label": "Alt text"
1108
+ }
1109
+ ),
1110
+ errors.length > 0 && /* @__PURE__ */ jsx9("div", { className: "sk-field__errors", children: errors.map((err3, i) => /* @__PURE__ */ jsx9("p", { className: "sk-field__error", children: err3 }, i)) })
1111
+ ] });
1112
+ });
1113
+
1114
+ // src/fields/override-field-renderer.tsx
1115
+ import { memo as memo9 } from "react";
1116
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1117
+ var OverrideFieldRenderer = memo9(function OverrideFieldRenderer2({
1118
+ field,
1119
+ path,
1120
+ store
1121
+ }) {
1122
+ const overrideField = field;
1123
+ const { value, setValue } = useField(store, path);
1124
+ const overrideValue = value ?? { active: false };
1125
+ const isActive = overrideValue.active;
1126
+ const toggleActive = () => {
1127
+ setValue({ ...overrideValue, active: !isActive });
1128
+ };
1129
+ return /* @__PURE__ */ jsxs9("div", { className: "sk-field sk-field--override", "data-active": isActive || void 0, children: [
1130
+ /* @__PURE__ */ jsx10("div", { className: "sk-field__override-header", children: /* @__PURE__ */ jsxs9("label", { className: "sk-field__toggle", children: [
1131
+ /* @__PURE__ */ jsx10("input", { type: "checkbox", checked: isActive, onChange: toggleActive }),
1132
+ /* @__PURE__ */ jsx10("span", { className: "sk-field__toggle-label", children: overrideField.label })
1133
+ ] }) }),
1134
+ isActive && /* @__PURE__ */ jsx10("div", { className: "sk-field__override-fields", children: Object.entries(overrideField.fields).map(([key, childField]) => /* @__PURE__ */ jsx10(
1135
+ FieldRenderer,
1136
+ {
1137
+ field: childField,
1138
+ path: [...path, key],
1139
+ store
1140
+ },
1141
+ key
1142
+ )) })
1143
+ ] });
1144
+ });
1145
+
1146
+ // src/fields/field-renderer.tsx
1147
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1148
+ var rendererMap = {
1149
+ text: TextFieldRenderer,
1150
+ number: NumberFieldRenderer,
1151
+ boolean: BooleanFieldRenderer,
1152
+ select: SelectFieldRenderer,
1153
+ icon: IconFieldRenderer,
1154
+ array: ArrayFieldRenderer,
1155
+ object: ObjectFieldRenderer,
1156
+ image: ImageFieldRenderer,
1157
+ override: OverrideFieldRenderer
1158
+ };
1159
+ var FieldRenderer = memo10(function FieldRenderer2({
1160
+ field,
1161
+ path,
1162
+ store
1163
+ }) {
1164
+ const Renderer = rendererMap[field.type];
1165
+ if (!Renderer) {
1166
+ return /* @__PURE__ */ jsxs10("div", { style: { color: "#ef4444", padding: "8px", fontSize: "14px" }, children: [
1167
+ "Unknown field type: ",
1168
+ field.type
1169
+ ] });
1170
+ }
1171
+ return /* @__PURE__ */ jsx11(Renderer, { field, path, store });
1172
+ });
1173
+
1174
+ // src/components/entry-form.tsx
1175
+ import { memo as memo11, useEffect as useEffect3, useMemo as useMemo4 } from "react";
1176
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1177
+ var EntryForm = memo11(function EntryForm2({
1178
+ schema,
1179
+ initialValues,
1180
+ draftValues,
1181
+ onSave,
1182
+ storeRef
1183
+ }) {
1184
+ const store = useMemo4(() => createFormStore(), []);
1185
+ useEffect3(() => {
1186
+ if (storeRef) {
1187
+ storeRef.current = store;
1188
+ }
1189
+ return () => {
1190
+ if (storeRef) {
1191
+ storeRef.current = null;
1192
+ }
1193
+ };
1194
+ }, [store, storeRef]);
1195
+ useEffect3(() => {
1196
+ store.getState().init(schema, initialValues, draftValues);
1197
+ }, [store, schema, initialValues, draftValues]);
1198
+ const handleSave = () => {
1199
+ const { values, isDirty } = store.getState();
1200
+ if (isDirty() && onSave) {
1201
+ onSave(values);
1202
+ }
1203
+ };
1204
+ return /* @__PURE__ */ jsxs11("div", { className: "sk-entry-form", children: [
1205
+ /* @__PURE__ */ jsx12("div", { className: "sk-entry-form__fields", children: Object.entries(schema).map(([key, field]) => /* @__PURE__ */ jsx12(
1206
+ FieldRenderer,
1207
+ {
1208
+ field,
1209
+ path: [key],
1210
+ store
1211
+ },
1212
+ key
1213
+ )) }),
1214
+ onSave && /* @__PURE__ */ jsx12("div", { className: "sk-entry-form__actions", children: /* @__PURE__ */ jsx12("button", { type: "button", className: "sk-button sk-button--primary", onClick: handleSave, children: "Speichern" }) })
1215
+ ] });
1216
+ });
1217
+
1218
+ // src/components/entry-list.tsx
1219
+ import { useEffect as useEffect4, useState as useState5, useCallback as useCallback6 } from "react";
1220
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1221
+ function EntryList({
1222
+ collection,
1223
+ label,
1224
+ onSelect,
1225
+ onCreate,
1226
+ selectedSlug
1227
+ }) {
1228
+ const repository = useRepository();
1229
+ const [entries, setEntries] = useState5([]);
1230
+ const [loading, setLoading] = useState5(true);
1231
+ const loadEntries = useCallback6(async () => {
1232
+ setLoading(true);
1233
+ const result = await repository.listEntries(collection);
1234
+ if (result.ok) {
1235
+ setEntries(result.value);
1236
+ }
1237
+ setLoading(false);
1238
+ }, [repository, collection]);
1239
+ useEffect4(() => {
1240
+ loadEntries();
1241
+ }, [loadEntries]);
1242
+ const handleDelete = useCallback6(
1243
+ async (slug) => {
1244
+ if (!confirm(`"${slug}" wirklich l\xF6schen?`)) return;
1245
+ const result = await repository.deleteEntry(collection, slug);
1246
+ if (result.ok) {
1247
+ loadEntries();
1248
+ }
1249
+ },
1250
+ [repository, collection, loadEntries]
1251
+ );
1252
+ if (loading) {
1253
+ return /* @__PURE__ */ jsxs12("div", { className: "sk-entry-list sk-entry-list--loading", children: [
1254
+ /* @__PURE__ */ jsx13("div", { className: "sk-entry-list__header", children: /* @__PURE__ */ jsx13("h2", { className: "sk-entry-list__title", children: label }) }),
1255
+ /* @__PURE__ */ jsx13("div", { className: "sk-entry-list__empty", children: "Lade..." })
1256
+ ] });
1257
+ }
1258
+ return /* @__PURE__ */ jsxs12("div", { className: "sk-entry-list", children: [
1259
+ /* @__PURE__ */ jsxs12("div", { className: "sk-entry-list__header", children: [
1260
+ /* @__PURE__ */ jsx13("h2", { className: "sk-entry-list__title", children: label }),
1261
+ /* @__PURE__ */ jsx13("span", { className: "sk-entry-list__count", children: entries.length }),
1262
+ /* @__PURE__ */ jsx13(
1263
+ "button",
1264
+ {
1265
+ type: "button",
1266
+ className: "sk-button sk-button--primary sk-button--sm",
1267
+ onClick: onCreate,
1268
+ children: "+ Neu"
1269
+ }
1270
+ )
1271
+ ] }),
1272
+ entries.length === 0 ? /* @__PURE__ */ jsxs12("div", { className: "sk-entry-list__empty", children: [
1273
+ /* @__PURE__ */ jsx13("p", { children: "Noch keine Eintr\xE4ge." }),
1274
+ /* @__PURE__ */ jsx13(
1275
+ "button",
1276
+ {
1277
+ type: "button",
1278
+ className: "sk-button sk-button--primary",
1279
+ onClick: onCreate,
1280
+ children: "Ersten Eintrag erstellen"
1281
+ }
1282
+ )
1283
+ ] }) : /* @__PURE__ */ jsx13("ul", { className: "sk-entry-list__items", role: "list", children: entries.map((entry) => /* @__PURE__ */ jsxs12(
1284
+ "li",
1285
+ {
1286
+ className: `sk-entry-list__item ${entry.slug === selectedSlug ? "sk-entry-list__item--active" : ""}`,
1287
+ children: [
1288
+ /* @__PURE__ */ jsxs12(
1289
+ "button",
1290
+ {
1291
+ type: "button",
1292
+ className: "sk-entry-list__item-btn",
1293
+ onClick: () => onSelect(entry.slug),
1294
+ children: [
1295
+ /* @__PURE__ */ jsx13("span", { className: "sk-entry-list__item-name", children: entry.name }),
1296
+ /* @__PURE__ */ jsx13("span", { className: "sk-entry-list__item-slug", children: entry.slug })
1297
+ ]
1298
+ }
1299
+ ),
1300
+ /* @__PURE__ */ jsx13(
1301
+ "button",
1302
+ {
1303
+ type: "button",
1304
+ className: "sk-entry-list__item-delete",
1305
+ onClick: (e) => {
1306
+ e.stopPropagation();
1307
+ handleDelete(entry.slug);
1308
+ },
1309
+ title: "L\xF6schen",
1310
+ "aria-label": `${entry.name} l\xF6schen`,
1311
+ children: "\xD7"
1312
+ }
1313
+ )
1314
+ ]
1315
+ },
1316
+ entry.slug
1317
+ )) })
1318
+ ] });
1319
+ }
1320
+
1321
+ // src/components/collection-view.tsx
1322
+ import { useState as useState6, useCallback as useCallback7, useRef as useRef5 } from "react";
1323
+ import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
1324
+ function CollectionView({ collectionKey, collection }) {
1325
+ const repository = useRepository();
1326
+ const [selectedSlug, setSelectedSlug] = useState6(null);
1327
+ const [entryData, setEntryData] = useState6(null);
1328
+ const [loading, setLoading] = useState6(false);
1329
+ const [creating, setCreating] = useState6(false);
1330
+ const formStoreRef = useRef5(null);
1331
+ const loadEntry = useCallback7(
1332
+ async (slug) => {
1333
+ setLoading(true);
1334
+ setCreating(false);
1335
+ const result = await repository.getEntry(collectionKey, slug);
1336
+ if (result.ok && result.value) {
1337
+ setSelectedSlug(slug);
1338
+ setEntryData(result.value.content);
1339
+ }
1340
+ setLoading(false);
1341
+ },
1342
+ [repository, collectionKey]
1343
+ );
1344
+ const handleCreate = useCallback7(() => {
1345
+ setSelectedSlug(null);
1346
+ setEntryData({});
1347
+ setCreating(true);
1348
+ }, []);
1349
+ const handleSave = useCallback7(
1350
+ async (values) => {
1351
+ const slug = creating ? prompt("Slug f\xFCr den neuen Eintrag:") : selectedSlug;
1352
+ if (!slug) return;
1353
+ const result = await repository.saveEntry(collectionKey, slug, {
1354
+ content: values
1355
+ });
1356
+ if (result.ok) {
1357
+ setSelectedSlug(slug);
1358
+ setCreating(false);
1359
+ console.log(`[CollectionView] Saved ${slug}`);
1360
+ }
1361
+ },
1362
+ [repository, collectionKey, selectedSlug, creating]
1363
+ );
1364
+ return /* @__PURE__ */ jsxs13("div", { className: "sk-collection-view", children: [
1365
+ /* @__PURE__ */ jsx14("div", { className: "sk-collection-view__list", children: /* @__PURE__ */ jsx14(
1366
+ EntryList,
1367
+ {
1368
+ collection: collectionKey,
1369
+ label: collection.label,
1370
+ onSelect: loadEntry,
1371
+ onCreate: handleCreate,
1372
+ selectedSlug: selectedSlug ?? void 0
1373
+ }
1374
+ ) }),
1375
+ /* @__PURE__ */ jsx14("div", { className: "sk-collection-view__editor", children: loading ? /* @__PURE__ */ jsx14("div", { className: "sk-empty", children: /* @__PURE__ */ jsx14("p", { children: "Lade Eintrag..." }) }) : entryData !== null ? /* @__PURE__ */ jsxs13("div", { children: [
1376
+ /* @__PURE__ */ jsx14("div", { className: "sk-collection-view__editor-header", children: /* @__PURE__ */ jsx14("h3", { children: creating ? "Neuer Eintrag" : selectedSlug }) }),
1377
+ /* @__PURE__ */ jsx14(
1378
+ EntryForm,
1379
+ {
1380
+ schema: collection.fields,
1381
+ initialValues: entryData,
1382
+ onSave: handleSave,
1383
+ storeRef: formStoreRef
1384
+ },
1385
+ selectedSlug ?? "new"
1386
+ )
1387
+ ] }) : /* @__PURE__ */ jsx14("div", { className: "sk-empty", children: /* @__PURE__ */ jsx14("p", { children: "W\xE4hle einen Eintrag aus der Liste oder erstelle einen neuen." }) }) })
1388
+ ] });
1389
+ }
1390
+
1391
+ // src/components/admin-app.tsx
1392
+ import { useState as useState9, useEffect as useEffect7 } from "react";
1393
+
1394
+ // src/components/page-builder.tsx
1395
+ import { useState as useState8, useEffect as useEffect6, useCallback as useCallback9, useRef as useRef6, useSyncExternalStore } from "react";
1396
+
1397
+ // src/components/toast.tsx
1398
+ import { useState as useState7, useCallback as useCallback8, createContext as createContext2, useContext as useContext2 } from "react";
1399
+ import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
1400
+ var ToastContext = createContext2({ toast: () => {
1401
+ } });
1402
+ function useToast() {
1403
+ return useContext2(ToastContext);
1404
+ }
1405
+ var nextId = 0;
1406
+ function ToastProvider({ children }) {
1407
+ const [toasts, setToasts] = useState7([]);
1408
+ const toast = useCallback8((message, type = "info") => {
1409
+ const id = nextId++;
1410
+ setToasts((prev) => [...prev, { id, message, type }]);
1411
+ setTimeout(() => {
1412
+ setToasts((prev) => prev.filter((t) => t.id !== id));
1413
+ }, 3e3);
1414
+ }, []);
1415
+ return /* @__PURE__ */ jsxs14(ToastContext.Provider, { value: { toast }, children: [
1416
+ children,
1417
+ toasts.length > 0 && /* @__PURE__ */ jsx15("div", { className: "sk-toast-container", children: toasts.map((t) => /* @__PURE__ */ jsx15("div", { className: `sk-toast sk-toast--${t.type}`, children: t.message }, t.id)) })
1418
+ ] });
1419
+ }
1420
+
1421
+ // src/components/page-builder.tsx
1422
+ import {
1423
+ LayoutGrid,
1424
+ Star,
1425
+ Megaphone,
1426
+ Puzzle,
1427
+ Code,
1428
+ Eye,
1429
+ EyeOff,
1430
+ Shield,
1431
+ Palette,
1432
+ Zap,
1433
+ Image,
1434
+ Link as Link2,
1435
+ Settings,
1436
+ FileText,
1437
+ Folder,
1438
+ List,
1439
+ Edit,
1440
+ Tag,
1441
+ Globe,
1442
+ Heart,
1443
+ Users,
1444
+ X,
1445
+ GripVertical,
1446
+ Monitor,
1447
+ AppWindow,
1448
+ Tablet,
1449
+ Smartphone,
1450
+ Save,
1451
+ ChevronUp,
1452
+ ChevronDown,
1453
+ PanelRightClose,
1454
+ PanelRightOpen,
1455
+ RotateCcw,
1456
+ Undo2,
1457
+ Redo2,
1458
+ Trash2
1459
+ } from "lucide-react";
1460
+ import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
1461
+ var ICON_MAP = {
1462
+ layout: LayoutGrid,
1463
+ star: Star,
1464
+ megaphone: Megaphone,
1465
+ puzzle: Puzzle,
1466
+ code: Code,
1467
+ eye: Eye,
1468
+ shield: Shield,
1469
+ palette: Palette,
1470
+ zap: Zap,
1471
+ image: Image,
1472
+ link: Link2,
1473
+ settings: Settings,
1474
+ file: FileText,
1475
+ folder: Folder,
1476
+ list: List,
1477
+ edit: Edit,
1478
+ tag: Tag,
1479
+ globe: Globe,
1480
+ heart: Heart,
1481
+ users: Users
1482
+ };
1483
+ function useDraggable() {
1484
+ const [offset, setOffset] = useState8({ x: 0, y: 0 });
1485
+ const dragging = useRef6(false);
1486
+ const start = useRef6({ x: 0, y: 0 });
1487
+ const startOffset = useRef6({ x: 0, y: 0 });
1488
+ const onMouseDown = useCallback9((e) => {
1489
+ const target = e.target;
1490
+ if (target.closest("button") || target.closest("input") || target.closest("select")) return;
1491
+ if (!target.closest(".sk-pb__nav-header, .sk-pb__slide-over-header, .sk-pb__drag-handle")) return;
1492
+ e.preventDefault();
1493
+ dragging.current = true;
1494
+ start.current = { x: e.clientX, y: e.clientY };
1495
+ startOffset.current = { ...offset };
1496
+ const onMouseMove = (ev) => {
1497
+ if (!dragging.current) return;
1498
+ setOffset({
1499
+ x: startOffset.current.x + ev.clientX - start.current.x,
1500
+ y: startOffset.current.y + ev.clientY - start.current.y
1501
+ });
1502
+ };
1503
+ const onMouseUp = () => {
1504
+ dragging.current = false;
1505
+ document.removeEventListener("mousemove", onMouseMove);
1506
+ document.removeEventListener("mouseup", onMouseUp);
1507
+ };
1508
+ document.addEventListener("mousemove", onMouseMove);
1509
+ document.addEventListener("mouseup", onMouseUp);
1510
+ }, [offset]);
1511
+ const reset = useCallback9(() => setOffset({ x: 0, y: 0 }), []);
1512
+ const hasMoved = offset.x !== 0 || offset.y !== 0;
1513
+ const style = hasMoved ? { transform: `translate(${offset.x}px, ${offset.y}px)` } : void 0;
1514
+ return { onMouseDown, style, offset, reset, hasMoved };
1515
+ }
1516
+ function useResizable(defaultWidth, minW = 280, maxW = 800, minH = 200, maxH = 900) {
1517
+ const [width, setWidth] = useState8(defaultWidth);
1518
+ const [height, setHeight] = useState8(0);
1519
+ const resizing = useRef6(false);
1520
+ const startMouse = useRef6({ x: 0, y: 0 });
1521
+ const startW = useRef6(defaultWidth);
1522
+ const startH = useRef6(0);
1523
+ const mode = useRef6("bl");
1524
+ const onResizeStart = useCallback9((e, corner) => {
1525
+ e.preventDefault();
1526
+ e.stopPropagation();
1527
+ resizing.current = true;
1528
+ startMouse.current = { x: e.clientX, y: e.clientY };
1529
+ startW.current = width;
1530
+ startH.current = height || (e.currentTarget.parentElement?.offsetHeight ?? 400);
1531
+ mode.current = corner;
1532
+ document.querySelector(".sk-pb")?.classList.add("sk-pb--splitting");
1533
+ const onMouseMove = (ev) => {
1534
+ if (!resizing.current) return;
1535
+ const dx = ev.clientX - startMouse.current.x;
1536
+ const dy = ev.clientY - startMouse.current.y;
1537
+ const newW = corner === "bl" ? startW.current - dx : startW.current + dx;
1538
+ const newH = startH.current + dy;
1539
+ setWidth(Math.min(maxW, Math.max(minW, newW)));
1540
+ setHeight(Math.min(maxH, Math.max(minH, newH)));
1541
+ };
1542
+ const onMouseUp = () => {
1543
+ resizing.current = false;
1544
+ document.querySelector(".sk-pb")?.classList.remove("sk-pb--splitting");
1545
+ document.removeEventListener("mousemove", onMouseMove);
1546
+ document.removeEventListener("mouseup", onMouseUp);
1547
+ };
1548
+ document.addEventListener("mousemove", onMouseMove);
1549
+ document.addEventListener("mouseup", onMouseUp);
1550
+ }, [width, height, minW, maxW, minH, maxH]);
1551
+ const reset = useCallback9(() => {
1552
+ setWidth(defaultWidth);
1553
+ setHeight(0);
1554
+ }, [defaultWidth]);
1555
+ const hasResized = width !== defaultWidth || height !== 0;
1556
+ return { width, height, onResizeStart, reset, hasResized };
1557
+ }
1558
+ function PageBuilder({ pageKey = "index", pages = [], onPageChange, onExit }) {
1559
+ const config = useConfig();
1560
+ const repository = useRepository();
1561
+ const { toast } = useToast();
1562
+ const [pageConfig, setPageConfig] = useState8(null);
1563
+ const [visibleSection, setVisibleSection] = useState8(null);
1564
+ const [editingSection, setEditingSection] = useState8(null);
1565
+ const [editorOpen, setEditorOpen] = useState8(false);
1566
+ const [viewport, setViewport] = useState8("compact");
1567
+ const [loading, setLoading] = useState8(true);
1568
+ const [previewKey, setPreviewKey] = useState8(0);
1569
+ const [configDirty, setConfigDirty] = useState8(false);
1570
+ const [savingConfig, setSavingConfig] = useState8(false);
1571
+ const [configSha, setConfigSha] = useState8();
1572
+ const [navCollapsed, setNavCollapsed] = useState8(true);
1573
+ const [editorCollapsed, setEditorCollapsed] = useState8(false);
1574
+ const [dragIndex, setDragIndex] = useState8(null);
1575
+ const [dragOverIndex, setDragOverIndex] = useState8(null);
1576
+ const iframeRef = useRef6(null);
1577
+ const navRef = useRef6(null);
1578
+ const scrollAfterLoadRef = useRef6(null);
1579
+ const draftCacheRef = useRef6({});
1580
+ const navDrag = useDraggable();
1581
+ const editorDrag = useDraggable();
1582
+ const editorResize = useResizable(380);
1583
+ const [compactSplit, setCompactSplit] = useState8(66);
1584
+ const compactSplitDefault = 66;
1585
+ const pbRef = useRef6(null);
1586
+ const onSplitDragStart = useCallback9((e) => {
1587
+ e.preventDefault();
1588
+ const root = pbRef.current;
1589
+ if (!root) return;
1590
+ root.classList.add("sk-pb--splitting");
1591
+ const onMouseMove = (ev) => {
1592
+ const pct = ev.clientX / window.innerWidth * 100;
1593
+ const clamped = Math.min(95, Math.max(30, pct));
1594
+ root.style.setProperty("--sk-split", `${clamped}`);
1595
+ };
1596
+ const onMouseUp = (ev) => {
1597
+ root.classList.remove("sk-pb--splitting");
1598
+ const pct = ev.clientX / window.innerWidth * 100;
1599
+ setCompactSplit(Math.min(95, Math.max(30, pct)));
1600
+ root.style.removeProperty("--sk-split");
1601
+ document.removeEventListener("mousemove", onMouseMove);
1602
+ document.removeEventListener("mouseup", onMouseUp);
1603
+ };
1604
+ document.addEventListener("mousemove", onMouseMove);
1605
+ document.addEventListener("mouseup", onMouseUp);
1606
+ }, []);
1607
+ const resetAllPositions = useCallback9(() => {
1608
+ navDrag.reset();
1609
+ editorDrag.reset();
1610
+ editorResize.reset();
1611
+ setCompactSplit(compactSplitDefault);
1612
+ }, [navDrag.reset, editorDrag.reset, editorResize.reset]);
1613
+ const compactSplitMoved = compactSplit !== compactSplitDefault;
1614
+ const anyMoved = navDrag.hasMoved || editorDrag.hasMoved || editorResize.hasResized || compactSplitMoved;
1615
+ const configKey = "_" + pageKey.replace(/\//g, "--");
1616
+ useEffect6(() => {
1617
+ loadPageConfig();
1618
+ }, [pageKey]);
1619
+ async function loadPageConfig() {
1620
+ try {
1621
+ const result = await repository.getEntry("pages", configKey);
1622
+ if (result.ok) {
1623
+ setPageConfig(result.value.content);
1624
+ setConfigSha(result.value.sha);
1625
+ } else {
1626
+ setPageConfig({
1627
+ label: pageKey === "index" ? "Startseite" : pageKey,
1628
+ sections: []
1629
+ });
1630
+ }
1631
+ } catch (e) {
1632
+ console.error("[PageBuilder] Failed to load page config:", e);
1633
+ } finally {
1634
+ setLoading(false);
1635
+ }
1636
+ }
1637
+ function getSectionDef(key) {
1638
+ for (const product of Object.values(config.products)) {
1639
+ if (product.sections[key]) return product.sections[key];
1640
+ }
1641
+ return null;
1642
+ }
1643
+ const scrollToSection = useCallback9((sectionKey) => {
1644
+ const iframe = iframeRef.current;
1645
+ if (!iframe?.contentWindow) return;
1646
+ iframe.contentWindow.postMessage(
1647
+ { type: "sk:scroll-to", section: sectionKey },
1648
+ "*"
1649
+ );
1650
+ }, []);
1651
+ const syncSectionsToPreview = useCallback9((sections2) => {
1652
+ const iframe = iframeRef.current;
1653
+ if (!iframe?.contentWindow) return;
1654
+ iframe.contentWindow.postMessage(
1655
+ { type: "sk:update-sections", sections: sections2 },
1656
+ "*"
1657
+ );
1658
+ }, []);
1659
+ const pageConfigRef = useRef6(pageConfig);
1660
+ pageConfigRef.current = pageConfig;
1661
+ const handleIframeLoad = useCallback9(() => {
1662
+ setTimeout(() => {
1663
+ if (scrollAfterLoadRef.current) {
1664
+ scrollToSection(scrollAfterLoadRef.current);
1665
+ scrollAfterLoadRef.current = null;
1666
+ }
1667
+ if (pageConfigRef.current?.sections) {
1668
+ syncSectionsToPreview(pageConfigRef.current.sections);
1669
+ }
1670
+ }, 200);
1671
+ }, [scrollToSection, syncSectionsToPreview]);
1672
+ useEffect6(() => {
1673
+ function handleMessage(event) {
1674
+ if (event.data?.type === "sk:visible-section") {
1675
+ setVisibleSection(event.data.section);
1676
+ }
1677
+ }
1678
+ window.addEventListener("message", handleMessage);
1679
+ return () => window.removeEventListener("message", handleMessage);
1680
+ }, []);
1681
+ const openEditor = useCallback9((sectionKey) => {
1682
+ setEditingSection(sectionKey);
1683
+ setEditorOpen(true);
1684
+ setEditorCollapsed(false);
1685
+ scrollToSection(sectionKey);
1686
+ }, [scrollToSection]);
1687
+ const closeEditor = useCallback9(() => {
1688
+ setEditorOpen(false);
1689
+ setEditorCollapsed(false);
1690
+ setEditingSection(null);
1691
+ }, []);
1692
+ const reloadPreview = useCallback9((scrollTo) => {
1693
+ scrollAfterLoadRef.current = scrollTo ?? visibleSection;
1694
+ setPreviewKey((k) => k + 1);
1695
+ }, [visibleSection]);
1696
+ const needsSsrReload = useCallback9((sectionKey, values) => {
1697
+ const product = Object.values(config.products)[0];
1698
+ const section = product?.sections[sectionKey];
1699
+ if (!section) return true;
1700
+ const prev = draftCacheRef.current[sectionKey];
1701
+ if (!prev) return false;
1702
+ function hasNonTextChanges(prevObj, nextObj, fields) {
1703
+ for (const [key, val] of Object.entries(nextObj)) {
1704
+ if (JSON.stringify(prevObj[key]) === JSON.stringify(val)) continue;
1705
+ const fieldDef = fields[key];
1706
+ if (!fieldDef) return true;
1707
+ if (fieldDef.type === "text") continue;
1708
+ if (fieldDef.type === "array" && fieldDef.itemField?.type === "object" && fieldDef.itemField.fields) {
1709
+ const prevArr = prevObj[key] ?? [];
1710
+ const nextArr = val ?? [];
1711
+ if (prevArr.length !== nextArr.length) return true;
1712
+ for (let i = 0; i < nextArr.length; i++) {
1713
+ const prevItem = prevArr[i] ?? {};
1714
+ const nextItem = nextArr[i] ?? {};
1715
+ if (hasNonTextChanges(prevItem, nextItem, fieldDef.itemField.fields)) return true;
1716
+ }
1717
+ continue;
1718
+ }
1719
+ return true;
1720
+ }
1721
+ return false;
1722
+ }
1723
+ return hasNonTextChanges(prev, values, section.fields);
1724
+ }, [config]);
1725
+ const updateDraft = useCallback9((sectionKey, values) => {
1726
+ const requiresReload = needsSsrReload(sectionKey, values);
1727
+ draftCacheRef.current[sectionKey] = values;
1728
+ if (requiresReload) {
1729
+ fetch("/api/setzkasten/draft", {
1730
+ method: "POST",
1731
+ headers: { "Content-Type": "application/json" },
1732
+ body: JSON.stringify({ section: sectionKey, values })
1733
+ }).then(() => {
1734
+ reloadPreview(sectionKey);
1735
+ });
1736
+ } else {
1737
+ const iframe = iframeRef.current;
1738
+ if (!iframe?.contentWindow) return;
1739
+ iframe.contentWindow.postMessage(
1740
+ { type: "sk:draft-update", section: sectionKey, values },
1741
+ "*"
1742
+ );
1743
+ }
1744
+ }, [needsSsrReload, reloadPreview]);
1745
+ const clearDraftCache = useCallback9((sectionKey) => {
1746
+ delete draftCacheRef.current[sectionKey];
1747
+ fetch("/api/setzkasten/draft", {
1748
+ method: "DELETE",
1749
+ headers: { "Content-Type": "application/json" },
1750
+ body: JSON.stringify({ section: sectionKey })
1751
+ }).catch(() => {
1752
+ });
1753
+ }, []);
1754
+ const discardDraft = useCallback9((sectionKey) => {
1755
+ clearDraftCache(sectionKey);
1756
+ reloadPreview(sectionKey);
1757
+ }, [clearDraftCache, reloadPreview]);
1758
+ const toggleSection = useCallback9((sectionKey) => {
1759
+ setPageConfig((prev) => {
1760
+ if (!prev) return prev;
1761
+ const updated = {
1762
+ ...prev,
1763
+ sections: prev.sections.map(
1764
+ (s) => s.key === sectionKey ? { ...s, enabled: !s.enabled } : s
1765
+ )
1766
+ };
1767
+ syncSectionsToPreview(updated.sections);
1768
+ return updated;
1769
+ });
1770
+ setConfigDirty(true);
1771
+ }, [syncSectionsToPreview]);
1772
+ const handleDragStart = useCallback9((index) => {
1773
+ setDragIndex(index);
1774
+ }, []);
1775
+ const handleDragOver = useCallback9((e, index) => {
1776
+ e.preventDefault();
1777
+ setDragOverIndex(index);
1778
+ }, []);
1779
+ const handleDragEnd = useCallback9(() => {
1780
+ if (dragIndex !== null && dragOverIndex !== null && dragIndex !== dragOverIndex) {
1781
+ setPageConfig((prev) => {
1782
+ if (!prev) return prev;
1783
+ const newSections = [...prev.sections];
1784
+ const [moved] = newSections.splice(dragIndex, 1);
1785
+ if (!moved) return prev;
1786
+ newSections.splice(dragOverIndex, 0, moved);
1787
+ const updated = { ...prev, sections: newSections };
1788
+ syncSectionsToPreview(updated.sections);
1789
+ return updated;
1790
+ });
1791
+ setConfigDirty(true);
1792
+ }
1793
+ setDragIndex(null);
1794
+ setDragOverIndex(null);
1795
+ }, [dragIndex, dragOverIndex, syncSectionsToPreview]);
1796
+ const savePageConfig = useCallback9(async () => {
1797
+ if (!pageConfig || savingConfig) return;
1798
+ setSavingConfig(true);
1799
+ try {
1800
+ const result = await repository.saveEntry("pages", configKey, {
1801
+ content: pageConfig,
1802
+ sha: configSha
1803
+ });
1804
+ if (result.ok) {
1805
+ setConfigDirty(false);
1806
+ setConfigSha(void 0);
1807
+ toast("Seitenkonfiguration gespeichert", "success");
1808
+ } else {
1809
+ toast(`Fehler: ${result.error?.message ?? "Unbekannt"}`, "error");
1810
+ }
1811
+ } catch (e) {
1812
+ toast("Speichern fehlgeschlagen", "error");
1813
+ }
1814
+ setSavingConfig(false);
1815
+ }, [pageConfig, configSha, savingConfig, repository, toast]);
1816
+ const viewportWidths = {
1817
+ compact: `${compactSplit}vw`,
1818
+ desktop: "100%",
1819
+ tablet: "768px",
1820
+ mobile: "375px"
1821
+ };
1822
+ const viewportWidth = viewportWidths[viewport];
1823
+ const isCompact = viewport === "compact";
1824
+ if (loading) {
1825
+ return /* @__PURE__ */ jsx16("div", { className: "sk-pb", children: /* @__PURE__ */ jsx16("div", { className: "sk-pb__loading", children: "Lade Page Builder..." }) });
1826
+ }
1827
+ const sections = pageConfig?.sections ?? [];
1828
+ const navContent = /* @__PURE__ */ jsxs15(Fragment3, { children: [
1829
+ /* @__PURE__ */ jsxs15("div", { className: "sk-pb__nav-header", children: [
1830
+ navCollapsed && visibleSection && /* @__PURE__ */ jsxs15(
1831
+ "button",
1832
+ {
1833
+ type: "button",
1834
+ className: "sk-pb__inline-edit-btn",
1835
+ onClick: () => openEditor(visibleSection),
1836
+ children: [
1837
+ /* @__PURE__ */ jsx16(Edit, { size: 14 }),
1838
+ getSectionDef(visibleSection)?.label ?? visibleSection
1839
+ ]
1840
+ }
1841
+ ),
1842
+ !navCollapsed && /* @__PURE__ */ jsx16("span", { className: "sk-pb__nav-title", children: "Sections" }),
1843
+ !navCollapsed && /* @__PURE__ */ jsxs15("div", { className: "sk-pb__nav-actions", children: [
1844
+ /* @__PURE__ */ jsx16(
1845
+ "button",
1846
+ {
1847
+ type: "button",
1848
+ className: `sk-pb__viewport-btn${viewport === "compact" ? " sk-pb__viewport-btn--active" : ""}`,
1849
+ onClick: () => {
1850
+ setViewport("compact");
1851
+ setNavCollapsed(true);
1852
+ },
1853
+ title: "Compact Desktop",
1854
+ children: /* @__PURE__ */ jsx16(AppWindow, { size: 14 })
1855
+ }
1856
+ ),
1857
+ /* @__PURE__ */ jsx16(
1858
+ "button",
1859
+ {
1860
+ type: "button",
1861
+ className: `sk-pb__viewport-btn${viewport === "desktop" ? " sk-pb__viewport-btn--active" : ""}`,
1862
+ onClick: () => {
1863
+ setViewport("desktop");
1864
+ setNavCollapsed(true);
1865
+ },
1866
+ title: "Desktop",
1867
+ children: /* @__PURE__ */ jsx16(Monitor, { size: 14 })
1868
+ }
1869
+ ),
1870
+ /* @__PURE__ */ jsx16(
1871
+ "button",
1872
+ {
1873
+ type: "button",
1874
+ className: `sk-pb__viewport-btn${viewport === "tablet" ? " sk-pb__viewport-btn--active" : ""}`,
1875
+ onClick: () => {
1876
+ setViewport("tablet");
1877
+ setNavCollapsed(false);
1878
+ },
1879
+ title: "Tablet",
1880
+ children: /* @__PURE__ */ jsx16(Tablet, { size: 14 })
1881
+ }
1882
+ ),
1883
+ /* @__PURE__ */ jsx16(
1884
+ "button",
1885
+ {
1886
+ type: "button",
1887
+ className: `sk-pb__viewport-btn${viewport === "mobile" ? " sk-pb__viewport-btn--active" : ""}`,
1888
+ onClick: () => {
1889
+ setViewport("mobile");
1890
+ setNavCollapsed(false);
1891
+ },
1892
+ title: "Mobile",
1893
+ children: /* @__PURE__ */ jsx16(Smartphone, { size: 14 })
1894
+ }
1895
+ )
1896
+ ] }),
1897
+ /* @__PURE__ */ jsxs15("div", { className: "sk-pb__nav-header-right", children: [
1898
+ /* @__PURE__ */ jsx16(
1899
+ "button",
1900
+ {
1901
+ type: "button",
1902
+ className: "sk-pb__collapse-btn",
1903
+ onClick: () => setNavCollapsed((c) => !c),
1904
+ title: navCollapsed ? "Navigation aufklappen" : "Navigation einklappen",
1905
+ children: navCollapsed ? /* @__PURE__ */ jsx16(ChevronDown, { size: 14 }) : /* @__PURE__ */ jsx16(ChevronUp, { size: 14 })
1906
+ }
1907
+ ),
1908
+ /* @__PURE__ */ jsx16(
1909
+ "button",
1910
+ {
1911
+ type: "button",
1912
+ className: "sk-pb__exit-btn",
1913
+ onClick: onExit,
1914
+ title: "Page Builder verlassen",
1915
+ children: /* @__PURE__ */ jsx16(X, { size: 16 })
1916
+ }
1917
+ )
1918
+ ] })
1919
+ ] }),
1920
+ !navCollapsed && /* @__PURE__ */ jsxs15(Fragment3, { children: [
1921
+ pages.length > 1 && onPageChange && /* @__PURE__ */ jsxs15("div", { className: "sk-pb__page-switcher", children: [
1922
+ /* @__PURE__ */ jsx16("label", { className: "sk-pb__page-switcher-label", children: "Seite" }),
1923
+ /* @__PURE__ */ jsx16(
1924
+ "select",
1925
+ {
1926
+ className: "sk-pb__page-select",
1927
+ value: pageKey,
1928
+ onChange: (e) => onPageChange(e.target.value),
1929
+ children: pages.map((p) => /* @__PURE__ */ jsx16("option", { value: p.pageKey, children: p.label }, p.pageKey))
1930
+ }
1931
+ )
1932
+ ] }),
1933
+ /* @__PURE__ */ jsx16("div", { className: "sk-pb__section-list", children: sections.map((section, index) => {
1934
+ const def = getSectionDef(section.key);
1935
+ const IconComp = def?.icon ? ICON_MAP[def.icon] : null;
1936
+ const isVisible = visibleSection === section.key;
1937
+ const isEditing = editingSection === section.key;
1938
+ const isDragging = dragIndex === index;
1939
+ const isDragOver = dragOverIndex === index;
1940
+ return /* @__PURE__ */ jsxs15(
1941
+ "div",
1942
+ {
1943
+ className: `sk-pb__section-pill${isEditing ? " sk-pb__section-pill--editing" : ""}${isVisible && !isEditing ? " sk-pb__section-pill--visible" : ""}${!section.enabled ? " sk-pb__section-pill--disabled" : ""}${isDragging ? " sk-pb__section-pill--dragging" : ""}${isDragOver ? " sk-pb__section-pill--dragover" : ""}`,
1944
+ draggable: true,
1945
+ onDragStart: () => handleDragStart(index),
1946
+ onDragOver: (e) => handleDragOver(e, index),
1947
+ onDragEnd: handleDragEnd,
1948
+ children: [
1949
+ /* @__PURE__ */ jsx16("span", { className: "sk-pb__section-grip", children: /* @__PURE__ */ jsx16(GripVertical, { size: 14 }) }),
1950
+ /* @__PURE__ */ jsxs15(
1951
+ "button",
1952
+ {
1953
+ type: "button",
1954
+ className: "sk-pb__section-main",
1955
+ onClick: () => openEditor(section.key),
1956
+ children: [
1957
+ IconComp && /* @__PURE__ */ jsx16("span", { className: "sk-pb__section-icon", children: /* @__PURE__ */ jsx16(IconComp, { size: 16 }) }),
1958
+ /* @__PURE__ */ jsx16("span", { className: "sk-pb__section-label", children: def?.label ?? section.key })
1959
+ ]
1960
+ }
1961
+ ),
1962
+ /* @__PURE__ */ jsx16(
1963
+ "button",
1964
+ {
1965
+ type: "button",
1966
+ className: "sk-pb__section-toggle",
1967
+ onClick: (e) => {
1968
+ e.stopPropagation();
1969
+ toggleSection(section.key);
1970
+ },
1971
+ title: section.enabled ? "Section ausblenden" : "Section einblenden",
1972
+ children: section.enabled ? /* @__PURE__ */ jsx16(Eye, { size: 14 }) : /* @__PURE__ */ jsx16(EyeOff, { size: 14 })
1973
+ }
1974
+ )
1975
+ ]
1976
+ },
1977
+ section.key
1978
+ );
1979
+ }) }),
1980
+ configDirty && /* @__PURE__ */ jsx16("div", { className: "sk-pb__nav-footer", children: /* @__PURE__ */ jsxs15(
1981
+ "button",
1982
+ {
1983
+ type: "button",
1984
+ className: "sk-pb__save-btn",
1985
+ onClick: savePageConfig,
1986
+ disabled: savingConfig,
1987
+ children: [
1988
+ /* @__PURE__ */ jsx16(Save, { size: 14 }),
1989
+ savingConfig ? "Speichert..." : "Reihenfolge speichern"
1990
+ ]
1991
+ }
1992
+ ) })
1993
+ ] })
1994
+ ] });
1995
+ const editorPanel = editorOpen && editingSection && !editorCollapsed && (getSectionDef(editingSection) ? /* @__PURE__ */ jsx16(
1996
+ SectionSlideOver,
1997
+ {
1998
+ sectionKey: editingSection,
1999
+ section: getSectionDef(editingSection),
2000
+ cachedValues: draftCacheRef.current[editingSection],
2001
+ onClose: closeEditor,
2002
+ onCollapse: () => setEditorCollapsed(true),
2003
+ onDraftUpdate: updateDraft,
2004
+ onDiscard: discardDraft,
2005
+ onSaved: clearDraftCache
2006
+ },
2007
+ editingSection
2008
+ ) : /* @__PURE__ */ jsxs15("div", { className: "sk-pb__slide-over", children: [
2009
+ /* @__PURE__ */ jsxs15("div", { className: "sk-pb__slide-over-header", children: [
2010
+ /* @__PURE__ */ jsx16("h3", { children: editingSection }),
2011
+ /* @__PURE__ */ jsxs15("div", { className: "sk-pb__slide-over-actions", children: [
2012
+ /* @__PURE__ */ jsx16("button", { type: "button", className: "sk-pb__collapse-btn", onClick: () => setEditorCollapsed(true), title: "Einklappen", children: /* @__PURE__ */ jsx16(PanelRightClose, { size: 16 }) }),
2013
+ /* @__PURE__ */ jsx16("button", { type: "button", className: "sk-pb__close-btn", onClick: closeEditor, children: /* @__PURE__ */ jsx16(X, { size: 18 }) })
2014
+ ] })
2015
+ ] }),
2016
+ /* @__PURE__ */ jsx16("div", { className: "sk-pb__slide-over-body", children: /* @__PURE__ */ jsx16("p", { className: "sk-pb__slide-over-info", children: "Diese Section hat noch kein editierbares Schema." }) })
2017
+ ] }));
2018
+ const editorFloatTransform = editorDrag.hasMoved ? { transform: `translate(${editorDrag.offset.x}px, ${editorDrag.offset.y}px)` } : {};
2019
+ return /* @__PURE__ */ jsxs15(
2020
+ "div",
2021
+ {
2022
+ ref: pbRef,
2023
+ className: `sk-pb${isCompact ? " sk-pb--compact" : ""}`,
2024
+ style: isCompact ? { "--sk-split": `${compactSplit}` } : void 0,
2025
+ children: [
2026
+ /* @__PURE__ */ jsx16("div", { className: "sk-pb__preview", children: /* @__PURE__ */ jsx16(
2027
+ "div",
2028
+ {
2029
+ className: "sk-pb__iframe-container",
2030
+ style: {
2031
+ maxWidth: isCompact ? "calc(var(--sk-split, 66) * 1vw)" : viewportWidth,
2032
+ margin: viewport === "desktop" ? void 0 : isCompact ? "0" : "0 auto"
2033
+ },
2034
+ children: /* @__PURE__ */ jsx16(
2035
+ "iframe",
2036
+ {
2037
+ ref: iframeRef,
2038
+ src: `/${pageKey === "index" ? "" : pageKey}?_sk_preview=1&_t=${previewKey}`,
2039
+ className: "sk-pb__iframe",
2040
+ title: "Live Preview",
2041
+ onLoad: handleIframeLoad
2042
+ },
2043
+ previewKey
2044
+ )
2045
+ }
2046
+ ) }),
2047
+ isCompact && /* @__PURE__ */ jsx16(
2048
+ "div",
2049
+ {
2050
+ className: "sk-pb__split-divider",
2051
+ style: { left: "calc(var(--sk-split, 66) * 1vw)" },
2052
+ onMouseDown: onSplitDragStart
2053
+ }
2054
+ ),
2055
+ /* @__PURE__ */ jsx16(
2056
+ "div",
2057
+ {
2058
+ ref: navRef,
2059
+ className: `sk-pb__nav${navCollapsed ? " sk-pb__nav--collapsed" : ""}`,
2060
+ style: navDrag.style,
2061
+ onMouseDown: navDrag.onMouseDown,
2062
+ children: navContent
2063
+ }
2064
+ ),
2065
+ editorPanel && /* @__PURE__ */ jsxs15(
2066
+ "div",
2067
+ {
2068
+ className: "sk-pb__editor-float",
2069
+ style: {
2070
+ ...!isCompact || editorResize.hasResized ? { width: editorResize.width } : {},
2071
+ ...editorResize.height > 0 ? { height: editorResize.height } : {},
2072
+ ...editorFloatTransform
2073
+ },
2074
+ onMouseDown: editorDrag.onMouseDown,
2075
+ children: [
2076
+ /* @__PURE__ */ jsx16(
2077
+ "div",
2078
+ {
2079
+ className: "sk-pb__resize-corner sk-pb__resize-corner--bl",
2080
+ onMouseDown: (e) => editorResize.onResizeStart(e, "bl")
2081
+ }
2082
+ ),
2083
+ editorPanel
2084
+ ]
2085
+ }
2086
+ ),
2087
+ editorOpen && editingSection && editorCollapsed && /* @__PURE__ */ jsx16(
2088
+ "button",
2089
+ {
2090
+ type: "button",
2091
+ className: "sk-pb__editor-expand-btn",
2092
+ onClick: () => setEditorCollapsed(false),
2093
+ title: "Editor aufklappen",
2094
+ children: /* @__PURE__ */ jsx16(PanelRightOpen, { size: 16 })
2095
+ }
2096
+ ),
2097
+ anyMoved && /* @__PURE__ */ jsx16(
2098
+ "button",
2099
+ {
2100
+ type: "button",
2101
+ className: "sk-pb__reset-pos-btn",
2102
+ onClick: resetAllPositions,
2103
+ title: "Panel-Positionen zur\xFCcksetzen",
2104
+ children: /* @__PURE__ */ jsx16(RotateCcw, { size: 14 })
2105
+ }
2106
+ )
2107
+ ]
2108
+ }
2109
+ );
2110
+ }
2111
+ function SectionSlideOver({ sectionKey, section, cachedValues, onClose, onCollapse, onDraftUpdate, onDiscard, onSaved }) {
2112
+ const repository = useRepository();
2113
+ const { toast } = useToast();
2114
+ const [initialValues, setInitialValues] = useState8({});
2115
+ const [loading, setLoading] = useState8(true);
2116
+ const [saving, setSaving] = useState8(false);
2117
+ const [sha, setSha] = useState8();
2118
+ const [conflict, setConflict] = useState8(null);
2119
+ const storeRef = useRef6(null);
2120
+ const [formStore, setFormStore] = useState8(null);
2121
+ const draftTimerRef = useRef6(null);
2122
+ const saveRef = useRef6(null);
2123
+ const undoRef = useRef6(null);
2124
+ const redoRef = useRef6(null);
2125
+ const storeSnapshotRef = useRef6({ dirty: false, canUndo: false, canRedo: false });
2126
+ const storeState = useSyncExternalStore(
2127
+ useCallback9((cb) => formStore?.subscribe(cb) ?? (() => {
2128
+ }), [formStore]),
2129
+ useCallback9(() => {
2130
+ if (!formStore) return storeSnapshotRef.current;
2131
+ const s = formStore.getState();
2132
+ const dirty = s.isDirty();
2133
+ const canUndo = s.canUndo();
2134
+ const canRedo = s.canRedo();
2135
+ const prev = storeSnapshotRef.current;
2136
+ if (prev.dirty !== dirty || prev.canUndo !== canUndo || prev.canRedo !== canRedo) {
2137
+ storeSnapshotRef.current = { dirty, canUndo, canRedo };
2138
+ }
2139
+ return storeSnapshotRef.current;
2140
+ }, [formStore])
2141
+ );
2142
+ useEffect6(() => {
2143
+ setLoading(true);
2144
+ repository.getEntry("_sections", sectionKey).then((result) => {
2145
+ if (result.ok) {
2146
+ setInitialValues(result.value.content);
2147
+ setSha(result.value.sha);
2148
+ }
2149
+ setLoading(false);
2150
+ });
2151
+ }, [sectionKey, repository]);
2152
+ useEffect6(() => {
2153
+ if (formStore) return;
2154
+ const check = setInterval(() => {
2155
+ if (storeRef.current && !formStore) {
2156
+ setFormStore(storeRef.current);
2157
+ clearInterval(check);
2158
+ }
2159
+ }, 50);
2160
+ return () => clearInterval(check);
2161
+ }, [formStore, loading]);
2162
+ useEffect6(() => {
2163
+ if (!formStore) return;
2164
+ const unsub = formStore.subscribe((state) => {
2165
+ if (draftTimerRef.current) clearTimeout(draftTimerRef.current);
2166
+ draftTimerRef.current = setTimeout(() => {
2167
+ onDraftUpdate(sectionKey, state.values);
2168
+ }, 500);
2169
+ });
2170
+ return () => {
2171
+ unsub();
2172
+ if (draftTimerRef.current) clearTimeout(draftTimerRef.current);
2173
+ };
2174
+ }, [formStore, sectionKey, onDraftUpdate]);
2175
+ const handleSave = useCallback9(async (values) => {
2176
+ setSaving(true);
2177
+ const result = await repository.saveEntry("_sections", sectionKey, {
2178
+ content: values,
2179
+ sha
2180
+ });
2181
+ if (result.ok) {
2182
+ setInitialValues(values);
2183
+ setSha(result.value.sha);
2184
+ setConflict(null);
2185
+ onSaved(sectionKey);
2186
+ toast("Gespeichert \u2013 \xC4nderungen sind in 1\u20132 Minuten live.", "success");
2187
+ } else {
2188
+ if (result.error?.type === "conflict") {
2189
+ const remoteResult = await repository.getEntry("_sections", sectionKey);
2190
+ if (remoteResult.ok) {
2191
+ setConflict({
2192
+ local: values,
2193
+ remote: remoteResult.value.content,
2194
+ remoteSha: remoteResult.value.sha
2195
+ });
2196
+ } else {
2197
+ toast("Konflikt: Jemand hat gleichzeitig bearbeitet. Seite neu laden.", "error");
2198
+ }
2199
+ } else {
2200
+ toast(`Fehler: ${result.error?.message ?? "Unbekannt"}`, "error");
2201
+ }
2202
+ }
2203
+ setSaving(false);
2204
+ }, [repository, sectionKey, sha, toast]);
2205
+ saveRef.current = () => {
2206
+ const store = storeRef.current;
2207
+ if (!store || saving) return;
2208
+ const { values } = store.getState();
2209
+ handleSave(values);
2210
+ };
2211
+ undoRef.current = () => {
2212
+ const store = storeRef.current;
2213
+ if (!store) return;
2214
+ store.getState().undo();
2215
+ };
2216
+ redoRef.current = () => {
2217
+ const store = storeRef.current;
2218
+ if (!store) return;
2219
+ store.getState().redo();
2220
+ };
2221
+ const resolveConflictLocal = useCallback9(async () => {
2222
+ if (!conflict) return;
2223
+ setSaving(true);
2224
+ const result = await repository.saveEntry("_sections", sectionKey, {
2225
+ content: conflict.local,
2226
+ sha: conflict.remoteSha
2227
+ });
2228
+ if (result.ok) {
2229
+ setInitialValues(conflict.local);
2230
+ setSha(result.value.sha);
2231
+ setConflict(null);
2232
+ onSaved(sectionKey);
2233
+ toast("Gespeichert (deine Version \xFCbernommen).", "success");
2234
+ } else {
2235
+ toast("Speichern fehlgeschlagen. Bitte erneut versuchen.", "error");
2236
+ }
2237
+ setSaving(false);
2238
+ }, [conflict, repository, sectionKey, onSaved, toast]);
2239
+ const resolveConflictRemote = useCallback9(() => {
2240
+ if (!conflict) return;
2241
+ const store = storeRef.current;
2242
+ if (store) {
2243
+ store.getState().init(section.fields, conflict.remote);
2244
+ }
2245
+ setInitialValues(conflict.remote);
2246
+ setSha(conflict.remoteSha);
2247
+ setConflict(null);
2248
+ onDiscard(sectionKey);
2249
+ toast("Remote-Version \xFCbernommen.", "success");
2250
+ }, [conflict, section.fields, sectionKey, onDiscard, toast]);
2251
+ const handleDiscard = useCallback9(() => {
2252
+ const store = storeRef.current;
2253
+ if (!store) return;
2254
+ store.getState().resetToInitial();
2255
+ onDiscard(sectionKey);
2256
+ }, [sectionKey, onDiscard]);
2257
+ useEffect6(() => {
2258
+ const handler = (e) => {
2259
+ if ((e.metaKey || e.ctrlKey) && e.key === "s") {
2260
+ e.preventDefault();
2261
+ saveRef.current?.();
2262
+ }
2263
+ if ((e.metaKey || e.ctrlKey) && e.key === "z" && !e.shiftKey) {
2264
+ e.preventDefault();
2265
+ undoRef.current?.();
2266
+ }
2267
+ if ((e.metaKey || e.ctrlKey) && e.key === "z" && e.shiftKey) {
2268
+ e.preventDefault();
2269
+ redoRef.current?.();
2270
+ }
2271
+ if (e.key === "Escape") {
2272
+ onClose();
2273
+ }
2274
+ };
2275
+ document.addEventListener("keydown", handler);
2276
+ return () => document.removeEventListener("keydown", handler);
2277
+ }, [onClose]);
2278
+ return /* @__PURE__ */ jsxs15("div", { className: "sk-pb__slide-over", children: [
2279
+ /* @__PURE__ */ jsxs15("div", { className: "sk-pb__slide-over-header", children: [
2280
+ /* @__PURE__ */ jsx16("div", { className: "sk-pb__slide-over-title", children: storeState.dirty && /* @__PURE__ */ jsxs15("span", { className: "sk-pb__draft-indicator", title: "Ungespeicherte \xC4nderungen", children: [
2281
+ /* @__PURE__ */ jsx16("span", { className: "sk-pb__draft-dot" }),
2282
+ "Entwurf"
2283
+ ] }) }),
2284
+ /* @__PURE__ */ jsxs15("div", { className: "sk-pb__slide-over-actions", children: [
2285
+ storeState.dirty && /* @__PURE__ */ jsx16(
2286
+ "button",
2287
+ {
2288
+ type: "button",
2289
+ className: "sk-pb__discard-btn",
2290
+ onClick: handleDiscard,
2291
+ title: "\xC4nderungen verwerfen",
2292
+ children: /* @__PURE__ */ jsx16(Trash2, { size: 14 })
2293
+ }
2294
+ ),
2295
+ /* @__PURE__ */ jsx16(
2296
+ "button",
2297
+ {
2298
+ type: "button",
2299
+ className: "sk-pb__undo-btn",
2300
+ onClick: () => undoRef.current?.(),
2301
+ disabled: !storeState.canUndo,
2302
+ title: "R\xFCckg\xE4ngig (Cmd+Z)",
2303
+ children: /* @__PURE__ */ jsx16(Undo2, { size: 14 })
2304
+ }
2305
+ ),
2306
+ /* @__PURE__ */ jsx16(
2307
+ "button",
2308
+ {
2309
+ type: "button",
2310
+ className: "sk-pb__undo-btn",
2311
+ onClick: () => redoRef.current?.(),
2312
+ disabled: !storeState.canRedo,
2313
+ title: "Wiederholen (Cmd+Shift+Z)",
2314
+ children: /* @__PURE__ */ jsx16(Redo2, { size: 14 })
2315
+ }
2316
+ ),
2317
+ /* @__PURE__ */ jsx16(
2318
+ "button",
2319
+ {
2320
+ type: "button",
2321
+ className: "sk-pb__header-save-btn",
2322
+ onClick: () => saveRef.current?.(),
2323
+ disabled: saving || !storeState.dirty,
2324
+ title: "Speichern (Cmd+S)",
2325
+ children: /* @__PURE__ */ jsx16(Save, { size: 14 })
2326
+ }
2327
+ ),
2328
+ /* @__PURE__ */ jsx16("button", { type: "button", className: "sk-pb__collapse-btn", onClick: onCollapse, title: "Einklappen", children: /* @__PURE__ */ jsx16(PanelRightClose, { size: 16 }) }),
2329
+ /* @__PURE__ */ jsx16("button", { type: "button", className: "sk-pb__close-btn", onClick: onClose, children: /* @__PURE__ */ jsx16(X, { size: 18 }) })
2330
+ ] })
2331
+ ] }),
2332
+ /* @__PURE__ */ jsxs15("div", { className: "sk-pb__slide-over-body", children: [
2333
+ conflict && /* @__PURE__ */ jsxs15("div", { className: "sk-pb__conflict", children: [
2334
+ /* @__PURE__ */ jsxs15("div", { className: "sk-pb__conflict-header", children: [
2335
+ /* @__PURE__ */ jsx16(Shield, { size: 16 }),
2336
+ /* @__PURE__ */ jsx16("strong", { children: "Konflikt" }),
2337
+ " \u2014 Die Datei wurde extern ge\xE4ndert."
2338
+ ] }),
2339
+ /* @__PURE__ */ jsx16("div", { className: "sk-pb__conflict-diff", children: Object.keys({ ...conflict.local, ...conflict.remote }).filter((key) => JSON.stringify(conflict.local[key]) !== JSON.stringify(conflict.remote[key])).map((key) => {
2340
+ const fieldDef = section.fields[key];
2341
+ const label = fieldDef?.label ?? key;
2342
+ const localVal = JSON.stringify(conflict.local[key], null, 2) ?? "\u2014";
2343
+ const remoteVal = JSON.stringify(conflict.remote[key], null, 2) ?? "\u2014";
2344
+ return /* @__PURE__ */ jsxs15("div", { className: "sk-pb__conflict-field", children: [
2345
+ /* @__PURE__ */ jsx16("div", { className: "sk-pb__conflict-label", children: label }),
2346
+ /* @__PURE__ */ jsxs15("div", { className: "sk-pb__conflict-values", children: [
2347
+ /* @__PURE__ */ jsxs15("div", { className: "sk-pb__conflict-local", children: [
2348
+ /* @__PURE__ */ jsx16("span", { className: "sk-pb__conflict-tag", children: "Deine Version" }),
2349
+ /* @__PURE__ */ jsx16("pre", { children: localVal })
2350
+ ] }),
2351
+ /* @__PURE__ */ jsxs15("div", { className: "sk-pb__conflict-remote", children: [
2352
+ /* @__PURE__ */ jsx16("span", { className: "sk-pb__conflict-tag", children: "Remote" }),
2353
+ /* @__PURE__ */ jsx16("pre", { children: remoteVal })
2354
+ ] })
2355
+ ] })
2356
+ ] }, key);
2357
+ }) }),
2358
+ /* @__PURE__ */ jsxs15("div", { className: "sk-pb__conflict-actions", children: [
2359
+ /* @__PURE__ */ jsx16(
2360
+ "button",
2361
+ {
2362
+ type: "button",
2363
+ className: "sk-button sk-button--primary",
2364
+ onClick: resolveConflictLocal,
2365
+ disabled: saving,
2366
+ children: "Meine Version \xFCbernehmen"
2367
+ }
2368
+ ),
2369
+ /* @__PURE__ */ jsx16(
2370
+ "button",
2371
+ {
2372
+ type: "button",
2373
+ className: "sk-button",
2374
+ onClick: resolveConflictRemote,
2375
+ children: "Remote \xFCbernehmen"
2376
+ }
2377
+ )
2378
+ ] })
2379
+ ] }),
2380
+ loading ? /* @__PURE__ */ jsx16("p", { className: "sk-pb__slide-over-loading", children: "Lade..." }) : /* @__PURE__ */ jsx16(
2381
+ EntryForm,
2382
+ {
2383
+ schema: section.fields,
2384
+ initialValues,
2385
+ draftValues: cachedValues,
2386
+ onSave: handleSave,
2387
+ storeRef
2388
+ }
2389
+ )
2390
+ ] })
2391
+ ] });
2392
+ }
2393
+
2394
+ // src/components/admin-app.tsx
2395
+ import { Layers, Globe as Globe2, Settings as Settings2 } from "lucide-react";
2396
+ import { jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
2397
+ function AdminApp() {
2398
+ const config = useConfig();
2399
+ const [user, setUser] = useState9(null);
2400
+ const [checking, setChecking] = useState9(true);
2401
+ useEffect7(() => {
2402
+ checkSession();
2403
+ }, []);
2404
+ async function checkSession() {
2405
+ try {
2406
+ const response = await fetch("/api/setzkasten/auth/session");
2407
+ const session = await response.json();
2408
+ if (session.authenticated) {
2409
+ setUser(session.user);
2410
+ }
2411
+ } catch {
2412
+ } finally {
2413
+ setChecking(false);
2414
+ }
2415
+ }
2416
+ if (checking) {
2417
+ return /* @__PURE__ */ jsx17(LoadingScreen, {});
2418
+ }
2419
+ if (!user) {
2420
+ return /* @__PURE__ */ jsx17(LoginScreen, { config });
2421
+ }
2422
+ return /* @__PURE__ */ jsx17(ToastProvider, { children: /* @__PURE__ */ jsx17(AdminShell, { config, user }) });
2423
+ }
2424
+ function LoadingScreen() {
2425
+ return /* @__PURE__ */ jsxs16("div", { className: "sk-admin-loading", children: [
2426
+ /* @__PURE__ */ jsx17("div", { className: "sk-admin-loading__spinner" }),
2427
+ /* @__PURE__ */ jsx17("p", { className: "sk-admin-loading__text", children: "Lade Setzkasten..." })
2428
+ ] });
2429
+ }
2430
+ function LoginScreen({ config }) {
2431
+ const providers = config.auth?.providers ?? ["github"];
2432
+ return /* @__PURE__ */ jsx17("div", { className: "sk-login", children: /* @__PURE__ */ jsxs16("div", { className: "sk-login__card", children: [
2433
+ /* @__PURE__ */ jsx17("div", { className: "sk-login__logo", children: "S" }),
2434
+ /* @__PURE__ */ jsx17("h1", { className: "sk-login__title", children: config.theme?.brandName ?? "Setzkasten" }),
2435
+ /* @__PURE__ */ jsx17("p", { className: "sk-login__subtitle", children: "Melde dich an, um Inhalte zu bearbeiten." }),
2436
+ providers.includes("github") && /* @__PURE__ */ jsxs16("a", { href: "/api/setzkasten/auth/login?provider=github", className: "sk-login__btn", children: [
2437
+ /* @__PURE__ */ jsx17("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx17("path", { d: "M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844a9.59 9.59 0 012.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0022 12.017C22 6.484 17.522 2 12 2Z" }) }),
2438
+ "Mit GitHub anmelden"
2439
+ ] }),
2440
+ providers.includes("google") && /* @__PURE__ */ jsxs16("a", { href: "/api/setzkasten/auth/login?provider=google", className: "sk-login__btn", children: [
2441
+ /* @__PURE__ */ jsxs16("svg", { viewBox: "0 0 24 24", width: "20", height: "20", children: [
2442
+ /* @__PURE__ */ jsx17("path", { d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z", fill: "#4285F4" }),
2443
+ /* @__PURE__ */ jsx17("path", { d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z", fill: "#34A853" }),
2444
+ /* @__PURE__ */ jsx17("path", { d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z", fill: "#FBBC05" }),
2445
+ /* @__PURE__ */ jsx17("path", { d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", fill: "#EA4335" })
2446
+ ] }),
2447
+ "Mit Google anmelden"
2448
+ ] })
2449
+ ] }) });
2450
+ }
2451
+ function AdminShell({ config, user }) {
2452
+ const [view, setView] = useState9("dashboard");
2453
+ const [pages, setPages] = useState9([]);
2454
+ const [selectedPageKey, setSelectedPageKey] = useState9("index");
2455
+ const [loadingPages, setLoadingPages] = useState9(true);
2456
+ useEffect7(() => {
2457
+ fetch("/api/setzkasten/pages").then((r) => r.json()).then((data) => {
2458
+ setPages(data.pages ?? []);
2459
+ }).catch(() => {
2460
+ }).finally(() => setLoadingPages(false));
2461
+ }, []);
2462
+ if (view === "page-builder") {
2463
+ return /* @__PURE__ */ jsx17(
2464
+ PageBuilder,
2465
+ {
2466
+ pageKey: selectedPageKey,
2467
+ pages,
2468
+ onPageChange: setSelectedPageKey,
2469
+ onExit: () => setView("dashboard")
2470
+ }
2471
+ );
2472
+ }
2473
+ return /* @__PURE__ */ jsx17(
2474
+ Dashboard,
2475
+ {
2476
+ config,
2477
+ user,
2478
+ pages,
2479
+ loadingPages,
2480
+ selectedPageKey,
2481
+ onSelectPage: setSelectedPageKey,
2482
+ onOpenPageBuilder: () => setView("page-builder")
2483
+ }
2484
+ );
2485
+ }
2486
+ function Dashboard({
2487
+ config,
2488
+ user,
2489
+ pages,
2490
+ loadingPages,
2491
+ selectedPageKey,
2492
+ onSelectPage,
2493
+ onOpenPageBuilder
2494
+ }) {
2495
+ const brandName = config.theme?.brandName ?? "Setzkasten";
2496
+ return /* @__PURE__ */ jsxs16("div", { className: "sk-dashboard", children: [
2497
+ /* @__PURE__ */ jsxs16("header", { className: "sk-dashboard__topbar", children: [
2498
+ /* @__PURE__ */ jsxs16("div", { className: "sk-dashboard__brand", children: [
2499
+ /* @__PURE__ */ jsx17("span", { className: "sk-dashboard__logo", children: "S" }),
2500
+ /* @__PURE__ */ jsx17("span", { className: "sk-dashboard__brand-name", children: "Setzkasten" })
2501
+ ] }),
2502
+ /* @__PURE__ */ jsxs16("div", { className: "sk-dashboard__user", children: [
2503
+ user.avatarUrl ? /* @__PURE__ */ jsx17("img", { src: user.avatarUrl, className: "sk-dashboard__avatar", alt: "" }) : /* @__PURE__ */ jsx17("span", { className: "sk-dashboard__avatar sk-dashboard__avatar--placeholder", children: (user.name ?? user.email ?? "?")[0].toUpperCase() }),
2504
+ /* @__PURE__ */ jsx17("span", { className: "sk-dashboard__user-name", children: user.name || user.email }),
2505
+ /* @__PURE__ */ jsx17("a", { href: "/api/setzkasten/auth/logout", className: "sk-dashboard__logout", children: "Abmelden" })
2506
+ ] })
2507
+ ] }),
2508
+ /* @__PURE__ */ jsxs16("main", { className: "sk-dashboard__content", children: [
2509
+ /* @__PURE__ */ jsxs16("div", { className: "sk-dashboard__card sk-dashboard__card--primary", children: [
2510
+ /* @__PURE__ */ jsx17("div", { className: "sk-dashboard__card-icon", children: /* @__PURE__ */ jsx17(Layers, { size: 28 }) }),
2511
+ /* @__PURE__ */ jsx17("h2", { className: "sk-dashboard__card-title", children: "Zum Setzkasten f\xFCr" }),
2512
+ /* @__PURE__ */ jsxs16("div", { className: "sk-dashboard__card-controls", children: [
2513
+ /* @__PURE__ */ jsx17(
2514
+ "select",
2515
+ {
2516
+ className: "sk-dashboard__select",
2517
+ disabled: true,
2518
+ value: brandName,
2519
+ children: /* @__PURE__ */ jsx17("option", { children: brandName })
2520
+ }
2521
+ ),
2522
+ /* @__PURE__ */ jsx17(
2523
+ "select",
2524
+ {
2525
+ className: "sk-dashboard__select",
2526
+ value: selectedPageKey,
2527
+ onChange: (e) => onSelectPage(e.target.value),
2528
+ disabled: loadingPages || pages.length === 0,
2529
+ children: loadingPages ? /* @__PURE__ */ jsx17("option", { children: "Lade..." }) : pages.length === 0 ? /* @__PURE__ */ jsx17("option", { value: "index", children: "Startseite" }) : pages.map((p) => /* @__PURE__ */ jsx17("option", { value: p.pageKey, children: p.label }, p.pageKey))
2530
+ }
2531
+ ),
2532
+ /* @__PURE__ */ jsx17(
2533
+ "button",
2534
+ {
2535
+ type: "button",
2536
+ className: "sk-dashboard__btn",
2537
+ onClick: onOpenPageBuilder,
2538
+ children: "\xD6ffnen"
2539
+ }
2540
+ )
2541
+ ] })
2542
+ ] }),
2543
+ /* @__PURE__ */ jsxs16("div", { className: "sk-dashboard__cards-row", children: [
2544
+ /* @__PURE__ */ jsxs16("div", { className: "sk-dashboard__card sk-dashboard__card--disabled", children: [
2545
+ /* @__PURE__ */ jsx17("div", { className: "sk-dashboard__card-icon", children: /* @__PURE__ */ jsx17(Globe2, { size: 22 }) }),
2546
+ /* @__PURE__ */ jsx17("h3", { className: "sk-dashboard__card-subtitle", children: "Websites verwalten" }),
2547
+ /* @__PURE__ */ jsx17("span", { className: "sk-dashboard__badge", children: "Kommt bald" })
2548
+ ] }),
2549
+ /* @__PURE__ */ jsxs16("div", { className: "sk-dashboard__card sk-dashboard__card--disabled", children: [
2550
+ /* @__PURE__ */ jsx17("div", { className: "sk-dashboard__card-icon", children: /* @__PURE__ */ jsx17(Settings2, { size: 22 }) }),
2551
+ /* @__PURE__ */ jsx17("h3", { className: "sk-dashboard__card-subtitle", children: "Globale Konfiguration" }),
2552
+ /* @__PURE__ */ jsx17("span", { className: "sk-dashboard__badge", children: "Kommt bald" })
2553
+ ] })
2554
+ ] })
2555
+ ] })
2556
+ ] });
2557
+ }
2558
+
2559
+ // src/hooks/use-save.ts
2560
+ import { useCallback as useCallback10, useState as useState10 } from "react";
2561
+ function useSave(store, collection, slug) {
2562
+ const repository = useRepository();
2563
+ const eventBus = useEventBus();
2564
+ const [saving, setSaving] = useState10(false);
2565
+ const save = useCallback10(async () => {
2566
+ const state = store.getState();
2567
+ if (!state.isDirty()) {
2568
+ return { ok: true, value: { sha: "", message: "No changes" } };
2569
+ }
2570
+ setSaving(true);
2571
+ state.setStatus("saving");
2572
+ const result = await repository.saveEntry(collection, slug, {
2573
+ content: state.values
2574
+ });
2575
+ if (result.ok) {
2576
+ state.init(state.schema, state.values);
2577
+ state.setStatus("idle");
2578
+ eventBus.emit({ type: "entry-saved", collection, slug });
2579
+ } else {
2580
+ state.setStatus("error", result.error.message);
2581
+ }
2582
+ setSaving(false);
2583
+ return result;
2584
+ }, [store, repository, eventBus, collection, slug]);
2585
+ return { save, saving };
2586
+ }
2587
+
2588
+ // src/adapters/proxy-content-repository.ts
2589
+ import {
2590
+ ok,
2591
+ err,
2592
+ networkError,
2593
+ conflictError
2594
+ } from "@setzkasten-cms/core";
2595
+ var ProxyContentRepository = class {
2596
+ baseUrl;
2597
+ owner;
2598
+ repo;
2599
+ branch;
2600
+ contentPath;
2601
+ constructor(config) {
2602
+ this.baseUrl = config.proxyBaseUrl.replace(/\/$/, "");
2603
+ this.owner = config.owner;
2604
+ this.repo = config.repo;
2605
+ this.branch = config.branch;
2606
+ this.contentPath = config.contentPath ?? "src/content";
2607
+ }
2608
+ apiUrl(path) {
2609
+ return `${this.baseUrl}/repos/${this.owner}/${this.repo}/${path}`;
2610
+ }
2611
+ contentFilePath(collection, slug) {
2612
+ return `${this.contentPath}/${collection}/${slug}.json`;
2613
+ }
2614
+ async listEntries(collection) {
2615
+ try {
2616
+ const dirPath = `${this.contentPath}/${collection}`;
2617
+ const response = await fetch(
2618
+ this.apiUrl(`contents/${dirPath}?ref=${this.branch}`)
2619
+ );
2620
+ if (!response.ok) {
2621
+ if (response.status === 404) return ok([]);
2622
+ return err(networkError(`HTTP ${response.status}`, null));
2623
+ }
2624
+ const data = await response.json();
2625
+ return ok(
2626
+ data.filter((item) => item.name.endsWith(".json")).map((item) => ({
2627
+ slug: item.name.replace(/\.json$/, ""),
2628
+ name: item.name.replace(/\.json$/, "")
2629
+ }))
2630
+ );
2631
+ } catch (error) {
2632
+ return err(networkError(error instanceof Error ? error.message : "Network error", error));
2633
+ }
2634
+ }
2635
+ async getEntry(collection, slug) {
2636
+ try {
2637
+ const filePath = this.contentFilePath(collection, slug);
2638
+ const response = await fetch(
2639
+ this.apiUrl(`contents/${filePath}?ref=${this.branch}`)
2640
+ );
2641
+ if (!response.ok) {
2642
+ return err(networkError(`HTTP ${response.status}`, null));
2643
+ }
2644
+ const data = await response.json();
2645
+ const binary = atob(data.content.replace(/\n/g, ""));
2646
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
2647
+ const decoded = new TextDecoder().decode(bytes);
2648
+ const content = JSON.parse(decoded);
2649
+ return ok({ content, sha: data.sha });
2650
+ } catch (error) {
2651
+ return err(networkError(error instanceof Error ? error.message : "Network error", error));
2652
+ }
2653
+ }
2654
+ async saveEntry(collection, slug, data, assets) {
2655
+ try {
2656
+ if (assets && assets.length > 0) {
2657
+ for (const asset of assets) {
2658
+ const assetResult = await this.uploadAsset(asset);
2659
+ if (!assetResult.ok) return assetResult;
2660
+ }
2661
+ }
2662
+ const filePath = this.contentFilePath(collection, slug);
2663
+ const jsonContent = JSON.stringify(data.content, null, 2);
2664
+ const base64Content = btoa(unescape(encodeURIComponent(jsonContent)));
2665
+ const body = {
2666
+ message: `Update ${collection}/${slug}`,
2667
+ content: base64Content,
2668
+ branch: this.branch
2669
+ };
2670
+ if (data.sha) {
2671
+ body.sha = data.sha;
2672
+ }
2673
+ const response = await fetch(this.apiUrl(`contents/${filePath}`), {
2674
+ method: "PUT",
2675
+ headers: { "Content-Type": "application/json" },
2676
+ body: JSON.stringify(body)
2677
+ });
2678
+ if (!response.ok) {
2679
+ if (response.status === 409) {
2680
+ return err(conflictError("Concurrent edit detected. The file was modified since you loaded it."));
2681
+ }
2682
+ const text = await response.text();
2683
+ return err(networkError(`Save failed: HTTP ${response.status} - ${text}`, null));
2684
+ }
2685
+ const result = await response.json();
2686
+ return ok({
2687
+ sha: result.content.sha,
2688
+ message: `Update ${collection}/${slug}`,
2689
+ url: result.commit.html_url
2690
+ });
2691
+ } catch (error) {
2692
+ return err(networkError(error instanceof Error ? error.message : "Save failed", error));
2693
+ }
2694
+ }
2695
+ async deleteEntry(collection, slug) {
2696
+ try {
2697
+ const entryResult = await this.getEntry(collection, slug);
2698
+ if (!entryResult.ok) return entryResult;
2699
+ const filePath = this.contentFilePath(collection, slug);
2700
+ const response = await fetch(this.apiUrl(`contents/${filePath}`), {
2701
+ method: "DELETE",
2702
+ headers: { "Content-Type": "application/json" },
2703
+ body: JSON.stringify({
2704
+ message: `Delete ${collection}/${slug}`,
2705
+ sha: entryResult.value.sha,
2706
+ branch: this.branch
2707
+ })
2708
+ });
2709
+ if (!response.ok) {
2710
+ return err(networkError(`Delete failed: HTTP ${response.status}`, null));
2711
+ }
2712
+ const result = await response.json();
2713
+ return ok({
2714
+ sha: result.commit.sha,
2715
+ message: `Delete ${collection}/${slug}`
2716
+ });
2717
+ } catch (error) {
2718
+ return err(networkError(error instanceof Error ? error.message : "Delete failed", error));
2719
+ }
2720
+ }
2721
+ async getTree(ref) {
2722
+ try {
2723
+ const response = await fetch(
2724
+ this.apiUrl(`git/trees/${ref ?? this.branch}?recursive=1`)
2725
+ );
2726
+ if (!response.ok) {
2727
+ return err(networkError(`HTTP ${response.status}`, null));
2728
+ }
2729
+ const data = await response.json();
2730
+ return ok(
2731
+ data.tree.filter((item) => item.path.startsWith(this.contentPath)).map((item) => ({
2732
+ path: item.path,
2733
+ type: item.type === "tree" ? "dir" : "file",
2734
+ sha: item.sha
2735
+ }))
2736
+ );
2737
+ } catch (error) {
2738
+ return err(networkError(error instanceof Error ? error.message : "Network error", error));
2739
+ }
2740
+ }
2741
+ async uploadAsset(asset) {
2742
+ try {
2743
+ const base64Content = this.uint8ToBase64(asset.content);
2744
+ const response = await fetch(this.apiUrl(`contents/${asset.path}`), {
2745
+ method: "PUT",
2746
+ headers: { "Content-Type": "application/json" },
2747
+ body: JSON.stringify({
2748
+ message: `Upload ${asset.path}`,
2749
+ content: base64Content,
2750
+ branch: this.branch
2751
+ })
2752
+ });
2753
+ if (!response.ok) {
2754
+ return err(networkError(`Asset upload failed: HTTP ${response.status}`, null));
2755
+ }
2756
+ const result = await response.json();
2757
+ return ok({ sha: result.content.sha });
2758
+ } catch (error) {
2759
+ return err(networkError(error instanceof Error ? error.message : "Upload failed", error));
2760
+ }
2761
+ }
2762
+ uint8ToBase64(bytes) {
2763
+ let binary = "";
2764
+ for (let i = 0; i < bytes.length; i++) {
2765
+ binary += String.fromCharCode(bytes[i]);
2766
+ }
2767
+ return btoa(binary);
2768
+ }
2769
+ };
2770
+
2771
+ // src/adapters/proxy-asset-store.ts
2772
+ import {
2773
+ ok as ok2,
2774
+ err as err2,
2775
+ networkError as networkError2
2776
+ } from "@setzkasten-cms/core";
2777
+ var ProxyAssetStore = class {
2778
+ baseUrl;
2779
+ owner;
2780
+ repo;
2781
+ branch;
2782
+ assetsPath;
2783
+ publicUrlPrefix;
2784
+ constructor(config) {
2785
+ this.baseUrl = config.proxyBaseUrl.replace(/\/$/, "");
2786
+ this.owner = config.owner;
2787
+ this.repo = config.repo;
2788
+ this.branch = config.branch;
2789
+ this.assetsPath = config.assetsPath ?? "public/images";
2790
+ this.publicUrlPrefix = config.publicUrlPrefix ?? "/images";
2791
+ }
2792
+ apiUrl(path) {
2793
+ return `${this.baseUrl}/repos/${this.owner}/${this.repo}/${path}`;
2794
+ }
2795
+ async upload(directory, filename, content, mimeType) {
2796
+ try {
2797
+ const dir = directory.replace(/^\/+|\/+$/g, "");
2798
+ const repoPath = `${this.assetsPath}/${dir}/${filename}`;
2799
+ const base64Content = this.uint8ToBase64(content);
2800
+ const response = await fetch(this.apiUrl(`contents/${repoPath}`), {
2801
+ method: "PUT",
2802
+ headers: { "Content-Type": "application/json" },
2803
+ body: JSON.stringify({
2804
+ message: `Upload ${filename}`,
2805
+ content: base64Content,
2806
+ branch: this.branch
2807
+ })
2808
+ });
2809
+ if (!response.ok) {
2810
+ const text = await response.text();
2811
+ return err2(networkError2(`Upload failed: HTTP ${response.status} - ${text}`, null));
2812
+ }
2813
+ return ok2({
2814
+ path: repoPath,
2815
+ size: content.byteLength,
2816
+ mimeType
2817
+ });
2818
+ } catch (error) {
2819
+ return err2(networkError2(error instanceof Error ? error.message : "Upload failed", error));
2820
+ }
2821
+ }
2822
+ async delete(path) {
2823
+ try {
2824
+ const fileResponse = await fetch(
2825
+ this.apiUrl(`contents/${path}?ref=${this.branch}`)
2826
+ );
2827
+ if (!fileResponse.ok) {
2828
+ return err2(networkError2(`File not found: ${path}`, null));
2829
+ }
2830
+ const fileData = await fileResponse.json();
2831
+ const response = await fetch(this.apiUrl(`contents/${path}`), {
2832
+ method: "DELETE",
2833
+ headers: { "Content-Type": "application/json" },
2834
+ body: JSON.stringify({
2835
+ message: `Delete ${path}`,
2836
+ sha: fileData.sha,
2837
+ branch: this.branch
2838
+ })
2839
+ });
2840
+ if (!response.ok) {
2841
+ return err2(networkError2(`Delete failed: HTTP ${response.status}`, null));
2842
+ }
2843
+ return ok2(void 0);
2844
+ } catch (error) {
2845
+ return err2(networkError2(error instanceof Error ? error.message : "Delete failed", error));
2846
+ }
2847
+ }
2848
+ async list(directory) {
2849
+ try {
2850
+ const dirPath = `${this.assetsPath}/${directory}`.replace(/\/+$/, "");
2851
+ return await this.listRecursive(dirPath);
2852
+ } catch (error) {
2853
+ return err2(networkError2(error instanceof Error ? error.message : "List failed", error));
2854
+ }
2855
+ }
2856
+ async listRecursive(dirPath) {
2857
+ const response = await fetch(
2858
+ this.apiUrl(`contents/${dirPath}?ref=${this.branch}`)
2859
+ );
2860
+ if (!response.ok) {
2861
+ if (response.status === 404) return ok2([]);
2862
+ return err2(networkError2(`HTTP ${response.status}`, null));
2863
+ }
2864
+ const data = await response.json();
2865
+ const files = data.filter((item) => item.type === "file" && this.isImageFile(item.name)).map((item) => ({
2866
+ path: item.path,
2867
+ size: item.size,
2868
+ mimeType: this.guessMimeType(item.name)
2869
+ }));
2870
+ const dirs = data.filter((item) => item.type === "dir");
2871
+ for (const dir of dirs) {
2872
+ const subResult = await this.listRecursive(dir.path);
2873
+ if (subResult.ok) {
2874
+ files.push(...subResult.value);
2875
+ }
2876
+ }
2877
+ return ok2(files);
2878
+ }
2879
+ isImageFile(name) {
2880
+ const ext = name.split(".").pop()?.toLowerCase() ?? "";
2881
+ return ["jpg", "jpeg", "png", "gif", "webp", "avif", "svg"].includes(ext);
2882
+ }
2883
+ getUrl(path) {
2884
+ if (path.startsWith(this.assetsPath)) {
2885
+ return this.publicUrlPrefix + path.slice(this.assetsPath.length);
2886
+ }
2887
+ return path;
2888
+ }
2889
+ getPreviewUrl(path) {
2890
+ const repoPath = path.startsWith(this.publicUrlPrefix) ? this.assetsPath + path.slice(this.publicUrlPrefix.length) : path;
2891
+ return `/api/setzkasten/asset/${repoPath}`;
2892
+ }
2893
+ uint8ToBase64(bytes) {
2894
+ let binary = "";
2895
+ for (let i = 0; i < bytes.length; i++) {
2896
+ binary += String.fromCharCode(bytes[i]);
2897
+ }
2898
+ return btoa(binary);
2899
+ }
2900
+ guessMimeType(filename) {
2901
+ const ext = filename.split(".").pop()?.toLowerCase();
2902
+ const mimeTypes = {
2903
+ jpg: "image/jpeg",
2904
+ jpeg: "image/jpeg",
2905
+ png: "image/png",
2906
+ gif: "image/gif",
2907
+ webp: "image/webp",
2908
+ avif: "image/avif",
2909
+ svg: "image/svg+xml",
2910
+ pdf: "application/pdf"
2911
+ };
2912
+ return mimeTypes[ext ?? ""] ?? "application/octet-stream";
2913
+ }
2914
+ };
2915
+ export {
2916
+ AdminApp,
2917
+ CollectionView,
2918
+ EntryForm,
2919
+ EntryList,
2920
+ FieldRenderer,
2921
+ ProxyAssetStore,
2922
+ ProxyContentRepository,
2923
+ SetzKastenProvider,
2924
+ ToastProvider,
2925
+ createAppStore,
2926
+ createFormStore,
2927
+ useAssets,
2928
+ useAuth,
2929
+ useConfig,
2930
+ useEventBus,
2931
+ useField,
2932
+ useRepository,
2933
+ useSave,
2934
+ useSetzKasten,
2935
+ useToast
2936
+ };