@xiwebdesign/xicms-core 4.18.0 → 5.0.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 (117) hide show
  1. package/dist/XiCMSProvider.d.ts +7 -0
  2. package/dist/XiCMSProvider.js +29 -6
  3. package/dist/XiCMSProvider.js.map +1 -1
  4. package/dist/admin/blockEditors/GenericEditor.d.ts +22 -0
  5. package/dist/admin/blockEditors/GenericEditor.js +201 -0
  6. package/dist/admin/blockEditors/GenericEditor.js.map +1 -0
  7. package/dist/admin/blockEditors/SymbolRefEditor.d.ts +2 -0
  8. package/dist/admin/blockEditors/SymbolRefEditor.js +55 -0
  9. package/dist/admin/blockEditors/SymbolRefEditor.js.map +1 -0
  10. package/dist/admin/blockEditors/index.d.ts +11 -6
  11. package/dist/admin/blockEditors/index.js +4 -608
  12. package/dist/admin/blockEditors/index.js.map +1 -1
  13. package/dist/admin/customBlocks/CustomBlocksPanel.d.ts +3 -0
  14. package/dist/admin/customBlocks/CustomBlocksPanel.js +145 -0
  15. package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -0
  16. package/dist/admin/lib/symbolsClient.d.ts +13 -0
  17. package/dist/admin/lib/symbolsClient.js +51 -0
  18. package/dist/admin/lib/symbolsClient.js.map +1 -0
  19. package/dist/admin/lib/zodFields.d.ts +18 -0
  20. package/dist/admin/lib/zodFields.js +65 -2
  21. package/dist/admin/lib/zodFields.js.map +1 -1
  22. package/dist/admin/panelRegistry.js +3 -0
  23. package/dist/admin/panelRegistry.js.map +1 -1
  24. package/dist/admin/settings/SettingsPanel.js +2 -0
  25. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  26. package/dist/admin/settings/SnapshotsCard.d.ts +3 -0
  27. package/dist/admin/settings/SnapshotsCard.js +98 -0
  28. package/dist/admin/settings/SnapshotsCard.js.map +1 -0
  29. package/dist/admin/ui/dialog.js +11 -4
  30. package/dist/admin/ui/dialog.js.map +1 -1
  31. package/dist/admin/visual/TranslationPanel.d.ts +13 -0
  32. package/dist/admin/visual/TranslationPanel.js +73 -0
  33. package/dist/admin/visual/TranslationPanel.js.map +1 -0
  34. package/dist/admin/visual/VisualEditor.js +142 -17
  35. package/dist/admin/visual/VisualEditor.js.map +1 -1
  36. package/dist/admin.css +1 -1
  37. package/dist/agentFiles/catalog.d.ts +21 -0
  38. package/dist/agentFiles/catalog.js +78 -0
  39. package/dist/agentFiles/catalog.js.map +1 -0
  40. package/dist/agentFiles/files.d.ts +59 -0
  41. package/dist/agentFiles/files.js +109 -0
  42. package/dist/agentFiles/files.js.map +1 -0
  43. package/dist/blocks/builtins/BeforeAfter.js +1 -0
  44. package/dist/blocks/builtins/BeforeAfter.js.map +1 -1
  45. package/dist/blocks/builtins/Cards.js +1 -0
  46. package/dist/blocks/builtins/Cards.js.map +1 -1
  47. package/dist/blocks/builtins/Cta.js +1 -0
  48. package/dist/blocks/builtins/Cta.js.map +1 -1
  49. package/dist/blocks/builtins/Elements.js +1 -0
  50. package/dist/blocks/builtins/Elements.js.map +1 -1
  51. package/dist/blocks/builtins/Hero.js +2 -2
  52. package/dist/blocks/builtins/Hero.js.map +1 -1
  53. package/dist/blocks/builtins/Jobs.js +1 -0
  54. package/dist/blocks/builtins/Jobs.js.map +1 -1
  55. package/dist/blocks/builtins/Sections.js +1 -0
  56. package/dist/blocks/builtins/Sections.js.map +1 -1
  57. package/dist/blocks/builtins/SymbolRef.d.ts +2 -0
  58. package/dist/blocks/builtins/SymbolRef.js +61 -0
  59. package/dist/blocks/builtins/SymbolRef.js.map +1 -0
  60. package/dist/blocks/customTemplate/definition.d.ts +7 -0
  61. package/dist/blocks/customTemplate/definition.js +34 -0
  62. package/dist/blocks/customTemplate/definition.js.map +1 -0
  63. package/dist/blocks/customTemplate/interpret.d.ts +10 -0
  64. package/dist/blocks/customTemplate/interpret.js +136 -0
  65. package/dist/blocks/customTemplate/interpret.js.map +1 -0
  66. package/dist/blocks/registry.d.ts +8 -0
  67. package/dist/blocks/registry.js.map +1 -1
  68. package/dist/i18n/overlay.d.ts +61 -0
  69. package/dist/i18n/overlay.js +90 -0
  70. package/dist/i18n/overlay.js.map +1 -0
  71. package/dist/index.d.ts +10 -8
  72. package/dist/index.js +32 -9
  73. package/dist/index.js.map +1 -1
  74. package/dist/modules/builtins.js +3 -2
  75. package/dist/modules/builtins.js.map +1 -1
  76. package/dist/schema/blocks.d.ts +159 -0
  77. package/dist/schema/blocks.js +8 -0
  78. package/dist/schema/blocks.js.map +1 -1
  79. package/dist/schema/customBlocks.d.ts +47 -0
  80. package/dist/schema/customBlocks.js +32 -0
  81. package/dist/schema/customBlocks.js.map +1 -0
  82. package/dist/schema/fields.d.ts +122 -0
  83. package/dist/schema/fields.js +120 -0
  84. package/dist/schema/fields.js.map +1 -0
  85. package/dist/schema/forms.d.ts +6 -6
  86. package/dist/schema/pages.d.ts +15 -1
  87. package/dist/schema/pages.js +6 -2
  88. package/dist/schema/pages.js.map +1 -1
  89. package/dist/schema/siteConfig.d.ts +14 -8
  90. package/dist/schema/siteConfig.js +2 -1
  91. package/dist/schema/siteConfig.js.map +1 -1
  92. package/dist/schema/symbols.d.ts +205 -0
  93. package/dist/schema/symbols.js +18 -0
  94. package/dist/schema/symbols.js.map +1 -0
  95. package/dist/server/index.d.ts +2 -1
  96. package/dist/server/index.js +24 -0
  97. package/dist/server/index.js.map +1 -1
  98. package/dist/server/pages.d.ts +21 -0
  99. package/dist/server/pages.js +77 -0
  100. package/dist/server/pages.js.map +1 -1
  101. package/dist/server/seo.d.ts +10 -3
  102. package/dist/server/seo.js +22 -10
  103. package/dist/server/seo.js.map +1 -1
  104. package/dist/styles.css +27 -0
  105. package/package.json +9 -9
  106. package/dist/components/EditableImage.d.ts +0 -18
  107. package/dist/components/EditableImage.js +0 -62
  108. package/dist/components/EditableImage.js.map +0 -1
  109. package/dist/components/EditableRichText.d.ts +0 -14
  110. package/dist/components/EditableRichText.js +0 -26
  111. package/dist/components/EditableRichText.js.map +0 -1
  112. package/dist/components/EditableText.d.ts +0 -21
  113. package/dist/components/EditableText.js +0 -25
  114. package/dist/components/EditableText.js.map +0 -1
  115. package/dist/content/ContentProvider.d.ts +0 -23
  116. package/dist/content/ContentProvider.js +0 -86
  117. package/dist/content/ContentProvider.js.map +0 -1
@@ -0,0 +1,3 @@
1
+ export declare function SnapshotsCard({ siteId }: {
2
+ siteId: string;
3
+ }): import("react").JSX.Element;
@@ -0,0 +1,98 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { collection, limit, onSnapshot, orderBy, query } from "firebase/firestore";
5
+ import { getFunctions, httpsCallable } from "firebase/functions";
6
+ import { getDb, getFirebaseApp } from "../../firebase/client";
7
+ import { functionsRegion } from "../../firebase/region";
8
+ import { Button, Card, Input } from "../ui/primitives";
9
+ import { useToast } from "../ui/toast";
10
+ import { useConfirm } from "../ui/ConfirmDialog";
11
+ function formatDate(createdAt) {
12
+ const date = createdAt && typeof createdAt.toDate === "function" ? createdAt.toDate() : null;
13
+ return date ? date.toLocaleString("nl-NL", { dateStyle: "short", timeStyle: "short" }) : "";
14
+ }
15
+ function SnapshotsCard({ siteId }) {
16
+ const { toast } = useToast();
17
+ const confirm = useConfirm();
18
+ const [snapshots, setSnapshots] = useState([]);
19
+ const [label, setLabel] = useState("");
20
+ const [busy, setBusy] = useState(null);
21
+ useEffect(() => {
22
+ const db = getDb();
23
+ if (!db) return;
24
+ const q = query(collection(db, "sites", siteId, "snapshots"), orderBy("createdAt", "desc"), limit(20));
25
+ return onSnapshot(
26
+ q,
27
+ (snap) => setSnapshots(snap.docs.map((d) => ({ id: d.id, ...d.data() }))),
28
+ () => setSnapshots([])
29
+ );
30
+ }, [siteId]);
31
+ async function create() {
32
+ const app = getFirebaseApp();
33
+ if (!app) return;
34
+ setBusy("create");
35
+ try {
36
+ const fn = httpsCallable(getFunctions(app, functionsRegion()), "createSnapshot");
37
+ await fn({ siteId, label: label.trim() || void 0 });
38
+ setLabel("");
39
+ toast({ title: "Herstelpunt aangemaakt", tone: "success" });
40
+ } catch (e) {
41
+ toast({ title: "Herstelpunt mislukt", description: e.message, tone: "error" });
42
+ } finally {
43
+ setBusy(null);
44
+ }
45
+ }
46
+ async function restore(s) {
47
+ const ok = await confirm({
48
+ title: "Herstelpunt terugzetten?",
49
+ description: `De site wordt teruggezet naar "${s.label ?? s.id}" (${formatDate(s.createdAt)}). Er wordt eerst automatisch een nieuw herstelpunt van de huidige stand gemaakt.`,
50
+ confirmLabel: "Terugzetten",
51
+ tone: "danger"
52
+ });
53
+ if (!ok) return;
54
+ const app = getFirebaseApp();
55
+ if (!app) return;
56
+ setBusy(s.id);
57
+ try {
58
+ const fn = httpsCallable(getFunctions(app, functionsRegion()), "restoreSnapshot");
59
+ await fn({ siteId, snapshotId: s.id });
60
+ toast({ title: "Herstelpunt teruggezet", description: "Herlaad de pagina om alles bij te werken.", tone: "success" });
61
+ } catch (e) {
62
+ toast({ title: "Herstellen mislukt", description: e.message, tone: "error" });
63
+ } finally {
64
+ setBusy(null);
65
+ }
66
+ }
67
+ return /* @__PURE__ */ jsxs(Card, { children: [
68
+ /* @__PURE__ */ jsx("h3", { className: "mb-1 text-base font-semibold", children: "Herstelpunten" }),
69
+ /* @__PURE__ */ jsx("p", { className: "mb-3 text-xs text-slate-500", children: "Leg de huidige stand van de hele site vast (pagina's, instellingen, symbolen, bouwstenen) en zet hem later met \xE9\xE9n klik terug \u2014 zonder bestanden te downloaden. V\xF3\xF3r elk herstel wordt automatisch een nieuw herstelpunt gemaakt, dus terugzetten is altijd omkeerbaar." }),
70
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex flex-wrap items-center gap-2", children: [
71
+ /* @__PURE__ */ jsx(
72
+ Input,
73
+ {
74
+ value: label,
75
+ placeholder: "Naam (bv. v\xF3\xF3r grote wijziging)",
76
+ onChange: (e) => setLabel(e.target.value),
77
+ className: "max-w-xs"
78
+ }
79
+ ),
80
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", disabled: busy !== null, onClick: () => void create(), children: busy === "create" ? "Bezig\u2026" : "Herstelpunt maken" })
81
+ ] }),
82
+ snapshots.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-400", children: "Nog geen herstelpunten." }) : /* @__PURE__ */ jsx("ul", { className: "grid gap-1 text-sm", children: snapshots.map((s) => /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-2 rounded border border-slate-200 px-2 py-1.5", children: [
83
+ /* @__PURE__ */ jsxs("span", { children: [
84
+ s.kind === "auto" ? "\u{1F6DF} " : "\u{1F4CC} ",
85
+ s.label ?? "Herstelpunt",
86
+ /* @__PURE__ */ jsxs("span", { className: "text-slate-400", children: [
87
+ " \xB7 ",
88
+ formatDate(s.createdAt)
89
+ ] })
90
+ ] }),
91
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", disabled: busy !== null, onClick: () => void restore(s), children: busy === s.id ? "Bezig\u2026" : "Terugzetten" })
92
+ ] }, s.id)) })
93
+ ] });
94
+ }
95
+ export {
96
+ SnapshotsCard
97
+ };
98
+ //# sourceMappingURL=SnapshotsCard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/settings/SnapshotsCard.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * \"Herstelpunten\" (5.0, primo-concept site_snapshots): met één klik de volledige content-stand\n * van de site vastleggen in Firestore, en later terugzetten. Herstellen maakt automatisch eerst\n * een nieuw herstelpunt van de huidige stand (server-side), dus het is altijd omkeerbaar.\n * Aanvulling op de download-back-up: geen bestand nodig, en de automatische vangnetten\n * (vóór restore/push-site) verschijnen hier ook.\n */\nimport { useEffect, useState } from \"react\";\nimport { collection, limit, onSnapshot, orderBy, query } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { Button, Card, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\n\ninterface SnapshotMeta {\n id: string;\n label?: string;\n kind?: string;\n createdAt?: { toDate?: () => Date };\n}\n\nfunction formatDate(createdAt: SnapshotMeta[\"createdAt\"]): string {\n const date = createdAt && typeof createdAt.toDate === \"function\" ? createdAt.toDate() : null;\n return date ? date.toLocaleString(\"nl-NL\", { dateStyle: \"short\", timeStyle: \"short\" }) : \"\";\n}\n\nexport function SnapshotsCard({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [snapshots, setSnapshots] = useState<SnapshotMeta[]>([]);\n const [label, setLabel] = useState(\"\");\n const [busy, setBusy] = useState<string | null>(null);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const q = query(collection(db, \"sites\", siteId, \"snapshots\"), orderBy(\"createdAt\", \"desc\"), limit(20));\n return onSnapshot(\n q,\n (snap) => setSnapshots(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<SnapshotMeta, \"id\">) }))),\n () => setSnapshots([]),\n );\n }, [siteId]);\n\n async function create() {\n const app = getFirebaseApp();\n if (!app) return;\n setBusy(\"create\");\n try {\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"createSnapshot\");\n await fn({ siteId, label: label.trim() || undefined });\n setLabel(\"\");\n toast({ title: \"Herstelpunt aangemaakt\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Herstelpunt mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(null);\n }\n }\n\n async function restore(s: SnapshotMeta) {\n const ok = await confirm({\n title: \"Herstelpunt terugzetten?\",\n description: `De site wordt teruggezet naar \"${s.label ?? s.id}\" (${formatDate(s.createdAt)}). Er wordt eerst automatisch een nieuw herstelpunt van de huidige stand gemaakt.`,\n confirmLabel: \"Terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n const app = getFirebaseApp();\n if (!app) return;\n setBusy(s.id);\n try {\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"restoreSnapshot\");\n await fn({ siteId, snapshotId: s.id });\n toast({ title: \"Herstelpunt teruggezet\", description: \"Herlaad de pagina om alles bij te werken.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Herstellen mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(null);\n }\n }\n\n return (\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Herstelpunten</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Leg de huidige stand van de hele site vast (pagina's, instellingen, symbolen, bouwstenen) en\n zet hem later met één klik terug — zonder bestanden te downloaden. Vóór elk herstel wordt\n automatisch een nieuw herstelpunt gemaakt, dus terugzetten is altijd omkeerbaar.\n </p>\n <div className=\"mb-3 flex flex-wrap items-center gap-2\">\n <Input\n value={label}\n placeholder=\"Naam (bv. vóór grote wijziging)\"\n onChange={(e) => setLabel(e.target.value)}\n className=\"max-w-xs\"\n />\n <Button variant=\"secondary\" disabled={busy !== null} onClick={() => void create()}>\n {busy === \"create\" ? \"Bezig…\" : \"Herstelpunt maken\"}\n </Button>\n </div>\n {snapshots.length === 0 ? (\n <p className=\"text-xs text-slate-400\">Nog geen herstelpunten.</p>\n ) : (\n <ul className=\"grid gap-1 text-sm\">\n {snapshots.map((s) => (\n <li key={s.id} className=\"flex items-center justify-between gap-2 rounded border border-slate-200 px-2 py-1.5\">\n <span>\n {s.kind === \"auto\" ? \"🛟 \" : \"📌 \"}\n {s.label ?? \"Herstelpunt\"}\n <span className=\"text-slate-400\"> · {formatDate(s.createdAt)}</span>\n </span>\n <Button variant=\"ghost\" size=\"sm\" disabled={busy !== null} onClick={() => void restore(s)}>\n {busy === s.id ? \"Bezig…\" : \"Terugzetten\"}\n </Button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
@@ -17,10 +17,17 @@ function DialogContent({
17
17
  /* @__PURE__ */ jsxs(
18
18
  DialogPrimitive.Content,
19
19
  {
20
- className: cn(
21
- "fixed left-1/2 top-1/2 z-[10011] w-[92vw] max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-xl bg-white p-5 shadow-xl focus:outline-none",
22
- className
23
- ),
20
+ style: {
21
+ position: "fixed",
22
+ left: "50%",
23
+ top: "50%",
24
+ transform: "translate(-50%, -50%)",
25
+ width: "min(92vw, 32rem)",
26
+ maxHeight: "85vh",
27
+ overflowY: "auto",
28
+ zIndex: 10011
29
+ },
30
+ className: cn("rounded-xl bg-white p-5 shadow-xl focus:outline-none", className),
24
31
  ...props,
25
32
  children: [
26
33
  /* @__PURE__ */ jsx(DialogPrimitive.Title, { className: "mb-3 text-lg font-semibold", children: title }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/ui/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { cn } from \"./cn\";\n\nexport const Dialog = DialogPrimitive.Root;\nexport const DialogTrigger = DialogPrimitive.Trigger;\nexport const DialogClose = DialogPrimitive.Close;\n\nexport function DialogContent({\n className,\n children,\n title,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & { title: string }) {\n // De admin-Tailwind is gescoped onder `.xicms-admin` (important: \".xicms-admin\"). Radix portalt\n // standaard naar <body> — buiten die scope, waardoor de dialog volledig ONGESTYLED zou renderen\n // (static, geen achtergrond, achter de editor). Daarom portalen we IN de admin-scope.\n const container =\n typeof document !== \"undefined\"\n ? document.querySelector<HTMLElement>(\".xicms-ve\") ?? document.querySelector<HTMLElement>(\".xicms-admin\")\n : null;\n return (\n <DialogPrimitive.Portal container={container ?? undefined}>\n {/* Boven de VisualEditor-overlay (z-index 10000) zodat de kiezer niet erachter verdwijnt. */}\n <DialogPrimitive.Overlay className=\"fixed inset-0 z-[10010] bg-black/50\" />\n <DialogPrimitive.Content\n className={cn(\n \"fixed left-1/2 top-1/2 z-[10011] w-[92vw] max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-xl bg-white p-5 shadow-xl focus:outline-none\",\n className,\n )}\n {...props}\n >\n <DialogPrimitive.Title className=\"mb-3 text-lg font-semibold\">{title}</DialogPrimitive.Title>\n {children}\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n}\n\nexport function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return <DialogPrimitive.Description className={cn(\"text-sm text-slate-600\", className)} {...props} />;\n}\n\nexport function DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={cn(\"mt-5 flex items-center justify-end gap-2\", className)} {...props} />;\n}\n"],"mappings":";AAyBM,cACA,YADA;AAvBN,YAAY,qBAAqB;AACjC,SAAS,UAAU;AAEZ,MAAM,SAAS,gBAAgB;AAC/B,MAAM,gBAAgB,gBAAgB;AACtC,MAAM,cAAc,gBAAgB;AAEpC,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA6E;AAI3E,QAAM,YACJ,OAAO,aAAa,cAChB,SAAS,cAA2B,WAAW,KAAK,SAAS,cAA2B,cAAc,IACtG;AACN,SACE,qBAAC,gBAAgB,QAAhB,EAAuB,WAAW,aAAa,QAE9C;AAAA,wBAAC,gBAAgB,SAAhB,EAAwB,WAAU,uCAAsC;AAAA,IACzE;AAAA,MAAC,gBAAgB;AAAA,MAAhB;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QACC,GAAG;AAAA,QAEJ;AAAA,8BAAC,gBAAgB,OAAhB,EAAsB,WAAU,8BAA8B,iBAAM;AAAA,UACpE;AAAA;AAAA;AAAA,IACH;AAAA,KACF;AAEJ;AAEO,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAA6D;AACnH,SAAO,oBAAC,gBAAgB,aAAhB,EAA4B,WAAW,GAAG,0BAA0B,SAAS,GAAI,GAAG,OAAO;AACrG;AAEO,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAgC;AACjF,SAAO,oBAAC,SAAI,WAAW,GAAG,4CAA4C,SAAS,GAAI,GAAG,OAAO;AAC/F;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/ui/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { cn } from \"./cn\";\n\nexport const Dialog = DialogPrimitive.Root;\nexport const DialogTrigger = DialogPrimitive.Trigger;\nexport const DialogClose = DialogPrimitive.Close;\n\nexport function DialogContent({\n className,\n children,\n title,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & { title: string }) {\n // De admin-Tailwind is gescoped onder `.xicms-admin` (important: \".xicms-admin\"). Radix portalt\n // standaard naar <body> — buiten die scope, waardoor de dialog volledig ONGESTYLED zou renderen\n // (static, geen achtergrond, achter de editor). Daarom portalen we IN de admin-scope.\n const container =\n typeof document !== \"undefined\"\n ? document.querySelector<HTMLElement>(\".xicms-ve\") ?? document.querySelector<HTMLElement>(\".xicms-admin\")\n : null;\n return (\n <DialogPrimitive.Portal container={container ?? undefined}>\n {/* Boven de VisualEditor-overlay (z-index 10000) zodat de kiezer niet erachter verdwijnt. */}\n <DialogPrimitive.Overlay className=\"fixed inset-0 z-[10010] bg-black/50\" />\n <DialogPrimitive.Content\n // Centreren + maat via INLINE style i.p.v. Tailwind-utilities: `-translate-x-1/2` werd in de\n // gescopete/geportalde admin-context (Tailwind v4) niet toegepast, waardoor de dialog op\n // `left:50%` bleef hangen en op smalle schermen rechts buiten beeld liep. Inline styles omzeilen\n // de scoping/laag-volgorde volledig en winnen altijd. Content blijft een directe Portal-child\n // (wrappen in een <div> brak Radix → \"Maximum update depth exceeded\"). max-height + overflow\n // houdt de kiezer op mobiel binnen het scherm.\n style={{\n position: \"fixed\",\n left: \"50%\",\n top: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: \"min(92vw, 32rem)\",\n maxHeight: \"85vh\",\n overflowY: \"auto\",\n zIndex: 10011,\n }}\n className={cn(\"rounded-xl bg-white p-5 shadow-xl focus:outline-none\", className)}\n {...props}\n >\n <DialogPrimitive.Title className=\"mb-3 text-lg font-semibold\">{title}</DialogPrimitive.Title>\n {children}\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n}\n\nexport function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return <DialogPrimitive.Description className={cn(\"text-sm text-slate-600\", className)} {...props} />;\n}\n\nexport function DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={cn(\"mt-5 flex items-center justify-end gap-2\", className)} {...props} />;\n}\n"],"mappings":";AAyBM,cACA,YADA;AAvBN,YAAY,qBAAqB;AACjC,SAAS,UAAU;AAEZ,MAAM,SAAS,gBAAgB;AAC/B,MAAM,gBAAgB,gBAAgB;AACtC,MAAM,cAAc,gBAAgB;AAEpC,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA6E;AAI3E,QAAM,YACJ,OAAO,aAAa,cAChB,SAAS,cAA2B,WAAW,KAAK,SAAS,cAA2B,cAAc,IACtG;AACN,SACE,qBAAC,gBAAgB,QAAhB,EAAuB,WAAW,aAAa,QAE9C;AAAA,wBAAC,gBAAgB,SAAhB,EAAwB,WAAU,uCAAsC;AAAA,IACzE;AAAA,MAAC,gBAAgB;AAAA,MAAhB;AAAA,QAOC,OAAO;AAAA,UACL,UAAU;AAAA,UACV,MAAM;AAAA,UACN,KAAK;AAAA,UACL,WAAW;AAAA,UACX,OAAO;AAAA,UACP,WAAW;AAAA,UACX,WAAW;AAAA,UACX,QAAQ;AAAA,QACV;AAAA,QACA,WAAW,GAAG,wDAAwD,SAAS;AAAA,QAC9E,GAAG;AAAA,QAEJ;AAAA,8BAAC,gBAAgB,OAAhB,EAAsB,WAAU,8BAA8B,iBAAM;AAAA,UACpE;AAAA;AAAA;AAAA,IACH;AAAA,KACF;AAEJ;AAEO,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAA6D;AACnH,SAAO,oBAAC,gBAAgB,aAAhB,EAA4B,WAAW,GAAG,0BAA0B,SAAS,GAAI,GAAG,OAAO;AACrG;AAEO,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAgC;AACjF,SAAO,oBAAC,SAAI,WAAW,GAAG,4CAA4C,SAAS,GAAI,GAAG,OAAO;AAC/F;","names":[]}
@@ -0,0 +1,13 @@
1
+ import type { BlockRegistry } from "../../blocks/registry";
2
+ export declare function TranslationPanel({ block, siteId, registry, locale, overlay, onTranslate, }: {
3
+ block: Record<string, unknown> & {
4
+ _key: string;
5
+ _type: string;
6
+ };
7
+ siteId: string;
8
+ registry: BlockRegistry;
9
+ locale: string;
10
+ /** De overlay van de actieve taal (gepunte sleutels → vertaalde tekst). */
11
+ overlay: Record<string, string>;
12
+ onTranslate: (key: string, value: string) => void;
13
+ }): import("react").JSX.Element;
@@ -0,0 +1,73 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useMemo } from "react";
4
+ import { introspectToFields } from "../lib/zodFields";
5
+ import { RichTextEditor } from "../editors/RichTextEditor";
6
+ import { Field as FieldRow, Input, Textarea } from "../ui/primitives";
7
+ function collectEntries(fields, value, prefix = "", labelPrefix = "") {
8
+ const out = [];
9
+ for (const f of fields) {
10
+ const v = value?.[f.key];
11
+ const label = `${labelPrefix}${f.label || f.key}`;
12
+ switch (f.type) {
13
+ case "text":
14
+ case "textarea":
15
+ case "richtext":
16
+ out.push({ path: `${prefix}${f.key}`, label, kind: f.type, canonical: typeof v === "string" ? v : "" });
17
+ break;
18
+ case "link": {
19
+ const lbl = v?.label;
20
+ out.push({ path: `${prefix}${f.key}.label`, label: `${label} (tekst)`, kind: "text", canonical: typeof lbl === "string" ? lbl : "" });
21
+ break;
22
+ }
23
+ case "group":
24
+ out.push(...collectEntries(f.fields, v ?? {}, `${prefix}${f.key}.`, `${label} \xB7 `));
25
+ break;
26
+ case "repeater": {
27
+ const items = Array.isArray(v) ? v : [];
28
+ items.forEach((item, i) => {
29
+ out.push(...collectEntries(f.fields, item ?? {}, `${prefix}${f.key}.${i}.`, `${f.itemLabel ?? label} ${i + 1} \xB7 `));
30
+ });
31
+ break;
32
+ }
33
+ default:
34
+ break;
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+ function TranslationPanel({
40
+ block,
41
+ siteId,
42
+ registry,
43
+ locale,
44
+ overlay,
45
+ onTranslate
46
+ }) {
47
+ const def = registry.get(block._type);
48
+ const entries = useMemo(() => {
49
+ if (!def) return [];
50
+ const custom = def.customFields;
51
+ const fields = custom ?? introspectToFields(def.schema, def.fields);
52
+ return collectEntries(fields, block);
53
+ }, [def, block]);
54
+ if (entries.length === 0) {
55
+ return /* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Dit blok heeft geen vertaalbare tekstvelden \u2014 de canonieke inhoud wordt in elke taal getoond." });
56
+ }
57
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
58
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-slate-500", children: [
59
+ "Vertaling ",
60
+ /* @__PURE__ */ jsx("strong", { children: locale }),
61
+ " \u2014 leeg laten = de originele tekst gebruiken. Blokken, afbeeldingen en links wijzig je in de standaardtaal."
62
+ ] }),
63
+ entries.map((e) => {
64
+ const key = `${block._key}.${e.path}`;
65
+ const value = overlay[key] ?? "";
66
+ return /* @__PURE__ */ jsx(FieldRow, { label: e.label, hint: e.canonical ? `Origineel: ${e.canonical.replace(/<[^>]+>/g, "").slice(0, 120)}` : void 0, children: e.kind === "richtext" ? /* @__PURE__ */ jsx(RichTextEditor, { siteId, value: value || e.canonical, onChange: (html) => onTranslate(key, html) }) : e.kind === "textarea" ? /* @__PURE__ */ jsx(Textarea, { rows: 3, value, placeholder: e.canonical, onChange: (ev) => onTranslate(key, ev.target.value) }) : /* @__PURE__ */ jsx(Input, { value, placeholder: e.canonical, onChange: (ev) => onTranslate(key, ev.target.value) }) }, key);
67
+ })
68
+ ] });
69
+ }
70
+ export {
71
+ TranslationPanel
72
+ };
73
+ //# sourceMappingURL=TranslationPanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/visual/TranslationPanel.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Vertaalpaneel (5.0): het zijpaneel van de visuele editor in een NIET-default taal. Toont per\n * blok alleen de vertaalbare velden (text/textarea/richtext + link-labels, afgeleid uit de\n * Field-IR en de canonieke waarden — repeater-items dus stuk voor stuk) en schrijft naar de\n * taal-overlay (`translations[locale][\"<blokKey>.<veldpad>\"]`). Leegmaken = terugvallen op de\n * canonieke tekst. Structuurwijzigingen (blokken, afbeeldingen, links) blijven canoniek.\n */\nimport { useMemo } from \"react\";\nimport type { BlockRegistry } from \"../../blocks/registry\";\nimport type { Field } from \"../../schema/fields\";\nimport { introspectToFields } from \"../lib/zodFields\";\nimport { RichTextEditor } from \"../editors/RichTextEditor\";\nimport { Field as FieldRow, Input, Textarea } from \"../ui/primitives\";\n\ninterface Entry {\n /** Veldpad binnen het blok, bv. \"items.0.title\". */\n path: string;\n label: string;\n kind: \"text\" | \"textarea\" | \"richtext\";\n canonical: string;\n}\n\nfunction collectEntries(fields: Field[], value: Record<string, unknown>, prefix = \"\", labelPrefix = \"\"): Entry[] {\n const out: Entry[] = [];\n for (const f of fields) {\n const v = value?.[f.key];\n const label = `${labelPrefix}${f.label || f.key}`;\n switch (f.type) {\n case \"text\":\n case \"textarea\":\n case \"richtext\":\n out.push({ path: `${prefix}${f.key}`, label, kind: f.type, canonical: typeof v === \"string\" ? v : \"\" });\n break;\n case \"link\": {\n const lbl = (v as { label?: unknown } | undefined)?.label;\n out.push({ path: `${prefix}${f.key}.label`, label: `${label} (tekst)`, kind: \"text\", canonical: typeof lbl === \"string\" ? lbl : \"\" });\n break;\n }\n case \"group\":\n out.push(...collectEntries(f.fields, (v as Record<string, unknown>) ?? {}, `${prefix}${f.key}.`, `${label} · `));\n break;\n case \"repeater\": {\n const items = Array.isArray(v) ? (v as Record<string, unknown>[]) : [];\n items.forEach((item, i) => {\n out.push(...collectEntries(f.fields, item ?? {}, `${prefix}${f.key}.${i}.`, `${f.itemLabel ?? label} ${i + 1} · `));\n });\n break;\n }\n default:\n break;\n }\n }\n return out;\n}\n\nexport function TranslationPanel({\n block,\n siteId,\n registry,\n locale,\n overlay,\n onTranslate,\n}: {\n block: Record<string, unknown> & { _key: string; _type: string };\n siteId: string;\n registry: BlockRegistry;\n locale: string;\n /** De overlay van de actieve taal (gepunte sleutels → vertaalde tekst). */\n overlay: Record<string, string>;\n onTranslate: (key: string, value: string) => void;\n}) {\n const def = registry.get(block._type);\n const entries = useMemo(() => {\n if (!def) return [];\n const custom = (def as { customFields?: Field[] }).customFields;\n const fields = custom ?? introspectToFields(def.schema, def.fields);\n return collectEntries(fields, block);\n }, [def, block]);\n\n if (entries.length === 0) {\n return <p className=\"xicms-ve__hint\">Dit blok heeft geen vertaalbare tekstvelden — de canonieke inhoud wordt in elke taal getoond.</p>;\n }\n\n return (\n <div className=\"grid gap-3\">\n <p className=\"text-xs text-slate-500\">\n Vertaling <strong>{locale}</strong> — leeg laten = de originele tekst gebruiken. Blokken,\n afbeeldingen en links wijzig je in de standaardtaal.\n </p>\n {entries.map((e) => {\n const key = `${block._key}.${e.path}`;\n const value = overlay[key] ?? \"\";\n return (\n <FieldRow key={key} label={e.label} hint={e.canonical ? `Origineel: ${e.canonical.replace(/<[^>]+>/g, \"\").slice(0, 120)}` : undefined}>\n {e.kind === \"richtext\" ? (\n <RichTextEditor siteId={siteId} value={value || e.canonical} onChange={(html) => onTranslate(key, html)} />\n ) : e.kind === \"textarea\" ? (\n <Textarea rows={3} value={value} placeholder={e.canonical} onChange={(ev) => onTranslate(key, ev.target.value)} />\n ) : (\n <Input value={value} placeholder={e.canonical} onChange={(ev) => onTranslate(key, ev.target.value)} />\n )}\n </FieldRow>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -4,6 +4,7 @@ import "@xiwebdesign/xicms-core/admin.css";
4
4
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { useSiteConfig } from "../../config";
6
6
  import { useRegistry } from "../../blocks/context";
7
+ import { introspectToFields } from "../lib/zodFields";
7
8
  import { BlockErrorBoundary } from "../../blocks/BlockErrorBoundary";
8
9
  import { InlineImageEditProvider } from "../../media/ImageContext";
9
10
  import { MediaPickerDialog } from "../media/MediaPicker";
@@ -13,22 +14,29 @@ import { getDraftByPath, saveDraft, publishPage, subscribeDraft, subscribeVersio
13
14
  import { isDemoActive } from "../../demo/demoStore";
14
15
  import { builtinBlockEditors, GenericEditor } from "../blockEditors";
15
16
  import { StyleEditor } from "../blockEditors/StyleEditor";
17
+ import { siteLocales, parseLocaleSlugs, localizeBlocks } from "../../i18n/overlay";
18
+ import { TranslationPanel } from "./TranslationPanel";
16
19
  import { reorderBlocks } from "./reorder";
17
20
  import { PatternInserter } from "../patterns/PatternInserter";
18
21
  import { Button, Field, Input, Textarea } from "../ui/primitives";
19
22
  import { Select } from "../ui/select";
20
23
  import { ToastProvider, useToast } from "../ui/toast";
21
24
  import { ConfirmHost, useConfirm } from "../ui/ConfirmDialog";
22
- const INLINE_FIELDS = {
23
- hero: [
24
- { selector: ".xicms-hero__title", field: "title" },
25
- { selector: ".xicms-hero__sub", field: "subtitle" }
26
- ]
27
- };
28
25
  const GLOBAL_INLINE = [
29
26
  { selector: ".xicms-richtext", field: "html", html: true },
30
27
  { selector: "[data-xicms-edit]", field: "", html: false, fromData: true }
31
28
  ];
29
+ const inlineCoverageWarned = /* @__PURE__ */ new Set();
30
+ function warnInlineCoverage(type, def, host) {
31
+ if (process.env.NODE_ENV === "production" || !def || inlineCoverageWarned.has(type)) return;
32
+ inlineCoverageWarned.add(type);
33
+ 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}"]`));
34
+ if (missing.length > 0) {
35
+ console.warn(
36
+ `[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.`
37
+ );
38
+ }
39
+ }
32
40
  function getPath(obj, path) {
33
41
  return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], obj);
34
42
  }
@@ -59,11 +67,20 @@ function VisualEditor({ onClose }) {
59
67
  return /* @__PURE__ */ jsx("div", { className: "xicms-admin xicms-ve", children: /* @__PURE__ */ jsx(ToastProvider, { children: /* @__PURE__ */ jsx(ConfirmHost, { children: /* @__PURE__ */ jsx(VisualEditorInner, { onClose }) }) }) });
60
68
  }
61
69
  function VisualEditorInner({ onClose }) {
62
- const { siteId, name } = useSiteConfig();
70
+ const config = useSiteConfig();
71
+ const { siteId, name } = config;
63
72
  const registry = useRegistry();
64
73
  const { toast } = useToast();
65
74
  const confirm = useConfirm();
66
- const path = useMemo(() => typeof window !== "undefined" ? window.location.pathname || "/" : "/", []);
75
+ const localeCfg = useMemo(() => siteLocales(config), [config]);
76
+ const { path, urlLocale } = useMemo(() => {
77
+ const raw = typeof window !== "undefined" ? window.location.pathname || "/" : "/";
78
+ const { locale: locale2, slugs } = parseLocaleSlugs(raw.split("/").filter(Boolean), localeCfg);
79
+ const p = "/" + slugs.join("/");
80
+ return { path: p === "/" ? "/" : p.replace(/\/$/, ""), urlLocale: locale2 };
81
+ }, [localeCfg]);
82
+ const [locale, setLocale] = useState(urlLocale);
83
+ const isTranslating = locale !== localeCfg.defaultLocale;
67
84
  const [draft, setDraft] = useState(null);
68
85
  const [status, setStatus] = useState("loading");
69
86
  const [selectedKey, setSelectedKey] = useState(null);
@@ -138,7 +155,9 @@ function VisualEditorInner({ onClose }) {
138
155
  title: d.title,
139
156
  slug: d.slug,
140
157
  path: d.path,
141
- seo: d.seo
158
+ seo: d.seo,
159
+ // Alleen meesturen wanneer aanwezig: `undefined` weigert de Firestore-client.
160
+ ...d.translations ? { translations: d.translations } : {}
142
161
  });
143
162
  const flushSave = useCallback(async () => {
144
163
  const d = draftRef.current;
@@ -160,7 +179,7 @@ function VisualEditorInner({ onClose }) {
160
179
  if (!active) return;
161
180
  if (d) {
162
181
  setDraft(d);
163
- lastSavedRef.current = stableStringify({ blocks: d.blocks, title: d.title, slug: d.slug, path: d.path, seo: d.seo });
182
+ lastSavedRef.current = stableStringify(draftPayload(d));
164
183
  setStatus("idle");
165
184
  } else {
166
185
  setStatus("none");
@@ -175,13 +194,18 @@ function VisualEditorInner({ onClose }) {
175
194
  if (!draftId) return;
176
195
  return subscribeDraft(siteId, draftId, (remote, meta) => {
177
196
  if (!meta.fromServer || !remote) return;
178
- const remotePayload = stableStringify({ blocks: remote.blocks, title: remote.title, slug: remote.slug, path: remote.path, seo: remote.seo });
197
+ const remotePayload = stableStringify(draftPayload(remote));
179
198
  const local = draftRef.current;
180
199
  const localPayload = local ? stableStringify(draftPayload(local)) : null;
181
200
  if (remotePayload !== lastSavedRef.current && remotePayload !== localPayload) setConflict(true);
182
201
  });
183
202
  }, [siteId, draftId]);
184
203
  const blocks = draft?.blocks ?? [];
204
+ const overlay = draft?.translations?.[locale];
205
+ const displayBlocks = useMemo(
206
+ () => isTranslating ? localizeBlocks(blocks, overlay) : blocks,
207
+ [blocks, isTranslating, overlay]
208
+ );
185
209
  const commitInline = useCallback(
186
210
  (key, field, value) => {
187
211
  mutateDraft(
@@ -198,12 +222,34 @@ function VisualEditorInner({ onClose }) {
198
222
  },
199
223
  [mutateDraft]
200
224
  );
225
+ const setTranslation = useCallback(
226
+ (tkey, value) => {
227
+ mutateDraft(
228
+ (d) => {
229
+ const overlay2 = { ...d.translations?.[locale] ?? {} };
230
+ if (value.trim() === "" || value === void 0) delete overlay2[tkey];
231
+ else overlay2[tkey] = value;
232
+ return { ...d, translations: { ...d.translations ?? {}, [locale]: overlay2 } };
233
+ },
234
+ { coalesce: true }
235
+ );
236
+ },
237
+ [mutateDraft, locale]
238
+ );
239
+ const commitText = useCallback(
240
+ (key, field, value) => {
241
+ if (isTranslating) setTranslation(`${key}.${field}`, value);
242
+ else commitInline(key, field, value);
243
+ },
244
+ [isTranslating, setTranslation, commitInline]
245
+ );
201
246
  useEffect(() => {
202
247
  if (!selectedKey) return;
203
248
  const host = canvasRef.current?.querySelector(`[data-ve-content="${selectedKey}"]`);
204
249
  if (!host) return;
205
250
  const type = blocks.find((b) => b._key === selectedKey)?._type ?? "";
206
- const specs = [...INLINE_FIELDS[type] ?? [], ...GLOBAL_INLINE];
251
+ const specs = [...GLOBAL_INLINE];
252
+ warnInlineCoverage(String(type), registry.get(String(type)), host);
207
253
  const bound = /* @__PURE__ */ new Set();
208
254
  const bind = (el, field, html) => {
209
255
  if (el.dataset.veInline === "1") return;
@@ -211,7 +257,7 @@ function VisualEditorInner({ onClose }) {
211
257
  el.setAttribute("contenteditable", "true");
212
258
  el.setAttribute("spellcheck", "true");
213
259
  el.classList.add("xicms-ve-inline");
214
- el.addEventListener("blur", () => commitInline(selectedKey, field, html ? el.innerHTML : el.textContent ?? ""));
260
+ el.addEventListener("blur", () => commitText(selectedKey, field, html ? el.innerHTML : el.textContent ?? ""));
215
261
  el.addEventListener("keydown", (e) => {
216
262
  if (e.key === "Escape") el.blur();
217
263
  else if (!html && e.key === "Enter") {
@@ -241,7 +287,7 @@ function VisualEditorInner({ onClose }) {
241
287
  delete el.dataset.veInline;
242
288
  });
243
289
  };
244
- }, [selectedKey, blocks, commitInline]);
290
+ }, [selectedKey, blocks, commitText]);
245
291
  const dirtySinceRef = useRef(null);
246
292
  useEffect(() => {
247
293
  if (!draft) return;
@@ -327,6 +373,28 @@ function VisualEditorInner({ onClose }) {
327
373
  copy.splice(i + 1, 0, { ...blocks[i], _key: newKey() });
328
374
  setBlocks(copy);
329
375
  }
376
+ async function detachSymbol(i) {
377
+ const blk = blocks[i];
378
+ if (blk._type !== "symbolRef" || !blk.symbolId) return;
379
+ const ok = await confirm({
380
+ title: "Symbool loskoppelen?",
381
+ description: "Dit blok wordt een losse kopie en beweegt niet meer mee met het gedeelde symbool.",
382
+ confirmLabel: "Loskoppelen"
383
+ });
384
+ if (!ok) return;
385
+ const { getSymbolOnce } = await import("../lib/symbolsClient");
386
+ const sym = await getSymbolOnce(siteId, blk.symbolId);
387
+ if (!sym) {
388
+ toast({ title: "Symbool niet gevonden", tone: "error" });
389
+ return;
390
+ }
391
+ const clones = sym.blocks.map((b) => ({ ...b, _key: newKey() }));
392
+ const copy = [...blocks];
393
+ copy.splice(i, 1, ...clones);
394
+ setBlocks(copy);
395
+ if (selectedKey === blk._key) setSelectedKey(null);
396
+ toast({ title: "Losgekoppeld", description: "Dit is nu een losse kopie.", tone: "success" });
397
+ }
330
398
  async function remove(i) {
331
399
  const ok = await confirm({ title: "Blok verwijderen?", confirmLabel: "Verwijderen", tone: "danger" });
332
400
  if (!ok) return;
@@ -353,7 +421,7 @@ function VisualEditorInner({ onClose }) {
353
421
  if (!ok) return;
354
422
  setPublishing(true);
355
423
  try {
356
- await saveDraft(siteId, draft.id, { blocks: draft.blocks, title: draft.title, slug: draft.slug, path: draft.path, seo: draft.seo });
424
+ await saveDraft(siteId, draft.id, draftPayload(draft));
357
425
  await publishPage(siteId, draft.id);
358
426
  toast({ title: "Gepubliceerd", description: "Herlaad de pagina om het live resultaat te zien.", tone: "success" });
359
427
  } catch (e) {
@@ -384,6 +452,17 @@ function VisualEditorInner({ onClose }) {
384
452
  name
385
453
  ] }),
386
454
  /* @__PURE__ */ jsx("span", { className: "xicms-ve__path", children: path }),
455
+ 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: [
456
+ "\u{1F310}",
457
+ /* @__PURE__ */ jsx(
458
+ Select,
459
+ {
460
+ value: locale,
461
+ onValueChange: setLocale,
462
+ options: localeCfg.locales.map((l) => ({ value: l, label: l === localeCfg.defaultLocale ? `${l} (standaard)` : l }))
463
+ }
464
+ )
465
+ ] }),
387
466
  /* @__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" }),
388
467
  /* @__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" }),
389
468
  /* @__PURE__ */ jsx("span", { className: "xicms-ve__status", "aria-live": "polite", children: status === "saving" ? "Opslaan\u2026" : status === "saved" ? "\u2713 Opgeslagen" : status === "error" ? "\u26A0 Niet opgeslagen" : "" }),
@@ -411,7 +490,7 @@ function VisualEditorInner({ onClose }) {
411
490
  ),
412
491
  draft && /* @__PURE__ */ jsxs("div", { className: "container", ref: canvasRef, children: [
413
492
  blocks.length === 0 && /* @__PURE__ */ jsx(EmptyState, { icon: "\u{1F9F1}", title: "Nog geen blokken", description: "Voeg hieronder je eerste blok toe." }),
414
- blocks.map((b, i) => {
493
+ displayBlocks.map((b, i) => {
415
494
  const def = registry.get(b._type);
416
495
  const Render = def?.Render;
417
496
  const safe = (() => {
@@ -462,6 +541,7 @@ function VisualEditorInner({ onClose }) {
462
541
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setSelectedKey(b._key), "aria-label": `Bewerk ${def?.label ?? b._type}`, title: "Bewerken", children: "\u270F\uFE0F" }),
463
542
  /* @__PURE__ */ jsx("button", { type: "button", disabled: i === 0, onClick: () => move(i, -1), "aria-label": "Omhoog", children: "\u2191" }),
464
543
  /* @__PURE__ */ jsx("button", { type: "button", disabled: i === blocks.length - 1, onClick: () => move(i, 1), "aria-label": "Omlaag", children: "\u2193" }),
544
+ b._type === "symbolRef" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void detachSymbol(i), "aria-label": "Symbool loskoppelen", title: "Loskoppelen (losse kopie)", children: "\u{1F517}\u2702" }),
465
545
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => duplicate(i), "aria-label": "Dupliceren", children: "\u2398" }),
466
546
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void remove(i), "aria-label": "Verwijderen", children: "\u{1F5D1}" })
467
547
  ] }),
@@ -486,7 +566,33 @@ function VisualEditorInner({ onClose }) {
486
566
  ] })
487
567
  ] })
488
568
  ] }),
489
- /* @__PURE__ */ jsx("aside", { className: "xicms-ve__panel", children: selected ? /* @__PURE__ */ jsx(SelectedEditor, { block: selected, siteId, registry, onChange: (next) => updateByKey(selected._key, next) }, selected._key) : draft ? /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
569
+ /* @__PURE__ */ jsx("aside", { className: "xicms-ve__panel", children: selected && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
570
+ /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold", children: [
571
+ registry.get(selected._type)?.label ?? selected._type,
572
+ " \u2014 vertaling (",
573
+ locale,
574
+ ")"
575
+ ] }),
576
+ /* @__PURE__ */ jsx(
577
+ TranslationPanel,
578
+ {
579
+ block: selected,
580
+ siteId,
581
+ registry,
582
+ locale,
583
+ overlay: overlay ?? {},
584
+ onTranslate: setTranslation
585
+ },
586
+ `${selected._key}:${locale}`
587
+ )
588
+ ] }) : selected ? /* @__PURE__ */ jsx(SelectedEditor, { block: selected, siteId, registry, onChange: (next) => updateByKey(selected._key, next) }, selected._key) : draft && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
589
+ /* @__PURE__ */ jsxs("p", { className: "xicms-ve__hint", children: [
590
+ "Je bewerkt de vertaling ",
591
+ /* @__PURE__ */ jsx("strong", { children: locale }),
592
+ ". Klik op een blok om zijn teksten te vertalen; lege velden vallen terug op de standaardtaal. Blokken toevoegen/verplaatsen doe je in de standaardtaal."
593
+ ] }),
594
+ /* @__PURE__ */ jsx(PageTranslationSettings, { draft, locale, onTranslate: setTranslation })
595
+ ] }) : draft ? /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
490
596
  /* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Klik op een blok om het te bewerken." }),
491
597
  /* @__PURE__ */ jsx(PageSettings, { draft, onChange: (part) => mutateDraft((d) => ({ ...d, ...part }), { coalesce: true }) }),
492
598
  !isDemoActive() && /* @__PURE__ */ jsx(
@@ -532,6 +638,25 @@ function PageSettings({
532
638
  ] })
533
639
  ] });
534
640
  }
641
+ function PageTranslationSettings({
642
+ draft,
643
+ locale,
644
+ onTranslate
645
+ }) {
646
+ const ov = draft.translations?.[locale] ?? {};
647
+ return /* @__PURE__ */ jsxs("details", { className: "xicms-ve__section", open: true, children: [
648
+ /* @__PURE__ */ jsxs("summary", { className: "text-sm font-semibold", children: [
649
+ "Pagina-instellingen (",
650
+ locale,
651
+ ")"
652
+ ] }),
653
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 grid gap-3", children: [
654
+ /* @__PURE__ */ jsx(Field, { label: "Paginatitel", hint: `Origineel: ${draft.title ?? ""}`, children: /* @__PURE__ */ jsx(Input, { value: ov["title"] ?? "", placeholder: draft.title ?? "", onChange: (e) => onTranslate("title", e.target.value) }) }),
655
+ /* @__PURE__ */ jsx(Field, { label: "SEO-titel", children: /* @__PURE__ */ jsx(Input, { value: ov["seo.title"] ?? "", placeholder: draft.seo?.title ?? "", onChange: (e) => onTranslate("seo.title", e.target.value) }) }),
656
+ /* @__PURE__ */ jsx(Field, { label: "SEO-omschrijving", children: /* @__PURE__ */ jsx(Textarea, { rows: 3, value: ov["seo.description"] ?? "", placeholder: draft.seo?.description ?? "", onChange: (e) => onTranslate("seo.description", e.target.value) }) })
657
+ ] })
658
+ ] });
659
+ }
535
660
  function VersionsPanel({
536
661
  siteId,
537
662
  pageId,