@xiwebdesign/xicms-core 7.7.0 → 7.8.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 (32) 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 +296 -43
  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 +32 -6
  22. package/dist/components/EditBar.js.map +1 -1
  23. package/dist/editor/EditorProvider.d.ts +5 -0
  24. package/dist/editor/EditorProvider.js +28 -11
  25. package/dist/editor/EditorProvider.js.map +1 -1
  26. package/dist/modules/shop/blocks/ProductDetail.js +3 -3
  27. package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
  28. package/dist/modules/shop/blocks/ProductGrid.js +5 -2
  29. package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
  30. package/dist/styles.css +163 -46
  31. package/dist/version.js +1 -1
  32. 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,16 @@ 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 { aantalWijzigingen, bepaalWijzigingen, GEEN_WIJZIGINGEN } from "./wijzigingen.js";
21
25
  import { TranslationPanel } from "./TranslationPanel.js";
22
26
  import { PatternInserter } from "../patterns/PatternInserter.js";
23
27
  import { Button, Field, Input, Textarea } from "../ui/primitives.js";
@@ -97,11 +101,13 @@ function VisualEditorInner({ onClose }) {
97
101
  const [panelOpen, setPanelOpen] = useState(false);
98
102
  const [status, setStatus] = useState("loading");
99
103
  const [selectedKey, setSelectedKey] = useState(null);
104
+ const [selectedProduct, setSelectedProduct] = useState(null);
100
105
  const [publishing, setPublishing] = useState(false);
101
106
  const [imgPicker, setImgPicker] = useState(null);
102
107
  const firstSave = useRef(true);
103
108
  const canvasRef = useRef(null);
104
109
  const [conflict, setConflict] = useState(false);
110
+ const [liveBlocks, setLiveBlocks] = useState(void 0);
105
111
  const { vars: themeVars, darkAuto } = useThemeVars();
106
112
  const nestedDialogOpen = useNestedDialogOpen();
107
113
  const { containerRef, focusFirst } = useFocusTrap({ isActive: !nestedDialogOpen });
@@ -215,6 +221,20 @@ function VisualEditorInner({ onClose }) {
215
221
  active = false;
216
222
  };
217
223
  }, [siteId, path]);
224
+ useEffect(() => {
225
+ const id = draft?.id;
226
+ if (!id) {
227
+ setLiveBlocks(void 0);
228
+ return;
229
+ }
230
+ let active = true;
231
+ getPublishedPage(siteId, id).then((pagina) => {
232
+ if (active) setLiveBlocks(pagina?.blocks ?? null);
233
+ }).catch(() => active && setLiveBlocks(void 0));
234
+ return () => {
235
+ active = false;
236
+ };
237
+ }, [siteId, draft?.id]);
218
238
  const handleCreatePage = useCallback(async () => {
219
239
  setStatus("loading");
220
240
  try {
@@ -254,6 +274,12 @@ function VisualEditorInner({ onClose }) {
254
274
  () => isTranslating ? localizeBlocks(blocks, overlay) : blocks,
255
275
  [blocks, isTranslating, overlay]
256
276
  );
277
+ const nietLiveRef = useRef(false);
278
+ const wijzigingen = useMemo(
279
+ () => liveBlocks === void 0 ? GEEN_WIJZIGINGEN : bepaalWijzigingen(blocks, liveBlocks),
280
+ [blocks, liveBlocks]
281
+ );
282
+ nietLiveRef.current = wijzigingen.iets;
257
283
  const { publishDraft } = useEditor();
258
284
  const laatstGepubliceerd = useRef("");
259
285
  useEffect(() => {
@@ -320,28 +346,41 @@ function VisualEditorInner({ onClose }) {
320
346
  );
321
347
  const registryRef = useRef(registry);
322
348
  registryRef.current = registry;
323
- const [gears, setGears] = useState([]);
349
+ function veiligeBovenkant() {
350
+ let onderkant = 0;
351
+ for (const el of Array.from(document.querySelectorAll("body *"))) {
352
+ const pos = getComputedStyle(el).position;
353
+ if (pos !== "fixed" && pos !== "sticky") continue;
354
+ const r = el.getBoundingClientRect();
355
+ if (r.height === 0 || r.top > 4) continue;
356
+ onderkant = Math.max(onderkant, r.bottom);
357
+ }
358
+ return onderkant;
359
+ }
360
+ const [blokknoppen, setBlokknoppen] = useState([]);
324
361
  useEffect(() => {
325
362
  if (!draft) {
326
- setGears((v) => v.length === 0 ? v : []);
363
+ setBlokknoppen((v) => v.length === 0 ? v : []);
327
364
  return;
328
365
  }
329
366
  const meet = () => {
367
+ const veilig = veiligeBovenkant();
330
368
  const gevonden = [];
331
369
  for (const el of Array.from(document.querySelectorAll("[data-xicms-block]"))) {
332
370
  const key = el.dataset.xicmsBlock;
333
371
  if (!key) continue;
334
372
  const r = el.getBoundingClientRect();
335
373
  if (r.width === 0 && r.height === 0) continue;
374
+ if (r.bottom < veilig || r.top > window.innerHeight) continue;
336
375
  const type = el.dataset.xicmsBlockType ?? "";
337
376
  gevonden.push({
338
377
  key,
339
- top: Math.max(r.top + 8, 56),
340
- left: Math.max(r.right - 36, 8),
378
+ top: Math.max(r.top + 8, veilig + 8),
379
+ left: Math.max(r.right - 62, 8),
341
380
  label: registryRef.current.get(type)?.label ?? type
342
381
  });
343
382
  }
344
- setGears((vorige) => {
383
+ setBlokknoppen((vorige) => {
345
384
  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
385
  return vorige;
347
386
  }
@@ -356,9 +395,117 @@ function VisualEditorInner({ onClose }) {
356
395
  window.removeEventListener("resize", meet);
357
396
  };
358
397
  }, [draft, displayBlocks, registryRef]);
398
+ const [productKnop, setProductKnop] = useState(null);
399
+ useEffect(() => {
400
+ if (!draft) return;
401
+ const onOver = (e) => {
402
+ const doel = e.target?.closest?.("[data-xicms-product]");
403
+ if (!doel) {
404
+ const naarKnop = e.target?.closest?.(".xicms-ve-blokknoppen");
405
+ if (!naarKnop) setProductKnop(null);
406
+ return;
407
+ }
408
+ const id = doel.dataset.xicmsProduct;
409
+ if (!id) return;
410
+ const r = doel.getBoundingClientRect();
411
+ setProductKnop(
412
+ (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) }
413
+ );
414
+ };
415
+ document.addEventListener("mouseover", onOver, true);
416
+ return () => document.removeEventListener("mouseover", onOver, true);
417
+ }, [draft]);
418
+ const productBewerkRef = useRef(null);
419
+ const startProductInline = useCallback(
420
+ (productId) => {
421
+ const host = document.querySelector(`[data-xicms-product="${CSS.escape(productId)}"]`);
422
+ const el = host?.querySelector("[data-xicms-product-edit]");
423
+ if (!el) {
424
+ toast({
425
+ title: "Hier valt niets te typen",
426
+ description: "Gebruik het tandwiel voor prijs, foto's, varianten en voorraad."
427
+ });
428
+ return;
429
+ }
430
+ const veld = el.dataset.xicmsProductEdit;
431
+ if (!veld) return;
432
+ const origineel = el.textContent ?? "";
433
+ el.setAttribute("contenteditable", "true");
434
+ el.setAttribute("spellcheck", "true");
435
+ el.classList.add("is-editing");
436
+ productBewerkRef.current = el;
437
+ const stop = () => {
438
+ el.removeAttribute("contenteditable");
439
+ el.classList.remove("is-editing");
440
+ el.removeEventListener("blur", opBlur);
441
+ el.removeEventListener("keydown", opToets);
442
+ if (productBewerkRef.current === el) productBewerkRef.current = null;
443
+ };
444
+ const opBlur = () => {
445
+ const waarde = el.textContent ?? "";
446
+ stop();
447
+ if (waarde === origineel) return;
448
+ void setProductField(siteId, productId, veld, waarde).then(
449
+ () => toast({
450
+ title: "Product bijgewerkt",
451
+ description: "Producten hebben geen werkversie: dit staat meteen live, op elke pagina.",
452
+ tone: "success"
453
+ })
454
+ ).catch((err) => {
455
+ el.textContent = origineel;
456
+ toast({ title: "Opslaan mislukt", description: err.message, tone: "error" });
457
+ });
458
+ };
459
+ const opToets = (ev) => {
460
+ if (ev.key === "Escape") {
461
+ ev.stopPropagation();
462
+ el.textContent = origineel;
463
+ el.blur();
464
+ } else if (ev.key === "Enter") {
465
+ ev.preventDefault();
466
+ el.blur();
467
+ }
468
+ };
469
+ el.addEventListener("blur", opBlur);
470
+ el.addEventListener("keydown", opToets);
471
+ el.focus();
472
+ const selection = window.getSelection();
473
+ const range = document.createRange();
474
+ range.selectNodeContents(el);
475
+ range.collapse(false);
476
+ selection?.removeAllRanges();
477
+ selection?.addRange(range);
478
+ },
479
+ [siteId, toast]
480
+ );
481
+ useEffect(() => {
482
+ if (!draft) return;
483
+ const gemarkeerd = [];
484
+ const markeer = (el, klasse) => {
485
+ if (!el) return;
486
+ el.classList.add(klasse);
487
+ gemarkeerd.push(el);
488
+ };
489
+ for (const key of wijzigingen.blokken) {
490
+ markeer(document.querySelector(`[data-xicms-block="${CSS.escape(key)}"]`), "xicms-ve-nieuw");
491
+ }
492
+ for (const [key, velden] of wijzigingen.velden) {
493
+ const host = document.querySelector(`[data-xicms-block="${CSS.escape(key)}"]`);
494
+ if (!host) continue;
495
+ for (const veld of velden) {
496
+ host.querySelectorAll(`[data-xicms-edit="${CSS.escape(veld)}"]`).forEach((el) => markeer(el, "xicms-ve-gewijzigd"));
497
+ }
498
+ }
499
+ return () => {
500
+ for (const el of gemarkeerd) {
501
+ el.classList.remove("xicms-ve-nieuw", "xicms-ve-gewijzigd");
502
+ }
503
+ };
504
+ }, [draft, wijzigingen, displayBlocks]);
359
505
  const [spots, setSpots] = useState([]);
360
506
  const targetsRef = useRef([]);
361
507
  const editingRef = useRef(null);
508
+ const wilInlineRef = useRef(false);
362
509
  const stopInline = useCallback((el) => {
363
510
  el.removeAttribute("contenteditable");
364
511
  el.classList.remove("is-editing");
@@ -420,8 +567,10 @@ function VisualEditorInner({ onClose }) {
420
567
  });
421
568
  }
422
569
  el.addEventListener("keydown", (e) => {
423
- if (e.key === "Escape") el.blur();
424
- else if (!html && e.key === "Enter") {
570
+ if (e.key === "Escape") {
571
+ e.stopPropagation();
572
+ el.blur();
573
+ } else if (!html && e.key === "Enter") {
425
574
  e.preventDefault();
426
575
  el.blur();
427
576
  }
@@ -454,6 +603,16 @@ function VisualEditorInner({ onClose }) {
454
603
  measure();
455
604
  };
456
605
  apply();
606
+ if (wilInlineRef.current) {
607
+ wilInlineRef.current = false;
608
+ if (targetsRef.current.length > 0) startInline(0);
609
+ else {
610
+ toast({
611
+ title: "Dit blok heeft geen eigen tekst",
612
+ description: "Wijs een product aan voor zijn naam, of gebruik het tandwiel voor de instellingen van dit blok."
613
+ });
614
+ }
615
+ }
457
616
  const obs = new MutationObserver(() => {
458
617
  if (!draggingRef.current) apply();
459
618
  });
@@ -478,7 +637,7 @@ function VisualEditorInner({ onClose }) {
478
637
  targetsRef.current = [];
479
638
  setSpots([]);
480
639
  };
481
- }, [selectedKey, commitText, positionFmtBar, stopInline]);
640
+ }, [selectedKey, commitText, positionFmtBar, stopInline, startInline, toast]);
482
641
  const dirtySinceRef = useRef(null);
483
642
  useEffect(() => {
484
643
  if (!draft) return;
@@ -507,7 +666,7 @@ function VisualEditorInner({ onClose }) {
507
666
  }, [draft, siteId, conflict]);
508
667
  useEffect(() => {
509
668
  const beforeUnload = (e) => {
510
- if (dirtyRef.current) {
669
+ if (dirtyRef.current || nietLiveRef.current) {
511
670
  e.preventDefault();
512
671
  e.returnValue = "";
513
672
  }
@@ -648,12 +807,24 @@ function VisualEditorInner({ onClose }) {
648
807
  setConflict(false);
649
808
  }, [siteId, path, mutateDraft]);
650
809
  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 }),
810
+ /* @__PURE__ */ jsxs("header", { className: "xicms-ve__bar", role: "toolbar", "aria-label": "Beheer", children: [
811
+ /* @__PURE__ */ jsx("span", { className: "xicms-editbar__brand", children: isDemoActive() ? "XiCMS \xB7 demo" : "XiCMS" }),
812
+ /* @__PURE__ */ jsx(
813
+ AdminModes,
814
+ {
815
+ actief: "bewerken",
816
+ voorSwitch: (m) => {
817
+ if (m === "live" && nietLiveRef.current) {
818
+ toast({
819
+ title: "Nog niet gepubliceerd",
820
+ description: `${aantalWijzigingen(wijzigingen)} blijft bewaard als werkversie. Je ziet ze weer zodra je terug naar Bewerken gaat.`
821
+ });
822
+ }
823
+ }
824
+ }
825
+ ),
826
+ /* @__PURE__ */ jsx("span", { className: "xicms-ve__sep", "aria-hidden": "true" }),
827
+ /* @__PURE__ */ jsx("span", { className: "xicms-ve__path", title: `Je bewerkt ${name} \u2014 ${path}`, children: path }),
657
828
  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
829
  "\u{1F310}",
659
830
  /* @__PURE__ */ jsx(
@@ -695,13 +866,9 @@ function VisualEditorInner({ onClose }) {
695
866
  ] }),
696
867
  /* @__PURE__ */ jsxs("span", { className: "xicms-ve__status", "aria-live": "polite", children: [
697
868
  status === "loading" ? "Werkversie laden\u2026" : null,
698
- status === "saving" ? "Opslaan\u2026" : status === "saved" ? "\u2713 Opgeslagen" : status === "error" ? "\u26A0 Niet opgeslagen" : ""
869
+ status === "saving" ? "Opslaan\u2026" : status === "saved" ? "\u2713 Bewaard als werkversie" : status === "error" ? "\u26A0 Niet opgeslagen" : ""
699
870
  ] }),
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
- ] })
871
+ status === "error" && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: () => void flushSave().catch(() => setStatus("error")), children: "Opnieuw opslaan" })
705
872
  ] }),
706
873
  conflict && /* @__PURE__ */ jsxs("div", { className: "xicms-ve__conflict", role: "alert", children: [
707
874
  /* @__PURE__ */ jsx("span", { children: "\u26A0 Deze pagina is intussen ergens anders gewijzigd (andere tab of collega). Doorwerken kan die wijzigingen overschrijven." }),
@@ -718,7 +885,16 @@ function VisualEditorInner({ onClose }) {
718
885
  /* @__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
886
  /* @__PURE__ */ jsx(PatternInserter, { onInsert: insertAfterSelected })
720
887
  ] }),
721
- selected && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
888
+ selectedProduct ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
889
+ /* @__PURE__ */ jsxs("div", { className: "xicms-ve__panel-head", children: [
890
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-slate-500", children: "Webwinkel" }),
891
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => {
892
+ setSelectedProduct(null);
893
+ setPanelOpen(false);
894
+ }, children: "\u2715 Sluiten" })
895
+ ] }),
896
+ /* @__PURE__ */ jsx(ProductPaneel, { siteId, productId: selectedProduct })
897
+ ] }) : selected && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
722
898
  /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold", children: [
723
899
  registry.get(selected._type)?.label ?? selected._type,
724
900
  " \u2014 vertaling (",
@@ -792,26 +968,46 @@ function VisualEditorInner({ onClose }) {
792
968
  }
793
969
  }
794
970
  ),
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
- )),
971
+ blokknoppen.map((b) => /* @__PURE__ */ jsxs("div", { className: "xicms-ve-blokknoppen", style: { top: b.top, left: b.left }, children: [
972
+ /* @__PURE__ */ jsx(
973
+ "button",
974
+ {
975
+ type: "button",
976
+ className: `xicms-ve-blokknop${b.key === selectedKey ? " is-on" : ""}`,
977
+ title: `Tekst van ${b.label} bewerken`,
978
+ "aria-label": `Tekst van ${b.label} bewerken`,
979
+ onMouseDown: (e) => e.preventDefault(),
980
+ onClick: () => {
981
+ wilInlineRef.current = true;
982
+ setSelectedProduct(null);
983
+ setSelectedKey(b.key);
984
+ },
985
+ 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: [
986
+ /* @__PURE__ */ jsx("path", { d: "M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3z" }),
987
+ /* @__PURE__ */ jsx("path", { d: "M14.5 6.5l3 3" })
988
+ ] })
989
+ }
990
+ ),
991
+ /* @__PURE__ */ jsx(
992
+ "button",
993
+ {
994
+ type: "button",
995
+ className: `xicms-ve-blokknop${b.key === selectedKey && panelOpen ? " is-on" : ""}`,
996
+ title: `Instellingen van ${b.label}`,
997
+ "aria-label": `Instellingen van ${b.label}`,
998
+ onMouseDown: (e) => e.preventDefault(),
999
+ onClick: () => {
1000
+ setSelectedProduct(null);
1001
+ setSelectedKey(b.key);
1002
+ setPanelOpen(true);
1003
+ },
1004
+ 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: [
1005
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
1006
+ /* @__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" })
1007
+ ] })
1008
+ }
1009
+ )
1010
+ ] }, b.key)),
815
1011
  spots.map((s, i) => /* @__PURE__ */ jsx(
816
1012
  "button",
817
1013
  {
@@ -831,6 +1027,63 @@ function VisualEditorInner({ onClose }) {
831
1027
  },
832
1028
  i
833
1029
  )),
1030
+ productKnop && /* @__PURE__ */ jsxs(
1031
+ "div",
1032
+ {
1033
+ className: "xicms-ve-blokknoppen is-product",
1034
+ style: { top: productKnop.top, left: productKnop.left },
1035
+ children: [
1036
+ /* @__PURE__ */ jsx(
1037
+ "button",
1038
+ {
1039
+ type: "button",
1040
+ className: "xicms-ve-blokknop",
1041
+ title: "Productnaam bewerken (staat meteen live)",
1042
+ "aria-label": "Productnaam bewerken",
1043
+ onMouseDown: (e) => e.preventDefault(),
1044
+ onClick: () => startProductInline(productKnop.id),
1045
+ 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: [
1046
+ /* @__PURE__ */ jsx("path", { d: "M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3z" }),
1047
+ /* @__PURE__ */ jsx("path", { d: "M14.5 6.5l3 3" })
1048
+ ] })
1049
+ }
1050
+ ),
1051
+ /* @__PURE__ */ jsx(
1052
+ "button",
1053
+ {
1054
+ type: "button",
1055
+ className: `xicms-ve-blokknop${selectedProduct === productKnop.id ? " is-on" : ""}`,
1056
+ title: "Product bewerken: prijs, foto's, varianten, voorraad",
1057
+ "aria-label": "Product bewerken",
1058
+ onMouseDown: (e) => e.preventDefault(),
1059
+ onClick: () => {
1060
+ setSelectedKey(null);
1061
+ setSelectedProduct(productKnop.id);
1062
+ setPanelOpen(true);
1063
+ },
1064
+ 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: [
1065
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
1066
+ /* @__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" })
1067
+ ] })
1068
+ }
1069
+ )
1070
+ ]
1071
+ }
1072
+ ),
1073
+ draft && /* @__PURE__ */ jsxs(
1074
+ "button",
1075
+ {
1076
+ type: "button",
1077
+ className: `xicms-ve-publiceer${wijzigingen.iets ? " is-klaar" : ""}`,
1078
+ disabled: publishing || !wijzigingen.iets,
1079
+ onClick: () => void onPublish(),
1080
+ title: wijzigingen.iets ? "Zet deze wijzigingen op de live site" : "Er is niets veranderd sinds de laatste publicatie",
1081
+ children: [
1082
+ /* @__PURE__ */ jsx("span", { className: "xicms-ve-publiceer__label", children: publishing ? "Bezig\u2026" : "Publiceren" }),
1083
+ /* @__PURE__ */ jsx("span", { className: "xicms-ve-publiceer__sub", children: wijzigingen.iets ? aantalWijzigingen(wijzigingen) : "niets gewijzigd" })
1084
+ ]
1085
+ }
1086
+ ),
834
1087
  fmtBar && /* @__PURE__ */ jsxs("div", { className: "xicms-ve-fmtbar", style: { top: fmtBar.top, left: fmtBar.left }, role: "toolbar", "aria-label": "Tekstopmaak", children: [
835
1088
  /* @__PURE__ */ jsx(FmtBtn, { label: /* @__PURE__ */ jsx("b", { children: "B" }), title: "Vet (Ctrl/Cmd+B)", onRun: () => document.execCommand("bold") }),
836
1089
  /* @__PURE__ */ jsx(FmtBtn, { label: /* @__PURE__ */ jsx("i", { children: "I" }), title: "Cursief (Ctrl/Cmd+I)", onRun: () => document.execCommand("italic") }),