@xiwebdesign/xicms-core 7.12.1 → 7.14.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/admin/AdminApp.js +2 -0
- package/dist/admin/AdminApp.js.map +1 -1
- package/dist/admin/lib/mediaUsage.d.ts +17 -0
- package/dist/admin/lib/mediaUsage.js +32 -1
- package/dist/admin/lib/mediaUsage.js.map +1 -1
- package/dist/admin/lib/zodFields.d.ts +2 -2
- package/dist/admin/lib/zodFields.js +2 -0
- package/dist/admin/lib/zodFields.js.map +1 -1
- package/dist/admin/media/MediaManager.js +74 -17
- package/dist/admin/media/MediaManager.js.map +1 -1
- package/dist/admin/ui/toast.d.ts +4 -0
- package/dist/admin/ui/toast.js +9 -3
- package/dist/admin/ui/toast.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +69 -5
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/blocks/BlockErrorBoundary.d.ts +1 -0
- package/dist/blocks/BlockErrorBoundary.js +4 -0
- package/dist/blocks/BlockErrorBoundary.js.map +1 -1
- package/dist/blocks/BlockRenderer.js +1 -1
- package/dist/blocks/BlockRenderer.js.map +1 -1
- package/dist/blocks/builtinSchemas.d.ts +23 -0
- package/dist/blocks/builtinSchemas.js +138 -0
- package/dist/blocks/builtinSchemas.js.map +1 -0
- package/dist/blocks/builtins/Accordion.js +0 -2
- package/dist/blocks/builtins/Accordion.js.map +1 -1
- package/dist/blocks/builtins/Agenda.js +0 -2
- package/dist/blocks/builtins/Agenda.js.map +1 -1
- package/dist/blocks/builtins/Banner.js +0 -2
- package/dist/blocks/builtins/Banner.js.map +1 -1
- package/dist/blocks/builtins/BeforeAfter.js +0 -2
- package/dist/blocks/builtins/BeforeAfter.js.map +1 -1
- package/dist/blocks/builtins/Booking.js +0 -2
- package/dist/blocks/builtins/Booking.js.map +1 -1
- package/dist/blocks/builtins/Breadcrumbs.js +0 -2
- package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
- package/dist/blocks/builtins/Cards.js +0 -2
- package/dist/blocks/builtins/Cards.js.map +1 -1
- package/dist/blocks/builtins/Columns.js +0 -2
- package/dist/blocks/builtins/Columns.js.map +1 -1
- package/dist/blocks/builtins/Contact.d.ts +1 -1
- package/dist/blocks/builtins/Contact.js +0 -3
- package/dist/blocks/builtins/Contact.js.map +1 -1
- package/dist/blocks/builtins/Countdown.js +0 -2
- package/dist/blocks/builtins/Countdown.js.map +1 -1
- package/dist/blocks/builtins/Cta.js +0 -2
- package/dist/blocks/builtins/Cta.js.map +1 -1
- package/dist/blocks/builtins/Downloads.js +0 -2
- package/dist/blocks/builtins/Downloads.js.map +1 -1
- package/dist/blocks/builtins/Elements.js +0 -4
- package/dist/blocks/builtins/Elements.js.map +1 -1
- package/dist/blocks/builtins/Embed.js +0 -2
- package/dist/blocks/builtins/Embed.js.map +1 -1
- package/dist/blocks/builtins/Faq.js +0 -2
- package/dist/blocks/builtins/Faq.js.map +1 -1
- package/dist/blocks/builtins/FormBlock.js +0 -2
- package/dist/blocks/builtins/FormBlock.js.map +1 -1
- package/dist/blocks/builtins/Gallery.d.ts +1 -1
- package/dist/blocks/builtins/Gallery.js +0 -2
- package/dist/blocks/builtins/Gallery.js.map +1 -1
- package/dist/blocks/builtins/Hero.js +0 -2
- package/dist/blocks/builtins/Hero.js.map +1 -1
- package/dist/blocks/builtins/ImageBlock.js +0 -2
- package/dist/blocks/builtins/ImageBlock.js.map +1 -1
- package/dist/blocks/builtins/Jobs.d.ts +1 -1
- package/dist/blocks/builtins/Jobs.js +0 -2
- package/dist/blocks/builtins/Jobs.js.map +1 -1
- package/dist/blocks/builtins/Layout.js +0 -3
- package/dist/blocks/builtins/Layout.js.map +1 -1
- package/dist/blocks/builtins/MapBlock.js +0 -2
- package/dist/blocks/builtins/MapBlock.js.map +1 -1
- package/dist/blocks/builtins/NewsListBlock.js +0 -2
- package/dist/blocks/builtins/NewsListBlock.js.map +1 -1
- package/dist/blocks/builtins/Newsletter.js +0 -2
- package/dist/blocks/builtins/Newsletter.js.map +1 -1
- package/dist/blocks/builtins/OpeningHours.js +0 -2
- package/dist/blocks/builtins/OpeningHours.js.map +1 -1
- package/dist/blocks/builtins/PdfBlock.js +0 -2
- package/dist/blocks/builtins/PdfBlock.js.map +1 -1
- package/dist/blocks/builtins/Portfolio.js +0 -2
- package/dist/blocks/builtins/Portfolio.js.map +1 -1
- package/dist/blocks/builtins/Pricing.d.ts +1 -1
- package/dist/blocks/builtins/Pricing.js +0 -2
- package/dist/blocks/builtins/Pricing.js.map +1 -1
- package/dist/blocks/builtins/RichTextBlock.js +0 -2
- package/dist/blocks/builtins/RichTextBlock.js.map +1 -1
- package/dist/blocks/builtins/Search.js +0 -2
- package/dist/blocks/builtins/Search.js.map +1 -1
- package/dist/blocks/builtins/Sections.js +0 -10
- package/dist/blocks/builtins/Sections.js.map +1 -1
- package/dist/blocks/builtins/Slider.js +0 -2
- package/dist/blocks/builtins/Slider.js.map +1 -1
- package/dist/blocks/builtins/Spacer.js +0 -2
- package/dist/blocks/builtins/Spacer.js.map +1 -1
- package/dist/blocks/builtins/SymbolRef.js +3 -5
- package/dist/blocks/builtins/SymbolRef.js.map +1 -1
- package/dist/blocks/builtins/Team.js +0 -2
- package/dist/blocks/builtins/Team.js.map +1 -1
- package/dist/blocks/builtins/Testimonials.d.ts +1 -1
- package/dist/blocks/builtins/Testimonials.js +0 -2
- package/dist/blocks/builtins/Testimonials.js.map +1 -1
- package/dist/blocks/builtins/Video.d.ts +1 -1
- package/dist/blocks/builtins/Video.js +0 -2
- package/dist/blocks/builtins/Video.js.map +1 -1
- package/dist/blocks/builtins/WhatsappChat.js +0 -2
- package/dist/blocks/builtins/WhatsappChat.js.map +1 -1
- package/dist/blocks/builtins/Widgets.js +0 -26
- package/dist/blocks/builtins/Widgets.js.map +1 -1
- package/dist/blocks/customTemplate/definition.js +1 -1
- package/dist/blocks/customTemplate/definition.js.map +1 -1
- package/dist/blocks/registry.d.ts +24 -13
- package/dist/blocks/registry.js +15 -4
- package/dist/blocks/registry.js.map +1 -1
- package/dist/blocks/reportRenderFout.d.ts +13 -0
- package/dist/blocks/reportRenderFout.js +31 -0
- package/dist/blocks/reportRenderFout.js.map +1 -0
- package/dist/media/ImageContext.js +24 -1
- package/dist/media/ImageContext.js.map +1 -1
- package/dist/modules/shop/blocks/Cart.js +1 -2
- package/dist/modules/shop/blocks/Cart.js.map +1 -1
- package/dist/modules/shop/blocks/Checkout.js +1 -2
- package/dist/modules/shop/blocks/Checkout.js.map +1 -1
- package/dist/modules/shop/blocks/Donation.js +1 -2
- package/dist/modules/shop/blocks/Donation.js.map +1 -1
- package/dist/modules/shop/blocks/OrderStatus.js +0 -2
- package/dist/modules/shop/blocks/OrderStatus.js.map +1 -1
- package/dist/modules/shop/blocks/ProductDetail.js +1 -7
- package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
- package/dist/modules/shop/blocks/ProductGrid.js +1 -2
- package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
- package/dist/modules/shop/blocks/ShopCategories.js +0 -2
- package/dist/modules/shop/blocks/ShopCategories.js.map +1 -1
- package/dist/modules/shop/module.js +1 -0
- package/dist/modules/shop/module.js.map +1 -1
- package/dist/modules/shop/schemas.d.ts +1 -0
- package/dist/modules/shop/schemas.js +32 -0
- package/dist/modules/shop/schemas.js.map +1 -0
- package/dist/modules/types.d.ts +6 -0
- package/dist/schema/blockBase.d.ts +99 -0
- package/dist/schema/blockBase.js +22 -0
- package/dist/schema/blockBase.js.map +1 -0
- package/dist/schema/blocks.d.ts +1 -91
- package/dist/schema/blocks.js +64 -77
- package/dist/schema/blocks.js.map +1 -1
- package/dist/schema/pages.js +1 -1
- package/dist/schema/pages.js.map +1 -1
- package/dist/schema/shop.d.ts +1 -11
- package/dist/schema/shop.js +2 -21
- package/dist/schema/shop.js.map +1 -1
- package/dist/schema/shopGeld.d.ts +18 -0
- package/dist/schema/shopGeld.js +27 -0
- package/dist/schema/shopGeld.js.map +1 -0
- package/dist/schema/symbols.js +1 -1
- package/dist/schema/symbols.js.map +1 -1
- package/dist/server/blockData.js +14 -1
- package/dist/server/blockData.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +2 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/pages.d.ts +16 -0
- package/dist/server/pages.js +9 -0
- package/dist/server/pages.js.map +1 -1
- package/dist/styles.css +62 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/admin/AdminApp.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { registerBuiltinSchemas } from "../blocks/builtinSchemas.js";
|
|
4
|
+
registerBuiltinSchemas();
|
|
3
5
|
import { useState } from "react";
|
|
4
6
|
import { useAuth } from "../auth/AuthProvider.js";
|
|
5
7
|
import { LoginForm } from "../auth/LoginForm.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/AdminApp.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { LoginForm } from \"../auth/LoginForm\";\nimport { useSiteConfig } from \"../config\";\nimport { useModules } from \"../modules/context\";\nimport { canEdit, canManageSite, type Role } from \"../auth/roles\";\nimport { Tabs, TabsContent } from \"./ui/tabs\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { ToastProvider } from \"./ui/toast\";\nimport { ConfirmHost } from \"./ui/ConfirmDialog\";\nimport { Button } from \"./ui/primitives\";\nimport { PanelIcon } from \"./ui/PanelIcon\";\nimport { builtinPanels, type PanelDefinition } from \"./panelRegistry\";\nimport { AdminHelp } from \"./AdminHelp\";\nimport { CoreVersion } from \"./CoreVersion\";\n\n/**\n * Rol-bewaakt beheer (XiCMS 3.0). De secties komen uit de paneel-registry, gefilterd op de actieve\n * modules (`SiteConfig.modules`/`features`) en de rol van de gebruiker. Een module met een eigen\n * paneel (ook klant-modules via `extraPanels`) verschijnt automatisch.\n *\n * VORM: een schermvullend werkblad met een vaste zijbalk, geen kaart in de pagina. Het beheer viel\n * hiervoor binnen de site-chrome — nav erboven, footer eronder, cookiebanner eroverheen — en las\n * daardoor als een blok ergens op de website in plaats van als gereedschap. De site-chrome leeft nu\n * in `app/(site)/layout.tsx`; /beheer valt daarbuiten.\n *\n * De zijbalk verving de horizontale tabstrip omdat die bij zeven of meer panelen al omsloeg naar een\n * tweede regel. Verticaal is er bovendien ruimte voor een icoon naast het woord, en `orientation`\n * houdt de pijltjestoetsen correct (Radix).\n *\n * Wikkel in `.xicms-admin` zodat de Tailwind-stijlen uit `@xiwebdesign/xicms-core/admin.css` scoped\n * blijven en niet naar de site lekken. Importeer die CSS alleen op de /beheer-route.\n */\nfunction roleAllows(role: Role | null, min: PanelDefinition[\"minRole\"]): boolean {\n return min === \"siteadmin\" ? canManageSite(role) : canEdit(role);\n}\n\n/** Gecentreerde boodschap voor de toestanden waarin er (nog) geen beheer te tonen is. */\nfunction Melding({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"xicms-admin__midden grid place-items-center p-6\">\n <div className=\"w-full max-w-md rounded-xl border border-slate-200 bg-white p-6 text-sm shadow-sm\">{children}</div>\n </div>\n );\n}\n\nexport function AdminApp({\n extraPanels = [],\n embedded = false,\n}: {\n extraPanels?: PanelDefinition[];\n /**\n * Draait het beheer BINNEN de site-schil (header, logo, menu, footer eromheen) in plaats van als\n * schermvullend werkblad op een kale pagina?\n *\n * Het verschil is niet cosmetisch. Los rekent het werkblad met het VENSTER: `min-h-screen`, en een\n * zijbalk die met `sticky top-0` aan de bovenkant van het scherm plakt. Zet je daar een\n * site-header boven, dan loopt de pagina 100vh + de hoogte van die header, en plakt de zijbalk\n * onder de header vast in plaats van erlangs. Ingebed laat de zijbalk daarom los van het venster\n * en wordt hij een balk over de volle breedte — dat vecht niet met de navigatie erboven.\n */\n embedded?: boolean;\n}) {\n const { user, role, loading, configured, signOut } = useAuth();\n const { name, siteId } = useSiteConfig();\n const { activeIds } = useModules();\n\n // Panelen van actieve modules waar de rol toegang toe heeft, gesorteerd op volgorde.\n const panels = [...builtinPanels, ...extraPanels]\n .filter((p) => activeIds.has(p.moduleId) && roleAllows(role, p.minRole))\n .sort((a, b) => a.order - b.order);\n /** Sleutel van een paneel in de tabbladen: eigen `id` als die er is, anders de module-id. */\n const sleutel = (p: PanelDefinition) => p.id ?? p.moduleId;\n const firstTab = panels[0] && sleutel(panels[0]);\n // Gecontroleerd i.p.v. `defaultValue`: het werkblad toont de naam van de sectie waar je in zit.\n // Zonder die kop lees je alleen aan de zijbalk-markering af waar je bent, en dat is te weinig\n // houvast op een breed scherm waar de zijbalk ver van je blik af staat.\n const [actief, setActief] = useState<string | undefined>(firstTab);\n const actievePanel = panels.find((p) => sleutel(p) === (actief ?? firstTab));\n\n return (\n <div className={embedded ? \"xicms-admin xicms-admin--ingebed\" : \"xicms-admin\"}>\n <ToastProvider>\n <ConfirmHost>\n {!configured ? (\n <Melding>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>-variabelen in\n om het beheer aan te zetten.\n </Melding>\n ) : loading ? (\n <Melding>\n <span className=\"text-slate-500\">Bezig met laden…</span>\n </Melding>\n ) : !user ? (\n <div className=\"xicms-admin__midden grid place-items-center p-6\">\n <div className=\"w-full max-w-sm\">\n <p className=\"mb-4 text-center text-sm text-slate-500\">Beheer · {name}</p>\n {/*\n Na inloggen naar de HOMEPAGINA, niet naar dit scherm.\n\n Dat is een sturende keuze, geen detail. Wie op het beheer uitkomt gaat daar\n rondkijken en bewerkt zijn teksten in formulieren. Wie op zijn eigen site uitkomt\n ziet de beheerbalk over de pagina heen en past de tekst aan op de plek waar hij\n staat — en dat is waar XiCMS voor gemaakt is. Het beheer blijft één klik weg, via\n \"Beheren\" in die balk, voor het werk dat er wél thuishoort.\n\n Ook hier en niet alleen op /login: je komt op /beheer terecht via een bladwijzer of\n een oude link, en dan gold die keuze ineens niet.\n */}\n <LoginForm onSuccess={() => { window.location.replace(\"/\"); }} />\n </div>\n </div>\n ) : !canEdit(role) ? (\n <Melding>\n Je bent ingelogd als <strong>{user.email}</strong>, maar hebt geen beheerrechten voor deze site.\n <div className=\"mt-3\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>Uitloggen</Button>\n </div>\n </Melding>\n ) : panels.length === 0 ? (\n <Melding>Geen actieve beheer-modules voor deze site.</Melding>\n ) : (\n <Tabs value={actief ?? firstTab} onValueChange={setActief} orientation=\"vertical\" className=\"xicms-admin__raster md:grid md:grid-cols-[248px_1fr]\">\n {/* ── Zijbalk ─────────────────────────────────────────────────────────── */}\n <div\n className=\"xicms-admin__rail flex flex-col gap-4 border-b border-slate-200 p-4 md:sticky md:top-0 md:h-screen md:border-r md:border-b-0\"\n style={{ background: \"var(--admin-sidebar)\" }}\n >\n <div className=\"min-w-0 px-2 pt-1\">\n <p className=\"truncate text-sm font-semibold\">{name}</p>\n <p className=\"text-xs text-slate-500\">Beheer</p>\n </div>\n\n <TabsPrimitive.List aria-label=\"Beheer-secties\" className=\"xicms-admin__lijst -mx-1 flex gap-1 overflow-x-auto md:flex-col md:overflow-visible\">\n {panels.map((p) => (\n <TabsPrimitive.Trigger\n key={sleutel(p)}\n value={sleutel(p)}\n className=\"flex shrink-0 items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-200 hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none data-[state=active]:bg-slate-100 data-[state=active]:text-slate-900 data-[state=active]:shadow-[inset_2px_0_0_0_var(--color-brand-500)]\"\n >\n <PanelIcon id={p.moduleId} />\n {p.label}\n </TabsPrimitive.Trigger>\n ))}\n </TabsPrimitive.List>\n\n {/* Voet van de zijbalk: wie ben ik, welke engine draait er, en eruit. Stond eerder als\n vier losse regels op elkaar gepakt — e-mail, rol, versie en een onderstreepte link\n liepen visueel in elkaar over. Nu drie duidelijke lagen met lucht ertussen, en\n Uitloggen als een echte knop in plaats van een tekstlink. */}\n <div className=\"xicms-admin__voet mt-auto hidden min-w-0 flex-col gap-3 border-t border-slate-200 pt-3 md:flex\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs font-medium text-slate-600\" title={user.email ?? undefined}>{user.email}</p>\n <p className=\"text-xs text-slate-400\">{role}</p>\n </div>\n <CoreVersion />\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>\n Uitloggen\n </Button>\n </div>\n </div>\n\n {/* ── Werkblad ────────────────────────────────────────────────────────── */}\n <div className=\"min-w-0\">\n <header className=\"xicms-admin__kop sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-200 px-4 py-3 md:px-8\" style={{ background: \"var(--admin-canvas)\" }}>\n <h1 className=\"truncate text-base font-semibold\">{actievePanel?.label ?? \"Beheer\"}</h1>\n <a\n className=\"inline-flex h-8 shrink-0 items-center rounded-lg border border-slate-200 px-3 text-sm hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n href=\"/\"\n >\n Bekijk site ↗\n </a>\n </header>\n <div className=\"grid gap-6 p-4 md:p-8\">\n {panels.map((p) => (\n <TabsContent key={sleutel(p)} value={sleutel(p)} className=\"pt-0\">\n <p.Panel siteId={siteId} />\n </TabsContent>\n ))}\n <AdminHelp />\n </div>\n </div>\n </Tabs>\n )}\n </ConfirmHost>\n </ToastProvider>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/admin/AdminApp.tsx"],"sourcesContent":["\"use client\";\n\nimport { registerBuiltinSchemas } from \"../blocks/builtinSchemas\";\n// Schema's zijn lazy (7.13): dit is een editor-chunk, dus hier horen ze thuis.\nregisterBuiltinSchemas();\nimport { useState } from \"react\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { LoginForm } from \"../auth/LoginForm\";\nimport { useSiteConfig } from \"../config\";\nimport { useModules } from \"../modules/context\";\nimport { canEdit, canManageSite, type Role } from \"../auth/roles\";\nimport { Tabs, TabsContent } from \"./ui/tabs\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { ToastProvider } from \"./ui/toast\";\nimport { ConfirmHost } from \"./ui/ConfirmDialog\";\nimport { Button } from \"./ui/primitives\";\nimport { PanelIcon } from \"./ui/PanelIcon\";\nimport { builtinPanels, type PanelDefinition } from \"./panelRegistry\";\nimport { AdminHelp } from \"./AdminHelp\";\nimport { CoreVersion } from \"./CoreVersion\";\n\n/**\n * Rol-bewaakt beheer (XiCMS 3.0). De secties komen uit de paneel-registry, gefilterd op de actieve\n * modules (`SiteConfig.modules`/`features`) en de rol van de gebruiker. Een module met een eigen\n * paneel (ook klant-modules via `extraPanels`) verschijnt automatisch.\n *\n * VORM: een schermvullend werkblad met een vaste zijbalk, geen kaart in de pagina. Het beheer viel\n * hiervoor binnen de site-chrome — nav erboven, footer eronder, cookiebanner eroverheen — en las\n * daardoor als een blok ergens op de website in plaats van als gereedschap. De site-chrome leeft nu\n * in `app/(site)/layout.tsx`; /beheer valt daarbuiten.\n *\n * De zijbalk verving de horizontale tabstrip omdat die bij zeven of meer panelen al omsloeg naar een\n * tweede regel. Verticaal is er bovendien ruimte voor een icoon naast het woord, en `orientation`\n * houdt de pijltjestoetsen correct (Radix).\n *\n * Wikkel in `.xicms-admin` zodat de Tailwind-stijlen uit `@xiwebdesign/xicms-core/admin.css` scoped\n * blijven en niet naar de site lekken. Importeer die CSS alleen op de /beheer-route.\n */\nfunction roleAllows(role: Role | null, min: PanelDefinition[\"minRole\"]): boolean {\n return min === \"siteadmin\" ? canManageSite(role) : canEdit(role);\n}\n\n/** Gecentreerde boodschap voor de toestanden waarin er (nog) geen beheer te tonen is. */\nfunction Melding({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"xicms-admin__midden grid place-items-center p-6\">\n <div className=\"w-full max-w-md rounded-xl border border-slate-200 bg-white p-6 text-sm shadow-sm\">{children}</div>\n </div>\n );\n}\n\nexport function AdminApp({\n extraPanels = [],\n embedded = false,\n}: {\n extraPanels?: PanelDefinition[];\n /**\n * Draait het beheer BINNEN de site-schil (header, logo, menu, footer eromheen) in plaats van als\n * schermvullend werkblad op een kale pagina?\n *\n * Het verschil is niet cosmetisch. Los rekent het werkblad met het VENSTER: `min-h-screen`, en een\n * zijbalk die met `sticky top-0` aan de bovenkant van het scherm plakt. Zet je daar een\n * site-header boven, dan loopt de pagina 100vh + de hoogte van die header, en plakt de zijbalk\n * onder de header vast in plaats van erlangs. Ingebed laat de zijbalk daarom los van het venster\n * en wordt hij een balk over de volle breedte — dat vecht niet met de navigatie erboven.\n */\n embedded?: boolean;\n}) {\n const { user, role, loading, configured, signOut } = useAuth();\n const { name, siteId } = useSiteConfig();\n const { activeIds } = useModules();\n\n // Panelen van actieve modules waar de rol toegang toe heeft, gesorteerd op volgorde.\n const panels = [...builtinPanels, ...extraPanels]\n .filter((p) => activeIds.has(p.moduleId) && roleAllows(role, p.minRole))\n .sort((a, b) => a.order - b.order);\n /** Sleutel van een paneel in de tabbladen: eigen `id` als die er is, anders de module-id. */\n const sleutel = (p: PanelDefinition) => p.id ?? p.moduleId;\n const firstTab = panels[0] && sleutel(panels[0]);\n // Gecontroleerd i.p.v. `defaultValue`: het werkblad toont de naam van de sectie waar je in zit.\n // Zonder die kop lees je alleen aan de zijbalk-markering af waar je bent, en dat is te weinig\n // houvast op een breed scherm waar de zijbalk ver van je blik af staat.\n const [actief, setActief] = useState<string | undefined>(firstTab);\n const actievePanel = panels.find((p) => sleutel(p) === (actief ?? firstTab));\n\n return (\n <div className={embedded ? \"xicms-admin xicms-admin--ingebed\" : \"xicms-admin\"}>\n <ToastProvider>\n <ConfirmHost>\n {!configured ? (\n <Melding>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>-variabelen in\n om het beheer aan te zetten.\n </Melding>\n ) : loading ? (\n <Melding>\n <span className=\"text-slate-500\">Bezig met laden…</span>\n </Melding>\n ) : !user ? (\n <div className=\"xicms-admin__midden grid place-items-center p-6\">\n <div className=\"w-full max-w-sm\">\n <p className=\"mb-4 text-center text-sm text-slate-500\">Beheer · {name}</p>\n {/*\n Na inloggen naar de HOMEPAGINA, niet naar dit scherm.\n\n Dat is een sturende keuze, geen detail. Wie op het beheer uitkomt gaat daar\n rondkijken en bewerkt zijn teksten in formulieren. Wie op zijn eigen site uitkomt\n ziet de beheerbalk over de pagina heen en past de tekst aan op de plek waar hij\n staat — en dat is waar XiCMS voor gemaakt is. Het beheer blijft één klik weg, via\n \"Beheren\" in die balk, voor het werk dat er wél thuishoort.\n\n Ook hier en niet alleen op /login: je komt op /beheer terecht via een bladwijzer of\n een oude link, en dan gold die keuze ineens niet.\n */}\n <LoginForm onSuccess={() => { window.location.replace(\"/\"); }} />\n </div>\n </div>\n ) : !canEdit(role) ? (\n <Melding>\n Je bent ingelogd als <strong>{user.email}</strong>, maar hebt geen beheerrechten voor deze site.\n <div className=\"mt-3\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>Uitloggen</Button>\n </div>\n </Melding>\n ) : panels.length === 0 ? (\n <Melding>Geen actieve beheer-modules voor deze site.</Melding>\n ) : (\n <Tabs value={actief ?? firstTab} onValueChange={setActief} orientation=\"vertical\" className=\"xicms-admin__raster md:grid md:grid-cols-[248px_1fr]\">\n {/* ── Zijbalk ─────────────────────────────────────────────────────────── */}\n <div\n className=\"xicms-admin__rail flex flex-col gap-4 border-b border-slate-200 p-4 md:sticky md:top-0 md:h-screen md:border-r md:border-b-0\"\n style={{ background: \"var(--admin-sidebar)\" }}\n >\n <div className=\"min-w-0 px-2 pt-1\">\n <p className=\"truncate text-sm font-semibold\">{name}</p>\n <p className=\"text-xs text-slate-500\">Beheer</p>\n </div>\n\n <TabsPrimitive.List aria-label=\"Beheer-secties\" className=\"xicms-admin__lijst -mx-1 flex gap-1 overflow-x-auto md:flex-col md:overflow-visible\">\n {panels.map((p) => (\n <TabsPrimitive.Trigger\n key={sleutel(p)}\n value={sleutel(p)}\n className=\"flex shrink-0 items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-200 hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none data-[state=active]:bg-slate-100 data-[state=active]:text-slate-900 data-[state=active]:shadow-[inset_2px_0_0_0_var(--color-brand-500)]\"\n >\n <PanelIcon id={p.moduleId} />\n {p.label}\n </TabsPrimitive.Trigger>\n ))}\n </TabsPrimitive.List>\n\n {/* Voet van de zijbalk: wie ben ik, welke engine draait er, en eruit. Stond eerder als\n vier losse regels op elkaar gepakt — e-mail, rol, versie en een onderstreepte link\n liepen visueel in elkaar over. Nu drie duidelijke lagen met lucht ertussen, en\n Uitloggen als een echte knop in plaats van een tekstlink. */}\n <div className=\"xicms-admin__voet mt-auto hidden min-w-0 flex-col gap-3 border-t border-slate-200 pt-3 md:flex\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs font-medium text-slate-600\" title={user.email ?? undefined}>{user.email}</p>\n <p className=\"text-xs text-slate-400\">{role}</p>\n </div>\n <CoreVersion />\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>\n Uitloggen\n </Button>\n </div>\n </div>\n\n {/* ── Werkblad ────────────────────────────────────────────────────────── */}\n <div className=\"min-w-0\">\n <header className=\"xicms-admin__kop sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-200 px-4 py-3 md:px-8\" style={{ background: \"var(--admin-canvas)\" }}>\n <h1 className=\"truncate text-base font-semibold\">{actievePanel?.label ?? \"Beheer\"}</h1>\n <a\n className=\"inline-flex h-8 shrink-0 items-center rounded-lg border border-slate-200 px-3 text-sm hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n href=\"/\"\n >\n Bekijk site ↗\n </a>\n </header>\n <div className=\"grid gap-6 p-4 md:p-8\">\n {panels.map((p) => (\n <TabsContent key={sleutel(p)} value={sleutel(p)} className=\"pt-0\">\n <p.Panel siteId={siteId} />\n </TabsContent>\n ))}\n <AdminHelp />\n </div>\n </div>\n </Tabs>\n )}\n </ConfirmHost>\n </ToastProvider>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -8,3 +8,20 @@ export declare function findMediaUsage(siteId: string, item: {
|
|
|
8
8
|
id: string;
|
|
9
9
|
url?: string;
|
|
10
10
|
}): Promise<string[]>;
|
|
11
|
+
/**
|
|
12
|
+
* Bulk-variant voor het opruim-paneel (7.14): één content-read voor de HELE bibliotheek in
|
|
13
|
+
* plaats van (zoals `findMediaUsage`) alle content per afbeelding opnieuw te lezen — bij
|
|
14
|
+
* 60 foto's scheelt dat ~59 volledige site-reads. Kijkt in meer bronnen dan de losse check:
|
|
15
|
+
* ook symbolen en producten kunnen naar een foto verwijzen.
|
|
16
|
+
*
|
|
17
|
+
* "Ongebruikt" is bewust voorzichtig: een item dat nog verwerkt wordt (status "processing")
|
|
18
|
+
* of jonger is dan een uur telt níét mee — een net geüploade foto die de klant nog in een
|
|
19
|
+
* blok gaat zetten, hoort niet in een opruimlijst.
|
|
20
|
+
*/
|
|
21
|
+
export declare function findUnusedMedia(siteId: string, items: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
url?: string;
|
|
24
|
+
status?: string;
|
|
25
|
+
createdAt?: unknown;
|
|
26
|
+
size?: number;
|
|
27
|
+
}>): Promise<string[]>;
|
|
@@ -30,7 +30,38 @@ async function findMediaUsage(siteId, item) {
|
|
|
30
30
|
return hits;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
async function findUnusedMedia(siteId, items) {
|
|
34
|
+
const db = getDb();
|
|
35
|
+
if (!db) return [];
|
|
36
|
+
const blobs = [];
|
|
37
|
+
const [pages, drafts, news, symbols, products, settings] = await Promise.all([
|
|
38
|
+
getDocs(collection(db, "sites", siteId, "pages")),
|
|
39
|
+
getDocs(collection(db, "sites", siteId, "drafts")).catch(() => null),
|
|
40
|
+
getDocs(collection(db, "sites", siteId, "news")).catch(() => null),
|
|
41
|
+
getDocs(collection(db, "sites", siteId, "symbols")).catch(() => null),
|
|
42
|
+
getDocs(collection(db, "sites", siteId, "products")).catch(() => null),
|
|
43
|
+
getDoc(doc(db, "sites", siteId, "settings", "public")).catch(() => null)
|
|
44
|
+
]);
|
|
45
|
+
for (const snap of [pages, drafts, news, symbols, products]) {
|
|
46
|
+
for (const d of snap?.docs ?? []) blobs.push(JSON.stringify(d.data()));
|
|
47
|
+
}
|
|
48
|
+
if (settings?.exists()) blobs.push(JSON.stringify(settings.data()));
|
|
49
|
+
const corpus = blobs.join("\n");
|
|
50
|
+
const uurGeleden = Date.now() - 60 * 60 * 1e3;
|
|
51
|
+
const millis = (v) => {
|
|
52
|
+
const t = v;
|
|
53
|
+
return typeof t?.toMillis === "function" ? t.toMillis() : 0;
|
|
54
|
+
};
|
|
55
|
+
return items.filter((it) => {
|
|
56
|
+
if (it.status === "processing") return false;
|
|
57
|
+
if (millis(it.createdAt) > uurGeleden) return false;
|
|
58
|
+
if (it.url && corpus.includes(it.url)) return false;
|
|
59
|
+
if (corpus.includes(it.id)) return false;
|
|
60
|
+
return true;
|
|
61
|
+
}).map((it) => it.id);
|
|
62
|
+
}
|
|
33
63
|
export {
|
|
34
|
-
findMediaUsage
|
|
64
|
+
findMediaUsage,
|
|
65
|
+
findUnusedMedia
|
|
35
66
|
};
|
|
36
67
|
//# sourceMappingURL=mediaUsage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/lib/mediaUsage.ts"],"sourcesContent":["\"use client\";\n\nimport { collection, doc, getDoc, getDocs } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\n\n/**\n * Zoekt (goedkoop) of een media-item ergens in gebruik is, zodat we kunnen waarschuwen vóór\n * verwijderen (anders breekt een live pagina met een dode afbeelding). Gescand worden:\n * gepubliceerde pagina's, concepten (drafts), nieuwsartikelen (covers/inline beelden) en de\n * site-instellingen (og-beeld e.d.). Sites zijn klein → een JSON-substring-scan is ruim voldoende.\n */\nexport async function findMediaUsage(\n siteId: string,\n item: { id: string; url?: string },\n): Promise<string[]> {\n const db = getDb();\n if (!db) return [];\n const matches = (blob: string) => (item.url && blob.includes(item.url)) || blob.includes(item.id);\n const hits: string[] = [];\n try {\n const [pages, drafts, news, settings] = await Promise.all([\n getDocs(collection(db, \"sites\", siteId, \"pages\")),\n getDocs(collection(db, \"sites\", siteId, \"drafts\")).catch(() => null),\n getDocs(collection(db, \"sites\", siteId, \"news\")).catch(() => null),\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"public\")).catch(() => null),\n ]);\n for (const d of pages.docs) {\n const data = d.data() as Record<string, unknown>;\n if (matches(JSON.stringify(data.blocks ?? []))) hits.push(String(data.path ?? data.title ?? d.id));\n }\n for (const d of drafts?.docs ?? []) {\n const data = d.data() as Record<string, unknown>;\n if (matches(JSON.stringify(data.blocks ?? []))) hits.push(`${String(data.path ?? d.id)} (concept)`);\n }\n for (const d of news?.docs ?? []) {\n if (matches(JSON.stringify(d.data()))) hits.push(`nieuws: ${String((d.data() as { title?: string }).title ?? d.id)}`);\n }\n if (settings?.exists() && matches(JSON.stringify(settings.data()))) hits.push(\"site-instellingen\");\n return [...new Set(hits)];\n } catch {\n return hits;\n }\n}\n"],"mappings":";AAEA,SAAS,YAAY,KAAK,QAAQ,eAAe;AACjD,SAAS,aAAa;AAQtB,eAAsB,eACpB,QACA,MACmB;AACnB,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,QAAO,CAAC;AACjB,QAAM,UAAU,CAAC,SAAkB,KAAK,OAAO,KAAK,SAAS,KAAK,GAAG,KAAM,KAAK,SAAS,KAAK,EAAE;AAChG,QAAM,OAAiB,CAAC;AACxB,MAAI;AACF,UAAM,CAAC,OAAO,QAAQ,MAAM,QAAQ,IAAI,MAAM,QAAQ,IAAI;AAAA,MACxD,QAAQ,WAAW,IAAI,SAAS,QAAQ,OAAO,CAAC;AAAA,MAChD,QAAQ,WAAW,IAAI,SAAS,QAAQ,QAAQ,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,MACnE,QAAQ,WAAW,IAAI,SAAS,QAAQ,MAAM,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,MACjE,OAAO,IAAI,IAAI,SAAS,QAAQ,YAAY,QAAQ,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,IACzE,CAAC;AACD,eAAW,KAAK,MAAM,MAAM;AAC1B,YAAM,OAAO,EAAE,KAAK;AACpB,UAAI,QAAQ,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC,CAAC,EAAG,MAAK,KAAK,OAAO,KAAK,QAAQ,KAAK,SAAS,EAAE,EAAE,CAAC;AAAA,IACnG;AACA,eAAW,KAAK,QAAQ,QAAQ,CAAC,GAAG;AAClC,YAAM,OAAO,EAAE,KAAK;AACpB,UAAI,QAAQ,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC,CAAC,EAAG,MAAK,KAAK,GAAG,OAAO,KAAK,QAAQ,EAAE,EAAE,CAAC,YAAY;AAAA,IACpG;AACA,eAAW,KAAK,MAAM,QAAQ,CAAC,GAAG;AAChC,UAAI,QAAQ,KAAK,UAAU,EAAE,KAAK,CAAC,CAAC,EAAG,MAAK,KAAK,WAAW,OAAQ,EAAE,KAAK,EAAyB,SAAS,EAAE,EAAE,CAAC,EAAE;AAAA,IACtH;AACA,QAAI,UAAU,OAAO,KAAK,QAAQ,KAAK,UAAU,SAAS,KAAK,CAAC,CAAC,EAAG,MAAK,KAAK,mBAAmB;AACjG,WAAO,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/lib/mediaUsage.ts"],"sourcesContent":["\"use client\";\n\nimport { collection, doc, getDoc, getDocs } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\n\n/**\n * Zoekt (goedkoop) of een media-item ergens in gebruik is, zodat we kunnen waarschuwen vóór\n * verwijderen (anders breekt een live pagina met een dode afbeelding). Gescand worden:\n * gepubliceerde pagina's, concepten (drafts), nieuwsartikelen (covers/inline beelden) en de\n * site-instellingen (og-beeld e.d.). Sites zijn klein → een JSON-substring-scan is ruim voldoende.\n */\nexport async function findMediaUsage(\n siteId: string,\n item: { id: string; url?: string },\n): Promise<string[]> {\n const db = getDb();\n if (!db) return [];\n const matches = (blob: string) => (item.url && blob.includes(item.url)) || blob.includes(item.id);\n const hits: string[] = [];\n try {\n const [pages, drafts, news, settings] = await Promise.all([\n getDocs(collection(db, \"sites\", siteId, \"pages\")),\n getDocs(collection(db, \"sites\", siteId, \"drafts\")).catch(() => null),\n getDocs(collection(db, \"sites\", siteId, \"news\")).catch(() => null),\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"public\")).catch(() => null),\n ]);\n for (const d of pages.docs) {\n const data = d.data() as Record<string, unknown>;\n if (matches(JSON.stringify(data.blocks ?? []))) hits.push(String(data.path ?? data.title ?? d.id));\n }\n for (const d of drafts?.docs ?? []) {\n const data = d.data() as Record<string, unknown>;\n if (matches(JSON.stringify(data.blocks ?? []))) hits.push(`${String(data.path ?? d.id)} (concept)`);\n }\n for (const d of news?.docs ?? []) {\n if (matches(JSON.stringify(d.data()))) hits.push(`nieuws: ${String((d.data() as { title?: string }).title ?? d.id)}`);\n }\n if (settings?.exists() && matches(JSON.stringify(settings.data()))) hits.push(\"site-instellingen\");\n return [...new Set(hits)];\n } catch {\n return hits;\n }\n}\n\n/**\n * Bulk-variant voor het opruim-paneel (7.14): één content-read voor de HELE bibliotheek in\n * plaats van (zoals `findMediaUsage`) alle content per afbeelding opnieuw te lezen — bij\n * 60 foto's scheelt dat ~59 volledige site-reads. Kijkt in meer bronnen dan de losse check:\n * ook symbolen en producten kunnen naar een foto verwijzen.\n *\n * \"Ongebruikt\" is bewust voorzichtig: een item dat nog verwerkt wordt (status \"processing\")\n * of jonger is dan een uur telt níét mee — een net geüploade foto die de klant nog in een\n * blok gaat zetten, hoort niet in een opruimlijst.\n */\nexport async function findUnusedMedia(\n siteId: string,\n items: Array<{ id: string; url?: string; status?: string; createdAt?: unknown; size?: number }>,\n): Promise<string[]> {\n const db = getDb();\n if (!db) return [];\n const blobs: string[] = [];\n const [pages, drafts, news, symbols, products, settings] = await Promise.all([\n getDocs(collection(db, \"sites\", siteId, \"pages\")),\n getDocs(collection(db, \"sites\", siteId, \"drafts\")).catch(() => null),\n getDocs(collection(db, \"sites\", siteId, \"news\")).catch(() => null),\n getDocs(collection(db, \"sites\", siteId, \"symbols\")).catch(() => null),\n getDocs(collection(db, \"sites\", siteId, \"products\")).catch(() => null),\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"public\")).catch(() => null),\n ]);\n for (const snap of [pages, drafts, news, symbols, products]) {\n for (const d of snap?.docs ?? []) blobs.push(JSON.stringify(d.data()));\n }\n if (settings?.exists()) blobs.push(JSON.stringify(settings.data()));\n const corpus = blobs.join(\"\\n\");\n\n const uurGeleden = Date.now() - 60 * 60 * 1000;\n const millis = (v: unknown): number => {\n const t = v as { toMillis?: () => number } | null;\n return typeof t?.toMillis === \"function\" ? t.toMillis() : 0;\n };\n return items\n .filter((it) => {\n if (it.status === \"processing\") return false;\n if (millis(it.createdAt) > uurGeleden) return false;\n if (it.url && corpus.includes(it.url)) return false;\n if (corpus.includes(it.id)) return false;\n return true;\n })\n .map((it) => it.id);\n}\n"],"mappings":";AAEA,SAAS,YAAY,KAAK,QAAQ,eAAe;AACjD,SAAS,aAAa;AAQtB,eAAsB,eACpB,QACA,MACmB;AACnB,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,QAAO,CAAC;AACjB,QAAM,UAAU,CAAC,SAAkB,KAAK,OAAO,KAAK,SAAS,KAAK,GAAG,KAAM,KAAK,SAAS,KAAK,EAAE;AAChG,QAAM,OAAiB,CAAC;AACxB,MAAI;AACF,UAAM,CAAC,OAAO,QAAQ,MAAM,QAAQ,IAAI,MAAM,QAAQ,IAAI;AAAA,MACxD,QAAQ,WAAW,IAAI,SAAS,QAAQ,OAAO,CAAC;AAAA,MAChD,QAAQ,WAAW,IAAI,SAAS,QAAQ,QAAQ,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,MACnE,QAAQ,WAAW,IAAI,SAAS,QAAQ,MAAM,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,MACjE,OAAO,IAAI,IAAI,SAAS,QAAQ,YAAY,QAAQ,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,IACzE,CAAC;AACD,eAAW,KAAK,MAAM,MAAM;AAC1B,YAAM,OAAO,EAAE,KAAK;AACpB,UAAI,QAAQ,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC,CAAC,EAAG,MAAK,KAAK,OAAO,KAAK,QAAQ,KAAK,SAAS,EAAE,EAAE,CAAC;AAAA,IACnG;AACA,eAAW,KAAK,QAAQ,QAAQ,CAAC,GAAG;AAClC,YAAM,OAAO,EAAE,KAAK;AACpB,UAAI,QAAQ,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC,CAAC,EAAG,MAAK,KAAK,GAAG,OAAO,KAAK,QAAQ,EAAE,EAAE,CAAC,YAAY;AAAA,IACpG;AACA,eAAW,KAAK,MAAM,QAAQ,CAAC,GAAG;AAChC,UAAI,QAAQ,KAAK,UAAU,EAAE,KAAK,CAAC,CAAC,EAAG,MAAK,KAAK,WAAW,OAAQ,EAAE,KAAK,EAAyB,SAAS,EAAE,EAAE,CAAC,EAAE;AAAA,IACtH;AACA,QAAI,UAAU,OAAO,KAAK,QAAQ,KAAK,UAAU,SAAS,KAAK,CAAC,CAAC,EAAG,MAAK,KAAK,mBAAmB;AACjG,WAAO,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYA,eAAsB,gBACpB,QACA,OACmB;AACnB,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,QAAO,CAAC;AACjB,QAAM,QAAkB,CAAC;AACzB,QAAM,CAAC,OAAO,QAAQ,MAAM,SAAS,UAAU,QAAQ,IAAI,MAAM,QAAQ,IAAI;AAAA,IAC3E,QAAQ,WAAW,IAAI,SAAS,QAAQ,OAAO,CAAC;AAAA,IAChD,QAAQ,WAAW,IAAI,SAAS,QAAQ,QAAQ,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,IACnE,QAAQ,WAAW,IAAI,SAAS,QAAQ,MAAM,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,IACjE,QAAQ,WAAW,IAAI,SAAS,QAAQ,SAAS,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,IACpE,QAAQ,WAAW,IAAI,SAAS,QAAQ,UAAU,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,IACrE,OAAO,IAAI,IAAI,SAAS,QAAQ,YAAY,QAAQ,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,EACzE,CAAC;AACD,aAAW,QAAQ,CAAC,OAAO,QAAQ,MAAM,SAAS,QAAQ,GAAG;AAC3D,eAAW,KAAK,MAAM,QAAQ,CAAC,EAAG,OAAM,KAAK,KAAK,UAAU,EAAE,KAAK,CAAC,CAAC;AAAA,EACvE;AACA,MAAI,UAAU,OAAO,EAAG,OAAM,KAAK,KAAK,UAAU,SAAS,KAAK,CAAC,CAAC;AAClE,QAAM,SAAS,MAAM,KAAK,IAAI;AAE9B,QAAM,aAAa,KAAK,IAAI,IAAI,KAAK,KAAK;AAC1C,QAAM,SAAS,CAAC,MAAuB;AACrC,UAAM,IAAI;AACV,WAAO,OAAO,GAAG,aAAa,aAAa,EAAE,SAAS,IAAI;AAAA,EAC5D;AACA,SAAO,MACJ,OAAO,CAAC,OAAO;AACd,QAAI,GAAG,WAAW,aAAc,QAAO;AACvC,QAAI,OAAO,GAAG,SAAS,IAAI,WAAY,QAAO;AAC9C,QAAI,GAAG,OAAO,OAAO,SAAS,GAAG,GAAG,EAAG,QAAO;AAC9C,QAAI,OAAO,SAAS,GAAG,EAAE,EAAG,QAAO;AACnC,WAAO;AAAA,EACT,CAAC,EACA,IAAI,CAAC,OAAO,GAAG,EAAE;AACtB;","names":[]}
|
|
@@ -17,7 +17,7 @@ export interface SchemaField {
|
|
|
17
17
|
* (array/object/record) worden als `json` teruggegeven zodat een fallback-editor ze tóch kan
|
|
18
18
|
* aanbieden; een aanroeper die alleen simpele velden wil, filtert op `kind !== "json"`.
|
|
19
19
|
*/
|
|
20
|
-
export declare function introspectObjectSchema(schema: ZodTypeAny): SchemaField[];
|
|
20
|
+
export declare function introspectObjectSchema(schema: ZodTypeAny | undefined): SchemaField[];
|
|
21
21
|
export type { FieldHints } from "../../schema/fields.js";
|
|
22
22
|
import type { FieldHints } from "../../schema/fields.js";
|
|
23
23
|
/**
|
|
@@ -28,4 +28,4 @@ import type { FieldHints } from "../../schema/fields.js";
|
|
|
28
28
|
* - al het overige (records, scalar-arrays, unions) → **json** (laatste redmiddel).
|
|
29
29
|
* `hints` verfijnt per (dotted) sleutel: type-overrides (date/slider/…) en `showIf`.
|
|
30
30
|
*/
|
|
31
|
-
export declare function introspectToFields(schema: ZodTypeAny, hints?: FieldHints, prefix?: string): Field[];
|
|
31
|
+
export declare function introspectToFields(schema: ZodTypeAny | undefined, hints?: FieldHints, prefix?: string): Field[];
|
|
@@ -29,6 +29,7 @@ function unwrap(schema) {
|
|
|
29
29
|
return { def: defOf(s), description, inner: s };
|
|
30
30
|
}
|
|
31
31
|
function introspectObjectSchema(schema) {
|
|
32
|
+
if (!schema) return [];
|
|
32
33
|
const shape = shapeOf(schema);
|
|
33
34
|
if (!shape) return [];
|
|
34
35
|
const out = [];
|
|
@@ -65,6 +66,7 @@ function introspectObjectSchema(schema) {
|
|
|
65
66
|
const IMAGE_KEY = /image|photo|foto|logo|cover|avatar|poster/i;
|
|
66
67
|
const URL_KEY = /^(href|url|link)$|Url$|Href$/;
|
|
67
68
|
function introspectToFields(schema, hints, prefix = "") {
|
|
69
|
+
if (!schema) return [];
|
|
68
70
|
const shape = shapeOf(schema);
|
|
69
71
|
if (!shape) return [];
|
|
70
72
|
const out = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/lib/zodFields.ts"],"sourcesContent":["/**\n * Zod-introspectie voor automatisch gegenereerde formulieren. Gedeeld door:\n * - de `GenericEditor` (fallback-editor voor blokken zonder eigen `Editor`), en\n * - het `ModuleSettingsForm` (instellingen van een module in /beheer).\n *\n * Twee lagen:\n * - `introspectObjectSchema` (legacy, plat): simpele kinds — blijft bestaan voor module-settings.\n * - `introspectToFields` (5.0): levert de rijke **Field-IR** (schema/fields.ts, primo-port) met\n * échte repeaters/groepen (recursief), richtext-/image-/link-detectie en optionele per-block\n * hints (`defineBlock({ fields })`). De GenericEditor 2.0 rendert deze IR.\n *\n * Een veld krijgt zijn label uit `.describe(\"…\")` op het schema; zonder beschrijving valt het\n * terug op de veldsleutel. Puur (geen React) — apart testbaar.\n */\nimport { globalRegistry } from \"zod/mini\";\nimport type { $ZodType as ZodTypeAny } from \"zod/v4/core\";\nimport type { Field } from \"../../schema/fields\";\n\nexport interface SchemaField {\n key: string;\n kind: \"number\" | \"boolean\" | \"string\" | \"enum\" | \"json\";\n /** Leesbaar label: `.describe()` van het schema, anders de veldsleutel. */\n label: string;\n options?: string[];\n /** Max-lengte van een string (bepaalt Input vs Textarea in de UI). */\n maxLength?: number;\n /** Het volledige veld-schema — voor validatie van json-velden vóór commit. */\n schema: ZodTypeAny;\n}\n\n/**\n * Zod 4-interne vorm. In Zod 3 heette dit `_def.typeName` (\"ZodObject\") met `shape` als FUNCTIE en\n * de beschrijving op `_def.description`; sinds Zod 4 is het `_def.type` (\"object\") met `shape` als\n * gewoon object, staat de beschrijving op de schema-instantie, dragen enums hun waarden in\n * `entries`, en zit een string-check achter `checks[i]._zod.def` als\n * `{ check: \"max_length\", maximum: n }`.\n *\n * Dit blijft leunen op interne velden — er is geen publieke introspectie-API in Zod. De\n * Field-IR-tests (fields.test.ts) zijn daarom het vangnet: die vallen om zodra Zod zijn interne\n * vorm opnieuw wijzigt, in plaats van dat de GenericEditor stilletjes lege formulieren toont.\n */\ntype ZodKind =\n | \"object\" | \"string\" | \"number\" | \"boolean\" | \"enum\" | \"array\" | \"record\"\n | \"default\" | \"optional\" | \"nullable\" | \"literal\" | (string & {});\n\ninterface AnyDef {\n type?: ZodKind;\n innerType?: unknown;\n entries?: Record<string, string>;\n element?: unknown;\n checks?: { _zod?: { def?: { check?: string; maximum?: number } } }[];\n shape?: Record<string, unknown>;\n}\n\n/**\n * De interne definitie van een schema.\n *\n * LET OP: `_def` is een alias die alléén de classic-façade aanbiedt — `zod/mini` (waar core op\n * draait) heeft hem niet en zet alles op `_zod.def`. Lees dus `_zod.def` eerst, met `_def` als\n * terugval voor classic-schema's uit een klantrepo. Zonder die volgorde levert introspectie\n * stilletjes niets op en toont de GenericEditor lege formulieren.\n */\nconst defOf = (schema: unknown): AnyDef | undefined =>\n (schema as { _zod?: { def?: AnyDef } })?._zod?.def ?? (schema as { _def?: AnyDef })?._def;\n\n/** Max-lengte van een string-schema (Zod 4: checks dragen hun regel onder `_zod.def`). */\nfunction maxLengthOf(def: AnyDef | undefined): number | undefined {\n for (const c of def?.checks ?? []) {\n const rule = c?._zod?.def;\n if (rule?.check === \"max_length\") return rule.maximum;\n }\n return undefined;\n}\n\n/** Waarden van een enum (Zod 4 levert een entries-map i.p.v. een values-array). */\nconst valuesOf = (def: AnyDef | undefined): string[] => Object.values(def?.entries ?? {});\n\n/** De velden van een objectschema; leeg als het geen object is. */\nfunction shapeOf(schema: unknown): Record<string, unknown> | null {\n const def = defOf(schema);\n if (def?.type !== \"object\" || !def.shape || typeof def.shape !== \"object\") return null;\n return def.shape;\n}\n\n/**\n * De beschrijving van een schema — de bron van de veldlabels in /beheer.\n *\n * `zod/mini` heeft géén `.description`-getter (die hoort bij de classic-façade), maar `.describe()`\n * en `.register()` schrijven allebei naar dezelfde `globalRegistry`. Die lezen we hier dus direct,\n * met de getter als terugval zodat een classic-schema uit een klantrepo blijft werken.\n */\nfunction describedAs(s: unknown): string | undefined {\n const uitRegister = globalRegistry.get(s as never)?.description;\n return uitRegister ?? (s as { description?: string } | null)?.description;\n}\n\n/** Pakt het binnenste type uit default/optional/nullable; bewaart de buitenste beschrijving. */\nfunction unwrap(schema: unknown): { def?: AnyDef; description?: string; inner?: unknown } {\n let s: unknown = schema;\n let description = describedAs(s);\n for (;;) {\n const def = defOf(s);\n if (def?.type !== \"default\" && def?.type !== \"optional\" && def?.type !== \"nullable\") break;\n s = def.innerType;\n description = description ?? describedAs(s);\n }\n return { def: defOf(s), description, inner: s };\n}\n\n/**\n * Leidt een formulier-beschrijving af uit een Zod-objectschema. Interne velden (`_key`/`_type`/\n * `_style`) worden overgeslagen — die beheren de editor-chrome en de StyleEditor. Complexe velden\n * (array/object/record) worden als `json` teruggegeven zodat een fallback-editor ze tóch kan\n * aanbieden; een aanroeper die alleen simpele velden wil, filtert op `kind !== \"json\"`.\n */\nexport function introspectObjectSchema(schema: ZodTypeAny): SchemaField[] {\n const shape = shapeOf(schema);\n if (!shape) return [];\n const out: SchemaField[] = [];\n for (const [key, field] of Object.entries(shape)) {\n if (key.startsWith(\"_\")) continue;\n const { def: inner, description } = unwrap(field);\n const label = description ?? key;\n const fieldSchema = field as ZodTypeAny;\n switch (inner?.type) {\n case \"number\":\n out.push({ key, kind: \"number\", label, schema: fieldSchema });\n break;\n case \"boolean\":\n out.push({ key, kind: \"boolean\", label, schema: fieldSchema });\n break;\n case \"enum\":\n out.push({ key, kind: \"enum\", label, options: valuesOf(inner), schema: fieldSchema });\n break;\n case \"string\": {\n out.push({ key, kind: \"string\", label, maxLength: maxLengthOf(inner), schema: fieldSchema });\n break;\n }\n case \"array\":\n case \"object\":\n case \"record\":\n out.push({ key, kind: \"json\", label, schema: fieldSchema });\n break;\n default:\n // ZodLiteral (bv. _type), unions e.d.: geen zinvol generiek veld.\n break;\n }\n }\n return out;\n}\n\nexport type { FieldHints } from \"../../schema/fields\";\nimport type { FieldHints } from \"../../schema/fields\";\n\n/** Herkent een beeld-URL-veld aan de sleutelnaam (zelfde heuristiek als de oude GenericEditor). */\nconst IMAGE_KEY = /image|photo|foto|logo|cover|avatar|poster/i;\n/** Herkent een URL-veld aan de sleutelnaam. */\nconst URL_KEY = /^(href|url|link)$|Url$|Href$/;\n\n/**\n * Leidt de rijke Field-IR af uit een Zod-objectschema (5.0). Verschillen met de platte laag:\n * - `z.array(z.object)` → **repeater** met recursief geïntrospecteerde subvelden;\n * - genest `z.object` → **group** (of **link** als de vorm exact `{label, href}` is);\n * - stringvelden `html`/`*Html` → **richtext**, beeld-sleutels → **image**, URL-sleutels → **url**;\n * - al het overige (records, scalar-arrays, unions) → **json** (laatste redmiddel).\n * `hints` verfijnt per (dotted) sleutel: type-overrides (date/slider/…) en `showIf`.\n */\nexport function introspectToFields(schema: ZodTypeAny, hints?: FieldHints, prefix = \"\"): Field[] {\n const shape = shapeOf(schema);\n if (!shape) return [];\n const out: Field[] = [];\n for (const [key, raw] of Object.entries(shape)) {\n if (key.startsWith(\"_\")) continue;\n const { def: inner, description, inner: innerSchema } = unwrap(raw);\n const label = description ?? key;\n const path = prefix ? `${prefix}.${key}` : key;\n const hint = hints?.[path];\n const push = (f: Field) => out.push({ ...f, ...(hint as Partial<Field>), key, label: (hint?.label as string) ?? label } as Field);\n switch (inner?.type) {\n case \"number\":\n push({ key, label, type: \"number\" });\n break;\n case \"boolean\":\n push({ key, label, type: \"switch\" });\n break;\n case \"enum\":\n push({ key, label, type: \"select\", options: valuesOf(inner).map((v) => ({ value: v, label: v })) });\n break;\n case \"string\": {\n const max = maxLengthOf(inner);\n if (key === \"html\" || key.endsWith(\"Html\")) push({ key, label, type: \"richtext\" });\n else if (IMAGE_KEY.test(key)) push({ key, label, type: \"image\" });\n else if (URL_KEY.test(key)) push({ key, label, type: \"url\" });\n else if ((max ?? 0) > 500) push({ key, label, type: \"textarea\", maxLength: max });\n else push({ key, label, type: \"text\", maxLength: max });\n break;\n }\n case \"object\": {\n const sub = introspectToFields(innerSchema as ZodTypeAny, hints, path);\n const isLink = sub.length === 2 && sub.some((f) => f.key === \"label\") && sub.some((f) => f.key === \"href\");\n if (isLink) push({ key, label, type: \"link\" });\n else if (sub.length > 0) push({ key, label, type: \"group\", fields: sub });\n else push({ key, label, type: \"json\" });\n break;\n }\n case \"array\": {\n const { def: elDef, inner: elSchema } = unwrap(inner?.element);\n if (elDef?.type === \"object\") {\n const sub = introspectToFields(elSchema as ZodTypeAny, hints, path);\n if (sub.length > 0) {\n push({ key, label, type: \"repeater\", fields: sub });\n break;\n }\n }\n push({ key, label, type: \"json\" });\n break;\n }\n case \"record\":\n push({ key, label, type: \"json\" });\n break;\n default:\n break;\n }\n }\n return out;\n}\n"],"mappings":"AAcA,SAAS,sBAAsB;AAgD/B,MAAM,QAAQ,CAAC,WACZ,QAAwC,MAAM,OAAQ,QAA8B;AAGvF,SAAS,YAAY,KAA6C;AAChE,aAAW,KAAK,KAAK,UAAU,CAAC,GAAG;AACjC,UAAM,OAAO,GAAG,MAAM;AACtB,QAAI,MAAM,UAAU,aAAc,QAAO,KAAK;AAAA,EAChD;AACA,SAAO;AACT;AAGA,MAAM,WAAW,CAAC,QAAsC,OAAO,OAAO,KAAK,WAAW,CAAC,CAAC;AAGxF,SAAS,QAAQ,QAAiD;AAChE,QAAM,MAAM,MAAM,MAAM;AACxB,MAAI,KAAK,SAAS,YAAY,CAAC,IAAI,SAAS,OAAO,IAAI,UAAU,SAAU,QAAO;AAClF,SAAO,IAAI;AACb;AASA,SAAS,YAAY,GAAgC;AACnD,QAAM,cAAc,eAAe,IAAI,CAAU,GAAG;AACpD,SAAO,eAAgB,GAAuC;AAChE;AAGA,SAAS,OAAO,QAA0E;AACxF,MAAI,IAAa;AACjB,MAAI,cAAc,YAAY,CAAC;AAC/B,aAAS;AACP,UAAM,MAAM,MAAM,CAAC;AACnB,QAAI,KAAK,SAAS,aAAa,KAAK,SAAS,cAAc,KAAK,SAAS,WAAY;AACrF,QAAI,IAAI;AACR,kBAAc,eAAe,YAAY,CAAC;AAAA,EAC5C;AACA,SAAO,EAAE,KAAK,MAAM,CAAC,GAAG,aAAa,OAAO,EAAE;AAChD;AAQO,SAAS,uBAAuB,QAAmC;AACxE,QAAM,QAAQ,QAAQ,MAAM;AAC5B,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,QAAM,MAAqB,CAAC;AAC5B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,IAAI,WAAW,GAAG,EAAG;AACzB,UAAM,EAAE,KAAK,OAAO,YAAY,IAAI,OAAO,KAAK;AAChD,UAAM,QAAQ,eAAe;AAC7B,UAAM,cAAc;AACpB,YAAQ,OAAO,MAAM;AAAA,MACnB,KAAK;AACH,YAAI,KAAK,EAAE,KAAK,MAAM,UAAU,OAAO,QAAQ,YAAY,CAAC;AAC5D;AAAA,MACF,KAAK;AACH,YAAI,KAAK,EAAE,KAAK,MAAM,WAAW,OAAO,QAAQ,YAAY,CAAC;AAC7D;AAAA,MACF,KAAK;AACH,YAAI,KAAK,EAAE,KAAK,MAAM,QAAQ,OAAO,SAAS,SAAS,KAAK,GAAG,QAAQ,YAAY,CAAC;AACpF;AAAA,MACF,KAAK,UAAU;AACb,YAAI,KAAK,EAAE,KAAK,MAAM,UAAU,OAAO,WAAW,YAAY,KAAK,GAAG,QAAQ,YAAY,CAAC;AAC3F;AAAA,MACF;AAAA,MACA,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,YAAI,KAAK,EAAE,KAAK,MAAM,QAAQ,OAAO,QAAQ,YAAY,CAAC;AAC1D;AAAA,MACF;AAEE;AAAA,IACJ;AAAA,EACF;AACA,SAAO;AACT;AAMA,MAAM,YAAY;AAElB,MAAM,UAAU;AAUT,SAAS,mBAAmB,QAAoB,OAAoB,SAAS,IAAa;AAC/F,QAAM,QAAQ,QAAQ,MAAM;AAC5B,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,QAAM,MAAe,CAAC;AACtB,aAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC9C,QAAI,IAAI,WAAW,GAAG,EAAG;AACzB,UAAM,EAAE,KAAK,OAAO,aAAa,OAAO,YAAY,IAAI,OAAO,GAAG;AAClE,UAAM,QAAQ,eAAe;AAC7B,UAAM,OAAO,SAAS,GAAG,MAAM,IAAI,GAAG,KAAK;AAC3C,UAAM,OAAO,QAAQ,IAAI;AACzB,UAAM,OAAO,CAAC,MAAa,IAAI,KAAK,EAAE,GAAG,GAAG,GAAI,MAAyB,KAAK,OAAQ,MAAM,SAAoB,MAAM,CAAU;AAChI,YAAQ,OAAO,MAAM;AAAA,MACnB,KAAK;AACH,aAAK,EAAE,KAAK,OAAO,MAAM,SAAS,CAAC;AACnC;AAAA,MACF,KAAK;AACH,aAAK,EAAE,KAAK,OAAO,MAAM,SAAS,CAAC;AACnC;AAAA,MACF,KAAK;AACH,aAAK,EAAE,KAAK,OAAO,MAAM,UAAU,SAAS,SAAS,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,EAAE,EAAE,CAAC;AAClG;AAAA,MACF,KAAK,UAAU;AACb,cAAM,MAAM,YAAY,KAAK;AAC7B,YAAI,QAAQ,UAAU,IAAI,SAAS,MAAM,EAAG,MAAK,EAAE,KAAK,OAAO,MAAM,WAAW,CAAC;AAAA,iBACxE,UAAU,KAAK,GAAG,EAAG,MAAK,EAAE,KAAK,OAAO,MAAM,QAAQ,CAAC;AAAA,iBACvD,QAAQ,KAAK,GAAG,EAAG,MAAK,EAAE,KAAK,OAAO,MAAM,MAAM,CAAC;AAAA,kBAClD,OAAO,KAAK,IAAK,MAAK,EAAE,KAAK,OAAO,MAAM,YAAY,WAAW,IAAI,CAAC;AAAA,YAC3E,MAAK,EAAE,KAAK,OAAO,MAAM,QAAQ,WAAW,IAAI,CAAC;AACtD;AAAA,MACF;AAAA,MACA,KAAK,UAAU;AACb,cAAM,MAAM,mBAAmB,aAA2B,OAAO,IAAI;AACrE,cAAM,SAAS,IAAI,WAAW,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,QAAQ,OAAO,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,QAAQ,MAAM;AACzG,YAAI,OAAQ,MAAK,EAAE,KAAK,OAAO,MAAM,OAAO,CAAC;AAAA,iBACpC,IAAI,SAAS,EAAG,MAAK,EAAE,KAAK,OAAO,MAAM,SAAS,QAAQ,IAAI,CAAC;AAAA,YACnE,MAAK,EAAE,KAAK,OAAO,MAAM,OAAO,CAAC;AACtC;AAAA,MACF;AAAA,MACA,KAAK,SAAS;AACZ,cAAM,EAAE,KAAK,OAAO,OAAO,SAAS,IAAI,OAAO,OAAO,OAAO;AAC7D,YAAI,OAAO,SAAS,UAAU;AAC5B,gBAAM,MAAM,mBAAmB,UAAwB,OAAO,IAAI;AAClE,cAAI,IAAI,SAAS,GAAG;AAClB,iBAAK,EAAE,KAAK,OAAO,MAAM,YAAY,QAAQ,IAAI,CAAC;AAClD;AAAA,UACF;AAAA,QACF;AACA,aAAK,EAAE,KAAK,OAAO,MAAM,OAAO,CAAC;AACjC;AAAA,MACF;AAAA,MACA,KAAK;AACH,aAAK,EAAE,KAAK,OAAO,MAAM,OAAO,CAAC;AACjC;AAAA,MACF;AACE;AAAA,IACJ;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/lib/zodFields.ts"],"sourcesContent":["/**\n * Zod-introspectie voor automatisch gegenereerde formulieren. Gedeeld door:\n * - de `GenericEditor` (fallback-editor voor blokken zonder eigen `Editor`), en\n * - het `ModuleSettingsForm` (instellingen van een module in /beheer).\n *\n * Twee lagen:\n * - `introspectObjectSchema` (legacy, plat): simpele kinds — blijft bestaan voor module-settings.\n * - `introspectToFields` (5.0): levert de rijke **Field-IR** (schema/fields.ts, primo-port) met\n * échte repeaters/groepen (recursief), richtext-/image-/link-detectie en optionele per-block\n * hints (`defineBlock({ fields })`). De GenericEditor 2.0 rendert deze IR.\n *\n * Een veld krijgt zijn label uit `.describe(\"…\")` op het schema; zonder beschrijving valt het\n * terug op de veldsleutel. Puur (geen React) — apart testbaar.\n */\nimport { globalRegistry } from \"zod/mini\";\nimport type { $ZodType as ZodTypeAny } from \"zod/v4/core\";\nimport type { Field } from \"../../schema/fields\";\n\nexport interface SchemaField {\n key: string;\n kind: \"number\" | \"boolean\" | \"string\" | \"enum\" | \"json\";\n /** Leesbaar label: `.describe()` van het schema, anders de veldsleutel. */\n label: string;\n options?: string[];\n /** Max-lengte van een string (bepaalt Input vs Textarea in de UI). */\n maxLength?: number;\n /** Het volledige veld-schema — voor validatie van json-velden vóór commit. */\n schema: ZodTypeAny;\n}\n\n/**\n * Zod 4-interne vorm. In Zod 3 heette dit `_def.typeName` (\"ZodObject\") met `shape` als FUNCTIE en\n * de beschrijving op `_def.description`; sinds Zod 4 is het `_def.type` (\"object\") met `shape` als\n * gewoon object, staat de beschrijving op de schema-instantie, dragen enums hun waarden in\n * `entries`, en zit een string-check achter `checks[i]._zod.def` als\n * `{ check: \"max_length\", maximum: n }`.\n *\n * Dit blijft leunen op interne velden — er is geen publieke introspectie-API in Zod. De\n * Field-IR-tests (fields.test.ts) zijn daarom het vangnet: die vallen om zodra Zod zijn interne\n * vorm opnieuw wijzigt, in plaats van dat de GenericEditor stilletjes lege formulieren toont.\n */\ntype ZodKind =\n | \"object\" | \"string\" | \"number\" | \"boolean\" | \"enum\" | \"array\" | \"record\"\n | \"default\" | \"optional\" | \"nullable\" | \"literal\" | (string & {});\n\ninterface AnyDef {\n type?: ZodKind;\n innerType?: unknown;\n entries?: Record<string, string>;\n element?: unknown;\n checks?: { _zod?: { def?: { check?: string; maximum?: number } } }[];\n shape?: Record<string, unknown>;\n}\n\n/**\n * De interne definitie van een schema.\n *\n * LET OP: `_def` is een alias die alléén de classic-façade aanbiedt — `zod/mini` (waar core op\n * draait) heeft hem niet en zet alles op `_zod.def`. Lees dus `_zod.def` eerst, met `_def` als\n * terugval voor classic-schema's uit een klantrepo. Zonder die volgorde levert introspectie\n * stilletjes niets op en toont de GenericEditor lege formulieren.\n */\nconst defOf = (schema: unknown): AnyDef | undefined =>\n (schema as { _zod?: { def?: AnyDef } })?._zod?.def ?? (schema as { _def?: AnyDef })?._def;\n\n/** Max-lengte van een string-schema (Zod 4: checks dragen hun regel onder `_zod.def`). */\nfunction maxLengthOf(def: AnyDef | undefined): number | undefined {\n for (const c of def?.checks ?? []) {\n const rule = c?._zod?.def;\n if (rule?.check === \"max_length\") return rule.maximum;\n }\n return undefined;\n}\n\n/** Waarden van een enum (Zod 4 levert een entries-map i.p.v. een values-array). */\nconst valuesOf = (def: AnyDef | undefined): string[] => Object.values(def?.entries ?? {});\n\n/** De velden van een objectschema; leeg als het geen object is. */\nfunction shapeOf(schema: unknown): Record<string, unknown> | null {\n const def = defOf(schema);\n if (def?.type !== \"object\" || !def.shape || typeof def.shape !== \"object\") return null;\n return def.shape;\n}\n\n/**\n * De beschrijving van een schema — de bron van de veldlabels in /beheer.\n *\n * `zod/mini` heeft géén `.description`-getter (die hoort bij de classic-façade), maar `.describe()`\n * en `.register()` schrijven allebei naar dezelfde `globalRegistry`. Die lezen we hier dus direct,\n * met de getter als terugval zodat een classic-schema uit een klantrepo blijft werken.\n */\nfunction describedAs(s: unknown): string | undefined {\n const uitRegister = globalRegistry.get(s as never)?.description;\n return uitRegister ?? (s as { description?: string } | null)?.description;\n}\n\n/** Pakt het binnenste type uit default/optional/nullable; bewaart de buitenste beschrijving. */\nfunction unwrap(schema: unknown): { def?: AnyDef; description?: string; inner?: unknown } {\n let s: unknown = schema;\n let description = describedAs(s);\n for (;;) {\n const def = defOf(s);\n if (def?.type !== \"default\" && def?.type !== \"optional\" && def?.type !== \"nullable\") break;\n s = def.innerType;\n description = description ?? describedAs(s);\n }\n return { def: defOf(s), description, inner: s };\n}\n\n/**\n * Leidt een formulier-beschrijving af uit een Zod-objectschema. Interne velden (`_key`/`_type`/\n * `_style`) worden overgeslagen — die beheren de editor-chrome en de StyleEditor. Complexe velden\n * (array/object/record) worden als `json` teruggegeven zodat een fallback-editor ze tóch kan\n * aanbieden; een aanroeper die alleen simpele velden wil, filtert op `kind !== \"json\"`.\n */\nexport function introspectObjectSchema(schema: ZodTypeAny | undefined): SchemaField[] {\n if (!schema) return [];\n const shape = shapeOf(schema);\n if (!shape) return [];\n const out: SchemaField[] = [];\n for (const [key, field] of Object.entries(shape)) {\n if (key.startsWith(\"_\")) continue;\n const { def: inner, description } = unwrap(field);\n const label = description ?? key;\n const fieldSchema = field as ZodTypeAny;\n switch (inner?.type) {\n case \"number\":\n out.push({ key, kind: \"number\", label, schema: fieldSchema });\n break;\n case \"boolean\":\n out.push({ key, kind: \"boolean\", label, schema: fieldSchema });\n break;\n case \"enum\":\n out.push({ key, kind: \"enum\", label, options: valuesOf(inner), schema: fieldSchema });\n break;\n case \"string\": {\n out.push({ key, kind: \"string\", label, maxLength: maxLengthOf(inner), schema: fieldSchema });\n break;\n }\n case \"array\":\n case \"object\":\n case \"record\":\n out.push({ key, kind: \"json\", label, schema: fieldSchema });\n break;\n default:\n // ZodLiteral (bv. _type), unions e.d.: geen zinvol generiek veld.\n break;\n }\n }\n return out;\n}\n\nexport type { FieldHints } from \"../../schema/fields\";\nimport type { FieldHints } from \"../../schema/fields\";\n\n/** Herkent een beeld-URL-veld aan de sleutelnaam (zelfde heuristiek als de oude GenericEditor). */\nconst IMAGE_KEY = /image|photo|foto|logo|cover|avatar|poster/i;\n/** Herkent een URL-veld aan de sleutelnaam. */\nconst URL_KEY = /^(href|url|link)$|Url$|Href$/;\n\n/**\n * Leidt de rijke Field-IR af uit een Zod-objectschema (5.0). Verschillen met de platte laag:\n * - `z.array(z.object)` → **repeater** met recursief geïntrospecteerde subvelden;\n * - genest `z.object` → **group** (of **link** als de vorm exact `{label, href}` is);\n * - stringvelden `html`/`*Html` → **richtext**, beeld-sleutels → **image**, URL-sleutels → **url**;\n * - al het overige (records, scalar-arrays, unions) → **json** (laatste redmiddel).\n * `hints` verfijnt per (dotted) sleutel: type-overrides (date/slider/…) en `showIf`.\n */\nexport function introspectToFields(schema: ZodTypeAny | undefined, hints?: FieldHints, prefix = \"\"): Field[] {\n // Lazy schema's (7.13): vóór registerBuiltinSchemas kan een def nog geen schema hebben —\n // dan geen velden i.p.v. een crash (komt in de praktijk alleen in tests/randgevallen voor).\n if (!schema) return [];\n const shape = shapeOf(schema);\n if (!shape) return [];\n const out: Field[] = [];\n for (const [key, raw] of Object.entries(shape)) {\n if (key.startsWith(\"_\")) continue;\n const { def: inner, description, inner: innerSchema } = unwrap(raw);\n const label = description ?? key;\n const path = prefix ? `${prefix}.${key}` : key;\n const hint = hints?.[path];\n const push = (f: Field) => out.push({ ...f, ...(hint as Partial<Field>), key, label: (hint?.label as string) ?? label } as Field);\n switch (inner?.type) {\n case \"number\":\n push({ key, label, type: \"number\" });\n break;\n case \"boolean\":\n push({ key, label, type: \"switch\" });\n break;\n case \"enum\":\n push({ key, label, type: \"select\", options: valuesOf(inner).map((v) => ({ value: v, label: v })) });\n break;\n case \"string\": {\n const max = maxLengthOf(inner);\n if (key === \"html\" || key.endsWith(\"Html\")) push({ key, label, type: \"richtext\" });\n else if (IMAGE_KEY.test(key)) push({ key, label, type: \"image\" });\n else if (URL_KEY.test(key)) push({ key, label, type: \"url\" });\n else if ((max ?? 0) > 500) push({ key, label, type: \"textarea\", maxLength: max });\n else push({ key, label, type: \"text\", maxLength: max });\n break;\n }\n case \"object\": {\n const sub = introspectToFields(innerSchema as ZodTypeAny, hints, path);\n const isLink = sub.length === 2 && sub.some((f) => f.key === \"label\") && sub.some((f) => f.key === \"href\");\n if (isLink) push({ key, label, type: \"link\" });\n else if (sub.length > 0) push({ key, label, type: \"group\", fields: sub });\n else push({ key, label, type: \"json\" });\n break;\n }\n case \"array\": {\n const { def: elDef, inner: elSchema } = unwrap(inner?.element);\n if (elDef?.type === \"object\") {\n const sub = introspectToFields(elSchema as ZodTypeAny, hints, path);\n if (sub.length > 0) {\n push({ key, label, type: \"repeater\", fields: sub });\n break;\n }\n }\n push({ key, label, type: \"json\" });\n break;\n }\n case \"record\":\n push({ key, label, type: \"json\" });\n break;\n default:\n break;\n }\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import {
|
|
5
5
|
collection,
|
|
@@ -18,13 +18,15 @@ import { Button, Card, Badge, Input } from "../ui/primitives.js";
|
|
|
18
18
|
import { useToast } from "../ui/toast.js";
|
|
19
19
|
import { useConfirm } from "../ui/ConfirmDialog.js";
|
|
20
20
|
import { uploadImage } from "../lib/upload.js";
|
|
21
|
-
import { findMediaUsage } from "../lib/mediaUsage.js";
|
|
21
|
+
import { findMediaUsage, findUnusedMedia } from "../lib/mediaUsage.js";
|
|
22
22
|
const PAGE_SIZE = 48;
|
|
23
23
|
function MediaManager({ siteId }) {
|
|
24
24
|
const { toast } = useToast();
|
|
25
25
|
const confirm = useConfirm();
|
|
26
26
|
const [items, setItems] = useState([]);
|
|
27
27
|
const [busy, setBusy] = useState(false);
|
|
28
|
+
const [ongebruikt, setOngebruikt] = useState(null);
|
|
29
|
+
const [scant, setScant] = useState(false);
|
|
28
30
|
const [pageCount, setPageCount] = useState(1);
|
|
29
31
|
const [hasMore, setHasMore] = useState(false);
|
|
30
32
|
const inputRef = useRef(null);
|
|
@@ -89,28 +91,83 @@ function MediaManager({ siteId }) {
|
|
|
89
91
|
});
|
|
90
92
|
if (!ok) return;
|
|
91
93
|
try {
|
|
92
|
-
await
|
|
93
|
-
if (bucket) {
|
|
94
|
-
const paths = [
|
|
95
|
-
item.path,
|
|
96
|
-
item.variants?.webp?.path,
|
|
97
|
-
...(item.variants?.sizes ?? []).map((v) => v.path)
|
|
98
|
-
].filter((p) => Boolean(p));
|
|
99
|
-
await Promise.all(
|
|
100
|
-
paths.map((p) => deleteObject(storageRef(bucket, p)).catch(
|
|
101
|
-
() => void 0
|
|
102
|
-
/* al weg */
|
|
103
|
-
))
|
|
104
|
-
);
|
|
105
|
-
}
|
|
94
|
+
await verwijderItem(item);
|
|
106
95
|
toast({ title: "Verwijderd" });
|
|
107
96
|
} catch (e) {
|
|
108
97
|
toast({ title: "Verwijderen mislukt", description: e.message, tone: "error" });
|
|
109
98
|
}
|
|
110
99
|
}
|
|
100
|
+
async function verwijderItem(item) {
|
|
101
|
+
const db = getDb();
|
|
102
|
+
const bucket = getBucket();
|
|
103
|
+
if (!db) throw new Error("Geen databaseverbinding.");
|
|
104
|
+
await deleteDoc(doc(db, "sites", siteId, "media", item.id));
|
|
105
|
+
if (bucket) {
|
|
106
|
+
const paths = [
|
|
107
|
+
item.path,
|
|
108
|
+
item.variants?.webp?.path,
|
|
109
|
+
...(item.variants?.sizes ?? []).map((v) => v.path)
|
|
110
|
+
].filter((p) => Boolean(p));
|
|
111
|
+
await Promise.all(
|
|
112
|
+
paths.map((p) => deleteObject(storageRef(bucket, p)).catch(
|
|
113
|
+
() => void 0
|
|
114
|
+
/* al weg */
|
|
115
|
+
))
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function scanOngebruikt() {
|
|
120
|
+
setScant(true);
|
|
121
|
+
try {
|
|
122
|
+
const ids = await findUnusedMedia(siteId, items);
|
|
123
|
+
setOngebruikt(ids);
|
|
124
|
+
if (ids.length === 0) toast({ title: "Niets op te ruimen", description: "Elke afbeelding in de bibliotheek wordt ergens gebruikt.", tone: "success" });
|
|
125
|
+
} catch (e) {
|
|
126
|
+
toast({ title: "Scan mislukt", description: e.message, tone: "error" });
|
|
127
|
+
} finally {
|
|
128
|
+
setScant(false);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function ruimOngebruiktOp() {
|
|
132
|
+
const doelen = items.filter((it) => ongebruikt?.includes(it.id));
|
|
133
|
+
if (doelen.length === 0) return;
|
|
134
|
+
const mb = (doelen.reduce((som, it) => som + (it.size ?? 0), 0) / (1024 * 1024)).toFixed(1);
|
|
135
|
+
const ok = await confirm({
|
|
136
|
+
title: `${doelen.length} ongebruikte afbeelding(en) verwijderen?`,
|
|
137
|
+
description: `Samen \xB1${mb} MB, inclusief alle automatisch gemaakte varianten. Dit kan niet ongedaan worden gemaakt.`,
|
|
138
|
+
confirmLabel: "Opruimen",
|
|
139
|
+
tone: "danger"
|
|
140
|
+
});
|
|
141
|
+
if (!ok) return;
|
|
142
|
+
setBusy(true);
|
|
143
|
+
let gelukt = 0;
|
|
144
|
+
const fouten = [];
|
|
145
|
+
for (const it of doelen) {
|
|
146
|
+
try {
|
|
147
|
+
await verwijderItem(it);
|
|
148
|
+
gelukt++;
|
|
149
|
+
} catch (e) {
|
|
150
|
+
fouten.push(`${it.name}: ${e.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
setOngebruikt(null);
|
|
154
|
+
setBusy(false);
|
|
155
|
+
if (gelukt) toast({ title: `${gelukt} afbeelding(en) opgeruimd`, description: `\xB1${mb} MB opslag vrijgemaakt.`, tone: "success" });
|
|
156
|
+
if (fouten.length) toast({ title: "Niet alles kon weg", description: fouten.join(" \xB7 "), tone: "error" });
|
|
157
|
+
}
|
|
111
158
|
return /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
|
|
112
159
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
113
160
|
/* @__PURE__ */ jsx(Button, { disabled: busy, onClick: () => inputRef.current?.click(), children: busy ? "Uploaden\u2026" : "Foto's uploaden" }),
|
|
161
|
+
" ",
|
|
162
|
+
!isDemoActive() && items.length > 0 && (ongebruikt === null ? /* @__PURE__ */ jsx(Button, { variant: "ghost", disabled: scant || busy, onClick: () => void scanOngebruikt(), children: scant ? "Zoeken\u2026" : "\u{1F9F9} Zoek ongebruikte foto's" }) : ongebruikt.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
163
|
+
/* @__PURE__ */ jsxs(Button, { variant: "secondary", disabled: busy, onClick: () => void ruimOngebruiktOp(), children: [
|
|
164
|
+
"\u{1F9F9} ",
|
|
165
|
+
ongebruikt.length,
|
|
166
|
+
" ongebruikte verwijderen"
|
|
167
|
+
] }),
|
|
168
|
+
" ",
|
|
169
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: () => setOngebruikt(null), children: "Annuleren" })
|
|
170
|
+
] }) : null),
|
|
114
171
|
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-slate-500", children: "Je kunt meerdere foto's tegelijk kiezen. Ze worden automatisch verkleind en geoptimaliseerd (webp) \u2014 dat houdt je site snel en bespaart opslag. Max. 15 MB per bestand." }),
|
|
115
172
|
/* @__PURE__ */ jsx(
|
|
116
173
|
"input",
|
|
@@ -140,7 +197,7 @@ function MediaManager({ siteId }) {
|
|
|
140
197
|
m.height
|
|
141
198
|
] })
|
|
142
199
|
] }),
|
|
143
|
-
m.status === "processing" ? /* @__PURE__ */ jsx(Badge, { tone: "amber", children: "verwerken" }) : /* @__PURE__ */ jsx(Badge, { tone: "green", children: "klaar" })
|
|
200
|
+
m.status === "processing" ? /* @__PURE__ */ jsx(Badge, { tone: "amber", children: "verwerken" }) : ongebruikt?.includes(m.id) ? /* @__PURE__ */ jsx(Badge, { tone: "amber", children: "ongebruikt" }) : /* @__PURE__ */ jsx(Badge, { tone: "green", children: "klaar" })
|
|
144
201
|
] }),
|
|
145
202
|
/* @__PURE__ */ jsx(AltInput, { item: m, onSave: saveAlt }),
|
|
146
203
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/media/MediaManager.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n collection,\n deleteDoc,\n doc,\n limit,\n onSnapshot,\n orderBy,\n query,\n updateDoc,\n} from \"firebase/firestore\";\nimport { deleteObject, ref as storageRef } from \"firebase/storage\";\nimport { getBucket, getDb } from \"../../firebase/client\";\nimport { DEMO_EVENT, isDemoActive, listDemoMediaAll } from \"../../demo/demoStore\";\nimport type { MediaItem } from \"../../content/types\";\nimport { Button, Card, Badge, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { uploadImage } from \"../lib/upload\";\nimport { findMediaUsage } from \"../lib/mediaUsage\";\n\n/** Paginagrootte van de bibliotheek: begrensd laden i.p.v. álles (kosten + geheugen bij 100+ foto's). */\nconst PAGE_SIZE = 48;\n\nexport function MediaManager({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [items, setItems] = useState<MediaItem[]>([]);\n const [busy, setBusy] = useState(false);\n const [pageCount, setPageCount] = useState(1);\n const [hasMore, setHasMore] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n // Demo-modus: lokale bibliotheek (geseede foto's + eigen \"uploads\") i.p.v. Firestore.\n if (isDemoActive()) {\n const sync = () => setItems(listDemoMediaAll(siteId));\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }\n const db = getDb();\n if (!db) return;\n // limit + \"meer laden\" (pageCount groeit): geen volledige collectie-read per open.\n const max = pageCount * PAGE_SIZE;\n const q = query(collection(db, \"sites\", siteId, \"media\"), orderBy(\"createdAt\", \"desc\"), limit(max + 1));\n return onSnapshot(\n q,\n (snap) => {\n setHasMore(snap.docs.length > max);\n setItems(snap.docs.slice(0, max).map((d) => ({ id: d.id, ...(d.data() as Omit<MediaItem, \"id\">) })));\n },\n // Leesfouten (rules/App Check) niet stil slikken — anders lijkt de bibliotheek leeg.\n (err) => toast({ title: \"Kon mediabibliotheek niet laden\", description: err.message, tone: \"error\" }),\n );\n }, [siteId, toast, pageCount]);\n\n async function onFiles(files: FileList) {\n setBusy(true);\n let ok = 0;\n const errors: string[] = [];\n for (const file of Array.from(files)) {\n try {\n await uploadImage(siteId, file);\n ok++;\n } catch (e) {\n errors.push(`${file.name}: ${(e as Error).message}`);\n }\n }\n if (ok > 0) toast({ title: `${ok} foto('s) geüpload`, description: \"Automatisch verkleind; varianten worden op de achtergrond gemaakt.\", tone: \"success\" });\n if (errors.length) toast({ title: \"Sommige uploads mislukten\", description: errors.join(\" · \"), tone: \"error\" });\n setBusy(false);\n }\n\n async function saveAlt(item: MediaItem, alt: string) {\n const db = getDb();\n if (!db) return;\n if ((item.alt ?? \"\") === alt) return; // niets gewijzigd → geen write/toast\n try {\n await updateDoc(doc(db, \"sites\", siteId, \"media\", item.id), { alt });\n toast({ title: \"Alt-tekst opgeslagen\", tone: \"success\" });\n } catch (e) {\n // Niet stil slikken: de klant dacht anders dat de alt-tekst opgeslagen was.\n toast({ title: \"Alt-tekst opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n async function remove(item: MediaItem) {\n const db = getDb();\n const bucket = getBucket();\n if (!db) return;\n const usedOn = await findMediaUsage(siteId, item);\n const ok = await confirm({\n title: \"Afbeelding verwijderen?\",\n description:\n usedOn.length > 0\n ? `Let op: deze afbeelding wordt gebruikt op ${usedOn.length} gepubliceerde pagina('s) (${usedOn.join(\", \")}). Daar verschijnt dan een gebroken afbeelding.`\n : \"De afbeelding wordt definitief verwijderd.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n try {\n await deleteDoc(doc(db, \"sites\", siteId, \"media\", item.id));\n if (bucket) {\n // Ook de srcset-varianten opruimen die de Cloud Function heeft gemaakt — anders blijven\n // die (publieke) bestanden voor altijd in Storage staan.\n const paths = [\n item.path,\n item.variants?.webp?.path,\n ...(item.variants?.sizes ?? []).map((v) => v.path),\n ].filter((p): p is string => Boolean(p));\n await Promise.all(\n paths.map((p) => deleteObject(storageRef(bucket, p)).catch(() => undefined /* al weg */)),\n );\n }\n toast({ title: \"Verwijderd\" });\n } catch (e) {\n // Rules/offline-fout niet stil slikken — anders lijkt verwijderen \"niets te doen\".\n toast({ title: \"Verwijderen mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n return (\n <div className=\"grid gap-4\">\n <div>\n <Button disabled={busy} onClick={() => inputRef.current?.click()}>\n {busy ? \"Uploaden…\" : \"Foto's uploaden\"}\n </Button>\n <p className=\"mt-1 text-xs text-slate-500\">\n Je kunt meerdere foto's tegelijk kiezen. Ze worden automatisch verkleind en geoptimaliseerd\n (webp) — dat houdt je site snel en bespaart opslag. Max. 15 MB per bestand.\n </p>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n hidden\n onChange={(e) => {\n if (e.target.files?.length) void onFiles(e.target.files);\n e.target.value = \"\";\n }}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4\">\n {items.map((m) => (\n <Card key={m.id} className=\"p-3\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={m.url} alt={m.alt ?? m.name} className=\"mb-2 h-28 w-full rounded-md object-cover\" />\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <span className=\"truncate text-xs text-slate-500\">\n {m.name}\n {(m as { width?: number; height?: number }).width && (\n <span className=\"text-slate-400\"> · {(m as { width?: number }).width}×{(m as { height?: number }).height}</span>\n )}\n </span>\n {m.status === \"processing\" ? <Badge tone=\"amber\">verwerken</Badge> : <Badge tone=\"green\">klaar</Badge>}\n </div>\n <AltInput item={m} onSave={saveAlt} />\n <div className=\"flex gap-1\">\n <Button variant=\"ghost\" size=\"sm\" className=\"text-xs\" onClick={() => void navigator.clipboard?.writeText(m.url)}>\n Kopieer URL\n </Button>\n <Button variant=\"ghost\" size=\"sm\" className=\"text-xs\" onClick={() => void remove(m)}>\n Verwijder\n </Button>\n </div>\n </Card>\n ))}\n {items.length === 0 && <p className=\"text-sm text-slate-500\">Nog geen media.</p>}\n </div>\n {hasMore && (\n <div>\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setPageCount((c) => c + 1)}>\n Meer laden…\n </Button>\n </div>\n )}\n </div>\n );\n}\n\n/** Controlled alt-veld: volgt externe updates (realtime snapshot) en slaat alleen échte wijzigingen op. */\nfunction AltInput({ item, onSave }: { item: MediaItem; onSave: (item: MediaItem, alt: string) => Promise<void> }) {\n const [value, setValue] = useState(item.alt ?? \"\");\n // Extern gewijzigd (andere tab/verwerking) → veld bijtrekken, behalve terwijl de klant zelf typt.\n const [focused, setFocused] = useState(false);\n useEffect(() => {\n if (!focused) setValue(item.alt ?? \"\");\n }, [item.alt, focused]);\n return (\n <Input\n value={value}\n placeholder=\"Alt-tekst\"\n className=\"mb-2 h-8 text-xs\"\n onChange={(e) => setValue(e.target.value)}\n onFocus={() => setFocused(true)}\n onBlur={() => {\n setFocused(false);\n void onSave(item, value);\n }}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/media/MediaManager.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n collection,\n deleteDoc,\n doc,\n limit,\n onSnapshot,\n orderBy,\n query,\n updateDoc,\n} from \"firebase/firestore\";\nimport { deleteObject, ref as storageRef } from \"firebase/storage\";\nimport { getBucket, getDb } from \"../../firebase/client\";\nimport { DEMO_EVENT, isDemoActive, listDemoMediaAll } from \"../../demo/demoStore\";\nimport type { MediaItem } from \"../../content/types\";\nimport { Button, Card, Badge, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { uploadImage } from \"../lib/upload\";\nimport { findMediaUsage, findUnusedMedia } from \"../lib/mediaUsage\";\n\n/** Paginagrootte van de bibliotheek: begrensd laden i.p.v. álles (kosten + geheugen bij 100+ foto's). */\nconst PAGE_SIZE = 48;\n\nexport function MediaManager({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [items, setItems] = useState<MediaItem[]>([]);\n const [busy, setBusy] = useState(false);\n /** Ids van aantoonbaar ongebruikte afbeeldingen (na een expliciete scan); null = nog niet gescand. */\n const [ongebruikt, setOngebruikt] = useState<string[] | null>(null);\n const [scant, setScant] = useState(false);\n const [pageCount, setPageCount] = useState(1);\n const [hasMore, setHasMore] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n // Demo-modus: lokale bibliotheek (geseede foto's + eigen \"uploads\") i.p.v. Firestore.\n if (isDemoActive()) {\n const sync = () => setItems(listDemoMediaAll(siteId));\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }\n const db = getDb();\n if (!db) return;\n // limit + \"meer laden\" (pageCount groeit): geen volledige collectie-read per open.\n const max = pageCount * PAGE_SIZE;\n const q = query(collection(db, \"sites\", siteId, \"media\"), orderBy(\"createdAt\", \"desc\"), limit(max + 1));\n return onSnapshot(\n q,\n (snap) => {\n setHasMore(snap.docs.length > max);\n setItems(snap.docs.slice(0, max).map((d) => ({ id: d.id, ...(d.data() as Omit<MediaItem, \"id\">) })));\n },\n // Leesfouten (rules/App Check) niet stil slikken — anders lijkt de bibliotheek leeg.\n (err) => toast({ title: \"Kon mediabibliotheek niet laden\", description: err.message, tone: \"error\" }),\n );\n }, [siteId, toast, pageCount]);\n\n async function onFiles(files: FileList) {\n setBusy(true);\n let ok = 0;\n const errors: string[] = [];\n for (const file of Array.from(files)) {\n try {\n await uploadImage(siteId, file);\n ok++;\n } catch (e) {\n errors.push(`${file.name}: ${(e as Error).message}`);\n }\n }\n if (ok > 0) toast({ title: `${ok} foto('s) geüpload`, description: \"Automatisch verkleind; varianten worden op de achtergrond gemaakt.\", tone: \"success\" });\n if (errors.length) toast({ title: \"Sommige uploads mislukten\", description: errors.join(\" · \"), tone: \"error\" });\n setBusy(false);\n }\n\n async function saveAlt(item: MediaItem, alt: string) {\n const db = getDb();\n if (!db) return;\n if ((item.alt ?? \"\") === alt) return; // niets gewijzigd → geen write/toast\n try {\n await updateDoc(doc(db, \"sites\", siteId, \"media\", item.id), { alt });\n toast({ title: \"Alt-tekst opgeslagen\", tone: \"success\" });\n } catch (e) {\n // Niet stil slikken: de klant dacht anders dat de alt-tekst opgeslagen was.\n toast({ title: \"Alt-tekst opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n async function remove(item: MediaItem) {\n const db = getDb();\n const bucket = getBucket();\n if (!db) return;\n const usedOn = await findMediaUsage(siteId, item);\n const ok = await confirm({\n title: \"Afbeelding verwijderen?\",\n description:\n usedOn.length > 0\n ? `Let op: deze afbeelding wordt gebruikt op ${usedOn.length} gepubliceerde pagina('s) (${usedOn.join(\", \")}). Daar verschijnt dan een gebroken afbeelding.`\n : \"De afbeelding wordt definitief verwijderd.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n try {\n await verwijderItem(item);\n toast({ title: \"Verwijderd\" });\n } catch (e) {\n // Rules/offline-fout niet stil slikken — anders lijkt verwijderen \"niets te doen\".\n toast({ title: \"Verwijderen mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n /** Doc + origineel + alle varianten weg — gedeeld door los verwijderen en de opruimer. */\n async function verwijderItem(item: MediaItem) {\n const db = getDb();\n const bucket = getBucket();\n if (!db) throw new Error(\"Geen databaseverbinding.\");\n await deleteDoc(doc(db, \"sites\", siteId, \"media\", item.id));\n if (bucket) {\n // Ook de srcset-varianten opruimen die de Cloud Function heeft gemaakt — anders blijven\n // die (publieke) bestanden voor altijd in Storage staan.\n const paths = [\n item.path,\n item.variants?.webp?.path,\n ...(item.variants?.sizes ?? []).map((v) => v.path),\n ].filter((p): p is string => Boolean(p));\n await Promise.all(\n paths.map((p) => deleteObject(storageRef(bucket, p)).catch(() => undefined /* al weg */)),\n );\n }\n }\n\n /**\n * Opruimer (7.14): Storage groeide anders onbeperkt — elke ooit geüploade foto (plus zijn\n * webp/srcset-varianten) bleef staan, ook als hij allang uit elk blok was gehaald. De scan is\n * expliciet (één knop, één gecombineerde content-read via findUnusedMedia) en het verwijderen\n * vraagt altijd eerst bevestiging met aantal en omvang.\n */\n async function scanOngebruikt() {\n setScant(true);\n try {\n const ids = await findUnusedMedia(siteId, items);\n setOngebruikt(ids);\n if (ids.length === 0) toast({ title: \"Niets op te ruimen\", description: \"Elke afbeelding in de bibliotheek wordt ergens gebruikt.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Scan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setScant(false);\n }\n }\n\n async function ruimOngebruiktOp() {\n const doelen = items.filter((it) => ongebruikt?.includes(it.id));\n if (doelen.length === 0) return;\n const mb = (doelen.reduce((som, it) => som + (it.size ?? 0), 0) / (1024 * 1024)).toFixed(1);\n const ok = await confirm({\n title: `${doelen.length} ongebruikte afbeelding(en) verwijderen?`,\n description: `Samen ±${mb} MB, inclusief alle automatisch gemaakte varianten. Dit kan niet ongedaan worden gemaakt.`,\n confirmLabel: \"Opruimen\",\n tone: \"danger\",\n });\n if (!ok) return;\n setBusy(true);\n let gelukt = 0;\n const fouten: string[] = [];\n for (const it of doelen) {\n try { await verwijderItem(it); gelukt++; } catch (e) { fouten.push(`${it.name}: ${(e as Error).message}`); }\n }\n setOngebruikt(null);\n setBusy(false);\n if (gelukt) toast({ title: `${gelukt} afbeelding(en) opgeruimd`, description: `±${mb} MB opslag vrijgemaakt.`, tone: \"success\" });\n if (fouten.length) toast({ title: \"Niet alles kon weg\", description: fouten.join(\" · \"), tone: \"error\" });\n }\n\n return (\n <div className=\"grid gap-4\">\n <div>\n <Button disabled={busy} onClick={() => inputRef.current?.click()}>\n {busy ? \"Uploaden…\" : \"Foto's uploaden\"}\n </Button>{\" \"}\n {!isDemoActive() && items.length > 0 && (\n ongebruikt === null ? (\n <Button variant=\"ghost\" disabled={scant || busy} onClick={() => void scanOngebruikt()}>\n {scant ? \"Zoeken…\" : \"🧹 Zoek ongebruikte foto's\"}\n </Button>\n ) : ongebruikt.length > 0 ? (\n <>\n <Button variant=\"secondary\" disabled={busy} onClick={() => void ruimOngebruiktOp()}>\n 🧹 {ongebruikt.length} ongebruikte verwijderen\n </Button>{\" \"}\n <Button variant=\"ghost\" onClick={() => setOngebruikt(null)}>Annuleren</Button>\n </>\n ) : null\n )}\n <p className=\"mt-1 text-xs text-slate-500\">\n Je kunt meerdere foto's tegelijk kiezen. Ze worden automatisch verkleind en geoptimaliseerd\n (webp) — dat houdt je site snel en bespaart opslag. Max. 15 MB per bestand.\n </p>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n hidden\n onChange={(e) => {\n if (e.target.files?.length) void onFiles(e.target.files);\n e.target.value = \"\";\n }}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4\">\n {items.map((m) => (\n <Card key={m.id} className=\"p-3\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={m.url} alt={m.alt ?? m.name} className=\"mb-2 h-28 w-full rounded-md object-cover\" />\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <span className=\"truncate text-xs text-slate-500\">\n {m.name}\n {(m as { width?: number; height?: number }).width && (\n <span className=\"text-slate-400\"> · {(m as { width?: number }).width}×{(m as { height?: number }).height}</span>\n )}\n </span>\n {m.status === \"processing\" ? (\n <Badge tone=\"amber\">verwerken</Badge>\n ) : ongebruikt?.includes(m.id) ? (\n <Badge tone=\"amber\">ongebruikt</Badge>\n ) : (\n <Badge tone=\"green\">klaar</Badge>\n )}\n </div>\n <AltInput item={m} onSave={saveAlt} />\n <div className=\"flex gap-1\">\n <Button variant=\"ghost\" size=\"sm\" className=\"text-xs\" onClick={() => void navigator.clipboard?.writeText(m.url)}>\n Kopieer URL\n </Button>\n <Button variant=\"ghost\" size=\"sm\" className=\"text-xs\" onClick={() => void remove(m)}>\n Verwijder\n </Button>\n </div>\n </Card>\n ))}\n {items.length === 0 && <p className=\"text-sm text-slate-500\">Nog geen media.</p>}\n </div>\n {hasMore && (\n <div>\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setPageCount((c) => c + 1)}>\n Meer laden…\n </Button>\n </div>\n )}\n </div>\n );\n}\n\n/** Controlled alt-veld: volgt externe updates (realtime snapshot) en slaat alleen échte wijzigingen op. */\nfunction AltInput({ item, onSave }: { item: MediaItem; onSave: (item: MediaItem, alt: string) => Promise<void> }) {\n const [value, setValue] = useState(item.alt ?? \"\");\n // Extern gewijzigd (andere tab/verwerking) → veld bijtrekken, behalve terwijl de klant zelf typt.\n const [focused, setFocused] = useState(false);\n useEffect(() => {\n if (!focused) setValue(item.alt ?? \"\");\n }, [item.alt, focused]);\n return (\n <Input\n value={value}\n placeholder=\"Alt-tekst\"\n className=\"mb-2 h-8 text-xs\"\n onChange={(e) => setValue(e.target.value)}\n onFocus={() => setFocused(true)}\n onBlur={() => {\n setFocused(false);\n void onSave(item, value);\n }}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/admin/ui/toast.d.ts
CHANGED
package/dist/admin/ui/toast.js
CHANGED
|
@@ -9,14 +9,20 @@ function useToast() {
|
|
|
9
9
|
const show = useAstryxToast();
|
|
10
10
|
const showRef = useRef(show);
|
|
11
11
|
showRef.current = show;
|
|
12
|
-
const toast = useCallback(({ title, description, tone = "default" }) => {
|
|
12
|
+
const toast = useCallback(({ title, description, tone = "default", action }) => {
|
|
13
13
|
showRef.current({
|
|
14
14
|
body: /* @__PURE__ */ jsxs("span", { children: [
|
|
15
15
|
/* @__PURE__ */ jsx("strong", { children: title }),
|
|
16
|
-
description ? /* @__PURE__ */ jsx("span", { style: { display: "block", opacity: 0.8 }, children: description }) : null
|
|
16
|
+
description ? /* @__PURE__ */ jsx("span", { style: { display: "block", opacity: 0.8 }, children: description }) : null,
|
|
17
|
+
action ? (
|
|
18
|
+
// Eén optionele vervolg-actie in de melding zelf ("Bekijk als bezoeker →"): de melding
|
|
19
|
+
// is precies het moment waarop die vraag leeft, en een knop sluit de mentale lus die
|
|
20
|
+
// een instructie in prozavorm open laat staan.
|
|
21
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "xicms-toast__actie", onClick: action.onClick, children: action.label })
|
|
22
|
+
) : null
|
|
17
23
|
] }),
|
|
18
24
|
type: tone === "error" ? "error" : "info",
|
|
19
|
-
isAutoHide: tone !== "error"
|
|
25
|
+
isAutoHide: tone !== "error" && !action
|
|
20
26
|
});
|
|
21
27
|
}, []);
|
|
22
28
|
return useMemo(() => ({ toast }), [toast]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/ui/toast.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo, useRef, type ReactNode } from \"react\";\nimport { useToast as useAstryxToast, ToastViewport } from \"@astryxdesign/core/Toast\";\n\ntype ToastTone = \"default\" | \"success\" | \"error\";\ninterface ToastApi {\n toast: (t: { title: string; description?: string; tone?: ToastTone }) => void;\n}\n\n/**\n * Admin-toasts op de **Astryx Toast** (ToastViewport + useToast). De bestaande API\n * `toast({title, description, tone})` blijft, zodat de editors ongewijzigd blijven. Fouten blijven\n * staan tot de gebruiker ze sluit (auto-hide uit); overige verdwijnen vanzelf.\n */\nexport function ToastProvider({ children }: { children: ReactNode }) {\n return (\n <ToastViewport position=\"bottomEnd\" maxVisible={4}>\n {children}\n </ToastViewport>\n );\n}\n\n/**\n * `toast` MOET stabiel zijn over renders heen.\n *\n * Dit gaf `{ toast: (t) => … }` terug — een nieuw object met een nieuwe functie bij ELKE render.\n * Op zichzelf onschuldig, tot je bedenkt dat drie effecten in het beheer `toast` in hun\n * afhankelijkheden hebben (de mediakiezer, de mediabeheerder en de visuele editor). Zo'n effect\n * draait dan bij elke render opnieuw, zet state, en die state veroorzaakt weer een render:\n * \"Maximum update depth exceeded\", en een pagina die niet meer laadt.\n *\n * Dat maakte de visuele editor onbruikbaar op élke pagina mét werkversie — daar hangt de\n * mediakiezer in de boom. Op een pagina zónder werkversie kwam de editor nooit zo ver, en precies\n * daarom kon dit lang onopgemerkt blijven: wie het probeerde op een lege pagina zag niets bijzonders.\n *\n * `show` komt uit Astryx en biedt geen garantie over zijn eigen stabiliteit; daarom via een ref, en\n * niet als afhankelijkheid. Anders verplaatsen we het probleem alleen maar één laag.\n */\nexport function useToast(): ToastApi {\n const show = useAstryxToast();\n const showRef = useRef(show);\n showRef.current = show;\n\n const toast = useCallback<ToastApi[\"toast\"]>(({ title, description, tone = \"default\" }) => {\n showRef.current({\n body: (\n <span>\n <strong>{title}</strong>\n {description ? <span style={{ display: \"block\", opacity: 0.8 }}>{description}</span> : null}\n </span>\n ),\n type: tone === \"error\" ? \"error\" : \"info\",\n isAutoHide: tone !== \"error\",\n });\n }, []);\n\n return useMemo(() => ({ toast }), [toast]);\n}\n"],"mappings":";AAiBI,cA8BI,YA9BJ;AAfJ,SAAS,aAAa,SAAS,cAA8B;AAC7D,SAAS,YAAY,gBAAgB,qBAAqB;AAYnD,SAAS,cAAc,EAAE,SAAS,GAA4B;AACnE,SACE,oBAAC,iBAAc,UAAS,aAAY,YAAY,GAC7C,UACH;AAEJ;AAkBO,SAAS,WAAqB;AACnC,QAAM,OAAO,eAAe;AAC5B,QAAM,UAAU,OAAO,IAAI;AAC3B,UAAQ,UAAU;AAElB,QAAM,QAAQ,YAA+B,CAAC,EAAE,OAAO,aAAa,OAAO,
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/ui/toast.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo, useRef, type ReactNode } from \"react\";\nimport { useToast as useAstryxToast, ToastViewport } from \"@astryxdesign/core/Toast\";\n\ntype ToastTone = \"default\" | \"success\" | \"error\";\ninterface ToastApi {\n toast: (t: { title: string; description?: string; tone?: ToastTone; action?: { label: string; onClick: () => void } }) => void;\n}\n\n/**\n * Admin-toasts op de **Astryx Toast** (ToastViewport + useToast). De bestaande API\n * `toast({title, description, tone})` blijft, zodat de editors ongewijzigd blijven. Fouten blijven\n * staan tot de gebruiker ze sluit (auto-hide uit); overige verdwijnen vanzelf.\n */\nexport function ToastProvider({ children }: { children: ReactNode }) {\n return (\n <ToastViewport position=\"bottomEnd\" maxVisible={4}>\n {children}\n </ToastViewport>\n );\n}\n\n/**\n * `toast` MOET stabiel zijn over renders heen.\n *\n * Dit gaf `{ toast: (t) => … }` terug — een nieuw object met een nieuwe functie bij ELKE render.\n * Op zichzelf onschuldig, tot je bedenkt dat drie effecten in het beheer `toast` in hun\n * afhankelijkheden hebben (de mediakiezer, de mediabeheerder en de visuele editor). Zo'n effect\n * draait dan bij elke render opnieuw, zet state, en die state veroorzaakt weer een render:\n * \"Maximum update depth exceeded\", en een pagina die niet meer laadt.\n *\n * Dat maakte de visuele editor onbruikbaar op élke pagina mét werkversie — daar hangt de\n * mediakiezer in de boom. Op een pagina zónder werkversie kwam de editor nooit zo ver, en precies\n * daarom kon dit lang onopgemerkt blijven: wie het probeerde op een lege pagina zag niets bijzonders.\n *\n * `show` komt uit Astryx en biedt geen garantie over zijn eigen stabiliteit; daarom via een ref, en\n * niet als afhankelijkheid. Anders verplaatsen we het probleem alleen maar één laag.\n */\nexport function useToast(): ToastApi {\n const show = useAstryxToast();\n const showRef = useRef(show);\n showRef.current = show;\n\n const toast = useCallback<ToastApi[\"toast\"]>(({ title, description, tone = \"default\", action }) => {\n showRef.current({\n body: (\n <span>\n <strong>{title}</strong>\n {description ? <span style={{ display: \"block\", opacity: 0.8 }}>{description}</span> : null}\n {action ? (\n // Eén optionele vervolg-actie in de melding zelf (\"Bekijk als bezoeker →\"): de melding\n // is precies het moment waarop die vraag leeft, en een knop sluit de mentale lus die\n // een instructie in prozavorm open laat staan.\n <button type=\"button\" className=\"xicms-toast__actie\" onClick={action.onClick}>\n {action.label}\n </button>\n ) : null}\n </span>\n ),\n type: tone === \"error\" ? \"error\" : \"info\",\n isAutoHide: tone !== \"error\" && !action,\n });\n }, []);\n\n return useMemo(() => ({ toast }), [toast]);\n}\n"],"mappings":";AAiBI,cA8BI,YA9BJ;AAfJ,SAAS,aAAa,SAAS,cAA8B;AAC7D,SAAS,YAAY,gBAAgB,qBAAqB;AAYnD,SAAS,cAAc,EAAE,SAAS,GAA4B;AACnE,SACE,oBAAC,iBAAc,UAAS,aAAY,YAAY,GAC7C,UACH;AAEJ;AAkBO,SAAS,WAAqB;AACnC,QAAM,OAAO,eAAe;AAC5B,QAAM,UAAU,OAAO,IAAI;AAC3B,UAAQ,UAAU;AAElB,QAAM,QAAQ,YAA+B,CAAC,EAAE,OAAO,aAAa,OAAO,WAAW,OAAO,MAAM;AACjG,YAAQ,QAAQ;AAAA,MACd,MACE,qBAAC,UACC;AAAA,4BAAC,YAAQ,iBAAM;AAAA,QACd,cAAc,oBAAC,UAAK,OAAO,EAAE,SAAS,SAAS,SAAS,IAAI,GAAI,uBAAY,IAAU;AAAA,QACtF;AAAA;AAAA;AAAA;AAAA,UAIC,oBAAC,YAAO,MAAK,UAAS,WAAU,sBAAqB,SAAS,OAAO,SAClE,iBAAO,OACV;AAAA,YACE;AAAA,SACN;AAAA,MAEF,MAAM,SAAS,UAAU,UAAU;AAAA,MACnC,YAAY,SAAS,WAAW,CAAC;AAAA,IACnC,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,SAAO,QAAQ,OAAO,EAAE,MAAM,IAAI,CAAC,KAAK,CAAC;AAC3C;","names":[]}
|