@xiwebdesign/xicms-core 4.17.1 → 5.0.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 (169) hide show
  1. package/dist/XiCMSProvider.d.ts +7 -0
  2. package/dist/XiCMSProvider.js +32 -7
  3. package/dist/XiCMSProvider.js.map +1 -1
  4. package/dist/admin/blockEditors/GenericEditor.d.ts +22 -0
  5. package/dist/admin/blockEditors/GenericEditor.js +201 -0
  6. package/dist/admin/blockEditors/GenericEditor.js.map +1 -0
  7. package/dist/admin/blockEditors/SymbolRefEditor.d.ts +2 -0
  8. package/dist/admin/blockEditors/SymbolRefEditor.js +55 -0
  9. package/dist/admin/blockEditors/SymbolRefEditor.js.map +1 -0
  10. package/dist/admin/blockEditors/index.d.ts +11 -6
  11. package/dist/admin/blockEditors/index.js +49 -620
  12. package/dist/admin/blockEditors/index.js.map +1 -1
  13. package/dist/admin/customBlocks/CustomBlocksPanel.d.ts +3 -0
  14. package/dist/admin/customBlocks/CustomBlocksPanel.js +145 -0
  15. package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -0
  16. package/dist/admin/lib/compressImage.d.ts +4 -2
  17. package/dist/admin/lib/compressImage.js +7 -5
  18. package/dist/admin/lib/compressImage.js.map +1 -1
  19. package/dist/admin/lib/mediaUsage.d.ts +4 -3
  20. package/dist/admin/lib/mediaUsage.js +21 -10
  21. package/dist/admin/lib/mediaUsage.js.map +1 -1
  22. package/dist/admin/lib/symbolsClient.d.ts +13 -0
  23. package/dist/admin/lib/symbolsClient.js +51 -0
  24. package/dist/admin/lib/symbolsClient.js.map +1 -0
  25. package/dist/admin/lib/upload.d.ts +4 -0
  26. package/dist/admin/lib/upload.js +26 -6
  27. package/dist/admin/lib/upload.js.map +1 -1
  28. package/dist/admin/lib/zodFields.d.ts +18 -0
  29. package/dist/admin/lib/zodFields.js +65 -2
  30. package/dist/admin/lib/zodFields.js.map +1 -1
  31. package/dist/admin/media/MediaManager.js +23 -7
  32. package/dist/admin/media/MediaManager.js.map +1 -1
  33. package/dist/admin/media/MediaPicker.js +12 -2
  34. package/dist/admin/media/MediaPicker.js.map +1 -1
  35. package/dist/admin/panelRegistry.js +3 -0
  36. package/dist/admin/panelRegistry.js.map +1 -1
  37. package/dist/admin/settings/SettingsPanel.js +6 -1
  38. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  39. package/dist/admin/settings/SnapshotsCard.d.ts +3 -0
  40. package/dist/admin/settings/SnapshotsCard.js +98 -0
  41. package/dist/admin/settings/SnapshotsCard.js.map +1 -0
  42. package/dist/admin/visual/TranslationPanel.d.ts +13 -0
  43. package/dist/admin/visual/TranslationPanel.js +73 -0
  44. package/dist/admin/visual/TranslationPanel.js.map +1 -0
  45. package/dist/admin/visual/VisualEditor.js +146 -18
  46. package/dist/admin/visual/VisualEditor.js.map +1 -1
  47. package/dist/admin.css +1 -1
  48. package/dist/agentFiles/catalog.d.ts +21 -0
  49. package/dist/agentFiles/catalog.js +78 -0
  50. package/dist/agentFiles/catalog.js.map +1 -0
  51. package/dist/agentFiles/files.d.ts +59 -0
  52. package/dist/agentFiles/files.js +109 -0
  53. package/dist/agentFiles/files.js.map +1 -0
  54. package/dist/blocks/builtins/Agenda.d.ts +6 -0
  55. package/dist/blocks/builtins/Agenda.js +14 -6
  56. package/dist/blocks/builtins/Agenda.js.map +1 -1
  57. package/dist/blocks/builtins/BeforeAfter.js +1 -0
  58. package/dist/blocks/builtins/BeforeAfter.js.map +1 -1
  59. package/dist/blocks/builtins/Breadcrumbs.js +5 -1
  60. package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
  61. package/dist/blocks/builtins/Cards.js +4 -2
  62. package/dist/blocks/builtins/Cards.js.map +1 -1
  63. package/dist/blocks/builtins/Contact.js +2 -1
  64. package/dist/blocks/builtins/Contact.js.map +1 -1
  65. package/dist/blocks/builtins/Cta.js +1 -0
  66. package/dist/blocks/builtins/Cta.js.map +1 -1
  67. package/dist/blocks/builtins/Elements.js +1 -0
  68. package/dist/blocks/builtins/Elements.js.map +1 -1
  69. package/dist/blocks/builtins/Faq.js +5 -4
  70. package/dist/blocks/builtins/Faq.js.map +1 -1
  71. package/dist/blocks/builtins/Gallery.js +2 -2
  72. package/dist/blocks/builtins/Gallery.js.map +1 -1
  73. package/dist/blocks/builtins/Hero.js +2 -2
  74. package/dist/blocks/builtins/Hero.js.map +1 -1
  75. package/dist/blocks/builtins/Jobs.d.ts +8 -1
  76. package/dist/blocks/builtins/Jobs.js +38 -21
  77. package/dist/blocks/builtins/Jobs.js.map +1 -1
  78. package/dist/blocks/builtins/MapBlock.js +31 -4
  79. package/dist/blocks/builtins/MapBlock.js.map +1 -1
  80. package/dist/blocks/builtins/PdfBlock.js +2 -3
  81. package/dist/blocks/builtins/PdfBlock.js.map +1 -1
  82. package/dist/blocks/builtins/Portfolio.js +2 -2
  83. package/dist/blocks/builtins/Portfolio.js.map +1 -1
  84. package/dist/blocks/builtins/Pricing.d.ts +2 -0
  85. package/dist/blocks/builtins/Pricing.js +18 -10
  86. package/dist/blocks/builtins/Pricing.js.map +1 -1
  87. package/dist/blocks/builtins/Sections.js +4 -2
  88. package/dist/blocks/builtins/Sections.js.map +1 -1
  89. package/dist/blocks/builtins/Slider.js +21 -3
  90. package/dist/blocks/builtins/Slider.js.map +1 -1
  91. package/dist/blocks/builtins/SymbolRef.d.ts +2 -0
  92. package/dist/blocks/builtins/SymbolRef.js +61 -0
  93. package/dist/blocks/builtins/SymbolRef.js.map +1 -0
  94. package/dist/blocks/builtins/Testimonials.js +21 -18
  95. package/dist/blocks/builtins/Testimonials.js.map +1 -1
  96. package/dist/blocks/builtins/WhatsappChat.js +1 -1
  97. package/dist/blocks/builtins/WhatsappChat.js.map +1 -1
  98. package/dist/blocks/builtins/Widgets.js +2 -3
  99. package/dist/blocks/builtins/Widgets.js.map +1 -1
  100. package/dist/blocks/builtins/lib/ics.js +1 -1
  101. package/dist/blocks/builtins/lib/ics.js.map +1 -1
  102. package/dist/blocks/builtins/lib/openingHours.d.ts +4 -1
  103. package/dist/blocks/builtins/lib/openingHours.js +11 -5
  104. package/dist/blocks/builtins/lib/openingHours.js.map +1 -1
  105. package/dist/blocks/customTemplate/definition.d.ts +7 -0
  106. package/dist/blocks/customTemplate/definition.js +34 -0
  107. package/dist/blocks/customTemplate/definition.js.map +1 -0
  108. package/dist/blocks/customTemplate/interpret.d.ts +10 -0
  109. package/dist/blocks/customTemplate/interpret.js +136 -0
  110. package/dist/blocks/customTemplate/interpret.js.map +1 -0
  111. package/dist/blocks/registry.d.ts +8 -0
  112. package/dist/blocks/registry.js.map +1 -1
  113. package/dist/demo/demoStore.d.ts +11 -1
  114. package/dist/demo/demoStore.js +50 -0
  115. package/dist/demo/demoStore.js.map +1 -1
  116. package/dist/firebase/client.js +2 -1
  117. package/dist/firebase/client.js.map +1 -1
  118. package/dist/i18n/overlay.d.ts +61 -0
  119. package/dist/i18n/overlay.js +90 -0
  120. package/dist/i18n/overlay.js.map +1 -0
  121. package/dist/index.d.ts +11 -9
  122. package/dist/index.js +34 -9
  123. package/dist/index.js.map +1 -1
  124. package/dist/modules/builtins.js +3 -2
  125. package/dist/modules/builtins.js.map +1 -1
  126. package/dist/modules/consent/useConsent.js +15 -3
  127. package/dist/modules/consent/useConsent.js.map +1 -1
  128. package/dist/schema/blocks.d.ts +183 -3
  129. package/dist/schema/blocks.js +12 -1
  130. package/dist/schema/blocks.js.map +1 -1
  131. package/dist/schema/customBlocks.d.ts +47 -0
  132. package/dist/schema/customBlocks.js +32 -0
  133. package/dist/schema/customBlocks.js.map +1 -0
  134. package/dist/schema/fields.d.ts +122 -0
  135. package/dist/schema/fields.js +120 -0
  136. package/dist/schema/fields.js.map +1 -0
  137. package/dist/schema/forms.d.ts +6 -6
  138. package/dist/schema/pages.d.ts +15 -1
  139. package/dist/schema/pages.js +6 -2
  140. package/dist/schema/pages.js.map +1 -1
  141. package/dist/schema/siteConfig.d.ts +14 -8
  142. package/dist/schema/siteConfig.js +2 -1
  143. package/dist/schema/siteConfig.js.map +1 -1
  144. package/dist/schema/symbols.d.ts +205 -0
  145. package/dist/schema/symbols.js +18 -0
  146. package/dist/schema/symbols.js.map +1 -0
  147. package/dist/server/index.d.ts +2 -1
  148. package/dist/server/index.js +24 -0
  149. package/dist/server/index.js.map +1 -1
  150. package/dist/server/pages.d.ts +21 -0
  151. package/dist/server/pages.js +77 -0
  152. package/dist/server/pages.js.map +1 -1
  153. package/dist/server/seo.d.ts +12 -3
  154. package/dist/server/seo.js +23 -11
  155. package/dist/server/seo.js.map +1 -1
  156. package/dist/styles.css +62 -6
  157. package/package.json +1 -1
  158. package/dist/components/EditableImage.d.ts +0 -18
  159. package/dist/components/EditableImage.js +0 -62
  160. package/dist/components/EditableImage.js.map +0 -1
  161. package/dist/components/EditableRichText.d.ts +0 -14
  162. package/dist/components/EditableRichText.js +0 -26
  163. package/dist/components/EditableRichText.js.map +0 -1
  164. package/dist/components/EditableText.d.ts +0 -21
  165. package/dist/components/EditableText.js +0 -25
  166. package/dist/components/EditableText.js.map +0 -1
  167. package/dist/content/ContentProvider.d.ts +0 -23
  168. package/dist/content/ContentProvider.js +0 -86
  169. package/dist/content/ContentProvider.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/blocks/builtins/Widgets.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport {\n NewsWidgetSchema,\n AgendaWidgetSchema,\n OpeningHoursWidgetSchema,\n ContactWidgetSchema,\n SocialWidgetSchema,\n NewsletterWidgetSchema,\n CtaWidgetSchema,\n GalleryWidgetSchema,\n TestimonialsWidgetSchema,\n CardsWidgetSchema,\n DownloadsWidgetSchema,\n CountdownWidgetSchema,\n} from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { useNews } from \"../../components/News\";\nimport { selectAgendaEvents, formatEventDate } from \"./Agenda\";\nimport { useCountdown, CountdownDigits } from \"./Countdown\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { Button } from \"@astryxdesign/core/Button\";\n\n/**\n * Widgets = compacte zijbalk-varianten van de modules. Elke widget hergebruikt de gegevens en\n * logica van de volwaardige versie (zelfde nieuws-collectie, zelfde agenda-selectie, zelfde\n * countdown-hook) en rendert die alleen compacter. Plaats ze in het \"Met zijbalk\"-blok of in een\n * smalle kolom naast je hoofdinhoud.\n */\n\n/* ── Laatste nieuws ────────────────────────────────────────────────────────── */\ntype NewsW = z.infer<typeof NewsWidgetSchema>;\nfunction NewsWidgetRender({ block }: BlockRenderProps<NewsW>) {\n const { articles, loading } = useNews({ max: block.limit });\n const shown = articles.slice(0, block.limit);\n return (\n <div className=\"xicms-widget xicms-widget--news\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Laatste nieuws\"}</h3>\n {loading ? (\n <p className=\"xicms-widget__empty\">Nieuws laden…</p>\n ) : shown.length === 0 ? (\n <p className=\"xicms-widget__empty\">Nog geen nieuws.</p>\n ) : (\n <ul className=\"xicms-widget__list\">\n {shown.map((a) => (\n <li key={a.id} className=\"xicms-widget__item\">\n {block.showImages && a.coverUrl && (\n // eslint-disable-next-line @next/next/no-img-element\n <img className=\"xicms-widget__thumb\" src={a.coverUrl} alt=\"\" loading=\"lazy\" />\n )}\n <div>\n <a className=\"xicms-widget__link\" href={`${block.basePath}/${a.slug}`}>{a.title}</a>\n <span className=\"xicms-widget__meta\">{a.date}</span>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\nexport const newsWidget = defineBlock({\n type: \"newsWidget\",\n schema: NewsWidgetSchema,\n Render: NewsWidgetRender,\n label: \"Laatste nieuws\",\n icon: \"🗞️\",\n category: \"widget\",\n defaults: () => ({ heading: \"Laatste nieuws\", limit: 5, basePath: \"/nieuws\", showImages: false }),\n});\n\n/* ── Komende agenda ────────────────────────────────────────────────────────── */\ntype AgendaW = z.infer<typeof AgendaWidgetSchema>;\nfunction AgendaWidgetRender({ block }: BlockRenderProps<AgendaW>) {\n const events = selectAgendaEvents(block.events, { hidePast: block.hidePast, limit: block.limit });\n return (\n <div className=\"xicms-widget xicms-widget--agenda\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Komende evenementen\"}</h3>\n {events.length === 0 ? (\n <p className=\"xicms-widget__empty\">Geen geplande evenementen.</p>\n ) : (\n <ul className=\"xicms-widget__list\">\n {events.map((e, i) => (\n <li key={i} className=\"xicms-widget__item xicms-widget__item--event\">\n <time className=\"xicms-widget__meta\" dateTime={e.start}>{formatEventDate(e.start)}</time>\n <span className=\"xicms-widget__link\">{e.url ? <a href={e.url}>{e.title}</a> : e.title}</span>\n {e.location && <span className=\"xicms-widget__sub\">{e.location}</span>}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\nexport const agendaWidget = defineBlock({\n type: \"agendaWidget\",\n schema: AgendaWidgetSchema,\n Render: AgendaWidgetRender,\n label: \"Komende agenda\",\n icon: \"📅\",\n category: \"widget\",\n defaults: () => ({ heading: \"Komende evenementen\", limit: 5, events: [], hidePast: true }),\n});\n\n/* ── Openingstijden (vandaag gemarkeerd) ───────────────────────────────────── */\nconst DAY_KEYWORDS = [\"zondag\", \"maandag\", \"dinsdag\", \"woensdag\", \"donderdag\", \"vrijdag\", \"zaterdag\"];\ntype OpeningW = z.infer<typeof OpeningHoursWidgetSchema>;\nfunction OpeningHoursWidgetRender({ block }: BlockRenderProps<OpeningW>) {\n // \"Vandaag\" client-side bepalen (voorkomt hydration-mismatch t.o.v. de servertijd).\n const [todayIdx, setTodayIdx] = useState<number | null>(null);\n useEffect(() => setTodayIdx(new Date().getDay()), []);\n const rows = block.rows ?? [];\n const todayWord = todayIdx === null ? \"\" : DAY_KEYWORDS[todayIdx];\n return (\n <div className=\"xicms-widget xicms-widget--hours\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Openingstijden\"}</h3>\n <table className=\"xicms-widget__hours\">\n <tbody>\n {rows.map((r, i) => {\n const isToday =\n block.highlightToday && !!todayWord && r.day.toLowerCase().includes(todayWord);\n return (\n <tr key={i} className={isToday ? \"xicms-widget__hours-today\" : undefined}>\n <th scope=\"row\">{r.day}</th>\n <td>{r.hours}</td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n );\n}\nexport const openingHoursWidget = defineBlock({\n type: \"openingHoursWidget\",\n schema: OpeningHoursWidgetSchema,\n Render: OpeningHoursWidgetRender,\n label: \"Openingstijden (compact)\",\n icon: \"🕒\",\n category: \"widget\",\n defaults: () => ({\n heading: \"Openingstijden\",\n highlightToday: true,\n rows: [\n { day: \"Maandag t/m vrijdag\", hours: \"09:00 – 17:00\" },\n { day: \"Zaterdag\", hours: \"10:00 – 16:00\" },\n { day: \"Zondag\", hours: \"Gesloten\" },\n ],\n }),\n});\n\n/* ── Contact (compact) ─────────────────────────────────────────────────────── */\ntype ContactW = z.infer<typeof ContactWidgetSchema>;\nfunction ContactWidgetRender({ block }: BlockRenderProps<ContactW>) {\n if (!block.address && !block.phone && !block.email) return null;\n return (\n <div className=\"xicms-widget xicms-widget--contact\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Contact\"}</h3>\n <ul className=\"xicms-widget__list\">\n {block.address && <li className=\"xicms-widget__item\">{block.address}</li>}\n {block.phone && (\n <li className=\"xicms-widget__item\"><a href={`tel:${block.phone.replace(/\\s+/g, \"\")}`}>{block.phone}</a></li>\n )}\n {block.email && (\n <li className=\"xicms-widget__item\"><a href={`mailto:${block.email}`}>{block.email}</a></li>\n )}\n </ul>\n </div>\n );\n}\nexport const contactWidget = defineBlock({\n type: \"contactWidget\",\n schema: ContactWidgetSchema,\n Render: ContactWidgetRender,\n label: \"Contact (compact)\",\n icon: \"📇\",\n category: \"widget\",\n defaults: () => ({ heading: \"Contact\" }),\n});\n\n/* ── Social (compact) ──────────────────────────────────────────────────────── */\ntype SocialW = z.infer<typeof SocialWidgetSchema>;\nfunction SocialWidgetRender({ block }: BlockRenderProps<SocialW>) {\n const items = (block.items ?? []).filter((i) => i.href && i.platform);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--social\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <ul className=\"xicms-widget__social\">\n {items.map((it, i) => (\n <li key={i}><a href={it.href} rel=\"noopener noreferrer\" target=\"_blank\">{it.platform}</a></li>\n ))}\n </ul>\n </div>\n );\n}\nexport const socialWidget = defineBlock({\n type: \"socialWidget\",\n schema: SocialWidgetSchema,\n Render: SocialWidgetRender,\n label: \"Social (compact)\",\n icon: \"🔗\",\n category: \"widget\",\n defaults: () => ({ heading: \"Volg ons\", items: [{ platform: \"Instagram\", href: \"\" }] }),\n});\n\n/* ── Nieuwsbrief (compact) ─────────────────────────────────────────────────── */\ntype NewsletterW = z.infer<typeof NewsletterWidgetSchema>;\nfunction NewsletterWidgetRender({ block }: BlockRenderProps<NewsletterW>) {\n const [email, setEmail] = useState(\"\");\n return (\n <div className=\"xicms-widget xicms-widget--newsletter\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Nieuwsbrief\"}</h3>\n {block.text && <p className=\"xicms-widget__sub\">{block.text}</p>}\n {block.actionUrl ? (\n <form className=\"xicms-widget__form\" action={block.actionUrl} method=\"post\" target=\"_blank\" style={{ display: \"grid\", gap: \"0.6rem\" }}>\n <TextInput label=\"E-mailadres\" isLabelHidden type=\"email\" htmlName=\"EMAIL\" isRequired placeholder=\"je@email.nl\" value={email} onChange={setEmail} />\n <Button label={block.buttonLabel} variant=\"primary\" type=\"submit\" />\n </form>\n ) : (\n <p className=\"xicms-widget__empty\">Stel een aanmeld-URL in.</p>\n )}\n </div>\n );\n}\nexport const newsletterWidget = defineBlock({\n type: \"newsletterWidget\",\n schema: NewsletterWidgetSchema,\n Render: NewsletterWidgetRender,\n label: \"Nieuwsbrief (compact)\",\n icon: \"✉️\",\n category: \"widget\",\n defaults: () => ({ heading: \"Blijf op de hoogte\", buttonLabel: \"Aanmelden\" }),\n});\n\n/* ── CTA (compact) ─────────────────────────────────────────────────────────── */\ntype CtaW = z.infer<typeof CtaWidgetSchema>;\nfunction CtaWidgetRender({ block }: BlockRenderProps<CtaW>) {\n if (!block.title && !block.text) return null;\n return (\n <div className=\"xicms-widget xicms-widget--cta\" style={block.backgroundColor ? { background: block.backgroundColor } : undefined}>\n {block.title && <h3 className=\"xicms-widget__title\">{block.title}</h3>}\n {block.text && <p className=\"xicms-widget__sub\">{block.text}</p>}\n {block.buttonLabel && (\n <a className=\"xicms-btn xicms-btn--primary\" href={block.buttonHref}>{block.buttonLabel}</a>\n )}\n </div>\n );\n}\nexport const ctaWidget = defineBlock({\n type: \"ctaWidget\",\n schema: CtaWidgetSchema,\n Render: CtaWidgetRender,\n label: \"Call-to-action (compact)\",\n icon: \"👉\",\n category: \"widget\",\n defaults: () => ({ title: \"Vragen?\", text: \"Neem gerust contact op.\", buttonLabel: \"Contact\", buttonHref: \"#contact\" }),\n});\n\n/* ── Galerij (compacte strip) ──────────────────────────────────────────────── */\ntype GalleryW = z.infer<typeof GalleryWidgetSchema>;\nfunction GalleryWidgetRender({ block }: BlockRenderProps<GalleryW>) {\n const items = (block.items ?? []).filter((i) => i.url);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--gallery\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <div className=\"xicms-widget__grid\" data-cols={block.columns}>\n {items.map((it, i) => (\n // eslint-disable-next-line @next/next/no-img-element\n <img key={i} className=\"xicms-widget__photo\" src={it.url} alt={it.alt ?? \"\"} loading=\"lazy\" />\n ))}\n </div>\n </div>\n );\n}\nexport const galleryWidget = defineBlock({\n type: \"galleryWidget\",\n schema: GalleryWidgetSchema,\n Render: GalleryWidgetRender,\n label: \"Fotostrip (compact)\",\n icon: \"🖼️\",\n category: \"widget\",\n defaults: () => ({ heading: \"Foto's\", columns: 3, items: [] }),\n});\n\n/* ── Review (compact) ──────────────────────────────────────────────────────── */\ntype TestimonialsW = z.infer<typeof TestimonialsWidgetSchema>;\nfunction TestimonialsWidgetRender({ block }: BlockRenderProps<TestimonialsW>) {\n const items = (block.items ?? []).filter((t) => t.quote);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--testimonial\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <ul className=\"xicms-widget__list\">\n {items.map((t, i) => (\n <li key={i} className=\"xicms-widget__quote\">\n {typeof t.rating === \"number\" && (\n <span className=\"xicms-widget__stars\" aria-label={`${t.rating} van 5`}>{\"★\".repeat(t.rating)}{\"☆\".repeat(5 - t.rating)}</span>\n )}\n <p>“{t.quote}”</p>\n {t.author && <span className=\"xicms-widget__meta\">— {t.author}</span>}\n </li>\n ))}\n </ul>\n </div>\n );\n}\nexport const testimonialsWidget = defineBlock({\n type: \"testimonialsWidget\",\n schema: TestimonialsWidgetSchema,\n Render: TestimonialsWidgetRender,\n label: \"Review (compact)\",\n icon: \"⭐\",\n category: \"widget\",\n defaults: () => ({ heading: \"Wat klanten zeggen\", items: [{ quote: \"Echt een aanrader!\", author: \"Tevreden klant\", rating: 5 }] }),\n});\n\n/* ── Diensten (compact) ────────────────────────────────────────────────────── */\ntype CardsW = z.infer<typeof CardsWidgetSchema>;\nfunction CardsWidgetRender({ block }: BlockRenderProps<CardsW>) {\n const items = (block.items ?? []).filter((c) => c.title);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--cards\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <ul className=\"xicms-widget__list\">\n {items.map((c, i) => {\n const inner = (\n <>\n {c.icon && <span className=\"xicms-widget__cardicon\" aria-hidden=\"true\">{c.icon}</span>}\n <span>\n <span className=\"xicms-widget__link\">{c.title}</span>\n {c.text && <span className=\"xicms-widget__sub\">{c.text}</span>}\n </span>\n </>\n );\n return (\n <li key={i} className=\"xicms-widget__item xicms-widget__item--card\">\n {c.href ? <a href={c.href} className=\"xicms-widget__cardlink\">{inner}</a> : inner}\n </li>\n );\n })}\n </ul>\n </div>\n );\n}\nexport const cardsWidget = defineBlock({\n type: \"cardsWidget\",\n schema: CardsWidgetSchema,\n Render: CardsWidgetRender,\n label: \"Diensten (compact)\",\n icon: \"🧩\",\n category: \"widget\",\n defaults: () => ({ heading: \"Uitgelicht\", items: [{ icon: \"⭐\", title: \"Dienst\", text: \"Korte omschrijving.\" }] }),\n});\n\n/* ── Downloads (compact) ───────────────────────────────────────────────────── */\ntype DownloadsW = z.infer<typeof DownloadsWidgetSchema>;\nfunction DownloadsWidgetRender({ block }: BlockRenderProps<DownloadsW>) {\n const items = (block.items ?? []).filter((d) => d.label && d.href).slice(0, block.limit);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--downloads\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Downloads\"}</h3>\n <ul className=\"xicms-widget__list\">\n {items.map((d, i) => (\n <li key={i} className=\"xicms-widget__item\">\n <a className=\"xicms-widget__link\" href={d.href} download>\n <span aria-hidden=\"true\">📄</span> {d.label}\n </a>\n {d.meta && <span className=\"xicms-widget__meta\">{d.meta}</span>}\n </li>\n ))}\n </ul>\n </div>\n );\n}\nexport const downloadsWidget = defineBlock({\n type: \"downloadsWidget\",\n schema: DownloadsWidgetSchema,\n Render: DownloadsWidgetRender,\n label: \"Downloads (compact)\",\n icon: \"📥\",\n category: \"widget\",\n defaults: () => ({ heading: \"Downloads\", limit: 5, items: [] }),\n});\n\n/* ── Aftelteller (compact) ─────────────────────────────────────────────────── */\ntype CountdownW = z.infer<typeof CountdownWidgetSchema>;\nfunction CountdownWidgetRender({ block }: BlockRenderProps<CountdownW>) {\n const r = useCountdown(block.target);\n if (!block.target) return null;\n return (\n <div className=\"xicms-widget xicms-widget--countdown\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n {r === null ? (\n <p className=\"xicms-widget__empty\" aria-hidden=\"true\">…</p>\n ) : r.done ? (\n <p className=\"xicms-countdown__expired\">{block.expiredText}</p>\n ) : (\n <CountdownDigits r={r} showLabels />\n )}\n </div>\n );\n}\nexport const countdownWidget = defineBlock({\n type: \"countdownWidget\",\n schema: CountdownWidgetSchema,\n Render: CountdownWidgetRender,\n label: \"Aftelteller (compact)\",\n icon: \"⏳\",\n category: \"widget\",\n defaults: () => ({ heading: \"Aftellen\", target: \"\", expiredText: \"Het is zover!\" }),\n});\n\n// Let op: exporteer GEEN aggregaat-array vanuit dit \"use client\"-bestand. Een array die hier\n// wordt opgebouwd is niet server-importeerbaar (de individuele block-exports wél). De volledige\n// lijst staat in blocks/builtins/index.ts.\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/blocks/builtins/Widgets.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport {\n NewsWidgetSchema,\n AgendaWidgetSchema,\n OpeningHoursWidgetSchema,\n ContactWidgetSchema,\n SocialWidgetSchema,\n NewsletterWidgetSchema,\n CtaWidgetSchema,\n GalleryWidgetSchema,\n TestimonialsWidgetSchema,\n CardsWidgetSchema,\n DownloadsWidgetSchema,\n CountdownWidgetSchema,\n} from \"../../schema/blocks\";\nimport { defineBlock, type BlockRenderProps } from \"../registry\";\nimport { useNews } from \"../../components/News\";\nimport { selectAgendaEvents, formatEventDate } from \"./Agenda\";\nimport { parseDays } from \"./lib/openingHours\";\nimport { useCountdown, CountdownDigits } from \"./Countdown\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { Button } from \"@astryxdesign/core/Button\";\n\n/**\n * Widgets = compacte zijbalk-varianten van de modules. Elke widget hergebruikt de gegevens en\n * logica van de volwaardige versie (zelfde nieuws-collectie, zelfde agenda-selectie, zelfde\n * countdown-hook) en rendert die alleen compacter. Plaats ze in het \"Met zijbalk\"-blok of in een\n * smalle kolom naast je hoofdinhoud.\n */\n\n/* ── Laatste nieuws ────────────────────────────────────────────────────────── */\ntype NewsW = z.infer<typeof NewsWidgetSchema>;\nfunction NewsWidgetRender({ block }: BlockRenderProps<NewsW>) {\n const { articles, loading } = useNews({ max: block.limit });\n const shown = articles.slice(0, block.limit);\n return (\n <div className=\"xicms-widget xicms-widget--news\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Laatste nieuws\"}</h3>\n {loading ? (\n <p className=\"xicms-widget__empty\">Nieuws laden…</p>\n ) : shown.length === 0 ? (\n <p className=\"xicms-widget__empty\">Nog geen nieuws.</p>\n ) : (\n <ul className=\"xicms-widget__list\">\n {shown.map((a) => (\n <li key={a.id} className=\"xicms-widget__item\">\n {block.showImages && a.coverUrl && (\n // eslint-disable-next-line @next/next/no-img-element\n <img className=\"xicms-widget__thumb\" src={a.coverUrl} alt=\"\" loading=\"lazy\" />\n )}\n <div>\n <a className=\"xicms-widget__link\" href={`${block.basePath}/${a.slug}`}>{a.title}</a>\n <span className=\"xicms-widget__meta\">{a.date}</span>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\nexport const newsWidget = defineBlock({\n type: \"newsWidget\",\n schema: NewsWidgetSchema,\n Render: NewsWidgetRender,\n label: \"Laatste nieuws\",\n icon: \"🗞️\",\n category: \"widget\",\n defaults: () => ({ heading: \"Laatste nieuws\", limit: 5, basePath: \"/nieuws\", showImages: false }),\n});\n\n/* ── Komende agenda ────────────────────────────────────────────────────────── */\ntype AgendaW = z.infer<typeof AgendaWidgetSchema>;\nfunction AgendaWidgetRender({ block }: BlockRenderProps<AgendaW>) {\n const events = selectAgendaEvents(block.events, { hidePast: block.hidePast, limit: block.limit });\n return (\n <div className=\"xicms-widget xicms-widget--agenda\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Komende evenementen\"}</h3>\n {events.length === 0 ? (\n <p className=\"xicms-widget__empty\">Geen geplande evenementen.</p>\n ) : (\n <ul className=\"xicms-widget__list\">\n {events.map((e, i) => (\n <li key={i} className=\"xicms-widget__item xicms-widget__item--event\">\n <time className=\"xicms-widget__meta\" dateTime={e.start}>{formatEventDate(e.start)}</time>\n <span className=\"xicms-widget__link\">{e.url ? <a href={e.url}>{e.title}</a> : e.title}</span>\n {e.location && <span className=\"xicms-widget__sub\">{e.location}</span>}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\nexport const agendaWidget = defineBlock({\n type: \"agendaWidget\",\n schema: AgendaWidgetSchema,\n Render: AgendaWidgetRender,\n label: \"Komende agenda\",\n icon: \"📅\",\n category: \"widget\",\n defaults: () => ({ heading: \"Komende evenementen\", limit: 5, events: [], hidePast: true }),\n});\n\n/* ── Openingstijden (vandaag gemarkeerd) ───────────────────────────────────── */\ntype OpeningW = z.infer<typeof OpeningHoursWidgetSchema>;\nfunction OpeningHoursWidgetRender({ block }: BlockRenderProps<OpeningW>) {\n // \"Vandaag\" client-side bepalen (voorkomt hydration-mismatch t.o.v. de servertijd).\n const [todayIdx, setTodayIdx] = useState<number | null>(null);\n useEffect(() => setTodayIdx(new Date().getDay()), []);\n const rows = block.rows ?? [];\n return (\n <div className=\"xicms-widget xicms-widget--hours\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Openingstijden\"}</h3>\n <table className=\"xicms-widget__hours\">\n <tbody>\n {rows.map((r, i) => {\n // parseDays begrijpt ook bereiken (\"Ma-vr\", \"Maandag t/m vrijdag\") - een woensdag\n // hoort dan net zo goed gemarkeerd als de letterlijk genoemde dagen.\n const isToday =\n block.highlightToday && todayIdx !== null && parseDays(r.day).includes(todayIdx);\n return (\n <tr key={i} className={isToday ? \"xicms-widget__hours-today\" : undefined}>\n <th scope=\"row\">{r.day}</th>\n <td>{r.hours}</td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n );\n}\nexport const openingHoursWidget = defineBlock({\n type: \"openingHoursWidget\",\n schema: OpeningHoursWidgetSchema,\n Render: OpeningHoursWidgetRender,\n label: \"Openingstijden (compact)\",\n icon: \"🕒\",\n category: \"widget\",\n defaults: () => ({\n heading: \"Openingstijden\",\n highlightToday: true,\n rows: [\n { day: \"Maandag t/m vrijdag\", hours: \"09:00 – 17:00\" },\n { day: \"Zaterdag\", hours: \"10:00 – 16:00\" },\n { day: \"Zondag\", hours: \"Gesloten\" },\n ],\n }),\n});\n\n/* ── Contact (compact) ─────────────────────────────────────────────────────── */\ntype ContactW = z.infer<typeof ContactWidgetSchema>;\nfunction ContactWidgetRender({ block }: BlockRenderProps<ContactW>) {\n if (!block.address && !block.phone && !block.email) return null;\n return (\n <div className=\"xicms-widget xicms-widget--contact\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Contact\"}</h3>\n <ul className=\"xicms-widget__list\">\n {block.address && <li className=\"xicms-widget__item\">{block.address}</li>}\n {block.phone && (\n <li className=\"xicms-widget__item\"><a href={`tel:${block.phone.replace(/\\s+/g, \"\")}`}>{block.phone}</a></li>\n )}\n {block.email && (\n <li className=\"xicms-widget__item\"><a href={`mailto:${block.email}`}>{block.email}</a></li>\n )}\n </ul>\n </div>\n );\n}\nexport const contactWidget = defineBlock({\n type: \"contactWidget\",\n schema: ContactWidgetSchema,\n Render: ContactWidgetRender,\n label: \"Contact (compact)\",\n icon: \"📇\",\n category: \"widget\",\n defaults: () => ({ heading: \"Contact\" }),\n});\n\n/* ── Social (compact) ──────────────────────────────────────────────────────── */\ntype SocialW = z.infer<typeof SocialWidgetSchema>;\nfunction SocialWidgetRender({ block }: BlockRenderProps<SocialW>) {\n const items = (block.items ?? []).filter((i) => i.href && i.platform);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--social\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <ul className=\"xicms-widget__social\">\n {items.map((it, i) => (\n <li key={i}><a href={it.href} rel=\"noopener noreferrer\" target=\"_blank\">{it.platform}</a></li>\n ))}\n </ul>\n </div>\n );\n}\nexport const socialWidget = defineBlock({\n type: \"socialWidget\",\n schema: SocialWidgetSchema,\n Render: SocialWidgetRender,\n label: \"Social (compact)\",\n icon: \"🔗\",\n category: \"widget\",\n defaults: () => ({ heading: \"Volg ons\", items: [{ platform: \"Instagram\", href: \"\" }] }),\n});\n\n/* ── Nieuwsbrief (compact) ─────────────────────────────────────────────────── */\ntype NewsletterW = z.infer<typeof NewsletterWidgetSchema>;\nfunction NewsletterWidgetRender({ block }: BlockRenderProps<NewsletterW>) {\n const [email, setEmail] = useState(\"\");\n return (\n <div className=\"xicms-widget xicms-widget--newsletter\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Nieuwsbrief\"}</h3>\n {block.text && <p className=\"xicms-widget__sub\">{block.text}</p>}\n {block.actionUrl ? (\n <form className=\"xicms-widget__form\" action={block.actionUrl} method=\"post\" target=\"_blank\" style={{ display: \"grid\", gap: \"0.6rem\" }}>\n <TextInput label=\"E-mailadres\" isLabelHidden type=\"email\" htmlName=\"EMAIL\" isRequired placeholder=\"je@email.nl\" value={email} onChange={setEmail} />\n <Button label={block.buttonLabel} variant=\"primary\" type=\"submit\" />\n </form>\n ) : (\n <p className=\"xicms-widget__empty\">Stel een aanmeld-URL in.</p>\n )}\n </div>\n );\n}\nexport const newsletterWidget = defineBlock({\n type: \"newsletterWidget\",\n schema: NewsletterWidgetSchema,\n Render: NewsletterWidgetRender,\n label: \"Nieuwsbrief (compact)\",\n icon: \"✉️\",\n category: \"widget\",\n defaults: () => ({ heading: \"Blijf op de hoogte\", buttonLabel: \"Aanmelden\" }),\n});\n\n/* ── CTA (compact) ─────────────────────────────────────────────────────────── */\ntype CtaW = z.infer<typeof CtaWidgetSchema>;\nfunction CtaWidgetRender({ block }: BlockRenderProps<CtaW>) {\n if (!block.title && !block.text) return null;\n return (\n <div className=\"xicms-widget xicms-widget--cta\" style={block.backgroundColor ? { background: block.backgroundColor } : undefined}>\n {block.title && <h3 className=\"xicms-widget__title\">{block.title}</h3>}\n {block.text && <p className=\"xicms-widget__sub\">{block.text}</p>}\n {block.buttonLabel && (\n <a className=\"xicms-btn xicms-btn--primary\" href={block.buttonHref}>{block.buttonLabel}</a>\n )}\n </div>\n );\n}\nexport const ctaWidget = defineBlock({\n type: \"ctaWidget\",\n schema: CtaWidgetSchema,\n Render: CtaWidgetRender,\n label: \"Call-to-action (compact)\",\n icon: \"👉\",\n category: \"widget\",\n defaults: () => ({ title: \"Vragen?\", text: \"Neem gerust contact op.\", buttonLabel: \"Contact\", buttonHref: \"#contact\" }),\n});\n\n/* ── Galerij (compacte strip) ──────────────────────────────────────────────── */\ntype GalleryW = z.infer<typeof GalleryWidgetSchema>;\nfunction GalleryWidgetRender({ block }: BlockRenderProps<GalleryW>) {\n const items = (block.items ?? []).filter((i) => i.url);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--gallery\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <div className=\"xicms-widget__grid\" data-cols={block.columns}>\n {items.map((it, i) => (\n // eslint-disable-next-line @next/next/no-img-element\n <img key={i} className=\"xicms-widget__photo\" src={it.url} alt={it.alt ?? \"\"} loading=\"lazy\" />\n ))}\n </div>\n </div>\n );\n}\nexport const galleryWidget = defineBlock({\n type: \"galleryWidget\",\n schema: GalleryWidgetSchema,\n Render: GalleryWidgetRender,\n label: \"Fotostrip (compact)\",\n icon: \"🖼️\",\n category: \"widget\",\n defaults: () => ({ heading: \"Foto's\", columns: 3, items: [] }),\n});\n\n/* ── Review (compact) ──────────────────────────────────────────────────────── */\ntype TestimonialsW = z.infer<typeof TestimonialsWidgetSchema>;\nfunction TestimonialsWidgetRender({ block }: BlockRenderProps<TestimonialsW>) {\n const items = (block.items ?? []).filter((t) => t.quote);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--testimonial\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <ul className=\"xicms-widget__list\">\n {items.map((t, i) => (\n <li key={i} className=\"xicms-widget__quote\">\n {typeof t.rating === \"number\" && (\n <span className=\"xicms-widget__stars\" aria-label={`${t.rating} van 5`}>{\"★\".repeat(t.rating)}{\"☆\".repeat(5 - t.rating)}</span>\n )}\n <p>“{t.quote}”</p>\n {t.author && <span className=\"xicms-widget__meta\">— {t.author}</span>}\n </li>\n ))}\n </ul>\n </div>\n );\n}\nexport const testimonialsWidget = defineBlock({\n type: \"testimonialsWidget\",\n schema: TestimonialsWidgetSchema,\n Render: TestimonialsWidgetRender,\n label: \"Review (compact)\",\n icon: \"⭐\",\n category: \"widget\",\n defaults: () => ({ heading: \"Wat klanten zeggen\", items: [{ quote: \"Echt een aanrader!\", author: \"Tevreden klant\", rating: 5 }] }),\n});\n\n/* ── Diensten (compact) ────────────────────────────────────────────────────── */\ntype CardsW = z.infer<typeof CardsWidgetSchema>;\nfunction CardsWidgetRender({ block }: BlockRenderProps<CardsW>) {\n const items = (block.items ?? []).filter((c) => c.title);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--cards\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n <ul className=\"xicms-widget__list\">\n {items.map((c, i) => {\n const inner = (\n <>\n {c.icon && <span className=\"xicms-widget__cardicon\" aria-hidden=\"true\">{c.icon}</span>}\n <span>\n <span className=\"xicms-widget__link\">{c.title}</span>\n {c.text && <span className=\"xicms-widget__sub\">{c.text}</span>}\n </span>\n </>\n );\n return (\n <li key={i} className=\"xicms-widget__item xicms-widget__item--card\">\n {c.href ? <a href={c.href} className=\"xicms-widget__cardlink\">{inner}</a> : inner}\n </li>\n );\n })}\n </ul>\n </div>\n );\n}\nexport const cardsWidget = defineBlock({\n type: \"cardsWidget\",\n schema: CardsWidgetSchema,\n Render: CardsWidgetRender,\n label: \"Diensten (compact)\",\n icon: \"🧩\",\n category: \"widget\",\n defaults: () => ({ heading: \"Uitgelicht\", items: [{ icon: \"⭐\", title: \"Dienst\", text: \"Korte omschrijving.\" }] }),\n});\n\n/* ── Downloads (compact) ───────────────────────────────────────────────────── */\ntype DownloadsW = z.infer<typeof DownloadsWidgetSchema>;\nfunction DownloadsWidgetRender({ block }: BlockRenderProps<DownloadsW>) {\n const items = (block.items ?? []).filter((d) => d.label && d.href).slice(0, block.limit);\n if (items.length === 0) return null;\n return (\n <div className=\"xicms-widget xicms-widget--downloads\">\n <h3 className=\"xicms-widget__title\">{block.heading ?? \"Downloads\"}</h3>\n <ul className=\"xicms-widget__list\">\n {items.map((d, i) => (\n <li key={i} className=\"xicms-widget__item\">\n <a className=\"xicms-widget__link\" href={d.href} download>\n <span aria-hidden=\"true\">📄</span> {d.label}\n </a>\n {d.meta && <span className=\"xicms-widget__meta\">{d.meta}</span>}\n </li>\n ))}\n </ul>\n </div>\n );\n}\nexport const downloadsWidget = defineBlock({\n type: \"downloadsWidget\",\n schema: DownloadsWidgetSchema,\n Render: DownloadsWidgetRender,\n label: \"Downloads (compact)\",\n icon: \"📥\",\n category: \"widget\",\n defaults: () => ({ heading: \"Downloads\", limit: 5, items: [] }),\n});\n\n/* ── Aftelteller (compact) ─────────────────────────────────────────────────── */\ntype CountdownW = z.infer<typeof CountdownWidgetSchema>;\nfunction CountdownWidgetRender({ block }: BlockRenderProps<CountdownW>) {\n const r = useCountdown(block.target);\n if (!block.target) return null;\n return (\n <div className=\"xicms-widget xicms-widget--countdown\">\n {block.heading && <h3 className=\"xicms-widget__title\">{block.heading}</h3>}\n {r === null ? (\n <p className=\"xicms-widget__empty\" aria-hidden=\"true\">…</p>\n ) : r.done ? (\n <p className=\"xicms-countdown__expired\">{block.expiredText}</p>\n ) : (\n <CountdownDigits r={r} showLabels />\n )}\n </div>\n );\n}\nexport const countdownWidget = defineBlock({\n type: \"countdownWidget\",\n schema: CountdownWidgetSchema,\n Render: CountdownWidgetRender,\n label: \"Aftelteller (compact)\",\n icon: \"⏳\",\n category: \"widget\",\n defaults: () => ({ heading: \"Aftellen\", target: \"\", expiredText: \"Het is zover!\" }),\n});\n\n// Let op: exporteer GEEN aggregaat-array vanuit dit \"use client\"-bestand. Een array die hier\n// wordt opgebouwd is niet server-importeerbaar (de individuele block-exports wél). De volledige\n// lijst staat in blocks/builtins/index.ts.\n"],"mappings":"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","names":[]}
@@ -7,7 +7,7 @@ function hash(s) {
7
7
  return (h >>> 0).toString(36);
8
8
  }
9
9
  function fmt(value, allDay) {
10
- const digits = value.replace(/[-:]/g, "");
10
+ const digits = value.replace(/(z|[+-]\d{2}:?\d{2})$/i, "").replace(/[-:]/g, "");
11
11
  return allDay ? digits.slice(0, 8) : `${digits.slice(0, 8)}T${(digits.slice(9) + "0000").slice(0, 6)}`;
12
12
  }
13
13
  function nextDay(isoDate) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/blocks/builtins/lib/ics.ts"],"sourcesContent":["/**\n * Minimale iCalendar-generator voor agenda-events (\"Zet in agenda\") — puur, geen dependency.\n * Datums zonder tijd worden hele-dag-events; met tijd gebruiken we de lokale notatie (floating),\n * zodat 19:00 bij de bezoeker ook 19:00 blijft.\n */\n\nexport interface IcsEvent {\n title: string;\n /** ISO-datum of datum+tijd, bv. \"2026-07-01\" of \"2026-07-01T19:00\". */\n start: string;\n end?: string;\n location?: string;\n description?: string;\n url?: string;\n}\n\nfunction escapeText(s: string): string {\n return s.replace(/\\\\/g, \"\\\\\\\\\").replace(/;/g, \"\\\\;\").replace(/,/g, \"\\\\,\").replace(/\\r?\\n/g, \"\\\\n\");\n}\n\n/** Deterministische korte hash: zelfde event → zelfde UID (her-import werkt dan als update). */\nfunction hash(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;\n return (h >>> 0).toString(36);\n}\n\nfunction fmt(value: string, allDay: boolean): string {\n const digits = value.replace(/[-:]/g, \"\");\n return allDay ? digits.slice(0, 8) : `${digits.slice(0, 8)}T${(digits.slice(9) + \"0000\").slice(0, 6)}`;\n}\n\n/** Dag ná een ISO-datum (DTEND is exclusief bij hele-dag-events). */\nfunction nextDay(isoDate: string): string {\n const d = new Date(`${isoDate}T12:00:00Z`);\n d.setUTCDate(d.getUTCDate() + 1);\n return d.toISOString().slice(0, 10);\n}\n\nexport function makeIcs(event: IcsEvent): string | null {\n if (!event.title || !event.start || Number.isNaN(new Date(event.start).getTime())) return null;\n const allDay = !event.start.includes(\"T\");\n const dtStart = fmt(event.start, allDay);\n const endSource = event.end && !Number.isNaN(new Date(event.end).getTime()) ? event.end : null;\n const dtEnd = allDay\n ? fmt(nextDay((endSource ?? event.start).slice(0, 10)), true)\n : fmt(endSource ?? event.start, false);\n const lines = [\n \"BEGIN:VCALENDAR\",\n \"VERSION:2.0\",\n \"PRODID:-//XiCMS//Agenda//NL\",\n \"BEGIN:VEVENT\",\n `UID:${dtStart}-${hash(JSON.stringify([event.title, event.start, event.end, event.location]))}@xicms`,\n allDay ? `DTSTART;VALUE=DATE:${dtStart}` : `DTSTART:${dtStart}`,\n allDay ? `DTEND;VALUE=DATE:${dtEnd}` : `DTEND:${dtEnd}`,\n `SUMMARY:${escapeText(event.title)}`,\n ...(event.location ? [`LOCATION:${escapeText(event.location)}`] : []),\n ...(event.description ? [`DESCRIPTION:${escapeText(event.description)}`] : []),\n // URI-waarde: geen tekst-escaping, wél regeleinden strippen (voorkomt property-injectie).\n ...(event.url ? [`URL:${event.url.replace(/[\\r\\n]/g, \"\")}`] : []),\n \"END:VEVENT\",\n \"END:VCALENDAR\",\n ];\n return lines.join(\"\\r\\n\");\n}\n\n/** data:-URL voor een download-link (server-safe, geen Blob nodig). */\nexport function icsDataUrl(event: IcsEvent): string | null {\n const ics = makeIcs(event);\n return ics ? `data:text/calendar;charset=utf-8,${encodeURIComponent(ics)}` : null;\n}\n"],"mappings":"AAgBA,SAAS,WAAW,GAAmB;AACrC,SAAO,EAAE,QAAQ,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAK,EAAE,QAAQ,MAAM,KAAK,EAAE,QAAQ,UAAU,KAAK;AACnG;AAGA,SAAS,KAAK,GAAmB;AAC/B,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAK,KAAK,IAAI,KAAK,EAAE,WAAW,CAAC,IAAK;AACpE,UAAQ,MAAM,GAAG,SAAS,EAAE;AAC9B;AAEA,SAAS,IAAI,OAAe,QAAyB;AACnD,QAAM,SAAS,MAAM,QAAQ,SAAS,EAAE;AACxC,SAAO,SAAS,OAAO,MAAM,GAAG,CAAC,IAAI,GAAG,OAAO,MAAM,GAAG,CAAC,CAAC,KAAK,OAAO,MAAM,CAAC,IAAI,QAAQ,MAAM,GAAG,CAAC,CAAC;AACtG;AAGA,SAAS,QAAQ,SAAyB;AACxC,QAAM,IAAI,oBAAI,KAAK,GAAG,OAAO,YAAY;AACzC,IAAE,WAAW,EAAE,WAAW,IAAI,CAAC;AAC/B,SAAO,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACpC;AAEO,SAAS,QAAQ,OAAgC;AACtD,MAAI,CAAC,MAAM,SAAS,CAAC,MAAM,SAAS,OAAO,MAAM,IAAI,KAAK,MAAM,KAAK,EAAE,QAAQ,CAAC,EAAG,QAAO;AAC1F,QAAM,SAAS,CAAC,MAAM,MAAM,SAAS,GAAG;AACxC,QAAM,UAAU,IAAI,MAAM,OAAO,MAAM;AACvC,QAAM,YAAY,MAAM,OAAO,CAAC,OAAO,MAAM,IAAI,KAAK,MAAM,GAAG,EAAE,QAAQ,CAAC,IAAI,MAAM,MAAM;AAC1F,QAAM,QAAQ,SACV,IAAI,SAAS,aAAa,MAAM,OAAO,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,IAC1D,IAAI,aAAa,MAAM,OAAO,KAAK;AACvC,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,OAAO,IAAI,KAAK,KAAK,UAAU,CAAC,MAAM,OAAO,MAAM,OAAO,MAAM,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC;AAAA,IAC7F,SAAS,sBAAsB,OAAO,KAAK,WAAW,OAAO;AAAA,IAC7D,SAAS,oBAAoB,KAAK,KAAK,SAAS,KAAK;AAAA,IACrD,WAAW,WAAW,MAAM,KAAK,CAAC;AAAA,IAClC,GAAI,MAAM,WAAW,CAAC,YAAY,WAAW,MAAM,QAAQ,CAAC,EAAE,IAAI,CAAC;AAAA,IACnE,GAAI,MAAM,cAAc,CAAC,eAAe,WAAW,MAAM,WAAW,CAAC,EAAE,IAAI,CAAC;AAAA;AAAA,IAE5E,GAAI,MAAM,MAAM,CAAC,OAAO,MAAM,IAAI,QAAQ,WAAW,EAAE,CAAC,EAAE,IAAI,CAAC;AAAA,IAC/D;AAAA,IACA;AAAA,EACF;AACA,SAAO,MAAM,KAAK,MAAM;AAC1B;AAGO,SAAS,WAAW,OAAgC;AACzD,QAAM,MAAM,QAAQ,KAAK;AACzB,SAAO,MAAM,oCAAoC,mBAAmB,GAAG,CAAC,KAAK;AAC/E;","names":[]}
1
+ {"version":3,"sources":["../../../../src/blocks/builtins/lib/ics.ts"],"sourcesContent":["/**\n * Minimale iCalendar-generator voor agenda-events (\"Zet in agenda\") — puur, geen dependency.\n * Datums zonder tijd worden hele-dag-events; met tijd gebruiken we de lokale notatie (floating),\n * zodat 19:00 bij de bezoeker ook 19:00 blijft.\n */\n\nexport interface IcsEvent {\n title: string;\n /** ISO-datum of datum+tijd, bv. \"2026-07-01\" of \"2026-07-01T19:00\". */\n start: string;\n end?: string;\n location?: string;\n description?: string;\n url?: string;\n}\n\nfunction escapeText(s: string): string {\n return s.replace(/\\\\/g, \"\\\\\\\\\").replace(/;/g, \"\\\\;\").replace(/,/g, \"\\\\,\").replace(/\\r?\\n/g, \"\\\\n\");\n}\n\n/** Deterministische korte hash: zelfde event → zelfde UID (her-import werkt dan als update). */\nfunction hash(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;\n return (h >>> 0).toString(36);\n}\n\nfunction fmt(value: string, allDay: boolean): string {\n // Floating time is het contract (19:00 blijft 19:00 bij de bezoeker) - een meegegeven\n // TZ-suffix (\"Z\"/\"+02:00\") zou de digits-reeks corrumperen, dus die strippen we.\n const digits = value.replace(/(z|[+-]\\d{2}:?\\d{2})$/i, \"\").replace(/[-:]/g, \"\");\n return allDay ? digits.slice(0, 8) : `${digits.slice(0, 8)}T${(digits.slice(9) + \"0000\").slice(0, 6)}`;\n}\n\n/** Dag ná een ISO-datum (DTEND is exclusief bij hele-dag-events). */\nfunction nextDay(isoDate: string): string {\n const d = new Date(`${isoDate}T12:00:00Z`);\n d.setUTCDate(d.getUTCDate() + 1);\n return d.toISOString().slice(0, 10);\n}\n\nexport function makeIcs(event: IcsEvent): string | null {\n if (!event.title || !event.start || Number.isNaN(new Date(event.start).getTime())) return null;\n const allDay = !event.start.includes(\"T\");\n const dtStart = fmt(event.start, allDay);\n const endSource = event.end && !Number.isNaN(new Date(event.end).getTime()) ? event.end : null;\n const dtEnd = allDay\n ? fmt(nextDay((endSource ?? event.start).slice(0, 10)), true)\n : fmt(endSource ?? event.start, false);\n const lines = [\n \"BEGIN:VCALENDAR\",\n \"VERSION:2.0\",\n \"PRODID:-//XiCMS//Agenda//NL\",\n \"BEGIN:VEVENT\",\n `UID:${dtStart}-${hash(JSON.stringify([event.title, event.start, event.end, event.location]))}@xicms`,\n allDay ? `DTSTART;VALUE=DATE:${dtStart}` : `DTSTART:${dtStart}`,\n allDay ? `DTEND;VALUE=DATE:${dtEnd}` : `DTEND:${dtEnd}`,\n `SUMMARY:${escapeText(event.title)}`,\n ...(event.location ? [`LOCATION:${escapeText(event.location)}`] : []),\n ...(event.description ? [`DESCRIPTION:${escapeText(event.description)}`] : []),\n // URI-waarde: geen tekst-escaping, wél regeleinden strippen (voorkomt property-injectie).\n ...(event.url ? [`URL:${event.url.replace(/[\\r\\n]/g, \"\")}`] : []),\n \"END:VEVENT\",\n \"END:VCALENDAR\",\n ];\n return lines.join(\"\\r\\n\");\n}\n\n/** data:-URL voor een download-link (server-safe, geen Blob nodig). */\nexport function icsDataUrl(event: IcsEvent): string | null {\n const ics = makeIcs(event);\n return ics ? `data:text/calendar;charset=utf-8,${encodeURIComponent(ics)}` : null;\n}\n"],"mappings":"AAgBA,SAAS,WAAW,GAAmB;AACrC,SAAO,EAAE,QAAQ,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAK,EAAE,QAAQ,MAAM,KAAK,EAAE,QAAQ,UAAU,KAAK;AACnG;AAGA,SAAS,KAAK,GAAmB;AAC/B,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAK,KAAK,IAAI,KAAK,EAAE,WAAW,CAAC,IAAK;AACpE,UAAQ,MAAM,GAAG,SAAS,EAAE;AAC9B;AAEA,SAAS,IAAI,OAAe,QAAyB;AAGnD,QAAM,SAAS,MAAM,QAAQ,0BAA0B,EAAE,EAAE,QAAQ,SAAS,EAAE;AAC9E,SAAO,SAAS,OAAO,MAAM,GAAG,CAAC,IAAI,GAAG,OAAO,MAAM,GAAG,CAAC,CAAC,KAAK,OAAO,MAAM,CAAC,IAAI,QAAQ,MAAM,GAAG,CAAC,CAAC;AACtG;AAGA,SAAS,QAAQ,SAAyB;AACxC,QAAM,IAAI,oBAAI,KAAK,GAAG,OAAO,YAAY;AACzC,IAAE,WAAW,EAAE,WAAW,IAAI,CAAC;AAC/B,SAAO,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACpC;AAEO,SAAS,QAAQ,OAAgC;AACtD,MAAI,CAAC,MAAM,SAAS,CAAC,MAAM,SAAS,OAAO,MAAM,IAAI,KAAK,MAAM,KAAK,EAAE,QAAQ,CAAC,EAAG,QAAO;AAC1F,QAAM,SAAS,CAAC,MAAM,MAAM,SAAS,GAAG;AACxC,QAAM,UAAU,IAAI,MAAM,OAAO,MAAM;AACvC,QAAM,YAAY,MAAM,OAAO,CAAC,OAAO,MAAM,IAAI,KAAK,MAAM,GAAG,EAAE,QAAQ,CAAC,IAAI,MAAM,MAAM;AAC1F,QAAM,QAAQ,SACV,IAAI,SAAS,aAAa,MAAM,OAAO,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,IAC1D,IAAI,aAAa,MAAM,OAAO,KAAK;AACvC,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,OAAO,IAAI,KAAK,KAAK,UAAU,CAAC,MAAM,OAAO,MAAM,OAAO,MAAM,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC;AAAA,IAC7F,SAAS,sBAAsB,OAAO,KAAK,WAAW,OAAO;AAAA,IAC7D,SAAS,oBAAoB,KAAK,KAAK,SAAS,KAAK;AAAA,IACrD,WAAW,WAAW,MAAM,KAAK,CAAC;AAAA,IAClC,GAAI,MAAM,WAAW,CAAC,YAAY,WAAW,MAAM,QAAQ,CAAC,EAAE,IAAI,CAAC;AAAA,IACnE,GAAI,MAAM,cAAc,CAAC,eAAe,WAAW,MAAM,WAAW,CAAC,EAAE,IAAI,CAAC;AAAA;AAAA,IAE5E,GAAI,MAAM,MAAM,CAAC,OAAO,MAAM,IAAI,QAAQ,WAAW,EAAE,CAAC,EAAE,IAAI,CAAC;AAAA,IAC/D;AAAA,IACA;AAAA,EACF;AACA,SAAO,MAAM,KAAK,MAAM;AAC1B;AAGO,SAAS,WAAW,OAAgC;AACzD,QAAM,MAAM,QAAQ,KAAK;AACzB,SAAO,MAAM,oCAAoC,mBAAmB,GAAG,CAAC,KAAK;AAC/E;","names":[]}
@@ -9,7 +9,10 @@ export interface HoursRow {
9
9
  }
10
10
  /** "Maandag t/m vrijdag" / "Ma–Vr" / "Zaterdag" → JS-dagindexen; leeg als onbekend. */
11
11
  export declare function parseDays(label: string): number[];
12
- /** "09:00 – 17:00" / "9.00-17.00" → minuten sinds middernacht; null bij "Gesloten"/onparseerbaar. */
12
+ /**
13
+ * "09:00 – 17:00" / "9.00-17.00" → minuten sinds middernacht; null bij "Gesloten"/onparseerbaar.
14
+ * Sluiten ná middernacht (horeca: "18:00 – 02:00") geeft close > 24u — isOpenNow rekent daarmee.
15
+ */
13
16
  export declare function parseTimeRange(hours: string): {
14
17
  open: number;
15
18
  close: number;
@@ -36,8 +36,12 @@ function parseTimeRange(hours) {
36
36
  const m = hours.replace(/\./g, ":").match(/(\d{1,2}):(\d{2})\s*[-–—]\s*(\d{1,2}):(\d{2})/);
37
37
  if (!m) return null;
38
38
  const open = Number(m[1]) * 60 + Number(m[2]);
39
- const close = Number(m[3]) * 60 + Number(m[4]);
40
- if (open >= close || close > 24 * 60) return null;
39
+ let close = Number(m[3]) * 60 + Number(m[4]);
40
+ if (close > 24 * 60 || open > 24 * 60) return null;
41
+ if (open >= close) {
42
+ if (close > 6 * 60) return null;
43
+ close += 24 * 60;
44
+ }
41
45
  return { open, close };
42
46
  }
43
47
  function isOpenNow(rows, now = /* @__PURE__ */ new Date()) {
@@ -49,8 +53,10 @@ function isOpenNow(rows, now = /* @__PURE__ */ new Date()) {
49
53
  if (days.length === 0) continue;
50
54
  const range = parseTimeRange(r.hours);
51
55
  sawParseable = sawParseable || range !== null || /gesloten/i.test(r.hours);
52
- if (!days.includes(day)) continue;
53
- if (range && minutes >= range.open && minutes < range.close) return true;
56
+ if (range) {
57
+ if (days.includes(day) && minutes >= range.open && minutes < range.close) return true;
58
+ if (range.close > 24 * 60 && days.includes((day + 6) % 7) && minutes < range.close - 24 * 60) return true;
59
+ }
54
60
  }
55
61
  return sawParseable ? false : null;
56
62
  }
@@ -65,7 +71,7 @@ function openingHoursSpecJsonLd(rows) {
65
71
  "@type": "OpeningHoursSpecification",
66
72
  dayOfWeek: days.map((d) => DAY_SCHEMA_NAMES[d]),
67
73
  opens: fmt(range.open),
68
- closes: fmt(range.close)
74
+ closes: fmt(range.close % (24 * 60))
69
75
  });
70
76
  }
71
77
  return out;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/blocks/builtins/lib/openingHours.ts"],"sourcesContent":["/**\n * Pure helpers rond openingstijden-rijen ({ day, hours } vrije tekst, bv.\n * \"Maandag t/m vrijdag\" + \"09:00 – 17:00\"). Best-effort parsen van de gangbare NL-notaties;\n * onparseerbare rijen doen gewoon niet mee (geen badge/JSON-LD is beter dan een foute).\n */\n\nexport interface HoursRow {\n day: string;\n hours: string;\n}\n\n/** Dagnamen → JS-dagindex (0 = zondag). Volledige namen én afkortingen. */\nconst DAYS: Record<string, number> = {\n zondag: 0, zo: 0,\n maandag: 1, ma: 1,\n dinsdag: 2, di: 2,\n woensdag: 3, wo: 3,\n donderdag: 4, do: 4,\n vrijdag: 5, vr: 5,\n zaterdag: 6, za: 6,\n};\n\nconst DAY_SCHEMA_NAMES = [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"];\n\n/** \"Maandag t/m vrijdag\" / \"Ma–Vr\" / \"Zaterdag\" → JS-dagindexen; leeg als onbekend. */\nexport function parseDays(label: string): number[] {\n const norm = label.toLowerCase().replace(/\\./g, \"\");\n // Bereik: \"x t/m y\", \"x tm y\", \"x - y\", \"x – y\"\n const range = norm.match(/([a-z]+)\\s*(?:t\\/m|tm|tot en met|[-–])\\s*([a-z]+)/);\n if (range && DAYS[range[1]] !== undefined && DAYS[range[2]] !== undefined) {\n const from = DAYS[range[1]];\n const to = DAYS[range[2]];\n const out: number[] = [];\n for (let d = from; ; d = (d + 1) % 7) {\n out.push(d);\n if (d === to || out.length > 7) break;\n }\n return out;\n }\n // Opsomming of losse dag(en): \"ma, wo, vr\" / \"zaterdag\"\n const found = norm.match(/[a-z]+/g) ?? [];\n const days = found.map((w) => DAYS[w]).filter((d): d is number => d !== undefined);\n return [...new Set(days)];\n}\n\n/** \"09:00 – 17:00\" / \"9.00-17.00\" → minuten sinds middernacht; null bij \"Gesloten\"/onparseerbaar. */\nexport function parseTimeRange(hours: string): { open: number; close: number } | null {\n const m = hours.replace(/\\./g, \":\").match(/(\\d{1,2}):(\\d{2})\\s*[-–—]\\s*(\\d{1,2}):(\\d{2})/);\n if (!m) return null;\n const open = Number(m[1]) * 60 + Number(m[2]);\n const close = Number(m[3]) * 60 + Number(m[4]);\n if (open >= close || close > 24 * 60) return null;\n return { open, close };\n}\n\n/** Is er volgens de rijen NU open? null = niet te bepalen (niets parseerbaars). */\nexport function isOpenNow(rows: HoursRow[], now: Date = new Date()): boolean | null {\n const day = now.getDay();\n const minutes = now.getHours() * 60 + now.getMinutes();\n let sawParseable = false;\n for (const r of rows) {\n const days = parseDays(r.day);\n if (days.length === 0) continue;\n const range = parseTimeRange(r.hours);\n sawParseable = sawParseable || range !== null || /gesloten/i.test(r.hours);\n if (!days.includes(day)) continue;\n if (range && minutes >= range.open && minutes < range.close) return true;\n }\n return sawParseable ? false : null;\n}\n\n/** schema.org OpeningHoursSpecification-array uit de rijen (alleen parseerbare rijen). */\nexport function openingHoursSpecJsonLd(rows: HoursRow[]): Array<Record<string, unknown>> {\n const fmt = (mins: number) => `${String(Math.floor(mins / 60)).padStart(2, \"0\")}:${String(mins % 60).padStart(2, \"0\")}`;\n const out: Array<Record<string, unknown>> = [];\n for (const r of rows) {\n const days = parseDays(r.day);\n const range = parseTimeRange(r.hours);\n if (days.length === 0 || !range) continue;\n out.push({\n \"@type\": \"OpeningHoursSpecification\",\n dayOfWeek: days.map((d) => DAY_SCHEMA_NAMES[d]),\n opens: fmt(range.open),\n closes: fmt(range.close),\n });\n }\n return out;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/blocks/builtins/lib/openingHours.ts"],"sourcesContent":["/**\n * Pure helpers rond openingstijden-rijen ({ day, hours } vrije tekst, bv.\n * \"Maandag t/m vrijdag\" + \"09:00 – 17:00\"). Best-effort parsen van de gangbare NL-notaties;\n * onparseerbare rijen doen gewoon niet mee (geen badge/JSON-LD is beter dan een foute).\n */\n\nexport interface HoursRow {\n day: string;\n hours: string;\n}\n\n/** Dagnamen → JS-dagindex (0 = zondag). Volledige namen én afkortingen. */\nconst DAYS: Record<string, number> = {\n zondag: 0, zo: 0,\n maandag: 1, ma: 1,\n dinsdag: 2, di: 2,\n woensdag: 3, wo: 3,\n donderdag: 4, do: 4,\n vrijdag: 5, vr: 5,\n zaterdag: 6, za: 6,\n};\n\nconst DAY_SCHEMA_NAMES = [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"];\n\n/** \"Maandag t/m vrijdag\" / \"Ma–Vr\" / \"Zaterdag\" → JS-dagindexen; leeg als onbekend. */\nexport function parseDays(label: string): number[] {\n const norm = label.toLowerCase().replace(/\\./g, \"\");\n // Bereik: \"x t/m y\", \"x tm y\", \"x - y\", \"x – y\"\n const range = norm.match(/([a-z]+)\\s*(?:t\\/m|tm|tot en met|[-–])\\s*([a-z]+)/);\n if (range && DAYS[range[1]] !== undefined && DAYS[range[2]] !== undefined) {\n const from = DAYS[range[1]];\n const to = DAYS[range[2]];\n const out: number[] = [];\n for (let d = from; ; d = (d + 1) % 7) {\n out.push(d);\n if (d === to || out.length > 7) break;\n }\n return out;\n }\n // Opsomming of losse dag(en): \"ma, wo, vr\" / \"zaterdag\"\n const found = norm.match(/[a-z]+/g) ?? [];\n const days = found.map((w) => DAYS[w]).filter((d): d is number => d !== undefined);\n return [...new Set(days)];\n}\n\n/**\n * \"09:00 – 17:00\" / \"9.00-17.00\" → minuten sinds middernacht; null bij \"Gesloten\"/onparseerbaar.\n * Sluiten ná middernacht (horeca: \"18:00 – 02:00\") geeft close > 24u — isOpenNow rekent daarmee.\n */\nexport function parseTimeRange(hours: string): { open: number; close: number } | null {\n const m = hours.replace(/\\./g, \":\").match(/(\\d{1,2}):(\\d{2})\\s*[-–—]\\s*(\\d{1,2}):(\\d{2})/);\n if (!m) return null;\n const open = Number(m[1]) * 60 + Number(m[2]);\n let close = Number(m[3]) * 60 + Number(m[4]);\n if (close > 24 * 60 || open > 24 * 60) return null;\n if (open >= close) {\n // Alleen een sluittijd tot 06:00 telt als nachtzaak (\"18:00 – 02:00\");\n // iets als \"17:00 – 09:00\" is vrijwel zeker een omgedraaide typfout.\n if (close > 6 * 60) return null;\n close += 24 * 60; // over middernacht heen\n }\n return { open, close };\n}\n\n/** Is er volgens de rijen NU open? null = niet te bepalen (niets parseerbaars). */\nexport function isOpenNow(rows: HoursRow[], now: Date = new Date()): boolean | null {\n const day = now.getDay();\n const minutes = now.getHours() * 60 + now.getMinutes();\n let sawParseable = false;\n for (const r of rows) {\n const days = parseDays(r.day);\n if (days.length === 0) continue;\n const range = parseTimeRange(r.hours);\n sawParseable = sawParseable || range !== null || /gesloten/i.test(r.hours);\n if (range) {\n // Vandaag geopend (ook als de sluittijd na middernacht valt)…\n if (days.includes(day) && minutes >= range.open && minutes < range.close) return true;\n // …of gisteren geopend tot ná middernacht (bv. za 18:00–02:00 → zo 01:00 nog open).\n if (range.close > 24 * 60 && days.includes((day + 6) % 7) && minutes < range.close - 24 * 60) return true;\n }\n }\n return sawParseable ? false : null;\n}\n\n/** schema.org OpeningHoursSpecification-array uit de rijen (alleen parseerbare rijen). */\nexport function openingHoursSpecJsonLd(rows: HoursRow[]): Array<Record<string, unknown>> {\n const fmt = (mins: number) => `${String(Math.floor(mins / 60)).padStart(2, \"0\")}:${String(mins % 60).padStart(2, \"0\")}`;\n const out: Array<Record<string, unknown>> = [];\n for (const r of rows) {\n const days = parseDays(r.day);\n const range = parseTimeRange(r.hours);\n if (days.length === 0 || !range) continue;\n out.push({\n \"@type\": \"OpeningHoursSpecification\",\n dayOfWeek: days.map((d) => DAY_SCHEMA_NAMES[d]),\n opens: fmt(range.open),\n closes: fmt(range.close % (24 * 60)),\n });\n }\n return out;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,7 @@
1
+ import { type Field } from "../../schema/fields";
2
+ import type { CustomBlock } from "../../schema/customBlocks";
3
+ import { type BlockDefinition } from "../registry";
4
+ /** Custom block-document → BlockDefinition (voor mergeRegistry, server én client). */
5
+ export declare function customBlockToDefinition(def: CustomBlock): BlockDefinition & {
6
+ customFields: Field[];
7
+ };
@@ -0,0 +1,34 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { z } from "zod";
3
+ import { blockBase } from "../../schema/blocks";
4
+ import { fieldsToZod, fieldsDefaults } from "../../schema/fields";
5
+ import { defineBlock } from "../registry";
6
+ import { interpretTemplate, scopeCss } from "./interpret";
7
+ function CustomBlockRender(def) {
8
+ const scopedCss = def.css ? scopeCss(def.css, def.type) : "";
9
+ return function Render({ block }) {
10
+ const html = interpretTemplate(def.template, block);
11
+ return /* @__PURE__ */ jsxs("div", { "data-xicms-cb": def.type, className: `xicms-custom xicms-${def.type}`, children: [
12
+ scopedCss ? /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: scopedCss } }) : null,
13
+ /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: html } })
14
+ ] });
15
+ };
16
+ }
17
+ function customBlockToDefinition(def) {
18
+ const valueSchema = z.object({ ...blockBase, _type: z.literal(def.type) }).merge(fieldsToZod(def.fields));
19
+ const definition = defineBlock({
20
+ type: def.type,
21
+ schema: valueSchema,
22
+ Render: CustomBlockRender(def),
23
+ label: def.label,
24
+ icon: def.icon || "\u{1F9E9}",
25
+ category: "content",
26
+ defaults: () => fieldsDefaults(def.fields)
27
+ });
28
+ definition.customFields = def.fields;
29
+ return definition;
30
+ }
31
+ export {
32
+ customBlockToDefinition
33
+ };
34
+ //# sourceMappingURL=definition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/blocks/customTemplate/definition.tsx"],"sourcesContent":["/**\n * Bouwt een volwaardige BlockDefinition uit een opgeslagen custom block (5.0). Server-safe:\n * de Render interpreteert de veilige template naar HTML (SSR-vriendelijk, geen hooks); de\n * waarden zijn gevalideerd via `fieldsToZod` — runtime-gedefinieerde blokken krijgen dus\n * dezelfde harde validatie als code-blokken. De editor is de generieke FieldsForm: GenericEditor\n * herkent `customFields` op de definitie (zie GenericEditor.tsx).\n */\nimport { z } from \"zod\";\nimport { blockBase } from \"../../schema/blocks\";\nimport { fieldsToZod, fieldsDefaults, type Field } from \"../../schema/fields\";\nimport type { CustomBlock } from \"../../schema/customBlocks\";\nimport { defineBlock, type BlockDefinition, type BlockRenderProps } from \"../registry\";\nimport { interpretTemplate, scopeCss } from \"./interpret\";\n\nfunction CustomBlockRender(def: CustomBlock) {\n const scopedCss = def.css ? scopeCss(def.css, def.type) : \"\";\n return function Render({ block }: BlockRenderProps<Record<string, unknown>>) {\n const html = interpretTemplate(def.template, block);\n return (\n <div data-xicms-cb={def.type} className={`xicms-custom xicms-${def.type}`}>\n {scopedCss ? <style dangerouslySetInnerHTML={{ __html: scopedCss }} /> : null}\n {/* Template is opslag-tijd gesanitized; interpolaties zijn render-tijd ge-escaped. */}\n <div dangerouslySetInnerHTML={{ __html: html }} />\n </div>\n );\n };\n}\n\n/** Custom block-document → BlockDefinition (voor mergeRegistry, server én client). */\nexport function customBlockToDefinition(def: CustomBlock): BlockDefinition & { customFields: Field[] } {\n const valueSchema = z.object({ ...blockBase, _type: z.literal(def.type) }).merge(fieldsToZod(def.fields));\n const definition = defineBlock({\n type: def.type,\n schema: valueSchema,\n Render: CustomBlockRender(def),\n label: def.label,\n icon: def.icon || \"🧩\",\n category: \"content\",\n defaults: () => fieldsDefaults(def.fields),\n }) as BlockDefinition & { customFields: Field[] };\n // De opgeslagen Field-IR meegeven zodat de GenericEditor die direct rendert (incl. showIf/date/\n // repeater-metadata die uit een Zod-schema niet volledig terug te winnen is).\n definition.customFields = def.fields;\n return definition;\n}\n"],"mappings":"AAmBM,SACe,KADf;AAZN,SAAS,SAAS;AAClB,SAAS,iBAAiB;AAC1B,SAAS,aAAa,sBAAkC;AAExD,SAAS,mBAAgE;AACzE,SAAS,mBAAmB,gBAAgB;AAE5C,SAAS,kBAAkB,KAAkB;AAC3C,QAAM,YAAY,IAAI,MAAM,SAAS,IAAI,KAAK,IAAI,IAAI,IAAI;AAC1D,SAAO,SAAS,OAAO,EAAE,MAAM,GAA8C;AAC3E,UAAM,OAAO,kBAAkB,IAAI,UAAU,KAAK;AAClD,WACE,qBAAC,SAAI,iBAAe,IAAI,MAAM,WAAW,sBAAsB,IAAI,IAAI,IACpE;AAAA,kBAAY,oBAAC,WAAM,yBAAyB,EAAE,QAAQ,UAAU,GAAG,IAAK;AAAA,MAEzE,oBAAC,SAAI,yBAAyB,EAAE,QAAQ,KAAK,GAAG;AAAA,OAClD;AAAA,EAEJ;AACF;AAGO,SAAS,wBAAwB,KAA+D;AACrG,QAAM,cAAc,EAAE,OAAO,EAAE,GAAG,WAAW,OAAO,EAAE,QAAQ,IAAI,IAAI,EAAE,CAAC,EAAE,MAAM,YAAY,IAAI,MAAM,CAAC;AACxG,QAAM,aAAa,YAAY;AAAA,IAC7B,MAAM,IAAI;AAAA,IACV,QAAQ;AAAA,IACR,QAAQ,kBAAkB,GAAG;AAAA,IAC7B,OAAO,IAAI;AAAA,IACX,MAAM,IAAI,QAAQ;AAAA,IAClB,UAAU;AAAA,IACV,UAAU,MAAM,eAAe,IAAI,MAAM;AAAA,EAC3C,CAAC;AAGD,aAAW,eAAe,IAAI;AAC9B,SAAO;AACT;","names":[]}
@@ -0,0 +1,10 @@
1
+ export declare function escapeHtml(s: string): string;
2
+ /** Rendert een custom-block-template met de blokwaarden. Output = HTML-string (SSR-safe). */
3
+ export declare function interpretTemplate(template: string, values: Record<string, unknown>): string;
4
+ /**
5
+ * Scopet CSS op het custom block: elke top-level selector wordt geprefixt met
6
+ * `[data-xicms-cb="<type>"]`, at-rules met blokken (@media/@supports) recursief. Gevaarlijke
7
+ * constructies (@import, expression(), url(javascript:…)) worden gestript. Bewust simpel en
8
+ * fail-safe: liever een kale stijl dan een lek.
9
+ */
10
+ export declare function scopeCss(css: string, type: string): string;
@@ -0,0 +1,136 @@
1
+ import { isRichTextKey } from "../../schema/fields";
2
+ const ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
3
+ function escapeHtml(s) {
4
+ return s.replace(/[&<>"']/g, (c) => ESC[c]);
5
+ }
6
+ function neutralizeUrlSchemes(s) {
7
+ return /^\s*(javascript|vbscript|data)\s*:/i.test(s) ? "#" : s;
8
+ }
9
+ function lookup(scope, path) {
10
+ return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], scope);
11
+ }
12
+ function truthy(v) {
13
+ if (Array.isArray(v)) return v.length > 0;
14
+ return Boolean(v);
15
+ }
16
+ function parse(tpl) {
17
+ const tokens = tpl.split(/(\{\{\{[^}]+\}\}\}|\{\{[^}]+\}\})/g);
18
+ const root = [];
19
+ const stack = [{ list: root }];
20
+ const top = () => stack[stack.length - 1];
21
+ const push = (n) => {
22
+ const f = top();
23
+ if (f.node?.t === "if" && f.inElse) f.node.elseBody.push(n);
24
+ else f.list.push(n);
25
+ };
26
+ for (const tok of tokens) {
27
+ if (!tok) continue;
28
+ const triple = /^\{\{\{\s*([\w.]+)\s*\}\}\}$/.exec(tok);
29
+ if (triple) {
30
+ push({ t: "var", path: triple[1], raw: true });
31
+ continue;
32
+ }
33
+ const m = /^\{\{\s*([#/]?)\s*([\w.@]+)?(?:\s+([\w.]+))?\s*\}\}$/.exec(tok);
34
+ if (!m) {
35
+ push({ t: "text", s: tok });
36
+ continue;
37
+ }
38
+ const [, sigil, word, arg] = m;
39
+ if (sigil === "#" && word === "each" && arg) {
40
+ const node = { t: "each", path: arg, body: [] };
41
+ push(node);
42
+ stack.push({ list: node.body, node });
43
+ } else if (sigil === "#" && word === "if" && arg) {
44
+ const node = { t: "if", path: arg, body: [], elseBody: [] };
45
+ push(node);
46
+ stack.push({ list: node.body, node });
47
+ } else if (word === "else" && !sigil) {
48
+ const f = top();
49
+ if (f.node?.t === "if") f.inElse = true;
50
+ else push({ t: "text", s: tok });
51
+ } else if (sigil === "/" && (word === "each" || word === "if")) {
52
+ if (stack.length > 1) stack.pop();
53
+ } else if (word && !sigil) {
54
+ push({ t: "var", path: word, raw: false });
55
+ } else {
56
+ push({ t: "text", s: tok });
57
+ }
58
+ }
59
+ return root;
60
+ }
61
+ function render(nodes, scope) {
62
+ let out = "";
63
+ for (const n of nodes) {
64
+ if (n.t === "text") out += n.s;
65
+ else if (n.t === "var") {
66
+ const leaf = n.path.split(".").pop() ?? n.path;
67
+ const v = lookup(scope, n.path);
68
+ const s = v == null ? "" : typeof v === "object" ? "" : String(v);
69
+ if (n.raw) {
70
+ out += isRichTextKey(leaf) ? s : escapeHtml(s);
71
+ } else {
72
+ out += escapeHtml(neutralizeUrlSchemes(s));
73
+ }
74
+ } else if (n.t === "each") {
75
+ const arr = lookup(scope, n.path);
76
+ if (Array.isArray(arr)) {
77
+ arr.forEach((item, i) => {
78
+ const child = item && typeof item === "object" ? { ...item } : { value: item };
79
+ child["@index"] = i + 1;
80
+ out += render(n.body, child);
81
+ });
82
+ }
83
+ } else if (n.t === "if") {
84
+ out += render(truthy(lookup(scope, n.path)) ? n.body : n.elseBody, scope);
85
+ }
86
+ }
87
+ return out;
88
+ }
89
+ function interpretTemplate(template, values) {
90
+ return render(parse(template), values);
91
+ }
92
+ function scopeCss(css, type) {
93
+ const prefix = `[data-xicms-cb="${type}"]`;
94
+ const cleaned = css.replace(/@import[^;]*;?/gi, "").replace(/@charset[^;]*;?/gi, "").replace(/expression\s*\(/gi, "invalid(").replace(/url\(\s*['"]?\s*(javascript|vbscript|data)\s*:[^)]*\)/gi, "url()");
95
+ return scopeBlock(cleaned, prefix);
96
+ }
97
+ function scopeBlock(css, prefix) {
98
+ let out = "";
99
+ let i = 0;
100
+ while (i < css.length) {
101
+ const brace = css.indexOf("{", i);
102
+ if (brace < 0) break;
103
+ const header = css.slice(i, brace).trim();
104
+ let depth = 1;
105
+ let j = brace + 1;
106
+ while (j < css.length && depth > 0) {
107
+ if (css[j] === "{") depth++;
108
+ else if (css[j] === "}") depth--;
109
+ j++;
110
+ }
111
+ const body = css.slice(brace + 1, j - 1);
112
+ if (header.startsWith("@")) {
113
+ if (/^@(media|supports|container)/i.test(header)) {
114
+ out += `${header}{${scopeBlock(body, prefix)}}`;
115
+ } else if (/^@(keyframes|font-face)/i.test(header)) {
116
+ out += `${header}{${body}}`;
117
+ }
118
+ } else if (header) {
119
+ const scoped = header.split(",").map((sel) => {
120
+ const s = sel.trim();
121
+ if (!s) return "";
122
+ if (/^(:root|html|body)$/i.test(s)) return prefix;
123
+ return `${prefix} ${s}`;
124
+ }).filter(Boolean).join(", ");
125
+ out += `${scoped}{${body}}`;
126
+ }
127
+ i = j;
128
+ }
129
+ return out;
130
+ }
131
+ export {
132
+ escapeHtml,
133
+ interpretTemplate,
134
+ scopeCss
135
+ };
136
+ //# sourceMappingURL=interpret.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/blocks/customTemplate/interpret.ts"],"sourcesContent":["/**\n * Veilige template-interpreter voor custom blocks (5.0) — een bewuste mustache-SUBSET, géén\n * JavaScript, geen expressies, geen helpers. Dubbele verdediging tegen XSS:\n *\n * 1. **Opslag-tijd** (saveCustomBlock-callable): de template zelf wordt gesanitized met een\n * strikte allowlist (geen <script>/<style>-tags, geen on*-attributen, alleen http(s)/mailto/\n * tel/relative URL's).\n * 2. **Render-tijd** (hier): élke `{{veld}}`-interpolatie wordt HTML-escaped. Alleen\n * `{{{veldHtml}}}` (triple, en uitsluitend voor sleutels die op de sanitizer-conventie\n * `html`/`*Html` matchen) injecteert HTML — en die waarden zijn bij publiceren al\n * server-side gesanitized door `sanitizeBlocks`. In attributen worden bovendien\n * `javascript:`-achtige URL-schema's geneutraliseerd.\n *\n * Syntax:\n * {{veld}} → escaped waarde (ook dotted: {{cta.label}})\n * {{{veldHtml}}} → rauwe (gesanitizede) HTML — alleen voor *Html-sleutels\n * {{#each items}} … {{/each}} → herhaal over een repeater (binnen: velden van het item;\n * {{@index}} = 1-based nummer)\n * {{#if veld}} … {{else}} … {{/if}} → conditioneel op truthy (lege string/0/false = falsy)\n *\n * Server-safe (geen React, geen DOM) — apart getest, incl. XSS-pogingen.\n */\nimport { isRichTextKey } from \"../../schema/fields\";\n\nconst ESC: Record<string, string> = { \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" };\nexport function escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) => ESC[c]);\n}\n\n/** Neutraliseert gevaarlijke URL-schema's in een geïnterpoleerde waarde (voor href/src-posities). */\nfunction neutralizeUrlSchemes(s: string): string {\n return /^\\s*(javascript|vbscript|data)\\s*:/i.test(s) ? \"#\" : s;\n}\n\nfunction lookup(scope: Record<string, unknown>, path: string): unknown {\n return path.split(\".\").reduce<unknown>((o, k) => (o == null ? undefined : (o as Record<string, unknown>)[k]), scope);\n}\n\nfunction truthy(v: unknown): boolean {\n if (Array.isArray(v)) return v.length > 0;\n return Boolean(v);\n}\n\ntype Node =\n | { t: \"text\"; s: string }\n | { t: \"var\"; path: string; raw: boolean }\n | { t: \"each\"; path: string; body: Node[] }\n | { t: \"if\"; path: string; body: Node[]; elseBody: Node[] };\n\n/** Parseert de template naar een boom. Onbekende/kapotte tags worden als tekst behandeld. */\nfunction parse(tpl: string): Node[] {\n const tokens = tpl.split(/(\\{\\{\\{[^}]+\\}\\}\\}|\\{\\{[^}]+\\}\\})/g);\n const root: Node[] = [];\n // Stack van open blokken; elk frame houdt de node-lijst bij waar we aan toevoegen.\n const stack: { list: Node[]; node?: Extract<Node, { t: \"each\" | \"if\" }>; inElse?: boolean }[] = [{ list: root }];\n const top = () => stack[stack.length - 1];\n const push = (n: Node) => {\n const f = top();\n if (f.node?.t === \"if\" && f.inElse) f.node.elseBody.push(n);\n else f.list.push(n);\n };\n for (const tok of tokens) {\n if (!tok) continue;\n const triple = /^\\{\\{\\{\\s*([\\w.]+)\\s*\\}\\}\\}$/.exec(tok);\n if (triple) {\n push({ t: \"var\", path: triple[1], raw: true });\n continue;\n }\n const m = /^\\{\\{\\s*([#/]?)\\s*([\\w.@]+)?(?:\\s+([\\w.]+))?\\s*\\}\\}$/.exec(tok);\n if (!m) {\n push({ t: \"text\", s: tok });\n continue;\n }\n const [, sigil, word, arg] = m;\n if (sigil === \"#\" && word === \"each\" && arg) {\n const node: Node = { t: \"each\", path: arg, body: [] };\n push(node);\n stack.push({ list: node.body, node });\n } else if (sigil === \"#\" && word === \"if\" && arg) {\n const node: Node = { t: \"if\", path: arg, body: [], elseBody: [] };\n push(node);\n stack.push({ list: node.body, node });\n } else if (word === \"else\" && !sigil) {\n const f = top();\n if (f.node?.t === \"if\") f.inElse = true;\n else push({ t: \"text\", s: tok });\n } else if (sigil === \"/\" && (word === \"each\" || word === \"if\")) {\n if (stack.length > 1) stack.pop();\n // ongebalanceerde sluittag: negeren (fail-soft)\n } else if (word && !sigil) {\n push({ t: \"var\", path: word, raw: false });\n } else {\n push({ t: \"text\", s: tok });\n }\n }\n return root;\n}\n\nfunction render(nodes: Node[], scope: Record<string, unknown>): string {\n let out = \"\";\n for (const n of nodes) {\n if (n.t === \"text\") out += n.s;\n else if (n.t === \"var\") {\n const leaf = n.path.split(\".\").pop() ?? n.path;\n const v = lookup(scope, n.path);\n const s = v == null ? \"\" : typeof v === \"object\" ? \"\" : String(v);\n if (n.raw) {\n // Triple-stache: alléén voor rich-text-sleutels (waarde is server-side gesanitized).\n out += isRichTextKey(leaf) ? s : escapeHtml(s);\n } else {\n out += escapeHtml(neutralizeUrlSchemes(s));\n }\n } else if (n.t === \"each\") {\n const arr = lookup(scope, n.path);\n if (Array.isArray(arr)) {\n arr.forEach((item, i) => {\n const child = item && typeof item === \"object\" ? { ...(item as Record<string, unknown>) } : { value: item };\n (child as Record<string, unknown>)[\"@index\"] = i + 1;\n out += render(n.body, child as Record<string, unknown>);\n });\n }\n } else if (n.t === \"if\") {\n out += render(truthy(lookup(scope, n.path)) ? n.body : n.elseBody, scope);\n }\n }\n return out;\n}\n\n/** Rendert een custom-block-template met de blokwaarden. Output = HTML-string (SSR-safe). */\nexport function interpretTemplate(template: string, values: Record<string, unknown>): string {\n return render(parse(template), values);\n}\n\n/**\n * Scopet CSS op het custom block: elke top-level selector wordt geprefixt met\n * `[data-xicms-cb=\"<type>\"]`, at-rules met blokken (@media/@supports) recursief. Gevaarlijke\n * constructies (@import, expression(), url(javascript:…)) worden gestript. Bewust simpel en\n * fail-safe: liever een kale stijl dan een lek.\n */\nexport function scopeCss(css: string, type: string): string {\n const prefix = `[data-xicms-cb=\"${type}\"]`;\n const cleaned = css\n .replace(/@import[^;]*;?/gi, \"\")\n .replace(/@charset[^;]*;?/gi, \"\")\n .replace(/expression\\s*\\(/gi, \"invalid(\")\n .replace(/url\\(\\s*['\"]?\\s*(javascript|vbscript|data)\\s*:[^)]*\\)/gi, \"url()\");\n return scopeBlock(cleaned, prefix);\n}\n\nfunction scopeBlock(css: string, prefix: string): string {\n let out = \"\";\n let i = 0;\n while (i < css.length) {\n const brace = css.indexOf(\"{\", i);\n if (brace < 0) break;\n const header = css.slice(i, brace).trim();\n // Vind de bijbehorende sluit-brace (genest tellen).\n let depth = 1;\n let j = brace + 1;\n while (j < css.length && depth > 0) {\n if (css[j] === \"{\") depth++;\n else if (css[j] === \"}\") depth--;\n j++;\n }\n const body = css.slice(brace + 1, j - 1);\n if (header.startsWith(\"@\")) {\n if (/^@(media|supports|container)/i.test(header)) {\n out += `${header}{${scopeBlock(body, prefix)}}`;\n } else if (/^@(keyframes|font-face)/i.test(header)) {\n // keyframes/font-face niet prefixen (selectors zijn geen elementen).\n out += `${header}{${body}}`;\n }\n // overige at-rules: weglaten (fail-safe)\n } else if (header) {\n const scoped = header\n .split(\",\")\n .map((sel) => {\n const s = sel.trim();\n if (!s) return \"\";\n // `:root`/`html`/`body` binnen een blok-scope slaat nergens op → scope zelf.\n if (/^(:root|html|body)$/i.test(s)) return prefix;\n return `${prefix} ${s}`;\n })\n .filter(Boolean)\n .join(\", \");\n out += `${scoped}{${body}}`;\n }\n i = j;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
@@ -8,6 +8,7 @@
8
8
  import type { ComponentType } from "react";
9
9
  import type { ZodTypeAny, z } from "zod";
10
10
  import type { BlockBase } from "../schema/blocks";
11
+ import type { FieldHints } from "../schema/fields";
11
12
  export interface BlockRenderProps<T = unknown> {
12
13
  block: T;
13
14
  siteId: string;
@@ -42,6 +43,13 @@ export interface BlockDefinition<S extends ZodTypeAny = ZodTypeAny> {
42
43
  category?: BlockCategory;
43
44
  /** Defaultwaarden voor een nieuw block (zonder _key/_type). */
44
45
  defaults?: () => Record<string, unknown>;
46
+ /**
47
+ * Optionele hints voor het automatisch gegenereerde formulier (GenericEditor 2.0, 5.0):
48
+ * per (dotted) veldsleutel een verfijning van de uit het Zod-schema afgeleide Field-IR —
49
+ * bv. `{ "items.date": { type: "date" }, cta: { showIf: { field: "showCta" } } }`.
50
+ * Zie `introspectToFields` (admin/lib/zodFields.ts) en `schema/fields.ts`.
51
+ */
52
+ fields?: FieldHints;
45
53
  }
46
54
  /**
47
55
  * 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 { ZodTypeAny, z } from \"zod\";\nimport type { BlockBase } from \"../schema/blocks\";\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 ZodTypeAny = ZodTypeAny> {\n type: string;\n schema: S;\n Render: ComponentType<BlockRenderProps<z.infer<S>>>;\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 Editor?: ComponentType<BlockEditorProps<z.infer<S>>>;\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\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 ZodTypeAny>(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 */\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 const res = def.schema.safeParse(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.error.issues);\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 { ZodTypeAny, z } from \"zod\";\nimport type { BlockBase } from \"../schema/blocks\";\nimport type { FieldHints } from \"../schema/fields\";\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 ZodTypeAny = ZodTypeAny> {\n type: string;\n schema: S;\n Render: ComponentType<BlockRenderProps<z.infer<S>>>;\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 Editor?: ComponentType<BlockEditorProps<z.infer<S>>>;\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 ZodTypeAny>(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 */\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 const res = def.schema.safeParse(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.error.issues);\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":"AAiEO,SAAS,YAAkC,KAA0C;AAG1F,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;AAWO,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;AACA,UAAM,MAAM,IAAI,OAAO,UAAU,GAAG;AACpC,QAAI,IAAI,QAAS,KAAI,KAAK,qBAAqB,IAAI,MAAmB,QAAQ,CAAC;AAAA,QAC1E,MAAK,UAAU,IAAI,sDAAsD,IAAI,MAAM,MAAM;AAAA,EAChG;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":[]}
@@ -1,5 +1,5 @@
1
1
  import type { Page } from "../schema/pages";
2
- import type { NewsArticle } from "../content/types";
2
+ import type { MediaItem, NewsArticle } from "../content/types";
3
3
  import type { FormDefinition } from "../schema/forms";
4
4
  export declare const DEMO_EVENT = "xicms:demo";
5
5
  /** Zet de demo-bewerkmodus aan/uit (door de provider, op basis van config.demoEditing). */
@@ -15,6 +15,16 @@ export declare function registerDemoNews(siteId: string, articles: NewsArticle[]
15
15
  export declare function getDemoNews(siteId: string): NewsArticle[];
16
16
  export declare function registerDemoForm(siteId: string, formId: string, def: FormDefinition): void;
17
17
  export declare function getDemoForm(siteId: string, formId: string): FormDefinition | null;
18
+ /** Seed de mediabibliotheek van de demo (curated set; alt = zoektekst in de kiezer). */
19
+ export declare function registerDemoMedia(siteId: string, items: Array<Pick<MediaItem, "url" | "name" | "alt">>): void;
20
+ /** Bibliotheek in demo-modus: lokale uploads (nieuwste eerst) + de geseede set. */
21
+ export declare function listDemoMedia(siteId: string): MediaItem[];
22
+ export declare function addDemoMedia(siteId: string, item: {
23
+ url: string;
24
+ name: string;
25
+ }): MediaItem;
26
+ /** Inclusief geheugen-only items (opslag-vol-vangnet) — gebruikt door de kiezer. */
27
+ export declare function listDemoMediaAll(siteId: string): MediaItem[];
18
28
  export declare function loadDemoDraft(siteId: string, id: string): Partial<Page> | null;
19
29
  export declare function saveDemoDraft(siteId: string, id: string, patch: Partial<Page>): void;
20
30
  /** Verwijdert alle lokale demo-bewerkingen + login voor deze site (de "Reset"-knop). */