@xiwebdesign/xicms-core 7.5.2 → 7.7.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 (44) hide show
  1. package/dist/admin/AdminApp.js +6 -5
  2. package/dist/admin/AdminApp.js.map +1 -1
  3. package/dist/admin/NewsEditor.js +15 -4
  4. package/dist/admin/NewsEditor.js.map +1 -1
  5. package/dist/admin/menu/MenuPanel.d.ts +20 -0
  6. package/dist/admin/menu/MenuPanel.js +144 -0
  7. package/dist/admin/menu/MenuPanel.js.map +1 -0
  8. package/dist/admin/panelRegistry.d.ts +10 -0
  9. package/dist/admin/panelRegistry.js +5 -1
  10. package/dist/admin/panelRegistry.js.map +1 -1
  11. package/dist/admin/settings/SettingsPanel.js +10 -2
  12. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  13. package/dist/admin/ui/dialog.js +6 -3
  14. package/dist/admin/ui/dialog.js.map +1 -1
  15. package/dist/admin/ui/toast.d.ts +16 -0
  16. package/dist/admin/ui/toast.js +8 -4
  17. package/dist/admin/ui/toast.js.map +1 -1
  18. package/dist/admin/visual/VisualEditor.js +89 -123
  19. package/dist/admin/visual/VisualEditor.js.map +1 -1
  20. package/dist/admin.css +1 -1
  21. package/dist/auth/AuthProvider.js +23 -5
  22. package/dist/auth/AuthProvider.js.map +1 -1
  23. package/dist/auth/LoginForm.js +7 -2
  24. package/dist/auth/LoginForm.js.map +1 -1
  25. package/dist/components/ExternEmbed.d.ts +23 -0
  26. package/dist/components/ExternEmbed.js +52 -0
  27. package/dist/components/ExternEmbed.js.map +1 -0
  28. package/dist/editor/EditorProvider.d.ts +8 -0
  29. package/dist/editor/EditorProvider.js +30 -6
  30. package/dist/editor/EditorProvider.js.map +1 -1
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +2 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/render/PublishedPage.js +9 -1
  35. package/dist/render/PublishedPage.js.map +1 -1
  36. package/dist/schema/settings.d.ts +62 -1
  37. package/dist/schema/settings.js +23 -1
  38. package/dist/schema/settings.js.map +1 -1
  39. package/dist/schema/siteConfig.d.ts +5 -0
  40. package/dist/settings/useSiteSettings.js +1 -1
  41. package/dist/settings/useSiteSettings.js.map +1 -1
  42. package/dist/styles.css +67 -12
  43. package/dist/version.js +1 -1
  44. package/package.json +1 -1
package/dist/admin.css CHANGED
@@ -1,2 +1,2 @@
1
1
  /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-indigo-500:oklch(58.5% .233 277.117);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-900:oklch(20.8% .042 265.755);--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-3xl:48rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--color-brand-50:#eef2ff;--color-brand-500:#4f46e5;--color-brand-600:#4f46e5;--color-brand-700:#3730a3}}@scope(.xicms-admin){@layer utilities{.visible{visibility:visible!important}.absolute{position:absolute!important}.fixed{position:fixed!important}.relative{position:relative!important}.static{position:static!important}.sticky{position:sticky!important}.-top-2{top:calc(var(--spacing) * -2)!important}.top-0{top:0!important}.-right-2{right:calc(var(--spacing) * -2)!important}.bottom-0{bottom:0!important}.z-10{z-index:10!important}.container{width:100%!important}@media (min-width:40rem){.container{max-width:40rem!important}}@media (min-width:48rem){.container{max-width:48rem!important}}@media (min-width:64rem){.container{max-width:64rem!important}}@media (min-width:80rem){.container{max-width:80rem!important}}@media (min-width:96rem){.container{max-width:96rem!important}}.-mx-1{margin-inline:calc(var(--spacing) * -1)!important}.-mx-4{margin-inline:calc(var(--spacing) * -4)!important}.mt-0\.5{margin-top:calc(var(--spacing) * .5)!important}.mt-1{margin-top:var(--spacing)!important}.mt-2{margin-top:calc(var(--spacing) * 2)!important}.mt-3{margin-top:calc(var(--spacing) * 3)!important}.mt-4{margin-top:calc(var(--spacing) * 4)!important}.mt-5{margin-top:calc(var(--spacing) * 5)!important}.mt-6{margin-top:calc(var(--spacing) * 6)!important}.mt-auto{margin-top:auto!important}.mr-1{margin-right:var(--spacing)!important}.mr-2{margin-right:calc(var(--spacing) * 2)!important}.mb-1{margin-bottom:var(--spacing)!important}.mb-2{margin-bottom:calc(var(--spacing) * 2)!important}.mb-3{margin-bottom:calc(var(--spacing) * 3)!important}.mb-4{margin-bottom:calc(var(--spacing) * 4)!important}.ml-1{margin-left:var(--spacing)!important}.ml-auto{margin-left:auto!important}.block{display:block!important}.contents{display:contents!important}.flex{display:flex!important}.grid{display:grid!important}.hidden{display:none!important}.inline{display:inline!important}.inline-block{display:inline-block!important}.inline-flex{display:inline-flex!important}.table{display:table!important}.aspect-square{aspect-ratio:1!important}.h-5{height:calc(var(--spacing) * 5)!important}.h-6{height:calc(var(--spacing) * 6)!important}.h-8{height:calc(var(--spacing) * 8)!important}.h-9{height:calc(var(--spacing) * 9)!important}.h-10{height:calc(var(--spacing) * 10)!important}.h-14{height:calc(var(--spacing) * 14)!important}.h-16{height:calc(var(--spacing) * 16)!important}.h-28{height:calc(var(--spacing) * 28)!important}.h-32{height:calc(var(--spacing) * 32)!important}.h-\[70vh\]{height:70vh!important}.h-fit{height:fit-content!important}.max-h-\[55vh\]{max-height:55vh!important}.max-h-\[60vh\]{max-height:60vh!important}.min-h-\[8rem\]{min-height:8rem!important}.min-h-screen{min-height:100vh!important}.w-5{width:calc(var(--spacing) * 5)!important}.w-6{width:calc(var(--spacing) * 6)!important}.w-8{width:calc(var(--spacing) * 8)!important}.w-14{width:calc(var(--spacing) * 14)!important}.w-16{width:calc(var(--spacing) * 16)!important}.w-20{width:calc(var(--spacing) * 20)!important}.w-24{width:calc(var(--spacing) * 24)!important}.w-28{width:calc(var(--spacing) * 28)!important}.w-32{width:calc(var(--spacing) * 32)!important}.w-36{width:calc(var(--spacing) * 36)!important}.w-56{width:calc(var(--spacing) * 56)!important}.w-fit{width:fit-content!important}.w-full{width:100%!important}.max-w-3xl{max-width:var(--container-3xl)!important}.max-w-4xl{max-width:var(--container-4xl)!important}.max-w-md{max-width:var(--container-md)!important}.max-w-sm{max-width:var(--container-sm)!important}.max-w-xs{max-width:var(--container-xs)!important}.min-w-0{min-width:0!important}.min-w-8{min-width:calc(var(--spacing) * 8)!important}.flex-1{flex:1!important}.shrink-0{flex-shrink:0!important}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1)!important;translate:var(--tw-translate-x) var(--tw-translate-y)!important}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)!important}.cursor-pointer{cursor:pointer!important}.resize{resize:both!important}.list-decimal{list-style-type:decimal!important}.list-disc{list-style-type:disc!important}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))!important}.grid-cols-\[120px_1fr\]{grid-template-columns:120px 1fr!important}.flex-col{flex-direction:column!important}.flex-wrap{flex-wrap:wrap!important}.place-items-center{place-items:center!important}.items-center{align-items:center!important}.items-end{align-items:flex-end!important}.items-start{align-items:flex-start!important}.justify-between{justify-content:space-between!important}.justify-center{justify-content:center!important}.justify-end{justify-content:flex-end!important}.gap-0\.5{gap:calc(var(--spacing) * .5)!important}.gap-1{gap:var(--spacing)!important}.gap-1\.5{gap:calc(var(--spacing) * 1.5)!important}.gap-2{gap:calc(var(--spacing) * 2)!important}.gap-2\.5{gap:calc(var(--spacing) * 2.5)!important}.gap-3{gap:calc(var(--spacing) * 3)!important}.gap-4{gap:calc(var(--spacing) * 4)!important}.gap-6{gap:calc(var(--spacing) * 6)!important}.self-center{align-self:center!important}.self-end{align-self:flex-end!important}.truncate{text-overflow:ellipsis!important;white-space:nowrap!important;overflow:hidden!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-x-auto{overflow-x:auto!important}.overflow-y-auto{overflow-y:auto!important}.rounded{border-radius:.25rem!important}.rounded-full{border-radius:3.40282e38px!important}.rounded-lg{border-radius:var(--radius-lg)!important}.rounded-md{border-radius:var(--radius-md)!important}.rounded-xl{border-radius:var(--radius-xl)!important}.rounded-t{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-t-lg{border-top-left-radius:var(--radius-lg)!important;border-top-right-radius:var(--radius-lg)!important}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg)!important;border-bottom-left-radius:var(--radius-lg)!important}.border{border-style:var(--tw-border-style)!important;border-width:1px!important}.border-t{border-top-style:var(--tw-border-style)!important;border-top-width:1px!important}.border-t-0{border-top-style:var(--tw-border-style)!important;border-top-width:0!important}.border-b{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:1px!important}.border-amber-300{border-color:var(--color-amber-300)!important}.border-brand-500{border-color:var(--color-brand-500)!important}.border-red-200{border-color:var(--color-red-200)!important}.border-slate-100{border-color:var(--color-slate-100)!important}.border-slate-200{border-color:var(--color-slate-200)!important}.border-slate-300{border-color:var(--color-slate-300)!important}.bg-amber-50{background-color:var(--color-amber-50)!important}.bg-brand-50{background-color:var(--color-brand-50)!important}.bg-brand-500{background-color:var(--color-brand-500)!important}.bg-red-50{background-color:var(--color-red-50)!important}.bg-slate-50{background-color:var(--color-slate-50)!important}.bg-slate-100{background-color:var(--color-slate-100)!important}.bg-slate-200{background-color:var(--color-slate-200)!important}.bg-slate-700{background-color:var(--color-slate-700)!important}.bg-white{background-color:var(--color-white)!important}.object-cover{object-fit:cover!important}.p-0\.5{padding:calc(var(--spacing) * .5)!important}.p-1{padding:var(--spacing)!important}.p-2{padding:calc(var(--spacing) * 2)!important}.p-3{padding:calc(var(--spacing) * 3)!important}.p-4{padding:calc(var(--spacing) * 4)!important}.p-5{padding:calc(var(--spacing) * 5)!important}.p-6{padding:calc(var(--spacing) * 6)!important}.px-1{padding-inline:var(--spacing)!important}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)!important}.px-2{padding-inline:calc(var(--spacing) * 2)!important}.px-3{padding-inline:calc(var(--spacing) * 3)!important}.px-4{padding-inline:calc(var(--spacing) * 4)!important}.py-0\.5{padding-block:calc(var(--spacing) * .5)!important}.py-1{padding-block:var(--spacing)!important}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)!important}.py-2{padding-block:calc(var(--spacing) * 2)!important}.py-3{padding-block:calc(var(--spacing) * 3)!important}.py-8{padding-block:calc(var(--spacing) * 8)!important}.py-16{padding-block:calc(var(--spacing) * 16)!important}.pt-0{padding-top:0!important}.pt-1{padding-top:var(--spacing)!important}.pt-3{padding-top:calc(var(--spacing) * 3)!important}.pt-4{padding-top:calc(var(--spacing) * 4)!important}.pt-5{padding-top:calc(var(--spacing) * 5)!important}.pr-2{padding-right:calc(var(--spacing) * 2)!important}.pl-5{padding-left:calc(var(--spacing) * 5)!important}.text-center{text-align:center!important}.text-left{text-align:left!important}.text-right{text-align:right!important}.align-top{vertical-align:top!important}.font-mono{font-family:var(--font-mono)!important}.text-3xl{font-size:var(--text-3xl)!important;line-height:var(--tw-leading,var(--text-3xl--line-height))!important}.text-base{font-size:var(--text-base)!important;line-height:var(--tw-leading,var(--text-base--line-height))!important}.text-lg{font-size:var(--text-lg)!important;line-height:var(--tw-leading,var(--text-lg--line-height))!important}.text-sm{font-size:var(--text-sm)!important;line-height:var(--tw-leading,var(--text-sm--line-height))!important}.text-xl{font-size:var(--text-xl)!important;line-height:var(--tw-leading,var(--text-xl--line-height))!important}.text-xs{font-size:var(--text-xs)!important;line-height:var(--tw-leading,var(--text-xs--line-height))!important}.text-\[11px\]{font-size:11px!important}.font-bold{--tw-font-weight:var(--font-weight-bold)!important;font-weight:var(--font-weight-bold)!important}.font-medium{--tw-font-weight:var(--font-weight-medium)!important;font-weight:var(--font-weight-medium)!important}.font-semibold{--tw-font-weight:var(--font-weight-semibold)!important;font-weight:var(--font-weight-semibold)!important}.tracking-wide{--tw-tracking:var(--tracking-wide)!important;letter-spacing:var(--tracking-wide)!important}.whitespace-pre-wrap{white-space:pre-wrap!important}.text-amber-600{color:var(--color-amber-600)!important}.text-amber-700{color:var(--color-amber-700)!important}.text-amber-800{color:var(--color-amber-800)!important}.text-brand-700{color:var(--color-brand-700)!important}.text-red-600{color:var(--color-red-600)!important}.text-red-700{color:var(--color-red-700)!important}.text-slate-400{color:var(--color-slate-400)!important}.text-slate-500{color:var(--color-slate-500)!important}.text-slate-600{color:var(--color-slate-600)!important}.text-slate-700{color:var(--color-slate-700)!important}.text-white{color:var(--color-white)!important}.uppercase{text-transform:uppercase!important}.italic{font-style:italic!important}.line-through{text-decoration-line:line-through!important}.underline{text-decoration-line:underline!important}.opacity-70{opacity:.7!important}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.outline{outline-style:var(--tw-outline-style)!important;outline-width:1px!important}.blur{--tw-blur:blur(8px)!important;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.grayscale{--tw-grayscale:grayscale(100%)!important;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events!important;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--default-transition-duration))!important}.transition-\[filter\]{transition-property:filter!important;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--default-transition-duration))!important}.ease-out{--tw-ease:var(--ease-out)!important;transition-timing-function:var(--ease-out)!important}.outline-none{--tw-outline-style:none!important;outline-style:none!important}@media (hover:hover){.group-hover\:opacity-80:is(:where(.group):hover *){opacity:.8!important}}.last\:border-0:last-child{border-style:var(--tw-border-style)!important;border-width:0!important}@media (hover:hover){.hover\:border-brand-500:hover{border-color:var(--color-brand-500)!important}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)!important}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)!important}.hover\:bg-slate-200:hover{background-color:var(--color-slate-200)!important}.hover\:text-slate-700:hover{color:var(--color-slate-700)!important}.hover\:text-slate-900:hover{color:var(--color-slate-900)!important}.hover\:underline:hover{text-decoration-line:underline!important}.hover\:brightness-95:hover{--tw-brightness:brightness(95%)!important;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.focus\:ring-indigo-500:focus{--tw-ring-color:var(--color-indigo-500)!important}.focus\:outline-none:focus{--tw-outline-style:none!important;outline-style:none!important}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.focus-visible\:ring-brand-500:focus-visible{--tw-ring-color:var(--color-brand-500)!important}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none!important;outline-style:none!important}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed!important}.disabled\:opacity-50:disabled{opacity:.5!important}.aria-\[current\=page\]\:bg-slate-200[aria-current=page]{background-color:var(--color-slate-200)!important}.aria-\[current\=page\]\:text-slate-900[aria-current=page]{color:var(--color-slate-900)!important}.data-\[state\=active\]\:border-b-2[data-state=active]{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:2px!important}.data-\[state\=active\]\:border-brand-600[data-state=active]{border-color:var(--color-brand-600)!important}.data-\[state\=active\]\:bg-slate-100[data-state=active]{background-color:var(--color-slate-100)!important}.data-\[state\=active\]\:text-brand-700[data-state=active]{color:var(--color-brand-700)!important}.data-\[state\=active\]\:text-slate-900[data-state=active]{color:var(--color-slate-900)!important}.data-\[state\=active\]\:shadow-\[inset_2px_0_0_0_var\(--color-brand-500\)\][data-state=active]{--tw-shadow:inset 2px 0 0 0 var(--tw-shadow-color,var(--color-brand-500))!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))!important}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))!important}}@media (min-width:48rem){.md\:sticky{position:sticky!important}.md\:top-0{top:0!important}.md\:col-span-5{grid-column:span 5/span 5!important}.md\:flex{display:flex!important}.md\:grid{display:grid!important}.md\:h-screen{height:100vh!important}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))!important}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))!important}.md\:grid-cols-\[1fr_1fr_140px_auto_auto\]{grid-template-columns:1fr 1fr 140px auto auto!important}.md\:grid-cols-\[1fr_110px\]{grid-template-columns:1fr 110px!important}.md\:grid-cols-\[1fr_130px_90px_110px_auto\]{grid-template-columns:1fr 130px 90px 110px auto!important}.md\:grid-cols-\[248px_1fr\]{grid-template-columns:248px 1fr!important}.md\:grid-cols-\[260px_1fr\]{grid-template-columns:260px 1fr!important}.md\:grid-cols-\[280px_1fr\]{grid-template-columns:280px 1fr!important}.md\:grid-cols-\[300px_1fr\]{grid-template-columns:300px 1fr!important}.md\:grid-cols-\[340px_1fr\]{grid-template-columns:340px 1fr!important}.md\:flex-col{flex-direction:column!important}.md\:overflow-visible{overflow:visible!important}.md\:border-r{border-right-style:var(--tw-border-style)!important;border-right-width:1px!important}.md\:border-b-0{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:0!important}.md\:p-8{padding:calc(var(--spacing) * 8)!important}.md\:px-8{padding-inline:calc(var(--spacing) * 8)!important}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2!important}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))!important}}}}.xicms-admin,.xicms-admin *,.xicms-admin :before,.xicms-admin :after{box-sizing:border-box}.xicms-admin{-webkit-font-smoothing:antialiased;--color-white:#fff;--color-slate-50:#faf9f6;--color-slate-100:#f3f1ec;--color-slate-200:#e5e2da;--color-slate-300:#d5d1c7;--color-slate-400:#a8a399;--color-slate-500:#6e6a62;--color-slate-600:#57534b;--color-slate-700:#403c36;--color-slate-900:#1b1a17;--color-brand:var(--xicms-primary,#4f46e5);--color-brand-500:var(--xicms-primary,#4f46e5);--color-brand-600:var(--xicms-primary,#4f46e5);--color-brand-700:var(--xicms-primary,#4f46e5);font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.5}@supports (color:color-mix(in lab, red, red)){.xicms-admin{--color-brand-700:color-mix(in srgb, var(--xicms-primary,#4f46e5) 78%, #1b1a17)}}.xicms-admin{--color-brand-50:var(--xicms-primary,#4f46e5)}@supports (color:color-mix(in lab, red, red)){.xicms-admin{--color-brand-50:color-mix(in srgb, var(--xicms-primary,#4f46e5) 10%, #fff)}}.xicms-admin{--admin-canvas:#f6f5f2;--admin-sidebar:#f0eee9;--color-background-body:var(--color-white);--color-background-surface:var(--color-white);--color-background-card:var(--color-white);--color-background-muted:var(--color-slate-100);--color-text-primary:var(--color-slate-900);--color-text-secondary:var(--color-slate-500);--color-border:var(--color-slate-200);--color-accent:var(--color-brand-500);--color-on-accent:#fff;--color-error:#dc2626;color-scheme:light;color:var(--color-text-primary);background:var(--admin-canvas)}.xicms-admin h1,.xicms-admin h2,.xicms-admin h3,.xicms-admin p,.xicms-admin figure{margin:0}.xicms-admin button{font:inherit;cursor:pointer}.xicms-admin input,.xicms-admin textarea,.xicms-admin select{font:inherit;color:inherit}.xicms-admin .ProseMirror{outline:none;min-height:8rem}.xicms-admin .ProseMirror p{margin:0 0 .6rem}.xicms-admin .ProseMirror:focus{outline:none}@media (prefers-reduced-motion:reduce){.xicms-admin *,.xicms-admin :before,.xicms-admin :after,.xicms-ve *,.xicms-ve :before,.xicms-ve :after{transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}.xicms-admin__midden,.xicms-admin__raster{min-height:100vh}.xicms-admin--ingebed .xicms-admin__midden{min-height:50vh}.xicms-admin--ingebed .xicms-admin__raster{min-height:60vh;display:block}.xicms-admin--ingebed .xicms-admin__rail{border-bottom-width:1px;border-right:0;flex-flow:wrap;align-items:center;gap:1rem;height:auto;padding:.5rem 1rem;position:static}.xicms-admin--ingebed .xicms-admin__lijst{flex-direction:row;overflow-x:auto}.xicms-admin--ingebed .xicms-admin__voet{border-top:0;flex-direction:row;align-items:center;gap:.75rem;margin-top:0;margin-left:auto;padding-top:0;display:flex}.xicms-admin--ingebed .xicms-admin__kop{position:static}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-indigo-500:oklch(58.5% .233 277.117);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-900:oklch(20.8% .042 265.755);--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-3xl:48rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--color-brand-50:#eef2ff;--color-brand-500:#4f46e5;--color-brand-600:#4f46e5;--color-brand-700:#3730a3}}@scope(.xicms-admin){@layer utilities{.visible{visibility:visible!important}.absolute{position:absolute!important}.fixed{position:fixed!important}.relative{position:relative!important}.static{position:static!important}.sticky{position:sticky!important}.-top-2{top:calc(var(--spacing) * -2)!important}.top-0{top:0!important}.-right-2{right:calc(var(--spacing) * -2)!important}.bottom-0{bottom:0!important}.z-10{z-index:10!important}.container{width:100%!important}@media (min-width:40rem){.container{max-width:40rem!important}}@media (min-width:48rem){.container{max-width:48rem!important}}@media (min-width:64rem){.container{max-width:64rem!important}}@media (min-width:80rem){.container{max-width:80rem!important}}@media (min-width:96rem){.container{max-width:96rem!important}}.-mx-1{margin-inline:calc(var(--spacing) * -1)!important}.-mx-4{margin-inline:calc(var(--spacing) * -4)!important}.mt-0\.5{margin-top:calc(var(--spacing) * .5)!important}.mt-1{margin-top:var(--spacing)!important}.mt-2{margin-top:calc(var(--spacing) * 2)!important}.mt-3{margin-top:calc(var(--spacing) * 3)!important}.mt-4{margin-top:calc(var(--spacing) * 4)!important}.mt-5{margin-top:calc(var(--spacing) * 5)!important}.mt-6{margin-top:calc(var(--spacing) * 6)!important}.mt-auto{margin-top:auto!important}.mr-1{margin-right:var(--spacing)!important}.mr-2{margin-right:calc(var(--spacing) * 2)!important}.mb-1{margin-bottom:var(--spacing)!important}.mb-2{margin-bottom:calc(var(--spacing) * 2)!important}.mb-3{margin-bottom:calc(var(--spacing) * 3)!important}.mb-4{margin-bottom:calc(var(--spacing) * 4)!important}.ml-1{margin-left:var(--spacing)!important}.ml-auto{margin-left:auto!important}.block{display:block!important}.contents{display:contents!important}.flex{display:flex!important}.grid{display:grid!important}.hidden{display:none!important}.inline{display:inline!important}.inline-block{display:inline-block!important}.inline-flex{display:inline-flex!important}.table{display:table!important}.aspect-square{aspect-ratio:1!important}.h-5{height:calc(var(--spacing) * 5)!important}.h-6{height:calc(var(--spacing) * 6)!important}.h-8{height:calc(var(--spacing) * 8)!important}.h-9{height:calc(var(--spacing) * 9)!important}.h-10{height:calc(var(--spacing) * 10)!important}.h-14{height:calc(var(--spacing) * 14)!important}.h-16{height:calc(var(--spacing) * 16)!important}.h-28{height:calc(var(--spacing) * 28)!important}.h-32{height:calc(var(--spacing) * 32)!important}.h-\[70vh\]{height:70vh!important}.h-fit{height:fit-content!important}.max-h-\[55vh\]{max-height:55vh!important}.max-h-\[60vh\]{max-height:60vh!important}.min-h-\[8rem\]{min-height:8rem!important}.min-h-screen{min-height:100vh!important}.w-5{width:calc(var(--spacing) * 5)!important}.w-6{width:calc(var(--spacing) * 6)!important}.w-8{width:calc(var(--spacing) * 8)!important}.w-14{width:calc(var(--spacing) * 14)!important}.w-16{width:calc(var(--spacing) * 16)!important}.w-20{width:calc(var(--spacing) * 20)!important}.w-24{width:calc(var(--spacing) * 24)!important}.w-28{width:calc(var(--spacing) * 28)!important}.w-32{width:calc(var(--spacing) * 32)!important}.w-36{width:calc(var(--spacing) * 36)!important}.w-56{width:calc(var(--spacing) * 56)!important}.w-fit{width:fit-content!important}.w-full{width:100%!important}.max-w-3xl{max-width:var(--container-3xl)!important}.max-w-4xl{max-width:var(--container-4xl)!important}.max-w-md{max-width:var(--container-md)!important}.max-w-sm{max-width:var(--container-sm)!important}.max-w-xs{max-width:var(--container-xs)!important}.min-w-0{min-width:0!important}.min-w-8{min-width:calc(var(--spacing) * 8)!important}.flex-1{flex:1!important}.shrink-0{flex-shrink:0!important}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1)!important;translate:var(--tw-translate-x) var(--tw-translate-y)!important}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)!important}.cursor-pointer{cursor:pointer!important}.resize{resize:both!important}.list-decimal{list-style-type:decimal!important}.list-disc{list-style-type:disc!important}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))!important}.grid-cols-\[120px_1fr\]{grid-template-columns:120px 1fr!important}.flex-col{flex-direction:column!important}.flex-wrap{flex-wrap:wrap!important}.place-items-center{place-items:center!important}.items-center{align-items:center!important}.items-end{align-items:flex-end!important}.items-start{align-items:flex-start!important}.justify-between{justify-content:space-between!important}.justify-center{justify-content:center!important}.justify-end{justify-content:flex-end!important}.gap-0\.5{gap:calc(var(--spacing) * .5)!important}.gap-1{gap:var(--spacing)!important}.gap-1\.5{gap:calc(var(--spacing) * 1.5)!important}.gap-2{gap:calc(var(--spacing) * 2)!important}.gap-2\.5{gap:calc(var(--spacing) * 2.5)!important}.gap-3{gap:calc(var(--spacing) * 3)!important}.gap-4{gap:calc(var(--spacing) * 4)!important}.gap-6{gap:calc(var(--spacing) * 6)!important}.self-center{align-self:center!important}.self-end{align-self:flex-end!important}.truncate{text-overflow:ellipsis!important;white-space:nowrap!important;overflow:hidden!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-x-auto{overflow-x:auto!important}.overflow-y-auto{overflow-y:auto!important}.rounded{border-radius:.25rem!important}.rounded-full{border-radius:3.40282e38px!important}.rounded-lg{border-radius:var(--radius-lg)!important}.rounded-md{border-radius:var(--radius-md)!important}.rounded-xl{border-radius:var(--radius-xl)!important}.rounded-t{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-t-lg{border-top-left-radius:var(--radius-lg)!important;border-top-right-radius:var(--radius-lg)!important}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg)!important;border-bottom-left-radius:var(--radius-lg)!important}.border{border-style:var(--tw-border-style)!important;border-width:1px!important}.border-t{border-top-style:var(--tw-border-style)!important;border-top-width:1px!important}.border-t-0{border-top-style:var(--tw-border-style)!important;border-top-width:0!important}.border-b{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:1px!important}.border-amber-300{border-color:var(--color-amber-300)!important}.border-brand-500{border-color:var(--color-brand-500)!important}.border-red-200{border-color:var(--color-red-200)!important}.border-slate-100{border-color:var(--color-slate-100)!important}.border-slate-200{border-color:var(--color-slate-200)!important}.border-slate-300{border-color:var(--color-slate-300)!important}.bg-amber-50{background-color:var(--color-amber-50)!important}.bg-brand-50{background-color:var(--color-brand-50)!important}.bg-brand-500{background-color:var(--color-brand-500)!important}.bg-red-50{background-color:var(--color-red-50)!important}.bg-slate-50{background-color:var(--color-slate-50)!important}.bg-slate-100{background-color:var(--color-slate-100)!important}.bg-slate-200{background-color:var(--color-slate-200)!important}.bg-slate-700{background-color:var(--color-slate-700)!important}.bg-white{background-color:var(--color-white)!important}.object-cover{object-fit:cover!important}.p-0\.5{padding:calc(var(--spacing) * .5)!important}.p-1{padding:var(--spacing)!important}.p-2{padding:calc(var(--spacing) * 2)!important}.p-3{padding:calc(var(--spacing) * 3)!important}.p-4{padding:calc(var(--spacing) * 4)!important}.p-5{padding:calc(var(--spacing) * 5)!important}.p-6{padding:calc(var(--spacing) * 6)!important}.px-1{padding-inline:var(--spacing)!important}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)!important}.px-2{padding-inline:calc(var(--spacing) * 2)!important}.px-3{padding-inline:calc(var(--spacing) * 3)!important}.px-4{padding-inline:calc(var(--spacing) * 4)!important}.py-0\.5{padding-block:calc(var(--spacing) * .5)!important}.py-1{padding-block:var(--spacing)!important}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)!important}.py-2{padding-block:calc(var(--spacing) * 2)!important}.py-3{padding-block:calc(var(--spacing) * 3)!important}.py-8{padding-block:calc(var(--spacing) * 8)!important}.py-16{padding-block:calc(var(--spacing) * 16)!important}.pt-0{padding-top:0!important}.pt-1{padding-top:var(--spacing)!important}.pt-3{padding-top:calc(var(--spacing) * 3)!important}.pt-4{padding-top:calc(var(--spacing) * 4)!important}.pt-5{padding-top:calc(var(--spacing) * 5)!important}.pr-2{padding-right:calc(var(--spacing) * 2)!important}.pl-5{padding-left:calc(var(--spacing) * 5)!important}.text-center{text-align:center!important}.text-left{text-align:left!important}.text-right{text-align:right!important}.align-top{vertical-align:top!important}.font-mono{font-family:var(--font-mono)!important}.text-3xl{font-size:var(--text-3xl)!important;line-height:var(--tw-leading,var(--text-3xl--line-height))!important}.text-base{font-size:var(--text-base)!important;line-height:var(--tw-leading,var(--text-base--line-height))!important}.text-lg{font-size:var(--text-lg)!important;line-height:var(--tw-leading,var(--text-lg--line-height))!important}.text-sm{font-size:var(--text-sm)!important;line-height:var(--tw-leading,var(--text-sm--line-height))!important}.text-xl{font-size:var(--text-xl)!important;line-height:var(--tw-leading,var(--text-xl--line-height))!important}.text-xs{font-size:var(--text-xs)!important;line-height:var(--tw-leading,var(--text-xs--line-height))!important}.text-\[11px\]{font-size:11px!important}.font-bold{--tw-font-weight:var(--font-weight-bold)!important;font-weight:var(--font-weight-bold)!important}.font-medium{--tw-font-weight:var(--font-weight-medium)!important;font-weight:var(--font-weight-medium)!important}.font-semibold{--tw-font-weight:var(--font-weight-semibold)!important;font-weight:var(--font-weight-semibold)!important}.tracking-wide{--tw-tracking:var(--tracking-wide)!important;letter-spacing:var(--tracking-wide)!important}.whitespace-pre-wrap{white-space:pre-wrap!important}.text-amber-600{color:var(--color-amber-600)!important}.text-amber-700{color:var(--color-amber-700)!important}.text-amber-800{color:var(--color-amber-800)!important}.text-amber-900{color:var(--color-amber-900)!important}.text-brand-700{color:var(--color-brand-700)!important}.text-red-600{color:var(--color-red-600)!important}.text-red-700{color:var(--color-red-700)!important}.text-slate-400{color:var(--color-slate-400)!important}.text-slate-500{color:var(--color-slate-500)!important}.text-slate-600{color:var(--color-slate-600)!important}.text-slate-700{color:var(--color-slate-700)!important}.text-white{color:var(--color-white)!important}.uppercase{text-transform:uppercase!important}.italic{font-style:italic!important}.line-through{text-decoration-line:line-through!important}.underline{text-decoration-line:underline!important}.opacity-70{opacity:.7!important}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.outline{outline-style:var(--tw-outline-style)!important;outline-width:1px!important}.blur{--tw-blur:blur(8px)!important;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.grayscale{--tw-grayscale:grayscale(100%)!important;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events!important;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--default-transition-duration))!important}.transition-\[filter\]{transition-property:filter!important;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--default-transition-duration))!important}.ease-out{--tw-ease:var(--ease-out)!important;transition-timing-function:var(--ease-out)!important}.outline-none{--tw-outline-style:none!important;outline-style:none!important}@media (hover:hover){.group-hover\:opacity-80:is(:where(.group):hover *){opacity:.8!important}}.last\:border-0:last-child{border-style:var(--tw-border-style)!important;border-width:0!important}@media (hover:hover){.hover\:border-brand-500:hover{border-color:var(--color-brand-500)!important}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)!important}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)!important}.hover\:bg-slate-200:hover{background-color:var(--color-slate-200)!important}.hover\:text-slate-700:hover{color:var(--color-slate-700)!important}.hover\:text-slate-900:hover{color:var(--color-slate-900)!important}.hover\:underline:hover{text-decoration-line:underline!important}.hover\:brightness-95:hover{--tw-brightness:brightness(95%)!important;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.focus\:ring-indigo-500:focus{--tw-ring-color:var(--color-indigo-500)!important}.focus\:outline-none:focus{--tw-outline-style:none!important;outline-style:none!important}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.focus-visible\:ring-brand-500:focus-visible{--tw-ring-color:var(--color-brand-500)!important}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none!important;outline-style:none!important}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed!important}.disabled\:opacity-50:disabled{opacity:.5!important}.aria-\[current\=page\]\:bg-slate-200[aria-current=page]{background-color:var(--color-slate-200)!important}.aria-\[current\=page\]\:text-slate-900[aria-current=page]{color:var(--color-slate-900)!important}.data-\[state\=active\]\:border-b-2[data-state=active]{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:2px!important}.data-\[state\=active\]\:border-brand-600[data-state=active]{border-color:var(--color-brand-600)!important}.data-\[state\=active\]\:bg-slate-100[data-state=active]{background-color:var(--color-slate-100)!important}.data-\[state\=active\]\:text-brand-700[data-state=active]{color:var(--color-brand-700)!important}.data-\[state\=active\]\:text-slate-900[data-state=active]{color:var(--color-slate-900)!important}.data-\[state\=active\]\:shadow-\[inset_2px_0_0_0_var\(--color-brand-500\)\][data-state=active]{--tw-shadow:inset 2px 0 0 0 var(--tw-shadow-color,var(--color-brand-500))!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))!important}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))!important}}@media (min-width:48rem){.md\:sticky{position:sticky!important}.md\:top-0{top:0!important}.md\:col-span-5{grid-column:span 5/span 5!important}.md\:flex{display:flex!important}.md\:grid{display:grid!important}.md\:h-screen{height:100vh!important}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))!important}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))!important}.md\:grid-cols-\[1fr_1fr_140px_auto_auto\]{grid-template-columns:1fr 1fr 140px auto auto!important}.md\:grid-cols-\[1fr_110px\]{grid-template-columns:1fr 110px!important}.md\:grid-cols-\[1fr_130px_90px_110px_auto\]{grid-template-columns:1fr 130px 90px 110px auto!important}.md\:grid-cols-\[1fr_140px_1fr_150px\]{grid-template-columns:1fr 140px 1fr 150px!important}.md\:grid-cols-\[248px_1fr\]{grid-template-columns:248px 1fr!important}.md\:grid-cols-\[260px_1fr\]{grid-template-columns:260px 1fr!important}.md\:grid-cols-\[280px_1fr\]{grid-template-columns:280px 1fr!important}.md\:grid-cols-\[300px_1fr\]{grid-template-columns:300px 1fr!important}.md\:grid-cols-\[340px_1fr\]{grid-template-columns:340px 1fr!important}.md\:flex-col{flex-direction:column!important}.md\:overflow-visible{overflow:visible!important}.md\:border-r{border-right-style:var(--tw-border-style)!important;border-right-width:1px!important}.md\:border-b-0{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:0!important}.md\:p-8{padding:calc(var(--spacing) * 8)!important}.md\:px-8{padding-inline:calc(var(--spacing) * 8)!important}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2!important}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))!important}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))!important}}}}.xicms-admin,.xicms-admin *,.xicms-admin :before,.xicms-admin :after{box-sizing:border-box}.xicms-admin{-webkit-font-smoothing:antialiased;--color-white:#fff;--color-slate-50:#faf9f6;--color-slate-100:#f3f1ec;--color-slate-200:#e5e2da;--color-slate-300:#d5d1c7;--color-slate-400:#a8a399;--color-slate-500:#6e6a62;--color-slate-600:#57534b;--color-slate-700:#403c36;--color-slate-900:#1b1a17;--color-brand:var(--xicms-primary,#4f46e5);--color-brand-500:var(--xicms-primary,#4f46e5);--color-brand-600:var(--xicms-primary,#4f46e5);--color-brand-700:var(--xicms-primary,#4f46e5);font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.5}@supports (color:color-mix(in lab, red, red)){.xicms-admin{--color-brand-700:color-mix(in srgb, var(--xicms-primary,#4f46e5) 78%, #1b1a17)}}.xicms-admin{--color-brand-50:var(--xicms-primary,#4f46e5)}@supports (color:color-mix(in lab, red, red)){.xicms-admin{--color-brand-50:color-mix(in srgb, var(--xicms-primary,#4f46e5) 10%, #fff)}}.xicms-admin{--admin-canvas:#f6f5f2;--admin-sidebar:#f0eee9;--color-background-body:var(--color-white);--color-background-surface:var(--color-white);--color-background-card:var(--color-white);--color-background-muted:var(--color-slate-100);--color-text-primary:var(--color-slate-900);--color-text-secondary:var(--color-slate-500);--color-border:var(--color-slate-200);--color-accent:var(--color-brand-500);--color-on-accent:#fff;--color-error:#dc2626;color-scheme:light;color:var(--color-text-primary);background:var(--admin-canvas)}.xicms-admin h1,.xicms-admin h2,.xicms-admin h3,.xicms-admin p,.xicms-admin figure{margin:0}.xicms-admin button{font:inherit;cursor:pointer}.xicms-admin input,.xicms-admin textarea,.xicms-admin select{font:inherit;color:inherit}.xicms-admin .ProseMirror{outline:none;min-height:8rem}.xicms-admin .ProseMirror p{margin:0 0 .6rem}.xicms-admin .ProseMirror:focus{outline:none}@media (prefers-reduced-motion:reduce){.xicms-admin *,.xicms-admin :before,.xicms-admin :after,.xicms-ve *,.xicms-ve :before,.xicms-ve :after{transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}.xicms-admin__midden,.xicms-admin__raster{min-height:100vh}.xicms-admin--ingebed .xicms-admin__midden{min-height:50vh}.xicms-admin--ingebed .xicms-admin__raster{min-height:60vh;display:block}.xicms-admin--ingebed .xicms-admin__rail{border-bottom-width:1px;border-right:0;flex-flow:wrap;align-items:center;gap:1rem;height:auto;padding:.5rem 1rem;position:static}.xicms-admin--ingebed .xicms-admin__lijst{flex-direction:row;overflow-x:auto}.xicms-admin--ingebed .xicms-admin__voet{border-top:0;flex-direction:row;align-items:center;gap:.75rem;margin-top:0;margin-left:auto;padding-top:0;display:flex}.xicms-admin--ingebed .xicms-admin__kop{position:static}.xicms-admin--ingebed{width:100vw;max-width:100vw;margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
@@ -53,6 +53,18 @@ function AuthProvider({
53
53
  window.addEventListener(DEMO_EVENT, sync);
54
54
  return () => window.removeEventListener(DEMO_EVENT, sync);
55
55
  }, [demo]);
56
+ async function weigerZonderToegang(auth, sdk) {
57
+ const u = auth.currentUser;
58
+ if (!u) return;
59
+ const token = await u.getIdTokenResult(true);
60
+ const c = token.claims;
61
+ const rol = c.roles?.[siteId];
62
+ if (c.superadmin === true || rol === "siteadmin" || rol === "editor") return;
63
+ await sdk.signOut(auth);
64
+ const fout = new Error("geen-toegang");
65
+ fout.code = "xicms/geen-toegang";
66
+ throw fout;
67
+ }
56
68
  async function authSdk() {
57
69
  const [client, sdk] = await Promise.all([import("../firebase/client.js"), import("firebase/auth")]);
58
70
  const auth = client.getFirebaseAuth();
@@ -129,27 +141,32 @@ function AuthProvider({
129
141
  async signIn(email, password) {
130
142
  const { auth, sdk } = await authSdk();
131
143
  await sdk.signInWithEmailAndPassword(auth, email, password);
144
+ await weigerZonderToegang(auth, sdk);
132
145
  wekOp();
133
146
  },
134
147
  async signInWithGoogle() {
135
148
  const { auth, sdk } = await authSdk();
136
149
  const provider = new sdk.GoogleAuthProvider();
150
+ wekOp();
137
151
  try {
138
152
  await sdk.signInWithPopup(auth, provider);
139
- wekOp();
153
+ await weigerZonderToegang(auth, sdk);
140
154
  } catch (e) {
141
155
  const code = e?.code ?? "";
142
- wekOp();
143
156
  if (code !== "auth/popup-blocked") throw e;
144
157
  setAuthHint(true);
145
158
  await sdk.signInWithRedirect(auth, provider);
146
159
  }
147
160
  },
148
161
  async sendLoginLink(email) {
149
- const { auth, sdk } = await authSdk();
150
162
  const adres = email.trim();
151
- const url = `${window.location.origin}${window.location.pathname}`;
152
- await sdk.sendSignInLinkToEmail(auth, adres, { url, handleCodeInApp: true });
163
+ const [client, fns] = await Promise.all([
164
+ import("../firebase/client.js"),
165
+ import("../firebase/functionsClient.js")
166
+ ]);
167
+ const app = client.getFirebaseApp();
168
+ if (!app) throw new Error("Firebase is niet geconfigureerd.");
169
+ await fns.callable(app, "requestLoginLink")({ siteId, email: adres });
153
170
  try {
154
171
  window.localStorage.setItem(LINK_EMAIL_KEY, adres);
155
172
  } catch {
@@ -160,6 +177,7 @@ function AuthProvider({
160
177
  async completeLoginLink(email) {
161
178
  const { auth, sdk } = await authSdk();
162
179
  await sdk.signInWithEmailLink(auth, email.trim(), window.location.href);
180
+ await weigerZonderToegang(auth, sdk);
163
181
  opruimenNaLinkLogin();
164
182
  wekOp();
165
183
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/AuthProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from \"react\";\nimport type { User } from \"firebase/auth\";\nimport { hasAuthHint, isFirebaseConfigured, setAuthHint } from \"../firebase/config\";\nimport { roleForSite, type Role, type XiClaims } from \"./roles\";\nimport { DEMO_EVENT, isDemoLoggedIn, setDemoLoggedIn } from \"../demo/demoStore\";\n\ninterface AuthState {\n /** Of Firebase überhaupt geconfigureerd is. */\n configured: boolean;\n /** Bezig met laden van de auth-status. */\n loading: boolean;\n user: User | null;\n claims: XiClaims | null;\n /** Effectieve rol op de huidige site (uit `siteId`). */\n role: Role | null;\n signIn: (email: string, password: string) => Promise<void>;\n /** Inloggen met Google (popup). */\n signInWithGoogle: () => Promise<void>;\n /**\n * Stuur een inloglink naar dit adres (magic link). Geen wachtwoord, dus ook niets te vergeten —\n * en anders dan de Google-flow raakt dit geen enkel domein van een derde partij, waardoor het\n * werkt in browsers die opslag van derden blokkeren.\n */\n sendLoginLink: (email: string) => Promise<void>;\n /**\n * Er staat een geldige inloglink in de adresbalk, maar het bijbehorende adres is hier niet\n * bekend — de link is op een ander apparaat geopend dan waar hij is aangevraagd. Het\n * inlogscherm vraagt er dan om; zonder die vraag zou een geldige link doodlopen.\n */\n loginLinkNeedsEmail: boolean;\n /** De link inwisselen met een handmatig opgegeven adres (zie `loginLinkNeedsEmail`). */\n completeLoginLink: (email: string) => Promise<void>;\n /** Ging het inwisselen van de link mis (verlopen, al gebruikt, ander adres)? */\n loginLinkError: unknown;\n signOut: () => Promise<void>;\n /** Forceer het opnieuw inlezen van claims (na een rolwijziging). */\n refresh: () => Promise<void>;\n /** Demo-bewerkmodus actief (config.demoEditing) — pseudo-login, niets wordt opgeslagen. */\n demo: boolean;\n /** Pseudo-login voor de demo: geeft lokaal een siteadmin-rol zonder Firebase. */\n demoSignIn: () => void;\n}\n\nconst AuthContext = createContext<AuthState | null>(null);\n\n/**\n * Het adres waar de inloglink heen ging, bewaard tot de klik terugkomt.\n *\n * Firebase eist bij het afronden hetzelfde adres als waar de link naartoe is gestuurd — dat is de\n * hele beveiliging van deze methode: wie de link onderschept maar het adres niet weet, komt er\n * niet in. Staat het er niet (link geopend op een ander apparaat, of opslag geweigerd), dan vraagt\n * het inlogscherm er alsnog om in plaats van te falen.\n */\nconst LINK_EMAIL_KEY = \"xicms.loginlink.email\";\n\n/**\n * Is deze URL een inloglink? Bewust ZONDER de SDK: deze vraag bepaalt óf we de auth-SDK van ~1 MB\n * gaan laden, dus hem beantwoorden met de SDK zou de besparing meteen opeten. Firebase hangt de\n * link altijd op aan `mode=signIn` met een `oobCode`; dat is genoeg voor de voorselectie, waarna\n * `isSignInWithEmailLink` het echte oordeel velt.\n */\nfunction lijktOpInloglink(): boolean {\n if (typeof window === \"undefined\") return false;\n const q = window.location.search;\n return q.includes(\"mode=signIn\") && q.includes(\"oobCode=\");\n}\n\nexport function AuthProvider({\n siteId,\n demo = false,\n children,\n}: {\n siteId: string;\n demo?: boolean;\n children: ReactNode;\n}) {\n const configured = isFirebaseConfigured();\n const [loading, setLoading] = useState(configured);\n const [user, setUser] = useState<User | null>(null);\n const [claims, setClaims] = useState<XiClaims | null>(null);\n const [demoLoggedIn, setDemoLoggedInState] = useState(false);\n // Er staat een geldige inloglink in de adresbalk, maar we weten niet bij welk adres hij hoort.\n const [linkVraagtEmail, setLinkVraagtEmail] = useState(false);\n const [linkFout, setLinkFout] = useState<unknown>(null);\n // Alleen om het abonnement-effect opnieuw te laten draaien na een login zonder paginawissel.\n const [wakker, setWakker] = useState(0);\n\n /**\n * Na een geslaagde link-login: het bewaarde adres weg, en de `oobCode` uit de adresbalk.\n *\n * Die code is eenmalig, maar hij blijft wél in de geschiedenis en in gedeelde/gekopieerde URL's\n * staan. Hem laten staan levert bovendien een verwarrende herhaling op: één keer verversen en de\n * SDK probeert een verbruikte code opnieuw in te wisselen, met een foutmelding terwijl je gewoon\n * ingelogd bent. `replaceState` haalt hem weg zonder de pagina opnieuw te laden.\n */\n function opruimenNaLinkLogin(): void {\n try {\n window.localStorage.removeItem(LINK_EMAIL_KEY);\n } catch {\n /* niets bewaard, niets op te ruimen */\n }\n setLinkVraagtEmail(false);\n setLinkFout(null);\n try {\n window.history.replaceState({}, \"\", window.location.pathname + window.location.hash);\n } catch {\n /* geschiedenis niet bewerkbaar: onschuldig, de login is al gelukt */\n }\n }\n\n /**\n * Zet de sessie-hint ÉN maak het effect hieronder wakker.\n *\n * Dat tweede is de kern van een bug die zich als \"ik kom er niet in\" voordeed. Het effect dat\n * `onAuthStateChanged` abonneert draait alleen als er bij het LADEN van de pagina al een hint is,\n * en hangt af van `[configured]` — een hint die later wordt gezet laat het dus niet opnieuw\n * draaien. Gevolg: log je in zónder paginawissel (popup of wachtwoord), dan maakt Firebase keurig\n * een sessie, maar luistert er niemand. React hoort het nooit en het inlogscherm blijft staan.\n *\n * Dat het toch vaak leek te werken kwam door de redirect-terugval: die verlaat de pagina en komt\n * terug op een verse load, waar de hint er wél staat. Precies daarom viel dit pas op ná het\n * repareren van de popup — toen verdween de herlaad die het gebrek maskeerde.\n *\n * De teller hoeft nergens te worden uitgelezen; hij bestaat alleen om de afhankelijkheidslijst\n * te laten wijzigen.\n */\n function wekOp(): void {\n setAuthHint(true);\n setWakker((n) => n + 1);\n }\n\n // Demo-login uit de (sessie)opslag inlezen + meeluisteren met wijzigingen (login/logout/reset).\n useEffect(() => {\n if (!demo) return;\n const sync = () => setDemoLoggedInState(isDemoLoggedIn());\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }, [demo]);\n\n /** Laadt de auth-SDK pas wanneer er iets mee moet gebeuren (inloggen, uitloggen, verversen). */\n async function authSdk() {\n const [client, sdk] = await Promise.all([import(\"../firebase/client\"), import(\"firebase/auth\")]);\n const auth = client.getFirebaseAuth();\n if (!auth) throw new Error(\"Firebase is niet geconfigureerd.\");\n return { auth, sdk };\n }\n\n async function loadClaims(u: User | null, forceRefresh = false) {\n if (!u) {\n setClaims(null);\n return;\n }\n // Standaard GEEN geforceerde token-refresh: dat is een netwerk-roundtrip op élke\n // auth-state-change (incl. eerste load van elke ingelogde editor). Alleen refresh() —\n // na een rolwijziging — forceert, want dan is het gecachte token per definitie verouderd.\n const token = await u.getIdTokenResult(forceRefresh);\n setClaims({\n superadmin: token.claims.superadmin === true,\n roles: (token.claims.roles as XiClaims[\"roles\"]) ?? {},\n });\n }\n\n useEffect(() => {\n // Zonder sessie-hint laden we de auth-SDK NIET: een anonieme bezoeker hoeft ~1 MB niet te\n // downloaden om te ontdekken dat hij niet is ingelogd. Zodra hij inlogt of het beheer opent\n // komt de SDK alsnog, en vindt onAuthStateChanged een bestaande sessie meteen terug.\n // …behalve als dit een inloglink IS. Wie op de link in zijn mail klikt komt per definitie\n // zonder sessie-hint binnen — vaak zelfs in een andere browser dan waar hij hem aanvroeg.\n // Zonder deze uitzondering laadt de SDK niet, wordt de link nooit ingewisseld en belandt hij\n // op het inlogscherm met de geldige link ongebruikt in de adresbalk.\n if (!configured || (!hasAuthHint() && !lijktOpInloglink())) {\n setLoading(false);\n return;\n }\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const { auth, sdk } = await authSdk().catch(() => ({ auth: null, sdk: null }) as never);\n if (!auth || afgebroken) {\n setLoading(false);\n return;\n }\n // Terugkeer van een redirect-login afronden (zie signInWithGoogle). `onAuthStateChanged`\n // pikt de sessie zelf al op; deze aanroep is er om een FOUT bij terugkomst niet stil te\n // laten verdwijnen — anders sta je weer op het inlogscherm zonder te weten waarom.\n void sdk.getRedirectResult(auth).catch((e: unknown) => {\n console.error(\"[xicms] inloggen via redirect mislukt:\", e);\n });\n\n // Een aangeklikte inloglink meteen inwisselen. Het adres staat lokaal opgeslagen sinds het\n // versturen; ontbreekt het — link geopend op een ander apparaat, of opslag geweigerd — dan\n // laten we de link stáán en vraagt het inlogscherm erom. Weggooien zou de gebruiker terug\n // naar zijn mailbox sturen voor een link die hij al heeft.\n if (sdk.isSignInWithEmailLink(auth, window.location.href)) {\n let bewaard: string | null = null;\n try {\n bewaard = window.localStorage.getItem(LINK_EMAIL_KEY);\n } catch {\n /* opslag geweigerd → hieronder vragen we het adres gewoon */\n }\n if (bewaard) {\n try {\n await sdk.signInWithEmailLink(auth, bewaard, window.location.href);\n opruimenNaLinkLogin();\n } catch (e) {\n console.error(\"[xicms] inloggen via link mislukt:\", e);\n setLinkFout(e);\n }\n } else if (!afgebroken) {\n setLinkVraagtEmail(true);\n }\n }\n\n unsub = sdk.onAuthStateChanged(auth, async (u: User | null) => {\n setUser(u);\n setAuthHint(Boolean(u));\n await loadClaims(u);\n setLoading(false);\n });\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [configured, wakker]);\n\n const value = useMemo<AuthState>(() => {\n // In de demo met actieve pseudo-login: doe alsof een siteadmin is ingelogd (lokaal, geen Firebase).\n const demoActiveLogin = demo && demoLoggedIn;\n const effectiveUser = demoActiveLogin\n ? ({ uid: \"demo\", email: \"demo@voorbeeld.nl\", displayName: \"Demo-beheerder\" } as unknown as User)\n : user;\n const effectiveClaims: XiClaims | null = demoActiveLogin ? { roles: { [siteId]: \"siteadmin\" } } : claims;\n\n return {\n configured,\n loading,\n user: effectiveUser,\n claims: effectiveClaims,\n role: roleForSite(effectiveClaims, siteId),\n async signIn(email, password) {\n const { auth, sdk } = await authSdk();\n await sdk.signInWithEmailAndPassword(auth, email, password);\n wekOp();\n },\n async signInWithGoogle() {\n const { auth, sdk } = await authSdk();\n const provider = new sdk.GoogleAuthProvider();\n try {\n await sdk.signInWithPopup(auth, provider);\n wekOp();\n } catch (e) {\n // TERUGVAL OP REDIRECT — maar ALLEEN als de popup niet eens open kón.\n //\n // Dit stond hier breed: elke fout behalve annuleren stuurde de hele pagina naar Google.\n // Dat is destructief zodra de popup het wél doet, en precies dat gebeurde. Waargenomen op\n // christelijkkraamcadeau: je koos je account in de popup, en tegelijk opende de\n // ONDERLIGGENDE beheerpagina een tweede Google-scherm. In de console was ondertussen te\n // zien dat `accounts:signInWithIdp` en `accounts:lookup` gewoon slaagden — Firebase had je\n // dus allang ingelogd. Twee flows die door elkaar heen liepen, met \"Inloggen met Google\n // mislukt of geannuleerd\" als eindresultaat terwijl er niets mislukt was.\n //\n // Een redirect gooit de hele pagina weg. Dat is een zwaar middel en hoort alleen bij de\n // ene fout waarbij er echt geen andere weg is: de browser weigerde het venster te openen.\n // Alle andere fouten — netwerk, configuratie, een afgebroken relay — horen zichtbaar te\n // falen, niet stilzwijgend een tweede inlogpoging te starten.\n //\n // De aanleiding voor de brede terugval was een popup die dichtging zonder foutcode. Die\n // kwam door de COOP-blokkade (zie next/authRewrites.ts), en die is nu bij de bron\n // opgelost. Daarmee vervalt de reden om hier ruim te zijn.\n const code = (e as { code?: string })?.code ?? \"\";\n // Een afgewezen belofte betekent niet dat er geen sessie is. In de gemeten gevallen\n // slaagden `signInWithIdp` en `lookup` gewoon — de relay uit de popup was klaar — terwijl\n // deze `catch` liep. Abonneer daarom óók bij een fout: is er wél een sessie, dan ziet\n // `onAuthStateChanged` hem alsnog en verdwijnt het inlogscherm. Is die er niet, dan wist\n // dezelfde luisteraar de hint meteen weer.\n wekOp();\n if (code !== \"auth/popup-blocked\") throw e;\n // ZET DE HINT VÓÓR HET VERTREK. Zonder deze regel heeft de redirect-terugval nog nooit\n // gewerkt voor een eerste login, en wel zo stil dat het op de popup werd geschoven. De\n // keten: je gaat naar Google, komt terug op een verse pagina, en daar besluit het effect\n // hierboven de auth-SDK NIET te laden — want er is geen sessie-hint (je was immers nog\n // nooit ingelogd) en het is geen inloglink. Laadt de SDK niet, dan draait\n // `getRedirectResult` niet, en wordt het antwoord van Google nooit opgehaald.\n setAuthHint(true);\n await sdk.signInWithRedirect(auth, provider);\n }\n },\n async sendLoginLink(email: string) {\n const { auth, sdk } = await authSdk();\n const adres = email.trim();\n // De link komt terug op DEZE pagina, zodat de gebruiker na het inloggen staat waar hij\n // begon (meestal /login of /beheer) in plaats van op de voorpagina.\n const url = `${window.location.origin}${window.location.pathname}`;\n await sdk.sendSignInLinkToEmail(auth, adres, { url, handleCodeInApp: true });\n try {\n window.localStorage.setItem(LINK_EMAIL_KEY, adres);\n } catch {\n // Opslag geweigerd (private mode): niet fataal. Bij terugkomst vraagt het inlogscherm\n // het adres alsnog — dezelfde route als bij een link die op een ander apparaat opengaat.\n }\n },\n loginLinkNeedsEmail: linkVraagtEmail,\n loginLinkError: linkFout,\n async completeLoginLink(email: string) {\n const { auth, sdk } = await authSdk();\n await sdk.signInWithEmailLink(auth, email.trim(), window.location.href);\n opruimenNaLinkLogin();\n wekOp();\n },\n async signOut() {\n if (demo) {\n setDemoLoggedIn(false);\n return;\n }\n const { auth, sdk } = await authSdk();\n await sdk.signOut(auth);\n setAuthHint(false);\n },\n async refresh() {\n // Altijd de actuele Firebase-user (niet de gememoïseerde) — na een snelle\n // uitlog/inlog-wissel zou de closure anders claims van de vorige user verversen.\n // Force-refresh: na een rolwijziging is het gecachte token verouderd.\n const { auth } = await authSdk();\n await loadClaims(auth.currentUser ?? user, true);\n },\n demo,\n demoSignIn() {\n setDemoLoggedIn(true);\n },\n };\n }, [configured, loading, user, claims, siteId, demo, demoLoggedIn, linkVraagtEmail, linkFout]);\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\nexport function useAuth(): AuthState {\n const ctx = useContext(AuthContext);\n if (!ctx) throw new Error(\"useAuth moet binnen <XiCMSProvider> worden gebruikt.\");\n return ctx;\n}\n"],"mappings":";AAuVS;AArVT;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,SAAS,aAAa,sBAAsB,mBAAmB;AAC/D,SAAS,mBAA6C;AACtD,SAAS,YAAY,gBAAgB,uBAAuB;AAuC5D,MAAM,cAAc,cAAgC,IAAI;AAUxD,MAAM,iBAAiB;AAQvB,SAAS,mBAA4B;AACnC,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAM,IAAI,OAAO,SAAS;AAC1B,SAAO,EAAE,SAAS,aAAa,KAAK,EAAE,SAAS,UAAU;AAC3D;AAEO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,OAAO;AAAA,EACP;AACF,GAIG;AACD,QAAM,aAAa,qBAAqB;AACxC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,UAAU;AACjD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsB,IAAI;AAClD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA0B,IAAI;AAC1D,QAAM,CAAC,cAAc,oBAAoB,IAAI,SAAS,KAAK;AAE3D,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAS,KAAK;AAC5D,QAAM,CAAC,UAAU,WAAW,IAAI,SAAkB,IAAI;AAEtD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,CAAC;AAUtC,WAAS,sBAA4B;AACnC,QAAI;AACF,aAAO,aAAa,WAAW,cAAc;AAAA,IAC/C,QAAQ;AAAA,IAER;AACA,uBAAmB,KAAK;AACxB,gBAAY,IAAI;AAChB,QAAI;AACF,aAAO,QAAQ,aAAa,CAAC,GAAG,IAAI,OAAO,SAAS,WAAW,OAAO,SAAS,IAAI;AAAA,IACrF,QAAQ;AAAA,IAER;AAAA,EACF;AAkBA,WAAS,QAAc;AACrB,gBAAY,IAAI;AAChB,cAAU,CAAC,MAAM,IAAI,CAAC;AAAA,EACxB;AAGA,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,OAAO,MAAM,qBAAqB,eAAe,CAAC;AACxD,SAAK;AACL,WAAO,iBAAiB,YAAY,IAAI;AACxC,WAAO,MAAM,OAAO,oBAAoB,YAAY,IAAI;AAAA,EAC1D,GAAG,CAAC,IAAI,CAAC;AAGT,iBAAe,UAAU;AACvB,UAAM,CAAC,QAAQ,GAAG,IAAI,MAAM,QAAQ,IAAI,CAAC,OAAO,oBAAoB,GAAG,OAAO,eAAe,CAAC,CAAC;AAC/F,UAAM,OAAO,OAAO,gBAAgB;AACpC,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,kCAAkC;AAC7D,WAAO,EAAE,MAAM,IAAI;AAAA,EACrB;AAEA,iBAAe,WAAW,GAAgB,eAAe,OAAO;AAC9D,QAAI,CAAC,GAAG;AACN,gBAAU,IAAI;AACd;AAAA,IACF;AAIA,UAAM,QAAQ,MAAM,EAAE,iBAAiB,YAAY;AACnD,cAAU;AAAA,MACR,YAAY,MAAM,OAAO,eAAe;AAAA,MACxC,OAAQ,MAAM,OAAO,SAA+B,CAAC;AAAA,IACvD,CAAC;AAAA,EACH;AAEA,YAAU,MAAM;AAQd,QAAI,CAAC,cAAe,CAAC,YAAY,KAAK,CAAC,iBAAiB,GAAI;AAC1D,iBAAW,KAAK;AAChB;AAAA,IACF;AACA,QAAI;AACJ,QAAI,aAAa;AACjB,UAAM,YAAY;AAChB,YAAM,EAAE,MAAM,IAAI,IAAI,MAAM,QAAQ,EAAE,MAAM,OAAO,EAAE,MAAM,MAAM,KAAK,KAAK,EAAW;AACtF,UAAI,CAAC,QAAQ,YAAY;AACvB,mBAAW,KAAK;AAChB;AAAA,MACF;AAIA,WAAK,IAAI,kBAAkB,IAAI,EAAE,MAAM,CAAC,MAAe;AACrD,gBAAQ,MAAM,0CAA0C,CAAC;AAAA,MAC3D,CAAC;AAMD,UAAI,IAAI,sBAAsB,MAAM,OAAO,SAAS,IAAI,GAAG;AACzD,YAAI,UAAyB;AAC7B,YAAI;AACF,oBAAU,OAAO,aAAa,QAAQ,cAAc;AAAA,QACtD,QAAQ;AAAA,QAER;AACA,YAAI,SAAS;AACX,cAAI;AACF,kBAAM,IAAI,oBAAoB,MAAM,SAAS,OAAO,SAAS,IAAI;AACjE,gCAAoB;AAAA,UACtB,SAAS,GAAG;AACV,oBAAQ,MAAM,sCAAsC,CAAC;AACrD,wBAAY,CAAC;AAAA,UACf;AAAA,QACF,WAAW,CAAC,YAAY;AACtB,6BAAmB,IAAI;AAAA,QACzB;AAAA,MACF;AAEA,cAAQ,IAAI,mBAAmB,MAAM,OAAO,MAAmB;AAC7D,gBAAQ,CAAC;AACT,oBAAY,QAAQ,CAAC,CAAC;AACtB,cAAM,WAAW,CAAC;AAClB,mBAAW,KAAK;AAAA,MAClB,CAAC;AAAA,IACH,GAAG;AACH,WAAO,MAAM;AACX,mBAAa;AACb,cAAQ;AAAA,IACV;AAAA,EACF,GAAG,CAAC,YAAY,MAAM,CAAC;AAEvB,QAAM,QAAQ,QAAmB,MAAM;AAErC,UAAM,kBAAkB,QAAQ;AAChC,UAAM,gBAAgB,kBACjB,EAAE,KAAK,QAAQ,OAAO,qBAAqB,aAAa,iBAAiB,IAC1E;AACJ,UAAM,kBAAmC,kBAAkB,EAAE,OAAO,EAAE,CAAC,MAAM,GAAG,YAAY,EAAE,IAAI;AAElG,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,MAAM,YAAY,iBAAiB,MAAM;AAAA,MACzC,MAAM,OAAO,OAAO,UAAU;AAC5B,cAAM,EAAE,MAAM,IAAI,IAAI,MAAM,QAAQ;AACpC,cAAM,IAAI,2BAA2B,MAAM,OAAO,QAAQ;AAC1D,cAAM;AAAA,MACR;AAAA,MACA,MAAM,mBAAmB;AACvB,cAAM,EAAE,MAAM,IAAI,IAAI,MAAM,QAAQ;AACpC,cAAM,WAAW,IAAI,IAAI,mBAAmB;AAC5C,YAAI;AACF,gBAAM,IAAI,gBAAgB,MAAM,QAAQ;AACxC,gBAAM;AAAA,QACR,SAAS,GAAG;AAmBV,gBAAM,OAAQ,GAAyB,QAAQ;AAM/C,gBAAM;AACN,cAAI,SAAS,qBAAsB,OAAM;AAOzC,sBAAY,IAAI;AAChB,gBAAM,IAAI,mBAAmB,MAAM,QAAQ;AAAA,QAC7C;AAAA,MACF;AAAA,MACA,MAAM,cAAc,OAAe;AACjC,cAAM,EAAE,MAAM,IAAI,IAAI,MAAM,QAAQ;AACpC,cAAM,QAAQ,MAAM,KAAK;AAGzB,cAAM,MAAM,GAAG,OAAO,SAAS,MAAM,GAAG,OAAO,SAAS,QAAQ;AAChE,cAAM,IAAI,sBAAsB,MAAM,OAAO,EAAE,KAAK,iBAAiB,KAAK,CAAC;AAC3E,YAAI;AACF,iBAAO,aAAa,QAAQ,gBAAgB,KAAK;AAAA,QACnD,QAAQ;AAAA,QAGR;AAAA,MACF;AAAA,MACA,qBAAqB;AAAA,MACrB,gBAAgB;AAAA,MAChB,MAAM,kBAAkB,OAAe;AACrC,cAAM,EAAE,MAAM,IAAI,IAAI,MAAM,QAAQ;AACpC,cAAM,IAAI,oBAAoB,MAAM,MAAM,KAAK,GAAG,OAAO,SAAS,IAAI;AACtE,4BAAoB;AACpB,cAAM;AAAA,MACR;AAAA,MACA,MAAM,UAAU;AACd,YAAI,MAAM;AACR,0BAAgB,KAAK;AACrB;AAAA,QACF;AACA,cAAM,EAAE,MAAM,IAAI,IAAI,MAAM,QAAQ;AACpC,cAAM,IAAI,QAAQ,IAAI;AACtB,oBAAY,KAAK;AAAA,MACnB;AAAA,MACA,MAAM,UAAU;AAId,cAAM,EAAE,KAAK,IAAI,MAAM,QAAQ;AAC/B,cAAM,WAAW,KAAK,eAAe,MAAM,IAAI;AAAA,MACjD;AAAA,MACA;AAAA,MACA,aAAa;AACX,wBAAgB,IAAI;AAAA,MACtB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,YAAY,SAAS,MAAM,QAAQ,QAAQ,MAAM,cAAc,iBAAiB,QAAQ,CAAC;AAE7F,SAAO,oBAAC,YAAY,UAAZ,EAAqB,OAAe,UAAS;AACvD;AAEO,SAAS,UAAqB;AACnC,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,sDAAsD;AAChF,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/auth/AuthProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from \"react\";\nimport type { User } from \"firebase/auth\";\nimport { hasAuthHint, isFirebaseConfigured, setAuthHint } from \"../firebase/config\";\nimport { roleForSite, type Role, type XiClaims } from \"./roles\";\nimport { DEMO_EVENT, isDemoLoggedIn, setDemoLoggedIn } from \"../demo/demoStore\";\n\ninterface AuthState {\n /** Of Firebase überhaupt geconfigureerd is. */\n configured: boolean;\n /** Bezig met laden van de auth-status. */\n loading: boolean;\n user: User | null;\n claims: XiClaims | null;\n /** Effectieve rol op de huidige site (uit `siteId`). */\n role: Role | null;\n signIn: (email: string, password: string) => Promise<void>;\n /** Inloggen met Google (popup). */\n signInWithGoogle: () => Promise<void>;\n /**\n * Stuur een inloglink naar dit adres (magic link). Geen wachtwoord, dus ook niets te vergeten —\n * en anders dan de Google-flow raakt dit geen enkel domein van een derde partij, waardoor het\n * werkt in browsers die opslag van derden blokkeren.\n */\n sendLoginLink: (email: string) => Promise<void>;\n /**\n * Er staat een geldige inloglink in de adresbalk, maar het bijbehorende adres is hier niet\n * bekend — de link is op een ander apparaat geopend dan waar hij is aangevraagd. Het\n * inlogscherm vraagt er dan om; zonder die vraag zou een geldige link doodlopen.\n */\n loginLinkNeedsEmail: boolean;\n /** De link inwisselen met een handmatig opgegeven adres (zie `loginLinkNeedsEmail`). */\n completeLoginLink: (email: string) => Promise<void>;\n /** Ging het inwisselen van de link mis (verlopen, al gebruikt, ander adres)? */\n loginLinkError: unknown;\n signOut: () => Promise<void>;\n /** Forceer het opnieuw inlezen van claims (na een rolwijziging). */\n refresh: () => Promise<void>;\n /** Demo-bewerkmodus actief (config.demoEditing) — pseudo-login, niets wordt opgeslagen. */\n demo: boolean;\n /** Pseudo-login voor de demo: geeft lokaal een siteadmin-rol zonder Firebase. */\n demoSignIn: () => void;\n}\n\nconst AuthContext = createContext<AuthState | null>(null);\n\n/**\n * Het adres waar de inloglink heen ging, bewaard tot de klik terugkomt.\n *\n * Firebase eist bij het afronden hetzelfde adres als waar de link naartoe is gestuurd — dat is de\n * hele beveiliging van deze methode: wie de link onderschept maar het adres niet weet, komt er\n * niet in. Staat het er niet (link geopend op een ander apparaat, of opslag geweigerd), dan vraagt\n * het inlogscherm er alsnog om in plaats van te falen.\n */\nconst LINK_EMAIL_KEY = \"xicms.loginlink.email\";\n\n/**\n * Is deze URL een inloglink? Bewust ZONDER de SDK: deze vraag bepaalt óf we de auth-SDK van ~1 MB\n * gaan laden, dus hem beantwoorden met de SDK zou de besparing meteen opeten. Firebase hangt de\n * link altijd op aan `mode=signIn` met een `oobCode`; dat is genoeg voor de voorselectie, waarna\n * `isSignInWithEmailLink` het echte oordeel velt.\n */\nfunction lijktOpInloglink(): boolean {\n if (typeof window === \"undefined\") return false;\n const q = window.location.search;\n return q.includes(\"mode=signIn\") && q.includes(\"oobCode=\");\n}\n\nexport function AuthProvider({\n siteId,\n demo = false,\n children,\n}: {\n siteId: string;\n demo?: boolean;\n children: ReactNode;\n}) {\n const configured = isFirebaseConfigured();\n const [loading, setLoading] = useState(configured);\n const [user, setUser] = useState<User | null>(null);\n const [claims, setClaims] = useState<XiClaims | null>(null);\n const [demoLoggedIn, setDemoLoggedInState] = useState(false);\n // Er staat een geldige inloglink in de adresbalk, maar we weten niet bij welk adres hij hoort.\n const [linkVraagtEmail, setLinkVraagtEmail] = useState(false);\n const [linkFout, setLinkFout] = useState<unknown>(null);\n // Alleen om het abonnement-effect opnieuw te laten draaien na een login zonder paginawissel.\n const [wakker, setWakker] = useState(0);\n\n /**\n * Na een geslaagde link-login: het bewaarde adres weg, en de `oobCode` uit de adresbalk.\n *\n * Die code is eenmalig, maar hij blijft wél in de geschiedenis en in gedeelde/gekopieerde URL's\n * staan. Hem laten staan levert bovendien een verwarrende herhaling op: één keer verversen en de\n * SDK probeert een verbruikte code opnieuw in te wisselen, met een foutmelding terwijl je gewoon\n * ingelogd bent. `replaceState` haalt hem weg zonder de pagina opnieuw te laden.\n */\n function opruimenNaLinkLogin(): void {\n try {\n window.localStorage.removeItem(LINK_EMAIL_KEY);\n } catch {\n /* niets bewaard, niets op te ruimen */\n }\n setLinkVraagtEmail(false);\n setLinkFout(null);\n try {\n window.history.replaceState({}, \"\", window.location.pathname + window.location.hash);\n } catch {\n /* geschiedenis niet bewerkbaar: onschuldig, de login is al gelukt */\n }\n }\n\n /**\n * Zet de sessie-hint ÉN maak het effect hieronder wakker.\n *\n * Dat tweede is de kern van een bug die zich als \"ik kom er niet in\" voordeed. Het effect dat\n * `onAuthStateChanged` abonneert draait alleen als er bij het LADEN van de pagina al een hint is,\n * en hangt af van `[configured]` — een hint die later wordt gezet laat het dus niet opnieuw\n * draaien. Gevolg: log je in zónder paginawissel (popup of wachtwoord), dan maakt Firebase keurig\n * een sessie, maar luistert er niemand. React hoort het nooit en het inlogscherm blijft staan.\n *\n * Dat het toch vaak leek te werken kwam door de redirect-terugval: die verlaat de pagina en komt\n * terug op een verse load, waar de hint er wél staat. Precies daarom viel dit pas op ná het\n * repareren van de popup — toen verdween de herlaad die het gebrek maskeerde.\n *\n * De teller hoeft nergens te worden uitgelezen; hij bestaat alleen om de afhankelijkheidslijst\n * te laten wijzigen.\n */\n function wekOp(): void {\n setAuthHint(true);\n setWakker((n) => n + 1);\n }\n\n // Demo-login uit de (sessie)opslag inlezen + meeluisteren met wijzigingen (login/logout/reset).\n useEffect(() => {\n if (!demo) return;\n const sync = () => setDemoLoggedInState(isDemoLoggedIn());\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }, [demo]);\n\n /**\n * Meteen weer uitloggen als dit account geen toegang heeft tot DEZE site.\n *\n * Bij Google kun je niet vooraf weten wie er inlogt — je krijgt het adres pas ná de\n * accountkeuze. Zonder deze controle komt een willekeurige Google-gebruiker binnen met een\n * geldige sessie, ziet hij \"je hebt geen beheerrechten\", en blijft hij ingelogd op een site\n * waar hij niets te zoeken heeft. Dat is niet gevaarlijk (de rules kijken naar claims), maar\n * het is wel een half-open deur en het leest als een fout in plaats van als een grens.\n *\n * `forceRefresh` is nodig: het token dat je meteen na het inloggen krijgt kan nog van vóór een\n * recente rolwijziging zijn, en dan zou een net toegevoegde beheerder onterecht worden\n * geweigerd.\n */\n async function weigerZonderToegang(\n auth: { currentUser: User | null; },\n sdk: { signOut: (a: never) => Promise<void> },\n ): Promise<void> {\n const u = auth.currentUser;\n if (!u) return;\n const token = await u.getIdTokenResult(true);\n const c = token.claims as { superadmin?: boolean; roles?: Record<string, string> };\n const rol = c.roles?.[siteId];\n if (c.superadmin === true || rol === \"siteadmin\" || rol === \"editor\") return;\n\n await sdk.signOut(auth as never);\n const fout = new Error(\"geen-toegang\") as Error & { code?: string };\n fout.code = \"xicms/geen-toegang\";\n throw fout;\n }\n\n /** Laadt de auth-SDK pas wanneer er iets mee moet gebeuren (inloggen, uitloggen, verversen). */\n async function authSdk() {\n const [client, sdk] = await Promise.all([import(\"../firebase/client\"), import(\"firebase/auth\")]);\n const auth = client.getFirebaseAuth();\n if (!auth) throw new Error(\"Firebase is niet geconfigureerd.\");\n return { auth, sdk };\n }\n\n async function loadClaims(u: User | null, forceRefresh = false) {\n if (!u) {\n setClaims(null);\n return;\n }\n // Standaard GEEN geforceerde token-refresh: dat is een netwerk-roundtrip op élke\n // auth-state-change (incl. eerste load van elke ingelogde editor). Alleen refresh() —\n // na een rolwijziging — forceert, want dan is het gecachte token per definitie verouderd.\n const token = await u.getIdTokenResult(forceRefresh);\n setClaims({\n superadmin: token.claims.superadmin === true,\n roles: (token.claims.roles as XiClaims[\"roles\"]) ?? {},\n });\n }\n\n useEffect(() => {\n // Zonder sessie-hint laden we de auth-SDK NIET: een anonieme bezoeker hoeft ~1 MB niet te\n // downloaden om te ontdekken dat hij niet is ingelogd. Zodra hij inlogt of het beheer opent\n // komt de SDK alsnog, en vindt onAuthStateChanged een bestaande sessie meteen terug.\n // …behalve als dit een inloglink IS. Wie op de link in zijn mail klikt komt per definitie\n // zonder sessie-hint binnen — vaak zelfs in een andere browser dan waar hij hem aanvroeg.\n // Zonder deze uitzondering laadt de SDK niet, wordt de link nooit ingewisseld en belandt hij\n // op het inlogscherm met de geldige link ongebruikt in de adresbalk.\n if (!configured || (!hasAuthHint() && !lijktOpInloglink())) {\n setLoading(false);\n return;\n }\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const { auth, sdk } = await authSdk().catch(() => ({ auth: null, sdk: null }) as never);\n if (!auth || afgebroken) {\n setLoading(false);\n return;\n }\n // Terugkeer van een redirect-login afronden (zie signInWithGoogle). `onAuthStateChanged`\n // pikt de sessie zelf al op; deze aanroep is er om een FOUT bij terugkomst niet stil te\n // laten verdwijnen — anders sta je weer op het inlogscherm zonder te weten waarom.\n void sdk.getRedirectResult(auth).catch((e: unknown) => {\n console.error(\"[xicms] inloggen via redirect mislukt:\", e);\n });\n\n // Een aangeklikte inloglink meteen inwisselen. Het adres staat lokaal opgeslagen sinds het\n // versturen; ontbreekt het — link geopend op een ander apparaat, of opslag geweigerd — dan\n // laten we de link stáán en vraagt het inlogscherm erom. Weggooien zou de gebruiker terug\n // naar zijn mailbox sturen voor een link die hij al heeft.\n if (sdk.isSignInWithEmailLink(auth, window.location.href)) {\n let bewaard: string | null = null;\n try {\n bewaard = window.localStorage.getItem(LINK_EMAIL_KEY);\n } catch {\n /* opslag geweigerd → hieronder vragen we het adres gewoon */\n }\n if (bewaard) {\n try {\n await sdk.signInWithEmailLink(auth, bewaard, window.location.href);\n opruimenNaLinkLogin();\n } catch (e) {\n console.error(\"[xicms] inloggen via link mislukt:\", e);\n setLinkFout(e);\n }\n } else if (!afgebroken) {\n setLinkVraagtEmail(true);\n }\n }\n\n unsub = sdk.onAuthStateChanged(auth, async (u: User | null) => {\n setUser(u);\n setAuthHint(Boolean(u));\n await loadClaims(u);\n setLoading(false);\n });\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [configured, wakker]);\n\n const value = useMemo<AuthState>(() => {\n // In de demo met actieve pseudo-login: doe alsof een siteadmin is ingelogd (lokaal, geen Firebase).\n const demoActiveLogin = demo && demoLoggedIn;\n const effectiveUser = demoActiveLogin\n ? ({ uid: \"demo\", email: \"demo@voorbeeld.nl\", displayName: \"Demo-beheerder\" } as unknown as User)\n : user;\n const effectiveClaims: XiClaims | null = demoActiveLogin ? { roles: { [siteId]: \"siteadmin\" } } : claims;\n\n return {\n configured,\n loading,\n user: effectiveUser,\n claims: effectiveClaims,\n role: roleForSite(effectiveClaims, siteId),\n async signIn(email, password) {\n const { auth, sdk } = await authSdk();\n await sdk.signInWithEmailAndPassword(auth, email, password);\n // Zelfde grens als bij Google: een geldig wachtwoord is niet hetzelfde als toegang tot\n // DEZE site. Zonder deze regel zou de terugval een ruimere deur zijn dan de hoofdingang.\n await weigerZonderToegang(auth, sdk);\n wekOp();\n },\n async signInWithGoogle() {\n const { auth, sdk } = await authSdk();\n const provider = new sdk.GoogleAuthProvider();\n /**\n * Abonneren VÓÓR de popup opengaat, niet erna.\n *\n * `signInWithPopup` lost pas op als Firebase de hele tokenuitwisseling heeft afgerond —\n * `signInWithIdp`, dan `lookup` — en pas dán sluit de handler zijn eigen venster. Wachtte de\n * UI op die belofte, dan bleef het inlogscherm al die tijd staan terwijl de sessie er vaak\n * al lang was. Voor de gebruiker las dat als \"er gebeurt niets\".\n *\n * Met de luisteraar al actief reageert het scherm op het moment dat de sessie ontstaat, niet\n * op het moment dat Firebase klaar is met opruimen. Het venster sluit daarna alsnog wat later\n * — dat is Firebase' eigen tijd en die willen we niet inkorten, want dan sluiten we het\n * midden in de tokenuitwisseling.\n */\n wekOp();\n try {\n await sdk.signInWithPopup(auth, provider);\n await weigerZonderToegang(auth, sdk);\n } catch (e) {\n // TERUGVAL OP REDIRECT — maar ALLEEN als de popup niet eens open kón.\n //\n // Dit stond hier breed: elke fout behalve annuleren stuurde de hele pagina naar Google.\n // Dat is destructief zodra de popup het wél doet, en precies dat gebeurde. Waargenomen op\n // christelijkkraamcadeau: je koos je account in de popup, en tegelijk opende de\n // ONDERLIGGENDE beheerpagina een tweede Google-scherm. In de console was ondertussen te\n // zien dat `accounts:signInWithIdp` en `accounts:lookup` gewoon slaagden — Firebase had je\n // dus allang ingelogd. Twee flows die door elkaar heen liepen, met \"Inloggen met Google\n // mislukt of geannuleerd\" als eindresultaat terwijl er niets mislukt was.\n //\n // Een redirect gooit de hele pagina weg. Dat is een zwaar middel en hoort alleen bij de\n // ene fout waarbij er echt geen andere weg is: de browser weigerde het venster te openen.\n // Alle andere fouten — netwerk, configuratie, een afgebroken relay — horen zichtbaar te\n // falen, niet stilzwijgend een tweede inlogpoging te starten.\n //\n // De aanleiding voor de brede terugval was een popup die dichtging zonder foutcode. Die\n // kwam door de COOP-blokkade (zie next/authRewrites.ts), en die is nu bij de bron\n // opgelost. Daarmee vervalt de reden om hier ruim te zijn.\n // Merk op dat hier GEEN `wekOp()` meer staat: de luisteraar is al vóór de popup\n // geabonneerd. Dat dekt meteen het geval dat de belofte afwijst terwijl er tóch een sessie\n // is — in de gemeten gevallen slaagden `signInWithIdp` en `lookup` gewoon terwijl deze\n // catch liep. `onAuthStateChanged` ziet die sessie dan alsnog en het inlogscherm verdwijnt.\n const code = (e as { code?: string })?.code ?? \"\";\n if (code !== \"auth/popup-blocked\") throw e;\n // ZET DE HINT VÓÓR HET VERTREK. Zonder deze regel heeft de redirect-terugval nog nooit\n // gewerkt voor een eerste login, en wel zo stil dat het op de popup werd geschoven. De\n // keten: je gaat naar Google, komt terug op een verse pagina, en daar besluit het effect\n // hierboven de auth-SDK NIET te laden — want er is geen sessie-hint (je was immers nog\n // nooit ingelogd) en het is geen inloglink. Laadt de SDK niet, dan draait\n // `getRedirectResult` niet, en wordt het antwoord van Google nooit opgehaald.\n setAuthHint(true);\n await sdk.signInWithRedirect(auth, provider);\n }\n },\n async sendLoginLink(email: string) {\n const adres = email.trim();\n /**\n * Via de SERVER, niet via de client-SDK.\n *\n * `sendSignInLinkToEmail` stuurt een link naar élk adres dat je intikt. Firebase maakt bij\n * het inwisselen een account aan, dat vervolgens nergens een rol heeft en dus niets kan —\n * functioneel veilig, want de rules kijken naar claims en niet naar \"is ingelogd\". Maar het\n * betekent wel dat iedereen op het internet een account kan aanmaken op dit project, en dat\n * een vreemde een echte, werkende inloglink in zijn mailbox krijgt.\n *\n * De `requestLoginLink`-callable draait de volgorde om: eerst kijken of het adres een rol\n * heeft op DEZE site, en pas dan een link maken en versturen. Hij antwoordt altijd\n * hetzelfde, ook als er niets is verstuurd — anders zou het scherm verklappen wie beheerder\n * is. Daarom mag de bevestiging hierna ook niet beweren dát er een mail onderweg is.\n */\n // DYNAMISCH importeren, net als `authSdk()`. Zowel `firebase/client` als `functionsClient`\n // trekken de Firebase-SDK statisch mee; een gewone import bovenaan dit bestand zou die ~1 MB\n // aan élke anonieme bezoeker uitleveren — precies wat de sessie-hint hierboven voorkomt.\n const [client, fns] = await Promise.all([\n import(\"../firebase/client\"),\n import(\"../firebase/functionsClient\"),\n ]);\n const app = client.getFirebaseApp();\n if (!app) throw new Error(\"Firebase is niet geconfigureerd.\");\n await fns.callable(app, \"requestLoginLink\")({ siteId, email: adres });\n try {\n window.localStorage.setItem(LINK_EMAIL_KEY, adres);\n } catch {\n // Opslag geweigerd (private mode): niet fataal. Bij terugkomst vraagt het inlogscherm\n // het adres alsnog — dezelfde route als bij een link die op een ander apparaat opengaat.\n }\n },\n loginLinkNeedsEmail: linkVraagtEmail,\n loginLinkError: linkFout,\n async completeLoginLink(email: string) {\n const { auth, sdk } = await authSdk();\n await sdk.signInWithEmailLink(auth, email.trim(), window.location.href);\n // Ook hier, ondanks dat de server alleen naar beheerders stuurt: een mail kan worden\n // doorgestuurd, en dan zou de link in andere handen alsnog een sessie opleveren.\n await weigerZonderToegang(auth, sdk);\n opruimenNaLinkLogin();\n wekOp();\n },\n async signOut() {\n if (demo) {\n setDemoLoggedIn(false);\n return;\n }\n const { auth, sdk } = await authSdk();\n await sdk.signOut(auth);\n setAuthHint(false);\n },\n async refresh() {\n // Altijd de actuele Firebase-user (niet de gememoïseerde) — na een snelle\n // uitlog/inlog-wissel zou de closure anders claims van de vorige user verversen.\n // Force-refresh: na een rolwijziging is het gecachte token verouderd.\n const { auth } = await authSdk();\n await loadClaims(auth.currentUser ?? user, true);\n },\n demo,\n demoSignIn() {\n setDemoLoggedIn(true);\n },\n };\n }, [configured, loading, user, claims, siteId, demo, demoLoggedIn, linkVraagtEmail, linkFout]);\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\nexport function useAuth(): AuthState {\n const ctx = useContext(AuthContext);\n if (!ctx) throw new Error(\"useAuth moet binnen <XiCMSProvider> worden gebruikt.\");\n return ctx;\n}\n"],"mappings":"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","names":[]}
@@ -28,6 +28,11 @@ function authErrorMessage(e, fallback) {
28
28
  case "auth/cancelled-popup-request":
29
29
  return "Het Google-venster werd gesloten voordat het inloggen klaar was. Probeer het opnieuw, of gebruik de inloglink hieronder.";
30
30
  // ── Inloglink ──────────────────────────────────────────────────────────────────────────
31
+ // Geen Firebase-code maar de onze: het account bestaat en de login slaagde, maar het hoort
32
+ // niet bij deze site. Bewust concreet — "inloggen mislukt" zou de gebruiker laten zoeken naar
33
+ // een fout die er niet is.
34
+ case "xicms/geen-toegang":
35
+ return "Dit account heeft geen toegang tot deze site. Vraag de beheerder om je toe te voegen, of log in met een ander account.";
31
36
  case "auth/expired-action-code":
32
37
  return "Deze inloglink is verlopen. Vraag hieronder een nieuwe aan.";
33
38
  case "auth/invalid-action-code":
@@ -123,9 +128,9 @@ function LoginForm({ onSuccess }) {
123
128
  return /* @__PURE__ */ jsxs("div", { className: "xicms-card", style: kaart, children: [
124
129
  /* @__PURE__ */ jsx("h2", { style: { margin: 0 }, children: "Kijk in je mail" }),
125
130
  /* @__PURE__ */ jsxs("p", { style: { margin: 0 }, children: [
126
- "We stuurden een inloglink naar ",
131
+ "Heeft ",
127
132
  /* @__PURE__ */ jsx("strong", { children: email }),
128
- ". Klik erop en je bent binnen \u2014 geen wachtwoord nodig."
133
+ " toegang tot deze site, dan staat er nu een inloglink in die mailbox. Klik erop en je bent binnen \u2014 geen wachtwoord nodig."
129
134
  ] }),
130
135
  /* @__PURE__ */ jsxs("p", { style: { margin: 0, fontSize: 13, opacity: 0.7 }, children: [
131
136
  "Niets ontvangen? Kijk in je spam, of",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/LoginForm.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState, type CSSProperties, type FormEvent } from \"react\";\nimport { useAuth } from \"./AuthProvider\";\n\n/**\n * Vertaalt een Firebase-auth-fout naar een bruikbare melding. Maakt de meest voorkomende\n * deploy-/configuratieproblemen zelf-diagnosticerend i.p.v. een generieke \"inloggen mislukt\".\n */\nfunction authErrorMessage(e: unknown, fallback: string): string {\n const code = (e as { code?: string })?.code ?? \"\";\n switch (code) {\n case \"auth/unauthorized-domain\":\n return \"Dit domein mag nog niet inloggen. Voeg het toe in Firebase → Authentication → Settings → Authorized domains (bv. je Vercel-domein).\";\n case \"auth/invalid-api-key\":\n case \"auth/api-key-not-valid\":\n case \"auth/api-key-not-valid.-please-pass-a-valid-api-key.\":\n return \"De Firebase API-sleutel klopt niet. Controleer NEXT_PUBLIC_FIREBASE_API_KEY in de (Vercel-)omgevingsvariabelen.\";\n case \"auth/configuration-not-found\":\n return \"Firebase Authentication is niet (juist) ingericht voor dit project. Controleer de NEXT_PUBLIC_FIREBASE_*-env en of Authentication aan staat.\";\n case \"auth/operation-not-allowed\":\n return \"Deze inlogmethode staat uit. Zet 'm aan in Firebase → Authentication → Sign-in method (Google of E-mailkoppeling).\";\n case \"auth/network-request-failed\":\n return \"Geen verbinding met Firebase. Controleer je netwerk en de NEXT_PUBLIC_FIREBASE_*-config.\";\n case \"auth/invalid-credential\":\n case \"auth/wrong-password\":\n case \"auth/user-not-found\":\n case \"auth/invalid-email\":\n return \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\";\n case \"auth/popup-blocked\":\n return \"De browser blokkeerde het Google-venster. Sta popups toe voor deze site, of gebruik de inloglink hieronder.\";\n case \"auth/popup-closed-by-user\":\n case \"auth/cancelled-popup-request\":\n return \"Het Google-venster werd gesloten voordat het inloggen klaar was. Probeer het opnieuw, of gebruik de inloglink hieronder.\";\n // ── Inloglink ──────────────────────────────────────────────────────────────────────────\n case \"auth/expired-action-code\":\n return \"Deze inloglink is verlopen. Vraag hieronder een nieuwe aan.\";\n case \"auth/invalid-action-code\":\n return \"Deze inloglink is al gebruikt of niet meer geldig. Vraag hieronder een nieuwe aan.\";\n default:\n return code ? `${fallback} (${code})` : fallback;\n }\n}\n\n/**\n * Inlogscherm voor beheerders/editors — wachtwoordloos van opzet.\n *\n * Twee wegen naar binnen: Google, of een inloglink per mail. Geen van beide kent een wachtwoord,\n * en dus ook geen vergeten-wachtwoord-mailtjes. De inloglink is bovendien de betrouwbaarste van de\n * twee: hij raakt geen enkel domein van een derde partij en werkt daarom ook in browsers die\n * opslag van derden blokkeren — precies waar de Google-redirect op stukliep.\n *\n * Het wachtwoordformulier is niet weggegooid maar weggevouwen. Als beide wachtwoordloze wegen over\n * e-mail lopen, is e-mail een enkelvoudig storingspunt: hapert de mailbezorging, dan komt niemand\n * er meer in — ook de beheerder niet. Deze terugval blijft daarom bestaan voor wie hem nodig heeft,\n * zonder hem aan iedereen op te dringen.\n */\nexport function LoginForm({ onSuccess }: { onSuccess?: () => void }) {\n const {\n signIn,\n signInWithGoogle,\n sendLoginLink,\n loginLinkNeedsEmail,\n loginLinkError,\n completeLoginLink,\n configured,\n user,\n } = useAuth();\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n const [linkVerstuurd, setLinkVerstuurd] = useState(false);\n const [wachtwoordModus, setWachtwoordModus] = useState(false);\n const [busy, setBusy] = useState(false);\n\n // Een mislukte link-inwisseling gebeurt in de provider (bij het laden), niet hier. Zonder deze\n // doorgifte zou de gebruiker een leeg inlogscherm zien terwijl er wél iets mis was.\n useEffect(() => {\n if (loginLinkError) setError(authErrorMessage(loginLinkError, \"Inloggen via de link mislukt.\"));\n }, [loginLinkError]);\n\n async function handleGoogle() {\n setError(null);\n setBusy(true);\n try {\n await signInWithGoogle();\n onSuccess?.();\n } catch (e) {\n setError(authErrorMessage(e, \"Inloggen met Google mislukt of geannuleerd.\"));\n } finally {\n setBusy(false);\n }\n }\n\n if (!configured) {\n return (\n <div className=\"xicms-card\" style={{ padding: \"1.5rem\" }}>\n <p>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>\n omgevingsvariabelen in om inloggen aan te zetten.\n </p>\n </div>\n );\n }\n\n if (user) {\n return (\n <div className=\"xicms-card\" style={{ padding: \"1.5rem\" }}>\n <p>Je bent ingelogd als <strong>{user.email}</strong>.</p>\n </div>\n );\n }\n\n /** Verstuur de inloglink, of — bij terugkomst zonder bekend adres — wissel de link juist in. */\n async function handleEmail(e: FormEvent) {\n e.preventDefault();\n setError(null);\n setBusy(true);\n try {\n if (loginLinkNeedsEmail) {\n await completeLoginLink(email);\n onSuccess?.();\n } else {\n await sendLoginLink(email);\n setLinkVerstuurd(true);\n }\n } catch (err) {\n setError(authErrorMessage(err, \"Versturen van de inloglink mislukt.\"));\n } finally {\n setBusy(false);\n }\n }\n\n async function handleWachtwoord(e: FormEvent) {\n e.preventDefault();\n setError(null);\n setBusy(true);\n try {\n await signIn(email, password);\n onSuccess?.();\n } catch (err) {\n setError(authErrorMessage(err, \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\"));\n } finally {\n setBusy(false);\n }\n }\n\n const kaart: CSSProperties = {\n padding: \"1.5rem\",\n display: \"grid\",\n gap: \"0.75rem\",\n maxWidth: 360,\n };\n\n // Bevestiging ná het versturen. Bewust geen formulier meer: de volgende stap staat in de mailbox,\n // en een nog zichtbaar veld nodigt uit tot nog een keer klikken (waarmee de eerste link vervalt).\n if (linkVerstuurd) {\n return (\n <div className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Kijk in je mail</h2>\n <p style={{ margin: 0 }}>\n We stuurden een inloglink naar <strong>{email}</strong>. Klik erop en je bent binnen — geen\n wachtwoord nodig.\n </p>\n <p style={{ margin: 0, fontSize: 13, opacity: 0.7 }}>\n Niets ontvangen? Kijk in je spam, of{\" \"}\n <button\n type=\"button\"\n onClick={() => setLinkVerstuurd(false)}\n className=\"xicms-btn xicms-btn--ghost\"\n style={{ padding: 0, border: 0, background: \"none\", textDecoration: \"underline\", cursor: \"pointer\" }}\n >\n probeer een ander adres\n </button>\n .\n </p>\n </div>\n );\n }\n\n if (wachtwoordModus) {\n return (\n <form onSubmit={handleWachtwoord} className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Inloggen met wachtwoord</h2>\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>E-mailadres</span>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"xicms-input\"\n />\n </label>\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>Wachtwoord</span>\n <input\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n autoComplete=\"current-password\"\n className=\"xicms-input\"\n />\n </label>\n {error && <p style={{ color: \"#dc2626\", margin: 0 }}>{error}</p>}\n <button type=\"submit\" disabled={busy} className=\"xicms-btn\">\n {busy ? \"Bezig…\" : \"Inloggen\"}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setWachtwoordModus(false);\n setError(null);\n }}\n className=\"xicms-btn xicms-btn--ghost\"\n >\n Liever zonder wachtwoord\n </button>\n </form>\n );\n }\n\n return (\n <form onSubmit={handleEmail} className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Inloggen</h2>\n\n {loginLinkNeedsEmail && (\n <p style={{ margin: 0, fontSize: 14 }}>\n Bevestig even bij welk e-mailadres deze inloglink hoort. Dat is wat de link beveiligt: wie\n hem onderschept maar het adres niet weet, komt er niet in.\n </p>\n )}\n\n <button type=\"button\" onClick={handleGoogle} disabled={busy} className=\"xicms-btn\">\n Inloggen met Google\n </button>\n\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"0.5rem\", opacity: 0.6, fontSize: 13 }}>\n <span style={{ flex: 1, height: 1, background: \"currentColor\" }} /> of{\" \"}\n <span style={{ flex: 1, height: 1, background: \"currentColor\" }} />\n </div>\n\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>E-mailadres</span>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"xicms-input\"\n />\n </label>\n {error && <p style={{ color: \"#dc2626\", margin: 0 }}>{error}</p>}\n <button type=\"submit\" disabled={busy} className=\"xicms-btn xicms-btn--ghost\">\n {busy ? \"Bezig…\" : loginLinkNeedsEmail ? \"Inloggen afronden\" : \"Stuur mij een inloglink\"}\n </button>\n\n {/* De terugval: aanwezig voor wie hem nodig heeft, stil voor wie dat niet heeft. */}\n {!loginLinkNeedsEmail && (\n <button\n type=\"button\"\n onClick={() => {\n setWachtwoordModus(true);\n setError(null);\n }}\n style={{\n justifySelf: \"start\",\n padding: 0,\n border: 0,\n background: \"none\",\n font: \"inherit\",\n fontSize: 12,\n opacity: 0.55,\n textDecoration: \"underline\",\n cursor: \"pointer\",\n }}\n >\n Inloggen met wachtwoord\n </button>\n )}\n </form>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/auth/LoginForm.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState, type CSSProperties, type FormEvent } from \"react\";\nimport { useAuth } from \"./AuthProvider\";\n\n/**\n * Vertaalt een Firebase-auth-fout naar een bruikbare melding. Maakt de meest voorkomende\n * deploy-/configuratieproblemen zelf-diagnosticerend i.p.v. een generieke \"inloggen mislukt\".\n */\nfunction authErrorMessage(e: unknown, fallback: string): string {\n const code = (e as { code?: string })?.code ?? \"\";\n switch (code) {\n case \"auth/unauthorized-domain\":\n return \"Dit domein mag nog niet inloggen. Voeg het toe in Firebase → Authentication → Settings → Authorized domains (bv. je Vercel-domein).\";\n case \"auth/invalid-api-key\":\n case \"auth/api-key-not-valid\":\n case \"auth/api-key-not-valid.-please-pass-a-valid-api-key.\":\n return \"De Firebase API-sleutel klopt niet. Controleer NEXT_PUBLIC_FIREBASE_API_KEY in de (Vercel-)omgevingsvariabelen.\";\n case \"auth/configuration-not-found\":\n return \"Firebase Authentication is niet (juist) ingericht voor dit project. Controleer de NEXT_PUBLIC_FIREBASE_*-env en of Authentication aan staat.\";\n case \"auth/operation-not-allowed\":\n return \"Deze inlogmethode staat uit. Zet 'm aan in Firebase → Authentication → Sign-in method (Google of E-mailkoppeling).\";\n case \"auth/network-request-failed\":\n return \"Geen verbinding met Firebase. Controleer je netwerk en de NEXT_PUBLIC_FIREBASE_*-config.\";\n case \"auth/invalid-credential\":\n case \"auth/wrong-password\":\n case \"auth/user-not-found\":\n case \"auth/invalid-email\":\n return \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\";\n case \"auth/popup-blocked\":\n return \"De browser blokkeerde het Google-venster. Sta popups toe voor deze site, of gebruik de inloglink hieronder.\";\n case \"auth/popup-closed-by-user\":\n case \"auth/cancelled-popup-request\":\n return \"Het Google-venster werd gesloten voordat het inloggen klaar was. Probeer het opnieuw, of gebruik de inloglink hieronder.\";\n // ── Inloglink ──────────────────────────────────────────────────────────────────────────\n // Geen Firebase-code maar de onze: het account bestaat en de login slaagde, maar het hoort\n // niet bij deze site. Bewust concreet — \"inloggen mislukt\" zou de gebruiker laten zoeken naar\n // een fout die er niet is.\n case \"xicms/geen-toegang\":\n return \"Dit account heeft geen toegang tot deze site. Vraag de beheerder om je toe te voegen, of log in met een ander account.\";\n case \"auth/expired-action-code\":\n return \"Deze inloglink is verlopen. Vraag hieronder een nieuwe aan.\";\n case \"auth/invalid-action-code\":\n return \"Deze inloglink is al gebruikt of niet meer geldig. Vraag hieronder een nieuwe aan.\";\n default:\n return code ? `${fallback} (${code})` : fallback;\n }\n}\n\n/**\n * Inlogscherm voor beheerders/editors — wachtwoordloos van opzet.\n *\n * Twee wegen naar binnen: Google, of een inloglink per mail. Geen van beide kent een wachtwoord,\n * en dus ook geen vergeten-wachtwoord-mailtjes. De inloglink is bovendien de betrouwbaarste van de\n * twee: hij raakt geen enkel domein van een derde partij en werkt daarom ook in browsers die\n * opslag van derden blokkeren — precies waar de Google-redirect op stukliep.\n *\n * Het wachtwoordformulier is niet weggegooid maar weggevouwen. Als beide wachtwoordloze wegen over\n * e-mail lopen, is e-mail een enkelvoudig storingspunt: hapert de mailbezorging, dan komt niemand\n * er meer in — ook de beheerder niet. Deze terugval blijft daarom bestaan voor wie hem nodig heeft,\n * zonder hem aan iedereen op te dringen.\n */\nexport function LoginForm({ onSuccess }: { onSuccess?: () => void }) {\n const {\n signIn,\n signInWithGoogle,\n sendLoginLink,\n loginLinkNeedsEmail,\n loginLinkError,\n completeLoginLink,\n configured,\n user,\n } = useAuth();\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n const [linkVerstuurd, setLinkVerstuurd] = useState(false);\n const [wachtwoordModus, setWachtwoordModus] = useState(false);\n const [busy, setBusy] = useState(false);\n\n // Een mislukte link-inwisseling gebeurt in de provider (bij het laden), niet hier. Zonder deze\n // doorgifte zou de gebruiker een leeg inlogscherm zien terwijl er wél iets mis was.\n useEffect(() => {\n if (loginLinkError) setError(authErrorMessage(loginLinkError, \"Inloggen via de link mislukt.\"));\n }, [loginLinkError]);\n\n async function handleGoogle() {\n setError(null);\n setBusy(true);\n try {\n await signInWithGoogle();\n onSuccess?.();\n } catch (e) {\n setError(authErrorMessage(e, \"Inloggen met Google mislukt of geannuleerd.\"));\n } finally {\n setBusy(false);\n }\n }\n\n if (!configured) {\n return (\n <div className=\"xicms-card\" style={{ padding: \"1.5rem\" }}>\n <p>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>\n omgevingsvariabelen in om inloggen aan te zetten.\n </p>\n </div>\n );\n }\n\n if (user) {\n return (\n <div className=\"xicms-card\" style={{ padding: \"1.5rem\" }}>\n <p>Je bent ingelogd als <strong>{user.email}</strong>.</p>\n </div>\n );\n }\n\n /** Verstuur de inloglink, of — bij terugkomst zonder bekend adres — wissel de link juist in. */\n async function handleEmail(e: FormEvent) {\n e.preventDefault();\n setError(null);\n setBusy(true);\n try {\n if (loginLinkNeedsEmail) {\n await completeLoginLink(email);\n onSuccess?.();\n } else {\n await sendLoginLink(email);\n setLinkVerstuurd(true);\n }\n } catch (err) {\n setError(authErrorMessage(err, \"Versturen van de inloglink mislukt.\"));\n } finally {\n setBusy(false);\n }\n }\n\n async function handleWachtwoord(e: FormEvent) {\n e.preventDefault();\n setError(null);\n setBusy(true);\n try {\n await signIn(email, password);\n onSuccess?.();\n } catch (err) {\n setError(authErrorMessage(err, \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\"));\n } finally {\n setBusy(false);\n }\n }\n\n const kaart: CSSProperties = {\n padding: \"1.5rem\",\n display: \"grid\",\n gap: \"0.75rem\",\n maxWidth: 360,\n };\n\n // Bevestiging ná het versturen. Bewust geen formulier meer: de volgende stap staat in de mailbox,\n // en een nog zichtbaar veld nodigt uit tot nog een keer klikken (waarmee de eerste link vervalt).\n if (linkVerstuurd) {\n return (\n <div className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Kijk in je mail</h2>\n {/* Bewust voorwaardelijk geformuleerd. De server stuurt alleen een link naar een adres dat\n ook echt beheerder is van deze site, en antwoordt altijd hetzelfde — anders zou dit\n scherm verklappen wie dat zijn. Zou hier \"we stuurden een link naar X\" staan, dan is dat\n een bewering die we niet kunnen waarmaken én een oracle. */}\n <p style={{ margin: 0 }}>\n Heeft <strong>{email}</strong> toegang tot deze site, dan staat er nu een inloglink in die\n mailbox. Klik erop en je bent binnen — geen wachtwoord nodig.\n </p>\n <p style={{ margin: 0, fontSize: 13, opacity: 0.7 }}>\n Niets ontvangen? Kijk in je spam, of{\" \"}\n <button\n type=\"button\"\n onClick={() => setLinkVerstuurd(false)}\n className=\"xicms-btn xicms-btn--ghost\"\n style={{ padding: 0, border: 0, background: \"none\", textDecoration: \"underline\", cursor: \"pointer\" }}\n >\n probeer een ander adres\n </button>\n .\n </p>\n </div>\n );\n }\n\n if (wachtwoordModus) {\n return (\n <form onSubmit={handleWachtwoord} className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Inloggen met wachtwoord</h2>\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>E-mailadres</span>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"xicms-input\"\n />\n </label>\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>Wachtwoord</span>\n <input\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n autoComplete=\"current-password\"\n className=\"xicms-input\"\n />\n </label>\n {error && <p style={{ color: \"#dc2626\", margin: 0 }}>{error}</p>}\n <button type=\"submit\" disabled={busy} className=\"xicms-btn\">\n {busy ? \"Bezig…\" : \"Inloggen\"}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setWachtwoordModus(false);\n setError(null);\n }}\n className=\"xicms-btn xicms-btn--ghost\"\n >\n Liever zonder wachtwoord\n </button>\n </form>\n );\n }\n\n return (\n <form onSubmit={handleEmail} className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Inloggen</h2>\n\n {loginLinkNeedsEmail && (\n <p style={{ margin: 0, fontSize: 14 }}>\n Bevestig even bij welk e-mailadres deze inloglink hoort. Dat is wat de link beveiligt: wie\n hem onderschept maar het adres niet weet, komt er niet in.\n </p>\n )}\n\n <button type=\"button\" onClick={handleGoogle} disabled={busy} className=\"xicms-btn\">\n Inloggen met Google\n </button>\n\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"0.5rem\", opacity: 0.6, fontSize: 13 }}>\n <span style={{ flex: 1, height: 1, background: \"currentColor\" }} /> of{\" \"}\n <span style={{ flex: 1, height: 1, background: \"currentColor\" }} />\n </div>\n\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>E-mailadres</span>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"xicms-input\"\n />\n </label>\n {error && <p style={{ color: \"#dc2626\", margin: 0 }}>{error}</p>}\n <button type=\"submit\" disabled={busy} className=\"xicms-btn xicms-btn--ghost\">\n {busy ? \"Bezig…\" : loginLinkNeedsEmail ? \"Inloggen afronden\" : \"Stuur mij een inloglink\"}\n </button>\n\n {/* De terugval: aanwezig voor wie hem nodig heeft, stil voor wie dat niet heeft. */}\n {!loginLinkNeedsEmail && (\n <button\n type=\"button\"\n onClick={() => {\n setWachtwoordModus(true);\n setError(null);\n }}\n style={{\n justifySelf: \"start\",\n padding: 0,\n border: 0,\n background: \"none\",\n font: \"inherit\",\n fontSize: 12,\n opacity: 0.55,\n textDecoration: \"underline\",\n cursor: \"pointer\",\n }}\n >\n Inloggen met wachtwoord\n </button>\n )}\n </form>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Toont een externe pagina in een kader BINNEN de site, onder de logo/menubalk.
3
+ *
4
+ * De klassen heten `xicms-extern*` en niet `xicms-embed*`: die laatste naam is al in gebruik voor
5
+ * het consent-blok dat video's en kaarten inkadert. Twee ongerelateerde dingen met dezelfde
6
+ * klassenaam gaan elkaars opmaak overschrijven, en dat merk je pas als er ergens iets scheef staat.
7
+ *
8
+ * TWEE DINGEN DIE HIER MISGAAN ALS JE ZE NIET REGELT.
9
+ *
10
+ * 1. WIE BEPAALT WAT ER IN HET KADER KOMT. De URL staat in de adresbalk, dus iedereen kan hem
11
+ * aanpassen. Zonder controle kan een willekeurige vreemde pagina in jouw huisstijl verschijnen,
12
+ * compleet met jouw logo eromheen — een uitstekend vertrekpunt voor phishing. Daarom laden we
13
+ * alleen adressen die de sitebeheerder zélf als ingekaderd menu-item heeft ingesteld.
14
+ *
15
+ * 2. HET KADER BLIJFT VAAK LEEG. Veel sites weigeren te worden ingekaderd (`X-Frame-Options` of
16
+ * `frame-ancestors`). Dat kunnen we niet omzeilen — het is hun keuze — maar we kunnen wél
17
+ * voorkomen dat de bezoeker naar een wit vlak zit te kijken zonder te weten waarom. Laadt er
18
+ * binnen een paar tellen niets, dan tonen we een uitleg met een gewone link.
19
+ */
20
+ export declare function ExternEmbed({ url, hoogte }: {
21
+ url: string;
22
+ hoogte?: string;
23
+ }): import("react").JSX.Element;
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { useMergedSiteConfig } from "../settings/useSiteSettings.js";
5
+ import { isToegestaneEmbed } from "../schema/settings.js";
6
+ function ExternEmbed({ url, hoogte = "70vh" }) {
7
+ const cfg = useMergedSiteConfig();
8
+ const [geladen, setGeladen] = useState(false);
9
+ const [traag, setTraag] = useState(false);
10
+ const frame = useRef(null);
11
+ useEffect(() => {
12
+ const t = setTimeout(() => setTraag(true), 4e3);
13
+ return () => clearTimeout(t);
14
+ }, [url]);
15
+ if (!isToegestaneEmbed(url, cfg.nav)) {
16
+ return /* @__PURE__ */ jsxs("div", { className: "xicms-extern__melding", children: [
17
+ /* @__PURE__ */ jsx("h1", { children: "Pagina niet beschikbaar" }),
18
+ /* @__PURE__ */ jsx("p", { children: "Deze pagina hoort niet bij het menu van deze site en wordt daarom niet getoond." })
19
+ ] });
20
+ }
21
+ let host = url;
22
+ try {
23
+ host = new URL(url).host;
24
+ } catch {
25
+ }
26
+ return /* @__PURE__ */ jsxs("div", { className: "xicms-extern", children: [
27
+ /* @__PURE__ */ jsx(
28
+ "iframe",
29
+ {
30
+ ref: frame,
31
+ src: url,
32
+ title: host,
33
+ className: "xicms-extern__frame",
34
+ style: { height: hoogte },
35
+ onLoad: () => setGeladen(true),
36
+ sandbox: "allow-scripts allow-forms allow-same-origin",
37
+ referrerPolicy: "strict-origin-when-cross-origin",
38
+ loading: "lazy"
39
+ }
40
+ ),
41
+ traag && !geladen && /* @__PURE__ */ jsxs("p", { className: "xicms-extern__uitleg", children: [
42
+ /* @__PURE__ */ jsx("strong", { children: host }),
43
+ " laat zich niet in een kader tonen. Veel sites staan dat niet toe.",
44
+ " ",
45
+ /* @__PURE__ */ jsx("a", { href: url, target: "_blank", rel: "noopener noreferrer", children: "Open de pagina in een nieuw venster \u2197" })
46
+ ] })
47
+ ] });
48
+ }
49
+ export {
50
+ ExternEmbed
51
+ };
52
+ //# sourceMappingURL=ExternEmbed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/ExternEmbed.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { useMergedSiteConfig } from \"../settings/useSiteSettings\";\nimport { isToegestaneEmbed } from \"../schema/settings\";\n\n/**\n * Toont een externe pagina in een kader BINNEN de site, onder de logo/menubalk.\n *\n * De klassen heten `xicms-extern*` en niet `xicms-embed*`: die laatste naam is al in gebruik voor\n * het consent-blok dat video's en kaarten inkadert. Twee ongerelateerde dingen met dezelfde\n * klassenaam gaan elkaars opmaak overschrijven, en dat merk je pas als er ergens iets scheef staat.\n *\n * TWEE DINGEN DIE HIER MISGAAN ALS JE ZE NIET REGELT.\n *\n * 1. WIE BEPAALT WAT ER IN HET KADER KOMT. De URL staat in de adresbalk, dus iedereen kan hem\n * aanpassen. Zonder controle kan een willekeurige vreemde pagina in jouw huisstijl verschijnen,\n * compleet met jouw logo eromheen — een uitstekend vertrekpunt voor phishing. Daarom laden we\n * alleen adressen die de sitebeheerder zélf als ingekaderd menu-item heeft ingesteld.\n *\n * 2. HET KADER BLIJFT VAAK LEEG. Veel sites weigeren te worden ingekaderd (`X-Frame-Options` of\n * `frame-ancestors`). Dat kunnen we niet omzeilen — het is hun keuze — maar we kunnen wél\n * voorkomen dat de bezoeker naar een wit vlak zit te kijken zonder te weten waarom. Laadt er\n * binnen een paar tellen niets, dan tonen we een uitleg met een gewone link.\n */\nexport function ExternEmbed({ url, hoogte = \"70vh\" }: { url: string; hoogte?: string }) {\n const cfg = useMergedSiteConfig();\n const [geladen, setGeladen] = useState(false);\n const [traag, setTraag] = useState(false);\n const frame = useRef<HTMLIFrameElement>(null);\n\n // `load` vuurt óók als de externe site het inkaderen weigert — de browser laadt dan een lege\n // pagina en meldt dat als succes. Vandaar niet alléén op `load` vertrouwen maar op een klok: is\n // er na een paar tellen niets zichtbaars, dan is er iets aan de hand en zeggen we dat.\n useEffect(() => {\n const t = setTimeout(() => setTraag(true), 4000);\n return () => clearTimeout(t);\n }, [url]);\n\n if (!isToegestaneEmbed(url, cfg.nav)) {\n return (\n <div className=\"xicms-extern__melding\">\n <h1>Pagina niet beschikbaar</h1>\n <p>Deze pagina hoort niet bij het menu van deze site en wordt daarom niet getoond.</p>\n </div>\n );\n }\n\n let host = url;\n try {\n host = new URL(url).host;\n } catch {\n /* onmogelijk na de controle hierboven, maar een kapotte URL mag nooit de pagina slopen */\n }\n\n return (\n <div className=\"xicms-extern\">\n <iframe\n ref={frame}\n src={url}\n title={host}\n className=\"xicms-extern__frame\"\n style={{ height: hoogte }}\n onLoad={() => setGeladen(true)}\n // Zo krap als het kan terwijl een normale pagina blijft werken: geen toegang tot onze\n // opslag, geen popups, en niet zelf de bovenliggende pagina kunnen wegnavigeren.\n sandbox=\"allow-scripts allow-forms allow-same-origin\"\n referrerPolicy=\"strict-origin-when-cross-origin\"\n loading=\"lazy\"\n />\n {traag && !geladen && (\n <p className=\"xicms-extern__uitleg\">\n <strong>{host}</strong> laat zich niet in een kader tonen. Veel sites staan dat niet toe.{\" \"}\n <a href={url} target=\"_blank\" rel=\"noopener noreferrer\">Open de pagina in een nieuw venster ↗</a>\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";AAyCM,SACE,KADF;AAvCN,SAAS,WAAW,QAAQ,gBAAgB;AAC5C,SAAS,2BAA2B;AACpC,SAAS,yBAAyB;AAqB3B,SAAS,YAAY,EAAE,KAAK,SAAS,OAAO,GAAqC;AACtF,QAAM,MAAM,oBAAoB;AAChC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AACxC,QAAM,QAAQ,OAA0B,IAAI;AAK5C,YAAU,MAAM;AACd,UAAM,IAAI,WAAW,MAAM,SAAS,IAAI,GAAG,GAAI;AAC/C,WAAO,MAAM,aAAa,CAAC;AAAA,EAC7B,GAAG,CAAC,GAAG,CAAC;AAER,MAAI,CAAC,kBAAkB,KAAK,IAAI,GAAG,GAAG;AACpC,WACE,qBAAC,SAAI,WAAU,yBACb;AAAA,0BAAC,QAAG,qCAAuB;AAAA,MAC3B,oBAAC,OAAE,6FAA+E;AAAA,OACpF;AAAA,EAEJ;AAEA,MAAI,OAAO;AACX,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE;AAAA,EACtB,QAAQ;AAAA,EAER;AAEA,SACE,qBAAC,SAAI,WAAU,gBACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,KAAK;AAAA,QACL,OAAO;AAAA,QACP,WAAU;AAAA,QACV,OAAO,EAAE,QAAQ,OAAO;AAAA,QACxB,QAAQ,MAAM,WAAW,IAAI;AAAA,QAG7B,SAAQ;AAAA,QACR,gBAAe;AAAA,QACf,SAAQ;AAAA;AAAA,IACV;AAAA,IACC,SAAS,CAAC,WACT,qBAAC,OAAE,WAAU,wBACX;AAAA,0BAAC,YAAQ,gBAAK;AAAA,MAAS;AAAA,MAAmE;AAAA,MAC1F,oBAAC,OAAE,MAAM,KAAK,QAAO,UAAS,KAAI,uBAAsB,wDAAqC;AAAA,OAC/F;AAAA,KAEJ;AAEJ;","names":[]}
@@ -1,8 +1,16 @@
1
1
  import { type ReactNode } from "react";
2
+ import type { Page } from "../schema/pages.js";
3
+ type PaginaBlokken = Page["blocks"];
2
4
  interface EditorCtx {
3
5
  open: boolean;
4
6
  openEditor: () => void;
5
7
  closeEditor: () => void;
8
+ /** De blokken van de werkversie die de pagina moet tonen; `null` = toon gewoon het gepubliceerde. */
9
+ draftBlocks: PaginaBlokken | null;
10
+ /** Het pad waarvoor `draftBlocks` geldt. */
11
+ editPath: string | null;
12
+ /** Door de editor aangeroepen zodra de werkversie laadt of wijzigt. */
13
+ publishDraft: (path: string, blocks: PaginaBlokken | null) => void;
6
14
  }
7
15
  export declare function EditorProvider({ children }: {
8
16
  children: ReactNode;
@@ -1,15 +1,39 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { createContext, useContext, useState, lazy, Suspense } from "react";
3
+ import { createContext, useCallback, useContext, useMemo, useState, lazy, Suspense } from "react";
4
4
  const VisualEditor = lazy(() => import("../admin/visual/VisualEditor.js"));
5
- const Ctx = createContext({ open: false, openEditor: () => {
6
- }, closeEditor: () => {
7
- } });
5
+ const Ctx = createContext({
6
+ open: false,
7
+ openEditor: () => {
8
+ },
9
+ closeEditor: () => {
10
+ },
11
+ draftBlocks: null,
12
+ editPath: null,
13
+ publishDraft: () => {
14
+ }
15
+ });
8
16
  function EditorProvider({ children }) {
9
17
  const [open, setOpen] = useState(false);
10
- return /* @__PURE__ */ jsxs(Ctx.Provider, { value: { open, openEditor: () => setOpen(true), closeEditor: () => setOpen(false) }, children: [
18
+ const [draft, setDraft] = useState(null);
19
+ const publishDraft = useCallback((path, blocks) => {
20
+ setDraft(blocks ? { path, blocks } : null);
21
+ }, []);
22
+ const closeEditor = useCallback(() => {
23
+ setOpen(false);
24
+ setDraft(null);
25
+ }, []);
26
+ const value = useMemo(() => ({
27
+ open,
28
+ openEditor: () => setOpen(true),
29
+ closeEditor,
30
+ draftBlocks: draft?.blocks ?? null,
31
+ editPath: draft?.path ?? null,
32
+ publishDraft
33
+ }), [open, draft, closeEditor, publishDraft]);
34
+ return /* @__PURE__ */ jsxs(Ctx.Provider, { value, children: [
11
35
  children,
12
- open && /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(VisualEditor, { onClose: () => setOpen(false) }) })
36
+ open && /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(VisualEditor, { onClose: closeEditor }) })
13
37
  ] });
14
38
  }
15
39
  function useEditor() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/editor/EditorProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useState, lazy, Suspense, type ReactNode } from \"react\";\n\n/**\n * Lichte runtime-context voor het inline (visuele) bewerken op de live pagina. De echte editor\n * (TipTap/Radix, zwaar) wordt PAS geladen als een editor hem opent — publieke bezoekers downloaden\n * hem nooit. De editor leeft in de admin-entry; we laden 'm via een dynamische import (eigen chunk).\n */\nconst VisualEditor = lazy(() => import(\"../admin/visual/VisualEditor\"));\n\ninterface EditorCtx {\n open: boolean;\n openEditor: () => void;\n closeEditor: () => void;\n}\n\nconst Ctx = createContext<EditorCtx>({ open: false, openEditor: () => {}, closeEditor: () => {} });\n\nexport function EditorProvider({ children }: { children: ReactNode }) {\n const [open, setOpen] = useState(false);\n return (\n <Ctx.Provider value={{ open, openEditor: () => setOpen(true), closeEditor: () => setOpen(false) }}>\n {children}\n {open && (\n <Suspense fallback={null}>\n <VisualEditor onClose={() => setOpen(false)} />\n </Suspense>\n )}\n </Ctx.Provider>\n );\n}\n\n/** Open/sluit de visuele pagina-editor. */\nexport function useEditor(): EditorCtx {\n return useContext(Ctx);\n}\n"],"mappings":";AAsBI,SAIM,KAJN;AApBJ,SAAS,eAAe,YAAY,UAAU,MAAM,gBAAgC;AAOpF,MAAM,eAAe,KAAK,MAAM,OAAO,8BAA8B,CAAC;AAQtE,MAAM,MAAM,cAAyB,EAAE,MAAM,OAAO,YAAY,MAAM;AAAC,GAAG,aAAa,MAAM;AAAC,EAAE,CAAC;AAE1F,SAAS,eAAe,EAAE,SAAS,GAA4B;AACpE,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,SACE,qBAAC,IAAI,UAAJ,EAAa,OAAO,EAAE,MAAM,YAAY,MAAM,QAAQ,IAAI,GAAG,aAAa,MAAM,QAAQ,KAAK,EAAE,GAC7F;AAAA;AAAA,IACA,QACC,oBAAC,YAAS,UAAU,MAClB,8BAAC,gBAAa,SAAS,MAAM,QAAQ,KAAK,GAAG,GAC/C;AAAA,KAEJ;AAEJ;AAGO,SAAS,YAAuB;AACrC,SAAO,WAAW,GAAG;AACvB;","names":[]}
1
+ {"version":3,"sources":["../../src/editor/EditorProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useCallback, useContext, useMemo, useState, lazy, Suspense, type ReactNode } from \"react\";\n// Het bloktype van de PAGINA zelf, zodat wat hier langskomt per definitie past in wat\n// `PublishedPage` rendert. Een eigen definitie zou hier stil uit de pas kunnen lopen.\nimport type { Page } from \"../schema/pages\";\n\ntype PaginaBlokken = Page[\"blocks\"];\n\n/**\n * Lichte runtime-context voor het inline (visuele) bewerken op de live pagina. De echte editor\n * (TipTap/Radix, zwaar) wordt PAS geladen als een editor hem opent — publieke bezoekers downloaden\n * hem nooit. De editor leeft in de admin-entry; we laden 'm via een dynamische import (eigen chunk).\n *\n * ── WAAROM DE BLOKKEN HIER LANGSKOMEN ─────────────────────────────────────────────────────\n *\n * De editor tekende de pagina vroeger zélf, in een eigen canvas binnen een schermvullende overlay.\n * Je klikte \"Pagina bewerken\" en je site verdween achter een donker paneel met een nabootsing erin.\n * Dat voelde niet als je site bewerken maar als een ander programma openen.\n *\n * Nu blijft de PAGINA de pagina en wordt alleen de DATA vervangen: de editor laadt de werkversie en\n * zet die hier neer, waarna `PublishedPage` de werkversie rendert in plaats van de gepubliceerde\n * blokken. Dezelfde componenten, dezelfde huisstijl, dezelfde layout — alleen andere inhoud.\n *\n * Die omweg via de data is wezenlijk, geen implementatiedetail. Alleen potloodjes over de live\n * pagina leggen zou betekenen dat je de WERKVERSIE bewerkt terwijl je de GEPUBLICEERDE tekst ziet.\n * Zodra je iets wijzigt en nog niet publiceert, lopen die twee uit elkaar en bewerk je iets anders\n * dan er staat. Wat je ziet moet zijn wat je bewerkt.\n *\n * `editPath` hoort erbij: een site kan meerdere gerenderde pagina's in beeld hebben (een\n * ingekaderde pagina, een preview). Alleen het pad dat de editor open heeft mag zijn blokken\n * verwisselen.\n */\nconst VisualEditor = lazy(() => import(\"../admin/visual/VisualEditor\"));\n\ninterface EditorCtx {\n open: boolean;\n openEditor: () => void;\n closeEditor: () => void;\n /** De blokken van de werkversie die de pagina moet tonen; `null` = toon gewoon het gepubliceerde. */\n draftBlocks: PaginaBlokken | null;\n /** Het pad waarvoor `draftBlocks` geldt. */\n editPath: string | null;\n /** Door de editor aangeroepen zodra de werkversie laadt of wijzigt. */\n publishDraft: (path: string, blocks: PaginaBlokken | null) => void;\n}\n\nconst Ctx = createContext<EditorCtx>({\n open: false,\n openEditor: () => {},\n closeEditor: () => {},\n draftBlocks: null,\n editPath: null,\n publishDraft: () => {},\n});\n\nexport function EditorProvider({ children }: { children: ReactNode }) {\n const [open, setOpen] = useState(false);\n const [draft, setDraft] = useState<{ path: string; blocks: PaginaBlokken } | null>(null);\n\n const publishDraft = useCallback((path: string, blocks: PaginaBlokken | null) => {\n setDraft(blocks ? { path, blocks } : null);\n }, []);\n\n const closeEditor = useCallback(() => {\n setOpen(false);\n // De werkversie mee opruimen: laat je hem staan, dan blijft de bezoeker na het sluiten de\n // ongepubliceerde tekst zien terwijl de balk weg is — een pagina die iets toont wat niet live is.\n setDraft(null);\n }, []);\n\n const value = useMemo<EditorCtx>(() => ({\n open,\n openEditor: () => setOpen(true),\n closeEditor,\n draftBlocks: draft?.blocks ?? null,\n editPath: draft?.path ?? null,\n publishDraft,\n }), [open, draft, closeEditor, publishDraft]);\n\n return (\n <Ctx.Provider value={value}>\n {children}\n {open && (\n <Suspense fallback={null}>\n <VisualEditor onClose={closeEditor} />\n </Suspense>\n )}\n </Ctx.Provider>\n );\n}\n\n/** Open/sluit de visuele pagina-editor. */\nexport function useEditor(): EditorCtx {\n return useContext(Ctx);\n}\n"],"mappings":";AAiFI,SAIM,KAJN;AA/EJ,SAAS,eAAe,aAAa,YAAY,SAAS,UAAU,MAAM,gBAAgC;AA+B1G,MAAM,eAAe,KAAK,MAAM,OAAO,8BAA8B,CAAC;AActE,MAAM,MAAM,cAAyB;AAAA,EACnC,MAAM;AAAA,EACN,YAAY,MAAM;AAAA,EAAC;AAAA,EACnB,aAAa,MAAM;AAAA,EAAC;AAAA,EACpB,aAAa;AAAA,EACb,UAAU;AAAA,EACV,cAAc,MAAM;AAAA,EAAC;AACvB,CAAC;AAEM,SAAS,eAAe,EAAE,SAAS,GAA4B;AACpE,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAyD,IAAI;AAEvF,QAAM,eAAe,YAAY,CAAC,MAAc,WAAiC;AAC/E,aAAS,SAAS,EAAE,MAAM,OAAO,IAAI,IAAI;AAAA,EAC3C,GAAG,CAAC,CAAC;AAEL,QAAM,cAAc,YAAY,MAAM;AACpC,YAAQ,KAAK;AAGb,aAAS,IAAI;AAAA,EACf,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ,QAAmB,OAAO;AAAA,IACtC;AAAA,IACA,YAAY,MAAM,QAAQ,IAAI;AAAA,IAC9B;AAAA,IACA,aAAa,OAAO,UAAU;AAAA,IAC9B,UAAU,OAAO,QAAQ;AAAA,IACzB;AAAA,EACF,IAAI,CAAC,MAAM,OAAO,aAAa,YAAY,CAAC;AAE5C,SACE,qBAAC,IAAI,UAAJ,EAAa,OACX;AAAA;AAAA,IACA,QACC,oBAAC,YAAS,UAAU,MAClB,8BAAC,gBAAa,SAAS,aAAa,GACtC;AAAA,KAEJ;AAEJ;AAGO,SAAS,YAAuB;AACrC,SAAO,WAAW,GAAG;AACvB;","names":[]}
package/dist/index.d.ts CHANGED
@@ -39,6 +39,7 @@ export { LoginForm } from "./auth/LoginForm.js";
39
39
  export { roleForSite, canEdit, canManageSite, isSuperadmin, type Role, type XiClaims, } from "./auth/roles.js";
40
40
  export type { NewsArticle, MediaItem, MediaVariant } from "./content/types.js";
41
41
  export { EditBar } from "./components/EditBar.js";
42
+ export { ExternEmbed } from "./components/ExternEmbed.js";
42
43
  export { ResponsiveImage } from "./components/ResponsiveImage.js";
43
44
  export { PageView } from "./components/PageView.js";
44
45
  export { NewsList, NewsArticleView, useNews } from "./components/News.js";