@xiwebdesign/xicms-core 7.8.1 → 7.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/lib/pagesClient.js +5 -1
- package/dist/admin/lib/pagesClient.js.map +1 -1
- package/dist/admin/shop/ProductsTab.js +4 -5
- package/dist/admin/shop/ProductsTab.js.map +1 -1
- package/dist/admin/shop/productClient.d.ts +11 -13
- package/dist/admin/shop/productClient.js +44 -4
- package/dist/admin/shop/productClient.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +158 -55
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/components/barOffset.js +7 -1
- package/dist/components/barOffset.js.map +1 -1
- package/dist/demo/demoStore.d.ts +2 -0
- package/dist/demo/demoStore.js +4 -0
- package/dist/demo/demoStore.js.map +1 -1
- package/dist/editor/EditorProvider.d.ts +9 -0
- package/dist/editor/EditorProvider.js +11 -2
- package/dist/editor/EditorProvider.js.map +1 -1
- package/dist/media/ImageContext.d.ts +1 -1
- package/dist/media/ImageContext.js +3 -1
- package/dist/media/ImageContext.js.map +1 -1
- package/dist/render/PublishedPage.js +13 -3
- package/dist/render/PublishedPage.js.map +1 -1
- package/dist/styles.css +59 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -17,10 +17,16 @@ function useBalkOffset(actief, hoogte = 40) {
|
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
verschuif();
|
|
20
|
-
|
|
20
|
+
let frame = 0;
|
|
21
|
+
const gepland = () => {
|
|
22
|
+
cancelAnimationFrame(frame);
|
|
23
|
+
frame = requestAnimationFrame(verschuif);
|
|
24
|
+
};
|
|
25
|
+
const obs = new MutationObserver(gepland);
|
|
21
26
|
obs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style"] });
|
|
22
27
|
return () => {
|
|
23
28
|
obs.disconnect();
|
|
29
|
+
cancelAnimationFrame(frame);
|
|
24
30
|
for (const { el, top } of aangepast) el.style.top = top;
|
|
25
31
|
};
|
|
26
32
|
}, [actief, hoogte]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/barOffset.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\n\n/**\n * Schuif de plakkende elementen van de SITE onder onze balk vandaan.\n *\n * ── WAAROM PADDING NIET GENOEG IS ─────────────────────────────────────────────────────────\n *\n * De beheerbalk staat `fixed` bovenaan en de pagina wordt omlaag geduwd met padding op `:root`.\n * Dat werkt voor alles wat gewoon meestroomt — maar niet voor een header met\n * `position: sticky; top: 0`. Die plakt aan de bovenkant van het VENSTER, niet aan de bovenkant\n * van de pagina, en schuift dus vrolijk onder onze balk door. Op groenhof was het logo en het hele\n * menu daardoor half afgedekt zodra je inlogde.\n *\n * Het is geen randgeval: een plakkende header is zo'n beetje de standaard, en elke klantsite kiest\n * daar zijn eigen klassenaam en eigen CSS voor. Wij kunnen die CSS niet kennen en willen hem al\n * helemaal niet overschrijven met een gok van een selector.\n *\n * Dus meten we het gewoon: welke elementen plakken of hangen vast aan de bovenrand, en zet daar\n * zolang de balk er staat een `top` van de balkhoogte op. Bij het opruimen gaat de oorspronkelijke\n * waarde terug — de site blijft van de klant.\n *\n * Bewust GEEN elementen aanraken die al verder naar beneden hangen (een plakkende zijbalk, een\n * knop rechtsonder): die zitten onze balk niet in de weg en verplaatsen zou ze juist verschuiven.\n */\nexport function useBalkOffset(actief: boolean, hoogte = 40): void {\n useEffect(() => {\n if (!actief || typeof document === \"undefined\") return;\n\n const aangepast: { el: HTMLElement; top: string }[] = [];\n\n const verschuif = () => {\n for (const el of Array.from(document.querySelectorAll<HTMLElement>(\"body *\"))) {\n // Onze eigen gereedschapslaag overslaan: die staat er met opzet bovenop.\n if (el.closest(\"[data-xicms-tooling]\")) continue;\n if (aangepast.some((a) => a.el === el)) continue;\n const cs = getComputedStyle(el);\n if (cs.position !== \"sticky\" && cs.position !== \"fixed\") continue;\n // `top: auto` betekent \"waar je toevallig staat\" — daar valt niets te corrigeren.\n const top = parseFloat(cs.top);\n if (!Number.isFinite(top) || top > 4) continue;\n aangepast.push({ el, top: el.style.top });\n el.style.top = `${top + hoogte}px`;\n }\n };\n\n verschuif();\n // Een site kan later nog iets plakkends tonen (een aankondigingsbalk die pas na consent komt,\n // een menu dat bij het scrollen van gedrag verandert). Eén meting bij het openen zou dat missen.\n const obs = new MutationObserver(
|
|
1
|
+
{"version":3,"sources":["../../src/components/barOffset.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\n\n/**\n * Schuif de plakkende elementen van de SITE onder onze balk vandaan.\n *\n * ── WAAROM PADDING NIET GENOEG IS ─────────────────────────────────────────────────────────\n *\n * De beheerbalk staat `fixed` bovenaan en de pagina wordt omlaag geduwd met padding op `:root`.\n * Dat werkt voor alles wat gewoon meestroomt — maar niet voor een header met\n * `position: sticky; top: 0`. Die plakt aan de bovenkant van het VENSTER, niet aan de bovenkant\n * van de pagina, en schuift dus vrolijk onder onze balk door. Op groenhof was het logo en het hele\n * menu daardoor half afgedekt zodra je inlogde.\n *\n * Het is geen randgeval: een plakkende header is zo'n beetje de standaard, en elke klantsite kiest\n * daar zijn eigen klassenaam en eigen CSS voor. Wij kunnen die CSS niet kennen en willen hem al\n * helemaal niet overschrijven met een gok van een selector.\n *\n * Dus meten we het gewoon: welke elementen plakken of hangen vast aan de bovenrand, en zet daar\n * zolang de balk er staat een `top` van de balkhoogte op. Bij het opruimen gaat de oorspronkelijke\n * waarde terug — de site blijft van de klant.\n *\n * Bewust GEEN elementen aanraken die al verder naar beneden hangen (een plakkende zijbalk, een\n * knop rechtsonder): die zitten onze balk niet in de weg en verplaatsen zou ze juist verschuiven.\n */\nexport function useBalkOffset(actief: boolean, hoogte = 40): void {\n useEffect(() => {\n if (!actief || typeof document === \"undefined\") return;\n\n const aangepast: { el: HTMLElement; top: string }[] = [];\n\n const verschuif = () => {\n for (const el of Array.from(document.querySelectorAll<HTMLElement>(\"body *\"))) {\n // Onze eigen gereedschapslaag overslaan: die staat er met opzet bovenop.\n if (el.closest(\"[data-xicms-tooling]\")) continue;\n if (aangepast.some((a) => a.el === el)) continue;\n const cs = getComputedStyle(el);\n if (cs.position !== \"sticky\" && cs.position !== \"fixed\") continue;\n // `top: auto` betekent \"waar je toevallig staat\" — daar valt niets te corrigeren.\n const top = parseFloat(cs.top);\n if (!Number.isFinite(top) || top > 4) continue;\n aangepast.push({ el, top: el.style.top });\n el.style.top = `${top + hoogte}px`;\n }\n };\n\n verschuif();\n // Een site kan later nog iets plakkends tonen (een aankondigingsbalk die pas na consent komt,\n // een menu dat bij het scrollen van gedrag verandert). Eén meting bij het openen zou dat missen.\n //\n // Wel gebundeld per frame: de observer kijkt naar class/style op de HELE boom, en tijdens het\n // typen in een contentEditable-veld komen mutaties in salvo's. Elke mutatie een volledige\n // DOM-scan geven zou het bewerken zelf traag maken — precies het scherm waar deze hook voor\n // bestaat. En: onze eigen `el.style.top` hieronder is zélf een mutatie; zonder bundeling\n // triggert de correctie de observer opnieuw (onschuldig maar zinloos rondpompen).\n let frame = 0;\n const gepland = () => {\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(verschuif);\n };\n const obs = new MutationObserver(gepland);\n obs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [\"class\", \"style\"] });\n\n return () => {\n obs.disconnect();\n cancelAnimationFrame(frame);\n for (const { el, top } of aangepast) el.style.top = top;\n };\n }, [actief, hoogte]);\n}\n"],"mappings":";AAEA,SAAS,iBAAiB;AAwBnB,SAAS,cAAc,QAAiB,SAAS,IAAU;AAChE,YAAU,MAAM;AACd,QAAI,CAAC,UAAU,OAAO,aAAa,YAAa;AAEhD,UAAM,YAAgD,CAAC;AAEvD,UAAM,YAAY,MAAM;AACtB,iBAAW,MAAM,MAAM,KAAK,SAAS,iBAA8B,QAAQ,CAAC,GAAG;AAE7E,YAAI,GAAG,QAAQ,sBAAsB,EAAG;AACxC,YAAI,UAAU,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,EAAG;AACxC,cAAM,KAAK,iBAAiB,EAAE;AAC9B,YAAI,GAAG,aAAa,YAAY,GAAG,aAAa,QAAS;AAEzD,cAAM,MAAM,WAAW,GAAG,GAAG;AAC7B,YAAI,CAAC,OAAO,SAAS,GAAG,KAAK,MAAM,EAAG;AACtC,kBAAU,KAAK,EAAE,IAAI,KAAK,GAAG,MAAM,IAAI,CAAC;AACxC,WAAG,MAAM,MAAM,GAAG,MAAM,MAAM;AAAA,MAChC;AAAA,IACF;AAEA,cAAU;AASV,QAAI,QAAQ;AACZ,UAAM,UAAU,MAAM;AACpB,2BAAqB,KAAK;AAC1B,cAAQ,sBAAsB,SAAS;AAAA,IACzC;AACA,UAAM,MAAM,IAAI,iBAAiB,OAAO;AACxC,QAAI,QAAQ,SAAS,MAAM,EAAE,WAAW,MAAM,SAAS,MAAM,YAAY,MAAM,iBAAiB,CAAC,SAAS,OAAO,EAAE,CAAC;AAEpH,WAAO,MAAM;AACX,UAAI,WAAW;AACf,2BAAqB,KAAK;AAC1B,iBAAW,EAAE,IAAI,IAAI,KAAK,UAAW,IAAG,MAAM,MAAM;AAAA,IACtD;AAAA,EACF,GAAG,CAAC,QAAQ,MAAM,CAAC;AACrB;","names":[]}
|
package/dist/demo/demoStore.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare function setDemoActive(on: boolean): void;
|
|
|
7
7
|
export declare function isDemoActive(): boolean;
|
|
8
8
|
/** Stabiel, deterministisch draft-id per pad, zodat pagina + editor naar dezelfde opslag wijzen. */
|
|
9
9
|
export declare function demoIdForPath(path: string): string;
|
|
10
|
+
/** De omkering: uit een demo-draft-id het pad terug. `null` voor een niet-demo-id. */
|
|
11
|
+
export declare function demoPathForId(id: string): string | null;
|
|
10
12
|
export declare function isDemoLoggedIn(): boolean;
|
|
11
13
|
export declare function setDemoLoggedIn(on: boolean): void;
|
|
12
14
|
export declare function registerSeed(siteId: string, path: string, page: Page): void;
|
package/dist/demo/demoStore.js
CHANGED
|
@@ -17,6 +17,9 @@ function isDemoActive() {
|
|
|
17
17
|
function demoIdForPath(path) {
|
|
18
18
|
return `demo:${path || "/"}`;
|
|
19
19
|
}
|
|
20
|
+
function demoPathForId(id) {
|
|
21
|
+
return id.startsWith("demo:") ? id.slice("demo:".length) : null;
|
|
22
|
+
}
|
|
20
23
|
function emit() {
|
|
21
24
|
if (typeof window !== "undefined") window.dispatchEvent(new Event(DEMO_EVENT));
|
|
22
25
|
}
|
|
@@ -144,6 +147,7 @@ export {
|
|
|
144
147
|
DEMO_EVENT,
|
|
145
148
|
addDemoMedia,
|
|
146
149
|
demoIdForPath,
|
|
150
|
+
demoPathForId,
|
|
147
151
|
getDemoForm,
|
|
148
152
|
getDemoNews,
|
|
149
153
|
getSeed,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/demo/demoStore.ts"],"sourcesContent":["\"use client\";\n\nimport type { Page } from \"../schema/pages\";\nimport type { MediaItem, NewsArticle } from \"../content/types\";\nimport type { FormDefinition } from \"../schema/forms\";\n\n/**\n * Lokale opslag voor de DEMO-bewerkmodus (`config.demoEditing`). Alle \"bewerkingen\" van een\n * bezoeker (teksten, blokken, publiceren) blijven UITSLUITEND in de browser van die bezoeker\n * (localStorage) — er gaat niets naar Firebase. Zo kunnen eindgebruikers het beheer veilig\n * uitproberen zonder dat er echt iets verandert. Een \"Reset\" wist alles weer.\n *\n * Bewust GEEN onderdeel van de echte (Firebase-)opslag: pagesClient en AuthProvider takken hier\n * alleen naartoe als de demo-modus actief is, en die staat alleen aan op een demo-site.\n */\n\nconst DRAFT_PREFIX = \"xicms-demo-draft\";\nconst MEDIA_PREFIX = \"xicms-demo-media\";\nconst SESSION_KEY = \"xicms-demo-session\";\nexport const DEMO_EVENT = \"xicms:demo\";\n\nlet active = false;\nconst seeds = new Map<string, Page>();\nconst newsSeeds = new Map<string, NewsArticle[]>();\nconst formSeeds = new Map<string, FormDefinition>();\nconst mediaSeeds = new Map<string, MediaItem[]>();\n\n/** Zet de demo-bewerkmodus aan/uit (door de provider, op basis van config.demoEditing). */\nexport function setDemoActive(on: boolean) {\n active = on;\n}\nexport function isDemoActive(): boolean {\n return active;\n}\n\n/** Stabiel, deterministisch draft-id per pad, zodat pagina + editor naar dezelfde opslag wijzen. */\nexport function demoIdForPath(path: string): string {\n return `demo:${path || \"/\"}`;\n}\n\nfunction emit() {\n if (typeof window !== \"undefined\") window.dispatchEvent(new Event(DEMO_EVENT));\n}\n\n// ── Pseudo-login (sessie) ──────────────────────────────────────────────────\n// In sessionStorage: verdwijnt als het tabblad sluit — benadrukt het tijdelijke karakter.\nexport function isDemoLoggedIn(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return window.sessionStorage.getItem(SESSION_KEY) === \"1\";\n } catch {\n return false;\n }\n}\nexport function setDemoLoggedIn(on: boolean) {\n if (typeof window === \"undefined\") return;\n try {\n if (on) window.sessionStorage.setItem(SESSION_KEY, \"1\");\n else window.sessionStorage.removeItem(SESSION_KEY);\n } catch {\n /* opslag geweigerd — login geldt dan alleen voor deze render */\n }\n emit();\n}\n\n// ── Seed-pagina's (de oorspronkelijke inhoud waar een bezoeker op verder bewerkt) ──\nexport function registerSeed(siteId: string, path: string, page: Page) {\n seeds.set(`${siteId}::${path}`, page);\n}\nexport function getSeed(siteId: string, path: string): Page | null {\n return seeds.get(`${siteId}::${path}`) ?? null;\n}\n\n// ── Demo-nieuws (zodat nieuws-blokken/widgets ook in de demo gevuld zijn, zonder Firebase) ──\nexport function registerDemoNews(siteId: string, articles: NewsArticle[]) {\n newsSeeds.set(siteId, articles);\n}\nexport function getDemoNews(siteId: string): NewsArticle[] {\n return newsSeeds.get(siteId) ?? [];\n}\n\n// ── Demo-formulieren (zodat het form-blok werkt zonder Firebase; inzenden doet lokaal niets) ──\nexport function registerDemoForm(siteId: string, formId: string, def: FormDefinition) {\n formSeeds.set(`${siteId}::${formId}`, def);\n}\nexport function getDemoForm(siteId: string, formId: string): FormDefinition | null {\n return formSeeds.get(`${siteId}::${formId}`) ?? null;\n}\n\n// ── Demo-media (bibliotheek + upload zonder Firebase) ─────────────────────\n// Geseede items (bv. de foto's van de demo-site zelf) + lokale \"uploads\" als data-URL in\n// localStorage. Zo werken kiezen, uploaden en vervangen in de demo écht — alles blijft lokaal.\n\n/** Seed de mediabibliotheek van de demo (curated set; alt = zoektekst in de kiezer). */\nexport function registerDemoMedia(siteId: string, items: Array<Pick<MediaItem, \"url\" | \"name\" | \"alt\">>) {\n mediaSeeds.set(\n siteId,\n items.map((m, i) => ({ id: `demo-seed-${i}`, url: m.url, path: \"demo\", name: m.name, alt: m.alt ?? \"\" })),\n );\n}\n\nfunction mediaKey(siteId: string) {\n return `${MEDIA_PREFIX}:${siteId}`;\n}\n\nfunction loadLocalDemoMedia(siteId: string): MediaItem[] {\n if (typeof window === \"undefined\") return [];\n try {\n const raw = window.localStorage.getItem(mediaKey(siteId));\n return raw ? (JSON.parse(raw) as MediaItem[]) : [];\n } catch {\n return [];\n }\n}\n\n/** Bibliotheek in demo-modus: lokale uploads (nieuwste eerst) + de geseede set. */\nexport function listDemoMedia(siteId: string): MediaItem[] {\n return [...loadLocalDemoMedia(siteId), ...(mediaSeeds.get(siteId) ?? [])];\n}\n\n/**\n * \"Upload\" in demo-modus: bewaar de (gecomprimeerde) afbeelding als data-URL in localStorage.\n * localStorage is beperkt (~5 MB) — we houden maximaal 10 lokale uploads aan en laten bij een\n * volle opslag het item alleen voor deze sessie in het geheugen bestaan (zelfde vangnet als drafts).\n */\nconst memoryOnlyMedia = new Map<string, MediaItem[]>();\nexport function addDemoMedia(siteId: string, item: { url: string; name: string }): MediaItem {\n const media: MediaItem = {\n id: `demo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n url: item.url,\n path: \"demo\",\n name: item.name,\n alt: \"\",\n };\n const next = [media, ...loadLocalDemoMedia(siteId)].slice(0, 10);\n try {\n window.localStorage.setItem(mediaKey(siteId), JSON.stringify(next));\n } catch {\n memoryOnlyMedia.set(siteId, [media, ...(memoryOnlyMedia.get(siteId) ?? [])]);\n }\n emit();\n return media;\n}\n\n/** Inclusief geheugen-only items (opslag-vol-vangnet) — gebruikt door de kiezer. */\nexport function listDemoMediaAll(siteId: string): MediaItem[] {\n return [...(memoryOnlyMedia.get(siteId) ?? []), ...listDemoMedia(siteId)];\n}\n\n// ── Lokale drafts (de bewerkingen) ─────────────────────────────────────────\nfunction draftKey(siteId: string, id: string) {\n return `${DRAFT_PREFIX}:${siteId}:${id}`;\n}\n\nexport function loadDemoDraft(siteId: string, id: string): Partial<Page> | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(draftKey(siteId, id));\n return raw ? (JSON.parse(raw) as Partial<Page>) : null;\n } catch {\n return null;\n }\n}\n\nexport function saveDemoDraft(siteId: string, id: string, patch: Partial<Page>) {\n if (typeof window === \"undefined\") return;\n try {\n const current = loadDemoDraft(siteId, id) ?? {};\n window.localStorage.setItem(draftKey(siteId, id), JSON.stringify({ ...current, ...patch }));\n } catch {\n /* opslag vol/geweigerd — bewerking geldt dan alleen voor deze sessie in het geheugen */\n }\n emit();\n}\n\n/** Verwijdert alle lokale demo-bewerkingen + login voor deze site (de \"Reset\"-knop). */\nexport function resetDemo(siteId: string) {\n if (typeof window === \"undefined\") return;\n try {\n const toRemove: string[] = [];\n for (let i = 0; i < window.localStorage.length; i++) {\n const k = window.localStorage.key(i);\n if (k && k.startsWith(`${DRAFT_PREFIX}:${siteId}:`)) toRemove.push(k);\n }\n toRemove.forEach((k) => window.localStorage.removeItem(k));\n window.localStorage.removeItem(mediaKey(siteId));\n memoryOnlyMedia.delete(siteId);\n window.sessionStorage.removeItem(SESSION_KEY);\n } catch {\n /* negeren */\n }\n emit();\n}\n\n/**\n * De effectieve demo-pagina = seed met lokale bewerkingen eroverheen. Gebruikt door de demo-pagina\n * om de wijzigingen van de bezoeker direct te tonen (ook na sluiten van de editor).\n */\nexport function resolveDemoPage(siteId: string, path: string): Page | null {\n const seed = getSeed(siteId, path);\n if (!seed) return null;\n const draft = loadDemoDraft(siteId, demoIdForPath(path));\n return draft ? ({ ...seed, ...draft } as Page) : seed;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/demo/demoStore.ts"],"sourcesContent":["\"use client\";\n\nimport type { Page } from \"../schema/pages\";\nimport type { MediaItem, NewsArticle } from \"../content/types\";\nimport type { FormDefinition } from \"../schema/forms\";\n\n/**\n * Lokale opslag voor de DEMO-bewerkmodus (`config.demoEditing`). Alle \"bewerkingen\" van een\n * bezoeker (teksten, blokken, publiceren) blijven UITSLUITEND in de browser van die bezoeker\n * (localStorage) — er gaat niets naar Firebase. Zo kunnen eindgebruikers het beheer veilig\n * uitproberen zonder dat er echt iets verandert. Een \"Reset\" wist alles weer.\n *\n * Bewust GEEN onderdeel van de echte (Firebase-)opslag: pagesClient en AuthProvider takken hier\n * alleen naartoe als de demo-modus actief is, en die staat alleen aan op een demo-site.\n */\n\nconst DRAFT_PREFIX = \"xicms-demo-draft\";\nconst MEDIA_PREFIX = \"xicms-demo-media\";\nconst SESSION_KEY = \"xicms-demo-session\";\nexport const DEMO_EVENT = \"xicms:demo\";\n\nlet active = false;\nconst seeds = new Map<string, Page>();\nconst newsSeeds = new Map<string, NewsArticle[]>();\nconst formSeeds = new Map<string, FormDefinition>();\nconst mediaSeeds = new Map<string, MediaItem[]>();\n\n/** Zet de demo-bewerkmodus aan/uit (door de provider, op basis van config.demoEditing). */\nexport function setDemoActive(on: boolean) {\n active = on;\n}\nexport function isDemoActive(): boolean {\n return active;\n}\n\n/** Stabiel, deterministisch draft-id per pad, zodat pagina + editor naar dezelfde opslag wijzen. */\nexport function demoIdForPath(path: string): string {\n return `demo:${path || \"/\"}`;\n}\n\n/** De omkering: uit een demo-draft-id het pad terug. `null` voor een niet-demo-id. */\nexport function demoPathForId(id: string): string | null {\n return id.startsWith(\"demo:\") ? id.slice(\"demo:\".length) : null;\n}\n\nfunction emit() {\n if (typeof window !== \"undefined\") window.dispatchEvent(new Event(DEMO_EVENT));\n}\n\n// ── Pseudo-login (sessie) ──────────────────────────────────────────────────\n// In sessionStorage: verdwijnt als het tabblad sluit — benadrukt het tijdelijke karakter.\nexport function isDemoLoggedIn(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return window.sessionStorage.getItem(SESSION_KEY) === \"1\";\n } catch {\n return false;\n }\n}\nexport function setDemoLoggedIn(on: boolean) {\n if (typeof window === \"undefined\") return;\n try {\n if (on) window.sessionStorage.setItem(SESSION_KEY, \"1\");\n else window.sessionStorage.removeItem(SESSION_KEY);\n } catch {\n /* opslag geweigerd — login geldt dan alleen voor deze render */\n }\n emit();\n}\n\n// ── Seed-pagina's (de oorspronkelijke inhoud waar een bezoeker op verder bewerkt) ──\nexport function registerSeed(siteId: string, path: string, page: Page) {\n seeds.set(`${siteId}::${path}`, page);\n}\nexport function getSeed(siteId: string, path: string): Page | null {\n return seeds.get(`${siteId}::${path}`) ?? null;\n}\n\n// ── Demo-nieuws (zodat nieuws-blokken/widgets ook in de demo gevuld zijn, zonder Firebase) ──\nexport function registerDemoNews(siteId: string, articles: NewsArticle[]) {\n newsSeeds.set(siteId, articles);\n}\nexport function getDemoNews(siteId: string): NewsArticle[] {\n return newsSeeds.get(siteId) ?? [];\n}\n\n// ── Demo-formulieren (zodat het form-blok werkt zonder Firebase; inzenden doet lokaal niets) ──\nexport function registerDemoForm(siteId: string, formId: string, def: FormDefinition) {\n formSeeds.set(`${siteId}::${formId}`, def);\n}\nexport function getDemoForm(siteId: string, formId: string): FormDefinition | null {\n return formSeeds.get(`${siteId}::${formId}`) ?? null;\n}\n\n// ── Demo-media (bibliotheek + upload zonder Firebase) ─────────────────────\n// Geseede items (bv. de foto's van de demo-site zelf) + lokale \"uploads\" als data-URL in\n// localStorage. Zo werken kiezen, uploaden en vervangen in de demo écht — alles blijft lokaal.\n\n/** Seed de mediabibliotheek van de demo (curated set; alt = zoektekst in de kiezer). */\nexport function registerDemoMedia(siteId: string, items: Array<Pick<MediaItem, \"url\" | \"name\" | \"alt\">>) {\n mediaSeeds.set(\n siteId,\n items.map((m, i) => ({ id: `demo-seed-${i}`, url: m.url, path: \"demo\", name: m.name, alt: m.alt ?? \"\" })),\n );\n}\n\nfunction mediaKey(siteId: string) {\n return `${MEDIA_PREFIX}:${siteId}`;\n}\n\nfunction loadLocalDemoMedia(siteId: string): MediaItem[] {\n if (typeof window === \"undefined\") return [];\n try {\n const raw = window.localStorage.getItem(mediaKey(siteId));\n return raw ? (JSON.parse(raw) as MediaItem[]) : [];\n } catch {\n return [];\n }\n}\n\n/** Bibliotheek in demo-modus: lokale uploads (nieuwste eerst) + de geseede set. */\nexport function listDemoMedia(siteId: string): MediaItem[] {\n return [...loadLocalDemoMedia(siteId), ...(mediaSeeds.get(siteId) ?? [])];\n}\n\n/**\n * \"Upload\" in demo-modus: bewaar de (gecomprimeerde) afbeelding als data-URL in localStorage.\n * localStorage is beperkt (~5 MB) — we houden maximaal 10 lokale uploads aan en laten bij een\n * volle opslag het item alleen voor deze sessie in het geheugen bestaan (zelfde vangnet als drafts).\n */\nconst memoryOnlyMedia = new Map<string, MediaItem[]>();\nexport function addDemoMedia(siteId: string, item: { url: string; name: string }): MediaItem {\n const media: MediaItem = {\n id: `demo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n url: item.url,\n path: \"demo\",\n name: item.name,\n alt: \"\",\n };\n const next = [media, ...loadLocalDemoMedia(siteId)].slice(0, 10);\n try {\n window.localStorage.setItem(mediaKey(siteId), JSON.stringify(next));\n } catch {\n memoryOnlyMedia.set(siteId, [media, ...(memoryOnlyMedia.get(siteId) ?? [])]);\n }\n emit();\n return media;\n}\n\n/** Inclusief geheugen-only items (opslag-vol-vangnet) — gebruikt door de kiezer. */\nexport function listDemoMediaAll(siteId: string): MediaItem[] {\n return [...(memoryOnlyMedia.get(siteId) ?? []), ...listDemoMedia(siteId)];\n}\n\n// ── Lokale drafts (de bewerkingen) ─────────────────────────────────────────\nfunction draftKey(siteId: string, id: string) {\n return `${DRAFT_PREFIX}:${siteId}:${id}`;\n}\n\nexport function loadDemoDraft(siteId: string, id: string): Partial<Page> | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(draftKey(siteId, id));\n return raw ? (JSON.parse(raw) as Partial<Page>) : null;\n } catch {\n return null;\n }\n}\n\nexport function saveDemoDraft(siteId: string, id: string, patch: Partial<Page>) {\n if (typeof window === \"undefined\") return;\n try {\n const current = loadDemoDraft(siteId, id) ?? {};\n window.localStorage.setItem(draftKey(siteId, id), JSON.stringify({ ...current, ...patch }));\n } catch {\n /* opslag vol/geweigerd — bewerking geldt dan alleen voor deze sessie in het geheugen */\n }\n emit();\n}\n\n/** Verwijdert alle lokale demo-bewerkingen + login voor deze site (de \"Reset\"-knop). */\nexport function resetDemo(siteId: string) {\n if (typeof window === \"undefined\") return;\n try {\n const toRemove: string[] = [];\n for (let i = 0; i < window.localStorage.length; i++) {\n const k = window.localStorage.key(i);\n if (k && k.startsWith(`${DRAFT_PREFIX}:${siteId}:`)) toRemove.push(k);\n }\n toRemove.forEach((k) => window.localStorage.removeItem(k));\n window.localStorage.removeItem(mediaKey(siteId));\n memoryOnlyMedia.delete(siteId);\n window.sessionStorage.removeItem(SESSION_KEY);\n } catch {\n /* negeren */\n }\n emit();\n}\n\n/**\n * De effectieve demo-pagina = seed met lokale bewerkingen eroverheen. Gebruikt door de demo-pagina\n * om de wijzigingen van de bezoeker direct te tonen (ook na sluiten van de editor).\n */\nexport function resolveDemoPage(siteId: string, path: string): Page | null {\n const seed = getSeed(siteId, path);\n if (!seed) return null;\n const draft = loadDemoDraft(siteId, demoIdForPath(path));\n return draft ? ({ ...seed, ...draft } as Page) : seed;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -16,6 +16,15 @@ interface EditorCtx {
|
|
|
16
16
|
editPath: string | null;
|
|
17
17
|
/** Door de editor aangeroepen zodra de werkversie laadt of wijzigt. */
|
|
18
18
|
publishDraft: (path: string, blocks: PaginaBlokken | null) => void;
|
|
19
|
+
/**
|
|
20
|
+
* "Vervang de foto in veld X van blok Y" — gezet door de VisualEditor zolang hij open is.
|
|
21
|
+
*
|
|
22
|
+
* De 📷-knopjes op de foto's zitten in de RENDER van de pagina (Img), de mediakiezer zit in de
|
|
23
|
+
* EDITOR. Die twee delen geen boom meer sinds het canvas weg is; dit is de brug. `null` buiten
|
|
24
|
+
* de bewerkmodus, en dan tekent Img geen knopjes.
|
|
25
|
+
*/
|
|
26
|
+
fotoVervanger: ((field: string, blockKey?: string) => void) | null;
|
|
27
|
+
zetFotoVervanger: (fn: ((field: string, blockKey?: string) => void) | null) => void;
|
|
19
28
|
}
|
|
20
29
|
export declare function EditorProvider({ children }: {
|
|
21
30
|
children: ReactNode;
|
|
@@ -15,11 +15,18 @@ const Ctx = createContext({
|
|
|
15
15
|
draftBlocks: null,
|
|
16
16
|
editPath: null,
|
|
17
17
|
publishDraft: () => {
|
|
18
|
+
},
|
|
19
|
+
fotoVervanger: null,
|
|
20
|
+
zetFotoVervanger: () => {
|
|
18
21
|
}
|
|
19
22
|
});
|
|
20
23
|
function EditorProvider({ children }) {
|
|
21
24
|
const [modus, setModusState] = useState("live");
|
|
22
25
|
const [draft, setDraft] = useState(null);
|
|
26
|
+
const [fotoVervanger, setFotoVervanger] = useState(null);
|
|
27
|
+
const zetFotoVervanger = useCallback((fn) => {
|
|
28
|
+
setFotoVervanger(() => fn);
|
|
29
|
+
}, []);
|
|
23
30
|
useEffect(() => {
|
|
24
31
|
try {
|
|
25
32
|
if (sessionStorage.getItem(OPSLAGSLEUTEL) === "bewerken") setModusState("bewerken");
|
|
@@ -46,8 +53,10 @@ function EditorProvider({ children }) {
|
|
|
46
53
|
closeEditor: () => zetModus("live"),
|
|
47
54
|
draftBlocks: draft?.blocks ?? null,
|
|
48
55
|
editPath: draft?.path ?? null,
|
|
49
|
-
publishDraft
|
|
50
|
-
|
|
56
|
+
publishDraft,
|
|
57
|
+
fotoVervanger,
|
|
58
|
+
zetFotoVervanger
|
|
59
|
+
}), [modus, draft, zetModus, publishDraft, fotoVervanger, zetFotoVervanger]);
|
|
51
60
|
return /* @__PURE__ */ jsxs(Ctx.Provider, { value, children: [
|
|
52
61
|
children,
|
|
53
62
|
modus === "bewerken" && /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(VisualEditor, { onClose: () => zetModus("live") }) })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/editor/EditorProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState, lazy, Suspense, type ReactNode } from \"react\";\n// Het bloktype van de PAGINA zelf, zodat wat hier langskomt per definitie past in wat\n// `PublishedPage` rendert. Een eigen definitie zou hier stil uit de pas kunnen lopen.\nimport type { Page } from \"../schema/pages\";\n\ntype PaginaBlokken = Page[\"blocks\"];\n\n/**\n * Lichte runtime-context voor het inline (visuele) bewerken op de live pagina. De echte editor\n * (TipTap/Radix, zwaar) wordt PAS geladen als een editor hem opent — publieke bezoekers downloaden\n * hem nooit. De editor leeft in de admin-entry; we laden 'm via een dynamische import (eigen chunk).\n *\n * ── WAAROM DE BLOKKEN HIER LANGSKOMEN ─────────────────────────────────────────────────────\n *\n * De editor tekende de pagina vroeger zélf, in een eigen canvas binnen een schermvullende overlay.\n * Je klikte \"Pagina bewerken\" en je site verdween achter een donker paneel met een nabootsing erin.\n * Dat voelde niet als je site bewerken maar als een ander programma openen.\n *\n * Nu blijft de PAGINA de pagina en wordt alleen de DATA vervangen: de editor laadt de werkversie en\n * zet die hier neer, waarna `PublishedPage` de werkversie rendert in plaats van de gepubliceerde\n * blokken. Dezelfde componenten, dezelfde huisstijl, dezelfde layout — alleen andere inhoud.\n *\n * Die omweg via de data is wezenlijk, geen implementatiedetail. Alleen potloodjes over de live\n * pagina leggen zou betekenen dat je de WERKVERSIE bewerkt terwijl je de GEPUBLICEERDE tekst ziet.\n * Zodra je iets wijzigt en nog niet publiceert, lopen die twee uit elkaar en bewerk je iets anders\n * dan er staat. Wat je ziet moet zijn wat je bewerkt.\n *\n * `editPath` hoort erbij: een site kan meerdere gerenderde pagina's in beeld hebben (een\n * ingekaderde pagina, een preview). Alleen het pad dat de editor open heeft mag zijn blokken\n * verwisselen.\n *\n * ── WAAROM DE MODUS EEN REFRESH MOET OVERLEVEN (7.8) ──────────────────────────────────────\n *\n * Bewerken was een toestand die alleen in het geheugen van één render-boom bestond. Een per ongeluk\n * ingedrukte F5 — of een klik op een link in je eigen site, want dat is een volledige navigatie —\n * zette je terug op de live site. Je wijzigingen waren niet weg (die staan in de werkversie in\n * Firestore), maar je zág ze niet meer, en dat is niet te onderscheiden van kwijt zijn.\n *\n * Daarom staat de modus in `sessionStorage`: hij overleeft een refresh en het navigeren door de\n * site, en verdwijnt vanzelf zodra het tabblad dichtgaat. Bewust niet `localStorage` — dan zit je\n * morgen in een nieuw venster nog steeds in de bewerkmodus zonder dat je erom vroeg.\n */\nconst VisualEditor = lazy(() => import(\"../admin/visual/VisualEditor\"));\n\n/** \"live\" = precies wat een bezoeker ziet. \"bewerken\" = dezelfde pagina, met werkversie en gereedschap. */\nexport type Bewerkmodus = \"live\" | \"bewerken\";\n\nconst OPSLAGSLEUTEL = \"xicms-bewerkmodus\";\n\ninterface EditorCtx {\n modus: Bewerkmodus;\n /** Blijft bestaan naast `modus` omdat het door de hele codebase heen gelezen wordt. */\n open: boolean;\n zetModus: (modus: Bewerkmodus) => void;\n openEditor: () => void;\n closeEditor: () => void;\n /** De blokken van de werkversie die de pagina moet tonen; `null` = toon gewoon het gepubliceerde. */\n draftBlocks: PaginaBlokken | null;\n /** Het pad waarvoor `draftBlocks` geldt. */\n editPath: string | null;\n /** Door de editor aangeroepen zodra de werkversie laadt of wijzigt. */\n publishDraft: (path: string, blocks: PaginaBlokken | null) => void;\n}\n\nconst Ctx = createContext<EditorCtx>({\n modus: \"live\",\n open: false,\n zetModus: () => {},\n openEditor: () => {},\n closeEditor: () => {},\n draftBlocks: null,\n editPath: null,\n publishDraft: () => {},\n});\n\nexport function EditorProvider({ children }: { children: ReactNode }) {\n /**\n * Start altijd op \"live\", ook als de opslag iets anders zegt.\n *\n * De server rendert deze boom zonder toegang tot `sessionStorage`. Zou de eerste client-render\n * meteen \"bewerken\" teruggeven, dan wijkt hij af van de HTML die er al staat en gooit React de\n * boom weg (hydration mismatch). Vandaar: eerst hetzelfde als de server, daarna in een effect\n * bijstellen. Eén frame \"live\" is niet zichtbaar; een kapotte hydration wel.\n */\n const [modus, setModusState] = useState<Bewerkmodus>(\"live\");\n const [draft, setDraft] = useState<{ path: string; blocks: PaginaBlokken } | null>(null);\n\n useEffect(() => {\n try {\n if (sessionStorage.getItem(OPSLAGSLEUTEL) === \"bewerken\") setModusState(\"bewerken\");\n } catch {\n /* privémodus of geblokkeerde opslag: dan gewoon op de live site beginnen */\n }\n }, []);\n\n const zetModus = useCallback((next: Bewerkmodus) => {\n setModusState(next);\n try {\n if (next === \"bewerken\") sessionStorage.setItem(OPSLAGSLEUTEL, \"bewerken\");\n else sessionStorage.removeItem(OPSLAGSLEUTEL);\n } catch {\n /* de modus werkt ook zonder opslag; alleen een refresh valt dan terug op live */\n }\n // De werkversie hoort bij de bewerkmodus. Blijft hij staan na het sluiten, dan ziet de bezoeker\n // ongepubliceerde tekst terwijl het gereedschap weg is — een pagina die iets toont wat niet\n // live is. De werkversie zelf blijft in Firestore staan; alleen deze weergave stopt.\n if (next === \"live\") setDraft(null);\n }, []);\n\n const publishDraft = useCallback((path: string, blocks: PaginaBlokken | null) => {\n setDraft(blocks ? { path, blocks } : null);\n }, []);\n\n const value = useMemo<EditorCtx>(() => ({\n modus,\n open: modus === \"bewerken\",\n zetModus,\n openEditor: () => zetModus(\"bewerken\"),\n closeEditor: () => zetModus(\"live\"),\n draftBlocks: draft?.blocks ?? null,\n editPath: draft?.path ?? null,\n publishDraft,\n }), [modus, draft, zetModus, publishDraft]);\n\n return (\n <Ctx.Provider value={value}>\n {children}\n {modus === \"bewerken\" && (\n <Suspense fallback={null}>\n <VisualEditor onClose={() => zetModus(\"live\")} />\n </Suspense>\n )}\n </Ctx.Provider>\n );\n}\n\n/** Open/sluit de visuele pagina-editor. */\nexport function useEditor(): EditorCtx {\n return useContext(Ctx);\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/editor/EditorProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState, lazy, Suspense, type ReactNode } from \"react\";\n// Het bloktype van de PAGINA zelf, zodat wat hier langskomt per definitie past in wat\n// `PublishedPage` rendert. Een eigen definitie zou hier stil uit de pas kunnen lopen.\nimport type { Page } from \"../schema/pages\";\n\ntype PaginaBlokken = Page[\"blocks\"];\n\n/**\n * Lichte runtime-context voor het inline (visuele) bewerken op de live pagina. De echte editor\n * (TipTap/Radix, zwaar) wordt PAS geladen als een editor hem opent — publieke bezoekers downloaden\n * hem nooit. De editor leeft in de admin-entry; we laden 'm via een dynamische import (eigen chunk).\n *\n * ── WAAROM DE BLOKKEN HIER LANGSKOMEN ─────────────────────────────────────────────────────\n *\n * De editor tekende de pagina vroeger zélf, in een eigen canvas binnen een schermvullende overlay.\n * Je klikte \"Pagina bewerken\" en je site verdween achter een donker paneel met een nabootsing erin.\n * Dat voelde niet als je site bewerken maar als een ander programma openen.\n *\n * Nu blijft de PAGINA de pagina en wordt alleen de DATA vervangen: de editor laadt de werkversie en\n * zet die hier neer, waarna `PublishedPage` de werkversie rendert in plaats van de gepubliceerde\n * blokken. Dezelfde componenten, dezelfde huisstijl, dezelfde layout — alleen andere inhoud.\n *\n * Die omweg via de data is wezenlijk, geen implementatiedetail. Alleen potloodjes over de live\n * pagina leggen zou betekenen dat je de WERKVERSIE bewerkt terwijl je de GEPUBLICEERDE tekst ziet.\n * Zodra je iets wijzigt en nog niet publiceert, lopen die twee uit elkaar en bewerk je iets anders\n * dan er staat. Wat je ziet moet zijn wat je bewerkt.\n *\n * `editPath` hoort erbij: een site kan meerdere gerenderde pagina's in beeld hebben (een\n * ingekaderde pagina, een preview). Alleen het pad dat de editor open heeft mag zijn blokken\n * verwisselen.\n *\n * ── WAAROM DE MODUS EEN REFRESH MOET OVERLEVEN (7.8) ──────────────────────────────────────\n *\n * Bewerken was een toestand die alleen in het geheugen van één render-boom bestond. Een per ongeluk\n * ingedrukte F5 — of een klik op een link in je eigen site, want dat is een volledige navigatie —\n * zette je terug op de live site. Je wijzigingen waren niet weg (die staan in de werkversie in\n * Firestore), maar je zág ze niet meer, en dat is niet te onderscheiden van kwijt zijn.\n *\n * Daarom staat de modus in `sessionStorage`: hij overleeft een refresh en het navigeren door de\n * site, en verdwijnt vanzelf zodra het tabblad dichtgaat. Bewust niet `localStorage` — dan zit je\n * morgen in een nieuw venster nog steeds in de bewerkmodus zonder dat je erom vroeg.\n */\nconst VisualEditor = lazy(() => import(\"../admin/visual/VisualEditor\"));\n\n/** \"live\" = precies wat een bezoeker ziet. \"bewerken\" = dezelfde pagina, met werkversie en gereedschap. */\nexport type Bewerkmodus = \"live\" | \"bewerken\";\n\nconst OPSLAGSLEUTEL = \"xicms-bewerkmodus\";\n\ninterface EditorCtx {\n modus: Bewerkmodus;\n /** Blijft bestaan naast `modus` omdat het door de hele codebase heen gelezen wordt. */\n open: boolean;\n zetModus: (modus: Bewerkmodus) => void;\n openEditor: () => void;\n closeEditor: () => void;\n /** De blokken van de werkversie die de pagina moet tonen; `null` = toon gewoon het gepubliceerde. */\n draftBlocks: PaginaBlokken | null;\n /** Het pad waarvoor `draftBlocks` geldt. */\n editPath: string | null;\n /** Door de editor aangeroepen zodra de werkversie laadt of wijzigt. */\n publishDraft: (path: string, blocks: PaginaBlokken | null) => void;\n /**\n * \"Vervang de foto in veld X van blok Y\" — gezet door de VisualEditor zolang hij open is.\n *\n * De 📷-knopjes op de foto's zitten in de RENDER van de pagina (Img), de mediakiezer zit in de\n * EDITOR. Die twee delen geen boom meer sinds het canvas weg is; dit is de brug. `null` buiten\n * de bewerkmodus, en dan tekent Img geen knopjes.\n */\n fotoVervanger: ((field: string, blockKey?: string) => void) | null;\n zetFotoVervanger: (fn: ((field: string, blockKey?: string) => void) | null) => void;\n}\n\nconst Ctx = createContext<EditorCtx>({\n modus: \"live\",\n open: false,\n zetModus: () => {},\n openEditor: () => {},\n closeEditor: () => {},\n draftBlocks: null,\n editPath: null,\n publishDraft: () => {},\n fotoVervanger: null,\n zetFotoVervanger: () => {},\n});\n\nexport function EditorProvider({ children }: { children: ReactNode }) {\n /**\n * Start altijd op \"live\", ook als de opslag iets anders zegt.\n *\n * De server rendert deze boom zonder toegang tot `sessionStorage`. Zou de eerste client-render\n * meteen \"bewerken\" teruggeven, dan wijkt hij af van de HTML die er al staat en gooit React de\n * boom weg (hydration mismatch). Vandaar: eerst hetzelfde als de server, daarna in een effect\n * bijstellen. Eén frame \"live\" is niet zichtbaar; een kapotte hydration wel.\n */\n const [modus, setModusState] = useState<Bewerkmodus>(\"live\");\n const [draft, setDraft] = useState<{ path: string; blocks: PaginaBlokken } | null>(null);\n const [fotoVervanger, setFotoVervanger] = useState<((field: string, blockKey?: string) => void) | null>(null);\n // In state gewikkeld: een functie rechtstreeks aan een setState geven zou hem AANROEPEN\n // (functionele updater) in plaats van opslaan.\n const zetFotoVervanger = useCallback((fn: ((field: string, blockKey?: string) => void) | null) => {\n setFotoVervanger(() => fn);\n }, []);\n\n useEffect(() => {\n try {\n if (sessionStorage.getItem(OPSLAGSLEUTEL) === \"bewerken\") setModusState(\"bewerken\");\n } catch {\n /* privémodus of geblokkeerde opslag: dan gewoon op de live site beginnen */\n }\n }, []);\n\n const zetModus = useCallback((next: Bewerkmodus) => {\n setModusState(next);\n try {\n if (next === \"bewerken\") sessionStorage.setItem(OPSLAGSLEUTEL, \"bewerken\");\n else sessionStorage.removeItem(OPSLAGSLEUTEL);\n } catch {\n /* de modus werkt ook zonder opslag; alleen een refresh valt dan terug op live */\n }\n // De werkversie hoort bij de bewerkmodus. Blijft hij staan na het sluiten, dan ziet de bezoeker\n // ongepubliceerde tekst terwijl het gereedschap weg is — een pagina die iets toont wat niet\n // live is. De werkversie zelf blijft in Firestore staan; alleen deze weergave stopt.\n if (next === \"live\") setDraft(null);\n }, []);\n\n const publishDraft = useCallback((path: string, blocks: PaginaBlokken | null) => {\n setDraft(blocks ? { path, blocks } : null);\n }, []);\n\n const value = useMemo<EditorCtx>(() => ({\n modus,\n open: modus === \"bewerken\",\n zetModus,\n openEditor: () => zetModus(\"bewerken\"),\n closeEditor: () => zetModus(\"live\"),\n draftBlocks: draft?.blocks ?? null,\n editPath: draft?.path ?? null,\n publishDraft,\n fotoVervanger,\n zetFotoVervanger,\n }), [modus, draft, zetModus, publishDraft, fotoVervanger, zetFotoVervanger]);\n\n return (\n <Ctx.Provider value={value}>\n {children}\n {modus === \"bewerken\" && (\n <Suspense fallback={null}>\n <VisualEditor onClose={() => zetModus(\"live\")} />\n </Suspense>\n )}\n </Ctx.Provider>\n );\n}\n\n/** Open/sluit de visuele pagina-editor. */\nexport function useEditor(): EditorCtx {\n return useContext(Ctx);\n}\n"],"mappings":";AAkJI,SAIM,KAJN;AAhJJ,SAAS,eAAe,aAAa,YAAY,WAAW,SAAS,UAAU,MAAM,gBAAgC;AA0CrH,MAAM,eAAe,KAAK,MAAM,OAAO,8BAA8B,CAAC;AAKtE,MAAM,gBAAgB;AA0BtB,MAAM,MAAM,cAAyB;AAAA,EACnC,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,MAAM;AAAA,EAAC;AAAA,EACjB,YAAY,MAAM;AAAA,EAAC;AAAA,EACnB,aAAa,MAAM;AAAA,EAAC;AAAA,EACpB,aAAa;AAAA,EACb,UAAU;AAAA,EACV,cAAc,MAAM;AAAA,EAAC;AAAA,EACrB,eAAe;AAAA,EACf,kBAAkB,MAAM;AAAA,EAAC;AAC3B,CAAC;AAEM,SAAS,eAAe,EAAE,SAAS,GAA4B;AASpE,QAAM,CAAC,OAAO,aAAa,IAAI,SAAsB,MAAM;AAC3D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAyD,IAAI;AACvF,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAA8D,IAAI;AAG5G,QAAM,mBAAmB,YAAY,CAAC,OAA4D;AAChG,qBAAiB,MAAM,EAAE;AAAA,EAC3B,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI;AACF,UAAI,eAAe,QAAQ,aAAa,MAAM,WAAY,eAAc,UAAU;AAAA,IACpF,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,WAAW,YAAY,CAAC,SAAsB;AAClD,kBAAc,IAAI;AAClB,QAAI;AACF,UAAI,SAAS,WAAY,gBAAe,QAAQ,eAAe,UAAU;AAAA,UACpE,gBAAe,WAAW,aAAa;AAAA,IAC9C,QAAQ;AAAA,IAER;AAIA,QAAI,SAAS,OAAQ,UAAS,IAAI;AAAA,EACpC,GAAG,CAAC,CAAC;AAEL,QAAM,eAAe,YAAY,CAAC,MAAc,WAAiC;AAC/E,aAAS,SAAS,EAAE,MAAM,OAAO,IAAI,IAAI;AAAA,EAC3C,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ,QAAmB,OAAO;AAAA,IACtC;AAAA,IACA,MAAM,UAAU;AAAA,IAChB;AAAA,IACA,YAAY,MAAM,SAAS,UAAU;AAAA,IACrC,aAAa,MAAM,SAAS,MAAM;AAAA,IAClC,aAAa,OAAO,UAAU;AAAA,IAC9B,UAAU,OAAO,QAAQ;AAAA,IACzB;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,CAAC,OAAO,OAAO,UAAU,cAAc,eAAe,gBAAgB,CAAC;AAE3E,SACE,qBAAC,IAAI,UAAJ,EAAa,OACX;AAAA;AAAA,IACA,UAAU,cACT,oBAAC,YAAS,UAAU,MAClB,8BAAC,gBAAa,SAAS,MAAM,SAAS,MAAM,GAAG,GACjD;AAAA,KAEJ;AAEJ;AAGO,SAAS,YAAuB;AACrC,SAAO,WAAW,GAAG;AACvB;","names":[]}
|
|
@@ -13,7 +13,7 @@ export declare function ImageProvider({ component, children }: {
|
|
|
13
13
|
}): import("react").JSX.Element;
|
|
14
14
|
export declare function useImageComponent(): ImageComponentType | null;
|
|
15
15
|
export declare function InlineImageEditProvider({ onReplace, children, }: {
|
|
16
|
-
onReplace: ((field: string) => void) | null;
|
|
16
|
+
onReplace: ((field: string, blockKey?: string) => void) | null;
|
|
17
17
|
children: ReactNode;
|
|
18
18
|
}): import("react").JSX.Element;
|
|
19
19
|
export interface ImgProps {
|
|
@@ -51,7 +51,9 @@ function Img({ src, alt, width, height, sizes, priority, className, style, editF
|
|
|
51
51
|
const replace = (e) => {
|
|
52
52
|
e.preventDefault();
|
|
53
53
|
e.stopPropagation();
|
|
54
|
-
|
|
54
|
+
const el = e.currentTarget;
|
|
55
|
+
const key = el?.closest?.("[data-xicms-block]")?.getAttribute("data-xicms-block") ?? void 0;
|
|
56
|
+
onReplace(editField, key);
|
|
55
57
|
};
|
|
56
58
|
const btn = /* @__PURE__ */ jsx("button", { type: "button", className: `xicms-ve-img__btn${editFill ? " xicms-ve-img__btn--fill" : ""}`, title: "Klik om de foto te wijzigen", onClick: replace, children: "\u{1F4F7} Foto wijzigen" });
|
|
57
59
|
if (editFill) return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/media/ImageContext.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, type CSSProperties, type ElementType, type ReactNode } from \"react\";\n\n/**\n * Optionele framework-beeldcomponent (bv. Next `Image`) voor automatische optimalisatie: responsive\n * `srcset`, moderne formaten (AVIF/WebP), correcte `width/height` (geen layout-shift), lazy-loading.\n * Core importeert `next/image` niet zelf (optionele peer) — de site injecteert 'm via de\n * `imageComponent`-prop op `XiCMSProvider`, net als `linkComponent`. Zonder injectie valt alles terug\n * op een gewone `<img>` (huidig gedrag, werkt overal).\n */\nexport type ImageComponentType = ElementType;\n\nconst ImageContext = createContext<ImageComponentType | null>(null);\n\nexport function ImageProvider({ component, children }: { component?: ImageComponentType; children: ReactNode }) {\n return <ImageContext.Provider value={component ?? null}>{children}</ImageContext.Provider>;\n}\n\nexport function useImageComponent(): ImageComponentType | null {\n return useContext(ImageContext);\n}\n\n/**\n * Inline beeld-bewerken: de visuele editor levert via deze context een `onReplace(field)`-callback\n * voor het GESELECTEERDE blok. Is die actief én geeft een `<Img>` een `editField` mee, dan toont het\n * beeld een vervang-icoontje. Buiten de editor (context = null) rendert `<Img>` exact als voorheen.\n */\nconst InlineImageEditContext = createContext<((field: string) => void) | null>(null);\n\nexport function InlineImageEditProvider({\n onReplace,\n children,\n}: {\n onReplace: ((field: string) => void) | null;\n children: ReactNode;\n}) {\n return <InlineImageEditContext.Provider value={onReplace}>{children}</InlineImageEditContext.Provider>;\n}\n\nexport interface ImgProps {\n src: string;\n alt: string;\n /** Intrinsieke breedte/hoogte (verhouding) → nodig voor next/image én tegen CLS. */\n width: number;\n height: number;\n /** Hint voor responsive selectie, bv. \"(max-width: 720px) 100vw, 33vw\". */\n sizes?: string;\n /** Above-the-fold (hero): eager + hoge prioriteit voor LCP. */\n priority?: boolean;\n className?: string;\n style?: CSSProperties;\n /** Blokveld (pad) met de URL — schakelt inline vervangen in de visuele editor in (bv. \"imageUrl\"). */\n editField?: string;\n /** Beeld vult absoluut z'n parent (bv. hero-achtergrond): geen wrapper-box, knop absoluut t.o.v. het blok. */\n editFill?: boolean;\n}\n\n/**\n * Eén beeld-wrapper voor alle blokken. Gebruikt de geïnjecteerde `imageComponent` (next/image) als\n * die er is, anders een gewone `<img>`. De display-grootte regel je met CSS (`style`/`className`,\n * bv. `width:100%;height:100%;object-fit:cover`); `width`/`height` zijn de verhouding.\n */\nexport function Img({ src, alt, width, height, sizes, priority, className, style, editField, editFill }: ImgProps) {\n const ImageComponent = useImageComponent();\n const onReplace = useContext(InlineImageEditContext);\n const imageEl = ImageComponent ? (\n <ImageComponent\n src={src}\n alt={alt}\n width={width}\n height={height}\n sizes={sizes}\n priority={priority || undefined}\n className={className}\n style={style}\n />\n ) : (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={src}\n alt={alt}\n width={width}\n height={height}\n sizes={sizes}\n loading={priority ? \"eager\" : \"lazy\"}\n fetchPriority={priority ? \"high\" : undefined}\n className={className}\n style={style}\n />\n );\n\n // In de visuele editor (geselecteerd blok): toon een vervang-icoontje over het beeld. We wijzigen\n // alleen de URL → de grootte/stijl blijft (die komt uit het blok), dus blok én beeld behouden hun maat.\n if (onReplace && editField) {\n const replace = (e: { preventDefault: () => void; stopPropagation: () => void }) => {\n e.preventDefault();\n e.stopPropagation();\n onReplace(editField);\n };\n const btn = (\n <button type=\"button\" className={`xicms-ve-img__btn${editFill ? \" xicms-ve-img__btn--fill\" : \"\"}`} title=\"Klik om de foto te wijzigen\" onClick={replace}>\n 📷 Foto wijzigen\n </button>\n );\n // Vullend (hero-achtergrond): geen wrapper-box (zou het absoluut gepositioneerde beeld breken);\n // de knop is absoluut t.o.v. het blok-element zelf (dat is `position:relative`).\n if (editFill) return (<>{imageEl}{btn}</>);\n // In-flow beeld: hele beeld klikbaar (duidelijke affordance) + knop rechtsboven.\n return (\n <span className=\"xicms-ve-img\" style={{ position: \"relative\", display: \"block\" }} title=\"Klik om de foto te wijzigen\" onClick={replace}>\n {imageEl}\n {btn}\n </span>\n );\n }\n return imageEl;\n}\n"],"mappings":";AAgBS,
|
|
1
|
+
{"version":3,"sources":["../../src/media/ImageContext.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, type CSSProperties, type ElementType, type ReactNode } from \"react\";\n\n/**\n * Optionele framework-beeldcomponent (bv. Next `Image`) voor automatische optimalisatie: responsive\n * `srcset`, moderne formaten (AVIF/WebP), correcte `width/height` (geen layout-shift), lazy-loading.\n * Core importeert `next/image` niet zelf (optionele peer) — de site injecteert 'm via de\n * `imageComponent`-prop op `XiCMSProvider`, net als `linkComponent`. Zonder injectie valt alles terug\n * op een gewone `<img>` (huidig gedrag, werkt overal).\n */\nexport type ImageComponentType = ElementType;\n\nconst ImageContext = createContext<ImageComponentType | null>(null);\n\nexport function ImageProvider({ component, children }: { component?: ImageComponentType; children: ReactNode }) {\n return <ImageContext.Provider value={component ?? null}>{children}</ImageContext.Provider>;\n}\n\nexport function useImageComponent(): ImageComponentType | null {\n return useContext(ImageContext);\n}\n\n/**\n * Inline beeld-bewerken: de visuele editor levert via deze context een `onReplace(field)`-callback\n * voor het GESELECTEERDE blok. Is die actief én geeft een `<Img>` een `editField` mee, dan toont het\n * beeld een vervang-icoontje. Buiten de editor (context = null) rendert `<Img>` exact als voorheen.\n */\n/**\n * `blockKey` komt sinds 7.9 uit de DOM, niet uit een per-blok provider.\n *\n * Tot 7.4 wikkelde de editor élk blok in zijn canvas apart in deze provider, met de blok-key in de\n * closure. Dat canvas is weg — de pagina van de klant ís de editor — en één provider om de hele\n * pagina weet niet meer bij welk blok een foto hoort. De knop lost dat op het moment van klikken\n * op: elk blok draagt al `data-xicms-block` (BlockRenderer), dus `closest()` vanaf de knop is\n * betrouwbaar en kost niets zolang er niemand klikt.\n */\nconst InlineImageEditContext = createContext<((field: string, blockKey?: string) => void) | null>(null);\n\nexport function InlineImageEditProvider({\n onReplace,\n children,\n}: {\n onReplace: ((field: string, blockKey?: string) => void) | null;\n children: ReactNode;\n}) {\n return <InlineImageEditContext.Provider value={onReplace}>{children}</InlineImageEditContext.Provider>;\n}\n\nexport interface ImgProps {\n src: string;\n alt: string;\n /** Intrinsieke breedte/hoogte (verhouding) → nodig voor next/image én tegen CLS. */\n width: number;\n height: number;\n /** Hint voor responsive selectie, bv. \"(max-width: 720px) 100vw, 33vw\". */\n sizes?: string;\n /** Above-the-fold (hero): eager + hoge prioriteit voor LCP. */\n priority?: boolean;\n className?: string;\n style?: CSSProperties;\n /** Blokveld (pad) met de URL — schakelt inline vervangen in de visuele editor in (bv. \"imageUrl\"). */\n editField?: string;\n /** Beeld vult absoluut z'n parent (bv. hero-achtergrond): geen wrapper-box, knop absoluut t.o.v. het blok. */\n editFill?: boolean;\n}\n\n/**\n * Eén beeld-wrapper voor alle blokken. Gebruikt de geïnjecteerde `imageComponent` (next/image) als\n * die er is, anders een gewone `<img>`. De display-grootte regel je met CSS (`style`/`className`,\n * bv. `width:100%;height:100%;object-fit:cover`); `width`/`height` zijn de verhouding.\n */\nexport function Img({ src, alt, width, height, sizes, priority, className, style, editField, editFill }: ImgProps) {\n const ImageComponent = useImageComponent();\n const onReplace = useContext(InlineImageEditContext);\n const imageEl = ImageComponent ? (\n <ImageComponent\n src={src}\n alt={alt}\n width={width}\n height={height}\n sizes={sizes}\n priority={priority || undefined}\n className={className}\n style={style}\n />\n ) : (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={src}\n alt={alt}\n width={width}\n height={height}\n sizes={sizes}\n loading={priority ? \"eager\" : \"lazy\"}\n fetchPriority={priority ? \"high\" : undefined}\n className={className}\n style={style}\n />\n );\n\n // In de visuele editor (geselecteerd blok): toon een vervang-icoontje over het beeld. We wijzigen\n // alleen de URL → de grootte/stijl blijft (die komt uit het blok), dus blok én beeld behouden hun maat.\n if (onReplace && editField) {\n const replace = (e: { preventDefault: () => void; stopPropagation: () => void; currentTarget?: unknown }) => {\n e.preventDefault();\n e.stopPropagation();\n const el = e.currentTarget as HTMLElement | undefined;\n const key = el?.closest?.(\"[data-xicms-block]\")?.getAttribute(\"data-xicms-block\") ?? undefined;\n onReplace(editField, key);\n };\n const btn = (\n <button type=\"button\" className={`xicms-ve-img__btn${editFill ? \" xicms-ve-img__btn--fill\" : \"\"}`} title=\"Klik om de foto te wijzigen\" onClick={replace}>\n 📷 Foto wijzigen\n </button>\n );\n // Vullend (hero-achtergrond): geen wrapper-box (zou het absoluut gepositioneerde beeld breken);\n // de knop is absoluut t.o.v. het blok-element zelf (dat is `position:relative`).\n if (editFill) return (<>{imageEl}{btn}</>);\n // In-flow beeld: hele beeld klikbaar (duidelijke affordance) + knop rechtsboven.\n return (\n <span className=\"xicms-ve-img\" style={{ position: \"relative\", display: \"block\" }} title=\"Klik om de foto te wijzigen\" onClick={replace}>\n {imageEl}\n {btn}\n </span>\n );\n }\n return imageEl;\n}\n"],"mappings":";AAgBS,SAsGiB,UAtGjB,KAsGiB,YAtGjB;AAdT,SAAS,eAAe,kBAAwE;AAWhG,MAAM,eAAe,cAAyC,IAAI;AAE3D,SAAS,cAAc,EAAE,WAAW,SAAS,GAA4D;AAC9G,SAAO,oBAAC,aAAa,UAAb,EAAsB,OAAO,aAAa,MAAO,UAAS;AACpE;AAEO,SAAS,oBAA+C;AAC7D,SAAO,WAAW,YAAY;AAChC;AAgBA,MAAM,yBAAyB,cAAmE,IAAI;AAE/F,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AACF,GAGG;AACD,SAAO,oBAAC,uBAAuB,UAAvB,EAAgC,OAAO,WAAY,UAAS;AACtE;AAyBO,SAAS,IAAI,EAAE,KAAK,KAAK,OAAO,QAAQ,OAAO,UAAU,WAAW,OAAO,WAAW,SAAS,GAAa;AACjH,QAAM,iBAAiB,kBAAkB;AACzC,QAAM,YAAY,WAAW,sBAAsB;AACnD,QAAM,UAAU,iBACd;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,YAAY;AAAA,MACtB;AAAA,MACA;AAAA;AAAA,EACF;AAAA;AAAA,IAGA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,WAAW,UAAU;AAAA,QAC9B,eAAe,WAAW,SAAS;AAAA,QACnC;AAAA,QACA;AAAA;AAAA,IACF;AAAA;AAKF,MAAI,aAAa,WAAW;AAC1B,UAAM,UAAU,CAAC,MAA4F;AAC3G,QAAE,eAAe;AACjB,QAAE,gBAAgB;AAClB,YAAM,KAAK,EAAE;AACb,YAAM,MAAM,IAAI,UAAU,oBAAoB,GAAG,aAAa,kBAAkB,KAAK;AACrF,gBAAU,WAAW,GAAG;AAAA,IAC1B;AACA,UAAM,MACJ,oBAAC,YAAO,MAAK,UAAS,WAAW,oBAAoB,WAAW,6BAA6B,EAAE,IAAI,OAAM,+BAA8B,SAAS,SAAS,qCAEzJ;AAIF,QAAI,SAAU,QAAQ,iCAAG;AAAA;AAAA,MAAS;AAAA,OAAI;AAEtC,WACE,qBAAC,UAAK,WAAU,gBAAe,OAAO,EAAE,UAAU,YAAY,SAAS,QAAQ,GAAG,OAAM,+BAA8B,SAAS,SAC5H;AAAA;AAAA,MACA;AAAA,OACH;AAAA,EAEJ;AACA,SAAO;AACT;","names":[]}
|
|
@@ -5,6 +5,9 @@ import { PageRenderer } from "./PageRenderer.js";
|
|
|
5
5
|
import { useRegistry, BlockDataProvider } from "../blocks/context.js";
|
|
6
6
|
import { useEditor } from "../editor/EditorProvider.js";
|
|
7
7
|
import { isDemoActive, registerSeed } from "../demo/demoStore.js";
|
|
8
|
+
import { InlineImageEditProvider } from "../media/ImageContext.js";
|
|
9
|
+
import { useSiteConfig } from "../config.js";
|
|
10
|
+
import { parseLocaleSlugs, siteLocales } from "../i18n/overlay.js";
|
|
8
11
|
function PublishedPage({
|
|
9
12
|
page,
|
|
10
13
|
siteId,
|
|
@@ -12,7 +15,8 @@ function PublishedPage({
|
|
|
12
15
|
blockData
|
|
13
16
|
}) {
|
|
14
17
|
const registry = useRegistry();
|
|
15
|
-
const
|
|
18
|
+
const config = useSiteConfig();
|
|
19
|
+
const { draftBlocks, editPath, fotoVervanger } = useEditor();
|
|
16
20
|
useEffect(() => {
|
|
17
21
|
if (!isDemoActive()) return;
|
|
18
22
|
const pad = huidigPad(window.location.pathname);
|
|
@@ -24,9 +28,15 @@ function PublishedPage({
|
|
|
24
28
|
path: pad
|
|
25
29
|
});
|
|
26
30
|
}, [siteId, page]);
|
|
27
|
-
const opDezePagina = draftBlocks !== null && typeof window !== "undefined" &&
|
|
31
|
+
const opDezePagina = draftBlocks !== null && typeof window !== "undefined" && (() => {
|
|
32
|
+
const { slugs } = parseLocaleSlugs(
|
|
33
|
+
window.location.pathname.split("/").filter(Boolean),
|
|
34
|
+
siteLocales(config)
|
|
35
|
+
);
|
|
36
|
+
return huidigPad("/" + slugs.join("/")) === editPath;
|
|
37
|
+
})();
|
|
28
38
|
const teTonen = opDezePagina ? { ...page, blocks: draftBlocks } : page;
|
|
29
|
-
return /* @__PURE__ */ jsx(BlockDataProvider, { data: blockData, children: /* @__PURE__ */ jsx(PageRenderer, { page: teTonen, registry, siteId, mode }) });
|
|
39
|
+
return /* @__PURE__ */ jsx(BlockDataProvider, { data: blockData, children: /* @__PURE__ */ jsx(InlineImageEditProvider, { onReplace: opDezePagina ? fotoVervanger : null, children: /* @__PURE__ */ jsx(PageRenderer, { page: teTonen, registry, siteId, mode }) }) });
|
|
30
40
|
}
|
|
31
41
|
function huidigPad(p) {
|
|
32
42
|
const schoon = p.replace(/\/+$/, "");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/render/PublishedPage.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\nimport { PageRenderer } from \"./PageRenderer\";\nimport { useRegistry, BlockDataProvider } from \"../blocks/context\";\nimport { useEditor } from \"../editor/EditorProvider\";\nimport { isDemoActive, registerSeed } from \"../demo/demoStore\";\nimport type { Page } from \"../schema/pages\";\n\n/**\n * Rendert een (gepubliceerde) pagina via de **context-registry** (module-resolved, opgebouwd in\n * XiCMSProvider). Gebruik dit i.p.v. zelf `createRegistry([...builtinBlocks])` server-side te bouwen:\n * sinds de blokken op Astryx draaien zijn de meeste `\"use client\"`, en een server-opgebouwde registry\n * krijgt daar alleen opake client-referenties van → die blokken worden stil overgeslagen. Deze client-\n * component zit binnen de provider en heeft dus de volledige registry.\n *\n * `blockData` (5.6, optioneel): server-gefetchte startdata per domein (bv. `{ shop: … }` uit\n * `getShopPageData`). Data-blokken renderen dan direct met inhoud (SSR/ISR) i.p.v. eerst\n * client-side te fetchen. Moet serialiseerbare plain data zijn (server → client-prop).\n */\nexport function PublishedPage({\n page,\n siteId,\n mode = \"view\",\n blockData,\n}: {\n page: Pick<Page, \"blocks\">;\n siteId: string;\n mode?: \"view\" | \"preview\";\n blockData?: Record<string, unknown>;\n}) {\n const registry = useRegistry();\n const { draftBlocks, editPath } = useEditor();\n\n /**\n * In een DEMO is deze pagina zijn eigen uitgangspunt.\n *\n * De demo-bewerkmodus bewaart wijzigingen in de browser en legt die over een \"seed\": de\n * oorspronkelijke pagina waar de bezoeker op verder bewerkt. Zonder geregistreerde seed geeft\n * `resolveDemoPage` niets terug, meldt de editor \"Nog geen werkversie\", en loopt de enige knop\n * die dan overblijft (\"+ Pagina aanmaken\") tegen de échte database aan.\n *\n * Dat overkwam elke demo-site behalve xicms.nl zelf, want daar stond de registratie met de hand\n * in een eigen catch-all route. Een klantsite heeft die route niet en had dus wél een\n * demo-bewerkmodus in de balk, maar geen enkele pagina om te bewerken — op sites die bestaan om\n * te laten zien hoe prettig het bewerken werkt.\n *\n * Hier hoort het thuis: dit component wéét welke pagina er op dit pad staat. Buiten de demo doet\n * het niets, en in de demo is de seed per definitie gelijk aan wat de bezoeker ziet.\n */\n useEffect(() => {\n if (!isDemoActive()) return;\n const pad = huidigPad(window.location.pathname);\n // Titel en slug horen erbij: de editor toont ze in zijn balk en in de pagina-instellingen.\n // `page` levert in de praktijk het hele pagina-object; deze twee zijn de terugval voor een site\n // die alleen blokken doorgeeft — het type belooft immers niet meer dan `blocks`.\n const volledig = page as Partial<Page> & Pick<Page, \"blocks\">;\n registerSeed(siteId, pad, {\n ...volledig,\n title: volledig.title || document.title,\n slug: volledig.slug || (pad.split(\"/\").filter(Boolean).pop() ?? \"\"),\n path: pad,\n } as Page);\n // Alleen op het pad en de inhoud: opnieuw registreren bij elke render zou de seed tijdens het\n // bewerken vervangen door de zojuist bewerkte tekst, en dan is er geen origineel meer om op\n // terug te vallen.\n }, [siteId, page]);\n\n /**\n * Staat de editor open op DEZE pagina, dan tonen we de werkversie in plaats van het\n * gepubliceerde. Zo blijft de site zijn eigen layout en huisstijl tekenen, en is wat je ziet\n * precies wat je bewerkt — in plaats van een nabootsing in een apart editorvenster.\n *\n * De padvergelijking is niet overdreven voorzichtig: een pagina kan meerdere gerenderde pagina's\n * in beeld hebben (een preview, een ingekaderde pagina). Zonder die controle zou de werkversie\n * van de ene pagina in de andere opduiken.\n */\n const opDezePagina =\n draftBlocks !== null &&\n typeof window !== \"undefined\" &&\n
|
|
1
|
+
{"version":3,"sources":["../../src/render/PublishedPage.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\nimport { PageRenderer } from \"./PageRenderer\";\nimport { useRegistry, BlockDataProvider } from \"../blocks/context\";\nimport { useEditor } from \"../editor/EditorProvider\";\nimport { isDemoActive, registerSeed } from \"../demo/demoStore\";\nimport { InlineImageEditProvider } from \"../media/ImageContext\";\nimport { useSiteConfig } from \"../config\";\nimport { parseLocaleSlugs, siteLocales } from \"../i18n/overlay\";\nimport type { Page } from \"../schema/pages\";\n\n/**\n * Rendert een (gepubliceerde) pagina via de **context-registry** (module-resolved, opgebouwd in\n * XiCMSProvider). Gebruik dit i.p.v. zelf `createRegistry([...builtinBlocks])` server-side te bouwen:\n * sinds de blokken op Astryx draaien zijn de meeste `\"use client\"`, en een server-opgebouwde registry\n * krijgt daar alleen opake client-referenties van → die blokken worden stil overgeslagen. Deze client-\n * component zit binnen de provider en heeft dus de volledige registry.\n *\n * `blockData` (5.6, optioneel): server-gefetchte startdata per domein (bv. `{ shop: … }` uit\n * `getShopPageData`). Data-blokken renderen dan direct met inhoud (SSR/ISR) i.p.v. eerst\n * client-side te fetchen. Moet serialiseerbare plain data zijn (server → client-prop).\n */\nexport function PublishedPage({\n page,\n siteId,\n mode = \"view\",\n blockData,\n}: {\n page: Pick<Page, \"blocks\">;\n siteId: string;\n mode?: \"view\" | \"preview\";\n blockData?: Record<string, unknown>;\n}) {\n const registry = useRegistry();\n const config = useSiteConfig();\n const { draftBlocks, editPath, fotoVervanger } = useEditor();\n\n /**\n * In een DEMO is deze pagina zijn eigen uitgangspunt.\n *\n * De demo-bewerkmodus bewaart wijzigingen in de browser en legt die over een \"seed\": de\n * oorspronkelijke pagina waar de bezoeker op verder bewerkt. Zonder geregistreerde seed geeft\n * `resolveDemoPage` niets terug, meldt de editor \"Nog geen werkversie\", en loopt de enige knop\n * die dan overblijft (\"+ Pagina aanmaken\") tegen de échte database aan.\n *\n * Dat overkwam elke demo-site behalve xicms.nl zelf, want daar stond de registratie met de hand\n * in een eigen catch-all route. Een klantsite heeft die route niet en had dus wél een\n * demo-bewerkmodus in de balk, maar geen enkele pagina om te bewerken — op sites die bestaan om\n * te laten zien hoe prettig het bewerken werkt.\n *\n * Hier hoort het thuis: dit component wéét welke pagina er op dit pad staat. Buiten de demo doet\n * het niets, en in de demo is de seed per definitie gelijk aan wat de bezoeker ziet.\n */\n useEffect(() => {\n if (!isDemoActive()) return;\n const pad = huidigPad(window.location.pathname);\n // Titel en slug horen erbij: de editor toont ze in zijn balk en in de pagina-instellingen.\n // `page` levert in de praktijk het hele pagina-object; deze twee zijn de terugval voor een site\n // die alleen blokken doorgeeft — het type belooft immers niet meer dan `blocks`.\n const volledig = page as Partial<Page> & Pick<Page, \"blocks\">;\n registerSeed(siteId, pad, {\n ...volledig,\n title: volledig.title || document.title,\n slug: volledig.slug || (pad.split(\"/\").filter(Boolean).pop() ?? \"\"),\n path: pad,\n } as Page);\n // Alleen op het pad en de inhoud: opnieuw registreren bij elke render zou de seed tijdens het\n // bewerken vervangen door de zojuist bewerkte tekst, en dan is er geen origineel meer om op\n // terug te vallen.\n }, [siteId, page]);\n\n /**\n * Staat de editor open op DEZE pagina, dan tonen we de werkversie in plaats van het\n * gepubliceerde. Zo blijft de site zijn eigen layout en huisstijl tekenen, en is wat je ziet\n * precies wat je bewerkt — in plaats van een nabootsing in een apart editorvenster.\n *\n * De padvergelijking is niet overdreven voorzichtig: een pagina kan meerdere gerenderde pagina's\n * in beeld hebben (een preview, een ingekaderde pagina). Zonder die controle zou de werkversie\n * van de ene pagina in de andere opduiken.\n */\n /**\n * `editPath` is het CANONIEKE pad — zonder taalprefix, want zo houdt de editor zijn werkversie\n * bij. De URL-balk heeft die prefix wél (`/de/over-ons`). Vergeleken zonder die prefix eraf te\n * halen was de werkversie op elke vertaalde pagina onzichtbaar: de editor stond open, je typte,\n * en de pagina bleef stug het gepubliceerde tonen. Eéntalige sites merkten hier niets van —\n * precies waarom dit zo lang kon blijven staan.\n */\n const opDezePagina =\n draftBlocks !== null &&\n typeof window !== \"undefined\" &&\n (() => {\n const { slugs } = parseLocaleSlugs(\n window.location.pathname.split(\"/\").filter(Boolean),\n siteLocales(config),\n );\n return huidigPad(\"/\" + slugs.join(\"/\")) === editPath;\n })();\n\n const teTonen = opDezePagina ? { ...page, blocks: draftBlocks } : page;\n\n return (\n <BlockDataProvider data={blockData}>\n {/* Alleen op de pagina die de editor open heeft: anders krijgt een preview of ingekaderde\n pagina óók 📷-knopjes, terwijl een klik daar in het verkeerde draft zou landen. */}\n <InlineImageEditProvider onReplace={opDezePagina ? fotoVervanger : null}>\n <PageRenderer page={teTonen} registry={registry} siteId={siteId} mode={mode} />\n </InlineImageEditProvider>\n </BlockDataProvider>\n );\n}\n\n/** Normaliseert een pad zoals de editor het bijhoudt: geen trailing slash, root blijft \"/\". */\nfunction huidigPad(p: string): string {\n const schoon = p.replace(/\\/+$/, \"\");\n return schoon === \"\" ? \"/\" : schoon;\n}\n"],"mappings":";AA0GQ;AAxGR,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAC7B,SAAS,aAAa,yBAAyB;AAC/C,SAAS,iBAAiB;AAC1B,SAAS,cAAc,oBAAoB;AAC3C,SAAS,+BAA+B;AACxC,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB,mBAAmB;AAcvC,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AACF,GAKG;AACD,QAAM,WAAW,YAAY;AAC7B,QAAM,SAAS,cAAc;AAC7B,QAAM,EAAE,aAAa,UAAU,cAAc,IAAI,UAAU;AAkB3D,YAAU,MAAM;AACd,QAAI,CAAC,aAAa,EAAG;AACrB,UAAM,MAAM,UAAU,OAAO,SAAS,QAAQ;AAI9C,UAAM,WAAW;AACjB,iBAAa,QAAQ,KAAK;AAAA,MACxB,GAAG;AAAA,MACH,OAAO,SAAS,SAAS,SAAS;AAAA,MAClC,MAAM,SAAS,SAAS,IAAI,MAAM,GAAG,EAAE,OAAO,OAAO,EAAE,IAAI,KAAK;AAAA,MAChE,MAAM;AAAA,IACR,CAAS;AAAA,EAIX,GAAG,CAAC,QAAQ,IAAI,CAAC;AAkBjB,QAAM,eACJ,gBAAgB,QAChB,OAAO,WAAW,gBACjB,MAAM;AACL,UAAM,EAAE,MAAM,IAAI;AAAA,MAChB,OAAO,SAAS,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AAAA,MAClD,YAAY,MAAM;AAAA,IACpB;AACA,WAAO,UAAU,MAAM,MAAM,KAAK,GAAG,CAAC,MAAM;AAAA,EAC9C,GAAG;AAEL,QAAM,UAAU,eAAe,EAAE,GAAG,MAAM,QAAQ,YAAY,IAAI;AAElE,SACE,oBAAC,qBAAkB,MAAM,WAGvB,8BAAC,2BAAwB,WAAW,eAAe,gBAAgB,MACjE,8BAAC,gBAAa,MAAM,SAAS,UAAoB,QAAgB,MAAY,GAC/E,GACF;AAEJ;AAGA,SAAS,UAAU,GAAmB;AACpC,QAAM,SAAS,EAAE,QAAQ,QAAQ,EAAE;AACnC,SAAO,WAAW,KAAK,MAAM;AAC/B;","names":[]}
|
package/dist/styles.css
CHANGED
|
@@ -333,7 +333,11 @@
|
|
|
333
333
|
min-height: 0;
|
|
334
334
|
}
|
|
335
335
|
.xicms-ve__body.is-panel-open {
|
|
336
|
-
|
|
336
|
+
/* 360px was gekozen voor blok-instellingen (een handvol velden). Sinds het productformulier in
|
|
337
|
+
ditzelfde paneel opent (7.8) is dat te smal: twee kolommen velden, varianten-tabellen en
|
|
338
|
+
foto-rijen vouwden zich dubbel. clamp: nooit smaller dan de oude 360, nooit meer dan 480, en
|
|
339
|
+
op een gewoon scherm ± een kwart — de pagina blijft de hoofdzaak. */
|
|
340
|
+
grid-template-columns: 1fr clamp(360px, 28vw, 480px);
|
|
337
341
|
}
|
|
338
342
|
.xicms-ve__canvas {
|
|
339
343
|
overflow: auto;
|
|
@@ -782,6 +786,29 @@
|
|
|
782
786
|
:root:has(.xicms-ve__bar) {
|
|
783
787
|
padding-top: 2.6rem;
|
|
784
788
|
}
|
|
789
|
+
/* Smalle schermen: de balk hield alles en werd twee regels hoog — en de pagina-offset (2.6rem,
|
|
790
|
+
vast) rekent op één regel, dus de site schoof onder de balk. Het pad en de statustekst zijn op
|
|
791
|
+
een telefoon ballast; de vier moduskoppen en het gereedschap zijn de balk. */
|
|
792
|
+
@media (max-width: 720px) {
|
|
793
|
+
.xicms-ve__path,
|
|
794
|
+
.xicms-ve__status,
|
|
795
|
+
.xicms-ve__locale {
|
|
796
|
+
display: none;
|
|
797
|
+
}
|
|
798
|
+
.xicms-editbar,
|
|
799
|
+
.xicms-ve__bar {
|
|
800
|
+
gap: 0.15rem;
|
|
801
|
+
padding: 0.4rem 0.5rem;
|
|
802
|
+
overflow-x: auto;
|
|
803
|
+
/* Eén regel afdwingen: krimpen mag, breken niet. */
|
|
804
|
+
flex-wrap: nowrap;
|
|
805
|
+
white-space: nowrap;
|
|
806
|
+
}
|
|
807
|
+
.xicms-editbar__btn {
|
|
808
|
+
padding: 0.42rem 0.55rem;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
785
812
|
.xicms-ve__sep {
|
|
786
813
|
width: 1px;
|
|
787
814
|
align-self: stretch;
|
|
@@ -3112,6 +3139,37 @@
|
|
|
3112
3139
|
outline: 2px solid #a5b4fc;
|
|
3113
3140
|
outline-offset: 1px;
|
|
3114
3141
|
}
|
|
3142
|
+
/* De rest van het gereedschap (verplaatsen, dupliceren, verwijderen) toont zich bij aanwijzen,
|
|
3143
|
+
toetsenbordfocus of een geselecteerd blok (`is-open` — op een telefoon bestaat aanwijzen niet).
|
|
3144
|
+
Dicht in rust: zes permanente knoppen naast elk blok zijn geen gereedschap maar een hek om de
|
|
3145
|
+
site heen.
|
|
3146
|
+
|
|
3147
|
+
Een kale display-toggle, bewust zonder uitklap-animatie. Hier hebben twee animatietrucs
|
|
3148
|
+
(max-width en 0fr→1fr) allebei stilletjes gefaald in de fixed+flex-context van dit cluster —
|
|
3149
|
+
dicht terwijl de selector aantoonbaar matchte. Een knoppenrij die er ALTIJD is wanneer hij er
|
|
3150
|
+
hoort te zijn is meer waard dan een die er mooi in schuift maar soms niet komt. */
|
|
3151
|
+
.xicms-ve-blokknoppen__meer,
|
|
3152
|
+
.xicms-ve-blokknoppen__meer-in {
|
|
3153
|
+
display: none;
|
|
3154
|
+
}
|
|
3155
|
+
.xicms-ve-blokknoppen:hover .xicms-ve-blokknoppen__meer,
|
|
3156
|
+
.xicms-ve-blokknoppen:focus-within .xicms-ve-blokknoppen__meer,
|
|
3157
|
+
.xicms-ve-blokknoppen.is-open .xicms-ve-blokknoppen__meer {
|
|
3158
|
+
display: contents;
|
|
3159
|
+
}
|
|
3160
|
+
.xicms-ve-blokknoppen:hover .xicms-ve-blokknoppen__meer-in,
|
|
3161
|
+
.xicms-ve-blokknoppen:focus-within .xicms-ve-blokknoppen__meer-in,
|
|
3162
|
+
.xicms-ve-blokknoppen.is-open .xicms-ve-blokknoppen__meer-in {
|
|
3163
|
+
display: flex;
|
|
3164
|
+
gap: 2px;
|
|
3165
|
+
}
|
|
3166
|
+
.xicms-ve-blokknop:disabled {
|
|
3167
|
+
opacity: 0.35;
|
|
3168
|
+
cursor: default;
|
|
3169
|
+
}
|
|
3170
|
+
.xicms-ve-blokknop--gevaar:hover {
|
|
3171
|
+
background: rgba(220, 38, 38, 0.85);
|
|
3172
|
+
}
|
|
3115
3173
|
|
|
3116
3174
|
/* Producten dragen een iets andere tint zodat je ziet dat je iets anders aanwijst dan een blok:
|
|
3117
3175
|
wijzigingen aan een product gaan meteen live en horen niet met paginatekst verward te worden. */
|
package/dist/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const CORE_VERSION = true ? "7.
|
|
1
|
+
const CORE_VERSION = true ? "7.9.1" : "0.0.0-dev";
|
|
2
2
|
const CORE_PACKAGE = "@xiwebdesign/xicms-core";
|
|
3
3
|
function isNieuwer(beschikbaar, huidig) {
|
|
4
4
|
const delen = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiwebdesign/xicms-core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.9.1",
|
|
4
4
|
"description": "XiCMS engine: gestructureerde content, pagina's, draft/publish, admin (TipTap+Radix), SEO en beveiliging voor klantsites.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|