@xiwebdesign/xicms-core 5.0.6 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/XiCMSProvider.d.ts +1 -1
- package/dist/XiCMSProvider.js +42 -30
- package/dist/XiCMSProvider.js.map +1 -1
- package/dist/admin/editors/RichTextEditor.js +7 -1
- package/dist/admin/editors/RichTextEditor.js.map +1 -1
- package/dist/admin/ui/ArrayEditor.d.ts +6 -1
- package/dist/admin/ui/ArrayEditor.js +22 -3
- package/dist/admin/ui/ArrayEditor.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +104 -12
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/blocks/builtins/Agenda.js +2 -1
- package/dist/blocks/builtins/Agenda.js.map +1 -1
- package/dist/blocks/builtins/Breadcrumbs.js +2 -1
- package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
- package/dist/blocks/builtins/Contact.js +2 -1
- package/dist/blocks/builtins/Contact.js.map +1 -1
- package/dist/blocks/builtins/Faq.js +2 -1
- package/dist/blocks/builtins/Faq.js.map +1 -1
- package/dist/blocks/builtins/Jobs.js +2 -1
- package/dist/blocks/builtins/Jobs.js.map +1 -1
- package/dist/blocks/builtins/OpeningHours.js +2 -1
- package/dist/blocks/builtins/OpeningHours.js.map +1 -1
- package/dist/blocks/builtins/Pricing.js +2 -1
- package/dist/blocks/builtins/Pricing.js.map +1 -1
- package/dist/blocks/builtins/Search.js +18 -8
- package/dist/blocks/builtins/Search.js.map +1 -1
- package/dist/blocks/builtins/Team.js +2 -1
- package/dist/blocks/builtins/Team.js.map +1 -1
- package/dist/blocks/builtins/Testimonials.js +2 -1
- package/dist/blocks/builtins/Testimonials.js.map +1 -1
- package/dist/blocks/builtins/jsonld.d.ts +9 -0
- package/dist/blocks/builtins/jsonld.js +7 -0
- package/dist/blocks/builtins/jsonld.js.map +1 -0
- package/dist/components/News.js +2 -1
- package/dist/components/News.js.map +1 -1
- package/dist/i18n/overlay.d.ts +13 -4
- package/dist/i18n/overlay.js +54 -8
- package/dist/i18n/overlay.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/pages.js +34 -13
- package/dist/server/pages.js.map +1 -1
- package/dist/styles.css +37 -0
- package/package.json +1 -1
package/dist/XiCMSProvider.d.ts
CHANGED
|
@@ -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
|
package/dist/XiCMSProvider.js
CHANGED
|
@@ -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
|
-
|
|
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",
|
|
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
|
-
|
|
190
|
+
onData(defs);
|
|
157
191
|
},
|
|
158
192
|
() => void 0
|
|
159
193
|
// leesfout → SSR-set blijft staan
|
|
160
194
|
);
|
|
161
|
-
}, [
|
|
162
|
-
|
|
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":";
|
|
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":";AAuEI,cAgBE,YAhBF;AArEJ,SAAS,WAAW,qBAAqB;AACzC,OAAO,gBAAgB;AACvB,OAAO,UAAU;AACjB,OAAO,WAAW;AAClB,OAAO,eAAe;AACtB,SAAS,aAAa,iBAAiB;AACvC,SAAS,UAAU;AACnB,SAAS,yBAAyB;AAO3B,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AAED,QAAM,SAAS,UAAU;AAAA,IACvB,mBAAmB;AAAA,IACnB,YAAY;AAAA,MACV,WAAW,UAAU,EAAE,SAAS,EAAE,QAAQ,CAAC,GAAG,GAAG,CAAC,EAAE,EAAE,CAAC;AAAA,MACvD,KAAK,UAAU,EAAE,aAAa,OAAO,UAAU,MAAM,gBAAgB,EAAE,KAAK,+BAA+B,EAAE,CAAC;AAAA,MAC9G,MAAM,UAAU,EAAE,QAAQ,OAAO,aAAa,OAAO,gBAAgB,EAAE,SAAS,QAAQ,UAAU,QAAQ,EAAE,CAAC;AAAA,IAC/G;AAAA,IACA,SAAS,SAAS;AAAA,IAClB,UAAU,CAAC,EAAE,QAAAA,QAAO,MAAM,SAAS,UAAU,SAASA,QAAO,QAAQ,CAAC,CAAC;AAAA,IACvE,aAAa;AAAA,MACX,YAAY,EAAE,OAAO,oFAAoF;AAAA,IAC3G;AAAA,EACF,CAAC;AAMD,YAAU,MAAM;AACd,QAAI,CAAC,UAAU,OAAO,UAAW;AACjC,UAAM,UAAU,OAAO,QAAQ;AAC/B,UAAM,OAAO,SAAS;AACtB,QAAI,YAAY,KAAM,QAAO,SAAS,WAAW,MAAM,KAAK;AAAA,EAC9D,GAAG,CAAC,OAAO,MAAM,CAAC;AAElB,QAAM,UAAU,YAAY,MAAM;AAChC,QAAI,CAAC,OAAQ;AACb,UAAM,OAAO,OAAO,cAAc,MAAM,EAAE;AAC1C,UAAM,MAAM,OAAO,OAAO,iCAAiC,QAAQ,UAAU;AAC7E,QAAI,QAAQ,KAAM;AAClB,QAAI,QAAQ,GAAI,QAAO,MAAM,EAAE,MAAM,EAAE,gBAAgB,MAAM,EAAE,UAAU,EAAE,IAAI;AAAA,QAC1E,QAAO,MAAM,EAAE,MAAM,EAAE,gBAAgB,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC,EAAE,IAAI;AAAA,EACjF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,mBAAmB,YAAY,MAAM;AACzC,QAAI,CAAC,OAAQ;AACb,UAAM,MAAM,OAAO,OAAO,yBAAyB;AACnD,QAAI,CAAC,IAAK;AACV,UAAM,MAAM,OAAO,OAAO,iDAAiD,KAAK;AAChF,WAAO,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,KAAK,IAAI,CAAC,EAAE,IAAI;AAAA,EACzD,GAAG,CAAC,MAAM,CAAC;AAEX,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,MAAM,CAAC,EAAE,IAAI,QAAQ,OAAO,MAAM,MACtC;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL;AAAA,MACA,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,MACrC,SAAS;AAAA,MACT,WAAW;AAAA,QACT;AAAA,QACA,UAAU;AAAA,MACZ;AAAA,MAEC;AAAA;AAAA,EACH;AAGF,SACE,qBAAC,SACC;AAAA,yBAAC,SAAI,WAAU,6EACb;AAAA,0BAAC,OAAI,IAAI,MAAM,OAAO,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,GAAG,QAAQ,OAAO,SAAS,MAAM,GAAG,OAAM,KAAI,OAAM,OAAM;AAAA,MACjH,oBAAC,OAAI,IAAI,MAAM,OAAO,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,GAAG,QAAQ,OAAO,SAAS,QAAQ,GAAG,OAAM,KAAI,OAAM,WAAU;AAAA,MACzH,oBAAC,OAAI,IAAI,MAAM,OAAO,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,GAAG,QAAQ,OAAO,SAAS,WAAW,EAAE,OAAO,EAAE,CAAC,GAAG,OAAM,MAAK,OAAM,OAAM;AAAA,MAClJ,oBAAC,OAAI,IAAI,MAAM,OAAO,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,GAAG,QAAQ,OAAO,SAAS,WAAW,EAAE,OAAO,EAAE,CAAC,GAAG,OAAM,MAAK,OAAM,UAAS;AAAA,MACrJ,oBAAC,OAAI,IAAI,MAAM,OAAO,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI,GAAG,QAAQ,OAAO,SAAS,YAAY,GAAG,OAAM,gBAAU,OAAM,aAAY;AAAA,MACzI,oBAAC,OAAI,IAAI,MAAM,OAAO,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,IAAI,GAAG,QAAQ,OAAO,SAAS,aAAa,GAAG,OAAM,YAAW,OAAM,oBAAmB;AAAA,MACnJ,oBAAC,OAAI,IAAI,MAAM,OAAO,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI,GAAG,QAAQ,OAAO,SAAS,YAAY,GAAG,OAAM,UAAI,OAAM,UAAS;AAAA,MAChI,oBAAC,OAAI,IAAI,SAAS,QAAQ,OAAO,SAAS,MAAM,GAAG,OAAM,aAAK,OAAM,QAAO;AAAA,MAC1E,UACC;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,OAAM;AAAA,UACN,SAAQ;AAAA,UACR,UAAU,CAAC,MAAM,OAAO,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,OAAO,GAAG,CAAC,EAAE,IAAI;AAAA;AAAA,MACzF;AAAA,MAEF,oBAAC,OAAI,IAAI,kBAAkB,OAAM,iBAAS,OAAM,uCAAsC;AAAA,OACxF;AAAA,IACA,oBAAC,iBAAc,QAAgB;AAAA,KACjC;AAEJ;","names":["editor"]}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type
|
|
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) =>
|
|
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:
|
|
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
|
|
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,8 @@ 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
|
|
13
|
+
import DOMPurify from "dompurify";
|
|
14
|
+
import { createPage, getDraftByPath, saveDraft, publishPage, subscribeDraft, subscribeVersions, restoreVersion } from "../lib/pagesClient";
|
|
14
15
|
import { isDemoActive } from "../../demo/demoStore";
|
|
15
16
|
import { builtinBlockEditors, GenericEditor } from "../blockEditors";
|
|
16
17
|
import { StyleEditor } from "../blockEditors/StyleEditor";
|
|
@@ -124,6 +125,7 @@ function VisualEditorInner({ onClose }) {
|
|
|
124
125
|
if (next === d) return;
|
|
125
126
|
pushHistory(d, opts?.coalesce ?? false);
|
|
126
127
|
draftRef.current = next;
|
|
128
|
+
dirtyRef.current = true;
|
|
127
129
|
setDraft(next);
|
|
128
130
|
},
|
|
129
131
|
[pushHistory]
|
|
@@ -136,6 +138,7 @@ function VisualEditorInner({ onClose }) {
|
|
|
136
138
|
h.future.push(d);
|
|
137
139
|
h.lastPush = 0;
|
|
138
140
|
draftRef.current = prev;
|
|
141
|
+
dirtyRef.current = true;
|
|
139
142
|
setDraft(prev);
|
|
140
143
|
setHistVersion((v) => v + 1);
|
|
141
144
|
}, []);
|
|
@@ -147,6 +150,7 @@ function VisualEditorInner({ onClose }) {
|
|
|
147
150
|
h.past.push(d);
|
|
148
151
|
h.lastPush = 0;
|
|
149
152
|
draftRef.current = next;
|
|
153
|
+
dirtyRef.current = true;
|
|
150
154
|
setDraft(next);
|
|
151
155
|
setHistVersion((v) => v + 1);
|
|
152
156
|
}, []);
|
|
@@ -189,6 +193,26 @@ function VisualEditorInner({ onClose }) {
|
|
|
189
193
|
active = false;
|
|
190
194
|
};
|
|
191
195
|
}, [siteId, path]);
|
|
196
|
+
const handleCreatePage = useCallback(async () => {
|
|
197
|
+
setStatus("loading");
|
|
198
|
+
try {
|
|
199
|
+
const segment = path.split("/").filter(Boolean).pop() ?? "";
|
|
200
|
+
const title = segment ? segment.replace(/[-_]+/g, " ").replace(/^\w/, (c) => c.toUpperCase()) : "Home";
|
|
201
|
+
await createPage(siteId, { title, slug: segment || "home", path });
|
|
202
|
+
const d = await getDraftByPath(siteId, path);
|
|
203
|
+
if (d) {
|
|
204
|
+
setDraft(d);
|
|
205
|
+
lastSavedRef.current = stableStringify(draftPayload(d));
|
|
206
|
+
setStatus("idle");
|
|
207
|
+
toast({ title: "Pagina aangemaakt", description: "Voeg je eerste blok toe en publiceer wanneer je klaar bent.", tone: "success" });
|
|
208
|
+
} else {
|
|
209
|
+
setStatus("none");
|
|
210
|
+
}
|
|
211
|
+
} catch (e) {
|
|
212
|
+
setStatus("none");
|
|
213
|
+
toast({ title: "Pagina aanmaken mislukt", description: e?.message, tone: "error" });
|
|
214
|
+
}
|
|
215
|
+
}, [siteId, path, toast]);
|
|
192
216
|
const draftId = draft?.id;
|
|
193
217
|
useEffect(() => {
|
|
194
218
|
if (!draftId) return;
|
|
@@ -236,6 +260,24 @@ function VisualEditorInner({ onClose }) {
|
|
|
236
260
|
},
|
|
237
261
|
[mutateDraft, locale]
|
|
238
262
|
);
|
|
263
|
+
const [fmtBar, setFmtBar] = useState(null);
|
|
264
|
+
const fmtElRef = useRef(null);
|
|
265
|
+
const positionFmtBar = useCallback((el) => {
|
|
266
|
+
const r = el.getBoundingClientRect();
|
|
267
|
+
setFmtBar({
|
|
268
|
+
top: Math.max(56, r.top - 44),
|
|
269
|
+
left: Math.min(Math.max(8, r.left), Math.max(8, window.innerWidth - 300))
|
|
270
|
+
});
|
|
271
|
+
}, []);
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (!fmtBar) return;
|
|
274
|
+
const onScroll = () => {
|
|
275
|
+
const el = fmtElRef.current;
|
|
276
|
+
if (el) positionFmtBar(el);
|
|
277
|
+
};
|
|
278
|
+
window.addEventListener("scroll", onScroll, { capture: true, passive: true });
|
|
279
|
+
return () => window.removeEventListener("scroll", onScroll, { capture: true });
|
|
280
|
+
}, [fmtBar !== null, positionFmtBar]);
|
|
239
281
|
const commitText = useCallback(
|
|
240
282
|
(key, field, value) => {
|
|
241
283
|
if (isTranslating) setTranslation(`${key}.${field}`, value);
|
|
@@ -257,7 +299,17 @@ function VisualEditorInner({ onClose }) {
|
|
|
257
299
|
el.setAttribute("contenteditable", "true");
|
|
258
300
|
el.setAttribute("spellcheck", "true");
|
|
259
301
|
el.classList.add("xicms-ve-inline");
|
|
260
|
-
el.addEventListener("blur", () => commitText(selectedKey, field, html ? el.innerHTML : el.textContent ?? ""));
|
|
302
|
+
el.addEventListener("blur", () => commitText(selectedKey, field, html ? DOMPurify.sanitize(el.innerHTML) : el.textContent ?? ""));
|
|
303
|
+
if (html) {
|
|
304
|
+
el.addEventListener("focus", () => {
|
|
305
|
+
fmtElRef.current = el;
|
|
306
|
+
positionFmtBar(el);
|
|
307
|
+
});
|
|
308
|
+
el.addEventListener("blur", () => {
|
|
309
|
+
fmtElRef.current = null;
|
|
310
|
+
setFmtBar(null);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
261
313
|
el.addEventListener("keydown", (e) => {
|
|
262
314
|
if (e.key === "Escape") el.blur();
|
|
263
315
|
else if (!html && e.key === "Enter") {
|
|
@@ -287,7 +339,7 @@ function VisualEditorInner({ onClose }) {
|
|
|
287
339
|
delete el.dataset.veInline;
|
|
288
340
|
});
|
|
289
341
|
};
|
|
290
|
-
}, [selectedKey, blocks, commitText]);
|
|
342
|
+
}, [selectedKey, blocks, commitText, positionFmtBar]);
|
|
291
343
|
const dirtySinceRef = useRef(null);
|
|
292
344
|
useEffect(() => {
|
|
293
345
|
if (!draft) return;
|
|
@@ -480,14 +532,17 @@ function VisualEditorInner({ onClose }) {
|
|
|
480
532
|
/* @__PURE__ */ jsxs("div", { className: "xicms-ve__body", children: [
|
|
481
533
|
/* @__PURE__ */ jsxs("main", { className: "xicms-ve__canvas", children: [
|
|
482
534
|
status === "loading" && /* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Laden\u2026" }),
|
|
483
|
-
status === "none" && /* @__PURE__ */
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
535
|
+
status === "none" && /* @__PURE__ */ jsxs("div", { className: "xicms-ve__empty", children: [
|
|
536
|
+
/* @__PURE__ */ jsx(
|
|
537
|
+
EmptyState,
|
|
538
|
+
{
|
|
539
|
+
icon: "\u{1F4C4}",
|
|
540
|
+
title: "Geen werkversie gevonden",
|
|
541
|
+
description: `Voor deze pagina (${path}) bestaat nog geen werkversie. Maak hem hier direct aan, of ga naar /beheer \u2192 Pagina's.`
|
|
542
|
+
}
|
|
543
|
+
),
|
|
544
|
+
/* @__PURE__ */ jsx("p", { style: { textAlign: "center", marginTop: "1rem" }, children: /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-btn", onClick: () => void handleCreatePage(), children: "+ Deze pagina aanmaken" }) })
|
|
545
|
+
] }),
|
|
491
546
|
draft && /* @__PURE__ */ jsxs("div", { className: "container", ref: canvasRef, children: [
|
|
492
547
|
blocks.length === 0 && /* @__PURE__ */ jsx(EmptyState, { icon: "\u{1F9F1}", title: "Nog geen blokken", description: "Voeg hieronder je eerste blok toe." }),
|
|
493
548
|
displayBlocks.map((b, i) => {
|
|
@@ -621,9 +676,46 @@ function VisualEditorInner({ onClose }) {
|
|
|
621
676
|
setImgPicker(null);
|
|
622
677
|
}
|
|
623
678
|
}
|
|
624
|
-
)
|
|
679
|
+
),
|
|
680
|
+
fmtBar && /* @__PURE__ */ jsxs("div", { className: "xicms-ve-fmtbar", style: { top: fmtBar.top, left: fmtBar.left }, role: "toolbar", "aria-label": "Tekstopmaak", children: [
|
|
681
|
+
/* @__PURE__ */ jsx(FmtBtn, { label: /* @__PURE__ */ jsx("b", { children: "B" }), title: "Vet (Ctrl/Cmd+B)", onRun: () => document.execCommand("bold") }),
|
|
682
|
+
/* @__PURE__ */ jsx(FmtBtn, { label: /* @__PURE__ */ jsx("i", { children: "I" }), title: "Cursief (Ctrl/Cmd+I)", onRun: () => document.execCommand("italic") }),
|
|
683
|
+
/* @__PURE__ */ jsx(FmtBtn, { label: /* @__PURE__ */ jsx("u", { children: "U" }), title: "Onderstrepen (Ctrl/Cmd+U)", onRun: () => document.execCommand("underline") }),
|
|
684
|
+
/* @__PURE__ */ jsx(
|
|
685
|
+
FmtBtn,
|
|
686
|
+
{
|
|
687
|
+
label: "\u{1F517}",
|
|
688
|
+
title: "Link (selecteer eerst tekst; leeg = link verwijderen)",
|
|
689
|
+
onRun: () => {
|
|
690
|
+
const url = window.prompt("Link-URL (leeg = link verwijderen)", "https://");
|
|
691
|
+
if (url === null) return;
|
|
692
|
+
if (url === "" || url === "https://") document.execCommand("unlink");
|
|
693
|
+
else document.execCommand("createLink", false, url);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
),
|
|
697
|
+
/* @__PURE__ */ jsx("span", { className: "xicms-ve-fmtbar__sep", "aria-hidden": true }),
|
|
698
|
+
/* @__PURE__ */ jsx(FmtBtn, { label: "H2", title: "Grote kop", onRun: () => document.execCommand("formatBlock", false, "<h2>") }),
|
|
699
|
+
/* @__PURE__ */ jsx(FmtBtn, { label: "H3", title: "Kleine kop", onRun: () => document.execCommand("formatBlock", false, "<h3>") }),
|
|
700
|
+
/* @__PURE__ */ jsx(FmtBtn, { label: "\xB6", title: "Normale tekst", onRun: () => document.execCommand("formatBlock", false, "<p>") }),
|
|
701
|
+
/* @__PURE__ */ jsx("span", { className: "xicms-ve-fmtbar__sep", "aria-hidden": true }),
|
|
702
|
+
/* @__PURE__ */ jsx(FmtBtn, { label: "\u2022", title: "Opsomming", onRun: () => document.execCommand("insertUnorderedList") })
|
|
703
|
+
] })
|
|
625
704
|
] });
|
|
626
705
|
}
|
|
706
|
+
function FmtBtn({ label, title, onRun }) {
|
|
707
|
+
return /* @__PURE__ */ jsx(
|
|
708
|
+
"button",
|
|
709
|
+
{
|
|
710
|
+
type: "button",
|
|
711
|
+
title,
|
|
712
|
+
"aria-label": title,
|
|
713
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
714
|
+
onClick: onRun,
|
|
715
|
+
children: label
|
|
716
|
+
}
|
|
717
|
+
);
|
|
718
|
+
}
|
|
627
719
|
function PageSettings({
|
|
628
720
|
draft,
|
|
629
721
|
onChange
|