@xiwebdesign/xicms-core 4.18.0 → 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.
- package/dist/XiCMSProvider.d.ts +7 -0
- package/dist/XiCMSProvider.js +29 -6
- package/dist/XiCMSProvider.js.map +1 -1
- package/dist/admin/blockEditors/GenericEditor.d.ts +22 -0
- package/dist/admin/blockEditors/GenericEditor.js +201 -0
- package/dist/admin/blockEditors/GenericEditor.js.map +1 -0
- package/dist/admin/blockEditors/SymbolRefEditor.d.ts +2 -0
- package/dist/admin/blockEditors/SymbolRefEditor.js +55 -0
- package/dist/admin/blockEditors/SymbolRefEditor.js.map +1 -0
- package/dist/admin/blockEditors/index.d.ts +11 -6
- package/dist/admin/blockEditors/index.js +4 -608
- package/dist/admin/blockEditors/index.js.map +1 -1
- package/dist/admin/customBlocks/CustomBlocksPanel.d.ts +3 -0
- package/dist/admin/customBlocks/CustomBlocksPanel.js +145 -0
- package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -0
- package/dist/admin/lib/symbolsClient.d.ts +13 -0
- package/dist/admin/lib/symbolsClient.js +51 -0
- package/dist/admin/lib/symbolsClient.js.map +1 -0
- package/dist/admin/lib/zodFields.d.ts +18 -0
- package/dist/admin/lib/zodFields.js +65 -2
- package/dist/admin/lib/zodFields.js.map +1 -1
- package/dist/admin/panelRegistry.js +3 -0
- package/dist/admin/panelRegistry.js.map +1 -1
- package/dist/admin/settings/SettingsPanel.js +2 -0
- package/dist/admin/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/settings/SnapshotsCard.d.ts +3 -0
- package/dist/admin/settings/SnapshotsCard.js +98 -0
- package/dist/admin/settings/SnapshotsCard.js.map +1 -0
- package/dist/admin/visual/TranslationPanel.d.ts +13 -0
- package/dist/admin/visual/TranslationPanel.js +73 -0
- package/dist/admin/visual/TranslationPanel.js.map +1 -0
- package/dist/admin/visual/VisualEditor.js +142 -17
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/agentFiles/catalog.d.ts +21 -0
- package/dist/agentFiles/catalog.js +78 -0
- package/dist/agentFiles/catalog.js.map +1 -0
- package/dist/agentFiles/files.d.ts +59 -0
- package/dist/agentFiles/files.js +109 -0
- package/dist/agentFiles/files.js.map +1 -0
- package/dist/blocks/builtins/BeforeAfter.js +1 -0
- package/dist/blocks/builtins/BeforeAfter.js.map +1 -1
- package/dist/blocks/builtins/Cards.js +1 -0
- package/dist/blocks/builtins/Cards.js.map +1 -1
- package/dist/blocks/builtins/Cta.js +1 -0
- package/dist/blocks/builtins/Cta.js.map +1 -1
- package/dist/blocks/builtins/Elements.js +1 -0
- package/dist/blocks/builtins/Elements.js.map +1 -1
- package/dist/blocks/builtins/Hero.js +2 -2
- package/dist/blocks/builtins/Hero.js.map +1 -1
- package/dist/blocks/builtins/Jobs.js +1 -0
- package/dist/blocks/builtins/Jobs.js.map +1 -1
- package/dist/blocks/builtins/Sections.js +1 -0
- package/dist/blocks/builtins/Sections.js.map +1 -1
- package/dist/blocks/builtins/SymbolRef.d.ts +2 -0
- package/dist/blocks/builtins/SymbolRef.js +61 -0
- package/dist/blocks/builtins/SymbolRef.js.map +1 -0
- package/dist/blocks/customTemplate/definition.d.ts +7 -0
- package/dist/blocks/customTemplate/definition.js +34 -0
- package/dist/blocks/customTemplate/definition.js.map +1 -0
- package/dist/blocks/customTemplate/interpret.d.ts +10 -0
- package/dist/blocks/customTemplate/interpret.js +136 -0
- package/dist/blocks/customTemplate/interpret.js.map +1 -0
- package/dist/blocks/registry.d.ts +8 -0
- package/dist/blocks/registry.js.map +1 -1
- package/dist/i18n/overlay.d.ts +61 -0
- package/dist/i18n/overlay.js +90 -0
- package/dist/i18n/overlay.js.map +1 -0
- package/dist/index.d.ts +10 -8
- package/dist/index.js +32 -9
- package/dist/index.js.map +1 -1
- package/dist/modules/builtins.js +3 -2
- package/dist/modules/builtins.js.map +1 -1
- package/dist/schema/blocks.d.ts +159 -0
- package/dist/schema/blocks.js +8 -0
- package/dist/schema/blocks.js.map +1 -1
- package/dist/schema/customBlocks.d.ts +47 -0
- package/dist/schema/customBlocks.js +32 -0
- package/dist/schema/customBlocks.js.map +1 -0
- package/dist/schema/fields.d.ts +122 -0
- package/dist/schema/fields.js +120 -0
- package/dist/schema/fields.js.map +1 -0
- package/dist/schema/forms.d.ts +6 -6
- package/dist/schema/pages.d.ts +15 -1
- package/dist/schema/pages.js +6 -2
- package/dist/schema/pages.js.map +1 -1
- package/dist/schema/siteConfig.d.ts +14 -8
- package/dist/schema/siteConfig.js +2 -1
- package/dist/schema/siteConfig.js.map +1 -1
- package/dist/schema/symbols.d.ts +205 -0
- package/dist/schema/symbols.js +18 -0
- package/dist/schema/symbols.js.map +1 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +24 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/pages.d.ts +21 -0
- package/dist/server/pages.js +77 -0
- package/dist/server/pages.js.map +1 -1
- package/dist/server/seo.d.ts +10 -3
- package/dist/server/seo.js +22 -10
- package/dist/server/seo.js.map +1 -1
- package/dist/styles.css +27 -0
- package/package.json +1 -1
- package/dist/components/EditableImage.d.ts +0 -18
- package/dist/components/EditableImage.js +0 -62
- package/dist/components/EditableImage.js.map +0 -1
- package/dist/components/EditableRichText.d.ts +0 -14
- package/dist/components/EditableRichText.js +0 -26
- package/dist/components/EditableRichText.js.map +0 -1
- package/dist/components/EditableText.d.ts +0 -21
- package/dist/components/EditableText.js +0 -25
- package/dist/components/EditableText.js.map +0 -1
- package/dist/content/ContentProvider.d.ts +0 -23
- package/dist/content/ContentProvider.js +0 -86
- 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":"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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
|
|
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
|
|
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(
|
|
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(
|
|
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 = [...
|
|
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", () =>
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|