@xiwebdesign/xicms-core 7.7.0 → 7.8.1

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 (37) hide show
  1. package/dist/admin/AdminApp.js +3 -1
  2. package/dist/admin/AdminApp.js.map +1 -1
  3. package/dist/admin/lib/pagesClient.d.ts +13 -0
  4. package/dist/admin/lib/pagesClient.js +13 -0
  5. package/dist/admin/lib/pagesClient.js.map +1 -1
  6. package/dist/admin/shop/ProductPaneel.d.ts +15 -0
  7. package/dist/admin/shop/ProductPaneel.js +55 -0
  8. package/dist/admin/shop/ProductPaneel.js.map +1 -0
  9. package/dist/admin/shop/ProductsTab.d.ts +27 -0
  10. package/dist/admin/shop/ProductsTab.js +3 -2
  11. package/dist/admin/shop/ProductsTab.js.map +1 -1
  12. package/dist/admin/shop/productClient.d.ts +32 -0
  13. package/dist/admin/shop/productClient.js +31 -0
  14. package/dist/admin/shop/productClient.js.map +1 -0
  15. package/dist/admin/visual/VisualEditor.js +299 -44
  16. package/dist/admin/visual/VisualEditor.js.map +1 -1
  17. package/dist/admin/visual/wijzigingen.d.ts +46 -0
  18. package/dist/admin/visual/wijzigingen.js +78 -0
  19. package/dist/admin/visual/wijzigingen.js.map +1 -0
  20. package/dist/components/EditBar.d.ts +30 -0
  21. package/dist/components/EditBar.js +35 -7
  22. package/dist/components/EditBar.js.map +1 -1
  23. package/dist/components/barOffset.d.ts +23 -0
  24. package/dist/components/barOffset.js +31 -0
  25. package/dist/components/barOffset.js.map +1 -0
  26. package/dist/editor/EditorProvider.d.ts +5 -0
  27. package/dist/editor/EditorProvider.js +28 -11
  28. package/dist/editor/EditorProvider.js.map +1 -1
  29. package/dist/modules/shop/blocks/ProductDetail.js +3 -3
  30. package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
  31. package/dist/modules/shop/blocks/ProductGrid.js +5 -2
  32. package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
  33. package/dist/render/PublishedPage.js +13 -0
  34. package/dist/render/PublishedPage.js.map +1 -1
  35. package/dist/styles.css +166 -46
  36. package/dist/version.js +1 -1
  37. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/shop/productClient.ts"],"sourcesContent":["\"use client\";\n\nimport { doc, getDoc, onSnapshot, updateDoc } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport type { Product } from \"../../schema/shop\";\n\nexport type StoredProduct = Product & { id: string };\n\n/**\n * Eén product, live — voor het paneel dat de bewerkmodus opent bij een productblok.\n *\n * Een abonnement en geen eenmalige lezing: hetzelfde product kan tegelijk openstaan in het\n * Webwinkel-paneel of in een tweede tabblad, en dan hoort dit paneel niet met een oude prijs te\n * blijven staan.\n */\nexport function subscribeProduct(\n siteId: string,\n productId: string,\n cb: (p: StoredProduct | null) => void,\n): () => void {\n const db = getDb();\n if (!db) {\n cb(null);\n return () => {};\n }\n return onSnapshot(doc(db, \"sites\", siteId, \"products\", productId), (snap) => {\n cb(snap.exists() ? ({ id: snap.id, ...(snap.data() as Product) }) : null);\n });\n}\n\n/**\n * Schrijf één veld van een product weg — de inline-tegenhanger van het volledige formulier.\n *\n * ── WAAROM DIT ANDERS IS DAN EEN TEKST OP EEN PAGINA ──────────────────────────────────────\n *\n * Een blokveld gaat naar de WERKVERSIE van die ene pagina en wordt pas zichtbaar als je\n * publiceert. Een product heeft geen werkversie: het staat in de productencollectie en wordt door\n * elke pagina gelezen die het toont. Wie hier de naam wijzigt, wijzigt hem dus METEEN en OVERAL —\n * op de cataloguspagina, in de zoekresultaten, op de bestelbevestiging.\n *\n * Dat verschil is niet weg te poetsen zonder een tweede publicatiestroom voor producten te bouwen,\n * en dat zou de webwinkel ingewikkelder maken dan hij is. Het staat daarom expliciet in de tekst\n * bij het potlood, zodat het een keuze is en geen verrassing.\n *\n * `updateDoc` en niet `setDoc`: een product heeft velden die dit pad niet kent (varianten,\n * voorraad, personalisatie). Een hele-document-schrijfactie zou die stilzwijgend wissen.\n */\nexport async function setProductField(\n siteId: string,\n productId: string,\n field: string,\n value: unknown,\n): Promise<void> {\n const db = getDb();\n if (!db) throw new Error(\"Firebase niet geconfigureerd.\");\n await updateDoc(doc(db, \"sites\", siteId, \"products\", productId), { [field]: value });\n}\n\n/** Eenmalige lezing — voor het moment waarop de editor moet weten wat er nú staat. */\nexport async function getProduct(siteId: string, productId: string): Promise<StoredProduct | null> {\n const db = getDb();\n if (!db) return null;\n const snap = await getDoc(doc(db, \"sites\", siteId, \"products\", productId));\n return snap.exists() ? ({ id: snap.id, ...(snap.data() as Product) }) : null;\n}\n"],"mappings":";AAEA,SAAS,KAAK,QAAQ,YAAY,iBAAiB;AACnD,SAAS,aAAa;AAYf,SAAS,iBACd,QACA,WACA,IACY;AACZ,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,IAAI;AACP,OAAG,IAAI;AACP,WAAO,MAAM;AAAA,IAAC;AAAA,EAChB;AACA,SAAO,WAAW,IAAI,IAAI,SAAS,QAAQ,YAAY,SAAS,GAAG,CAAC,SAAS;AAC3E,OAAG,KAAK,OAAO,IAAK,EAAE,IAAI,KAAK,IAAI,GAAI,KAAK,KAAK,EAAc,IAAK,IAAI;AAAA,EAC1E,CAAC;AACH;AAmBA,eAAsB,gBACpB,QACA,WACA,OACA,OACe;AACf,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,OAAM,IAAI,MAAM,+BAA+B;AACxD,QAAM,UAAU,IAAI,IAAI,SAAS,QAAQ,YAAY,SAAS,GAAG,EAAE,CAAC,KAAK,GAAG,MAAM,CAAC;AACrF;AAGA,eAAsB,WAAW,QAAgB,WAAkD;AACjG,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,OAAO,MAAM,OAAO,IAAI,IAAI,SAAS,QAAQ,YAAY,SAAS,CAAC;AACzE,SAAO,KAAK,OAAO,IAAK,EAAE,IAAI,KAAK,IAAI,GAAI,KAAK,KAAK,EAAc,IAAK;AAC1E;","names":[]}
@@ -12,12 +12,17 @@ import { useNestedDialogOpen } from "../lib/useNestedDialog.js";
12
12
  import { useThemeVars } from "../../XiCMSProvider.js";
13
13
  import { buildBlockAddOptions } from "../../blocks/registry.js";
14
14
  import DOMPurify from "dompurify";
15
- import { createPage, getDraftByPath, saveDraft, publishPage, subscribeDraft, subscribeVersions, restoreVersion } from "../lib/pagesClient.js";
15
+ import { createPage, getDraftByPath, getPublishedPage, saveDraft, publishPage, subscribeDraft, subscribeVersions, restoreVersion } from "../lib/pagesClient.js";
16
16
  import { isDemoActive } from "../../demo/demoStore.js";
17
+ import { setProductField } from "../shop/productClient.js";
18
+ import { ProductPaneel } from "../shop/ProductPaneel.js";
17
19
  import { builtinBlockEditors, GenericEditor } from "../blockEditors/index.js";
18
20
  import { StyleEditor } from "../blockEditors/StyleEditor.js";
19
21
  import { siteLocales, parseLocaleSlugs, localizeBlocks } from "../../i18n/overlay.js";
20
22
  import { useEditor } from "../../editor/EditorProvider.js";
23
+ import { AdminModes } from "../../components/EditBar.js";
24
+ import { useBalkOffset } from "../../components/barOffset.js";
25
+ import { aantalWijzigingen, bepaalWijzigingen, GEEN_WIJZIGINGEN } from "./wijzigingen.js";
21
26
  import { TranslationPanel } from "./TranslationPanel.js";
22
27
  import { PatternInserter } from "../patterns/PatternInserter.js";
23
28
  import { Button, Field, Input, Textarea } from "../ui/primitives.js";
@@ -97,11 +102,14 @@ function VisualEditorInner({ onClose }) {
97
102
  const [panelOpen, setPanelOpen] = useState(false);
98
103
  const [status, setStatus] = useState("loading");
99
104
  const [selectedKey, setSelectedKey] = useState(null);
105
+ const [selectedProduct, setSelectedProduct] = useState(null);
100
106
  const [publishing, setPublishing] = useState(false);
101
107
  const [imgPicker, setImgPicker] = useState(null);
102
108
  const firstSave = useRef(true);
103
109
  const canvasRef = useRef(null);
104
110
  const [conflict, setConflict] = useState(false);
111
+ const [liveBlocks, setLiveBlocks] = useState(void 0);
112
+ useBalkOffset(true);
105
113
  const { vars: themeVars, darkAuto } = useThemeVars();
106
114
  const nestedDialogOpen = useNestedDialogOpen();
107
115
  const { containerRef, focusFirst } = useFocusTrap({ isActive: !nestedDialogOpen });
@@ -215,6 +223,20 @@ function VisualEditorInner({ onClose }) {
215
223
  active = false;
216
224
  };
217
225
  }, [siteId, path]);
226
+ useEffect(() => {
227
+ const id = draft?.id;
228
+ if (!id) {
229
+ setLiveBlocks(void 0);
230
+ return;
231
+ }
232
+ let active = true;
233
+ getPublishedPage(siteId, id).then((pagina) => {
234
+ if (active) setLiveBlocks(pagina?.blocks ?? null);
235
+ }).catch(() => active && setLiveBlocks(void 0));
236
+ return () => {
237
+ active = false;
238
+ };
239
+ }, [siteId, draft?.id]);
218
240
  const handleCreatePage = useCallback(async () => {
219
241
  setStatus("loading");
220
242
  try {
@@ -254,6 +276,12 @@ function VisualEditorInner({ onClose }) {
254
276
  () => isTranslating ? localizeBlocks(blocks, overlay) : blocks,
255
277
  [blocks, isTranslating, overlay]
256
278
  );
279
+ const nietLiveRef = useRef(false);
280
+ const wijzigingen = useMemo(
281
+ () => liveBlocks === void 0 ? GEEN_WIJZIGINGEN : bepaalWijzigingen(blocks, liveBlocks),
282
+ [blocks, liveBlocks]
283
+ );
284
+ nietLiveRef.current = wijzigingen.iets;
257
285
  const { publishDraft } = useEditor();
258
286
  const laatstGepubliceerd = useRef("");
259
287
  useEffect(() => {
@@ -320,28 +348,41 @@ function VisualEditorInner({ onClose }) {
320
348
  );
321
349
  const registryRef = useRef(registry);
322
350
  registryRef.current = registry;
323
- const [gears, setGears] = useState([]);
351
+ function veiligeBovenkant() {
352
+ let onderkant = 0;
353
+ for (const el of Array.from(document.querySelectorAll("body *"))) {
354
+ const pos = getComputedStyle(el).position;
355
+ if (pos !== "fixed" && pos !== "sticky") continue;
356
+ const r = el.getBoundingClientRect();
357
+ if (r.height === 0 || r.top > 4) continue;
358
+ onderkant = Math.max(onderkant, r.bottom);
359
+ }
360
+ return onderkant;
361
+ }
362
+ const [blokknoppen, setBlokknoppen] = useState([]);
324
363
  useEffect(() => {
325
364
  if (!draft) {
326
- setGears((v) => v.length === 0 ? v : []);
365
+ setBlokknoppen((v) => v.length === 0 ? v : []);
327
366
  return;
328
367
  }
329
368
  const meet = () => {
369
+ const veilig = veiligeBovenkant();
330
370
  const gevonden = [];
331
371
  for (const el of Array.from(document.querySelectorAll("[data-xicms-block]"))) {
332
372
  const key = el.dataset.xicmsBlock;
333
373
  if (!key) continue;
334
374
  const r = el.getBoundingClientRect();
335
375
  if (r.width === 0 && r.height === 0) continue;
376
+ if (r.bottom < veilig || r.top > window.innerHeight) continue;
336
377
  const type = el.dataset.xicmsBlockType ?? "";
337
378
  gevonden.push({
338
379
  key,
339
- top: Math.max(r.top + 8, 56),
340
- left: Math.max(r.right - 36, 8),
380
+ top: Math.max(r.top + 8, veilig + 8),
381
+ left: Math.max(r.right - 62, 8),
341
382
  label: registryRef.current.get(type)?.label ?? type
342
383
  });
343
384
  }
344
- setGears((vorige) => {
385
+ setBlokknoppen((vorige) => {
345
386
  if (vorige.length === gevonden.length && vorige.every((v, i) => v.key === gevonden[i].key && v.top === gevonden[i].top && v.left === gevonden[i].left)) {
346
387
  return vorige;
347
388
  }
@@ -356,9 +397,117 @@ function VisualEditorInner({ onClose }) {
356
397
  window.removeEventListener("resize", meet);
357
398
  };
358
399
  }, [draft, displayBlocks, registryRef]);
400
+ const [productKnop, setProductKnop] = useState(null);
401
+ useEffect(() => {
402
+ if (!draft) return;
403
+ const onOver = (e) => {
404
+ const doel = e.target?.closest?.("[data-xicms-product]");
405
+ if (!doel) {
406
+ const naarKnop = e.target?.closest?.(".xicms-ve-blokknoppen");
407
+ if (!naarKnop) setProductKnop(null);
408
+ return;
409
+ }
410
+ const id = doel.dataset.xicmsProduct;
411
+ if (!id) return;
412
+ const r = doel.getBoundingClientRect();
413
+ setProductKnop(
414
+ (v) => v && v.id === id && v.top === Math.max(r.top + 8, 44) && v.left === Math.max(r.right - 62, 8) ? v : { id, top: Math.max(r.top + 8, 44), left: Math.max(r.right - 62, 8) }
415
+ );
416
+ };
417
+ document.addEventListener("mouseover", onOver, true);
418
+ return () => document.removeEventListener("mouseover", onOver, true);
419
+ }, [draft]);
420
+ const productBewerkRef = useRef(null);
421
+ const startProductInline = useCallback(
422
+ (productId) => {
423
+ const host = document.querySelector(`[data-xicms-product="${CSS.escape(productId)}"]`);
424
+ const el = host?.querySelector("[data-xicms-product-edit]");
425
+ if (!el) {
426
+ toast({
427
+ title: "Hier valt niets te typen",
428
+ description: "Gebruik het tandwiel voor prijs, foto's, varianten en voorraad."
429
+ });
430
+ return;
431
+ }
432
+ const veld = el.dataset.xicmsProductEdit;
433
+ if (!veld) return;
434
+ const origineel = el.textContent ?? "";
435
+ el.setAttribute("contenteditable", "true");
436
+ el.setAttribute("spellcheck", "true");
437
+ el.classList.add("is-editing");
438
+ productBewerkRef.current = el;
439
+ const stop = () => {
440
+ el.removeAttribute("contenteditable");
441
+ el.classList.remove("is-editing");
442
+ el.removeEventListener("blur", opBlur);
443
+ el.removeEventListener("keydown", opToets);
444
+ if (productBewerkRef.current === el) productBewerkRef.current = null;
445
+ };
446
+ const opBlur = () => {
447
+ const waarde = el.textContent ?? "";
448
+ stop();
449
+ if (waarde === origineel) return;
450
+ void setProductField(siteId, productId, veld, waarde).then(
451
+ () => toast({
452
+ title: "Product bijgewerkt",
453
+ description: "Producten hebben geen werkversie: dit staat meteen live, op elke pagina.",
454
+ tone: "success"
455
+ })
456
+ ).catch((err) => {
457
+ el.textContent = origineel;
458
+ toast({ title: "Opslaan mislukt", description: err.message, tone: "error" });
459
+ });
460
+ };
461
+ const opToets = (ev) => {
462
+ if (ev.key === "Escape") {
463
+ ev.stopPropagation();
464
+ el.textContent = origineel;
465
+ el.blur();
466
+ } else if (ev.key === "Enter") {
467
+ ev.preventDefault();
468
+ el.blur();
469
+ }
470
+ };
471
+ el.addEventListener("blur", opBlur);
472
+ el.addEventListener("keydown", opToets);
473
+ el.focus();
474
+ const selection = window.getSelection();
475
+ const range = document.createRange();
476
+ range.selectNodeContents(el);
477
+ range.collapse(false);
478
+ selection?.removeAllRanges();
479
+ selection?.addRange(range);
480
+ },
481
+ [siteId, toast]
482
+ );
483
+ useEffect(() => {
484
+ if (!draft) return;
485
+ const gemarkeerd = [];
486
+ const markeer = (el, klasse) => {
487
+ if (!el) return;
488
+ el.classList.add(klasse);
489
+ gemarkeerd.push(el);
490
+ };
491
+ for (const key of wijzigingen.blokken) {
492
+ markeer(document.querySelector(`[data-xicms-block="${CSS.escape(key)}"]`), "xicms-ve-nieuw");
493
+ }
494
+ for (const [key, velden] of wijzigingen.velden) {
495
+ const host = document.querySelector(`[data-xicms-block="${CSS.escape(key)}"]`);
496
+ if (!host) continue;
497
+ for (const veld of velden) {
498
+ host.querySelectorAll(`[data-xicms-edit="${CSS.escape(veld)}"]`).forEach((el) => markeer(el, "xicms-ve-gewijzigd"));
499
+ }
500
+ }
501
+ return () => {
502
+ for (const el of gemarkeerd) {
503
+ el.classList.remove("xicms-ve-nieuw", "xicms-ve-gewijzigd");
504
+ }
505
+ };
506
+ }, [draft, wijzigingen, displayBlocks]);
359
507
  const [spots, setSpots] = useState([]);
360
508
  const targetsRef = useRef([]);
361
509
  const editingRef = useRef(null);
510
+ const wilInlineRef = useRef(false);
362
511
  const stopInline = useCallback((el) => {
363
512
  el.removeAttribute("contenteditable");
364
513
  el.classList.remove("is-editing");
@@ -420,8 +569,10 @@ function VisualEditorInner({ onClose }) {
420
569
  });
421
570
  }
422
571
  el.addEventListener("keydown", (e) => {
423
- if (e.key === "Escape") el.blur();
424
- else if (!html && e.key === "Enter") {
572
+ if (e.key === "Escape") {
573
+ e.stopPropagation();
574
+ el.blur();
575
+ } else if (!html && e.key === "Enter") {
425
576
  e.preventDefault();
426
577
  el.blur();
427
578
  }
@@ -454,6 +605,16 @@ function VisualEditorInner({ onClose }) {
454
605
  measure();
455
606
  };
456
607
  apply();
608
+ if (wilInlineRef.current) {
609
+ wilInlineRef.current = false;
610
+ if (targetsRef.current.length > 0) startInline(0);
611
+ else {
612
+ toast({
613
+ title: "Dit blok heeft geen eigen tekst",
614
+ description: "Wijs een product aan voor zijn naam, of gebruik het tandwiel voor de instellingen van dit blok."
615
+ });
616
+ }
617
+ }
457
618
  const obs = new MutationObserver(() => {
458
619
  if (!draggingRef.current) apply();
459
620
  });
@@ -478,7 +639,7 @@ function VisualEditorInner({ onClose }) {
478
639
  targetsRef.current = [];
479
640
  setSpots([]);
480
641
  };
481
- }, [selectedKey, commitText, positionFmtBar, stopInline]);
642
+ }, [selectedKey, commitText, positionFmtBar, stopInline, startInline, toast]);
482
643
  const dirtySinceRef = useRef(null);
483
644
  useEffect(() => {
484
645
  if (!draft) return;
@@ -507,7 +668,7 @@ function VisualEditorInner({ onClose }) {
507
668
  }, [draft, siteId, conflict]);
508
669
  useEffect(() => {
509
670
  const beforeUnload = (e) => {
510
- if (dirtyRef.current) {
671
+ if (dirtyRef.current || nietLiveRef.current) {
511
672
  e.preventDefault();
512
673
  e.returnValue = "";
513
674
  }
@@ -647,13 +808,25 @@ function VisualEditorInner({ onClose }) {
647
808
  }
648
809
  setConflict(false);
649
810
  }, [siteId, path, mutateDraft]);
650
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "xicms-ve__shell", role: "group", "aria-label": `Pagina bewerken: ${path}`, children: [
651
- /* @__PURE__ */ jsxs("header", { className: "xicms-ve__bar", children: [
652
- /* @__PURE__ */ jsxs("strong", { className: "xicms-ve__title", children: [
653
- "Bewerken \xB7 ",
654
- name
655
- ] }),
656
- /* @__PURE__ */ jsx("span", { className: "xicms-ve__path", children: path }),
811
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "xicms-ve__shell", "data-xicms-tooling": "", role: "group", "aria-label": `Pagina bewerken: ${path}`, children: [
812
+ /* @__PURE__ */ jsxs("header", { className: "xicms-ve__bar", role: "toolbar", "aria-label": "Beheer", children: [
813
+ /* @__PURE__ */ jsx("span", { className: "xicms-editbar__brand", children: isDemoActive() ? "XiCMS \xB7 demo" : "XiCMS" }),
814
+ /* @__PURE__ */ jsx(
815
+ AdminModes,
816
+ {
817
+ actief: "bewerken",
818
+ voorSwitch: (m) => {
819
+ if (m === "live" && nietLiveRef.current) {
820
+ toast({
821
+ title: "Nog niet gepubliceerd",
822
+ description: `${aantalWijzigingen(wijzigingen)} blijft bewaard als werkversie. Je ziet ze weer zodra je terug naar Bewerken gaat.`
823
+ });
824
+ }
825
+ }
826
+ }
827
+ ),
828
+ /* @__PURE__ */ jsx("span", { className: "xicms-ve__sep", "aria-hidden": "true" }),
829
+ /* @__PURE__ */ jsx("span", { className: "xicms-ve__path", title: `Je bewerkt ${name} \u2014 ${path}`, children: path }),
657
830
  localeCfg.locales.length > 1 && /* @__PURE__ */ jsxs("span", { className: "xicms-ve__locale", title: "Taal \u2014 in een andere taal dan de standaard bewerk je vertalingen", children: [
658
831
  "\u{1F310}",
659
832
  /* @__PURE__ */ jsx(
@@ -695,13 +868,9 @@ function VisualEditorInner({ onClose }) {
695
868
  ] }),
696
869
  /* @__PURE__ */ jsxs("span", { className: "xicms-ve__status", "aria-live": "polite", children: [
697
870
  status === "loading" ? "Werkversie laden\u2026" : null,
698
- status === "saving" ? "Opslaan\u2026" : status === "saved" ? "\u2713 Opgeslagen" : status === "error" ? "\u26A0 Niet opgeslagen" : ""
871
+ status === "saving" ? "Opslaan\u2026" : status === "saved" ? "\u2713 Bewaard als werkversie" : status === "error" ? "\u26A0 Niet opgeslagen" : ""
699
872
  ] }),
700
- status === "error" && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: () => void flushSave().catch(() => setStatus("error")), children: "Opnieuw opslaan" }),
701
- /* @__PURE__ */ jsxs("div", { className: "xicms-ve__bar-actions", children: [
702
- /* @__PURE__ */ jsx(Button, { size: "sm", disabled: publishing || !draft, onClick: onPublish, children: publishing ? "Bezig\u2026" : "Publiceren" }),
703
- /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: handleClose, children: "Sluiten" })
704
- ] })
873
+ status === "error" && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: () => void flushSave().catch(() => setStatus("error")), children: "Opnieuw opslaan" })
705
874
  ] }),
706
875
  conflict && /* @__PURE__ */ jsxs("div", { className: "xicms-ve__conflict", role: "alert", children: [
707
876
  /* @__PURE__ */ jsx("span", { children: "\u26A0 Deze pagina is intussen ergens anders gewijzigd (andere tab of collega). Doorwerken kan die wijzigingen overschrijven." }),
@@ -718,7 +887,16 @@ function VisualEditorInner({ onClose }) {
718
887
  /* @__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 }) }),
719
888
  /* @__PURE__ */ jsx(PatternInserter, { onInsert: insertAfterSelected })
720
889
  ] }),
721
- selected && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
890
+ selectedProduct ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
891
+ /* @__PURE__ */ jsxs("div", { className: "xicms-ve__panel-head", children: [
892
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-slate-500", children: "Webwinkel" }),
893
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => {
894
+ setSelectedProduct(null);
895
+ setPanelOpen(false);
896
+ }, children: "\u2715 Sluiten" })
897
+ ] }),
898
+ /* @__PURE__ */ jsx(ProductPaneel, { siteId, productId: selectedProduct })
899
+ ] }) : selected && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
722
900
  /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold", children: [
723
901
  registry.get(selected._type)?.label ?? selected._type,
724
902
  " \u2014 vertaling (",
@@ -792,26 +970,46 @@ function VisualEditorInner({ onClose }) {
792
970
  }
793
971
  }
794
972
  ),
795
- gears.map((g) => /* @__PURE__ */ jsx(
796
- "button",
797
- {
798
- type: "button",
799
- className: `xicms-ve-gear${g.key === selectedKey ? " is-on" : ""}`,
800
- style: { top: g.top, left: g.left },
801
- title: `Instellingen van ${g.label}`,
802
- "aria-label": `Instellingen van ${g.label}`,
803
- onMouseDown: (e) => e.preventDefault(),
804
- onClick: () => {
805
- setSelectedKey(g.key);
806
- setPanelOpen(true);
807
- },
808
- children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
809
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
810
- /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })
811
- ] })
812
- },
813
- g.key
814
- )),
973
+ blokknoppen.map((b) => /* @__PURE__ */ jsxs("div", { className: "xicms-ve-blokknoppen", style: { top: b.top, left: b.left }, children: [
974
+ /* @__PURE__ */ jsx(
975
+ "button",
976
+ {
977
+ type: "button",
978
+ className: `xicms-ve-blokknop${b.key === selectedKey ? " is-on" : ""}`,
979
+ title: `Tekst van ${b.label} bewerken`,
980
+ "aria-label": `Tekst van ${b.label} bewerken`,
981
+ onMouseDown: (e) => e.preventDefault(),
982
+ onClick: () => {
983
+ wilInlineRef.current = true;
984
+ setSelectedProduct(null);
985
+ setSelectedKey(b.key);
986
+ },
987
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
988
+ /* @__PURE__ */ jsx("path", { d: "M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3z" }),
989
+ /* @__PURE__ */ jsx("path", { d: "M14.5 6.5l3 3" })
990
+ ] })
991
+ }
992
+ ),
993
+ /* @__PURE__ */ jsx(
994
+ "button",
995
+ {
996
+ type: "button",
997
+ className: `xicms-ve-blokknop${b.key === selectedKey && panelOpen ? " is-on" : ""}`,
998
+ title: `Instellingen van ${b.label}`,
999
+ "aria-label": `Instellingen van ${b.label}`,
1000
+ onMouseDown: (e) => e.preventDefault(),
1001
+ onClick: () => {
1002
+ setSelectedProduct(null);
1003
+ setSelectedKey(b.key);
1004
+ setPanelOpen(true);
1005
+ },
1006
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
1007
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
1008
+ /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })
1009
+ ] })
1010
+ }
1011
+ )
1012
+ ] }, b.key)),
815
1013
  spots.map((s, i) => /* @__PURE__ */ jsx(
816
1014
  "button",
817
1015
  {
@@ -831,6 +1029,63 @@ function VisualEditorInner({ onClose }) {
831
1029
  },
832
1030
  i
833
1031
  )),
1032
+ productKnop && /* @__PURE__ */ jsxs(
1033
+ "div",
1034
+ {
1035
+ className: "xicms-ve-blokknoppen is-product",
1036
+ style: { top: productKnop.top, left: productKnop.left },
1037
+ children: [
1038
+ /* @__PURE__ */ jsx(
1039
+ "button",
1040
+ {
1041
+ type: "button",
1042
+ className: "xicms-ve-blokknop",
1043
+ title: "Productnaam bewerken (staat meteen live)",
1044
+ "aria-label": "Productnaam bewerken",
1045
+ onMouseDown: (e) => e.preventDefault(),
1046
+ onClick: () => startProductInline(productKnop.id),
1047
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
1048
+ /* @__PURE__ */ jsx("path", { d: "M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3z" }),
1049
+ /* @__PURE__ */ jsx("path", { d: "M14.5 6.5l3 3" })
1050
+ ] })
1051
+ }
1052
+ ),
1053
+ /* @__PURE__ */ jsx(
1054
+ "button",
1055
+ {
1056
+ type: "button",
1057
+ className: `xicms-ve-blokknop${selectedProduct === productKnop.id ? " is-on" : ""}`,
1058
+ title: "Product bewerken: prijs, foto's, varianten, voorraad",
1059
+ "aria-label": "Product bewerken",
1060
+ onMouseDown: (e) => e.preventDefault(),
1061
+ onClick: () => {
1062
+ setSelectedKey(null);
1063
+ setSelectedProduct(productKnop.id);
1064
+ setPanelOpen(true);
1065
+ },
1066
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
1067
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
1068
+ /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })
1069
+ ] })
1070
+ }
1071
+ )
1072
+ ]
1073
+ }
1074
+ ),
1075
+ draft && /* @__PURE__ */ jsxs(
1076
+ "button",
1077
+ {
1078
+ type: "button",
1079
+ className: `xicms-ve-publiceer${wijzigingen.iets ? " is-klaar" : ""}`,
1080
+ disabled: publishing || !wijzigingen.iets,
1081
+ onClick: () => void onPublish(),
1082
+ title: wijzigingen.iets ? "Zet deze wijzigingen op de live site" : "Er is niets veranderd sinds de laatste publicatie",
1083
+ children: [
1084
+ /* @__PURE__ */ jsx("span", { className: "xicms-ve-publiceer__label", children: publishing ? "Bezig\u2026" : "Publiceren" }),
1085
+ /* @__PURE__ */ jsx("span", { className: "xicms-ve-publiceer__sub", children: wijzigingen.iets ? aantalWijzigingen(wijzigingen) : "niets gewijzigd" })
1086
+ ]
1087
+ }
1088
+ ),
834
1089
  fmtBar && /* @__PURE__ */ jsxs("div", { className: "xicms-ve-fmtbar", style: { top: fmtBar.top, left: fmtBar.left }, role: "toolbar", "aria-label": "Tekstopmaak", children: [
835
1090
  /* @__PURE__ */ jsx(FmtBtn, { label: /* @__PURE__ */ jsx("b", { children: "B" }), title: "Vet (Ctrl/Cmd+B)", onRun: () => document.execCommand("bold") }),
836
1091
  /* @__PURE__ */ jsx(FmtBtn, { label: /* @__PURE__ */ jsx("i", { children: "I" }), title: "Cursief (Ctrl/Cmd+I)", onRun: () => document.execCommand("italic") }),