@xiwebdesign/xicms-core 7.2.0 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/XiCMSProvider.d.ts +2 -8
  2. package/dist/XiCMSProvider.js +1 -8
  3. package/dist/XiCMSProvider.js.map +1 -1
  4. package/dist/admin/AdminApp.js +7 -12
  5. package/dist/admin/AdminApp.js.map +1 -1
  6. package/dist/admin/NewsEditor.d.ts +8 -1
  7. package/dist/admin/NewsEditor.js +19 -18
  8. package/dist/admin/NewsEditor.js.map +1 -1
  9. package/dist/admin/SuperadminPanel.js +6 -7
  10. package/dist/admin/SuperadminPanel.js.map +1 -1
  11. package/dist/admin/customBlocks/CustomBlocksPanel.js +3 -4
  12. package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
  13. package/dist/admin/lib/pagesClient.js +3 -4
  14. package/dist/admin/lib/pagesClient.js.map +1 -1
  15. package/dist/admin/lib/symbolsClient.js +3 -4
  16. package/dist/admin/lib/symbolsClient.js.map +1 -1
  17. package/dist/admin/pages/PagesPanel.js +5 -2
  18. package/dist/admin/pages/PagesPanel.js.map +1 -1
  19. package/dist/admin/settings/MailSettings.js +2 -3
  20. package/dist/admin/settings/MailSettings.js.map +1 -1
  21. package/dist/admin/settings/MailTemplates.js +2 -3
  22. package/dist/admin/settings/MailTemplates.js.map +1 -1
  23. package/dist/admin/settings/SettingsPanel.js +250 -225
  24. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  25. package/dist/admin/settings/SnapshotsCard.js +3 -4
  26. package/dist/admin/settings/SnapshotsCard.js.map +1 -1
  27. package/dist/admin/shop/lib.js +2 -3
  28. package/dist/admin/shop/lib.js.map +1 -1
  29. package/dist/admin/visual/VisualEditor.js +239 -103
  30. package/dist/admin/visual/VisualEditor.js.map +1 -1
  31. package/dist/admin.css +1 -1
  32. package/dist/blocks/builtins/Agenda.js +6 -6
  33. package/dist/blocks/builtins/Agenda.js.map +1 -1
  34. package/dist/blocks/builtins/Banner.js +1 -1
  35. package/dist/blocks/builtins/Banner.js.map +1 -1
  36. package/dist/blocks/builtins/Breadcrumbs.js +5 -2
  37. package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
  38. package/dist/blocks/builtins/Cards.js +2 -2
  39. package/dist/blocks/builtins/Cards.js.map +1 -1
  40. package/dist/blocks/builtins/Contact.js +3 -3
  41. package/dist/blocks/builtins/Contact.js.map +1 -1
  42. package/dist/blocks/builtins/Cta.js +3 -3
  43. package/dist/blocks/builtins/Cta.js.map +1 -1
  44. package/dist/blocks/builtins/Downloads.js +5 -5
  45. package/dist/blocks/builtins/Downloads.js.map +1 -1
  46. package/dist/blocks/builtins/Elements.js +8 -5
  47. package/dist/blocks/builtins/Elements.js.map +1 -1
  48. package/dist/blocks/builtins/Hero.js +1 -1
  49. package/dist/blocks/builtins/Hero.js.map +1 -1
  50. package/dist/blocks/builtins/Jobs.js +4 -1
  51. package/dist/blocks/builtins/Jobs.js.map +1 -1
  52. package/dist/blocks/builtins/MapBlock.js +11 -11
  53. package/dist/blocks/builtins/MapBlock.js.map +1 -1
  54. package/dist/blocks/builtins/Newsletter.js +3 -2
  55. package/dist/blocks/builtins/Newsletter.js.map +1 -1
  56. package/dist/blocks/builtins/OpeningHours.js +2 -2
  57. package/dist/blocks/builtins/OpeningHours.js.map +1 -1
  58. package/dist/blocks/builtins/PdfBlock.js +2 -2
  59. package/dist/blocks/builtins/PdfBlock.js.map +1 -1
  60. package/dist/blocks/builtins/Portfolio.js +4 -4
  61. package/dist/blocks/builtins/Portfolio.js.map +1 -1
  62. package/dist/blocks/builtins/Pricing.js +3 -3
  63. package/dist/blocks/builtins/Pricing.js.map +1 -1
  64. package/dist/blocks/builtins/Sections.js +7 -7
  65. package/dist/blocks/builtins/Sections.js.map +1 -1
  66. package/dist/blocks/builtins/Team.js +3 -3
  67. package/dist/blocks/builtins/Team.js.map +1 -1
  68. package/dist/blocks/builtins/Widgets.js +28 -28
  69. package/dist/blocks/builtins/Widgets.js.map +1 -1
  70. package/dist/blocks/builtins/lib/submitForm.js +3 -4
  71. package/dist/blocks/builtins/lib/submitForm.js.map +1 -1
  72. package/dist/blocks/ui/index.d.ts +8 -1
  73. package/dist/blocks/ui/index.js +3 -2
  74. package/dist/blocks/ui/index.js.map +1 -1
  75. package/dist/components/EditBar.d.ts +9 -1
  76. package/dist/components/EditBar.js +6 -5
  77. package/dist/components/EditBar.js.map +1 -1
  78. package/dist/components/PageView.js +3 -5
  79. package/dist/components/PageView.js.map +1 -1
  80. package/dist/firebase/admin.js +4 -0
  81. package/dist/firebase/admin.js.map +1 -1
  82. package/dist/firebase/client.js +23 -5
  83. package/dist/firebase/client.js.map +1 -1
  84. package/dist/firebase/emulators.d.ts +9 -0
  85. package/dist/firebase/emulators.js +24 -0
  86. package/dist/firebase/emulators.js.map +1 -0
  87. package/dist/firebase/functionsClient.d.ts +18 -0
  88. package/dist/firebase/functionsClient.js +17 -0
  89. package/dist/firebase/functionsClient.js.map +1 -0
  90. package/dist/lock/DemoLock.js +19 -2
  91. package/dist/lock/DemoLock.js.map +1 -1
  92. package/dist/modules/shop/cartStore.js +2 -1
  93. package/dist/modules/shop/cartStore.js.map +1 -1
  94. package/dist/modules/shop/lib.js +9 -10
  95. package/dist/modules/shop/lib.js.map +1 -1
  96. package/dist/server/index.d.ts +1 -1
  97. package/dist/server/index.js +2 -0
  98. package/dist/server/index.js.map +1 -1
  99. package/dist/server/pages.d.ts +11 -0
  100. package/dist/server/pages.js +47 -0
  101. package/dist/server/pages.js.map +1 -1
  102. package/dist/styles.css +130 -29
  103. package/dist/theme/color.d.ts +17 -0
  104. package/dist/theme/color.js +12 -0
  105. package/dist/theme/color.js.map +1 -0
  106. package/dist/version.js +1 -1
  107. package/package.json +1 -1
@@ -38,10 +38,14 @@ const GLOBAL_INLINE = [
38
38
  { selector: "[data-xicms-edit]", field: "", html: false, fromData: true }
39
39
  ];
40
40
  const inlineCoverageWarned = /* @__PURE__ */ new Set();
41
- function warnInlineCoverage(type, def, host) {
41
+ function warnInlineCoverage(type, def, host, values) {
42
42
  if (process.env.NODE_ENV === "production" || !def || inlineCoverageWarned.has(type)) return;
43
43
  inlineCoverageWarned.add(type);
44
- const missing = introspectToFields(def.schema, def.fields).filter((f) => f.type === "text" || f.type === "textarea").map((f) => f.key).filter((key) => !host.querySelector(`[data-xicms-edit="${key}"], [data-xicms-edit$=".${key}"]`));
44
+ const zichtbaar = host.textContent ?? "";
45
+ const missing = introspectToFields(def.schema, def.fields).filter((f) => f.type === "text" || f.type === "textarea").filter((f) => {
46
+ const waarde = getPath(values, f.key);
47
+ return typeof waarde === "string" && waarde.trim().length > 1 && zichtbaar.includes(waarde.trim());
48
+ }).map((f) => f.key).filter((key) => !host.querySelector(`[data-xicms-edit="${key}"], [data-xicms-edit$=".${key}"]`));
45
49
  if (missing.length > 0) {
46
50
  console.warn(
47
51
  `[xicms] blok "${type}": tekstveld(en) zonder inline-haak: ${missing.join(", ")} \u2014 voeg data-xicms-edit="<veld>" toe aan de Render zodat ze direct op de pagina bewerkbaar zijn.`
@@ -93,6 +97,7 @@ function VisualEditorInner({ onClose }) {
93
97
  const [locale, setLocale] = useState(urlLocale);
94
98
  const isTranslating = locale !== localeCfg.defaultLocale;
95
99
  const [draft, setDraft] = useState(null);
100
+ const [panelOpen, setPanelOpen] = useState(false);
96
101
  const [status, setStatus] = useState("loading");
97
102
  const [selectedKey, setSelectedKey] = useState(null);
98
103
  const [publishing, setPublishing] = useState(false);
@@ -245,6 +250,8 @@ function VisualEditorInner({ onClose }) {
245
250
  });
246
251
  }, [siteId, draftId]);
247
252
  const blocks = draft?.blocks ?? [];
253
+ const blocksRef = useRef(blocks);
254
+ blocksRef.current = blocks;
248
255
  const overlay = draft?.translations?.[locale];
249
256
  const displayBlocks = useMemo(
250
257
  () => isTranslating ? localizeBlocks(blocks, overlay) : blocks,
@@ -305,21 +312,59 @@ function VisualEditorInner({ onClose }) {
305
312
  },
306
313
  [isTranslating, setTranslation, commitInline]
307
314
  );
315
+ const [spots, setSpots] = useState([]);
316
+ const targetsRef = useRef([]);
317
+ const editingRef = useRef(null);
318
+ const stopInline = useCallback((el) => {
319
+ el.removeAttribute("contenteditable");
320
+ el.classList.remove("is-editing");
321
+ if (editingRef.current === el) editingRef.current = null;
322
+ }, []);
323
+ const startInline = useCallback((index) => {
324
+ const target = targetsRef.current[index];
325
+ if (!target) return;
326
+ const { el } = target;
327
+ if (editingRef.current && editingRef.current !== el) editingRef.current.blur();
328
+ el.setAttribute("contenteditable", "true");
329
+ el.setAttribute("spellcheck", "true");
330
+ el.classList.add("is-editing");
331
+ editingRef.current = el;
332
+ el.focus();
333
+ const selection = window.getSelection();
334
+ const range = document.createRange();
335
+ range.selectNodeContents(el);
336
+ range.collapse(false);
337
+ selection?.removeAllRanges();
338
+ selection?.addRange(range);
339
+ }, []);
308
340
  useEffect(() => {
309
- if (!selectedKey) return;
341
+ if (!selectedKey) {
342
+ targetsRef.current = [];
343
+ setSpots([]);
344
+ return;
345
+ }
310
346
  const host = canvasRef.current?.querySelector(`[data-ve-content="${selectedKey}"]`);
311
347
  if (!host) return;
312
- const type = blocks.find((b) => b._key === selectedKey)?._type ?? "";
313
- const specs = [...GLOBAL_INLINE];
314
- warnInlineCoverage(String(type), registry.get(String(type)), host);
348
+ const geselecteerd = blocksRef.current.find((b) => b._key === selectedKey);
349
+ const type = geselecteerd?._type ?? "";
350
+ const def = registry.get(String(type));
351
+ warnInlineCoverage(String(type), def, host, geselecteerd ?? {});
352
+ const labels = /* @__PURE__ */ new Map();
353
+ if (def) {
354
+ try {
355
+ for (const f of introspectToFields(def.schema, def.fields)) labels.set(f.key, f.label);
356
+ } catch {
357
+ }
358
+ }
315
359
  const bound = /* @__PURE__ */ new Set();
316
360
  const bind = (el, field, html) => {
317
361
  if (el.dataset.veInline === "1") return;
318
362
  el.dataset.veInline = "1";
319
- el.setAttribute("contenteditable", "true");
320
- el.setAttribute("spellcheck", "true");
321
363
  el.classList.add("xicms-ve-inline");
322
- el.addEventListener("blur", () => commitText(selectedKey, field, html ? DOMPurify.sanitize(el.innerHTML) : el.textContent ?? ""));
364
+ el.addEventListener("blur", () => {
365
+ commitText(selectedKey, field, html ? DOMPurify.sanitize(el.innerHTML) : el.textContent ?? "");
366
+ stopInline(el);
367
+ });
323
368
  if (html) {
324
369
  el.addEventListener("focus", () => {
325
370
  fmtElRef.current = el;
@@ -339,30 +384,57 @@ function VisualEditorInner({ onClose }) {
339
384
  });
340
385
  bound.add(el);
341
386
  };
387
+ const measure = () => {
388
+ const next = targetsRef.current.map(({ el, field }) => {
389
+ const r = el.getBoundingClientRect();
390
+ return {
391
+ top: Math.min(window.innerHeight - 22, Math.max(4, r.top + r.height / 2 - 9)),
392
+ left: Math.min(window.innerWidth - 22, Math.max(4, r.right + 6)),
393
+ title: `Bewerk: ${labels.get(field) ?? field}`
394
+ };
395
+ });
396
+ setSpots(
397
+ (prev) => prev.length === next.length && prev.every((p, i) => p.top === next[i].top && p.left === next[i].left && p.title === next[i].title) ? prev : next
398
+ );
399
+ };
342
400
  const apply = () => {
343
- for (const m of specs) {
344
- host.querySelectorAll(m.selector).forEach(
345
- (el) => (
346
- // Een expliciete `data-xicms-edit` wint altijd; anders het veld van de matcher.
347
- bind(el, el.dataset.xicmsEdit ?? m.field, !!m.html)
348
- )
349
- );
401
+ const found = [];
402
+ for (const m of GLOBAL_INLINE) {
403
+ host.querySelectorAll(m.selector).forEach((el) => {
404
+ const field = el.dataset.xicmsEdit ?? m.field;
405
+ bind(el, field, !!m.html);
406
+ found.push({ el, field, html: !!m.html });
407
+ });
350
408
  }
409
+ targetsRef.current = found;
410
+ measure();
351
411
  };
352
412
  apply();
353
413
  const obs = new MutationObserver(() => {
354
414
  if (!draggingRef.current) apply();
355
415
  });
356
416
  obs.observe(host, { childList: true, subtree: true });
417
+ let frame = 0;
418
+ const onMove = () => {
419
+ cancelAnimationFrame(frame);
420
+ frame = requestAnimationFrame(measure);
421
+ };
422
+ window.addEventListener("scroll", onMove, { capture: true, passive: true });
423
+ window.addEventListener("resize", onMove, { passive: true });
357
424
  return () => {
358
425
  obs.disconnect();
426
+ cancelAnimationFrame(frame);
427
+ window.removeEventListener("scroll", onMove, { capture: true });
428
+ window.removeEventListener("resize", onMove);
359
429
  bound.forEach((el) => {
360
- el.removeAttribute("contenteditable");
430
+ stopInline(el);
361
431
  el.classList.remove("xicms-ve-inline");
362
432
  delete el.dataset.veInline;
363
433
  });
434
+ targetsRef.current = [];
435
+ setSpots([]);
364
436
  };
365
- }, [selectedKey, blocks, commitText, positionFmtBar]);
437
+ }, [selectedKey, commitText, positionFmtBar, stopInline]);
366
438
  const dirtySinceRef = useRef(null);
367
439
  useEffect(() => {
368
440
  if (!draft) return;
@@ -544,11 +616,33 @@ function VisualEditorInner({ onClose }) {
544
616
  Select,
545
617
  {
546
618
  value: locale,
547
- onValueChange: setLocale,
619
+ onValueChange: (l) => {
620
+ setLocale(l);
621
+ if (l !== localeCfg.defaultLocale) setPanelOpen(true);
622
+ },
548
623
  options: localeCfg.locales.map((l) => ({ value: l, label: l === localeCfg.defaultLocale ? `${l} (standaard)` : l }))
549
624
  }
550
625
  )
551
626
  ] }),
627
+ /* @__PURE__ */ jsx(
628
+ "button",
629
+ {
630
+ type: "button",
631
+ className: `xicms-ve__histbtn${panelOpen && !selectedKey ? " is-on" : ""}`,
632
+ onClick: () => {
633
+ if (panelOpen && !selectedKey) {
634
+ setPanelOpen(false);
635
+ } else {
636
+ setSelectedKey(null);
637
+ setPanelOpen(true);
638
+ }
639
+ },
640
+ title: "Pagina-instellingen (titel, SEO, versies)",
641
+ "aria-label": "Pagina-instellingen",
642
+ "aria-pressed": panelOpen && !selectedKey,
643
+ children: "\u2699"
644
+ }
645
+ ),
552
646
  /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-ve__histbtn", disabled: !canUndo, onClick: undo, title: "Ongedaan maken (Ctrl+Z)", "aria-label": "Ongedaan maken (Ctrl+Z)", children: "\u21B6" }),
553
647
  /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-ve__histbtn", disabled: !canRedo, onClick: redo, title: "Opnieuw (Ctrl+Shift+Z)", "aria-label": "Opnieuw (Ctrl+Shift+Z)", children: "\u21B7" }),
554
648
  /* @__PURE__ */ jsx("span", { className: "xicms-ve__status", "aria-live": "polite", children: status === "saving" ? "Opslaan\u2026" : status === "saved" ? "\u2713 Opgeslagen" : status === "error" ? "\u26A0 Niet opgeslagen" : "" }),
@@ -563,7 +657,7 @@ function VisualEditorInner({ onClose }) {
563
657
  /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => void reloadRemote(), children: "Nieuwste versie laden" }),
564
658
  /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: () => setConflict(false), children: "Mijn versie behouden" })
565
659
  ] }),
566
- /* @__PURE__ */ jsxs("div", { className: "xicms-ve__body", children: [
660
+ /* @__PURE__ */ jsxs("div", { className: `xicms-ve__body${panelOpen ? " is-panel-open" : ""}`, children: [
567
661
  /* @__PURE__ */ jsxs("main", { className: "xicms-ve__canvas xicms-root", "data-dark": darkAuto ? "auto" : void 0, style: themeVars, children: [
568
662
  status === "loading" && /* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Laden\u2026" }),
569
663
  status === "none" && /* @__PURE__ */ jsxs("div", { className: "xicms-ve__empty", children: [
@@ -575,87 +669,110 @@ function VisualEditorInner({ onClose }) {
575
669
  description: `Voor deze pagina (${path}) bestaat nog geen werkversie. Maak hem hier direct aan, of ga naar /beheer \u2192 Pagina's.`
576
670
  }
577
671
  ),
578
- /* @__PURE__ */ jsx("p", { style: { textAlign: "center", marginTop: "1rem" }, children: /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-btn", onClick: () => void handleCreatePage(), children: "+ Deze pagina aanmaken" }) })
672
+ /* @__PURE__ */ jsx("p", { style: { textAlign: "center", marginTop: "1rem" }, children: /* @__PURE__ */ jsx(Button, { onClick: () => void handleCreatePage(), children: "+ Deze pagina aanmaken" }) })
579
673
  ] }),
580
- draft && /* @__PURE__ */ jsxs("div", { className: "container", ref: canvasRef, children: [
581
- blocks.length === 0 && /* @__PURE__ */ jsx(EmptyState, { icon: "\u{1F9F1}", title: "Nog geen blokken", description: "Voeg hieronder je eerste blok toe." }),
582
- displayBlocks.map((b, i) => {
583
- const def = registry.get(b._type);
584
- const Render = def?.Render;
585
- const safe = (() => {
586
- const parsed = def?.schema ? safeParse(def.schema, b) : void 0;
587
- return parsed?.success ? parsed.data : b;
588
- })();
589
- const dropClass = dropIndex === i ? " is-drop-before" : dropIndex === i + 1 ? " is-drop-after" : "";
590
- return /* @__PURE__ */ jsxs(
591
- "div",
592
- {
593
- className: `xicms-ve-block${b._key === selectedKey ? " is-selected" : ""}${dragIndex === i ? " is-dragging" : ""}${dropClass}`,
594
- onDragOver: (e) => {
595
- if (dragIndex === null) return;
596
- e.preventDefault();
597
- e.dataTransfer.dropEffect = "move";
598
- const rect = e.currentTarget.getBoundingClientRect();
599
- setDropIndex(e.clientY < rect.top + rect.height / 2 ? i : i + 1);
600
- },
601
- onDrop: (e) => {
602
- if (dragIndex === null || dropIndex === null) return;
603
- e.preventDefault();
604
- setBlocks(reorderBlocks(blocks, dragIndex, dropIndex));
605
- endDrag();
606
- },
607
- children: [
608
- /* @__PURE__ */ jsxs("div", { className: "xicms-ve-block__toolbar", children: [
609
- /* @__PURE__ */ jsx(
610
- "button",
611
- {
612
- type: "button",
613
- className: "xicms-ve-block__drag",
614
- draggable: true,
615
- onDragStart: (e) => {
616
- draggingRef.current = true;
617
- setDragIndex(i);
618
- e.dataTransfer.effectAllowed = "move";
619
- e.dataTransfer.setData("text/plain", String(i));
620
- const blockEl = e.currentTarget.closest(".xicms-ve-block");
621
- if (blockEl) e.dataTransfer.setDragImage(blockEl, 24, 24);
622
- },
623
- onDragEnd: endDrag,
624
- "aria-label": `Versleep ${def?.label ?? b._type}`,
625
- title: "Slepen om te verplaatsen",
626
- children: "\u22EE\u22EE"
627
- }
628
- ),
629
- /* @__PURE__ */ jsx("span", { className: "xicms-ve-block__label", children: def?.label ?? b._type }),
630
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setSelectedKey(b._key), "aria-label": `Bewerk ${def?.label ?? b._type}`, title: "Bewerken", children: "\u270F\uFE0F" }),
631
- /* @__PURE__ */ jsx("button", { type: "button", disabled: i === 0, onClick: () => move(i, -1), "aria-label": "Omhoog", children: "\u2191" }),
632
- /* @__PURE__ */ jsx("button", { type: "button", disabled: i === blocks.length - 1, onClick: () => move(i, 1), "aria-label": "Omlaag", children: "\u2193" }),
633
- b._type === "symbolRef" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void detachSymbol(i), "aria-label": "Symbool loskoppelen", title: "Loskoppelen (losse kopie)", children: "\u{1F517}\u2702" }),
634
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => duplicate(i), "aria-label": "Dupliceren", children: "\u2398" }),
635
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void remove(i), "aria-label": "Verwijderen", children: "\u{1F5D1}" })
636
- ] }),
637
- /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-ve-block__select", "aria-label": `Selecteer ${def?.label ?? b._type}`, onClick: () => setSelectedKey(b._key) }),
638
- /* @__PURE__ */ jsx("div", { className: "xicms-ve-block__content", "data-ve-content": b._key, children: Render ? /* @__PURE__ */ jsx(BlockErrorBoundary, { mode: "preview", type: b._type, blockKey: b._key, children: /* @__PURE__ */ jsx(InlineImageEditProvider, { onReplace: b._key === selectedKey ? (field) => setImgPicker({ key: b._key, field }) : null, children: /* @__PURE__ */ jsx(Render, { block: safe, siteId, mode: "preview" }) }) }) : /* @__PURE__ */ jsxs("p", { className: "xicms-ve__hint", children: [
639
- "Onbekend blok: ",
640
- b._type
641
- ] }) })
642
- ]
643
- },
644
- b._key
645
- );
646
- }),
647
- /* @__PURE__ */ jsxs("div", { className: "xicms-ve__inserter", children: [
648
- /* @__PURE__ */ jsxs("span", { children: [
649
- "Toevoegen",
650
- selected ? " (na selectie)" : "",
651
- ":"
652
- ] }),
653
- /* @__PURE__ */ jsx("div", { className: "xicms-ve__inserter-select", children: /* @__PURE__ */ jsx(Select, { value: "", placeholder: "Kies een blok\u2026", onValueChange: (v) => v && addBlock(v), options: addOptions }) }),
654
- /* @__PURE__ */ jsx(PatternInserter, { onInsert: insertAfterSelected })
655
- ] })
656
- ] })
674
+ draft && /* @__PURE__ */ jsxs(
675
+ "div",
676
+ {
677
+ className: "container",
678
+ ref: canvasRef,
679
+ onClickCapture: (e) => {
680
+ const el = e.target;
681
+ if (el?.closest?.("a[href]")) e.preventDefault();
682
+ },
683
+ children: [
684
+ blocks.length === 0 && /* @__PURE__ */ jsx(EmptyState, { icon: "\u{1F9F1}", title: "Nog geen blokken", description: "Voeg hieronder je eerste blok toe." }),
685
+ displayBlocks.map((b, i) => {
686
+ const def = registry.get(b._type);
687
+ const Render = def?.Render;
688
+ const safe = (() => {
689
+ const parsed = def?.schema ? safeParse(def.schema, b) : void 0;
690
+ return parsed?.success ? parsed.data : b;
691
+ })();
692
+ const dropClass = dropIndex === i ? " is-drop-before" : dropIndex === i + 1 ? " is-drop-after" : "";
693
+ return /* @__PURE__ */ jsxs(
694
+ "div",
695
+ {
696
+ className: `xicms-ve-block${b._key === selectedKey ? " is-selected" : ""}${dragIndex === i ? " is-dragging" : ""}${dropClass}`,
697
+ onDragOver: (e) => {
698
+ if (dragIndex === null) return;
699
+ e.preventDefault();
700
+ e.dataTransfer.dropEffect = "move";
701
+ const rect = e.currentTarget.getBoundingClientRect();
702
+ setDropIndex(e.clientY < rect.top + rect.height / 2 ? i : i + 1);
703
+ },
704
+ onDrop: (e) => {
705
+ if (dragIndex === null || dropIndex === null) return;
706
+ e.preventDefault();
707
+ setBlocks(reorderBlocks(blocks, dragIndex, dropIndex));
708
+ endDrag();
709
+ },
710
+ children: [
711
+ /* @__PURE__ */ jsxs("div", { className: "xicms-ve-block__toolbar", children: [
712
+ /* @__PURE__ */ jsx(
713
+ "button",
714
+ {
715
+ type: "button",
716
+ className: "xicms-ve-block__drag",
717
+ draggable: true,
718
+ onDragStart: (e) => {
719
+ draggingRef.current = true;
720
+ setDragIndex(i);
721
+ e.dataTransfer.effectAllowed = "move";
722
+ e.dataTransfer.setData("text/plain", String(i));
723
+ const blockEl = e.currentTarget.closest(".xicms-ve-block");
724
+ if (blockEl) e.dataTransfer.setDragImage(blockEl, 24, 24);
725
+ },
726
+ onDragEnd: endDrag,
727
+ "aria-label": `Versleep ${def?.label ?? b._type}`,
728
+ title: "Slepen om te verplaatsen",
729
+ children: "\u22EE\u22EE"
730
+ }
731
+ ),
732
+ /* @__PURE__ */ jsx("span", { className: "xicms-ve-block__label", children: def?.label ?? b._type }),
733
+ /* @__PURE__ */ jsx(
734
+ "button",
735
+ {
736
+ type: "button",
737
+ onClick: () => {
738
+ setSelectedKey(b._key);
739
+ setPanelOpen(true);
740
+ },
741
+ "aria-label": `Instellingen van ${def?.label ?? b._type}`,
742
+ title: "Instellingen (link, kleur, opties)",
743
+ children: "\u2699"
744
+ }
745
+ ),
746
+ /* @__PURE__ */ jsx("button", { type: "button", disabled: i === 0, onClick: () => move(i, -1), "aria-label": "Omhoog", children: "\u2191" }),
747
+ /* @__PURE__ */ jsx("button", { type: "button", disabled: i === blocks.length - 1, onClick: () => move(i, 1), "aria-label": "Omlaag", children: "\u2193" }),
748
+ b._type === "symbolRef" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void detachSymbol(i), "aria-label": "Symbool loskoppelen", title: "Loskoppelen (losse kopie)", children: "\u{1F517}\u2702" }),
749
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => duplicate(i), "aria-label": "Dupliceren", children: "\u2398" }),
750
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void remove(i), "aria-label": "Verwijderen", children: "\u{1F5D1}" })
751
+ ] }),
752
+ /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-ve-block__select", "aria-label": `Selecteer ${def?.label ?? b._type}`, onClick: () => setSelectedKey(b._key) }),
753
+ /* @__PURE__ */ jsx("div", { className: "xicms-ve-block__content", "data-ve-content": b._key, children: Render ? /* @__PURE__ */ jsx(BlockErrorBoundary, { mode: "preview", type: b._type, blockKey: b._key, children: /* @__PURE__ */ jsx(InlineImageEditProvider, { onReplace: b._key === selectedKey ? (field) => setImgPicker({ key: b._key, field }) : null, children: /* @__PURE__ */ jsx(Render, { block: safe, siteId, mode: "preview" }) }) }) : /* @__PURE__ */ jsxs("p", { className: "xicms-ve__hint", children: [
754
+ "Onbekend blok: ",
755
+ b._type
756
+ ] }) })
757
+ ]
758
+ },
759
+ b._key
760
+ );
761
+ }),
762
+ /* @__PURE__ */ jsxs("div", { className: "xicms-ve__inserter", children: [
763
+ /* @__PURE__ */ jsxs("span", { children: [
764
+ "Toevoegen",
765
+ selected ? " (na selectie)" : "",
766
+ ":"
767
+ ] }),
768
+ /* @__PURE__ */ jsx("div", { className: "xicms-ve__inserter-select", children: /* @__PURE__ */ jsx(Select, { value: "", placeholder: "Kies een blok\u2026", onValueChange: (v) => v && addBlock(v), options: addOptions }) }),
769
+ /* @__PURE__ */ jsx(PatternInserter, { onInsert: insertAfterSelected })
770
+ ] })
771
+ ]
772
+ }
773
+ )
657
774
  ] }),
658
- /* @__PURE__ */ jsx("aside", { className: "xicms-ve__panel", children: selected && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
775
+ panelOpen && /* @__PURE__ */ jsx("aside", { className: "xicms-ve__panel", children: selected && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
659
776
  /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold", children: [
660
777
  registry.get(selected._type)?.label ?? selected._type,
661
778
  " \u2014 vertaling (",
@@ -677,15 +794,15 @@ function VisualEditorInner({ onClose }) {
677
794
  ] }) : selected ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
678
795
  /* @__PURE__ */ jsxs("div", { className: "xicms-ve__panel-head", children: [
679
796
  /* @__PURE__ */ jsx(
680
- "button",
797
+ Button,
681
798
  {
682
- type: "button",
683
- className: `xicms-btn${status === "saving" || status === "error" ? "" : " xicms-btn--ghost"}`,
799
+ size: "sm",
800
+ variant: status === "saving" || status === "error" ? "primary" : "ghost",
684
801
  onClick: () => void flushSave().catch(() => setStatus("error")),
685
802
  children: status === "saving" ? "Opslaan\u2026" : status === "error" ? "\u26A0 Opnieuw opslaan" : "\u2713 Opgeslagen"
686
803
  }
687
804
  ),
688
- /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-btn xicms-btn--ghost", onClick: () => setSelectedKey(null), children: "\u2715 Sluiten" })
805
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => setPanelOpen(false), children: "\u2715 Sluiten" })
689
806
  ] }),
690
807
  /* @__PURE__ */ jsx(SelectedEditor, { block: selected, siteId, registry, onChange: (next) => updateByKey(selected._key, next) }, selected._key)
691
808
  ] }) : draft && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
@@ -729,6 +846,25 @@ function VisualEditorInner({ onClose }) {
729
846
  }
730
847
  }
731
848
  ),
849
+ spots.map((s, i) => /* @__PURE__ */ jsx(
850
+ "button",
851
+ {
852
+ type: "button",
853
+ className: "xicms-ve-pencil",
854
+ style: { top: s.top, left: s.left },
855
+ title: s.title,
856
+ "aria-label": s.title,
857
+ onMouseDown: (e) => e.preventDefault(),
858
+ onMouseEnter: () => targetsRef.current[i]?.el.classList.add("is-hover"),
859
+ onMouseLeave: () => targetsRef.current[i]?.el.classList.remove("is-hover"),
860
+ onClick: () => startInline(i),
861
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "12", height: "12", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
862
+ /* @__PURE__ */ jsx("path", { d: "M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3z" }),
863
+ /* @__PURE__ */ jsx("path", { d: "M14.5 6.5l3 3" })
864
+ ] })
865
+ },
866
+ i
867
+ )),
732
868
  fmtBar && /* @__PURE__ */ jsxs("div", { className: "xicms-ve-fmtbar", style: { top: fmtBar.top, left: fmtBar.left }, role: "toolbar", "aria-label": "Tekstopmaak", children: [
733
869
  /* @__PURE__ */ jsx(FmtBtn, { label: /* @__PURE__ */ jsx("b", { children: "B" }), title: "Vet (Ctrl/Cmd+B)", onRun: () => document.execCommand("bold") }),
734
870
  /* @__PURE__ */ jsx(FmtBtn, { label: /* @__PURE__ */ jsx("i", { children: "I" }), title: "Cursief (Ctrl/Cmd+I)", onRun: () => document.execCommand("italic") }),