@xiwebdesign/xicms-core 7.4.0 → 7.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/AdminApp.d.ts +12 -1
- package/dist/admin/AdminApp.js +18 -14
- package/dist/admin/AdminApp.js.map +1 -1
- package/dist/admin/NewsEditor.js +15 -4
- package/dist/admin/NewsEditor.js.map +1 -1
- package/dist/admin/menu/MenuPanel.d.ts +20 -0
- package/dist/admin/menu/MenuPanel.js +144 -0
- package/dist/admin/menu/MenuPanel.js.map +1 -0
- package/dist/admin/panelRegistry.d.ts +10 -0
- package/dist/admin/panelRegistry.js +5 -1
- package/dist/admin/panelRegistry.js.map +1 -1
- package/dist/admin/settings/SettingsPanel.js +10 -2
- package/dist/admin/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/ui/dialog.js +6 -3
- package/dist/admin/ui/dialog.js.map +1 -1
- package/dist/admin/ui/toast.d.ts +16 -0
- package/dist/admin/ui/toast.js +8 -4
- package/dist/admin/ui/toast.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +89 -123
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/auth/AuthProvider.js +11 -5
- package/dist/auth/AuthProvider.js.map +1 -1
- package/dist/auth/LoginForm.js +2 -1
- package/dist/auth/LoginForm.js.map +1 -1
- package/dist/components/ExternEmbed.d.ts +23 -0
- package/dist/components/ExternEmbed.js +52 -0
- package/dist/components/ExternEmbed.js.map +1 -0
- package/dist/editor/EditorProvider.d.ts +8 -0
- package/dist/editor/EditorProvider.js +30 -6
- package/dist/editor/EditorProvider.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/next/authRewrites.d.ts +29 -0
- package/dist/next/authRewrites.js +11 -0
- package/dist/next/authRewrites.js.map +1 -1
- package/dist/next/index.cjs +12 -0
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +2 -1
- package/dist/next/index.js.map +1 -1
- package/dist/render/PublishedPage.js +9 -1
- package/dist/render/PublishedPage.js.map +1 -1
- package/dist/schema/settings.d.ts +62 -1
- package/dist/schema/settings.js +23 -1
- package/dist/schema/settings.js.map +1 -1
- package/dist/schema/siteConfig.d.ts +5 -0
- package/dist/settings/useSiteSettings.js +1 -1
- package/dist/settings/useSiteSettings.js.map +1 -1
- package/dist/styles.css +67 -12
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/next/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Helpers voor `next.config.ts`. Bewust een eigen ingang: dit draait in Node tijdens de build,
|
|
3
3
|
* niet in de browser, en mag dus niets uit de React-bundel meetrekken.
|
|
4
4
|
*/
|
|
5
|
-
export { xicmsAuthRewrites, type XicmsRewrite } from "./authRewrites.js";
|
|
5
|
+
export { xicmsAuthRewrites, xicmsAuthHeaders, type XicmsRewrite, type XicmsHeader } from "./authRewrites.js";
|
package/dist/next/index.js
CHANGED
package/dist/next/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/next/index.ts"],"sourcesContent":["/**\n * Helpers voor `next.config.ts`. Bewust een eigen ingang: dit draait in Node tijdens de build,\n * niet in de browser, en mag dus niets uit de React-bundel meetrekken.\n */\nexport { xicmsAuthRewrites, type XicmsRewrite } from \"./authRewrites\";\n"],"mappings":"AAIA,SAAS,
|
|
1
|
+
{"version":3,"sources":["../../src/next/index.ts"],"sourcesContent":["/**\n * Helpers voor `next.config.ts`. Bewust een eigen ingang: dit draait in Node tijdens de build,\n * niet in de browser, en mag dus niets uit de React-bundel meetrekken.\n */\nexport { xicmsAuthRewrites, xicmsAuthHeaders, type XicmsRewrite, type XicmsHeader } from \"./authRewrites\";\n"],"mappings":"AAIA,SAAS,mBAAmB,wBAA6D;","names":[]}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import { PageRenderer } from "./PageRenderer.js";
|
|
4
4
|
import { useRegistry, BlockDataProvider } from "../blocks/context.js";
|
|
5
|
+
import { useEditor } from "../editor/EditorProvider.js";
|
|
5
6
|
function PublishedPage({
|
|
6
7
|
page,
|
|
7
8
|
siteId,
|
|
@@ -9,7 +10,14 @@ function PublishedPage({
|
|
|
9
10
|
blockData
|
|
10
11
|
}) {
|
|
11
12
|
const registry = useRegistry();
|
|
12
|
-
|
|
13
|
+
const { draftBlocks, editPath } = useEditor();
|
|
14
|
+
const opDezePagina = draftBlocks !== null && typeof window !== "undefined" && huidigPad(window.location.pathname) === editPath;
|
|
15
|
+
const teTonen = opDezePagina ? { ...page, blocks: draftBlocks } : page;
|
|
16
|
+
return /* @__PURE__ */ jsx(BlockDataProvider, { data: blockData, children: /* @__PURE__ */ jsx(PageRenderer, { page: teTonen, registry, siteId, mode }) });
|
|
17
|
+
}
|
|
18
|
+
function huidigPad(p) {
|
|
19
|
+
const schoon = p.replace(/\/+$/, "");
|
|
20
|
+
return schoon === "" ? "/" : schoon;
|
|
13
21
|
}
|
|
14
22
|
export {
|
|
15
23
|
PublishedPage
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/render/PublishedPage.tsx"],"sourcesContent":["\"use client\";\n\nimport { PageRenderer } from \"./PageRenderer\";\nimport { useRegistry, BlockDataProvider } from \"../blocks/context\";\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 return (\n <BlockDataProvider data={blockData}>\n <PageRenderer page={
|
|
1
|
+
{"version":3,"sources":["../../src/render/PublishedPage.tsx"],"sourcesContent":["\"use client\";\n\nimport { PageRenderer } from \"./PageRenderer\";\nimport { useRegistry, BlockDataProvider } from \"../blocks/context\";\nimport { useEditor } from \"../editor/EditorProvider\";\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 * 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 huidigPad(window.location.pathname) === editPath;\n\n const teTonen = opDezePagina ? { ...page, blocks: draftBlocks } : page;\n\n return (\n <BlockDataProvider data={blockData}>\n <PageRenderer page={teTonen} registry={registry} siteId={siteId} mode={mode} />\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":";AAkDM;AAhDN,SAAS,oBAAoB;AAC7B,SAAS,aAAa,yBAAyB;AAC/C,SAAS,iBAAiB;AAcnB,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AACF,GAKG;AACD,QAAM,WAAW,YAAY;AAC7B,QAAM,EAAE,aAAa,SAAS,IAAI,UAAU;AAW5C,QAAM,eACJ,gBAAgB,QAChB,OAAO,WAAW,eAClB,UAAU,OAAO,SAAS,QAAQ,MAAM;AAE1C,QAAM,UAAU,eAAe,EAAE,GAAG,MAAM,QAAQ,YAAY,IAAI;AAElE,SACE,oBAAC,qBAAkB,MAAM,WACvB,8BAAC,gBAAa,MAAM,SAAS,UAAoB,QAAgB,MAAY,GAC/E;AAEJ;AAGA,SAAS,UAAU,GAAmB;AACpC,QAAM,SAAS,EAAE,QAAQ,QAAQ,EAAE;AACnC,SAAO,WAAW,KAAK,MAAM;AAC/B;","names":[]}
|
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
import * as z from "zod/mini";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Eén navigatie-item.
|
|
4
|
+
*
|
|
5
|
+
* `target` is OPTIONEEL en dat is opzet: elk bestaand menu in Git en in Firestore mist dit veld, en
|
|
6
|
+
* moet blijven valideren. Ontbreekt het, dan opent de link in hetzelfde venster — precies wat een
|
|
7
|
+
* gewone `<a href>` altijd al deed.
|
|
8
|
+
*
|
|
9
|
+
* Er staat bewust geen `type: "page" | "extern"` in. Dat onderscheid is uit de href zelf af te
|
|
10
|
+
* lezen (begint hij met `/` of met `http`), en een apart veld zou een tweede waarheid introduceren
|
|
11
|
+
* die uit de pas kan lopen met de href. Het beheerscherm mag de gebruiker wél die keuze voorleggen;
|
|
12
|
+
* wat er wordt opgeslagen is één ondubbelzinnig adres.
|
|
13
|
+
*/
|
|
3
14
|
export declare const NavItemSchema: z.ZodMiniObject<{
|
|
4
15
|
label: z.ZodMiniString<string>;
|
|
5
16
|
href: z.ZodMiniString<string>;
|
|
17
|
+
target: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
18
|
+
_blank: "_blank";
|
|
19
|
+
_embed: "_embed";
|
|
20
|
+
_self: "_self";
|
|
21
|
+
}>>;
|
|
6
22
|
}, z.core.$strip>;
|
|
7
23
|
export type NavItem = z.infer<typeof NavItemSchema>;
|
|
24
|
+
/** Verwijst dit item naar buiten? Afgeleid uit de href, zodat er geen tweede waarheid ontstaat. */
|
|
25
|
+
export declare function isExterneLink(href: string): boolean;
|
|
26
|
+
/** Het pad waarop een ingekaderde externe pagina wordt getoond. */
|
|
27
|
+
export declare const EMBED_PAD = "/extern";
|
|
28
|
+
/**
|
|
29
|
+
* De href waar dit menu-item feitelijk heen linkt.
|
|
30
|
+
*
|
|
31
|
+
* Bij `_embed` is dat niet de externe URL zelf maar de eigen `/extern`-route, die hem in een kader
|
|
32
|
+
* binnen de site toont. De externe URL gaat als parameter mee; de route controleert vervolgens of
|
|
33
|
+
* dat adres écht in het menu staat vóór hij het inlaadt. Zonder die controle zou iedereen een
|
|
34
|
+
* willekeurige pagina in jouw huisstijl kunnen laten verschijnen door de URL te bewerken.
|
|
35
|
+
*/
|
|
36
|
+
export declare function navHref(item: NavItem): string;
|
|
37
|
+
/** Staat dit adres als ingekaderd menu-item in de navigatie? Zo niet, dan niet tonen. */
|
|
38
|
+
export declare function isToegestaneEmbed(url: string, nav: NavItem[] | undefined): boolean;
|
|
8
39
|
/** Huisstijl-tokens (worden CSS custom properties). Alle uitbreidingen zijn optioneel/update-veilig. */
|
|
9
40
|
export declare const ThemeSchema: z.ZodMiniObject<{
|
|
10
41
|
primary: z.ZodMiniString<string>;
|
|
@@ -79,6 +110,11 @@ export declare const FooterColumnSchema: z.ZodMiniObject<{
|
|
|
79
110
|
links: z.ZodMiniDefault<z.ZodMiniArray<z.ZodMiniObject<{
|
|
80
111
|
label: z.ZodMiniString<string>;
|
|
81
112
|
href: z.ZodMiniString<string>;
|
|
113
|
+
target: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
114
|
+
_blank: "_blank";
|
|
115
|
+
_embed: "_embed";
|
|
116
|
+
_self: "_self";
|
|
117
|
+
}>>;
|
|
82
118
|
}, z.core.$strip>>>;
|
|
83
119
|
}, z.core.$strip>;
|
|
84
120
|
/** Bewerkbare footer (klant past dit aan in /beheer; core levert de unstyled `SiteFooter`). */
|
|
@@ -88,6 +124,11 @@ export declare const FooterSettingsSchema: z.ZodMiniObject<{
|
|
|
88
124
|
links: z.ZodMiniDefault<z.ZodMiniArray<z.ZodMiniObject<{
|
|
89
125
|
label: z.ZodMiniString<string>;
|
|
90
126
|
href: z.ZodMiniString<string>;
|
|
127
|
+
target: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
128
|
+
_blank: "_blank";
|
|
129
|
+
_embed: "_embed";
|
|
130
|
+
_self: "_self";
|
|
131
|
+
}>>;
|
|
91
132
|
}, z.core.$strip>>>;
|
|
92
133
|
}, z.core.$strip>>>;
|
|
93
134
|
social: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
|
|
@@ -111,6 +152,11 @@ export declare const SiteSettingsSchema: z.ZodMiniObject<{
|
|
|
111
152
|
nav: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
|
|
112
153
|
label: z.ZodMiniString<string>;
|
|
113
154
|
href: z.ZodMiniString<string>;
|
|
155
|
+
target: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
156
|
+
_blank: "_blank";
|
|
157
|
+
_embed: "_embed";
|
|
158
|
+
_self: "_self";
|
|
159
|
+
}>>;
|
|
114
160
|
}, z.core.$strip>>>;
|
|
115
161
|
theme: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
116
162
|
primary: z.ZodMiniString<string>;
|
|
@@ -172,6 +218,11 @@ export declare const SiteSettingsSchema: z.ZodMiniObject<{
|
|
|
172
218
|
links: z.ZodMiniDefault<z.ZodMiniArray<z.ZodMiniObject<{
|
|
173
219
|
label: z.ZodMiniString<string>;
|
|
174
220
|
href: z.ZodMiniString<string>;
|
|
221
|
+
target: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
222
|
+
_blank: "_blank";
|
|
223
|
+
_embed: "_embed";
|
|
224
|
+
_self: "_self";
|
|
225
|
+
}>>;
|
|
175
226
|
}, z.core.$strip>>>;
|
|
176
227
|
}, z.core.$strip>>>;
|
|
177
228
|
social: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
|
|
@@ -197,6 +248,11 @@ export declare const PublicSiteSettingsSchema: z.ZodMiniObject<{
|
|
|
197
248
|
nav: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
|
|
198
249
|
label: z.ZodMiniString<string>;
|
|
199
250
|
href: z.ZodMiniString<string>;
|
|
251
|
+
target: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
252
|
+
_blank: "_blank";
|
|
253
|
+
_embed: "_embed";
|
|
254
|
+
_self: "_self";
|
|
255
|
+
}>>;
|
|
200
256
|
}, z.core.$strip>>>;
|
|
201
257
|
theme: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
202
258
|
primary: z.ZodMiniString<string>;
|
|
@@ -249,6 +305,11 @@ export declare const PublicSiteSettingsSchema: z.ZodMiniObject<{
|
|
|
249
305
|
links: z.ZodMiniDefault<z.ZodMiniArray<z.ZodMiniObject<{
|
|
250
306
|
label: z.ZodMiniString<string>;
|
|
251
307
|
href: z.ZodMiniString<string>;
|
|
308
|
+
target: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
309
|
+
_blank: "_blank";
|
|
310
|
+
_embed: "_embed";
|
|
311
|
+
_self: "_self";
|
|
312
|
+
}>>;
|
|
252
313
|
}, z.core.$strip>>>;
|
|
253
314
|
}, z.core.$strip>>>;
|
|
254
315
|
social: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
|
package/dist/schema/settings.js
CHANGED
|
@@ -2,8 +2,26 @@ import * as z from "zod/mini";
|
|
|
2
2
|
import { SeoSchema } from "./seo.js";
|
|
3
3
|
const NavItemSchema = z.object({
|
|
4
4
|
label: z.string().check(z.minLength(1)).check(z.maxLength(80)),
|
|
5
|
-
href: z.string().check(z.minLength(1)).check(z.maxLength(2e3))
|
|
5
|
+
href: z.string().check(z.minLength(1)).check(z.maxLength(2e3)),
|
|
6
|
+
/**
|
|
7
|
+
* `_embed` toont een externe pagina in een kader BINNEN de site, onder de logo/menubalk, alsof
|
|
8
|
+
* hij erbij hoort. Alleen zinvol bij een externe href.
|
|
9
|
+
*/
|
|
10
|
+
target: z.optional(z.enum(["_self", "_blank", "_embed"]))
|
|
6
11
|
});
|
|
12
|
+
function isExterneLink(href) {
|
|
13
|
+
return /^https?:\/\//i.test(href);
|
|
14
|
+
}
|
|
15
|
+
const EMBED_PAD = "/extern";
|
|
16
|
+
function navHref(item) {
|
|
17
|
+
if (item.target === "_embed" && isExterneLink(item.href)) {
|
|
18
|
+
return `${EMBED_PAD}?url=${encodeURIComponent(item.href)}`;
|
|
19
|
+
}
|
|
20
|
+
return item.href;
|
|
21
|
+
}
|
|
22
|
+
function isToegestaneEmbed(url, nav) {
|
|
23
|
+
return (nav ?? []).some((i) => i.target === "_embed" && i.href === url);
|
|
24
|
+
}
|
|
7
25
|
const ThemeSchema = z.object({
|
|
8
26
|
primary: z.string().check(z.maxLength(40)),
|
|
9
27
|
accent: z.optional(z.string().check(z.maxLength(40))),
|
|
@@ -124,6 +142,7 @@ function toPublicSettings(s) {
|
|
|
124
142
|
};
|
|
125
143
|
}
|
|
126
144
|
export {
|
|
145
|
+
EMBED_PAD,
|
|
127
146
|
FooterColumnSchema,
|
|
128
147
|
FooterSettingsSchema,
|
|
129
148
|
FormsSenderSchema,
|
|
@@ -133,7 +152,10 @@ export {
|
|
|
133
152
|
SiteSettingsSchema,
|
|
134
153
|
SocialLinkSchema,
|
|
135
154
|
ThemeSchema,
|
|
155
|
+
isExterneLink,
|
|
156
|
+
isToegestaneEmbed,
|
|
136
157
|
mergeThemeTokens,
|
|
158
|
+
navHref,
|
|
137
159
|
toPublicSettings
|
|
138
160
|
};
|
|
139
161
|
//# sourceMappingURL=settings.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/schema/settings.ts"],"sourcesContent":["import * as z from \"zod/mini\";\nimport { SeoSchema } from \"./seo\";\n\n/** Eén navigatie-item. */\nexport const NavItemSchema = z.object({\n label: z.string().check(z.minLength(1)).check(z.maxLength(80)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n});\nexport type NavItem = z.infer<typeof NavItemSchema>;\n\n/** Huisstijl-tokens (worden CSS custom properties). Alle uitbreidingen zijn optioneel/update-veilig. */\nexport const ThemeSchema = z.object({\n primary: z.string().check(z.maxLength(40)),\n accent: z.optional(z.string().check(z.maxLength(40))),\n /** Logo-URL (absoluut, https) — gebruikt in de mail-header en op de factuur-PDF. */\n logo: z.optional(z.string().check(z.maxLength(500))),\n background: z.optional(z.string().check(z.maxLength(40))),\n foreground: z.optional(z.string().check(z.maxLength(40))),\n fontFamily: z.optional(z.string().check(z.maxLength(200))),\n /** Optioneel apart lettertype voor koppen (display/serif), voor een elegantere typografie. */\n headingFont: z.optional(z.string().check(z.maxLength(200))),\n /** Optionele stylesheet-URL voor het lettertype (bv. een Google Fonts CSS-link). */\n fontUrl: z.optional(z.string().check(z.maxLength(500))),\n /** Afrondingsschaal (sm/md/lg). */\n radius: z.optional(z.object({ sm: z.optional(z.string().check(z.maxLength(20))), md: z.optional(z.string().check(z.maxLength(20))), lg: z.optional(z.string().check(z.maxLength(20))) })),\n /** Ruimteschaal voor block-padding (xs..xl). */\n spacing: z.optional(z.object({\n xs: z.optional(z.string().check(z.maxLength(20))),\n sm: z.optional(z.string().check(z.maxLength(20))),\n md: z.optional(z.string().check(z.maxLength(20))),\n lg: z.optional(z.string().check(z.maxLength(20))),\n xl: z.optional(z.string().check(z.maxLength(20))),\n })),\n /** Maximale container-breedte + horizontale padding. */\n container: z.optional(z.object({ maxWidth: z.optional(z.string().check(z.maxLength(20))), padding: z.optional(z.string().check(z.maxLength(20))) })),\n /** Donkere modus: uit, altijd aan, of meebewegen met het systeem van de bezoeker. */\n dark: z.optional(z.object({\n /**\n * \"auto\" — volgt het besturingssysteem van de bezoeker (light-dark()).\n * \"dark\" — de site is altijd donker, ongeacht de bezoeker.\n * \"light\" — de site is altijd licht (gelijk aan geen donkere modus).\n *\n * Vervangt de oude `auto`-boolean, die alleen \"volg het systeem\" of \"niets\" kon zeggen —\n * \"deze site is gewoon donker\" was daarmee niet uit te drukken. `auto` blijft gelezen worden\n * zodat bestaande site-configs blijven werken (zie themeMode in XiCMSProvider).\n */\n mode: z.optional(z.enum([\"light\", \"dark\", \"auto\"])),\n /**\n * @deprecated Gebruik `mode`. Blijft werken: true → \"auto\", afwezig/false → \"light\".\n * OPTIONEEL (niet `_default(false)`): met een verplichte default draagt elk gemerged\n * dark-object een `auto`, en dan botst een partiële update uit /beheer op het type.\n */\n auto: z.optional(z.boolean()),\n background: z.optional(z.string().check(z.maxLength(40))),\n foreground: z.optional(z.string().check(z.maxLength(40))),\n primary: z.optional(z.string().check(z.maxLength(40))),\n })),\n});\nexport type ThemeTokens = z.infer<typeof ThemeSchema>;\n\n/**\n * Voegt live thema-instellingen (uit `settings/public.theme`, /beheer) per token samen met het\n * statische SiteConfig-thema. Firestore wint per veld waar gezet; geneste schalen (radius/spacing/\n * container/dark) mergen per subveld, zodat een klant die alleen kleuren wijzigt de fonts/radius\n * uit de repo behoudt. Puur (geen React) — gedeeld door provider en `useMergedSiteConfig`.\n */\nexport function mergeThemeTokens(base: ThemeTokens, override?: ThemeTokens): ThemeTokens {\n if (!override) return base;\n const merged: ThemeTokens = { ...base, ...override };\n for (const key of [\"radius\", \"spacing\", \"container\", \"dark\"] as const) {\n if (base[key] && override[key]) {\n merged[key] = { ...base[key], ...override[key] } as never;\n } else if (base[key] && !override[key]) {\n merged[key] = base[key] as never;\n }\n }\n return merged;\n}\n\n/**\n * Afzender-instelling voor formulier-e-mail. Een provider mag alleen versturen vanaf een\n * geverifieerd domein, dus dit is per site instelbaar:\n * - mode \"xicms\": from = verifiedSender (XiCMS-domein), replyTo = inzender/klant. Werkt direct.\n * - mode \"client\": from = het geverifieerde eigen domein van de klant.\n */\nexport const FormsSenderSchema = z.object({\n mode: z._default(z.enum([\"xicms\", \"client\"]), \"xicms\"),\n /** Geverifieerd from-adres (verplicht ongeacht mode; bij \"client\" het klant-domein). */\n fromEmail: z.optional(z.email()),\n fromName: z.optional(z.string().check(z.maxLength(120))),\n /** Waar notificaties heen gaan (meestal het klant-mailadres). */\n notifyTo: z.optional(z.email()),\n});\nexport type FormsSender = z.infer<typeof FormsSenderSchema>;\n\n/** Eén social-link (platform + url). */\nexport const SocialLinkSchema = z.object({\n platform: z.string().check(z.minLength(1)).check(z.maxLength(40)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n});\nexport type SocialLink = z.infer<typeof SocialLinkSchema>;\n\n/** Eén footer-kolom: kop + links. */\nexport const FooterColumnSchema = z.object({\n heading: z.optional(z.string().check(z.maxLength(120))),\n links: z._default(z.array(NavItemSchema).check(z.maxLength(20)), []),\n});\n\n/** Bewerkbare footer (klant past dit aan in /beheer; core levert de unstyled `SiteFooter`). */\nexport const FooterSettingsSchema = z.object({\n columns: z.optional(z.array(FooterColumnSchema).check(z.maxLength(6))),\n social: z.optional(z.array(SocialLinkSchema).check(z.maxLength(12))),\n copyright: z.optional(z.string().check(z.maxLength(300))),\n /** Toon de \"Beheer\"-link in de footer (default: aan). */\n showAdminLink: z.optional(z.boolean()),\n});\nexport type FooterSettings = z.infer<typeof FooterSettingsSchema>;\n\n/** Per-module activatie/instellingen (spiegelt ModulesSiteConfig; permissief gehouden). */\nexport const ModulesSettingsSchema = z.record(\n z.string(),\n z.union([\n z.boolean(),\n z.object({ enabled: z.optional(z.boolean()), config: z.optional(z.record(z.string(), z.unknown())) }),\n ]),\n);\n\n/** Het `sites/{siteId}/settings/site`-document: alle bewerkbare site-instellingen. */\nexport const SiteSettingsSchema = z.object({\n name: z.optional(z.string().check(z.minLength(1)).check(z.maxLength(160))),\n // Zelfde hostname-regel als SiteConfigSchema.domain (geen protocol/pad).\n domain: z.union([z.optional(z.string().check(z.maxLength(255)).check(z.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i, \"alleen een hostname, bv. 'www.klant.nl' (zonder https:// of pad)\"))), z.literal(\"\")]),\n contactEmail: z.optional(z.email()),\n nav: z.optional(z.array(NavItemSchema).check(z.maxLength(50))),\n theme: z.optional(ThemeSchema),\n seoDefaults: z.optional(SeoSchema),\n forms: z.optional(FormsSenderSchema),\n footer: z.optional(FooterSettingsSchema),\n modules: z.optional(ModulesSettingsSchema),\n});\nexport type SiteSettings = z.infer<typeof SiteSettingsSchema>;\n\n/**\n * Publiek-veilige presentatie-instellingen (`sites/{siteId}/settings/public`): wordt door de live\n * site gelezen (footer, thema, nav, modules) zónder de gevoelige velden (e-mailafzender, contact)\n * uit `settings/site` bloot te geven.\n */\nexport const PublicSiteSettingsSchema = z.object({\n name: z.optional(z.string().check(z.maxLength(160))),\n nav: z.optional(z.array(NavItemSchema).check(z.maxLength(50))),\n theme: z.optional(ThemeSchema),\n seoDefaults: z.optional(SeoSchema),\n footer: z.optional(FooterSettingsSchema),\n modules: z.optional(ModulesSettingsSchema),\n});\nexport type PublicSiteSettings = z.infer<typeof PublicSiteSettingsSchema>;\n\n/** Haalt de publiek-veilige subset uit de volledige settings (voor het `settings/public`-doc). */\nexport function toPublicSettings(s: SiteSettings): PublicSiteSettings {\n return {\n name: s.name,\n nav: s.nav,\n theme: s.theme,\n seoDefaults: s.seoDefaults,\n footer: s.footer,\n modules: s.modules,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/schema/settings.ts"],"sourcesContent":["import * as z from \"zod/mini\";\nimport { SeoSchema } from \"./seo\";\n\n/**\n * Eén navigatie-item.\n *\n * `target` is OPTIONEEL en dat is opzet: elk bestaand menu in Git en in Firestore mist dit veld, en\n * moet blijven valideren. Ontbreekt het, dan opent de link in hetzelfde venster — precies wat een\n * gewone `<a href>` altijd al deed.\n *\n * Er staat bewust geen `type: \"page\" | \"extern\"` in. Dat onderscheid is uit de href zelf af te\n * lezen (begint hij met `/` of met `http`), en een apart veld zou een tweede waarheid introduceren\n * die uit de pas kan lopen met de href. Het beheerscherm mag de gebruiker wél die keuze voorleggen;\n * wat er wordt opgeslagen is één ondubbelzinnig adres.\n */\nexport const NavItemSchema = z.object({\n label: z.string().check(z.minLength(1)).check(z.maxLength(80)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n /**\n * `_embed` toont een externe pagina in een kader BINNEN de site, onder de logo/menubalk, alsof\n * hij erbij hoort. Alleen zinvol bij een externe href.\n */\n target: z.optional(z.enum([\"_self\", \"_blank\", \"_embed\"])),\n});\nexport type NavItem = z.infer<typeof NavItemSchema>;\n\n/** Verwijst dit item naar buiten? Afgeleid uit de href, zodat er geen tweede waarheid ontstaat. */\nexport function isExterneLink(href: string): boolean {\n return /^https?:\\/\\//i.test(href);\n}\n\n/** Het pad waarop een ingekaderde externe pagina wordt getoond. */\nexport const EMBED_PAD = \"/extern\";\n\n/**\n * De href waar dit menu-item feitelijk heen linkt.\n *\n * Bij `_embed` is dat niet de externe URL zelf maar de eigen `/extern`-route, die hem in een kader\n * binnen de site toont. De externe URL gaat als parameter mee; de route controleert vervolgens of\n * dat adres écht in het menu staat vóór hij het inlaadt. Zonder die controle zou iedereen een\n * willekeurige pagina in jouw huisstijl kunnen laten verschijnen door de URL te bewerken.\n */\nexport function navHref(item: NavItem): string {\n if (item.target === \"_embed\" && isExterneLink(item.href)) {\n return `${EMBED_PAD}?url=${encodeURIComponent(item.href)}`;\n }\n return item.href;\n}\n\n/** Staat dit adres als ingekaderd menu-item in de navigatie? Zo niet, dan niet tonen. */\nexport function isToegestaneEmbed(url: string, nav: NavItem[] | undefined): boolean {\n return (nav ?? []).some((i) => i.target === \"_embed\" && i.href === url);\n}\n\n/** Huisstijl-tokens (worden CSS custom properties). Alle uitbreidingen zijn optioneel/update-veilig. */\nexport const ThemeSchema = z.object({\n primary: z.string().check(z.maxLength(40)),\n accent: z.optional(z.string().check(z.maxLength(40))),\n /** Logo-URL (absoluut, https) — gebruikt in de mail-header en op de factuur-PDF. */\n logo: z.optional(z.string().check(z.maxLength(500))),\n background: z.optional(z.string().check(z.maxLength(40))),\n foreground: z.optional(z.string().check(z.maxLength(40))),\n fontFamily: z.optional(z.string().check(z.maxLength(200))),\n /** Optioneel apart lettertype voor koppen (display/serif), voor een elegantere typografie. */\n headingFont: z.optional(z.string().check(z.maxLength(200))),\n /** Optionele stylesheet-URL voor het lettertype (bv. een Google Fonts CSS-link). */\n fontUrl: z.optional(z.string().check(z.maxLength(500))),\n /** Afrondingsschaal (sm/md/lg). */\n radius: z.optional(z.object({ sm: z.optional(z.string().check(z.maxLength(20))), md: z.optional(z.string().check(z.maxLength(20))), lg: z.optional(z.string().check(z.maxLength(20))) })),\n /** Ruimteschaal voor block-padding (xs..xl). */\n spacing: z.optional(z.object({\n xs: z.optional(z.string().check(z.maxLength(20))),\n sm: z.optional(z.string().check(z.maxLength(20))),\n md: z.optional(z.string().check(z.maxLength(20))),\n lg: z.optional(z.string().check(z.maxLength(20))),\n xl: z.optional(z.string().check(z.maxLength(20))),\n })),\n /** Maximale container-breedte + horizontale padding. */\n container: z.optional(z.object({ maxWidth: z.optional(z.string().check(z.maxLength(20))), padding: z.optional(z.string().check(z.maxLength(20))) })),\n /** Donkere modus: uit, altijd aan, of meebewegen met het systeem van de bezoeker. */\n dark: z.optional(z.object({\n /**\n * \"auto\" — volgt het besturingssysteem van de bezoeker (light-dark()).\n * \"dark\" — de site is altijd donker, ongeacht de bezoeker.\n * \"light\" — de site is altijd licht (gelijk aan geen donkere modus).\n *\n * Vervangt de oude `auto`-boolean, die alleen \"volg het systeem\" of \"niets\" kon zeggen —\n * \"deze site is gewoon donker\" was daarmee niet uit te drukken. `auto` blijft gelezen worden\n * zodat bestaande site-configs blijven werken (zie themeMode in XiCMSProvider).\n */\n mode: z.optional(z.enum([\"light\", \"dark\", \"auto\"])),\n /**\n * @deprecated Gebruik `mode`. Blijft werken: true → \"auto\", afwezig/false → \"light\".\n * OPTIONEEL (niet `_default(false)`): met een verplichte default draagt elk gemerged\n * dark-object een `auto`, en dan botst een partiële update uit /beheer op het type.\n */\n auto: z.optional(z.boolean()),\n background: z.optional(z.string().check(z.maxLength(40))),\n foreground: z.optional(z.string().check(z.maxLength(40))),\n primary: z.optional(z.string().check(z.maxLength(40))),\n })),\n});\nexport type ThemeTokens = z.infer<typeof ThemeSchema>;\n\n/**\n * Voegt live thema-instellingen (uit `settings/public.theme`, /beheer) per token samen met het\n * statische SiteConfig-thema. Firestore wint per veld waar gezet; geneste schalen (radius/spacing/\n * container/dark) mergen per subveld, zodat een klant die alleen kleuren wijzigt de fonts/radius\n * uit de repo behoudt. Puur (geen React) — gedeeld door provider en `useMergedSiteConfig`.\n */\nexport function mergeThemeTokens(base: ThemeTokens, override?: ThemeTokens): ThemeTokens {\n if (!override) return base;\n const merged: ThemeTokens = { ...base, ...override };\n for (const key of [\"radius\", \"spacing\", \"container\", \"dark\"] as const) {\n if (base[key] && override[key]) {\n merged[key] = { ...base[key], ...override[key] } as never;\n } else if (base[key] && !override[key]) {\n merged[key] = base[key] as never;\n }\n }\n return merged;\n}\n\n/**\n * Afzender-instelling voor formulier-e-mail. Een provider mag alleen versturen vanaf een\n * geverifieerd domein, dus dit is per site instelbaar:\n * - mode \"xicms\": from = verifiedSender (XiCMS-domein), replyTo = inzender/klant. Werkt direct.\n * - mode \"client\": from = het geverifieerde eigen domein van de klant.\n */\nexport const FormsSenderSchema = z.object({\n mode: z._default(z.enum([\"xicms\", \"client\"]), \"xicms\"),\n /** Geverifieerd from-adres (verplicht ongeacht mode; bij \"client\" het klant-domein). */\n fromEmail: z.optional(z.email()),\n fromName: z.optional(z.string().check(z.maxLength(120))),\n /** Waar notificaties heen gaan (meestal het klant-mailadres). */\n notifyTo: z.optional(z.email()),\n});\nexport type FormsSender = z.infer<typeof FormsSenderSchema>;\n\n/** Eén social-link (platform + url). */\nexport const SocialLinkSchema = z.object({\n platform: z.string().check(z.minLength(1)).check(z.maxLength(40)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n});\nexport type SocialLink = z.infer<typeof SocialLinkSchema>;\n\n/** Eén footer-kolom: kop + links. */\nexport const FooterColumnSchema = z.object({\n heading: z.optional(z.string().check(z.maxLength(120))),\n links: z._default(z.array(NavItemSchema).check(z.maxLength(20)), []),\n});\n\n/** Bewerkbare footer (klant past dit aan in /beheer; core levert de unstyled `SiteFooter`). */\nexport const FooterSettingsSchema = z.object({\n columns: z.optional(z.array(FooterColumnSchema).check(z.maxLength(6))),\n social: z.optional(z.array(SocialLinkSchema).check(z.maxLength(12))),\n copyright: z.optional(z.string().check(z.maxLength(300))),\n /** Toon de \"Beheer\"-link in de footer (default: aan). */\n showAdminLink: z.optional(z.boolean()),\n});\nexport type FooterSettings = z.infer<typeof FooterSettingsSchema>;\n\n/** Per-module activatie/instellingen (spiegelt ModulesSiteConfig; permissief gehouden). */\nexport const ModulesSettingsSchema = z.record(\n z.string(),\n z.union([\n z.boolean(),\n z.object({ enabled: z.optional(z.boolean()), config: z.optional(z.record(z.string(), z.unknown())) }),\n ]),\n);\n\n/** Het `sites/{siteId}/settings/site`-document: alle bewerkbare site-instellingen. */\nexport const SiteSettingsSchema = z.object({\n name: z.optional(z.string().check(z.minLength(1)).check(z.maxLength(160))),\n // Zelfde hostname-regel als SiteConfigSchema.domain (geen protocol/pad).\n domain: z.union([z.optional(z.string().check(z.maxLength(255)).check(z.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i, \"alleen een hostname, bv. 'www.klant.nl' (zonder https:// of pad)\"))), z.literal(\"\")]),\n contactEmail: z.optional(z.email()),\n nav: z.optional(z.array(NavItemSchema).check(z.maxLength(50))),\n theme: z.optional(ThemeSchema),\n seoDefaults: z.optional(SeoSchema),\n forms: z.optional(FormsSenderSchema),\n footer: z.optional(FooterSettingsSchema),\n modules: z.optional(ModulesSettingsSchema),\n});\nexport type SiteSettings = z.infer<typeof SiteSettingsSchema>;\n\n/**\n * Publiek-veilige presentatie-instellingen (`sites/{siteId}/settings/public`): wordt door de live\n * site gelezen (footer, thema, nav, modules) zónder de gevoelige velden (e-mailafzender, contact)\n * uit `settings/site` bloot te geven.\n */\nexport const PublicSiteSettingsSchema = z.object({\n name: z.optional(z.string().check(z.maxLength(160))),\n nav: z.optional(z.array(NavItemSchema).check(z.maxLength(50))),\n theme: z.optional(ThemeSchema),\n seoDefaults: z.optional(SeoSchema),\n footer: z.optional(FooterSettingsSchema),\n modules: z.optional(ModulesSettingsSchema),\n});\nexport type PublicSiteSettings = z.infer<typeof PublicSiteSettingsSchema>;\n\n/** Haalt de publiek-veilige subset uit de volledige settings (voor het `settings/public`-doc). */\nexport function toPublicSettings(s: SiteSettings): PublicSiteSettings {\n return {\n name: s.name,\n nav: s.nav,\n theme: s.theme,\n seoDefaults: s.seoDefaults,\n footer: s.footer,\n modules: s.modules,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -118,6 +118,11 @@ export declare const SiteConfigSchema: z.ZodMiniObject<{
|
|
|
118
118
|
nav: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
|
|
119
119
|
label: z.ZodMiniString<string>;
|
|
120
120
|
href: z.ZodMiniString<string>;
|
|
121
|
+
target: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
122
|
+
_blank: "_blank";
|
|
123
|
+
_embed: "_embed";
|
|
124
|
+
_self: "_self";
|
|
125
|
+
}>>;
|
|
121
126
|
}, z.core.$strip>>>;
|
|
122
127
|
contactEmail: z.ZodMiniOptional<z.ZodMiniEmail>;
|
|
123
128
|
seoDefaults: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
@@ -90,7 +90,7 @@ function useMergedSiteConfig() {
|
|
|
90
90
|
const base = useSiteConfig();
|
|
91
91
|
const { settings } = useSiteSettings();
|
|
92
92
|
return useMemo(() => {
|
|
93
|
-
const nav = settings.nav
|
|
93
|
+
const nav = settings.nav ?? base.nav;
|
|
94
94
|
return {
|
|
95
95
|
...base,
|
|
96
96
|
name: settings.name ?? base.name,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/settings/useSiteSettings.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { useSiteConfig } from \"../config\";\nimport {\n PublicSiteSettingsSchema,\n mergeThemeTokens,\n type PublicSiteSettings,\n} from \"../schema/settings\";\nimport type { SiteConfig } from \"../schema/siteConfig\";\n\n/**\n * Live, publiek-veilige site-instellingen (`sites/{siteId}/settings/public`) zodat dingen die de\n * klant in /beheer wijzigt (footer, nav, thema, module-instellingen) direct op de site verschijnen.\n *\n * - Met SSR-seed (`initial`, via XiCMSProvider `settingsData`): géén client-read voor publiek —\n * de settings zitten al in de server-HTML (geen theme-flits) en verversen via ISR.\n * - Zonder seed (publiek): één `getDoc` (geen per-bezoeker listener).\n * - Editors: `useSiteSettingsLiveSync` (onSnapshot) zodat /beheer-wijzigingen meteen zichtbaar zijn.\n * Zonder Firebase of zonder doc valt alles terug op de statische `SiteConfig` (identiek aan vroeger).\n */\ninterface Ctx {\n settings: PublicSiteSettings;\n loaded: boolean;\n /** Interne setter voor de editor-live-sync (useSiteSettingsLiveSync). */\n _set?: (s: PublicSiteSettings) => void;\n}\nconst SettingsContext = createContext<Ctx>({ settings: {}, loaded: false });\n\nexport function SiteSettingsProvider({\n siteId,\n live = false,\n initial,\n children,\n}: {\n siteId: string;\n live?: boolean;\n /** SSR-startset (server-side gelezen via `getPublicSiteSettings`); slaat de client-read over. */\n initial?: PublicSiteSettings;\n children: ReactNode;\n}) {\n const hasInitial = initial !== undefined;\n const [settings, setSettings] = useState<PublicSiteSettings>(initial ?? {});\n const [loaded, setLoaded] = useState(hasInitial);\n\n useEffect(() => {\n // Met een SSR-seed doet de publieke site géén client-read meer: de layout-route is ISR-gecached\n // en levert verse settings per revalidate; editors krijgen apart een live sync.\n if (hasInitial && !live) return;\n const apply = (data: unknown) => {\n const res = PublicSiteSettingsSchema.safeParse(data ?? {});\n if (res.success) setSettings(res.data);\n setLoaded(true);\n };\n // Firestore DYNAMISCH: met een SSR-seed komt de publieke site hier niet eens, en een bezoeker\n // hoort de SDK nooit te downloaden. Statisch importeren zette hem in de bundel van elke pagina.\n let cancelled = false;\n let unsub: (() => void) | undefined;\n void (async () => {\n const [{ getDb }, { doc, getDoc, onSnapshot }] = await Promise.all([\n import(\"../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || cancelled) {\n setLoaded(true);\n return;\n }\n const ref = doc(db, \"sites\", siteId, \"settings\", \"public\");\n if (live) {\n unsub = onSnapshot(ref, (snap) => apply(snap.exists() ? snap.data() : {}), () => setLoaded(true));\n return;\n }\n try {\n const snap = await getDoc(ref);\n if (!cancelled) apply(snap.exists() ? snap.data() : {});\n } catch {\n if (!cancelled) setLoaded(true);\n }\n })();\n return () => {\n cancelled = true;\n unsub?.();\n };\n }, [siteId, live, hasInitial]);\n\n const value = useMemo<Ctx>(() => ({ settings, loaded, _set: setSettings }), [settings, loaded]);\n return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;\n}\n\n/** Ruwe publieke settings + laadstatus. */\nexport function useSiteSettings(): Ctx {\n return useContext(SettingsContext);\n}\n\n/**\n * Editor-live-sync: onSnapshot op `settings/public` zolang `enabled` (ingelogde editor). Aparte\n * hook (i.p.v. `live` op de provider) zodat publiek nooit een listener krijgt — zelfde patroon\n * als CustomBlocksSync in XiCMSProvider. Een leesfout laat de bestaande settings staan.\n */\nexport function useSiteSettingsLiveSync(siteId: string, enabled: boolean) {\n const { _set } = useContext(SettingsContext);\n useEffect(() => {\n // Alleen editors (enabled) krijgen een listener, en pas dán laadt de SDK.\n if (!enabled || !_set) return;\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const [{ getDb }, { doc, onSnapshot }] = await Promise.all([\n import(\"../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || afgebroken) return;\n unsub = onSnapshot(\n doc(db, \"sites\", siteId, \"settings\", \"public\"),\n (snap) => {\n const res = PublicSiteSettingsSchema.safeParse(snap.exists() ? snap.data() : {});\n if (res.success) _set(res.data);\n },\n () => undefined,\n );\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [enabled, siteId, _set]);\n}\n\n/** SiteConfig met de live Firestore-settings eroverheen (Firestore wint waar gezet). */\nexport function useMergedSiteConfig(): SiteConfig {\n const base = useSiteConfig();\n const { settings } = useSiteSettings();\n return useMemo(() => {\n
|
|
1
|
+
{"version":3,"sources":["../../src/settings/useSiteSettings.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { useSiteConfig } from \"../config\";\nimport {\n PublicSiteSettingsSchema,\n mergeThemeTokens,\n type PublicSiteSettings,\n} from \"../schema/settings\";\nimport type { SiteConfig } from \"../schema/siteConfig\";\n\n/**\n * Live, publiek-veilige site-instellingen (`sites/{siteId}/settings/public`) zodat dingen die de\n * klant in /beheer wijzigt (footer, nav, thema, module-instellingen) direct op de site verschijnen.\n *\n * - Met SSR-seed (`initial`, via XiCMSProvider `settingsData`): géén client-read voor publiek —\n * de settings zitten al in de server-HTML (geen theme-flits) en verversen via ISR.\n * - Zonder seed (publiek): één `getDoc` (geen per-bezoeker listener).\n * - Editors: `useSiteSettingsLiveSync` (onSnapshot) zodat /beheer-wijzigingen meteen zichtbaar zijn.\n * Zonder Firebase of zonder doc valt alles terug op de statische `SiteConfig` (identiek aan vroeger).\n */\ninterface Ctx {\n settings: PublicSiteSettings;\n loaded: boolean;\n /** Interne setter voor de editor-live-sync (useSiteSettingsLiveSync). */\n _set?: (s: PublicSiteSettings) => void;\n}\nconst SettingsContext = createContext<Ctx>({ settings: {}, loaded: false });\n\nexport function SiteSettingsProvider({\n siteId,\n live = false,\n initial,\n children,\n}: {\n siteId: string;\n live?: boolean;\n /** SSR-startset (server-side gelezen via `getPublicSiteSettings`); slaat de client-read over. */\n initial?: PublicSiteSettings;\n children: ReactNode;\n}) {\n const hasInitial = initial !== undefined;\n const [settings, setSettings] = useState<PublicSiteSettings>(initial ?? {});\n const [loaded, setLoaded] = useState(hasInitial);\n\n useEffect(() => {\n // Met een SSR-seed doet de publieke site géén client-read meer: de layout-route is ISR-gecached\n // en levert verse settings per revalidate; editors krijgen apart een live sync.\n if (hasInitial && !live) return;\n const apply = (data: unknown) => {\n const res = PublicSiteSettingsSchema.safeParse(data ?? {});\n if (res.success) setSettings(res.data);\n setLoaded(true);\n };\n // Firestore DYNAMISCH: met een SSR-seed komt de publieke site hier niet eens, en een bezoeker\n // hoort de SDK nooit te downloaden. Statisch importeren zette hem in de bundel van elke pagina.\n let cancelled = false;\n let unsub: (() => void) | undefined;\n void (async () => {\n const [{ getDb }, { doc, getDoc, onSnapshot }] = await Promise.all([\n import(\"../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || cancelled) {\n setLoaded(true);\n return;\n }\n const ref = doc(db, \"sites\", siteId, \"settings\", \"public\");\n if (live) {\n unsub = onSnapshot(ref, (snap) => apply(snap.exists() ? snap.data() : {}), () => setLoaded(true));\n return;\n }\n try {\n const snap = await getDoc(ref);\n if (!cancelled) apply(snap.exists() ? snap.data() : {});\n } catch {\n if (!cancelled) setLoaded(true);\n }\n })();\n return () => {\n cancelled = true;\n unsub?.();\n };\n }, [siteId, live, hasInitial]);\n\n const value = useMemo<Ctx>(() => ({ settings, loaded, _set: setSettings }), [settings, loaded]);\n return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;\n}\n\n/** Ruwe publieke settings + laadstatus. */\nexport function useSiteSettings(): Ctx {\n return useContext(SettingsContext);\n}\n\n/**\n * Editor-live-sync: onSnapshot op `settings/public` zolang `enabled` (ingelogde editor). Aparte\n * hook (i.p.v. `live` op de provider) zodat publiek nooit een listener krijgt — zelfde patroon\n * als CustomBlocksSync in XiCMSProvider. Een leesfout laat de bestaande settings staan.\n */\nexport function useSiteSettingsLiveSync(siteId: string, enabled: boolean) {\n const { _set } = useContext(SettingsContext);\n useEffect(() => {\n // Alleen editors (enabled) krijgen een listener, en pas dán laadt de SDK.\n if (!enabled || !_set) return;\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const [{ getDb }, { doc, onSnapshot }] = await Promise.all([\n import(\"../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || afgebroken) return;\n unsub = onSnapshot(\n doc(db, \"sites\", siteId, \"settings\", \"public\"),\n (snap) => {\n const res = PublicSiteSettingsSchema.safeParse(snap.exists() ? snap.data() : {});\n if (res.success) _set(res.data);\n },\n () => undefined,\n );\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [enabled, siteId, _set]);\n}\n\n/** SiteConfig met de live Firestore-settings eroverheen (Firestore wint waar gezet). */\nexport function useMergedSiteConfig(): SiteConfig {\n const base = useSiteConfig();\n const { settings } = useSiteSettings();\n return useMemo(() => {\n // AANWEZIG wint, ook als de lijst leeg is. Dat was `length > 0`, en dan kon je het menu niet\n // leegmaken: sla je een leeg menu op, dan kwam de nav uit `site.config.ts` gewoon terug. Het\n // verschil dat telt is \"heeft iemand hier een keuze gemaakt\", niet \"staat er iets in\".\n const nav = settings.nav ?? base.nav;\n return {\n ...base,\n name: settings.name ?? base.name,\n nav,\n theme: mergeThemeTokens(base.theme, settings.theme),\n seoDefaults: settings.seoDefaults ?? base.seoDefaults,\n };\n }, [base, settings]);\n}\n"],"mappings":"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","names":[]}
|
package/dist/styles.css
CHANGED
|
@@ -236,29 +236,36 @@
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
/* ── Visuele/inline pagina-editor (overlay) ───────────────────────────────── */
|
|
239
|
+
/* ── Bewerken OP de pagina, niet ervoor ────────────────────────────────────────────────────
|
|
240
|
+
*
|
|
241
|
+
* Dit was een schermvullende overlay: `position: fixed; inset: 0` met een eigen achtergrond en een
|
|
242
|
+
* canvas dat de pagina nábouwde. Je klikte "Pagina bewerken" op je eigen site en de site verdween
|
|
243
|
+
* achter een donker paneel — en dat canvas kon per definitie nooit precies je site zijn.
|
|
244
|
+
*
|
|
245
|
+
* Nu levert de editor alleen nog GEREEDSCHAP: een balk bovenaan en een zijpaneel op verzoek. De
|
|
246
|
+
* pagina eronder is de echte pagina, met de werkversie als inhoud (zie EditorProvider). */
|
|
239
247
|
.xicms-ve {
|
|
240
|
-
position:
|
|
241
|
-
inset: 0;
|
|
242
|
-
z-index: 10000;
|
|
248
|
+
position: static;
|
|
243
249
|
}
|
|
244
250
|
.xicms-ve__shell {
|
|
245
|
-
|
|
246
|
-
inset: 0;
|
|
247
|
-
display: flex;
|
|
248
|
-
flex-direction: column;
|
|
249
|
-
/* Tokens, geen vaste kleuren: het beheer stond hardgecodeerd licht terwijl de velden erin de
|
|
250
|
-
Astryx-tokens volgden — in donkere modus werd dat een wit paneel met onleesbare labels. */
|
|
251
|
-
background: var(--color-background-body, #f1f5f9);
|
|
252
|
-
color: var(--color-text-primary, #0f172a);
|
|
251
|
+
display: contents;
|
|
253
252
|
}
|
|
254
253
|
.xicms-ve__bar {
|
|
254
|
+
position: fixed;
|
|
255
|
+
top: 0;
|
|
256
|
+
left: 0;
|
|
257
|
+
right: 0;
|
|
258
|
+
z-index: 10001;
|
|
255
259
|
display: flex;
|
|
256
260
|
align-items: center;
|
|
257
261
|
gap: 0.75rem;
|
|
258
262
|
padding: 0.6rem 1rem;
|
|
259
263
|
background: #0f172a;
|
|
260
264
|
color: #fff;
|
|
261
|
-
|
|
265
|
+
}
|
|
266
|
+
/* De balk dekt de bovenkant van de site af; duw de pagina omlaag zolang er bewerkt wordt. */
|
|
267
|
+
:root:has(.xicms-ve__bar) body {
|
|
268
|
+
padding-top: 3rem;
|
|
262
269
|
}
|
|
263
270
|
.xicms-ve__title {
|
|
264
271
|
font-size: 0.95rem;
|
|
@@ -3025,3 +3032,51 @@
|
|
|
3025
3032
|
padding: 0.3rem 0.5rem;
|
|
3026
3033
|
border-radius: var(--xicms-radius-sm, 8px);
|
|
3027
3034
|
}
|
|
3035
|
+
|
|
3036
|
+
/* ── Externe pagina in een kader binnen de site ────────────────────────────────────────────
|
|
3037
|
+
* Hoort onder de logo/menubalk en moet aanvoelen als een deel van de site, niet als een venster
|
|
3038
|
+
* dat erin zweeft: dus volle breedte van de kolom, geen rand, geen schaduw. */
|
|
3039
|
+
.xicms-extern { display: grid; gap: 0.75rem; }
|
|
3040
|
+
.xicms-extern__frame { width: 100%; border: 0; display: block; }
|
|
3041
|
+
.xicms-extern__uitleg {
|
|
3042
|
+
margin: 0;
|
|
3043
|
+
padding: 0.75rem 1rem;
|
|
3044
|
+
border-radius: 0.5rem;
|
|
3045
|
+
background: var(--color-background-card, #f8fafc);
|
|
3046
|
+
font-size: 0.875rem;
|
|
3047
|
+
}
|
|
3048
|
+
.xicms-extern__melding { padding: 2rem 0; }
|
|
3049
|
+
|
|
3050
|
+
/* Tandwiel naast een blok op de pagina. Zelfde familie als het potlood — even ingetogen, want ze
|
|
3051
|
+
liggen over de site van de klant heen — maar duidelijk een andere handeling: het potlood zit bij
|
|
3052
|
+
een tekstveld, het tandwiel rechtsboven bij het blok als geheel. */
|
|
3053
|
+
.xicms-ve-gear {
|
|
3054
|
+
position: fixed;
|
|
3055
|
+
z-index: 10000;
|
|
3056
|
+
width: 26px;
|
|
3057
|
+
height: 26px;
|
|
3058
|
+
display: grid;
|
|
3059
|
+
place-items: center;
|
|
3060
|
+
border: 0;
|
|
3061
|
+
border-radius: 999px;
|
|
3062
|
+
background: rgba(15, 23, 42, 0.4);
|
|
3063
|
+
color: #fff;
|
|
3064
|
+
cursor: pointer;
|
|
3065
|
+
opacity: 0.45;
|
|
3066
|
+
transition: opacity 0.12s ease, background 0.12s ease;
|
|
3067
|
+
}
|
|
3068
|
+
.xicms-ve-gear:hover,
|
|
3069
|
+
.xicms-ve-gear:focus-visible,
|
|
3070
|
+
.xicms-ve-gear.is-on {
|
|
3071
|
+
opacity: 1;
|
|
3072
|
+
background: rgba(15, 23, 42, 0.85);
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
/* Melding in de editorbalk (nog geen werkversie). Hoort bij het gereedschap, niet bij de pagina. */
|
|
3076
|
+
.xicms-ve__balkmelding {
|
|
3077
|
+
display: inline-flex;
|
|
3078
|
+
align-items: center;
|
|
3079
|
+
gap: 0.5rem;
|
|
3080
|
+
font-size: 0.8rem;
|
|
3081
|
+
opacity: 0.9;
|
|
3082
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const CORE_VERSION = true ? "7.
|
|
1
|
+
const CORE_VERSION = true ? "7.6.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.6.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": {
|