@xiwebdesign/xicms-core 7.4.0 → 7.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/admin/AdminApp.d.ts +12 -1
  2. package/dist/admin/AdminApp.js +18 -14
  3. package/dist/admin/AdminApp.js.map +1 -1
  4. package/dist/admin/NewsEditor.js +15 -4
  5. package/dist/admin/NewsEditor.js.map +1 -1
  6. package/dist/admin/menu/MenuPanel.d.ts +20 -0
  7. package/dist/admin/menu/MenuPanel.js +144 -0
  8. package/dist/admin/menu/MenuPanel.js.map +1 -0
  9. package/dist/admin/panelRegistry.d.ts +10 -0
  10. package/dist/admin/panelRegistry.js +5 -1
  11. package/dist/admin/panelRegistry.js.map +1 -1
  12. package/dist/admin/settings/SettingsPanel.js +10 -2
  13. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  14. package/dist/admin/ui/dialog.js +6 -3
  15. package/dist/admin/ui/dialog.js.map +1 -1
  16. package/dist/admin/ui/toast.d.ts +16 -0
  17. package/dist/admin/ui/toast.js +8 -4
  18. package/dist/admin/ui/toast.js.map +1 -1
  19. package/dist/admin/visual/VisualEditor.js +89 -123
  20. package/dist/admin/visual/VisualEditor.js.map +1 -1
  21. package/dist/admin.css +1 -1
  22. package/dist/auth/AuthProvider.js +11 -5
  23. package/dist/auth/AuthProvider.js.map +1 -1
  24. package/dist/auth/LoginForm.js +2 -1
  25. package/dist/auth/LoginForm.js.map +1 -1
  26. package/dist/components/ExternEmbed.d.ts +23 -0
  27. package/dist/components/ExternEmbed.js +52 -0
  28. package/dist/components/ExternEmbed.js.map +1 -0
  29. package/dist/editor/EditorProvider.d.ts +8 -0
  30. package/dist/editor/EditorProvider.js +30 -6
  31. package/dist/editor/EditorProvider.js.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +2 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/next/authRewrites.d.ts +29 -0
  36. package/dist/next/authRewrites.js +11 -0
  37. package/dist/next/authRewrites.js.map +1 -1
  38. package/dist/next/index.cjs +12 -0
  39. package/dist/next/index.d.ts +1 -1
  40. package/dist/next/index.js +2 -1
  41. package/dist/next/index.js.map +1 -1
  42. package/dist/render/PublishedPage.js +9 -1
  43. package/dist/render/PublishedPage.js.map +1 -1
  44. package/dist/schema/settings.d.ts +62 -1
  45. package/dist/schema/settings.js +23 -1
  46. package/dist/schema/settings.js.map +1 -1
  47. package/dist/schema/siteConfig.d.ts +5 -0
  48. package/dist/settings/useSiteSettings.js +1 -1
  49. package/dist/settings/useSiteSettings.js.map +1 -1
  50. package/dist/styles.css +67 -12
  51. package/dist/version.js +1 -1
  52. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/settings/SettingsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { doc, onSnapshot } from \"firebase/firestore\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { SiteSettingsSchema, type SiteSettings } from \"../../schema/settings\";\nimport { useModules } from \"../../modules/context\";\nimport type { ModulesSiteConfig } from \"../../modules/types\";\nimport { builtinThemePresets } from \"../../theme/presets\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { ArrayEditor } from \"../ui/ArrayEditor\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { ModuleSettingsForm } from \"./moduleSettingsForm\";\nimport { SnapshotsCard } from \"./SnapshotsCard\";\nimport { MailSettings } from \"./MailSettings\";\n\nexport function SettingsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const { defs } = useModules();\n const [s, setS] = useState<SiteSettings>({});\n const [loaded, setLoaded] = useState(false);\n const [exporting, setExporting] = useState(false);\n const [restoring, setRestoring] = useState(false);\n const restoreInput = useRef<HTMLInputElement>(null);\n\n async function downloadBackup(silent = false): Promise<boolean> {\n const app = getFirebaseApp();\n if (!app) return false;\n setExporting(true);\n try {\n const fn = callable<{ siteId: string }, Record<string, unknown>>(app, \"exportSite\");\n const res = await fn({ siteId });\n const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `xicms-backup-${siteId}-${new Date().toISOString().slice(0, 10)}.json`;\n a.click();\n URL.revokeObjectURL(url);\n if (!silent) toast({ title: \"Back-up gedownload\", tone: \"success\" });\n return true;\n } catch (e) {\n toast({ title: \"Back-up mislukt\", description: (e as Error).message, tone: \"error\" });\n return false;\n } finally {\n setExporting(false);\n }\n }\n\n async function restoreFromFile(file: File) {\n let backup: Record<string, unknown>;\n try {\n backup = JSON.parse(await file.text()) as Record<string, unknown>;\n } catch {\n toast({ title: \"Ongeldig bestand\", description: \"Dit is geen geldig back-upbestand.\", tone: \"error\" });\n return;\n }\n if (backup._format !== \"xicms-export\") {\n toast({ title: \"Ongeldig bestand\", description: \"Dit lijkt geen XiCMS-back-up.\", tone: \"error\" });\n return;\n }\n if (backup.siteId && backup.siteId !== siteId) {\n const ok = await confirm({\n title: \"Back-up van een andere site\",\n description: `Deze back-up is van “${String(backup.siteId)}”, niet van deze site (${siteId}). Toch terugzetten?`,\n confirmLabel: \"Toch terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n }\n const ok = await confirm({\n title: \"Back-up terugzetten?\",\n description: `Dit VERVANGT alle huidige pagina's, nieuws, media en instellingen door de inhoud van de back-up${backup.exportedAt ? ` van ${new Date(String(backup.exportedAt)).toLocaleString(\"nl-NL\")}` : \"\"}. We maken eerst automatisch een back-up van de huidige stand.`,\n confirmLabel: \"Terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n setRestoring(true);\n try {\n // Veiligheid: eerst de huidige stand wegschrijven (als download) vóór we overschrijven.\n // Lukt die back-up niet, dan NIET doorgaan met de destructieve restore (anders ben je de\n // huidige stand kwijt zonder vangnet). De gebruiker kan het daarna opnieuw proberen.\n if (!(await downloadBackup(true))) {\n toast({ title: \"Terugzetten afgebroken\", description: \"De veiligheidsback-up van de huidige stand mislukte; er is niets overschreven.\", tone: \"error\" });\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = callable(app, \"restoreSite\");\n await fn({ siteId, backup });\n toast({ title: \"Back-up teruggezet\", description: \"Herlaad de pagina om alles bij te werken.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Terugzetten mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setRestoring(false);\n }\n }\n\n /**\n * Instellingen was één scroll van negen kaarten — van sitenaam tot back-up, alles onder elkaar.\n * Dat leest niet als \"waar stel ik mijn huisstijl in?\" maar als \"zoek het maar\". De kaarten zijn\n * nu per onderwerp gegroepeerd; je ziet er één tegelijk. De staat zit in `s` hierboven, dus\n * wisselen van sectie raakt niets kwijt en één \"Opslaan\" bewaart nog steeds het geheel.\n */\n const [sectie, setSectie] = useState<string>(\"algemeen\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n return onSnapshot(doc(db, \"sites\", siteId, \"settings\", \"site\"), (snap) => {\n if (snap.exists()) setS(snap.data() as SiteSettings);\n setLoaded(true);\n });\n }, [siteId]);\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = SiteSettingsSchema.safeParse(s);\n if (!parsed.success) {\n toast({ title: \"Ongeldige instellingen\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Via de callable i.p.v. rechtstreeks: die valideert server-side, leidt de publieke subset\n // zelf af (zodat `site` en `public` niet uit elkaar kunnen lopen) én duwt de cache-tag door,\n // waardoor de wijziging binnen seconden live staat i.p.v. na het ISR-vangnet.\n try {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase is niet geconfigureerd.\");\n const fn = callable(app, \"saveSiteSettings\");\n const res = await fn({ siteId, settings: parsed.data });\n toast({ title: \"Opgeslagen\", description: (res.data as { message?: string })?.message ?? \"Wijzigingen zijn live.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n if (!loaded) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n const forms = s.forms ?? { mode: \"xicms\" as const };\n const theme = s.theme ?? { primary: \"#4f46e5\" };\n // Zelfde terugval als XiCMSProvider: `mode` wint, anders de oude boolean.\n const donkerModus: \"light\" | \"dark\" | \"auto\" = theme.dark?.mode ?? (theme.dark?.auto ? \"auto\" : \"light\");\n const footer = s.footer ?? {};\n\n // Hulpfuncties voor de module-instellingen.\n const moduleState = (id: string, defaultEnabled: boolean) => {\n const raw = (s.modules as ModulesSiteConfig | undefined)?.[id];\n if (typeof raw === \"boolean\") return { enabled: raw, config: {} as Record<string, unknown> };\n return { enabled: raw?.enabled ?? defaultEnabled, config: raw?.config ?? {} };\n };\n const setModule = (id: string, next: { enabled: boolean; config: Record<string, unknown> }) =>\n setS({ ...s, modules: { ...(s.modules as ModulesSiteConfig | undefined), [id]: next } });\n\n const SECTIES: { sleutel: string; label: string }[] = [\n { sleutel: \"algemeen\", label: \"Algemeen\" },\n { sleutel: \"huisstijl\", label: \"Huisstijl\" },\n { sleutel: \"seo\", label: \"SEO\" },\n { sleutel: \"footer\", label: \"Footer\" },\n { sleutel: \"modules\", label: \"Modules\" },\n { sleutel: \"mail\", label: \"E-mail\" },\n { sleutel: \"backup\", label: \"Back-up\" },\n ];\n\n return (\n /* Een formulier leest niet op 1300px breed; een invoerveld hoort niet breder te zijn dan de\n tekst die erin komt. Vandaar een leesbreedte in plaats van het hele werkblad vullen. */\n <div className=\"grid max-w-4xl gap-4\">\n <nav aria-label=\"Onderdelen van de instellingen\" className=\"flex flex-wrap gap-1\">\n {SECTIES.map((x) => (\n <button\n key={x.sleutel}\n type=\"button\"\n aria-current={sectie === x.sleutel ? \"page\" : undefined}\n onClick={() => setSectie(x.sleutel)}\n className=\"rounded-lg px-3 py-1.5 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 aria-[current=page]:bg-slate-200 aria-[current=page]:text-slate-900\"\n >\n {x.label}\n </button>\n ))}\n </nav>\n\n {sectie === \"algemeen\" && (\n <>\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Algemeen</h3>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Sitenaam\"><Input value={s.name ?? \"\"} onChange={(e) => setS({ ...s, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={s.domain ?? \"\"} onChange={(e) => setS({ ...s, domain: e.target.value })} /></Field>\n <Field label=\"Contact-e-mail\"><Input value={s.contactEmail ?? \"\"} onChange={(e) => setS({ ...s, contactEmail: e.target.value })} /></Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"huisstijl\" && (\n <>\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Huisstijl</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Thema-preset\" hint=\"Vult kleuren, lettertype en afronding in één keer in.\">\n <Select\n value=\"\"\n placeholder=\"Kies een preset…\"\n onValueChange={(id) => {\n const preset = builtinThemePresets.find((p) => p.id === id);\n if (preset) setS({ ...s, theme: { ...theme, ...preset.theme } });\n }}\n options={builtinThemePresets.map((p) => ({ value: p.id, label: p.label }))}\n />\n </Field>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Primaire kleur\"><Input value={theme.primary} onChange={(e) => setS({ ...s, theme: { ...theme, primary: e.target.value } })} /></Field>\n <Field label=\"Accentkleur\"><Input value={theme.accent ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, accent: e.target.value } })} /></Field>\n <Field label=\"Achtergrondkleur\"><Input value={theme.background ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, background: e.target.value } })} placeholder=\"#ffffff\" /></Field>\n <Field label=\"Tekstkleur\"><Input value={theme.foreground ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, foreground: e.target.value } })} placeholder=\"#0f172a\" /></Field>\n </div>\n {/* Donkere modus. Het schema en de rendering ondersteunden dit al, maar er was geen knop:\n donker aanzetten kon alleen door site.config.ts te bewerken — dus niet door de klant\n zelf. De kleuren verschijnen pas als ze ergens toe doen; bij \"altijd licht\" zou een\n donkere achtergrondkleur alleen verwarren. */}\n <Field\n label=\"Donkere modus\"\n hint={\n donkerModus === \"auto\"\n ? \"Volgt de voorkeur van de bezoeker: wie zijn telefoon of computer op donker heeft staan, ziet de site donker.\"\n : donkerModus === \"dark\"\n ? \"De site is voor iedereen donker, ongeacht de voorkeur van de bezoeker.\"\n : \"De site is voor iedereen licht.\"\n }\n >\n <Select\n value={donkerModus}\n onValueChange={(v) =>\n setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), mode: v as \"light\" | \"dark\" | \"auto\" } } })\n }\n options={[\n { value: \"light\", label: \"Altijd licht\" },\n { value: \"dark\", label: \"Altijd donker\" },\n { value: \"auto\", label: \"Automatisch — volg de bezoeker\" },\n ]}\n />\n </Field>\n {donkerModus !== \"light\" && (\n <div className=\"grid gap-3 sm:grid-cols-3\">\n <Field label=\"Donkere achtergrond\">\n <Input\n value={theme.dark?.background ?? \"\"}\n onChange={(e) => setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), background: e.target.value } } })}\n placeholder=\"#0f172a\"\n />\n </Field>\n <Field label=\"Donkere tekstkleur\">\n <Input\n value={theme.dark?.foreground ?? \"\"}\n onChange={(e) => setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), foreground: e.target.value } } })}\n placeholder=\"#e2e8f0\"\n />\n </Field>\n {/* Een merkkleur die op wit werkt is op zwart vaak te donker; daarom een eigen waarde. */}\n <Field label=\"Donkere merkkleur\" hint=\"Laat leeg om dezelfde te gebruiken.\">\n <Input\n value={theme.dark?.primary ?? \"\"}\n onChange={(e) => setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), primary: e.target.value } } })}\n placeholder={theme.primary}\n />\n </Field>\n </div>\n )}\n <Field label=\"Logo-URL (e-mail & factuur)\" hint=\"Absolute https-URL naar een PNG/JPG. Verschijnt in de mail-header en op de factuur-PDF.\">\n <Input value={theme.logo ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, logo: e.target.value } })} placeholder=\"https://…/logo.png\" />\n </Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"seo\" && (\n <>\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">SEO (standaard)</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Standaard-omschrijving\">\n <Input value={s.seoDefaults?.description ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, description: e.target.value } })} />\n </Field>\n <Field label=\"OG-afbeelding (URL)\">\n <Input value={s.seoDefaults?.ogImage ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, ogImage: e.target.value } })} />\n </Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"footer\" && (\n <>\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Footer</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Kolommen met links, social-links en copyright. Wijzigingen zijn direct live.</p>\n <div className=\"grid gap-4\">\n <div>\n <span className=\"text-sm font-medium\">Kolommen</span>\n <ArrayEditor\n items={footer.columns ?? []}\n onChange={(columns) => setS({ ...s, footer: { ...footer, columns } })}\n newItem={() => ({ heading: \"\", links: [] })}\n addLabel=\"+ Kolom\"\n emptyLabel=\"Nog geen kolommen.\"\n renderRow={(col, update) => (\n <div className=\"grid gap-2\">\n <Field label=\"Kop\"><Input value={col.heading ?? \"\"} onChange={(e) => update({ heading: e.target.value })} /></Field>\n <ArrayEditor\n items={col.links ?? []}\n onChange={(links) => update({ links })}\n newItem={() => ({ label: \"\", href: \"\" })}\n addLabel=\"+ Link\"\n renderRow={(link, updL) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.label} placeholder=\"Label\" onChange={(e) => updL({ label: e.target.value })} />\n <Input value={link.href} placeholder=\"/pad of https://…\" onChange={(e) => updL({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n )}\n />\n </div>\n <div>\n <span className=\"text-sm font-medium\">Social-links</span>\n <ArrayEditor\n items={footer.social ?? []}\n onChange={(social) => setS({ ...s, footer: { ...footer, social } })}\n newItem={() => ({ platform: \"\", href: \"\" })}\n addLabel=\"+ Social-link\"\n renderRow={(link, update) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.platform} placeholder=\"bv. Instagram\" onChange={(e) => update({ platform: e.target.value })} />\n <Input value={link.href} placeholder=\"https://…\" onChange={(e) => update({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Copyright (leeg = automatisch)\">\n <Input value={footer.copyright ?? \"\"} onChange={(e) => setS({ ...s, footer: { ...footer, copyright: e.target.value } })} />\n </Field>\n <label className=\"flex items-center gap-2 self-end text-sm\">\n <Switch checked={footer.showAdminLink !== false} onCheckedChange={(v) => setS({ ...s, footer: { ...footer, showAdminLink: v } })} />\n “Beheer”-link tonen\n </label>\n </div>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"modules\" && (\n <>\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Modules</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Zet functies aan/uit en stel ze in. Uitgezette modules verbergen hun blokken en beheer-paneel.</p>\n <div className=\"grid gap-3\">\n {defs\n .filter((d) => !d.core)\n .map((d) => {\n const st = moduleState(d.id, d.defaultEnabled ?? true);\n return (\n <div key={d.id} className=\"rounded-lg border border-slate-200 p-3\">\n <label className=\"flex items-center justify-between gap-2\">\n <span>\n <strong className=\"text-sm\">{d.label}</strong>\n {d.description && <span className=\"block text-xs text-slate-500\">{d.description}</span>}\n </span>\n <Switch checked={st.enabled} onCheckedChange={(v) => setModule(d.id, { enabled: v, config: st.config })} />\n </label>\n {st.enabled && d.settingsSchema && (\n <div className=\"mt-3\">\n <ModuleSettingsForm\n schema={d.settingsSchema}\n value={st.config}\n onChange={(config) => setModule(d.id, { enabled: st.enabled, config })}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"mail\" && (\n <>\n {/* Site-brede mailprovider + opmaak (formulieren én shop). Zie docs/SHARED-SETTINGS.md. */}\n <MailSettings siteId={siteId} />\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">E-mailteksten</h3>\n <p className=\"text-sm text-slate-600\">\n De teksten van automatische e-mails bewerk je bij het onderdeel zelf: formulier-mails onder{\" \"}\n <strong>Formulieren</strong>, webwinkel-mails onder <strong>Webwinkel → E-mails</strong>. Provider en\n opmaak (logo, huisstijlkleur) gelden site-breed en stel je hier in.\n </p>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Formulier-afzender</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n De afzender-identiteit die formulier-notificaties gebruiken. Kies de XiCMS-afzender\n (werkt direct) of het eigen domein van de klant (vereist domeinverificatie). De provider\n waarmee daadwerkelijk verstuurd wordt, stel je hierboven bij <strong>E-mail</strong> in.\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzender-modus\">\n <Select\n value={forms.mode}\n onValueChange={(v) => setS({ ...s, forms: { ...forms, mode: v as \"xicms\" | \"client\" } })}\n options={[\n { value: \"xicms\", label: \"XiCMS-domein (aanbevolen)\" },\n { value: \"client\", label: \"Eigen domein van de klant\" },\n ]}\n />\n </Field>\n <Field label=\"Van (from) e-mail\" hint=\"Moet een geverifieerd domein zijn.\">\n <Input value={forms.fromEmail ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromEmail: e.target.value } })} />\n </Field>\n <Field label=\"Van (naam)\"><Input value={forms.fromName ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromName: e.target.value } })} /></Field>\n <Field label=\"Notificaties naar\" hint=\"Meestal het mailadres van de klant.\">\n <Input value={forms.notifyTo ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, notifyTo: e.target.value } })} />\n </Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"backup\" && (\n <>\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Back-up &amp; herstel</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Sla alle content van deze site (pagina's, nieuws, formulieren, instellingen en je foto-galerij)\n op in één bestand. Bewaar dat veilig; je kunt het later weer terugzetten. De foto-bestanden\n zelf blijven in de cloud staan — de back-up bevat de teksten, instellingen en verwijzingen.\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"secondary\" onClick={() => void downloadBackup()} disabled={exporting || restoring}>\n {exporting ? \"Bezig…\" : \"Back-up downloaden\"}\n </Button>\n <Button variant=\"secondary\" onClick={() => restoreInput.current?.click()} disabled={restoring || exporting}>\n {restoring ? \"Terugzetten…\" : \"Back-up terugzetten…\"}\n </Button>\n <input\n ref={restoreInput}\n type=\"file\"\n accept=\"application/json,.json\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void restoreFromFile(f);\n e.target.value = \"\";\n }}\n />\n </div>\n <p className=\"mt-2 text-xs text-slate-400\">\n Terugzetten vervangt de huidige content door de back-up. We maken automatisch eerst een\n back-up van de huidige stand, zodat je nooit iets kwijtraakt.\n </p>\n </Card>\n\n <SnapshotsCard siteId={siteId} />\n </>\n )}\n\n {/* Sticky: één formulier, één opslaan-knop — die moet bereikbaar blijven vanuit elke sectie,\n ook onderaan een lange (Huisstijl, Footer). */}\n <div className=\"sticky bottom-0 -mx-4 flex justify-end border-t px-4 py-3\" style={{ borderColor: \"var(--color-border)\", background: \"var(--admin-canvas)\" }}>\n <Button onClick={save}>Instellingen opslaan</Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["ok"]}
1
+ {"version":3,"sources":["../../../src/admin/settings/SettingsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { doc, onSnapshot } from \"firebase/firestore\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { SiteSettingsSchema, type SiteSettings } from \"../../schema/settings\";\nimport { useModules } from \"../../modules/context\";\nimport { useSiteConfig } from \"../../config\";\nimport type { ModulesSiteConfig } from \"../../modules/types\";\nimport { builtinThemePresets } from \"../../theme/presets\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { ArrayEditor } from \"../ui/ArrayEditor\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { ModuleSettingsForm } from \"./moduleSettingsForm\";\nimport { SnapshotsCard } from \"./SnapshotsCard\";\nimport { MailSettings } from \"./MailSettings\";\n\nexport function SettingsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const { defs } = useModules();\n const cfg = useSiteConfig();\n const [s, setS] = useState<SiteSettings>({});\n const [loaded, setLoaded] = useState(false);\n const [exporting, setExporting] = useState(false);\n const [restoring, setRestoring] = useState(false);\n const restoreInput = useRef<HTMLInputElement>(null);\n\n async function downloadBackup(silent = false): Promise<boolean> {\n const app = getFirebaseApp();\n if (!app) return false;\n setExporting(true);\n try {\n const fn = callable<{ siteId: string }, Record<string, unknown>>(app, \"exportSite\");\n const res = await fn({ siteId });\n const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `xicms-backup-${siteId}-${new Date().toISOString().slice(0, 10)}.json`;\n a.click();\n URL.revokeObjectURL(url);\n if (!silent) toast({ title: \"Back-up gedownload\", tone: \"success\" });\n return true;\n } catch (e) {\n toast({ title: \"Back-up mislukt\", description: (e as Error).message, tone: \"error\" });\n return false;\n } finally {\n setExporting(false);\n }\n }\n\n async function restoreFromFile(file: File) {\n let backup: Record<string, unknown>;\n try {\n backup = JSON.parse(await file.text()) as Record<string, unknown>;\n } catch {\n toast({ title: \"Ongeldig bestand\", description: \"Dit is geen geldig back-upbestand.\", tone: \"error\" });\n return;\n }\n if (backup._format !== \"xicms-export\") {\n toast({ title: \"Ongeldig bestand\", description: \"Dit lijkt geen XiCMS-back-up.\", tone: \"error\" });\n return;\n }\n if (backup.siteId && backup.siteId !== siteId) {\n const ok = await confirm({\n title: \"Back-up van een andere site\",\n description: `Deze back-up is van “${String(backup.siteId)}”, niet van deze site (${siteId}). Toch terugzetten?`,\n confirmLabel: \"Toch terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n }\n const ok = await confirm({\n title: \"Back-up terugzetten?\",\n description: `Dit VERVANGT alle huidige pagina's, nieuws, media en instellingen door de inhoud van de back-up${backup.exportedAt ? ` van ${new Date(String(backup.exportedAt)).toLocaleString(\"nl-NL\")}` : \"\"}. We maken eerst automatisch een back-up van de huidige stand.`,\n confirmLabel: \"Terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n setRestoring(true);\n try {\n // Veiligheid: eerst de huidige stand wegschrijven (als download) vóór we overschrijven.\n // Lukt die back-up niet, dan NIET doorgaan met de destructieve restore (anders ben je de\n // huidige stand kwijt zonder vangnet). De gebruiker kan het daarna opnieuw proberen.\n if (!(await downloadBackup(true))) {\n toast({ title: \"Terugzetten afgebroken\", description: \"De veiligheidsback-up van de huidige stand mislukte; er is niets overschreven.\", tone: \"error\" });\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = callable(app, \"restoreSite\");\n await fn({ siteId, backup });\n toast({ title: \"Back-up teruggezet\", description: \"Herlaad de pagina om alles bij te werken.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Terugzetten mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setRestoring(false);\n }\n }\n\n /**\n * Instellingen was één scroll van negen kaarten — van sitenaam tot back-up, alles onder elkaar.\n * Dat leest niet als \"waar stel ik mijn huisstijl in?\" maar als \"zoek het maar\". De kaarten zijn\n * nu per onderwerp gegroepeerd; je ziet er één tegelijk. De staat zit in `s` hierboven, dus\n * wisselen van sectie raakt niets kwijt en één \"Opslaan\" bewaart nog steeds het geheel.\n */\n const [sectie, setSectie] = useState<string>(\"algemeen\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n return onSnapshot(doc(db, \"sites\", siteId, \"settings\", \"site\"), (snap) => {\n const uitDb = snap.exists() ? (snap.data() as SiteSettings) : {};\n // Sitenaam, domein en contact-e-mail stonden hier leeg, en dat las als \"niet ingevuld\".\n // Ze wáren ingevuld, alleen ergens anders: in `content/site.config.ts`, waar ze bij het\n // bouwen van de site zijn gezet. Het instellingen-document kent ze pas zodra iemand hier\n // op Opslaan drukt, en dat had nog nooit iemand gedaan.\n //\n // Terugvallen op de site-config maakt het eerlijk: je ziet de waarde die de site\n // daadwerkelijk gebruikt, en kunt hem van daaruit aanpassen. Wat in het document staat wint\n // altijd — anders zou een bewust leeggemaakt veld telkens terugkomen.\n setS({\n ...uitDb,\n name: uitDb.name ?? cfg.name,\n domain: uitDb.domain ?? cfg.domain,\n contactEmail: uitDb.contactEmail ?? cfg.contactEmail,\n });\n setLoaded(true);\n });\n }, [siteId, cfg.name, cfg.domain, cfg.contactEmail]);\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = SiteSettingsSchema.safeParse(s);\n if (!parsed.success) {\n toast({ title: \"Ongeldige instellingen\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Via de callable i.p.v. rechtstreeks: die valideert server-side, leidt de publieke subset\n // zelf af (zodat `site` en `public` niet uit elkaar kunnen lopen) én duwt de cache-tag door,\n // waardoor de wijziging binnen seconden live staat i.p.v. na het ISR-vangnet.\n try {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase is niet geconfigureerd.\");\n const fn = callable(app, \"saveSiteSettings\");\n const res = await fn({ siteId, settings: parsed.data });\n toast({ title: \"Opgeslagen\", description: (res.data as { message?: string })?.message ?? \"Wijzigingen zijn live.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n if (!loaded) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n const forms = s.forms ?? { mode: \"xicms\" as const };\n const theme = s.theme ?? { primary: \"#4f46e5\" };\n // Zelfde terugval als XiCMSProvider: `mode` wint, anders de oude boolean.\n const donkerModus: \"light\" | \"dark\" | \"auto\" = theme.dark?.mode ?? (theme.dark?.auto ? \"auto\" : \"light\");\n const footer = s.footer ?? {};\n\n // Hulpfuncties voor de module-instellingen.\n const moduleState = (id: string, defaultEnabled: boolean) => {\n const raw = (s.modules as ModulesSiteConfig | undefined)?.[id];\n if (typeof raw === \"boolean\") return { enabled: raw, config: {} as Record<string, unknown> };\n return { enabled: raw?.enabled ?? defaultEnabled, config: raw?.config ?? {} };\n };\n const setModule = (id: string, next: { enabled: boolean; config: Record<string, unknown> }) =>\n setS({ ...s, modules: { ...(s.modules as ModulesSiteConfig | undefined), [id]: next } });\n\n const SECTIES: { sleutel: string; label: string }[] = [\n { sleutel: \"algemeen\", label: \"Algemeen\" },\n { sleutel: \"huisstijl\", label: \"Huisstijl\" },\n { sleutel: \"seo\", label: \"SEO\" },\n { sleutel: \"footer\", label: \"Footer\" },\n { sleutel: \"modules\", label: \"Modules\" },\n { sleutel: \"mail\", label: \"E-mail\" },\n { sleutel: \"backup\", label: \"Back-up\" },\n ];\n\n return (\n /* Een formulier leest niet op 1300px breed; een invoerveld hoort niet breder te zijn dan de\n tekst die erin komt. Vandaar een leesbreedte in plaats van het hele werkblad vullen. */\n <div className=\"grid max-w-4xl gap-4\">\n <nav aria-label=\"Onderdelen van de instellingen\" className=\"flex flex-wrap gap-1\">\n {SECTIES.map((x) => (\n <button\n key={x.sleutel}\n type=\"button\"\n aria-current={sectie === x.sleutel ? \"page\" : undefined}\n onClick={() => setSectie(x.sleutel)}\n className=\"rounded-lg px-3 py-1.5 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 aria-[current=page]:bg-slate-200 aria-[current=page]:text-slate-900\"\n >\n {x.label}\n </button>\n ))}\n </nav>\n\n {sectie === \"algemeen\" && (\n <>\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Algemeen</h3>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Sitenaam\"><Input value={s.name ?? \"\"} onChange={(e) => setS({ ...s, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={s.domain ?? \"\"} onChange={(e) => setS({ ...s, domain: e.target.value })} /></Field>\n <Field label=\"Contact-e-mail\"><Input value={s.contactEmail ?? \"\"} onChange={(e) => setS({ ...s, contactEmail: e.target.value })} /></Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"huisstijl\" && (\n <>\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Huisstijl</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Thema-preset\" hint=\"Vult kleuren, lettertype en afronding in één keer in.\">\n <Select\n value=\"\"\n placeholder=\"Kies een preset…\"\n onValueChange={(id) => {\n const preset = builtinThemePresets.find((p) => p.id === id);\n if (preset) setS({ ...s, theme: { ...theme, ...preset.theme } });\n }}\n options={builtinThemePresets.map((p) => ({ value: p.id, label: p.label }))}\n />\n </Field>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Primaire kleur\"><Input value={theme.primary} onChange={(e) => setS({ ...s, theme: { ...theme, primary: e.target.value } })} /></Field>\n <Field label=\"Accentkleur\"><Input value={theme.accent ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, accent: e.target.value } })} /></Field>\n <Field label=\"Achtergrondkleur\"><Input value={theme.background ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, background: e.target.value } })} placeholder=\"#ffffff\" /></Field>\n <Field label=\"Tekstkleur\"><Input value={theme.foreground ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, foreground: e.target.value } })} placeholder=\"#0f172a\" /></Field>\n </div>\n {/* Donkere modus. Het schema en de rendering ondersteunden dit al, maar er was geen knop:\n donker aanzetten kon alleen door site.config.ts te bewerken — dus niet door de klant\n zelf. De kleuren verschijnen pas als ze ergens toe doen; bij \"altijd licht\" zou een\n donkere achtergrondkleur alleen verwarren. */}\n <Field\n label=\"Donkere modus\"\n hint={\n donkerModus === \"auto\"\n ? \"Volgt de voorkeur van de bezoeker: wie zijn telefoon of computer op donker heeft staan, ziet de site donker.\"\n : donkerModus === \"dark\"\n ? \"De site is voor iedereen donker, ongeacht de voorkeur van de bezoeker.\"\n : \"De site is voor iedereen licht.\"\n }\n >\n <Select\n value={donkerModus}\n onValueChange={(v) =>\n setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), mode: v as \"light\" | \"dark\" | \"auto\" } } })\n }\n options={[\n { value: \"light\", label: \"Altijd licht\" },\n { value: \"dark\", label: \"Altijd donker\" },\n { value: \"auto\", label: \"Automatisch — volg de bezoeker\" },\n ]}\n />\n </Field>\n {donkerModus !== \"light\" && (\n <div className=\"grid gap-3 sm:grid-cols-3\">\n <Field label=\"Donkere achtergrond\">\n <Input\n value={theme.dark?.background ?? \"\"}\n onChange={(e) => setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), background: e.target.value } } })}\n placeholder=\"#0f172a\"\n />\n </Field>\n <Field label=\"Donkere tekstkleur\">\n <Input\n value={theme.dark?.foreground ?? \"\"}\n onChange={(e) => setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), foreground: e.target.value } } })}\n placeholder=\"#e2e8f0\"\n />\n </Field>\n {/* Een merkkleur die op wit werkt is op zwart vaak te donker; daarom een eigen waarde. */}\n <Field label=\"Donkere merkkleur\" hint=\"Laat leeg om dezelfde te gebruiken.\">\n <Input\n value={theme.dark?.primary ?? \"\"}\n onChange={(e) => setS({ ...s, theme: { ...theme, dark: { ...(theme.dark ?? {}), primary: e.target.value } } })}\n placeholder={theme.primary}\n />\n </Field>\n </div>\n )}\n <Field label=\"Logo-URL (e-mail & factuur)\" hint=\"Absolute https-URL naar een PNG/JPG. Verschijnt in de mail-header en op de factuur-PDF.\">\n <Input value={theme.logo ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, logo: e.target.value } })} placeholder=\"https://…/logo.png\" />\n </Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"seo\" && (\n <>\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">SEO (standaard)</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Standaard-omschrijving\">\n <Input value={s.seoDefaults?.description ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, description: e.target.value } })} />\n </Field>\n <Field label=\"OG-afbeelding (URL)\">\n <Input value={s.seoDefaults?.ogImage ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, ogImage: e.target.value } })} />\n </Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"footer\" && (\n <>\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Footer</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Kolommen met links, social-links en copyright. Wijzigingen zijn direct live.</p>\n <div className=\"grid gap-4\">\n <div>\n <span className=\"text-sm font-medium\">Kolommen</span>\n <ArrayEditor\n items={footer.columns ?? []}\n onChange={(columns) => setS({ ...s, footer: { ...footer, columns } })}\n newItem={() => ({ heading: \"\", links: [] })}\n addLabel=\"+ Kolom\"\n emptyLabel=\"Nog geen kolommen.\"\n renderRow={(col, update) => (\n <div className=\"grid gap-2\">\n <Field label=\"Kop\"><Input value={col.heading ?? \"\"} onChange={(e) => update({ heading: e.target.value })} /></Field>\n <ArrayEditor\n items={col.links ?? []}\n onChange={(links) => update({ links })}\n newItem={() => ({ label: \"\", href: \"\" })}\n addLabel=\"+ Link\"\n renderRow={(link, updL) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.label} placeholder=\"Label\" onChange={(e) => updL({ label: e.target.value })} />\n <Input value={link.href} placeholder=\"/pad of https://…\" onChange={(e) => updL({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n )}\n />\n </div>\n <div>\n <span className=\"text-sm font-medium\">Social-links</span>\n <ArrayEditor\n items={footer.social ?? []}\n onChange={(social) => setS({ ...s, footer: { ...footer, social } })}\n newItem={() => ({ platform: \"\", href: \"\" })}\n addLabel=\"+ Social-link\"\n renderRow={(link, update) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.platform} placeholder=\"bv. Instagram\" onChange={(e) => update({ platform: e.target.value })} />\n <Input value={link.href} placeholder=\"https://…\" onChange={(e) => update({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Copyright (leeg = automatisch)\">\n <Input value={footer.copyright ?? \"\"} onChange={(e) => setS({ ...s, footer: { ...footer, copyright: e.target.value } })} />\n </Field>\n <label className=\"flex items-center gap-2 self-end text-sm\">\n <Switch checked={footer.showAdminLink !== false} onCheckedChange={(v) => setS({ ...s, footer: { ...footer, showAdminLink: v } })} />\n “Beheer”-link tonen\n </label>\n </div>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"modules\" && (\n <>\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Modules</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Zet functies aan/uit en stel ze in. Uitgezette modules verbergen hun blokken en beheer-paneel.</p>\n <div className=\"grid gap-3\">\n {defs\n .filter((d) => !d.core)\n .map((d) => {\n const st = moduleState(d.id, d.defaultEnabled ?? true);\n return (\n <div key={d.id} className=\"rounded-lg border border-slate-200 p-3\">\n <label className=\"flex items-center justify-between gap-2\">\n <span>\n <strong className=\"text-sm\">{d.label}</strong>\n {d.description && <span className=\"block text-xs text-slate-500\">{d.description}</span>}\n </span>\n <Switch checked={st.enabled} onCheckedChange={(v) => setModule(d.id, { enabled: v, config: st.config })} />\n </label>\n {st.enabled && d.settingsSchema && (\n <div className=\"mt-3\">\n <ModuleSettingsForm\n schema={d.settingsSchema}\n value={st.config}\n onChange={(config) => setModule(d.id, { enabled: st.enabled, config })}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"mail\" && (\n <>\n {/* Site-brede mailprovider + opmaak (formulieren én shop). Zie docs/SHARED-SETTINGS.md. */}\n <MailSettings siteId={siteId} />\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">E-mailteksten</h3>\n <p className=\"text-sm text-slate-600\">\n De teksten van automatische e-mails bewerk je bij het onderdeel zelf: formulier-mails onder{\" \"}\n <strong>Formulieren</strong>, webwinkel-mails onder <strong>Webwinkel → E-mails</strong>. Provider en\n opmaak (logo, huisstijlkleur) gelden site-breed en stel je hier in.\n </p>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Formulier-afzender</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n De afzender-identiteit die formulier-notificaties gebruiken. Kies de XiCMS-afzender\n (werkt direct) of het eigen domein van de klant (vereist domeinverificatie). De provider\n waarmee daadwerkelijk verstuurd wordt, stel je hierboven bij <strong>E-mail</strong> in.\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzender-modus\">\n <Select\n value={forms.mode}\n onValueChange={(v) => setS({ ...s, forms: { ...forms, mode: v as \"xicms\" | \"client\" } })}\n options={[\n { value: \"xicms\", label: \"XiCMS-domein (aanbevolen)\" },\n { value: \"client\", label: \"Eigen domein van de klant\" },\n ]}\n />\n </Field>\n <Field label=\"Van (from) e-mail\" hint=\"Moet een geverifieerd domein zijn.\">\n <Input value={forms.fromEmail ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromEmail: e.target.value } })} />\n </Field>\n <Field label=\"Van (naam)\"><Input value={forms.fromName ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromName: e.target.value } })} /></Field>\n <Field label=\"Notificaties naar\" hint=\"Meestal het mailadres van de klant.\">\n <Input value={forms.notifyTo ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, notifyTo: e.target.value } })} />\n </Field>\n </div>\n </Card>\n </>\n )}\n\n {sectie === \"backup\" && (\n <>\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Back-up &amp; herstel</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Sla alle content van deze site (pagina's, nieuws, formulieren, instellingen en je foto-galerij)\n op in één bestand. Bewaar dat veilig; je kunt het later weer terugzetten. De foto-bestanden\n zelf blijven in de cloud staan — de back-up bevat de teksten, instellingen en verwijzingen.\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"secondary\" onClick={() => void downloadBackup()} disabled={exporting || restoring}>\n {exporting ? \"Bezig…\" : \"Back-up downloaden\"}\n </Button>\n <Button variant=\"secondary\" onClick={() => restoreInput.current?.click()} disabled={restoring || exporting}>\n {restoring ? \"Terugzetten…\" : \"Back-up terugzetten…\"}\n </Button>\n <input\n ref={restoreInput}\n type=\"file\"\n accept=\"application/json,.json\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void restoreFromFile(f);\n e.target.value = \"\";\n }}\n />\n </div>\n <p className=\"mt-2 text-xs text-slate-400\">\n Terugzetten vervangt de huidige content door de back-up. We maken automatisch eerst een\n back-up van de huidige stand, zodat je nooit iets kwijtraakt.\n </p>\n </Card>\n\n <SnapshotsCard siteId={siteId} />\n </>\n )}\n\n {/* Sticky: één formulier, één opslaan-knop — die moet bereikbaar blijven vanuit elke sectie,\n ook onderaan een lange (Huisstijl, Footer). */}\n <div className=\"sticky bottom-0 -mx-4 flex justify-end border-t px-4 py-3\" style={{ borderColor: \"var(--color-border)\", background: \"var(--admin-canvas)\" }}>\n <Button onClick={save}>Instellingen opslaan</Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["ok"]}
@@ -5,6 +5,8 @@ import {
5
5
  createContext,
6
6
  isValidElement,
7
7
  useContext,
8
+ useCallback,
9
+ useMemo,
8
10
  useState
9
11
  } from "react";
10
12
  import { Dialog as AstryxDialog, DialogHeader } from "@astryxdesign/core/Dialog";
@@ -32,11 +34,12 @@ function Dialog({
32
34
  }) {
33
35
  const [uncontrolled, setUncontrolled] = useState(false);
34
36
  const open = controlled ?? uncontrolled;
35
- const setOpen = (v) => {
37
+ const setOpen = useCallback((v) => {
36
38
  if (controlled === void 0) setUncontrolled(v);
37
39
  onOpenChange?.(v);
38
- };
39
- return /* @__PURE__ */ jsx(Ctx.Provider, { value: { open, setOpen }, children });
40
+ }, [controlled, onOpenChange]);
41
+ const value = useMemo(() => ({ open, setOpen }), [open, setOpen]);
42
+ return /* @__PURE__ */ jsx(Ctx.Provider, { value, children });
40
43
  }
41
44
  function DialogTrigger({ children }) {
42
45
  const { setOpen } = useDialog();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/ui/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n cloneElement,\n createContext,\n isValidElement,\n useContext,\n useState,\n type MouseEvent,\n type ReactElement,\n type ReactNode,\n} from \"react\";\nimport { Dialog as AstryxDialog, DialogHeader } from \"@astryxdesign/core/Dialog\";\nimport { cn } from \"./cn\";\n\n/**\n * Dialogen op Astryx in plaats van Radix.\n *\n * De aanroep-API blijft ongewijzigd (Dialog/DialogTrigger/DialogContent/DialogClose), zodat de\n * mediakiezer, de bevestiging, de patroonkiezer en de factuurweergave hetzelfde blijven lezen.\n *\n * Waarom dit een vereenvoudiging is: Astryx rendert een NATIEVE `<dialog>` en opent hem met\n * `showModal()`. Daarmee staat hij in de top layer van de browser, boven alles — inclusief de\n * editor-overlay. De hele reeks workarounds die het Radix-pad nodig had, vervalt:\n *\n * - portalen ín `.xicms-ve`/`.xicms-admin` omdat Radix anders buiten de gescopete admin-CSS\n * landde en volledig ongestyled rendeerde;\n * - `z-index: 10010/10011` om boven die overlay te komen;\n * - inline centreren omdat `-translate-x-1/2` in de gescopete Tailwind-context niet aansloeg,\n * waardoor de dialoog op smalle schermen rechts buiten beeld liep.\n *\n * Astryx regelt bovendien zelf scroll-lock, focus-insluiting én focusherstel naar het element dat\n * de dialoog opende, en stemt zijn Escape-afhandeling af op een actieve focus trap.\n */\ninterface DialogCtx {\n open: boolean;\n setOpen: (v: boolean) => void;\n}\nconst Ctx = createContext<DialogCtx | null>(null);\nconst useDialog = (): DialogCtx => {\n const ctx = useContext(Ctx);\n if (!ctx) throw new Error(\"Dialog-onderdelen horen binnen <Dialog> te staan.\");\n return ctx;\n};\n\n/** Kloont het kind en hangt er een klik-actie aan (het `asChild`-patroon van de vorige API). */\nfunction withClick(children: ReactNode, action: () => void): ReactNode {\n if (!isValidElement(children)) return children;\n const el = children as ReactElement<{ onClick?: (e: MouseEvent) => void }>;\n return cloneElement(el, {\n onClick: (e: MouseEvent) => {\n el.props.onClick?.(e);\n action();\n },\n });\n}\n\nexport function Dialog({\n open: controlled,\n onOpenChange,\n children,\n}: {\n open?: boolean;\n onOpenChange?: (v: boolean) => void;\n children: ReactNode;\n}) {\n const [uncontrolled, setUncontrolled] = useState(false);\n const open = controlled ?? uncontrolled;\n const setOpen = (v: boolean) => {\n if (controlled === undefined) setUncontrolled(v);\n onOpenChange?.(v);\n };\n return <Ctx.Provider value={{ open, setOpen }}>{children}</Ctx.Provider>;\n}\n\n/** `asChild` blijft geaccepteerd voor een ongewijzigde aanroep-API; het kind wordt altijd gekloond. */\nexport function DialogTrigger({ children }: { children: ReactNode; asChild?: boolean }) {\n const { setOpen } = useDialog();\n return <>{withClick(children, () => setOpen(true))}</>;\n}\n\nexport function DialogClose({ children }: { children: ReactNode; asChild?: boolean }) {\n const { setOpen } = useDialog();\n return <>{withClick(children, () => setOpen(false))}</>;\n}\n\nexport function DialogContent({\n title,\n width = \"min(92vw, 32rem)\",\n className,\n children,\n}: {\n title: string;\n /** Breedte van de dialoog. Verving de `max-w-*`-klassen: Astryx zet de breedte inline. */\n width?: number | string;\n className?: string;\n children: ReactNode;\n}) {\n const { open, setOpen } = useDialog();\n return (\n <AstryxDialog isOpen={open} onOpenChange={setOpen} width={width} purpose=\"form\" className={className}>\n <DialogHeader title={title} />\n {children}\n </AstryxDialog>\n );\n}\n\nexport function DialogDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return <p className={cn(\"text-sm text-slate-600\", className)} {...props} />;\n}\n\nexport function DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={cn(\"mt-5 flex items-center justify-end gap-2\", className)} {...props} />;\n}\n"],"mappings":";AAwES,SAMA,UANA,KA4BL,YA5BK;AAtET;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,UAAU,cAAc,oBAAoB;AACrD,SAAS,UAAU;AAyBnB,MAAM,MAAM,cAAgC,IAAI;AAChD,MAAM,YAAY,MAAiB;AACjC,QAAM,MAAM,WAAW,GAAG;AAC1B,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,mDAAmD;AAC7E,SAAO;AACT;AAGA,SAAS,UAAU,UAAqB,QAA+B;AACrE,MAAI,CAAC,eAAe,QAAQ,EAAG,QAAO;AACtC,QAAM,KAAK;AACX,SAAO,aAAa,IAAI;AAAA,IACtB,SAAS,CAAC,MAAkB;AAC1B,SAAG,MAAM,UAAU,CAAC;AACpB,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAEO,SAAS,OAAO;AAAA,EACrB,MAAM;AAAA,EACN;AAAA,EACA;AACF,GAIG;AACD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,OAAO,cAAc;AAC3B,QAAM,UAAU,CAAC,MAAe;AAC9B,QAAI,eAAe,OAAW,iBAAgB,CAAC;AAC/C,mBAAe,CAAC;AAAA,EAClB;AACA,SAAO,oBAAC,IAAI,UAAJ,EAAa,OAAO,EAAE,MAAM,QAAQ,GAAI,UAAS;AAC3D;AAGO,SAAS,cAAc,EAAE,SAAS,GAA+C;AACtF,QAAM,EAAE,QAAQ,IAAI,UAAU;AAC9B,SAAO,gCAAG,oBAAU,UAAU,MAAM,QAAQ,IAAI,CAAC,GAAE;AACrD;AAEO,SAAS,YAAY,EAAE,SAAS,GAA+C;AACpF,QAAM,EAAE,QAAQ,IAAI,UAAU;AAC9B,SAAO,gCAAG,oBAAU,UAAU,MAAM,QAAQ,KAAK,CAAC,GAAE;AACtD;AAEO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AACF,GAMG;AACD,QAAM,EAAE,MAAM,QAAQ,IAAI,UAAU;AACpC,SACE,qBAAC,gBAAa,QAAQ,MAAM,cAAc,SAAS,OAAc,SAAQ,QAAO,WAC9E;AAAA,wBAAC,gBAAa,OAAc;AAAA,IAC3B;AAAA,KACH;AAEJ;AAEO,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAA8B;AACpF,SAAO,oBAAC,OAAE,WAAW,GAAG,0BAA0B,SAAS,GAAI,GAAG,OAAO;AAC3E;AAEO,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAgC;AACjF,SAAO,oBAAC,SAAI,WAAW,GAAG,4CAA4C,SAAS,GAAI,GAAG,OAAO;AAC/F;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/ui/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n cloneElement,\n createContext,\n isValidElement,\n useContext,\n useCallback,\n useMemo,\n useState,\n type MouseEvent,\n type ReactElement,\n type ReactNode,\n} from \"react\";\nimport { Dialog as AstryxDialog, DialogHeader } from \"@astryxdesign/core/Dialog\";\nimport { cn } from \"./cn\";\n\n/**\n * Dialogen op Astryx in plaats van Radix.\n *\n * De aanroep-API blijft ongewijzigd (Dialog/DialogTrigger/DialogContent/DialogClose), zodat de\n * mediakiezer, de bevestiging, de patroonkiezer en de factuurweergave hetzelfde blijven lezen.\n *\n * Waarom dit een vereenvoudiging is: Astryx rendert een NATIEVE `<dialog>` en opent hem met\n * `showModal()`. Daarmee staat hij in de top layer van de browser, boven alles — inclusief de\n * editor-overlay. De hele reeks workarounds die het Radix-pad nodig had, vervalt:\n *\n * - portalen ín `.xicms-ve`/`.xicms-admin` omdat Radix anders buiten de gescopete admin-CSS\n * landde en volledig ongestyled rendeerde;\n * - `z-index: 10010/10011` om boven die overlay te komen;\n * - inline centreren omdat `-translate-x-1/2` in de gescopete Tailwind-context niet aansloeg,\n * waardoor de dialoog op smalle schermen rechts buiten beeld liep.\n *\n * Astryx regelt bovendien zelf scroll-lock, focus-insluiting én focusherstel naar het element dat\n * de dialoog opende, en stemt zijn Escape-afhandeling af op een actieve focus trap.\n */\ninterface DialogCtx {\n open: boolean;\n setOpen: (v: boolean) => void;\n}\nconst Ctx = createContext<DialogCtx | null>(null);\nconst useDialog = (): DialogCtx => {\n const ctx = useContext(Ctx);\n if (!ctx) throw new Error(\"Dialog-onderdelen horen binnen <Dialog> te staan.\");\n return ctx;\n};\n\n/** Kloont het kind en hangt er een klik-actie aan (het `asChild`-patroon van de vorige API). */\nfunction withClick(children: ReactNode, action: () => void): ReactNode {\n if (!isValidElement(children)) return children;\n const el = children as ReactElement<{ onClick?: (e: MouseEvent) => void }>;\n return cloneElement(el, {\n onClick: (e: MouseEvent) => {\n el.props.onClick?.(e);\n action();\n },\n });\n}\n\nexport function Dialog({\n open: controlled,\n onOpenChange,\n children,\n}: {\n open?: boolean;\n onOpenChange?: (v: boolean) => void;\n children: ReactNode;\n}) {\n const [uncontrolled, setUncontrolled] = useState(false);\n const open = controlled ?? uncontrolled;\n\n /**\n * `value` MOET gememoiseerd. Hier stond `value={{ open, setOpen }}` — een nieuw object bij elke\n * render, met daarin een nieuwe `setOpen`. Elke render vernieuwde dus de contextwaarde, waardoor\n * alle consumers opnieuw renderden, `DialogContent` nieuwe props aan de Astryx-dialoog gaf, en\n * die daar met state op reageerde. Rondje rond: \"Maximum update depth exceeded\", met een\n * onbruikbare pagina tot gevolg.\n *\n * Zichtbaar zodra je de visuele editor opende op een pagina mét werkversie — daar hangt de\n * mediakiezer in de boom. Op een pagina zonder werkversie kwam de editor nooit zo ver, en dat is\n * waarom dit lang kon blijven zitten.\n */\n const setOpen = useCallback((v: boolean) => {\n if (controlled === undefined) setUncontrolled(v);\n onOpenChange?.(v);\n }, [controlled, onOpenChange]);\n const value = useMemo(() => ({ open, setOpen }), [open, setOpen]);\n\n return <Ctx.Provider value={value}>{children}</Ctx.Provider>;\n}\n\n/** `asChild` blijft geaccepteerd voor een ongewijzigde aanroep-API; het kind wordt altijd gekloond. */\nexport function DialogTrigger({ children }: { children: ReactNode; asChild?: boolean }) {\n const { setOpen } = useDialog();\n return <>{withClick(children, () => setOpen(true))}</>;\n}\n\nexport function DialogClose({ children }: { children: ReactNode; asChild?: boolean }) {\n const { setOpen } = useDialog();\n return <>{withClick(children, () => setOpen(false))}</>;\n}\n\nexport function DialogContent({\n title,\n width = \"min(92vw, 32rem)\",\n className,\n children,\n}: {\n title: string;\n /** Breedte van de dialoog. Verving de `max-w-*`-klassen: Astryx zet de breedte inline. */\n width?: number | string;\n className?: string;\n children: ReactNode;\n}) {\n const { open, setOpen } = useDialog();\n return (\n <AstryxDialog isOpen={open} onOpenChange={setOpen} width={width} purpose=\"form\" className={className}>\n <DialogHeader title={title} />\n {children}\n </AstryxDialog>\n );\n}\n\nexport function DialogDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return <p className={cn(\"text-sm text-slate-600\", className)} {...props} />;\n}\n\nexport function DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={cn(\"mt-5 flex items-center justify-end gap-2\", className)} {...props} />;\n}\n"],"mappings":";AAwFS,SAMA,UANA,KA4BL,YA5BK;AAtFT;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,UAAU,cAAc,oBAAoB;AACrD,SAAS,UAAU;AAyBnB,MAAM,MAAM,cAAgC,IAAI;AAChD,MAAM,YAAY,MAAiB;AACjC,QAAM,MAAM,WAAW,GAAG;AAC1B,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,mDAAmD;AAC7E,SAAO;AACT;AAGA,SAAS,UAAU,UAAqB,QAA+B;AACrE,MAAI,CAAC,eAAe,QAAQ,EAAG,QAAO;AACtC,QAAM,KAAK;AACX,SAAO,aAAa,IAAI;AAAA,IACtB,SAAS,CAAC,MAAkB;AAC1B,SAAG,MAAM,UAAU,CAAC;AACpB,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAEO,SAAS,OAAO;AAAA,EACrB,MAAM;AAAA,EACN;AAAA,EACA;AACF,GAIG;AACD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,OAAO,cAAc;AAa3B,QAAM,UAAU,YAAY,CAAC,MAAe;AAC1C,QAAI,eAAe,OAAW,iBAAgB,CAAC;AAC/C,mBAAe,CAAC;AAAA,EAClB,GAAG,CAAC,YAAY,YAAY,CAAC;AAC7B,QAAM,QAAQ,QAAQ,OAAO,EAAE,MAAM,QAAQ,IAAI,CAAC,MAAM,OAAO,CAAC;AAEhE,SAAO,oBAAC,IAAI,UAAJ,EAAa,OAAe,UAAS;AAC/C;AAGO,SAAS,cAAc,EAAE,SAAS,GAA+C;AACtF,QAAM,EAAE,QAAQ,IAAI,UAAU;AAC9B,SAAO,gCAAG,oBAAU,UAAU,MAAM,QAAQ,IAAI,CAAC,GAAE;AACrD;AAEO,SAAS,YAAY,EAAE,SAAS,GAA+C;AACpF,QAAM,EAAE,QAAQ,IAAI,UAAU;AAC9B,SAAO,gCAAG,oBAAU,UAAU,MAAM,QAAQ,KAAK,CAAC,GAAE;AACtD;AAEO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AACF,GAMG;AACD,QAAM,EAAE,MAAM,QAAQ,IAAI,UAAU;AACpC,SACE,qBAAC,gBAAa,QAAQ,MAAM,cAAc,SAAS,OAAc,SAAQ,QAAO,WAC9E;AAAA,wBAAC,gBAAa,OAAc;AAAA,IAC3B;AAAA,KACH;AAEJ;AAEO,SAAS,kBAAkB,EAAE,WAAW,GAAG,MAAM,GAA8B;AACpF,SAAO,oBAAC,OAAE,WAAW,GAAG,0BAA0B,SAAS,GAAI,GAAG,OAAO;AAC3E;AAEO,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAgC;AACjF,SAAO,oBAAC,SAAI,WAAW,GAAG,4CAA4C,SAAS,GAAI,GAAG,OAAO;AAC/F;","names":[]}
@@ -15,5 +15,21 @@ interface ToastApi {
15
15
  export declare function ToastProvider({ children }: {
16
16
  children: ReactNode;
17
17
  }): import("react").JSX.Element;
18
+ /**
19
+ * `toast` MOET stabiel zijn over renders heen.
20
+ *
21
+ * Dit gaf `{ toast: (t) => … }` terug — een nieuw object met een nieuwe functie bij ELKE render.
22
+ * Op zichzelf onschuldig, tot je bedenkt dat drie effecten in het beheer `toast` in hun
23
+ * afhankelijkheden hebben (de mediakiezer, de mediabeheerder en de visuele editor). Zo'n effect
24
+ * draait dan bij elke render opnieuw, zet state, en die state veroorzaakt weer een render:
25
+ * "Maximum update depth exceeded", en een pagina die niet meer laadt.
26
+ *
27
+ * Dat maakte de visuele editor onbruikbaar op élke pagina mét werkversie — daar hangt de
28
+ * mediakiezer in de boom. Op een pagina zónder werkversie kwam de editor nooit zo ver, en precies
29
+ * daarom kon dit lang onopgemerkt blijven: wie het probeerde op een lege pagina zag niets bijzonders.
30
+ *
31
+ * `show` komt uit Astryx en biedt geen garantie over zijn eigen stabiliteit; daarom via een ref, en
32
+ * niet als afhankelijkheid. Anders verplaatsen we het probleem alleen maar één laag.
33
+ */
18
34
  export declare function useToast(): ToastApi;
19
35
  export {};
@@ -1,21 +1,25 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useMemo, useRef } from "react";
3
4
  import { useToast as useAstryxToast, ToastViewport } from "@astryxdesign/core/Toast";
4
5
  function ToastProvider({ children }) {
5
6
  return /* @__PURE__ */ jsx(ToastViewport, { position: "bottomEnd", maxVisible: 4, children });
6
7
  }
7
8
  function useToast() {
8
9
  const show = useAstryxToast();
9
- return {
10
- toast: ({ title, description, tone = "default" }) => show({
10
+ const showRef = useRef(show);
11
+ showRef.current = show;
12
+ const toast = useCallback(({ title, description, tone = "default" }) => {
13
+ showRef.current({
11
14
  body: /* @__PURE__ */ jsxs("span", { children: [
12
15
  /* @__PURE__ */ jsx("strong", { children: title }),
13
16
  description ? /* @__PURE__ */ jsx("span", { style: { display: "block", opacity: 0.8 }, children: description }) : null
14
17
  ] }),
15
18
  type: tone === "error" ? "error" : "info",
16
19
  isAutoHide: tone !== "error"
17
- })
18
- };
20
+ });
21
+ }, []);
22
+ return useMemo(() => ({ toast }), [toast]);
19
23
  }
20
24
  export {
21
25
  ToastProvider,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/ui/toast.tsx"],"sourcesContent":["\"use client\";\n\nimport { 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\nexport function useToast(): ToastApi {\n const show = useAstryxToast();\n return {\n toast: ({ title, description, tone = \"default\" }) =>\n show({\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"],"mappings":";AAiBI,cAYM,YAZN;AAdJ,SAAS,YAAY,gBAAgB,qBAAqB;AAYnD,SAAS,cAAc,EAAE,SAAS,GAA4B;AACnE,SACE,oBAAC,iBAAc,UAAS,aAAY,YAAY,GAC7C,UACH;AAEJ;AAEO,SAAS,WAAqB;AACnC,QAAM,OAAO,eAAe;AAC5B,SAAO;AAAA,IACL,OAAO,CAAC,EAAE,OAAO,aAAa,OAAO,UAAU,MAC7C,KAAK;AAAA,MACH,MACE,qBAAC,UACC;AAAA,4BAAC,YAAQ,iBAAM;AAAA,QACd,cAAc,oBAAC,UAAK,OAAO,EAAE,SAAS,SAAS,SAAS,IAAI,GAAI,uBAAY,IAAU;AAAA,SACzF;AAAA,MAEF,MAAM,SAAS,UAAU,UAAU;AAAA,MACnC,YAAY,SAAS;AAAA,IACvB,CAAC;AAAA,EACL;AACF;","names":[]}
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,UAAU,MAAM;AACzF,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,SACzF;AAAA,MAEF,MAAM,SAAS,UAAU,UAAU;AAAA,MACnC,YAAY,SAAS;AAAA,IACvB,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,SAAO,QAAQ,OAAO,EAAE,MAAM,IAAI,CAAC,KAAK,CAAC;AAC3C;","names":[]}
@@ -6,22 +6,19 @@ import { useSiteConfig } from "../../config.js";
6
6
  import { useRegistry } from "../../blocks/context.js";
7
7
  import { introspectToFields } from "../lib/zodFields.js";
8
8
  import { BlockErrorBoundary } from "../../blocks/BlockErrorBoundary.js";
9
- import { InlineImageEditProvider } from "../../media/ImageContext.js";
10
9
  import { MediaPickerDialog } from "../media/MediaPicker.js";
11
- import { EmptyState } from "@astryxdesign/core/EmptyState";
12
10
  import { useFocusTrap, useScrollLock } from "@astryxdesign/core/hooks";
13
11
  import { useNestedDialogOpen } from "../lib/useNestedDialog.js";
14
12
  import { useThemeVars } from "../../XiCMSProvider.js";
15
13
  import { buildBlockAddOptions } from "../../blocks/registry.js";
16
- import { safeParse } from "zod/v4/core";
17
14
  import DOMPurify from "dompurify";
18
15
  import { createPage, getDraftByPath, saveDraft, publishPage, subscribeDraft, subscribeVersions, restoreVersion } from "../lib/pagesClient.js";
19
16
  import { isDemoActive } from "../../demo/demoStore.js";
20
17
  import { builtinBlockEditors, GenericEditor } from "../blockEditors/index.js";
21
18
  import { StyleEditor } from "../blockEditors/StyleEditor.js";
22
19
  import { siteLocales, parseLocaleSlugs, localizeBlocks } from "../../i18n/overlay.js";
20
+ import { useEditor } from "../../editor/EditorProvider.js";
23
21
  import { TranslationPanel } from "./TranslationPanel.js";
24
- import { reorderBlocks } from "./reorder.js";
25
22
  import { PatternInserter } from "../patterns/PatternInserter.js";
26
23
  import { Button, Field, Input, Textarea } from "../ui/primitives.js";
27
24
  import { Select } from "../ui/select.js";
@@ -257,6 +254,15 @@ function VisualEditorInner({ onClose }) {
257
254
  () => isTranslating ? localizeBlocks(blocks, overlay) : blocks,
258
255
  [blocks, isTranslating, overlay]
259
256
  );
257
+ const { publishDraft } = useEditor();
258
+ const laatstGepubliceerd = useRef("");
259
+ useEffect(() => {
260
+ if (!draft) return;
261
+ const vinger = stableStringify(displayBlocks);
262
+ if (vinger === laatstGepubliceerd.current) return;
263
+ laatstGepubliceerd.current = vinger;
264
+ publishDraft(path, displayBlocks);
265
+ }, [draft, displayBlocks, path, publishDraft]);
260
266
  const commitInline = useCallback(
261
267
  (key, field, value) => {
262
268
  mutateDraft(
@@ -312,6 +318,44 @@ function VisualEditorInner({ onClose }) {
312
318
  },
313
319
  [isTranslating, setTranslation, commitInline]
314
320
  );
321
+ const registryRef = useRef(registry);
322
+ registryRef.current = registry;
323
+ const [gears, setGears] = useState([]);
324
+ useEffect(() => {
325
+ if (!draft) {
326
+ setGears((v) => v.length === 0 ? v : []);
327
+ return;
328
+ }
329
+ const meet = () => {
330
+ const gevonden = [];
331
+ for (const el of Array.from(document.querySelectorAll("[data-xicms-block]"))) {
332
+ const key = el.dataset.xicmsBlock;
333
+ if (!key) continue;
334
+ const r = el.getBoundingClientRect();
335
+ if (r.width === 0 && r.height === 0) continue;
336
+ const type = el.dataset.xicmsBlockType ?? "";
337
+ gevonden.push({
338
+ key,
339
+ top: Math.max(r.top + 8, 56),
340
+ left: Math.max(r.right - 36, 8),
341
+ label: registryRef.current.get(type)?.label ?? type
342
+ });
343
+ }
344
+ setGears((vorige) => {
345
+ if (vorige.length === gevonden.length && vorige.every((v, i) => v.key === gevonden[i].key && v.top === gevonden[i].top && v.left === gevonden[i].left)) {
346
+ return vorige;
347
+ }
348
+ return gevonden;
349
+ });
350
+ };
351
+ meet();
352
+ window.addEventListener("scroll", meet, true);
353
+ window.addEventListener("resize", meet);
354
+ return () => {
355
+ window.removeEventListener("scroll", meet, true);
356
+ window.removeEventListener("resize", meet);
357
+ };
358
+ }, [draft, displayBlocks, registryRef]);
315
359
  const [spots, setSpots] = useState([]);
316
360
  const targetsRef = useRef([]);
317
361
  const editingRef = useRef(null);
@@ -343,7 +387,7 @@ function VisualEditorInner({ onClose }) {
343
387
  setSpots([]);
344
388
  return;
345
389
  }
346
- const host = canvasRef.current?.querySelector(`[data-ve-content="${selectedKey}"]`);
390
+ const host = document.querySelector(`[data-xicms-block="${selectedKey}"]`);
347
391
  if (!host) return;
348
392
  const geselecteerd = blocksRef.current.find((b) => b._key === selectedKey);
349
393
  const type = geselecteerd?._type ?? "";
@@ -603,7 +647,7 @@ function VisualEditorInner({ onClose }) {
603
647
  }
604
648
  setConflict(false);
605
649
  }, [siteId, path, mutateDraft]);
606
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "xicms-ve__shell", role: "dialog", "aria-modal": "true", "aria-label": `Pagina bewerken: ${path}`, children: [
650
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "xicms-ve__shell", role: "group", "aria-label": `Pagina bewerken: ${path}`, children: [
607
651
  /* @__PURE__ */ jsxs("header", { className: "xicms-ve__bar", children: [
608
652
  /* @__PURE__ */ jsxs("strong", { className: "xicms-ve__title", children: [
609
653
  "Bewerken \xB7 ",
@@ -645,7 +689,14 @@ function VisualEditorInner({ onClose }) {
645
689
  ),
646
690
  /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-ve__histbtn", disabled: !canUndo, onClick: undo, title: "Ongedaan maken (Ctrl+Z)", "aria-label": "Ongedaan maken (Ctrl+Z)", children: "\u21B6" }),
647
691
  /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-ve__histbtn", disabled: !canRedo, onClick: redo, title: "Opnieuw (Ctrl+Shift+Z)", "aria-label": "Opnieuw (Ctrl+Shift+Z)", children: "\u21B7" }),
648
- /* @__PURE__ */ jsx("span", { className: "xicms-ve__status", "aria-live": "polite", children: status === "saving" ? "Opslaan\u2026" : status === "saved" ? "\u2713 Opgeslagen" : status === "error" ? "\u26A0 Niet opgeslagen" : "" }),
692
+ status === "none" && /* @__PURE__ */ jsxs("span", { className: "xicms-ve__balkmelding", children: [
693
+ "Nog geen werkversie",
694
+ /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => void handleCreatePage(), children: "+ Pagina aanmaken" })
695
+ ] }),
696
+ /* @__PURE__ */ jsxs("span", { className: "xicms-ve__status", "aria-live": "polite", children: [
697
+ status === "loading" ? "Werkversie laden\u2026" : null,
698
+ status === "saving" ? "Opslaan\u2026" : status === "saved" ? "\u2713 Opgeslagen" : status === "error" ? "\u26A0 Niet opgeslagen" : ""
699
+ ] }),
649
700
  status === "error" && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: () => void flushSave().catch(() => setStatus("error")), children: "Opnieuw opslaan" }),
650
701
  /* @__PURE__ */ jsxs("div", { className: "xicms-ve__bar-actions", children: [
651
702
  /* @__PURE__ */ jsx(Button, { size: "sm", disabled: publishing || !draft, onClick: onPublish, children: publishing ? "Bezig\u2026" : "Publiceren" }),
@@ -657,122 +708,17 @@ function VisualEditorInner({ onClose }) {
657
708
  /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => void reloadRemote(), children: "Nieuwste versie laden" }),
658
709
  /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: () => setConflict(false), children: "Mijn versie behouden" })
659
710
  ] }),
660
- /* @__PURE__ */ jsxs("div", { className: `xicms-ve__body${panelOpen ? " is-panel-open" : ""}`, children: [
661
- /* @__PURE__ */ jsxs("main", { className: "xicms-ve__canvas xicms-root", "data-dark": darkAuto ? "auto" : void 0, style: themeVars, children: [
662
- status === "loading" && /* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Laden\u2026" }),
663
- status === "none" && /* @__PURE__ */ jsxs("div", { className: "xicms-ve__empty", children: [
664
- /* @__PURE__ */ jsx(
665
- EmptyState,
666
- {
667
- icon: "\u{1F4C4}",
668
- title: "Geen werkversie gevonden",
669
- description: `Voor deze pagina (${path}) bestaat nog geen werkversie. Maak hem hier direct aan, of ga naar /beheer \u2192 Pagina's.`
670
- }
671
- ),
672
- /* @__PURE__ */ jsx("p", { style: { textAlign: "center", marginTop: "1rem" }, children: /* @__PURE__ */ jsx(Button, { onClick: () => void handleCreatePage(), children: "+ Deze pagina aanmaken" }) })
711
+ /* @__PURE__ */ jsx("div", { className: `xicms-ve__body${panelOpen ? " is-panel-open" : ""}`, children: panelOpen && /* @__PURE__ */ jsxs("aside", { className: "xicms-ve__panel", children: [
712
+ /* @__PURE__ */ jsxs("div", { className: "xicms-ve__inserter", children: [
713
+ /* @__PURE__ */ jsxs("span", { children: [
714
+ "Toevoegen",
715
+ selected ? " (na selectie)" : "",
716
+ ":"
673
717
  ] }),
674
- draft && /* @__PURE__ */ jsxs(
675
- "div",
676
- {
677
- className: "container",
678
- ref: canvasRef,
679
- onClickCapture: (e) => {
680
- const el = e.target;
681
- if (el?.closest?.("a[href]")) e.preventDefault();
682
- },
683
- children: [
684
- blocks.length === 0 && /* @__PURE__ */ jsx(EmptyState, { icon: "\u{1F9F1}", title: "Nog geen blokken", description: "Voeg hieronder je eerste blok toe." }),
685
- displayBlocks.map((b, i) => {
686
- const def = registry.get(b._type);
687
- const Render = def?.Render;
688
- const safe = (() => {
689
- const parsed = def?.schema ? safeParse(def.schema, b) : void 0;
690
- return parsed?.success ? parsed.data : b;
691
- })();
692
- const dropClass = dropIndex === i ? " is-drop-before" : dropIndex === i + 1 ? " is-drop-after" : "";
693
- return /* @__PURE__ */ jsxs(
694
- "div",
695
- {
696
- className: `xicms-ve-block${b._key === selectedKey ? " is-selected" : ""}${dragIndex === i ? " is-dragging" : ""}${dropClass}`,
697
- onDragOver: (e) => {
698
- if (dragIndex === null) return;
699
- e.preventDefault();
700
- e.dataTransfer.dropEffect = "move";
701
- const rect = e.currentTarget.getBoundingClientRect();
702
- setDropIndex(e.clientY < rect.top + rect.height / 2 ? i : i + 1);
703
- },
704
- onDrop: (e) => {
705
- if (dragIndex === null || dropIndex === null) return;
706
- e.preventDefault();
707
- setBlocks(reorderBlocks(blocks, dragIndex, dropIndex));
708
- endDrag();
709
- },
710
- children: [
711
- /* @__PURE__ */ jsxs("div", { className: "xicms-ve-block__toolbar", children: [
712
- /* @__PURE__ */ jsx(
713
- "button",
714
- {
715
- type: "button",
716
- className: "xicms-ve-block__drag",
717
- draggable: true,
718
- onDragStart: (e) => {
719
- draggingRef.current = true;
720
- setDragIndex(i);
721
- e.dataTransfer.effectAllowed = "move";
722
- e.dataTransfer.setData("text/plain", String(i));
723
- const blockEl = e.currentTarget.closest(".xicms-ve-block");
724
- if (blockEl) e.dataTransfer.setDragImage(blockEl, 24, 24);
725
- },
726
- onDragEnd: endDrag,
727
- "aria-label": `Versleep ${def?.label ?? b._type}`,
728
- title: "Slepen om te verplaatsen",
729
- children: "\u22EE\u22EE"
730
- }
731
- ),
732
- /* @__PURE__ */ jsx("span", { className: "xicms-ve-block__label", children: def?.label ?? b._type }),
733
- /* @__PURE__ */ jsx(
734
- "button",
735
- {
736
- type: "button",
737
- onClick: () => {
738
- setSelectedKey(b._key);
739
- setPanelOpen(true);
740
- },
741
- "aria-label": `Instellingen van ${def?.label ?? b._type}`,
742
- title: "Instellingen (link, kleur, opties)",
743
- children: "\u2699"
744
- }
745
- ),
746
- /* @__PURE__ */ jsx("button", { type: "button", disabled: i === 0, onClick: () => move(i, -1), "aria-label": "Omhoog", children: "\u2191" }),
747
- /* @__PURE__ */ jsx("button", { type: "button", disabled: i === blocks.length - 1, onClick: () => move(i, 1), "aria-label": "Omlaag", children: "\u2193" }),
748
- b._type === "symbolRef" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void detachSymbol(i), "aria-label": "Symbool loskoppelen", title: "Loskoppelen (losse kopie)", children: "\u{1F517}\u2702" }),
749
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => duplicate(i), "aria-label": "Dupliceren", children: "\u2398" }),
750
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void remove(i), "aria-label": "Verwijderen", children: "\u{1F5D1}" })
751
- ] }),
752
- /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-ve-block__select", "aria-label": `Selecteer ${def?.label ?? b._type}`, onClick: () => setSelectedKey(b._key) }),
753
- /* @__PURE__ */ jsx("div", { className: "xicms-ve-block__content", "data-ve-content": b._key, children: Render ? /* @__PURE__ */ jsx(BlockErrorBoundary, { mode: "preview", type: b._type, blockKey: b._key, children: /* @__PURE__ */ jsx(InlineImageEditProvider, { onReplace: b._key === selectedKey ? (field) => setImgPicker({ key: b._key, field }) : null, children: /* @__PURE__ */ jsx(Render, { block: safe, siteId, mode: "preview" }) }) }) : /* @__PURE__ */ jsxs("p", { className: "xicms-ve__hint", children: [
754
- "Onbekend blok: ",
755
- b._type
756
- ] }) })
757
- ]
758
- },
759
- b._key
760
- );
761
- }),
762
- /* @__PURE__ */ jsxs("div", { className: "xicms-ve__inserter", children: [
763
- /* @__PURE__ */ jsxs("span", { children: [
764
- "Toevoegen",
765
- selected ? " (na selectie)" : "",
766
- ":"
767
- ] }),
768
- /* @__PURE__ */ jsx("div", { className: "xicms-ve__inserter-select", children: /* @__PURE__ */ jsx(Select, { value: "", placeholder: "Kies een blok\u2026", onValueChange: (v) => v && addBlock(v), options: addOptions }) }),
769
- /* @__PURE__ */ jsx(PatternInserter, { onInsert: insertAfterSelected })
770
- ] })
771
- ]
772
- }
773
- )
718
+ /* @__PURE__ */ jsx("div", { className: "xicms-ve__inserter-select", children: /* @__PURE__ */ jsx(Select, { value: "", placeholder: "Kies een blok\u2026", onValueChange: (v) => v && addBlock(v), options: addOptions }) }),
719
+ /* @__PURE__ */ jsx(PatternInserter, { onInsert: insertAfterSelected })
774
720
  ] }),
775
- panelOpen && /* @__PURE__ */ jsx("aside", { className: "xicms-ve__panel", children: selected && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
721
+ selected && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
776
722
  /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold", children: [
777
723
  registry.get(selected._type)?.label ?? selected._type,
778
724
  " \u2014 vertaling (",
@@ -826,8 +772,8 @@ function VisualEditorInner({ onClose }) {
826
772
  }
827
773
  }
828
774
  )
829
- ] }) : /* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Klik op een blok om het te bewerken." }) })
830
- ] }),
775
+ ] }) : /* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Klik op een blok om het te bewerken." })
776
+ ] }) }),
831
777
  /* @__PURE__ */ jsx(
832
778
  MediaPickerDialog,
833
779
  {
@@ -846,6 +792,26 @@ function VisualEditorInner({ onClose }) {
846
792
  }
847
793
  }
848
794
  ),
795
+ gears.map((g) => /* @__PURE__ */ jsx(
796
+ "button",
797
+ {
798
+ type: "button",
799
+ className: `xicms-ve-gear${g.key === selectedKey ? " is-on" : ""}`,
800
+ style: { top: g.top, left: g.left },
801
+ title: `Instellingen van ${g.label}`,
802
+ "aria-label": `Instellingen van ${g.label}`,
803
+ onMouseDown: (e) => e.preventDefault(),
804
+ onClick: () => {
805
+ setSelectedKey(g.key);
806
+ setPanelOpen(true);
807
+ },
808
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
809
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
810
+ /* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })
811
+ ] })
812
+ },
813
+ g.key
814
+ )),
849
815
  spots.map((s, i) => /* @__PURE__ */ jsx(
850
816
  "button",
851
817
  {