@xiwebdesign/xicms-core 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/XiCMSProvider.d.ts +11 -2
  2. package/dist/XiCMSProvider.js +10 -5
  3. package/dist/XiCMSProvider.js.map +1 -1
  4. package/dist/admin/blockEditors/GenericEditor.js +18 -8
  5. package/dist/admin/blockEditors/GenericEditor.js.map +1 -1
  6. package/dist/admin/customBlocks/CustomBlocksPanel.js +12 -2
  7. package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
  8. package/dist/admin/lib/pagesClient.js +4 -1
  9. package/dist/admin/lib/pagesClient.js.map +1 -1
  10. package/dist/admin/lib/upload.js +14 -7
  11. package/dist/admin/lib/upload.js.map +1 -1
  12. package/dist/admin/media/MediaManager.js +60 -28
  13. package/dist/admin/media/MediaManager.js.map +1 -1
  14. package/dist/admin/media/MediaPicker.js +25 -15
  15. package/dist/admin/media/MediaPicker.js.map +1 -1
  16. package/dist/admin/pages/PageEditor.d.ts +2 -1
  17. package/dist/admin/pages/PageEditor.js +16 -2
  18. package/dist/admin/pages/PageEditor.js.map +1 -1
  19. package/dist/admin/pages/PagesPanel.js +9 -5
  20. package/dist/admin/pages/PagesPanel.js.map +1 -1
  21. package/dist/admin/visual/VisualEditor.js +11 -2
  22. package/dist/admin/visual/VisualEditor.js.map +1 -1
  23. package/dist/agentFiles/files.d.ts +15 -3
  24. package/dist/agentFiles/files.js +57 -0
  25. package/dist/agentFiles/files.js.map +1 -1
  26. package/dist/auth/AuthProvider.js +3 -3
  27. package/dist/auth/AuthProvider.js.map +1 -1
  28. package/dist/blocks/builtins/Columns.js +1 -1
  29. package/dist/blocks/builtins/Columns.js.map +1 -1
  30. package/dist/blocks/builtins/Gallery.js +2 -2
  31. package/dist/blocks/builtins/Gallery.js.map +1 -1
  32. package/dist/blocks/builtins/Hero.js +1 -1
  33. package/dist/blocks/builtins/Hero.js.map +1 -1
  34. package/dist/blocks/builtins/SymbolRef.js.map +1 -1
  35. package/dist/components/PageView.js +4 -3
  36. package/dist/components/PageView.js.map +1 -1
  37. package/dist/demo/index.d.ts +7 -0
  38. package/dist/demo/index.js +23 -0
  39. package/dist/demo/index.js.map +1 -0
  40. package/dist/index.d.ts +2 -4
  41. package/dist/index.js +0 -20
  42. package/dist/index.js.map +1 -1
  43. package/dist/modules/registry.d.ts +3 -4
  44. package/dist/modules/registry.js +2 -3
  45. package/dist/modules/registry.js.map +1 -1
  46. package/dist/schema/blocks.d.ts +27 -12
  47. package/dist/schema/blocks.js +3 -0
  48. package/dist/schema/blocks.js.map +1 -1
  49. package/dist/schema/settings.d.ts +1 -34
  50. package/dist/schema/settings.js +2 -8
  51. package/dist/schema/settings.js.map +1 -1
  52. package/dist/schema/siteConfig.d.ts +3 -71
  53. package/dist/schema/siteConfig.js +3 -8
  54. package/dist/schema/siteConfig.js.map +1 -1
  55. package/dist/schema/symbols.d.ts +2 -2
  56. package/dist/schema/symbols.js.map +1 -1
  57. package/dist/server/index.d.ts +1 -1
  58. package/dist/server/index.js +2 -0
  59. package/dist/server/index.js.map +1 -1
  60. package/dist/server/pages.d.ts +23 -3
  61. package/dist/server/pages.js +37 -3
  62. package/dist/server/pages.js.map +1 -1
  63. package/dist/server/seo.js +5 -3
  64. package/dist/server/seo.js.map +1 -1
  65. package/dist/settings/useSiteSettings.d.ts +15 -3
  66. package/dist/settings/useSiteSettings.js +26 -5
  67. package/dist/settings/useSiteSettings.js.map +1 -1
  68. package/dist/styles.css +14 -0
  69. package/package.json +6 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/seo.ts"],"sourcesContent":["import \"server-only\";\n\nimport type { Metadata, MetadataRoute } from \"next\";\nimport type { Seo } from \"../schema/seo\";\nimport type { Page } from \"../schema/pages\";\nimport { localizedPath, type LocaleConfig } from \"../i18n/overlay\";\n\n/** Absolute origin (https://domein) uit de site-config; \"\" als er geen domein is. */\nexport function siteOrigin(site: { domain?: string }): string {\n return site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\")}` : \"\";\n}\n\n/**\n * Bouwt een Next `sitemap.xml` uit een lijst pagina's. Gebruik in `app/sitemap.ts`:\n * export default function sitemap() { return buildSitemap(pages, siteConfig); }\n */\nexport function buildSitemap(\n pages: Array<{ path: string; updatedAt?: string | number | Date; seo?: { noindex?: boolean } }>,\n site: { domain?: string },\n i18n?: LocaleConfig,\n): MetadataRoute.Sitemap {\n const origin = siteOrigin(site);\n // Meertalige site (5.0): elke pagina één keer per taal (default zonder prefix, rest mét).\n const locales = i18n && i18n.locales.length > 1 ? i18n.locales : [undefined];\n return pages\n .filter((p) => !p.seo?.noindex)\n .flatMap((p) =>\n locales.map((locale) => {\n const path = locale && i18n ? localizedPath(p.path, locale, i18n.defaultLocale) : p.path;\n return {\n url: `${origin}${path === \"/\" ? \"/\" : path}`,\n lastModified: p.updatedAt ? new Date(p.updatedAt) : undefined,\n changeFrequency: p.path === \"/\" ? (\"weekly\" as const) : (\"monthly\" as const),\n priority: p.path === \"/\" ? 1 : 0.7,\n };\n }),\n );\n}\n\n/**\n * Bouwt een Next `robots.txt`. In `mode:\"demo\"` wordt de hele site op noindex gezet; anders alles\n * toegestaan behalve de beheerpaden, met verwijzing naar de sitemap. Gebruik in `app/robots.ts`.\n */\nexport function buildRobots(\n site: { domain?: string; mode?: string },\n opts?: { disallow?: string[] },\n): MetadataRoute.Robots {\n const origin = siteOrigin(site);\n if (site.mode === \"demo\") {\n // Let op: disallow blokkeert alleen het CRAWLEN — de URL kan alsnog (kaal) geïndexeerd\n // raken. buildMetadata zet in demo-modus daarom óók robots.index=false op elke pagina.\n return { rules: { userAgent: \"*\", disallow: \"/\" } };\n }\n return {\n rules: { userAgent: \"*\", allow: \"/\", disallow: opts?.disallow ?? [\"/beheer\", \"/login\"] },\n ...(origin ? { sitemap: `${origin}/sitemap.xml`, host: origin } : {}),\n };\n}\n\ninterface SeoSiteContext {\n name: string;\n domain?: string;\n /** \"demo\" zet elke pagina op noindex (robots.txt-disallow alleen is geen noindex). */\n mode?: string;\n seoDefaults?: Seo;\n}\n\n/**\n * Bouwt Next `Metadata` voor een pagina, met fallback op site-defaults. Geef voor een meertalige\n * site (5.0) `i18n` mee: de canonical wijst dan naar de taalvariant en er komen\n * hreflang-alternates voor élke taal (default zonder prefix, incl. x-default).\n */\nexport function buildMetadata(\n page: Pick<Page, \"title\" | \"path\" | \"seo\">,\n site: SeoSiteContext,\n i18n?: LocaleConfig & { locale: string },\n): Metadata {\n const seo = page.seo ?? {};\n const d = site.seoDefaults ?? {};\n const title = seo.title ?? page.title ?? site.name;\n const description = seo.description ?? d.description;\n // Absolute canonical/OG-URL (Google prefereert absoluut) wanneer het domein bekend is.\n const origin = site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\")}` : \"\";\n const rawPath = seo.canonical ?? page.path;\n const path = i18n && !/^https?:\\/\\//.test(rawPath) ? localizedPath(rawPath, i18n.locale, i18n.defaultLocale) : rawPath;\n const canonical = /^https?:\\/\\//.test(path) ? path : `${origin}${path.startsWith(\"/\") ? \"\" : \"/\"}${path}`;\n const languages =\n i18n && i18n.locales.length > 1\n ? Object.fromEntries([\n ...i18n.locales.map((l) => [l, `${origin}${localizedPath(page.path, l, i18n.defaultLocale)}`]),\n [\"x-default\", `${origin}${page.path}`],\n ])\n : undefined;\n const ogImage = seo.ogImage ?? d.ogImage;\n // OG-afbeeldingen met afmetingen (1200×630) → betere previews op social.\n const images = ogImage ? [{ url: ogImage, width: 1200, height: 630 }] : undefined;\n\n return {\n title,\n description,\n metadataBase: origin ? new URL(origin) : undefined,\n alternates: { canonical, ...(languages ? { languages } : {}) },\n robots: seo.noindex || site.mode === \"demo\" ? { index: false, follow: false } : undefined,\n openGraph: {\n title,\n description,\n url: canonical,\n siteName: site.name,\n type: \"website\",\n ...(images ? { images } : {}),\n },\n twitter: {\n card: images ? \"summary_large_image\" : \"summary\",\n title,\n description,\n ...(images ? { images: [ogImage as string] } : {}),\n },\n };\n}\n\n/** Bedrijfsgegevens voor gestructureerde data (schema.org LocalBusiness e.d.). */\nexport interface BusinessInfo {\n /** schema.org-type, bv. \"LocalBusiness\", \"LodgingBusiness\", \"Restaurant\", \"Store\". */\n type?: string;\n name: string;\n description?: string;\n url?: string;\n telephone?: string;\n email?: string;\n image?: string;\n /** Indicatie prijsniveau, bv. \"€€\". */\n priceRange?: string;\n address?: {\n street?: string;\n locality?: string;\n region?: string;\n postalCode?: string;\n country?: string;\n };\n geo?: { lat: number; lng: number };\n /** schema.org openingHours, bv. [\"Mo-Fr 09:00-17:00\", \"Sa 10:00-16:00\"]. */\n openingHours?: string[];\n /** Externe profielen (social, etc.). */\n sameAs?: string[];\n}\n\n/**\n * Bouwt gestructureerde data voor een lokaal bedrijf (herbruikbaar per branche). Plaats het\n * resultaat als JSON-LD in de layout: <script type=\"application/ld+json\">JSON.stringify(...)</script>\n */\nexport function businessJsonLd(info: BusinessInfo): Record<string, unknown> {\n const out: Record<string, unknown> = {\n \"@context\": \"https://schema.org\",\n \"@type\": info.type ?? \"LocalBusiness\",\n // Zelfde relatieve @id als de contact-/openingstijden-blokken: zo voegen zoekmachines de\n // layout-brede en blok-specifieke LocalBusiness-JSON-LD samen tot één entiteit per pagina.\n \"@id\": \"#business\",\n name: info.name,\n };\n if (info.description) out.description = info.description;\n if (info.url) out.url = info.url;\n if (info.telephone) out.telephone = info.telephone;\n if (info.email) out.email = info.email;\n if (info.image) out.image = info.image;\n if (info.priceRange) out.priceRange = info.priceRange;\n if (info.address) {\n out.address = {\n \"@type\": \"PostalAddress\",\n streetAddress: info.address.street,\n addressLocality: info.address.locality,\n addressRegion: info.address.region,\n postalCode: info.address.postalCode,\n addressCountry: info.address.country,\n };\n }\n if (info.geo) out.geo = { \"@type\": \"GeoCoordinates\", latitude: info.geo.lat, longitude: info.geo.lng };\n if (info.openingHours?.length) out.openingHours = info.openingHours;\n if (info.sameAs?.length) out.sameAs = info.sameAs;\n return out;\n}\n\n/** Breadcrumb-trail (schema.org BreadcrumbList) — voor genest navigeren + rich results. */\nexport function breadcrumbListJsonLd(items: { name: string; url: string }[]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((it, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n name: it.name,\n item: it.url,\n })),\n };\n}\n\n/** Article-JSON-LD voor nieuws/blog. */\nexport function articleJsonLd(a: {\n title: string;\n description?: string;\n author?: string;\n datePublished?: string;\n image?: string;\n url?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: a.title,\n ...(a.description ? { description: a.description } : {}),\n ...(a.author ? { author: { \"@type\": \"Person\", name: a.author } } : {}),\n ...(a.datePublished ? { datePublished: a.datePublished } : {}),\n ...(a.image ? { image: a.image } : {}),\n ...(a.url ? { url: a.url } : {}),\n };\n}\n\n/** Product/Offer-JSON-LD. */\nexport function productJsonLd(p: {\n name: string;\n description?: string;\n price?: string | number;\n currency?: string;\n image?: string;\n availability?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Product\",\n name: p.name,\n ...(p.description ? { description: p.description } : {}),\n ...(p.image ? { image: p.image } : {}),\n ...(p.price != null\n ? {\n offers: {\n \"@type\": \"Offer\",\n price: String(p.price),\n priceCurrency: p.currency ?? \"EUR\",\n ...(p.availability ? { availability: `https://schema.org/${p.availability}` } : {}),\n },\n }\n : {}),\n };\n}\n\n/** OpeningHoursSpecification — bv. `[{ days:[\"Mo\",\"Tu\"], opens:\"09:00\", closes:\"17:00\" }]`. */\nexport function openingHoursJsonLd(\n spec: { days: string[]; opens: string; closes: string }[],\n): Record<string, unknown>[] {\n return spec.map((s) => ({\n \"@type\": \"OpeningHoursSpecification\",\n dayOfWeek: s.days,\n opens: s.opens,\n closes: s.closes,\n }));\n}\n\n/** ImageGallery-JSON-LD voor een fotogalerij. */\nexport function galleryJsonLd(items: { url: string; caption?: string }[]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ImageGallery\",\n image: items.map((it) => ({ \"@type\": \"ImageObject\", contentUrl: it.url, ...(it.caption ? { caption: it.caption } : {}) })),\n };\n}\n\n/** VideoObject-JSON-LD. */\nexport function videoObjectJsonLd(v: {\n name: string;\n description?: string;\n thumbnailUrl?: string;\n contentUrl?: string;\n uploadDate?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"VideoObject\",\n name: v.name,\n ...(v.description ? { description: v.description } : {}),\n ...(v.thumbnailUrl ? { thumbnailUrl: v.thumbnailUrl } : {}),\n ...(v.contentUrl ? { contentUrl: v.contentUrl } : {}),\n ...(v.uploadDate ? { uploadDate: v.uploadDate } : {}),\n };\n}\n\n/** Bouwt een JSON-LD-object voor een pagina (schema.org). */\nexport function buildJsonLd(\n page: Pick<Page, \"title\" | \"path\" | \"seo\">,\n site: SeoSiteContext,\n): Record<string, unknown> {\n const type = page.seo?.jsonLdType ?? \"WebPage\";\n const base = site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\")}` : \"\";\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": type,\n name: page.seo?.title ?? page.title,\n description: page.seo?.description ?? site.seoDefaults?.description,\n url: base + page.path,\n publisher: { \"@type\": \"Organization\", name: site.name },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/server/seo.ts"],"sourcesContent":["import \"server-only\";\n\nimport type { Metadata, MetadataRoute } from \"next\";\nimport type { Seo } from \"../schema/seo\";\nimport type { Page } from \"../schema/pages\";\nimport { localizedPath, type LocaleConfig } from \"../i18n/overlay\";\n\n/** Absolute origin (https://domein) uit de site-config; \"\" als er geen domein is. */\nexport function siteOrigin(site: { domain?: string }): string {\n return site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\")}` : \"\";\n}\n\n/**\n * Bouwt een Next `sitemap.xml` uit een lijst pagina's. Gebruik in `app/sitemap.ts`:\n * export default function sitemap() { return buildSitemap(pages, siteConfig); }\n */\nexport function buildSitemap(\n pages: Array<{ path: string; updatedAt?: string | number | Date; seo?: { noindex?: boolean } }>,\n site: { domain?: string },\n i18n?: LocaleConfig,\n): MetadataRoute.Sitemap {\n const origin = siteOrigin(site);\n // Meertalige site (5.0): elke pagina één keer per taal (default zonder prefix, rest mét).\n const locales = i18n && i18n.locales.length > 1 ? i18n.locales : [undefined];\n return pages\n .filter((p) => !p.seo?.noindex)\n .flatMap((p) =>\n locales.map((locale) => {\n const path = locale && i18n ? localizedPath(p.path, locale, i18n.defaultLocale) : p.path;\n return {\n url: `${origin}${path === \"/\" ? \"/\" : path}`,\n lastModified: p.updatedAt ? new Date(p.updatedAt) : undefined,\n changeFrequency: p.path === \"/\" ? (\"weekly\" as const) : (\"monthly\" as const),\n priority: p.path === \"/\" ? 1 : 0.7,\n };\n }),\n );\n}\n\n/**\n * Bouwt een Next `robots.txt`. In `mode:\"demo\"` wordt de hele site op noindex gezet; anders alles\n * toegestaan behalve de beheerpaden, met verwijzing naar de sitemap. Gebruik in `app/robots.ts`.\n */\nexport function buildRobots(\n site: { domain?: string; mode?: string },\n opts?: { disallow?: string[] },\n): MetadataRoute.Robots {\n const origin = siteOrigin(site);\n if (site.mode === \"demo\") {\n // Let op: disallow blokkeert alleen het CRAWLEN — de URL kan alsnog (kaal) geïndexeerd\n // raken. buildMetadata zet in demo-modus daarom óók robots.index=false op elke pagina.\n return { rules: { userAgent: \"*\", disallow: \"/\" } };\n }\n return {\n rules: { userAgent: \"*\", allow: \"/\", disallow: opts?.disallow ?? [\"/beheer\", \"/login\"] },\n ...(origin ? { sitemap: `${origin}/sitemap.xml`, host: origin } : {}),\n };\n}\n\ninterface SeoSiteContext {\n name: string;\n domain?: string;\n /** \"demo\" zet elke pagina op noindex (robots.txt-disallow alleen is geen noindex). */\n mode?: string;\n seoDefaults?: Seo;\n}\n\n/**\n * Bouwt Next `Metadata` voor een pagina, met fallback op site-defaults. Geef voor een meertalige\n * site (5.0) `i18n` mee: de canonical wijst dan naar de taalvariant en er komen\n * hreflang-alternates voor élke taal (default zonder prefix, incl. x-default).\n */\nexport function buildMetadata(\n page: Pick<Page, \"title\" | \"path\" | \"seo\">,\n site: SeoSiteContext,\n i18n?: LocaleConfig & { locale: string },\n): Metadata {\n const seo = page.seo ?? {};\n const d = site.seoDefaults ?? {};\n const title = seo.title ?? page.title ?? site.name;\n const description = seo.description ?? d.description;\n // Absolute canonical/OG-URL (Google prefereert absoluut) wanneer het domein bekend is.\n const origin = site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\")}` : \"\";\n const rawPath = seo.canonical ?? page.path;\n // Een door de auteur EXPLICIET gezette canonical wordt verbatim gebruikt — die her-localiseren\n // (taalprefix ervoor plakken) zou de bewuste keuze van de auteur overschrijven.\n const path =\n i18n && !seo.canonical && !/^https?:\\/\\//.test(rawPath)\n ? localizedPath(rawPath, i18n.locale, i18n.defaultLocale)\n : rawPath;\n const canonical = /^https?:\\/\\//.test(path) ? path : `${origin}${path.startsWith(\"/\") ? \"\" : \"/\"}${path}`;\n const languages =\n i18n && i18n.locales.length > 1\n ? Object.fromEntries([\n ...i18n.locales.map((l) => [l, `${origin}${localizedPath(page.path, l, i18n.defaultLocale)}`]),\n [\"x-default\", `${origin}${page.path}`],\n ])\n : undefined;\n const ogImage = seo.ogImage ?? d.ogImage;\n // OG-afbeeldingen met afmetingen (1200×630) → betere previews op social.\n const images = ogImage ? [{ url: ogImage, width: 1200, height: 630 }] : undefined;\n\n return {\n title,\n description,\n metadataBase: origin ? new URL(origin) : undefined,\n alternates: { canonical, ...(languages ? { languages } : {}) },\n robots: seo.noindex || site.mode === \"demo\" ? { index: false, follow: false } : undefined,\n openGraph: {\n title,\n description,\n url: canonical,\n siteName: site.name,\n type: \"website\",\n ...(images ? { images } : {}),\n },\n twitter: {\n card: images ? \"summary_large_image\" : \"summary\",\n title,\n description,\n ...(images ? { images: [ogImage as string] } : {}),\n },\n };\n}\n\n/** Bedrijfsgegevens voor gestructureerde data (schema.org LocalBusiness e.d.). */\nexport interface BusinessInfo {\n /** schema.org-type, bv. \"LocalBusiness\", \"LodgingBusiness\", \"Restaurant\", \"Store\". */\n type?: string;\n name: string;\n description?: string;\n url?: string;\n telephone?: string;\n email?: string;\n image?: string;\n /** Indicatie prijsniveau, bv. \"€€\". */\n priceRange?: string;\n address?: {\n street?: string;\n locality?: string;\n region?: string;\n postalCode?: string;\n country?: string;\n };\n geo?: { lat: number; lng: number };\n /** schema.org openingHours, bv. [\"Mo-Fr 09:00-17:00\", \"Sa 10:00-16:00\"]. */\n openingHours?: string[];\n /** Externe profielen (social, etc.). */\n sameAs?: string[];\n}\n\n/**\n * Bouwt gestructureerde data voor een lokaal bedrijf (herbruikbaar per branche). Plaats het\n * resultaat als JSON-LD in de layout: <script type=\"application/ld+json\">JSON.stringify(...)</script>\n */\nexport function businessJsonLd(info: BusinessInfo): Record<string, unknown> {\n const out: Record<string, unknown> = {\n \"@context\": \"https://schema.org\",\n \"@type\": info.type ?? \"LocalBusiness\",\n // Zelfde relatieve @id als de contact-/openingstijden-blokken: zo voegen zoekmachines de\n // layout-brede en blok-specifieke LocalBusiness-JSON-LD samen tot één entiteit per pagina.\n \"@id\": \"#business\",\n name: info.name,\n };\n if (info.description) out.description = info.description;\n if (info.url) out.url = info.url;\n if (info.telephone) out.telephone = info.telephone;\n if (info.email) out.email = info.email;\n if (info.image) out.image = info.image;\n if (info.priceRange) out.priceRange = info.priceRange;\n // Alleen een PostalAddress-node emitten als er ook echt een adresveld is ingevuld — een leeg\n // object is ongeldige/lage-kwaliteit structured data.\n if (info.address && (info.address.street || info.address.locality || info.address.postalCode)) {\n out.address = {\n \"@type\": \"PostalAddress\",\n streetAddress: info.address.street,\n addressLocality: info.address.locality,\n addressRegion: info.address.region,\n postalCode: info.address.postalCode,\n addressCountry: info.address.country,\n };\n }\n if (info.geo) out.geo = { \"@type\": \"GeoCoordinates\", latitude: info.geo.lat, longitude: info.geo.lng };\n if (info.openingHours?.length) out.openingHours = info.openingHours;\n if (info.sameAs?.length) out.sameAs = info.sameAs;\n return out;\n}\n\n/** Breadcrumb-trail (schema.org BreadcrumbList) — voor genest navigeren + rich results. */\nexport function breadcrumbListJsonLd(items: { name: string; url: string }[]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((it, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n name: it.name,\n item: it.url,\n })),\n };\n}\n\n/** Article-JSON-LD voor nieuws/blog. */\nexport function articleJsonLd(a: {\n title: string;\n description?: string;\n author?: string;\n datePublished?: string;\n image?: string;\n url?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: a.title,\n ...(a.description ? { description: a.description } : {}),\n ...(a.author ? { author: { \"@type\": \"Person\", name: a.author } } : {}),\n ...(a.datePublished ? { datePublished: a.datePublished } : {}),\n ...(a.image ? { image: a.image } : {}),\n ...(a.url ? { url: a.url } : {}),\n };\n}\n\n/** Product/Offer-JSON-LD. */\nexport function productJsonLd(p: {\n name: string;\n description?: string;\n price?: string | number;\n currency?: string;\n image?: string;\n availability?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Product\",\n name: p.name,\n ...(p.description ? { description: p.description } : {}),\n ...(p.image ? { image: p.image } : {}),\n ...(p.price != null\n ? {\n offers: {\n \"@type\": \"Offer\",\n price: String(p.price),\n priceCurrency: p.currency ?? \"EUR\",\n ...(p.availability ? { availability: `https://schema.org/${p.availability}` } : {}),\n },\n }\n : {}),\n };\n}\n\n/** OpeningHoursSpecification — bv. `[{ days:[\"Mo\",\"Tu\"], opens:\"09:00\", closes:\"17:00\" }]`. */\nexport function openingHoursJsonLd(\n spec: { days: string[]; opens: string; closes: string }[],\n): Record<string, unknown>[] {\n return spec.map((s) => ({\n \"@type\": \"OpeningHoursSpecification\",\n dayOfWeek: s.days,\n opens: s.opens,\n closes: s.closes,\n }));\n}\n\n/** ImageGallery-JSON-LD voor een fotogalerij. */\nexport function galleryJsonLd(items: { url: string; caption?: string }[]): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"ImageGallery\",\n image: items.map((it) => ({ \"@type\": \"ImageObject\", contentUrl: it.url, ...(it.caption ? { caption: it.caption } : {}) })),\n };\n}\n\n/** VideoObject-JSON-LD. */\nexport function videoObjectJsonLd(v: {\n name: string;\n description?: string;\n thumbnailUrl?: string;\n contentUrl?: string;\n uploadDate?: string;\n}): Record<string, unknown> {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"VideoObject\",\n name: v.name,\n ...(v.description ? { description: v.description } : {}),\n ...(v.thumbnailUrl ? { thumbnailUrl: v.thumbnailUrl } : {}),\n ...(v.contentUrl ? { contentUrl: v.contentUrl } : {}),\n ...(v.uploadDate ? { uploadDate: v.uploadDate } : {}),\n };\n}\n\n/** Bouwt een JSON-LD-object voor een pagina (schema.org). */\nexport function buildJsonLd(\n page: Pick<Page, \"title\" | \"path\" | \"seo\">,\n site: SeoSiteContext,\n): Record<string, unknown> {\n const type = page.seo?.jsonLdType ?? \"WebPage\";\n const base = site.domain ? `https://${site.domain.replace(/^https?:\\/\\//, \"\")}` : \"\";\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": type,\n name: page.seo?.title ?? page.title,\n description: page.seo?.description ?? site.seoDefaults?.description,\n // Alleen een url-node met een echte origin: een relatieve url (\"/over-ons\") is ongeldige\n // structured data wanneer het domein (nog) niet geconfigureerd is.\n ...(base ? { url: base + page.path } : {}),\n publisher: { \"@type\": \"Organization\", name: site.name },\n };\n}\n"],"mappings":"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","names":[]}
@@ -5,21 +5,33 @@ import type { SiteConfig } from "../schema/siteConfig";
5
5
  * Live, publiek-veilige site-instellingen (`sites/{siteId}/settings/public`) zodat dingen die de
6
6
  * klant in /beheer wijzigt (footer, nav, thema, module-instellingen) direct op de site verschijnen.
7
7
  *
8
- * - Publiek (live site): één `getDoc` (geen per-bezoeker listener).
9
- * - `live` (editor/preview): `onSnapshot` zodat wijzigingen meteen zichtbaar zijn.
8
+ * - Met SSR-seed (`initial`, via XiCMSProvider `settingsData`): géén client-read voor publiek —
9
+ * de settings zitten al in de server-HTML (geen theme-flits) en verversen via ISR.
10
+ * - Zonder seed (publiek): één `getDoc` (geen per-bezoeker listener).
11
+ * - Editors: `useSiteSettingsLiveSync` (onSnapshot) zodat /beheer-wijzigingen meteen zichtbaar zijn.
10
12
  * Zonder Firebase of zonder doc valt alles terug op de statische `SiteConfig` (identiek aan vroeger).
11
13
  */
12
14
  interface Ctx {
13
15
  settings: PublicSiteSettings;
14
16
  loaded: boolean;
17
+ /** Interne setter voor de editor-live-sync (useSiteSettingsLiveSync). */
18
+ _set?: (s: PublicSiteSettings) => void;
15
19
  }
16
- export declare function SiteSettingsProvider({ siteId, live, children, }: {
20
+ export declare function SiteSettingsProvider({ siteId, live, initial, children, }: {
17
21
  siteId: string;
18
22
  live?: boolean;
23
+ /** SSR-startset (server-side gelezen via `getPublicSiteSettings`); slaat de client-read over. */
24
+ initial?: PublicSiteSettings;
19
25
  children: ReactNode;
20
26
  }): import("react").JSX.Element;
21
27
  /** Ruwe publieke settings + laadstatus. */
22
28
  export declare function useSiteSettings(): Ctx;
29
+ /**
30
+ * Editor-live-sync: onSnapshot op `settings/public` zolang `enabled` (ingelogde editor). Aparte
31
+ * hook (i.p.v. `live` op de provider) zodat publiek nooit een listener krijgt — zelfde patroon
32
+ * als CustomBlocksSync in XiCMSProvider. Een leesfout laat de bestaande settings staan.
33
+ */
34
+ export declare function useSiteSettingsLiveSync(siteId: string, enabled: boolean): void;
23
35
  /** SiteConfig met de live Firestore-settings eroverheen (Firestore wint waar gezet). */
24
36
  export declare function useMergedSiteConfig(): SiteConfig;
25
37
  export {};
@@ -12,11 +12,14 @@ const SettingsContext = createContext({ settings: {}, loaded: false });
12
12
  function SiteSettingsProvider({
13
13
  siteId,
14
14
  live = false,
15
+ initial,
15
16
  children
16
17
  }) {
17
- const [settings, setSettings] = useState({});
18
- const [loaded, setLoaded] = useState(false);
18
+ const hasInitial = initial !== void 0;
19
+ const [settings, setSettings] = useState(initial ?? {});
20
+ const [loaded, setLoaded] = useState(hasInitial);
19
21
  useEffect(() => {
22
+ if (hasInitial && !live) return;
20
23
  const db = getDb();
21
24
  if (!db) {
22
25
  setLoaded(true);
@@ -40,12 +43,29 @@ function SiteSettingsProvider({
40
43
  return () => {
41
44
  cancelled = true;
42
45
  };
43
- }, [siteId, live]);
44
- return /* @__PURE__ */ jsx(SettingsContext.Provider, { value: { settings, loaded }, children });
46
+ }, [siteId, live, hasInitial]);
47
+ const value = useMemo(() => ({ settings, loaded, _set: setSettings }), [settings, loaded]);
48
+ return /* @__PURE__ */ jsx(SettingsContext.Provider, { value, children });
45
49
  }
46
50
  function useSiteSettings() {
47
51
  return useContext(SettingsContext);
48
52
  }
53
+ function useSiteSettingsLiveSync(siteId, enabled) {
54
+ const { _set } = useContext(SettingsContext);
55
+ useEffect(() => {
56
+ if (!enabled || !_set) return;
57
+ const db = getDb();
58
+ if (!db) return;
59
+ return onSnapshot(
60
+ doc(db, "sites", siteId, "settings", "public"),
61
+ (snap) => {
62
+ const res = PublicSiteSettingsSchema.safeParse(snap.exists() ? snap.data() : {});
63
+ if (res.success) _set(res.data);
64
+ },
65
+ () => void 0
66
+ );
67
+ }, [enabled, siteId, _set]);
68
+ }
49
69
  function useMergedSiteConfig() {
50
70
  const base = useSiteConfig();
51
71
  const { settings } = useSiteSettings();
@@ -63,6 +83,7 @@ function useMergedSiteConfig() {
63
83
  export {
64
84
  SiteSettingsProvider,
65
85
  useMergedSiteConfig,
66
- useSiteSettings
86
+ useSiteSettings,
87
+ useSiteSettingsLiveSync
67
88
  };
68
89
  //# sourceMappingURL=useSiteSettings.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/settings/useSiteSettings.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { doc, getDoc, onSnapshot } from \"firebase/firestore\";\nimport { getDb } from \"../firebase/client\";\nimport { useSiteConfig } from \"../config\";\nimport {\n PublicSiteSettingsSchema,\n mergeThemeTokens,\n type PublicSiteSettings,\n} from \"../schema/settings\";\nimport type { SiteConfig } from \"../schema/siteConfig\";\n\n/**\n * Live, publiek-veilige site-instellingen (`sites/{siteId}/settings/public`) zodat dingen die de\n * klant in /beheer wijzigt (footer, nav, thema, module-instellingen) direct op de site verschijnen.\n *\n * - Publiek (live site): één `getDoc` (geen per-bezoeker listener).\n * - `live` (editor/preview): `onSnapshot` zodat wijzigingen meteen zichtbaar zijn.\n * Zonder Firebase of zonder doc valt alles terug op de statische `SiteConfig` (identiek aan vroeger).\n */\ninterface Ctx {\n settings: PublicSiteSettings;\n loaded: boolean;\n}\nconst SettingsContext = createContext<Ctx>({ settings: {}, loaded: false });\n\nexport function SiteSettingsProvider({\n siteId,\n live = false,\n children,\n}: {\n siteId: string;\n live?: boolean;\n children: ReactNode;\n}) {\n const [settings, setSettings] = useState<PublicSiteSettings>({});\n const [loaded, setLoaded] = useState(false);\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n const ref = doc(db, \"sites\", siteId, \"settings\", \"public\");\n const apply = (data: unknown) => {\n const res = PublicSiteSettingsSchema.safeParse(data ?? {});\n if (res.success) setSettings(res.data);\n setLoaded(true);\n };\n if (live) {\n return onSnapshot(\n ref,\n (snap) => apply(snap.exists() ? snap.data() : {}),\n () => setLoaded(true),\n );\n }\n let cancelled = false;\n getDoc(ref)\n .then((snap) => !cancelled && apply(snap.exists() ? snap.data() : {}))\n .catch(() => !cancelled && setLoaded(true));\n return () => {\n cancelled = true;\n };\n }, [siteId, live]);\n\n return <SettingsContext.Provider value={{ settings, loaded }}>{children}</SettingsContext.Provider>;\n}\n\n/** Ruwe publieke settings + laadstatus. */\nexport function useSiteSettings(): Ctx {\n return useContext(SettingsContext);\n}\n\n/** SiteConfig met de live Firestore-settings eroverheen (Firestore wint waar gezet). */\nexport function useMergedSiteConfig(): SiteConfig {\n const base = useSiteConfig();\n const { settings } = useSiteSettings();\n return useMemo(() => {\n const nav = settings.nav && settings.nav.length > 0 ? settings.nav : base.nav;\n return {\n ...base,\n name: settings.name ?? base.name,\n nav,\n theme: mergeThemeTokens(base.theme, settings.theme),\n seoDefaults: settings.seoDefaults ?? base.seoDefaults,\n };\n }, [base, settings]);\n}\n"],"mappings":";AAmES;AAjET,SAAS,eAAe,YAAY,WAAW,SAAS,gBAAgC;AACxF,SAAS,KAAK,QAAQ,kBAAkB;AACxC,SAAS,aAAa;AACtB,SAAS,qBAAqB;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AAeP,MAAM,kBAAkB,cAAmB,EAAE,UAAU,CAAC,GAAG,QAAQ,MAAM,CAAC;AAEnE,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA,OAAO;AAAA,EACP;AACF,GAIG;AACD,QAAM,CAAC,UAAU,WAAW,IAAI,SAA6B,CAAC,CAAC;AAC/D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAE1C,YAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,IAAI;AACP,gBAAU,IAAI;AACd;AAAA,IACF;AACA,UAAM,MAAM,IAAI,IAAI,SAAS,QAAQ,YAAY,QAAQ;AACzD,UAAM,QAAQ,CAAC,SAAkB;AAC/B,YAAM,MAAM,yBAAyB,UAAU,QAAQ,CAAC,CAAC;AACzD,UAAI,IAAI,QAAS,aAAY,IAAI,IAAI;AACrC,gBAAU,IAAI;AAAA,IAChB;AACA,QAAI,MAAM;AACR,aAAO;AAAA,QACL;AAAA,QACA,CAAC,SAAS,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC;AAAA,QAChD,MAAM,UAAU,IAAI;AAAA,MACtB;AAAA,IACF;AACA,QAAI,YAAY;AAChB,WAAO,GAAG,EACP,KAAK,CAAC,SAAS,CAAC,aAAa,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EACpE,MAAM,MAAM,CAAC,aAAa,UAAU,IAAI,CAAC;AAC5C,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,QAAQ,IAAI,CAAC;AAEjB,SAAO,oBAAC,gBAAgB,UAAhB,EAAyB,OAAO,EAAE,UAAU,OAAO,GAAI,UAAS;AAC1E;AAGO,SAAS,kBAAuB;AACrC,SAAO,WAAW,eAAe;AACnC;AAGO,SAAS,sBAAkC;AAChD,QAAM,OAAO,cAAc;AAC3B,QAAM,EAAE,SAAS,IAAI,gBAAgB;AACrC,SAAO,QAAQ,MAAM;AACnB,UAAM,MAAM,SAAS,OAAO,SAAS,IAAI,SAAS,IAAI,SAAS,MAAM,KAAK;AAC1E,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM,SAAS,QAAQ,KAAK;AAAA,MAC5B;AAAA,MACA,OAAO,iBAAiB,KAAK,OAAO,SAAS,KAAK;AAAA,MAClD,aAAa,SAAS,eAAe,KAAK;AAAA,IAC5C;AAAA,EACF,GAAG,CAAC,MAAM,QAAQ,CAAC;AACrB;","names":[]}
1
+ {"version":3,"sources":["../../src/settings/useSiteSettings.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { doc, getDoc, onSnapshot } from \"firebase/firestore\";\nimport { getDb } from \"../firebase/client\";\nimport { useSiteConfig } from \"../config\";\nimport {\n PublicSiteSettingsSchema,\n mergeThemeTokens,\n type PublicSiteSettings,\n} from \"../schema/settings\";\nimport type { SiteConfig } from \"../schema/siteConfig\";\n\n/**\n * Live, publiek-veilige site-instellingen (`sites/{siteId}/settings/public`) zodat dingen die de\n * klant in /beheer wijzigt (footer, nav, thema, module-instellingen) direct op de site verschijnen.\n *\n * - Met SSR-seed (`initial`, via XiCMSProvider `settingsData`): géén client-read voor publiek —\n * de settings zitten al in de server-HTML (geen theme-flits) en verversen via ISR.\n * - Zonder seed (publiek): één `getDoc` (geen per-bezoeker listener).\n * - Editors: `useSiteSettingsLiveSync` (onSnapshot) zodat /beheer-wijzigingen meteen zichtbaar zijn.\n * Zonder Firebase of zonder doc valt alles terug op de statische `SiteConfig` (identiek aan vroeger).\n */\ninterface Ctx {\n settings: PublicSiteSettings;\n loaded: boolean;\n /** Interne setter voor de editor-live-sync (useSiteSettingsLiveSync). */\n _set?: (s: PublicSiteSettings) => void;\n}\nconst SettingsContext = createContext<Ctx>({ settings: {}, loaded: false });\n\nexport function SiteSettingsProvider({\n siteId,\n live = false,\n initial,\n children,\n}: {\n siteId: string;\n live?: boolean;\n /** SSR-startset (server-side gelezen via `getPublicSiteSettings`); slaat de client-read over. */\n initial?: PublicSiteSettings;\n children: ReactNode;\n}) {\n const hasInitial = initial !== undefined;\n const [settings, setSettings] = useState<PublicSiteSettings>(initial ?? {});\n const [loaded, setLoaded] = useState(hasInitial);\n\n useEffect(() => {\n // Met een SSR-seed doet de publieke site géén client-read meer: de layout-route is ISR-gecached\n // en levert verse settings per revalidate; editors krijgen apart een live sync.\n if (hasInitial && !live) return;\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n const ref = doc(db, \"sites\", siteId, \"settings\", \"public\");\n const apply = (data: unknown) => {\n const res = PublicSiteSettingsSchema.safeParse(data ?? {});\n if (res.success) setSettings(res.data);\n setLoaded(true);\n };\n if (live) {\n return onSnapshot(\n ref,\n (snap) => apply(snap.exists() ? snap.data() : {}),\n () => setLoaded(true),\n );\n }\n let cancelled = false;\n getDoc(ref)\n .then((snap) => !cancelled && apply(snap.exists() ? snap.data() : {}))\n .catch(() => !cancelled && setLoaded(true));\n return () => {\n cancelled = true;\n };\n }, [siteId, live, hasInitial]);\n\n const value = useMemo<Ctx>(() => ({ settings, loaded, _set: setSettings }), [settings, loaded]);\n return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;\n}\n\n/** Ruwe publieke settings + laadstatus. */\nexport function useSiteSettings(): Ctx {\n return useContext(SettingsContext);\n}\n\n/**\n * Editor-live-sync: onSnapshot op `settings/public` zolang `enabled` (ingelogde editor). Aparte\n * hook (i.p.v. `live` op de provider) zodat publiek nooit een listener krijgt — zelfde patroon\n * als CustomBlocksSync in XiCMSProvider. Een leesfout laat de bestaande settings staan.\n */\nexport function useSiteSettingsLiveSync(siteId: string, enabled: boolean) {\n const { _set } = useContext(SettingsContext);\n useEffect(() => {\n if (!enabled || !_set) return;\n const db = getDb();\n if (!db) return;\n return onSnapshot(\n doc(db, \"sites\", siteId, \"settings\", \"public\"),\n (snap) => {\n const res = PublicSiteSettingsSchema.safeParse(snap.exists() ? snap.data() : {});\n if (res.success) _set(res.data);\n },\n () => undefined,\n );\n }, [enabled, siteId, _set]);\n}\n\n/** SiteConfig met de live Firestore-settings eroverheen (Firestore wint waar gezet). */\nexport function useMergedSiteConfig(): SiteConfig {\n const base = useSiteConfig();\n const { settings } = useSiteSettings();\n return useMemo(() => {\n const nav = settings.nav && settings.nav.length > 0 ? settings.nav : base.nav;\n return {\n ...base,\n name: settings.name ?? base.name,\n nav,\n theme: mergeThemeTokens(base.theme, settings.theme),\n seoDefaults: settings.seoDefaults ?? base.seoDefaults,\n };\n }, [base, settings]);\n}\n"],"mappings":";AA+ES;AA7ET,SAAS,eAAe,YAAY,WAAW,SAAS,gBAAgC;AACxF,SAAS,KAAK,QAAQ,kBAAkB;AACxC,SAAS,aAAa;AACtB,SAAS,qBAAqB;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AAmBP,MAAM,kBAAkB,cAAmB,EAAE,UAAU,CAAC,GAAG,QAAQ,MAAM,CAAC;AAEnE,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AACF,GAMG;AACD,QAAM,aAAa,YAAY;AAC/B,QAAM,CAAC,UAAU,WAAW,IAAI,SAA6B,WAAW,CAAC,CAAC;AAC1E,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,UAAU;AAE/C,YAAU,MAAM;AAGd,QAAI,cAAc,CAAC,KAAM;AACzB,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,IAAI;AACP,gBAAU,IAAI;AACd;AAAA,IACF;AACA,UAAM,MAAM,IAAI,IAAI,SAAS,QAAQ,YAAY,QAAQ;AACzD,UAAM,QAAQ,CAAC,SAAkB;AAC/B,YAAM,MAAM,yBAAyB,UAAU,QAAQ,CAAC,CAAC;AACzD,UAAI,IAAI,QAAS,aAAY,IAAI,IAAI;AACrC,gBAAU,IAAI;AAAA,IAChB;AACA,QAAI,MAAM;AACR,aAAO;AAAA,QACL;AAAA,QACA,CAAC,SAAS,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC;AAAA,QAChD,MAAM,UAAU,IAAI;AAAA,MACtB;AAAA,IACF;AACA,QAAI,YAAY;AAChB,WAAO,GAAG,EACP,KAAK,CAAC,SAAS,CAAC,aAAa,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EACpE,MAAM,MAAM,CAAC,aAAa,UAAU,IAAI,CAAC;AAC5C,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,QAAQ,MAAM,UAAU,CAAC;AAE7B,QAAM,QAAQ,QAAa,OAAO,EAAE,UAAU,QAAQ,MAAM,YAAY,IAAI,CAAC,UAAU,MAAM,CAAC;AAC9F,SAAO,oBAAC,gBAAgB,UAAhB,EAAyB,OAAe,UAAS;AAC3D;AAGO,SAAS,kBAAuB;AACrC,SAAO,WAAW,eAAe;AACnC;AAOO,SAAS,wBAAwB,QAAgB,SAAkB;AACxE,QAAM,EAAE,KAAK,IAAI,WAAW,eAAe;AAC3C,YAAU,MAAM;AACd,QAAI,CAAC,WAAW,CAAC,KAAM;AACvB,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,WAAO;AAAA,MACL,IAAI,IAAI,SAAS,QAAQ,YAAY,QAAQ;AAAA,MAC7C,CAAC,SAAS;AACR,cAAM,MAAM,yBAAyB,UAAU,KAAK,OAAO,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC;AAC/E,YAAI,IAAI,QAAS,MAAK,IAAI,IAAI;AAAA,MAChC;AAAA,MACA,MAAM;AAAA,IACR;AAAA,EACF,GAAG,CAAC,SAAS,QAAQ,IAAI,CAAC;AAC5B;AAGO,SAAS,sBAAkC;AAChD,QAAM,OAAO,cAAc;AAC3B,QAAM,EAAE,SAAS,IAAI,gBAAgB;AACrC,SAAO,QAAQ,MAAM;AACnB,UAAM,MAAM,SAAS,OAAO,SAAS,IAAI,SAAS,IAAI,SAAS,MAAM,KAAK;AAC1E,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM,SAAS,QAAQ,KAAK;AAAA,MAC5B;AAAA,MACA,OAAO,iBAAiB,KAAK,OAAO,SAAS,KAAK;AAAA,MAClD,aAAa,SAAS,eAAe,KAAK;AAAA,IAC5C;AAAA,EACF,GAAG,CAAC,MAAM,QAAQ,CAAC;AACrB;","names":[]}
package/dist/styles.css CHANGED
@@ -370,6 +370,13 @@
370
370
  .xicms-ve__inserter {
371
371
  flex-wrap: wrap;
372
372
  }
373
+ /* Touch-raakdoelen: de blok-toolbarknoppen (✏️ ↑ ↓ ⎘ 🗑) waren op een telefoon te klein om
374
+ betrouwbaar te raken (WCAG raakdoel ≥ 44px op mobiel). */
375
+ .xicms-ve-block__toolbar button {
376
+ min-width: 40px;
377
+ min-height: 40px;
378
+ font-size: 1rem;
379
+ }
373
380
  .xicms-ve__inserter-select {
374
381
  width: 100%;
375
382
  }
@@ -1074,6 +1081,13 @@
1074
1081
  grid-template-columns: 1fr !important;
1075
1082
  }
1076
1083
  }
1084
+ /* Tablet: 3-4 kolommen squishen tussen 641-959px — terug naar 2 (mobiel heeft al 1). */
1085
+ @media (min-width: 641px) and (max-width: 959px) {
1086
+ .xicms-columns[data-cols="3"],
1087
+ .xicms-columns[data-cols="4"] {
1088
+ grid-template-columns: repeat(2, 1fr) !important;
1089
+ }
1090
+ }
1077
1091
  .xicms-newslist {
1078
1092
  margin: 1.5rem 0;
1079
1093
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiwebdesign/xicms-core",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
4
4
  "description": "XiCMS engine: gestructureerde content, pagina's, draft/publish, admin (TipTap+Radix), SEO en beveiliging voor klantsites.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -42,7 +42,11 @@
42
42
  },
43
43
  "./layers.css": "./dist/layers.css",
44
44
  "./styles.css": "./dist/styles.css",
45
- "./admin.css": "./dist/admin.css"
45
+ "./admin.css": "./dist/admin.css",
46
+ "./demo": {
47
+ "types": "./dist/demo/index.d.ts",
48
+ "import": "./dist/demo/index.js"
49
+ }
46
50
  },
47
51
  "files": [
48
52
  "dist"