@xiwebdesign/xicms-core 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/XiCMSProvider.d.ts +11 -2
- package/dist/XiCMSProvider.js +10 -5
- package/dist/XiCMSProvider.js.map +1 -1
- package/dist/admin/blockEditors/GenericEditor.js +18 -8
- package/dist/admin/blockEditors/GenericEditor.js.map +1 -1
- package/dist/admin/blockEditors/index.js +2 -2
- package/dist/admin/blockEditors/index.js.map +1 -1
- package/dist/admin/customBlocks/CustomBlocksPanel.js +12 -2
- package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
- package/dist/admin/lib/pagesClient.js +4 -1
- package/dist/admin/lib/pagesClient.js.map +1 -1
- package/dist/admin/lib/upload.js +14 -7
- package/dist/admin/lib/upload.js.map +1 -1
- package/dist/admin/media/MediaManager.js +60 -28
- package/dist/admin/media/MediaManager.js.map +1 -1
- package/dist/admin/media/MediaPicker.js +25 -15
- package/dist/admin/media/MediaPicker.js.map +1 -1
- package/dist/admin/pages/PageEditor.d.ts +2 -1
- package/dist/admin/pages/PageEditor.js +16 -2
- package/dist/admin/pages/PageEditor.js.map +1 -1
- package/dist/admin/pages/PagesPanel.js +9 -5
- package/dist/admin/pages/PagesPanel.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +11 -2
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/agentFiles/files.d.ts +15 -3
- package/dist/agentFiles/files.js +57 -0
- package/dist/agentFiles/files.js.map +1 -1
- package/dist/auth/AuthProvider.js +3 -3
- package/dist/auth/AuthProvider.js.map +1 -1
- package/dist/blocks/builtins/Booking.js +1 -1
- package/dist/blocks/builtins/Booking.js.map +1 -1
- package/dist/blocks/builtins/Columns.js +1 -1
- package/dist/blocks/builtins/Columns.js.map +1 -1
- package/dist/blocks/builtins/Embed.js +2 -0
- package/dist/blocks/builtins/Embed.js.map +1 -1
- package/dist/blocks/builtins/Gallery.js +2 -2
- package/dist/blocks/builtins/Gallery.js.map +1 -1
- package/dist/blocks/builtins/Hero.js +46 -7
- package/dist/blocks/builtins/Hero.js.map +1 -1
- package/dist/blocks/builtins/ImageBlock.js +14 -3
- package/dist/blocks/builtins/ImageBlock.js.map +1 -1
- package/dist/blocks/builtins/Jobs.d.ts +2 -2
- package/dist/blocks/builtins/Jobs.js +1 -1
- package/dist/blocks/builtins/Jobs.js.map +1 -1
- package/dist/blocks/builtins/Newsletter.js +18 -5
- package/dist/blocks/builtins/Newsletter.js.map +1 -1
- package/dist/blocks/builtins/Pricing.js +3 -1
- package/dist/blocks/builtins/Pricing.js.map +1 -1
- package/dist/blocks/builtins/Sections.js +4 -4
- package/dist/blocks/builtins/Sections.js.map +1 -1
- package/dist/blocks/builtins/Slider.js +9 -4
- package/dist/blocks/builtins/Slider.js.map +1 -1
- package/dist/blocks/builtins/SymbolRef.js.map +1 -1
- package/dist/blocks/builtins/Video.js +1 -1
- package/dist/blocks/builtins/Video.js.map +1 -1
- package/dist/blocks/builtins/Widgets.js +2 -1
- package/dist/blocks/builtins/Widgets.js.map +1 -1
- package/dist/components/PageView.js +4 -3
- package/dist/components/PageView.js.map +1 -1
- package/dist/demo/index.d.ts +7 -0
- package/dist/demo/index.js +23 -0
- package/dist/demo/index.js.map +1 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.js +0 -20
- package/dist/index.js.map +1 -1
- package/dist/modules/registry.d.ts +3 -4
- package/dist/modules/registry.js +2 -3
- package/dist/modules/registry.js.map +1 -1
- package/dist/schema/blocks.d.ts +27 -90
- package/dist/schema/blocks.js +4 -6
- package/dist/schema/blocks.js.map +1 -1
- package/dist/schema/settings.d.ts +1 -34
- package/dist/schema/settings.js +2 -8
- package/dist/schema/settings.js.map +1 -1
- package/dist/schema/siteConfig.d.ts +3 -71
- package/dist/schema/siteConfig.js +3 -8
- package/dist/schema/siteConfig.js.map +1 -1
- package/dist/schema/symbols.d.ts +2 -2
- package/dist/schema/symbols.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/pages.d.ts +23 -3
- package/dist/server/pages.js +37 -3
- package/dist/server/pages.js.map +1 -1
- package/dist/server/seo.js +5 -3
- package/dist/server/seo.js.map +1 -1
- package/dist/settings/useSiteSettings.d.ts +15 -3
- package/dist/settings/useSiteSettings.js +26 -5
- package/dist/settings/useSiteSettings.js.map +1 -1
- package/dist/styles.css +64 -9
- package/package.json +6 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/AuthProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from \"react\";\nimport {\n GoogleAuthProvider,\n onAuthStateChanged,\n signInWithEmailAndPassword,\n signInWithPopup,\n signOut as fbSignOut,\n type User,\n} from \"firebase/auth\";\nimport { getFirebaseAuth, isFirebaseConfigured } from \"../firebase/client\";\nimport { roleForSite, type Role, type XiClaims } from \"./roles\";\nimport { DEMO_EVENT, isDemoLoggedIn, setDemoLoggedIn } from \"../demo/demoStore\";\n\ninterface AuthState {\n /** Of Firebase überhaupt geconfigureerd is. */\n configured: boolean;\n /** Bezig met laden van de auth-status. */\n loading: boolean;\n user: User | null;\n claims: XiClaims | null;\n /** Effectieve rol op de huidige site (uit `siteId`). */\n role: Role | null;\n signIn: (email: string, password: string) => Promise<void>;\n /** Inloggen met Google (popup). */\n signInWithGoogle: () => Promise<void>;\n signOut: () => Promise<void>;\n /** Forceer het opnieuw inlezen van claims (na een rolwijziging). */\n refresh: () => Promise<void>;\n /** Demo-bewerkmodus actief (config.demoEditing) — pseudo-login, niets wordt opgeslagen. */\n demo: boolean;\n /** Pseudo-login voor de demo: geeft lokaal een siteadmin-rol zonder Firebase. */\n demoSignIn: () => void;\n}\n\nconst AuthContext = createContext<AuthState | null>(null);\n\nexport function AuthProvider({\n siteId,\n demo = false,\n children,\n}: {\n siteId: string;\n demo?: boolean;\n children: ReactNode;\n}) {\n const configured = isFirebaseConfigured();\n const [loading, setLoading] = useState(configured);\n const [user, setUser] = useState<User | null>(null);\n const [claims, setClaims] = useState<XiClaims | null>(null);\n const [demoLoggedIn, setDemoLoggedInState] = useState(false);\n\n // Demo-login uit de (sessie)opslag inlezen + meeluisteren met wijzigingen (login/logout/reset).\n useEffect(() => {\n if (!demo) return;\n const sync = () => setDemoLoggedInState(isDemoLoggedIn());\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }, [demo]);\n\n async function loadClaims(u: User | null) {\n if (!u) {\n setClaims(null);\n return;\n }\n const token = await u.getIdTokenResult(
|
|
1
|
+
{"version":3,"sources":["../../src/auth/AuthProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from \"react\";\nimport {\n GoogleAuthProvider,\n onAuthStateChanged,\n signInWithEmailAndPassword,\n signInWithPopup,\n signOut as fbSignOut,\n type User,\n} from \"firebase/auth\";\nimport { getFirebaseAuth, isFirebaseConfigured } from \"../firebase/client\";\nimport { roleForSite, type Role, type XiClaims } from \"./roles\";\nimport { DEMO_EVENT, isDemoLoggedIn, setDemoLoggedIn } from \"../demo/demoStore\";\n\ninterface AuthState {\n /** Of Firebase überhaupt geconfigureerd is. */\n configured: boolean;\n /** Bezig met laden van de auth-status. */\n loading: boolean;\n user: User | null;\n claims: XiClaims | null;\n /** Effectieve rol op de huidige site (uit `siteId`). */\n role: Role | null;\n signIn: (email: string, password: string) => Promise<void>;\n /** Inloggen met Google (popup). */\n signInWithGoogle: () => Promise<void>;\n signOut: () => Promise<void>;\n /** Forceer het opnieuw inlezen van claims (na een rolwijziging). */\n refresh: () => Promise<void>;\n /** Demo-bewerkmodus actief (config.demoEditing) — pseudo-login, niets wordt opgeslagen. */\n demo: boolean;\n /** Pseudo-login voor de demo: geeft lokaal een siteadmin-rol zonder Firebase. */\n demoSignIn: () => void;\n}\n\nconst AuthContext = createContext<AuthState | null>(null);\n\nexport function AuthProvider({\n siteId,\n demo = false,\n children,\n}: {\n siteId: string;\n demo?: boolean;\n children: ReactNode;\n}) {\n const configured = isFirebaseConfigured();\n const [loading, setLoading] = useState(configured);\n const [user, setUser] = useState<User | null>(null);\n const [claims, setClaims] = useState<XiClaims | null>(null);\n const [demoLoggedIn, setDemoLoggedInState] = useState(false);\n\n // Demo-login uit de (sessie)opslag inlezen + meeluisteren met wijzigingen (login/logout/reset).\n useEffect(() => {\n if (!demo) return;\n const sync = () => setDemoLoggedInState(isDemoLoggedIn());\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }, [demo]);\n\n async function loadClaims(u: User | null, forceRefresh = false) {\n if (!u) {\n setClaims(null);\n return;\n }\n // Standaard GEEN geforceerde token-refresh: dat is een netwerk-roundtrip op élke\n // auth-state-change (incl. eerste load van elke ingelogde editor). Alleen refresh() —\n // na een rolwijziging — forceert, want dan is het gecachte token per definitie verouderd.\n const token = await u.getIdTokenResult(forceRefresh);\n setClaims({\n superadmin: token.claims.superadmin === true,\n roles: (token.claims.roles as XiClaims[\"roles\"]) ?? {},\n });\n }\n\n useEffect(() => {\n const auth = getFirebaseAuth();\n if (!auth) {\n setLoading(false);\n return;\n }\n const unsub = onAuthStateChanged(auth, async (u) => {\n setUser(u);\n await loadClaims(u);\n setLoading(false);\n });\n return () => unsub();\n }, []);\n\n const value = useMemo<AuthState>(() => {\n // In de demo met actieve pseudo-login: doe alsof een siteadmin is ingelogd (lokaal, geen Firebase).\n const demoActiveLogin = demo && demoLoggedIn;\n const effectiveUser = demoActiveLogin\n ? ({ uid: \"demo\", email: \"demo@voorbeeld.nl\", displayName: \"Demo-beheerder\" } as unknown as User)\n : user;\n const effectiveClaims: XiClaims | null = demoActiveLogin ? { roles: { [siteId]: \"siteadmin\" } } : claims;\n\n return {\n configured,\n loading,\n user: effectiveUser,\n claims: effectiveClaims,\n role: roleForSite(effectiveClaims, siteId),\n async signIn(email, password) {\n const auth = getFirebaseAuth();\n if (!auth) throw new Error(\"Firebase is niet geconfigureerd.\");\n await signInWithEmailAndPassword(auth, email, password);\n },\n async signInWithGoogle() {\n const auth = getFirebaseAuth();\n if (!auth) throw new Error(\"Firebase is niet geconfigureerd.\");\n await signInWithPopup(auth, new GoogleAuthProvider());\n },\n async signOut() {\n if (demo) {\n setDemoLoggedIn(false);\n return;\n }\n const auth = getFirebaseAuth();\n if (auth) await fbSignOut(auth);\n },\n async refresh() {\n // Altijd de actuele Firebase-user (niet de gememoïseerde) — na een snelle\n // uitlog/inlog-wissel zou de closure anders claims van de vorige user verversen.\n // Force-refresh: na een rolwijziging is het gecachte token verouderd.\n await loadClaims(getFirebaseAuth()?.currentUser ?? user, true);\n },\n demo,\n demoSignIn() {\n setDemoLoggedIn(true);\n },\n };\n }, [configured, loading, user, claims, siteId, demo, demoLoggedIn]);\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\nexport function useAuth(): AuthState {\n const ctx = useContext(AuthContext);\n if (!ctx) throw new Error(\"useAuth moet binnen <XiCMSProvider> worden gebruikt.\");\n return ctx;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -10,7 +10,7 @@ function BookingRender({ block, mode }) {
|
|
|
10
10
|
if (!src) return null;
|
|
11
11
|
return /* @__PURE__ */ jsxs("section", { className: "xicms-booking", children: [
|
|
12
12
|
block.heading && /* @__PURE__ */ jsx(Heading, { level: 2, "data-xicms-edit": "heading", children: block.heading }),
|
|
13
|
-
/* @__PURE__ */ jsx("div", { className: "xicms-booking__frame", style: { height: `${block.height}px` }, children: /* @__PURE__ */ jsx(ConsentEmbed, { gated: block.consentGated, preview: mode === "preview", label: "Afspraak/booking", children: /* @__PURE__ */ jsx("iframe", { src, title: block.heading ?? "Afspraak maken", loading: "lazy", style: { border: 0, width: "100%", height: "100%" } }) }) })
|
|
13
|
+
/* @__PURE__ */ jsx("div", { className: "xicms-booking__frame", style: { height: `${block.height}px` }, children: /* @__PURE__ */ jsx(ConsentEmbed, { gated: block.consentGated, preview: mode === "preview", label: "Afspraak/booking", children: /* @__PURE__ */ jsx("iframe", { src, title: block.heading ?? "Afspraak maken", loading: "lazy", referrerPolicy: "no-referrer-when-downgrade", sandbox: "allow-scripts allow-same-origin allow-forms allow-popups", style: { border: 0, width: "100%", height: "100%" } }) }) })
|
|
14
14
|
] });
|
|
15
15
|
}
|
|
16
16
|
const bookingBlock = defineBlock({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/blocks/builtins/Booking.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { BookingBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { Heading } from \"../ui\";\nimport { ConsentEmbed } from \"./consentEmbed\";\nimport { buildBookingUrl, type BookingProvider } from \"./lib/bookingUrl\";\n\ntype Booking = z.infer<typeof BookingBlockSchema>;\n\n/** Boekings-/afspraak-embed (bv. Calendly). Consent-gated by default. De provider-keuze bouwt de\n * volledige embed-URL uit een geplakte link óf een kale slug/id (buildBookingUrl). */\nfunction BookingRender({ block, mode }: BlockRenderProps<Booking>) {\n const src = buildBookingUrl((block.provider ?? \"custom\") as BookingProvider, block.embedUrl);\n if (!src) return null;\n return (\n <section className=\"xicms-booking\">\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n <div className=\"xicms-booking__frame\" style={{ height: `${block.height}px` }}>\n <ConsentEmbed gated={block.consentGated} preview={mode === \"preview\"} label=\"Afspraak/booking\">\n <iframe src={src} title={block.heading ?? \"Afspraak maken\"} loading=\"lazy\" style={{ border: 0, width: \"100%\", height: \"100%\" }} />\n </ConsentEmbed>\n </div>\n </section>\n );\n}\n\nexport const bookingBlock = defineBlock({\n type: \"booking\",\n schema: BookingBlockSchema,\n Render: BookingRender,\n label: \"Afspraak/booking\",\n icon: \"📆\",\n defaults: () => ({ embedUrl: \"\", height: 630, consentGated: true }),\n});\n"],"mappings":";AAiBI,SACoB,KADpB;AAdJ,SAAS,0BAA0B;AACnC,SAAS,mBAA0C;AACnD,SAAS,eAAe;AACxB,SAAS,oBAAoB;AAC7B,SAAS,uBAA6C;AAMtD,SAAS,cAAc,EAAE,OAAO,KAAK,GAA8B;AACjE,QAAM,MAAM,gBAAiB,MAAM,YAAY,UAA8B,MAAM,QAAQ;AAC3F,MAAI,CAAC,IAAK,QAAO;AACjB,SACE,qBAAC,aAAQ,WAAU,iBAChB;AAAA,UAAM,WAAW,oBAAC,WAAQ,OAAO,GAAG,mBAAgB,WAAW,gBAAM,SAAQ;AAAA,IAC9E,oBAAC,SAAI,WAAU,wBAAuB,OAAO,EAAE,QAAQ,GAAG,MAAM,MAAM,KAAK,GACzE,8BAAC,gBAAa,OAAO,MAAM,cAAc,SAAS,SAAS,WAAW,OAAM,oBAC1E,8BAAC,YAAO,KAAU,OAAO,MAAM,WAAW,kBAAkB,SAAQ,QAAO,OAAO,EAAE,QAAQ,GAAG,OAAO,QAAQ,QAAQ,OAAO,GAAG,
|
|
1
|
+
{"version":3,"sources":["../../../src/blocks/builtins/Booking.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { BookingBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { Heading } from \"../ui\";\nimport { ConsentEmbed } from \"./consentEmbed\";\nimport { buildBookingUrl, type BookingProvider } from \"./lib/bookingUrl\";\n\ntype Booking = z.infer<typeof BookingBlockSchema>;\n\n/** Boekings-/afspraak-embed (bv. Calendly). Consent-gated by default. De provider-keuze bouwt de\n * volledige embed-URL uit een geplakte link óf een kale slug/id (buildBookingUrl). */\nfunction BookingRender({ block, mode }: BlockRenderProps<Booking>) {\n const src = buildBookingUrl((block.provider ?? \"custom\") as BookingProvider, block.embedUrl);\n if (!src) return null;\n return (\n <section className=\"xicms-booking\">\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n <div className=\"xicms-booking__frame\" style={{ height: `${block.height}px` }}>\n <ConsentEmbed gated={block.consentGated} preview={mode === \"preview\"} label=\"Afspraak/booking\">\n <iframe src={src} title={block.heading ?? \"Afspraak maken\"} loading=\"lazy\" referrerPolicy=\"no-referrer-when-downgrade\" sandbox=\"allow-scripts allow-same-origin allow-forms allow-popups\" style={{ border: 0, width: \"100%\", height: \"100%\" }} />\n </ConsentEmbed>\n </div>\n </section>\n );\n}\n\nexport const bookingBlock = defineBlock({\n type: \"booking\",\n schema: BookingBlockSchema,\n Render: BookingRender,\n label: \"Afspraak/booking\",\n icon: \"📆\",\n defaults: () => ({ embedUrl: \"\", height: 630, consentGated: true }),\n});\n"],"mappings":";AAiBI,SACoB,KADpB;AAdJ,SAAS,0BAA0B;AACnC,SAAS,mBAA0C;AACnD,SAAS,eAAe;AACxB,SAAS,oBAAoB;AAC7B,SAAS,uBAA6C;AAMtD,SAAS,cAAc,EAAE,OAAO,KAAK,GAA8B;AACjE,QAAM,MAAM,gBAAiB,MAAM,YAAY,UAA8B,MAAM,QAAQ;AAC3F,MAAI,CAAC,IAAK,QAAO;AACjB,SACE,qBAAC,aAAQ,WAAU,iBAChB;AAAA,UAAM,WAAW,oBAAC,WAAQ,OAAO,GAAG,mBAAgB,WAAW,gBAAM,SAAQ;AAAA,IAC9E,oBAAC,SAAI,WAAU,wBAAuB,OAAO,EAAE,QAAQ,GAAG,MAAM,MAAM,KAAK,GACzE,8BAAC,gBAAa,OAAO,MAAM,cAAc,SAAS,SAAS,WAAW,OAAM,oBAC1E,8BAAC,YAAO,KAAU,OAAO,MAAM,WAAW,kBAAkB,SAAQ,QAAO,gBAAe,8BAA6B,SAAQ,4DAA2D,OAAO,EAAE,QAAQ,GAAG,OAAO,QAAQ,QAAQ,OAAO,GAAG,GACjP,GACF;AAAA,KACF;AAEJ;AAEO,MAAM,eAAe,YAAY;AAAA,EACtC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,UAAU,IAAI,QAAQ,KAAK,cAAc,KAAK;AACnE,CAAC;","names":[]}
|
|
@@ -6,7 +6,7 @@ import { BlockRenderer } from "../BlockRenderer";
|
|
|
6
6
|
import { useRegistry } from "../context";
|
|
7
7
|
function ColumnsRender({ block, siteId, mode }) {
|
|
8
8
|
const registry = useRegistry();
|
|
9
|
-
return /* @__PURE__ */ jsx("div", { className: "xicms-columns", style: { gridTemplateColumns: `repeat(${block.columns.length}, 1fr)` }, children: block.columns.map((col, idx) => /* @__PURE__ */ jsx("div", { className: "xicms-column", children: /* @__PURE__ */ jsx(BlockRenderer, { blocks: col.blocks, registry, siteId, mode }) }, idx)) });
|
|
9
|
+
return /* @__PURE__ */ jsx("div", { className: "xicms-columns", "data-cols": block.columns.length, style: { gridTemplateColumns: `repeat(${block.columns.length}, 1fr)` }, children: block.columns.map((col, idx) => /* @__PURE__ */ jsx("div", { className: "xicms-column", children: /* @__PURE__ */ jsx(BlockRenderer, { blocks: col.blocks, registry, siteId, mode }) }, idx)) });
|
|
10
10
|
}
|
|
11
11
|
const columnsBlock = defineBlock({
|
|
12
12
|
type: "columns",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/blocks/builtins/Columns.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { ColumnsBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { BlockRenderer } from \"../BlockRenderer\";\nimport { useRegistry } from \"../context\";\n\ntype Columns = z.infer<typeof ColumnsBlockSchema>;\n\nfunction ColumnsRender({ block, siteId, mode }: BlockRenderProps<Columns>) {\n const registry = useRegistry();\n return (\n <div className=\"xicms-columns\" style={{ gridTemplateColumns: `repeat(${block.columns.length}, 1fr)` }}>\n {block.columns.map((col, idx) => (\n <div key={idx} className=\"xicms-column\">\n <BlockRenderer blocks={col.blocks} registry={registry} siteId={siteId} mode={mode} />\n </div>\n ))}\n </div>\n );\n}\n\nexport const columnsBlock = defineBlock({\n type: \"columns\",\n schema: ColumnsBlockSchema,\n Render: ColumnsRender,\n label: \"Kolommen\",\n icon: \"▥\",\n defaults: () => ({ columns: [{ blocks: [] }, { blocks: [] }] }),\n});\n"],"mappings":";AAgBU;AAbV,SAAS,0BAA0B;AACnC,SAAS,mBAA0C;AACnD,SAAS,qBAAqB;AAC9B,SAAS,mBAAmB;AAI5B,SAAS,cAAc,EAAE,OAAO,QAAQ,KAAK,GAA8B;AACzE,QAAM,WAAW,YAAY;AAC7B,SACE,oBAAC,SAAI,WAAU,iBAAgB,OAAO,EAAE,qBAAqB,UAAU,MAAM,QAAQ,MAAM,SAAS,
|
|
1
|
+
{"version":3,"sources":["../../../src/blocks/builtins/Columns.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { ColumnsBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { BlockRenderer } from \"../BlockRenderer\";\nimport { useRegistry } from \"../context\";\n\ntype Columns = z.infer<typeof ColumnsBlockSchema>;\n\nfunction ColumnsRender({ block, siteId, mode }: BlockRenderProps<Columns>) {\n const registry = useRegistry();\n return (\n <div className=\"xicms-columns\" data-cols={block.columns.length} style={{ gridTemplateColumns: `repeat(${block.columns.length}, 1fr)` }}>\n {block.columns.map((col, idx) => (\n <div key={idx} className=\"xicms-column\">\n <BlockRenderer blocks={col.blocks} registry={registry} siteId={siteId} mode={mode} />\n </div>\n ))}\n </div>\n );\n}\n\nexport const columnsBlock = defineBlock({\n type: \"columns\",\n schema: ColumnsBlockSchema,\n Render: ColumnsRender,\n label: \"Kolommen\",\n icon: \"▥\",\n defaults: () => ({ columns: [{ blocks: [] }, { blocks: [] }] }),\n});\n"],"mappings":";AAgBU;AAbV,SAAS,0BAA0B;AACnC,SAAS,mBAA0C;AACnD,SAAS,qBAAqB;AAC9B,SAAS,mBAAmB;AAI5B,SAAS,cAAc,EAAE,OAAO,QAAQ,KAAK,GAA8B;AACzE,QAAM,WAAW,YAAY;AAC7B,SACE,oBAAC,SAAI,WAAU,iBAAgB,aAAW,MAAM,QAAQ,QAAQ,OAAO,EAAE,qBAAqB,UAAU,MAAM,QAAQ,MAAM,SAAS,GAClI,gBAAM,QAAQ,IAAI,CAAC,KAAK,QACvB,oBAAC,SAAc,WAAU,gBACvB,8BAAC,iBAAc,QAAQ,IAAI,QAAQ,UAAoB,QAAgB,MAAY,KAD3E,GAEV,CACD,GACH;AAEJ;AAEO,MAAM,eAAe,YAAY;AAAA,EACtC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,SAAS,CAAC,EAAE,QAAQ,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC,EAAE;AAC/D,CAAC;","names":[]}
|
|
@@ -12,6 +12,8 @@ function EmbedRender({ block, mode }) {
|
|
|
12
12
|
src: block.url,
|
|
13
13
|
title: block.title ?? "Embed",
|
|
14
14
|
loading: "lazy",
|
|
15
|
+
referrerPolicy: "no-referrer-when-downgrade",
|
|
16
|
+
sandbox: "allow-scripts allow-same-origin allow-forms allow-popups",
|
|
15
17
|
style: { border: 0, width: "100%", height: "100%" }
|
|
16
18
|
}
|
|
17
19
|
) }) });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/blocks/builtins/Embed.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { EmbedBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { ConsentEmbed } from \"./consentEmbed\";\n\ntype Embed = z.infer<typeof EmbedBlockSchema>;\n\n/** Generieke responsive iframe-embed (consent-gated by default) — voor wat dan ook met een URL. */\nfunction EmbedRender({ block, mode }: BlockRenderProps<Embed>) {\n if (!block.url) return null;\n // \"fixed\" = vaste hoogte (bestaand gedrag); anders schaalt de embed mee met de beeldverhouding.\n const sizing =\n block.aspectRatio && block.aspectRatio !== \"fixed\"\n ? { aspectRatio: block.aspectRatio.replace(\"/\", \" / \") }\n : { height: `${block.height}px` };\n return (\n <div className=\"xicms-embed\" style={sizing}>\n <ConsentEmbed gated={block.consentGated} preview={mode === \"preview\"} label={block.title ?? \"Embed\"}>\n <iframe\n src={block.url}\n title={block.title ?? \"Embed\"}\n loading=\"lazy\"\n style={{ border: 0, width: \"100%\", height: \"100%\" }}\n />\n </ConsentEmbed>\n </div>\n );\n}\n\nexport const embedBlock = defineBlock({\n type: \"embed\",\n schema: EmbedBlockSchema,\n Render: EmbedRender,\n label: \"Embed (iframe)\",\n icon: \"🧩\",\n defaults: () => ({ url: \"\", height: 420, consentGated: true }),\n});\n"],"mappings":";AAoBQ;AAjBR,SAAS,wBAAwB;AACjC,SAAS,mBAA0C;AACnD,SAAS,oBAAoB;AAK7B,SAAS,YAAY,EAAE,OAAO,KAAK,GAA4B;AAC7D,MAAI,CAAC,MAAM,IAAK,QAAO;AAEvB,QAAM,SACJ,MAAM,eAAe,MAAM,gBAAgB,UACvC,EAAE,aAAa,MAAM,YAAY,QAAQ,KAAK,KAAK,EAAE,IACrD,EAAE,QAAQ,GAAG,MAAM,MAAM,KAAK;AACpC,SACE,oBAAC,SAAI,WAAU,eAAc,OAAO,QAClC,8BAAC,gBAAa,OAAO,MAAM,cAAc,SAAS,SAAS,WAAW,OAAO,MAAM,SAAS,SAC1F;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,MAAM;AAAA,MACX,OAAO,MAAM,SAAS;AAAA,MACtB,SAAQ;AAAA,MACR,OAAO,EAAE,QAAQ,GAAG,OAAO,QAAQ,QAAQ,OAAO;AAAA;AAAA,EACpD,GACF,GACF;AAEJ;AAEO,MAAM,aAAa,YAAY;AAAA,EACpC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,KAAK,IAAI,QAAQ,KAAK,cAAc,KAAK;AAC9D,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/blocks/builtins/Embed.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { EmbedBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { ConsentEmbed } from \"./consentEmbed\";\n\ntype Embed = z.infer<typeof EmbedBlockSchema>;\n\n/** Generieke responsive iframe-embed (consent-gated by default) — voor wat dan ook met een URL. */\nfunction EmbedRender({ block, mode }: BlockRenderProps<Embed>) {\n if (!block.url) return null;\n // \"fixed\" = vaste hoogte (bestaand gedrag); anders schaalt de embed mee met de beeldverhouding.\n const sizing =\n block.aspectRatio && block.aspectRatio !== \"fixed\"\n ? { aspectRatio: block.aspectRatio.replace(\"/\", \" / \") }\n : { height: `${block.height}px` };\n return (\n <div className=\"xicms-embed\" style={sizing}>\n <ConsentEmbed gated={block.consentGated} preview={mode === \"preview\"} label={block.title ?? \"Embed\"}>\n <iframe\n src={block.url}\n title={block.title ?? \"Embed\"}\n loading=\"lazy\"\n referrerPolicy=\"no-referrer-when-downgrade\"\n sandbox=\"allow-scripts allow-same-origin allow-forms allow-popups\"\n style={{ border: 0, width: \"100%\", height: \"100%\" }}\n />\n </ConsentEmbed>\n </div>\n );\n}\n\nexport const embedBlock = defineBlock({\n type: \"embed\",\n schema: EmbedBlockSchema,\n Render: EmbedRender,\n label: \"Embed (iframe)\",\n icon: \"🧩\",\n defaults: () => ({ url: \"\", height: 420, consentGated: true }),\n});\n"],"mappings":";AAoBQ;AAjBR,SAAS,wBAAwB;AACjC,SAAS,mBAA0C;AACnD,SAAS,oBAAoB;AAK7B,SAAS,YAAY,EAAE,OAAO,KAAK,GAA4B;AAC7D,MAAI,CAAC,MAAM,IAAK,QAAO;AAEvB,QAAM,SACJ,MAAM,eAAe,MAAM,gBAAgB,UACvC,EAAE,aAAa,MAAM,YAAY,QAAQ,KAAK,KAAK,EAAE,IACrD,EAAE,QAAQ,GAAG,MAAM,MAAM,KAAK;AACpC,SACE,oBAAC,SAAI,WAAU,eAAc,OAAO,QAClC,8BAAC,gBAAa,OAAO,MAAM,cAAc,SAAS,SAAS,WAAW,OAAO,MAAM,SAAS,SAC1F;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,MAAM;AAAA,MACX,OAAO,MAAM,SAAS;AAAA,MACtB,SAAQ;AAAA,MACR,gBAAe;AAAA,MACf,SAAQ;AAAA,MACR,OAAO,EAAE,QAAQ,GAAG,OAAO,QAAQ,QAAQ,OAAO;AAAA;AAAA,EACpD,GACF,GACF;AAEJ;AAEO,MAAM,aAAa,YAAY;AAAA,EACpC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,KAAK,IAAI,QAAQ,KAAK,cAAc,KAAK;AAC9D,CAAC;","names":[]}
|
|
@@ -109,7 +109,7 @@ function LightboxOverlay({
|
|
|
109
109
|
}
|
|
110
110
|
function GalleryRender({ block }) {
|
|
111
111
|
const settings = useModuleSettings("gallery");
|
|
112
|
-
const allItems = (block.items ?? []).filter((i) => i.url);
|
|
112
|
+
const allItems = (block.items ?? []).map((it, _i) => ({ ...it, _i })).filter((i) => i.url);
|
|
113
113
|
const [open, setOpen] = useState(false);
|
|
114
114
|
const [index, setIndex] = useState(0);
|
|
115
115
|
const [activeTag, setActiveTag] = useState(null);
|
|
@@ -157,7 +157,7 @@ function GalleryRender({ block }) {
|
|
|
157
157
|
children: /* @__PURE__ */ jsx(Img, { src: i.url, alt: i.alt ?? "", width: 600, height: 600, sizes: "(max-width: 640px) 50vw, 280px", style: imgStyle })
|
|
158
158
|
}
|
|
159
159
|
) : /* @__PURE__ */ jsx(Img, { src: i.url, alt: i.alt ?? "", width: 600, height: 600, sizes: "(max-width: 640px) 50vw, 280px", style: imgStyle }),
|
|
160
|
-
i.caption && /* @__PURE__ */ jsx("figcaption", { className: "xicms-gallery__caption", style: { marginTop: "0.4rem", fontSize: "0.85rem", color: "var(--color-text-secondary)" }, children: i.caption })
|
|
160
|
+
i.caption && /* @__PURE__ */ jsx("figcaption", { className: "xicms-gallery__caption", "data-xicms-edit": `items.${i._i}.caption`, style: { marginTop: "0.4rem", fontSize: "0.85rem", color: "var(--color-text-secondary)" }, children: i.caption })
|
|
161
161
|
] }, idx)) }),
|
|
162
162
|
lightbox && open && /* @__PURE__ */ jsx(LightboxOverlay, { items, index, onIndex: setIndex, onClose: close })
|
|
163
163
|
] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/blocks/builtins/Gallery.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type { z } from \"zod\";\nimport { GalleryBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { useModuleSettings } from \"../../modules/context\";\nimport { Grid } from \"../ui\";\nimport { Img } from \"../../media/ImageContext\";\n\ntype Gallery = z.infer<typeof GalleryBlockSchema>;\ntype Item = Gallery[\"items\"][number];\n\n/** Per-site instelbaar via `modules.gallery.config` (door de agent of in /beheer). */\nexport interface GallerySettings {\n columns?: number;\n lightbox?: boolean;\n}\n\n/**\n * Eigen lightbox-overlay: rendert via een portal naar `document.body` (zo raakt geen enkele\n * ancestor-transform — bv. de scroll-reveal — de full-screen positionering). Sluit op klik náást\n * de foto én op Escape; vorige/volgende met teller; herstelt focus zónder de pagina te laten\n * verspringen (`preventScroll`) en zet de body-scroll vast zolang hij open is.\n */\nexport function LightboxOverlay({\n items,\n index,\n onIndex,\n onClose,\n}: {\n items: Item[];\n index: number;\n onIndex: (i: number) => void;\n onClose: () => void;\n}) {\n const it = items[index];\n const total = items.length;\n const overlayRef = useRef<HTMLDivElement | null>(null);\n const prev = useCallback(() => onIndex((index - 1 + total) % total), [index, total, onIndex]);\n const next = useCallback(() => onIndex((index + 1) % total), [index, total, onIndex]);\n // Swipe (pointer events, werkt voor touch én muis-sleep): horizontaal > 40px = vorige/volgende.\n const swipeStart = useRef<number | null>(null);\n const onPointerDown = (e: React.PointerEvent) => {\n swipeStart.current = e.clientX;\n };\n const onPointerUp = (e: React.PointerEvent) => {\n if (swipeStart.current === null || total < 2) return;\n const dx = e.clientX - swipeStart.current;\n swipeStart.current = null;\n if (dx > 40) prev();\n else if (dx < -40) next();\n };\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n else if (e.key === \"ArrowLeft\") prev();\n else if (e.key === \"ArrowRight\") next();\n else if (e.key === \"Tab\") {\n // Focus-trap (aria-modal): Tab cirkelt binnen de dialog i.p.v. naar de pagina eronder.\n const el = overlayRef.current;\n const focusables = el ? [...el.querySelectorAll<HTMLElement>(\"button, a[href]\")] : [];\n if (focusables.length === 0) return;\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const active = document.activeElement;\n if (e.shiftKey && (active === first || !el?.contains(active))) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && (active === last || !el?.contains(active))) {\n e.preventDefault();\n first.focus();\n }\n }\n };\n document.addEventListener(\"keydown\", onKey);\n const prevOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\"; // scroll-lock terwijl open\n // Initiële focus in de dialog (dialog-conventie; toetsenbord/screenreader landt niet \"achter\" de overlay).\n overlayRef.current?.querySelector<HTMLElement>(\".xicms-lightbox__close\")?.focus({ preventScroll: true });\n return () => {\n document.removeEventListener(\"keydown\", onKey);\n document.body.style.overflow = prevOverflow;\n };\n }, [onClose, prev, next]);\n\n if (typeof document === \"undefined\") return null;\n return createPortal(\n <div\n ref={overlayRef}\n className=\"xicms-lightbox\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={it.alt || it.caption || \"Foto\"}\n onClick={onClose}\n >\n <button type=\"button\" className=\"xicms-lightbox__close\" aria-label=\"Sluiten\" onClick={onClose}>✕</button>\n {total > 1 && (\n <>\n <button type=\"button\" className=\"xicms-lightbox__nav xicms-lightbox__nav--prev\" aria-label=\"Vorige\" onClick={(e) => { e.stopPropagation(); prev(); }}>‹</button>\n <button type=\"button\" className=\"xicms-lightbox__nav xicms-lightbox__nav--next\" aria-label=\"Volgende\" onClick={(e) => { e.stopPropagation(); next(); }}>›</button>\n <span className=\"xicms-lightbox__counter\">{index + 1} / {total}</span>\n </>\n )}\n {/* Klik op de foto/onderschrift sluit NIET (stopPropagation); klik op de rand wel.\n touchAction pan-y: verticaal scrollen blijft van de browser, horizontaal is swipen. */}\n <figure\n className=\"xicms-lightbox__figure\"\n onClick={(e) => e.stopPropagation()}\n onPointerDown={onPointerDown}\n onPointerUp={onPointerUp}\n style={{ touchAction: \"pan-y\" }}\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={it.url} alt={it.alt ?? \"\"} />\n {it.caption && <figcaption>{it.caption}</figcaption>}\n </figure>\n </div>,\n document.body,\n );\n}\n\n/** Galerij — responsive grid met instelbare beeldverhouding en optionele lightbox. */\nfunction GalleryRender({ block }: BlockRenderProps<Gallery>) {\n const settings = useModuleSettings<GallerySettings>(\"gallery\");\n const allItems = (block.items ?? []).filter((i) => i.url);\n const [open, setOpen] = useState(false);\n const [index, setIndex] = useState(0);\n const [activeTag, setActiveTag] = useState<string | null>(null);\n const triggerRef = useRef<HTMLButtonElement | null>(null);\n\n // Filterbalk op item-tags (opt-in): lightbox-navigatie volgt de gefilterde lijst.\n const tags = block.showFilter ? [...new Set(allItems.map((i) => i.tag).filter((t): t is string => Boolean(t)))] : [];\n const items = activeTag ? allItems.filter((i) => i.tag === activeTag) : allItems;\n\n if (allItems.length === 0) return null;\n\n const cols = block.columns && block.columns > 0 ? block.columns : settings.columns ?? 3;\n const lightbox =\n block.lightbox === \"on\" ? true : block.lightbox === \"off\" ? false : settings.lightbox ?? true;\n const ar = block.aspectRatio ?? \"1/1\";\n\n const imgStyle = {\n width: \"100%\",\n aspectRatio: ar === \"auto\" ? undefined : ar.replace(\"/\", \" / \"),\n objectFit: \"cover\" as const,\n borderRadius: \"var(--radius-element)\",\n display: \"block\",\n };\n\n const close = () => {\n setOpen(false);\n triggerRef.current?.focus({ preventScroll: true }); // geen page-jump bij sluiten\n };\n\n return (\n <div className=\"xicms-gallery\">\n {tags.length > 0 && (\n <div className=\"xicms-gallery__filter\" role=\"group\" aria-label=\"Filter op categorie\">\n <button type=\"button\" className=\"xicms-gallery__filterbtn\" aria-pressed={activeTag === null} data-active={activeTag === null ? \"1\" : \"0\"} onClick={() => { setActiveTag(null); setIndex(0); }}>\n Alles\n </button>\n {tags.map((t) => (\n <button key={t} type=\"button\" className=\"xicms-gallery__filterbtn\" aria-pressed={activeTag === t} data-active={activeTag === t ? \"1\" : \"0\"} onClick={() => { setActiveTag(t); setIndex(0); }}>\n {t}\n </button>\n ))}\n </div>\n )}\n <Grid columns={{ minWidth: 160, max: Math.min(cols, 6) }} gap={2}>\n {items.map((i, idx) => (\n <figure key={idx} style={{ margin: 0 }} className=\"xicms-gallery__item\">\n {lightbox ? (\n <button\n type=\"button\"\n onClick={(e) => { triggerRef.current = e.currentTarget; setIndex(idx); setOpen(true); }}\n aria-label={i.alt || i.caption || `Foto ${idx + 1} vergroten`}\n style={{ display: \"block\", width: \"100%\", padding: 0, border: 0, background: \"none\", cursor: \"zoom-in\" }}\n >\n <Img src={i.url as string} alt={i.alt ?? \"\"} width={600} height={600} sizes=\"(max-width: 640px) 50vw, 280px\" style={imgStyle} />\n </button>\n ) : (\n <Img src={i.url as string} alt={i.alt ?? \"\"} width={600} height={600} sizes=\"(max-width: 640px) 50vw, 280px\" style={imgStyle} />\n )}\n {i.caption && (\n <figcaption className=\"xicms-gallery__caption\" style={{ marginTop: \"0.4rem\", fontSize: \"0.85rem\", color: \"var(--color-text-secondary)\" }}>\n {i.caption}\n </figcaption>\n )}\n </figure>\n ))}\n </Grid>\n\n {lightbox && open && (\n <LightboxOverlay items={items} index={index} onIndex={setIndex} onClose={close} />\n )}\n </div>\n );\n}\n\nexport const galleryBlock = defineBlock({\n type: \"gallery\",\n schema: GalleryBlockSchema,\n Render: GalleryRender,\n label: \"Galerij\",\n icon: \"🖼\",\n defaults: () => ({ items: [], columns: 0, lightbox: \"inherit\", aspectRatio: \"1/1\" }),\n});\n"],"mappings":";AAkGM,SAEE,UAFF,KAKI,YALJ;AAhGN,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AACzD,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,mBAA0C;AACnD,SAAS,yBAAyB;AAClC,SAAS,YAAY;AACrB,SAAS,WAAW;AAiBb,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,KAAK,MAAM,KAAK;AACtB,QAAM,QAAQ,MAAM;AACpB,QAAM,aAAa,OAA8B,IAAI;AACrD,QAAM,OAAO,YAAY,MAAM,SAAS,QAAQ,IAAI,SAAS,KAAK,GAAG,CAAC,OAAO,OAAO,OAAO,CAAC;AAC5F,QAAM,OAAO,YAAY,MAAM,SAAS,QAAQ,KAAK,KAAK,GAAG,CAAC,OAAO,OAAO,OAAO,CAAC;AAEpF,QAAM,aAAa,OAAsB,IAAI;AAC7C,QAAM,gBAAgB,CAAC,MAA0B;AAC/C,eAAW,UAAU,EAAE;AAAA,EACzB;AACA,QAAM,cAAc,CAAC,MAA0B;AAC7C,QAAI,WAAW,YAAY,QAAQ,QAAQ,EAAG;AAC9C,UAAM,KAAK,EAAE,UAAU,WAAW;AAClC,eAAW,UAAU;AACrB,QAAI,KAAK,GAAI,MAAK;AAAA,aACT,KAAK,IAAK,MAAK;AAAA,EAC1B;AAEA,YAAU,MAAM;AACd,UAAM,QAAQ,CAAC,MAAqB;AAClC,UAAI,EAAE,QAAQ,SAAU,SAAQ;AAAA,eACvB,EAAE,QAAQ,YAAa,MAAK;AAAA,eAC5B,EAAE,QAAQ,aAAc,MAAK;AAAA,eAC7B,EAAE,QAAQ,OAAO;AAExB,cAAM,KAAK,WAAW;AACtB,cAAM,aAAa,KAAK,CAAC,GAAG,GAAG,iBAA8B,iBAAiB,CAAC,IAAI,CAAC;AACpF,YAAI,WAAW,WAAW,EAAG;AAC7B,cAAM,QAAQ,WAAW,CAAC;AAC1B,cAAM,OAAO,WAAW,WAAW,SAAS,CAAC;AAC7C,cAAM,SAAS,SAAS;AACxB,YAAI,EAAE,aAAa,WAAW,SAAS,CAAC,IAAI,SAAS,MAAM,IAAI;AAC7D,YAAE,eAAe;AACjB,eAAK,MAAM;AAAA,QACb,WAAW,CAAC,EAAE,aAAa,WAAW,QAAQ,CAAC,IAAI,SAAS,MAAM,IAAI;AACpE,YAAE,eAAe;AACjB,gBAAM,MAAM;AAAA,QACd;AAAA,MACF;AAAA,IACF;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,UAAM,eAAe,SAAS,KAAK,MAAM;AACzC,aAAS,KAAK,MAAM,WAAW;AAE/B,eAAW,SAAS,cAA2B,wBAAwB,GAAG,MAAM,EAAE,eAAe,KAAK,CAAC;AACvG,WAAO,MAAM;AACX,eAAS,oBAAoB,WAAW,KAAK;AAC7C,eAAS,KAAK,MAAM,WAAW;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,SAAS,MAAM,IAAI,CAAC;AAExB,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,SAAO;AAAA,IACL;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,WAAU;AAAA,QACV,MAAK;AAAA,QACL,cAAW;AAAA,QACX,cAAY,GAAG,OAAO,GAAG,WAAW;AAAA,QACpC,SAAS;AAAA,QAET;AAAA,8BAAC,YAAO,MAAK,UAAS,WAAU,yBAAwB,cAAW,WAAU,SAAS,SAAS,oBAAC;AAAA,UAC/F,QAAQ,KACP,iCACE;AAAA,gCAAC,YAAO,MAAK,UAAS,WAAU,iDAAgD,cAAW,UAAS,SAAS,CAAC,MAAM;AAAE,gBAAE,gBAAgB;AAAG,mBAAK;AAAA,YAAG,GAAG,oBAAC;AAAA,YACvJ,oBAAC,YAAO,MAAK,UAAS,WAAU,iDAAgD,cAAW,YAAW,SAAS,CAAC,MAAM;AAAE,gBAAE,gBAAgB;AAAG,mBAAK;AAAA,YAAG,GAAG,oBAAC;AAAA,YACzJ,qBAAC,UAAK,WAAU,2BAA2B;AAAA,sBAAQ;AAAA,cAAE;AAAA,cAAI;AAAA,eAAM;AAAA,aACjE;AAAA,UAIF;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,SAAS,CAAC,MAAM,EAAE,gBAAgB;AAAA,cAClC;AAAA,cACA;AAAA,cACA,OAAO,EAAE,aAAa,QAAQ;AAAA,cAG9B;AAAA,oCAAC,SAAI,KAAK,GAAG,KAAK,KAAK,GAAG,OAAO,IAAI;AAAA,gBACpC,GAAG,WAAW,oBAAC,gBAAY,aAAG,SAAQ;AAAA;AAAA;AAAA,UACzC;AAAA;AAAA;AAAA,IACF;AAAA,IACA,SAAS;AAAA,EACX;AACF;AAGA,SAAS,cAAc,EAAE,MAAM,GAA8B;AAC3D,QAAM,WAAW,kBAAmC,SAAS;AAC7D,QAAM,YAAY,MAAM,SAAS,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG;AACxD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,CAAC;AACpC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAwB,IAAI;AAC9D,QAAM,aAAa,OAAiC,IAAI;AAGxD,QAAM,OAAO,MAAM,aAAa,CAAC,GAAG,IAAI,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,MAAmB,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACnH,QAAM,QAAQ,YAAY,SAAS,OAAO,CAAC,MAAM,EAAE,QAAQ,SAAS,IAAI;AAExE,MAAI,SAAS,WAAW,EAAG,QAAO;AAElC,QAAM,OAAO,MAAM,WAAW,MAAM,UAAU,IAAI,MAAM,UAAU,SAAS,WAAW;AACtF,QAAM,WACJ,MAAM,aAAa,OAAO,OAAO,MAAM,aAAa,QAAQ,QAAQ,SAAS,YAAY;AAC3F,QAAM,KAAK,MAAM,eAAe;AAEhC,QAAM,WAAW;AAAA,IACf,OAAO;AAAA,IACP,aAAa,OAAO,SAAS,SAAY,GAAG,QAAQ,KAAK,KAAK;AAAA,IAC9D,WAAW;AAAA,IACX,cAAc;AAAA,IACd,SAAS;AAAA,EACX;AAEA,QAAM,QAAQ,MAAM;AAClB,YAAQ,KAAK;AACb,eAAW,SAAS,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,EACnD;AAEA,SACE,qBAAC,SAAI,WAAU,iBACZ;AAAA,SAAK,SAAS,KACb,qBAAC,SAAI,WAAU,yBAAwB,MAAK,SAAQ,cAAW,uBAC7D;AAAA,0BAAC,YAAO,MAAK,UAAS,WAAU,4BAA2B,gBAAc,cAAc,MAAM,eAAa,cAAc,OAAO,MAAM,KAAK,SAAS,MAAM;AAAE,qBAAa,IAAI;AAAG,iBAAS,CAAC;AAAA,MAAG,GAAG,mBAE/L;AAAA,MACC,KAAK,IAAI,CAAC,MACT,oBAAC,YAAe,MAAK,UAAS,WAAU,4BAA2B,gBAAc,cAAc,GAAG,eAAa,cAAc,IAAI,MAAM,KAAK,SAAS,MAAM;AAAE,qBAAa,CAAC;AAAG,iBAAS,CAAC;AAAA,MAAG,GACxL,eADU,CAEb,CACD;AAAA,OACH;AAAA,IAEF,oBAAC,QAAK,SAAS,EAAE,UAAU,KAAK,KAAK,KAAK,IAAI,MAAM,CAAC,EAAE,GAAG,KAAK,GAC5D,gBAAM,IAAI,CAAC,GAAG,QACb,qBAAC,YAAiB,OAAO,EAAE,QAAQ,EAAE,GAAG,WAAU,uBAC/C;AAAA,iBACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,CAAC,MAAM;AAAE,uBAAW,UAAU,EAAE;AAAe,qBAAS,GAAG;AAAG,oBAAQ,IAAI;AAAA,UAAG;AAAA,UACtF,cAAY,EAAE,OAAO,EAAE,WAAW,QAAQ,MAAM,CAAC;AAAA,UACjD,OAAO,EAAE,SAAS,SAAS,OAAO,QAAQ,SAAS,GAAG,QAAQ,GAAG,YAAY,QAAQ,QAAQ,UAAU;AAAA,UAEvG,8BAAC,OAAI,KAAK,EAAE,KAAe,KAAK,EAAE,OAAO,IAAI,OAAO,KAAK,QAAQ,KAAK,OAAM,kCAAiC,OAAO,UAAU;AAAA;AAAA,MAChI,IAEA,oBAAC,OAAI,KAAK,EAAE,KAAe,KAAK,EAAE,OAAO,IAAI,OAAO,KAAK,QAAQ,KAAK,OAAM,kCAAiC,OAAO,UAAU;AAAA,MAE/H,EAAE,WACD,oBAAC,gBAAW,WAAU,0BAAyB,OAAO,EAAE,WAAW,UAAU,UAAU,WAAW,OAAO,8BAA8B,GACpI,YAAE,SACL;AAAA,SAhBS,GAkBb,CACD,GACH;AAAA,IAEC,YAAY,QACX,oBAAC,mBAAgB,OAAc,OAAc,SAAS,UAAU,SAAS,OAAO;AAAA,KAEpF;AAEJ;AAEO,MAAM,eAAe,YAAY;AAAA,EACtC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,UAAU,WAAW,aAAa,MAAM;AACpF,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/blocks/builtins/Gallery.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type { z } from \"zod\";\nimport { GalleryBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { useModuleSettings } from \"../../modules/context\";\nimport { Grid } from \"../ui\";\nimport { Img } from \"../../media/ImageContext\";\n\ntype Gallery = z.infer<typeof GalleryBlockSchema>;\ntype Item = Gallery[\"items\"][number];\n\n/** Per-site instelbaar via `modules.gallery.config` (door de agent of in /beheer). */\nexport interface GallerySettings {\n columns?: number;\n lightbox?: boolean;\n}\n\n/**\n * Eigen lightbox-overlay: rendert via een portal naar `document.body` (zo raakt geen enkele\n * ancestor-transform — bv. de scroll-reveal — de full-screen positionering). Sluit op klik náást\n * de foto én op Escape; vorige/volgende met teller; herstelt focus zónder de pagina te laten\n * verspringen (`preventScroll`) en zet de body-scroll vast zolang hij open is.\n */\nexport function LightboxOverlay({\n items,\n index,\n onIndex,\n onClose,\n}: {\n items: Item[];\n index: number;\n onIndex: (i: number) => void;\n onClose: () => void;\n}) {\n const it = items[index];\n const total = items.length;\n const overlayRef = useRef<HTMLDivElement | null>(null);\n const prev = useCallback(() => onIndex((index - 1 + total) % total), [index, total, onIndex]);\n const next = useCallback(() => onIndex((index + 1) % total), [index, total, onIndex]);\n // Swipe (pointer events, werkt voor touch én muis-sleep): horizontaal > 40px = vorige/volgende.\n const swipeStart = useRef<number | null>(null);\n const onPointerDown = (e: React.PointerEvent) => {\n swipeStart.current = e.clientX;\n };\n const onPointerUp = (e: React.PointerEvent) => {\n if (swipeStart.current === null || total < 2) return;\n const dx = e.clientX - swipeStart.current;\n swipeStart.current = null;\n if (dx > 40) prev();\n else if (dx < -40) next();\n };\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n else if (e.key === \"ArrowLeft\") prev();\n else if (e.key === \"ArrowRight\") next();\n else if (e.key === \"Tab\") {\n // Focus-trap (aria-modal): Tab cirkelt binnen de dialog i.p.v. naar de pagina eronder.\n const el = overlayRef.current;\n const focusables = el ? [...el.querySelectorAll<HTMLElement>(\"button, a[href]\")] : [];\n if (focusables.length === 0) return;\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const active = document.activeElement;\n if (e.shiftKey && (active === first || !el?.contains(active))) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && (active === last || !el?.contains(active))) {\n e.preventDefault();\n first.focus();\n }\n }\n };\n document.addEventListener(\"keydown\", onKey);\n const prevOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\"; // scroll-lock terwijl open\n // Initiële focus in de dialog (dialog-conventie; toetsenbord/screenreader landt niet \"achter\" de overlay).\n overlayRef.current?.querySelector<HTMLElement>(\".xicms-lightbox__close\")?.focus({ preventScroll: true });\n return () => {\n document.removeEventListener(\"keydown\", onKey);\n document.body.style.overflow = prevOverflow;\n };\n }, [onClose, prev, next]);\n\n if (typeof document === \"undefined\") return null;\n return createPortal(\n <div\n ref={overlayRef}\n className=\"xicms-lightbox\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={it.alt || it.caption || \"Foto\"}\n onClick={onClose}\n >\n <button type=\"button\" className=\"xicms-lightbox__close\" aria-label=\"Sluiten\" onClick={onClose}>✕</button>\n {total > 1 && (\n <>\n <button type=\"button\" className=\"xicms-lightbox__nav xicms-lightbox__nav--prev\" aria-label=\"Vorige\" onClick={(e) => { e.stopPropagation(); prev(); }}>‹</button>\n <button type=\"button\" className=\"xicms-lightbox__nav xicms-lightbox__nav--next\" aria-label=\"Volgende\" onClick={(e) => { e.stopPropagation(); next(); }}>›</button>\n <span className=\"xicms-lightbox__counter\">{index + 1} / {total}</span>\n </>\n )}\n {/* Klik op de foto/onderschrift sluit NIET (stopPropagation); klik op de rand wel.\n touchAction pan-y: verticaal scrollen blijft van de browser, horizontaal is swipen. */}\n <figure\n className=\"xicms-lightbox__figure\"\n onClick={(e) => e.stopPropagation()}\n onPointerDown={onPointerDown}\n onPointerUp={onPointerUp}\n style={{ touchAction: \"pan-y\" }}\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={it.url} alt={it.alt ?? \"\"} />\n {it.caption && <figcaption>{it.caption}</figcaption>}\n </figure>\n </div>,\n document.body,\n );\n}\n\n/** Galerij — responsive grid met instelbare beeldverhouding en optionele lightbox. */\nfunction GalleryRender({ block }: BlockRenderProps<Gallery>) {\n const settings = useModuleSettings<GallerySettings>(\"gallery\");\n // `_i` = de index in block.items (vóór url-/tagfilters): het inline-edit-pad\n // (`items.<_i>.caption`) moet naar het ORIGINELE array-item wijzen, niet naar de filterpositie.\n const allItems = (block.items ?? []).map((it, _i) => ({ ...it, _i })).filter((i) => i.url);\n const [open, setOpen] = useState(false);\n const [index, setIndex] = useState(0);\n const [activeTag, setActiveTag] = useState<string | null>(null);\n const triggerRef = useRef<HTMLButtonElement | null>(null);\n\n // Filterbalk op item-tags (opt-in): lightbox-navigatie volgt de gefilterde lijst.\n const tags = block.showFilter ? [...new Set(allItems.map((i) => i.tag).filter((t): t is string => Boolean(t)))] : [];\n const items = activeTag ? allItems.filter((i) => i.tag === activeTag) : allItems;\n\n if (allItems.length === 0) return null;\n\n const cols = block.columns && block.columns > 0 ? block.columns : settings.columns ?? 3;\n const lightbox =\n block.lightbox === \"on\" ? true : block.lightbox === \"off\" ? false : settings.lightbox ?? true;\n const ar = block.aspectRatio ?? \"1/1\";\n\n const imgStyle = {\n width: \"100%\",\n aspectRatio: ar === \"auto\" ? undefined : ar.replace(\"/\", \" / \"),\n objectFit: \"cover\" as const,\n borderRadius: \"var(--radius-element)\",\n display: \"block\",\n };\n\n const close = () => {\n setOpen(false);\n triggerRef.current?.focus({ preventScroll: true }); // geen page-jump bij sluiten\n };\n\n return (\n <div className=\"xicms-gallery\">\n {tags.length > 0 && (\n <div className=\"xicms-gallery__filter\" role=\"group\" aria-label=\"Filter op categorie\">\n <button type=\"button\" className=\"xicms-gallery__filterbtn\" aria-pressed={activeTag === null} data-active={activeTag === null ? \"1\" : \"0\"} onClick={() => { setActiveTag(null); setIndex(0); }}>\n Alles\n </button>\n {tags.map((t) => (\n <button key={t} type=\"button\" className=\"xicms-gallery__filterbtn\" aria-pressed={activeTag === t} data-active={activeTag === t ? \"1\" : \"0\"} onClick={() => { setActiveTag(t); setIndex(0); }}>\n {t}\n </button>\n ))}\n </div>\n )}\n <Grid columns={{ minWidth: 160, max: Math.min(cols, 6) }} gap={2}>\n {items.map((i, idx) => (\n <figure key={idx} style={{ margin: 0 }} className=\"xicms-gallery__item\">\n {lightbox ? (\n <button\n type=\"button\"\n onClick={(e) => { triggerRef.current = e.currentTarget; setIndex(idx); setOpen(true); }}\n aria-label={i.alt || i.caption || `Foto ${idx + 1} vergroten`}\n style={{ display: \"block\", width: \"100%\", padding: 0, border: 0, background: \"none\", cursor: \"zoom-in\" }}\n >\n <Img src={i.url as string} alt={i.alt ?? \"\"} width={600} height={600} sizes=\"(max-width: 640px) 50vw, 280px\" style={imgStyle} />\n </button>\n ) : (\n <Img src={i.url as string} alt={i.alt ?? \"\"} width={600} height={600} sizes=\"(max-width: 640px) 50vw, 280px\" style={imgStyle} />\n )}\n {i.caption && (\n <figcaption className=\"xicms-gallery__caption\" data-xicms-edit={`items.${i._i}.caption`} style={{ marginTop: \"0.4rem\", fontSize: \"0.85rem\", color: \"var(--color-text-secondary)\" }}>\n {i.caption}\n </figcaption>\n )}\n </figure>\n ))}\n </Grid>\n\n {lightbox && open && (\n <LightboxOverlay items={items} index={index} onIndex={setIndex} onClose={close} />\n )}\n </div>\n );\n}\n\nexport const galleryBlock = defineBlock({\n type: \"gallery\",\n schema: GalleryBlockSchema,\n Render: GalleryRender,\n label: \"Galerij\",\n icon: \"🖼\",\n defaults: () => ({ items: [], columns: 0, lightbox: \"inherit\", aspectRatio: \"1/1\" }),\n});\n"],"mappings":"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","names":[]}
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
4
|
import { HeroBlockSchema } from "../../schema/blocks";
|
|
4
5
|
import { defineBlock } from "../registry";
|
|
5
|
-
import { Heading, Text } from "../ui";
|
|
6
|
-
import { useLinkComponent } from "@astryxdesign/core/Link";
|
|
6
|
+
import { Btn, Heading, Text } from "../ui";
|
|
7
7
|
import { Img } from "../../media/ImageContext";
|
|
8
8
|
function HeroRender({ block }) {
|
|
9
|
-
const
|
|
9
|
+
const videoRef = useRef(null);
|
|
10
|
+
const [playing, setPlaying] = useState(false);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const v = videoRef.current;
|
|
13
|
+
if (!v) return;
|
|
14
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
|
|
15
|
+
v.play().then(() => setPlaying(true)).catch(() => void 0);
|
|
16
|
+
}, [block.videoUrl]);
|
|
17
|
+
const toggleVideo = () => {
|
|
18
|
+
const v = videoRef.current;
|
|
19
|
+
if (!v) return;
|
|
20
|
+
if (v.paused) v.play().then(() => setPlaying(true)).catch(() => void 0);
|
|
21
|
+
else {
|
|
22
|
+
v.pause();
|
|
23
|
+
setPlaying(false);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
10
26
|
const overlay = block.overlayOpacity > 0 ? { background: block.overlayColor ?? "#000000", opacity: block.overlayOpacity / 100 } : null;
|
|
11
27
|
const coverMedia = Boolean(block.videoUrl || block.imageUrl) && block.minHeight !== "auto";
|
|
12
28
|
const light = coverMedia ? { color: "#ffffff", textShadow: "0 2px 14px rgba(0,0,0,.45)" } : void 0;
|
|
@@ -20,15 +36,38 @@ function HeroRender({ block }) {
|
|
|
20
36
|
"data-height": block.minHeight,
|
|
21
37
|
"data-media": coverMedia ? "cover" : void 0,
|
|
22
38
|
children: [
|
|
23
|
-
block.videoUrl ? /* @__PURE__ */ jsx(
|
|
39
|
+
block.videoUrl ? /* @__PURE__ */ jsx(
|
|
40
|
+
"video",
|
|
41
|
+
{
|
|
42
|
+
ref: videoRef,
|
|
43
|
+
className: "xicms-hero__media",
|
|
44
|
+
src: block.videoUrl,
|
|
45
|
+
poster: block.imageUrl || void 0,
|
|
46
|
+
muted: true,
|
|
47
|
+
loop: true,
|
|
48
|
+
playsInline: true,
|
|
49
|
+
preload: "metadata",
|
|
50
|
+
"aria-hidden": "true"
|
|
51
|
+
}
|
|
52
|
+
) : block.imageUrl ? /* @__PURE__ */ jsx(Img, { src: block.imageUrl, alt: "", className: "xicms-hero__media", width: 1600, height: 900, sizes: "100vw", priority: true, editField: "imageUrl", editFill: true }) : null,
|
|
24
53
|
overlay && /* @__PURE__ */ jsx("div", { className: "xicms-hero__overlay", style: overlay, "aria-hidden": "true" }),
|
|
25
54
|
block.overlayGradient && coverMedia && /* @__PURE__ */ jsx("div", { className: "xicms-hero__gradient", "aria-hidden": "true" }),
|
|
26
55
|
/* @__PURE__ */ jsxs("div", { className: "xicms-hero__inner", children: [
|
|
27
56
|
block.eyebrow && /* @__PURE__ */ jsx(Text, { type: "label", className: "xicms-hero__eyebrow", style: light, "data-xicms-edit": "eyebrow", children: block.eyebrow }),
|
|
28
|
-
block.title && /* @__PURE__ */ jsx(Heading, { level: 1, type: "display-1", className: "xicms-hero__title", style: light, "data-xicms-edit": "title", children: block.title }),
|
|
57
|
+
block.title && /* @__PURE__ */ jsx(Heading, { level: block.headingLevel === "2" ? 2 : 1, type: "display-1", className: "xicms-hero__title", style: light, "data-xicms-edit": "title", children: block.title }),
|
|
29
58
|
block.subtitle && /* @__PURE__ */ jsx(Text, { type: "large", className: "xicms-hero__sub", style: light, "data-xicms-edit": "subtitle", children: block.subtitle }),
|
|
30
|
-
buttons.length > 0 && /* @__PURE__ */ jsx("div", { className: "xicms-hero__buttons", children: buttons.filter((b) => b.label).map((b, i) => /* @__PURE__ */ jsx(
|
|
31
|
-
] })
|
|
59
|
+
buttons.length > 0 && /* @__PURE__ */ jsx("div", { className: "xicms-hero__buttons", children: buttons.filter((b) => b.label).map((b, i) => /* @__PURE__ */ jsx(Btn, { label: b.label, href: b.href, variant: b.variant, size: "lg" }, i)) })
|
|
60
|
+
] }),
|
|
61
|
+
block.videoUrl && /* @__PURE__ */ jsx(
|
|
62
|
+
"button",
|
|
63
|
+
{
|
|
64
|
+
type: "button",
|
|
65
|
+
className: "xicms-hero__videotoggle",
|
|
66
|
+
onClick: toggleVideo,
|
|
67
|
+
"aria-label": playing ? "Achtergrondvideo pauzeren" : "Achtergrondvideo afspelen",
|
|
68
|
+
children: playing ? "\u23F8" : "\u25B6"
|
|
69
|
+
}
|
|
70
|
+
)
|
|
32
71
|
]
|
|
33
72
|
}
|
|
34
73
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/blocks/builtins/Hero.tsx"],"sourcesContent":["\"use client\";\n\nimport type
|
|
1
|
+
{"version":3,"sources":["../../../src/blocks/builtins/Hero.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState, type CSSProperties } from \"react\";\nimport type { z } from \"zod\";\nimport { HeroBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { Btn, Heading, Text } from \"../ui\";\nimport { Img } from \"../../media/ImageContext\";\n\ntype Hero = z.infer<typeof HeroBlockSchema>;\n\nfunction HeroRender({ block }: BlockRenderProps<Hero>) {\n // Achtergrondvideo WCAG-proof (2.2.2): geen autoPlay-attribuut (hydration-veilig) — playback\n // start in een effect, en alléén zonder reduced-motion-voorkeur. Altijd een pauzeknop, en\n // imageUrl dient als poster (direct beeld i.p.v. zwart vlak → betere LCP).\n const videoRef = useRef<HTMLVideoElement>(null);\n const [playing, setPlaying] = useState(false);\n useEffect(() => {\n const v = videoRef.current;\n if (!v) return;\n if (window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches) return;\n v.play().then(() => setPlaying(true)).catch(() => undefined);\n }, [block.videoUrl]);\n const toggleVideo = () => {\n const v = videoRef.current;\n if (!v) return;\n if (v.paused) v.play().then(() => setPlaying(true)).catch(() => undefined);\n else {\n v.pause();\n setPlaying(false);\n }\n };\n const overlay =\n block.overlayOpacity > 0\n ? { background: block.overlayColor ?? \"#000000\", opacity: block.overlayOpacity / 100 }\n : null;\n // Beeld/video als dekkende achtergrond (niet bij height \"auto\"): dan moet de tekst licht zijn.\n const coverMedia = Boolean(block.videoUrl || block.imageUrl) && block.minHeight !== \"auto\";\n const light: CSSProperties | undefined = coverMedia\n ? { color: \"#ffffff\", textShadow: \"0 2px 14px rgba(0,0,0,.45)\" }\n : undefined;\n const buttons = block.buttons ?? [];\n\n return (\n <section\n className=\"xicms-hero\"\n data-align={block.align}\n data-valign={block.vAlign}\n data-height={block.minHeight}\n data-media={coverMedia ? \"cover\" : undefined}\n >\n {block.videoUrl ? (\n <video\n ref={videoRef}\n className=\"xicms-hero__media\"\n src={block.videoUrl}\n poster={block.imageUrl || undefined}\n muted\n loop\n playsInline\n preload=\"metadata\"\n aria-hidden=\"true\"\n />\n ) : block.imageUrl ? (\n <Img src={block.imageUrl} alt=\"\" className=\"xicms-hero__media\" width={1600} height={900} sizes=\"100vw\" priority editField=\"imageUrl\" editFill />\n ) : null}\n {overlay && <div className=\"xicms-hero__overlay\" style={overlay} aria-hidden=\"true\" />}\n {/* Gradient (donker onderaan → transparant): leesbare tekst zonder de hele foto te dimmen. */}\n {block.overlayGradient && coverMedia && <div className=\"xicms-hero__gradient\" aria-hidden=\"true\" />}\n <div className=\"xicms-hero__inner\">\n {block.eyebrow && (\n <Text type=\"label\" className=\"xicms-hero__eyebrow\" style={light} data-xicms-edit=\"eyebrow\">\n {block.eyebrow}\n </Text>\n )}\n {block.title && (\n <Heading level={block.headingLevel === \"2\" ? 2 : 1} type=\"display-1\" className=\"xicms-hero__title\" style={light} data-xicms-edit=\"title\">\n {block.title}\n </Heading>\n )}\n {block.subtitle && (\n <Text type=\"large\" className=\"xicms-hero__sub\" style={light} data-xicms-edit=\"subtitle\">\n {block.subtitle}\n </Text>\n )}\n {buttons.length > 0 && (\n <div className=\"xicms-hero__buttons\">\n {/* Via Astryx Btn (zelfde systeem als Cta/Pricing/…): volgt de site-theming en de\n outline→ghost-variantmapping — geen tweede, handmatig te onderhouden knop-CSS. */}\n {buttons.filter((b) => b.label).map((b, i) => (\n <Btn key={i} label={b.label} href={b.href} variant={b.variant} size=\"lg\" />\n ))}\n </div>\n )}\n </div>\n {block.videoUrl && (\n <button\n type=\"button\"\n className=\"xicms-hero__videotoggle\"\n onClick={toggleVideo}\n aria-label={playing ? \"Achtergrondvideo pauzeren\" : \"Achtergrondvideo afspelen\"}\n >\n {playing ? \"⏸\" : \"▶\"}\n </button>\n )}\n </section>\n );\n}\n\nexport const heroBlock = defineBlock({\n type: \"hero\",\n schema: HeroBlockSchema,\n Render: HeroRender,\n label: \"Hero\",\n icon: \"✨\",\n defaults: () => ({ title: \"Welkom\", subtitle: \"\", align: \"left\", vAlign: \"center\", minHeight: \"auto\", overlayOpacity: 0, buttons: [] }),\n});\n"],"mappings":"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","names":[]}
|
|
@@ -1,13 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { ImageBlockSchema } from "../../schema/blocks";
|
|
3
4
|
import { defineBlock } from "../registry";
|
|
4
|
-
import {
|
|
5
|
+
import { Img } from "../../media/ImageContext";
|
|
5
6
|
function ImageRender({ block }) {
|
|
6
7
|
if (!block.url) return null;
|
|
7
|
-
const img = /* @__PURE__ */ jsx(
|
|
8
|
+
const img = /* @__PURE__ */ jsx(
|
|
9
|
+
Img,
|
|
10
|
+
{
|
|
11
|
+
src: block.url,
|
|
12
|
+
alt: block.alt ?? "",
|
|
13
|
+
width: 1600,
|
|
14
|
+
height: 1067,
|
|
15
|
+
sizes: "(max-width: 900px) 100vw, 900px",
|
|
16
|
+
editField: "url"
|
|
17
|
+
}
|
|
18
|
+
);
|
|
8
19
|
return /* @__PURE__ */ jsxs("figure", { className: "xicms-image", "data-align": block.align, "data-ratio": block.aspectRatio, children: [
|
|
9
20
|
block.href ? /* @__PURE__ */ jsx("a", { href: block.href, children: img }) : img,
|
|
10
|
-
block.caption && /* @__PURE__ */ jsx("figcaption", { children: block.caption })
|
|
21
|
+
block.caption && /* @__PURE__ */ jsx("figcaption", { "data-xicms-edit": "caption", children: block.caption })
|
|
11
22
|
] });
|
|
12
23
|
}
|
|
13
24
|
const imageBlock = defineBlock({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/blocks/builtins/ImageBlock.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../../src/blocks/builtins/ImageBlock.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { ImageBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { Img } from \"../../media/ImageContext\";\n\ntype ImageBlk = z.infer<typeof ImageBlockSchema>;\n\nfunction ImageRender({ block }: BlockRenderProps<ImageBlk>) {\n if (!block.url) return null;\n // Via het geïnjecteerde Img-component (next/image op consumer-sites → optimalisatie/lazy-load)\n // mét editField: klik-om-te-vervangen in de visuele editor. Dit blok — welks hele doel beeld is —\n // was gek genoeg de enige beeld-route zónder beide (rendde om Img heen). De aspect-ratio-CSS\n // (.xicms-image[data-ratio] img) blijft gewoon werken.\n const img = (\n <Img\n src={block.url}\n alt={block.alt ?? \"\"}\n width={1600}\n height={1067}\n sizes=\"(max-width: 900px) 100vw, 900px\"\n editField=\"url\"\n />\n );\n return (\n <figure className=\"xicms-image\" data-align={block.align} data-ratio={block.aspectRatio}>\n {block.href ? <a href={block.href}>{img}</a> : img}\n {block.caption && <figcaption data-xicms-edit=\"caption\">{block.caption}</figcaption>}\n </figure>\n );\n}\n\nexport const imageBlock = defineBlock({\n type: \"image\",\n schema: ImageBlockSchema,\n Render: ImageRender,\n label: \"Afbeelding\",\n icon: \"🖼️\",\n defaults: () => ({ alt: \"\", align: \"center\", aspectRatio: \"auto\" }),\n});\n"],"mappings":";AAgBI,cAUA,YAVA;AAbJ,SAAS,wBAAwB;AACjC,SAAS,mBAA0C;AACnD,SAAS,WAAW;AAIpB,SAAS,YAAY,EAAE,MAAM,GAA+B;AAC1D,MAAI,CAAC,MAAM,IAAK,QAAO;AAKvB,QAAM,MACJ;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,MAAM;AAAA,MACX,KAAK,MAAM,OAAO;AAAA,MAClB,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,OAAM;AAAA,MACN,WAAU;AAAA;AAAA,EACZ;AAEF,SACE,qBAAC,YAAO,WAAU,eAAc,cAAY,MAAM,OAAO,cAAY,MAAM,aACxE;AAAA,UAAM,OAAO,oBAAC,OAAE,MAAM,MAAM,MAAO,eAAI,IAAO;AAAA,IAC9C,MAAM,WAAW,oBAAC,gBAAW,mBAAgB,WAAW,gBAAM,SAAQ;AAAA,KACzE;AAEJ;AAEO,MAAM,aAAa,YAAY;AAAA,EACpC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,KAAK,IAAI,OAAO,UAAU,aAAa,OAAO;AACnE,CAAC;","names":[]}
|
|
@@ -6,8 +6,8 @@ type JobItem = Jobs["items"][number];
|
|
|
6
6
|
export declare function employmentTypeEnum(value?: string): string | null;
|
|
7
7
|
/**
|
|
8
8
|
* JobPosting-JSON-LD per vacature (Google Jobs). Puur en getest. Alleen vacatures met de door
|
|
9
|
-
* Google vereiste kernvelden (titel + plaatsingsdatum + locatie) komen in de JSON-LD
|
|
10
|
-
* items renderen wél op de pagina maar leveren geen Search-Console-errors op.
|
|
9
|
+
* Google vereiste kernvelden (titel + plaatsingsdatum + locatie + omschrijving) komen in de JSON-LD
|
|
10
|
+
* — incomplete items renderen wél op de pagina maar leveren geen Search-Console-errors op. Salaris
|
|
11
11
|
* ("€ 2.800 – € 3.400 p/m") is geen geldige MonetaryAmount en wordt bewust weggelaten.
|
|
12
12
|
*/
|
|
13
13
|
export declare function jobsJsonLd(items: JobItem[], orgName: string): Array<Record<string, unknown>>;
|
|
@@ -16,7 +16,7 @@ function employmentTypeEnum(value) {
|
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
18
|
function jobsJsonLd(items, orgName) {
|
|
19
|
-
return items.filter((j) => j.title && j.datePosted && j.location).map((j) => {
|
|
19
|
+
return items.filter((j) => j.title && j.datePosted && j.location && j.description).map((j) => {
|
|
20
20
|
const employment = employmentTypeEnum(j.employmentType);
|
|
21
21
|
return {
|
|
22
22
|
"@context": "https://schema.org",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/blocks/builtins/Jobs.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { JobsBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { useSiteConfig } from \"../../config\";\nimport { VStack, Card, Heading, Text, Btn } from \"../ui\";\nimport { jsonLdHtml } from \"./jsonld\";\n\ntype Jobs = z.infer<typeof JobsBlockSchema>;\ntype JobItem = Jobs[\"items\"][number];\n\n/** Vrije-tekst dienstverband → schema.org-enum (Google keurt vrije tekst af). */\nexport function employmentTypeEnum(value?: string): string | null {\n const v = (value ?? \"\").toLowerCase();\n if (!v) return null;\n if (v.includes(\"full\") || v.includes(\"voltijd\")) return \"FULL_TIME\";\n if (v.includes(\"part\") || v.includes(\"deeltijd\") || /\\d+\\s*[–-]?\\s*\\d*\\s*uur/.test(v)) return \"PART_TIME\";\n if (v.includes(\"bbl\") || v.includes(\"leerling\") || v.includes(\"stage\") || v.includes(\"intern\")) return \"INTERN\";\n if (v.includes(\"tijdelijk\") || v.includes(\"temp\")) return \"TEMPORARY\";\n if (v.includes(\"zzp\") || v.includes(\"freelance\") || v.includes(\"contract\")) return \"CONTRACTOR\";\n return null;\n}\n\n/**\n * JobPosting-JSON-LD per vacature (Google Jobs). Puur en getest. Alleen vacatures met de door\n * Google vereiste kernvelden (titel + plaatsingsdatum + locatie) komen in de JSON-LD — incomplete
|
|
1
|
+
{"version":3,"sources":["../../../src/blocks/builtins/Jobs.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { JobsBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { useSiteConfig } from \"../../config\";\nimport { VStack, Card, Heading, Text, Btn } from \"../ui\";\nimport { jsonLdHtml } from \"./jsonld\";\n\ntype Jobs = z.infer<typeof JobsBlockSchema>;\ntype JobItem = Jobs[\"items\"][number];\n\n/** Vrije-tekst dienstverband → schema.org-enum (Google keurt vrije tekst af). */\nexport function employmentTypeEnum(value?: string): string | null {\n const v = (value ?? \"\").toLowerCase();\n if (!v) return null;\n if (v.includes(\"full\") || v.includes(\"voltijd\")) return \"FULL_TIME\";\n if (v.includes(\"part\") || v.includes(\"deeltijd\") || /\\d+\\s*[–-]?\\s*\\d*\\s*uur/.test(v)) return \"PART_TIME\";\n if (v.includes(\"bbl\") || v.includes(\"leerling\") || v.includes(\"stage\") || v.includes(\"intern\")) return \"INTERN\";\n if (v.includes(\"tijdelijk\") || v.includes(\"temp\")) return \"TEMPORARY\";\n if (v.includes(\"zzp\") || v.includes(\"freelance\") || v.includes(\"contract\")) return \"CONTRACTOR\";\n return null;\n}\n\n/**\n * JobPosting-JSON-LD per vacature (Google Jobs). Puur en getest. Alleen vacatures met de door\n * Google vereiste kernvelden (titel + plaatsingsdatum + locatie + omschrijving) komen in de JSON-LD\n * — incomplete items renderen wél op de pagina maar leveren geen Search-Console-errors op. Salaris\n * (\"€ 2.800 – € 3.400 p/m\") is geen geldige MonetaryAmount en wordt bewust weggelaten.\n */\nexport function jobsJsonLd(items: JobItem[], orgName: string): Array<Record<string, unknown>> {\n return items\n .filter((j) => j.title && j.datePosted && j.location && j.description)\n .map((j) => {\n const employment = employmentTypeEnum(j.employmentType);\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"JobPosting\",\n title: j.title,\n hiringOrganization: { \"@type\": \"Organization\", name: orgName },\n ...(j.description ? { description: j.description } : {}),\n datePosted: j.datePosted,\n ...(employment ? { employmentType: employment } : {}),\n jobLocation: { \"@type\": \"Place\", address: { \"@type\": \"PostalAddress\", addressLocality: j.location } },\n ...(j.applyUrl ? { url: j.applyUrl } : {}),\n };\n });\n}\n\n/** Vacature-overzicht met JobPosting-JSON-LD — vindbaar via Google Jobs. */\nfunction JobsRender({ block }: BlockRenderProps<Jobs>) {\n const { name } = useSiteConfig();\n // _i = index in de OORSPRONKELIJKE array — inline-edit-paden (items.N.title) moeten daarheen wijzen.\n const items = (block.items ?? []).map((it, i) => ({ ...it, _i: i })).filter((j) => j.title);\n if (items.length === 0 && !block.heading) return null;\n return (\n <VStack gap={4} as=\"section\" className=\"xicms-jobs\">\n {(block.heading || block.intro) && (\n <VStack gap={1}>\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n {block.intro && <Text color=\"secondary\" data-xicms-edit=\"intro\">{block.intro}</Text>}\n </VStack>\n )}\n {items.length === 0 && <Text color=\"secondary\">Er zijn op dit moment geen openstaande vacatures.</Text>}\n <VStack gap={3}>\n {items.map((j) => {\n const i = j._i;\n return (\n <Card key={i} padding={4} className=\"xicms-jobs__item\">\n <VStack gap={1.5}>\n <div className=\"xicms-jobs__head\">\n <Heading level={3} data-xicms-edit={`items.${i}.title`}>{j.title}</Heading>\n <span className=\"xicms-jobs__meta\">\n {[j.employmentType, j.location, j.salary].filter(Boolean).join(\" · \")}\n </span>\n </div>\n {j.description && <Text color=\"secondary\" data-xicms-edit={`items.${i}.description`}>{j.description}</Text>}\n {j.applyUrl && (\n <div>\n <Btn label=\"Solliciteer\" href={j.applyUrl} variant=\"primary\" />\n </div>\n )}\n </VStack>\n </Card>\n );\n })}\n </VStack>\n {block.jsonLd &&\n jobsJsonLd(items, name).map((ld, i) => (\n <script\n key={i}\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: jsonLdHtml(ld) }}\n />\n ))}\n </VStack>\n );\n}\n\nexport const jobsBlock = defineBlock({\n type: \"jobs\",\n fields: { \"items.datePosted\": { type: \"date\" } },\n schema: JobsBlockSchema,\n Render: JobsRender,\n label: \"Vacatures\",\n icon: \"🧑🔧\",\n defaults: () => ({\n heading: \"Vacatures\",\n items: [{ title: \"Vacaturetitel\", employmentType: \"Fulltime\", location: \"\" }],\n jsonLd: true,\n }),\n});\n"],"mappings":"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","names":[]}
|
|
@@ -49,11 +49,24 @@ function NewsletterRender({ block, siteId, mode }) {
|
|
|
49
49
|
return /* @__PURE__ */ jsx(Card, { padding: 5, variant: "muted", children: /* @__PURE__ */ jsxs(VStack, { gap: 2, hAlign: "center", style: { textAlign: "center" }, children: [
|
|
50
50
|
block.heading && /* @__PURE__ */ jsx(Heading, { level: 2, "data-xicms-edit": "heading", children: block.heading }),
|
|
51
51
|
block.text && /* @__PURE__ */ jsx(Text, { color: "secondary", "data-xicms-edit": "text", children: block.text }),
|
|
52
|
-
status === "ok" ? /* @__PURE__ */ jsx(Text, { role: "status", children: "Bedankt voor je aanmelding!" }) : block.actionUrl ? /* @__PURE__ */ jsxs(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
status === "ok" ? /* @__PURE__ */ jsx(Text, { role: "status", children: "Bedankt voor je aanmelding!" }) : block.actionUrl ? /* @__PURE__ */ jsxs(
|
|
53
|
+
"form",
|
|
54
|
+
{
|
|
55
|
+
action: block.actionUrl,
|
|
56
|
+
method: "post",
|
|
57
|
+
target: "_blank",
|
|
58
|
+
style: rowStyle,
|
|
59
|
+
onSubmit: (e) => {
|
|
60
|
+
if (block.consentText && !agreed) e.preventDefault();
|
|
61
|
+
},
|
|
62
|
+
children: [
|
|
63
|
+
emailInput,
|
|
64
|
+
consentBox,
|
|
65
|
+
block.consentText && /* @__PURE__ */ jsx("input", { type: "hidden", name: "CONSENT", value: agreed ? "yes" : "" }),
|
|
66
|
+
/* @__PURE__ */ jsx(Button, { label: block.buttonLabel, variant: "primary", size: "lg", type: "submit" })
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
) : useXicmsForm ? /* @__PURE__ */ jsxs("form", { onSubmit: submitViaXicms, style: rowStyle, children: [
|
|
57
70
|
emailInput,
|
|
58
71
|
consentBox,
|
|
59
72
|
/* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/blocks/builtins/Newsletter.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, type FormEvent } from \"react\";\nimport type { z } from \"zod\";\nimport { NewsletterBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { Button } from \"@astryxdesign/core/Button\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { CheckboxInput } from \"@astryxdesign/core/CheckboxInput\";\nimport { VStack, Card, Heading, Text } from \"../ui\";\nimport { isDemoActive } from \"../../demo/demoStore\";\nimport { getFirebaseApp } from \"../../firebase/client\";\nimport { submitFormValues } from \"./lib/submitForm\";\n\ntype Newsletter = z.infer<typeof NewsletterBlockSchema>;\n\n/**\n * Nieuwsbrief-aanmelding. Twee modi:\n * - `actionUrl` (bv. de embed-action van Mailchimp/Brevo): native form-POST naar de provider.\n * - `formId` (zonder actionUrl): opslag via het XiCMS-formulier (submitForm-callable) — inzendingen\n * verschijnen in /beheer → Formulieren, met dezelfde rate-limiting/validatie als het formulier-blok.\n * Optionele AVG-checkbox via `consentText`.\n */\nfunction NewsletterRender({ block, siteId, mode }: BlockRenderProps<Newsletter>) {\n const [email, setEmail] = useState(\"\");\n const [agreed, setAgreed] = useState(false);\n const [status, setStatus] = useState<\"idle\" | \"busy\" | \"ok\" | \"error\">(\"idle\");\n const useXicmsForm = !block.actionUrl && Boolean(block.formId);\n\n async function submitViaXicms(e: FormEvent) {\n e.preventDefault();\n if (mode === \"preview\" || !block.formId) return;\n // Zelfde vangnet als het formulier-blok: in demo of zonder Firebase-backend lokaal \"slagen\"\n // i.p.v. een foutmelding tonen op een statische (referentie)site.\n if (isDemoActive() || !getFirebaseApp()) {\n setStatus(\"ok\");\n return;\n }\n setStatus(\"busy\");\n try {\n await submitFormValues(siteId, block.formId, { email });\n setStatus(\"ok\");\n setEmail(\"\");\n } catch {\n setStatus(\"error\");\n }\n }\n\n const consentBox = block.consentText ? (\n <div style={{ textAlign: \"left\", maxWidth: 440, width: \"100%\" }}>\n <CheckboxInput label={block.consentText} isRequired value={agreed} onChange={setAgreed} />\n </div>\n ) : null;\n\n const emailInput = (\n <div style={{ flex: 1, minWidth: 200, textAlign: \"left\" }}>\n <TextInput\n label=\"E-mailadres\"\n isLabelHidden\n type=\"email\"\n htmlName=\"EMAIL\"\n isRequired\n placeholder=\"je@email.nl\"\n value={email}\n onChange={setEmail}\n />\n </div>\n );\n const rowStyle = { display: \"flex\", gap: \"0.75rem\", flexWrap: \"wrap\", alignItems: \"end\", justifyContent: \"center\", maxWidth: 440, width: \"100%\" } as const;\n\n return (\n <Card padding={5} variant=\"muted\">\n <VStack gap={2} hAlign=\"center\" style={{ textAlign: \"center\" }}>\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n {block.text && <Text color=\"secondary\" data-xicms-edit=\"text\">{block.text}</Text>}\n {status === \"ok\" ? (\n <Text role=\"status\">Bedankt voor je aanmelding!</Text>\n ) : block.actionUrl ? (\n <form
|
|
1
|
+
{"version":3,"sources":["../../../src/blocks/builtins/Newsletter.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, type FormEvent } from \"react\";\nimport type { z } from \"zod\";\nimport { NewsletterBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { Button } from \"@astryxdesign/core/Button\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { CheckboxInput } from \"@astryxdesign/core/CheckboxInput\";\nimport { VStack, Card, Heading, Text } from \"../ui\";\nimport { isDemoActive } from \"../../demo/demoStore\";\nimport { getFirebaseApp } from \"../../firebase/client\";\nimport { submitFormValues } from \"./lib/submitForm\";\n\ntype Newsletter = z.infer<typeof NewsletterBlockSchema>;\n\n/**\n * Nieuwsbrief-aanmelding. Twee modi:\n * - `actionUrl` (bv. de embed-action van Mailchimp/Brevo): native form-POST naar de provider.\n * - `formId` (zonder actionUrl): opslag via het XiCMS-formulier (submitForm-callable) — inzendingen\n * verschijnen in /beheer → Formulieren, met dezelfde rate-limiting/validatie als het formulier-blok.\n * Optionele AVG-checkbox via `consentText`.\n */\nfunction NewsletterRender({ block, siteId, mode }: BlockRenderProps<Newsletter>) {\n const [email, setEmail] = useState(\"\");\n const [agreed, setAgreed] = useState(false);\n const [status, setStatus] = useState<\"idle\" | \"busy\" | \"ok\" | \"error\">(\"idle\");\n const useXicmsForm = !block.actionUrl && Boolean(block.formId);\n\n async function submitViaXicms(e: FormEvent) {\n e.preventDefault();\n if (mode === \"preview\" || !block.formId) return;\n // Zelfde vangnet als het formulier-blok: in demo of zonder Firebase-backend lokaal \"slagen\"\n // i.p.v. een foutmelding tonen op een statische (referentie)site.\n if (isDemoActive() || !getFirebaseApp()) {\n setStatus(\"ok\");\n return;\n }\n setStatus(\"busy\");\n try {\n await submitFormValues(siteId, block.formId, { email });\n setStatus(\"ok\");\n setEmail(\"\");\n } catch {\n setStatus(\"error\");\n }\n }\n\n const consentBox = block.consentText ? (\n <div style={{ textAlign: \"left\", maxWidth: 440, width: \"100%\" }}>\n <CheckboxInput label={block.consentText} isRequired value={agreed} onChange={setAgreed} />\n </div>\n ) : null;\n\n const emailInput = (\n <div style={{ flex: 1, minWidth: 200, textAlign: \"left\" }}>\n <TextInput\n label=\"E-mailadres\"\n isLabelHidden\n type=\"email\"\n htmlName=\"EMAIL\"\n isRequired\n placeholder=\"je@email.nl\"\n value={email}\n onChange={setEmail}\n />\n </div>\n );\n const rowStyle = { display: \"flex\", gap: \"0.75rem\", flexWrap: \"wrap\", alignItems: \"end\", justifyContent: \"center\", maxWidth: 440, width: \"100%\" } as const;\n\n return (\n <Card padding={5} variant=\"muted\">\n <VStack gap={2} hAlign=\"center\" style={{ textAlign: \"center\" }}>\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n {block.text && <Text color=\"secondary\" data-xicms-edit=\"text\">{block.text}</Text>}\n {status === \"ok\" ? (\n <Text role=\"status\">Bedankt voor je aanmelding!</Text>\n ) : block.actionUrl ? (\n <form\n action={block.actionUrl}\n method=\"post\"\n target=\"_blank\"\n style={rowStyle}\n // AVG: zonder deze guard kon de native POST doorgaan zonder aangevinkte toestemming\n // (Astryx CheckboxInput heeft geen htmlName, dus de browser hield 'm niet tegen).\n onSubmit={(e) => {\n if (block.consentText && !agreed) e.preventDefault();\n }}\n >\n {emailInput}\n {consentBox}\n {/* De toestemmingswaarde ook daadwerkelijk meesturen naar de provider. */}\n {block.consentText && <input type=\"hidden\" name=\"CONSENT\" value={agreed ? \"yes\" : \"\"} />}\n <Button label={block.buttonLabel} variant=\"primary\" size=\"lg\" type=\"submit\" />\n </form>\n ) : useXicmsForm ? (\n <form onSubmit={submitViaXicms} style={rowStyle}>\n {emailInput}\n {consentBox}\n <Button\n label={status === \"busy\" ? \"Aanmelden…\" : block.buttonLabel}\n variant=\"primary\"\n size=\"lg\"\n type=\"submit\"\n isLoading={status === \"busy\"}\n />\n {status === \"error\" && (\n <Text role=\"alert\" type=\"supporting\" style={{ color: \"var(--color-error, #c0392b)\" }}>\n Aanmelden mislukt. Probeer het later opnieuw.\n </Text>\n )}\n </form>\n ) : (\n <Text type=\"supporting\" color=\"secondary\">Configureer een aanmeld-URL of formulier in de editor.</Text>\n )}\n </VStack>\n </Card>\n );\n}\n\nexport const newsletterBlock = defineBlock({\n type: \"newsletter\",\n schema: NewsletterBlockSchema,\n Render: NewsletterRender,\n label: \"Nieuwsbrief\",\n icon: \"✉️\",\n defaults: () => ({ heading: \"Blijf op de hoogte\", buttonLabel: \"Aanmelden\" }),\n});\n"],"mappings":"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","names":[]}
|
|
@@ -47,9 +47,11 @@ function PricingRender({ block }) {
|
|
|
47
47
|
it.detail && /* @__PURE__ */ jsx(Text, { color: "secondary", children: it.detail }),
|
|
48
48
|
it.features && it.features.length > 0 && /* @__PURE__ */ jsx(VStack, { gap: 1, as: "ul", style: { listStyle: "none", margin: 0, padding: 0, flex: 1 }, children: it.features.map((f, j) => (
|
|
49
49
|
// Echte <li> binnen de <ul> (geldige HTML; screenreaders kondigen de lijst aan).
|
|
50
|
+
// Inline edit op een span BINNEN de tekst: anders zou het ✓-teken mee worden
|
|
51
|
+
// teruggeschreven in de opgeslagen feature (zelfde patroon als Testimonials).
|
|
50
52
|
/* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(Text, { display: "block", children: [
|
|
51
53
|
"\u2713 ",
|
|
52
|
-
f
|
|
54
|
+
/* @__PURE__ */ jsx("span", { "data-xicms-edit": `items.${i}.features.${j}`, children: f })
|
|
53
55
|
] }) }, j)
|
|
54
56
|
)) }),
|
|
55
57
|
it.ctaLabel && /* @__PURE__ */ jsx(Btn, { label: it.ctaLabel, href: it.ctaHref ?? "#", variant: it.highlighted ? "primary" : "outline" })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/blocks/builtins/Pricing.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { PricingBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { VStack, Grid, Card, Heading, Text, Btn } from \"../ui\";\nimport { jsonLdHtml } from \"./jsonld\";\n\ntype Pricing = z.infer<typeof PricingBlockSchema>;\n\n/** \"€ 120\", \"120,50 p/m\" → \"120.50\"; null als er geen bedrag in te herkennen is. */\nexport function parsePriceAmount(price: string): string | null {\n const m = price.replace(/\\./g, \"\").replace(\",\", \".\").match(/(\\d+(?:\\.\\d{1,2})?)/);\n return m ? m[1] : null;\n}\n\nexport function pricingJsonLd(items: Pricing[\"items\"], currency = \"EUR\"): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ItemList\",\n itemListElement: items\n .filter((it) => it.label)\n .map((it, i) => {\n // schema.org-`price` moet een kaal getal zijn (\"120\" of \"120.50\"); vrije tekst zoals\n // \"€ 120/mnd\" keurt Google af. Geen herkenbaar bedrag → dan geen Offer.\n const amount = parsePriceAmount(it.price);\n return {\n \"@type\": \"ListItem\",\n position: i + 1,\n item: {\n \"@type\": \"Product\",\n name: it.label,\n ...(it.detail ? { description: it.detail } : {}),\n ...(amount ? { offers: { \"@type\": \"Offer\", price: amount, priceCurrency: currency } } : {}),\n },\n };\n }),\n };\n}\n\n/** Prijzen als kaarten met optionele features, uitgelichte tier en per-tier knop (Astryx). */\nfunction PricingRender({ block }: BlockRenderProps<Pricing>) {\n const items = block.items ?? [];\n return (\n <VStack gap={4} as=\"section\" className=\"xicms-pricing\">\n {block.heading && (\n <Heading level={2} justify=\"center\" data-xicms-edit=\"heading\" style={{ textAlign: \"center\" }}>{block.heading}</Heading>\n )}\n <Grid columns={{ minWidth: 240, max: 4 }} gap={4}>\n {items.map((it, i) => (\n <Card\n key={i}\n padding={5}\n height=\"100%\"\n className={`xicms-pricing__card${it.highlighted ? \" xicms-pricing__card--highlighted\" : \"\"}`}\n style={it.highlighted ? { outline: \"2px solid var(--color-accent)\", outlineOffset: -2 } : undefined}\n >\n <VStack gap={2} style={{ height: \"100%\" }}>\n <Text type=\"label\" color=\"secondary\" className=\"xicms-pricing__label\" data-xicms-edit={`items.${i}.label`} style={{ textTransform: \"uppercase\", letterSpacing: \"0.05em\" }}>\n {it.label}\n </Text>\n <VStack gap={0}>\n <Heading level={3} className=\"xicms-pricing__price\" data-xicms-edit={`items.${i}.price`} style={{ fontSize: \"clamp(1.75rem, 4vw, 2.25rem)\", lineHeight: 1.1, fontWeight: 700 }}>\n {it.price}\n </Heading>\n {it.billingPeriod && <Text type=\"supporting\" color=\"secondary\">{it.billingPeriod}</Text>}\n </VStack>\n {it.detail && <Text color=\"secondary\">{it.detail}</Text>}\n {it.features && it.features.length > 0 && (\n <VStack gap={1} as=\"ul\" style={{ listStyle: \"none\", margin: 0, padding: 0, flex: 1 }}>\n {it.features.map((f, j) => (\n // Echte <li> binnen de <ul> (geldige HTML; screenreaders kondigen de lijst aan).\n <li key={j}>\n <Text display=\"block\">✓ {f}</Text>\n </li>\n ))}\n </VStack>\n )}\n {it.ctaLabel && (\n <Btn label={it.ctaLabel} href={it.ctaHref ?? \"#\"} variant={it.highlighted ? \"primary\" : \"outline\"} />\n )}\n </VStack>\n </Card>\n ))}\n </Grid>\n {block.note && <Text color=\"secondary\" justify=\"center\" style={{ textAlign: \"center\" }}>{block.note}</Text>}\n {block.jsonLd && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: jsonLdHtml(pricingJsonLd(items, block.currency)) }}\n />\n )}\n </VStack>\n );\n}\n\nexport const pricingBlock = defineBlock({\n type: \"pricing\",\n schema: PricingBlockSchema,\n Render: PricingRender,\n label: \"Prijzen\",\n icon: \"💶\",\n defaults: () => ({\n heading: \"Prijzen\",\n items: [{ label: \"Pakket\", price: \"€ 00\", billingPeriod: \"/maand\", features: [], highlighted: false }],\n }),\n});\n"],"mappings":";AA8CQ,cAeM,YAfN;AA3CR,SAAS,0BAA0B;AACnC,SAAS,mBAA0C;AACnD,SAAS,QAAQ,MAAM,MAAM,SAAS,MAAM,WAAW;AACvD,SAAS,kBAAkB;AAKpB,SAAS,iBAAiB,OAA8B;AAC7D,QAAM,IAAI,MAAM,QAAQ,OAAO,EAAE,EAAE,QAAQ,KAAK,GAAG,EAAE,MAAM,qBAAqB;AAChF,SAAO,IAAI,EAAE,CAAC,IAAI;AACpB;AAEO,SAAS,cAAc,OAAyB,WAAW,OAAgC;AAChG,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,iBAAiB,MACd,OAAO,CAAC,OAAO,GAAG,KAAK,EACvB,IAAI,CAAC,IAAI,MAAM;AAGd,YAAM,SAAS,iBAAiB,GAAG,KAAK;AACxC,aAAO;AAAA,QACL,SAAS;AAAA,QACT,UAAU,IAAI;AAAA,QACd,MAAM;AAAA,UACJ,SAAS;AAAA,UACT,MAAM,GAAG;AAAA,UACT,GAAI,GAAG,SAAS,EAAE,aAAa,GAAG,OAAO,IAAI,CAAC;AAAA,UAC9C,GAAI,SAAS,EAAE,QAAQ,EAAE,SAAS,SAAS,OAAO,QAAQ,eAAe,SAAS,EAAE,IAAI,CAAC;AAAA,QAC3F;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACL;AACF;AAGA,SAAS,cAAc,EAAE,MAAM,GAA8B;AAC3D,QAAM,QAAQ,MAAM,SAAS,CAAC;AAC9B,SACE,qBAAC,UAAO,KAAK,GAAG,IAAG,WAAU,WAAU,iBACpC;AAAA,UAAM,WACL,oBAAC,WAAQ,OAAO,GAAG,SAAQ,UAAS,mBAAgB,WAAU,OAAO,EAAE,WAAW,SAAS,GAAI,gBAAM,SAAQ;AAAA,IAE/G,oBAAC,QAAK,SAAS,EAAE,UAAU,KAAK,KAAK,EAAE,GAAG,KAAK,GAC5C,gBAAM,IAAI,CAAC,IAAI,MACd;AAAA,MAAC;AAAA;AAAA,QAEC,SAAS;AAAA,QACT,QAAO;AAAA,QACP,WAAW,sBAAsB,GAAG,cAAc,sCAAsC,EAAE;AAAA,QAC1F,OAAO,GAAG,cAAc,EAAE,SAAS,iCAAiC,eAAe,GAAG,IAAI;AAAA,QAE1F,+BAAC,UAAO,KAAK,GAAG,OAAO,EAAE,QAAQ,OAAO,GACtC;AAAA,8BAAC,QAAK,MAAK,SAAQ,OAAM,aAAY,WAAU,wBAAuB,mBAAiB,SAAS,CAAC,UAAU,OAAO,EAAE,eAAe,aAAa,eAAe,SAAS,GACrK,aAAG,OACN;AAAA,UACA,qBAAC,UAAO,KAAK,GACX;AAAA,gCAAC,WAAQ,OAAO,GAAG,WAAU,wBAAuB,mBAAiB,SAAS,CAAC,UAAU,OAAO,EAAE,UAAU,gCAAgC,YAAY,KAAK,YAAY,IAAI,GAC1K,aAAG,OACN;AAAA,YACC,GAAG,iBAAiB,oBAAC,QAAK,MAAK,cAAa,OAAM,aAAa,aAAG,eAAc;AAAA,aACnF;AAAA,UACC,GAAG,UAAU,oBAAC,QAAK,OAAM,aAAa,aAAG,QAAO;AAAA,UAChD,GAAG,YAAY,GAAG,SAAS,SAAS,KACnC,oBAAC,UAAO,KAAK,GAAG,IAAG,MAAK,OAAO,EAAE,WAAW,QAAQ,QAAQ,GAAG,SAAS,GAAG,MAAM,EAAE,GAChF,aAAG,SAAS,IAAI,CAAC,GAAG;AAAA;AAAA,
|
|
1
|
+
{"version":3,"sources":["../../../src/blocks/builtins/Pricing.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { PricingBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { VStack, Grid, Card, Heading, Text, Btn } from \"../ui\";\nimport { jsonLdHtml } from \"./jsonld\";\n\ntype Pricing = z.infer<typeof PricingBlockSchema>;\n\n/** \"€ 120\", \"120,50 p/m\" → \"120.50\"; null als er geen bedrag in te herkennen is. */\nexport function parsePriceAmount(price: string): string | null {\n const m = price.replace(/\\./g, \"\").replace(\",\", \".\").match(/(\\d+(?:\\.\\d{1,2})?)/);\n return m ? m[1] : null;\n}\n\nexport function pricingJsonLd(items: Pricing[\"items\"], currency = \"EUR\"): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ItemList\",\n itemListElement: items\n .filter((it) => it.label)\n .map((it, i) => {\n // schema.org-`price` moet een kaal getal zijn (\"120\" of \"120.50\"); vrije tekst zoals\n // \"€ 120/mnd\" keurt Google af. Geen herkenbaar bedrag → dan geen Offer.\n const amount = parsePriceAmount(it.price);\n return {\n \"@type\": \"ListItem\",\n position: i + 1,\n item: {\n \"@type\": \"Product\",\n name: it.label,\n ...(it.detail ? { description: it.detail } : {}),\n ...(amount ? { offers: { \"@type\": \"Offer\", price: amount, priceCurrency: currency } } : {}),\n },\n };\n }),\n };\n}\n\n/** Prijzen als kaarten met optionele features, uitgelichte tier en per-tier knop (Astryx). */\nfunction PricingRender({ block }: BlockRenderProps<Pricing>) {\n const items = block.items ?? [];\n return (\n <VStack gap={4} as=\"section\" className=\"xicms-pricing\">\n {block.heading && (\n <Heading level={2} justify=\"center\" data-xicms-edit=\"heading\" style={{ textAlign: \"center\" }}>{block.heading}</Heading>\n )}\n <Grid columns={{ minWidth: 240, max: 4 }} gap={4}>\n {items.map((it, i) => (\n <Card\n key={i}\n padding={5}\n height=\"100%\"\n className={`xicms-pricing__card${it.highlighted ? \" xicms-pricing__card--highlighted\" : \"\"}`}\n style={it.highlighted ? { outline: \"2px solid var(--color-accent)\", outlineOffset: -2 } : undefined}\n >\n <VStack gap={2} style={{ height: \"100%\" }}>\n <Text type=\"label\" color=\"secondary\" className=\"xicms-pricing__label\" data-xicms-edit={`items.${i}.label`} style={{ textTransform: \"uppercase\", letterSpacing: \"0.05em\" }}>\n {it.label}\n </Text>\n <VStack gap={0}>\n <Heading level={3} className=\"xicms-pricing__price\" data-xicms-edit={`items.${i}.price`} style={{ fontSize: \"clamp(1.75rem, 4vw, 2.25rem)\", lineHeight: 1.1, fontWeight: 700 }}>\n {it.price}\n </Heading>\n {it.billingPeriod && <Text type=\"supporting\" color=\"secondary\">{it.billingPeriod}</Text>}\n </VStack>\n {it.detail && <Text color=\"secondary\">{it.detail}</Text>}\n {it.features && it.features.length > 0 && (\n <VStack gap={1} as=\"ul\" style={{ listStyle: \"none\", margin: 0, padding: 0, flex: 1 }}>\n {it.features.map((f, j) => (\n // Echte <li> binnen de <ul> (geldige HTML; screenreaders kondigen de lijst aan).\n // Inline edit op een span BINNEN de tekst: anders zou het ✓-teken mee worden\n // teruggeschreven in de opgeslagen feature (zelfde patroon als Testimonials).\n <li key={j}>\n <Text display=\"block\">✓ <span data-xicms-edit={`items.${i}.features.${j}`}>{f}</span></Text>\n </li>\n ))}\n </VStack>\n )}\n {it.ctaLabel && (\n <Btn label={it.ctaLabel} href={it.ctaHref ?? \"#\"} variant={it.highlighted ? \"primary\" : \"outline\"} />\n )}\n </VStack>\n </Card>\n ))}\n </Grid>\n {block.note && <Text color=\"secondary\" justify=\"center\" style={{ textAlign: \"center\" }}>{block.note}</Text>}\n {block.jsonLd && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: jsonLdHtml(pricingJsonLd(items, block.currency)) }}\n />\n )}\n </VStack>\n );\n}\n\nexport const pricingBlock = defineBlock({\n type: \"pricing\",\n schema: PricingBlockSchema,\n Render: PricingRender,\n label: \"Prijzen\",\n icon: \"💶\",\n defaults: () => ({\n heading: \"Prijzen\",\n items: [{ label: \"Pakket\", price: \"€ 00\", billingPeriod: \"/maand\", features: [], highlighted: false }],\n }),\n});\n"],"mappings":"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","names":[]}
|