@xiwebdesign/xicms-core 4.17.1 → 4.18.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 (81) hide show
  1. package/dist/XiCMSProvider.js +3 -1
  2. package/dist/XiCMSProvider.js.map +1 -1
  3. package/dist/admin/blockEditors/index.js +47 -14
  4. package/dist/admin/blockEditors/index.js.map +1 -1
  5. package/dist/admin/lib/compressImage.d.ts +4 -2
  6. package/dist/admin/lib/compressImage.js +7 -5
  7. package/dist/admin/lib/compressImage.js.map +1 -1
  8. package/dist/admin/lib/mediaUsage.d.ts +4 -3
  9. package/dist/admin/lib/mediaUsage.js +21 -10
  10. package/dist/admin/lib/mediaUsage.js.map +1 -1
  11. package/dist/admin/lib/upload.d.ts +4 -0
  12. package/dist/admin/lib/upload.js +26 -6
  13. package/dist/admin/lib/upload.js.map +1 -1
  14. package/dist/admin/media/MediaManager.js +23 -7
  15. package/dist/admin/media/MediaManager.js.map +1 -1
  16. package/dist/admin/media/MediaPicker.js +12 -2
  17. package/dist/admin/media/MediaPicker.js.map +1 -1
  18. package/dist/admin/settings/SettingsPanel.js +4 -1
  19. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  20. package/dist/admin/visual/VisualEditor.js +4 -1
  21. package/dist/admin/visual/VisualEditor.js.map +1 -1
  22. package/dist/blocks/builtins/Agenda.d.ts +6 -0
  23. package/dist/blocks/builtins/Agenda.js +14 -6
  24. package/dist/blocks/builtins/Agenda.js.map +1 -1
  25. package/dist/blocks/builtins/Breadcrumbs.js +5 -1
  26. package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
  27. package/dist/blocks/builtins/Cards.js +3 -2
  28. package/dist/blocks/builtins/Cards.js.map +1 -1
  29. package/dist/blocks/builtins/Contact.js +2 -1
  30. package/dist/blocks/builtins/Contact.js.map +1 -1
  31. package/dist/blocks/builtins/Faq.js +5 -4
  32. package/dist/blocks/builtins/Faq.js.map +1 -1
  33. package/dist/blocks/builtins/Gallery.js +2 -2
  34. package/dist/blocks/builtins/Gallery.js.map +1 -1
  35. package/dist/blocks/builtins/Jobs.d.ts +8 -1
  36. package/dist/blocks/builtins/Jobs.js +37 -21
  37. package/dist/blocks/builtins/Jobs.js.map +1 -1
  38. package/dist/blocks/builtins/MapBlock.js +31 -4
  39. package/dist/blocks/builtins/MapBlock.js.map +1 -1
  40. package/dist/blocks/builtins/PdfBlock.js +2 -3
  41. package/dist/blocks/builtins/PdfBlock.js.map +1 -1
  42. package/dist/blocks/builtins/Portfolio.js +2 -2
  43. package/dist/blocks/builtins/Portfolio.js.map +1 -1
  44. package/dist/blocks/builtins/Pricing.d.ts +2 -0
  45. package/dist/blocks/builtins/Pricing.js +18 -10
  46. package/dist/blocks/builtins/Pricing.js.map +1 -1
  47. package/dist/blocks/builtins/Sections.js +3 -2
  48. package/dist/blocks/builtins/Sections.js.map +1 -1
  49. package/dist/blocks/builtins/Slider.js +21 -3
  50. package/dist/blocks/builtins/Slider.js.map +1 -1
  51. package/dist/blocks/builtins/Testimonials.js +21 -18
  52. package/dist/blocks/builtins/Testimonials.js.map +1 -1
  53. package/dist/blocks/builtins/WhatsappChat.js +1 -1
  54. package/dist/blocks/builtins/WhatsappChat.js.map +1 -1
  55. package/dist/blocks/builtins/Widgets.js +2 -3
  56. package/dist/blocks/builtins/Widgets.js.map +1 -1
  57. package/dist/blocks/builtins/lib/ics.js +1 -1
  58. package/dist/blocks/builtins/lib/ics.js.map +1 -1
  59. package/dist/blocks/builtins/lib/openingHours.d.ts +4 -1
  60. package/dist/blocks/builtins/lib/openingHours.js +11 -5
  61. package/dist/blocks/builtins/lib/openingHours.js.map +1 -1
  62. package/dist/demo/demoStore.d.ts +11 -1
  63. package/dist/demo/demoStore.js +50 -0
  64. package/dist/demo/demoStore.js.map +1 -1
  65. package/dist/firebase/client.js +2 -1
  66. package/dist/firebase/client.js.map +1 -1
  67. package/dist/index.d.ts +1 -1
  68. package/dist/index.js +2 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/modules/builtins.js +1 -1
  71. package/dist/modules/builtins.js.map +1 -1
  72. package/dist/modules/consent/useConsent.js +15 -3
  73. package/dist/modules/consent/useConsent.js.map +1 -1
  74. package/dist/schema/blocks.d.ts +24 -3
  75. package/dist/schema/blocks.js +4 -1
  76. package/dist/schema/blocks.js.map +1 -1
  77. package/dist/server/seo.d.ts +2 -0
  78. package/dist/server/seo.js +1 -1
  79. package/dist/server/seo.js.map +1 -1
  80. package/dist/styles.css +35 -6
  81. package/package.json +1 -1
@@ -105,7 +105,7 @@ const countersBlock = defineBlock({
105
105
  defaults: () => ({ items: [{ value: "100", label: "Tevreden klanten", suffix: "+" }] })
106
106
  });
107
107
  function LogosRender({ block }) {
108
- const items = (block.items ?? []).filter((i) => i.src);
108
+ const items = (block.items ?? []).map((it, i) => ({ ...it, _i: i })).filter((i) => i.src);
109
109
  if (items.length === 0) return null;
110
110
  return /* @__PURE__ */ jsxs(VStack, { gap: 3, as: "section", hAlign: "center", className: "xicms-logos", children: [
111
111
  block.heading && /* @__PURE__ */ jsx(Text, { type: "label", color: "secondary", style: { textTransform: "uppercase", letterSpacing: "0.06em" }, children: block.heading }),
@@ -119,7 +119,8 @@ function LogosRender({ block }) {
119
119
  className: block.marquee ? "xicms-logos__row" : void 0,
120
120
  style: block.marquee ? void 0 : { flexWrap: "wrap" },
121
121
  "aria-hidden": ariaHidden || void 0,
122
- children: items.map((it, i) => {
122
+ children: items.map((it) => {
123
+ const i = it._i;
123
124
  const img = /* @__PURE__ */ jsx(
124
125
  Img,
125
126
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/Sections.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport type { z } from \"zod\";\nimport {\n FeatureBlockSchema,\n CountersBlockSchema,\n LogosBlockSchema,\n TimelineBlockSchema,\n} from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { VStack, HStack, Grid, Heading, Text, Btn } from \"../ui\";\nimport { Img } from \"../../media/ImageContext\";\n\n/* ── Feature (beeld + tekst) ─────────────────────────────────────────────── */\ntype Feature = z.infer<typeof FeatureBlockSchema>;\nfunction FeatureRender({ block }: BlockRenderProps<Feature>) {\n const media = block.imageUrl ? (\n <Img\n src={block.imageUrl}\n alt=\"\"\n width={900}\n height={700}\n sizes=\"(max-width: 720px) 100vw, 50vw\"\n editField=\"imageUrl\"\n style={{ width: \"100%\", height: \"100%\", maxHeight: 460, objectFit: \"cover\", borderRadius: \"var(--radius-container)\" }}\n />\n ) : null;\n const body = (\n <VStack gap={3} vAlign=\"center\">\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n {block.html && (\n // eslint-disable-next-line react/no-danger\n <div className=\"xicms-richtext\" dangerouslySetInnerHTML={{ __html: block.html }} />\n )}\n {block.buttonLabel && (\n <div><Btn label={block.buttonLabel} href={block.buttonHref ?? \"#\"} variant=\"primary\" /></div>\n )}\n </VStack>\n );\n return (\n <Grid columns={{ minWidth: 320, max: 2 }} gap={5} className=\"xicms-feature\">\n {block.imagePosition === \"left\" ? (\n <>\n {media}\n {body}\n </>\n ) : (\n <>\n {body}\n {media}\n </>\n )}\n </Grid>\n );\n}\nexport const featureBlock = defineBlock({\n type: \"feature\",\n schema: FeatureBlockSchema,\n Render: FeatureRender,\n label: \"Beeld + tekst\",\n icon: \"🧱\",\n defaults: () => ({ heading: \"Kop\", html: \"<p>Tekst…</p>\", imagePosition: \"left\" }),\n});\n\n/* ── Counters (tellers) ──────────────────────────────────────────────────── */\ntype Counters = z.infer<typeof CountersBlockSchema>;\n\n/**\n * Telt van 0 naar `value` (rAF, ease-out) zodra het element in beeld scrolt. Niet-numerieke\n * waarden en reduced-motion tonen direct de eindwaarde; decimalen volgen de invoer (\"4.9\" → 1 dec).\n *\n * HYDRATION-VEILIG: de starttekst is ALTIJD `value` (server en client identiek); pas in het\n * effect (client-only) springt hij naar \"0\" en start de animatie. De vorige opzet initialiseerde\n * client-side op \"0\" → server \"13\" ≠ client \"0\" ≠ hydration-mismatch op elke tellers-pagina.\n */\nfunction CountUpValue({ value, animate }: { value: string; animate: boolean }) {\n const target = Number(value.replace(\",\", \".\"));\n const decimals = /[.,](\\d+)/.exec(value)?.[1].length ?? 0;\n const [display, setDisplay] = useState(value);\n const ref = useRef<HTMLSpanElement>(null);\n const started = useRef(false);\n\n useEffect(() => {\n const canAnimate =\n animate && Number.isFinite(target) && !window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches;\n if (!canAnimate || !ref.current) {\n // Volg latere waarde-wijzigingen (bv. bewerken in de editor-preview) ook zonder animatie.\n setDisplay(value);\n return;\n }\n const el = ref.current;\n let raf = 0;\n const obs = new IntersectionObserver((entries) => {\n if (!entries.some((e) => e.isIntersecting) || started.current) return;\n started.current = true;\n obs.disconnect();\n const t0 = performance.now();\n const duration = 1200;\n const tick = (t: number) => {\n const p = Math.min(1, (t - t0) / duration);\n const eased = 1 - Math.pow(1 - p, 3);\n setDisplay(p < 1 ? (target * eased).toFixed(decimals).replace(\".\", \",\") : value);\n if (p < 1) raf = requestAnimationFrame(tick);\n };\n raf = requestAnimationFrame(tick);\n });\n obs.observe(el);\n return () => {\n obs.disconnect();\n cancelAnimationFrame(raf);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [animate, target, decimals, value]);\n\n return <span ref={ref}>{display}</span>;\n}\n\nfunction CountersRender({ block, mode }: BlockRenderProps<Counters>) {\n const items = (block.items ?? []).filter((i) => i.value || i.label);\n if (items.length === 0) return null;\n return (\n <VStack gap={4} as=\"section\" className=\"xicms-counters\">\n {block.heading && (\n <Heading level={2} justify=\"center\" data-xicms-edit=\"heading\" style={{ textAlign: \"center\" }}>{block.heading}</Heading>\n )}\n <Grid columns={{ minWidth: 150 }} gap={4}>\n {items.map((it, i) => (\n <VStack key={i} gap={0.5} hAlign=\"center\" style={{ textAlign: \"center\" }}>\n <Heading level={2} className=\"xicms-counters__value\">\n {it.prefix}\n {/* In de editor-preview geen animatie (mode-contract: licht en voorspelbaar). */}\n <CountUpValue value={it.value} animate={block.animate !== false && mode !== \"preview\"} />\n {it.suffix}\n </Heading>\n <Text color=\"secondary\">{it.label}</Text>\n </VStack>\n ))}\n </Grid>\n </VStack>\n );\n}\nexport const countersBlock = defineBlock({\n type: \"counters\",\n schema: CountersBlockSchema,\n Render: CountersRender,\n label: \"Tellers\",\n icon: \"🔢\",\n defaults: () => ({ items: [{ value: \"100\", label: \"Tevreden klanten\", suffix: \"+\" }] }),\n});\n\n/* ── Logos (partner-/logostrip) ──────────────────────────────────────────── */\ntype Logos = z.infer<typeof LogosBlockSchema>;\nfunction LogosRender({ block }: BlockRenderProps<Logos>) {\n const items = (block.items ?? []).filter((i) => i.src);\n if (items.length === 0) return null;\n return (\n <VStack gap={3} as=\"section\" hAlign=\"center\" className=\"xicms-logos\">\n {block.heading && (\n <Text type=\"label\" color=\"secondary\" style={{ textTransform: \"uppercase\", letterSpacing: \"0.06em\" }}>\n {block.heading}\n </Text>\n )}\n {(() => {\n const row = (ariaHidden: boolean) => (\n <HStack\n gap={6}\n hAlign=\"center\"\n vAlign=\"center\"\n className={block.marquee ? \"xicms-logos__row\" : undefined}\n style={block.marquee ? undefined : { flexWrap: \"wrap\" }}\n aria-hidden={ariaHidden || undefined}\n >\n {items.map((it, i) => {\n const img = (\n <Img\n src={it.src}\n alt={ariaHidden ? \"\" : it.alt}\n width={160}\n height={48}\n editField={ariaHidden ? undefined : `items.${i}.src`}\n style={{ height: 40, width: \"auto\", objectFit: \"contain\", filter: block.grayscale ? \"grayscale(1)\" : undefined, opacity: block.grayscale ? 0.7 : 1 }}\n />\n );\n return it.href && !ariaHidden ? (\n <a key={i} href={it.href} rel=\"noopener noreferrer\" target=\"_blank\">{img}</a>\n ) : (\n <span key={i}>{img}</span>\n );\n })}\n </HStack>\n );\n if (!block.marquee) return row(false);\n // Doorlopende band: de rij + een aria-hidden kopie schuiven samen 50% op (naadloze loop).\n // CSS pauzeert bij hover en staat stil bij prefers-reduced-motion.\n return (\n <div className=\"xicms-logos__marquee\">\n <div className=\"xicms-logos__track\">\n {row(false)}\n {row(true)}\n </div>\n </div>\n );\n })()}\n </VStack>\n );\n}\nexport const logosBlock = defineBlock({\n type: \"logos\",\n schema: LogosBlockSchema,\n Render: LogosRender,\n label: \"Logo's\",\n icon: \"🏷️\",\n defaults: () => ({ items: [], grayscale: true }),\n});\n\n/* ── Timeline (stappen) ──────────────────────────────────────────────────── */\ntype Timeline = z.infer<typeof TimelineBlockSchema>;\nfunction TimelineRender({ block }: BlockRenderProps<Timeline>) {\n const items = (block.items ?? []).filter((i) => i.title);\n if (items.length === 0) return null;\n return (\n <VStack gap={4} as=\"section\" className={`xicms-timeline${block.alternate ? \" xicms-timeline--alternate\" : \"\"}`}>\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n <ol className=\"xicms-timeline__list\">\n {items.map((it, i) => (\n <li key={i} className=\"xicms-timeline__item\">\n <span className=\"xicms-timeline__marker\" aria-hidden>{it.icon || \"●\"}</span>\n <VStack gap={0.5} className=\"xicms-timeline__body\">\n {it.date && (\n <Text type=\"label\" style={{ color: \"var(--color-accent)\" }}>{it.date}</Text>\n )}\n <Heading level={3}>{it.title}</Heading>\n {it.text && <Text color=\"secondary\">{it.text}</Text>}\n </VStack>\n </li>\n ))}\n </ol>\n </VStack>\n );\n}\nexport const timelineBlock = defineBlock({\n type: \"timeline\",\n schema: TimelineBlockSchema,\n Render: TimelineRender,\n label: \"Tijdlijn\",\n icon: \"📍\",\n defaults: () => ({ items: [{ title: \"Stap 1\", text: \"\" }] }),\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/Sections.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport type { z } from \"zod\";\nimport {\n FeatureBlockSchema,\n CountersBlockSchema,\n LogosBlockSchema,\n TimelineBlockSchema,\n} from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { VStack, HStack, Grid, Heading, Text, Btn } from \"../ui\";\nimport { Img } from \"../../media/ImageContext\";\n\n/* ── Feature (beeld + tekst) ─────────────────────────────────────────────── */\ntype Feature = z.infer<typeof FeatureBlockSchema>;\nfunction FeatureRender({ block }: BlockRenderProps<Feature>) {\n const media = block.imageUrl ? (\n <Img\n src={block.imageUrl}\n alt=\"\"\n width={900}\n height={700}\n sizes=\"(max-width: 720px) 100vw, 50vw\"\n editField=\"imageUrl\"\n style={{ width: \"100%\", height: \"100%\", maxHeight: 460, objectFit: \"cover\", borderRadius: \"var(--radius-container)\" }}\n />\n ) : null;\n const body = (\n <VStack gap={3} vAlign=\"center\">\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n {block.html && (\n // eslint-disable-next-line react/no-danger\n <div className=\"xicms-richtext\" dangerouslySetInnerHTML={{ __html: block.html }} />\n )}\n {block.buttonLabel && (\n <div><Btn label={block.buttonLabel} href={block.buttonHref ?? \"#\"} variant=\"primary\" /></div>\n )}\n </VStack>\n );\n return (\n <Grid columns={{ minWidth: 320, max: 2 }} gap={5} className=\"xicms-feature\">\n {block.imagePosition === \"left\" ? (\n <>\n {media}\n {body}\n </>\n ) : (\n <>\n {body}\n {media}\n </>\n )}\n </Grid>\n );\n}\nexport const featureBlock = defineBlock({\n type: \"feature\",\n schema: FeatureBlockSchema,\n Render: FeatureRender,\n label: \"Beeld + tekst\",\n icon: \"🧱\",\n defaults: () => ({ heading: \"Kop\", html: \"<p>Tekst…</p>\", imagePosition: \"left\" }),\n});\n\n/* ── Counters (tellers) ──────────────────────────────────────────────────── */\ntype Counters = z.infer<typeof CountersBlockSchema>;\n\n/**\n * Telt van 0 naar `value` (rAF, ease-out) zodra het element in beeld scrolt. Niet-numerieke\n * waarden en reduced-motion tonen direct de eindwaarde; decimalen volgen de invoer (\"4.9\" → 1 dec).\n *\n * HYDRATION-VEILIG: de starttekst is ALTIJD `value` (server en client identiek); pas in het\n * effect (client-only) springt hij naar \"0\" en start de animatie. De vorige opzet initialiseerde\n * client-side op \"0\" → server \"13\" ≠ client \"0\" ≠ hydration-mismatch op elke tellers-pagina.\n */\nfunction CountUpValue({ value, animate }: { value: string; animate: boolean }) {\n const target = Number(value.replace(\",\", \".\"));\n const decimals = /[.,](\\d+)/.exec(value)?.[1].length ?? 0;\n const [display, setDisplay] = useState(value);\n const ref = useRef<HTMLSpanElement>(null);\n const started = useRef(false);\n\n useEffect(() => {\n const canAnimate =\n animate && Number.isFinite(target) && !window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches;\n if (!canAnimate || !ref.current) {\n // Volg latere waarde-wijzigingen (bv. bewerken in de editor-preview) ook zonder animatie.\n setDisplay(value);\n return;\n }\n const el = ref.current;\n let raf = 0;\n const obs = new IntersectionObserver((entries) => {\n if (!entries.some((e) => e.isIntersecting) || started.current) return;\n started.current = true;\n obs.disconnect();\n const t0 = performance.now();\n const duration = 1200;\n const tick = (t: number) => {\n const p = Math.min(1, (t - t0) / duration);\n const eased = 1 - Math.pow(1 - p, 3);\n setDisplay(p < 1 ? (target * eased).toFixed(decimals).replace(\".\", \",\") : value);\n if (p < 1) raf = requestAnimationFrame(tick);\n };\n raf = requestAnimationFrame(tick);\n });\n obs.observe(el);\n return () => {\n obs.disconnect();\n cancelAnimationFrame(raf);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [animate, target, decimals, value]);\n\n return <span ref={ref}>{display}</span>;\n}\n\nfunction CountersRender({ block, mode }: BlockRenderProps<Counters>) {\n const items = (block.items ?? []).filter((i) => i.value || i.label);\n if (items.length === 0) return null;\n return (\n <VStack gap={4} as=\"section\" className=\"xicms-counters\">\n {block.heading && (\n <Heading level={2} justify=\"center\" data-xicms-edit=\"heading\" style={{ textAlign: \"center\" }}>{block.heading}</Heading>\n )}\n <Grid columns={{ minWidth: 150 }} gap={4}>\n {items.map((it, i) => (\n <VStack key={i} gap={0.5} hAlign=\"center\" style={{ textAlign: \"center\" }}>\n <Heading level={2} className=\"xicms-counters__value\">\n {it.prefix}\n {/* In de editor-preview geen animatie (mode-contract: licht en voorspelbaar). */}\n <CountUpValue value={it.value} animate={block.animate !== false && mode !== \"preview\"} />\n {it.suffix}\n </Heading>\n <Text color=\"secondary\">{it.label}</Text>\n </VStack>\n ))}\n </Grid>\n </VStack>\n );\n}\nexport const countersBlock = defineBlock({\n type: \"counters\",\n schema: CountersBlockSchema,\n Render: CountersRender,\n label: \"Tellers\",\n icon: \"🔢\",\n defaults: () => ({ items: [{ value: \"100\", label: \"Tevreden klanten\", suffix: \"+\" }] }),\n});\n\n/* ── Logos (partner-/logostrip) ──────────────────────────────────────────── */\ntype Logos = z.infer<typeof LogosBlockSchema>;\nfunction LogosRender({ block }: BlockRenderProps<Logos>) {\n // _i = index in de OORSPRONKELIJKE array — nodig voor het inline vervang-pad (items.N.src).\n const items = (block.items ?? []).map((it, i) => ({ ...it, _i: i })).filter((i) => i.src);\n if (items.length === 0) return null;\n return (\n <VStack gap={3} as=\"section\" hAlign=\"center\" className=\"xicms-logos\">\n {block.heading && (\n <Text type=\"label\" color=\"secondary\" style={{ textTransform: \"uppercase\", letterSpacing: \"0.06em\" }}>\n {block.heading}\n </Text>\n )}\n {(() => {\n const row = (ariaHidden: boolean) => (\n <HStack\n gap={6}\n hAlign=\"center\"\n vAlign=\"center\"\n className={block.marquee ? \"xicms-logos__row\" : undefined}\n style={block.marquee ? undefined : { flexWrap: \"wrap\" }}\n aria-hidden={ariaHidden || undefined}\n >\n {items.map((it) => {\n const i = it._i;\n const img = (\n <Img\n src={it.src}\n alt={ariaHidden ? \"\" : it.alt}\n width={160}\n height={48}\n editField={ariaHidden ? undefined : `items.${i}.src`}\n style={{ height: 40, width: \"auto\", objectFit: \"contain\", filter: block.grayscale ? \"grayscale(1)\" : undefined, opacity: block.grayscale ? 0.7 : 1 }}\n />\n );\n return it.href && !ariaHidden ? (\n <a key={i} href={it.href} rel=\"noopener noreferrer\" target=\"_blank\">{img}</a>\n ) : (\n <span key={i}>{img}</span>\n );\n })}\n </HStack>\n );\n if (!block.marquee) return row(false);\n // Doorlopende band: de rij + een aria-hidden kopie schuiven samen 50% op (naadloze loop).\n // CSS pauzeert bij hover en staat stil bij prefers-reduced-motion.\n return (\n <div className=\"xicms-logos__marquee\">\n <div className=\"xicms-logos__track\">\n {row(false)}\n {row(true)}\n </div>\n </div>\n );\n })()}\n </VStack>\n );\n}\nexport const logosBlock = defineBlock({\n type: \"logos\",\n schema: LogosBlockSchema,\n Render: LogosRender,\n label: \"Logo's\",\n icon: \"🏷️\",\n defaults: () => ({ items: [], grayscale: true }),\n});\n\n/* ── Timeline (stappen) ──────────────────────────────────────────────────── */\ntype Timeline = z.infer<typeof TimelineBlockSchema>;\nfunction TimelineRender({ block }: BlockRenderProps<Timeline>) {\n const items = (block.items ?? []).filter((i) => i.title);\n if (items.length === 0) return null;\n return (\n <VStack gap={4} as=\"section\" className={`xicms-timeline${block.alternate ? \" xicms-timeline--alternate\" : \"\"}`}>\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n <ol className=\"xicms-timeline__list\">\n {items.map((it, i) => (\n <li key={i} className=\"xicms-timeline__item\">\n <span className=\"xicms-timeline__marker\" aria-hidden>{it.icon || \"●\"}</span>\n <VStack gap={0.5} className=\"xicms-timeline__body\">\n {it.date && (\n <Text type=\"label\" style={{ color: \"var(--color-accent)\" }}>{it.date}</Text>\n )}\n <Heading level={3}>{it.title}</Heading>\n {it.text && <Text color=\"secondary\">{it.text}</Text>}\n </VStack>\n </li>\n ))}\n </ol>\n </VStack>\n );\n}\nexport const timelineBlock = defineBlock({\n type: \"timeline\",\n schema: TimelineBlockSchema,\n Render: TimelineRender,\n label: \"Tijdlijn\",\n icon: \"📍\",\n defaults: () => ({ items: [{ title: \"Stap 1\", text: \"\" }] }),\n});\n"],"mappings":"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","names":[]}
@@ -11,16 +11,17 @@ function SliderRender({ block }) {
11
11
  const slides = (block.slides ?? []).filter((s) => s.imageUrl || s.caption);
12
12
  const [index, setIndex] = useState(0);
13
13
  const [paused, setPaused] = useState(false);
14
+ const [userPaused, setUserPaused] = useState(false);
14
15
  const swipeStart = useRef(null);
15
16
  const count = slides.length;
16
17
  useEffect(() => {
17
18
  setIndex((i) => count === 0 ? 0 : Math.min(i, count - 1));
18
19
  }, [count]);
19
20
  useEffect(() => {
20
- if (!block.autoplay || paused || count <= 1 || prefersReducedMotion()) return;
21
+ if (!block.autoplay || paused || userPaused || count <= 1 || prefersReducedMotion()) return;
21
22
  const id = setInterval(() => setIndex((i) => (i + 1) % count), block.interval * 1e3);
22
23
  return () => clearInterval(id);
23
- }, [block.autoplay, block.interval, paused, count]);
24
+ }, [block.autoplay, block.interval, paused, userPaused, count]);
24
25
  if (count === 0) return null;
25
26
  const go = (i) => setIndex((i % count + count) % count);
26
27
  const onPointerDown = (e) => {
@@ -63,6 +64,12 @@ function SliderRender({ block }) {
63
64
  className: "xicms-slider__viewport",
64
65
  onPointerDown,
65
66
  onPointerUp,
67
+ onPointerCancel: () => {
68
+ swipeStart.current = null;
69
+ },
70
+ onPointerLeave: () => {
71
+ swipeStart.current = null;
72
+ },
66
73
  style: { touchAction: "pan-y" },
67
74
  children: [
68
75
  /* @__PURE__ */ jsx("div", { className: "xicms-slider__track", style: { transform: `translateX(-${index * 100}%)` }, children: slides.map((s, i) => {
@@ -91,7 +98,18 @@ function SliderRender({ block }) {
91
98
  block.showArrows && count > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
92
99
  /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-slider__arrow xicms-slider__arrow--prev", "aria-label": "Vorige", onClick: () => go(index - 1), children: "\u2039" }),
93
100
  /* @__PURE__ */ jsx("button", { type: "button", className: "xicms-slider__arrow xicms-slider__arrow--next", "aria-label": "Volgende", onClick: () => go(index + 1), children: "\u203A" })
94
- ] })
101
+ ] }),
102
+ block.autoplay && count > 1 && /* @__PURE__ */ jsx(
103
+ "button",
104
+ {
105
+ type: "button",
106
+ className: "xicms-slider__pause",
107
+ "aria-pressed": userPaused,
108
+ "aria-label": userPaused ? "Diavoorstelling afspelen" : "Diavoorstelling pauzeren",
109
+ onClick: () => setUserPaused((v) => !v),
110
+ children: userPaused ? "\u25B6" : "\u23F8"
111
+ }
112
+ )
95
113
  ]
96
114
  }
97
115
  ),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/Slider.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { SliderBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { Img } from \"../../media/ImageContext\";\n\ntype Slider = z.infer<typeof SliderBlockSchema>;\n\nfunction prefersReducedMotion(): boolean {\n return typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n/** Beeld-/contentcarrousel. Client-side; autoplay respecteert reduced-motion en pauzeert bij hover. */\nfunction SliderRender({ block }: BlockRenderProps<Slider>) {\n const slides = (block.slides ?? []).filter((s) => s.imageUrl || s.caption);\n const [index, setIndex] = useState(0);\n const [paused, setPaused] = useState(false);\n const swipeStart = useRef<number | null>(null);\n const count = slides.length;\n\n // Clamp de index als het aantal slides krimpt (bv. tijdens bewerken in preview), anders blijft\n // de carrousel op een lege positie hangen tot de bezoeker interacteert.\n useEffect(() => {\n setIndex((i) => (count === 0 ? 0 : Math.min(i, count - 1)));\n }, [count]);\n\n useEffect(() => {\n if (!block.autoplay || paused || count <= 1 || prefersReducedMotion()) return;\n const id = setInterval(() => setIndex((i) => (i + 1) % count), block.interval * 1000);\n return () => clearInterval(id);\n }, [block.autoplay, block.interval, paused, count]);\n\n if (count === 0) return null;\n const go = (i: number) => setIndex(((i % count) + count) % count);\n\n // Swipe (pointer events: touch én muis-sleep) + pijltjestoetsen wanneer de carrousel focus heeft.\n const onPointerDown = (e: React.PointerEvent) => {\n swipeStart.current = e.clientX;\n };\n const onPointerUp = (e: React.PointerEvent) => {\n if (swipeStart.current === null || count < 2) return;\n const dx = e.clientX - swipeStart.current;\n swipeStart.current = null;\n if (dx > 40) go(index - 1);\n else if (dx < -40) go(index + 1);\n };\n const onKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"ArrowLeft\") {\n e.preventDefault();\n go(index - 1);\n } else if (e.key === \"ArrowRight\") {\n e.preventDefault();\n go(index + 1);\n }\n };\n\n return (\n <section\n className=\"xicms-slider\"\n style={{ [\"--xicms-slider-ratio\" as string]: block.aspectRatio.replace(\"/\", \" / \") }}\n onMouseEnter={() => setPaused(true)}\n onMouseLeave={() => setPaused(false)}\n onFocus={() => setPaused(true)}\n onBlur={() => setPaused(false)}\n onKeyDown={onKeyDown}\n tabIndex={0}\n role=\"region\"\n aria-roledescription=\"carousel\"\n aria-label=\"Carrousel\"\n >\n <div\n className=\"xicms-slider__viewport\"\n onPointerDown={onPointerDown}\n onPointerUp={onPointerUp}\n style={{ touchAction: \"pan-y\" }}\n >\n <div className=\"xicms-slider__track\" style={{ transform: `translateX(-${index * 100}%)` }}>\n {slides.map((s, i) => {\n const img = s.imageUrl ? (\n <Img className=\"xicms-slider__image\" src={s.imageUrl} alt={s.alt ?? \"\"} width={1800} height={800} sizes=\"100vw\" priority={i === 0} />\n ) : null;\n return (\n /* inert haalt links in verborgen slides ook uit de tabvolgorde — aria-hidden\n mét bereikbare focusables is een WCAG-schending (4.1.2). */\n <div\n key={i}\n className=\"xicms-slider__slide\"\n role=\"group\"\n aria-roledescription=\"slide\"\n aria-label={`${i + 1} / ${count}`}\n aria-hidden={i !== index}\n inert={i !== index || undefined}\n >\n {s.href ? <a href={s.href}>{img}</a> : img}\n {s.caption && <div className=\"xicms-slider__caption\">{s.caption}</div>}\n </div>\n );\n })}\n </div>\n\n {block.showArrows && count > 1 && (\n <>\n <button type=\"button\" className=\"xicms-slider__arrow xicms-slider__arrow--prev\" aria-label=\"Vorige\" onClick={() => go(index - 1)}>‹</button>\n <button type=\"button\" className=\"xicms-slider__arrow xicms-slider__arrow--next\" aria-label=\"Volgende\" onClick={() => go(index + 1)}>›</button>\n </>\n )}\n </div>\n\n {/* Dots als gewone knoppen (geen tablist: er zijn geen tabpanels om aan te koppelen). */}\n {block.showDots && count > 1 && (\n <div className=\"xicms-slider__dots\" role=\"group\" aria-label=\"Kies slide\">\n {slides.map((_, i) => (\n <button\n key={i}\n type=\"button\"\n aria-current={i === index || undefined}\n aria-label={`Naar slide ${i + 1}`}\n className={`xicms-slider__dot${i === index ? \" xicms-slider__dot--active\" : \"\"}`}\n onClick={() => go(i)}\n />\n ))}\n </div>\n )}\n </section>\n );\n}\n\nexport const sliderBlock = defineBlock({\n type: \"slider\",\n schema: SliderBlockSchema,\n Render: SliderRender,\n label: \"Carrousel / slider\",\n icon: \"🎠\",\n defaults: () => ({\n slides: [{ imageUrl: \"\", alt: \"\", caption: \"\" }],\n autoplay: false,\n interval: 5,\n showDots: true,\n showArrows: true,\n aspectRatio: \"16/9\",\n }),\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/Slider.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { SliderBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { Img } from \"../../media/ImageContext\";\n\ntype Slider = z.infer<typeof SliderBlockSchema>;\n\nfunction prefersReducedMotion(): boolean {\n return typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n/** Beeld-/contentcarrousel. Client-side; autoplay respecteert reduced-motion en pauzeert bij hover. */\nfunction SliderRender({ block }: BlockRenderProps<Slider>) {\n const slides = (block.slides ?? []).filter((s) => s.imageUrl || s.caption);\n const [index, setIndex] = useState(0);\n const [paused, setPaused] = useState(false);\n // Expliciete stopknop voor autoplay: hover-/focus-pauze helpt touch- en screenreadergebruikers\n // niet, dus WCAG (2.2.2, bewegende content) vraagt een echte pauzeknop.\n const [userPaused, setUserPaused] = useState(false);\n const swipeStart = useRef<number | null>(null);\n const count = slides.length;\n\n // Clamp de index als het aantal slides krimpt (bv. tijdens bewerken in preview), anders blijft\n // de carrousel op een lege positie hangen tot de bezoeker interacteert.\n useEffect(() => {\n setIndex((i) => (count === 0 ? 0 : Math.min(i, count - 1)));\n }, [count]);\n\n useEffect(() => {\n if (!block.autoplay || paused || userPaused || count <= 1 || prefersReducedMotion()) return;\n const id = setInterval(() => setIndex((i) => (i + 1) % count), block.interval * 1000);\n return () => clearInterval(id);\n }, [block.autoplay, block.interval, paused, userPaused, count]);\n\n if (count === 0) return null;\n const go = (i: number) => setIndex(((i % count) + count) % count);\n\n // Swipe (pointer events: touch én muis-sleep) + pijltjestoetsen wanneer de carrousel focus heeft.\n const onPointerDown = (e: React.PointerEvent) => {\n swipeStart.current = e.clientX;\n };\n const onPointerUp = (e: React.PointerEvent) => {\n if (swipeStart.current === null || count < 2) return;\n const dx = e.clientX - swipeStart.current;\n swipeStart.current = null;\n if (dx > 40) go(index - 1);\n else if (dx < -40) go(index + 1);\n };\n const onKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"ArrowLeft\") {\n e.preventDefault();\n go(index - 1);\n } else if (e.key === \"ArrowRight\") {\n e.preventDefault();\n go(index + 1);\n }\n };\n\n return (\n <section\n className=\"xicms-slider\"\n style={{ [\"--xicms-slider-ratio\" as string]: block.aspectRatio.replace(\"/\", \" / \") }}\n onMouseEnter={() => setPaused(true)}\n onMouseLeave={() => setPaused(false)}\n onFocus={() => setPaused(true)}\n onBlur={() => setPaused(false)}\n onKeyDown={onKeyDown}\n tabIndex={0}\n role=\"region\"\n aria-roledescription=\"carousel\"\n aria-label=\"Carrousel\"\n >\n <div\n className=\"xicms-slider__viewport\"\n onPointerDown={onPointerDown}\n onPointerUp={onPointerUp}\n onPointerCancel={() => { swipeStart.current = null; }}\n onPointerLeave={() => { swipeStart.current = null; }}\n style={{ touchAction: \"pan-y\" }}\n >\n <div className=\"xicms-slider__track\" style={{ transform: `translateX(-${index * 100}%)` }}>\n {slides.map((s, i) => {\n const img = s.imageUrl ? (\n <Img className=\"xicms-slider__image\" src={s.imageUrl} alt={s.alt ?? \"\"} width={1800} height={800} sizes=\"100vw\" priority={i === 0} />\n ) : null;\n return (\n /* inert haalt links in verborgen slides ook uit de tabvolgorde — aria-hidden\n mét bereikbare focusables is een WCAG-schending (4.1.2). */\n <div\n key={i}\n className=\"xicms-slider__slide\"\n role=\"group\"\n aria-roledescription=\"slide\"\n aria-label={`${i + 1} / ${count}`}\n aria-hidden={i !== index}\n inert={i !== index || undefined}\n >\n {s.href ? <a href={s.href}>{img}</a> : img}\n {s.caption && <div className=\"xicms-slider__caption\">{s.caption}</div>}\n </div>\n );\n })}\n </div>\n\n {block.showArrows && count > 1 && (\n <>\n <button type=\"button\" className=\"xicms-slider__arrow xicms-slider__arrow--prev\" aria-label=\"Vorige\" onClick={() => go(index - 1)}>‹</button>\n <button type=\"button\" className=\"xicms-slider__arrow xicms-slider__arrow--next\" aria-label=\"Volgende\" onClick={() => go(index + 1)}>›</button>\n </>\n )}\n {/* Geen reduced-motion-check hier: die zou server en client verschillend laten renderen. */}\n {block.autoplay && count > 1 && (\n <button\n type=\"button\"\n className=\"xicms-slider__pause\"\n aria-pressed={userPaused}\n aria-label={userPaused ? \"Diavoorstelling afspelen\" : \"Diavoorstelling pauzeren\"}\n onClick={() => setUserPaused((v) => !v)}\n >\n {userPaused ? \"▶\" : \"⏸\"}\n </button>\n )}\n </div>\n\n {/* Dots als gewone knoppen (geen tablist: er zijn geen tabpanels om aan te koppelen). */}\n {block.showDots && count > 1 && (\n <div className=\"xicms-slider__dots\" role=\"group\" aria-label=\"Kies slide\">\n {slides.map((_, i) => (\n <button\n key={i}\n type=\"button\"\n aria-current={i === index || undefined}\n aria-label={`Naar slide ${i + 1}`}\n className={`xicms-slider__dot${i === index ? \" xicms-slider__dot--active\" : \"\"}`}\n onClick={() => go(i)}\n />\n ))}\n </div>\n )}\n </section>\n );\n}\n\nexport const sliderBlock = defineBlock({\n type: \"slider\",\n schema: SliderBlockSchema,\n Render: SliderRender,\n label: \"Carrousel / slider\",\n icon: \"🎠\",\n defaults: () => ({\n slides: [{ imageUrl: \"\", alt: \"\", caption: \"\" }],\n autoplay: false,\n interval: 5,\n showDots: true,\n showArrows: true,\n aspectRatio: \"16/9\",\n }),\n});\n"],"mappings":"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","names":[]}
@@ -30,29 +30,32 @@ function reviewJsonLd(items) {
30
30
  };
31
31
  }
32
32
  function TestimonialsRender({ block }) {
33
- const items = (block.items ?? []).filter((i) => i.quote);
33
+ const items = (block.items ?? []).map((it, i) => ({ ...it, _i: i })).filter((i) => i.quote);
34
34
  if (items.length === 0) return null;
35
35
  const cols = block.layout === "list" ? { minWidth: 320, max: 1 } : { minWidth: 280, max: 3 };
36
36
  return /* @__PURE__ */ jsxs(VStack, { gap: 4, as: "section", className: "xicms-testimonials", children: [
37
37
  block.heading && /* @__PURE__ */ jsx(Heading, { level: 2, justify: "center", "data-xicms-edit": "heading", style: { textAlign: "center" }, children: block.heading }),
38
- /* @__PURE__ */ jsx(Grid, { columns: cols, gap: 4, children: items.map((it, i) => /* @__PURE__ */ jsx(Card, { padding: 4, height: "100%", className: "xicms-testimonials__card", children: /* @__PURE__ */ jsxs(VStack, { gap: 2, style: { height: "100%" }, children: [
39
- typeof it.rating === "number" && /* @__PURE__ */ jsxs(Text, { className: "xicms-testimonials__rating", style: { color: "var(--xicms-star, #f59e0b)", letterSpacing: "0.1em" }, "aria-label": `${it.rating} van 5 sterren`, children: [
40
- "\u2605".repeat(it.rating),
41
- /* @__PURE__ */ jsx("span", { style: { opacity: 0.25 }, children: "\u2605".repeat(5 - it.rating) })
42
- ] }),
43
- /* @__PURE__ */ jsxs(Text, { type: "large", className: "xicms-testimonials__quote", style: { flex: 1 }, children: [
44
- "\u201C",
45
- /* @__PURE__ */ jsx("span", { "data-xicms-edit": `items.${i}.quote`, children: it.quote }),
46
- "\u201D"
47
- ] }),
48
- /* @__PURE__ */ jsxs(HStack, { gap: 1.5, vAlign: "center", children: [
49
- it.avatarUrl && /* @__PURE__ */ jsx(Avatar, { src: it.avatarUrl, name: it.author, size: 40 }),
50
- /* @__PURE__ */ jsxs(VStack, { gap: 0, children: [
51
- /* @__PURE__ */ jsx(Text, { weight: "bold", className: "xicms-testimonials__author", "data-xicms-edit": `items.${i}.author`, children: it.author }),
52
- it.role && /* @__PURE__ */ jsx(Text, { type: "supporting", color: "secondary", className: "xicms-testimonials__role", "data-xicms-edit": `items.${i}.role`, children: it.role })
38
+ /* @__PURE__ */ jsx(Grid, { columns: cols, gap: 4, children: items.map((it) => {
39
+ const i = it._i;
40
+ return /* @__PURE__ */ jsx(Card, { padding: 4, height: "100%", className: "xicms-testimonials__card", children: /* @__PURE__ */ jsxs(VStack, { gap: 2, style: { height: "100%" }, children: [
41
+ typeof it.rating === "number" && /* @__PURE__ */ jsxs(Text, { className: "xicms-testimonials__rating", style: { color: "var(--xicms-star, #f59e0b)", letterSpacing: "0.1em" }, "aria-label": `${it.rating} van 5 sterren`, children: [
42
+ "\u2605".repeat(it.rating),
43
+ /* @__PURE__ */ jsx("span", { style: { opacity: 0.25 }, children: "\u2605".repeat(5 - it.rating) })
44
+ ] }),
45
+ /* @__PURE__ */ jsxs(Text, { type: "large", className: "xicms-testimonials__quote", style: { flex: 1 }, children: [
46
+ "\u201C",
47
+ /* @__PURE__ */ jsx("span", { "data-xicms-edit": `items.${i}.quote`, children: it.quote }),
48
+ "\u201D"
49
+ ] }),
50
+ /* @__PURE__ */ jsxs(HStack, { gap: 1.5, vAlign: "center", children: [
51
+ it.avatarUrl && /* @__PURE__ */ jsx(Avatar, { src: it.avatarUrl, name: it.author, size: 40 }),
52
+ /* @__PURE__ */ jsxs(VStack, { gap: 0, children: [
53
+ /* @__PURE__ */ jsx(Text, { weight: "bold", className: "xicms-testimonials__author", "data-xicms-edit": `items.${i}.author`, children: it.author }),
54
+ it.role && /* @__PURE__ */ jsx(Text, { type: "supporting", color: "secondary", className: "xicms-testimonials__role", "data-xicms-edit": `items.${i}.role`, children: it.role })
55
+ ] })
53
56
  ] })
54
- ] })
55
- ] }) }, i)) }),
57
+ ] }) }, i);
58
+ }) }),
56
59
  block.jsonLd && /* @__PURE__ */ jsx(
57
60
  "script",
58
61
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/Testimonials.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { TestimonialsBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { VStack, HStack, Grid, Card, Heading, Text, Avatar } from \"../ui\";\n\ntype Testimonials = z.infer<typeof TestimonialsBlockSchema>;\n\n/** Gemiddelde beoordeling over de items met een rating — voor de sterren-snippet in Google. */\nexport function aggregateRatingJsonLd(items: Testimonials[\"items\"]): Record<string, unknown> | null {\n const rated = items.filter((it) => typeof it.rating === \"number\");\n if (rated.length === 0) return null;\n const avg = rated.reduce((sum, it) => sum + (it.rating as number), 0) / rated.length;\n return {\n \"@type\": \"AggregateRating\",\n ratingValue: Math.round(avg * 10) / 10,\n reviewCount: rated.length,\n bestRating: 5,\n };\n}\n\nexport function reviewJsonLd(items: Testimonials[\"items\"]): Record<string, unknown> {\n const aggregate = aggregateRatingJsonLd(items);\n // Reviews/AggregateRating zijn voor rich results alleen geldig aan een \"ding\" (bedrijf/product);\n // een losse ItemList negeert Google. We hangen ze aan dezelfde #business-entiteit als de\n // contact-/openingstijden-JSON-LD, zodat alles samenvloeit tot één LocalBusiness.\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"LocalBusiness\",\n \"@id\": \"#business\",\n ...(aggregate ? { aggregateRating: aggregate } : {}),\n review: items\n .filter((it) => it.quote && it.author)\n .map((it) => ({\n \"@type\": \"Review\",\n reviewBody: it.quote,\n author: { \"@type\": \"Person\", name: it.author },\n ...(it.rating\n ? { reviewRating: { \"@type\": \"Rating\", ratingValue: it.rating, bestRating: 5 } }\n : {}),\n })),\n };\n}\n\n/** Klant­quotes / reviews als kaarten (Astryx). */\nfunction TestimonialsRender({ block }: BlockRenderProps<Testimonials>) {\n const items = (block.items ?? []).filter((i) => i.quote);\n if (items.length === 0) return null;\n const cols = block.layout === \"list\" ? { minWidth: 320, max: 1 as const } : { minWidth: 280, max: 3 as const };\n return (\n <VStack gap={4} as=\"section\" className=\"xicms-testimonials\">\n {block.heading && (\n <Heading level={2} justify=\"center\" data-xicms-edit=\"heading\" style={{ textAlign: \"center\" }}>{block.heading}</Heading>\n )}\n <Grid columns={cols} gap={4}>\n {items.map((it, i) => (\n <Card key={i} padding={4} height=\"100%\" className=\"xicms-testimonials__card\">\n <VStack gap={2} style={{ height: \"100%\" }}>\n {typeof it.rating === \"number\" && (\n <Text className=\"xicms-testimonials__rating\" style={{ color: \"var(--xicms-star, #f59e0b)\", letterSpacing: \"0.1em\" }} aria-label={`${it.rating} van 5 sterren`}>\n {\"★\".repeat(it.rating)}\n <span style={{ opacity: 0.25 }}>{\"★\".repeat(5 - it.rating)}</span>\n </Text>\n )}\n <Text type=\"large\" className=\"xicms-testimonials__quote\" style={{ flex: 1 }}>“<span data-xicms-edit={`items.${i}.quote`}>{it.quote}</span>”</Text>\n <HStack gap={1.5} vAlign=\"center\">\n {it.avatarUrl && <Avatar src={it.avatarUrl} name={it.author} size={40} />}\n <VStack gap={0}>\n <Text weight=\"bold\" className=\"xicms-testimonials__author\" data-xicms-edit={`items.${i}.author`}>{it.author}</Text>\n {it.role && <Text type=\"supporting\" color=\"secondary\" className=\"xicms-testimonials__role\" data-xicms-edit={`items.${i}.role`}>{it.role}</Text>}\n </VStack>\n </HStack>\n </VStack>\n </Card>\n ))}\n </Grid>\n {block.jsonLd && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: JSON.stringify(reviewJsonLd(items)) }}\n />\n )}\n </VStack>\n );\n}\n\nexport const testimonialsBlock = defineBlock({\n type: \"testimonials\",\n schema: TestimonialsBlockSchema,\n Render: TestimonialsRender,\n label: \"Reviews\",\n icon: \"⭐\",\n defaults: () => ({\n heading: \"Wat klanten zeggen\",\n items: [{ quote: \"Top geholpen!\", author: \"Tevreden klant\", rating: 5 }],\n jsonLd: true,\n }),\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/Testimonials.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { TestimonialsBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { VStack, HStack, Grid, Card, Heading, Text, Avatar } from \"../ui\";\n\ntype Testimonials = z.infer<typeof TestimonialsBlockSchema>;\n\n/** Gemiddelde beoordeling over de items met een rating — voor de sterren-snippet in Google. */\nexport function aggregateRatingJsonLd(items: Testimonials[\"items\"]): Record<string, unknown> | null {\n const rated = items.filter((it) => typeof it.rating === \"number\");\n if (rated.length === 0) return null;\n const avg = rated.reduce((sum, it) => sum + (it.rating as number), 0) / rated.length;\n return {\n \"@type\": \"AggregateRating\",\n ratingValue: Math.round(avg * 10) / 10,\n reviewCount: rated.length,\n bestRating: 5,\n };\n}\n\nexport function reviewJsonLd(items: Testimonials[\"items\"]): Record<string, unknown> {\n const aggregate = aggregateRatingJsonLd(items);\n // Reviews/AggregateRating zijn voor rich results alleen geldig aan een \"ding\" (bedrijf/product);\n // een losse ItemList negeert Google. We hangen ze aan dezelfde #business-entiteit als de\n // contact-/openingstijden-JSON-LD, zodat alles samenvloeit tot één LocalBusiness.\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"LocalBusiness\",\n \"@id\": \"#business\",\n ...(aggregate ? { aggregateRating: aggregate } : {}),\n review: items\n .filter((it) => it.quote && it.author)\n .map((it) => ({\n \"@type\": \"Review\",\n reviewBody: it.quote,\n author: { \"@type\": \"Person\", name: it.author },\n ...(it.rating\n ? { reviewRating: { \"@type\": \"Rating\", ratingValue: it.rating, bestRating: 5 } }\n : {}),\n })),\n };\n}\n\n/** Klant­quotes / reviews als kaarten (Astryx). */\nfunction TestimonialsRender({ block }: BlockRenderProps<Testimonials>) {\n // _i = index in de OORSPRONKELIJKE array — inline-edit-paden (items.N.quote) moeten daarheen wijzen.\n const items = (block.items ?? []).map((it, i) => ({ ...it, _i: i })).filter((i) => i.quote);\n if (items.length === 0) return null;\n const cols = block.layout === \"list\" ? { minWidth: 320, max: 1 as const } : { minWidth: 280, max: 3 as const };\n return (\n <VStack gap={4} as=\"section\" className=\"xicms-testimonials\">\n {block.heading && (\n <Heading level={2} justify=\"center\" data-xicms-edit=\"heading\" style={{ textAlign: \"center\" }}>{block.heading}</Heading>\n )}\n <Grid columns={cols} gap={4}>\n {items.map((it) => {\n const i = it._i;\n return (\n <Card key={i} padding={4} height=\"100%\" className=\"xicms-testimonials__card\">\n <VStack gap={2} style={{ height: \"100%\" }}>\n {typeof it.rating === \"number\" && (\n <Text className=\"xicms-testimonials__rating\" style={{ color: \"var(--xicms-star, #f59e0b)\", letterSpacing: \"0.1em\" }} aria-label={`${it.rating} van 5 sterren`}>\n {\"★\".repeat(it.rating)}\n <span style={{ opacity: 0.25 }}>{\"★\".repeat(5 - it.rating)}</span>\n </Text>\n )}\n <Text type=\"large\" className=\"xicms-testimonials__quote\" style={{ flex: 1 }}>“<span data-xicms-edit={`items.${i}.quote`}>{it.quote}</span>”</Text>\n <HStack gap={1.5} vAlign=\"center\">\n {it.avatarUrl && <Avatar src={it.avatarUrl} name={it.author} size={40} />}\n <VStack gap={0}>\n <Text weight=\"bold\" className=\"xicms-testimonials__author\" data-xicms-edit={`items.${i}.author`}>{it.author}</Text>\n {it.role && <Text type=\"supporting\" color=\"secondary\" className=\"xicms-testimonials__role\" data-xicms-edit={`items.${i}.role`}>{it.role}</Text>}\n </VStack>\n </HStack>\n </VStack>\n </Card>\n );\n })}\n </Grid>\n {block.jsonLd && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: JSON.stringify(reviewJsonLd(items)) }}\n />\n )}\n </VStack>\n );\n}\n\nexport const testimonialsBlock = defineBlock({\n type: \"testimonials\",\n schema: TestimonialsBlockSchema,\n Render: TestimonialsRender,\n label: \"Reviews\",\n icon: \"⭐\",\n defaults: () => ({\n heading: \"Wat klanten zeggen\",\n items: [{ quote: \"Top geholpen!\", author: \"Tevreden klant\", rating: 5 }],\n jsonLd: true,\n }),\n});\n"],"mappings":"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","names":[]}
@@ -3,7 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { WhatsappChatBlockSchema } from "../../schema/blocks";
4
4
  import { defineBlock } from "../registry";
5
5
  function waLink(phone, text) {
6
- const trimmed = phone.trim();
6
+ const trimmed = phone.trim().replace(/\(0\)/g, "");
7
7
  const digits = trimmed.replace(/\D/g, "");
8
8
  const intl = trimmed.startsWith("+") ? digits : digits.startsWith("00") ? digits.slice(2) : digits.startsWith("0") ? null : digits;
9
9
  if (!intl || intl.length < 8 || intl.length > 15) return null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/WhatsappChat.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { WhatsappChatBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\n\ntype WhatsappChat = z.infer<typeof WhatsappChatBlockSchema>;\n\n/**\n * wa.me accepteert alleen cijfers (internationaal, zonder + of spaties). Een nationaal nummer\n * (\"06-12345678\") heeft geen landcode — die zomaar strippen levert een verkeerd nummer op, dus\n * dan liever geen link (de editor-preview vraagt om internationale notatie).\n */\nexport function waLink(phone: string, text?: string): string | null {\n const trimmed = phone.trim();\n const digits = trimmed.replace(/\\D/g, \"\");\n const intl = trimmed.startsWith(\"+\")\n ? digits\n : digits.startsWith(\"00\")\n ? digits.slice(2)\n : digits.startsWith(\"0\")\n ? null\n : digits;\n if (!intl || intl.length < 8 || intl.length > 15) return null;\n return `https://wa.me/${intl}${text ? `?text=${encodeURIComponent(text)}` : \"\"}`;\n}\n\n/**\n * Zwevende WhatsApp-knop — dé laagdrempelige contactknop voor lokale bedrijven. Gewone link\n * (geen embed/scripts), dus geen consent nodig. In de editor-preview inline weergegeven zodat\n * hij het canvas niet overlapt.\n */\nfunction WhatsappChatRender({ block, mode }: BlockRenderProps<WhatsappChat>) {\n const href = waLink(block.phone, block.prefilledText);\n if (!href) {\n return mode === \"preview\" ? <p className=\"xicms-ve__hint\">💬 WhatsApp-knop: vul een internationaal telefoonnummer in (bv. +31 6 12345678).</p> : null;\n }\n const icon = (\n <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.9\" aria-hidden>\n <path d=\"M12 4.2a7.8 7.8 0 0 0-6.7 11.8L4.4 19.6l3.7-.9A7.8 7.8 0 1 0 12 4.2z\" />\n <path d=\"M9.3 8.9c.7 2.4 2.6 4.4 5 5l1-1.2 2 1.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n );\n return (\n <a\n className=\"xicms-whatsapp\"\n data-position={block.position}\n data-preview={mode === \"preview\" ? \"1\" : undefined}\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={block.label || \"Chat via WhatsApp\"}\n >\n {icon}\n {block.label && <span className=\"xicms-whatsapp__label\" data-xicms-edit=\"label\">{block.label}</span>}\n </a>\n );\n}\n\nexport const whatsappChatBlock = defineBlock({\n type: \"whatsappChat\",\n schema: WhatsappChatBlockSchema,\n Render: WhatsappChatRender,\n label: \"WhatsApp-knop\",\n icon: \"💬\",\n category: \"widget\",\n defaults: () => ({ phone: \"\", label: \"App ons\", position: \"br\" }),\n});\n"],"mappings":";AAmCgC,cAG5B,YAH4B;AAhChC,SAAS,+BAA+B;AACxC,SAAS,mBAA0C;AAS5C,SAAS,OAAO,OAAe,MAA8B;AAClE,QAAM,UAAU,MAAM,KAAK;AAC3B,QAAM,SAAS,QAAQ,QAAQ,OAAO,EAAE;AACxC,QAAM,OAAO,QAAQ,WAAW,GAAG,IAC/B,SACA,OAAO,WAAW,IAAI,IACpB,OAAO,MAAM,CAAC,IACd,OAAO,WAAW,GAAG,IACnB,OACA;AACR,MAAI,CAAC,QAAQ,KAAK,SAAS,KAAK,KAAK,SAAS,GAAI,QAAO;AACzD,SAAO,iBAAiB,IAAI,GAAG,OAAO,SAAS,mBAAmB,IAAI,CAAC,KAAK,EAAE;AAChF;AAOA,SAAS,mBAAmB,EAAE,OAAO,KAAK,GAAmC;AAC3E,QAAM,OAAO,OAAO,MAAM,OAAO,MAAM,aAAa;AACpD,MAAI,CAAC,MAAM;AACT,WAAO,SAAS,YAAY,oBAAC,OAAE,WAAU,kBAAiB,qGAAgF,IAAO;AAAA,EACnJ;AACA,QAAM,OACJ,qBAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,OAAM,eAAW,MAC7G;AAAA,wBAAC,UAAK,GAAE,wEAAuE;AAAA,IAC/E,oBAAC,UAAK,GAAE,2CAA0C,eAAc,SAAQ,gBAAe,SAAQ;AAAA,KACjG;AAEF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,iBAAe,MAAM;AAAA,MACrB,gBAAc,SAAS,YAAY,MAAM;AAAA,MACzC;AAAA,MACA,QAAO;AAAA,MACP,KAAI;AAAA,MACJ,cAAY,MAAM,SAAS;AAAA,MAE1B;AAAA;AAAA,QACA,MAAM,SAAS,oBAAC,UAAK,WAAU,yBAAwB,mBAAgB,SAAS,gBAAM,OAAM;AAAA;AAAA;AAAA,EAC/F;AAEJ;AAEO,MAAM,oBAAoB,YAAY;AAAA,EAC3C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU,OAAO,EAAE,OAAO,IAAI,OAAO,WAAW,UAAU,KAAK;AACjE,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/WhatsappChat.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { WhatsappChatBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\n\ntype WhatsappChat = z.infer<typeof WhatsappChatBlockSchema>;\n\n/**\n * wa.me accepteert alleen cijfers (internationaal, zonder + of spaties). Een nationaal nummer\n * (\"06-12345678\") heeft geen landcode — die zomaar strippen levert een verkeerd nummer op, dus\n * dan liever geen link (de editor-preview vraagt om internationale notatie).\n */\nexport function waLink(phone: string, text?: string): string | null {\n // \"(0)\" in \"+31 (0)6 12345678\" is een schrijfconventie, geen kiescijfer — eerst strippen,\n // anders belandt die valse nul midden in het wa.me-nummer.\n const trimmed = phone.trim().replace(/\\(0\\)/g, \"\");\n const digits = trimmed.replace(/\\D/g, \"\");\n const intl = trimmed.startsWith(\"+\")\n ? digits\n : digits.startsWith(\"00\")\n ? digits.slice(2)\n : digits.startsWith(\"0\")\n ? null\n : digits;\n if (!intl || intl.length < 8 || intl.length > 15) return null;\n return `https://wa.me/${intl}${text ? `?text=${encodeURIComponent(text)}` : \"\"}`;\n}\n\n/**\n * Zwevende WhatsApp-knop — dé laagdrempelige contactknop voor lokale bedrijven. Gewone link\n * (geen embed/scripts), dus geen consent nodig. In de editor-preview inline weergegeven zodat\n * hij het canvas niet overlapt.\n */\nfunction WhatsappChatRender({ block, mode }: BlockRenderProps<WhatsappChat>) {\n const href = waLink(block.phone, block.prefilledText);\n if (!href) {\n return mode === \"preview\" ? <p className=\"xicms-ve__hint\">💬 WhatsApp-knop: vul een internationaal telefoonnummer in (bv. +31 6 12345678).</p> : null;\n }\n const icon = (\n <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.9\" aria-hidden>\n <path d=\"M12 4.2a7.8 7.8 0 0 0-6.7 11.8L4.4 19.6l3.7-.9A7.8 7.8 0 1 0 12 4.2z\" />\n <path d=\"M9.3 8.9c.7 2.4 2.6 4.4 5 5l1-1.2 2 1.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n );\n return (\n <a\n className=\"xicms-whatsapp\"\n data-position={block.position}\n data-preview={mode === \"preview\" ? \"1\" : undefined}\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={block.label || \"Chat via WhatsApp\"}\n >\n {icon}\n {block.label && <span className=\"xicms-whatsapp__label\" data-xicms-edit=\"label\">{block.label}</span>}\n </a>\n );\n}\n\nexport const whatsappChatBlock = defineBlock({\n type: \"whatsappChat\",\n schema: WhatsappChatBlockSchema,\n Render: WhatsappChatRender,\n label: \"WhatsApp-knop\",\n icon: \"💬\",\n category: \"widget\",\n defaults: () => ({ phone: \"\", label: \"App ons\", position: \"br\" }),\n});\n"],"mappings":";AAqCgC,cAG5B,YAH4B;AAlChC,SAAS,+BAA+B;AACxC,SAAS,mBAA0C;AAS5C,SAAS,OAAO,OAAe,MAA8B;AAGlE,QAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,UAAU,EAAE;AACjD,QAAM,SAAS,QAAQ,QAAQ,OAAO,EAAE;AACxC,QAAM,OAAO,QAAQ,WAAW,GAAG,IAC/B,SACA,OAAO,WAAW,IAAI,IACpB,OAAO,MAAM,CAAC,IACd,OAAO,WAAW,GAAG,IACnB,OACA;AACR,MAAI,CAAC,QAAQ,KAAK,SAAS,KAAK,KAAK,SAAS,GAAI,QAAO;AACzD,SAAO,iBAAiB,IAAI,GAAG,OAAO,SAAS,mBAAmB,IAAI,CAAC,KAAK,EAAE;AAChF;AAOA,SAAS,mBAAmB,EAAE,OAAO,KAAK,GAAmC;AAC3E,QAAM,OAAO,OAAO,MAAM,OAAO,MAAM,aAAa;AACpD,MAAI,CAAC,MAAM;AACT,WAAO,SAAS,YAAY,oBAAC,OAAE,WAAU,kBAAiB,qGAAgF,IAAO;AAAA,EACnJ;AACA,QAAM,OACJ,qBAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,OAAM,eAAW,MAC7G;AAAA,wBAAC,UAAK,GAAE,wEAAuE;AAAA,IAC/E,oBAAC,UAAK,GAAE,2CAA0C,eAAc,SAAQ,gBAAe,SAAQ;AAAA,KACjG;AAEF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,iBAAe,MAAM;AAAA,MACrB,gBAAc,SAAS,YAAY,MAAM;AAAA,MACzC;AAAA,MACA,QAAO;AAAA,MACP,KAAI;AAAA,MACJ,cAAY,MAAM,SAAS;AAAA,MAE1B;AAAA;AAAA,QACA,MAAM,SAAS,oBAAC,UAAK,WAAU,yBAAwB,mBAAgB,SAAS,gBAAM,OAAM;AAAA;AAAA;AAAA,EAC/F;AAEJ;AAEO,MAAM,oBAAoB,YAAY;AAAA,EAC3C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU,OAAO,EAAE,OAAO,IAAI,OAAO,WAAW,UAAU,KAAK;AACjE,CAAC;","names":[]}
@@ -18,6 +18,7 @@ import {
18
18
  import { defineBlock } from "../registry";
19
19
  import { useNews } from "../../components/News";
20
20
  import { selectAgendaEvents, formatEventDate } from "./Agenda";
21
+ import { parseDays } from "./lib/openingHours";
21
22
  import { useCountdown, CountdownDigits } from "./Countdown";
22
23
  import { TextInput } from "@astryxdesign/core/TextInput";
23
24
  import { Button } from "@astryxdesign/core/Button";
@@ -65,16 +66,14 @@ const agendaWidget = defineBlock({
65
66
  category: "widget",
66
67
  defaults: () => ({ heading: "Komende evenementen", limit: 5, events: [], hidePast: true })
67
68
  });
68
- const DAY_KEYWORDS = ["zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"];
69
69
  function OpeningHoursWidgetRender({ block }) {
70
70
  const [todayIdx, setTodayIdx] = useState(null);
71
71
  useEffect(() => setTodayIdx((/* @__PURE__ */ new Date()).getDay()), []);
72
72
  const rows = block.rows ?? [];
73
- const todayWord = todayIdx === null ? "" : DAY_KEYWORDS[todayIdx];
74
73
  return /* @__PURE__ */ jsxs("div", { className: "xicms-widget xicms-widget--hours", children: [
75
74
  /* @__PURE__ */ jsx("h3", { className: "xicms-widget__title", children: block.heading ?? "Openingstijden" }),
76
75
  /* @__PURE__ */ jsx("table", { className: "xicms-widget__hours", children: /* @__PURE__ */ jsx("tbody", { children: rows.map((r, i) => {
77
- const isToday = block.highlightToday && !!todayWord && r.day.toLowerCase().includes(todayWord);
76
+ const isToday = block.highlightToday && todayIdx !== null && parseDays(r.day).includes(todayIdx);
78
77
  return /* @__PURE__ */ jsxs("tr", { className: isToday ? "xicms-widget__hours-today" : void 0, children: [
79
78
  /* @__PURE__ */ jsx("th", { scope: "row", children: r.day }),
80
79
  /* @__PURE__ */ jsx("td", { children: r.hours })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/Widgets.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport {\n NewsWidgetSchema,\n AgendaWidgetSchema,\n OpeningHoursWidgetSchema,\n ContactWidgetSchema,\n SocialWidgetSchema,\n NewsletterWidgetSchema,\n CtaWidgetSchema,\n GalleryWidgetSchema,\n TestimonialsWidgetSchema,\n CardsWidgetSchema,\n DownloadsWidgetSchema,\n CountdownWidgetSchema,\n} from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { useNews } from \"../../components/News\";\nimport { selectAgendaEvents, formatEventDate } from \"./Agenda\";\nimport { useCountdown, CountdownDigits } from \"./Countdown\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { Button } from \"@astryxdesign/core/Button\";\n\n/**\n * Widgets = compacte zijbalk-varianten van de modules. Elke widget hergebruikt de gegevens en\n * logica van de volwaardige versie (zelfde nieuws-collectie, zelfde agenda-selectie, zelfde\n * countdown-hook) en rendert die alleen compacter. Plaats ze in het \"Met zijbalk\"-blok of in een\n * smalle kolom naast je hoofdinhoud.\n */\n\n/* ── Laatste nieuws ────────────────────────────────────────────────────────── */\ntype NewsW = z.infer<typeof NewsWidgetSchema>;\nfunction NewsWidgetRender({ block }: BlockRenderProps<NewsW>) {\n const { articles, loading } = useNews({ max: block.limit });\n const shown = articles.slice(0, block.limit);\n return (\n <div className=\"xicms-widget xicms-widget--news\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Laatste nieuws\"}</h3>\n {loading ? (\n <p className=\"xicms-widget__empty\">Nieuws laden…</p>\n ) : shown.length === 0 ? (\n <p className=\"xicms-widget__empty\">Nog geen nieuws.</p>\n ) : (\n <ul className=\"xicms-widget__list\">\n {shown.map((a) => (\n <li key={a.id} className=\"xicms-widget__item\">\n {block.showImages && a.coverUrl && (\n // eslint-disable-next-line @next/next/no-img-element\n <img className=\"xicms-widget__thumb\" src={a.coverUrl} alt=\"\" loading=\"lazy\" />\n )}\n <div>\n <a className=\"xicms-widget__link\" href={`${block.basePath}/${a.slug}`}>{a.title}</a>\n <span className=\"xicms-widget__meta\">{a.date}</span>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\nexport const newsWidget = defineBlock({\n type: \"newsWidget\",\n schema: NewsWidgetSchema,\n Render: NewsWidgetRender,\n label: \"Laatste nieuws\",\n icon: \"🗞️\",\n category: \"widget\",\n defaults: () => ({ heading: \"Laatste nieuws\", limit: 5, basePath: \"/nieuws\", showImages: false }),\n});\n\n/* ── Komende agenda ────────────────────────────────────────────────────────── */\ntype AgendaW = z.infer<typeof AgendaWidgetSchema>;\nfunction AgendaWidgetRender({ block }: BlockRenderProps<AgendaW>) {\n const events = selectAgendaEvents(block.events, { hidePast: block.hidePast, limit: block.limit });\n return (\n <div className=\"xicms-widget xicms-widget--agenda\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Komende evenementen\"}</h3>\n {events.length === 0 ? (\n <p className=\"xicms-widget__empty\">Geen geplande evenementen.</p>\n ) : (\n <ul className=\"xicms-widget__list\">\n {events.map((e, i) => (\n <li key={i} className=\"xicms-widget__item xicms-widget__item--event\">\n <time className=\"xicms-widget__meta\" dateTime={e.start}>{formatEventDate(e.start)}</time>\n <span className=\"xicms-widget__link\">{e.url ? <a href={e.url}>{e.title}</a> : e.title}</span>\n {e.location && <span className=\"xicms-widget__sub\">{e.location}</span>}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\nexport const agendaWidget = defineBlock({\n type: \"agendaWidget\",\n schema: AgendaWidgetSchema,\n Render: AgendaWidgetRender,\n label: \"Komende agenda\",\n icon: \"📅\",\n category: \"widget\",\n defaults: () => ({ heading: \"Komende evenementen\", limit: 5, events: [], hidePast: true }),\n});\n\n/* ── Openingstijden (vandaag gemarkeerd) ───────────────────────────────────── */\nconst DAY_KEYWORDS = [\"zondag\", \"maandag\", \"dinsdag\", \"woensdag\", \"donderdag\", \"vrijdag\", \"zaterdag\"];\ntype OpeningW = z.infer<typeof OpeningHoursWidgetSchema>;\nfunction OpeningHoursWidgetRender({ block }: BlockRenderProps<OpeningW>) {\n // \"Vandaag\" client-side bepalen (voorkomt hydration-mismatch t.o.v. de servertijd).\n const [todayIdx, setTodayIdx] = useState<number | null>(null);\n useEffect(() => setTodayIdx(new Date().getDay()), []);\n const rows = block.rows ?? [];\n const todayWord = todayIdx === null ? \"\" : DAY_KEYWORDS[todayIdx];\n return (\n <div className=\"xicms-widget xicms-widget--hours\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Openingstijden\"}</h3>\n <table className=\"xicms-widget__hours\">\n <tbody>\n {rows.map((r, i) => {\n const isToday =\n block.highlightToday && !!todayWord && r.day.toLowerCase().includes(todayWord);\n return (\n <tr key={i} className={isToday ? \"xicms-widget__hours-today\" : undefined}>\n <th scope=\"row\">{r.day}</th>\n <td>{r.hours}</td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n );\n}\nexport const openingHoursWidget = defineBlock({\n type: \"openingHoursWidget\",\n schema: OpeningHoursWidgetSchema,\n Render: OpeningHoursWidgetRender,\n label: \"Openingstijden (compact)\",\n icon: \"🕒\",\n category: \"widget\",\n defaults: () => ({\n heading: \"Openingstijden\",\n highlightToday: true,\n rows: [\n { day: \"Maandag t/m vrijdag\", hours: \"09:00 – 17:00\" },\n { day: \"Zaterdag\", hours: \"10:00 – 16:00\" },\n { day: \"Zondag\", hours: \"Gesloten\" },\n ],\n }),\n});\n\n/* ── Contact (compact) ─────────────────────────────────────────────────────── */\ntype ContactW = z.infer<typeof ContactWidgetSchema>;\nfunction ContactWidgetRender({ block }: BlockRenderProps<ContactW>) {\n if (!block.address && !block.phone && !block.email) return null;\n return (\n <div className=\"xicms-widget xicms-widget--contact\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Contact\"}</h3>\n <ul className=\"xicms-widget__list\">\n {block.address && <li className=\"xicms-widget__item\">{block.address}</li>}\n {block.phone && (\n <li className=\"xicms-widget__item\"><a href={`tel:${block.phone.replace(/\\s+/g, \"\")}`}>{block.phone}</a></li>\n )}\n {block.email && (\n <li className=\"xicms-widget__item\"><a href={`mailto:${block.email}`}>{block.email}</a></li>\n )}\n </ul>\n </div>\n );\n}\nexport const contactWidget = defineBlock({\n type: \"contactWidget\",\n schema: ContactWidgetSchema,\n Render: ContactWidgetRender,\n label: \"Contact (compact)\",\n icon: \"📇\",\n category: \"widget\",\n defaults: () => ({ heading: \"Contact\" }),\n});\n\n/* ── Social (compact) ──────────────────────────────────────────────────────── */\ntype SocialW = z.infer<typeof SocialWidgetSchema>;\nfunction SocialWidgetRender({ block }: BlockRenderProps<SocialW>) {\n const items = (block.items ?? []).filter((i) => i.href && i.platform);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--social\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <ul className=\"xicms-widget__social\">\n {items.map((it, i) => (\n <li key={i}><a href={it.href} rel=\"noopener noreferrer\" target=\"_blank\">{it.platform}</a></li>\n ))}\n </ul>\n </div>\n );\n}\nexport const socialWidget = defineBlock({\n type: \"socialWidget\",\n schema: SocialWidgetSchema,\n Render: SocialWidgetRender,\n label: \"Social (compact)\",\n icon: \"🔗\",\n category: \"widget\",\n defaults: () => ({ heading: \"Volg ons\", items: [{ platform: \"Instagram\", href: \"\" }] }),\n});\n\n/* ── Nieuwsbrief (compact) ─────────────────────────────────────────────────── */\ntype NewsletterW = z.infer<typeof NewsletterWidgetSchema>;\nfunction NewsletterWidgetRender({ block }: BlockRenderProps<NewsletterW>) {\n const [email, setEmail] = useState(\"\");\n return (\n <div className=\"xicms-widget xicms-widget--newsletter\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Nieuwsbrief\"}</h3>\n {block.text && <p className=\"xicms-widget__sub\">{block.text}</p>}\n {block.actionUrl ? (\n <form className=\"xicms-widget__form\" action={block.actionUrl} method=\"post\" target=\"_blank\" style={{ display: \"grid\", gap: \"0.6rem\" }}>\n <TextInput label=\"E-mailadres\" isLabelHidden type=\"email\" htmlName=\"EMAIL\" isRequired placeholder=\"je@email.nl\" value={email} onChange={setEmail} />\n <Button label={block.buttonLabel} variant=\"primary\" type=\"submit\" />\n </form>\n ) : (\n <p className=\"xicms-widget__empty\">Stel een aanmeld-URL in.</p>\n )}\n </div>\n );\n}\nexport const newsletterWidget = defineBlock({\n type: \"newsletterWidget\",\n schema: NewsletterWidgetSchema,\n Render: NewsletterWidgetRender,\n label: \"Nieuwsbrief (compact)\",\n icon: \"✉️\",\n category: \"widget\",\n defaults: () => ({ heading: \"Blijf op de hoogte\", buttonLabel: \"Aanmelden\" }),\n});\n\n/* ── CTA (compact) ─────────────────────────────────────────────────────────── */\ntype CtaW = z.infer<typeof CtaWidgetSchema>;\nfunction CtaWidgetRender({ block }: BlockRenderProps<CtaW>) {\n if (!block.title && !block.text) return null;\n return (\n <div className=\"xicms-widget xicms-widget--cta\" style={block.backgroundColor ? { background: block.backgroundColor } : undefined}>\n {block.title && <h3 className=\"xicms-widget__title\">{block.title}</h3>}\n {block.text && <p className=\"xicms-widget__sub\">{block.text}</p>}\n {block.buttonLabel && (\n <a className=\"xicms-btn xicms-btn--primary\" href={block.buttonHref}>{block.buttonLabel}</a>\n )}\n </div>\n );\n}\nexport const ctaWidget = defineBlock({\n type: \"ctaWidget\",\n schema: CtaWidgetSchema,\n Render: CtaWidgetRender,\n label: \"Call-to-action (compact)\",\n icon: \"👉\",\n category: \"widget\",\n defaults: () => ({ title: \"Vragen?\", text: \"Neem gerust contact op.\", buttonLabel: \"Contact\", buttonHref: \"#contact\" }),\n});\n\n/* ── Galerij (compacte strip) ──────────────────────────────────────────────── */\ntype GalleryW = z.infer<typeof GalleryWidgetSchema>;\nfunction GalleryWidgetRender({ block }: BlockRenderProps<GalleryW>) {\n const items = (block.items ?? []).filter((i) => i.url);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--gallery\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <div className=\"xicms-widget__grid\" data-cols={block.columns}>\n {items.map((it, i) => (\n // eslint-disable-next-line @next/next/no-img-element\n <img key={i} className=\"xicms-widget__photo\" src={it.url} alt={it.alt ?? \"\"} loading=\"lazy\" />\n ))}\n </div>\n </div>\n );\n}\nexport const galleryWidget = defineBlock({\n type: \"galleryWidget\",\n schema: GalleryWidgetSchema,\n Render: GalleryWidgetRender,\n label: \"Fotostrip (compact)\",\n icon: \"🖼️\",\n category: \"widget\",\n defaults: () => ({ heading: \"Foto's\", columns: 3, items: [] }),\n});\n\n/* ── Review (compact) ──────────────────────────────────────────────────────── */\ntype TestimonialsW = z.infer<typeof TestimonialsWidgetSchema>;\nfunction TestimonialsWidgetRender({ block }: BlockRenderProps<TestimonialsW>) {\n const items = (block.items ?? []).filter((t) => t.quote);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--testimonial\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <ul className=\"xicms-widget__list\">\n {items.map((t, i) => (\n <li key={i} className=\"xicms-widget__quote\">\n {typeof t.rating === \"number\" && (\n <span className=\"xicms-widget__stars\" aria-label={`${t.rating} van 5`}>{\"★\".repeat(t.rating)}{\"☆\".repeat(5 - t.rating)}</span>\n )}\n <p>“{t.quote}”</p>\n {t.author && <span className=\"xicms-widget__meta\">— {t.author}</span>}\n </li>\n ))}\n </ul>\n </div>\n );\n}\nexport const testimonialsWidget = defineBlock({\n type: \"testimonialsWidget\",\n schema: TestimonialsWidgetSchema,\n Render: TestimonialsWidgetRender,\n label: \"Review (compact)\",\n icon: \"⭐\",\n category: \"widget\",\n defaults: () => ({ heading: \"Wat klanten zeggen\", items: [{ quote: \"Echt een aanrader!\", author: \"Tevreden klant\", rating: 5 }] }),\n});\n\n/* ── Diensten (compact) ────────────────────────────────────────────────────── */\ntype CardsW = z.infer<typeof CardsWidgetSchema>;\nfunction CardsWidgetRender({ block }: BlockRenderProps<CardsW>) {\n const items = (block.items ?? []).filter((c) => c.title);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--cards\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <ul className=\"xicms-widget__list\">\n {items.map((c, i) => {\n const inner = (\n <>\n {c.icon && <span className=\"xicms-widget__cardicon\" aria-hidden=\"true\">{c.icon}</span>}\n <span>\n <span className=\"xicms-widget__link\">{c.title}</span>\n {c.text && <span className=\"xicms-widget__sub\">{c.text}</span>}\n </span>\n </>\n );\n return (\n <li key={i} className=\"xicms-widget__item xicms-widget__item--card\">\n {c.href ? <a href={c.href} className=\"xicms-widget__cardlink\">{inner}</a> : inner}\n </li>\n );\n })}\n </ul>\n </div>\n );\n}\nexport const cardsWidget = defineBlock({\n type: \"cardsWidget\",\n schema: CardsWidgetSchema,\n Render: CardsWidgetRender,\n label: \"Diensten (compact)\",\n icon: \"🧩\",\n category: \"widget\",\n defaults: () => ({ heading: \"Uitgelicht\", items: [{ icon: \"⭐\", title: \"Dienst\", text: \"Korte omschrijving.\" }] }),\n});\n\n/* ── Downloads (compact) ───────────────────────────────────────────────────── */\ntype DownloadsW = z.infer<typeof DownloadsWidgetSchema>;\nfunction DownloadsWidgetRender({ block }: BlockRenderProps<DownloadsW>) {\n const items = (block.items ?? []).filter((d) => d.label && d.href).slice(0, block.limit);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--downloads\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Downloads\"}</h3>\n <ul className=\"xicms-widget__list\">\n {items.map((d, i) => (\n <li key={i} className=\"xicms-widget__item\">\n <a className=\"xicms-widget__link\" href={d.href} download>\n <span aria-hidden=\"true\">📄</span> {d.label}\n </a>\n {d.meta && <span className=\"xicms-widget__meta\">{d.meta}</span>}\n </li>\n ))}\n </ul>\n </div>\n );\n}\nexport const downloadsWidget = defineBlock({\n type: \"downloadsWidget\",\n schema: DownloadsWidgetSchema,\n Render: DownloadsWidgetRender,\n label: \"Downloads (compact)\",\n icon: \"📥\",\n category: \"widget\",\n defaults: () => ({ heading: \"Downloads\", limit: 5, items: [] }),\n});\n\n/* ── Aftelteller (compact) ─────────────────────────────────────────────────── */\ntype CountdownW = z.infer<typeof CountdownWidgetSchema>;\nfunction CountdownWidgetRender({ block }: BlockRenderProps<CountdownW>) {\n const r = useCountdown(block.target);\n if (!block.target) return null;\n return (\n <div className=\"xicms-widget xicms-widget--countdown\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n {r === null ? (\n <p className=\"xicms-widget__empty\" aria-hidden=\"true\">…</p>\n ) : r.done ? (\n <p className=\"xicms-countdown__expired\">{block.expiredText}</p>\n ) : (\n <CountdownDigits r={r} showLabels />\n )}\n </div>\n );\n}\nexport const countdownWidget = defineBlock({\n type: \"countdownWidget\",\n schema: CountdownWidgetSchema,\n Render: CountdownWidgetRender,\n label: \"Aftelteller (compact)\",\n icon: \"⏳\",\n category: \"widget\",\n defaults: () => ({ heading: \"Aftellen\", target: \"\", expiredText: \"Het is zover!\" }),\n});\n\n// Let op: exporteer GEEN aggregaat-array vanuit dit \"use client\"-bestand. Een array die hier\n// wordt opgebouwd is niet server-importeerbaar (de individuele block-exports wél). De volledige\n// lijst staat in blocks/builtins/index.ts.\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/Widgets.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport {\n NewsWidgetSchema,\n AgendaWidgetSchema,\n OpeningHoursWidgetSchema,\n ContactWidgetSchema,\n SocialWidgetSchema,\n NewsletterWidgetSchema,\n CtaWidgetSchema,\n GalleryWidgetSchema,\n TestimonialsWidgetSchema,\n CardsWidgetSchema,\n DownloadsWidgetSchema,\n CountdownWidgetSchema,\n} from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { useNews } from \"../../components/News\";\nimport { selectAgendaEvents, formatEventDate } from \"./Agenda\";\nimport { parseDays } from \"./lib/openingHours\";\nimport { useCountdown, CountdownDigits } from \"./Countdown\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { Button } from \"@astryxdesign/core/Button\";\n\n/**\n * Widgets = compacte zijbalk-varianten van de modules. Elke widget hergebruikt de gegevens en\n * logica van de volwaardige versie (zelfde nieuws-collectie, zelfde agenda-selectie, zelfde\n * countdown-hook) en rendert die alleen compacter. Plaats ze in het \"Met zijbalk\"-blok of in een\n * smalle kolom naast je hoofdinhoud.\n */\n\n/* ── Laatste nieuws ────────────────────────────────────────────────────────── */\ntype NewsW = z.infer<typeof NewsWidgetSchema>;\nfunction NewsWidgetRender({ block }: BlockRenderProps<NewsW>) {\n const { articles, loading } = useNews({ max: block.limit });\n const shown = articles.slice(0, block.limit);\n return (\n <div className=\"xicms-widget xicms-widget--news\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Laatste nieuws\"}</h3>\n {loading ? (\n <p className=\"xicms-widget__empty\">Nieuws laden…</p>\n ) : shown.length === 0 ? (\n <p className=\"xicms-widget__empty\">Nog geen nieuws.</p>\n ) : (\n <ul className=\"xicms-widget__list\">\n {shown.map((a) => (\n <li key={a.id} className=\"xicms-widget__item\">\n {block.showImages && a.coverUrl && (\n // eslint-disable-next-line @next/next/no-img-element\n <img className=\"xicms-widget__thumb\" src={a.coverUrl} alt=\"\" loading=\"lazy\" />\n )}\n <div>\n <a className=\"xicms-widget__link\" href={`${block.basePath}/${a.slug}`}>{a.title}</a>\n <span className=\"xicms-widget__meta\">{a.date}</span>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\nexport const newsWidget = defineBlock({\n type: \"newsWidget\",\n schema: NewsWidgetSchema,\n Render: NewsWidgetRender,\n label: \"Laatste nieuws\",\n icon: \"🗞️\",\n category: \"widget\",\n defaults: () => ({ heading: \"Laatste nieuws\", limit: 5, basePath: \"/nieuws\", showImages: false }),\n});\n\n/* ── Komende agenda ────────────────────────────────────────────────────────── */\ntype AgendaW = z.infer<typeof AgendaWidgetSchema>;\nfunction AgendaWidgetRender({ block }: BlockRenderProps<AgendaW>) {\n const events = selectAgendaEvents(block.events, { hidePast: block.hidePast, limit: block.limit });\n return (\n <div className=\"xicms-widget xicms-widget--agenda\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Komende evenementen\"}</h3>\n {events.length === 0 ? (\n <p className=\"xicms-widget__empty\">Geen geplande evenementen.</p>\n ) : (\n <ul className=\"xicms-widget__list\">\n {events.map((e, i) => (\n <li key={i} className=\"xicms-widget__item xicms-widget__item--event\">\n <time className=\"xicms-widget__meta\" dateTime={e.start}>{formatEventDate(e.start)}</time>\n <span className=\"xicms-widget__link\">{e.url ? <a href={e.url}>{e.title}</a> : e.title}</span>\n {e.location && <span className=\"xicms-widget__sub\">{e.location}</span>}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\nexport const agendaWidget = defineBlock({\n type: \"agendaWidget\",\n schema: AgendaWidgetSchema,\n Render: AgendaWidgetRender,\n label: \"Komende agenda\",\n icon: \"📅\",\n category: \"widget\",\n defaults: () => ({ heading: \"Komende evenementen\", limit: 5, events: [], hidePast: true }),\n});\n\n/* ── Openingstijden (vandaag gemarkeerd) ───────────────────────────────────── */\ntype OpeningW = z.infer<typeof OpeningHoursWidgetSchema>;\nfunction OpeningHoursWidgetRender({ block }: BlockRenderProps<OpeningW>) {\n // \"Vandaag\" client-side bepalen (voorkomt hydration-mismatch t.o.v. de servertijd).\n const [todayIdx, setTodayIdx] = useState<number | null>(null);\n useEffect(() => setTodayIdx(new Date().getDay()), []);\n const rows = block.rows ?? [];\n return (\n <div className=\"xicms-widget xicms-widget--hours\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Openingstijden\"}</h3>\n <table className=\"xicms-widget__hours\">\n <tbody>\n {rows.map((r, i) => {\n // parseDays begrijpt ook bereiken (\"Ma-vr\", \"Maandag t/m vrijdag\") - een woensdag\n // hoort dan net zo goed gemarkeerd als de letterlijk genoemde dagen.\n const isToday =\n block.highlightToday && todayIdx !== null && parseDays(r.day).includes(todayIdx);\n return (\n <tr key={i} className={isToday ? \"xicms-widget__hours-today\" : undefined}>\n <th scope=\"row\">{r.day}</th>\n <td>{r.hours}</td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n );\n}\nexport const openingHoursWidget = defineBlock({\n type: \"openingHoursWidget\",\n schema: OpeningHoursWidgetSchema,\n Render: OpeningHoursWidgetRender,\n label: \"Openingstijden (compact)\",\n icon: \"🕒\",\n category: \"widget\",\n defaults: () => ({\n heading: \"Openingstijden\",\n highlightToday: true,\n rows: [\n { day: \"Maandag t/m vrijdag\", hours: \"09:00 – 17:00\" },\n { day: \"Zaterdag\", hours: \"10:00 – 16:00\" },\n { day: \"Zondag\", hours: \"Gesloten\" },\n ],\n }),\n});\n\n/* ── Contact (compact) ─────────────────────────────────────────────────────── */\ntype ContactW = z.infer<typeof ContactWidgetSchema>;\nfunction ContactWidgetRender({ block }: BlockRenderProps<ContactW>) {\n if (!block.address && !block.phone && !block.email) return null;\n return (\n <div className=\"xicms-widget xicms-widget--contact\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Contact\"}</h3>\n <ul className=\"xicms-widget__list\">\n {block.address && <li className=\"xicms-widget__item\">{block.address}</li>}\n {block.phone && (\n <li className=\"xicms-widget__item\"><a href={`tel:${block.phone.replace(/\\s+/g, \"\")}`}>{block.phone}</a></li>\n )}\n {block.email && (\n <li className=\"xicms-widget__item\"><a href={`mailto:${block.email}`}>{block.email}</a></li>\n )}\n </ul>\n </div>\n );\n}\nexport const contactWidget = defineBlock({\n type: \"contactWidget\",\n schema: ContactWidgetSchema,\n Render: ContactWidgetRender,\n label: \"Contact (compact)\",\n icon: \"📇\",\n category: \"widget\",\n defaults: () => ({ heading: \"Contact\" }),\n});\n\n/* ── Social (compact) ──────────────────────────────────────────────────────── */\ntype SocialW = z.infer<typeof SocialWidgetSchema>;\nfunction SocialWidgetRender({ block }: BlockRenderProps<SocialW>) {\n const items = (block.items ?? []).filter((i) => i.href && i.platform);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--social\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <ul className=\"xicms-widget__social\">\n {items.map((it, i) => (\n <li key={i}><a href={it.href} rel=\"noopener noreferrer\" target=\"_blank\">{it.platform}</a></li>\n ))}\n </ul>\n </div>\n );\n}\nexport const socialWidget = defineBlock({\n type: \"socialWidget\",\n schema: SocialWidgetSchema,\n Render: SocialWidgetRender,\n label: \"Social (compact)\",\n icon: \"🔗\",\n category: \"widget\",\n defaults: () => ({ heading: \"Volg ons\", items: [{ platform: \"Instagram\", href: \"\" }] }),\n});\n\n/* ── Nieuwsbrief (compact) ─────────────────────────────────────────────────── */\ntype NewsletterW = z.infer<typeof NewsletterWidgetSchema>;\nfunction NewsletterWidgetRender({ block }: BlockRenderProps<NewsletterW>) {\n const [email, setEmail] = useState(\"\");\n return (\n <div className=\"xicms-widget xicms-widget--newsletter\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Nieuwsbrief\"}</h3>\n {block.text && <p className=\"xicms-widget__sub\">{block.text}</p>}\n {block.actionUrl ? (\n <form className=\"xicms-widget__form\" action={block.actionUrl} method=\"post\" target=\"_blank\" style={{ display: \"grid\", gap: \"0.6rem\" }}>\n <TextInput label=\"E-mailadres\" isLabelHidden type=\"email\" htmlName=\"EMAIL\" isRequired placeholder=\"je@email.nl\" value={email} onChange={setEmail} />\n <Button label={block.buttonLabel} variant=\"primary\" type=\"submit\" />\n </form>\n ) : (\n <p className=\"xicms-widget__empty\">Stel een aanmeld-URL in.</p>\n )}\n </div>\n );\n}\nexport const newsletterWidget = defineBlock({\n type: \"newsletterWidget\",\n schema: NewsletterWidgetSchema,\n Render: NewsletterWidgetRender,\n label: \"Nieuwsbrief (compact)\",\n icon: \"✉️\",\n category: \"widget\",\n defaults: () => ({ heading: \"Blijf op de hoogte\", buttonLabel: \"Aanmelden\" }),\n});\n\n/* ── CTA (compact) ─────────────────────────────────────────────────────────── */\ntype CtaW = z.infer<typeof CtaWidgetSchema>;\nfunction CtaWidgetRender({ block }: BlockRenderProps<CtaW>) {\n if (!block.title && !block.text) return null;\n return (\n <div className=\"xicms-widget xicms-widget--cta\" style={block.backgroundColor ? { background: block.backgroundColor } : undefined}>\n {block.title && <h3 className=\"xicms-widget__title\">{block.title}</h3>}\n {block.text && <p className=\"xicms-widget__sub\">{block.text}</p>}\n {block.buttonLabel && (\n <a className=\"xicms-btn xicms-btn--primary\" href={block.buttonHref}>{block.buttonLabel}</a>\n )}\n </div>\n );\n}\nexport const ctaWidget = defineBlock({\n type: \"ctaWidget\",\n schema: CtaWidgetSchema,\n Render: CtaWidgetRender,\n label: \"Call-to-action (compact)\",\n icon: \"👉\",\n category: \"widget\",\n defaults: () => ({ title: \"Vragen?\", text: \"Neem gerust contact op.\", buttonLabel: \"Contact\", buttonHref: \"#contact\" }),\n});\n\n/* ── Galerij (compacte strip) ──────────────────────────────────────────────── */\ntype GalleryW = z.infer<typeof GalleryWidgetSchema>;\nfunction GalleryWidgetRender({ block }: BlockRenderProps<GalleryW>) {\n const items = (block.items ?? []).filter((i) => i.url);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--gallery\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <div className=\"xicms-widget__grid\" data-cols={block.columns}>\n {items.map((it, i) => (\n // eslint-disable-next-line @next/next/no-img-element\n <img key={i} className=\"xicms-widget__photo\" src={it.url} alt={it.alt ?? \"\"} loading=\"lazy\" />\n ))}\n </div>\n </div>\n );\n}\nexport const galleryWidget = defineBlock({\n type: \"galleryWidget\",\n schema: GalleryWidgetSchema,\n Render: GalleryWidgetRender,\n label: \"Fotostrip (compact)\",\n icon: \"🖼️\",\n category: \"widget\",\n defaults: () => ({ heading: \"Foto's\", columns: 3, items: [] }),\n});\n\n/* ── Review (compact) ──────────────────────────────────────────────────────── */\ntype TestimonialsW = z.infer<typeof TestimonialsWidgetSchema>;\nfunction TestimonialsWidgetRender({ block }: BlockRenderProps<TestimonialsW>) {\n const items = (block.items ?? []).filter((t) => t.quote);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--testimonial\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <ul className=\"xicms-widget__list\">\n {items.map((t, i) => (\n <li key={i} className=\"xicms-widget__quote\">\n {typeof t.rating === \"number\" && (\n <span className=\"xicms-widget__stars\" aria-label={`${t.rating} van 5`}>{\"★\".repeat(t.rating)}{\"☆\".repeat(5 - t.rating)}</span>\n )}\n <p>“{t.quote}”</p>\n {t.author && <span className=\"xicms-widget__meta\">— {t.author}</span>}\n </li>\n ))}\n </ul>\n </div>\n );\n}\nexport const testimonialsWidget = defineBlock({\n type: \"testimonialsWidget\",\n schema: TestimonialsWidgetSchema,\n Render: TestimonialsWidgetRender,\n label: \"Review (compact)\",\n icon: \"⭐\",\n category: \"widget\",\n defaults: () => ({ heading: \"Wat klanten zeggen\", items: [{ quote: \"Echt een aanrader!\", author: \"Tevreden klant\", rating: 5 }] }),\n});\n\n/* ── Diensten (compact) ────────────────────────────────────────────────────── */\ntype CardsW = z.infer<typeof CardsWidgetSchema>;\nfunction CardsWidgetRender({ block }: BlockRenderProps<CardsW>) {\n const items = (block.items ?? []).filter((c) => c.title);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--cards\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <ul className=\"xicms-widget__list\">\n {items.map((c, i) => {\n const inner = (\n <>\n {c.icon && <span className=\"xicms-widget__cardicon\" aria-hidden=\"true\">{c.icon}</span>}\n <span>\n <span className=\"xicms-widget__link\">{c.title}</span>\n {c.text && <span className=\"xicms-widget__sub\">{c.text}</span>}\n </span>\n </>\n );\n return (\n <li key={i} className=\"xicms-widget__item xicms-widget__item--card\">\n {c.href ? <a href={c.href} className=\"xicms-widget__cardlink\">{inner}</a> : inner}\n </li>\n );\n })}\n </ul>\n </div>\n );\n}\nexport const cardsWidget = defineBlock({\n type: \"cardsWidget\",\n schema: CardsWidgetSchema,\n Render: CardsWidgetRender,\n label: \"Diensten (compact)\",\n icon: \"🧩\",\n category: \"widget\",\n defaults: () => ({ heading: \"Uitgelicht\", items: [{ icon: \"⭐\", title: \"Dienst\", text: \"Korte omschrijving.\" }] }),\n});\n\n/* ── Downloads (compact) ───────────────────────────────────────────────────── */\ntype DownloadsW = z.infer<typeof DownloadsWidgetSchema>;\nfunction DownloadsWidgetRender({ block }: BlockRenderProps<DownloadsW>) {\n const items = (block.items ?? []).filter((d) => d.label && d.href).slice(0, block.limit);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--downloads\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Downloads\"}</h3>\n <ul className=\"xicms-widget__list\">\n {items.map((d, i) => (\n <li key={i} className=\"xicms-widget__item\">\n <a className=\"xicms-widget__link\" href={d.href} download>\n <span aria-hidden=\"true\">📄</span> {d.label}\n </a>\n {d.meta && <span className=\"xicms-widget__meta\">{d.meta}</span>}\n </li>\n ))}\n </ul>\n </div>\n );\n}\nexport const downloadsWidget = defineBlock({\n type: \"downloadsWidget\",\n schema: DownloadsWidgetSchema,\n Render: DownloadsWidgetRender,\n label: \"Downloads (compact)\",\n icon: \"📥\",\n category: \"widget\",\n defaults: () => ({ heading: \"Downloads\", limit: 5, items: [] }),\n});\n\n/* ── Aftelteller (compact) ─────────────────────────────────────────────────── */\ntype CountdownW = z.infer<typeof CountdownWidgetSchema>;\nfunction CountdownWidgetRender({ block }: BlockRenderProps<CountdownW>) {\n const r = useCountdown(block.target);\n if (!block.target) return null;\n return (\n <div className=\"xicms-widget xicms-widget--countdown\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n {r === null ? (\n <p className=\"xicms-widget__empty\" aria-hidden=\"true\">…</p>\n ) : r.done ? (\n <p className=\"xicms-countdown__expired\">{block.expiredText}</p>\n ) : (\n <CountdownDigits r={r} showLabels />\n )}\n </div>\n );\n}\nexport const countdownWidget = defineBlock({\n type: \"countdownWidget\",\n schema: CountdownWidgetSchema,\n Render: CountdownWidgetRender,\n label: \"Aftelteller (compact)\",\n icon: \"⏳\",\n category: \"widget\",\n defaults: () => ({ heading: \"Aftellen\", target: \"\", expiredText: \"Het is zover!\" }),\n});\n\n// Let op: exporteer GEEN aggregaat-array vanuit dit \"use client\"-bestand. Een array die hier\n// wordt opgebouwd is niet server-importeerbaar (de individuele block-exports wél). De volledige\n// lijst staat in blocks/builtins/index.ts.\n"],"mappings":"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","names":[]}
@@ -7,7 +7,7 @@ function hash(s) {
7
7
  return (h >>> 0).toString(36);
8
8
  }
9
9
  function fmt(value, allDay) {
10
- const digits = value.replace(/[-:]/g, "");
10
+ const digits = value.replace(/(z|[+-]\d{2}:?\d{2})$/i, "").replace(/[-:]/g, "");
11
11
  return allDay ? digits.slice(0, 8) : `${digits.slice(0, 8)}T${(digits.slice(9) + "0000").slice(0, 6)}`;
12
12
  }
13
13
  function nextDay(isoDate) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/blocks/builtins/lib/ics.ts"],"sourcesContent":["/**\n * Minimale iCalendar-generator voor agenda-events (\"Zet in agenda\") — puur, geen dependency.\n * Datums zonder tijd worden hele-dag-events; met tijd gebruiken we de lokale notatie (floating),\n * zodat 19:00 bij de bezoeker ook 19:00 blijft.\n */\n\nexport interface IcsEvent {\n title: string;\n /** ISO-datum of datum+tijd, bv. \"2026-07-01\" of \"2026-07-01T19:00\". */\n start: string;\n end?: string;\n location?: string;\n description?: string;\n url?: string;\n}\n\nfunction escapeText(s: string): string {\n return s.replace(/\\\\/g, \"\\\\\\\\\").replace(/;/g, \"\\\\;\").replace(/,/g, \"\\\\,\").replace(/\\r?\\n/g, \"\\\\n\");\n}\n\n/** Deterministische korte hash: zelfde event → zelfde UID (her-import werkt dan als update). */\nfunction hash(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;\n return (h >>> 0).toString(36);\n}\n\nfunction fmt(value: string, allDay: boolean): string {\n const digits = value.replace(/[-:]/g, \"\");\n return allDay ? digits.slice(0, 8) : `${digits.slice(0, 8)}T${(digits.slice(9) + \"0000\").slice(0, 6)}`;\n}\n\n/** Dag ná een ISO-datum (DTEND is exclusief bij hele-dag-events). */\nfunction nextDay(isoDate: string): string {\n const d = new Date(`${isoDate}T12:00:00Z`);\n d.setUTCDate(d.getUTCDate() + 1);\n return d.toISOString().slice(0, 10);\n}\n\nexport function makeIcs(event: IcsEvent): string | null {\n if (!event.title || !event.start || Number.isNaN(new Date(event.start).getTime())) return null;\n const allDay = !event.start.includes(\"T\");\n const dtStart = fmt(event.start, allDay);\n const endSource = event.end && !Number.isNaN(new Date(event.end).getTime()) ? event.end : null;\n const dtEnd = allDay\n ? fmt(nextDay((endSource ?? event.start).slice(0, 10)), true)\n : fmt(endSource ?? event.start, false);\n const lines = [\n \"BEGIN:VCALENDAR\",\n \"VERSION:2.0\",\n \"PRODID:-//XiCMS//Agenda//NL\",\n \"BEGIN:VEVENT\",\n `UID:${dtStart}-${hash(JSON.stringify([event.title, event.start, event.end, event.location]))}@xicms`,\n allDay ? `DTSTART;VALUE=DATE:${dtStart}` : `DTSTART:${dtStart}`,\n allDay ? `DTEND;VALUE=DATE:${dtEnd}` : `DTEND:${dtEnd}`,\n `SUMMARY:${escapeText(event.title)}`,\n ...(event.location ? [`LOCATION:${escapeText(event.location)}`] : []),\n ...(event.description ? [`DESCRIPTION:${escapeText(event.description)}`] : []),\n // URI-waarde: geen tekst-escaping, wél regeleinden strippen (voorkomt property-injectie).\n ...(event.url ? [`URL:${event.url.replace(/[\\r\\n]/g, \"\")}`] : []),\n \"END:VEVENT\",\n \"END:VCALENDAR\",\n ];\n return lines.join(\"\\r\\n\");\n}\n\n/** data:-URL voor een download-link (server-safe, geen Blob nodig). */\nexport function icsDataUrl(event: IcsEvent): string | null {\n const ics = makeIcs(event);\n return ics ? `data:text/calendar;charset=utf-8,${encodeURIComponent(ics)}` : null;\n}\n"],"mappings":"AAgBA,SAAS,WAAW,GAAmB;AACrC,SAAO,EAAE,QAAQ,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAK,EAAE,QAAQ,MAAM,KAAK,EAAE,QAAQ,UAAU,KAAK;AACnG;AAGA,SAAS,KAAK,GAAmB;AAC/B,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAK,KAAK,IAAI,KAAK,EAAE,WAAW,CAAC,IAAK;AACpE,UAAQ,MAAM,GAAG,SAAS,EAAE;AAC9B;AAEA,SAAS,IAAI,OAAe,QAAyB;AACnD,QAAM,SAAS,MAAM,QAAQ,SAAS,EAAE;AACxC,SAAO,SAAS,OAAO,MAAM,GAAG,CAAC,IAAI,GAAG,OAAO,MAAM,GAAG,CAAC,CAAC,KAAK,OAAO,MAAM,CAAC,IAAI,QAAQ,MAAM,GAAG,CAAC,CAAC;AACtG;AAGA,SAAS,QAAQ,SAAyB;AACxC,QAAM,IAAI,oBAAI,KAAK,GAAG,OAAO,YAAY;AACzC,IAAE,WAAW,EAAE,WAAW,IAAI,CAAC;AAC/B,SAAO,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACpC;AAEO,SAAS,QAAQ,OAAgC;AACtD,MAAI,CAAC,MAAM,SAAS,CAAC,MAAM,SAAS,OAAO,MAAM,IAAI,KAAK,MAAM,KAAK,EAAE,QAAQ,CAAC,EAAG,QAAO;AAC1F,QAAM,SAAS,CAAC,MAAM,MAAM,SAAS,GAAG;AACxC,QAAM,UAAU,IAAI,MAAM,OAAO,MAAM;AACvC,QAAM,YAAY,MAAM,OAAO,CAAC,OAAO,MAAM,IAAI,KAAK,MAAM,GAAG,EAAE,QAAQ,CAAC,IAAI,MAAM,MAAM;AAC1F,QAAM,QAAQ,SACV,IAAI,SAAS,aAAa,MAAM,OAAO,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,IAC1D,IAAI,aAAa,MAAM,OAAO,KAAK;AACvC,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,OAAO,IAAI,KAAK,KAAK,UAAU,CAAC,MAAM,OAAO,MAAM,OAAO,MAAM,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC;AAAA,IAC7F,SAAS,sBAAsB,OAAO,KAAK,WAAW,OAAO;AAAA,IAC7D,SAAS,oBAAoB,KAAK,KAAK,SAAS,KAAK;AAAA,IACrD,WAAW,WAAW,MAAM,KAAK,CAAC;AAAA,IAClC,GAAI,MAAM,WAAW,CAAC,YAAY,WAAW,MAAM,QAAQ,CAAC,EAAE,IAAI,CAAC;AAAA,IACnE,GAAI,MAAM,cAAc,CAAC,eAAe,WAAW,MAAM,WAAW,CAAC,EAAE,IAAI,CAAC;AAAA;AAAA,IAE5E,GAAI,MAAM,MAAM,CAAC,OAAO,MAAM,IAAI,QAAQ,WAAW,EAAE,CAAC,EAAE,IAAI,CAAC;AAAA,IAC/D;AAAA,IACA;AAAA,EACF;AACA,SAAO,MAAM,KAAK,MAAM;AAC1B;AAGO,SAAS,WAAW,OAAgC;AACzD,QAAM,MAAM,QAAQ,KAAK;AACzB,SAAO,MAAM,oCAAoC,mBAAmB,GAAG,CAAC,KAAK;AAC/E;","names":[]}
1
+ {"version":3,"sources":["../../../../src/blocks/builtins/lib/ics.ts"],"sourcesContent":["/**\n * Minimale iCalendar-generator voor agenda-events (\"Zet in agenda\") — puur, geen dependency.\n * Datums zonder tijd worden hele-dag-events; met tijd gebruiken we de lokale notatie (floating),\n * zodat 19:00 bij de bezoeker ook 19:00 blijft.\n */\n\nexport interface IcsEvent {\n title: string;\n /** ISO-datum of datum+tijd, bv. \"2026-07-01\" of \"2026-07-01T19:00\". */\n start: string;\n end?: string;\n location?: string;\n description?: string;\n url?: string;\n}\n\nfunction escapeText(s: string): string {\n return s.replace(/\\\\/g, \"\\\\\\\\\").replace(/;/g, \"\\\\;\").replace(/,/g, \"\\\\,\").replace(/\\r?\\n/g, \"\\\\n\");\n}\n\n/** Deterministische korte hash: zelfde event → zelfde UID (her-import werkt dan als update). */\nfunction hash(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;\n return (h >>> 0).toString(36);\n}\n\nfunction fmt(value: string, allDay: boolean): string {\n // Floating time is het contract (19:00 blijft 19:00 bij de bezoeker) - een meegegeven\n // TZ-suffix (\"Z\"/\"+02:00\") zou de digits-reeks corrumperen, dus die strippen we.\n const digits = value.replace(/(z|[+-]\\d{2}:?\\d{2})$/i, \"\").replace(/[-:]/g, \"\");\n return allDay ? digits.slice(0, 8) : `${digits.slice(0, 8)}T${(digits.slice(9) + \"0000\").slice(0, 6)}`;\n}\n\n/** Dag ná een ISO-datum (DTEND is exclusief bij hele-dag-events). */\nfunction nextDay(isoDate: string): string {\n const d = new Date(`${isoDate}T12:00:00Z`);\n d.setUTCDate(d.getUTCDate() + 1);\n return d.toISOString().slice(0, 10);\n}\n\nexport function makeIcs(event: IcsEvent): string | null {\n if (!event.title || !event.start || Number.isNaN(new Date(event.start).getTime())) return null;\n const allDay = !event.start.includes(\"T\");\n const dtStart = fmt(event.start, allDay);\n const endSource = event.end && !Number.isNaN(new Date(event.end).getTime()) ? event.end : null;\n const dtEnd = allDay\n ? fmt(nextDay((endSource ?? event.start).slice(0, 10)), true)\n : fmt(endSource ?? event.start, false);\n const lines = [\n \"BEGIN:VCALENDAR\",\n \"VERSION:2.0\",\n \"PRODID:-//XiCMS//Agenda//NL\",\n \"BEGIN:VEVENT\",\n `UID:${dtStart}-${hash(JSON.stringify([event.title, event.start, event.end, event.location]))}@xicms`,\n allDay ? `DTSTART;VALUE=DATE:${dtStart}` : `DTSTART:${dtStart}`,\n allDay ? `DTEND;VALUE=DATE:${dtEnd}` : `DTEND:${dtEnd}`,\n `SUMMARY:${escapeText(event.title)}`,\n ...(event.location ? [`LOCATION:${escapeText(event.location)}`] : []),\n ...(event.description ? [`DESCRIPTION:${escapeText(event.description)}`] : []),\n // URI-waarde: geen tekst-escaping, wél regeleinden strippen (voorkomt property-injectie).\n ...(event.url ? [`URL:${event.url.replace(/[\\r\\n]/g, \"\")}`] : []),\n \"END:VEVENT\",\n \"END:VCALENDAR\",\n ];\n return lines.join(\"\\r\\n\");\n}\n\n/** data:-URL voor een download-link (server-safe, geen Blob nodig). */\nexport function icsDataUrl(event: IcsEvent): string | null {\n const ics = makeIcs(event);\n return ics ? `data:text/calendar;charset=utf-8,${encodeURIComponent(ics)}` : null;\n}\n"],"mappings":"AAgBA,SAAS,WAAW,GAAmB;AACrC,SAAO,EAAE,QAAQ,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAK,EAAE,QAAQ,MAAM,KAAK,EAAE,QAAQ,UAAU,KAAK;AACnG;AAGA,SAAS,KAAK,GAAmB;AAC/B,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAK,KAAK,IAAI,KAAK,EAAE,WAAW,CAAC,IAAK;AACpE,UAAQ,MAAM,GAAG,SAAS,EAAE;AAC9B;AAEA,SAAS,IAAI,OAAe,QAAyB;AAGnD,QAAM,SAAS,MAAM,QAAQ,0BAA0B,EAAE,EAAE,QAAQ,SAAS,EAAE;AAC9E,SAAO,SAAS,OAAO,MAAM,GAAG,CAAC,IAAI,GAAG,OAAO,MAAM,GAAG,CAAC,CAAC,KAAK,OAAO,MAAM,CAAC,IAAI,QAAQ,MAAM,GAAG,CAAC,CAAC;AACtG;AAGA,SAAS,QAAQ,SAAyB;AACxC,QAAM,IAAI,oBAAI,KAAK,GAAG,OAAO,YAAY;AACzC,IAAE,WAAW,EAAE,WAAW,IAAI,CAAC;AAC/B,SAAO,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACpC;AAEO,SAAS,QAAQ,OAAgC;AACtD,MAAI,CAAC,MAAM,SAAS,CAAC,MAAM,SAAS,OAAO,MAAM,IAAI,KAAK,MAAM,KAAK,EAAE,QAAQ,CAAC,EAAG,QAAO;AAC1F,QAAM,SAAS,CAAC,MAAM,MAAM,SAAS,GAAG;AACxC,QAAM,UAAU,IAAI,MAAM,OAAO,MAAM;AACvC,QAAM,YAAY,MAAM,OAAO,CAAC,OAAO,MAAM,IAAI,KAAK,MAAM,GAAG,EAAE,QAAQ,CAAC,IAAI,MAAM,MAAM;AAC1F,QAAM,QAAQ,SACV,IAAI,SAAS,aAAa,MAAM,OAAO,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,IAC1D,IAAI,aAAa,MAAM,OAAO,KAAK;AACvC,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,OAAO,IAAI,KAAK,KAAK,UAAU,CAAC,MAAM,OAAO,MAAM,OAAO,MAAM,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC;AAAA,IAC7F,SAAS,sBAAsB,OAAO,KAAK,WAAW,OAAO;AAAA,IAC7D,SAAS,oBAAoB,KAAK,KAAK,SAAS,KAAK;AAAA,IACrD,WAAW,WAAW,MAAM,KAAK,CAAC;AAAA,IAClC,GAAI,MAAM,WAAW,CAAC,YAAY,WAAW,MAAM,QAAQ,CAAC,EAAE,IAAI,CAAC;AAAA,IACnE,GAAI,MAAM,cAAc,CAAC,eAAe,WAAW,MAAM,WAAW,CAAC,EAAE,IAAI,CAAC;AAAA;AAAA,IAE5E,GAAI,MAAM,MAAM,CAAC,OAAO,MAAM,IAAI,QAAQ,WAAW,EAAE,CAAC,EAAE,IAAI,CAAC;AAAA,IAC/D;AAAA,IACA;AAAA,EACF;AACA,SAAO,MAAM,KAAK,MAAM;AAC1B;AAGO,SAAS,WAAW,OAAgC;AACzD,QAAM,MAAM,QAAQ,KAAK;AACzB,SAAO,MAAM,oCAAoC,mBAAmB,GAAG,CAAC,KAAK;AAC/E;","names":[]}
@@ -9,7 +9,10 @@ export interface HoursRow {
9
9
  }
10
10
  /** "Maandag t/m vrijdag" / "Ma–Vr" / "Zaterdag" → JS-dagindexen; leeg als onbekend. */
11
11
  export declare function parseDays(label: string): number[];
12
- /** "09:00 – 17:00" / "9.00-17.00" → minuten sinds middernacht; null bij "Gesloten"/onparseerbaar. */
12
+ /**
13
+ * "09:00 – 17:00" / "9.00-17.00" → minuten sinds middernacht; null bij "Gesloten"/onparseerbaar.
14
+ * Sluiten ná middernacht (horeca: "18:00 – 02:00") geeft close > 24u — isOpenNow rekent daarmee.
15
+ */
13
16
  export declare function parseTimeRange(hours: string): {
14
17
  open: number;
15
18
  close: number;