@xiwebdesign/xicms-core 7.12.1 → 7.14.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 (164) hide show
  1. package/dist/admin/AdminApp.js +2 -0
  2. package/dist/admin/AdminApp.js.map +1 -1
  3. package/dist/admin/lib/mediaUsage.d.ts +17 -0
  4. package/dist/admin/lib/mediaUsage.js +32 -1
  5. package/dist/admin/lib/mediaUsage.js.map +1 -1
  6. package/dist/admin/lib/zodFields.d.ts +2 -2
  7. package/dist/admin/lib/zodFields.js +2 -0
  8. package/dist/admin/lib/zodFields.js.map +1 -1
  9. package/dist/admin/media/MediaManager.js +74 -17
  10. package/dist/admin/media/MediaManager.js.map +1 -1
  11. package/dist/admin/ui/toast.d.ts +4 -0
  12. package/dist/admin/ui/toast.js +9 -3
  13. package/dist/admin/ui/toast.js.map +1 -1
  14. package/dist/admin/visual/VisualEditor.js +69 -5
  15. package/dist/admin/visual/VisualEditor.js.map +1 -1
  16. package/dist/blocks/BlockErrorBoundary.d.ts +1 -0
  17. package/dist/blocks/BlockErrorBoundary.js +4 -0
  18. package/dist/blocks/BlockErrorBoundary.js.map +1 -1
  19. package/dist/blocks/BlockRenderer.js +1 -1
  20. package/dist/blocks/BlockRenderer.js.map +1 -1
  21. package/dist/blocks/builtinSchemas.d.ts +23 -0
  22. package/dist/blocks/builtinSchemas.js +138 -0
  23. package/dist/blocks/builtinSchemas.js.map +1 -0
  24. package/dist/blocks/builtins/Accordion.js +0 -2
  25. package/dist/blocks/builtins/Accordion.js.map +1 -1
  26. package/dist/blocks/builtins/Agenda.js +0 -2
  27. package/dist/blocks/builtins/Agenda.js.map +1 -1
  28. package/dist/blocks/builtins/Banner.js +0 -2
  29. package/dist/blocks/builtins/Banner.js.map +1 -1
  30. package/dist/blocks/builtins/BeforeAfter.js +0 -2
  31. package/dist/blocks/builtins/BeforeAfter.js.map +1 -1
  32. package/dist/blocks/builtins/Booking.js +0 -2
  33. package/dist/blocks/builtins/Booking.js.map +1 -1
  34. package/dist/blocks/builtins/Breadcrumbs.js +0 -2
  35. package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
  36. package/dist/blocks/builtins/Cards.js +0 -2
  37. package/dist/blocks/builtins/Cards.js.map +1 -1
  38. package/dist/blocks/builtins/Columns.js +0 -2
  39. package/dist/blocks/builtins/Columns.js.map +1 -1
  40. package/dist/blocks/builtins/Contact.d.ts +1 -1
  41. package/dist/blocks/builtins/Contact.js +0 -3
  42. package/dist/blocks/builtins/Contact.js.map +1 -1
  43. package/dist/blocks/builtins/Countdown.js +0 -2
  44. package/dist/blocks/builtins/Countdown.js.map +1 -1
  45. package/dist/blocks/builtins/Cta.js +0 -2
  46. package/dist/blocks/builtins/Cta.js.map +1 -1
  47. package/dist/blocks/builtins/Downloads.js +0 -2
  48. package/dist/blocks/builtins/Downloads.js.map +1 -1
  49. package/dist/blocks/builtins/Elements.js +0 -4
  50. package/dist/blocks/builtins/Elements.js.map +1 -1
  51. package/dist/blocks/builtins/Embed.js +0 -2
  52. package/dist/blocks/builtins/Embed.js.map +1 -1
  53. package/dist/blocks/builtins/Faq.js +0 -2
  54. package/dist/blocks/builtins/Faq.js.map +1 -1
  55. package/dist/blocks/builtins/FormBlock.js +0 -2
  56. package/dist/blocks/builtins/FormBlock.js.map +1 -1
  57. package/dist/blocks/builtins/Gallery.d.ts +1 -1
  58. package/dist/blocks/builtins/Gallery.js +0 -2
  59. package/dist/blocks/builtins/Gallery.js.map +1 -1
  60. package/dist/blocks/builtins/Hero.js +0 -2
  61. package/dist/blocks/builtins/Hero.js.map +1 -1
  62. package/dist/blocks/builtins/ImageBlock.js +0 -2
  63. package/dist/blocks/builtins/ImageBlock.js.map +1 -1
  64. package/dist/blocks/builtins/Jobs.d.ts +1 -1
  65. package/dist/blocks/builtins/Jobs.js +0 -2
  66. package/dist/blocks/builtins/Jobs.js.map +1 -1
  67. package/dist/blocks/builtins/Layout.js +0 -3
  68. package/dist/blocks/builtins/Layout.js.map +1 -1
  69. package/dist/blocks/builtins/MapBlock.js +0 -2
  70. package/dist/blocks/builtins/MapBlock.js.map +1 -1
  71. package/dist/blocks/builtins/NewsListBlock.js +0 -2
  72. package/dist/blocks/builtins/NewsListBlock.js.map +1 -1
  73. package/dist/blocks/builtins/Newsletter.js +0 -2
  74. package/dist/blocks/builtins/Newsletter.js.map +1 -1
  75. package/dist/blocks/builtins/OpeningHours.js +0 -2
  76. package/dist/blocks/builtins/OpeningHours.js.map +1 -1
  77. package/dist/blocks/builtins/PdfBlock.js +0 -2
  78. package/dist/blocks/builtins/PdfBlock.js.map +1 -1
  79. package/dist/blocks/builtins/Portfolio.js +0 -2
  80. package/dist/blocks/builtins/Portfolio.js.map +1 -1
  81. package/dist/blocks/builtins/Pricing.d.ts +1 -1
  82. package/dist/blocks/builtins/Pricing.js +0 -2
  83. package/dist/blocks/builtins/Pricing.js.map +1 -1
  84. package/dist/blocks/builtins/RichTextBlock.js +0 -2
  85. package/dist/blocks/builtins/RichTextBlock.js.map +1 -1
  86. package/dist/blocks/builtins/Search.js +0 -2
  87. package/dist/blocks/builtins/Search.js.map +1 -1
  88. package/dist/blocks/builtins/Sections.js +0 -10
  89. package/dist/blocks/builtins/Sections.js.map +1 -1
  90. package/dist/blocks/builtins/Slider.js +0 -2
  91. package/dist/blocks/builtins/Slider.js.map +1 -1
  92. package/dist/blocks/builtins/Spacer.js +0 -2
  93. package/dist/blocks/builtins/Spacer.js.map +1 -1
  94. package/dist/blocks/builtins/SymbolRef.js +3 -5
  95. package/dist/blocks/builtins/SymbolRef.js.map +1 -1
  96. package/dist/blocks/builtins/Team.js +0 -2
  97. package/dist/blocks/builtins/Team.js.map +1 -1
  98. package/dist/blocks/builtins/Testimonials.d.ts +1 -1
  99. package/dist/blocks/builtins/Testimonials.js +0 -2
  100. package/dist/blocks/builtins/Testimonials.js.map +1 -1
  101. package/dist/blocks/builtins/Video.d.ts +1 -1
  102. package/dist/blocks/builtins/Video.js +0 -2
  103. package/dist/blocks/builtins/Video.js.map +1 -1
  104. package/dist/blocks/builtins/WhatsappChat.js +0 -2
  105. package/dist/blocks/builtins/WhatsappChat.js.map +1 -1
  106. package/dist/blocks/builtins/Widgets.js +0 -26
  107. package/dist/blocks/builtins/Widgets.js.map +1 -1
  108. package/dist/blocks/customTemplate/definition.js +1 -1
  109. package/dist/blocks/customTemplate/definition.js.map +1 -1
  110. package/dist/blocks/registry.d.ts +24 -13
  111. package/dist/blocks/registry.js +15 -4
  112. package/dist/blocks/registry.js.map +1 -1
  113. package/dist/blocks/reportRenderFout.d.ts +13 -0
  114. package/dist/blocks/reportRenderFout.js +31 -0
  115. package/dist/blocks/reportRenderFout.js.map +1 -0
  116. package/dist/media/ImageContext.js +24 -1
  117. package/dist/media/ImageContext.js.map +1 -1
  118. package/dist/modules/shop/blocks/Cart.js +1 -2
  119. package/dist/modules/shop/blocks/Cart.js.map +1 -1
  120. package/dist/modules/shop/blocks/Checkout.js +1 -2
  121. package/dist/modules/shop/blocks/Checkout.js.map +1 -1
  122. package/dist/modules/shop/blocks/Donation.js +1 -2
  123. package/dist/modules/shop/blocks/Donation.js.map +1 -1
  124. package/dist/modules/shop/blocks/OrderStatus.js +0 -2
  125. package/dist/modules/shop/blocks/OrderStatus.js.map +1 -1
  126. package/dist/modules/shop/blocks/ProductDetail.js +1 -7
  127. package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
  128. package/dist/modules/shop/blocks/ProductGrid.js +1 -2
  129. package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
  130. package/dist/modules/shop/blocks/ShopCategories.js +0 -2
  131. package/dist/modules/shop/blocks/ShopCategories.js.map +1 -1
  132. package/dist/modules/shop/module.js +1 -0
  133. package/dist/modules/shop/module.js.map +1 -1
  134. package/dist/modules/shop/schemas.d.ts +1 -0
  135. package/dist/modules/shop/schemas.js +32 -0
  136. package/dist/modules/shop/schemas.js.map +1 -0
  137. package/dist/modules/types.d.ts +6 -0
  138. package/dist/schema/blockBase.d.ts +99 -0
  139. package/dist/schema/blockBase.js +22 -0
  140. package/dist/schema/blockBase.js.map +1 -0
  141. package/dist/schema/blocks.d.ts +1 -91
  142. package/dist/schema/blocks.js +64 -77
  143. package/dist/schema/blocks.js.map +1 -1
  144. package/dist/schema/pages.js +1 -1
  145. package/dist/schema/pages.js.map +1 -1
  146. package/dist/schema/shop.d.ts +1 -11
  147. package/dist/schema/shop.js +2 -21
  148. package/dist/schema/shop.js.map +1 -1
  149. package/dist/schema/shopGeld.d.ts +18 -0
  150. package/dist/schema/shopGeld.js +27 -0
  151. package/dist/schema/shopGeld.js.map +1 -0
  152. package/dist/schema/symbols.js +1 -1
  153. package/dist/schema/symbols.js.map +1 -1
  154. package/dist/server/blockData.js +14 -1
  155. package/dist/server/blockData.js.map +1 -1
  156. package/dist/server/index.d.ts +1 -1
  157. package/dist/server/index.js +2 -1
  158. package/dist/server/index.js.map +1 -1
  159. package/dist/server/pages.d.ts +16 -0
  160. package/dist/server/pages.js +9 -0
  161. package/dist/server/pages.js.map +1 -1
  162. package/dist/styles.css +62 -0
  163. package/dist/version.js +1 -1
  164. package/package.json +1 -1
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useState } from "react";
4
- import { SearchBlockSchema } from "../../schema/blocks.js";
5
4
  import { defineBlock } from "../registry.js";
6
5
  import { TextInput } from "@astryxdesign/core/TextInput";
7
6
  import { rankResults, highlightParts } from "./lib/searchRank.js";
@@ -81,7 +80,6 @@ function SearchRender({ block, siteId }) {
81
80
  }
82
81
  const searchBlock = defineBlock({
83
82
  type: "search",
84
- schema: SearchBlockSchema,
85
83
  Render: SearchRender,
86
84
  label: "Zoeken",
87
85
  icon: "\u{1F50D}",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/Search.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { SearchBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { rankResults, highlightParts } from \"./lib/searchRank\";\nimport { useBlockData } from \"../context\";\nimport { blocksToText, type SearchEntry } from \"../searchText\";\n\ntype SearchBlock = z.infer<typeof SearchBlockSchema>;\n\ntype Entry = SearchEntry;\n\n// De tekst-vlakkers (collectStrings/blocksToText) zijn gedeeld met de server-zoekindex:\n// zie ../searchText.ts — client en server moeten exact dezelfde tekst zien.\n\n/** Site-zoek — leest gepubliceerde pagina's (en optioneel nieuws) en filtert client-side. */\nfunction SearchRender({ block, siteId }: BlockRenderProps<SearchBlock>) {\n // Server-seed (7.12): de route levert de complete zoekindex als blockData mee. Dan is zoeken\n // instant én gratis — de client-fetch hieronder las anders de HELE pagina-collectie per\n // bezoeker, veruit de duurste read van het hele systeem.\n const serverIndex = useBlockData<Entry[]>(\"searchIndex\");\n const [entries, setEntries] = useState<Entry[]>(serverIndex ?? []);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n if (serverIndex) return;\n let cancelled = false;\n (async () => {\n // Firestore dynamisch: de zoekindex wordt pas opgebouwd wanneer het zoekblok in beeld is,\n // dus een gewone pagina downloadt de SDK niet.\n const [{ getDb }, { collection, getDocs, query, where }] = await Promise.all([\n import(\"../../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || cancelled) return;\n const out: Entry[] = [];\n try {\n const pagesSnap = await getDocs(\n query(collection(db, `sites/${siteId}/pages`), where(\"status\", \"==\", \"published\")),\n );\n pagesSnap.forEach((d) => {\n const data = d.data() as Record<string, unknown>;\n out.push({\n title: String(data.title ?? \"\"),\n path: String(data.path ?? \"/\"),\n text: blocksToText(data.blocks),\n });\n });\n if (block.includeNews) {\n const newsSnap = await getDocs(\n query(collection(db, `sites/${siteId}/news`), where(\"published\", \"==\", true)),\n );\n newsSnap.forEach((d) => {\n const data = d.data() as Record<string, unknown>;\n out.push({\n title: String(data.title ?? \"\"),\n path: `/nieuws/${String(data.slug ?? d.id)}`,\n text: String(data.excerpt ?? \"\") + \" \" + String(data.body ?? \"\").replace(/<[^>]*>/g, \" \"),\n });\n });\n }\n } catch {\n /* stil: zonder leesrechten/Firebase blijft het zoekveld leeg */\n }\n if (!cancelled) setEntries(out);\n })();\n return () => {\n cancelled = true;\n };\n }, [siteId, block.includeNews, serverIndex]);\n\n // Debounce (250ms): niet bij elke toetsaanslag opnieuw ranken/renderen.\n const [debouncedQ, setDebouncedQ] = useState(\"\");\n useEffect(() => {\n const t = setTimeout(() => setDebouncedQ(q), 250);\n return () => clearTimeout(t);\n }, [q]);\n\n const results = useMemo(() => rankResults(debouncedQ, entries), [entries, debouncedQ]);\n\n const mark = (text: string) =>\n highlightParts(text, debouncedQ).map((p, i) => (p.hit ? <mark key={i}>{p.text}</mark> : <span key={i}>{p.text}</span>));\n\n return (\n <div className=\"xicms-search\">\n <TextInput\n label={block.placeholder}\n isLabelHidden\n placeholder={block.placeholder}\n value={q}\n onChange={setQ}\n />\n {debouncedQ.trim() && (\n <ul className=\"xicms-search__results\">\n {results.length === 0 ? (\n <li className=\"xicms-search__empty\">Niets gevonden.</li>\n ) : (\n results.map((r, i) => (\n <li key={i} className=\"xicms-search__result\">\n <a href={r.path}>{mark(r.title || r.path)}</a>\n {r.snippet && <p className=\"xicms-search__snippet\">{mark(r.snippet)}</p>}\n </li>\n ))\n )}\n </ul>\n )}\n </div>\n );\n}\n\nexport const searchBlock = defineBlock({\n type: \"search\",\n schema: SearchBlockSchema,\n Render: SearchRender,\n label: \"Zoeken\",\n icon: \"🔍\",\n defaults: () => ({ placeholder: \"Zoeken…\", includeNews: true }),\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/Search.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport type { SearchBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { rankResults, highlightParts } from \"./lib/searchRank\";\nimport { useBlockData } from \"../context\";\nimport { blocksToText, type SearchEntry } from \"../searchText\";\n\ntype SearchBlock = z.infer<typeof SearchBlockSchema>;\n\ntype Entry = SearchEntry;\n\n// De tekst-vlakkers (collectStrings/blocksToText) zijn gedeeld met de server-zoekindex:\n// zie ../searchText.ts — client en server moeten exact dezelfde tekst zien.\n\n/** Site-zoek — leest gepubliceerde pagina's (en optioneel nieuws) en filtert client-side. */\nfunction SearchRender({ block, siteId }: BlockRenderProps<SearchBlock>) {\n // Server-seed (7.12): de route levert de complete zoekindex als blockData mee. Dan is zoeken\n // instant én gratis — de client-fetch hieronder las anders de HELE pagina-collectie per\n // bezoeker, veruit de duurste read van het hele systeem.\n const serverIndex = useBlockData<Entry[]>(\"searchIndex\");\n const [entries, setEntries] = useState<Entry[]>(serverIndex ?? []);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n if (serverIndex) return;\n let cancelled = false;\n (async () => {\n // Firestore dynamisch: de zoekindex wordt pas opgebouwd wanneer het zoekblok in beeld is,\n // dus een gewone pagina downloadt de SDK niet.\n const [{ getDb }, { collection, getDocs, query, where }] = await Promise.all([\n import(\"../../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || cancelled) return;\n const out: Entry[] = [];\n try {\n const pagesSnap = await getDocs(\n query(collection(db, `sites/${siteId}/pages`), where(\"status\", \"==\", \"published\")),\n );\n pagesSnap.forEach((d) => {\n const data = d.data() as Record<string, unknown>;\n out.push({\n title: String(data.title ?? \"\"),\n path: String(data.path ?? \"/\"),\n text: blocksToText(data.blocks),\n });\n });\n if (block.includeNews) {\n const newsSnap = await getDocs(\n query(collection(db, `sites/${siteId}/news`), where(\"published\", \"==\", true)),\n );\n newsSnap.forEach((d) => {\n const data = d.data() as Record<string, unknown>;\n out.push({\n title: String(data.title ?? \"\"),\n path: `/nieuws/${String(data.slug ?? d.id)}`,\n text: String(data.excerpt ?? \"\") + \" \" + String(data.body ?? \"\").replace(/<[^>]*>/g, \" \"),\n });\n });\n }\n } catch {\n /* stil: zonder leesrechten/Firebase blijft het zoekveld leeg */\n }\n if (!cancelled) setEntries(out);\n })();\n return () => {\n cancelled = true;\n };\n }, [siteId, block.includeNews, serverIndex]);\n\n // Debounce (250ms): niet bij elke toetsaanslag opnieuw ranken/renderen.\n const [debouncedQ, setDebouncedQ] = useState(\"\");\n useEffect(() => {\n const t = setTimeout(() => setDebouncedQ(q), 250);\n return () => clearTimeout(t);\n }, [q]);\n\n const results = useMemo(() => rankResults(debouncedQ, entries), [entries, debouncedQ]);\n\n const mark = (text: string) =>\n highlightParts(text, debouncedQ).map((p, i) => (p.hit ? <mark key={i}>{p.text}</mark> : <span key={i}>{p.text}</span>));\n\n return (\n <div className=\"xicms-search\">\n <TextInput\n label={block.placeholder}\n isLabelHidden\n placeholder={block.placeholder}\n value={q}\n onChange={setQ}\n />\n {debouncedQ.trim() && (\n <ul className=\"xicms-search__results\">\n {results.length === 0 ? (\n <li className=\"xicms-search__empty\">Niets gevonden.</li>\n ) : (\n results.map((r, i) => (\n <li key={i} className=\"xicms-search__result\">\n <a href={r.path}>{mark(r.title || r.path)}</a>\n {r.snippet && <p className=\"xicms-search__snippet\">{mark(r.snippet)}</p>}\n </li>\n ))\n )}\n </ul>\n )}\n </div>\n );\n}\n\nexport const searchBlock = defineBlock({\n type: \"search\",\n Render: SearchRender,\n label: \"Zoeken\",\n icon: \"🔍\",\n defaults: () => ({ placeholder: \"Zoeken…\", includeNews: true }),\n});\n"],"mappings":"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","names":[]}
@@ -1,12 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState } from "react";
4
- import {
5
- FeatureBlockSchema,
6
- CountersBlockSchema,
7
- LogosBlockSchema,
8
- TimelineBlockSchema
9
- } from "../../schema/blocks.js";
10
4
  import { defineBlock } from "../registry.js";
11
5
  import { VStack, HStack, Grid, Heading, Text, Btn } from "../ui/index.js";
12
6
  import { Img } from "../../media/ImageContext.js";
@@ -39,7 +33,6 @@ function FeatureRender({ block }) {
39
33
  }
40
34
  const featureBlock = defineBlock({
41
35
  type: "feature",
42
- schema: FeatureBlockSchema,
43
36
  Render: FeatureRender,
44
37
  label: "Beeld + tekst",
45
38
  icon: "\u{1F9F1}",
@@ -98,7 +91,6 @@ function CountersRender({ block, mode }) {
98
91
  }
99
92
  const countersBlock = defineBlock({
100
93
  type: "counters",
101
- schema: CountersBlockSchema,
102
94
  Render: CountersRender,
103
95
  label: "Tellers",
104
96
  icon: "\u{1F522}",
@@ -147,7 +139,6 @@ function LogosRender({ block }) {
147
139
  const logosBlock = defineBlock({
148
140
  type: "logos",
149
141
  fields: { "items.src": { type: "image" } },
150
- schema: LogosBlockSchema,
151
142
  Render: LogosRender,
152
143
  label: "Logo's",
153
144
  icon: "\u{1F3F7}\uFE0F",
@@ -170,7 +161,6 @@ function TimelineRender({ block }) {
170
161
  }
171
162
  const timelineBlock = defineBlock({
172
163
  type: "timeline",
173
- schema: TimelineBlockSchema,
174
164
  Render: TimelineRender,
175
165
  label: "Tijdlijn",
176
166
  icon: "\u{1F4CD}",
@@ -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\" editField=\"buttonLabel\" /></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 // _i = index in de OORSPRONKELIJKE array — de lijst wordt gefilterd op value || label.\n const items = (block.items ?? []).map((it, i) => ({ ...it, _i: i })).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) => (\n <VStack key={it._i} gap={0.5} hAlign=\"center\" style={{ textAlign: \"center\" }}>\n <Heading level={2} className=\"xicms-counters__value\">\n <span data-xicms-edit={`items.${it._i}.prefix`}>{it.prefix}</span>\n {/* In de editor-preview geen animatie (mode-contract: licht en voorspelbaar). */}\n <CountUpValue value={it.value} animate={block.animate !== false && mode !== \"preview\"} />\n <span data-xicms-edit={`items.${it._i}.suffix`}>{it.suffix}</span>\n </Heading>\n <Text color=\"secondary\" data-xicms-edit={`items.${it._i}.label`}>{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 fields: { \"items.src\": { type: \"image\" } },\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 // `_i` = ORIGINELE index (items zijn gefilterd op titel): inline edits moeten naar het juiste\n // item in de opgeslagen array schrijven — zelfde patroon als Team/Cards.\n const items = (block.items ?? []).map((it, i) => ({ ...it, _i: i })).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)\" }} data-xicms-edit={`items.${it._i}.date`}>{it.date}</Text>\n )}\n <Heading level={3} data-xicms-edit={`items.${it._i}.title`}>{it.title}</Heading>\n {it.text && <Text color=\"secondary\" data-xicms-edit={`items.${it._i}.text`}>{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 type {\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\" editField=\"buttonLabel\" /></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 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 // _i = index in de OORSPRONKELIJKE array — de lijst wordt gefilterd op value || label.\n const items = (block.items ?? []).map((it, i) => ({ ...it, _i: i })).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) => (\n <VStack key={it._i} gap={0.5} hAlign=\"center\" style={{ textAlign: \"center\" }}>\n <Heading level={2} className=\"xicms-counters__value\">\n <span data-xicms-edit={`items.${it._i}.prefix`}>{it.prefix}</span>\n {/* In de editor-preview geen animatie (mode-contract: licht en voorspelbaar). */}\n <CountUpValue value={it.value} animate={block.animate !== false && mode !== \"preview\"} />\n <span data-xicms-edit={`items.${it._i}.suffix`}>{it.suffix}</span>\n </Heading>\n <Text color=\"secondary\" data-xicms-edit={`items.${it._i}.label`}>{it.label}</Text>\n </VStack>\n ))}\n </Grid>\n </VStack>\n );\n}\nexport const countersBlock = defineBlock({\n type: \"counters\",\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 fields: { \"items.src\": { type: \"image\" } },\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 // `_i` = ORIGINELE index (items zijn gefilterd op titel): inline edits moeten naar het juiste\n // item in de opgeslagen array schrijven — zelfde patroon als Team/Cards.\n const items = (block.items ?? []).map((it, i) => ({ ...it, _i: i })).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)\" }} data-xicms-edit={`items.${it._i}.date`}>{it.date}</Text>\n )}\n <Heading level={3} data-xicms-edit={`items.${it._i}.title`}>{it.title}</Heading>\n {it.text && <Text color=\"secondary\" data-xicms-edit={`items.${it._i}.text`}>{it.text}</Text>}\n </VStack>\n </li>\n ))}\n </ol>\n </VStack>\n );\n}\nexport const timelineBlock = defineBlock({\n type: \"timeline\",\n Render: TimelineRender,\n label: \"Tijdlijn\",\n icon: \"📍\",\n defaults: () => ({ items: [{ title: \"Stap 1\", text: \"\" }] }),\n});\n"],"mappings":"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","names":[]}
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState } from "react";
4
- import { SliderBlockSchema } from "../../schema/blocks.js";
5
4
  import { defineBlock } from "../registry.js";
6
5
  import { Img } from "../../media/ImageContext.js";
7
6
  function prefersReducedMotion() {
@@ -135,7 +134,6 @@ function SliderRender({ block, mode }) {
135
134
  }
136
135
  const sliderBlock = defineBlock({
137
136
  type: "slider",
138
- schema: SliderBlockSchema,
139
137
  Render: SliderRender,
140
138
  label: "Carrousel / slider",
141
139
  icon: "\u{1F3A0}",
@@ -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, mode }: BlockRenderProps<Slider>) {\n // `_i` = ORIGINELE index (slides gefilterd): inline caption-edits schrijven naar het juiste item.\n const slides = (block.slides ?? []).map((s, i) => ({ ...s, _i: i })).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) {\n // In de editor was een verse slider ONZICHTBAAR (default-slide is leeg → weggefilterd → null):\n // de klant zag \"er gebeurt niets\" na toevoegen. Toon in preview een duidelijke lege staat;\n // publiek blijft het blok gewoon verborgen.\n if (mode === \"preview\") {\n return (\n <div className=\"xicms-slider xicms-slider--empty\">\n 🎠 Carrousel — voeg in het zijpaneel slides met een afbeelding of tekst toe.\n </div>\n );\n }\n return null;\n }\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\" data-xicms-edit={`slides.${s._i}.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":[]}
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 type { 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, mode }: BlockRenderProps<Slider>) {\n // `_i` = ORIGINELE index (slides gefilterd): inline caption-edits schrijven naar het juiste item.\n const slides = (block.slides ?? []).map((s, i) => ({ ...s, _i: i })).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) {\n // In de editor was een verse slider ONZICHTBAAR (default-slide is leeg → weggefilterd → null):\n // de klant zag \"er gebeurt niets\" na toevoegen. Toon in preview een duidelijke lege staat;\n // publiek blijft het blok gewoon verborgen.\n if (mode === \"preview\") {\n return (\n <div className=\"xicms-slider xicms-slider--empty\">\n 🎠 Carrousel — voeg in het zijpaneel slides met een afbeelding of tekst toe.\n </div>\n );\n }\n return null;\n }\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\" data-xicms-edit={`slides.${s._i}.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 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,5 +1,4 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { SpacerBlockSchema } from "../../schema/blocks.js";
3
2
  import { defineBlock } from "../registry.js";
4
3
  const HEIGHTS = { sm: 16, md: 32, lg: 64, xl: 120 };
5
4
  function SpacerRender({ block }) {
@@ -7,7 +6,6 @@ function SpacerRender({ block }) {
7
6
  }
8
7
  const spacerBlock = defineBlock({
9
8
  type: "spacer",
10
- schema: SpacerBlockSchema,
11
9
  Render: SpacerRender,
12
10
  label: "Witruimte",
13
11
  icon: "\u2195",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/Spacer.tsx"],"sourcesContent":["import type { z } from \"zod\";\nimport { SpacerBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\n\ntype Spacer = z.infer<typeof SpacerBlockSchema>;\n\nconst HEIGHTS: Record<Spacer[\"size\"], number> = { sm: 16, md: 32, lg: 64, xl: 120 };\n\nfunction SpacerRender({ block }: BlockRenderProps<Spacer>) {\n return <div aria-hidden style={{ height: HEIGHTS[block.size] }} />;\n}\n\nexport const spacerBlock = defineBlock({\n type: \"spacer\",\n schema: SpacerBlockSchema,\n Render: SpacerRender,\n label: \"Witruimte\",\n icon: \"↕\",\n defaults: () => ({ size: \"md\" }),\n});\n"],"mappings":"AASS;AART,SAAS,yBAAyB;AAClC,SAAS,mBAA0C;AAInD,MAAM,UAA0C,EAAE,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;AAElF,SAAS,aAAa,EAAE,MAAM,GAA6B;AACzD,SAAO,oBAAC,SAAI,eAAW,MAAC,OAAO,EAAE,QAAQ,QAAQ,MAAM,IAAI,EAAE,GAAG;AAClE;AAEO,MAAM,cAAc,YAAY;AAAA,EACrC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,MAAM,KAAK;AAChC,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/Spacer.tsx"],"sourcesContent":["import type { z } from \"zod\";\nimport type { SpacerBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\n\ntype Spacer = z.infer<typeof SpacerBlockSchema>;\n\nconst HEIGHTS: Record<Spacer[\"size\"], number> = { sm: 16, md: 32, lg: 64, xl: 120 };\n\nfunction SpacerRender({ block }: BlockRenderProps<Spacer>) {\n return <div aria-hidden style={{ height: HEIGHTS[block.size] }} />;\n}\n\nexport const spacerBlock = defineBlock({\n type: \"spacer\",\n Render: SpacerRender,\n label: \"Witruimte\",\n icon: \"↕\",\n defaults: () => ({ size: \"md\" }),\n});\n"],"mappings":"AASS;AAPT,SAAS,mBAA0C;AAInD,MAAM,UAA0C,EAAE,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;AAElF,SAAS,aAAa,EAAE,MAAM,GAA6B;AACzD,SAAO,oBAAC,SAAI,eAAW,MAAC,OAAO,EAAE,QAAQ,QAAQ,MAAM,IAAI,EAAE,GAAG;AAClE;AAEO,MAAM,cAAc,YAAY;AAAA,EACrC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,MAAM,KAAK;AAChC,CAAC;","names":[]}
@@ -1,8 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from "react";
4
- import { SymbolRefBlockSchema } from "../../schema/blocks.js";
5
- import { SymbolSchema } from "../../schema/symbols.js";
6
4
  import { defineBlock } from "../registry.js";
7
5
  import { BlockRenderer } from "../BlockRenderer.js";
8
6
  import { useRegistry } from "../context.js";
@@ -30,8 +28,9 @@ function SymbolRefRender({ block, siteId, mode }) {
30
28
  }
31
29
  const ref = doc(db, "sites", siteId, "symbols", block.symbolId);
32
30
  const zet = (data, bestaat) => {
33
- const parsed = bestaat ? SymbolSchema.safeParse(data) : null;
34
- setSymbol(parsed?.success ? parsed.data : null);
31
+ const d = bestaat && data && typeof data === "object" ? data : null;
32
+ const geldig = d && typeof d.name === "string" && Array.isArray(d.blocks);
33
+ setSymbol(geldig ? d : null);
35
34
  };
36
35
  if (bewerken) {
37
36
  unsub = onSnapshot(
@@ -77,7 +76,6 @@ function SymbolRefRender({ block, siteId, mode }) {
77
76
  }
78
77
  const symbolRefBlock = defineBlock({
79
78
  type: "symbolRef",
80
- schema: SymbolRefBlockSchema,
81
79
  Render: SymbolRefRender,
82
80
  label: "Symbool (gedeelde sectie)",
83
81
  icon: "\u{1F9E9}",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/SymbolRef.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { SymbolRefBlockSchema } from \"../../schema/blocks\";\nimport { SymbolSchema, type SymbolDefinition } from \"../../schema/symbols\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { BlockRenderer } from \"../BlockRenderer\";\nimport { useRegistry } from \"../context\";\nimport { useEditor } from \"../../editor/EditorProvider\";\n\ntype SymbolRef = z.infer<typeof SymbolRefBlockSchema>;\n\n/**\n * Verwijzing naar een gedeeld symbool. Op de LIVE site wordt een symbolRef normaliter al\n * server-side vervangen door de symbool-blokken (server/pages.ts `expandSymbols`) — dan komt\n * deze Render niet eens aan bod. In de editor/preview (en als client-fallback) rendert hij het\n * symbool live vanuit Firestore, met een subtiel kader zodat de editor ziet dat dit gedeelde\n * inhoud is die op meerdere pagina's meebeweegt.\n */\nfunction SymbolRefRender({ block, siteId, mode }: BlockRenderProps<SymbolRef>) {\n const registry = useRegistry();\n // Live meebewegen is alleen nuttig terwijl er BEWERKT wordt. Voor een gewone bezoeker was de\n // onSnapshot hieronder het duurste patroon van het systeem: een read plus een blijvend open\n // verbinding, per bezoeker — voor content die toch pas verandert als iemand publiceert.\n const bewerken = useEditor().open || mode === \"preview\";\n const [symbol, setSymbol] = useState<SymbolDefinition | null | undefined>(undefined);\n useEffect(() => {\n if (!block.symbolId) {\n setSymbol(null);\n return;\n }\n // Firestore dynamisch laden: een pagina zonder symbolen haalt de SDK zo nooit binnen, en een\n // pagina mét symbolen pas op het moment dat er echt iets op te halen valt.\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const [{ getDb }, { doc, onSnapshot }] = await Promise.all([\n import(\"../../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || afgebroken) {\n setSymbol(null);\n return;\n }\n const ref = doc(db, \"sites\", siteId, \"symbols\", block.symbolId!);\n const zet = (data: unknown, bestaat: boolean) => {\n const parsed = bestaat ? SymbolSchema.safeParse(data) : null;\n setSymbol(parsed?.success ? parsed.data : null);\n };\n if (bewerken) {\n unsub = onSnapshot(\n ref,\n (snap) => zet(snap.data(), snap.exists()),\n () => setSymbol(null),\n );\n } else {\n // Publieke fallback (zeldzaam: normaal is de ref al server-side geëxpandeerd): één read,\n // geen listener.\n const { getDoc } = await import(\"firebase/firestore\");\n try {\n const snap = await getDoc(ref);\n if (!afgebroken) zet(snap.data(), snap.exists());\n } catch {\n if (!afgebroken) setSymbol(null);\n }\n }\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [siteId, block.symbolId, bewerken]);\n\n if (symbol === undefined) return null; // laden — geen flits\n if (symbol === null) {\n // Verweesde referentie: nooit de pagina breken; in preview uitleggen, live niets tonen.\n return mode === \"preview\" ? (\n <p className=\"xicms-ve__hint\">🧩 Symbool niet gevonden ({block.symbolId}) — kies een ander symbool of verwijder dit blok.</p>\n ) : null;\n }\n const inner = <BlockRenderer blocks={symbol.blocks} registry={registry} siteId={siteId} mode={mode} />;\n if (mode === \"preview\") {\n return (\n <div className=\"xicms-symbol\" data-symbol={block.symbolId}>\n <div className=\"xicms-symbol__badge\">🧩 {symbol.name} — gedeeld symbool (wijzigingen gelden op élke pagina)</div>\n {inner}\n </div>\n );\n }\n return inner;\n}\n\nexport const symbolRefBlock = defineBlock({\n type: \"symbolRef\",\n schema: SymbolRefBlockSchema,\n Render: SymbolRefRender,\n label: \"Symbool (gedeelde sectie)\",\n icon: \"🧩\",\n defaults: () => ({ symbolId: \"\" }),\n});\n"],"mappings":";AA+EM,SAGU,KAHV;AA7EN,SAAS,WAAW,gBAAgB;AAEpC,SAAS,4BAA4B;AACrC,SAAS,oBAA2C;AACpD,SAAS,mBAA0C;AACnD,SAAS,qBAAqB;AAC9B,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB;AAW1B,SAAS,gBAAgB,EAAE,OAAO,QAAQ,KAAK,GAAgC;AAC7E,QAAM,WAAW,YAAY;AAI7B,QAAM,WAAW,UAAU,EAAE,QAAQ,SAAS;AAC9C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA8C,MAAS;AACnF,YAAU,MAAM;AACd,QAAI,CAAC,MAAM,UAAU;AACnB,gBAAU,IAAI;AACd;AAAA,IACF;AAGA,QAAI;AACJ,QAAI,aAAa;AACjB,UAAM,YAAY;AAChB,YAAM,CAAC,EAAE,MAAM,GAAG,EAAE,KAAK,WAAW,CAAC,IAAI,MAAM,QAAQ,IAAI;AAAA,QACzD,OAAO,uBAAuB;AAAA,QAC9B,OAAO,oBAAoB;AAAA,MAC7B,CAAC;AACD,YAAM,KAAK,MAAM;AACjB,UAAI,CAAC,MAAM,YAAY;AACrB,kBAAU,IAAI;AACd;AAAA,MACF;AACA,YAAM,MAAM,IAAI,IAAI,SAAS,QAAQ,WAAW,MAAM,QAAS;AAC/D,YAAM,MAAM,CAAC,MAAe,YAAqB;AAC/C,cAAM,SAAS,UAAU,aAAa,UAAU,IAAI,IAAI;AACxD,kBAAU,QAAQ,UAAU,OAAO,OAAO,IAAI;AAAA,MAChD;AACA,UAAI,UAAU;AACZ,gBAAQ;AAAA,UACN;AAAA,UACA,CAAC,SAAS,IAAI,KAAK,KAAK,GAAG,KAAK,OAAO,CAAC;AAAA,UACxC,MAAM,UAAU,IAAI;AAAA,QACtB;AAAA,MACF,OAAO;AAGL,cAAM,EAAE,OAAO,IAAI,MAAM,OAAO,oBAAoB;AACpD,YAAI;AACF,gBAAM,OAAO,MAAM,OAAO,GAAG;AAC7B,cAAI,CAAC,WAAY,KAAI,KAAK,KAAK,GAAG,KAAK,OAAO,CAAC;AAAA,QACjD,QAAQ;AACN,cAAI,CAAC,WAAY,WAAU,IAAI;AAAA,QACjC;AAAA,MACF;AAAA,IACF,GAAG;AACH,WAAO,MAAM;AACX,mBAAa;AACb,cAAQ;AAAA,IACV;AAAA,EACF,GAAG,CAAC,QAAQ,MAAM,UAAU,QAAQ,CAAC;AAErC,MAAI,WAAW,OAAW,QAAO;AACjC,MAAI,WAAW,MAAM;AAEnB,WAAO,SAAS,YACd,qBAAC,OAAE,WAAU,kBAAiB;AAAA;AAAA,MAA2B,MAAM;AAAA,MAAS;AAAA,OAAiD,IACvH;AAAA,EACN;AACA,QAAM,QAAQ,oBAAC,iBAAc,QAAQ,OAAO,QAAQ,UAAoB,QAAgB,MAAY;AACpG,MAAI,SAAS,WAAW;AACtB,WACE,qBAAC,SAAI,WAAU,gBAAe,eAAa,MAAM,UAC/C;AAAA,2BAAC,SAAI,WAAU,uBAAsB;AAAA;AAAA,QAAI,OAAO;AAAA,QAAK;AAAA,SAAsD;AAAA,MAC1G;AAAA,OACH;AAAA,EAEJ;AACA,SAAO;AACT;AAEO,MAAM,iBAAiB,YAAY;AAAA,EACxC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,UAAU,GAAG;AAClC,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/SymbolRef.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport type { SymbolRefBlockSchema } from \"../../schema/blocks\";\nimport type { SymbolDefinition } from \"../../schema/symbols\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { BlockRenderer } from \"../BlockRenderer\";\nimport { useRegistry } from \"../context\";\nimport { useEditor } from \"../../editor/EditorProvider\";\n\ntype SymbolRef = z.infer<typeof SymbolRefBlockSchema>;\n\n/**\n * Verwijzing naar een gedeeld symbool. Op de LIVE site wordt een symbolRef normaliter al\n * server-side vervangen door de symbool-blokken (server/pages.ts `expandSymbols`) — dan komt\n * deze Render niet eens aan bod. In de editor/preview (en als client-fallback) rendert hij het\n * symbool live vanuit Firestore, met een subtiel kader zodat de editor ziet dat dit gedeelde\n * inhoud is die op meerdere pagina's meebeweegt.\n */\nfunction SymbolRefRender({ block, siteId, mode }: BlockRenderProps<SymbolRef>) {\n const registry = useRegistry();\n // Live meebewegen is alleen nuttig terwijl er BEWERKT wordt. Voor een gewone bezoeker was de\n // onSnapshot hieronder het duurste patroon van het systeem: een read plus een blijvend open\n // verbinding, per bezoeker — voor content die toch pas verandert als iemand publiceert.\n const bewerken = useEditor().open || mode === \"preview\";\n const [symbol, setSymbol] = useState<SymbolDefinition | null | undefined>(undefined);\n useEffect(() => {\n if (!block.symbolId) {\n setSymbol(null);\n return;\n }\n // Firestore dynamisch laden: een pagina zonder symbolen haalt de SDK zo nooit binnen, en een\n // pagina mét symbolen pas op het moment dat er echt iets op te halen valt.\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const [{ getDb }, { doc, onSnapshot }] = await Promise.all([\n import(\"../../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || afgebroken) {\n setSymbol(null);\n return;\n }\n const ref = doc(db, \"sites\", siteId, \"symbols\", block.symbolId!);\n const zet = (data: unknown, bestaat: boolean) => {\n // Lichte vormcheck i.p.v. zod (7.13): dit is een publiek renderpad en de enige plek die\n // anders het hele symbols-schema (en daarmee de zod-runtime) in de bezoekersbundel trok.\n // De BlockRenderer eronder valideert de blokken zelf al per type.\n const d = bestaat && data && typeof data === \"object\" ? (data as { name?: unknown; blocks?: unknown }) : null;\n const geldig = d && typeof d.name === \"string\" && Array.isArray(d.blocks);\n setSymbol(geldig ? (d as unknown as SymbolDefinition) : null);\n };\n if (bewerken) {\n unsub = onSnapshot(\n ref,\n (snap) => zet(snap.data(), snap.exists()),\n () => setSymbol(null),\n );\n } else {\n // Publieke fallback (zeldzaam: normaal is de ref al server-side geëxpandeerd): één read,\n // geen listener.\n const { getDoc } = await import(\"firebase/firestore\");\n try {\n const snap = await getDoc(ref);\n if (!afgebroken) zet(snap.data(), snap.exists());\n } catch {\n if (!afgebroken) setSymbol(null);\n }\n }\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [siteId, block.symbolId, bewerken]);\n\n if (symbol === undefined) return null; // laden — geen flits\n if (symbol === null) {\n // Verweesde referentie: nooit de pagina breken; in preview uitleggen, live niets tonen.\n return mode === \"preview\" ? (\n <p className=\"xicms-ve__hint\">🧩 Symbool niet gevonden ({block.symbolId}) — kies een ander symbool of verwijder dit blok.</p>\n ) : null;\n }\n const inner = <BlockRenderer blocks={symbol.blocks} registry={registry} siteId={siteId} mode={mode} />;\n if (mode === \"preview\") {\n return (\n <div className=\"xicms-symbol\" data-symbol={block.symbolId}>\n <div className=\"xicms-symbol__badge\">🧩 {symbol.name} — gedeeld symbool (wijzigingen gelden op élke pagina)</div>\n {inner}\n </div>\n );\n }\n return inner;\n}\n\nexport const symbolRefBlock = defineBlock({\n type: \"symbolRef\",\n Render: SymbolRefRender,\n label: \"Symbool (gedeelde sectie)\",\n icon: \"🧩\",\n defaults: () => ({ symbolId: \"\" }),\n});\n"],"mappings":";AAmFM,SAGU,KAHV;AAjFN,SAAS,WAAW,gBAAgB;AAIpC,SAAS,mBAA0C;AACnD,SAAS,qBAAqB;AAC9B,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB;AAW1B,SAAS,gBAAgB,EAAE,OAAO,QAAQ,KAAK,GAAgC;AAC7E,QAAM,WAAW,YAAY;AAI7B,QAAM,WAAW,UAAU,EAAE,QAAQ,SAAS;AAC9C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA8C,MAAS;AACnF,YAAU,MAAM;AACd,QAAI,CAAC,MAAM,UAAU;AACnB,gBAAU,IAAI;AACd;AAAA,IACF;AAGA,QAAI;AACJ,QAAI,aAAa;AACjB,UAAM,YAAY;AAChB,YAAM,CAAC,EAAE,MAAM,GAAG,EAAE,KAAK,WAAW,CAAC,IAAI,MAAM,QAAQ,IAAI;AAAA,QACzD,OAAO,uBAAuB;AAAA,QAC9B,OAAO,oBAAoB;AAAA,MAC7B,CAAC;AACD,YAAM,KAAK,MAAM;AACjB,UAAI,CAAC,MAAM,YAAY;AACrB,kBAAU,IAAI;AACd;AAAA,MACF;AACA,YAAM,MAAM,IAAI,IAAI,SAAS,QAAQ,WAAW,MAAM,QAAS;AAC/D,YAAM,MAAM,CAAC,MAAe,YAAqB;AAI/C,cAAM,IAAI,WAAW,QAAQ,OAAO,SAAS,WAAY,OAAgD;AACzG,cAAM,SAAS,KAAK,OAAO,EAAE,SAAS,YAAY,MAAM,QAAQ,EAAE,MAAM;AACxE,kBAAU,SAAU,IAAoC,IAAI;AAAA,MAC9D;AACA,UAAI,UAAU;AACZ,gBAAQ;AAAA,UACN;AAAA,UACA,CAAC,SAAS,IAAI,KAAK,KAAK,GAAG,KAAK,OAAO,CAAC;AAAA,UACxC,MAAM,UAAU,IAAI;AAAA,QACtB;AAAA,MACF,OAAO;AAGL,cAAM,EAAE,OAAO,IAAI,MAAM,OAAO,oBAAoB;AACpD,YAAI;AACF,gBAAM,OAAO,MAAM,OAAO,GAAG;AAC7B,cAAI,CAAC,WAAY,KAAI,KAAK,KAAK,GAAG,KAAK,OAAO,CAAC;AAAA,QACjD,QAAQ;AACN,cAAI,CAAC,WAAY,WAAU,IAAI;AAAA,QACjC;AAAA,MACF;AAAA,IACF,GAAG;AACH,WAAO,MAAM;AACX,mBAAa;AACb,cAAQ;AAAA,IACV;AAAA,EACF,GAAG,CAAC,QAAQ,MAAM,UAAU,QAAQ,CAAC;AAErC,MAAI,WAAW,OAAW,QAAO;AACjC,MAAI,WAAW,MAAM;AAEnB,WAAO,SAAS,YACd,qBAAC,OAAE,WAAU,kBAAiB;AAAA;AAAA,MAA2B,MAAM;AAAA,MAAS;AAAA,OAAiD,IACvH;AAAA,EACN;AACA,QAAM,QAAQ,oBAAC,iBAAc,QAAQ,OAAO,QAAQ,UAAoB,QAAgB,MAAY;AACpG,MAAI,SAAS,WAAW;AACtB,WACE,qBAAC,SAAI,WAAU,gBAAe,eAAa,MAAM,UAC/C;AAAA,2BAAC,SAAI,WAAU,uBAAsB;AAAA;AAAA,QAAI,OAAO;AAAA,QAAK;AAAA,SAAsD;AAAA,MAC1G;AAAA,OACH;AAAA,EAEJ;AACA,SAAO;AACT;AAEO,MAAM,iBAAiB,YAAY;AAAA,EACxC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,UAAU,GAAG;AAClC,CAAC;","names":[]}
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { TeamBlockSchema } from "../../schema/blocks.js";
4
3
  import { defineBlock } from "../registry.js";
5
4
  import { VStack, HStack, Grid, Card, Heading, Text, Avatar } from "../ui/index.js";
6
5
  import { jsonLdHtml } from "./jsonld.js";
@@ -56,7 +55,6 @@ function TeamRender({ block }) {
56
55
  }
57
56
  const teamBlock = defineBlock({
58
57
  type: "team",
59
- schema: TeamBlockSchema,
60
58
  Render: TeamRender,
61
59
  label: "Team",
62
60
  icon: "\u{1F465}",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/Team.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport { TeamBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { VStack, HStack, Grid, Card, Heading, Text, Avatar } from \"../ui\";\nimport { jsonLdHtml } from \"./jsonld\";\n\ntype Team = z.infer<typeof TeamBlockSchema>;\n\nfunction personJsonLd(members: Team[\"members\"]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ItemList\",\n itemListElement: members\n .filter((m) => m.name)\n .map((m, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n item: {\n \"@type\": \"Person\",\n name: m.name,\n ...(m.role ? { jobTitle: m.role } : {}),\n ...(m.email ? { email: m.email } : {}),\n ...(m.phone ? { telephone: m.phone } : {}),\n ...(m.photoUrl ? { image: m.photoUrl } : {}),\n ...(m.social?.length ? { sameAs: m.social.map((s) => s.href) } : {}),\n },\n })),\n };\n}\n\n/** Team-/medewerkersoverzicht als kaarten (Astryx), optioneel gegroepeerd per afdeling. */\nfunction TeamRender({ block }: BlockRenderProps<Team>) {\n // _i = index in de OORSPRONKELIJKE array — nodig voor de inline-edit-paden bij groepering.\n const members = (block.members ?? []).map((m, i) => ({ ...m, _i: i })).filter((m) => m.name);\n if (members.length === 0) return null;\n\n // Groepen in volgorde van eerste voorkomen; leden zonder afdeling achteraan onder één kop.\n const groups = block.groupByDepartment\n ? [...new Set(members.map((m) => m.department ?? \"\"))]\n .sort((a, b) => (a === \"\" ? 1 : b === \"\" ? -1 : 0))\n .map((dep) => ({ dep, members: members.filter((m) => (m.department ?? \"\") === dep) }))\n : [{ dep: \"\", members }];\n\n const grid = (list: typeof members) => (\n <Grid columns={{ minWidth: 220, max: 4 }} gap={4}>\n {list.map((m) => {\n const i = m._i;\n return (\n <Card key={i} padding={4} height=\"100%\">\n <VStack gap={1} hAlign=\"center\" style={{ textAlign: \"center\", height: \"100%\" }}>\n <Avatar src={m.photoUrl} name={m.name} size={96} />\n <Heading level={3} data-xicms-edit={`members.${i}.name`}>{m.name}</Heading>\n {m.role && <Text type=\"label\" data-xicms-edit={`members.${i}.role`} style={{ color: \"var(--color-accent)\" }}>{m.role}</Text>}\n {m.bio && <Text color=\"secondary\" data-xicms-edit={`members.${i}.bio`}>{m.bio}</Text>}\n {m.email && (\n <a href={`mailto:${m.email}`} style={{ color: \"var(--color-accent)\" }}>\n <Text type=\"supporting\" data-xicms-edit={`members.${i}.email`}>{m.email}</Text>\n </a>\n )}\n {m.phone && <Text type=\"supporting\" color=\"secondary\" data-xicms-edit={`members.${i}.phone`}>{m.phone}</Text>}\n {m.social && m.social.length > 0 && (\n <HStack gap={2} hAlign=\"center\" as=\"ul\" style={{ listStyle: \"none\", margin: 0, padding: 0, flexWrap: \"wrap\" }}>\n {m.social.map((s, j) => (\n <li key={j}>\n <a href={s.href} rel=\"noopener noreferrer\" target=\"_blank\" style={{ color: \"var(--color-accent)\" }}>\n <Text type=\"supporting\" data-xicms-edit={`members.${i}.social.${j}.platform`}>{s.platform}</Text>\n </a>\n </li>\n ))}\n </HStack>\n )}\n </VStack>\n </Card>\n );\n })}\n </Grid>\n );\n\n return (\n <VStack gap={4} as=\"section\" className=\"xicms-team\">\n {block.heading && (\n <Heading level={2} justify=\"center\" data-xicms-edit=\"heading\" style={{ textAlign: \"center\" }}>{block.heading}</Heading>\n )}\n {groups.map(({ dep, members: list }) => (\n <VStack key={dep || \"_overig\"} gap={3}>\n {block.groupByDepartment && groups.length > 1 && (\n <Heading level={3} justify=\"center\" style={{ textAlign: \"center\" }}>\n {dep || \"Overig\"}\n </Heading>\n )}\n {grid(list)}\n </VStack>\n ))}\n {block.jsonLd && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: jsonLdHtml(personJsonLd(members)) }}\n />\n )}\n </VStack>\n );\n}\n\nexport const teamBlock = defineBlock({\n type: \"team\",\n schema: TeamBlockSchema,\n Render: TeamRender,\n label: \"Team\",\n icon: \"👥\",\n defaults: () => ({\n heading: \"Ons team\",\n members: [{ name: \"Naam Medewerker\", role: \"Functie\" }],\n }),\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/Team.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport type { TeamBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { VStack, HStack, Grid, Card, Heading, Text, Avatar } from \"../ui\";\nimport { jsonLdHtml } from \"./jsonld\";\n\ntype Team = z.infer<typeof TeamBlockSchema>;\n\nfunction personJsonLd(members: Team[\"members\"]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ItemList\",\n itemListElement: members\n .filter((m) => m.name)\n .map((m, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n item: {\n \"@type\": \"Person\",\n name: m.name,\n ...(m.role ? { jobTitle: m.role } : {}),\n ...(m.email ? { email: m.email } : {}),\n ...(m.phone ? { telephone: m.phone } : {}),\n ...(m.photoUrl ? { image: m.photoUrl } : {}),\n ...(m.social?.length ? { sameAs: m.social.map((s) => s.href) } : {}),\n },\n })),\n };\n}\n\n/** Team-/medewerkersoverzicht als kaarten (Astryx), optioneel gegroepeerd per afdeling. */\nfunction TeamRender({ block }: BlockRenderProps<Team>) {\n // _i = index in de OORSPRONKELIJKE array — nodig voor de inline-edit-paden bij groepering.\n const members = (block.members ?? []).map((m, i) => ({ ...m, _i: i })).filter((m) => m.name);\n if (members.length === 0) return null;\n\n // Groepen in volgorde van eerste voorkomen; leden zonder afdeling achteraan onder één kop.\n const groups = block.groupByDepartment\n ? [...new Set(members.map((m) => m.department ?? \"\"))]\n .sort((a, b) => (a === \"\" ? 1 : b === \"\" ? -1 : 0))\n .map((dep) => ({ dep, members: members.filter((m) => (m.department ?? \"\") === dep) }))\n : [{ dep: \"\", members }];\n\n const grid = (list: typeof members) => (\n <Grid columns={{ minWidth: 220, max: 4 }} gap={4}>\n {list.map((m) => {\n const i = m._i;\n return (\n <Card key={i} padding={4} height=\"100%\">\n <VStack gap={1} hAlign=\"center\" style={{ textAlign: \"center\", height: \"100%\" }}>\n <Avatar src={m.photoUrl} name={m.name} size={96} />\n <Heading level={3} data-xicms-edit={`members.${i}.name`}>{m.name}</Heading>\n {m.role && <Text type=\"label\" data-xicms-edit={`members.${i}.role`} style={{ color: \"var(--color-accent)\" }}>{m.role}</Text>}\n {m.bio && <Text color=\"secondary\" data-xicms-edit={`members.${i}.bio`}>{m.bio}</Text>}\n {m.email && (\n <a href={`mailto:${m.email}`} style={{ color: \"var(--color-accent)\" }}>\n <Text type=\"supporting\" data-xicms-edit={`members.${i}.email`}>{m.email}</Text>\n </a>\n )}\n {m.phone && <Text type=\"supporting\" color=\"secondary\" data-xicms-edit={`members.${i}.phone`}>{m.phone}</Text>}\n {m.social && m.social.length > 0 && (\n <HStack gap={2} hAlign=\"center\" as=\"ul\" style={{ listStyle: \"none\", margin: 0, padding: 0, flexWrap: \"wrap\" }}>\n {m.social.map((s, j) => (\n <li key={j}>\n <a href={s.href} rel=\"noopener noreferrer\" target=\"_blank\" style={{ color: \"var(--color-accent)\" }}>\n <Text type=\"supporting\" data-xicms-edit={`members.${i}.social.${j}.platform`}>{s.platform}</Text>\n </a>\n </li>\n ))}\n </HStack>\n )}\n </VStack>\n </Card>\n );\n })}\n </Grid>\n );\n\n return (\n <VStack gap={4} as=\"section\" className=\"xicms-team\">\n {block.heading && (\n <Heading level={2} justify=\"center\" data-xicms-edit=\"heading\" style={{ textAlign: \"center\" }}>{block.heading}</Heading>\n )}\n {groups.map(({ dep, members: list }) => (\n <VStack key={dep || \"_overig\"} gap={3}>\n {block.groupByDepartment && groups.length > 1 && (\n <Heading level={3} justify=\"center\" style={{ textAlign: \"center\" }}>\n {dep || \"Overig\"}\n </Heading>\n )}\n {grid(list)}\n </VStack>\n ))}\n {block.jsonLd && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: jsonLdHtml(personJsonLd(members)) }}\n />\n )}\n </VStack>\n );\n}\n\nexport const teamBlock = defineBlock({\n type: \"team\",\n Render: TeamRender,\n label: \"Team\",\n icon: \"👥\",\n defaults: () => ({\n heading: \"Ons team\",\n members: [{ name: \"Naam Medewerker\", role: \"Functie\" }],\n }),\n});\n"],"mappings":"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","names":[]}
@@ -1,5 +1,5 @@
1
1
  import type { z } from "zod";
2
- import { TestimonialsBlockSchema } from "../../schema/blocks.js";
2
+ import type { TestimonialsBlockSchema } from "../../schema/blocks.js";
3
3
  type Testimonials = z.infer<typeof TestimonialsBlockSchema>;
4
4
  /** Gemiddelde beoordeling over de items met een rating — voor de sterren-snippet in Google. */
5
5
  export declare function aggregateRatingJsonLd(items: Testimonials["items"]): Record<string, unknown> | null;
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { TestimonialsBlockSchema } from "../../schema/blocks.js";
4
3
  import { defineBlock } from "../registry.js";
5
4
  import { VStack, HStack, Grid, Card, Heading, Text, Avatar } from "../ui/index.js";
6
5
  import { jsonLdHtml } from "./jsonld.js";
@@ -68,7 +67,6 @@ function TestimonialsRender({ block }) {
68
67
  }
69
68
  const testimonialsBlock = defineBlock({
70
69
  type: "testimonials",
71
- schema: TestimonialsBlockSchema,
72
70
  Render: TestimonialsRender,
73
71
  label: "Reviews",
74
72
  icon: "\u2B50",
@@ -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\";\nimport { jsonLdHtml } from \"./jsonld\";\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: jsonLdHtml(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 type { TestimonialsBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { VStack, HStack, Grid, Card, Heading, Text, Avatar } from \"../ui\";\nimport { jsonLdHtml } from \"./jsonld\";\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: jsonLdHtml(reviewJsonLd(items)) }}\n />\n )}\n </VStack>\n );\n}\n\nexport const testimonialsBlock = defineBlock({\n type: \"testimonials\",\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,5 +1,5 @@
1
1
  import type { z } from "zod";
2
- import { VideoBlockSchema } from "../../schema/blocks.js";
2
+ import type { VideoBlockSchema } from "../../schema/blocks.js";
3
3
  type Video = z.infer<typeof VideoBlockSchema>;
4
4
  /** Bouwt een embed-URL uit een id of volledige URL. */
5
5
  export declare function embedUrl(provider: Video["provider"], src: string, autoplay?: boolean): string;
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
- import { VideoBlockSchema } from "../../schema/blocks.js";
5
4
  import { defineBlock } from "../registry.js";
6
5
  import { ConsentEmbed } from "./consentEmbed.js";
7
6
  function embedUrl(provider, src, autoplay = false) {
@@ -63,7 +62,6 @@ function VideoRender({ block, mode }) {
63
62
  }
64
63
  const videoBlock = defineBlock({
65
64
  type: "video",
66
- schema: VideoBlockSchema,
67
65
  Render: VideoRender,
68
66
  label: "Video",
69
67
  icon: "\u{1F3AC}",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/Video.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport type { z } from \"zod\";\nimport { VideoBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { ConsentEmbed } from \"./consentEmbed\";\n\ntype Video = z.infer<typeof VideoBlockSchema>;\n\n/** Bouwt een embed-URL uit een id of volledige URL. */\nexport function embedUrl(provider: Video[\"provider\"], src: string, autoplay = false): string {\n if (provider === \"youtube\") {\n const id = src.match(/(?:v=|youtu\\.be\\/|embed\\/)([\\w-]{6,})/)?.[1] ?? src;\n return `https://www.youtube-nocookie.com/embed/${id}${autoplay ? \"?autoplay=1\" : \"\"}`;\n }\n if (provider === \"vimeo\") {\n const id = src.match(/(?:vimeo\\.com\\/)?(\\d{6,})/)?.[1] ?? src;\n return `https://player.vimeo.com/video/${id}${autoplay ? \"?autoplay=1\" : \"\"}`;\n }\n return src;\n}\n\n/** Thumbnail voor de click-to-play-facade: eigen poster of de YouTube-thumb. */\nexport function facadeThumb(block: Pick<Video, \"provider\" | \"src\" | \"posterUrl\">): string | null {\n if (block.posterUrl) return block.posterUrl;\n if (block.provider === \"youtube\") {\n const id = block.src.match(/(?:v=|youtu\\.be\\/|embed\\/)([\\w-]{6,})/)?.[1] ?? block.src;\n return id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : null;\n }\n return null; // vimeo-thumbs vereisen een API-call; zonder poster geen facade-beeld\n}\n\nfunction VideoRender({ block, mode }: BlockRenderProps<Video>) {\n const [activated, setActivated] = useState(false);\n if (!block.src) return null;\n\n const useFacade = block.facade && block.provider !== \"file\" && !activated;\n const thumb = useFacade ? facadeThumb(block) : null;\n\n const media =\n block.provider === \"file\" ? (\n <video className=\"xicms-video__player\" src={block.src} controls poster={block.posterUrl || undefined} />\n ) : useFacade ? (\n // Click-to-play: geen third-party iframe tot de bezoeker klikt (sneller + privacyvriendelijker).\n <button\n type=\"button\"\n className=\"xicms-video__facade\"\n onClick={() => mode !== \"preview\" && setActivated(true)}\n aria-label={`Video afspelen: ${block.title ?? \"video\"}`}\n >\n {thumb ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img src={thumb} alt=\"\" loading=\"lazy\" />\n ) : (\n <span className=\"xicms-video__facade-bg\" aria-hidden />\n )}\n <span className=\"xicms-video__play\" aria-hidden>▶</span>\n </button>\n ) : (\n <div className=\"xicms-video__frame\">\n <iframe\n src={embedUrl(block.provider, block.src, activated)}\n title={block.title ?? \"Video\"}\n loading=\"lazy\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n />\n </div>\n );\n return (\n <figure className=\"xicms-video\">\n {/* Zelf-gehost bestand (provider \"file\") doet géén third-party-request en zet geen cookies —\n daar is geen cookie-toestemming voor nodig; alleen youtube/vimeo-embeds gaten. */}\n <ConsentEmbed gated={block.consentGated && !useFacade && block.provider !== \"file\"} preview={mode === \"preview\"} label=\"Video\">{media}</ConsentEmbed>\n {block.caption && <figcaption data-xicms-edit=\"caption\">{block.caption}</figcaption>}\n </figure>\n );\n}\n\nexport const videoBlock = defineBlock({\n type: \"video\",\n schema: VideoBlockSchema,\n Render: VideoRender,\n label: \"Video\",\n icon: \"🎬\",\n defaults: () => ({ provider: \"youtube\", src: \"\", consentGated: true }),\n});\n"],"mappings":";AA0CM,cAGA,YAHA;AAxCN,SAAS,gBAAgB;AAEzB,SAAS,wBAAwB;AACjC,SAAS,mBAA0C;AACnD,SAAS,oBAAoB;AAKtB,SAAS,SAAS,UAA6B,KAAa,WAAW,OAAe;AAC3F,MAAI,aAAa,WAAW;AAC1B,UAAM,KAAK,IAAI,MAAM,uCAAuC,IAAI,CAAC,KAAK;AACtE,WAAO,0CAA0C,EAAE,GAAG,WAAW,gBAAgB,EAAE;AAAA,EACrF;AACA,MAAI,aAAa,SAAS;AACxB,UAAM,KAAK,IAAI,MAAM,2BAA2B,IAAI,CAAC,KAAK;AAC1D,WAAO,kCAAkC,EAAE,GAAG,WAAW,gBAAgB,EAAE;AAAA,EAC7E;AACA,SAAO;AACT;AAGO,SAAS,YAAY,OAAqE;AAC/F,MAAI,MAAM,UAAW,QAAO,MAAM;AAClC,MAAI,MAAM,aAAa,WAAW;AAChC,UAAM,KAAK,MAAM,IAAI,MAAM,uCAAuC,IAAI,CAAC,KAAK,MAAM;AAClF,WAAO,KAAK,0BAA0B,EAAE,mBAAmB;AAAA,EAC7D;AACA,SAAO;AACT;AAEA,SAAS,YAAY,EAAE,OAAO,KAAK,GAA4B;AAC7D,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,MAAI,CAAC,MAAM,IAAK,QAAO;AAEvB,QAAM,YAAY,MAAM,UAAU,MAAM,aAAa,UAAU,CAAC;AAChE,QAAM,QAAQ,YAAY,YAAY,KAAK,IAAI;AAE/C,QAAM,QACJ,MAAM,aAAa,SACjB,oBAAC,WAAM,WAAU,uBAAsB,KAAK,MAAM,KAAK,UAAQ,MAAC,QAAQ,MAAM,aAAa,QAAW,IACpG;AAAA;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,SAAS,MAAM,SAAS,aAAa,aAAa,IAAI;AAAA,QACtD,cAAY,mBAAmB,MAAM,SAAS,OAAO;AAAA,QAEpD;AAAA;AAAA;AAAA,YAEC,oBAAC,SAAI,KAAK,OAAO,KAAI,IAAG,SAAQ,QAAO;AAAA,cAEvC,oBAAC,UAAK,WAAU,0BAAyB,eAAW,MAAC;AAAA,UAEvD,oBAAC,UAAK,WAAU,qBAAoB,eAAW,MAAC,oBAAC;AAAA;AAAA;AAAA,IACnD;AAAA,MAEA,oBAAC,SAAI,WAAU,sBACb;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,SAAS,MAAM,UAAU,MAAM,KAAK,SAAS;AAAA,MAClD,OAAO,MAAM,SAAS;AAAA,MACtB,SAAQ;AAAA,MACR,OAAM;AAAA,MACN,iBAAe;AAAA;AAAA,EACjB,GACF;AAEJ,SACE,qBAAC,YAAO,WAAU,eAGhB;AAAA,wBAAC,gBAAa,OAAO,MAAM,gBAAgB,CAAC,aAAa,MAAM,aAAa,QAAQ,SAAS,SAAS,WAAW,OAAM,SAAS,iBAAM;AAAA,IACrI,MAAM,WAAW,oBAAC,gBAAW,mBAAgB,WAAW,gBAAM,SAAQ;AAAA,KACzE;AAEJ;AAEO,MAAM,aAAa,YAAY;AAAA,EACpC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,UAAU,WAAW,KAAK,IAAI,cAAc,KAAK;AACtE,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/Video.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport type { z } from \"zod\";\nimport type { VideoBlockSchema } from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { ConsentEmbed } from \"./consentEmbed\";\n\ntype Video = z.infer<typeof VideoBlockSchema>;\n\n/** Bouwt een embed-URL uit een id of volledige URL. */\nexport function embedUrl(provider: Video[\"provider\"], src: string, autoplay = false): string {\n if (provider === \"youtube\") {\n const id = src.match(/(?:v=|youtu\\.be\\/|embed\\/)([\\w-]{6,})/)?.[1] ?? src;\n return `https://www.youtube-nocookie.com/embed/${id}${autoplay ? \"?autoplay=1\" : \"\"}`;\n }\n if (provider === \"vimeo\") {\n const id = src.match(/(?:vimeo\\.com\\/)?(\\d{6,})/)?.[1] ?? src;\n return `https://player.vimeo.com/video/${id}${autoplay ? \"?autoplay=1\" : \"\"}`;\n }\n return src;\n}\n\n/** Thumbnail voor de click-to-play-facade: eigen poster of de YouTube-thumb. */\nexport function facadeThumb(block: Pick<Video, \"provider\" | \"src\" | \"posterUrl\">): string | null {\n if (block.posterUrl) return block.posterUrl;\n if (block.provider === \"youtube\") {\n const id = block.src.match(/(?:v=|youtu\\.be\\/|embed\\/)([\\w-]{6,})/)?.[1] ?? block.src;\n return id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : null;\n }\n return null; // vimeo-thumbs vereisen een API-call; zonder poster geen facade-beeld\n}\n\nfunction VideoRender({ block, mode }: BlockRenderProps<Video>) {\n const [activated, setActivated] = useState(false);\n if (!block.src) return null;\n\n const useFacade = block.facade && block.provider !== \"file\" && !activated;\n const thumb = useFacade ? facadeThumb(block) : null;\n\n const media =\n block.provider === \"file\" ? (\n <video className=\"xicms-video__player\" src={block.src} controls poster={block.posterUrl || undefined} />\n ) : useFacade ? (\n // Click-to-play: geen third-party iframe tot de bezoeker klikt (sneller + privacyvriendelijker).\n <button\n type=\"button\"\n className=\"xicms-video__facade\"\n onClick={() => mode !== \"preview\" && setActivated(true)}\n aria-label={`Video afspelen: ${block.title ?? \"video\"}`}\n >\n {thumb ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img src={thumb} alt=\"\" loading=\"lazy\" />\n ) : (\n <span className=\"xicms-video__facade-bg\" aria-hidden />\n )}\n <span className=\"xicms-video__play\" aria-hidden>▶</span>\n </button>\n ) : (\n <div className=\"xicms-video__frame\">\n <iframe\n src={embedUrl(block.provider, block.src, activated)}\n title={block.title ?? \"Video\"}\n loading=\"lazy\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n />\n </div>\n );\n return (\n <figure className=\"xicms-video\">\n {/* Zelf-gehost bestand (provider \"file\") doet géén third-party-request en zet geen cookies —\n daar is geen cookie-toestemming voor nodig; alleen youtube/vimeo-embeds gaten. */}\n <ConsentEmbed gated={block.consentGated && !useFacade && block.provider !== \"file\"} preview={mode === \"preview\"} label=\"Video\">{media}</ConsentEmbed>\n {block.caption && <figcaption data-xicms-edit=\"caption\">{block.caption}</figcaption>}\n </figure>\n );\n}\n\nexport const videoBlock = defineBlock({\n type: \"video\",\n Render: VideoRender,\n label: \"Video\",\n icon: \"🎬\",\n defaults: () => ({ provider: \"youtube\", src: \"\", consentGated: true }),\n});\n"],"mappings":";AA0CM,cAGA,YAHA;AAxCN,SAAS,gBAAgB;AAGzB,SAAS,mBAA0C;AACnD,SAAS,oBAAoB;AAKtB,SAAS,SAAS,UAA6B,KAAa,WAAW,OAAe;AAC3F,MAAI,aAAa,WAAW;AAC1B,UAAM,KAAK,IAAI,MAAM,uCAAuC,IAAI,CAAC,KAAK;AACtE,WAAO,0CAA0C,EAAE,GAAG,WAAW,gBAAgB,EAAE;AAAA,EACrF;AACA,MAAI,aAAa,SAAS;AACxB,UAAM,KAAK,IAAI,MAAM,2BAA2B,IAAI,CAAC,KAAK;AAC1D,WAAO,kCAAkC,EAAE,GAAG,WAAW,gBAAgB,EAAE;AAAA,EAC7E;AACA,SAAO;AACT;AAGO,SAAS,YAAY,OAAqE;AAC/F,MAAI,MAAM,UAAW,QAAO,MAAM;AAClC,MAAI,MAAM,aAAa,WAAW;AAChC,UAAM,KAAK,MAAM,IAAI,MAAM,uCAAuC,IAAI,CAAC,KAAK,MAAM;AAClF,WAAO,KAAK,0BAA0B,EAAE,mBAAmB;AAAA,EAC7D;AACA,SAAO;AACT;AAEA,SAAS,YAAY,EAAE,OAAO,KAAK,GAA4B;AAC7D,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,MAAI,CAAC,MAAM,IAAK,QAAO;AAEvB,QAAM,YAAY,MAAM,UAAU,MAAM,aAAa,UAAU,CAAC;AAChE,QAAM,QAAQ,YAAY,YAAY,KAAK,IAAI;AAE/C,QAAM,QACJ,MAAM,aAAa,SACjB,oBAAC,WAAM,WAAU,uBAAsB,KAAK,MAAM,KAAK,UAAQ,MAAC,QAAQ,MAAM,aAAa,QAAW,IACpG;AAAA;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,SAAS,MAAM,SAAS,aAAa,aAAa,IAAI;AAAA,QACtD,cAAY,mBAAmB,MAAM,SAAS,OAAO;AAAA,QAEpD;AAAA;AAAA;AAAA,YAEC,oBAAC,SAAI,KAAK,OAAO,KAAI,IAAG,SAAQ,QAAO;AAAA,cAEvC,oBAAC,UAAK,WAAU,0BAAyB,eAAW,MAAC;AAAA,UAEvD,oBAAC,UAAK,WAAU,qBAAoB,eAAW,MAAC,oBAAC;AAAA;AAAA;AAAA,IACnD;AAAA,MAEA,oBAAC,SAAI,WAAU,sBACb;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,SAAS,MAAM,UAAU,MAAM,KAAK,SAAS;AAAA,MAClD,OAAO,MAAM,SAAS;AAAA,MACtB,SAAQ;AAAA,MACR,OAAM;AAAA,MACN,iBAAe;AAAA;AAAA,EACjB,GACF;AAEJ,SACE,qBAAC,YAAO,WAAU,eAGhB;AAAA,wBAAC,gBAAa,OAAO,MAAM,gBAAgB,CAAC,aAAa,MAAM,aAAa,QAAQ,SAAS,SAAS,WAAW,OAAM,SAAS,iBAAM;AAAA,IACrI,MAAM,WAAW,oBAAC,gBAAW,mBAAgB,WAAW,gBAAM,SAAQ;AAAA,KACzE;AAEJ;AAEO,MAAM,aAAa,YAAY;AAAA,EACpC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,UAAU,WAAW,KAAK,IAAI,cAAc,KAAK;AACtE,CAAC;","names":[]}
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { WhatsappChatBlockSchema } from "../../schema/blocks.js";
4
3
  import { defineBlock } from "../registry.js";
5
4
  function waLink(phone, text) {
6
5
  const trimmed = phone.trim().replace(/\(0\)/g, "");
@@ -37,7 +36,6 @@ function WhatsappChatRender({ block, mode }) {
37
36
  }
38
37
  const whatsappChatBlock = defineBlock({
39
38
  type: "whatsappChat",
40
- schema: WhatsappChatBlockSchema,
41
39
  Render: WhatsappChatRender,
42
40
  label: "WhatsApp-knop",
43
41
  icon: "\u{1F4AC}",
@@ -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 // \"(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":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/WhatsappChat.tsx"],"sourcesContent":["\"use client\";\n\nimport type { z } from \"zod\";\nimport type { 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 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;AAjChC,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,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU,OAAO,EAAE,OAAO,IAAI,OAAO,WAAW,UAAU,KAAK;AACjE,CAAC;","names":[]}