@xiwebdesign/xicms-core 7.15.0 → 7.16.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.
@@ -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 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}>\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}>\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,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":[]}
@@ -582,10 +582,14 @@ function VisualEditorInner({ onClose }) {
582
582
  const targetsRef = useRef([]);
583
583
  const editingRef = useRef(null);
584
584
  const wilInlineRef = useRef(false);
585
+ const [editingIndex, setEditingIndex] = useState(null);
585
586
  const stopInline = useCallback((el) => {
586
587
  el.removeAttribute("contenteditable");
587
588
  el.classList.remove("is-editing");
588
- if (editingRef.current === el) editingRef.current = null;
589
+ if (editingRef.current === el) {
590
+ editingRef.current = null;
591
+ setEditingIndex(null);
592
+ }
589
593
  }, []);
590
594
  const meldGeenTekst = useCallback(() => {
591
595
  toast({
@@ -602,6 +606,7 @@ function VisualEditorInner({ onClose }) {
602
606
  el.setAttribute("spellcheck", "true");
603
607
  el.classList.add("is-editing");
604
608
  editingRef.current = el;
609
+ setEditingIndex(index);
605
610
  el.focus();
606
611
  const selection = window.getSelection();
607
612
  const range = document.createRange();
@@ -871,6 +876,20 @@ function VisualEditorInner({ onClose }) {
871
876
  setBlocks(blocks.map((b, idx) => idx === i ? structuredClone(liveBlok) : b));
872
877
  toast({ title: "Blok teruggezet", description: "Dit blok is weer gelijk aan de live versie.", tone: "success" });
873
878
  }
879
+ async function verwerpAlles() {
880
+ const ok = await confirm({
881
+ title: "Alle wijzigingen op deze pagina verwerpen?",
882
+ description: "De hele pagina wordt teruggezet naar de versie die nu live staat. Met Ongedaan maken (Ctrl+Z) kun je hier nog op terugkomen.",
883
+ confirmLabel: "Alles verwerpen",
884
+ tone: "danger"
885
+ });
886
+ if (!ok) return;
887
+ document.activeElement?.blur();
888
+ setSelectedKey(null);
889
+ setPanelOpen(false);
890
+ setBlocks(structuredClone(liveBlocks ?? []));
891
+ toast({ title: "Wijzigingen verworpen", description: "De pagina is weer gelijk aan de live versie.", tone: "success" });
892
+ }
874
893
  function addBlock(type) {
875
894
  const def = registry.get(type);
876
895
  const base = def?.defaults?.() ?? {};
@@ -1254,25 +1273,32 @@ function VisualEditorInner({ onClose }) {
1254
1273
  ] }) })
1255
1274
  ] }, b.key);
1256
1275
  }),
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
- )),
1276
+ spots.map((s, i) => {
1277
+ const actief = editingIndex === i;
1278
+ return /* @__PURE__ */ jsx(
1279
+ "button",
1280
+ {
1281
+ type: "button",
1282
+ className: `xicms-ve-pencil${actief ? " is-klaar" : ""}`,
1283
+ style: { top: s.top, left: s.left },
1284
+ title: actief ? "Het is goed \u2014 klaar met dit blok (opslaan als werkversie)" : s.title,
1285
+ "aria-label": actief ? "Klaar met dit blok" : s.title,
1286
+ onMouseDown: (e) => e.preventDefault(),
1287
+ onMouseEnter: () => targetsRef.current[i]?.el.classList.add("is-hover"),
1288
+ onMouseLeave: () => targetsRef.current[i]?.el.classList.remove("is-hover"),
1289
+ onClick: () => actief ? klaarMetBlok() : startInline(i),
1290
+ 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" }) }) : (
1291
+ /* Fijn getekend potlood i.p.v. een emoji: een emoji is per platform anders, kleurt niet
1292
+ mee en oogt op deze schaal als een vlek. Eén pad, currentColor, 1.4 lijndikte. */
1293
+ /* @__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: [
1294
+ /* @__PURE__ */ jsx("path", { d: "M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3z" }),
1295
+ /* @__PURE__ */ jsx("path", { d: "M14.5 6.5l3 3" })
1296
+ ] })
1297
+ )
1298
+ },
1299
+ i
1300
+ );
1301
+ }),
1276
1302
  productKnop && /* @__PURE__ */ jsxs(
1277
1303
  "div",
1278
1304
  {
@@ -1316,6 +1342,16 @@ function VisualEditorInner({ onClose }) {
1316
1342
  ]
1317
1343
  }
1318
1344
  ),
1345
+ draft && wijzigingen.iets && !publishing && /* @__PURE__ */ jsx(
1346
+ "button",
1347
+ {
1348
+ type: "button",
1349
+ className: "xicms-ve-verwerp",
1350
+ onClick: () => void verwerpAlles(),
1351
+ title: "Alle wijzigingen op deze pagina verwerpen (terug naar live)",
1352
+ children: "\u21BA Verwerpen"
1353
+ }
1354
+ ),
1319
1355
  draft && /* @__PURE__ */ jsxs(
1320
1356
  "button",
1321
1357
  {