@xiwebdesign/xicms-core 4.17.1 → 5.0.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 (169) hide show
  1. package/dist/XiCMSProvider.d.ts +7 -0
  2. package/dist/XiCMSProvider.js +32 -7
  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 +49 -620
  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/compressImage.d.ts +4 -2
  17. package/dist/admin/lib/compressImage.js +7 -5
  18. package/dist/admin/lib/compressImage.js.map +1 -1
  19. package/dist/admin/lib/mediaUsage.d.ts +4 -3
  20. package/dist/admin/lib/mediaUsage.js +21 -10
  21. package/dist/admin/lib/mediaUsage.js.map +1 -1
  22. package/dist/admin/lib/symbolsClient.d.ts +13 -0
  23. package/dist/admin/lib/symbolsClient.js +51 -0
  24. package/dist/admin/lib/symbolsClient.js.map +1 -0
  25. package/dist/admin/lib/upload.d.ts +4 -0
  26. package/dist/admin/lib/upload.js +26 -6
  27. package/dist/admin/lib/upload.js.map +1 -1
  28. package/dist/admin/lib/zodFields.d.ts +18 -0
  29. package/dist/admin/lib/zodFields.js +65 -2
  30. package/dist/admin/lib/zodFields.js.map +1 -1
  31. package/dist/admin/media/MediaManager.js +23 -7
  32. package/dist/admin/media/MediaManager.js.map +1 -1
  33. package/dist/admin/media/MediaPicker.js +12 -2
  34. package/dist/admin/media/MediaPicker.js.map +1 -1
  35. package/dist/admin/panelRegistry.js +3 -0
  36. package/dist/admin/panelRegistry.js.map +1 -1
  37. package/dist/admin/settings/SettingsPanel.js +6 -1
  38. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  39. package/dist/admin/settings/SnapshotsCard.d.ts +3 -0
  40. package/dist/admin/settings/SnapshotsCard.js +98 -0
  41. package/dist/admin/settings/SnapshotsCard.js.map +1 -0
  42. package/dist/admin/visual/TranslationPanel.d.ts +13 -0
  43. package/dist/admin/visual/TranslationPanel.js +73 -0
  44. package/dist/admin/visual/TranslationPanel.js.map +1 -0
  45. package/dist/admin/visual/VisualEditor.js +146 -18
  46. package/dist/admin/visual/VisualEditor.js.map +1 -1
  47. package/dist/admin.css +1 -1
  48. package/dist/agentFiles/catalog.d.ts +21 -0
  49. package/dist/agentFiles/catalog.js +78 -0
  50. package/dist/agentFiles/catalog.js.map +1 -0
  51. package/dist/agentFiles/files.d.ts +59 -0
  52. package/dist/agentFiles/files.js +109 -0
  53. package/dist/agentFiles/files.js.map +1 -0
  54. package/dist/blocks/builtins/Agenda.d.ts +6 -0
  55. package/dist/blocks/builtins/Agenda.js +14 -6
  56. package/dist/blocks/builtins/Agenda.js.map +1 -1
  57. package/dist/blocks/builtins/BeforeAfter.js +1 -0
  58. package/dist/blocks/builtins/BeforeAfter.js.map +1 -1
  59. package/dist/blocks/builtins/Breadcrumbs.js +5 -1
  60. package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
  61. package/dist/blocks/builtins/Cards.js +4 -2
  62. package/dist/blocks/builtins/Cards.js.map +1 -1
  63. package/dist/blocks/builtins/Contact.js +2 -1
  64. package/dist/blocks/builtins/Contact.js.map +1 -1
  65. package/dist/blocks/builtins/Cta.js +1 -0
  66. package/dist/blocks/builtins/Cta.js.map +1 -1
  67. package/dist/blocks/builtins/Elements.js +1 -0
  68. package/dist/blocks/builtins/Elements.js.map +1 -1
  69. package/dist/blocks/builtins/Faq.js +5 -4
  70. package/dist/blocks/builtins/Faq.js.map +1 -1
  71. package/dist/blocks/builtins/Gallery.js +2 -2
  72. package/dist/blocks/builtins/Gallery.js.map +1 -1
  73. package/dist/blocks/builtins/Hero.js +2 -2
  74. package/dist/blocks/builtins/Hero.js.map +1 -1
  75. package/dist/blocks/builtins/Jobs.d.ts +8 -1
  76. package/dist/blocks/builtins/Jobs.js +38 -21
  77. package/dist/blocks/builtins/Jobs.js.map +1 -1
  78. package/dist/blocks/builtins/MapBlock.js +31 -4
  79. package/dist/blocks/builtins/MapBlock.js.map +1 -1
  80. package/dist/blocks/builtins/PdfBlock.js +2 -3
  81. package/dist/blocks/builtins/PdfBlock.js.map +1 -1
  82. package/dist/blocks/builtins/Portfolio.js +2 -2
  83. package/dist/blocks/builtins/Portfolio.js.map +1 -1
  84. package/dist/blocks/builtins/Pricing.d.ts +2 -0
  85. package/dist/blocks/builtins/Pricing.js +18 -10
  86. package/dist/blocks/builtins/Pricing.js.map +1 -1
  87. package/dist/blocks/builtins/Sections.js +4 -2
  88. package/dist/blocks/builtins/Sections.js.map +1 -1
  89. package/dist/blocks/builtins/Slider.js +21 -3
  90. package/dist/blocks/builtins/Slider.js.map +1 -1
  91. package/dist/blocks/builtins/SymbolRef.d.ts +2 -0
  92. package/dist/blocks/builtins/SymbolRef.js +61 -0
  93. package/dist/blocks/builtins/SymbolRef.js.map +1 -0
  94. package/dist/blocks/builtins/Testimonials.js +21 -18
  95. package/dist/blocks/builtins/Testimonials.js.map +1 -1
  96. package/dist/blocks/builtins/WhatsappChat.js +1 -1
  97. package/dist/blocks/builtins/WhatsappChat.js.map +1 -1
  98. package/dist/blocks/builtins/Widgets.js +2 -3
  99. package/dist/blocks/builtins/Widgets.js.map +1 -1
  100. package/dist/blocks/builtins/lib/ics.js +1 -1
  101. package/dist/blocks/builtins/lib/ics.js.map +1 -1
  102. package/dist/blocks/builtins/lib/openingHours.d.ts +4 -1
  103. package/dist/blocks/builtins/lib/openingHours.js +11 -5
  104. package/dist/blocks/builtins/lib/openingHours.js.map +1 -1
  105. package/dist/blocks/customTemplate/definition.d.ts +7 -0
  106. package/dist/blocks/customTemplate/definition.js +34 -0
  107. package/dist/blocks/customTemplate/definition.js.map +1 -0
  108. package/dist/blocks/customTemplate/interpret.d.ts +10 -0
  109. package/dist/blocks/customTemplate/interpret.js +136 -0
  110. package/dist/blocks/customTemplate/interpret.js.map +1 -0
  111. package/dist/blocks/registry.d.ts +8 -0
  112. package/dist/blocks/registry.js.map +1 -1
  113. package/dist/demo/demoStore.d.ts +11 -1
  114. package/dist/demo/demoStore.js +50 -0
  115. package/dist/demo/demoStore.js.map +1 -1
  116. package/dist/firebase/client.js +2 -1
  117. package/dist/firebase/client.js.map +1 -1
  118. package/dist/i18n/overlay.d.ts +61 -0
  119. package/dist/i18n/overlay.js +90 -0
  120. package/dist/i18n/overlay.js.map +1 -0
  121. package/dist/index.d.ts +11 -9
  122. package/dist/index.js +34 -9
  123. package/dist/index.js.map +1 -1
  124. package/dist/modules/builtins.js +3 -2
  125. package/dist/modules/builtins.js.map +1 -1
  126. package/dist/modules/consent/useConsent.js +15 -3
  127. package/dist/modules/consent/useConsent.js.map +1 -1
  128. package/dist/schema/blocks.d.ts +183 -3
  129. package/dist/schema/blocks.js +12 -1
  130. package/dist/schema/blocks.js.map +1 -1
  131. package/dist/schema/customBlocks.d.ts +47 -0
  132. package/dist/schema/customBlocks.js +32 -0
  133. package/dist/schema/customBlocks.js.map +1 -0
  134. package/dist/schema/fields.d.ts +122 -0
  135. package/dist/schema/fields.js +120 -0
  136. package/dist/schema/fields.js.map +1 -0
  137. package/dist/schema/forms.d.ts +6 -6
  138. package/dist/schema/pages.d.ts +15 -1
  139. package/dist/schema/pages.js +6 -2
  140. package/dist/schema/pages.js.map +1 -1
  141. package/dist/schema/siteConfig.d.ts +14 -8
  142. package/dist/schema/siteConfig.js +2 -1
  143. package/dist/schema/siteConfig.js.map +1 -1
  144. package/dist/schema/symbols.d.ts +205 -0
  145. package/dist/schema/symbols.js +18 -0
  146. package/dist/schema/symbols.js.map +1 -0
  147. package/dist/server/index.d.ts +2 -1
  148. package/dist/server/index.js +24 -0
  149. package/dist/server/index.js.map +1 -1
  150. package/dist/server/pages.d.ts +21 -0
  151. package/dist/server/pages.js +77 -0
  152. package/dist/server/pages.js.map +1 -1
  153. package/dist/server/seo.d.ts +12 -3
  154. package/dist/server/seo.js +23 -11
  155. package/dist/server/seo.js.map +1 -1
  156. package/dist/styles.css +62 -6
  157. package/package.json +1 -1
  158. package/dist/components/EditableImage.d.ts +0 -18
  159. package/dist/components/EditableImage.js +0 -62
  160. package/dist/components/EditableImage.js.map +0 -1
  161. package/dist/components/EditableRichText.d.ts +0 -14
  162. package/dist/components/EditableRichText.js +0 -26
  163. package/dist/components/EditableRichText.js.map +0 -1
  164. package/dist/components/EditableText.d.ts +0 -21
  165. package/dist/components/EditableText.js +0 -25
  166. package/dist/components/EditableText.js.map +0 -1
  167. package/dist/content/ContentProvider.d.ts +0 -23
  168. package/dist/content/ContentProvider.js +0 -86
  169. package/dist/content/ContentProvider.js.map +0 -1
@@ -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":[]}
@@ -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":";AAkFW,cAKL,YALK;AAzEX,SAAS,eAAe;AAGxB,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAC/B,SAAS,SAAS,UAAU,OAAO,gBAAgB;AAUnD,SAAS,eAAe,QAAiB,OAAgC,SAAS,IAAI,cAAc,IAAa;AAC/G,QAAM,MAAe,CAAC;AACtB,aAAW,KAAK,QAAQ;AACtB,UAAM,IAAI,QAAQ,EAAE,GAAG;AACvB,UAAM,QAAQ,GAAG,WAAW,GAAG,EAAE,SAAS,EAAE,GAAG;AAC/C,YAAQ,EAAE,MAAM;AAAA,MACd,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,YAAI,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,EAAE,GAAG,IAAI,OAAO,MAAM,EAAE,MAAM,WAAW,OAAO,MAAM,WAAW,IAAI,GAAG,CAAC;AACtG;AAAA,MACF,KAAK,QAAQ;AACX,cAAM,MAAO,GAAuC;AACpD,YAAI,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,EAAE,GAAG,UAAU,OAAO,GAAG,KAAK,YAAY,MAAM,QAAQ,WAAW,OAAO,QAAQ,WAAW,MAAM,GAAG,CAAC;AACpI;AAAA,MACF;AAAA,MACA,KAAK;AACH,YAAI,KAAK,GAAG,eAAe,EAAE,QAAS,KAAiC,CAAC,GAAG,GAAG,MAAM,GAAG,EAAE,GAAG,KAAK,GAAG,KAAK,QAAK,CAAC;AAC/G;AAAA,MACF,KAAK,YAAY;AACf,cAAM,QAAQ,MAAM,QAAQ,CAAC,IAAK,IAAkC,CAAC;AACrE,cAAM,QAAQ,CAAC,MAAM,MAAM;AACzB,cAAI,KAAK,GAAG,eAAe,EAAE,QAAQ,QAAQ,CAAC,GAAG,GAAG,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE,aAAa,KAAK,IAAI,IAAI,CAAC,QAAK,CAAC;AAAA,QACpH,CAAC;AACD;AAAA,MACF;AAAA,MACA;AACE;AAAA,IACJ;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,MAAM,SAAS,IAAI,MAAM,KAAK;AACpC,QAAM,UAAU,QAAQ,MAAM;AAC5B,QAAI,CAAC,IAAK,QAAO,CAAC;AAClB,UAAM,SAAU,IAAmC;AACnD,UAAM,SAAS,UAAU,mBAAmB,IAAI,QAAQ,IAAI,MAAM;AAClE,WAAO,eAAe,QAAQ,KAAK;AAAA,EACrC,GAAG,CAAC,KAAK,KAAK,CAAC;AAEf,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO,oBAAC,OAAE,WAAU,kBAAiB,gHAA6F;AAAA,EACpI;AAEA,SACE,qBAAC,SAAI,WAAU,cACb;AAAA,yBAAC,OAAE,WAAU,0BAAyB;AAAA;AAAA,MAC1B,oBAAC,YAAQ,kBAAO;AAAA,MAAS;AAAA,OAErC;AAAA,IACC,QAAQ,IAAI,CAAC,MAAM;AAClB,YAAM,MAAM,GAAG,MAAM,IAAI,IAAI,EAAE,IAAI;AACnC,YAAM,QAAQ,QAAQ,GAAG,KAAK;AAC9B,aACE,oBAAC,YAAmB,OAAO,EAAE,OAAO,MAAM,EAAE,YAAY,cAAc,EAAE,UAAU,QAAQ,YAAY,EAAE,EAAE,MAAM,GAAG,GAAG,CAAC,KAAK,QACzH,YAAE,SAAS,aACV,oBAAC,kBAAe,QAAgB,OAAO,SAAS,EAAE,WAAW,UAAU,CAAC,SAAS,YAAY,KAAK,IAAI,GAAG,IACvG,EAAE,SAAS,aACb,oBAAC,YAAS,MAAM,GAAG,OAAc,aAAa,EAAE,WAAW,UAAU,CAAC,OAAO,YAAY,KAAK,GAAG,OAAO,KAAK,GAAG,IAEhH,oBAAC,SAAM,OAAc,aAAa,EAAE,WAAW,UAAU,CAAC,OAAO,YAAY,KAAK,GAAG,OAAO,KAAK,GAAG,KANzF,GAQf;AAAA,IAEJ,CAAC;AAAA,KACH;AAEJ;","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;
@@ -280,7 +326,10 @@ function VisualEditorInner({ onClose }) {
280
326
  };
281
327
  const onKeyDown = (e) => {
282
328
  if (e.key === "Escape") {
283
- handleClose();
329
+ const t2 = e.target;
330
+ const inField2 = !!t2 && (t2.isContentEditable || t2.tagName === "INPUT" || t2.tagName === "TEXTAREA" || t2.tagName === "SELECT");
331
+ const dialogOpen = document.querySelector('[role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"]');
332
+ if (!inField2 && !dialogOpen) handleClose();
284
333
  return;
285
334
  }
286
335
  const t = e.target;
@@ -324,6 +373,28 @@ function VisualEditorInner({ onClose }) {
324
373
  copy.splice(i + 1, 0, { ...blocks[i], _key: newKey() });
325
374
  setBlocks(copy);
326
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
+ }
327
398
  async function remove(i) {
328
399
  const ok = await confirm({ title: "Blok verwijderen?", confirmLabel: "Verwijderen", tone: "danger" });
329
400
  if (!ok) return;
@@ -350,7 +421,7 @@ function VisualEditorInner({ onClose }) {
350
421
  if (!ok) return;
351
422
  setPublishing(true);
352
423
  try {
353
- 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));
354
425
  await publishPage(siteId, draft.id);
355
426
  toast({ title: "Gepubliceerd", description: "Herlaad de pagina om het live resultaat te zien.", tone: "success" });
356
427
  } catch (e) {
@@ -381,6 +452,17 @@ function VisualEditorInner({ onClose }) {
381
452
  name
382
453
  ] }),
383
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
+ ] }),
384
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" }),
385
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" }),
386
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" : "" }),
@@ -408,7 +490,7 @@ function VisualEditorInner({ onClose }) {
408
490
  ),
409
491
  draft && /* @__PURE__ */ jsxs("div", { className: "container", ref: canvasRef, children: [
410
492
  blocks.length === 0 && /* @__PURE__ */ jsx(EmptyState, { icon: "\u{1F9F1}", title: "Nog geen blokken", description: "Voeg hieronder je eerste blok toe." }),
411
- blocks.map((b, i) => {
493
+ displayBlocks.map((b, i) => {
412
494
  const def = registry.get(b._type);
413
495
  const Render = def?.Render;
414
496
  const safe = (() => {
@@ -459,6 +541,7 @@ function VisualEditorInner({ onClose }) {
459
541
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setSelectedKey(b._key), "aria-label": `Bewerk ${def?.label ?? b._type}`, title: "Bewerken", children: "\u270F\uFE0F" }),
460
542
  /* @__PURE__ */ jsx("button", { type: "button", disabled: i === 0, onClick: () => move(i, -1), "aria-label": "Omhoog", children: "\u2191" }),
461
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" }),
462
545
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => duplicate(i), "aria-label": "Dupliceren", children: "\u2398" }),
463
546
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void remove(i), "aria-label": "Verwijderen", children: "\u{1F5D1}" })
464
547
  ] }),
@@ -483,7 +566,33 @@ function VisualEditorInner({ onClose }) {
483
566
  ] })
484
567
  ] })
485
568
  ] }),
486
- /* @__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: [
487
596
  /* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Klik op een blok om het te bewerken." }),
488
597
  /* @__PURE__ */ jsx(PageSettings, { draft, onChange: (part) => mutateDraft((d) => ({ ...d, ...part }), { coalesce: true }) }),
489
598
  !isDemoActive() && /* @__PURE__ */ jsx(
@@ -529,6 +638,25 @@ function PageSettings({
529
638
  ] })
530
639
  ] });
531
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
+ }
532
660
  function VersionsPanel({
533
661
  siteId,
534
662
  pageId,