@xiwebdesign/xicms-core 7.23.0 → 7.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/admin/AdminApp.js +2 -1
  2. package/dist/admin/AdminApp.js.map +1 -1
  3. package/dist/admin/CoreVersion.d.ts +6 -0
  4. package/dist/admin/CoreVersion.js +42 -1
  5. package/dist/admin/CoreVersion.js.map +1 -1
  6. package/dist/admin/SuperadminPanel.js +19 -3
  7. package/dist/admin/SuperadminPanel.js.map +1 -1
  8. package/dist/admin/dashboard/DashboardPanel.js +46 -9
  9. package/dist/admin/dashboard/DashboardPanel.js.map +1 -1
  10. package/dist/admin/forms/FormsPanel.d.ts +9 -0
  11. package/dist/admin/forms/FormsPanel.js +102 -16
  12. package/dist/admin/forms/FormsPanel.js.map +1 -1
  13. package/dist/admin/forms/gezien.d.ts +3 -0
  14. package/dist/admin/forms/gezien.js +22 -0
  15. package/dist/admin/forms/gezien.js.map +1 -0
  16. package/dist/admin/lib/kleur.d.ts +32 -0
  17. package/dist/admin/lib/kleur.js +60 -0
  18. package/dist/admin/lib/kleur.js.map +1 -0
  19. package/dist/admin/lib/pagesClient.d.ts +32 -0
  20. package/dist/admin/lib/pagesClient.js +60 -1
  21. package/dist/admin/lib/pagesClient.js.map +1 -1
  22. package/dist/admin/lib/seoHulp.d.ts +28 -0
  23. package/dist/admin/lib/seoHulp.js +33 -0
  24. package/dist/admin/lib/seoHulp.js.map +1 -0
  25. package/dist/admin/media/MediaManager.js +133 -51
  26. package/dist/admin/media/MediaManager.js.map +1 -1
  27. package/dist/admin/pages/PageEditor.d.ts +14 -0
  28. package/dist/admin/pages/PageEditor.js +192 -123
  29. package/dist/admin/pages/PageEditor.js.map +1 -1
  30. package/dist/admin/pages/PagesPanel.js +88 -40
  31. package/dist/admin/pages/PagesPanel.js.map +1 -1
  32. package/dist/admin/settings/SettingsPanel.js +100 -19
  33. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  34. package/dist/admin/shop/OrdersTab.js +171 -3
  35. package/dist/admin/shop/OrdersTab.js.map +1 -1
  36. package/dist/admin/shop/ProductsTab.js +1 -1
  37. package/dist/admin/shop/ProductsTab.js.map +1 -1
  38. package/dist/admin/shop/ShopPanel.js +5 -2
  39. package/dist/admin/shop/ShopPanel.js.map +1 -1
  40. package/dist/admin/stats/StatsPanel.js +30 -2
  41. package/dist/admin/stats/StatsPanel.js.map +1 -1
  42. package/dist/admin/stats/lib.d.ts +14 -1
  43. package/dist/admin/stats/lib.js +46 -2
  44. package/dist/admin/stats/lib.js.map +1 -1
  45. package/dist/admin/visual/VisualEditor.js +80 -4
  46. package/dist/admin/visual/VisualEditor.js.map +1 -1
  47. package/dist/admin.css +1 -1
  48. package/dist/blocks/builtins/FormBlockRender.js +8 -2
  49. package/dist/blocks/builtins/FormBlockRender.js.map +1 -1
  50. package/dist/blocks/builtins/lib/submitForm.d.ts +3 -1
  51. package/dist/blocks/builtins/lib/submitForm.js +2 -2
  52. package/dist/blocks/builtins/lib/submitForm.js.map +1 -1
  53. package/dist/components/EditBar.d.ts +3 -1
  54. package/dist/components/EditBar.js +30 -4
  55. package/dist/components/EditBar.js.map +1 -1
  56. package/dist/components/News.js +8 -4
  57. package/dist/components/News.js.map +1 -1
  58. package/dist/components/PageView.d.ts +5 -0
  59. package/dist/components/PageView.js +24 -2
  60. package/dist/components/PageView.js.map +1 -1
  61. package/dist/components/ResponsiveImage.d.ts +1 -1
  62. package/dist/components/ResponsiveImage.js +3 -1
  63. package/dist/components/ResponsiveImage.js.map +1 -1
  64. package/dist/components/site/SiteNav.d.ts +29 -0
  65. package/dist/components/site/SiteNav.js +82 -0
  66. package/dist/components/site/SiteNav.js.map +1 -0
  67. package/dist/content/types.d.ts +8 -0
  68. package/dist/index.d.ts +3 -1
  69. package/dist/index.js +8 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/media/ImageContext.d.ts +1 -6
  72. package/dist/media/ImageContext.js +6 -1
  73. package/dist/media/ImageContext.js.map +1 -1
  74. package/dist/releaseNotes.d.ts +15 -0
  75. package/dist/releaseNotes.js +28 -0
  76. package/dist/releaseNotes.js.map +1 -0
  77. package/dist/schema/mail.d.ts +20 -0
  78. package/dist/schema/mail.js +14 -0
  79. package/dist/schema/mail.js.map +1 -1
  80. package/dist/schema/settings.d.ts +105 -0
  81. package/dist/schema/settings.js +45 -5
  82. package/dist/schema/settings.js.map +1 -1
  83. package/dist/schema/shop.d.ts +2 -0
  84. package/dist/schema/shop.js +12 -2
  85. package/dist/schema/shop.js.map +1 -1
  86. package/dist/schema/siteConfig.d.ts +9 -0
  87. package/dist/server/blockData.js +6 -2
  88. package/dist/server/blockData.js.map +1 -1
  89. package/dist/server/draft.d.ts +11 -0
  90. package/dist/server/draft.js +20 -1
  91. package/dist/server/draft.js.map +1 -1
  92. package/dist/server/index.d.ts +3 -1
  93. package/dist/server/index.js +7 -1
  94. package/dist/server/index.js.map +1 -1
  95. package/dist/server/pages.js +4 -1
  96. package/dist/server/pages.js.map +1 -1
  97. package/dist/server/previewToken.d.ts +34 -0
  98. package/dist/server/previewToken.js +44 -0
  99. package/dist/server/previewToken.js.map +1 -0
  100. package/dist/server/redirects.d.ts +15 -0
  101. package/dist/server/redirects.js +15 -0
  102. package/dist/server/redirects.js.map +1 -0
  103. package/dist/styles.css +110 -0
  104. package/dist/version.js +1 -1
  105. package/package.json +1 -1
@@ -5,7 +5,7 @@ import type { MediaItem } from "../content/types.js";
5
5
  * Server-safe. Valt terug op de originele `url` als er nog geen varianten zijn.
6
6
  */
7
7
  export declare function ResponsiveImage({ media, url, alt, className, style, sizes, priority, }: {
8
- media?: Pick<MediaItem, "url" | "alt" | "width" | "height" | "variants">;
8
+ media?: Pick<MediaItem, "url" | "alt" | "width" | "height" | "variants" | "focal">;
9
9
  url?: string;
10
10
  alt?: string;
11
11
  className?: string;
@@ -13,6 +13,8 @@ function ResponsiveImage({
13
13
  const altText = alt ?? media?.alt ?? "";
14
14
  const ladder = media?.variants?.sizes ?? [];
15
15
  const webp = media?.variants?.webp;
16
+ const focal = media?.focal;
17
+ const stijl = focal && !(style && style.objectPosition) ? { ...style, objectPosition: `${Math.round(focal.x * 100)}% ${Math.round(focal.y * 100)}%` } : style;
16
18
  const srcSet = ladder.length ? ladder.map((v) => `${v.url} ${v.width}w`).join(", ") : void 0;
17
19
  return /* @__PURE__ */ jsxs("picture", { children: [
18
20
  webp && /* @__PURE__ */ jsx("source", { type: "image/webp", srcSet: `${webp.url} ${webp.width}w`, sizes }),
@@ -28,7 +30,7 @@ function ResponsiveImage({
28
30
  loading: priority ? "eager" : "lazy",
29
31
  fetchPriority: priority ? "high" : void 0,
30
32
  className,
31
- style
33
+ style: stijl
32
34
  }
33
35
  )
34
36
  ] });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/ResponsiveImage.tsx"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport type { MediaItem } from \"../content/types\";\n\n/**\n * Rendert een afbeelding met srcset/webp uit de door de pipeline gegenereerde varianten.\n * Server-safe. Valt terug op de originele `url` als er nog geen varianten zijn.\n */\nexport function ResponsiveImage({\n media,\n url,\n alt,\n className,\n style,\n sizes = \"100vw\",\n priority = false,\n}: {\n media?: Pick<MediaItem, \"url\" | \"alt\" | \"width\" | \"height\" | \"variants\">;\n url?: string;\n alt?: string;\n className?: string;\n style?: CSSProperties;\n sizes?: string;\n /** Zet dit op een above-the-fold beeld (bv. hero): eager laden + hoge fetch-prioriteit voor LCP. */\n priority?: boolean;\n}) {\n const src = media?.url ?? url;\n if (!src) return null;\n const altText = alt ?? media?.alt ?? \"\";\n const ladder = media?.variants?.sizes ?? [];\n const webp = media?.variants?.webp;\n\n const srcSet = ladder.length\n ? ladder.map((v) => `${v.url} ${v.width}w`).join(\", \")\n : undefined;\n\n return (\n <picture>\n {webp && <source type=\"image/webp\" srcSet={`${webp.url} ${webp.width}w`} sizes={sizes} />}\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={src}\n srcSet={srcSet}\n sizes={srcSet ? sizes : undefined}\n alt={altText}\n width={media?.width}\n height={media?.height}\n loading={priority ? \"eager\" : \"lazy\"}\n fetchPriority={priority ? \"high\" : undefined}\n className={className}\n style={style}\n />\n </picture>\n );\n}\n"],"mappings":"AAoCI,SACW,KADX;AA7BG,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,WAAW;AACb,GASG;AACD,QAAM,MAAM,OAAO,OAAO;AAC1B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,UAAU,OAAO,OAAO,OAAO;AACrC,QAAM,SAAS,OAAO,UAAU,SAAS,CAAC;AAC1C,QAAM,OAAO,OAAO,UAAU;AAE9B,QAAM,SAAS,OAAO,SAClB,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI,EAAE,KAAK,GAAG,EAAE,KAAK,IAAI,IACnD;AAEJ,SACE,qBAAC,aACE;AAAA,YAAQ,oBAAC,YAAO,MAAK,cAAa,QAAQ,GAAG,KAAK,GAAG,IAAI,KAAK,KAAK,KAAK,OAAc;AAAA,IAEvF;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,OAAO,SAAS,QAAQ;AAAA,QACxB,KAAK;AAAA,QACL,OAAO,OAAO;AAAA,QACd,QAAQ,OAAO;AAAA,QACf,SAAS,WAAW,UAAU;AAAA,QAC9B,eAAe,WAAW,SAAS;AAAA,QACnC;AAAA,QACA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/ResponsiveImage.tsx"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport type { MediaItem } from \"../content/types\";\n\n/**\n * Rendert een afbeelding met srcset/webp uit de door de pipeline gegenereerde varianten.\n * Server-safe. Valt terug op de originele `url` als er nog geen varianten zijn.\n */\nexport function ResponsiveImage({\n media,\n url,\n alt,\n className,\n style,\n sizes = \"100vw\",\n priority = false,\n}: {\n media?: Pick<MediaItem, \"url\" | \"alt\" | \"width\" | \"height\" | \"variants\" | \"focal\">;\n url?: string;\n alt?: string;\n className?: string;\n style?: CSSProperties;\n sizes?: string;\n /** Zet dit op een above-the-fold beeld (bv. hero): eager laden + hoge fetch-prioriteit voor LCP. */\n priority?: boolean;\n}) {\n const src = media?.url ?? url;\n if (!src) return null;\n const altText = alt ?? media?.alt ?? \"\";\n const ladder = media?.variants?.sizes ?? [];\n const webp = media?.variants?.webp;\n // Focuspunt (7.24): bijsnijden rond het gekozen punt i.p.v. het midden (zie MediaItem.focal).\n const focal = media?.focal;\n const stijl = focal && !(style && style.objectPosition)\n ? { ...style, objectPosition: `${Math.round(focal.x * 100)}% ${Math.round(focal.y * 100)}%` }\n : style;\n\n const srcSet = ladder.length\n ? ladder.map((v) => `${v.url} ${v.width}w`).join(\", \")\n : undefined;\n\n return (\n <picture>\n {webp && <source type=\"image/webp\" srcSet={`${webp.url} ${webp.width}w`} sizes={sizes} />}\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={src}\n srcSet={srcSet}\n sizes={srcSet ? sizes : undefined}\n alt={altText}\n width={media?.width}\n height={media?.height}\n loading={priority ? \"eager\" : \"lazy\"}\n fetchPriority={priority ? \"high\" : undefined}\n className={className}\n style={stijl}\n />\n </picture>\n );\n}\n"],"mappings":"AAyCI,SACW,KADX;AAlCG,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,WAAW;AACb,GASG;AACD,QAAM,MAAM,OAAO,OAAO;AAC1B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,UAAU,OAAO,OAAO,OAAO;AACrC,QAAM,SAAS,OAAO,UAAU,SAAS,CAAC;AAC1C,QAAM,OAAO,OAAO,UAAU;AAE9B,QAAM,QAAQ,OAAO;AACrB,QAAM,QAAQ,SAAS,EAAE,SAAS,MAAM,kBACpC,EAAE,GAAG,OAAO,gBAAgB,GAAG,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,KAAK,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,IAAI,IAC1F;AAEJ,QAAM,SAAS,OAAO,SAClB,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI,EAAE,KAAK,GAAG,EAAE,KAAK,IAAI,IACnD;AAEJ,SACE,qBAAC,aACE;AAAA,YAAQ,oBAAC,YAAO,MAAK,cAAa,QAAQ,GAAG,KAAK,GAAG,IAAI,KAAK,KAAK,KAAK,OAAc;AAAA,IAEvF;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,OAAO,SAAS,QAAQ;AAAA,QACxB,KAAK;AAAA,QACL,OAAO,OAAO;AAAA,QACd,QAAQ,OAAO;AAAA,QACf,SAAS,WAAW,UAAU;AAAA,QAC9B,eAAe,WAAW,SAAS;AAAA,QACnC;AAAA,QACA,OAAO;AAAA;AAAA,IACT;AAAA,KACF;AAEJ;","names":[]}
@@ -0,0 +1,29 @@
1
+ import { type NavItem } from "../../schema/settings.js";
2
+ /**
3
+ * De hoofdnavigatie zoals de klant hem in /beheer → Menu beheert (7.24).
4
+ *
5
+ * Tot 7.24 schreef het Menu-paneel netjes naar `settings.nav`, meldde "De navigatie is
6
+ * bijgewerkt" — en las geen enkele header dat veld. De template en de klantsites renderden
7
+ * `siteConfig.nav` rechtstreeks uit Git, dus wat de klant in het beheer deed kwam nooit op de
8
+ * site. Deze component is de ontbrekende consument: hij leest de samengevoegde config
9
+ * (Firestore wint, Git is de terugval) en is daarmee de énige plek die een menu hoort te tekenen.
10
+ *
11
+ * SSR-veilig: de settings zitten in de server-seed (`settingsData` op XiCMSProvider), dus het menu
12
+ * staat al in de HTML en er is geen flits van het oude menu.
13
+ *
14
+ * Unstyled-by-default, net als `SiteFooter`: alle opmaak hangt aan `xicms-nav*`-klassen en de
15
+ * thema-variabelen, zodat elke site hem kan herstijlen zonder de logica te forken. De klassen die
16
+ * de template gebruikte (`.site-header nav a`) blijven werken doordat dit een gewone `<nav>` met
17
+ * `<a>`'s blijft.
18
+ *
19
+ * Submenu's (één niveau): een item mét kinderen is zélf een link naar zijn overzichtspagina, met
20
+ * ernaast een knop die het submenu openklapt (`aria-expanded`). Zo werkt het menu zonder
21
+ * JavaScript (alle links staan in de HTML), met het toetsenbord (knop = Enter/Spatie, Escape
22
+ * sluit) en op touch (geen hover nodig).
23
+ */
24
+ export declare function SiteNav({ className, ariaLabel, items, }: {
25
+ className?: string;
26
+ ariaLabel?: string;
27
+ /** Overschrijft de items (bv. voor een footer-menu); standaard de beheerde hoofdnavigatie. */
28
+ items?: NavItem[];
29
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
+ import { useLinkComponent } from "@astryxdesign/core/Link";
5
+ import { useMergedSiteConfig } from "../../settings/useSiteSettings.js";
6
+ import { isExterneLink, navHref } from "../../schema/settings.js";
7
+ function SiteNav({
8
+ className,
9
+ ariaLabel = "Hoofdnavigatie",
10
+ items
11
+ }) {
12
+ const cfg = useMergedSiteConfig();
13
+ const lijst = items ?? cfg.nav ?? [];
14
+ if (lijst.length === 0) return null;
15
+ return /* @__PURE__ */ jsx("nav", { className: className ?? "xicms-nav", "aria-label": ariaLabel, children: /* @__PURE__ */ jsx("ul", { className: "xicms-nav__list", children: lijst.map((item, i) => /* @__PURE__ */ jsx(NavEntry, { item }, `${item.href}-${i}`)) }) });
16
+ }
17
+ function NavEntry({ item }) {
18
+ const LinkC = useLinkComponent();
19
+ const kinderen = item.children ?? [];
20
+ const [open, setOpen] = useState(false);
21
+ const id = useId();
22
+ const ref = useRef(null);
23
+ useEffect(() => {
24
+ if (!open) return;
25
+ const onDown = (e) => {
26
+ if (!ref.current?.contains(e.target)) setOpen(false);
27
+ };
28
+ const onKey = (e) => {
29
+ if (e.key === "Escape") setOpen(false);
30
+ };
31
+ document.addEventListener("pointerdown", onDown);
32
+ document.addEventListener("keydown", onKey);
33
+ return () => {
34
+ document.removeEventListener("pointerdown", onDown);
35
+ document.removeEventListener("keydown", onKey);
36
+ };
37
+ }, [open]);
38
+ const extern = isExterneLink(item.href);
39
+ const linkProps = {
40
+ href: navHref(item),
41
+ target: item.target === "_blank" ? "_blank" : void 0,
42
+ rel: extern && item.target === "_blank" ? "noopener noreferrer" : void 0
43
+ };
44
+ if (kinderen.length === 0) {
45
+ return /* @__PURE__ */ jsx("li", { className: "xicms-nav__item", children: /* @__PURE__ */ jsx(LinkC, { className: "xicms-nav__link", ...linkProps, children: item.label }) });
46
+ }
47
+ return /* @__PURE__ */ jsxs("li", { ref, className: `xicms-nav__item xicms-nav__item--sub${open ? " is-open" : ""}`, children: [
48
+ /* @__PURE__ */ jsxs("span", { className: "xicms-nav__pair", children: [
49
+ /* @__PURE__ */ jsx(LinkC, { className: "xicms-nav__link", ...linkProps, children: item.label }),
50
+ /* @__PURE__ */ jsx(
51
+ "button",
52
+ {
53
+ type: "button",
54
+ className: "xicms-nav__toggle",
55
+ "aria-expanded": open,
56
+ "aria-controls": id,
57
+ "aria-label": `Submenu van ${item.label} ${open ? "sluiten" : "openen"}`,
58
+ onClick: () => setOpen((v) => !v),
59
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "14", height: "14", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M6 9l6 6 6-6" }) })
60
+ }
61
+ )
62
+ ] }),
63
+ /* @__PURE__ */ jsx("ul", { id, className: "xicms-nav__sub", hidden: !open, children: kinderen.map((kind, j) => {
64
+ const kExtern = isExterneLink(kind.href);
65
+ return /* @__PURE__ */ jsx("li", { className: "xicms-nav__subitem", children: /* @__PURE__ */ jsx(
66
+ LinkC,
67
+ {
68
+ className: "xicms-nav__sublink",
69
+ href: navHref(kind),
70
+ target: kind.target === "_blank" ? "_blank" : void 0,
71
+ rel: kExtern && kind.target === "_blank" ? "noopener noreferrer" : void 0,
72
+ onClick: () => setOpen(false),
73
+ children: kind.label
74
+ }
75
+ ) }, `${kind.href}-${j}`);
76
+ }) })
77
+ ] });
78
+ }
79
+ export {
80
+ SiteNav
81
+ };
82
+ //# sourceMappingURL=SiteNav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/site/SiteNav.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { useLinkComponent } from \"@astryxdesign/core/Link\";\nimport { useMergedSiteConfig } from \"../../settings/useSiteSettings\";\nimport { isExterneLink, navHref, type NavItem } from \"../../schema/settings\";\n\n/**\n * De hoofdnavigatie zoals de klant hem in /beheer → Menu beheert (7.24).\n *\n * Tot 7.24 schreef het Menu-paneel netjes naar `settings.nav`, meldde \"De navigatie is\n * bijgewerkt\" — en las geen enkele header dat veld. De template en de klantsites renderden\n * `siteConfig.nav` rechtstreeks uit Git, dus wat de klant in het beheer deed kwam nooit op de\n * site. Deze component is de ontbrekende consument: hij leest de samengevoegde config\n * (Firestore wint, Git is de terugval) en is daarmee de énige plek die een menu hoort te tekenen.\n *\n * SSR-veilig: de settings zitten in de server-seed (`settingsData` op XiCMSProvider), dus het menu\n * staat al in de HTML en er is geen flits van het oude menu.\n *\n * Unstyled-by-default, net als `SiteFooter`: alle opmaak hangt aan `xicms-nav*`-klassen en de\n * thema-variabelen, zodat elke site hem kan herstijlen zonder de logica te forken. De klassen die\n * de template gebruikte (`.site-header nav a`) blijven werken doordat dit een gewone `<nav>` met\n * `<a>`'s blijft.\n *\n * Submenu's (één niveau): een item mét kinderen is zélf een link naar zijn overzichtspagina, met\n * ernaast een knop die het submenu openklapt (`aria-expanded`). Zo werkt het menu zonder\n * JavaScript (alle links staan in de HTML), met het toetsenbord (knop = Enter/Spatie, Escape\n * sluit) en op touch (geen hover nodig).\n */\nexport function SiteNav({\n className,\n ariaLabel = \"Hoofdnavigatie\",\n items,\n}: {\n className?: string;\n ariaLabel?: string;\n /** Overschrijft de items (bv. voor een footer-menu); standaard de beheerde hoofdnavigatie. */\n items?: NavItem[];\n}) {\n const cfg = useMergedSiteConfig();\n const lijst = items ?? cfg.nav ?? [];\n if (lijst.length === 0) return null;\n return (\n <nav className={className ?? \"xicms-nav\"} aria-label={ariaLabel}>\n <ul className=\"xicms-nav__list\">\n {lijst.map((item, i) => (\n <NavEntry key={`${item.href}-${i}`} item={item} />\n ))}\n </ul>\n </nav>\n );\n}\n\nfunction NavEntry({ item }: { item: NavItem }) {\n const LinkC = useLinkComponent();\n const kinderen = item.children ?? [];\n const [open, setOpen] = useState(false);\n const id = useId();\n const ref = useRef<HTMLLIElement>(null);\n\n // Buiten klikken of Escape sluit het submenu — anders blijft het hangen tot je een ander opent.\n useEffect(() => {\n if (!open) return;\n const onDown = (e: PointerEvent) => {\n if (!ref.current?.contains(e.target as Node)) setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(\"pointerdown\", onDown);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(\"pointerdown\", onDown);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, [open]);\n\n const extern = isExterneLink(item.href);\n const linkProps = {\n href: navHref(item),\n target: item.target === \"_blank\" ? \"_blank\" : undefined,\n rel: extern && item.target === \"_blank\" ? \"noopener noreferrer\" : undefined,\n };\n\n if (kinderen.length === 0) {\n return (\n <li className=\"xicms-nav__item\">\n <LinkC className=\"xicms-nav__link\" {...linkProps}>{item.label}</LinkC>\n </li>\n );\n }\n\n return (\n <li ref={ref} className={`xicms-nav__item xicms-nav__item--sub${open ? \" is-open\" : \"\"}`}>\n <span className=\"xicms-nav__pair\">\n <LinkC className=\"xicms-nav__link\" {...linkProps}>{item.label}</LinkC>\n <button\n type=\"button\"\n className=\"xicms-nav__toggle\"\n aria-expanded={open}\n aria-controls={id}\n aria-label={`Submenu van ${item.label} ${open ? \"sluiten\" : \"openen\"}`}\n onClick={() => setOpen((v) => !v)}\n >\n <svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n </button>\n </span>\n <ul id={id} className=\"xicms-nav__sub\" hidden={!open}>\n {kinderen.map((kind, j) => {\n const kExtern = isExterneLink(kind.href);\n return (\n <li key={`${kind.href}-${j}`} className=\"xicms-nav__subitem\">\n <LinkC\n className=\"xicms-nav__sublink\"\n href={navHref(kind)}\n target={kind.target === \"_blank\" ? \"_blank\" : undefined}\n rel={kExtern && kind.target === \"_blank\" ? \"noopener noreferrer\" : undefined}\n onClick={() => setOpen(false)}\n >\n {kind.label}\n </LinkC>\n </li>\n );\n })}\n </ul>\n </li>\n );\n}\n"],"mappings":"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","names":[]}
@@ -38,4 +38,12 @@ export interface MediaItem {
38
38
  };
39
39
  /** "processing" tot de pipeline klaar is, daarna "ready". */
40
40
  status?: "processing" | "ready";
41
+ /**
42
+ * Focuspunt (7.24): welk deel van de foto in beeld moet blijven als hij bijgesneden wordt
43
+ * (hero op mobiel, vierkante kaart). Fracties 0–1 van links/boven; afwezig = het midden.
44
+ */
45
+ focal?: {
46
+ x: number;
47
+ y: number;
48
+ };
41
49
  }
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export type { ImageComponentType, ImgProps } from "./media/ImageContext.js";
15
15
  export * from "./blocks/ui/index.js";
16
16
  export { SiteSettingsProvider, useSiteSettings, useMergedSiteConfig, } from "./settings/useSiteSettings.js";
17
17
  export { SiteFooter } from "./components/site/SiteFooter.js";
18
+ export { SiteNav } from "./components/site/SiteNav.js";
18
19
  export { EditorProvider, useEditor } from "./editor/EditorProvider.js";
19
20
  export { definePattern, instantiatePattern, collectPatternTypes, builtinPatterns, type Pattern, type PatternBlock, } from "./patterns/index.js";
20
21
  export { builtinThemePresets, type ThemePreset } from "./theme/presets.js";
@@ -30,7 +31,8 @@ export { customBlockToDefinition } from "./blocks/customTemplate/definition.js";
30
31
  export { interpretTemplate, scopeCss } from "./blocks/customTemplate/interpret.js";
31
32
  export { siteLocales, parseLocaleSlugs, localizedPath, localizePage, localizeBlocks, pruneTranslations, TranslationsSchema, } from "./i18n/overlay.js";
32
33
  export type { Translations, LocaleConfig } from "./i18n/overlay.js";
33
- export type { ThemeTokens, NavItem, SiteSettings, PublicSiteSettings, FormsSender } from "./schema/settings.js";
34
+ export type { ThemeTokens, NavItem, NavLeaf, Redirect, SiteSettings, PublicSiteSettings, FormsSender } from "./schema/settings.js";
35
+ export { zoekRedirect, normaliseerPad, navHref, isExterneLink } from "./schema/settings.js";
34
36
  export type { Seo } from "./schema/seo.js";
35
37
  export type { Page, PageStatus } from "./schema/pages.js";
36
38
  export type { FormDefinition, FormField } from "./schema/forms.js";
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  useMergedSiteConfig
11
11
  } from "./settings/useSiteSettings.js";
12
12
  import { SiteFooter } from "./components/site/SiteFooter.js";
13
+ import { SiteNav } from "./components/site/SiteNav.js";
13
14
  import { EditorProvider, useEditor } from "./editor/EditorProvider.js";
14
15
  import {
15
16
  definePattern,
@@ -33,6 +34,7 @@ import {
33
34
  pruneTranslations,
34
35
  TranslationsSchema
35
36
  } from "./i18n/overlay.js";
37
+ import { zoekRedirect, normaliseerPad, navHref, isExterneLink } from "./schema/settings.js";
36
38
  import { AuthProvider, useAuth } from "./auth/AuthProvider.js";
37
39
  import { LoginForm } from "./auth/LoginForm.js";
38
40
  import {
@@ -119,6 +121,7 @@ export {
119
121
  ResponsiveImage,
120
122
  SiteChrome,
121
123
  SiteFooter,
124
+ SiteNav,
122
125
  SiteSettingsProvider,
123
126
  SymbolSchema,
124
127
  TranslationsSchema,
@@ -149,6 +152,7 @@ export {
149
152
  initAppCheck,
150
153
  instantiatePattern,
151
154
  interpretTemplate,
155
+ isExterneLink,
152
156
  isFieldVisible,
153
157
  isFirebaseConfigured,
154
158
  isModuleActive,
@@ -160,6 +164,8 @@ export {
160
164
  localizedPath,
161
165
  mergeRegistry,
162
166
  moduleIdForBlockType,
167
+ navHref,
168
+ normaliseerPad,
163
169
  parseLocaleSlugs,
164
170
  pruneTranslations,
165
171
  resolveModules,
@@ -182,6 +188,7 @@ export {
182
188
  useSiteSettings,
183
189
  useTheme,
184
190
  validateBlocks,
185
- validateModuleContract
191
+ validateModuleContract,
192
+ zoekRedirect
186
193
  };
187
194
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Publieke runtime-API van @xiwebdesign/xicms-core (lichte client/runtime laag).\n * - Admin (TipTap/Radix) zit in \"@xiwebdesign/xicms-core/admin\".\n * - Server-only code (firebase-admin, sanitize, SEO) zit in \"@xiwebdesign/xicms-core/server\".\n * - Pure schema's in \"@xiwebdesign/xicms-core/schema\".\n */\n\n// Provider & config\nexport { XiCMSProvider } from \"./XiCMSProvider\";\nexport { useSiteConfig } from \"./config\";\n\n// Theming & design-primitieven (maatwerk zonder core te forken).\n// - `defineTheme` maakt een eigen Astryx-thema (tokens + component-overrides) → geef door als\n// XiCMSProvider `astryxTheme`-prop.\n// - De `ui`-primitieven (Card/Stack/Grid/Heading/Text/Btn…) zijn de bouwstenen waarop de ingebouwde\n// blokken draaien; gebruik ze in eigen blokken voor een consistent maar te herstijlen uiterlijk.\nexport { defineTheme, useTheme } from \"@astryxdesign/core/theme\";\nexport type { DefinedTheme } from \"@astryxdesign/core/theme\";\n// Framework-router-integratie: geef `next/link` door als XiCMSProvider `linkComponent`-prop\n// (soepele client-nav + prefetch op alle Astryx-knoppen/links). `useLinkComponent` voor eigen blokken.\nexport { LinkProvider, useLinkComponent } from \"@astryxdesign/core/Link\";\nexport type { LinkComponentType } from \"@astryxdesign/core/Link\";\n// Beeldoptimalisatie: geef `next/image` door als XiCMSProvider `imageComponent`-prop. `Img` is de\n// wrapper die de blokken gebruiken; `useImageComponent` voor eigen blokken.\nexport { Img, ImageProvider, useImageComponent, InlineImageEditProvider } from \"./media/ImageContext\";\nexport type { ImageComponentType, ImgProps } from \"./media/ImageContext\";\nexport * from \"./blocks/ui\";\nexport {\n SiteSettingsProvider,\n useSiteSettings,\n useMergedSiteConfig,\n} from \"./settings/useSiteSettings\";\nexport { SiteFooter } from \"./components/site/SiteFooter\";\nexport { EditorProvider, useEditor } from \"./editor/EditorProvider\";\nexport {\n definePattern,\n instantiatePattern,\n collectPatternTypes,\n builtinPatterns,\n type Pattern,\n type PatternBlock,\n} from \"./patterns\";\nexport { builtinThemePresets, type ThemePreset } from \"./theme/presets\";\nexport { defineSiteConfig } from \"./schema/siteConfig\";\nexport type { SiteConfig, SiteMode } from \"./schema/siteConfig\";\n// 5.0: Field-IR (primo-port), symbolen en custom blocks.\nexport { FieldSchema, FieldsSchema, fieldsToZod, fieldsDefaults, isFieldVisible, isRichTextKey } from \"./schema/fields\";\nexport type { Field, FieldHints, ShowIf } from \"./schema/fields\";\nexport { SymbolSchema } from \"./schema/symbols\";\nexport type { SymbolDefinition, StoredSymbol } from \"./schema/symbols\";\nexport { CustomBlockSchema } from \"./schema/customBlocks\";\nexport type { CustomBlock, CustomBlock as CustomBlockData } from \"./schema/customBlocks\";\nexport { customBlockToDefinition } from \"./blocks/customTemplate/definition\";\nexport { interpretTemplate, scopeCss } from \"./blocks/customTemplate/interpret\";\n// 5.0: localisatie — overlay-model (primo's locale-per-waarde, additief vertaald).\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n} from \"./i18n/overlay\";\nexport type { Translations, LocaleConfig } from \"./i18n/overlay\";\nexport type { ThemeTokens, NavItem, SiteSettings, PublicSiteSettings, FormsSender } from \"./schema/settings\";\nexport type { Seo } from \"./schema/seo\";\nexport type { Page, PageStatus } from \"./schema/pages\";\nexport type { FormDefinition, FormField } from \"./schema/forms\";\n\n// Auth\nexport { AuthProvider, useAuth } from \"./auth/AuthProvider\";\nexport { LoginForm } from \"./auth/LoginForm\";\nexport {\n roleForSite,\n canEdit,\n canManageSite,\n isSuperadmin,\n type Role,\n type XiClaims,\n} from \"./auth/roles\";\n\n// De legacy inline-laag (ContentProvider/useContent + EditableText/EditableRichText/EditableImage,\n// het v1 platte `cms/content`-model) is in 5.0 VERWIJDERD, zoals bij de deprecatie aangekondigd.\n// Inline bewerken loopt via het block-model: markeer tekstvelden met `data-xicms-edit` in de Render.\nexport type { NewsArticle, MediaItem, MediaVariant } from \"./content/types\";\n\nexport { EditBar } from \"./components/EditBar\";\nexport { ExternEmbed } from \"./components/ExternEmbed\";\nexport { ResponsiveImage } from \"./components/ResponsiveImage\";\nexport { PageView } from \"./components/PageView\";\nexport { NewsList, NewsArticleView, useNews } from \"./components/News\";\n\n// Block-systeem (runtime: registry + renderer; editors zitten in /admin)\nexport {\n defineBlock,\n createRegistry,\n mergeRegistry,\n validateBlocks,\n buildBlockAddOptions,\n type BlockDefinition,\n type BlockRegistry,\n type BlockRenderProps,\n type BlockEditorProps,\n type BlockCategory,\n} from \"./blocks/registry\";\nexport { BlockRenderer } from \"./blocks/BlockRenderer\";\nexport { RegistryProvider, useRegistry, BlockDataProvider, useBlockData } from \"./blocks/context\";\nexport { builtinBlocks, ConsentEmbed } from \"./blocks/builtins\";\nexport { PageRenderer } from \"./render/PageRenderer\";\nexport { PublishedPage } from \"./render/PublishedPage\";\n\n// Module-systeem (XiCMS 3.0): modules bundelen blocks + instellingen; per site aan/uit + te stylen.\nexport {\n defineModule,\n resolveModules,\n isModuleActive,\n ModulesProvider,\n useModules,\n useModuleActive,\n useModuleSettings,\n applyBlockOverrides,\n moduleIdForBlockType,\n builtinModules,\n validateModuleContract,\n assertModuleContract,\n useConsent,\n ConsentBanner,\n ConsentGate,\n ConsentReopenLink,\n type ConsentState,\n type ConsentSettings,\n type ConsentCategory,\n type ModuleDefinition,\n type ModuleSiteSetting,\n type ModulesSiteConfig,\n type Capability,\n type ResolvedModules,\n type BlockOverride,\n type BlockOverrides,\n} from \"./modules\";\n\n// Demo-lock\nexport { DemoLock } from \"./lock/DemoLock\";\n\n// Demo-bewerkmodus (lokaal-only) leeft op het subpad `@xiwebdesign/xicms-core/demo` — dit zijn\n// demo-store-internals die niet in de publieke runtime-API thuishoren (klantsites gebruiken ze nooit;\n// alleen de XiCMS.nl-showcase). DemoStudio wordt intern door XiCMSProvider gerenderd.\n\n// Firebase (client)\nexport {\n getFirebaseApp,\n getFirebaseAuth,\n getDb,\n getBucket,\n isFirebaseConfigured,\n} from \"./firebase/client\";\nexport { initAppCheck } from \"./firebase/appcheck\";\nexport { SiteChrome } from \"./SiteChrome\";\nexport { CORE_VERSION, CORE_PACKAGE, isNieuwer } from \"./version\";\n"],"mappings":"AAQA,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAO9B,SAAS,aAAa,gBAAgB;AAItC,SAAS,cAAc,wBAAwB;AAI/C,SAAS,KAAK,eAAe,mBAAmB,+BAA+B;AAE/E,cAAc;AACd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB,iBAAiB;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,SAAS,2BAA6C;AACtD,SAAS,wBAAwB;AAGjC,SAAS,aAAa,cAAc,aAAa,gBAAgB,gBAAgB,qBAAqB;AAEtG,SAAS,oBAAoB;AAE7B,SAAS,yBAAyB;AAElC,SAAS,+BAA+B;AACxC,SAAS,mBAAmB,gBAAgB;AAE5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,SAAS,cAAc,eAAe;AACtC,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAOP,SAAS,eAAe;AACxB,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,gBAAgB;AACzB,SAAS,UAAU,iBAAiB,eAAe;AAGnD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AACP,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB,aAAa,mBAAmB,oBAAoB;AAC/E,SAAS,eAAe,oBAAoB;AAC5C,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAG9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAWK;AAGP,SAAS,gBAAgB;AAOzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,cAAc,cAAc,iBAAiB;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Publieke runtime-API van @xiwebdesign/xicms-core (lichte client/runtime laag).\n * - Admin (TipTap/Radix) zit in \"@xiwebdesign/xicms-core/admin\".\n * - Server-only code (firebase-admin, sanitize, SEO) zit in \"@xiwebdesign/xicms-core/server\".\n * - Pure schema's in \"@xiwebdesign/xicms-core/schema\".\n */\n\n// Provider & config\nexport { XiCMSProvider } from \"./XiCMSProvider\";\nexport { useSiteConfig } from \"./config\";\n\n// Theming & design-primitieven (maatwerk zonder core te forken).\n// - `defineTheme` maakt een eigen Astryx-thema (tokens + component-overrides) → geef door als\n// XiCMSProvider `astryxTheme`-prop.\n// - De `ui`-primitieven (Card/Stack/Grid/Heading/Text/Btn…) zijn de bouwstenen waarop de ingebouwde\n// blokken draaien; gebruik ze in eigen blokken voor een consistent maar te herstijlen uiterlijk.\nexport { defineTheme, useTheme } from \"@astryxdesign/core/theme\";\nexport type { DefinedTheme } from \"@astryxdesign/core/theme\";\n// Framework-router-integratie: geef `next/link` door als XiCMSProvider `linkComponent`-prop\n// (soepele client-nav + prefetch op alle Astryx-knoppen/links). `useLinkComponent` voor eigen blokken.\nexport { LinkProvider, useLinkComponent } from \"@astryxdesign/core/Link\";\nexport type { LinkComponentType } from \"@astryxdesign/core/Link\";\n// Beeldoptimalisatie: geef `next/image` door als XiCMSProvider `imageComponent`-prop. `Img` is de\n// wrapper die de blokken gebruiken; `useImageComponent` voor eigen blokken.\nexport { Img, ImageProvider, useImageComponent, InlineImageEditProvider } from \"./media/ImageContext\";\nexport type { ImageComponentType, ImgProps } from \"./media/ImageContext\";\nexport * from \"./blocks/ui\";\nexport {\n SiteSettingsProvider,\n useSiteSettings,\n useMergedSiteConfig,\n} from \"./settings/useSiteSettings\";\nexport { SiteFooter } from \"./components/site/SiteFooter\";\n// 7.24: de beheerde hoofdnavigatie — de enige plek die `settings.nav` leest. Zie SiteNav.tsx.\nexport { SiteNav } from \"./components/site/SiteNav\";\nexport { EditorProvider, useEditor } from \"./editor/EditorProvider\";\nexport {\n definePattern,\n instantiatePattern,\n collectPatternTypes,\n builtinPatterns,\n type Pattern,\n type PatternBlock,\n} from \"./patterns\";\nexport { builtinThemePresets, type ThemePreset } from \"./theme/presets\";\nexport { defineSiteConfig } from \"./schema/siteConfig\";\nexport type { SiteConfig, SiteMode } from \"./schema/siteConfig\";\n// 5.0: Field-IR (primo-port), symbolen en custom blocks.\nexport { FieldSchema, FieldsSchema, fieldsToZod, fieldsDefaults, isFieldVisible, isRichTextKey } from \"./schema/fields\";\nexport type { Field, FieldHints, ShowIf } from \"./schema/fields\";\nexport { SymbolSchema } from \"./schema/symbols\";\nexport type { SymbolDefinition, StoredSymbol } from \"./schema/symbols\";\nexport { CustomBlockSchema } from \"./schema/customBlocks\";\nexport type { CustomBlock, CustomBlock as CustomBlockData } from \"./schema/customBlocks\";\nexport { customBlockToDefinition } from \"./blocks/customTemplate/definition\";\nexport { interpretTemplate, scopeCss } from \"./blocks/customTemplate/interpret\";\n// 5.0: localisatie — overlay-model (primo's locale-per-waarde, additief vertaald).\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n} from \"./i18n/overlay\";\nexport type { Translations, LocaleConfig } from \"./i18n/overlay\";\nexport type { ThemeTokens, NavItem, NavLeaf, Redirect, SiteSettings, PublicSiteSettings, FormsSender } from \"./schema/settings\";\nexport { zoekRedirect, normaliseerPad, navHref, isExterneLink } from \"./schema/settings\";\nexport type { Seo } from \"./schema/seo\";\nexport type { Page, PageStatus } from \"./schema/pages\";\nexport type { FormDefinition, FormField } from \"./schema/forms\";\n\n// Auth\nexport { AuthProvider, useAuth } from \"./auth/AuthProvider\";\nexport { LoginForm } from \"./auth/LoginForm\";\nexport {\n roleForSite,\n canEdit,\n canManageSite,\n isSuperadmin,\n type Role,\n type XiClaims,\n} from \"./auth/roles\";\n\n// De legacy inline-laag (ContentProvider/useContent + EditableText/EditableRichText/EditableImage,\n// het v1 platte `cms/content`-model) is in 5.0 VERWIJDERD, zoals bij de deprecatie aangekondigd.\n// Inline bewerken loopt via het block-model: markeer tekstvelden met `data-xicms-edit` in de Render.\nexport type { NewsArticle, MediaItem, MediaVariant } from \"./content/types\";\n\nexport { EditBar } from \"./components/EditBar\";\nexport { ExternEmbed } from \"./components/ExternEmbed\";\nexport { ResponsiveImage } from \"./components/ResponsiveImage\";\nexport { PageView } from \"./components/PageView\";\nexport { NewsList, NewsArticleView, useNews } from \"./components/News\";\n\n// Block-systeem (runtime: registry + renderer; editors zitten in /admin)\nexport {\n defineBlock,\n createRegistry,\n mergeRegistry,\n validateBlocks,\n buildBlockAddOptions,\n type BlockDefinition,\n type BlockRegistry,\n type BlockRenderProps,\n type BlockEditorProps,\n type BlockCategory,\n} from \"./blocks/registry\";\nexport { BlockRenderer } from \"./blocks/BlockRenderer\";\nexport { RegistryProvider, useRegistry, BlockDataProvider, useBlockData } from \"./blocks/context\";\nexport { builtinBlocks, ConsentEmbed } from \"./blocks/builtins\";\nexport { PageRenderer } from \"./render/PageRenderer\";\nexport { PublishedPage } from \"./render/PublishedPage\";\n\n// Module-systeem (XiCMS 3.0): modules bundelen blocks + instellingen; per site aan/uit + te stylen.\nexport {\n defineModule,\n resolveModules,\n isModuleActive,\n ModulesProvider,\n useModules,\n useModuleActive,\n useModuleSettings,\n applyBlockOverrides,\n moduleIdForBlockType,\n builtinModules,\n validateModuleContract,\n assertModuleContract,\n useConsent,\n ConsentBanner,\n ConsentGate,\n ConsentReopenLink,\n type ConsentState,\n type ConsentSettings,\n type ConsentCategory,\n type ModuleDefinition,\n type ModuleSiteSetting,\n type ModulesSiteConfig,\n type Capability,\n type ResolvedModules,\n type BlockOverride,\n type BlockOverrides,\n} from \"./modules\";\n\n// Demo-lock\nexport { DemoLock } from \"./lock/DemoLock\";\n\n// Demo-bewerkmodus (lokaal-only) leeft op het subpad `@xiwebdesign/xicms-core/demo` — dit zijn\n// demo-store-internals die niet in de publieke runtime-API thuishoren (klantsites gebruiken ze nooit;\n// alleen de XiCMS.nl-showcase). DemoStudio wordt intern door XiCMSProvider gerenderd.\n\n// Firebase (client)\nexport {\n getFirebaseApp,\n getFirebaseAuth,\n getDb,\n getBucket,\n isFirebaseConfigured,\n} from \"./firebase/client\";\nexport { initAppCheck } from \"./firebase/appcheck\";\nexport { SiteChrome } from \"./SiteChrome\";\nexport { CORE_VERSION, CORE_PACKAGE, isNieuwer } from \"./version\";\n"],"mappings":"AAQA,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAO9B,SAAS,aAAa,gBAAgB;AAItC,SAAS,cAAc,wBAAwB;AAI/C,SAAS,KAAK,eAAe,mBAAmB,+BAA+B;AAE/E,cAAc;AACd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAE3B,SAAS,eAAe;AACxB,SAAS,gBAAgB,iBAAiB;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,SAAS,2BAA6C;AACtD,SAAS,wBAAwB;AAGjC,SAAS,aAAa,cAAc,aAAa,gBAAgB,gBAAgB,qBAAqB;AAEtG,SAAS,oBAAoB;AAE7B,SAAS,yBAAyB;AAElC,SAAS,+BAA+B;AACxC,SAAS,mBAAmB,gBAAgB;AAE5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,cAAc,gBAAgB,SAAS,qBAAqB;AAMrE,SAAS,cAAc,eAAe;AACtC,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAOP,SAAS,eAAe;AACxB,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,gBAAgB;AACzB,SAAS,UAAU,iBAAiB,eAAe;AAGnD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AACP,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB,aAAa,mBAAmB,oBAAoB;AAC/E,SAAS,eAAe,oBAAoB;AAC5C,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAG9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAWK;AAGP,SAAS,gBAAgB;AAOzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,cAAc,cAAc,iBAAiB;","names":[]}
@@ -33,9 +33,4 @@ export interface ImgProps {
33
33
  /** Beeld vult absoluut z'n parent (bv. hero-achtergrond): geen wrapper-box, knop absoluut t.o.v. het blok. */
34
34
  editFill?: boolean;
35
35
  }
36
- /**
37
- * Eén beeld-wrapper voor alle blokken. Gebruikt de geïnjecteerde `imageComponent` (next/image) als
38
- * die er is, anders een gewone `<img>`. De display-grootte regel je met CSS (`style`/`className`,
39
- * bv. `width:100%;height:100%;object-fit:cover`); `width`/`height` zijn de verhouding.
40
- */
41
- export declare function Img({ src, alt, width, height, sizes, priority, className, style, editField, editFill }: ImgProps): import("react").JSX.Element;
36
+ export declare function Img({ src, alt, width, height, sizes, priority, className, style: styleProp, editField, editFill }: ImgProps): import("react").JSX.Element;
@@ -16,13 +16,18 @@ function InlineImageEditProvider({
16
16
  }) {
17
17
  return /* @__PURE__ */ jsx(InlineImageEditContext.Provider, { value: onReplace, children });
18
18
  }
19
- function Img({ src, alt, width, height, sizes, priority, className, style, editField, editFill }) {
19
+ function style_heeftPositie(style) {
20
+ return Boolean(style && "objectPosition" in style && style.objectPosition);
21
+ }
22
+ function Img({ src, alt, width, height, sizes, priority, className, style: styleProp, editField, editFill }) {
20
23
  const ImageComponent = useImageComponent();
21
24
  const onReplace = useContext(InlineImageEditContext);
22
25
  const mediaSeed = useBlockData("media");
23
26
  const eigenLadder = mediaSeed?.[src];
24
27
  const ladder = eigenLadder?.variants?.sizes ?? [];
25
28
  const webp = eigenLadder?.variants?.webp;
29
+ const focal = eigenLadder?.focal;
30
+ const style = focal && !style_heeftPositie(styleProp) ? { ...styleProp, objectPosition: `${Math.round(focal.x * 100)}% ${Math.round(focal.y * 100)}%` } : styleProp;
26
31
  const imageEl = ladder.length > 0 ? /* @__PURE__ */ jsxs("picture", { children: [
27
32
  webp && /* @__PURE__ */ jsx("source", { type: "image/webp", srcSet: `${webp.url} ${webp.width}w`, sizes }),
28
33
  /* @__PURE__ */ jsx(
@@ -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\";\nimport { useBlockData } from \"../blocks/context\";\nimport type { MediaItem } from \"../content/types\";\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 /**\n * Eigen srcset-ladder vóór next/image (7.13). De upload-pipeline (onMediaUpload, sharp) maakt\n * bij élke upload al webp + een maten-ladder; next/image liet dat werk links liggen en\n * transformeerde dezelfde beelden nóg een keer bij Vercel — betaalde image-transformations,\n * plus transformatie-latency op de eerste hit. De route levert daarom per pagina een\n * `media`-seed (url → varianten, zie server/blockData.ts); staat de URL van dit beeld daarin,\n * dan renderen we een kale <picture> met de eigen ladder en blijft Vercel er helemaal buiten.\n * Geen seed (editor, oudere site, extern beeld zoals Unsplash) → het bestaande pad.\n */\n const mediaSeed = useBlockData<Record<string, Pick<MediaItem, \"variants\" | \"width\" | \"height\">>>(\"media\");\n const eigenLadder = mediaSeed?.[src];\n const ladder = eigenLadder?.variants?.sizes ?? [];\n const webp = eigenLadder?.variants?.webp;\n const imageEl = ladder.length > 0 ? (\n <picture>\n {webp && <source type=\"image/webp\" srcSet={`${webp.url} ${webp.width}w`} sizes={sizes} />}\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={src}\n srcSet={ladder.map((v) => `${v.url} ${v.width}w`).join(\", \")}\n sizes={sizes}\n alt={alt}\n width={eigenLadder?.width ?? width}\n height={eigenLadder?.height ?? height}\n loading={priority ? \"eager\" : \"lazy\"}\n fetchPriority={priority ? \"high\" : undefined}\n decoding=\"async\"\n className={className}\n style={style}\n />\n </picture>\n ) : 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":";AAkBS,SAqIiB,UArIjB,KAyEL,YAzEK;AAhBT,SAAS,eAAe,kBAAwE;AAChG,SAAS,oBAAoB;AAY7B,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;AAUnD,QAAM,YAAY,aAA+E,OAAO;AACxG,QAAM,cAAc,YAAY,GAAG;AACnC,QAAM,SAAS,aAAa,UAAU,SAAS,CAAC;AAChD,QAAM,OAAO,aAAa,UAAU;AACpC,QAAM,UAAU,OAAO,SAAS,IAC9B,qBAAC,aACE;AAAA,YAAQ,oBAAC,YAAO,MAAK,cAAa,QAAQ,GAAG,KAAK,GAAG,IAAI,KAAK,KAAK,KAAK,OAAc;AAAA,IAEvF;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,QAAQ,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI,EAAE,KAAK,GAAG,EAAE,KAAK,IAAI;AAAA,QAC3D;AAAA,QACA;AAAA,QACA,OAAO,aAAa,SAAS;AAAA,QAC7B,QAAQ,aAAa,UAAU;AAAA,QAC/B,SAAS,WAAW,UAAU;AAAA,QAC9B,eAAe,WAAW,SAAS;AAAA,QACnC,UAAS;AAAA,QACT;AAAA,QACA;AAAA;AAAA,IACF;AAAA,KACF,IACE,iBACF;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":[]}
1
+ {"version":3,"sources":["../../src/media/ImageContext.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, type CSSProperties, type ElementType, type ReactNode } from \"react\";\nimport { useBlockData } from \"../blocks/context\";\nimport type { MediaItem } from \"../content/types\";\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 */\nfunction style_heeftPositie(style?: CSSProperties): boolean {\n return Boolean(style && \"objectPosition\" in style && style.objectPosition);\n}\n\nexport function Img({ src, alt, width, height, sizes, priority, className, style: styleProp, editField, editFill }: ImgProps) {\n const ImageComponent = useImageComponent();\n const onReplace = useContext(InlineImageEditContext);\n /**\n * Eigen srcset-ladder vóór next/image (7.13). De upload-pipeline (onMediaUpload, sharp) maakt\n * bij élke upload al webp + een maten-ladder; next/image liet dat werk links liggen en\n * transformeerde dezelfde beelden nóg een keer bij Vercel — betaalde image-transformations,\n * plus transformatie-latency op de eerste hit. De route levert daarom per pagina een\n * `media`-seed (url → varianten, zie server/blockData.ts); staat de URL van dit beeld daarin,\n * dan renderen we een kale <picture> met de eigen ladder en blijft Vercel er helemaal buiten.\n * Geen seed (editor, oudere site, extern beeld zoals Unsplash) → het bestaande pad.\n */\n const mediaSeed = useBlockData<Record<string, Pick<MediaItem, \"variants\" | \"width\" | \"height\" | \"focal\">>>(\"media\");\n const eigenLadder = mediaSeed?.[src];\n const ladder = eigenLadder?.variants?.sizes ?? [];\n const webp = eigenLadder?.variants?.webp;\n // Focuspunt (7.24): staat er in het beheer een focuspunt op deze foto, dan snijdt `object-fit:\n // cover` rond dát punt bij in plaats van rond het midden. Alleen als het blok zelf geen\n // object-position zet — een blok dat bewust \"top\" wil, wint.\n const focal = eigenLadder?.focal;\n const style = focal && !style_heeftPositie(styleProp)\n ? { ...styleProp, objectPosition: `${Math.round(focal.x * 100)}% ${Math.round(focal.y * 100)}%` }\n : styleProp;\n const imageEl = ladder.length > 0 ? (\n <picture>\n {webp && <source type=\"image/webp\" srcSet={`${webp.url} ${webp.width}w`} sizes={sizes} />}\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={src}\n srcSet={ladder.map((v) => `${v.url} ${v.width}w`).join(\", \")}\n sizes={sizes}\n alt={alt}\n width={eigenLadder?.width ?? width}\n height={eigenLadder?.height ?? height}\n loading={priority ? \"eager\" : \"lazy\"}\n fetchPriority={priority ? \"high\" : undefined}\n decoding=\"async\"\n className={className}\n style={style}\n />\n </picture>\n ) : 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":"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","names":[]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * "Wat is nieuw" (7.24) — korte, klantgerichte notities per release, getoond in /beheer.
3
+ *
4
+ * Features die je bouwt zonder dat de klant het hoort, bestaan voor die klant niet. Dit is de
5
+ * enige plek waar dat verteld wordt; `CoreVersion` toont de notitie van de huidige minor één keer.
6
+ * Alleen wat de KLANT merkt, in zijn woorden — geen interne refactors.
7
+ */
8
+ export interface ReleaseNote {
9
+ /** major.minor — patches tellen niet als "nieuw". */
10
+ versie: string;
11
+ punten: string[];
12
+ }
13
+ export declare const RELEASE_NOTES: ReleaseNote[];
14
+ /** De notitie die bij deze engine-versie hoort (op major.minor). */
15
+ export declare function releaseNoteVoor(versie: string): ReleaseNote | null;
@@ -0,0 +1,28 @@
1
+ const RELEASE_NOTES = [
2
+ {
3
+ versie: "7.24",
4
+ punten: [
5
+ "Het menu dat je onder Menu instelt staat nu \xE9cht op de site \u2014 met submenu's.",
6
+ "Bij Pagina's zie je meteen welke pagina's wijzigingen hebben die nog niet live staan.",
7
+ "Een pagina kun je offline halen (en later weer publiceren) of naar de prullenbak doen \u2014 30 dagen te herstellen.",
8
+ "Verander je het adres van een pagina, dan verwijst het oude adres automatisch door.",
9
+ "Deel een preview-link van een werkversie met iemand zonder account.",
10
+ "Kies een focuspunt op een foto zodat het juiste deel in beeld blijft op mobiel.",
11
+ "Formulieren: inzendingen exporteren als CSV, verwijderen, en zien wat nieuw is.",
12
+ "Statistieken tonen nu ook waar bezoekers vandaan komen en op welk apparaat."
13
+ ]
14
+ },
15
+ {
16
+ versie: "7.23",
17
+ punten: ["Per productoptie (bijvoorbeeld Print of Kleur) kun je aangeven dat elke waarde een eigen foto heeft."]
18
+ }
19
+ ];
20
+ function releaseNoteVoor(versie) {
21
+ const mm = versie.split(".").slice(0, 2).join(".");
22
+ return RELEASE_NOTES.find((n) => n.versie === mm) ?? null;
23
+ }
24
+ export {
25
+ RELEASE_NOTES,
26
+ releaseNoteVoor
27
+ };
28
+ //# sourceMappingURL=releaseNotes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/releaseNotes.ts"],"sourcesContent":["/**\n * \"Wat is nieuw\" (7.24) — korte, klantgerichte notities per release, getoond in /beheer.\n *\n * Features die je bouwt zonder dat de klant het hoort, bestaan voor die klant niet. Dit is de\n * enige plek waar dat verteld wordt; `CoreVersion` toont de notitie van de huidige minor één keer.\n * Alleen wat de KLANT merkt, in zijn woorden — geen interne refactors.\n */\nexport interface ReleaseNote {\n /** major.minor — patches tellen niet als \"nieuw\". */\n versie: string;\n punten: string[];\n}\n\nexport const RELEASE_NOTES: ReleaseNote[] = [\n {\n versie: \"7.24\",\n punten: [\n \"Het menu dat je onder Menu instelt staat nu écht op de site — met submenu's.\",\n \"Bij Pagina's zie je meteen welke pagina's wijzigingen hebben die nog niet live staan.\",\n \"Een pagina kun je offline halen (en later weer publiceren) of naar de prullenbak doen — 30 dagen te herstellen.\",\n \"Verander je het adres van een pagina, dan verwijst het oude adres automatisch door.\",\n \"Deel een preview-link van een werkversie met iemand zonder account.\",\n \"Kies een focuspunt op een foto zodat het juiste deel in beeld blijft op mobiel.\",\n \"Formulieren: inzendingen exporteren als CSV, verwijderen, en zien wat nieuw is.\",\n \"Statistieken tonen nu ook waar bezoekers vandaan komen en op welk apparaat.\",\n ],\n },\n {\n versie: \"7.23\",\n punten: [\"Per productoptie (bijvoorbeeld Print of Kleur) kun je aangeven dat elke waarde een eigen foto heeft.\"],\n },\n];\n\n/** De notitie die bij deze engine-versie hoort (op major.minor). */\nexport function releaseNoteVoor(versie: string): ReleaseNote | null {\n const mm = versie.split(\".\").slice(0, 2).join(\".\");\n return RELEASE_NOTES.find((n) => n.versie === mm) ?? null;\n}\n"],"mappings":"AAaO,MAAM,gBAA+B;AAAA,EAC1C;AAAA,IACE,QAAQ;AAAA,IACR,QAAQ;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,QAAQ;AAAA,IACR,QAAQ,CAAC,sGAAsG;AAAA,EACjH;AACF;AAGO,SAAS,gBAAgB,QAAoC;AAClE,QAAM,KAAK,OAAO,MAAM,GAAG,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AACjD,SAAO,cAAc,KAAK,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK;AACvD;","names":[]}
@@ -51,6 +51,18 @@ export declare const MAIL_TEMPLATE_DEFS: readonly [{
51
51
  readonly hint: "Naar de klant bij annulering.";
52
52
  readonly module: "shop";
53
53
  readonly placeholders: "{{naam}} {{ordernummer}} {{sitenaam}}";
54
+ }, {
55
+ readonly key: "shopkeeperOrderCancelled";
56
+ readonly label: "Bestelling geannuleerd (naar jou)";
57
+ readonly hint: "Naar jou bij elke annulering — ook automatisch (bv. een verlopen betaling).";
58
+ readonly module: "shop";
59
+ readonly placeholders: "{{sitenaam}} {{ordernummer}} {{totaal}} {{klant}} {{regels}}";
60
+ }, {
61
+ readonly key: "paymentRequest";
62
+ readonly label: "Betaalverzoek";
63
+ readonly hint: "Naar de klant als je vanuit een openstaande bestelling een betaallink verstuurt.";
64
+ readonly module: "shop";
65
+ readonly placeholders: "{{naam}} {{ordernummer}} {{betaallink}} {{regels}} {{totaal}} {{verzendadres}} {{sitenaam}}";
54
66
  }, {
55
67
  readonly key: "orderRefunded";
56
68
  readonly label: "Terugbetaald";
@@ -103,9 +115,17 @@ export declare const MailTemplatesSchema: z.ZodMiniObject<{
103
115
  subject: z.ZodMiniOptional<z.ZodMiniString<string>>;
104
116
  bodyText: z.ZodMiniOptional<z.ZodMiniString<string>>;
105
117
  }, z.core.$strip>>;
118
+ paymentRequest: z.ZodMiniOptional<z.ZodMiniObject<{
119
+ subject: z.ZodMiniOptional<z.ZodMiniString<string>>;
120
+ bodyText: z.ZodMiniOptional<z.ZodMiniString<string>>;
121
+ }, z.core.$strip>>;
106
122
  shopkeeperNotification: z.ZodMiniOptional<z.ZodMiniObject<{
107
123
  subject: z.ZodMiniOptional<z.ZodMiniString<string>>;
108
124
  bodyText: z.ZodMiniOptional<z.ZodMiniString<string>>;
109
125
  }, z.core.$strip>>;
126
+ shopkeeperOrderCancelled: z.ZodMiniOptional<z.ZodMiniObject<{
127
+ subject: z.ZodMiniOptional<z.ZodMiniString<string>>;
128
+ bodyText: z.ZodMiniOptional<z.ZodMiniString<string>>;
129
+ }, z.core.$strip>>;
110
130
  }, z.core.$strip>;
111
131
  export type MailTemplates = z.infer<typeof MailTemplatesSchema>;
@@ -42,6 +42,20 @@ const MAIL_TEMPLATE_DEFS = [
42
42
  module: "shop",
43
43
  placeholders: "{{naam}} {{ordernummer}} {{sitenaam}}"
44
44
  },
45
+ {
46
+ key: "shopkeeperOrderCancelled",
47
+ label: "Bestelling geannuleerd (naar jou)",
48
+ hint: "Naar jou bij elke annulering \u2014 ook automatisch (bv. een verlopen betaling).",
49
+ module: "shop",
50
+ placeholders: "{{sitenaam}} {{ordernummer}} {{totaal}} {{klant}} {{regels}}"
51
+ },
52
+ {
53
+ key: "paymentRequest",
54
+ label: "Betaalverzoek",
55
+ hint: "Naar de klant als je vanuit een openstaande bestelling een betaallink verstuurt.",
56
+ module: "shop",
57
+ placeholders: "{{naam}} {{ordernummer}} {{betaallink}} {{regels}} {{totaal}} {{verzendadres}} {{sitenaam}}"
58
+ },
45
59
  {
46
60
  key: "orderRefunded",
47
61
  label: "Terugbetaald",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/schema/mail.ts"],"sourcesContent":["import * as z from \"zod/mini\";\n\n/**\n * E-mailtemplates (site-breed, 3.7) — `sites/{siteId}/settings/mailTemplates`. Per template een\n * onderwerp + platte-tekst-body met `{{placeholders}}`. Bewust géén HTML van beheerders; de\n * huisstijl-opmaak komt in de mail-pipeline (functions `mailLayout.ts`) om de tekst heen.\n *\n * De keys zijn getagd met de module waartoe ze horen (`core`/`shop`), zodat de editor in /beheer\n * shop-templates alleen toont op sites met de webwinkel. `core`-keys (formulieren) gelden altijd.\n */\n\nexport interface MailTemplateDef {\n key: string;\n label: string;\n hint: string;\n module: \"core\" | \"shop\";\n /** Beschikbare invulvelden, als tonende hint in de editor. */\n placeholders: string;\n}\n\nexport const MAIL_TEMPLATE_DEFS = [\n {\n key: \"formNotification\",\n label: \"Formulier-notificatie (naar jou)\",\n hint: \"Naar jou zodra iemand een formulier invult.\",\n module: \"core\",\n placeholders: \"{{sitenaam}} {{formulier}} {{regels}}\",\n },\n {\n key: \"formConfirmation\",\n label: \"Formulier-bevestiging (naar de inzender)\",\n hint: \"Ontvangstbevestiging naar wie het formulier invulde.\",\n module: \"core\",\n placeholders: \"{{sitenaam}} {{formulier}} {{regels}}\",\n },\n {\n key: \"orderConfirmation\",\n label: \"Bestelbevestiging\",\n hint: \"Naar de klant zodra de betaling binnen is — met factuur-PDF als bijlage.\",\n module: \"shop\",\n placeholders: \"{{naam}} {{ordernummer}} {{regels}} {{totaal}} {{verzendadres}} {{notitie}} {{sitenaam}}\",\n },\n {\n key: \"shopkeeperNotification\",\n label: \"Nieuwe-bestelling-notificatie (naar jou)\",\n hint: \"Naar jou bij elke nieuwe bestelling.\",\n module: \"shop\",\n placeholders: \"{{sitenaam}} {{ordernummer}} {{totaal}} {{klant}} {{regels}} {{verzendadres}} {{notitie}}\",\n },\n {\n key: \"orderShipped\",\n label: \"Bestelling verzonden\",\n hint: \"Naar de klant wanneer je de status op ‘verzonden’ zet.\",\n module: \"shop\",\n placeholders: \"{{naam}} {{ordernummer}} {{regels}} {{trackingUrl}} {{verzendadres}} {{sitenaam}}\",\n },\n {\n key: \"orderCancelled\",\n label: \"Bestelling geannuleerd\",\n hint: \"Naar de klant bij annulering.\",\n module: \"shop\",\n placeholders: \"{{naam}} {{ordernummer}} {{sitenaam}}\",\n },\n {\n key: \"orderRefunded\",\n label: \"Terugbetaald\",\n hint: \"Naar de klant na een terugbetaling.\",\n module: \"shop\",\n placeholders: \"{{naam}} {{ordernummer}} {{totaal}} {{sitenaam}}\",\n },\n {\n key: \"donationThanks\",\n label: \"Donatie-bedankje\",\n hint: \"Naar de gever na een donatie.\",\n module: \"shop\",\n placeholders: \"{{naam}} {{regels}} {{sitenaam}}\",\n },\n] as const satisfies readonly MailTemplateDef[];\n\nexport const MAIL_TEMPLATE_KEYS = MAIL_TEMPLATE_DEFS.map((d) => d.key) as unknown as readonly MailTemplateKey[];\nexport type MailTemplateKey = (typeof MAIL_TEMPLATE_DEFS)[number][\"key\"];\n\n/** Formulier-mails — bewerkbaar in het Formulieren-gedeelte. */\nexport const FORM_TEMPLATE_KEYS = MAIL_TEMPLATE_DEFS.filter((d) => d.module === \"core\").map((d) => d.key) as readonly MailTemplateKey[];\n/** Shop-mails — bewerkbaar in het Webwinkel-gedeelte. */\nexport const SHOP_TEMPLATE_KEYS = MAIL_TEMPLATE_DEFS.filter((d) => d.module === \"shop\").map((d) => d.key) as readonly MailTemplateKey[];\n\nexport const MailTemplateSchema = z.object({\n subject: z.optional(z.string().check(z.maxLength(200))),\n bodyText: z.optional(z.string().check(z.maxLength(10_000))),\n});\nexport const MailTemplatesSchema = z.object(\n Object.fromEntries(MAIL_TEMPLATE_DEFS.map((d) => [d.key, z.optional(MailTemplateSchema)])) as Record<\n MailTemplateKey,\n z.ZodMiniOptional<typeof MailTemplateSchema>\n >,\n);\nexport type MailTemplates = z.infer<typeof MailTemplatesSchema>;\n"],"mappings":"AAAA,YAAY,OAAO;AAoBZ,MAAM,qBAAqB;AAAA,EAChC;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AACF;AAEO,MAAM,qBAAqB,mBAAmB,IAAI,CAAC,MAAM,EAAE,GAAG;AAI9D,MAAM,qBAAqB,mBAAmB,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG;AAEjG,MAAM,qBAAqB,mBAAmB,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG;AAEjG,MAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC;AAAA,EACtD,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAM,CAAC,CAAC;AAC5D,CAAC;AACM,MAAM,sBAAsB,EAAE;AAAA,EACnC,OAAO,YAAY,mBAAmB,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,SAAS,kBAAkB,CAAC,CAAC,CAAC;AAI3F;","names":[]}
1
+ {"version":3,"sources":["../../src/schema/mail.ts"],"sourcesContent":["import * as z from \"zod/mini\";\n\n/**\n * E-mailtemplates (site-breed, 3.7) — `sites/{siteId}/settings/mailTemplates`. Per template een\n * onderwerp + platte-tekst-body met `{{placeholders}}`. Bewust géén HTML van beheerders; de\n * huisstijl-opmaak komt in de mail-pipeline (functions `mailLayout.ts`) om de tekst heen.\n *\n * De keys zijn getagd met de module waartoe ze horen (`core`/`shop`), zodat de editor in /beheer\n * shop-templates alleen toont op sites met de webwinkel. `core`-keys (formulieren) gelden altijd.\n */\n\nexport interface MailTemplateDef {\n key: string;\n label: string;\n hint: string;\n module: \"core\" | \"shop\";\n /** Beschikbare invulvelden, als tonende hint in de editor. */\n placeholders: string;\n}\n\nexport const MAIL_TEMPLATE_DEFS = [\n {\n key: \"formNotification\",\n label: \"Formulier-notificatie (naar jou)\",\n hint: \"Naar jou zodra iemand een formulier invult.\",\n module: \"core\",\n placeholders: \"{{sitenaam}} {{formulier}} {{regels}}\",\n },\n {\n key: \"formConfirmation\",\n label: \"Formulier-bevestiging (naar de inzender)\",\n hint: \"Ontvangstbevestiging naar wie het formulier invulde.\",\n module: \"core\",\n placeholders: \"{{sitenaam}} {{formulier}} {{regels}}\",\n },\n {\n key: \"orderConfirmation\",\n label: \"Bestelbevestiging\",\n hint: \"Naar de klant zodra de betaling binnen is — met factuur-PDF als bijlage.\",\n module: \"shop\",\n placeholders: \"{{naam}} {{ordernummer}} {{regels}} {{totaal}} {{verzendadres}} {{notitie}} {{sitenaam}}\",\n },\n {\n key: \"shopkeeperNotification\",\n label: \"Nieuwe-bestelling-notificatie (naar jou)\",\n hint: \"Naar jou bij elke nieuwe bestelling.\",\n module: \"shop\",\n placeholders: \"{{sitenaam}} {{ordernummer}} {{totaal}} {{klant}} {{regels}} {{verzendadres}} {{notitie}}\",\n },\n {\n key: \"orderShipped\",\n label: \"Bestelling verzonden\",\n hint: \"Naar de klant wanneer je de status op ‘verzonden’ zet.\",\n module: \"shop\",\n placeholders: \"{{naam}} {{ordernummer}} {{regels}} {{trackingUrl}} {{verzendadres}} {{sitenaam}}\",\n },\n {\n key: \"orderCancelled\",\n label: \"Bestelling geannuleerd\",\n hint: \"Naar de klant bij annulering.\",\n module: \"shop\",\n placeholders: \"{{naam}} {{ordernummer}} {{sitenaam}}\",\n },\n {\n key: \"shopkeeperOrderCancelled\",\n label: \"Bestelling geannuleerd (naar jou)\",\n hint: \"Naar jou bij elke annulering — ook automatisch (bv. een verlopen betaling).\",\n module: \"shop\",\n placeholders: \"{{sitenaam}} {{ordernummer}} {{totaal}} {{klant}} {{regels}}\",\n },\n {\n key: \"paymentRequest\",\n label: \"Betaalverzoek\",\n hint: \"Naar de klant als je vanuit een openstaande bestelling een betaallink verstuurt.\",\n module: \"shop\",\n placeholders: \"{{naam}} {{ordernummer}} {{betaallink}} {{regels}} {{totaal}} {{verzendadres}} {{sitenaam}}\",\n },\n {\n key: \"orderRefunded\",\n label: \"Terugbetaald\",\n hint: \"Naar de klant na een terugbetaling.\",\n module: \"shop\",\n placeholders: \"{{naam}} {{ordernummer}} {{totaal}} {{sitenaam}}\",\n },\n {\n key: \"donationThanks\",\n label: \"Donatie-bedankje\",\n hint: \"Naar de gever na een donatie.\",\n module: \"shop\",\n placeholders: \"{{naam}} {{regels}} {{sitenaam}}\",\n },\n] as const satisfies readonly MailTemplateDef[];\n\nexport const MAIL_TEMPLATE_KEYS = MAIL_TEMPLATE_DEFS.map((d) => d.key) as unknown as readonly MailTemplateKey[];\nexport type MailTemplateKey = (typeof MAIL_TEMPLATE_DEFS)[number][\"key\"];\n\n/** Formulier-mails — bewerkbaar in het Formulieren-gedeelte. */\nexport const FORM_TEMPLATE_KEYS = MAIL_TEMPLATE_DEFS.filter((d) => d.module === \"core\").map((d) => d.key) as readonly MailTemplateKey[];\n/** Shop-mails — bewerkbaar in het Webwinkel-gedeelte. */\nexport const SHOP_TEMPLATE_KEYS = MAIL_TEMPLATE_DEFS.filter((d) => d.module === \"shop\").map((d) => d.key) as readonly MailTemplateKey[];\n\nexport const MailTemplateSchema = z.object({\n subject: z.optional(z.string().check(z.maxLength(200))),\n bodyText: z.optional(z.string().check(z.maxLength(10_000))),\n});\nexport const MailTemplatesSchema = z.object(\n Object.fromEntries(MAIL_TEMPLATE_DEFS.map((d) => [d.key, z.optional(MailTemplateSchema)])) as Record<\n MailTemplateKey,\n z.ZodMiniOptional<typeof MailTemplateSchema>\n >,\n);\nexport type MailTemplates = z.infer<typeof MailTemplatesSchema>;\n"],"mappings":"AAAA,YAAY,OAAO;AAoBZ,MAAM,qBAAqB;AAAA,EAChC;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AACF;AAEO,MAAM,qBAAqB,mBAAmB,IAAI,CAAC,MAAM,EAAE,GAAG;AAI9D,MAAM,qBAAqB,mBAAmB,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG;AAEjG,MAAM,qBAAqB,mBAAmB,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG;AAEjG,MAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC;AAAA,EACtD,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAM,CAAC,CAAC;AAC5D,CAAC;AACM,MAAM,sBAAsB,EAAE;AAAA,EACnC,OAAO,YAAY,mBAAmB,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,SAAS,kBAAkB,CAAC,CAAC,CAAC;AAI3F;","names":[]}