@xiwebdesign/xicms-core 7.30.0 → 8.15.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 (463) hide show
  1. package/dist/XiCMSProvider.js +22 -7
  2. package/dist/XiCMSProvider.js.map +1 -1
  3. package/dist/admin/AdminApp.js +175 -93
  4. package/dist/admin/AdminApp.js.map +1 -1
  5. package/dist/admin/AdminHelp.d.ts +1 -1
  6. package/dist/admin/AdminHelp.js +44 -36
  7. package/dist/admin/AdminHelp.js.map +1 -1
  8. package/dist/admin/CoreVersion.d.ts +6 -3
  9. package/dist/admin/CoreVersion.js +60 -24
  10. package/dist/admin/CoreVersion.js.map +1 -1
  11. package/dist/admin/NewsEditor.d.ts +7 -0
  12. package/dist/admin/NewsEditor.js +117 -75
  13. package/dist/admin/NewsEditor.js.map +1 -1
  14. package/dist/admin/SuperadminPanel.js +1 -1
  15. package/dist/admin/SuperadminPanel.js.map +1 -1
  16. package/dist/admin/ZoekPalet.d.ts +15 -0
  17. package/dist/admin/ZoekPalet.js +123 -0
  18. package/dist/admin/ZoekPalet.js.map +1 -0
  19. package/dist/admin/blockEditors/GenericEditor.js +36 -18
  20. package/dist/admin/blockEditors/GenericEditor.js.map +1 -1
  21. package/dist/admin/blockEditors/StyleEditor.js +37 -3
  22. package/dist/admin/blockEditors/StyleEditor.js.map +1 -1
  23. package/dist/admin/blockEditors/SymbolRefEditor.js +44 -2
  24. package/dist/admin/blockEditors/SymbolRefEditor.js.map +1 -1
  25. package/dist/admin/blockEditors/index.js +154 -73
  26. package/dist/admin/blockEditors/index.js.map +1 -1
  27. package/dist/admin/customBlocks/CustomBlocksPanel.js +18 -7
  28. package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
  29. package/dist/admin/dashboard/DashboardPanel.js +26 -16
  30. package/dist/admin/dashboard/DashboardPanel.js.map +1 -1
  31. package/dist/admin/editors/RichTextEditor.d.ts +4 -3
  32. package/dist/admin/editors/RichTextEditor.js +19 -56
  33. package/dist/admin/editors/RichTextEditor.js.map +1 -1
  34. package/dist/admin/editors/fontSizeExtension.js +7 -1
  35. package/dist/admin/editors/fontSizeExtension.js.map +1 -1
  36. package/dist/admin/editors/slashMenu.d.ts +18 -0
  37. package/dist/admin/editors/slashMenu.js +34 -0
  38. package/dist/admin/editors/slashMenu.js.map +1 -0
  39. package/dist/admin/editors/useRijkeTekst.d.ts +48 -0
  40. package/dist/admin/editors/useRijkeTekst.js +310 -0
  41. package/dist/admin/editors/useRijkeTekst.js.map +1 -0
  42. package/dist/admin/forms/FormsPanel.d.ts +11 -2
  43. package/dist/admin/forms/FormsPanel.js +218 -65
  44. package/dist/admin/forms/FormsPanel.js.map +1 -1
  45. package/dist/admin/lib/blokInfo.d.ts +17 -0
  46. package/dist/admin/lib/blokInfo.js +80 -0
  47. package/dist/admin/lib/blokInfo.js.map +1 -0
  48. package/dist/admin/lib/foutNaarMens.d.ts +1 -0
  49. package/dist/admin/lib/foutNaarMens.js +22 -0
  50. package/dist/admin/lib/foutNaarMens.js.map +1 -0
  51. package/dist/admin/lib/gewisteVelden.d.ts +13 -0
  52. package/dist/admin/lib/gewisteVelden.js +31 -0
  53. package/dist/admin/lib/gewisteVelden.js.map +1 -0
  54. package/dist/admin/lib/heroHulp.d.ts +11 -0
  55. package/dist/admin/lib/heroHulp.js +18 -0
  56. package/dist/admin/lib/heroHulp.js.map +1 -0
  57. package/dist/admin/lib/kleur.js +2 -3
  58. package/dist/admin/lib/kleur.js.map +1 -1
  59. package/dist/admin/lib/leesbaarheid.d.ts +34 -0
  60. package/dist/admin/lib/leesbaarheid.js +82 -0
  61. package/dist/admin/lib/leesbaarheid.js.map +1 -0
  62. package/dist/admin/lib/lengteAdvies.d.ts +1 -0
  63. package/dist/admin/lib/lengteAdvies.js +16 -0
  64. package/dist/admin/lib/lengteAdvies.js.map +1 -0
  65. package/dist/admin/lib/mediaLijst.d.ts +26 -0
  66. package/dist/admin/lib/mediaLijst.js +42 -0
  67. package/dist/admin/lib/mediaLijst.js.map +1 -0
  68. package/dist/admin/lib/mediaUsage.d.ts +12 -1
  69. package/dist/admin/lib/mediaUsage.js +43 -33
  70. package/dist/admin/lib/mediaUsage.js.map +1 -1
  71. package/dist/admin/lib/menuHulp.d.ts +16 -0
  72. package/dist/admin/lib/menuHulp.js +41 -0
  73. package/dist/admin/lib/menuHulp.js.map +1 -0
  74. package/dist/admin/lib/onopgeslagen.d.ts +10 -0
  75. package/dist/admin/lib/onopgeslagen.js +35 -0
  76. package/dist/admin/lib/onopgeslagen.js.map +1 -0
  77. package/dist/admin/lib/pagesClient.d.ts +55 -2
  78. package/dist/admin/lib/pagesClient.js +126 -28
  79. package/dist/admin/lib/pagesClient.js.map +1 -1
  80. package/dist/admin/lib/paginaGebruik.d.ts +56 -0
  81. package/dist/admin/lib/paginaGebruik.js +109 -0
  82. package/dist/admin/lib/paginaGebruik.js.map +1 -0
  83. package/dist/admin/lib/publicatieCheck.d.ts +25 -0
  84. package/dist/admin/lib/publicatieCheck.js +114 -0
  85. package/dist/admin/lib/publicatieCheck.js.map +1 -0
  86. package/dist/admin/lib/scrollNaarDetail.d.ts +6 -0
  87. package/dist/admin/lib/scrollNaarDetail.js +20 -0
  88. package/dist/admin/lib/scrollNaarDetail.js.map +1 -0
  89. package/dist/admin/lib/symbolsClient.d.ts +10 -0
  90. package/dist/admin/lib/symbolsClient.js +14 -1
  91. package/dist/admin/lib/symbolsClient.js.map +1 -1
  92. package/dist/admin/lib/tekstVeld.d.ts +8 -0
  93. package/dist/admin/lib/tekstVeld.js +10 -0
  94. package/dist/admin/lib/tekstVeld.js.map +1 -0
  95. package/dist/admin/lib/useIsSuperadmin.d.ts +2 -0
  96. package/dist/admin/lib/useIsSuperadmin.js +13 -0
  97. package/dist/admin/lib/useIsSuperadmin.js.map +1 -0
  98. package/dist/admin/lib/veldTaal.d.ts +14 -0
  99. package/dist/admin/lib/veldTaal.js +247 -0
  100. package/dist/admin/lib/veldTaal.js.map +1 -0
  101. package/dist/admin/lib/versieVergelijk.d.ts +17 -0
  102. package/dist/admin/lib/versieVergelijk.js +24 -0
  103. package/dist/admin/lib/versieVergelijk.js.map +1 -0
  104. package/dist/admin/lib/zodFields.js +22 -4
  105. package/dist/admin/lib/zodFields.js.map +1 -1
  106. package/dist/admin/lib/zodFout.d.ts +9 -0
  107. package/dist/admin/lib/zodFout.js +63 -0
  108. package/dist/admin/lib/zodFout.js.map +1 -0
  109. package/dist/admin/media/MediaManager.js +162 -32
  110. package/dist/admin/media/MediaManager.js.map +1 -1
  111. package/dist/admin/media/MediaPicker.d.ts +3 -0
  112. package/dist/admin/media/MediaPicker.js +84 -31
  113. package/dist/admin/media/MediaPicker.js.map +1 -1
  114. package/dist/admin/menu/MenuPanel.js +144 -83
  115. package/dist/admin/menu/MenuPanel.js.map +1 -1
  116. package/dist/admin/navContext.d.ts +3 -0
  117. package/dist/admin/navContext.js +13 -1
  118. package/dist/admin/navContext.js.map +1 -1
  119. package/dist/admin/pages/PageEditor.js +146 -30
  120. package/dist/admin/pages/PageEditor.js.map +1 -1
  121. package/dist/admin/pages/PagesPanel.js +215 -61
  122. package/dist/admin/pages/PagesPanel.js.map +1 -1
  123. package/dist/admin/pages/StarterKiezer.d.ts +12 -0
  124. package/dist/admin/pages/StarterKiezer.js +113 -0
  125. package/dist/admin/pages/StarterKiezer.js.map +1 -0
  126. package/dist/admin/panelRegistry.d.ts +1 -1
  127. package/dist/admin/panelRegistry.js +2 -1
  128. package/dist/admin/panelRegistry.js.map +1 -1
  129. package/dist/admin/patterns/PatternInserter.js +1 -1
  130. package/dist/admin/patterns/PatternInserter.js.map +1 -1
  131. package/dist/admin/settings/BedrijfInstellingen.d.ts +20 -0
  132. package/dist/admin/settings/BedrijfInstellingen.js +144 -0
  133. package/dist/admin/settings/BedrijfInstellingen.js.map +1 -0
  134. package/dist/admin/settings/MailSettings.js +2 -2
  135. package/dist/admin/settings/MailSettings.js.map +1 -1
  136. package/dist/admin/settings/MailTemplates.js +5 -4
  137. package/dist/admin/settings/MailTemplates.js.map +1 -1
  138. package/dist/admin/settings/SettingsPanel.js +120 -78
  139. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  140. package/dist/admin/settings/SnapshotsCard.js +4 -4
  141. package/dist/admin/settings/SnapshotsCard.js.map +1 -1
  142. package/dist/admin/settings/VorigeTerugzetten.d.ts +15 -0
  143. package/dist/admin/settings/VorigeTerugzetten.js +89 -0
  144. package/dist/admin/settings/VorigeTerugzetten.js.map +1 -0
  145. package/dist/admin/shop/CategoriesTab.js +6 -4
  146. package/dist/admin/shop/CategoriesTab.js.map +1 -1
  147. package/dist/admin/shop/DiscountsTab.js +4 -2
  148. package/dist/admin/shop/DiscountsTab.js.map +1 -1
  149. package/dist/admin/shop/OrdersTab.js +57 -19
  150. package/dist/admin/shop/OrdersTab.js.map +1 -1
  151. package/dist/admin/shop/ProductPaneel.js +2 -1
  152. package/dist/admin/shop/ProductPaneel.js.map +1 -1
  153. package/dist/admin/shop/ProductsTab.js +40 -19
  154. package/dist/admin/shop/ProductsTab.js.map +1 -1
  155. package/dist/admin/shop/ShopPanel.js +13 -1
  156. package/dist/admin/shop/ShopPanel.js.map +1 -1
  157. package/dist/admin/shop/ShopSettingsTab.js +27 -1
  158. package/dist/admin/shop/ShopSettingsTab.js.map +1 -1
  159. package/dist/admin/shop/productClient.js +7 -3
  160. package/dist/admin/shop/productClient.js.map +1 -1
  161. package/dist/admin/stats/NietGevonden.d.ts +10 -0
  162. package/dist/admin/stats/NietGevonden.js +78 -0
  163. package/dist/admin/stats/NietGevonden.js.map +1 -0
  164. package/dist/admin/stats/StatsPanel.js +6 -1
  165. package/dist/admin/stats/StatsPanel.js.map +1 -1
  166. package/dist/admin/stats/lib.d.ts +2 -0
  167. package/dist/admin/stats/lib.js.map +1 -1
  168. package/dist/admin/ui/ConfirmDialog.js +10 -1
  169. package/dist/admin/ui/ConfirmDialog.js.map +1 -1
  170. package/dist/admin/ui/Icon.d.ts +34 -0
  171. package/dist/admin/ui/Icon.js +44 -0
  172. package/dist/admin/ui/Icon.js.map +1 -0
  173. package/dist/admin/ui/ImageField.d.ts +15 -0
  174. package/dist/admin/ui/ImageField.js +67 -0
  175. package/dist/admin/ui/ImageField.js.map +1 -0
  176. package/dist/admin/ui/KleurVeld.d.ts +25 -0
  177. package/dist/admin/ui/KleurVeld.js +76 -0
  178. package/dist/admin/ui/KleurVeld.js.map +1 -0
  179. package/dist/admin/ui/LaadStaat.d.ts +21 -0
  180. package/dist/admin/ui/LaadStaat.js +43 -0
  181. package/dist/admin/ui/LaadStaat.js.map +1 -0
  182. package/dist/admin/ui/LeesbaarheidPaneel.d.ts +10 -0
  183. package/dist/admin/ui/LeesbaarheidPaneel.js +68 -0
  184. package/dist/admin/ui/LeesbaarheidPaneel.js.map +1 -0
  185. package/dist/admin/ui/LinkDialog.d.ts +22 -0
  186. package/dist/admin/ui/LinkDialog.js +131 -0
  187. package/dist/admin/ui/LinkDialog.js.map +1 -0
  188. package/dist/admin/ui/LinkVeld.d.ts +14 -0
  189. package/dist/admin/ui/LinkVeld.js +49 -0
  190. package/dist/admin/ui/LinkVeld.js.map +1 -0
  191. package/dist/admin/ui/PreviewLinkResultaat.d.ts +14 -0
  192. package/dist/admin/ui/PreviewLinkResultaat.js +52 -0
  193. package/dist/admin/ui/PreviewLinkResultaat.js.map +1 -0
  194. package/dist/admin/ui/charts.js +5 -5
  195. package/dist/admin/ui/charts.js.map +1 -1
  196. package/dist/admin/ui/primitives.d.ts +16 -6
  197. package/dist/admin/ui/primitives.js +98 -26
  198. package/dist/admin/ui/primitives.js.map +1 -1
  199. package/dist/admin/ui/select.js +5 -2
  200. package/dist/admin/ui/select.js.map +1 -1
  201. package/dist/admin/ui/switch.d.ts +3 -1
  202. package/dist/admin/ui/switch.js +6 -3
  203. package/dist/admin/ui/switch.js.map +1 -1
  204. package/dist/admin/ui/toast.d.ts +2 -1
  205. package/dist/admin/ui/toast.js +17 -11
  206. package/dist/admin/ui/toast.js.map +1 -1
  207. package/dist/admin/visual/BlokKiezer.d.ts +34 -0
  208. package/dist/admin/visual/BlokKiezer.js +225 -0
  209. package/dist/admin/visual/BlokKiezer.js.map +1 -0
  210. package/dist/admin/visual/InlineGereedschap.d.ts +52 -0
  211. package/dist/admin/visual/InlineGereedschap.js +356 -0
  212. package/dist/admin/visual/InlineGereedschap.js.map +1 -0
  213. package/dist/admin/visual/InlineRijkeTekst.d.ts +23 -0
  214. package/dist/admin/visual/InlineRijkeTekst.js +152 -0
  215. package/dist/admin/visual/InlineRijkeTekst.js.map +1 -0
  216. package/dist/admin/visual/VisualEditor.js +273 -125
  217. package/dist/admin/visual/VisualEditor.js.map +1 -1
  218. package/dist/admin.css +1 -1
  219. package/dist/bedrijf/bedrijf.d.ts +36 -0
  220. package/dist/bedrijf/bedrijf.js +110 -0
  221. package/dist/bedrijf/bedrijf.js.map +1 -0
  222. package/dist/blocks/BlockRenderer.js +11 -1
  223. package/dist/blocks/BlockRenderer.js.map +1 -1
  224. package/dist/blocks/blockStyle.d.ts +7 -0
  225. package/dist/blocks/blockStyle.js +16 -3
  226. package/dist/blocks/blockStyle.js.map +1 -1
  227. package/dist/blocks/builtinSchemas.js +6 -0
  228. package/dist/blocks/builtinSchemas.js.map +1 -1
  229. package/dist/blocks/builtins/AgendaRender.d.ts +22 -0
  230. package/dist/blocks/builtins/AgendaRender.js +69 -9
  231. package/dist/blocks/builtins/AgendaRender.js.map +1 -1
  232. package/dist/blocks/builtins/Availability.d.ts +1 -0
  233. package/dist/blocks/builtins/Availability.js +23 -0
  234. package/dist/blocks/builtins/Availability.js.map +1 -0
  235. package/dist/blocks/builtins/AvailabilityRender.d.ts +12 -0
  236. package/dist/blocks/builtins/AvailabilityRender.js +105 -0
  237. package/dist/blocks/builtins/AvailabilityRender.js.map +1 -0
  238. package/dist/blocks/builtins/BeforeAfterRender.js +4 -3
  239. package/dist/blocks/builtins/BeforeAfterRender.js.map +1 -1
  240. package/dist/blocks/builtins/Booking.js +1 -1
  241. package/dist/blocks/builtins/Booking.js.map +1 -1
  242. package/dist/blocks/builtins/BreadcrumbsRender.d.ts +4 -3
  243. package/dist/blocks/builtins/BreadcrumbsRender.js +14 -8
  244. package/dist/blocks/builtins/BreadcrumbsRender.js.map +1 -1
  245. package/dist/blocks/builtins/ColumnsRender.d.ts +1 -1
  246. package/dist/blocks/builtins/ColumnsRender.js +3 -1
  247. package/dist/blocks/builtins/ColumnsRender.js.map +1 -1
  248. package/dist/blocks/builtins/Contact.js +1 -1
  249. package/dist/blocks/builtins/Contact.js.map +1 -1
  250. package/dist/blocks/builtins/ContactRender.d.ts +1 -1
  251. package/dist/blocks/builtins/ContactRender.js +29 -8
  252. package/dist/blocks/builtins/ContactRender.js.map +1 -1
  253. package/dist/blocks/builtins/Cta.js +1 -1
  254. package/dist/blocks/builtins/Cta.js.map +1 -1
  255. package/dist/blocks/builtins/CtaRender.d.ts +1 -1
  256. package/dist/blocks/builtins/CtaRender.js +5 -4
  257. package/dist/blocks/builtins/CtaRender.js.map +1 -1
  258. package/dist/blocks/builtins/Embed.js +1 -1
  259. package/dist/blocks/builtins/Embed.js.map +1 -1
  260. package/dist/blocks/builtins/FormBlock.js +1 -0
  261. package/dist/blocks/builtins/FormBlock.js.map +1 -1
  262. package/dist/blocks/builtins/FormBlockRender.d.ts +6 -0
  263. package/dist/blocks/builtins/FormBlockRender.js +88 -16
  264. package/dist/blocks/builtins/FormBlockRender.js.map +1 -1
  265. package/dist/blocks/builtins/GalleryRender.js +3 -2
  266. package/dist/blocks/builtins/GalleryRender.js.map +1 -1
  267. package/dist/blocks/builtins/Hero.js +1 -1
  268. package/dist/blocks/builtins/Hero.js.map +1 -1
  269. package/dist/blocks/builtins/HeroRender.js +7 -3
  270. package/dist/blocks/builtins/HeroRender.js.map +1 -1
  271. package/dist/blocks/builtins/OpenNowBadge.d.ts +4 -2
  272. package/dist/blocks/builtins/OpenNowBadge.js +6 -6
  273. package/dist/blocks/builtins/OpenNowBadge.js.map +1 -1
  274. package/dist/blocks/builtins/OpeningHoursRender.d.ts +8 -2
  275. package/dist/blocks/builtins/OpeningHoursRender.js +27 -6
  276. package/dist/blocks/builtins/OpeningHoursRender.js.map +1 -1
  277. package/dist/blocks/builtins/PriceList.d.ts +1 -0
  278. package/dist/blocks/builtins/PriceList.js +25 -0
  279. package/dist/blocks/builtins/PriceList.js.map +1 -0
  280. package/dist/blocks/builtins/PriceListRender.d.ts +17 -0
  281. package/dist/blocks/builtins/PriceListRender.js +87 -0
  282. package/dist/blocks/builtins/PriceListRender.js.map +1 -0
  283. package/dist/blocks/builtins/RichTextBlockRender.js +6 -1
  284. package/dist/blocks/builtins/RichTextBlockRender.js.map +1 -1
  285. package/dist/blocks/builtins/SectionHeading.d.ts +1 -0
  286. package/dist/blocks/builtins/SectionHeading.js +14 -0
  287. package/dist/blocks/builtins/SectionHeading.js.map +1 -0
  288. package/dist/blocks/builtins/SectionHeadingRender.d.ts +7 -0
  289. package/dist/blocks/builtins/SectionHeadingRender.js +14 -0
  290. package/dist/blocks/builtins/SectionHeadingRender.js.map +1 -0
  291. package/dist/blocks/builtins/SliderRender.js +3 -2
  292. package/dist/blocks/builtins/SliderRender.js.map +1 -1
  293. package/dist/blocks/builtins/SymbolRef.js +1 -1
  294. package/dist/blocks/builtins/SymbolRef.js.map +1 -1
  295. package/dist/blocks/builtins/TeamRender.js +6 -1
  296. package/dist/blocks/builtins/TeamRender.js.map +1 -1
  297. package/dist/blocks/builtins/Testimonials.js +3 -1
  298. package/dist/blocks/builtins/Testimonials.js.map +1 -1
  299. package/dist/blocks/builtins/TestimonialsRender.js +4 -0
  300. package/dist/blocks/builtins/TestimonialsRender.js.map +1 -1
  301. package/dist/blocks/builtins/WhatsappChatRender.d.ts +1 -1
  302. package/dist/blocks/builtins/WhatsappChatRender.js +1 -1
  303. package/dist/blocks/builtins/WhatsappChatRender.js.map +1 -1
  304. package/dist/blocks/builtins/Widgets.js +2 -2
  305. package/dist/blocks/builtins/Widgets.js.map +1 -1
  306. package/dist/blocks/builtins/WidgetsRender.js +3 -1
  307. package/dist/blocks/builtins/WidgetsRender.js.map +1 -1
  308. package/dist/blocks/builtins/index.d.ts +3 -0
  309. package/dist/blocks/builtins/index.js +6 -0
  310. package/dist/blocks/builtins/index.js.map +1 -1
  311. package/dist/blocks/builtins/lib/autoComplete.d.ts +7 -0
  312. package/dist/blocks/builtins/lib/autoComplete.js +20 -0
  313. package/dist/blocks/builtins/lib/autoComplete.js.map +1 -0
  314. package/dist/blocks/builtins/lib/bookingUrl.d.ts +2 -2
  315. package/dist/blocks/builtins/lib/bookingUrl.js +1 -1
  316. package/dist/blocks/builtins/lib/bookingUrl.js.map +1 -1
  317. package/dist/blocks/builtins/lib/ical.d.ts +24 -0
  318. package/dist/blocks/builtins/lib/ical.js +82 -0
  319. package/dist/blocks/builtins/lib/ical.js.map +1 -0
  320. package/dist/blocks/builtins/lib/vandaag.d.ts +8 -0
  321. package/dist/blocks/builtins/lib/vandaag.js +22 -0
  322. package/dist/blocks/builtins/lib/vandaag.js.map +1 -0
  323. package/dist/blocks/registry.d.ts +6 -0
  324. package/dist/blocks/registry.js.map +1 -1
  325. package/dist/blocks/ui/index.js +1 -0
  326. package/dist/blocks/ui/index.js.map +1 -1
  327. package/dist/components/EditBar.js +2 -4
  328. package/dist/components/EditBar.js.map +1 -1
  329. package/dist/components/News.js +26 -18
  330. package/dist/components/News.js.map +1 -1
  331. package/dist/components/PageView.d.ts +7 -1
  332. package/dist/components/PageView.js +17 -0
  333. package/dist/components/PageView.js.map +1 -1
  334. package/dist/components/ResponsiveImage.js +18 -20
  335. package/dist/components/ResponsiveImage.js.map +1 -1
  336. package/dist/components/site/SiteFooter.d.ts +6 -1
  337. package/dist/components/site/SiteFooter.js +61 -9
  338. package/dist/components/site/SiteFooter.js.map +1 -1
  339. package/dist/components/site/SiteHeader.d.ts +15 -0
  340. package/dist/components/site/SiteHeader.js +70 -0
  341. package/dist/components/site/SiteHeader.js.map +1 -0
  342. package/dist/components/site/SiteNav.d.ts +2 -0
  343. package/dist/components/site/SiteNav.js +29 -5
  344. package/dist/components/site/SiteNav.js.map +1 -1
  345. package/dist/demo/DemoStudio.js +2 -2
  346. package/dist/demo/DemoStudio.js.map +1 -1
  347. package/dist/demo/demoStore.d.ts +13 -0
  348. package/dist/demo/demoStore.js +36 -0
  349. package/dist/demo/demoStore.js.map +1 -1
  350. package/dist/editor/EditorProvider.js +15 -4
  351. package/dist/editor/EditorProvider.js.map +1 -1
  352. package/dist/index.d.ts +4 -1
  353. package/dist/index.js +16 -1
  354. package/dist/index.js.map +1 -1
  355. package/dist/media/ImageContext.d.ts +17 -0
  356. package/dist/media/ImageContext.js +65 -20
  357. package/dist/media/ImageContext.js.map +1 -1
  358. package/dist/media/altInHtml.d.ts +9 -0
  359. package/dist/media/altInHtml.js +17 -0
  360. package/dist/media/altInHtml.js.map +1 -0
  361. package/dist/media/srcset.d.ts +12 -0
  362. package/dist/media/srcset.js +13 -0
  363. package/dist/media/srcset.js.map +1 -0
  364. package/dist/modules/builtins.d.ts +1 -0
  365. package/dist/modules/builtins.js +16 -3
  366. package/dist/modules/builtins.js.map +1 -1
  367. package/dist/modules/shop/blocks/Cart.js +5 -3
  368. package/dist/modules/shop/blocks/Cart.js.map +1 -1
  369. package/dist/modules/shop/blocks/Checkout.js +56 -35
  370. package/dist/modules/shop/blocks/Checkout.js.map +1 -1
  371. package/dist/modules/shop/blocks/Donation.js +4 -4
  372. package/dist/modules/shop/blocks/Donation.js.map +1 -1
  373. package/dist/modules/shop/blocks/ProductDetail.js +19 -3
  374. package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
  375. package/dist/modules/shop/blocks/ProductGrid.d.ts +3 -1
  376. package/dist/modules/shop/blocks/ProductGrid.js +8 -4
  377. package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
  378. package/dist/modules/shop/blocks/ShopCategories.js +4 -3
  379. package/dist/modules/shop/blocks/ShopCategories.js.map +1 -1
  380. package/dist/modules/shop/lib.d.ts +4 -0
  381. package/dist/modules/shop/lib.js +14 -0
  382. package/dist/modules/shop/lib.js.map +1 -1
  383. package/dist/modules/shop/settings.d.ts +3 -0
  384. package/dist/modules/shop/settings.js +14 -0
  385. package/dist/modules/shop/settings.js.map +1 -1
  386. package/dist/modules/shop/strings.d.ts +4 -0
  387. package/dist/modules/shop/strings.js +8 -0
  388. package/dist/modules/shop/strings.js.map +1 -1
  389. package/dist/next/envCheck.d.ts +42 -0
  390. package/dist/next/envCheck.js +106 -0
  391. package/dist/next/envCheck.js.map +1 -0
  392. package/dist/next/index.cjs +185 -2
  393. package/dist/next/index.d.ts +2 -0
  394. package/dist/next/index.js +8 -1
  395. package/dist/next/index.js.map +1 -1
  396. package/dist/next/securityHeaders.d.ts +36 -0
  397. package/dist/next/securityHeaders.js +74 -0
  398. package/dist/next/securityHeaders.js.map +1 -0
  399. package/dist/patterns/builtins.d.ts +7 -0
  400. package/dist/patterns/builtins.js +138 -3
  401. package/dist/patterns/builtins.js.map +1 -1
  402. package/dist/releaseNotes.js +140 -0
  403. package/dist/releaseNotes.js.map +1 -1
  404. package/dist/render/PublishedPage.d.ts +2 -1
  405. package/dist/render/PublishedPage.js +10 -4
  406. package/dist/render/PublishedPage.js.map +1 -1
  407. package/dist/schema/blocks.d.ts +470 -0
  408. package/dist/schema/blocks.js +102 -4
  409. package/dist/schema/blocks.js.map +1 -1
  410. package/dist/schema/business.d.ts +117 -0
  411. package/dist/schema/business.js +73 -0
  412. package/dist/schema/business.js.map +1 -0
  413. package/dist/schema/fields.d.ts +2 -1
  414. package/dist/schema/fields.js.map +1 -1
  415. package/dist/schema/forms.d.ts +18 -0
  416. package/dist/schema/forms.js +18 -3
  417. package/dist/schema/forms.js.map +1 -1
  418. package/dist/schema/mail.d.ts +10 -0
  419. package/dist/schema/mail.js +7 -0
  420. package/dist/schema/mail.js.map +1 -1
  421. package/dist/schema/settings.d.ts +101 -0
  422. package/dist/schema/settings.js +24 -3
  423. package/dist/schema/settings.js.map +1 -1
  424. package/dist/schema/shop.d.ts +2 -0
  425. package/dist/schema/shop.js +4 -0
  426. package/dist/schema/shop.js.map +1 -1
  427. package/dist/schema/shopGeld.d.ts +1 -0
  428. package/dist/schema/shopGeld.js +1 -0
  429. package/dist/schema/shopGeld.js.map +1 -1
  430. package/dist/schema/siteConfig.d.ts +68 -1
  431. package/dist/schema/siteConfig.js +7 -2
  432. package/dist/schema/siteConfig.js.map +1 -1
  433. package/dist/server/blockData.d.ts +1 -1
  434. package/dist/server/blockData.js +74 -18
  435. package/dist/server/blockData.js.map +1 -1
  436. package/dist/server/cspRapport.d.ts +20 -0
  437. package/dist/server/cspRapport.js +56 -0
  438. package/dist/server/cspRapport.js.map +1 -0
  439. package/dist/server/fonts.d.ts +3 -0
  440. package/dist/server/fonts.js +39 -0
  441. package/dist/server/fonts.js.map +1 -0
  442. package/dist/server/index.d.ts +3 -0
  443. package/dist/server/index.js +8 -0
  444. package/dist/server/index.js.map +1 -1
  445. package/dist/server/sanitize.js +15 -2
  446. package/dist/server/sanitize.js.map +1 -1
  447. package/dist/settings/useSiteSettings.js +4 -1
  448. package/dist/settings/useSiteSettings.js.map +1 -1
  449. package/dist/starters/starters.d.ts +69 -0
  450. package/dist/starters/starters.js +171 -0
  451. package/dist/starters/starters.js.map +1 -0
  452. package/dist/styles.css +1149 -208
  453. package/dist/theme/color.d.ts +10 -0
  454. package/dist/theme/color.js +17 -1
  455. package/dist/theme/color.js.map +1 -1
  456. package/dist/theme/fontFallback.d.ts +23 -0
  457. package/dist/theme/fontFallback.js +58 -0
  458. package/dist/theme/fontFallback.js.map +1 -0
  459. package/dist/theme/fonts.d.ts +11 -0
  460. package/dist/theme/fonts.js +16 -0
  461. package/dist/theme/fonts.js.map +1 -0
  462. package/dist/version.js +1 -1
  463. package/package.json +6 -4
@@ -4,5 +4,5 @@
4
4
  * volledige embed-URL. Puur en getest — de provider-keuze in de editor doet hiermee écht iets
5
5
  * (voorheen was hij alleen een hint).
6
6
  */
7
- export type BookingProvider = "custom" | "calendly" | "cal" | "formitable" | "resengo" | "thefork" | "treatwell" | "salonized" | "tablebooker" | "bookingkit";
8
- export declare function buildBookingUrl(provider: BookingProvider, input: string): string;
7
+ export type BookingProvider = "custom" | "calendly" | "cal" | "formitable" | "resengo" | "thefork" | "treatwell" | "salonized" | "tablebooker" | "bookingkit" | "lodgify" | "bookingexperts" | "tommy" | "bookingmotor";
8
+ export declare function buildBookingUrl(provider: BookingProvider, input: string | undefined): string;
@@ -1,5 +1,5 @@
1
1
  function buildBookingUrl(provider, input) {
2
- const raw = input.trim();
2
+ const raw = (input ?? "").trim();
3
3
  if (!raw) return "";
4
4
  const isUrl = /^https?:\/\//i.test(raw);
5
5
  switch (provider) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/blocks/builtins/lib/bookingUrl.ts"],"sourcesContent":["/**\n * Bouwt de embed-URL voor een reserveersysteem uit wat de klant plakt: een volledige URL wordt\n * genormaliseerd naar de embed-variant van de provider; een kale slug/id wordt aangevuld tot de\n * volledige embed-URL. Puur en getest — de provider-keuze in de editor doet hiermee écht iets\n * (voorheen was hij alleen een hint).\n */\nexport type BookingProvider =\n | \"custom\"\n | \"calendly\"\n | \"cal\"\n | \"formitable\"\n | \"resengo\"\n | \"thefork\"\n | \"treatwell\"\n | \"salonized\"\n | \"tablebooker\"\n | \"bookingkit\";\n\nexport function buildBookingUrl(provider: BookingProvider, input: string): string {\n const raw = input.trim();\n if (!raw) return \"\";\n const isUrl = /^https?:\\/\\//i.test(raw);\n\n switch (provider) {\n case \"calendly\": {\n // Volledige link werkt direct als embed; een slug (\"naam/intake\") vullen we aan.\n if (isUrl) return raw;\n return `https://calendly.com/${raw.replace(/^\\/+/, \"\")}`;\n }\n case \"cal\": {\n if (isUrl) return raw;\n return `https://cal.com/${raw.replace(/^\\/+/, \"\")}`;\n }\n case \"formitable\": {\n // Embed-vorm: https://widget.formitable.com/<restaurant-id>\n if (isUrl) return raw;\n return `https://widget.formitable.com/${raw.replace(/^\\/+/, \"\")}`;\n }\n case \"resengo\": {\n if (isUrl) return raw;\n return `https://www.resengo.com/widget/${raw.replace(/^\\/+/, \"\")}`;\n }\n case \"thefork\": {\n if (isUrl) return raw;\n return `https://module.lafourchette.com/nl_NL/module/${raw.replace(/^\\/+/, \"\")}`;\n }\n case \"treatwell\": {\n if (isUrl) return raw;\n return `https://widget.treatwell.nl/salon/${raw.replace(/^\\/+/, \"\")}/`;\n }\n case \"salonized\": {\n // Salonized-bookingwidget: https://widget.salonized.com/widget_bookings/new?company=<id>\n if (isUrl) return raw;\n return `https://widget.salonized.com/widget_bookings/new?company=${encodeURIComponent(raw)}`;\n }\n case \"tablebooker\": {\n if (isUrl) return raw;\n return `https://book.tablebooker.com/widget/${raw.replace(/^\\/+/, \"\")}`;\n }\n case \"bookingkit\": {\n if (isUrl) return raw;\n return `https://widget.bookingkit.net/bkembed/?cw=${encodeURIComponent(raw)}`;\n }\n case \"custom\":\n default:\n return isUrl ? raw : \"\";\n }\n}\n"],"mappings":"AAkBO,SAAS,gBAAgB,UAA2B,OAAuB;AAChF,QAAM,MAAM,MAAM,KAAK;AACvB,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,QAAQ,gBAAgB,KAAK,GAAG;AAEtC,UAAQ,UAAU;AAAA,IAChB,KAAK,YAAY;AAEf,UAAI,MAAO,QAAO;AAClB,aAAO,wBAAwB,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IACxD;AAAA,IACA,KAAK,OAAO;AACV,UAAI,MAAO,QAAO;AAClB,aAAO,mBAAmB,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IACnD;AAAA,IACA,KAAK,cAAc;AAEjB,UAAI,MAAO,QAAO;AAClB,aAAO,iCAAiC,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IACjE;AAAA,IACA,KAAK,WAAW;AACd,UAAI,MAAO,QAAO;AAClB,aAAO,kCAAkC,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IAClE;AAAA,IACA,KAAK,WAAW;AACd,UAAI,MAAO,QAAO;AAClB,aAAO,gDAAgD,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IAChF;AAAA,IACA,KAAK,aAAa;AAChB,UAAI,MAAO,QAAO;AAClB,aAAO,qCAAqC,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IACrE;AAAA,IACA,KAAK,aAAa;AAEhB,UAAI,MAAO,QAAO;AAClB,aAAO,4DAA4D,mBAAmB,GAAG,CAAC;AAAA,IAC5F;AAAA,IACA,KAAK,eAAe;AAClB,UAAI,MAAO,QAAO;AAClB,aAAO,uCAAuC,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IACvE;AAAA,IACA,KAAK,cAAc;AACjB,UAAI,MAAO,QAAO;AAClB,aAAO,6CAA6C,mBAAmB,GAAG,CAAC;AAAA,IAC7E;AAAA,IACA,KAAK;AAAA,IACL;AACE,aAAO,QAAQ,MAAM;AAAA,EACzB;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../../src/blocks/builtins/lib/bookingUrl.ts"],"sourcesContent":["/**\n * Bouwt de embed-URL voor een reserveersysteem uit wat de klant plakt: een volledige URL wordt\n * genormaliseerd naar de embed-variant van de provider; een kale slug/id wordt aangevuld tot de\n * volledige embed-URL. Puur en getest — de provider-keuze in de editor doet hiermee écht iets\n * (voorheen was hij alleen een hint).\n */\nexport type BookingProvider =\n | \"custom\"\n | \"calendly\"\n | \"cal\"\n | \"formitable\"\n | \"resengo\"\n | \"thefork\"\n | \"treatwell\"\n | \"salonized\"\n | \"tablebooker\"\n | \"bookingkit\"\n | \"lodgify\"\n | \"bookingexperts\"\n | \"tommy\"\n | \"bookingmotor\";\n\nexport function buildBookingUrl(provider: BookingProvider, input: string | undefined): string {\n const raw = (input ?? \"\").trim();\n if (!raw) return \"\";\n const isUrl = /^https?:\\/\\//i.test(raw);\n\n switch (provider) {\n case \"calendly\": {\n // Volledige link werkt direct als embed; een slug (\"naam/intake\") vullen we aan.\n if (isUrl) return raw;\n return `https://calendly.com/${raw.replace(/^\\/+/, \"\")}`;\n }\n case \"cal\": {\n if (isUrl) return raw;\n return `https://cal.com/${raw.replace(/^\\/+/, \"\")}`;\n }\n case \"formitable\": {\n // Embed-vorm: https://widget.formitable.com/<restaurant-id>\n if (isUrl) return raw;\n return `https://widget.formitable.com/${raw.replace(/^\\/+/, \"\")}`;\n }\n case \"resengo\": {\n if (isUrl) return raw;\n return `https://www.resengo.com/widget/${raw.replace(/^\\/+/, \"\")}`;\n }\n case \"thefork\": {\n if (isUrl) return raw;\n return `https://module.lafourchette.com/nl_NL/module/${raw.replace(/^\\/+/, \"\")}`;\n }\n case \"treatwell\": {\n if (isUrl) return raw;\n return `https://widget.treatwell.nl/salon/${raw.replace(/^\\/+/, \"\")}/`;\n }\n case \"salonized\": {\n // Salonized-bookingwidget: https://widget.salonized.com/widget_bookings/new?company=<id>\n if (isUrl) return raw;\n return `https://widget.salonized.com/widget_bookings/new?company=${encodeURIComponent(raw)}`;\n }\n case \"tablebooker\": {\n if (isUrl) return raw;\n return `https://book.tablebooker.com/widget/${raw.replace(/^\\/+/, \"\")}`;\n }\n case \"bookingkit\": {\n if (isUrl) return raw;\n return `https://widget.bookingkit.net/bkembed/?cw=${encodeURIComponent(raw)}`;\n }\n case \"custom\":\n default:\n return isUrl ? raw : \"\";\n }\n}\n"],"mappings":"AAsBO,SAAS,gBAAgB,UAA2B,OAAmC;AAC5F,QAAM,OAAO,SAAS,IAAI,KAAK;AAC/B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,QAAQ,gBAAgB,KAAK,GAAG;AAEtC,UAAQ,UAAU;AAAA,IAChB,KAAK,YAAY;AAEf,UAAI,MAAO,QAAO;AAClB,aAAO,wBAAwB,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IACxD;AAAA,IACA,KAAK,OAAO;AACV,UAAI,MAAO,QAAO;AAClB,aAAO,mBAAmB,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IACnD;AAAA,IACA,KAAK,cAAc;AAEjB,UAAI,MAAO,QAAO;AAClB,aAAO,iCAAiC,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IACjE;AAAA,IACA,KAAK,WAAW;AACd,UAAI,MAAO,QAAO;AAClB,aAAO,kCAAkC,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IAClE;AAAA,IACA,KAAK,WAAW;AACd,UAAI,MAAO,QAAO;AAClB,aAAO,gDAAgD,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IAChF;AAAA,IACA,KAAK,aAAa;AAChB,UAAI,MAAO,QAAO;AAClB,aAAO,qCAAqC,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IACrE;AAAA,IACA,KAAK,aAAa;AAEhB,UAAI,MAAO,QAAO;AAClB,aAAO,4DAA4D,mBAAmB,GAAG,CAAC;AAAA,IAC5F;AAAA,IACA,KAAK,eAAe;AAClB,UAAI,MAAO,QAAO;AAClB,aAAO,uCAAuC,IAAI,QAAQ,QAAQ,EAAE,CAAC;AAAA,IACvE;AAAA,IACA,KAAK,cAAc;AACjB,UAAI,MAAO,QAAO;AAClB,aAAO,6CAA6C,mBAAmB,GAAG,CAAC;AAAA,IAC7E;AAAA,IACA,KAAK;AAAA,IACL;AACE,aAAO,QAAQ,MAAM;AAAA,EACzB;AACF;","names":[]}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Bezette dagen uit een iCal-agenda (8.15), voor de beschikbaarheidskalender.
3
+ *
4
+ * Airbnb, Booking.com, Vrbo, availcalendar en de meeste verhuursystemen exporteren hun boekingen
5
+ * als iCal-link (`.ics`). Elke VEVENT is een bezette periode: DTSTART tot (exclusief) DTEND —
6
+ * precies zoals een overnachting werkt: aankomst op de 1e, vertrek op de 8e = de nachten 1 t/m 7
7
+ * zijn bezet, en op de 8e kan een nieuwe gast aankomen. Puur en getest; de server haalt de agenda op.
8
+ */
9
+ export interface Bezet {
10
+ /** Eerste bezette dag, "2026-07-01". */
11
+ van: string;
12
+ /** Eerste VRIJE dag erna (vertrekdag), "2026-07-08". */
13
+ tot: string;
14
+ }
15
+ /** Alle bezette periodes uit de tekst van een .ics-bestand, gesorteerd en samengevoegd. */
16
+ export declare function parseIcalBezet(tekst: string): Bezet[];
17
+ /** Is deze nacht (aankomstdag) bezet? */
18
+ export declare function isBezet(dag: string, bezet: Bezet[]): boolean;
19
+ /** Zit er een bezette nacht tussen aankomst (incl.) en vertrek (excl.)? */
20
+ export declare function periodeVrij(van: string, tot: string, bezet: Bezet[]): boolean;
21
+ /** De weken van een maand, maandag eerst; `null` = dag buiten de maand. */
22
+ export declare function maandRaster(jaar: number, maand: number): (string | null)[][];
23
+ /** Mag de server deze link ophalen? Alleen https naar een gewone hostnaam (geen IP, geen intern adres). */
24
+ export declare function veiligeIcalUrl(url: string): boolean;
@@ -0,0 +1,82 @@
1
+ function datumUit(waarde) {
2
+ const m = /^(\d{4})(\d{2})(\d{2})/.exec(waarde.trim());
3
+ return m ? `${m[1]}-${m[2]}-${m[3]}` : null;
4
+ }
5
+ function volgendeDag(iso) {
6
+ const [j, m, d] = iso.split("-").map(Number);
7
+ const dt = new Date(Date.UTC(j, m - 1, d + 1));
8
+ return dt.toISOString().slice(0, 10);
9
+ }
10
+ function parseIcalBezet(tekst) {
11
+ const regels = tekst.replace(/\r?\n[ \t]/g, "").split(/\r?\n/);
12
+ const uit = [];
13
+ let inEvent = false;
14
+ let start = null;
15
+ let eind = null;
16
+ let geannuleerd = false;
17
+ for (const regel of regels) {
18
+ if (regel === "BEGIN:VEVENT") {
19
+ inEvent = true;
20
+ start = eind = null;
21
+ geannuleerd = false;
22
+ } else if (regel === "END:VEVENT") {
23
+ if (start && !geannuleerd) {
24
+ const tot = eind && eind > start ? eind : volgendeDag(start);
25
+ uit.push({ van: start, tot });
26
+ }
27
+ inEvent = false;
28
+ } else if (inEvent) {
29
+ const [sleutel, ...rest] = regel.split(":");
30
+ const waarde = rest.join(":");
31
+ const naam = sleutel.split(";")[0].toUpperCase();
32
+ if (naam === "DTSTART") start = datumUit(waarde);
33
+ else if (naam === "DTEND") eind = datumUit(waarde);
34
+ else if (naam === "STATUS" && /CANCELLED/i.test(waarde)) geannuleerd = true;
35
+ }
36
+ }
37
+ uit.sort((a, b) => a.van.localeCompare(b.van));
38
+ const samen = [];
39
+ for (const b of uit) {
40
+ const laatste = samen[samen.length - 1];
41
+ if (laatste && b.van <= laatste.tot) laatste.tot = b.tot > laatste.tot ? b.tot : laatste.tot;
42
+ else samen.push({ ...b });
43
+ }
44
+ return samen;
45
+ }
46
+ function isBezet(dag, bezet) {
47
+ return bezet.some((b) => b.van <= dag && dag < b.tot);
48
+ }
49
+ function periodeVrij(van, tot, bezet) {
50
+ return tot > van && !bezet.some((b) => b.van < tot && van < b.tot);
51
+ }
52
+ function maandRaster(jaar, maand) {
53
+ const eerste = new Date(Date.UTC(jaar, maand, 1));
54
+ const dagen = new Date(Date.UTC(jaar, maand + 1, 0)).getUTCDate();
55
+ const offset = (eerste.getUTCDay() + 6) % 7;
56
+ const cellen = Array.from({ length: offset }, () => null);
57
+ for (let d = 1; d <= dagen; d++) cellen.push(`${jaar}-${String(maand + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`);
58
+ while (cellen.length % 7) cellen.push(null);
59
+ const weken = [];
60
+ for (let i = 0; i < cellen.length; i += 7) weken.push(cellen.slice(i, i + 7));
61
+ return weken;
62
+ }
63
+ function veiligeIcalUrl(url) {
64
+ try {
65
+ const u = new URL(url.replace(/^webcal:\/\//i, "https://"));
66
+ if (u.protocol !== "https:") return false;
67
+ const h = u.hostname.toLowerCase();
68
+ if (h === "localhost" || h.endsWith(".local") || h.endsWith(".internal")) return false;
69
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(h) || h.includes(":") || h.startsWith("[")) return false;
70
+ return h.includes(".");
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ export {
76
+ isBezet,
77
+ maandRaster,
78
+ parseIcalBezet,
79
+ periodeVrij,
80
+ veiligeIcalUrl
81
+ };
82
+ //# sourceMappingURL=ical.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/blocks/builtins/lib/ical.ts"],"sourcesContent":["/**\n * Bezette dagen uit een iCal-agenda (8.15), voor de beschikbaarheidskalender.\n *\n * Airbnb, Booking.com, Vrbo, availcalendar en de meeste verhuursystemen exporteren hun boekingen\n * als iCal-link (`.ics`). Elke VEVENT is een bezette periode: DTSTART tot (exclusief) DTEND —\n * precies zoals een overnachting werkt: aankomst op de 1e, vertrek op de 8e = de nachten 1 t/m 7\n * zijn bezet, en op de 8e kan een nieuwe gast aankomen. Puur en getest; de server haalt de agenda op.\n */\nexport interface Bezet {\n /** Eerste bezette dag, \"2026-07-01\". */\n van: string;\n /** Eerste VRIJE dag erna (vertrekdag), \"2026-07-08\". */\n tot: string;\n}\n\nfunction datumUit(waarde: string): string | null {\n // \"20260701\", \"20260701T150000Z\", \"20260701T150000\"\n const m = /^(\\d{4})(\\d{2})(\\d{2})/.exec(waarde.trim());\n return m ? `${m[1]}-${m[2]}-${m[3]}` : null;\n}\n\nfunction volgendeDag(iso: string): string {\n const [j, m, d] = iso.split(\"-\").map(Number);\n const dt = new Date(Date.UTC(j, m - 1, d + 1));\n return dt.toISOString().slice(0, 10);\n}\n\n/** Alle bezette periodes uit de tekst van een .ics-bestand, gesorteerd en samengevoegd. */\nexport function parseIcalBezet(tekst: string): Bezet[] {\n // Regels die met een spatie beginnen zijn een vervolg van de vorige (RFC 5545 \"folding\").\n const regels = tekst.replace(/\\r?\\n[ \\t]/g, \"\").split(/\\r?\\n/);\n const uit: Bezet[] = [];\n let inEvent = false;\n let start: string | null = null;\n let eind: string | null = null;\n let geannuleerd = false;\n for (const regel of regels) {\n if (regel === \"BEGIN:VEVENT\") {\n inEvent = true;\n start = eind = null;\n geannuleerd = false;\n } else if (regel === \"END:VEVENT\") {\n if (start && !geannuleerd) {\n const tot = eind && eind > start ? eind : volgendeDag(start);\n uit.push({ van: start, tot });\n }\n inEvent = false;\n } else if (inEvent) {\n const [sleutel, ...rest] = regel.split(\":\");\n const waarde = rest.join(\":\");\n const naam = sleutel.split(\";\")[0].toUpperCase();\n if (naam === \"DTSTART\") start = datumUit(waarde);\n else if (naam === \"DTEND\") eind = datumUit(waarde);\n else if (naam === \"STATUS\" && /CANCELLED/i.test(waarde)) geannuleerd = true;\n }\n }\n // Samenvoegen van aansluitende of overlappende periodes.\n uit.sort((a, b) => a.van.localeCompare(b.van));\n const samen: Bezet[] = [];\n for (const b of uit) {\n const laatste = samen[samen.length - 1];\n if (laatste && b.van <= laatste.tot) laatste.tot = b.tot > laatste.tot ? b.tot : laatste.tot;\n else samen.push({ ...b });\n }\n return samen;\n}\n\n/** Is deze nacht (aankomstdag) bezet? */\nexport function isBezet(dag: string, bezet: Bezet[]): boolean {\n return bezet.some((b) => b.van <= dag && dag < b.tot);\n}\n\n/** Zit er een bezette nacht tussen aankomst (incl.) en vertrek (excl.)? */\nexport function periodeVrij(van: string, tot: string, bezet: Bezet[]): boolean {\n return tot > van && !bezet.some((b) => b.van < tot && van < b.tot);\n}\n\n/** De weken van een maand, maandag eerst; `null` = dag buiten de maand. */\nexport function maandRaster(jaar: number, maand: number): (string | null)[][] {\n const eerste = new Date(Date.UTC(jaar, maand, 1));\n const dagen = new Date(Date.UTC(jaar, maand + 1, 0)).getUTCDate();\n const offset = (eerste.getUTCDay() + 6) % 7; // maandag = 0\n const cellen: (string | null)[] = Array.from({ length: offset }, () => null);\n for (let d = 1; d <= dagen; d++) cellen.push(`${jaar}-${String(maand + 1).padStart(2, \"0\")}-${String(d).padStart(2, \"0\")}`);\n while (cellen.length % 7) cellen.push(null);\n const weken: (string | null)[][] = [];\n for (let i = 0; i < cellen.length; i += 7) weken.push(cellen.slice(i, i + 7));\n return weken;\n}\n\n/** Mag de server deze link ophalen? Alleen https naar een gewone hostnaam (geen IP, geen intern adres). */\nexport function veiligeIcalUrl(url: string): boolean {\n try {\n const u = new URL(url.replace(/^webcal:\\/\\//i, \"https://\"));\n if (u.protocol !== \"https:\") return false;\n const h = u.hostname.toLowerCase();\n if (h === \"localhost\" || h.endsWith(\".local\") || h.endsWith(\".internal\")) return false;\n if (/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(h) || h.includes(\":\") || h.startsWith(\"[\")) return false;\n return h.includes(\".\");\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * "Vandaag" zonder hydratie-mismatch (8.1). Een datumfilter in de render (verlopen agenda-items,
3
+ * ingepland nieuws) gaf op de server een ander resultaat dan in de browser zodra de HTML uit de
4
+ * cache een dag oud was — React gooide dan de hele server-render weg en bouwde alles opnieuw.
5
+ * Tijdens server-render én hydratie is dit `null` (= niet op datum filteren, identiek aan de
6
+ * server); direct daarna de echte datum van de bezoeker.
7
+ */
8
+ export declare function useVandaag(): Date | null;
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ import { useSyncExternalStore } from "react";
3
+ const geenAbonnement = () => () => {
4
+ };
5
+ function useVandaag() {
6
+ return useSyncExternalStore(geenAbonnement, vandaagSnapshot, () => null);
7
+ }
8
+ let cache = null;
9
+ function vandaagSnapshot() {
10
+ const nu = /* @__PURE__ */ new Date();
11
+ const dag = nu.toDateString();
12
+ if (!cache || cache.dag !== dag) {
13
+ const d = new Date(nu);
14
+ d.setHours(0, 0, 0, 0);
15
+ cache = { dag, datum: d };
16
+ }
17
+ return cache.datum;
18
+ }
19
+ export {
20
+ useVandaag
21
+ };
22
+ //# sourceMappingURL=vandaag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/blocks/builtins/lib/vandaag.ts"],"sourcesContent":["\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\nconst geenAbonnement = () => () => {};\n\n/**\n * \"Vandaag\" zonder hydratie-mismatch (8.1). Een datumfilter in de render (verlopen agenda-items,\n * ingepland nieuws) gaf op de server een ander resultaat dan in de browser zodra de HTML uit de\n * cache een dag oud was — React gooide dan de hele server-render weg en bouwde alles opnieuw.\n * Tijdens server-render én hydratie is dit `null` (= niet op datum filteren, identiek aan de\n * server); direct daarna de echte datum van de bezoeker.\n */\nexport function useVandaag(): Date | null {\n return useSyncExternalStore(geenAbonnement, vandaagSnapshot, () => null);\n}\n\nlet cache: { dag: string; datum: Date } | null = null;\nfunction vandaagSnapshot(): Date {\n // Stabiele referentie per dag: useSyncExternalStore eist dat opeenvolgende snapshots gelijk zijn.\n const nu = new Date();\n const dag = nu.toDateString();\n if (!cache || cache.dag !== dag) {\n const d = new Date(nu);\n d.setHours(0, 0, 0, 0);\n cache = { dag, datum: d };\n }\n return cache.datum;\n}\n"],"mappings":";AAEA,SAAS,4BAA4B;AAErC,MAAM,iBAAiB,MAAM,MAAM;AAAC;AAS7B,SAAS,aAA0B;AACxC,SAAO,qBAAqB,gBAAgB,iBAAiB,MAAM,IAAI;AACzE;AAEA,IAAI,QAA6C;AACjD,SAAS,kBAAwB;AAE/B,QAAM,KAAK,oBAAI,KAAK;AACpB,QAAM,MAAM,GAAG,aAAa;AAC5B,MAAI,CAAC,SAAS,MAAM,QAAQ,KAAK;AAC/B,UAAM,IAAI,IAAI,KAAK,EAAE;AACrB,MAAE,SAAS,GAAG,GAAG,GAAG,CAAC;AACrB,YAAQ,EAAE,KAAK,OAAO,EAAE;AAAA,EAC1B;AACA,SAAO,MAAM;AACf;","names":[]}
@@ -57,6 +57,12 @@ export interface BlockDefinition<S extends $ZodType = $ZodType> {
57
57
  * Zie `introspectToFields` (admin/lib/zodFields.ts) en `schema/fields.ts`.
58
58
  */
59
59
  fields?: FieldHints;
60
+ /**
61
+ * 8.10: een blok waar de werking van de site op leunt (winkelwagen, afrekenen, formulier).
62
+ * Verwijderen in de editor vraagt dan eerst om bevestiging met deze uitleg; gewone blokken gaan
63
+ * met één klik weg (er is ongedaan maken). De tekst zegt wat er stuk gaat, in klanttaal.
64
+ */
65
+ essentieel?: string;
60
66
  }
61
67
  /**
62
68
  * Registreert een block-type.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/blocks/registry.ts"],"sourcesContent":["/**\n * Block-registry: koppelt een `_type` aan zijn Zod-schema, een (server-safe) Render-component\n * en een optionele (client/admin) Editor. Klant-blokken worden hier ingebracht via `defineBlock`\n * — zo blijft de core klant-onafhankelijk: de klant levert de render-component, core de structuur.\n *\n * Pure logica + types (geen \"use client\"): bruikbaar op server en client.\n */\nimport type { ComponentType } from \"react\";\nimport type { $ZodType } from \"zod/v4/core\";\nimport type { BlockBase } from \"../schema/blocks\";\nimport type { FieldHints } from \"../schema/fields\";\n\n/**\n * Het uitvoertype van een blok-schema, onafhankelijk van de Zod-façade.\n *\n * Core draait op `zod/mini` (zie schema/) omdat de classic-façade niet tree-shaket en ~300 KB aan\n * de publieke bundel toevoegde. De grens hieronder is daarom `$ZodType` uit `zod/v4/core` — de\n * gedeelde bovengrens van BEIDE smaken. Gevolg: een klantrepo mag zijn eigen blok-schema's zowel\n * met `zod` als met `zod/mini` schrijven; `defineBlock` accepteert allebei.\n */\ntype InferBlock<S extends $ZodType> = S[\"_zod\"][\"output\"];\n\nexport interface BlockRenderProps<T = unknown> {\n block: T;\n siteId: string;\n /** \"view\" = publiek; \"preview\" = draft-weergave in de editor. */\n mode?: \"view\" | \"preview\";\n}\n\nexport interface BlockEditorProps<T = unknown> {\n block: T;\n siteId: string;\n onChange: (next: T) => void;\n}\n\n/**\n * Indeling van een block in het toevoeg-menu:\n * - `content` (default): een gewoon blok dat je op de pagina plaatst.\n * - `widget`: een compacte variant die je naast een blok (in een zijbalk/kolom) zet — haalt z'n\n * gegevens uit dezelfde module als de volwaardige versie (bv. \"laatste nieuws\", \"komende agenda\").\n * - `layout`: een indelingsblok dat andere blokken bevat (kolommen, zijbalk, tabs).\n */\nexport type BlockCategory = \"content\" | \"widget\" | \"layout\";\n\nexport interface BlockDefinition<S extends $ZodType = $ZodType> {\n type: string;\n /**\n * Zod-schema van het blok. Voor de INGEBOUWDE blokken is dit veld leeg en wordt het schema\n * lazy geregistreerd via `blocks/builtinSchemas` (7.13): de schema's zijn editor-kennis, en\n * mét dit veld reisden álle schema's plus de zod-runtime (±73 KB gzip) mee naar elke publieke\n * bezoeker die nooit iets bewerkt. Klant-blokken (defineBlock in een klantrepo) mogen hun\n * schema gewoon hier blijven meegeven — dat is klein en houdt de API eenvoudig.\n */\n schema?: S;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- registry-grens: het blok\n // typeert zijn eigen props; hier is de data per definitie \"wat de database gaf\".\n Render: ComponentType<BlockRenderProps<any>>;\n /**\n * Admin-editor (lazy/client). Optioneel; ontbreekt = automatische schema-editor (GenericEditor):\n * die genereert een formulier uit het Zod-schema, dus een blok zonder eigen editor blijft bewerkbaar.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- zie Render.\n Editor?: ComponentType<BlockEditorProps<any>>;\n label: string;\n icon?: string;\n /** Groep in het toevoeg-menu (content/widget/layout). Standaard \"content\". */\n category?: BlockCategory;\n /** Defaultwaarden voor een nieuw block (zonder _key/_type). */\n defaults?: () => Record<string, unknown>;\n /**\n * Optionele hints voor het automatisch gegenereerde formulier (GenericEditor 2.0, 5.0):\n * per (dotted) veldsleutel een verfijning van de uit het Zod-schema afgeleide Field-IR —\n * bv. `{ \"items.date\": { type: \"date\" }, cta: { showIf: { field: \"showCta\" } } }`.\n * Zie `introspectToFields` (admin/lib/zodFields.ts) en `schema/fields.ts`.\n */\n fields?: FieldHints;\n}\n\n/**\n * Registreert een block-type.\n *\n * UPDATE-VEILIGHEID: voeg in een nieuwe versie nooit een VERPLICHT veld toe aan een bestaand\n * block-schema zonder `.default()` of `.optional()`. Anders faalt validatie op al opgeslagen\n * content en wordt die overgeslagen. Met defaults/optioneel blijft oude content gewoon werken.\n */\nexport function defineBlock<S extends $ZodType>(def: BlockDefinition<S>): BlockDefinition {\n // Type-erased return: het schema-type wordt alleen gebruikt om Render/Editor-props te\n // inferreren bij definitie. Daarna behandelen we blokken generiek in de registry.\n return def as unknown as BlockDefinition;\n}\n\nexport type BlockRegistry = Map<string, BlockDefinition>;\n\n/**\n * Bouwt de opties voor het \"blok toevoegen\"-menu, gegroepeerd op categorie: eerst gewone blokken,\n * dan indeling (kolommen/zijbalk/tabs), dan widgets. Widgets/indeling krijgen een suffix zodat de\n * groep duidelijk is in een platte keuzelijst.\n */\nexport function buildBlockAddOptions(defs: Iterable<BlockDefinition>): { value: string; label: string }[] {\n const order: Record<BlockCategory, number> = { content: 0, layout: 1, widget: 2 };\n const suffix: Record<BlockCategory, string> = { content: \"\", layout: \" · indeling\", widget: \" · widget\" };\n return [...defs]\n .map((d) => ({ def: d, cat: d.category ?? \"content\" }))\n .sort((a, b) => order[a.cat] - order[b.cat])\n .map(({ def, cat }) => ({ value: def.type, label: `${def.icon ?? \"▫\"} ${def.label}${suffix[cat]}` }));\n}\n\nexport function createRegistry(defs: BlockDefinition[]): BlockRegistry {\n const m: BlockRegistry = new Map();\n for (const d of defs) m.set(d.type, d);\n return m;\n}\n\nexport function mergeRegistry(base: BlockRegistry, extra: BlockDefinition[]): BlockRegistry {\n const m: BlockRegistry = new Map(base);\n for (const d of extra) m.set(d.type, d);\n return m;\n}\n\n/**\n * Strikte per-type validatie van een blocks-array tegen de registry. Onbekende types en\n * blocks die hun schema niet halen worden weggelaten (defensief — nooit half-rendered content).\n * Gebruikt bij publiceren (server) en vóór renderen.\n *\n * Recursief: geneste blokken in de indelingsblokken (kolommen, \"Met zijbalk\", tabs) worden óók\n * per-type gevalideerd. Hun schema's valideren kinderen bewust alleen als `BlockBaseSchema`\n * (passthrough) zodat klant-blokken erin mogen; de strikte controle gebeurt hier.\n */\n/**\n * De zod-parser wordt hier INGEPLUGD in plaats van geïmporteerd: een statische `safeParse`-import\n * zou de zod-runtime alsnog in elke publieke bundel trekken, ook nu de schema's zelf lazy zijn.\n * `blocks/builtinSchemas` (geladen door de admin/editor-chunks) zet hem; klant-blokken die hun\n * schema meegeven krijgen daarmee ook pas volledige client-validatie zodra de editor laadt —\n * publiek geldt voor hen dezelfde lichte vormcheck als voor de ingebouwde blokken.\n */\ntype SchemaParser = (schema: $ZodType, raw: unknown) => { success: boolean; data?: unknown; issues?: unknown };\nlet schemaParser: SchemaParser | null = null;\nexport function setSchemaParser(fn: SchemaParser): void {\n schemaParser = fn;\n}\n\nexport function validateBlocks(blocks: unknown[] | undefined, registry: BlockRegistry): BlockBase[] {\n const out: BlockBase[] = [];\n for (const raw of blocks ?? []) {\n const type = (raw as { _type?: unknown } | null)?._type;\n const def = typeof type === \"string\" ? registry.get(type) : undefined;\n if (!def) {\n warn(`onbekend block-type \"${String(type)}\" overgeslagen (geen registratie).`);\n continue;\n }\n /**\n * Twee niveaus van controle, afhankelijk van wat er geladen is:\n * - Schema + parser aanwezig (editor/admin, of een klant-blok dat zijn schema meedraagt):\n * volledige zod-validatie, zoals altijd.\n * - Geen schema (publieke render van ingebouwde blokken): een lichte vormcheck volstaat —\n * de inhoudelijke garantie is dan al geleverd door de publiceer-pijplijn (functions\n * saniteert en valideert bij publishPage). Zo blijft zod uit de bezoekersbundel.\n */\n const parser = def.schema ? schemaParser : null;\n if (def.schema && parser) {\n const res = parser(def.schema, raw);\n if (res.success) out.push(validateNestedBlocks(res.data as BlockBase, registry));\n else warn(`block \"${type}\" voldoet niet aan zijn schema en is overgeslagen.`, res.issues);\n } else if (raw && typeof raw === \"object\" && typeof (raw as { _type?: unknown })._type === \"string\") {\n out.push(validateNestedBlocks(raw as BlockBase, registry));\n } else {\n warn(`block \"${String(type)}\" heeft geen geldige vorm en is overgeslagen.`);\n }\n }\n return out;\n}\n\n/** Valideert de geneste blok-lijsten van de bekende indelingsvormen (immutable). */\nfunction validateNestedBlocks(block: BlockBase, registry: BlockRegistry): BlockBase {\n const b = block as BlockBase & Record<string, unknown>;\n if (b._type === \"columns\" && Array.isArray(b.columns)) {\n return {\n ...b,\n columns: b.columns.map((col) =>\n col && typeof col === \"object\" && Array.isArray((col as { blocks?: unknown }).blocks)\n ? { ...col, blocks: validateBlocks((col as { blocks: unknown[] }).blocks, registry) }\n : col,\n ),\n };\n }\n if (b._type === \"withSidebar\" && (Array.isArray(b.main) || Array.isArray(b.aside))) {\n return {\n ...b,\n main: Array.isArray(b.main) ? validateBlocks(b.main, registry) : b.main,\n aside: Array.isArray(b.aside) ? validateBlocks(b.aside, registry) : b.aside,\n };\n }\n if (b._type === \"tabs\" && Array.isArray(b.tabs)) {\n return {\n ...b,\n tabs: b.tabs.map((tab) =>\n tab && typeof tab === \"object\" && Array.isArray((tab as { blocks?: unknown }).blocks)\n ? { ...tab, blocks: validateBlocks((tab as { blocks: unknown[] }).blocks, registry) }\n : tab,\n ),\n };\n }\n return block;\n}\n\nfunction warn(msg: string, detail?: unknown): void {\n // Geen stil verlies: log waarom een block niet rendert (helpt bij schema-wijzigingen).\n if (typeof console !== \"undefined\") console.warn(`[xicms] ${msg}`, detail ?? \"\");\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/blocks/registry.ts"],"sourcesContent":["/**\n * Block-registry: koppelt een `_type` aan zijn Zod-schema, een (server-safe) Render-component\n * en een optionele (client/admin) Editor. Klant-blokken worden hier ingebracht via `defineBlock`\n * — zo blijft de core klant-onafhankelijk: de klant levert de render-component, core de structuur.\n *\n * Pure logica + types (geen \"use client\"): bruikbaar op server en client.\n */\nimport type { ComponentType } from \"react\";\nimport type { $ZodType } from \"zod/v4/core\";\nimport type { BlockBase } from \"../schema/blocks\";\nimport type { FieldHints } from \"../schema/fields\";\n\n/**\n * Het uitvoertype van een blok-schema, onafhankelijk van de Zod-façade.\n *\n * Core draait op `zod/mini` (zie schema/) omdat de classic-façade niet tree-shaket en ~300 KB aan\n * de publieke bundel toevoegde. De grens hieronder is daarom `$ZodType` uit `zod/v4/core` — de\n * gedeelde bovengrens van BEIDE smaken. Gevolg: een klantrepo mag zijn eigen blok-schema's zowel\n * met `zod` als met `zod/mini` schrijven; `defineBlock` accepteert allebei.\n */\ntype InferBlock<S extends $ZodType> = S[\"_zod\"][\"output\"];\n\nexport interface BlockRenderProps<T = unknown> {\n block: T;\n siteId: string;\n /** \"view\" = publiek; \"preview\" = draft-weergave in de editor. */\n mode?: \"view\" | \"preview\";\n}\n\nexport interface BlockEditorProps<T = unknown> {\n block: T;\n siteId: string;\n onChange: (next: T) => void;\n}\n\n/**\n * Indeling van een block in het toevoeg-menu:\n * - `content` (default): een gewoon blok dat je op de pagina plaatst.\n * - `widget`: een compacte variant die je naast een blok (in een zijbalk/kolom) zet — haalt z'n\n * gegevens uit dezelfde module als de volwaardige versie (bv. \"laatste nieuws\", \"komende agenda\").\n * - `layout`: een indelingsblok dat andere blokken bevat (kolommen, zijbalk, tabs).\n */\nexport type BlockCategory = \"content\" | \"widget\" | \"layout\";\n\nexport interface BlockDefinition<S extends $ZodType = $ZodType> {\n type: string;\n /**\n * Zod-schema van het blok. Voor de INGEBOUWDE blokken is dit veld leeg en wordt het schema\n * lazy geregistreerd via `blocks/builtinSchemas` (7.13): de schema's zijn editor-kennis, en\n * mét dit veld reisden álle schema's plus de zod-runtime (±73 KB gzip) mee naar elke publieke\n * bezoeker die nooit iets bewerkt. Klant-blokken (defineBlock in een klantrepo) mogen hun\n * schema gewoon hier blijven meegeven — dat is klein en houdt de API eenvoudig.\n */\n schema?: S;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- registry-grens: het blok\n // typeert zijn eigen props; hier is de data per definitie \"wat de database gaf\".\n Render: ComponentType<BlockRenderProps<any>>;\n /**\n * Admin-editor (lazy/client). Optioneel; ontbreekt = automatische schema-editor (GenericEditor):\n * die genereert een formulier uit het Zod-schema, dus een blok zonder eigen editor blijft bewerkbaar.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- zie Render.\n Editor?: ComponentType<BlockEditorProps<any>>;\n label: string;\n icon?: string;\n /** Groep in het toevoeg-menu (content/widget/layout). Standaard \"content\". */\n category?: BlockCategory;\n /** Defaultwaarden voor een nieuw block (zonder _key/_type). */\n defaults?: () => Record<string, unknown>;\n /**\n * Optionele hints voor het automatisch gegenereerde formulier (GenericEditor 2.0, 5.0):\n * per (dotted) veldsleutel een verfijning van de uit het Zod-schema afgeleide Field-IR —\n * bv. `{ \"items.date\": { type: \"date\" }, cta: { showIf: { field: \"showCta\" } } }`.\n * Zie `introspectToFields` (admin/lib/zodFields.ts) en `schema/fields.ts`.\n */\n fields?: FieldHints;\n /**\n * 8.10: een blok waar de werking van de site op leunt (winkelwagen, afrekenen, formulier).\n * Verwijderen in de editor vraagt dan eerst om bevestiging met deze uitleg; gewone blokken gaan\n * met één klik weg (er is ongedaan maken). De tekst zegt wat er stuk gaat, in klanttaal.\n */\n essentieel?: string;\n}\n\n/**\n * Registreert een block-type.\n *\n * UPDATE-VEILIGHEID: voeg in een nieuwe versie nooit een VERPLICHT veld toe aan een bestaand\n * block-schema zonder `.default()` of `.optional()`. Anders faalt validatie op al opgeslagen\n * content en wordt die overgeslagen. Met defaults/optioneel blijft oude content gewoon werken.\n */\nexport function defineBlock<S extends $ZodType>(def: BlockDefinition<S>): BlockDefinition {\n // Type-erased return: het schema-type wordt alleen gebruikt om Render/Editor-props te\n // inferreren bij definitie. Daarna behandelen we blokken generiek in de registry.\n return def as unknown as BlockDefinition;\n}\n\nexport type BlockRegistry = Map<string, BlockDefinition>;\n\n/**\n * Bouwt de opties voor het \"blok toevoegen\"-menu, gegroepeerd op categorie: eerst gewone blokken,\n * dan indeling (kolommen/zijbalk/tabs), dan widgets. Widgets/indeling krijgen een suffix zodat de\n * groep duidelijk is in een platte keuzelijst.\n */\nexport function buildBlockAddOptions(defs: Iterable<BlockDefinition>): { value: string; label: string }[] {\n const order: Record<BlockCategory, number> = { content: 0, layout: 1, widget: 2 };\n const suffix: Record<BlockCategory, string> = { content: \"\", layout: \" · indeling\", widget: \" · widget\" };\n return [...defs]\n .map((d) => ({ def: d, cat: d.category ?? \"content\" }))\n .sort((a, b) => order[a.cat] - order[b.cat])\n .map(({ def, cat }) => ({ value: def.type, label: `${def.icon ?? \"▫\"} ${def.label}${suffix[cat]}` }));\n}\n\nexport function createRegistry(defs: BlockDefinition[]): BlockRegistry {\n const m: BlockRegistry = new Map();\n for (const d of defs) m.set(d.type, d);\n return m;\n}\n\nexport function mergeRegistry(base: BlockRegistry, extra: BlockDefinition[]): BlockRegistry {\n const m: BlockRegistry = new Map(base);\n for (const d of extra) m.set(d.type, d);\n return m;\n}\n\n/**\n * Strikte per-type validatie van een blocks-array tegen de registry. Onbekende types en\n * blocks die hun schema niet halen worden weggelaten (defensief — nooit half-rendered content).\n * Gebruikt bij publiceren (server) en vóór renderen.\n *\n * Recursief: geneste blokken in de indelingsblokken (kolommen, \"Met zijbalk\", tabs) worden óók\n * per-type gevalideerd. Hun schema's valideren kinderen bewust alleen als `BlockBaseSchema`\n * (passthrough) zodat klant-blokken erin mogen; de strikte controle gebeurt hier.\n */\n/**\n * De zod-parser wordt hier INGEPLUGD in plaats van geïmporteerd: een statische `safeParse`-import\n * zou de zod-runtime alsnog in elke publieke bundel trekken, ook nu de schema's zelf lazy zijn.\n * `blocks/builtinSchemas` (geladen door de admin/editor-chunks) zet hem; klant-blokken die hun\n * schema meegeven krijgen daarmee ook pas volledige client-validatie zodra de editor laadt —\n * publiek geldt voor hen dezelfde lichte vormcheck als voor de ingebouwde blokken.\n */\ntype SchemaParser = (schema: $ZodType, raw: unknown) => { success: boolean; data?: unknown; issues?: unknown };\nlet schemaParser: SchemaParser | null = null;\nexport function setSchemaParser(fn: SchemaParser): void {\n schemaParser = fn;\n}\n\nexport function validateBlocks(blocks: unknown[] | undefined, registry: BlockRegistry): BlockBase[] {\n const out: BlockBase[] = [];\n for (const raw of blocks ?? []) {\n const type = (raw as { _type?: unknown } | null)?._type;\n const def = typeof type === \"string\" ? registry.get(type) : undefined;\n if (!def) {\n warn(`onbekend block-type \"${String(type)}\" overgeslagen (geen registratie).`);\n continue;\n }\n /**\n * Twee niveaus van controle, afhankelijk van wat er geladen is:\n * - Schema + parser aanwezig (editor/admin, of een klant-blok dat zijn schema meedraagt):\n * volledige zod-validatie, zoals altijd.\n * - Geen schema (publieke render van ingebouwde blokken): een lichte vormcheck volstaat —\n * de inhoudelijke garantie is dan al geleverd door de publiceer-pijplijn (functions\n * saniteert en valideert bij publishPage). Zo blijft zod uit de bezoekersbundel.\n */\n const parser = def.schema ? schemaParser : null;\n if (def.schema && parser) {\n const res = parser(def.schema, raw);\n if (res.success) out.push(validateNestedBlocks(res.data as BlockBase, registry));\n else warn(`block \"${type}\" voldoet niet aan zijn schema en is overgeslagen.`, res.issues);\n } else if (raw && typeof raw === \"object\" && typeof (raw as { _type?: unknown })._type === \"string\") {\n out.push(validateNestedBlocks(raw as BlockBase, registry));\n } else {\n warn(`block \"${String(type)}\" heeft geen geldige vorm en is overgeslagen.`);\n }\n }\n return out;\n}\n\n/** Valideert de geneste blok-lijsten van de bekende indelingsvormen (immutable). */\nfunction validateNestedBlocks(block: BlockBase, registry: BlockRegistry): BlockBase {\n const b = block as BlockBase & Record<string, unknown>;\n if (b._type === \"columns\" && Array.isArray(b.columns)) {\n return {\n ...b,\n columns: b.columns.map((col) =>\n col && typeof col === \"object\" && Array.isArray((col as { blocks?: unknown }).blocks)\n ? { ...col, blocks: validateBlocks((col as { blocks: unknown[] }).blocks, registry) }\n : col,\n ),\n };\n }\n if (b._type === \"withSidebar\" && (Array.isArray(b.main) || Array.isArray(b.aside))) {\n return {\n ...b,\n main: Array.isArray(b.main) ? validateBlocks(b.main, registry) : b.main,\n aside: Array.isArray(b.aside) ? validateBlocks(b.aside, registry) : b.aside,\n };\n }\n if (b._type === \"tabs\" && Array.isArray(b.tabs)) {\n return {\n ...b,\n tabs: b.tabs.map((tab) =>\n tab && typeof tab === \"object\" && Array.isArray((tab as { blocks?: unknown }).blocks)\n ? { ...tab, blocks: validateBlocks((tab as { blocks: unknown[] }).blocks, registry) }\n : tab,\n ),\n };\n }\n return block;\n}\n\nfunction warn(msg: string, detail?: unknown): void {\n // Geen stil verlies: log waarom een block niet rendert (helpt bij schema-wijzigingen).\n if (typeof console !== \"undefined\") console.warn(`[xicms] ${msg}`, detail ?? \"\");\n}\n"],"mappings":"AA2FO,SAAS,YAAgC,KAA0C;AAGxF,SAAO;AACT;AASO,SAAS,qBAAqB,MAAqE;AACxG,QAAM,QAAuC,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,EAAE;AAChF,QAAM,SAAwC,EAAE,SAAS,IAAI,QAAQ,mBAAgB,QAAQ,gBAAa;AAC1G,SAAO,CAAC,GAAG,IAAI,EACZ,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,KAAK,EAAE,YAAY,UAAU,EAAE,EACrD,KAAK,CAAC,GAAG,MAAM,MAAM,EAAE,GAAG,IAAI,MAAM,EAAE,GAAG,CAAC,EAC1C,IAAI,CAAC,EAAE,KAAK,IAAI,OAAO,EAAE,OAAO,IAAI,MAAM,OAAO,GAAG,IAAI,QAAQ,QAAG,KAAK,IAAI,KAAK,GAAG,OAAO,GAAG,CAAC,GAAG,EAAE;AACzG;AAEO,SAAS,eAAe,MAAwC;AACrE,QAAM,IAAmB,oBAAI,IAAI;AACjC,aAAW,KAAK,KAAM,GAAE,IAAI,EAAE,MAAM,CAAC;AACrC,SAAO;AACT;AAEO,SAAS,cAAc,MAAqB,OAAyC;AAC1F,QAAM,IAAmB,IAAI,IAAI,IAAI;AACrC,aAAW,KAAK,MAAO,GAAE,IAAI,EAAE,MAAM,CAAC;AACtC,SAAO;AACT;AAmBA,IAAI,eAAoC;AACjC,SAAS,gBAAgB,IAAwB;AACtD,iBAAe;AACjB;AAEO,SAAS,eAAe,QAA+B,UAAsC;AAClG,QAAM,MAAmB,CAAC;AAC1B,aAAW,OAAO,UAAU,CAAC,GAAG;AAC9B,UAAM,OAAQ,KAAoC;AAClD,UAAM,MAAM,OAAO,SAAS,WAAW,SAAS,IAAI,IAAI,IAAI;AAC5D,QAAI,CAAC,KAAK;AACR,WAAK,wBAAwB,OAAO,IAAI,CAAC,oCAAoC;AAC7E;AAAA,IACF;AASA,UAAM,SAAS,IAAI,SAAS,eAAe;AAC3C,QAAI,IAAI,UAAU,QAAQ;AACxB,YAAM,MAAM,OAAO,IAAI,QAAQ,GAAG;AAClC,UAAI,IAAI,QAAS,KAAI,KAAK,qBAAqB,IAAI,MAAmB,QAAQ,CAAC;AAAA,UAC1E,MAAK,UAAU,IAAI,sDAAsD,IAAI,MAAM;AAAA,IAC1F,WAAW,OAAO,OAAO,QAAQ,YAAY,OAAQ,IAA4B,UAAU,UAAU;AACnG,UAAI,KAAK,qBAAqB,KAAkB,QAAQ,CAAC;AAAA,IAC3D,OAAO;AACL,WAAK,UAAU,OAAO,IAAI,CAAC,+CAA+C;AAAA,IAC5E;AAAA,EACF;AACA,SAAO;AACT;AAGA,SAAS,qBAAqB,OAAkB,UAAoC;AAClF,QAAM,IAAI;AACV,MAAI,EAAE,UAAU,aAAa,MAAM,QAAQ,EAAE,OAAO,GAAG;AACrD,WAAO;AAAA,MACL,GAAG;AAAA,MACH,SAAS,EAAE,QAAQ;AAAA,QAAI,CAAC,QACtB,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAS,IAA6B,MAAM,IAChF,EAAE,GAAG,KAAK,QAAQ,eAAgB,IAA8B,QAAQ,QAAQ,EAAE,IAClF;AAAA,MACN;AAAA,IACF;AAAA,EACF;AACA,MAAI,EAAE,UAAU,kBAAkB,MAAM,QAAQ,EAAE,IAAI,KAAK,MAAM,QAAQ,EAAE,KAAK,IAAI;AAClF,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM,MAAM,QAAQ,EAAE,IAAI,IAAI,eAAe,EAAE,MAAM,QAAQ,IAAI,EAAE;AAAA,MACnE,OAAO,MAAM,QAAQ,EAAE,KAAK,IAAI,eAAe,EAAE,OAAO,QAAQ,IAAI,EAAE;AAAA,IACxE;AAAA,EACF;AACA,MAAI,EAAE,UAAU,UAAU,MAAM,QAAQ,EAAE,IAAI,GAAG;AAC/C,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM,EAAE,KAAK;AAAA,QAAI,CAAC,QAChB,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAS,IAA6B,MAAM,IAChF,EAAE,GAAG,KAAK,QAAQ,eAAgB,IAA8B,QAAQ,QAAQ,EAAE,IAClF;AAAA,MACN;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,KAAK,KAAa,QAAwB;AAEjD,MAAI,OAAO,YAAY,YAAa,SAAQ,KAAK,WAAW,GAAG,IAAI,UAAU,EAAE;AACjF;","names":[]}
@@ -29,6 +29,7 @@ function Btn({ label, href, variant = "primary", size = "lg", type, target, onCl
29
29
  rel: target === "_blank" ? "noopener noreferrer" : void 0,
30
30
  type: href ? void 0 : type ?? "button",
31
31
  clickAction: onClick,
32
+ className: variant === "outline" ? "xicms-btn-omlijnd" : void 0,
32
33
  children: editField ? /* @__PURE__ */ jsx("span", { "data-xicms-edit": editField, children: label }) : void 0
33
34
  }
34
35
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/ui/index.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Gedeelde presentatie-primitieven voor de runtime-blokken — een dunne laag bovenop het Astryx\n * design system (Meta, StyleX). Blokken bouwen hierop i.p.v. losse `xicms-*`-classNames, zodat het\n * uiterlijk consistent is en we de onderlaag centraal kunnen wijzigen.\n *\n * De merk-look per site komt via de Astryx-token-mapping op `.xicms-root` (zie XiCMSProvider):\n * `--color-accent`, `--font-family-*`, `--radius-*` volgen de XiCMS-thema-tokens.\n */\n\nimport { type ReactNode } from \"react\";\nimport { Button, type ButtonVariant } from \"@astryxdesign/core/Button\";\n\nexport { VStack } from \"@astryxdesign/core/VStack\";\nexport { HStack } from \"@astryxdesign/core/HStack\";\nexport { Stack } from \"@astryxdesign/core/Stack\";\nexport { Grid } from \"@astryxdesign/core/Grid\";\nexport { Card } from \"@astryxdesign/core/Card\";\nexport { Heading } from \"@astryxdesign/core/Heading\";\nexport { Text } from \"@astryxdesign/core/Text\";\nexport { Divider } from \"@astryxdesign/core/Divider\";\nexport { Badge } from \"@astryxdesign/core/Badge\";\nexport { Avatar } from \"@astryxdesign/core/Avatar\";\n\n/** XiCMS-knopvarianten → Astryx-varianten (Astryx kent geen \"outline\" → ghost). */\nconst VARIANT_MAP: Record<string, ButtonVariant> = {\n primary: \"primary\",\n secondary: \"secondary\",\n outline: \"ghost\",\n ghost: \"ghost\",\n};\n\nexport interface BtnProps {\n label: string;\n href?: string;\n variant?: string;\n /** Marketing-knoppen zijn standaard wat groter dan de app-default. */\n size?: \"sm\" | \"md\" | \"lg\";\n type?: \"button\" | \"submit\" | \"reset\";\n target?: string;\n onClick?: () => void;\n children?: ReactNode;\n /**\n * Veldpad voor inline bewerken, bv. `\"buttonLabel\"` of `` `items.${i}.ctaLabel` ``. Het label\n * gaat dan door een eigen `<span>` met de `data-xicms-edit`-haak, zodat de knoptekst op het\n * canvas te bewerken is i.p.v. alleen via het zijpaneel. Astryx houdt `label` als toegankelijke\n * naam aan (het zet zelf `aria-label` zodra children afwijken), dus dit kost geen a11y.\n */\n editField?: string;\n}\n\n/**\n * Knop/CTA-helper bovenop Astryx `Button`. Rendert als link wanneer `href` gezet is. Houdt de\n * bestaande XiCMS-varianten (primary/secondary/outline) werkend via de mapping hierboven.\n */\nexport function Btn({ label, href, variant = \"primary\", size = \"lg\", type, target, onClick, editField }: BtnProps) {\n return (\n <Button\n label={label}\n variant={VARIANT_MAP[variant] ?? \"primary\"}\n size={size}\n href={href}\n target={href && target ? target : undefined}\n rel={target === \"_blank\" ? \"noopener noreferrer\" : undefined}\n type={href ? undefined : (type ?? \"button\")}\n clickAction={onClick}\n >\n {/* `undefined` laat Astryx op `label` terugvallen — alleen mét haak komt er een span omheen. */}\n {editField ? <span data-xicms-edit={editField}>{label}</span> : undefined}\n </Button>\n );\n}\n"],"mappings":";AAqEmB;AAzDnB,SAAS,cAAkC;AAE3C,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,SAAS,cAAc;AAGvB,MAAM,cAA6C;AAAA,EACjD,SAAS;AAAA,EACT,WAAW;AAAA,EACX,SAAS;AAAA,EACT,OAAO;AACT;AAyBO,SAAS,IAAI,EAAE,OAAO,MAAM,UAAU,WAAW,OAAO,MAAM,MAAM,QAAQ,SAAS,UAAU,GAAa;AACjH,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,YAAY,OAAO,KAAK;AAAA,MACjC;AAAA,MACA;AAAA,MACA,QAAQ,QAAQ,SAAS,SAAS;AAAA,MAClC,KAAK,WAAW,WAAW,wBAAwB;AAAA,MACnD,MAAM,OAAO,SAAa,QAAQ;AAAA,MAClC,aAAa;AAAA,MAGZ,sBAAY,oBAAC,UAAK,mBAAiB,WAAY,iBAAM,IAAU;AAAA;AAAA,EAClE;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/ui/index.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Gedeelde presentatie-primitieven voor de runtime-blokken — een dunne laag bovenop het Astryx\n * design system (Meta, StyleX). Blokken bouwen hierop i.p.v. losse `xicms-*`-classNames, zodat het\n * uiterlijk consistent is en we de onderlaag centraal kunnen wijzigen.\n *\n * De merk-look per site komt via de Astryx-token-mapping op `.xicms-root` (zie XiCMSProvider):\n * `--color-accent`, `--font-family-*`, `--radius-*` volgen de XiCMS-thema-tokens.\n */\n\nimport { type ReactNode } from \"react\";\nimport { Button, type ButtonVariant } from \"@astryxdesign/core/Button\";\n\nexport { VStack } from \"@astryxdesign/core/VStack\";\nexport { HStack } from \"@astryxdesign/core/HStack\";\nexport { Stack } from \"@astryxdesign/core/Stack\";\nexport { Grid } from \"@astryxdesign/core/Grid\";\nexport { Card } from \"@astryxdesign/core/Card\";\nexport { Heading } from \"@astryxdesign/core/Heading\";\nexport { Text } from \"@astryxdesign/core/Text\";\nexport { Divider } from \"@astryxdesign/core/Divider\";\nexport { Badge } from \"@astryxdesign/core/Badge\";\nexport { Avatar } from \"@astryxdesign/core/Avatar\";\n\n/** XiCMS-knopvarianten → Astryx-varianten (Astryx kent geen \"outline\" → ghost). */\nconst VARIANT_MAP: Record<string, ButtonVariant> = {\n primary: \"primary\",\n secondary: \"secondary\",\n outline: \"ghost\",\n ghost: \"ghost\",\n};\n\nexport interface BtnProps {\n label: string;\n href?: string;\n variant?: string;\n /** Marketing-knoppen zijn standaard wat groter dan de app-default. */\n size?: \"sm\" | \"md\" | \"lg\";\n type?: \"button\" | \"submit\" | \"reset\";\n target?: string;\n onClick?: () => void;\n children?: ReactNode;\n /**\n * Veldpad voor inline bewerken, bv. `\"buttonLabel\"` of `` `items.${i}.ctaLabel` ``. Het label\n * gaat dan door een eigen `<span>` met de `data-xicms-edit`-haak, zodat de knoptekst op het\n * canvas te bewerken is i.p.v. alleen via het zijpaneel. Astryx houdt `label` als toegankelijke\n * naam aan (het zet zelf `aria-label` zodra children afwijken), dus dit kost geen a11y.\n */\n editField?: string;\n}\n\n/**\n * Knop/CTA-helper bovenop Astryx `Button`. Rendert als link wanneer `href` gezet is. Houdt de\n * bestaande XiCMS-varianten (primary/secondary/outline) werkend via de mapping hierboven.\n */\nexport function Btn({ label, href, variant = \"primary\", size = \"lg\", type, target, onClick, editField }: BtnProps) {\n return (\n <Button\n label={label}\n variant={VARIANT_MAP[variant] ?? \"primary\"}\n size={size}\n href={href}\n target={href && target ? target : undefined}\n rel={target === \"_blank\" ? \"noopener noreferrer\" : undefined}\n type={href ? undefined : (type ?? \"button\")}\n clickAction={onClick}\n // 8.12: \"Omlijnd\" was een kale ghost-knop — losse tekst zonder rand, die naast een primaire\n // knop niet als knop las. Nu een echte omlijning (zie .xicms-btn-omlijnd).\n className={variant === \"outline\" ? \"xicms-btn-omlijnd\" : undefined}\n >\n {/* `undefined` laat Astryx op `label` terugvallen — alleen mét haak komt er een span omheen. */}\n {editField ? <span data-xicms-edit={editField}>{label}</span> : undefined}\n </Button>\n );\n}\n"],"mappings":";AAwEmB;AA5DnB,SAAS,cAAkC;AAE3C,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,SAAS,cAAc;AAGvB,MAAM,cAA6C;AAAA,EACjD,SAAS;AAAA,EACT,WAAW;AAAA,EACX,SAAS;AAAA,EACT,OAAO;AACT;AAyBO,SAAS,IAAI,EAAE,OAAO,MAAM,UAAU,WAAW,OAAO,MAAM,MAAM,QAAQ,SAAS,UAAU,GAAa;AACjH,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,YAAY,OAAO,KAAK;AAAA,MACjC;AAAA,MACA;AAAA,MACA,QAAQ,QAAQ,SAAS,SAAS;AAAA,MAClC,KAAK,WAAW,WAAW,wBAAwB;AAAA,MACnD,MAAM,OAAO,SAAa,QAAQ;AAAA,MAClC,aAAa;AAAA,MAGb,WAAW,YAAY,YAAY,sBAAsB;AAAA,MAGxD,sBAAY,oBAAC,UAAK,mBAAiB,WAAY,iBAAM,IAAU;AAAA;AAAA,EAClE;AAEJ;","names":[]}
@@ -8,11 +8,9 @@ import { useEditor } from "../editor/EditorProvider.js";
8
8
  function EditBar() {
9
9
  const { user, role } = useAuth();
10
10
  const { open } = useEditor();
11
+ const siteId = useSiteConfig().siteId;
11
12
  if (!canEdit(role) || !user || open) return null;
12
- return /* @__PURE__ */ jsx(EditBarVoorEditor, { siteId: useSiteConfigSiteId() });
13
- }
14
- function useSiteConfigSiteId() {
15
- return useSiteConfig().siteId;
13
+ return /* @__PURE__ */ jsx(EditBarVoorEditor, { siteId });
16
14
  }
17
15
  function EditBarVoorEditor({ siteId }) {
18
16
  const nietLive = useNietLive(siteId);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/EditBar.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { useSiteConfig } from \"../config\";\nimport { canEdit } from \"../auth/roles\";\nimport { useEditor, type Bewerkmodus } from \"../editor/EditorProvider\";\n\n/**\n * Zwevende werkbalk voor ingelogde beheerders/editors.\n *\n * Bewust met EIGEN klassen en eigen kleuren, niet met `.xicms-btn`. Die knop leest de\n * `--xicms-*`-tokens van de klantsite, en dit balkje zweeft over die site heen: het nam daardoor\n * half de huisstijl over (en op plekken waar de tokens niet in scope zijn, viel het terug op een\n * indigo die bij niets hoorde). Gereedschap hoort er als gereedschap uit te zien — hetzelfde op\n * elke site, herkenbaar los van de pagina eronder.\n *\n * ── VIER OPTIES, ALTIJD DEZELFDE (7.8) ────────────────────────────────────────────────────\n *\n * De balk had één hoofdactie (\"Pagina bewerken\") en verdween zodra je die indrukte: de editor\n * bracht zijn eigen balk mee, met andere knoppen op andere plekken. Je verloor dus je houvast op\n * het moment dat je het het hardst nodig had, en er was geen weg terug naar de live site zonder\n * de editor te sluiten.\n *\n * Nu staan er vier vaste opties — Live site · Bewerken · Beheren · Uitloggen — en die staan er in\n * béide toestanden, op dezelfde plek. `AdminModes` hieronder is letterlijk hetzelfde component in\n * de balk van de editor, zodat het overschakelen geen wisseling van balk is maar alleen een andere\n * knop die oplicht.\n */\nexport function EditBar() {\n // Bewust via useAuth (niet via het deprecated ContentProvider-pad): de EditBar hoort bij het\n // block-model en mag niet aan het legacy inline-systeem hangen.\n const { user, role } = useAuth();\n const { open } = useEditor();\n // Geen useBalkOffset meer: het pilletje zweeft óver de site (7.16), er wordt niets meer\n // omlaag geduwd — dus valt er ook niets meer onder een balk vandaan te schuiven.\n\n // In de bewerkmodus tekent de editor de balk (met dezelfde vier opties plus zijn gereedschap);\n // twee balken onder elkaar zou de site twee keer zo ver omlaag duwen.\n if (!canEdit(role) || !user || open) return null;\n\n return <EditBarVoorEditor siteId={useSiteConfigSiteId()} />;\n}\n\nfunction useSiteConfigSiteId(): string {\n return useSiteConfig().siteId;\n}\n\n/**\n * Apart component zodat de draft-lezing hieronder ALLEEN draait voor een ingelogde editor — het\n * bovenliggende EditBar rendert voor elke bezoeker en mag geen Firestore aanraken.\n */\nfunction EditBarVoorEditor({ siteId }: { siteId: string }) {\n const nietLive = useNietLive(siteId);\n return (\n <div className=\"xicms-editbar\" role=\"toolbar\" aria-label=\"Beheer\" data-xicms-tooling=\"\">\n <EditbarBrand label=\"XiCMS\" />\n <AdminModes actief=\"live\" nietLive={nietLive} />\n </div>\n );\n}\n\n/**\n * Klein merkicoontje + naam (7.26) — gedeeld door de balk hierboven en die van de visuele editor\n * (VisualEditor.tsx), zodat \"hetzelfde gereedschap, ongeacht waar\" ook geldt voor het merkje zelf.\n * Puur decoratief: het icoon is `aria-hidden`, de naam draagt de eigenlijke betekenis.\n */\nexport function EditbarBrand({ label }: { label: string }) {\n return (\n <span className=\"xicms-editbar__brand\">\n {/* Bewust een sterretje, geen kruis: twee kruisende lijnen op deze schaal las als een\n sluitknopje naast de merknaam — precies de handeling die je hier niet wilt suggereren. */}\n <svg className=\"xicms-editbar__mark\" viewBox=\"0 0 16 16\" width=\"13\" height=\"13\" aria-hidden=\"true\">\n <rect width=\"16\" height=\"16\" rx=\"4.5\" fill=\"currentColor\" opacity=\"0.22\" />\n <path d=\"M8 3c.28 1.95 1.17 2.84 3.1 3.1-1.93.26-2.82 1.15-3.1 3.1-.28-1.95-1.17-2.84-3.1-3.1 1.93-.26 2.82-1.15 3.1-3.1z\" fill=\"currentColor\" />\n </svg>\n {label}\n </span>\n );\n}\n\n/**\n * Staat er op DEZE pagina een werkversie die afwijkt van wat live staat? (7.24)\n *\n * Eén lezing van de draft bij het laden (geen listener): dit is een signaal bij binnenkomst, geen\n * monitor. De vergelijking is dezelfde als in /beheer → Pagina's (`heeftOngepubliceerdeWijzigingen`).\n * Faalt stil — een hik mag de balk niet raken, en zonder signaal is er hoogstens geen stipje.\n */\nfunction useNietLive(siteId: string): boolean {\n const [nietLive, setNietLive] = useState(false);\n useEffect(() => {\n let actief = true;\n const raw = window.location.pathname || \"/\";\n const path = raw === \"/\" ? \"/\" : raw.replace(/\\/$/, \"\");\n void import(\"../admin/lib/pagesClient\")\n .then(async ({ getDraftByPath, heeftOngepubliceerdeWijzigingen }) => {\n const d = await getDraftByPath(siteId, path);\n if (actief && d) setNietLive(heeftOngepubliceerdeWijzigingen(d));\n })\n .catch(() => undefined);\n return () => {\n actief = false;\n };\n }, [siteId]);\n return nietLive;\n}\n\n/**\n * De vier opties zelf, los zodat de editor ze kan hergebruiken.\n *\n * `actief` zegt welke toestand je nú bekijkt, niet wat de knop doet: \"Live site\" is ingedrukt\n * zolang je de pagina ziet zoals een bezoeker hem ziet. Zonder die markering is er geen verschil\n * te zien tussen \"ik kijk naar de live site\" en \"ik bewerk maar heb nog niets veranderd\", en dat\n * verschil is precies waar deze balk over gaat.\n */\nexport function AdminModes({\n actief,\n voorSwitch,\n nietLive = false,\n}: {\n actief: Bewerkmodus;\n /** 7.24: toon een stipje op \"Bewerken\" — er ligt een werkversie die niemand ziet. */\n nietLive?: boolean;\n /**\n * Kans voor de editor om iets te zeggen vóór je weggaat — bijvoorbeeld dat er nog wijzigingen\n * klaarstaan die niet live staan. Bewust niet blokkerend: sluiten bewaart je werk, dus een\n * bevestigingsvraag zou alleen maar in de weg zitten. Melden is genoeg, tegenhouden is te veel.\n */\n voorSwitch?: (modus: Bewerkmodus) => void;\n}) {\n const { demo, signOut } = useAuth();\n const { zetModus } = useEditor();\n\n const knop = (modus: Bewerkmodus, label: string, titel: string) => (\n <button\n type=\"button\"\n className={`xicms-editbar__btn${actief === modus ? \" is-actief\" : \"\"}${modus === \"bewerken\" && nietLive ? \" heeft-wijzigingen\" : \"\"}`}\n aria-pressed={actief === modus}\n title={titel}\n onClick={() => {\n if (modus !== actief) voorSwitch?.(modus);\n zetModus(modus);\n }}\n >\n {label}\n </button>\n );\n\n return (\n <>\n {knop(\"live\", \"Live site\", \"De pagina precies zoals een bezoeker hem ziet\")}\n {knop(\"bewerken\", \"Bewerken\", nietLive ? \"Er staan wijzigingen klaar die nog niet live zijn\" : \"Dezelfde pagina, met de werkversie en potlood/tandwiel per blok\")}\n <span className=\"xicms-editbar__sep\" aria-hidden=\"true\" />\n {/* In de demo bestaat er geen /beheer-route; daar bewerk je alleen op de pagina. */}\n {!demo && (\n <a href=\"/beheer\" className=\"xicms-editbar__btn\" title=\"Pagina's, media, instellingen, webwinkel\">\n Beheren\n </a>\n )}\n <button type=\"button\" className=\"xicms-editbar__btn\" onClick={() => void signOut()}>\n Uitloggen\n </button>\n </>\n );\n}\n"],"mappings":";AAyCS,SA4GL,UA5GK,KAcL,YAdK;AAvCT,SAAS,WAAW,gBAAgB;AACpC,SAAS,eAAe;AACxB,SAAS,qBAAqB;AAC9B,SAAS,eAAe;AACxB,SAAS,iBAAmC;AAuBrC,SAAS,UAAU;AAGxB,QAAM,EAAE,MAAM,KAAK,IAAI,QAAQ;AAC/B,QAAM,EAAE,KAAK,IAAI,UAAU;AAM3B,MAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,KAAM,QAAO;AAE5C,SAAO,oBAAC,qBAAkB,QAAQ,oBAAoB,GAAG;AAC3D;AAEA,SAAS,sBAA8B;AACrC,SAAO,cAAc,EAAE;AACzB;AAMA,SAAS,kBAAkB,EAAE,OAAO,GAAuB;AACzD,QAAM,WAAW,YAAY,MAAM;AACnC,SACE,qBAAC,SAAI,WAAU,iBAAgB,MAAK,WAAU,cAAW,UAAS,sBAAmB,IACnF;AAAA,wBAAC,gBAAa,OAAM,SAAQ;AAAA,IAC5B,oBAAC,cAAW,QAAO,QAAO,UAAoB;AAAA,KAChD;AAEJ;AAOO,SAAS,aAAa,EAAE,MAAM,GAAsB;AACzD,SACE,qBAAC,UAAK,WAAU,wBAGd;AAAA,yBAAC,SAAI,WAAU,uBAAsB,SAAQ,aAAY,OAAM,MAAK,QAAO,MAAK,eAAY,QAC1F;AAAA,0BAAC,UAAK,OAAM,MAAK,QAAO,MAAK,IAAG,OAAM,MAAK,gBAAe,SAAQ,QAAO;AAAA,MACzE,oBAAC,UAAK,GAAE,oHAAmH,MAAK,gBAAe;AAAA,OACjJ;AAAA,IACC;AAAA,KACH;AAEJ;AASA,SAAS,YAAY,QAAyB;AAC5C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,YAAU,MAAM;AACd,QAAI,SAAS;AACb,UAAM,MAAM,OAAO,SAAS,YAAY;AACxC,UAAM,OAAO,QAAQ,MAAM,MAAM,IAAI,QAAQ,OAAO,EAAE;AACtD,SAAK,OAAO,0BAA0B,EACnC,KAAK,OAAO,EAAE,gBAAgB,gCAAgC,MAAM;AACnE,YAAM,IAAI,MAAM,eAAe,QAAQ,IAAI;AAC3C,UAAI,UAAU,EAAG,aAAY,gCAAgC,CAAC,CAAC;AAAA,IACjE,CAAC,EACA,MAAM,MAAM,MAAS;AACxB,WAAO,MAAM;AACX,eAAS;AAAA,IACX;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AACX,SAAO;AACT;AAUO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAUG;AACD,QAAM,EAAE,MAAM,QAAQ,IAAI,QAAQ;AAClC,QAAM,EAAE,SAAS,IAAI,UAAU;AAE/B,QAAM,OAAO,CAAC,OAAoB,OAAe,UAC/C;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAW,qBAAqB,WAAW,QAAQ,eAAe,EAAE,GAAG,UAAU,cAAc,WAAW,uBAAuB,EAAE;AAAA,MACnI,gBAAc,WAAW;AAAA,MACzB,OAAO;AAAA,MACP,SAAS,MAAM;AACb,YAAI,UAAU,OAAQ,cAAa,KAAK;AACxC,iBAAS,KAAK;AAAA,MAChB;AAAA,MAEC;AAAA;AAAA,EACH;AAGF,SACE,iCACG;AAAA,SAAK,QAAQ,aAAa,+CAA+C;AAAA,IACzE,KAAK,YAAY,YAAY,WAAW,sDAAsD,iEAAiE;AAAA,IAChK,oBAAC,UAAK,WAAU,sBAAqB,eAAY,QAAO;AAAA,IAEvD,CAAC,QACA,oBAAC,OAAE,MAAK,WAAU,WAAU,sBAAqB,OAAM,4CAA2C,qBAElG;AAAA,IAEF,oBAAC,YAAO,MAAK,UAAS,WAAU,sBAAqB,SAAS,MAAM,KAAK,QAAQ,GAAG,uBAEpF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/EditBar.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { useSiteConfig } from \"../config\";\nimport { canEdit } from \"../auth/roles\";\nimport { useEditor, type Bewerkmodus } from \"../editor/EditorProvider\";\n\n/**\n * Zwevende werkbalk voor ingelogde beheerders/editors.\n *\n * Bewust met EIGEN klassen en eigen kleuren, niet met `.xicms-btn`. Die knop leest de\n * `--xicms-*`-tokens van de klantsite, en dit balkje zweeft over die site heen: het nam daardoor\n * half de huisstijl over (en op plekken waar de tokens niet in scope zijn, viel het terug op een\n * indigo die bij niets hoorde). Gereedschap hoort er als gereedschap uit te zien — hetzelfde op\n * elke site, herkenbaar los van de pagina eronder.\n *\n * ── VIER OPTIES, ALTIJD DEZELFDE (7.8) ────────────────────────────────────────────────────\n *\n * De balk had één hoofdactie (\"Pagina bewerken\") en verdween zodra je die indrukte: de editor\n * bracht zijn eigen balk mee, met andere knoppen op andere plekken. Je verloor dus je houvast op\n * het moment dat je het het hardst nodig had, en er was geen weg terug naar de live site zonder\n * de editor te sluiten.\n *\n * Nu staan er vier vaste opties — Live site · Bewerken · Beheren · Uitloggen — en die staan er in\n * béide toestanden, op dezelfde plek. `AdminModes` hieronder is letterlijk hetzelfde component in\n * de balk van de editor, zodat het overschakelen geen wisseling van balk is maar alleen een andere\n * knop die oplicht.\n */\nexport function EditBar() {\n // Bewust via useAuth (niet via het deprecated ContentProvider-pad): de EditBar hoort bij het\n // block-model en mag niet aan het legacy inline-systeem hangen.\n const { user, role } = useAuth();\n const { open } = useEditor();\n // Vóór de vroege return: een hook ná `return null` wisselde van volgorde zodra de editor\n // dichtging (React-waarschuwing \"change in the order of Hooks\").\n const siteId = useSiteConfig().siteId;\n // Geen useBalkOffset meer: het pilletje zweeft óver de site (7.16), er wordt niets meer\n // omlaag geduwd — dus valt er ook niets meer onder een balk vandaan te schuiven.\n\n // In de bewerkmodus tekent de editor de balk (met dezelfde vier opties plus zijn gereedschap);\n // twee balken onder elkaar zou de site twee keer zo ver omlaag duwen.\n if (!canEdit(role) || !user || open) return null;\n\n return <EditBarVoorEditor siteId={siteId} />;\n}\n\n/**\n * Apart component zodat de draft-lezing hieronder ALLEEN draait voor een ingelogde editor — het\n * bovenliggende EditBar rendert voor elke bezoeker en mag geen Firestore aanraken.\n */\nfunction EditBarVoorEditor({ siteId }: { siteId: string }) {\n const nietLive = useNietLive(siteId);\n return (\n <div className=\"xicms-editbar\" role=\"toolbar\" aria-label=\"Beheer\" data-xicms-tooling=\"\">\n <EditbarBrand label=\"XiCMS\" />\n <AdminModes actief=\"live\" nietLive={nietLive} />\n </div>\n );\n}\n\n/**\n * Klein merkicoontje + naam (7.26) — gedeeld door de balk hierboven en die van de visuele editor\n * (VisualEditor.tsx), zodat \"hetzelfde gereedschap, ongeacht waar\" ook geldt voor het merkje zelf.\n * Puur decoratief: het icoon is `aria-hidden`, de naam draagt de eigenlijke betekenis.\n */\nexport function EditbarBrand({ label }: { label: string }) {\n return (\n <span className=\"xicms-editbar__brand\">\n {/* Bewust een sterretje, geen kruis: twee kruisende lijnen op deze schaal las als een\n sluitknopje naast de merknaam — precies de handeling die je hier niet wilt suggereren. */}\n <svg className=\"xicms-editbar__mark\" viewBox=\"0 0 16 16\" width=\"13\" height=\"13\" aria-hidden=\"true\">\n <rect width=\"16\" height=\"16\" rx=\"4.5\" fill=\"currentColor\" opacity=\"0.22\" />\n <path d=\"M8 3c.28 1.95 1.17 2.84 3.1 3.1-1.93.26-2.82 1.15-3.1 3.1-.28-1.95-1.17-2.84-3.1-3.1 1.93-.26 2.82-1.15 3.1-3.1z\" fill=\"currentColor\" />\n </svg>\n {label}\n </span>\n );\n}\n\n/**\n * Staat er op DEZE pagina een werkversie die afwijkt van wat live staat? (7.24)\n *\n * Eén lezing van de draft bij het laden (geen listener): dit is een signaal bij binnenkomst, geen\n * monitor. De vergelijking is dezelfde als in /beheer → Pagina's (`heeftOngepubliceerdeWijzigingen`).\n * Faalt stil — een hik mag de balk niet raken, en zonder signaal is er hoogstens geen stipje.\n */\nfunction useNietLive(siteId: string): boolean {\n const [nietLive, setNietLive] = useState(false);\n useEffect(() => {\n let actief = true;\n const raw = window.location.pathname || \"/\";\n const path = raw === \"/\" ? \"/\" : raw.replace(/\\/$/, \"\");\n void import(\"../admin/lib/pagesClient\")\n .then(async ({ getDraftByPath, heeftOngepubliceerdeWijzigingen }) => {\n const d = await getDraftByPath(siteId, path);\n if (actief && d) setNietLive(heeftOngepubliceerdeWijzigingen(d));\n })\n .catch(() => undefined);\n return () => {\n actief = false;\n };\n }, [siteId]);\n return nietLive;\n}\n\n/**\n * De vier opties zelf, los zodat de editor ze kan hergebruiken.\n *\n * `actief` zegt welke toestand je nú bekijkt, niet wat de knop doet: \"Live site\" is ingedrukt\n * zolang je de pagina ziet zoals een bezoeker hem ziet. Zonder die markering is er geen verschil\n * te zien tussen \"ik kijk naar de live site\" en \"ik bewerk maar heb nog niets veranderd\", en dat\n * verschil is precies waar deze balk over gaat.\n */\nexport function AdminModes({\n actief,\n voorSwitch,\n nietLive = false,\n}: {\n actief: Bewerkmodus;\n /** 7.24: toon een stipje op \"Bewerken\" — er ligt een werkversie die niemand ziet. */\n nietLive?: boolean;\n /**\n * Kans voor de editor om iets te zeggen vóór je weggaat — bijvoorbeeld dat er nog wijzigingen\n * klaarstaan die niet live staan. Bewust niet blokkerend: sluiten bewaart je werk, dus een\n * bevestigingsvraag zou alleen maar in de weg zitten. Melden is genoeg, tegenhouden is te veel.\n */\n voorSwitch?: (modus: Bewerkmodus) => void;\n}) {\n const { demo, signOut } = useAuth();\n const { zetModus } = useEditor();\n\n const knop = (modus: Bewerkmodus, label: string, titel: string) => (\n <button\n type=\"button\"\n className={`xicms-editbar__btn${actief === modus ? \" is-actief\" : \"\"}${modus === \"bewerken\" && nietLive ? \" heeft-wijzigingen\" : \"\"}`}\n aria-pressed={actief === modus}\n title={titel}\n onClick={() => {\n if (modus !== actief) voorSwitch?.(modus);\n zetModus(modus);\n }}\n >\n {label}\n </button>\n );\n\n return (\n <>\n {knop(\"live\", \"Live site\", \"De pagina precies zoals een bezoeker hem ziet\")}\n {knop(\"bewerken\", \"Bewerken\", nietLive ? \"Er staan wijzigingen klaar die nog niet live zijn\" : \"Dezelfde pagina, met de werkversie en potlood/tandwiel per blok\")}\n <span className=\"xicms-editbar__sep\" aria-hidden=\"true\" />\n {/* In de demo bestaat er geen /beheer-route; daar bewerk je alleen op de pagina. */}\n {!demo && (\n <a href=\"/beheer\" className=\"xicms-editbar__btn\" title=\"Pagina's, media, instellingen, webwinkel\">\n Beheren\n </a>\n )}\n <button type=\"button\" className=\"xicms-editbar__btn\" onClick={() => void signOut()}>\n Uitloggen\n </button>\n </>\n );\n}\n"],"mappings":";AA4CS,SAwGL,UAxGK,KAUL,YAVK;AA1CT,SAAS,WAAW,gBAAgB;AACpC,SAAS,eAAe;AACxB,SAAS,qBAAqB;AAC9B,SAAS,eAAe;AACxB,SAAS,iBAAmC;AAuBrC,SAAS,UAAU;AAGxB,QAAM,EAAE,MAAM,KAAK,IAAI,QAAQ;AAC/B,QAAM,EAAE,KAAK,IAAI,UAAU;AAG3B,QAAM,SAAS,cAAc,EAAE;AAM/B,MAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,KAAM,QAAO;AAE5C,SAAO,oBAAC,qBAAkB,QAAgB;AAC5C;AAMA,SAAS,kBAAkB,EAAE,OAAO,GAAuB;AACzD,QAAM,WAAW,YAAY,MAAM;AACnC,SACE,qBAAC,SAAI,WAAU,iBAAgB,MAAK,WAAU,cAAW,UAAS,sBAAmB,IACnF;AAAA,wBAAC,gBAAa,OAAM,SAAQ;AAAA,IAC5B,oBAAC,cAAW,QAAO,QAAO,UAAoB;AAAA,KAChD;AAEJ;AAOO,SAAS,aAAa,EAAE,MAAM,GAAsB;AACzD,SACE,qBAAC,UAAK,WAAU,wBAGd;AAAA,yBAAC,SAAI,WAAU,uBAAsB,SAAQ,aAAY,OAAM,MAAK,QAAO,MAAK,eAAY,QAC1F;AAAA,0BAAC,UAAK,OAAM,MAAK,QAAO,MAAK,IAAG,OAAM,MAAK,gBAAe,SAAQ,QAAO;AAAA,MACzE,oBAAC,UAAK,GAAE,oHAAmH,MAAK,gBAAe;AAAA,OACjJ;AAAA,IACC;AAAA,KACH;AAEJ;AASA,SAAS,YAAY,QAAyB;AAC5C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,YAAU,MAAM;AACd,QAAI,SAAS;AACb,UAAM,MAAM,OAAO,SAAS,YAAY;AACxC,UAAM,OAAO,QAAQ,MAAM,MAAM,IAAI,QAAQ,OAAO,EAAE;AACtD,SAAK,OAAO,0BAA0B,EACnC,KAAK,OAAO,EAAE,gBAAgB,gCAAgC,MAAM;AACnE,YAAM,IAAI,MAAM,eAAe,QAAQ,IAAI;AAC3C,UAAI,UAAU,EAAG,aAAY,gCAAgC,CAAC,CAAC;AAAA,IACjE,CAAC,EACA,MAAM,MAAM,MAAS;AACxB,WAAO,MAAM;AACX,eAAS;AAAA,IACX;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AACX,SAAO;AACT;AAUO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAUG;AACD,QAAM,EAAE,MAAM,QAAQ,IAAI,QAAQ;AAClC,QAAM,EAAE,SAAS,IAAI,UAAU;AAE/B,QAAM,OAAO,CAAC,OAAoB,OAAe,UAC/C;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAW,qBAAqB,WAAW,QAAQ,eAAe,EAAE,GAAG,UAAU,cAAc,WAAW,uBAAuB,EAAE;AAAA,MACnI,gBAAc,WAAW;AAAA,MACzB,OAAO;AAAA,MACP,SAAS,MAAM;AACb,YAAI,UAAU,OAAQ,cAAa,KAAK;AACxC,iBAAS,KAAK;AAAA,MAChB;AAAA,MAEC;AAAA;AAAA,EACH;AAGF,SACE,iCACG;AAAA,SAAK,QAAQ,aAAa,+CAA+C;AAAA,IACzE,KAAK,YAAY,YAAY,WAAW,sDAAsD,iEAAiE;AAAA,IAChK,oBAAC,UAAK,WAAU,sBAAqB,eAAY,QAAO;AAAA,IAEvD,CAAC,QACA,oBAAC,OAAE,MAAK,WAAU,WAAU,sBAAqB,OAAM,4CAA2C,qBAElG;AAAA,IAEF,oBAAC,YAAO,MAAK,UAAS,WAAU,sBAAqB,SAAS,MAAM,KAAK,QAAQ,GAAG,uBAEpF;AAAA,KACF;AAEJ;","names":[]}
@@ -18,7 +18,7 @@ function useNews(opts = {}) {
18
18
  const live = opts.live ?? editor;
19
19
  const cap = Math.min(opts.max ?? 50, 50);
20
20
  const zichtbaar = (a) => a.published && (!a.date || String(a.date).slice(0, 10) <= (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
21
- const seed = serverNews ? (onlyPublished ? serverNews.filter(zichtbaar) : serverNews).slice(0, cap) : [];
21
+ const seed = serverNews ? (onlyPublished ? serverNews.filter((a) => a.published) : serverNews).slice(0, cap) : [];
22
22
  const [articles, setArticles] = useState(seed);
23
23
  const [loading, setLoading] = useState(!serverNews);
24
24
  useEffect(() => {
@@ -88,23 +88,31 @@ function NewsList({ basePath = "/nieuws", className, emptyLabel = "Nog geen nieu
88
88
  if (articles.length === 0) return /* @__PURE__ */ jsx("p", { children: emptyLabel });
89
89
  const all = typeof limit === "number" ? articles.slice(0, limit) : articles;
90
90
  const shown = paged ? all.slice(0, visible) : all;
91
- return /* @__PURE__ */ jsxs("div", { className, style: { display: "grid", gap: "1rem" }, children: [
92
- shown.map((a, i) => /* @__PURE__ */ jsxs("article", { className: "xicms-card", style: { padding: "1.25rem" }, children: [
93
- a.coverUrl && // Eerste cover boven de vouw = vaak de LCP → priority (geen lazy-load-vertraging).
94
- /* @__PURE__ */ jsx(Img, { src: a.coverUrl, alt: "", width: 1200, height: 700, sizes: "(max-width: 720px) 100vw, 33vw", priority: i === 0, style: { width: "100%", height: "auto", borderRadius: 8, marginBottom: "0.75rem" } }),
95
- /* @__PURE__ */ jsx("p", { style: { fontSize: 12, opacity: 0.7, margin: 0 }, children: a.date }),
96
- /* @__PURE__ */ jsxs("h2", { style: { margin: "0.25rem 0", fontSize: "1.35rem" }, children: [
97
- /* @__PURE__ */ jsx("a", { href: `${basePath}/${a.slug ?? a.id}`, children: a.title }),
98
- !a.published && /* @__PURE__ */ jsx("span", { style: { marginLeft: 8, fontSize: 12, color: "#b45309" }, children: "(concept)" })
99
- ] }),
100
- a.excerpt && /* @__PURE__ */ jsx("p", { style: { margin: 0, opacity: 0.85 }, children: a.excerpt })
101
- ] }, a.id)),
102
- paged && shown.length < all.length && /* @__PURE__ */ jsx("div", { style: { textAlign: "center" }, children: /* @__PURE__ */ jsxs("button", { type: "button", className: "xicms-btn", onClick: () => setVisible((v) => v + pageSize), children: [
103
- "Meer laden (",
104
- all.length - shown.length,
105
- ")"
106
- ] }) })
107
- ] });
91
+ return (
92
+ /* 8.12: een raster van gelijke kaarten. Tot nu toe stond elk bericht onder het vorige met zijn
93
+ cover op volle breedte en natuurlijke hoogte — één staande foto was een heel scherm. Nu een
94
+ vaste beeldverhouding, de titel in de tekstkleur (niet het blauw van een kale link) en de
95
+ hele kaart klikbaar. */
96
+ /* @__PURE__ */ jsxs("div", { className: ["xicms-nieuwslijst", className].filter(Boolean).join(" "), children: [
97
+ shown.map((a, i) => /* @__PURE__ */ jsxs("article", { className: "xicms-card xicms-nieuwskaart", children: [
98
+ a.coverUrl && // Eerste cover boven de vouw = vaak de LCP → priority (geen lazy-load-vertraging).
99
+ /* @__PURE__ */ jsx(Img, { src: a.coverUrl, alt: "", width: 1200, height: 750, sizes: "(max-width: 720px) 100vw, 33vw", priority: i === 0, className: "xicms-nieuwskaart__beeld" }),
100
+ /* @__PURE__ */ jsxs("div", { className: "xicms-nieuwskaart__tekst", children: [
101
+ /* @__PURE__ */ jsx("p", { className: "xicms-nieuwskaart__datum", children: a.date }),
102
+ /* @__PURE__ */ jsxs("h2", { className: "xicms-nieuwskaart__titel", children: [
103
+ /* @__PURE__ */ jsx("a", { href: `${basePath}/${a.slug ?? a.id}`, children: a.title }),
104
+ !a.published && /* @__PURE__ */ jsx("span", { className: "xicms-nieuwskaart__concept", children: "(concept)" })
105
+ ] }),
106
+ a.excerpt && /* @__PURE__ */ jsx("p", { className: "xicms-nieuwskaart__samenvatting", children: a.excerpt })
107
+ ] })
108
+ ] }, a.id)),
109
+ paged && shown.length < all.length && /* @__PURE__ */ jsx("div", { style: { textAlign: "center" }, children: /* @__PURE__ */ jsxs("button", { type: "button", className: "xicms-btn", onClick: () => setVisible((v) => v + pageSize), children: [
110
+ "Meer laden (",
111
+ all.length - shown.length,
112
+ ")"
113
+ ] }) })
114
+ ] })
115
+ );
108
116
  }
109
117
  function articleBlogPostingJsonLd(article, opts = {}) {
110
118
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/News.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useSiteConfig } from \"../config\";\nimport DOMPurify from \"dompurify\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { canEdit } from \"../auth/roles\";\nimport { useBlockData } from \"../blocks/context\";\nimport { isDemoActive, getDemoNews } from \"../demo/demoStore\";\nimport { Img } from \"../media/ImageContext\";\nimport type { NewsArticle } from \"../content/types\";\nimport { jsonLdHtml } from \"../blocks/builtins/jsonld\";\n\n/**\n * Lijst van nieuwsartikelen voor de huidige site.\n * - `live: false` (standaard publiek): één keer ophalen met getDocs → geen per-bezoeker listener\n * (scheelt Firestore-kosten op druk bezochte sites).\n * - `live: true` (admin): realtime via onSnapshot, zodat wijzigingen meteen zichtbaar zijn.\n */\nexport function useNews(opts: { onlyPublished?: boolean; live?: boolean; max?: number } = {}): {\n articles: NewsArticle[];\n loading: boolean;\n} {\n const { siteId } = useSiteConfig();\n // Via useAuth (niet via het deprecated ContentProvider-pad — News hoort niet aan legacy te hangen).\n const { role } = useAuth();\n const editor = canEdit(role);\n // Server-gefetchte nieuwsdata (content-bundle 5.7): als de publieke route deze via\n // <BlockDataProvider data={{ news }}> meegeeft, renderen we daaruit (SSR, geen browser-Firestore).\n // Afwezig (admin/preview/dev, of geen bundle) → val terug op het Firestore-pad hieronder.\n const serverNews = useBlockData<NewsArticle[]>(\"news\");\n const onlyPublished = opts.onlyPublished ?? !editor;\n const live = opts.live ?? editor; // editors zien live; publiek één keer\n // Begrens de query: zonder cap zou een nieuws-rijke site ALLE artikelen downloaden bij elk bezoek.\n const cap = Math.min(opts.max ?? 50, 50);\n // Bundle-data staat al bij de EERSTE render klaar (server + hydration, identiek): de nieuwsinhoud\n // zit zo in de SSR-HTML (SEO) i.p.v. pas na een client-effect (geen \"laden…\"-flits).\n // 7.24: gepubliceerd én niet in de toekomst gedateerd (ingepland artikel). Editors zien alles.\n const zichtbaar = (a: NewsArticle) => a.published && (!a.date || String(a.date).slice(0, 10) <= new Date().toISOString().slice(0, 10));\n const seed = serverNews ? (onlyPublished ? serverNews.filter(zichtbaar) : serverNews).slice(0, cap) : [];\n const [articles, setArticles] = useState<NewsArticle[]>(seed);\n const [loading, setLoading] = useState(!serverNews);\n\n useEffect(() => {\n // Bundle-data aanwezig? Render daaruit — geen listener, geen getDocs.\n if (serverNews) {\n const all = onlyPublished ? serverNews.filter(zichtbaar) : serverNews;\n setArticles(all.slice(0, cap));\n setLoading(false);\n return;\n }\n // Demo-modus: nieuws komt uit de lokale seed (geen Firebase), zodat nieuws-blokken/widgets\n // in de demo ook gevuld zijn.\n if (isDemoActive()) {\n const all = getDemoNews(siteId);\n setArticles((onlyPublished ? all.filter(zichtbaar) : all).slice(0, cap));\n setLoading(false);\n return;\n }\n // Firestore DYNAMISCH. Met bundle-data (hierboven) komen we hier niet eens; zonder bundle\n // laadt de SDK pas wanneer er echt nieuws opgehaald moet worden. Statisch importeren zette hem\n // in de bundel van élke publieke pagina, ook die zonder nieuws.\n let afgebroken = false;\n let unsub: (() => void) | undefined;\n void (async () => {\n const [{ getDb }, fs] = await Promise.all([\n import(\"../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || afgebroken) {\n setLoading(false);\n return;\n }\n const base = fs.collection(db, \"sites\", siteId, \"news\");\n const q = onlyPublished\n ? fs.query(base, fs.where(\"published\", \"==\", true), fs.orderBy(\"date\", \"desc\"), fs.limit(cap))\n : fs.query(base, fs.orderBy(\"date\", \"desc\"), fs.limit(cap));\n const map = (snap: { docs: { id: string; data: () => unknown }[] }) => {\n const rijen = snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<NewsArticle, \"id\">) }));\n setArticles(onlyPublished ? rijen.filter(zichtbaar) : rijen);\n };\n\n if (!live) {\n try {\n const snap = await fs.getDocs(q);\n if (!afgebroken) {\n map(snap);\n setLoading(false);\n }\n } catch {\n if (!afgebroken) setLoading(false);\n }\n return;\n }\n unsub = fs.onSnapshot(\n q,\n (snap) => {\n map(snap);\n setLoading(false);\n },\n () => setLoading(false),\n );\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [siteId, onlyPublished, live, cap, serverNews]);\n\n return { articles, loading };\n}\n\ninterface NewsListProps {\n /** Basispad voor links naar een artikel, bv. \"/nieuws\". */\n basePath?: string;\n className?: string;\n emptyLabel?: string;\n /** Maximaal aantal artikelen om te tonen. */\n limit?: number;\n /** Toon eerst dit aantal en laad de rest met een \"Meer laden\"-knop (0/leeg = alles ineens). */\n pageSize?: number;\n}\n\n/** Standaardweergave van nieuwsartikelen als kaartenlijst, optioneel gepagineerd. */\nexport function NewsList({ basePath = \"/nieuws\", className, emptyLabel = \"Nog geen nieuws.\", limit, pageSize }: NewsListProps) {\n const { articles, loading } = useNews({ max: limit });\n const paged = typeof pageSize === \"number\" && pageSize > 0;\n const [visible, setVisible] = useState(paged ? pageSize : Infinity);\n\n if (loading) return <p>Nieuws laden…</p>;\n if (articles.length === 0) return <p>{emptyLabel}</p>;\n\n const all = typeof limit === \"number\" ? articles.slice(0, limit) : articles;\n const shown = paged ? all.slice(0, visible) : all;\n\n return (\n <div className={className} style={{ display: \"grid\", gap: \"1rem\" }}>\n {shown.map((a, i) => (\n <article key={a.id} className=\"xicms-card\" style={{ padding: \"1.25rem\" }}>\n {a.coverUrl && (\n // Eerste cover boven de vouw = vaak de LCP → priority (geen lazy-load-vertraging).\n <Img src={a.coverUrl} alt=\"\" width={1200} height={700} sizes=\"(max-width: 720px) 100vw, 33vw\" priority={i === 0} style={{ width: \"100%\", height: \"auto\", borderRadius: 8, marginBottom: \"0.75rem\" }} />\n )}\n <p style={{ fontSize: 12, opacity: 0.7, margin: 0 }}>{a.date}</p>\n {/* h2: de lijst staat direct onder de pagina-h1 — h3 zou een kopniveau overslaan. */}\n <h2 style={{ margin: \"0.25rem 0\", fontSize: \"1.35rem\" }}>\n <a href={`${basePath}/${a.slug ?? a.id}`}>{a.title}</a>\n {!a.published && <span style={{ marginLeft: 8, fontSize: 12, color: \"#b45309\" }}>(concept)</span>}\n </h2>\n {a.excerpt && <p style={{ margin: 0, opacity: 0.85 }}>{a.excerpt}</p>}\n </article>\n ))}\n {paged && shown.length < all.length && (\n <div style={{ textAlign: \"center\" }}>\n <button type=\"button\" className=\"xicms-btn\" onClick={() => setVisible((v) => v + (pageSize as number))}>\n Meer laden ({all.length - shown.length})\n </button>\n </div>\n )}\n </div>\n );\n}\n\n/** BlogPosting-JSON-LD voor een artikel — puur, zodat het testbaar is. */\nexport function articleBlogPostingJsonLd(article: NewsArticle, opts: { siteName?: string; url?: string } = {}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BlogPosting\",\n headline: article.title,\n datePublished: article.date,\n ...(article.excerpt ? { description: article.excerpt } : {}),\n ...(article.coverUrl ? { image: article.coverUrl } : {}),\n ...(opts.url ? { url: opts.url, mainEntityOfPage: opts.url } : {}),\n ...(opts.siteName ? { publisher: { \"@type\": \"Organization\", name: opts.siteName } } : {}),\n };\n}\n\n/** Detailweergave van één artikel, met BlogPosting-JSON-LD voor zoekmachines. */\nexport function NewsArticleView({ article, jsonLd = true }: { article: NewsArticle; jsonLd?: boolean }) {\n const { name } = useSiteConfig();\n // De render is de veiligheidsgrens: rich-text gaat door DOMPurify. Die heeft echter een echte DOM\n // nodig, dus op de server is hij geen optie — waardoor de opmaak vroeger pas ná hydratie verscheen.\n // Een crawler zag dan alleen kale tekst, en de lezer een layoutverschuiving.\n //\n // Komt het artikel uit de content-bundle (server-side meegegeven via BlockDataProvider), dan is\n // het al server-side gesaneerd in `getBundleNews` en mogen we het direct renderen — óók in de\n // SSR-HTML. Alleen client-gefetchte artikelen (admin/preview/dev, of geen bundle) wachten nog op\n // DOMPurify; die kunnen immers ongesaneerd uit Firestore komen.\n const serverNews = useBlockData<NewsArticle[]>(\"news\");\n const trusted = Boolean(\n article.bodyHtml && serverNews?.some((a) => a.slug === article.slug && a.bodyHtml === article.bodyHtml),\n );\n const [html, setHtml] = useState<string | null>(trusted ? (article.bodyHtml ?? null) : null);\n useEffect(() => {\n // Ook voor vertrouwde HTML: opnieuw saneren is idempotent en houdt de client-grens hard.\n setHtml(article.bodyHtml ? DOMPurify.sanitize(article.bodyHtml) : null);\n }, [article.bodyHtml]);\n // Fallback tot de sanitizer klaar is: kale tekst (tags gestript, gerenderd als tekstnodes — veilig).\n const fallbackText = article.body ?? (article.bodyHtml ? article.bodyHtml.replace(/<[^>]*>/g, \" \") : \"\");\n return (\n <article>\n <p style={{ fontSize: 12, opacity: 0.6 }}>{article.date}</p>\n <h1>{article.title}</h1>\n {article.coverUrl && (\n <Img src={article.coverUrl} alt=\"\" width={1200} height={700} sizes=\"(max-width: 900px) 100vw, 800px\" priority style={{ width: \"100%\", height: \"auto\", borderRadius: 12, margin: \"1rem 0\" }} />\n )}\n {html ? (\n // eslint-disable-next-line react/no-danger\n <div className=\"xicms-richtext\" dangerouslySetInnerHTML={{ __html: html }} />\n ) : (\n fallbackText.split(/\\n{2,}/).map((p, i) => (\n <p key={i} style={{ whiteSpace: \"pre-wrap\" }}>\n {p}\n </p>\n ))\n )}\n {jsonLd && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: jsonLdHtml(articleBlogPostingJsonLd(article, { siteName: name })) }}\n />\n )}\n </article>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/News.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useSiteConfig } from \"../config\";\nimport DOMPurify from \"dompurify\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { canEdit } from \"../auth/roles\";\nimport { useBlockData } from \"../blocks/context\";\nimport { isDemoActive, getDemoNews } from \"../demo/demoStore\";\nimport { Img } from \"../media/ImageContext\";\nimport type { NewsArticle } from \"../content/types\";\nimport { jsonLdHtml } from \"../blocks/builtins/jsonld\";\n\n/**\n * Lijst van nieuwsartikelen voor de huidige site.\n * - `live: false` (standaard publiek): één keer ophalen met getDocs → geen per-bezoeker listener\n * (scheelt Firestore-kosten op druk bezochte sites).\n * - `live: true` (admin): realtime via onSnapshot, zodat wijzigingen meteen zichtbaar zijn.\n */\nexport function useNews(opts: { onlyPublished?: boolean; live?: boolean; max?: number } = {}): {\n articles: NewsArticle[];\n loading: boolean;\n} {\n const { siteId } = useSiteConfig();\n // Via useAuth (niet via het deprecated ContentProvider-pad — News hoort niet aan legacy te hangen).\n const { role } = useAuth();\n const editor = canEdit(role);\n // Server-gefetchte nieuwsdata (content-bundle 5.7): als de publieke route deze via\n // <BlockDataProvider data={{ news }}> meegeeft, renderen we daaruit (SSR, geen browser-Firestore).\n // Afwezig (admin/preview/dev, of geen bundle) → val terug op het Firestore-pad hieronder.\n const serverNews = useBlockData<NewsArticle[]>(\"news\");\n const onlyPublished = opts.onlyPublished ?? !editor;\n const live = opts.live ?? editor; // editors zien live; publiek één keer\n // Begrens de query: zonder cap zou een nieuws-rijke site ALLE artikelen downloaden bij elk bezoek.\n const cap = Math.min(opts.max ?? 50, 50);\n // Bundle-data staat al bij de EERSTE render klaar (server + hydration, identiek): de nieuwsinhoud\n // zit zo in de SSR-HTML (SEO) i.p.v. pas na een client-effect (geen \"laden…\"-flits).\n // 7.24: gepubliceerd én niet in de toekomst gedateerd (ingepland artikel). Editors zien alles.\n const zichtbaar = (a: NewsArticle) => a.published && (!a.date || String(a.date).slice(0, 10) <= new Date().toISOString().slice(0, 10));\n // Seed zonder datumfilter (8.1): de server-data is al gefilterd op het moment van ophalen, en\n // `new Date()` hier gaf een andere uitkomst op de server (gecachete HTML) dan bij de hydratie.\n // Het effect hieronder past het datumfilter direct daarna toe.\n const seed = serverNews ? (onlyPublished ? serverNews.filter((a) => a.published) : serverNews).slice(0, cap) : [];\n const [articles, setArticles] = useState<NewsArticle[]>(seed);\n const [loading, setLoading] = useState(!serverNews);\n\n useEffect(() => {\n // Bundle-data aanwezig? Render daaruit — geen listener, geen getDocs.\n if (serverNews) {\n const all = onlyPublished ? serverNews.filter(zichtbaar) : serverNews;\n setArticles(all.slice(0, cap));\n setLoading(false);\n return;\n }\n // Demo-modus: nieuws komt uit de lokale seed (geen Firebase), zodat nieuws-blokken/widgets\n // in de demo ook gevuld zijn.\n if (isDemoActive()) {\n const all = getDemoNews(siteId);\n setArticles((onlyPublished ? all.filter(zichtbaar) : all).slice(0, cap));\n setLoading(false);\n return;\n }\n // Firestore DYNAMISCH. Met bundle-data (hierboven) komen we hier niet eens; zonder bundle\n // laadt de SDK pas wanneer er echt nieuws opgehaald moet worden. Statisch importeren zette hem\n // in de bundel van élke publieke pagina, ook die zonder nieuws.\n let afgebroken = false;\n let unsub: (() => void) | undefined;\n void (async () => {\n const [{ getDb }, fs] = await Promise.all([\n import(\"../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || afgebroken) {\n setLoading(false);\n return;\n }\n const base = fs.collection(db, \"sites\", siteId, \"news\");\n const q = onlyPublished\n ? fs.query(base, fs.where(\"published\", \"==\", true), fs.orderBy(\"date\", \"desc\"), fs.limit(cap))\n : fs.query(base, fs.orderBy(\"date\", \"desc\"), fs.limit(cap));\n const map = (snap: { docs: { id: string; data: () => unknown }[] }) => {\n const rijen = snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<NewsArticle, \"id\">) }));\n setArticles(onlyPublished ? rijen.filter(zichtbaar) : rijen);\n };\n\n if (!live) {\n try {\n const snap = await fs.getDocs(q);\n if (!afgebroken) {\n map(snap);\n setLoading(false);\n }\n } catch {\n if (!afgebroken) setLoading(false);\n }\n return;\n }\n unsub = fs.onSnapshot(\n q,\n (snap) => {\n map(snap);\n setLoading(false);\n },\n () => setLoading(false),\n );\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [siteId, onlyPublished, live, cap, serverNews]);\n\n return { articles, loading };\n}\n\ninterface NewsListProps {\n /** Basispad voor links naar een artikel, bv. \"/nieuws\". */\n basePath?: string;\n className?: string;\n emptyLabel?: string;\n /** Maximaal aantal artikelen om te tonen. */\n limit?: number;\n /** Toon eerst dit aantal en laad de rest met een \"Meer laden\"-knop (0/leeg = alles ineens). */\n pageSize?: number;\n}\n\n/** Standaardweergave van nieuwsartikelen als kaartenlijst, optioneel gepagineerd. */\nexport function NewsList({ basePath = \"/nieuws\", className, emptyLabel = \"Nog geen nieuws.\", limit, pageSize }: NewsListProps) {\n const { articles, loading } = useNews({ max: limit });\n const paged = typeof pageSize === \"number\" && pageSize > 0;\n const [visible, setVisible] = useState(paged ? pageSize : Infinity);\n\n if (loading) return <p>Nieuws laden…</p>;\n if (articles.length === 0) return <p>{emptyLabel}</p>;\n\n const all = typeof limit === \"number\" ? articles.slice(0, limit) : articles;\n const shown = paged ? all.slice(0, visible) : all;\n\n return (\n /* 8.12: een raster van gelijke kaarten. Tot nu toe stond elk bericht onder het vorige met zijn\n cover op volle breedte en natuurlijke hoogte — één staande foto was een heel scherm. Nu een\n vaste beeldverhouding, de titel in de tekstkleur (niet het blauw van een kale link) en de\n hele kaart klikbaar. */\n <div className={[\"xicms-nieuwslijst\", className].filter(Boolean).join(\" \")}>\n {shown.map((a, i) => (\n <article key={a.id} className=\"xicms-card xicms-nieuwskaart\">\n {a.coverUrl && (\n // Eerste cover boven de vouw = vaak de LCP → priority (geen lazy-load-vertraging).\n <Img src={a.coverUrl} alt=\"\" width={1200} height={750} sizes=\"(max-width: 720px) 100vw, 33vw\" priority={i === 0} className=\"xicms-nieuwskaart__beeld\" />\n )}\n <div className=\"xicms-nieuwskaart__tekst\">\n <p className=\"xicms-nieuwskaart__datum\">{a.date}</p>\n {/* h2: de lijst staat direct onder de pagina-h1 — h3 zou een kopniveau overslaan. */}\n <h2 className=\"xicms-nieuwskaart__titel\">\n <a href={`${basePath}/${a.slug ?? a.id}`}>{a.title}</a>\n {!a.published && <span className=\"xicms-nieuwskaart__concept\">(concept)</span>}\n </h2>\n {a.excerpt && <p className=\"xicms-nieuwskaart__samenvatting\">{a.excerpt}</p>}\n </div>\n </article>\n ))}\n {paged && shown.length < all.length && (\n <div style={{ textAlign: \"center\" }}>\n <button type=\"button\" className=\"xicms-btn\" onClick={() => setVisible((v) => v + (pageSize as number))}>\n Meer laden ({all.length - shown.length})\n </button>\n </div>\n )}\n </div>\n );\n}\n\n/** BlogPosting-JSON-LD voor een artikel — puur, zodat het testbaar is. */\nexport function articleBlogPostingJsonLd(article: NewsArticle, opts: { siteName?: string; url?: string } = {}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BlogPosting\",\n headline: article.title,\n datePublished: article.date,\n ...(article.excerpt ? { description: article.excerpt } : {}),\n ...(article.coverUrl ? { image: article.coverUrl } : {}),\n ...(opts.url ? { url: opts.url, mainEntityOfPage: opts.url } : {}),\n ...(opts.siteName ? { publisher: { \"@type\": \"Organization\", name: opts.siteName } } : {}),\n };\n}\n\n/** Detailweergave van één artikel, met BlogPosting-JSON-LD voor zoekmachines. */\nexport function NewsArticleView({ article, jsonLd = true }: { article: NewsArticle; jsonLd?: boolean }) {\n const { name } = useSiteConfig();\n // De render is de veiligheidsgrens: rich-text gaat door DOMPurify. Die heeft echter een echte DOM\n // nodig, dus op de server is hij geen optie — waardoor de opmaak vroeger pas ná hydratie verscheen.\n // Een crawler zag dan alleen kale tekst, en de lezer een layoutverschuiving.\n //\n // Komt het artikel uit de content-bundle (server-side meegegeven via BlockDataProvider), dan is\n // het al server-side gesaneerd in `getBundleNews` en mogen we het direct renderen — óók in de\n // SSR-HTML. Alleen client-gefetchte artikelen (admin/preview/dev, of geen bundle) wachten nog op\n // DOMPurify; die kunnen immers ongesaneerd uit Firestore komen.\n const serverNews = useBlockData<NewsArticle[]>(\"news\");\n const trusted = Boolean(\n article.bodyHtml && serverNews?.some((a) => a.slug === article.slug && a.bodyHtml === article.bodyHtml),\n );\n const [html, setHtml] = useState<string | null>(trusted ? (article.bodyHtml ?? null) : null);\n useEffect(() => {\n // Ook voor vertrouwde HTML: opnieuw saneren is idempotent en houdt de client-grens hard.\n setHtml(article.bodyHtml ? DOMPurify.sanitize(article.bodyHtml) : null);\n }, [article.bodyHtml]);\n // Fallback tot de sanitizer klaar is: kale tekst (tags gestript, gerenderd als tekstnodes — veilig).\n const fallbackText = article.body ?? (article.bodyHtml ? article.bodyHtml.replace(/<[^>]*>/g, \" \") : \"\");\n return (\n <article>\n <p style={{ fontSize: 12, opacity: 0.6 }}>{article.date}</p>\n <h1>{article.title}</h1>\n {article.coverUrl && (\n <Img src={article.coverUrl} alt=\"\" width={1200} height={700} sizes=\"(max-width: 900px) 100vw, 800px\" priority style={{ width: \"100%\", height: \"auto\", borderRadius: 12, margin: \"1rem 0\" }} />\n )}\n {html ? (\n // eslint-disable-next-line react/no-danger\n <div className=\"xicms-richtext\" dangerouslySetInnerHTML={{ __html: html }} />\n ) : (\n fallbackText.split(/\\n{2,}/).map((p, i) => (\n <p key={i} style={{ whiteSpace: \"pre-wrap\" }}>\n {p}\n </p>\n ))\n )}\n {jsonLd && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: jsonLdHtml(articleBlogPostingJsonLd(article, { siteName: name })) }}\n />\n )}\n </article>\n );\n}\n"],"mappings":"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","names":[]}
@@ -10,4 +10,10 @@ export declare function PageView({ path }: {
10
10
  * Telt een gebeurtenis (7.24): "formulier" verstuurd, "uitgaand" geklikt, enz. Zelfde teller,
11
11
  * zelfde privacy-regels; fire-and-forget en nooit een fout naar de bezoeker.
12
12
  */
13
- export declare function trackEvent(siteId: string, event: "formulier" | "bestelling" | "uitgaand" | "telefoon" | "mail" | "whatsapp", path?: string): void;
13
+ export declare function trackEvent(siteId: string, event: "formulier" | "bestelling" | "uitgaand" | "telefoon" | "mail" | "whatsapp" | "niet-gevonden", path?: string): void;
14
+ /**
15
+ * 404-monitor (8.4): zet dit op de niet-gevonden-pagina van de site. Meldt het pad één keer per
16
+ * sessie; in /beheer → Statistieken verschijnen de vaakst gemiste adressen met een knop om er een
17
+ * doorverwijzing van te maken. Geen cookies, alleen het pad.
18
+ */
19
+ export declare function NietGevondenMelder(): null;
@@ -5,6 +5,7 @@ function PageView({ path }) {
5
5
  const { siteId } = useSiteConfig();
6
6
  useEffect(() => {
7
7
  if (typeof window === "undefined") return;
8
+ if (navigator.webdriver) return;
8
9
  const key = `xicms_view:${siteId}:${path}`;
9
10
  try {
10
11
  if (sessionStorage.getItem(key)) return;
@@ -45,7 +46,23 @@ function trackEvent(siteId, event, path) {
45
46
  })().catch(() => {
46
47
  });
47
48
  }
49
+ function NietGevondenMelder() {
50
+ const { siteId } = useSiteConfig();
51
+ useEffect(() => {
52
+ if (typeof window === "undefined" || navigator.webdriver) return;
53
+ const pad = window.location.pathname;
54
+ const key = `xicms_404:${siteId}:${pad}`;
55
+ try {
56
+ if (sessionStorage.getItem(key)) return;
57
+ sessionStorage.setItem(key, "1");
58
+ } catch {
59
+ }
60
+ trackEvent(siteId, "niet-gevonden", pad);
61
+ }, [siteId]);
62
+ return null;
63
+ }
48
64
  export {
65
+ NietGevondenMelder,
49
66
  PageView,
50
67
  trackEvent
51
68
  };