@xiwebdesign/xicms-core 7.15.0 → 7.16.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/pages/PagesPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Button, Card, Field, Input, Badge } from \"../ui/primitives\";\nimport { Dialog, DialogTrigger, DialogContent, DialogClose } from \"../ui/dialog\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { subscribePages, createPage, deletePage, type DraftPage } from \"../lib/pagesClient\";\nimport { PageEditor } from \"./PageEditor\";\n\nfunction slugify(s: string): string {\n return s\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/(^-|-$)/g, \"\");\n}\n\nexport function PagesPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [pages, setPages] = useState<DraftPage[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [newTitle, setNewTitle] = useState(\"\");\n\n useEffect(() => subscribePages(siteId, setPages), [siteId]);\n useEffect(() => {\n if (!selected && pages.length) setSelected(pages[0].id);\n }, [pages, selected]);\n\n const current = pages.find((p) => p.id === selected) ?? null;\n\n async function create() {\n const title = newTitle.trim();\n if (!title) return;\n const slug = slugify(title) || \"pagina\";\n const path = slug === \"home\" ? \"/\" : `/${slug}`;\n try {\n const id = await createPage(siteId, { title, slug, path });\n setNewTitle(\"\");\n setSelected(id);\n toast({ title: \"Pagina aangemaakt\", description: path });\n } catch (e) {\n // De dialoog sluit sowieso (DialogClose) — zonder deze toast dacht de klant dat de pagina\n // bestond terwijl de write (rules/offline) faalde.\n toast({ title: \"Pagina aanmaken mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[260px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <span className=\"text-xs font-medium tracking-wide text-slate-500 uppercase\">Overzicht</span>\n <Dialog>\n <DialogTrigger asChild>\n <Button size=\"sm\">+ Nieuw</Button>\n </DialogTrigger>\n <DialogContent title=\"Nieuwe pagina\">\n <div className=\"grid gap-3\">\n <Field label=\"Titel\">\n <Input value={newTitle} onChange={(e) => setNewTitle(e.target.value)} placeholder=\"bv. Over ons\" />\n </Field>\n <div className=\"flex justify-end gap-2\">\n <DialogClose asChild>\n <Button variant=\"secondary\">Annuleren</Button>\n </DialogClose>\n <DialogClose asChild>\n <Button onClick={create}>Aanmaken</Button>\n </DialogClose>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n </div>\n <ul className=\"grid gap-1\">\n {pages.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n <span className=\"truncate\">{p.title}</span>\n {p.published ? <Badge tone=\"green\">live</Badge> : <Badge tone=\"amber\">concept</Badge>}\n </button>\n </li>\n ))}\n {/* Geen lege-lijst-tekst hier: de lege staat rechts zegt hetzelfde én noemt de volgende\n stap. Twee keer \"Nog geen pagina's\" naast elkaar leest als een fout. */}\n </ul>\n </Card>\n\n <div>\n {current ? (\n <>\n <PageEditor siteId={siteId} page={current} allPages={pages} />\n {/* Verwijderen staat ONDER de editor en draagt geen rode vlakknop meer. Het was het\n opvallendste element op het scherm — helderrood, bovenaan, bóven \"Publiceren\" —\n waarmee de onomkeerbare actie meer aandacht kreeg dan de actie waarvoor je komt.\n Een tekstknop in de foutkleur is duidelijk genoeg; de bevestiging vangt de rest. */}\n <div className=\"mt-4 flex justify-end border-t border-slate-200 pt-4\">\n <button\n type=\"button\"\n className=\"rounded px-2 py-1 text-sm text-red-600 hover:underline focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n onClick={async () => {\n const ok = await confirm({\n title: `Pagina \"${current.title}\" verwijderen?`,\n description:\n \"De pagina en de bijbehorende werkversie worden verwijderd. Dit kan niet ongedaan worden gemaakt. \" +\n \"Van de live site verdwijnt hij zodra die opnieuw is gebouwd — meestal binnen enkele minuten.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n await deletePage(siteId, current.id);\n setSelected(null);\n // Eerlijk over de vertraging: de pagina is wég uit het beheer, maar de live site\n // laat hem pas los na een rebuild. Beloven dat hij \"verdwenen\" is terwijl de URL\n // nog werkt, kost meer vertrouwen dan het kost om het te zeggen.\n toast({\n title: \"Pagina verwijderd\",\n description: \"De live site laat de pagina los zodra hij opnieuw is gebouwd (meestal enkele minuten).\",\n });\n }}\n >\n Pagina verwijderen\n </button>\n </div>\n </>\n ) : (\n <Card className=\"grid place-items-center py-16 text-center\">\n <div className=\"max-w-sm\">\n {/* Een lege lijst is een uitnodiging, geen mededeling: zeg wat de volgende stap is. */}\n <p className=\"text-sm font-medium text-slate-700\">\n {pages.length === 0 ? \"Nog geen pagina's\" : \"Geen pagina geselecteerd\"}\n </p>\n <p className=\"mt-1 text-sm text-slate-500\">\n {pages.length === 0\n ? \"Maak je eerste pagina met + Nieuw. Wat je maakt blijft een concept tot je publiceert.\"\n : \"Kies links een pagina om de tekst en secties aan te passen.\"}\n </p>\n </div>\n </Card>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/pages/PagesPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Button, Card, Field, Input, Badge } from \"../ui/primitives\";\nimport { Dialog, DialogTrigger, DialogContent, DialogClose } from \"../ui/dialog\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { subscribePages, createPage, deletePage, type DraftPage } from \"../lib/pagesClient\";\nimport { PageEditor } from \"./PageEditor\";\n\nfunction slugify(s: string): string {\n return s\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/(^-|-$)/g, \"\");\n}\n\n/**\n * Bloktypes die een pagina FUNCTIONEEL maken (7.16): een afrekenpagina is \"een pagina met een\n * checkout-blok\", en die hoort in de lijst niet tussen \"Over ons\" te staan — de inhoud (producten,\n * bestellingen) beheer je onder Webwinkel, hier staat alleen het omhulsel. Er is bewust géén\n * pagina-type-veld in het schema (dat zou een tweede waarheid zijn naast de blokken zelf); we lezen\n * het af aan wat er op de pagina staat.\n */\nconst FUNCTIONELE_TYPES = new Set([\n \"shopCategories\",\n \"productGrid\",\n \"productDetail\",\n \"cart\",\n \"checkout\",\n \"donation\",\n \"orderStatus\",\n]);\nfunction isFunctioneel(p: DraftPage): boolean {\n // De homepage hoort ALTIJD bij de gewone pagina's: op een winkel-site staat er vaak een\n // productGrid op de home, maar de home is nooit alleen \"het omhulsel van een functie\" —\n // het is de voordeur van de site, en die moet bovenaan staan waar de klant hem zoekt.\n if (p.path === \"/\") return false;\n return (p.blocks ?? []).some((b) => FUNCTIONELE_TYPES.has(String((b as { _type?: unknown })._type)));\n}\n\n/**\n * Eén rij in de paginalijst. Titel + pad onder elkaar: de kolom is 260px en een SEO-titel als\n * \"Christelijk Kraamcadeau — christelijke geschenken en kraamcadeaus\" is dan onleesbaar afgekapt\n * zónder dat je kunt zien wélke pagina het is. Het pad is kort, uniek en herkenbaar (\"/\", \"/over-\n * ons\") — precies het houvast dat de afgekapte titel mist. `title` levert de volle titel bij hover.\n */\nfunction PaginaKnop({ p, selected, onClick }: { p: DraftPage; selected: boolean; onClick: () => void }) {\n return (\n <button\n onClick={onClick}\n title={`${p.title} — ${p.path}`}\n className={`flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate\">{p.title}</span>\n <span className={`block truncate text-xs ${selected ? \"text-brand-700/70\" : \"text-slate-400\"}`}>{p.path}</span>\n </span>\n {p.published ? <Badge tone=\"green\">live</Badge> : <Badge tone=\"amber\">concept</Badge>}\n </button>\n );\n}\n\nexport function PagesPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [pages, setPages] = useState<DraftPage[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [newTitle, setNewTitle] = useState(\"\");\n\n useEffect(() => subscribePages(siteId, setPages), [siteId]);\n\n const gewoon = pages.filter((p) => !isFunctioneel(p));\n const functioneel = pages.filter(isFunctioneel);\n useEffect(() => {\n // Standaard de eerste GEWONE pagina: /beheer → Pagina's gaat over teksten, niet over de winkel.\n if (!selected && pages.length) setSelected((gewoon[0] ?? pages[0]).id);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [pages, selected]);\n\n const current = pages.find((p) => p.id === selected) ?? null;\n\n async function create() {\n const title = newTitle.trim();\n if (!title) return;\n const slug = slugify(title) || \"pagina\";\n const path = slug === \"home\" ? \"/\" : `/${slug}`;\n try {\n const id = await createPage(siteId, { title, slug, path });\n setNewTitle(\"\");\n setSelected(id);\n toast({ title: \"Pagina aangemaakt\", description: path });\n } catch (e) {\n // De dialoog sluit sowieso (DialogClose) — zonder deze toast dacht de klant dat de pagina\n // bestond terwijl de write (rules/offline) faalde.\n toast({ title: \"Pagina aanmaken mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[260px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <span className=\"text-xs font-medium tracking-wide text-slate-500 uppercase\">Overzicht</span>\n <Dialog>\n <DialogTrigger asChild>\n <Button size=\"sm\">+ Nieuw</Button>\n </DialogTrigger>\n <DialogContent title=\"Nieuwe pagina\">\n <div className=\"grid gap-3\">\n <Field label=\"Titel\">\n <Input value={newTitle} onChange={(e) => setNewTitle(e.target.value)} placeholder=\"bv. Over ons\" />\n </Field>\n <div className=\"flex justify-end gap-2\">\n <DialogClose asChild>\n <Button variant=\"secondary\">Annuleren</Button>\n </DialogClose>\n <DialogClose asChild>\n <Button onClick={create}>Aanmaken</Button>\n </DialogClose>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n </div>\n <ul className=\"grid gap-1\">\n {gewoon.map((p) => (\n <li key={p.id} className=\"min-w-0\">\n <PaginaKnop p={p} selected={p.id === selected} onClick={() => setSelected(p.id)} />\n </li>\n ))}\n {/* Geen lege-lijst-tekst hier: de lege staat rechts zegt hetzelfde én noemt de volgende\n stap. Twee keer \"Nog geen pagina's\" naast elkaar leest als een fout. */}\n </ul>\n {functioneel.length > 0 && (\n <div className=\"mt-4 border-t border-slate-200 pt-3\">\n <p className=\"mb-1 px-3 text-xs font-medium tracking-wide text-slate-500 uppercase\">Winkel &amp; functioneel</p>\n <p className=\"mb-2 px-3 text-xs text-slate-400\">\n Deze pagina&apos;s zijn het omhulsel van een functie. Producten en bestellingen beheer je\n onder Webwinkel; hier pas je alleen de tekst eromheen aan.\n </p>\n <ul className=\"grid gap-1\">\n {functioneel.map((p) => (\n <li key={p.id} className=\"min-w-0\">\n <PaginaKnop p={p} selected={p.id === selected} onClick={() => setSelected(p.id)} />\n </li>\n ))}\n </ul>\n </div>\n )}\n </Card>\n\n <div>\n {current ? (\n <>\n <PageEditor siteId={siteId} page={current} allPages={pages} />\n {/* Verwijderen staat ONDER de editor en draagt geen rode vlakknop meer. Het was het\n opvallendste element op het scherm — helderrood, bovenaan, bóven \"Publiceren\" —\n waarmee de onomkeerbare actie meer aandacht kreeg dan de actie waarvoor je komt.\n Een tekstknop in de foutkleur is duidelijk genoeg; de bevestiging vangt de rest. */}\n <div className=\"mt-4 flex justify-end border-t border-slate-200 pt-4\">\n <button\n type=\"button\"\n className=\"rounded px-2 py-1 text-sm text-red-600 hover:underline focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n onClick={async () => {\n const ok = await confirm({\n title: `Pagina \"${current.title}\" verwijderen?`,\n description:\n \"De pagina en de bijbehorende werkversie worden verwijderd. Dit kan niet ongedaan worden gemaakt. \" +\n \"Van de live site verdwijnt hij zodra die opnieuw is gebouwd — meestal binnen enkele minuten.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n await deletePage(siteId, current.id);\n setSelected(null);\n // Eerlijk over de vertraging: de pagina is wég uit het beheer, maar de live site\n // laat hem pas los na een rebuild. Beloven dat hij \"verdwenen\" is terwijl de URL\n // nog werkt, kost meer vertrouwen dan het kost om het te zeggen.\n toast({\n title: \"Pagina verwijderd\",\n description: \"De live site laat de pagina los zodra hij opnieuw is gebouwd (meestal enkele minuten).\",\n });\n }}\n >\n Pagina verwijderen\n </button>\n </div>\n </>\n ) : (\n <Card className=\"grid place-items-center py-16 text-center\">\n <div className=\"max-w-sm\">\n {/* Een lege lijst is een uitnodiging, geen mededeling: zeg wat de volgende stap is. */}\n <p className=\"text-sm font-medium text-slate-700\">\n {pages.length === 0 ? \"Nog geen pagina's\" : \"Geen pagina geselecteerd\"}\n </p>\n <p className=\"mt-1 text-sm text-slate-500\">\n {pages.length === 0\n ? \"Maak je eerste pagina met + Nieuw. Wat je maakt blijft een concept tot je publiceert.\"\n : \"Kies links een pagina om de tekst en secties aan te passen.\"}\n </p>\n </div>\n </Card>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
+ import { DashboardPanel } from "./dashboard/DashboardPanel.js";
3
4
  import { PagesPanel } from "./pages/PagesPanel.js";
4
5
  import { MenuPanel } from "./menu/MenuPanel.js";
5
6
  import { MediaManager } from "./media/MediaManager.js";
@@ -12,6 +13,9 @@ function NewsPanel(_) {
12
13
  return /* @__PURE__ */ jsx(NewsEditor, {});
13
14
  }
14
15
  const builtinPanels = [
16
+ // Start (7.16): uitleg + widgets + snelkoppelingen. Hangt aan de altijd-actieve content-module
17
+ // en staat vooraan — /beheer opent hier, niet meer midden in het pagina-overzicht.
18
+ { moduleId: "content", id: "start", label: "Start", order: 5, minRole: "editor", Panel: DashboardPanel },
15
19
  { moduleId: "pages", label: "Pagina's", order: 10, minRole: "editor", Panel: PagesPanel },
16
20
  // Menu hoort bij de content-module (altijd actief) en staat vlak na Pagina's: een menu-item
17
21
  // wijst naar een pagina, dus die volgorde leest zoals je werkt.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/panelRegistry.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Paneel-registry — koppelt een beheer-paneel aan een module-id. `AdminApp` rendert de tabs\n * hieruit (gesorteerd op `order`, gated op rol én actieve-module-set) i.p.v. hardgecodeerd. Een\n * nieuwe module met een eigen paneel hoeft `AdminApp` dus niet meer aan te raken.\n */\nimport type { ComponentType } from \"react\";\nimport { PagesPanel } from \"./pages/PagesPanel\";\nimport { MenuPanel } from \"./menu/MenuPanel\";\nimport { MediaManager } from \"./media/MediaManager\";\nimport { FormsPanel } from \"./forms/FormsPanel\";\nimport { StatsPanel } from \"./stats/StatsPanel\";\nimport { SettingsPanel } from \"./settings/SettingsPanel\";\nimport { NewsEditor } from \"./NewsEditor\";\nimport { CustomBlocksPanel } from \"./customBlocks/CustomBlocksPanel\";\n\nexport type PanelMinRole = \"editor\" | \"siteadmin\";\n\nexport interface PanelDefinition {\n /** Moet overeenkomen met de module-`id`; bepaalt of het paneel actief is. */\n moduleId: string;\n /**\n * Unieke sleutel van dit paneel in de tabbladen. Standaard `moduleId`.\n *\n * Die twee liepen door elkaar: `moduleId` bepaalde zowel of een paneel actief is als welk\n * tabblad het is. Zolang elke module één paneel had viel dat niet op, maar het betekende dat\n * twee panelen van dezelfde module elkaars tabblad overschreven — Radix koppelt op de waarde, en\n * React op de key. Menu en Bouwstenen hangen allebei aan de altijd-actieve content-module, en\n * dat maakte het zichtbaar.\n */\n id?: string;\n label: string;\n /** Sorteervolgorde (laag = links). Instellingen staat vast achteraan (100). */\n order: number;\n minRole: PanelMinRole;\n Panel: ComponentType<{ siteId: string }>;\n}\n\n/** Adapter: de NewsEditor leest zelf siteId via context; wrap voor een uniforme prop-signatuur. */\nfunction NewsPanel(_: { siteId: string }) {\n return <NewsEditor />;\n}\n\n/** De panelen van de ingebouwde modules. */\nexport const builtinPanels: PanelDefinition[] = [\n { moduleId: \"pages\", label: \"Pagina's\", order: 10, minRole: \"editor\", Panel: PagesPanel },\n // Menu hoort bij de content-module (altijd actief) en staat vlak na Pagina's: een menu-item\n // wijst naar een pagina, dus die volgorde leest zoals je werkt.\n { moduleId: \"content\", id: \"menu\", label: \"Menu\", order: 15, minRole: \"siteadmin\", Panel: MenuPanel },\n { moduleId: \"news\", label: \"Nieuws\", order: 20, minRole: \"editor\", Panel: NewsPanel },\n { moduleId: \"media\", label: \"Media\", order: 30, minRole: \"editor\", Panel: MediaManager },\n { moduleId: \"forms\", label: \"Formulieren\", order: 40, minRole: \"editor\", Panel: FormsPanel },\n { moduleId: \"stats\", label: \"Statistieken\", order: 50, minRole: \"editor\", Panel: StatsPanel },\n // Webwinkel (6.0): opt-in via `@xiwebdesign/xicms-core/admin/shop` → `shopPanel` in AdminApp.extraPanels.\n // Bouwstenen (5.0): custom blocks zonder deploy — hangt aan de altijd-actieve content-module.\n { moduleId: \"content\", id: \"bouwstenen\", label: \"Bouwstenen\", order: 60, minRole: \"siteadmin\", Panel: CustomBlocksPanel },\n { moduleId: \"settings\", label: \"Instellingen\", order: 100, minRole: \"siteadmin\", Panel: SettingsPanel },\n];\n"],"mappings":";AAyCS;AAjCT,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAyBlC,SAAS,UAAU,GAAuB;AACxC,SAAO,oBAAC,cAAW;AACrB;AAGO,MAAM,gBAAmC;AAAA,EAC9C,EAAE,UAAU,SAAS,OAAO,YAAY,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA;AAAA;AAAA,EAGxF,EAAE,UAAU,WAAW,IAAI,QAAQ,OAAO,QAAQ,OAAO,IAAI,SAAS,aAAa,OAAO,UAAU;AAAA,EACpG,EAAE,UAAU,QAAQ,OAAO,UAAU,OAAO,IAAI,SAAS,UAAU,OAAO,UAAU;AAAA,EACpF,EAAE,UAAU,SAAS,OAAO,SAAS,OAAO,IAAI,SAAS,UAAU,OAAO,aAAa;AAAA,EACvF,EAAE,UAAU,SAAS,OAAO,eAAe,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA,EAC3F,EAAE,UAAU,SAAS,OAAO,gBAAgB,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA;AAAA;AAAA,EAG5F,EAAE,UAAU,WAAW,IAAI,cAAc,OAAO,cAAc,OAAO,IAAI,SAAS,aAAa,OAAO,kBAAkB;AAAA,EACxH,EAAE,UAAU,YAAY,OAAO,gBAAgB,OAAO,KAAK,SAAS,aAAa,OAAO,cAAc;AACxG;","names":[]}
1
+ {"version":3,"sources":["../../src/admin/panelRegistry.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Paneel-registry — koppelt een beheer-paneel aan een module-id. `AdminApp` rendert de tabs\n * hieruit (gesorteerd op `order`, gated op rol én actieve-module-set) i.p.v. hardgecodeerd. Een\n * nieuwe module met een eigen paneel hoeft `AdminApp` dus niet meer aan te raken.\n */\nimport type { ComponentType } from \"react\";\nimport { DashboardPanel } from \"./dashboard/DashboardPanel\";\nimport { PagesPanel } from \"./pages/PagesPanel\";\nimport { MenuPanel } from \"./menu/MenuPanel\";\nimport { MediaManager } from \"./media/MediaManager\";\nimport { FormsPanel } from \"./forms/FormsPanel\";\nimport { StatsPanel } from \"./stats/StatsPanel\";\nimport { SettingsPanel } from \"./settings/SettingsPanel\";\nimport { NewsEditor } from \"./NewsEditor\";\nimport { CustomBlocksPanel } from \"./customBlocks/CustomBlocksPanel\";\n\nexport type PanelMinRole = \"editor\" | \"siteadmin\";\n\nexport interface PanelDefinition {\n /** Moet overeenkomen met de module-`id`; bepaalt of het paneel actief is. */\n moduleId: string;\n /**\n * Unieke sleutel van dit paneel in de tabbladen. Standaard `moduleId`.\n *\n * Die twee liepen door elkaar: `moduleId` bepaalde zowel of een paneel actief is als welk\n * tabblad het is. Zolang elke module één paneel had viel dat niet op, maar het betekende dat\n * twee panelen van dezelfde module elkaars tabblad overschreven — Radix koppelt op de waarde, en\n * React op de key. Menu en Bouwstenen hangen allebei aan de altijd-actieve content-module, en\n * dat maakte het zichtbaar.\n */\n id?: string;\n label: string;\n /** Sorteervolgorde (laag = links). Instellingen staat vast achteraan (100). */\n order: number;\n minRole: PanelMinRole;\n Panel: ComponentType<{ siteId: string }>;\n}\n\n/** Adapter: de NewsEditor leest zelf siteId via context; wrap voor een uniforme prop-signatuur. */\nfunction NewsPanel(_: { siteId: string }) {\n return <NewsEditor />;\n}\n\n/** De panelen van de ingebouwde modules. */\nexport const builtinPanels: PanelDefinition[] = [\n // Start (7.16): uitleg + widgets + snelkoppelingen. Hangt aan de altijd-actieve content-module\n // en staat vooraan — /beheer opent hier, niet meer midden in het pagina-overzicht.\n { moduleId: \"content\", id: \"start\", label: \"Start\", order: 5, minRole: \"editor\", Panel: DashboardPanel },\n { moduleId: \"pages\", label: \"Pagina's\", order: 10, minRole: \"editor\", Panel: PagesPanel },\n // Menu hoort bij de content-module (altijd actief) en staat vlak na Pagina's: een menu-item\n // wijst naar een pagina, dus die volgorde leest zoals je werkt.\n { moduleId: \"content\", id: \"menu\", label: \"Menu\", order: 15, minRole: \"siteadmin\", Panel: MenuPanel },\n { moduleId: \"news\", label: \"Nieuws\", order: 20, minRole: \"editor\", Panel: NewsPanel },\n { moduleId: \"media\", label: \"Media\", order: 30, minRole: \"editor\", Panel: MediaManager },\n { moduleId: \"forms\", label: \"Formulieren\", order: 40, minRole: \"editor\", Panel: FormsPanel },\n { moduleId: \"stats\", label: \"Statistieken\", order: 50, minRole: \"editor\", Panel: StatsPanel },\n // Webwinkel (6.0): opt-in via `@xiwebdesign/xicms-core/admin/shop` → `shopPanel` in AdminApp.extraPanels.\n // Bouwstenen (5.0): custom blocks zonder deploy — hangt aan de altijd-actieve content-module.\n { moduleId: \"content\", id: \"bouwstenen\", label: \"Bouwstenen\", order: 60, minRole: \"siteadmin\", Panel: CustomBlocksPanel },\n { moduleId: \"settings\", label: \"Instellingen\", order: 100, minRole: \"siteadmin\", Panel: SettingsPanel },\n];\n"],"mappings":";AA0CS;AAlCT,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAyBlC,SAAS,UAAU,GAAuB;AACxC,SAAO,oBAAC,cAAW;AACrB;AAGO,MAAM,gBAAmC;AAAA;AAAA;AAAA,EAG9C,EAAE,UAAU,WAAW,IAAI,SAAS,OAAO,SAAS,OAAO,GAAG,SAAS,UAAU,OAAO,eAAe;AAAA,EACvG,EAAE,UAAU,SAAS,OAAO,YAAY,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA;AAAA;AAAA,EAGxF,EAAE,UAAU,WAAW,IAAI,QAAQ,OAAO,QAAQ,OAAO,IAAI,SAAS,aAAa,OAAO,UAAU;AAAA,EACpG,EAAE,UAAU,QAAQ,OAAO,UAAU,OAAO,IAAI,SAAS,UAAU,OAAO,UAAU;AAAA,EACpF,EAAE,UAAU,SAAS,OAAO,SAAS,OAAO,IAAI,SAAS,UAAU,OAAO,aAAa;AAAA,EACvF,EAAE,UAAU,SAAS,OAAO,eAAe,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA,EAC3F,EAAE,UAAU,SAAS,OAAO,gBAAgB,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA;AAAA;AAAA,EAG5F,EAAE,UAAU,WAAW,IAAI,cAAc,OAAO,cAAc,OAAO,IAAI,SAAS,aAAa,OAAO,kBAAkB;AAAA,EACxH,EAAE,UAAU,YAAY,OAAO,gBAAgB,OAAO,KAAK,SAAS,aAAa,OAAO,cAAc;AACxG;","names":[]}
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from "react";
4
- import { doc, getDoc, setDoc } from "firebase/firestore";
5
- import { getDb } from "../../firebase/client.js";
4
+ import { doc, getDoc } from "firebase/firestore";
5
+ import { getDb, getFirebaseApp } from "../../firebase/client.js";
6
+ import { callable } from "../../firebase/functionsClient.js";
6
7
  import { Button, Card, Field, Input } from "../ui/primitives.js";
7
8
  import { useToast } from "../ui/toast.js";
8
9
  import { centsToEuroInput, eurosToCents } from "./lib.js";
@@ -25,8 +26,8 @@ function ShopSettingsTab({ siteId }) {
25
26
  });
26
27
  }, [siteId]);
27
28
  async function save() {
28
- const db = getDb();
29
- if (!db || !cfg) return;
29
+ const app = getFirebaseApp();
30
+ if (!app || !cfg) return;
30
31
  setBusy(true);
31
32
  try {
32
33
  const next = {
@@ -34,11 +35,8 @@ function ShopSettingsTab({ siteId }) {
34
35
  shippingFlatCents: eurosToCents(shipInput) ?? 495,
35
36
  shippingFreeFromCents: freeInput.trim() === "" ? 0 : eurosToCents(freeInput) ?? 0
36
37
  };
37
- await setDoc(
38
- doc(db, "sites", siteId, "settings", "public"),
39
- { modules: { shop: { config: next } } },
40
- { merge: true }
41
- );
38
+ const fn = callable(app, "saveSiteSettings");
39
+ await fn({ siteId, settings: { modules: { shop: { config: next } } } });
42
40
  setCfg(next);
43
41
  toast({ title: "Instellingen opgeslagen", tone: "success" });
44
42
  } catch (e) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/shop/ShopSettingsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { doc, getDoc, setDoc } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { centsToEuroInput, eurosToCents } from \"./lib\";\n\n/**\n * Winkel-instellingen — leest/schrijft DEZELFDE module-config als Instellingen → Modules\n * (settings/public.modules.shop.config; geen tweede bron van waarheid), maar met winkel-\n * presentatie: bedragen in euro's en uitleg per veld. Siteadmin-only (rules).\n */\ninterface ShopCfg {\n shippingFlatCents?: number;\n shippingFreeFromCents?: number;\n orderPrefix?: string;\n currency?: string;\n businessName?: string;\n businessAddress?: string;\n businessZip?: string;\n businessCity?: string;\n businessKvk?: string;\n businessBtw?: string;\n [k: string]: unknown;\n}\n\nexport function ShopSettingsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [cfg, setCfg] = useState<ShopCfg | null>(null);\n const [shipInput, setShipInput] = useState(\"\");\n const [freeInput, setFreeInput] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n void getDoc(doc(db, \"sites\", siteId, \"settings\", \"public\")).then((snap) => {\n const modules = (snap.data()?.modules ?? {}) as Record<string, { config?: ShopCfg } | boolean>;\n const entry = modules.shop;\n const c = typeof entry === \"object\" && entry !== null ? (entry.config ?? {}) : {};\n setCfg(c);\n setShipInput(centsToEuroInput(c.shippingFlatCents ?? 495));\n setFreeInput(c.shippingFreeFromCents ? centsToEuroInput(c.shippingFreeFromCents) : \"\");\n });\n }, [siteId]);\n\n async function save() {\n const db = getDb();\n if (!db || !cfg) return;\n setBusy(true);\n try {\n const next: ShopCfg = {\n ...cfg,\n shippingFlatCents: eurosToCents(shipInput) ?? 495,\n shippingFreeFromCents: freeInput.trim() === \"\" ? 0 : (eurosToCents(freeInput) ?? 0),\n };\n // Merge-diep op precies dit pad; enabled-vlag blijft onaangeroerd.\n await setDoc(\n doc(db, \"sites\", siteId, \"settings\", \"public\"),\n { modules: { shop: { config: next } } },\n { merge: true },\n );\n setCfg(next);\n toast({ title: \"Instellingen opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n if (!cfg) return <Card><p className=\"text-sm text-slate-500\">Laden…</p></Card>;\n\n const set = (k: string, v: string) => setCfg((p) => ({ ...p, [k]: v }));\n\n return (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Card>\n <h3 className=\"mb-3 text-sm font-semibold\">Verzending & bestellen</h3>\n <div className=\"grid gap-4\">\n <Field label=\"Verzendkosten (€)\" hint=\"Vast bedrag per bestelling binnen Nederland\">\n <Input value={shipInput} onChange={(e) => setShipInput(e.target.value)} placeholder=\"4,95\" className=\"w-32\" />\n </Field>\n <Field label=\"Gratis verzending vanaf (€)\" hint=\"Leeg = nooit gratis\">\n <Input value={freeInput} onChange={(e) => setFreeInput(e.target.value)} placeholder=\"50,00\" className=\"w-32\" />\n </Field>\n <Field label=\"Ordernummer-prefix\" hint=\"Bijv. CKC → CKC-2026-0001\">\n <Input value={String(cfg.orderPrefix ?? \"ORDER\")} onChange={(e) => set(\"orderPrefix\", e.target.value.toUpperCase().slice(0, 12))} className=\"w-32\" />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-sm font-semibold\">Bedrijfsgegevens (factuur)</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Deze gegevens komen op de factuur-PDF die klanten bij hun bestelling krijgen.</p>\n <div className=\"grid gap-3\">\n <Field label=\"Bedrijfsnaam\"><Input value={String(cfg.businessName ?? \"\")} onChange={(e) => set(\"businessName\", e.target.value)} /></Field>\n <Field label=\"Adres\"><Input value={String(cfg.businessAddress ?? \"\")} onChange={(e) => set(\"businessAddress\", e.target.value)} /></Field>\n <div className=\"grid grid-cols-[120px_1fr] gap-3\">\n <Field label=\"Postcode\"><Input value={String(cfg.businessZip ?? \"\")} onChange={(e) => set(\"businessZip\", e.target.value)} /></Field>\n <Field label=\"Plaats\"><Input value={String(cfg.businessCity ?? \"\")} onChange={(e) => set(\"businessCity\", e.target.value)} /></Field>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"KVK-nummer\"><Input value={String(cfg.businessKvk ?? \"\")} onChange={(e) => set(\"businessKvk\", e.target.value)} /></Field>\n <Field label=\"Btw-nummer\"><Input value={String(cfg.businessBtw ?? \"\")} onChange={(e) => set(\"businessBtw\", e.target.value)} /></Field>\n </div>\n </div>\n </Card>\n\n <div className=\"lg:col-span-2\">\n <Button onClick={save} disabled={busy}>Opslaan</Button>\n <p className=\"mt-2 text-xs text-slate-500\">\n Paden en valuta van de winkel staan onder Instellingen → Modules → Webwinkel.\n </p>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/shop/ShopSettingsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { doc, getDoc } from \"firebase/firestore\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { centsToEuroInput, eurosToCents } from \"./lib\";\n\n/**\n * Winkel-instellingen — leest/schrijft DEZELFDE module-config als Instellingen → Modules\n * (settings/public.modules.shop.config; geen tweede bron van waarheid), maar met winkel-\n * presentatie: bedragen in euro's en uitleg per veld. Siteadmin-only (rules).\n */\ninterface ShopCfg {\n shippingFlatCents?: number;\n shippingFreeFromCents?: number;\n orderPrefix?: string;\n currency?: string;\n businessName?: string;\n businessAddress?: string;\n businessZip?: string;\n businessCity?: string;\n businessKvk?: string;\n businessBtw?: string;\n [k: string]: unknown;\n}\n\nexport function ShopSettingsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [cfg, setCfg] = useState<ShopCfg | null>(null);\n const [shipInput, setShipInput] = useState(\"\");\n const [freeInput, setFreeInput] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n void getDoc(doc(db, \"sites\", siteId, \"settings\", \"public\")).then((snap) => {\n const modules = (snap.data()?.modules ?? {}) as Record<string, { config?: ShopCfg } | boolean>;\n const entry = modules.shop;\n const c = typeof entry === \"object\" && entry !== null ? (entry.config ?? {}) : {};\n setCfg(c);\n setShipInput(centsToEuroInput(c.shippingFlatCents ?? 495));\n setFreeInput(c.shippingFreeFromCents ? centsToEuroInput(c.shippingFreeFromCents) : \"\");\n });\n }, [siteId]);\n\n async function save() {\n const app = getFirebaseApp();\n if (!app || !cfg) return;\n setBusy(true);\n try {\n const next: ShopCfg = {\n ...cfg,\n shippingFlatCents: eurosToCents(shipInput) ?? 495,\n shippingFreeFromCents: freeInput.trim() === \"\" ? 0 : (eurosToCents(freeInput) ?? 0),\n };\n /**\n * Via de saveSiteSettings-callable, NIET rechtstreeks naar Firestore. Deze tab deed nog een\n * directe setDoc op settings/public, maar die deur zit sinds de settings-verstrakking in de\n * rules voor iederéén dicht (ook superadmin) — de klant kreeg letterlijk \"Missing or\n * insufficient permissions\" bij het opslaan van verzendkosten. De callable is bovendien de\n * enige route die site/public consistent houdt én de cache-tag pusht, zodat de nieuwe\n * verzendkosten binnen seconden op de live winkel staan i.p.v. na het ISR-vangnet.\n * set+merge in de callable merged diep: de enabled-vlag en andere modules blijven staan.\n */\n const fn = callable(app, \"saveSiteSettings\");\n await fn({ siteId, settings: { modules: { shop: { config: next } } } });\n setCfg(next);\n toast({ title: \"Instellingen opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n if (!cfg) return <Card><p className=\"text-sm text-slate-500\">Laden…</p></Card>;\n\n const set = (k: string, v: string) => setCfg((p) => ({ ...p, [k]: v }));\n\n return (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Card>\n <h3 className=\"mb-3 text-sm font-semibold\">Verzending & bestellen</h3>\n <div className=\"grid gap-4\">\n <Field label=\"Verzendkosten (€)\" hint=\"Vast bedrag per bestelling binnen Nederland\">\n <Input value={shipInput} onChange={(e) => setShipInput(e.target.value)} placeholder=\"4,95\" className=\"w-32\" />\n </Field>\n <Field label=\"Gratis verzending vanaf (€)\" hint=\"Leeg = nooit gratis\">\n <Input value={freeInput} onChange={(e) => setFreeInput(e.target.value)} placeholder=\"50,00\" className=\"w-32\" />\n </Field>\n <Field label=\"Ordernummer-prefix\" hint=\"Bijv. CKC → CKC-2026-0001\">\n <Input value={String(cfg.orderPrefix ?? \"ORDER\")} onChange={(e) => set(\"orderPrefix\", e.target.value.toUpperCase().slice(0, 12))} className=\"w-32\" />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-sm font-semibold\">Bedrijfsgegevens (factuur)</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Deze gegevens komen op de factuur-PDF die klanten bij hun bestelling krijgen.</p>\n <div className=\"grid gap-3\">\n <Field label=\"Bedrijfsnaam\"><Input value={String(cfg.businessName ?? \"\")} onChange={(e) => set(\"businessName\", e.target.value)} /></Field>\n <Field label=\"Adres\"><Input value={String(cfg.businessAddress ?? \"\")} onChange={(e) => set(\"businessAddress\", e.target.value)} /></Field>\n <div className=\"grid grid-cols-[120px_1fr] gap-3\">\n <Field label=\"Postcode\"><Input value={String(cfg.businessZip ?? \"\")} onChange={(e) => set(\"businessZip\", e.target.value)} /></Field>\n <Field label=\"Plaats\"><Input value={String(cfg.businessCity ?? \"\")} onChange={(e) => set(\"businessCity\", e.target.value)} /></Field>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"KVK-nummer\"><Input value={String(cfg.businessKvk ?? \"\")} onChange={(e) => set(\"businessKvk\", e.target.value)} /></Field>\n <Field label=\"Btw-nummer\"><Input value={String(cfg.businessBtw ?? \"\")} onChange={(e) => set(\"businessBtw\", e.target.value)} /></Field>\n </div>\n </div>\n </Card>\n\n <div className=\"lg:col-span-2\">\n <Button onClick={save} disabled={busy}>Opslaan</Button>\n <p className=\"mt-2 text-xs text-slate-500\">\n Paden en valuta van de winkel staan onder Instellingen → Modules → Webwinkel.\n </p>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,12 +1,17 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  const PADEN = {
4
+ // Sleutels zijn de paneel-sleutels (id ?? moduleId) uit de registry.
5
+ start: "M3 11l9-8 9 8 M5 9v11h5v-6h4v6h5V9",
6
+ menu: "M4 6h16 M4 12h16 M4 18h16",
7
+ shop: "M6 6h15l-2 8H8z M6 6L5 3H2 M9 20a1 1 0 100-2 1 1 0 000 2z M17 20a1 1 0 100-2 1 1 0 000 2z",
4
8
  pages: "M4 3h9l4 4v14H4z M13 3v5h4",
5
9
  news: "M4 5h16v14H4z M7 9h10 M7 13h10 M7 17h6",
6
10
  media: "M3 5h18v14H3z M3 15l5-5 4 4 3-3 6 6",
7
11
  forms: "M5 3h14v18H5z M9 8h6 M9 12h6 M9 16h3",
8
12
  stats: "M4 20V10 M10 20V4 M16 20v-7 M22 20H2",
9
13
  blocks: "M4 4h7v7H4z M13 4h7v7h-7z M4 13h7v7H4z M13 13h7v7h-7z",
14
+ bouwstenen: "M4 4h7v7H4z M13 4h7v7h-7z M4 13h7v7H4z M13 13h7v7h-7z",
10
15
  settings: "M12 15a3 3 0 100-6 3 3 0 000 6z M19.4 15a1.6 1.6 0 00.3 1.8l.1.1a2 2 0 11-2.8 2.8l-.1-.1a1.6 1.6 0 00-2.7 1.1V21a2 2 0 11-4 0v-.1A1.6 1.6 0 007.5 19.4l-.1.1a2 2 0 11-2.8-2.8l.1-.1A1.6 1.6 0 003 14.6H3a2 2 0 110-4h.1A1.6 1.6 0 004.6 7.5l-.1-.1a2 2 0 112.8-2.8l.1.1a1.6 1.6 0 001.8.3H9.4a1.6 1.6 0 001-1.5V3a2 2 0 114 0v.1a1.6 1.6 0 001 1.5 1.6 1.6 0 001.8-.3l.1-.1a2 2 0 112.8 2.8l-.1.1a1.6 1.6 0 00-.3 1.8v.1a1.6 1.6 0 001.5 1H21a2 2 0 110 4h-.1a1.6 1.6 0 00-1.5 1z"
11
16
  };
12
17
  const FALLBACK = "M12 3l9 9-9 9-9-9z";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/ui/PanelIcon.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Kleine lijn-iconen voor de zijbalk. Bewust inline SVG en geen icoon-pakket: het zijn er zeven,\n * en een dependency voor zeven paden is niet te verdedigen in een bundel die élke klantsite laadt.\n *\n * Iconen zijn hier geen versiering maar herkenning: de beheerder van een hoveniersbedrijf logt een\n * keer per maand in en zoekt \"waar stond die tekst ook alweer\" — een vorm vindt hij sneller terug\n * dan een woord.\n */\nconst PADEN: Record<string, string> = {\n pages: \"M4 3h9l4 4v14H4z M13 3v5h4\",\n news: \"M4 5h16v14H4z M7 9h10 M7 13h10 M7 17h6\",\n media: \"M3 5h18v14H3z M3 15l5-5 4 4 3-3 6 6\",\n forms: \"M5 3h14v18H5z M9 8h6 M9 12h6 M9 16h3\",\n stats: \"M4 20V10 M10 20V4 M16 20v-7 M22 20H2\",\n blocks: \"M4 4h7v7H4z M13 4h7v7h-7z M4 13h7v7H4z M13 13h7v7h-7z\",\n settings: \"M12 15a3 3 0 100-6 3 3 0 000 6z M19.4 15a1.6 1.6 0 00.3 1.8l.1.1a2 2 0 11-2.8 2.8l-.1-.1a1.6 1.6 0 00-2.7 1.1V21a2 2 0 11-4 0v-.1A1.6 1.6 0 007.5 19.4l-.1.1a2 2 0 11-2.8-2.8l.1-.1A1.6 1.6 0 003 14.6H3a2 2 0 110-4h.1A1.6 1.6 0 004.6 7.5l-.1-.1a2 2 0 112.8-2.8l.1.1a1.6 1.6 0 001.8.3H9.4a1.6 1.6 0 001-1.5V3a2 2 0 114 0v.1a1.6 1.6 0 001 1.5 1.6 1.6 0 001.8-.3l.1-.1a2 2 0 112.8 2.8l-.1.1a1.6 1.6 0 00-.3 1.8v.1a1.6 1.6 0 001.5 1H21a2 2 0 110 4h-.1a1.6 1.6 0 00-1.5 1z\",\n};\n/** Onbekende module (klant-paneel): een neutrale ruit, zodat het rijtje visueel niet breekt. */\nconst FALLBACK = \"M12 3l9 9-9 9-9-9z\";\n\nexport function PanelIcon({ id }: { id: string }) {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 24 24\"\n width=\"18\"\n height=\"18\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"shrink-0\"\n >\n {(PADEN[id] ?? FALLBACK).split(\" M\").map((d, i) => (\n <path key={i} d={i === 0 ? d : `M${d}`} />\n ))}\n </svg>\n );\n}\n"],"mappings":";AAqCQ;AA3BR,MAAM,QAAgC;AAAA,EACpC,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,UAAU;AACZ;AAEA,MAAM,WAAW;AAEV,SAAS,UAAU,EAAE,GAAG,GAAmB;AAChD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAY;AAAA,MACZ,SAAQ;AAAA,MACR,OAAM;AAAA,MACN,QAAO;AAAA,MACP,MAAK;AAAA,MACL,QAAO;AAAA,MACP,aAAY;AAAA,MACZ,eAAc;AAAA,MACd,gBAAe;AAAA,MACf,WAAU;AAAA,MAER,iBAAM,EAAE,KAAK,UAAU,MAAM,IAAI,EAAE,IAAI,CAAC,GAAG,MAC3C,oBAAC,UAAa,GAAG,MAAM,IAAI,IAAI,IAAI,CAAC,MAAzB,CAA6B,CACzC;AAAA;AAAA,EACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/ui/PanelIcon.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Kleine lijn-iconen voor de zijbalk. Bewust inline SVG en geen icoon-pakket: het zijn er zeven,\n * en een dependency voor zeven paden is niet te verdedigen in een bundel die élke klantsite laadt.\n *\n * Iconen zijn hier geen versiering maar herkenning: de beheerder van een hoveniersbedrijf logt een\n * keer per maand in en zoekt \"waar stond die tekst ook alweer\" — een vorm vindt hij sneller terug\n * dan een woord.\n */\nconst PADEN: Record<string, string> = {\n // Sleutels zijn de paneel-sleutels (id ?? moduleId) uit de registry.\n start: \"M3 11l9-8 9 8 M5 9v11h5v-6h4v6h5V9\",\n menu: \"M4 6h16 M4 12h16 M4 18h16\",\n shop: \"M6 6h15l-2 8H8z M6 6L5 3H2 M9 20a1 1 0 100-2 1 1 0 000 2z M17 20a1 1 0 100-2 1 1 0 000 2z\",\n pages: \"M4 3h9l4 4v14H4z M13 3v5h4\",\n news: \"M4 5h16v14H4z M7 9h10 M7 13h10 M7 17h6\",\n media: \"M3 5h18v14H3z M3 15l5-5 4 4 3-3 6 6\",\n forms: \"M5 3h14v18H5z M9 8h6 M9 12h6 M9 16h3\",\n stats: \"M4 20V10 M10 20V4 M16 20v-7 M22 20H2\",\n blocks: \"M4 4h7v7H4z M13 4h7v7h-7z M4 13h7v7H4z M13 13h7v7h-7z\",\n bouwstenen: \"M4 4h7v7H4z M13 4h7v7h-7z M4 13h7v7H4z M13 13h7v7h-7z\",\n settings: \"M12 15a3 3 0 100-6 3 3 0 000 6z M19.4 15a1.6 1.6 0 00.3 1.8l.1.1a2 2 0 11-2.8 2.8l-.1-.1a1.6 1.6 0 00-2.7 1.1V21a2 2 0 11-4 0v-.1A1.6 1.6 0 007.5 19.4l-.1.1a2 2 0 11-2.8-2.8l.1-.1A1.6 1.6 0 003 14.6H3a2 2 0 110-4h.1A1.6 1.6 0 004.6 7.5l-.1-.1a2 2 0 112.8-2.8l.1.1a1.6 1.6 0 001.8.3H9.4a1.6 1.6 0 001-1.5V3a2 2 0 114 0v.1a1.6 1.6 0 001 1.5 1.6 1.6 0 001.8-.3l.1-.1a2 2 0 112.8 2.8l-.1.1a1.6 1.6 0 00-.3 1.8v.1a1.6 1.6 0 001.5 1H21a2 2 0 110 4h-.1a1.6 1.6 0 00-1.5 1z\",\n};\n/** Onbekende module (klant-paneel): een neutrale ruit, zodat het rijtje visueel niet breekt. */\nconst FALLBACK = \"M12 3l9 9-9 9-9-9z\";\n\nexport function PanelIcon({ id }: { id: string }) {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 24 24\"\n width=\"18\"\n height=\"18\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"shrink-0\"\n >\n {(PADEN[id] ?? FALLBACK).split(\" M\").map((d, i) => (\n <path key={i} d={i === 0 ? d : `M${d}`} />\n ))}\n </svg>\n );\n}\n"],"mappings":";AA0CQ;AAhCR,MAAM,QAAgC;AAAA;AAAA,EAEpC,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,UAAU;AACZ;AAEA,MAAM,WAAW;AAEV,SAAS,UAAU,EAAE,GAAG,GAAmB;AAChD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAY;AAAA,MACZ,SAAQ;AAAA,MACR,OAAM;AAAA,MACN,QAAO;AAAA,MACP,MAAK;AAAA,MACL,QAAO;AAAA,MACP,aAAY;AAAA,MACZ,eAAc;AAAA,MACd,gBAAe;AAAA,MACf,WAAU;AAAA,MAER,iBAAM,EAAE,KAAK,UAAU,MAAM,IAAI,EAAE,IAAI,CAAC,GAAG,MAC3C,oBAAC,UAAa,GAAG,MAAM,IAAI,IAAI,IAAI,CAAC,MAAzB,CAA6B,CACzC;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -22,7 +22,6 @@ import { StyleEditor } from "../blockEditors/StyleEditor.js";
22
22
  import { siteLocales, parseLocaleSlugs, localizeBlocks } from "../../i18n/overlay.js";
23
23
  import { useEditor } from "../../editor/EditorProvider.js";
24
24
  import { AdminModes } from "../../components/EditBar.js";
25
- import { useBalkOffset } from "../../components/barOffset.js";
26
25
  import { aantalWijzigingen, bepaalWijzigingen, GEEN_WIJZIGINGEN } from "./wijzigingen.js";
27
26
  import { TranslationPanel } from "./TranslationPanel.js";
28
27
  import { PatternInserter } from "../patterns/PatternInserter.js";
@@ -110,7 +109,6 @@ function VisualEditorInner({ onClose }) {
110
109
  const canvasRef = useRef(null);
111
110
  const [conflict, setConflict] = useState(false);
112
111
  const [liveBlocks, setLiveBlocks] = useState(void 0);
113
- useBalkOffset(true);
114
112
  const { vars: themeVars, darkAuto } = useThemeVars();
115
113
  const containerRef = useRef(null);
116
114
  useEffect(() => {
@@ -582,10 +580,14 @@ function VisualEditorInner({ onClose }) {
582
580
  const targetsRef = useRef([]);
583
581
  const editingRef = useRef(null);
584
582
  const wilInlineRef = useRef(false);
583
+ const [editingIndex, setEditingIndex] = useState(null);
585
584
  const stopInline = useCallback((el) => {
586
585
  el.removeAttribute("contenteditable");
587
586
  el.classList.remove("is-editing");
588
- if (editingRef.current === el) editingRef.current = null;
587
+ if (editingRef.current === el) {
588
+ editingRef.current = null;
589
+ setEditingIndex(null);
590
+ }
589
591
  }, []);
590
592
  const meldGeenTekst = useCallback(() => {
591
593
  toast({
@@ -602,6 +604,7 @@ function VisualEditorInner({ onClose }) {
602
604
  el.setAttribute("spellcheck", "true");
603
605
  el.classList.add("is-editing");
604
606
  editingRef.current = el;
607
+ setEditingIndex(index);
605
608
  el.focus();
606
609
  const selection = window.getSelection();
607
610
  const range = document.createRange();
@@ -871,6 +874,20 @@ function VisualEditorInner({ onClose }) {
871
874
  setBlocks(blocks.map((b, idx) => idx === i ? structuredClone(liveBlok) : b));
872
875
  toast({ title: "Blok teruggezet", description: "Dit blok is weer gelijk aan de live versie.", tone: "success" });
873
876
  }
877
+ async function verwerpAlles() {
878
+ const ok = await confirm({
879
+ title: "Alle wijzigingen op deze pagina verwerpen?",
880
+ description: "De hele pagina wordt teruggezet naar de versie die nu live staat. Met Ongedaan maken (Ctrl+Z) kun je hier nog op terugkomen.",
881
+ confirmLabel: "Alles verwerpen",
882
+ tone: "danger"
883
+ });
884
+ if (!ok) return;
885
+ document.activeElement?.blur();
886
+ setSelectedKey(null);
887
+ setPanelOpen(false);
888
+ setBlocks(structuredClone(liveBlocks ?? []));
889
+ toast({ title: "Wijzigingen verworpen", description: "De pagina is weer gelijk aan de live versie.", tone: "success" });
890
+ }
874
891
  function addBlock(type) {
875
892
  const def = registry.get(type);
876
893
  const base = def?.defaults?.() ?? {};
@@ -1254,25 +1271,32 @@ function VisualEditorInner({ onClose }) {
1254
1271
  ] }) })
1255
1272
  ] }, b.key);
1256
1273
  }),
1257
- spots.map((s, i) => /* @__PURE__ */ jsx(
1258
- "button",
1259
- {
1260
- type: "button",
1261
- className: "xicms-ve-pencil",
1262
- style: { top: s.top, left: s.left },
1263
- title: s.title,
1264
- "aria-label": s.title,
1265
- onMouseDown: (e) => e.preventDefault(),
1266
- onMouseEnter: () => targetsRef.current[i]?.el.classList.add("is-hover"),
1267
- onMouseLeave: () => targetsRef.current[i]?.el.classList.remove("is-hover"),
1268
- onClick: () => startInline(i),
1269
- children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "12", height: "12", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
1270
- /* @__PURE__ */ jsx("path", { d: "M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3z" }),
1271
- /* @__PURE__ */ jsx("path", { d: "M14.5 6.5l3 3" })
1272
- ] })
1273
- },
1274
- i
1275
- )),
1274
+ spots.map((s, i) => {
1275
+ const actief = editingIndex === i;
1276
+ return /* @__PURE__ */ jsx(
1277
+ "button",
1278
+ {
1279
+ type: "button",
1280
+ className: `xicms-ve-pencil${actief ? " is-klaar" : ""}`,
1281
+ style: { top: s.top, left: s.left },
1282
+ title: actief ? "Het is goed \u2014 klaar met dit blok (opslaan als werkversie)" : s.title,
1283
+ "aria-label": actief ? "Klaar met dit blok" : s.title,
1284
+ onMouseDown: (e) => e.preventDefault(),
1285
+ onMouseEnter: () => targetsRef.current[i]?.el.classList.add("is-hover"),
1286
+ onMouseLeave: () => targetsRef.current[i]?.el.classList.remove("is-hover"),
1287
+ onClick: () => actief ? klaarMetBlok() : startInline(i),
1288
+ children: actief ? /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "12", height: "12", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M20 6L9 17l-5-5" }) }) : (
1289
+ /* Fijn getekend potlood i.p.v. een emoji: een emoji is per platform anders, kleurt niet
1290
+ mee en oogt op deze schaal als een vlek. Eén pad, currentColor, 1.4 lijndikte. */
1291
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "12", height: "12", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
1292
+ /* @__PURE__ */ jsx("path", { d: "M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3z" }),
1293
+ /* @__PURE__ */ jsx("path", { d: "M14.5 6.5l3 3" })
1294
+ ] })
1295
+ )
1296
+ },
1297
+ i
1298
+ );
1299
+ }),
1276
1300
  productKnop && /* @__PURE__ */ jsxs(
1277
1301
  "div",
1278
1302
  {
@@ -1316,6 +1340,16 @@ function VisualEditorInner({ onClose }) {
1316
1340
  ]
1317
1341
  }
1318
1342
  ),
1343
+ draft && wijzigingen.iets && !publishing && /* @__PURE__ */ jsx(
1344
+ "button",
1345
+ {
1346
+ type: "button",
1347
+ className: "xicms-ve-verwerp",
1348
+ onClick: () => void verwerpAlles(),
1349
+ title: "Alle wijzigingen op deze pagina verwerpen (terug naar live)",
1350
+ children: "\u21BA Verwerpen"
1351
+ }
1352
+ ),
1319
1353
  draft && /* @__PURE__ */ jsxs(
1320
1354
  "button",
1321
1355
  {