@xiwebdesign/xicms-core 5.0.6 → 5.1.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.
Files changed (44) hide show
  1. package/dist/XiCMSProvider.d.ts +1 -1
  2. package/dist/XiCMSProvider.js +42 -30
  3. package/dist/XiCMSProvider.js.map +1 -1
  4. package/dist/admin/editors/RichTextEditor.js +7 -1
  5. package/dist/admin/editors/RichTextEditor.js.map +1 -1
  6. package/dist/admin/ui/ArrayEditor.d.ts +6 -1
  7. package/dist/admin/ui/ArrayEditor.js +22 -3
  8. package/dist/admin/ui/ArrayEditor.js.map +1 -1
  9. package/dist/admin/visual/VisualEditor.js +35 -9
  10. package/dist/admin/visual/VisualEditor.js.map +1 -1
  11. package/dist/blocks/builtins/Agenda.js +2 -1
  12. package/dist/blocks/builtins/Agenda.js.map +1 -1
  13. package/dist/blocks/builtins/Breadcrumbs.js +2 -1
  14. package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
  15. package/dist/blocks/builtins/Contact.js +2 -1
  16. package/dist/blocks/builtins/Contact.js.map +1 -1
  17. package/dist/blocks/builtins/Faq.js +2 -1
  18. package/dist/blocks/builtins/Faq.js.map +1 -1
  19. package/dist/blocks/builtins/Jobs.js +2 -1
  20. package/dist/blocks/builtins/Jobs.js.map +1 -1
  21. package/dist/blocks/builtins/OpeningHours.js +2 -1
  22. package/dist/blocks/builtins/OpeningHours.js.map +1 -1
  23. package/dist/blocks/builtins/Pricing.js +2 -1
  24. package/dist/blocks/builtins/Pricing.js.map +1 -1
  25. package/dist/blocks/builtins/Search.js +18 -8
  26. package/dist/blocks/builtins/Search.js.map +1 -1
  27. package/dist/blocks/builtins/Team.js +2 -1
  28. package/dist/blocks/builtins/Team.js.map +1 -1
  29. package/dist/blocks/builtins/Testimonials.js +2 -1
  30. package/dist/blocks/builtins/Testimonials.js.map +1 -1
  31. package/dist/blocks/builtins/jsonld.d.ts +9 -0
  32. package/dist/blocks/builtins/jsonld.js +7 -0
  33. package/dist/blocks/builtins/jsonld.js.map +1 -0
  34. package/dist/components/News.js +2 -1
  35. package/dist/components/News.js.map +1 -1
  36. package/dist/i18n/overlay.d.ts +13 -4
  37. package/dist/i18n/overlay.js +54 -8
  38. package/dist/i18n/overlay.js.map +1 -1
  39. package/dist/server/index.d.ts +1 -0
  40. package/dist/server/index.js +2 -0
  41. package/dist/server/index.js.map +1 -1
  42. package/dist/server/pages.js +34 -13
  43. package/dist/server/pages.js.map +1 -1
  44. package/package.json +1 -1
@@ -54,7 +54,7 @@ interface ProviderProps {
54
54
  * - de themavariabelen als CSS custom properties;
55
55
  * - App Check (alleen als geconfigureerd) en de EditBar/DemoLock.
56
56
  */
57
- export declare function XiCMSProvider({ config, blocks, modules, overrides, astryxTheme, linkComponent, imageComponent, children }: ProviderProps): import("react").JSX.Element;
57
+ export declare function XiCMSProvider({ config, blocks, modules, overrides, astryxTheme, linkComponent, imageComponent, customBlocksData, children }: ProviderProps): import("react").JSX.Element;
58
58
  /**
59
59
  * Is deze (hex-)kleur donker? Bepaalt de Astryx-thememodus voor sites met een vaste donkere
60
60
  * achtergrond (bv. presets donker-modern/luxe-donker, `dark.auto: false`): zonder deze afleiding
@@ -2,7 +2,8 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useState } from "react";
4
4
  import { ConfigProvider } from "./config";
5
- import { AuthProvider } from "./auth/AuthProvider";
5
+ import { AuthProvider, useAuth } from "./auth/AuthProvider";
6
+ import { canEdit } from "./auth/roles";
6
7
  import { EditBar } from "./components/EditBar";
7
8
  import { DemoLock } from "./lock/DemoLock";
8
9
  import { RegistryProvider } from "./blocks/context";
@@ -36,7 +37,7 @@ function warnOnce(message) {
36
37
  warned.add(message);
37
38
  console.warn(message);
38
39
  }
39
- function XiCMSProvider({ config, blocks = NO_BLOCKS, modules = NO_MODULES, overrides, astryxTheme, linkComponent, imageComponent, children }) {
40
+ function XiCMSProvider({ config, blocks = NO_BLOCKS, modules = NO_MODULES, overrides, astryxTheme, linkComponent, imageComponent, customBlocksData, children }) {
40
41
  useEffect(() => {
41
42
  initAppCheck();
42
43
  }, []);
@@ -55,7 +56,7 @@ function XiCMSProvider({ config, blocks = NO_BLOCKS, modules = NO_MODULES, overr
55
56
  }
56
57
  }
57
58
  }, [config, modules]);
58
- return /* @__PURE__ */ jsx(ConfigProvider, { config, children: /* @__PURE__ */ jsx(SiteSettingsProvider, { siteId: config.siteId, children: /* @__PURE__ */ jsx(ModuleLayer, { config, blocks, modules, overrides, astryxTheme, linkComponent, imageComponent, children }) }) });
59
+ return /* @__PURE__ */ jsx(ConfigProvider, { config, children: /* @__PURE__ */ jsx(SiteSettingsProvider, { siteId: config.siteId, children: /* @__PURE__ */ jsx(ModuleLayer, { config, blocks, modules, overrides, astryxTheme, linkComponent, imageComponent, customBlocksData, children }) }) });
59
60
  }
60
61
  function themeCssVars(theme) {
61
62
  const auto = Boolean(theme.dark?.auto);
@@ -132,7 +133,9 @@ function ModuleLayer({
132
133
  const { settings } = useSiteSettings();
133
134
  const theme = useMemo(() => mergeThemeTokens(config.theme, settings.theme), [config.theme, settings.theme]);
134
135
  const cssVars = useMemo(() => themeCssVars(theme), [theme]);
135
- setDemoActive(Boolean(config.demoEditing));
136
+ useEffect(() => {
137
+ setDemoActive(Boolean(config.demoEditing));
138
+ }, [config.demoEditing]);
136
139
  const resolved = useMemo(
137
140
  () => resolveModules([...builtinModules, ...modules], {
138
141
  modules: config.modules,
@@ -142,46 +145,55 @@ function ModuleLayer({
142
145
  [config.modules, config.features, modules, settings.modules]
143
146
  );
144
147
  const [customBlocks, setCustomBlocks] = useState(customBlocksData ?? []);
148
+ const customDefs = useMemo(() => customBlocks.map(customBlockToDefinition), [customBlocks]);
149
+ const registry = useMemo(
150
+ () => applyBlockOverrides(mergeRegistry(mergeRegistry(createRegistry(resolved.blocks), customDefs), blocks), overrides),
151
+ [resolved.blocks, customDefs, blocks, overrides]
152
+ );
153
+ return /* @__PURE__ */ jsx(ModulesProvider, { value: resolved, children: /* @__PURE__ */ jsx(RegistryProvider, { registry, children: /* @__PURE__ */ jsxs(AuthProvider, { siteId: config.siteId, demo: Boolean(config.demoEditing), children: [
154
+ /* @__PURE__ */ jsx(CustomBlocksSync, { siteId: config.siteId, onData: setCustomBlocks }),
155
+ /* @__PURE__ */ jsxs(EditorProvider, { children: [
156
+ theme.fontUrl && /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: theme.fontUrl }),
157
+ /* @__PURE__ */ jsx(Theme, { theme: astryxTheme ?? neutralTheme, mode: theme.dark?.auto ? "system" : isDarkColor(theme.background) ? "dark" : "light", children: /* @__PURE__ */ jsx(LinkProvider, { component: linkComponent ?? "a", children: /* @__PURE__ */ jsx(ImageProvider, { component: imageComponent, children: /* @__PURE__ */ jsxs(
158
+ "div",
159
+ {
160
+ className: "xicms-root",
161
+ "data-dark": theme.dark?.auto ? "auto" : void 0,
162
+ style: cssVars,
163
+ children: [
164
+ /* @__PURE__ */ jsx(DemoStudio, {}),
165
+ children,
166
+ /* @__PURE__ */ jsx(EditBar, {}),
167
+ /* @__PURE__ */ jsx(DemoLock, {}),
168
+ resolved.activeIds.has("consent") && /* @__PURE__ */ jsx(ConsentBanner, { settings: resolved.settings.consent })
169
+ ]
170
+ }
171
+ ) }) }) })
172
+ ] })
173
+ ] }) }) });
174
+ }
175
+ function CustomBlocksSync({ siteId, onData }) {
176
+ const { role } = useAuth();
177
+ const editing = canEdit(role);
145
178
  useEffect(() => {
179
+ if (!editing) return;
146
180
  const db = getDb();
147
181
  if (!db) return;
148
182
  return onSnapshot(
149
- collection(db, "sites", config.siteId, "customBlocks"),
183
+ collection(db, "sites", siteId, "customBlocks"),
150
184
  (snap) => {
151
185
  const defs = [];
152
186
  snap.forEach((d) => {
153
187
  const res = CustomBlockSchema.safeParse({ ...d.data(), type: d.data().type ?? d.id });
154
188
  if (res.success) defs.push(res.data);
155
189
  });
156
- setCustomBlocks(defs);
190
+ onData(defs);
157
191
  },
158
192
  () => void 0
159
193
  // leesfout → SSR-set blijft staan
160
194
  );
161
- }, [config.siteId]);
162
- const customDefs = useMemo(() => customBlocks.map(customBlockToDefinition), [customBlocks]);
163
- const registry = useMemo(
164
- () => applyBlockOverrides(mergeRegistry(mergeRegistry(createRegistry(resolved.blocks), customDefs), blocks), overrides),
165
- [resolved.blocks, customDefs, blocks, overrides]
166
- );
167
- return /* @__PURE__ */ jsx(ModulesProvider, { value: resolved, children: /* @__PURE__ */ jsx(RegistryProvider, { registry, children: /* @__PURE__ */ jsx(AuthProvider, { siteId: config.siteId, demo: Boolean(config.demoEditing), children: /* @__PURE__ */ jsxs(EditorProvider, { children: [
168
- theme.fontUrl && /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: theme.fontUrl }),
169
- /* @__PURE__ */ jsx(Theme, { theme: astryxTheme ?? neutralTheme, mode: theme.dark?.auto ? "system" : isDarkColor(theme.background) ? "dark" : "light", children: /* @__PURE__ */ jsx(LinkProvider, { component: linkComponent ?? "a", children: /* @__PURE__ */ jsx(ImageProvider, { component: imageComponent, children: /* @__PURE__ */ jsxs(
170
- "div",
171
- {
172
- className: "xicms-root",
173
- "data-dark": theme.dark?.auto ? "auto" : void 0,
174
- style: cssVars,
175
- children: [
176
- /* @__PURE__ */ jsx(DemoStudio, {}),
177
- children,
178
- /* @__PURE__ */ jsx(EditBar, {}),
179
- /* @__PURE__ */ jsx(DemoLock, {}),
180
- resolved.activeIds.has("consent") && /* @__PURE__ */ jsx(ConsentBanner, { settings: resolved.settings.consent })
181
- ]
182
- }
183
- ) }) }) })
184
- ] }) }) }) });
195
+ }, [editing, siteId, onData]);
196
+ return null;
185
197
  }
186
198
  export {
187
199
  XiCMSProvider,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/XiCMSProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { ConfigProvider } from \"./config\";\nimport { AuthProvider } from \"./auth/AuthProvider\";\nimport { EditBar } from \"./components/EditBar\";\nimport { DemoLock } from \"./lock/DemoLock\";\nimport { RegistryProvider } from \"./blocks/context\";\nimport { getDb } from \"./firebase/client\";\nimport { collection, onSnapshot } from \"firebase/firestore\";\nimport { createRegistry, mergeRegistry, type BlockDefinition } from \"./blocks/registry\";\nimport { CustomBlockSchema, type CustomBlock as CustomBlockData } from \"./schema/customBlocks\";\nimport { customBlockToDefinition } from \"./blocks/customTemplate/definition\";\nimport { resolveModules } from \"./modules/registry\";\nimport { ModulesProvider } from \"./modules/context\";\nimport { applyBlockOverrides, type BlockOverrides } from \"./modules/overrides\";\nimport { builtinModules } from \"./modules/builtins\";\nimport type { ModuleDefinition, ModulesSiteConfig } from \"./modules/types\";\nimport { ConsentBanner, type ConsentSettings } from \"./modules/consent/ConsentBanner\";\nimport { EditorProvider } from \"./editor/EditorProvider\";\nimport { SiteSettingsProvider, useSiteSettings } from \"./settings/useSiteSettings\";\nimport { mergeThemeTokens, type ThemeTokens } from \"./schema/settings\";\nimport { initAppCheck } from \"./firebase/appcheck\";\nimport { DemoStudio } from \"./demo/DemoStudio\";\nimport { setDemoActive } from \"./demo/demoStore\";\nimport { Theme, type DefinedTheme } from \"@astryxdesign/core/theme\";\nimport { neutralTheme } from \"@astryxdesign/theme-neutral/built\";\nimport { LinkProvider, type LinkComponentType } from \"@astryxdesign/core/Link\";\nimport { ImageProvider, type ImageComponentType } from \"./media/ImageContext\";\nimport { validateSiteConfig, type SiteConfig } from \"./schema/siteConfig\";\nimport { validateModuleContract } from \"./modules/contract\";\n\ninterface ProviderProps {\n config: SiteConfig;\n /** Losse klant-blokken (via `defineBlock`) buiten een module om. Altijd actief. */\n blocks?: BlockDefinition[];\n /**\n * Klant-modules (via `defineModule`) bovenop de ingebouwde. Worden gemerged op `id` (klant wint)\n * en respecteren `config.modules`/`config.features` voor activatie.\n */\n modules?: ModuleDefinition[];\n /**\n * Per-site presentatie-overrides per block-type (`{ gallery: { Render: MijnGalerij } }`). Vervangt\n * alleen het uiterlijk; schema/editor/logica blijven van core. De krachtigste maatwerk-naad.\n */\n overrides?: BlockOverrides;\n /**\n * SSR-startset van custom blocks (5.0): geef het resultaat van `getSiteCustomBlocks(siteId)`\n * (server) door zodat runtime-gedefinieerde blokken al in de server-HTML zitten. Zonder deze\n * prop laadt de provider ze alsnog client-side (werkend, maar pas na hydratie zichtbaar).\n */\n customBlocksData?: CustomBlockData[];\n /**\n * Astryx-basistheme voor de hele site (radius-/spacing-/neutralen-karakter). Standaard `neutral`.\n * Een site kan een ander pakket kiezen (`@astryxdesign/theme-butter`, `-stone`, …) of een eigen\n * `defineTheme(...)` doorgeven voor component-overrides — zónder core te forken.\n */\n astryxTheme?: DefinedTheme;\n /**\n * Framework-link-component (bv. Next `Link`) voor soepele client-navigatie + prefetch. Astryx-\n * componenten met een `href` (knoppen/links/tabs) gebruiken 'm dan i.p.v. een rauwe `<a>` (= volledige\n * page-reload). Core importeert `next/link` niet zelf (optionele peer) — geef 'm hier door vanuit de site.\n */\n linkComponent?: LinkComponentType;\n /**\n * Framework-beeldcomponent (bv. Next `Image`) voor automatische beeldoptimalisatie (responsive\n * srcset, AVIF/WebP, geen layout-shift). Core importeert `next/image` niet zelf — geef 'm hier door.\n */\n imageComponent?: ImageComponentType;\n children: ReactNode;\n}\n\n// Stabiele lege defaults: een `= []`-defaultparameter maakt bij ELKE render een nieuwe\n// array-referentie en laat memo's/effects met `modules`/`blocks` in de deps onnodig hervuren.\nconst NO_BLOCKS: BlockDefinition[] = [];\nconst NO_MODULES: ModuleDefinition[] = [];\n\n// Dev-waarschuwingen één keer per boodschap (React strict-mode dubbelt effects; zonder dedupe\n// verschijnt dezelfde config-waarschuwing telkens opnieuw in de console).\nconst warned = new Set<string>();\nfunction warnOnce(message: string) {\n if (warned.has(message)) return;\n warned.add(message);\n console.warn(message);\n}\n\n/**\n * Wikkel de hele site in deze provider (in `app/layout.tsx`). Hij zet:\n * - de SiteConfig-context (siteId, theme, mode) + live publieke settings (footer/nav/modules);\n * - auth + content (Firebase, env-gedreven; werkt ook zonder Firebase);\n * - de block-registry (ingebouwde + klant-blokken) voor de page-renderer;\n * - de themavariabelen als CSS custom properties;\n * - App Check (alleen als geconfigureerd) en de EditBar/DemoLock.\n */\nexport function XiCMSProvider({ config, blocks = NO_BLOCKS, modules = NO_MODULES, overrides, astryxTheme, linkComponent, imageComponent, children }: ProviderProps) {\n useEffect(() => {\n initAppCheck();\n }, []);\n\n // Dev-vangnet voor (agent-)geschreven configs: meld config- en module-contractfouten meteen in\n // de console i.p.v. een subtiel kapotte site. In productie geen werk — en er wordt nooit gegooid.\n useEffect(() => {\n if (process.env.NODE_ENV === \"production\") return;\n const configIssues = validateSiteConfig(config);\n if (configIssues.length > 0) {\n warnOnce(`[xicms] site.config.ts bevat problemen:\\n- ${configIssues.join(\"\\n- \")}`);\n }\n if (modules.length > 0) {\n const contractIssues = validateModuleContract(modules);\n if (contractIssues.length > 0) {\n warnOnce(`[xicms] klant-modules voldoen niet aan het module-contract:\\n- ${contractIssues.join(\"\\n- \")}`);\n }\n }\n }, [config, modules]);\n\n return (\n <ConfigProvider config={config}>\n <SiteSettingsProvider siteId={config.siteId}>\n <ModuleLayer config={config} blocks={blocks} modules={modules} overrides={overrides} astryxTheme={astryxTheme} linkComponent={linkComponent} imageComponent={imageComponent}>\n {children}\n </ModuleLayer>\n </SiteSettingsProvider>\n </ConfigProvider>\n );\n}\n\n/**\n * Theme-tokens → CSS custom properties. Puur; wordt in `ModuleLayer` op het LIVE-gemergde thema\n * toegepast (SiteConfig + `settings/public.theme`), zodat huisstijl-wijzigingen uit /beheer direct\n * op de site verschijnen — net als module-instellingen.\n */\nfunction themeCssVars(theme: ThemeTokens): Record<string, string> {\n // Dark-auto via light-dark(): deze variabelen staan INLINE op `.xicms-root`, en inline wint\n // altijd van het `@media (prefers-color-scheme: dark)`-blok in styles.css. Alleen dat blok kan\n // een inline lichte waarde dus nooit overrulen — de tokenwaarde zelf moet meeschakelen.\n // (styles.css zet `color-scheme: light dark` op `[data-dark=\"auto\"]`, vereist voor light-dark().)\n const auto = Boolean(theme.dark?.auto);\n const ld = (light: string, dark: string) => (auto ? `light-dark(${light}, ${dark})` : light);\n const cssVars: Record<string, string> = {\n \"--xicms-primary\": theme.dark?.primary ? ld(theme.primary, theme.dark.primary) : theme.primary,\n \"--xicms-accent\": theme.accent ?? theme.primary,\n \"--xicms-bg\": ld(theme.background ?? \"#ffffff\", theme.dark?.background ?? \"#0f172a\"),\n \"--xicms-fg\": ld(theme.foreground ?? \"#0f172a\", theme.dark?.foreground ?? \"#e2e8f0\"),\n };\n if (theme.fontFamily) cssVars[\"--xicms-font\"] = theme.fontFamily;\n // Optionele schalen → CSS-variabelen (alleen gezet als opgegeven; styles.css heeft defaults).\n for (const [k, v] of Object.entries(theme.spacing ?? {})) if (v) cssVars[`--xicms-spacing-${k}`] = v;\n if (theme.radius?.sm) cssVars[\"--xicms-radius-sm\"] = theme.radius.sm;\n if (theme.radius?.md) cssVars[\"--xicms-radius\"] = theme.radius.md;\n if (theme.radius?.lg) cssVars[\"--xicms-radius-lg\"] = theme.radius.lg;\n if (theme.container?.maxWidth) cssVars[\"--xicms-container-max\"] = theme.container.maxWidth;\n if (theme.container?.padding) cssVars[\"--xicms-container-pad\"] = theme.container.padding;\n if (theme.dark?.background) cssVars[\"--xicms-bg-dark\"] = theme.dark.background;\n if (theme.dark?.foreground) cssVars[\"--xicms-fg-dark\"] = theme.dark.foreground;\n if (theme.dark?.primary) cssVars[\"--xicms-primary-dark\"] = theme.dark.primary;\n\n // Astryx-tokens op XiCMS-waarden mappen, zodat de Astryx-componenten elke site z'n eigen merk\n // (kleur/typografie/radius) volgen. Inline op `.xicms-root` → wint over de theme.css-defaults.\n cssVars[\"--color-accent\"] = \"var(--xicms-primary)\";\n // Tekst óp de accentkleur: wit op een donkere merkkleur, donker op een lichte (geel/pastel) —\n // hardcoded wit maakte knop- en bannertekst onleesbaar op lichte primaire kleuren.\n const onAccent = isDarkColor(theme.primary) ? \"#ffffff\" : \"#0f172a\";\n cssVars[\"--color-on-accent\"] = onAccent;\n cssVars[\"--xicms-on-primary\"] = onAccent;\n cssVars[\"--color-text-accent\"] = \"var(--xicms-primary)\";\n cssVars[\"--radius-element\"] = \"var(--xicms-radius-sm, 8px)\";\n cssVars[\"--radius-container\"] = \"var(--xicms-radius, 14px)\";\n cssVars[\"--radius-page\"] = \"var(--xicms-radius-lg, 24px)\";\n // Volledige paletmapping — ALLEEN als de site een eigen achtergrond/tekst kiest. Zo verschilt niet\n // enkel het accent maar de hele sfeer (kaarten, borders, secundaire tekst) per site; een minimale\n // config blijft op de gepolijste neutrale basis (incl. automatische dark-mode via light-dark()).\n if (theme.background) {\n cssVars[\"--color-background-body\"] = \"var(--xicms-bg)\";\n cssVars[\"--color-background-surface\"] = \"color-mix(in srgb, var(--xicms-fg) 4%, var(--xicms-bg))\";\n cssVars[\"--color-background-card\"] = \"color-mix(in srgb, var(--xicms-fg) 3%, var(--xicms-bg))\";\n cssVars[\"--color-background-popover\"] = \"var(--xicms-bg)\";\n cssVars[\"--color-background-muted\"] = \"color-mix(in srgb, var(--xicms-fg) 6%, var(--xicms-bg))\";\n // Ook de XiCMS-oppervlaktetokens meeschakelen: hun defaults in styles.css zijn licht (#fff/zwart-\n // rgba) en het dark-blok vuurt alleen bij `data-dark=\"auto\"`. Zonder deze mapping tonen presets\n // met een vaste donkere achtergrond (donker-modern, luxe-donker) witte kaarten en zwarte borders.\n cssVars[\"--xicms-surface\"] = \"color-mix(in srgb, var(--xicms-fg) 3%, var(--xicms-bg))\";\n cssVars[\"--xicms-muted\"] = \"color-mix(in srgb, var(--xicms-fg) 5%, var(--xicms-bg))\";\n }\n if (theme.foreground) {\n cssVars[\"--color-text-primary\"] = \"var(--xicms-fg)\";\n cssVars[\"--color-text-secondary\"] = \"color-mix(in srgb, var(--xicms-fg) 65%, transparent)\";\n cssVars[\"--color-neutral\"] = \"var(--xicms-fg)\";\n cssVars[\"--color-border\"] = \"color-mix(in srgb, var(--xicms-fg) 14%, transparent)\";\n cssVars[\"--xicms-border\"] = \"color-mix(in srgb, var(--xicms-fg) 12%, transparent)\";\n cssVars[\"--xicms-border-strong\"] = \"color-mix(in srgb, var(--xicms-fg) 22%, transparent)\";\n }\n if (theme.headingFont) cssVars[\"--xicms-font-heading\"] = theme.headingFont;\n if (theme.fontFamily) {\n cssVars[\"--font-family-body\"] = \"var(--xicms-font)\";\n // Koppen volgen een apart display/serif-font als dat is opgegeven, anders het body-font.\n cssVars[\"--font-family-heading\"] = theme.headingFont ? \"var(--xicms-font-heading)\" : \"var(--xicms-font)\";\n } else if (theme.headingFont) {\n cssVars[\"--font-family-heading\"] = \"var(--xicms-font-heading)\";\n }\n\n return cssVars;\n}\n\n/**\n * Is deze (hex-)kleur donker? Bepaalt de Astryx-thememodus voor sites met een vaste donkere\n * achtergrond (bv. presets donker-modern/luxe-donker, `dark.auto: false`): zonder deze afleiding\n * bleef Astryx op \"light\" staan en tekenden zijn componenten lichte kaarten op een donkere pagina.\n * Puur en exporteerbaar voor tests; niet-hex waarden gelden als licht (veilige default).\n */\nexport function isDarkColor(color?: string): boolean {\n const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color?.trim() ?? \"\");\n if (!m) return false;\n let hex = m[1];\n if (hex.length === 3) hex = hex.replace(/./g, (c) => c + c);\n const [r, g, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);\n return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.45;\n}\n\n/**\n * Binnenlaag: leest de live publieke settings en resolvet daarmee de modules (settings/public.modules\n * legt zich over SiteConfig.modules) én het thema (settings/public.theme over SiteConfig.theme).\n * Zo werken module-aan/uit, -instellingen én huisstijl die de klant in /beheer zet direct door.\n */\nfunction ModuleLayer({\n config,\n blocks = NO_BLOCKS,\n modules = NO_MODULES,\n overrides,\n customBlocksData,\n astryxTheme,\n linkComponent,\n imageComponent,\n children,\n}: ProviderProps) {\n const { settings } = useSiteSettings();\n\n // LIVE thema: wijzigingen uit /beheer (settings/public.theme) winnen per token van SiteConfig.\n const theme = useMemo(() => mergeThemeTokens(config.theme, settings.theme), [config.theme, settings.theme]);\n const cssVars = useMemo(() => themeCssVars(theme), [theme]);\n\n // Demo-bewerkmodus globaal markeren zodat de (Firebase-)dataclients lokaal-only werken.\n setDemoActive(Boolean(config.demoEditing));\n\n const resolved = useMemo(\n () =>\n resolveModules([...builtinModules, ...modules], {\n modules: config.modules,\n features: config.features,\n firestoreModules: settings.modules as ModulesSiteConfig | undefined,\n }),\n [config.modules, config.features, modules, settings.modules],\n );\n\n // Custom blocks (5.0): runtime-gedefinieerde bloktypes uit sites/{siteId}/customBlocks.\n // SSR-startset via de prop; daarna live gesynchroniseerd (nieuw blok in /beheer of via een\n // agent-push verschijnt zonder redeploy in het toevoeg-menu en op de pagina).\n const [customBlocks, setCustomBlocks] = useState<CustomBlockData[]>(customBlocksData ?? []);\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n return onSnapshot(\n collection(db, \"sites\", config.siteId, \"customBlocks\"),\n (snap) => {\n const defs: CustomBlockData[] = [];\n snap.forEach((d) => {\n const res = CustomBlockSchema.safeParse({ ...(d.data() as Record<string, unknown>), type: (d.data() as { type?: string }).type ?? d.id });\n if (res.success) defs.push(res.data);\n });\n setCustomBlocks(defs);\n },\n () => undefined, // leesfout → SSR-set blijft staan\n );\n }, [config.siteId]);\n const customDefs = useMemo(() => customBlocks.map(customBlockToDefinition), [customBlocks]);\n\n const registry = useMemo(\n () => applyBlockOverrides(mergeRegistry(mergeRegistry(createRegistry(resolved.blocks), customDefs), blocks), overrides),\n [resolved.blocks, customDefs, blocks, overrides],\n );\n\n return (\n <ModulesProvider value={resolved}>\n <RegistryProvider registry={registry}>\n <AuthProvider siteId={config.siteId} demo={Boolean(config.demoEditing)}>\n <EditorProvider>\n {/* React 19 hijst <link rel=\"stylesheet\"> automatisch naar <head> → lettertype laadt. */}\n {theme.fontUrl && <link rel=\"stylesheet\" href={theme.fontUrl} />}\n {/* Astryx-thema (Neutral basis) — de XiCMS-token-mapping op `.xicms-root` legt elke\n site z'n eigen merk eroverheen. */}\n <Theme theme={astryxTheme ?? neutralTheme} mode={theme.dark?.auto ? \"system\" : isDarkColor(theme.background) ? \"dark\" : \"light\"}>\n {/* Framework-router voor Astryx-links/knoppen (soepele client-nav + prefetch);\n zonder `linkComponent` valt het terug op een gewone `<a>` (huidig gedrag). */}\n <LinkProvider component={linkComponent ?? \"a\"}>\n <ImageProvider component={imageComponent}>\n <div\n className=\"xicms-root\"\n data-dark={theme.dark?.auto ? \"auto\" : undefined}\n style={cssVars as React.CSSProperties}\n >\n <DemoStudio />\n {children}\n <EditBar />\n <DemoLock />\n {resolved.activeIds.has(\"consent\") && (\n <ConsentBanner settings={resolved.settings.consent as ConsentSettings} />\n )}\n </div>\n </ImageProvider>\n </LinkProvider>\n </Theme>\n </EditorProvider>\n </AuthProvider>\n </RegistryProvider>\n </ModulesProvider>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/XiCMSProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { ConfigProvider } from \"./config\";\nimport { AuthProvider, useAuth } from \"./auth/AuthProvider\";\nimport { canEdit } from \"./auth/roles\";\nimport { EditBar } from \"./components/EditBar\";\nimport { DemoLock } from \"./lock/DemoLock\";\nimport { RegistryProvider } from \"./blocks/context\";\nimport { getDb } from \"./firebase/client\";\nimport { collection, onSnapshot } from \"firebase/firestore\";\nimport { createRegistry, mergeRegistry, type BlockDefinition } from \"./blocks/registry\";\nimport { CustomBlockSchema, type CustomBlock as CustomBlockData } from \"./schema/customBlocks\";\nimport { customBlockToDefinition } from \"./blocks/customTemplate/definition\";\nimport { resolveModules } from \"./modules/registry\";\nimport { ModulesProvider } from \"./modules/context\";\nimport { applyBlockOverrides, type BlockOverrides } from \"./modules/overrides\";\nimport { builtinModules } from \"./modules/builtins\";\nimport type { ModuleDefinition, ModulesSiteConfig } from \"./modules/types\";\nimport { ConsentBanner, type ConsentSettings } from \"./modules/consent/ConsentBanner\";\nimport { EditorProvider } from \"./editor/EditorProvider\";\nimport { SiteSettingsProvider, useSiteSettings } from \"./settings/useSiteSettings\";\nimport { mergeThemeTokens, type ThemeTokens } from \"./schema/settings\";\nimport { initAppCheck } from \"./firebase/appcheck\";\nimport { DemoStudio } from \"./demo/DemoStudio\";\nimport { setDemoActive } from \"./demo/demoStore\";\nimport { Theme, type DefinedTheme } from \"@astryxdesign/core/theme\";\nimport { neutralTheme } from \"@astryxdesign/theme-neutral/built\";\nimport { LinkProvider, type LinkComponentType } from \"@astryxdesign/core/Link\";\nimport { ImageProvider, type ImageComponentType } from \"./media/ImageContext\";\nimport { validateSiteConfig, type SiteConfig } from \"./schema/siteConfig\";\nimport { validateModuleContract } from \"./modules/contract\";\n\ninterface ProviderProps {\n config: SiteConfig;\n /** Losse klant-blokken (via `defineBlock`) buiten een module om. Altijd actief. */\n blocks?: BlockDefinition[];\n /**\n * Klant-modules (via `defineModule`) bovenop de ingebouwde. Worden gemerged op `id` (klant wint)\n * en respecteren `config.modules`/`config.features` voor activatie.\n */\n modules?: ModuleDefinition[];\n /**\n * Per-site presentatie-overrides per block-type (`{ gallery: { Render: MijnGalerij } }`). Vervangt\n * alleen het uiterlijk; schema/editor/logica blijven van core. De krachtigste maatwerk-naad.\n */\n overrides?: BlockOverrides;\n /**\n * SSR-startset van custom blocks (5.0): geef het resultaat van `getSiteCustomBlocks(siteId)`\n * (server) door zodat runtime-gedefinieerde blokken al in de server-HTML zitten. Zonder deze\n * prop laadt de provider ze alsnog client-side (werkend, maar pas na hydratie zichtbaar).\n */\n customBlocksData?: CustomBlockData[];\n /**\n * Astryx-basistheme voor de hele site (radius-/spacing-/neutralen-karakter). Standaard `neutral`.\n * Een site kan een ander pakket kiezen (`@astryxdesign/theme-butter`, `-stone`, …) of een eigen\n * `defineTheme(...)` doorgeven voor component-overrides — zónder core te forken.\n */\n astryxTheme?: DefinedTheme;\n /**\n * Framework-link-component (bv. Next `Link`) voor soepele client-navigatie + prefetch. Astryx-\n * componenten met een `href` (knoppen/links/tabs) gebruiken 'm dan i.p.v. een rauwe `<a>` (= volledige\n * page-reload). Core importeert `next/link` niet zelf (optionele peer) — geef 'm hier door vanuit de site.\n */\n linkComponent?: LinkComponentType;\n /**\n * Framework-beeldcomponent (bv. Next `Image`) voor automatische beeldoptimalisatie (responsive\n * srcset, AVIF/WebP, geen layout-shift). Core importeert `next/image` niet zelf — geef 'm hier door.\n */\n imageComponent?: ImageComponentType;\n children: ReactNode;\n}\n\n// Stabiele lege defaults: een `= []`-defaultparameter maakt bij ELKE render een nieuwe\n// array-referentie en laat memo's/effects met `modules`/`blocks` in de deps onnodig hervuren.\nconst NO_BLOCKS: BlockDefinition[] = [];\nconst NO_MODULES: ModuleDefinition[] = [];\n\n// Dev-waarschuwingen één keer per boodschap (React strict-mode dubbelt effects; zonder dedupe\n// verschijnt dezelfde config-waarschuwing telkens opnieuw in de console).\nconst warned = new Set<string>();\nfunction warnOnce(message: string) {\n if (warned.has(message)) return;\n warned.add(message);\n console.warn(message);\n}\n\n/**\n * Wikkel de hele site in deze provider (in `app/layout.tsx`). Hij zet:\n * - de SiteConfig-context (siteId, theme, mode) + live publieke settings (footer/nav/modules);\n * - auth + content (Firebase, env-gedreven; werkt ook zonder Firebase);\n * - de block-registry (ingebouwde + klant-blokken) voor de page-renderer;\n * - de themavariabelen als CSS custom properties;\n * - App Check (alleen als geconfigureerd) en de EditBar/DemoLock.\n */\nexport function XiCMSProvider({ config, blocks = NO_BLOCKS, modules = NO_MODULES, overrides, astryxTheme, linkComponent, imageComponent, customBlocksData, children }: ProviderProps) {\n useEffect(() => {\n initAppCheck();\n }, []);\n\n // Dev-vangnet voor (agent-)geschreven configs: meld config- en module-contractfouten meteen in\n // de console i.p.v. een subtiel kapotte site. In productie geen werk — en er wordt nooit gegooid.\n useEffect(() => {\n if (process.env.NODE_ENV === \"production\") return;\n const configIssues = validateSiteConfig(config);\n if (configIssues.length > 0) {\n warnOnce(`[xicms] site.config.ts bevat problemen:\\n- ${configIssues.join(\"\\n- \")}`);\n }\n if (modules.length > 0) {\n const contractIssues = validateModuleContract(modules);\n if (contractIssues.length > 0) {\n warnOnce(`[xicms] klant-modules voldoen niet aan het module-contract:\\n- ${contractIssues.join(\"\\n- \")}`);\n }\n }\n }, [config, modules]);\n\n return (\n <ConfigProvider config={config}>\n <SiteSettingsProvider siteId={config.siteId}>\n <ModuleLayer config={config} blocks={blocks} modules={modules} overrides={overrides} astryxTheme={astryxTheme} linkComponent={linkComponent} imageComponent={imageComponent} customBlocksData={customBlocksData}>\n {children}\n </ModuleLayer>\n </SiteSettingsProvider>\n </ConfigProvider>\n );\n}\n\n/**\n * Theme-tokens → CSS custom properties. Puur; wordt in `ModuleLayer` op het LIVE-gemergde thema\n * toegepast (SiteConfig + `settings/public.theme`), zodat huisstijl-wijzigingen uit /beheer direct\n * op de site verschijnen — net als module-instellingen.\n */\nfunction themeCssVars(theme: ThemeTokens): Record<string, string> {\n // Dark-auto via light-dark(): deze variabelen staan INLINE op `.xicms-root`, en inline wint\n // altijd van het `@media (prefers-color-scheme: dark)`-blok in styles.css. Alleen dat blok kan\n // een inline lichte waarde dus nooit overrulen — de tokenwaarde zelf moet meeschakelen.\n // (styles.css zet `color-scheme: light dark` op `[data-dark=\"auto\"]`, vereist voor light-dark().)\n const auto = Boolean(theme.dark?.auto);\n const ld = (light: string, dark: string) => (auto ? `light-dark(${light}, ${dark})` : light);\n const cssVars: Record<string, string> = {\n \"--xicms-primary\": theme.dark?.primary ? ld(theme.primary, theme.dark.primary) : theme.primary,\n \"--xicms-accent\": theme.accent ?? theme.primary,\n \"--xicms-bg\": ld(theme.background ?? \"#ffffff\", theme.dark?.background ?? \"#0f172a\"),\n \"--xicms-fg\": ld(theme.foreground ?? \"#0f172a\", theme.dark?.foreground ?? \"#e2e8f0\"),\n };\n if (theme.fontFamily) cssVars[\"--xicms-font\"] = theme.fontFamily;\n // Optionele schalen → CSS-variabelen (alleen gezet als opgegeven; styles.css heeft defaults).\n for (const [k, v] of Object.entries(theme.spacing ?? {})) if (v) cssVars[`--xicms-spacing-${k}`] = v;\n if (theme.radius?.sm) cssVars[\"--xicms-radius-sm\"] = theme.radius.sm;\n if (theme.radius?.md) cssVars[\"--xicms-radius\"] = theme.radius.md;\n if (theme.radius?.lg) cssVars[\"--xicms-radius-lg\"] = theme.radius.lg;\n if (theme.container?.maxWidth) cssVars[\"--xicms-container-max\"] = theme.container.maxWidth;\n if (theme.container?.padding) cssVars[\"--xicms-container-pad\"] = theme.container.padding;\n if (theme.dark?.background) cssVars[\"--xicms-bg-dark\"] = theme.dark.background;\n if (theme.dark?.foreground) cssVars[\"--xicms-fg-dark\"] = theme.dark.foreground;\n if (theme.dark?.primary) cssVars[\"--xicms-primary-dark\"] = theme.dark.primary;\n\n // Astryx-tokens op XiCMS-waarden mappen, zodat de Astryx-componenten elke site z'n eigen merk\n // (kleur/typografie/radius) volgen. Inline op `.xicms-root` → wint over de theme.css-defaults.\n cssVars[\"--color-accent\"] = \"var(--xicms-primary)\";\n // Tekst óp de accentkleur: wit op een donkere merkkleur, donker op een lichte (geel/pastel) —\n // hardcoded wit maakte knop- en bannertekst onleesbaar op lichte primaire kleuren.\n const onAccent = isDarkColor(theme.primary) ? \"#ffffff\" : \"#0f172a\";\n cssVars[\"--color-on-accent\"] = onAccent;\n cssVars[\"--xicms-on-primary\"] = onAccent;\n cssVars[\"--color-text-accent\"] = \"var(--xicms-primary)\";\n cssVars[\"--radius-element\"] = \"var(--xicms-radius-sm, 8px)\";\n cssVars[\"--radius-container\"] = \"var(--xicms-radius, 14px)\";\n cssVars[\"--radius-page\"] = \"var(--xicms-radius-lg, 24px)\";\n // Volledige paletmapping — ALLEEN als de site een eigen achtergrond/tekst kiest. Zo verschilt niet\n // enkel het accent maar de hele sfeer (kaarten, borders, secundaire tekst) per site; een minimale\n // config blijft op de gepolijste neutrale basis (incl. automatische dark-mode via light-dark()).\n if (theme.background) {\n cssVars[\"--color-background-body\"] = \"var(--xicms-bg)\";\n cssVars[\"--color-background-surface\"] = \"color-mix(in srgb, var(--xicms-fg) 4%, var(--xicms-bg))\";\n cssVars[\"--color-background-card\"] = \"color-mix(in srgb, var(--xicms-fg) 3%, var(--xicms-bg))\";\n cssVars[\"--color-background-popover\"] = \"var(--xicms-bg)\";\n cssVars[\"--color-background-muted\"] = \"color-mix(in srgb, var(--xicms-fg) 6%, var(--xicms-bg))\";\n // Ook de XiCMS-oppervlaktetokens meeschakelen: hun defaults in styles.css zijn licht (#fff/zwart-\n // rgba) en het dark-blok vuurt alleen bij `data-dark=\"auto\"`. Zonder deze mapping tonen presets\n // met een vaste donkere achtergrond (donker-modern, luxe-donker) witte kaarten en zwarte borders.\n cssVars[\"--xicms-surface\"] = \"color-mix(in srgb, var(--xicms-fg) 3%, var(--xicms-bg))\";\n cssVars[\"--xicms-muted\"] = \"color-mix(in srgb, var(--xicms-fg) 5%, var(--xicms-bg))\";\n }\n if (theme.foreground) {\n cssVars[\"--color-text-primary\"] = \"var(--xicms-fg)\";\n cssVars[\"--color-text-secondary\"] = \"color-mix(in srgb, var(--xicms-fg) 65%, transparent)\";\n cssVars[\"--color-neutral\"] = \"var(--xicms-fg)\";\n cssVars[\"--color-border\"] = \"color-mix(in srgb, var(--xicms-fg) 14%, transparent)\";\n cssVars[\"--xicms-border\"] = \"color-mix(in srgb, var(--xicms-fg) 12%, transparent)\";\n cssVars[\"--xicms-border-strong\"] = \"color-mix(in srgb, var(--xicms-fg) 22%, transparent)\";\n }\n if (theme.headingFont) cssVars[\"--xicms-font-heading\"] = theme.headingFont;\n if (theme.fontFamily) {\n cssVars[\"--font-family-body\"] = \"var(--xicms-font)\";\n // Koppen volgen een apart display/serif-font als dat is opgegeven, anders het body-font.\n cssVars[\"--font-family-heading\"] = theme.headingFont ? \"var(--xicms-font-heading)\" : \"var(--xicms-font)\";\n } else if (theme.headingFont) {\n cssVars[\"--font-family-heading\"] = \"var(--xicms-font-heading)\";\n }\n\n return cssVars;\n}\n\n/**\n * Is deze (hex-)kleur donker? Bepaalt de Astryx-thememodus voor sites met een vaste donkere\n * achtergrond (bv. presets donker-modern/luxe-donker, `dark.auto: false`): zonder deze afleiding\n * bleef Astryx op \"light\" staan en tekenden zijn componenten lichte kaarten op een donkere pagina.\n * Puur en exporteerbaar voor tests; niet-hex waarden gelden als licht (veilige default).\n */\nexport function isDarkColor(color?: string): boolean {\n const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color?.trim() ?? \"\");\n if (!m) return false;\n let hex = m[1];\n if (hex.length === 3) hex = hex.replace(/./g, (c) => c + c);\n const [r, g, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);\n return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.45;\n}\n\n/**\n * Binnenlaag: leest de live publieke settings en resolvet daarmee de modules (settings/public.modules\n * legt zich over SiteConfig.modules) én het thema (settings/public.theme over SiteConfig.theme).\n * Zo werken module-aan/uit, -instellingen én huisstijl die de klant in /beheer zet direct door.\n */\nfunction ModuleLayer({\n config,\n blocks = NO_BLOCKS,\n modules = NO_MODULES,\n overrides,\n customBlocksData,\n astryxTheme,\n linkComponent,\n imageComponent,\n children,\n}: ProviderProps) {\n const { settings } = useSiteSettings();\n\n // LIVE thema: wijzigingen uit /beheer (settings/public.theme) winnen per token van SiteConfig.\n const theme = useMemo(() => mergeThemeTokens(config.theme, settings.theme), [config.theme, settings.theme]);\n const cssVars = useMemo(() => themeCssVars(theme), [theme]);\n\n // Demo-bewerkmodus globaal markeren zodat de (Firebase-)dataclients lokaal-only werken.\n // In een effect (niet tijdens render): render-pure + veilig onder concurrent rendering. De\n // demoStore-default is `false`, dus vóór het effect gedraagt alles zich als niet-demo.\n useEffect(() => {\n setDemoActive(Boolean(config.demoEditing));\n }, [config.demoEditing]);\n\n const resolved = useMemo(\n () =>\n resolveModules([...builtinModules, ...modules], {\n modules: config.modules,\n features: config.features,\n firestoreModules: settings.modules as ModulesSiteConfig | undefined,\n }),\n [config.modules, config.features, modules, settings.modules],\n );\n\n // Custom blocks (5.0): runtime-gedefinieerde bloktypes uit sites/{siteId}/customBlocks.\n // Publieke bezoekers krijgen UITSLUITEND de SSR-startset (customBlocksData, server-side gelezen) —\n // géén Firestore-listener per bezoeker (kosten/verbinding). Alleen ingelogde editors krijgen een\n // live sync (CustomBlocksSync, binnen AuthProvider) zodat een nieuw blok uit /beheer of een\n // agent-push zonder redeploy in het toevoeg-menu en op de pagina verschijnt.\n const [customBlocks, setCustomBlocks] = useState<CustomBlockData[]>(customBlocksData ?? []);\n const customDefs = useMemo(() => customBlocks.map(customBlockToDefinition), [customBlocks]);\n\n const registry = useMemo(\n () => applyBlockOverrides(mergeRegistry(mergeRegistry(createRegistry(resolved.blocks), customDefs), blocks), overrides),\n [resolved.blocks, customDefs, blocks, overrides],\n );\n\n return (\n <ModulesProvider value={resolved}>\n <RegistryProvider registry={registry}>\n <AuthProvider siteId={config.siteId} demo={Boolean(config.demoEditing)}>\n <CustomBlocksSync siteId={config.siteId} onData={setCustomBlocks} />\n <EditorProvider>\n {/* React 19 hijst <link rel=\"stylesheet\"> automatisch naar <head> → lettertype laadt. */}\n {theme.fontUrl && <link rel=\"stylesheet\" href={theme.fontUrl} />}\n {/* Astryx-thema (Neutral basis) — de XiCMS-token-mapping op `.xicms-root` legt elke\n site z'n eigen merk eroverheen. */}\n <Theme theme={astryxTheme ?? neutralTheme} mode={theme.dark?.auto ? \"system\" : isDarkColor(theme.background) ? \"dark\" : \"light\"}>\n {/* Framework-router voor Astryx-links/knoppen (soepele client-nav + prefetch);\n zonder `linkComponent` valt het terug op een gewone `<a>` (huidig gedrag). */}\n <LinkProvider component={linkComponent ?? \"a\"}>\n <ImageProvider component={imageComponent}>\n <div\n className=\"xicms-root\"\n data-dark={theme.dark?.auto ? \"auto\" : undefined}\n style={cssVars as React.CSSProperties}\n >\n <DemoStudio />\n {children}\n <EditBar />\n <DemoLock />\n {resolved.activeIds.has(\"consent\") && (\n <ConsentBanner settings={resolved.settings.consent as ConsentSettings} />\n )}\n </div>\n </ImageProvider>\n </LinkProvider>\n </Theme>\n </EditorProvider>\n </AuthProvider>\n </RegistryProvider>\n </ModulesProvider>\n );\n}\n\n/**\n * Live sync van custom blocks — ALLEEN voor ingelogde editors. Publieke bezoekers krijgen de\n * SSR-startset (customBlocksData) en géén Firestore-listener: dat scheelt een collection-read +\n * open verbinding per bezoeker, conform het getDoc-voor-publiek-patroon van useSiteSettings/News.\n */\nfunction CustomBlocksSync({ siteId, onData }: { siteId: string; onData: (defs: CustomBlockData[]) => void }) {\n const { role } = useAuth();\n const editing = canEdit(role);\n useEffect(() => {\n if (!editing) return;\n const db = getDb();\n if (!db) return;\n return onSnapshot(\n collection(db, \"sites\", siteId, \"customBlocks\"),\n (snap) => {\n const defs: CustomBlockData[] = [];\n snap.forEach((d) => {\n const res = CustomBlockSchema.safeParse({ ...(d.data() as Record<string, unknown>), type: (d.data() as { type?: string }).type ?? d.id });\n if (res.success) defs.push(res.data);\n });\n onData(defs);\n },\n () => undefined, // leesfout → SSR-set blijft staan\n );\n }, [editing, siteId, onData]);\n return null;\n}\n"],"mappings":"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","names":[]}
@@ -5,7 +5,7 @@ import StarterKit from "@tiptap/starter-kit";
5
5
  import Link from "@tiptap/extension-link";
6
6
  import Image from "@tiptap/extension-image";
7
7
  import DOMPurify from "dompurify";
8
- import { useCallback } from "react";
8
+ import { useCallback, useEffect } from "react";
9
9
  import { cn } from "../ui/cn";
10
10
  import { MediaPickerButton } from "../media/MediaPicker";
11
11
  function RichTextEditor({
@@ -26,6 +26,12 @@ function RichTextEditor({
26
26
  attributes: { class: "min-h-[8rem] rounded-b-lg border border-t-0 border-slate-300 bg-white p-3 text-sm" }
27
27
  }
28
28
  });
29
+ useEffect(() => {
30
+ if (!editor || editor.isFocused) return;
31
+ const current = editor.getHTML();
32
+ const next = value || "<p></p>";
33
+ if (current !== next) editor.commands.setContent(next, false);
34
+ }, [value, editor]);
29
35
  const setLink = useCallback(() => {
30
36
  if (!editor) return;
31
37
  const prev = editor.getAttributes("link").href;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/editors/RichTextEditor.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEditor, EditorContent } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport Link from \"@tiptap/extension-link\";\nimport Image from \"@tiptap/extension-image\";\nimport DOMPurify from \"dompurify\";\nimport { useCallback } from \"react\";\nimport { cn } from \"../ui/cn\";\nimport { MediaPickerButton } from \"../media/MediaPicker\";\n\n/**\n * WYSIWYG-editor (TipTap) met opmaak én afbeeldingen. Output wordt client-side met DOMPurify\n * gesanitized voor preview/opslag; de server sanitized nogmaals bij publiceren (server/sanitize.ts)\n * — nooit op de client vertrouwen. Geef `siteId` mee om afbeeldingen te kunnen uploaden/invoegen.\n */\nexport function RichTextEditor({\n value,\n onChange,\n siteId,\n}: {\n value: string;\n onChange: (html: string) => void;\n siteId?: string;\n}) {\n\n const editor = useEditor({\n immediatelyRender: false,\n extensions: [\n StarterKit.configure({ heading: { levels: [2, 3, 4] } }),\n Link.configure({ openOnClick: false, autolink: true, HTMLAttributes: { rel: \"noopener noreferrer nofollow\" } }),\n Image.configure({ inline: false, allowBase64: false, HTMLAttributes: { loading: \"lazy\", decoding: \"async\" } }),\n ],\n content: value || \"<p></p>\",\n onUpdate: ({ editor }) => onChange(DOMPurify.sanitize(editor.getHTML())),\n editorProps: {\n attributes: { class: \"min-h-[8rem] rounded-b-lg border border-t-0 border-slate-300 bg-white p-3 text-sm\" },\n },\n });\n\n const setLink = useCallback(() => {\n if (!editor) return;\n const prev = editor.getAttributes(\"link\").href as string | undefined;\n const url = window.prompt(\"Link-URL (leeg = verwijderen)\", prev ?? \"https://\");\n if (url === null) return;\n if (url === \"\") editor.chain().focus().extendMarkRange(\"link\").unsetLink().run();\n else editor.chain().focus().extendMarkRange(\"link\").setLink({ href: url }).run();\n }, [editor]);\n\n const insertImageByUrl = useCallback(() => {\n if (!editor) return;\n const url = window.prompt(\"Afbeeldings-URL (https)\");\n if (!url) return;\n const alt = window.prompt(\"Beschrijving (alt-tekst, voor toegankelijkheid)\") ?? \"\";\n editor.chain().focus().setImage({ src: url, alt }).run();\n }, [editor]);\n\n if (!editor) return null;\n\n const Btn = ({ on, active, label, title }: { on: () => void; active?: boolean; label: string; title?: string }) => (\n <button\n type=\"button\"\n title={title}\n onMouseDown={(e) => e.preventDefault()}\n onClick={on}\n className={cn(\n \"h-8 min-w-8 rounded px-2 text-sm hover:bg-slate-100\",\n active && \"bg-slate-200 font-semibold\",\n )}\n >\n {label}\n </button>\n );\n\n return (\n <div>\n <div className=\"flex flex-wrap gap-1 rounded-t-lg border border-slate-300 bg-slate-50 p-1\">\n <Btn on={() => editor.chain().focus().toggleBold().run()} active={editor.isActive(\"bold\")} label=\"B\" title=\"Vet\" />\n <Btn on={() => editor.chain().focus().toggleItalic().run()} active={editor.isActive(\"italic\")} label=\"I\" title=\"Cursief\" />\n <Btn on={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} active={editor.isActive(\"heading\", { level: 2 })} label=\"H2\" title=\"Kop\" />\n <Btn on={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} active={editor.isActive(\"heading\", { level: 3 })} label=\"H3\" title=\"Subkop\" />\n <Btn on={() => editor.chain().focus().toggleBulletList().run()} active={editor.isActive(\"bulletList\")} label=\"• Lijst\" title=\"Opsomming\" />\n <Btn on={() => editor.chain().focus().toggleOrderedList().run()} active={editor.isActive(\"orderedList\")} label=\"1. Lijst\" title=\"Genummerde lijst\" />\n <Btn on={() => editor.chain().focus().toggleBlockquote().run()} active={editor.isActive(\"blockquote\")} label=\"❝\" title=\"Citaat\" />\n <Btn on={setLink} active={editor.isActive(\"link\")} label=\"🔗\" title=\"Link\" />\n {siteId && (\n <MediaPickerButton\n siteId={siteId}\n label=\"🖼 Afbeelding\"\n variant=\"ghost\"\n onSelect={(m) => editor.chain().focus().setImage({ src: m.url, alt: m.alt ?? \"\" }).run()}\n />\n )}\n <Btn on={insertImageByUrl} label=\"🖼 URL\" title=\"Externe afbeelding via URL invoegen\" />\n </div>\n <EditorContent editor={editor} />\n </div>\n );\n}\n"],"mappings":"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","names":["editor"]}
1
+ {"version":3,"sources":["../../../src/admin/editors/RichTextEditor.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEditor, EditorContent } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport Link from \"@tiptap/extension-link\";\nimport Image from \"@tiptap/extension-image\";\nimport DOMPurify from \"dompurify\";\nimport { useCallback, useEffect } from \"react\";\nimport { cn } from \"../ui/cn\";\nimport { MediaPickerButton } from \"../media/MediaPicker\";\n\n/**\n * WYSIWYG-editor (TipTap) met opmaak én afbeeldingen. Output wordt client-side met DOMPurify\n * gesanitized voor preview/opslag; de server sanitized nogmaals bij publiceren (server/sanitize.ts)\n * — nooit op de client vertrouwen. Geef `siteId` mee om afbeeldingen te kunnen uploaden/invoegen.\n */\nexport function RichTextEditor({\n value,\n onChange,\n siteId,\n}: {\n value: string;\n onChange: (html: string) => void;\n siteId?: string;\n}) {\n\n const editor = useEditor({\n immediatelyRender: false,\n extensions: [\n StarterKit.configure({ heading: { levels: [2, 3, 4] } }),\n Link.configure({ openOnClick: false, autolink: true, HTMLAttributes: { rel: \"noopener noreferrer nofollow\" } }),\n Image.configure({ inline: false, allowBase64: false, HTMLAttributes: { loading: \"lazy\", decoding: \"async\" } }),\n ],\n content: value || \"<p></p>\",\n onUpdate: ({ editor }) => onChange(DOMPurify.sanitize(editor.getHTML())),\n editorProps: {\n attributes: { class: \"min-h-[8rem] rounded-b-lg border border-t-0 border-slate-300 bg-white p-3 text-sm\" },\n },\n });\n\n // Externe waarde-wijzigingen (undo/redo in de VisualEditor, taal-wissel in het vertaalpaneel)\n // in de editor doorvoeren: TipTap leest `content` alleen bij init. Zonder deze sync blijft de\n // zichtbare HTML stale terwijl de opgeslagen waarde al veranderd is — data-integriteitsval.\n // Niet syncen terwijl de gebruiker zelf typt (editor.isFocused) — dan is de editor de bron.\n useEffect(() => {\n if (!editor || editor.isFocused) return;\n const current = editor.getHTML();\n const next = value || \"<p></p>\";\n if (current !== next) editor.commands.setContent(next, false); // false = geen onUpdate-echo\n }, [value, editor]);\n\n const setLink = useCallback(() => {\n if (!editor) return;\n const prev = editor.getAttributes(\"link\").href as string | undefined;\n const url = window.prompt(\"Link-URL (leeg = verwijderen)\", prev ?? \"https://\");\n if (url === null) return;\n if (url === \"\") editor.chain().focus().extendMarkRange(\"link\").unsetLink().run();\n else editor.chain().focus().extendMarkRange(\"link\").setLink({ href: url }).run();\n }, [editor]);\n\n const insertImageByUrl = useCallback(() => {\n if (!editor) return;\n const url = window.prompt(\"Afbeeldings-URL (https)\");\n if (!url) return;\n const alt = window.prompt(\"Beschrijving (alt-tekst, voor toegankelijkheid)\") ?? \"\";\n editor.chain().focus().setImage({ src: url, alt }).run();\n }, [editor]);\n\n if (!editor) return null;\n\n const Btn = ({ on, active, label, title }: { on: () => void; active?: boolean; label: string; title?: string }) => (\n <button\n type=\"button\"\n title={title}\n onMouseDown={(e) => e.preventDefault()}\n onClick={on}\n className={cn(\n \"h-8 min-w-8 rounded px-2 text-sm hover:bg-slate-100\",\n active && \"bg-slate-200 font-semibold\",\n )}\n >\n {label}\n </button>\n );\n\n return (\n <div>\n <div className=\"flex flex-wrap gap-1 rounded-t-lg border border-slate-300 bg-slate-50 p-1\">\n <Btn on={() => editor.chain().focus().toggleBold().run()} active={editor.isActive(\"bold\")} label=\"B\" title=\"Vet\" />\n <Btn on={() => editor.chain().focus().toggleItalic().run()} active={editor.isActive(\"italic\")} label=\"I\" title=\"Cursief\" />\n <Btn on={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} active={editor.isActive(\"heading\", { level: 2 })} label=\"H2\" title=\"Kop\" />\n <Btn on={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} active={editor.isActive(\"heading\", { level: 3 })} label=\"H3\" title=\"Subkop\" />\n <Btn on={() => editor.chain().focus().toggleBulletList().run()} active={editor.isActive(\"bulletList\")} label=\"• Lijst\" title=\"Opsomming\" />\n <Btn on={() => editor.chain().focus().toggleOrderedList().run()} active={editor.isActive(\"orderedList\")} label=\"1. Lijst\" title=\"Genummerde lijst\" />\n <Btn on={() => editor.chain().focus().toggleBlockquote().run()} active={editor.isActive(\"blockquote\")} label=\"❝\" title=\"Citaat\" />\n <Btn on={setLink} active={editor.isActive(\"link\")} label=\"🔗\" title=\"Link\" />\n {siteId && (\n <MediaPickerButton\n siteId={siteId}\n label=\"🖼 Afbeelding\"\n variant=\"ghost\"\n onSelect={(m) => editor.chain().focus().setImage({ src: m.url, alt: m.alt ?? \"\" }).run()}\n />\n )}\n <Btn on={insertImageByUrl} label=\"🖼 URL\" title=\"Externe afbeelding via URL invoegen\" />\n </div>\n <EditorContent editor={editor} />\n </div>\n );\n}\n"],"mappings":"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","names":["editor"]}
@@ -1,8 +1,13 @@
1
- import type { ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
2
  /**
3
3
  * Generieke lijst-editor: rij toevoegen, omhoog/omlaag verplaatsen, verwijderen. Vervangt het
4
4
  * herhaalde patroon in de block-editors (FAQ, galerij, openingstijden, …) en wordt hergebruikt
5
5
  * voor nav/footer/social in het instellingenpaneel.
6
+ *
7
+ * BELANGRIJK: rijen krijgen een STABIELE key die met verplaatsen/verwijderen meebeweegt (géén
8
+ * array-index). Rijen bevatten stateful editors (TipTap, useState in JSON-/geo-velden); met
9
+ * index-keys zou React die state bij herordenen aan de verkeerde rij koppelen — stille
10
+ * datacorruptie door de klant zelf.
6
11
  */
7
12
  export declare function ArrayEditor<T>({ items, onChange, renderRow, newItem, addLabel, emptyLabel, }: {
8
13
  items: T[];
@@ -1,6 +1,9 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useRef } from "react";
3
4
  import { Button } from "./primitives";
5
+ let keyCounter = 0;
6
+ const genKey = () => `row-${++keyCounter}`;
4
7
  function ArrayEditor({
5
8
  items,
6
9
  onChange,
@@ -9,16 +12,32 @@ function ArrayEditor({
9
12
  addLabel = "+ Toevoegen",
10
13
  emptyLabel
11
14
  }) {
15
+ const keysRef = useRef([]);
16
+ if (keysRef.current.length !== items.length) {
17
+ const k = keysRef.current.slice(0, items.length);
18
+ while (k.length < items.length) k.push(genKey());
19
+ keysRef.current = k;
20
+ }
12
21
  const set = (next) => onChange(next);
13
22
  const update = (i, patch) => set(items.map((x, j) => j === i ? { ...x, ...patch } : x));
14
- const remove = (i) => set(items.filter((_, j) => j !== i));
23
+ const remove = (i) => {
24
+ keysRef.current = keysRef.current.filter((_, j) => j !== i);
25
+ set(items.filter((_, j) => j !== i));
26
+ };
15
27
  const move = (i, dir) => {
16
28
  const j = i + dir;
17
29
  if (j < 0 || j >= items.length) return;
18
30
  const copy = [...items];
19
31
  [copy[i], copy[j]] = [copy[j], copy[i]];
32
+ const keys = [...keysRef.current];
33
+ [keys[i], keys[j]] = [keys[j], keys[i]];
34
+ keysRef.current = keys;
20
35
  set(copy);
21
36
  };
37
+ const add = () => {
38
+ keysRef.current = [...keysRef.current, genKey()];
39
+ set([...items, newItem()]);
40
+ };
22
41
  return /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
23
42
  items.length === 0 && emptyLabel && /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: emptyLabel }),
24
43
  items.map((item, i) => /* @__PURE__ */ jsx("div", { className: "grid gap-2 rounded-lg border border-slate-200 p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
@@ -28,8 +47,8 @@ function ArrayEditor({
28
47
  /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => move(i, 1), "aria-label": "Omlaag", children: "\u2193" }),
29
48
  /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(i), "aria-label": "Verwijderen", children: "\u{1F5D1}" })
30
49
  ] })
31
- ] }) }, i)),
32
- /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => set([...items, newItem()]), children: addLabel }) })
50
+ ] }) }, keysRef.current[i])),
51
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: add, children: addLabel }) })
33
52
  ] });
34
53
  }
35
54
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/ui/ArrayEditor.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Button } from \"./primitives\";\n\n/**\n * Generieke lijst-editor: rij toevoegen, omhoog/omlaag verplaatsen, verwijderen. Vervangt het\n * herhaalde patroon in de block-editors (FAQ, galerij, openingstijden, …) en wordt hergebruikt\n * voor nav/footer/social in het instellingenpaneel.\n */\nexport function ArrayEditor<T>({\n items,\n onChange,\n renderRow,\n newItem,\n addLabel = \"+ Toevoegen\",\n emptyLabel,\n}: {\n items: T[];\n onChange: (next: T[]) => void;\n /** Render de bewerkbare velden van één rij. */\n renderRow: (item: T, update: (patch: Partial<T>) => void, index: number) => ReactNode;\n /** Maakt een nieuwe (lege) rij. */\n newItem: () => T;\n addLabel?: string;\n emptyLabel?: string;\n}) {\n const set = (next: T[]) => onChange(next);\n const update = (i: number, patch: Partial<T>) =>\n set(items.map((x, j) => (j === i ? { ...x, ...patch } : x)));\n const remove = (i: number) => set(items.filter((_, j) => j !== i));\n const move = (i: number, dir: -1 | 1) => {\n const j = i + dir;\n if (j < 0 || j >= items.length) return;\n const copy = [...items];\n [copy[i], copy[j]] = [copy[j], copy[i]];\n set(copy);\n };\n\n return (\n <div className=\"grid gap-2\">\n {items.length === 0 && emptyLabel && <p className=\"text-sm text-slate-500\">{emptyLabel}</p>}\n {items.map((item, i) => (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-200 p-3\">\n <div className=\"flex items-start gap-2\">\n <div className=\"flex-1\">{renderRow(item, (patch) => update(i, patch), i)}</div>\n <div className=\"flex flex-col gap-1\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => move(i, -1)} aria-label=\"Omhoog\">↑</Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => move(i, 1)} aria-label=\"Omlaag\">↓</Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => remove(i)} aria-label=\"Verwijderen\">🗑</Button>\n </div>\n </div>\n </div>\n ))}\n <div>\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={() => set([...items, newItem()])}>\n {addLabel}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";AAyC2C,cAK/B,YAL+B;AAtC3C,SAAS,cAAc;AAOhB,SAAS,YAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AACF,GASG;AACD,QAAM,MAAM,CAAC,SAAc,SAAS,IAAI;AACxC,QAAM,SAAS,CAAC,GAAW,UACzB,IAAI,MAAM,IAAI,CAAC,GAAG,MAAO,MAAM,IAAI,EAAE,GAAG,GAAG,GAAG,MAAM,IAAI,CAAE,CAAC;AAC7D,QAAM,SAAS,CAAC,MAAc,IAAI,MAAM,OAAO,CAAC,GAAG,MAAM,MAAM,CAAC,CAAC;AACjE,QAAM,OAAO,CAAC,GAAW,QAAgB;AACvC,UAAM,IAAI,IAAI;AACd,QAAI,IAAI,KAAK,KAAK,MAAM,OAAQ;AAChC,UAAM,OAAO,CAAC,GAAG,KAAK;AACtB,KAAC,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC;AACtC,QAAI,IAAI;AAAA,EACV;AAEA,SACE,qBAAC,SAAI,WAAU,cACZ;AAAA,UAAM,WAAW,KAAK,cAAc,oBAAC,OAAE,WAAU,0BAA0B,sBAAW;AAAA,IACtF,MAAM,IAAI,CAAC,MAAM,MAChB,oBAAC,SAAY,WAAU,qDACrB,+BAAC,SAAI,WAAU,0BACb;AAAA,0BAAC,SAAI,WAAU,UAAU,oBAAU,MAAM,CAAC,UAAU,OAAO,GAAG,KAAK,GAAG,CAAC,GAAE;AAAA,MACzE,qBAAC,SAAI,WAAU,uBACb;AAAA,4BAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,KAAK,GAAG,EAAE,GAAG,cAAW,UAAS,oBAAC;AAAA,QACjG,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,KAAK,GAAG,CAAC,GAAG,cAAW,UAAS,oBAAC;AAAA,QAChG,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,OAAO,CAAC,GAAG,cAAW,eAAc,uBAAE;AAAA,SACvG;AAAA,OACF,KARQ,CASV,CACD;AAAA,IACD,oBAAC,SACC,8BAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,MAAK,MAAK,SAAS,MAAM,IAAI,CAAC,GAAG,OAAO,QAAQ,CAAC,CAAC,GACzF,oBACH,GACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/ui/ArrayEditor.tsx"],"sourcesContent":["\"use client\";\n\nimport { useRef, type ReactNode } from \"react\";\nimport { Button } from \"./primitives\";\n\nlet keyCounter = 0;\nconst genKey = () => `row-${++keyCounter}`;\n\n/**\n * Generieke lijst-editor: rij toevoegen, omhoog/omlaag verplaatsen, verwijderen. Vervangt het\n * herhaalde patroon in de block-editors (FAQ, galerij, openingstijden, …) en wordt hergebruikt\n * voor nav/footer/social in het instellingenpaneel.\n *\n * BELANGRIJK: rijen krijgen een STABIELE key die met verplaatsen/verwijderen meebeweegt (géén\n * array-index). Rijen bevatten stateful editors (TipTap, useState in JSON-/geo-velden); met\n * index-keys zou React die state bij herordenen aan de verkeerde rij koppelen — stille\n * datacorruptie door de klant zelf.\n */\nexport function ArrayEditor<T>({\n items,\n onChange,\n renderRow,\n newItem,\n addLabel = \"+ Toevoegen\",\n emptyLabel,\n}: {\n items: T[];\n onChange: (next: T[]) => void;\n /** Render de bewerkbare velden van één rij. */\n renderRow: (item: T, update: (patch: Partial<T>) => void, index: number) => ReactNode;\n /** Maakt een nieuwe (lege) rij. */\n newItem: () => T;\n addLabel?: string;\n emptyLabel?: string;\n}) {\n // Parallelle key-lijst die met de mutaties van DEZE editor meebeweegt. Externe lengte-wijzigingen\n // (bv. undo in de VisualEditor) worden bijgetrokken door groeien/krimpen aan het eind.\n const keysRef = useRef<string[]>([]);\n if (keysRef.current.length !== items.length) {\n const k = keysRef.current.slice(0, items.length);\n while (k.length < items.length) k.push(genKey());\n keysRef.current = k;\n }\n\n const set = (next: T[]) => onChange(next);\n const update = (i: number, patch: Partial<T>) =>\n set(items.map((x, j) => (j === i ? { ...x, ...patch } : x)));\n const remove = (i: number) => {\n keysRef.current = keysRef.current.filter((_, j) => j !== i);\n set(items.filter((_, j) => j !== i));\n };\n const move = (i: number, dir: -1 | 1) => {\n const j = i + dir;\n if (j < 0 || j >= items.length) return;\n const copy = [...items];\n [copy[i], copy[j]] = [copy[j], copy[i]];\n const keys = [...keysRef.current];\n [keys[i], keys[j]] = [keys[j], keys[i]];\n keysRef.current = keys;\n set(copy);\n };\n const add = () => {\n keysRef.current = [...keysRef.current, genKey()];\n set([...items, newItem()]);\n };\n\n return (\n <div className=\"grid gap-2\">\n {items.length === 0 && emptyLabel && <p className=\"text-sm text-slate-500\">{emptyLabel}</p>}\n {items.map((item, i) => (\n <div key={keysRef.current[i]} className=\"grid gap-2 rounded-lg border border-slate-200 p-3\">\n <div className=\"flex items-start gap-2\">\n <div className=\"flex-1\">{renderRow(item, (patch) => update(i, patch), i)}</div>\n <div className=\"flex flex-col gap-1\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => move(i, -1)} aria-label=\"Omhoog\">↑</Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => move(i, 1)} aria-label=\"Omlaag\">↓</Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => remove(i)} aria-label=\"Verwijderen\">🗑</Button>\n </div>\n </div>\n </div>\n ))}\n <div>\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={add}>\n {addLabel}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";AAoE2C,cAK/B,YAL+B;AAlE3C,SAAS,cAA8B;AACvC,SAAS,cAAc;AAEvB,IAAI,aAAa;AACjB,MAAM,SAAS,MAAM,OAAO,EAAE,UAAU;AAYjC,SAAS,YAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AACF,GASG;AAGD,QAAM,UAAU,OAAiB,CAAC,CAAC;AACnC,MAAI,QAAQ,QAAQ,WAAW,MAAM,QAAQ;AAC3C,UAAM,IAAI,QAAQ,QAAQ,MAAM,GAAG,MAAM,MAAM;AAC/C,WAAO,EAAE,SAAS,MAAM,OAAQ,GAAE,KAAK,OAAO,CAAC;AAC/C,YAAQ,UAAU;AAAA,EACpB;AAEA,QAAM,MAAM,CAAC,SAAc,SAAS,IAAI;AACxC,QAAM,SAAS,CAAC,GAAW,UACzB,IAAI,MAAM,IAAI,CAAC,GAAG,MAAO,MAAM,IAAI,EAAE,GAAG,GAAG,GAAG,MAAM,IAAI,CAAE,CAAC;AAC7D,QAAM,SAAS,CAAC,MAAc;AAC5B,YAAQ,UAAU,QAAQ,QAAQ,OAAO,CAAC,GAAG,MAAM,MAAM,CAAC;AAC1D,QAAI,MAAM,OAAO,CAAC,GAAG,MAAM,MAAM,CAAC,CAAC;AAAA,EACrC;AACA,QAAM,OAAO,CAAC,GAAW,QAAgB;AACvC,UAAM,IAAI,IAAI;AACd,QAAI,IAAI,KAAK,KAAK,MAAM,OAAQ;AAChC,UAAM,OAAO,CAAC,GAAG,KAAK;AACtB,KAAC,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC;AACtC,UAAM,OAAO,CAAC,GAAG,QAAQ,OAAO;AAChC,KAAC,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC;AACtC,YAAQ,UAAU;AAClB,QAAI,IAAI;AAAA,EACV;AACA,QAAM,MAAM,MAAM;AAChB,YAAQ,UAAU,CAAC,GAAG,QAAQ,SAAS,OAAO,CAAC;AAC/C,QAAI,CAAC,GAAG,OAAO,QAAQ,CAAC,CAAC;AAAA,EAC3B;AAEA,SACE,qBAAC,SAAI,WAAU,cACZ;AAAA,UAAM,WAAW,KAAK,cAAc,oBAAC,OAAE,WAAU,0BAA0B,sBAAW;AAAA,IACtF,MAAM,IAAI,CAAC,MAAM,MAChB,oBAAC,SAA6B,WAAU,qDACtC,+BAAC,SAAI,WAAU,0BACb;AAAA,0BAAC,SAAI,WAAU,UAAU,oBAAU,MAAM,CAAC,UAAU,OAAO,GAAG,KAAK,GAAG,CAAC,GAAE;AAAA,MACzE,qBAAC,SAAI,WAAU,uBACb;AAAA,4BAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,KAAK,GAAG,EAAE,GAAG,cAAW,UAAS,oBAAC;AAAA,QACjG,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,KAAK,GAAG,CAAC,GAAG,cAAW,UAAS,oBAAC;AAAA,QAChG,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,OAAO,CAAC,GAAG,cAAW,eAAc,uBAAE;AAAA,SACvG;AAAA,OACF,KARQ,QAAQ,QAAQ,CAAC,CAS3B,CACD;AAAA,IACD,oBAAC,SACC,8BAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,MAAK,MAAK,SAAS,KAC1D,oBACH,GACF;AAAA,KACF;AAEJ;","names":[]}
@@ -10,7 +10,7 @@ import { InlineImageEditProvider } from "../../media/ImageContext";
10
10
  import { MediaPickerDialog } from "../media/MediaPicker";
11
11
  import { EmptyState } from "@astryxdesign/core/EmptyState";
12
12
  import { buildBlockAddOptions } from "../../blocks/registry";
13
- import { getDraftByPath, saveDraft, publishPage, subscribeDraft, subscribeVersions, restoreVersion } from "../lib/pagesClient";
13
+ import { createPage, getDraftByPath, saveDraft, publishPage, subscribeDraft, subscribeVersions, restoreVersion } from "../lib/pagesClient";
14
14
  import { isDemoActive } from "../../demo/demoStore";
15
15
  import { builtinBlockEditors, GenericEditor } from "../blockEditors";
16
16
  import { StyleEditor } from "../blockEditors/StyleEditor";
@@ -124,6 +124,7 @@ function VisualEditorInner({ onClose }) {
124
124
  if (next === d) return;
125
125
  pushHistory(d, opts?.coalesce ?? false);
126
126
  draftRef.current = next;
127
+ dirtyRef.current = true;
127
128
  setDraft(next);
128
129
  },
129
130
  [pushHistory]
@@ -136,6 +137,7 @@ function VisualEditorInner({ onClose }) {
136
137
  h.future.push(d);
137
138
  h.lastPush = 0;
138
139
  draftRef.current = prev;
140
+ dirtyRef.current = true;
139
141
  setDraft(prev);
140
142
  setHistVersion((v) => v + 1);
141
143
  }, []);
@@ -147,6 +149,7 @@ function VisualEditorInner({ onClose }) {
147
149
  h.past.push(d);
148
150
  h.lastPush = 0;
149
151
  draftRef.current = next;
152
+ dirtyRef.current = true;
150
153
  setDraft(next);
151
154
  setHistVersion((v) => v + 1);
152
155
  }, []);
@@ -189,6 +192,26 @@ function VisualEditorInner({ onClose }) {
189
192
  active = false;
190
193
  };
191
194
  }, [siteId, path]);
195
+ const handleCreatePage = useCallback(async () => {
196
+ setStatus("loading");
197
+ try {
198
+ const segment = path.split("/").filter(Boolean).pop() ?? "";
199
+ const title = segment ? segment.replace(/[-_]+/g, " ").replace(/^\w/, (c) => c.toUpperCase()) : "Home";
200
+ await createPage(siteId, { title, slug: segment || "home", path });
201
+ const d = await getDraftByPath(siteId, path);
202
+ if (d) {
203
+ setDraft(d);
204
+ lastSavedRef.current = stableStringify(draftPayload(d));
205
+ setStatus("idle");
206
+ toast({ title: "Pagina aangemaakt", description: "Voeg je eerste blok toe en publiceer wanneer je klaar bent.", tone: "success" });
207
+ } else {
208
+ setStatus("none");
209
+ }
210
+ } catch (e) {
211
+ setStatus("none");
212
+ toast({ title: "Pagina aanmaken mislukt", description: e?.message, tone: "error" });
213
+ }
214
+ }, [siteId, path, toast]);
192
215
  const draftId = draft?.id;
193
216
  useEffect(() => {
194
217
  if (!draftId) return;
@@ -480,14 +503,17 @@ function VisualEditorInner({ onClose }) {
480
503
  /* @__PURE__ */ jsxs("div", { className: "xicms-ve__body", children: [
481
504
  /* @__PURE__ */ jsxs("main", { className: "xicms-ve__canvas", children: [
482
505
  status === "loading" && /* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Laden\u2026" }),
483
- status === "none" && /* @__PURE__ */ jsx(
484
- EmptyState,
485
- {
486
- icon: "\u{1F4C4}",
487
- title: "Geen werkversie gevonden",
488
- description: `Voor deze pagina (${path}) bestaat nog geen werkversie. Maak de pagina eerst aan in /beheer \u2192 Pagina's.`
489
- }
490
- ),
506
+ status === "none" && /* @__PURE__ */ jsxs("div", { className: "xicms-ve__empty", children: [
507
+ /* @__PURE__ */ jsx(
508
+ EmptyState,
509
+ {
510
+ icon: "\u{1F4C4}",
511
+ title: "Geen werkversie gevonden",
512
+ description: `Voor deze pagina (${path}) bestaat nog geen werkversie. Maak hem hier direct aan, of ga naar /beheer \u2192 Pagina's.`
513
+ }
514
+ ),
515
+ /* @__PURE__ */ jsx("p", { style: { textAlign: "center", marginTop: "1rem" }, children: /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-btn", onClick: () => void handleCreatePage(), children: "+ Deze pagina aanmaken" }) })
516
+ ] }),
491
517
  draft && /* @__PURE__ */ jsxs("div", { className: "container", ref: canvasRef, children: [
492
518
  blocks.length === 0 && /* @__PURE__ */ jsx(EmptyState, { icon: "\u{1F9F1}", title: "Nog geen blokken", description: "Voeg hieronder je eerste blok toe." }),
493
519
  displayBlocks.map((b, i) => {