@xiwebdesign/xicms-core 7.15.0 → 7.16.1
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.
- package/dist/admin/AdminApp.js +4 -3
- package/dist/admin/AdminApp.js.map +1 -1
- package/dist/admin/dashboard/DashboardPanel.d.ts +3 -0
- package/dist/admin/dashboard/DashboardPanel.js +329 -0
- package/dist/admin/dashboard/DashboardPanel.js.map +1 -0
- package/dist/admin/navContext.d.ts +14 -0
- package/dist/admin/navContext.js +12 -0
- package/dist/admin/navContext.js.map +1 -0
- package/dist/admin/pages/PagesPanel.js +39 -12
- package/dist/admin/pages/PagesPanel.js.map +1 -1
- package/dist/admin/panelRegistry.js +4 -0
- package/dist/admin/panelRegistry.js.map +1 -1
- package/dist/admin/shop/ShopSettingsTab.js +7 -9
- package/dist/admin/shop/ShopSettingsTab.js.map +1 -1
- package/dist/admin/ui/PanelIcon.js +5 -0
- package/dist/admin/ui/PanelIcon.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +56 -22
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/auth/AuthProvider.js +9 -2
- package/dist/auth/AuthProvider.js.map +1 -1
- package/dist/blocks/builtins/CountdownRender.js +7 -1
- package/dist/blocks/builtins/CountdownRender.js.map +1 -1
- package/dist/components/EditBar.js +0 -2
- package/dist/components/EditBar.js.map +1 -1
- package/dist/components/barOffset.js +1 -1
- package/dist/components/barOffset.js.map +1 -1
- package/dist/schema/settings.d.ts +4 -0
- package/dist/schema/settings.js +10 -1
- package/dist/schema/settings.js.map +1 -1
- package/dist/styles.css +96 -22
- package/dist/version.js +1 -1
- 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-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}
|
|
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-800:oklch(27.9% .041 260.031);--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-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--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-\[1fr_1\.4fr_auto\]{grid-template-columns:1fr 1.4fr auto!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-baseline{align-items:baseline!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-2xl{font-size:var(--text-2xl)!important;line-height:var(--tw-leading,var(--text-2xl--line-height))!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-brand-700\/70{color:#3730a3b3!important}@supports (color:color-mix(in lab, red, red)){.text-brand-700\/70{color:color-mix(in oklab, var(--color-brand-700) 70%, transparent)!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-slate-800{color:var(--color-slate-800)!important}.text-slate-900{color:var(--color-slate-900)!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}
|
|
@@ -95,7 +95,9 @@ function AuthProvider({
|
|
|
95
95
|
setLoading(false);
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
|
-
void sdk.getRedirectResult(auth).
|
|
98
|
+
void sdk.getRedirectResult(auth).then(async (res) => {
|
|
99
|
+
if (res?.user) await weigerZonderToegang(auth, sdk);
|
|
100
|
+
}).catch((e) => {
|
|
99
101
|
console.error("[xicms] inloggen via redirect mislukt:", e);
|
|
100
102
|
});
|
|
101
103
|
if (sdk.isSignInWithEmailLink(auth, window.location.href)) {
|
|
@@ -118,7 +120,7 @@ function AuthProvider({
|
|
|
118
120
|
}
|
|
119
121
|
unsub = sdk.onAuthStateChanged(auth, async (u) => {
|
|
120
122
|
setUser(u);
|
|
121
|
-
|
|
123
|
+
if (u) setAuthHint(true);
|
|
122
124
|
await loadClaims(u);
|
|
123
125
|
setLoading(false);
|
|
124
126
|
});
|
|
@@ -148,6 +150,11 @@ function AuthProvider({
|
|
|
148
150
|
const { auth, sdk } = await authSdk();
|
|
149
151
|
const provider = new sdk.GoogleAuthProvider();
|
|
150
152
|
wekOp();
|
|
153
|
+
if (process.env.NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN === "1") {
|
|
154
|
+
setAuthHint(true);
|
|
155
|
+
await sdk.signInWithRedirect(auth, provider);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
151
158
|
try {
|
|
152
159
|
await sdk.signInWithPopup(auth, provider);
|
|
153
160
|
await weigerZonderToegang(auth, sdk);
|
|
@@ -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 /**\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":[]}
|
|
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 // Sinds redirect de primaire flow is (7.16) hoort hier ook de site-toegangscontrole die het\n // popup-pad na afloop deed: een geldig Google-account is niet hetzelfde als toegang tot\n // DEZE site (weigerZonderToegang logt dan meteen weer uit).\n void sdk\n .getRedirectResult(auth)\n .then(async (res) => {\n if (res?.user) await weigerZonderToegang(auth, sdk);\n })\n .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 /**\n * Hint alleen ZETTEN bij een sessie — nooit wissen op `null`.\n *\n * `null` betekent hier niet \"uitgelogd\" maar \"(nog) geen sessie\", en dat is óók de\n * toestand vlak vóór een redirect-login: signInWithGoogle zet de hint, en deze listener\n * vuurde daarna met `null` en WISTE hem weer — nog vóór de navigatie naar Google. Terug\n * op de site weigerde het effect hierboven daardoor de auth-SDK te laden, werd\n * getRedirectResult nooit gedraaid en bleef het antwoord van Google onaangeroerd in\n * localStorage liggen (`firebase:authEvent:…` aangetroffen naast een ontbrekende hint,\n * live op christelijkkraamcadeau; hint handmatig terugzetten + herladen logde alsnog in).\n * Wissen hoort alleen bij expliciet uitloggen — dat doet signOut al.\n */\n if (u) setAuthHint(true);\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 /**\n * SAME-ORIGIN-SETUP → REDIRECT ALS PRIMAIRE FLOW (7.16).\n *\n * De popup-flow is met COOP structureel onbetrouwbaar geworden, en dat is niet met onze\n * eigen headers te repareren. Gemeten op christelijkkraamcadeau mét de\n * `same-origin-allow-popups`-header correct live (curl bevestigd, ook op de handler):\n * `signInWithPopup` faalde alsnog met \"auth/popup-closed-by-user\" terwijl het venster\n * gewoon openstond — Google's eigen COOP op accounts.google.com knipt de band tussen\n * opener en popup door, waarna `popup.closed` vals `true` rapporteert. De handler maakt\n * de login ondertussen gewoon af (de sessie komt via onAuthStateChanged alsnog binnen),\n * maar niemand kan het venster meer sluiten: een wees-tab met de handler-URL blijft\n * achter. Precies zo gerapporteerd door de beheerder.\n *\n * Met een same-origin authDomain (de standaard-setup, zie next/authRewrites.ts) is de\n * redirect-flow de betrouwbare route: geen popup, geen COOP, geen wees-venster — de\n * pagina gaat naar Google en komt ingelogd terug. De popup blijft alleen bestaan voor\n * de niet-same-origin-configuratie, waar redirect juist de kapotte kant is.\n */\n if (process.env.NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN === \"1\") {\n // Hint vóór vertrek, om dezelfde reden als bij de terugval hieronder: zonder hint laadt\n // de teruggekeerde pagina de auth-SDK niet en wordt het antwoord nooit opgehaald.\n setAuthHint(true);\n await sdk.signInWithRedirect(auth, provider);\n return;\n }\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":[]}
|
|
@@ -40,7 +40,13 @@ function CountdownDigits({ r, showLabels = true }) {
|
|
|
40
40
|
}
|
|
41
41
|
function CountdownRender({ block, mode }) {
|
|
42
42
|
const r = useCountdown(block.target);
|
|
43
|
-
if (!block.target)
|
|
43
|
+
if (!block.target) {
|
|
44
|
+
if (mode !== "preview") return null;
|
|
45
|
+
return /* @__PURE__ */ jsxs("section", { className: "xicms-countdown", children: [
|
|
46
|
+
block.heading && /* @__PURE__ */ jsx(Heading, { level: 2, className: "xicms-countdown__heading", "data-xicms-edit": "heading", children: block.heading }),
|
|
47
|
+
/* @__PURE__ */ jsx("p", { className: "xicms-countdown__placeholder", children: "\u23F3 Stel een einddatum in om de aftelteller te tonen." })
|
|
48
|
+
] });
|
|
49
|
+
}
|
|
44
50
|
if (r?.done && block.onExpire === "hide" && mode !== "preview") return null;
|
|
45
51
|
return /* @__PURE__ */ jsxs("section", { className: "xicms-countdown", children: [
|
|
46
52
|
block.heading && /* @__PURE__ */ jsx(Heading, { level: 2, className: "xicms-countdown__heading", "data-xicms-edit": "heading", children: block.heading }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/blocks/builtins/CountdownRender.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport type { CountdownBlockSchema } from \"../../schema/blocks\";\nimport type { BlockRenderProps } from \"../registry\";\nimport { Heading } from \"../ui\";\n\nexport interface Remaining {\n days: number;\n hours: number;\n minutes: number;\n seconds: number;\n done: boolean;\n}\n\nfunction compute(target: string): Remaining | null {\n const t = new Date(target).getTime();\n if (Number.isNaN(t)) return null;\n const diff = t - Date.now();\n if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0, done: true };\n const s = Math.floor(diff / 1000);\n return {\n days: Math.floor(s / 86400),\n hours: Math.floor((s % 86400) / 3600),\n minutes: Math.floor((s % 3600) / 60),\n seconds: s % 60,\n done: false,\n };\n}\n\n/**\n * Tikkende afteltimer naar `target`. Client-only (start `null` om hydration-mismatch te voorkomen).\n * Gedeeld door het volwaardige Countdown-blok en de compacte widget.\n */\nexport function useCountdown(target: string): Remaining | null {\n const [r, setR] = useState<Remaining | null>(null);\n useEffect(() => {\n if (!target) return;\n setR(compute(target));\n const id = setInterval(() => setR(compute(target)), 1000);\n return () => clearInterval(id);\n }, [target]);\n return r;\n}\n\nconst UNITS: { key: keyof Remaining; label: string }[] = [\n { key: \"days\", label: \"dagen\" },\n { key: \"hours\", label: \"uur\" },\n { key: \"minutes\", label: \"min\" },\n { key: \"seconds\", label: \"sec\" },\n];\n\nexport function CountdownDigits({ r, showLabels = true }: { r: Remaining; showLabels?: boolean }) {\n return (\n <div className=\"xicms-countdown__digits\" role=\"timer\" aria-live=\"off\">\n {UNITS.map((u) => (\n <div key={u.key} className=\"xicms-countdown__unit\">\n <span className=\"xicms-countdown__value\">{String(r[u.key] as number).padStart(2, \"0\")}</span>\n {showLabels && <span className=\"xicms-countdown__label\">{u.label}</span>}\n </div>\n ))}\n </div>\n );\n}\n\ntype Countdown = z.infer<typeof CountdownBlockSchema>;\n\nexport function CountdownRender({ block, mode }: BlockRenderProps<Countdown>) {\n const r = useCountdown(block.target);\n if (!block.target) return null;\n // Na afloop optioneel het hele blok verbergen (bv. een verlopen actie-aankondiging) —\n // behalve in de editor-preview, anders kan de editor het blok niet meer terugvinden.\n if (r?.done && block.onExpire === \"hide\" && mode !== \"preview\") return null;\n return (\n <section className=\"xicms-countdown\">\n {block.heading && <Heading level={2} className=\"xicms-countdown__heading\" data-xicms-edit=\"heading\">{block.heading}</Heading>}\n {r === null ? (\n <p className=\"xicms-countdown__loading\" aria-hidden=\"true\">…</p>\n ) : r.done ? (\n <p className=\"xicms-countdown__expired\" data-xicms-edit=\"expiredText\">{block.expiredText}</p>\n ) : (\n <CountdownDigits r={r} showLabels={block.showLabels} />\n )}\n </section>\n );\n}\n"],"mappings":";AAyDQ,SACE,KADF;AAvDR,SAAS,WAAW,gBAAgB;AAIpC,SAAS,eAAe;AAUxB,SAAS,QAAQ,QAAkC;AACjD,QAAM,IAAI,IAAI,KAAK,MAAM,EAAE,QAAQ;AACnC,MAAI,OAAO,MAAM,CAAC,EAAG,QAAO;AAC5B,QAAM,OAAO,IAAI,KAAK,IAAI;AAC1B,MAAI,QAAQ,EAAG,QAAO,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,KAAK;AAC9E,QAAM,IAAI,KAAK,MAAM,OAAO,GAAI;AAChC,SAAO;AAAA,IACL,MAAM,KAAK,MAAM,IAAI,KAAK;AAAA,IAC1B,OAAO,KAAK,MAAO,IAAI,QAAS,IAAI;AAAA,IACpC,SAAS,KAAK,MAAO,IAAI,OAAQ,EAAE;AAAA,IACnC,SAAS,IAAI;AAAA,IACb,MAAM;AAAA,EACR;AACF;AAMO,SAAS,aAAa,QAAkC;AAC7D,QAAM,CAAC,GAAG,IAAI,IAAI,SAA2B,IAAI;AACjD,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,SAAK,QAAQ,MAAM,CAAC;AACpB,UAAM,KAAK,YAAY,MAAM,KAAK,QAAQ,MAAM,CAAC,GAAG,GAAI;AACxD,WAAO,MAAM,cAAc,EAAE;AAAA,EAC/B,GAAG,CAAC,MAAM,CAAC;AACX,SAAO;AACT;AAEA,MAAM,QAAmD;AAAA,EACvD,EAAE,KAAK,QAAQ,OAAO,QAAQ;AAAA,EAC9B,EAAE,KAAK,SAAS,OAAO,MAAM;AAAA,EAC7B,EAAE,KAAK,WAAW,OAAO,MAAM;AAAA,EAC/B,EAAE,KAAK,WAAW,OAAO,MAAM;AACjC;AAEO,SAAS,gBAAgB,EAAE,GAAG,aAAa,KAAK,GAA2C;AAChG,SACE,oBAAC,SAAI,WAAU,2BAA0B,MAAK,SAAQ,aAAU,OAC7D,gBAAM,IAAI,CAAC,MACV,qBAAC,SAAgB,WAAU,yBACzB;AAAA,wBAAC,UAAK,WAAU,0BAA0B,iBAAO,EAAE,EAAE,GAAG,CAAW,EAAE,SAAS,GAAG,GAAG,GAAE;AAAA,IACrF,cAAc,oBAAC,UAAK,WAAU,0BAA0B,YAAE,OAAM;AAAA,OAFzD,EAAE,GAGZ,CACD,GACH;AAEJ;AAIO,SAAS,gBAAgB,EAAE,OAAO,KAAK,GAAgC;AAC5E,QAAM,IAAI,aAAa,MAAM,MAAM;
|
|
1
|
+
{"version":3,"sources":["../../../src/blocks/builtins/CountdownRender.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport type { CountdownBlockSchema } from \"../../schema/blocks\";\nimport type { BlockRenderProps } from \"../registry\";\nimport { Heading } from \"../ui\";\n\nexport interface Remaining {\n days: number;\n hours: number;\n minutes: number;\n seconds: number;\n done: boolean;\n}\n\nfunction compute(target: string): Remaining | null {\n const t = new Date(target).getTime();\n if (Number.isNaN(t)) return null;\n const diff = t - Date.now();\n if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0, done: true };\n const s = Math.floor(diff / 1000);\n return {\n days: Math.floor(s / 86400),\n hours: Math.floor((s % 86400) / 3600),\n minutes: Math.floor((s % 3600) / 60),\n seconds: s % 60,\n done: false,\n };\n}\n\n/**\n * Tikkende afteltimer naar `target`. Client-only (start `null` om hydration-mismatch te voorkomen).\n * Gedeeld door het volwaardige Countdown-blok en de compacte widget.\n */\nexport function useCountdown(target: string): Remaining | null {\n const [r, setR] = useState<Remaining | null>(null);\n useEffect(() => {\n if (!target) return;\n setR(compute(target));\n const id = setInterval(() => setR(compute(target)), 1000);\n return () => clearInterval(id);\n }, [target]);\n return r;\n}\n\nconst UNITS: { key: keyof Remaining; label: string }[] = [\n { key: \"days\", label: \"dagen\" },\n { key: \"hours\", label: \"uur\" },\n { key: \"minutes\", label: \"min\" },\n { key: \"seconds\", label: \"sec\" },\n];\n\nexport function CountdownDigits({ r, showLabels = true }: { r: Remaining; showLabels?: boolean }) {\n return (\n <div className=\"xicms-countdown__digits\" role=\"timer\" aria-live=\"off\">\n {UNITS.map((u) => (\n <div key={u.key} className=\"xicms-countdown__unit\">\n <span className=\"xicms-countdown__value\">{String(r[u.key] as number).padStart(2, \"0\")}</span>\n {showLabels && <span className=\"xicms-countdown__label\">{u.label}</span>}\n </div>\n ))}\n </div>\n );\n}\n\ntype Countdown = z.infer<typeof CountdownBlockSchema>;\n\nexport function CountdownRender({ block, mode }: BlockRenderProps<Countdown>) {\n const r = useCountdown(block.target);\n // Zonder einddatum is er publiek niets te tonen, maar in de editor-preview zou het blok dan\n // onzichtbaar (en onvindbaar) zijn — toon daar een placeholder die uitlegt wat er mist.\n if (!block.target) {\n if (mode !== \"preview\") return null;\n return (\n <section className=\"xicms-countdown\">\n {block.heading && <Heading level={2} className=\"xicms-countdown__heading\" data-xicms-edit=\"heading\">{block.heading}</Heading>}\n <p className=\"xicms-countdown__placeholder\">⏳ Stel een einddatum in om de aftelteller te tonen.</p>\n </section>\n );\n }\n // Na afloop optioneel het hele blok verbergen (bv. een verlopen actie-aankondiging) —\n // behalve in de editor-preview, anders kan de editor het blok niet meer terugvinden.\n if (r?.done && block.onExpire === \"hide\" && mode !== \"preview\") return null;\n return (\n <section className=\"xicms-countdown\">\n {block.heading && <Heading level={2} className=\"xicms-countdown__heading\" data-xicms-edit=\"heading\">{block.heading}</Heading>}\n {r === null ? (\n <p className=\"xicms-countdown__loading\" aria-hidden=\"true\">…</p>\n ) : r.done ? (\n <p className=\"xicms-countdown__expired\" data-xicms-edit=\"expiredText\">{block.expiredText}</p>\n ) : (\n <CountdownDigits r={r} showLabels={block.showLabels} />\n )}\n </section>\n );\n}\n"],"mappings":";AAyDQ,SACE,KADF;AAvDR,SAAS,WAAW,gBAAgB;AAIpC,SAAS,eAAe;AAUxB,SAAS,QAAQ,QAAkC;AACjD,QAAM,IAAI,IAAI,KAAK,MAAM,EAAE,QAAQ;AACnC,MAAI,OAAO,MAAM,CAAC,EAAG,QAAO;AAC5B,QAAM,OAAO,IAAI,KAAK,IAAI;AAC1B,MAAI,QAAQ,EAAG,QAAO,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,KAAK;AAC9E,QAAM,IAAI,KAAK,MAAM,OAAO,GAAI;AAChC,SAAO;AAAA,IACL,MAAM,KAAK,MAAM,IAAI,KAAK;AAAA,IAC1B,OAAO,KAAK,MAAO,IAAI,QAAS,IAAI;AAAA,IACpC,SAAS,KAAK,MAAO,IAAI,OAAQ,EAAE;AAAA,IACnC,SAAS,IAAI;AAAA,IACb,MAAM;AAAA,EACR;AACF;AAMO,SAAS,aAAa,QAAkC;AAC7D,QAAM,CAAC,GAAG,IAAI,IAAI,SAA2B,IAAI;AACjD,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,SAAK,QAAQ,MAAM,CAAC;AACpB,UAAM,KAAK,YAAY,MAAM,KAAK,QAAQ,MAAM,CAAC,GAAG,GAAI;AACxD,WAAO,MAAM,cAAc,EAAE;AAAA,EAC/B,GAAG,CAAC,MAAM,CAAC;AACX,SAAO;AACT;AAEA,MAAM,QAAmD;AAAA,EACvD,EAAE,KAAK,QAAQ,OAAO,QAAQ;AAAA,EAC9B,EAAE,KAAK,SAAS,OAAO,MAAM;AAAA,EAC7B,EAAE,KAAK,WAAW,OAAO,MAAM;AAAA,EAC/B,EAAE,KAAK,WAAW,OAAO,MAAM;AACjC;AAEO,SAAS,gBAAgB,EAAE,GAAG,aAAa,KAAK,GAA2C;AAChG,SACE,oBAAC,SAAI,WAAU,2BAA0B,MAAK,SAAQ,aAAU,OAC7D,gBAAM,IAAI,CAAC,MACV,qBAAC,SAAgB,WAAU,yBACzB;AAAA,wBAAC,UAAK,WAAU,0BAA0B,iBAAO,EAAE,EAAE,GAAG,CAAW,EAAE,SAAS,GAAG,GAAG,GAAE;AAAA,IACrF,cAAc,oBAAC,UAAK,WAAU,0BAA0B,YAAE,OAAM;AAAA,OAFzD,EAAE,GAGZ,CACD,GACH;AAEJ;AAIO,SAAS,gBAAgB,EAAE,OAAO,KAAK,GAAgC;AAC5E,QAAM,IAAI,aAAa,MAAM,MAAM;AAGnC,MAAI,CAAC,MAAM,QAAQ;AACjB,QAAI,SAAS,UAAW,QAAO;AAC/B,WACE,qBAAC,aAAQ,WAAU,mBAChB;AAAA,YAAM,WAAW,oBAAC,WAAQ,OAAO,GAAG,WAAU,4BAA2B,mBAAgB,WAAW,gBAAM,SAAQ;AAAA,MACnH,oBAAC,OAAE,WAAU,gCAA+B,sEAAmD;AAAA,OACjG;AAAA,EAEJ;AAGA,MAAI,GAAG,QAAQ,MAAM,aAAa,UAAU,SAAS,UAAW,QAAO;AACvE,SACE,qBAAC,aAAQ,WAAU,mBAChB;AAAA,UAAM,WAAW,oBAAC,WAAQ,OAAO,GAAG,WAAU,4BAA2B,mBAAgB,WAAW,gBAAM,SAAQ;AAAA,IAClH,MAAM,OACL,oBAAC,OAAE,WAAU,4BAA2B,eAAY,QAAO,oBAAC,IAC1D,EAAE,OACJ,oBAAC,OAAE,WAAU,4BAA2B,mBAAgB,eAAe,gBAAM,aAAY,IAEzF,oBAAC,mBAAgB,GAAM,YAAY,MAAM,YAAY;AAAA,KAEzD;AAEJ;","names":[]}
|
|
@@ -3,11 +3,9 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useAuth } from "../auth/AuthProvider.js";
|
|
4
4
|
import { canEdit } from "../auth/roles.js";
|
|
5
5
|
import { useEditor } from "../editor/EditorProvider.js";
|
|
6
|
-
import { useBalkOffset } from "./barOffset.js";
|
|
7
6
|
function EditBar() {
|
|
8
7
|
const { user, role } = useAuth();
|
|
9
8
|
const { open } = useEditor();
|
|
10
|
-
useBalkOffset(Boolean(user) && canEdit(role) && !open);
|
|
11
9
|
if (!canEdit(role) || !user || open) return null;
|
|
12
10
|
return /* @__PURE__ */ jsxs("div", { className: "xicms-editbar", role: "toolbar", "aria-label": "Beheer", "data-xicms-tooling": "", children: [
|
|
13
11
|
/* @__PURE__ */ jsx("span", { className: "xicms-editbar__brand", children: "XiCMS" }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/EditBar.tsx"],"sourcesContent":["\"use client\";\n\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { canEdit } from \"../auth/roles\";\nimport { useEditor, type Bewerkmodus } from \"../editor/EditorProvider\";\
|
|
1
|
+
{"version":3,"sources":["../../src/components/EditBar.tsx"],"sourcesContent":["\"use client\";\n\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { canEdit } from \"../auth/roles\";\nimport { useEditor, type Bewerkmodus } from \"../editor/EditorProvider\";\n\n/**\n * Zwevende werkbalk voor ingelogde beheerders/editors.\n *\n * Bewust met EIGEN klassen en eigen kleuren, niet met `.xicms-btn`. Die knop leest de\n * `--xicms-*`-tokens van de klantsite, en dit balkje zweeft over die site heen: het nam daardoor\n * half de huisstijl over (en op plekken waar de tokens niet in scope zijn, viel het terug op een\n * indigo die bij niets hoorde). Gereedschap hoort er als gereedschap uit te zien — hetzelfde op\n * elke site, herkenbaar los van de pagina eronder.\n *\n * ── VIER OPTIES, ALTIJD DEZELFDE (7.8) ────────────────────────────────────────────────────\n *\n * De balk had één hoofdactie (\"Pagina bewerken\") en verdween zodra je die indrukte: de editor\n * bracht zijn eigen balk mee, met andere knoppen op andere plekken. Je verloor dus je houvast op\n * het moment dat je het het hardst nodig had, en er was geen weg terug naar de live site zonder\n * de editor te sluiten.\n *\n * Nu staan er vier vaste opties — Live site · Bewerken · Beheren · Uitloggen — en die staan er in\n * béide toestanden, op dezelfde plek. `AdminModes` hieronder is letterlijk hetzelfde component in\n * de balk van de editor, zodat het overschakelen geen wisseling van balk is maar alleen een andere\n * knop die oplicht.\n */\nexport function EditBar() {\n // Bewust via useAuth (niet via het deprecated ContentProvider-pad): de EditBar hoort bij het\n // block-model en mag niet aan het legacy inline-systeem hangen.\n const { user, role } = useAuth();\n const { open } = useEditor();\n // Geen useBalkOffset meer: het pilletje zweeft óver de site (7.16), er wordt niets meer\n // omlaag geduwd — dus valt er ook niets meer onder een balk vandaan te schuiven.\n\n // In de bewerkmodus tekent de editor de balk (met dezelfde vier opties plus zijn gereedschap);\n // twee balken onder elkaar zou de site twee keer zo ver omlaag duwen.\n if (!canEdit(role) || !user || open) return null;\n\n return (\n <div className=\"xicms-editbar\" role=\"toolbar\" aria-label=\"Beheer\" data-xicms-tooling=\"\">\n <span className=\"xicms-editbar__brand\">XiCMS</span>\n <AdminModes actief=\"live\" />\n </div>\n );\n}\n\n/**\n * De vier opties zelf, los zodat de editor ze kan hergebruiken.\n *\n * `actief` zegt welke toestand je nú bekijkt, niet wat de knop doet: \"Live site\" is ingedrukt\n * zolang je de pagina ziet zoals een bezoeker hem ziet. Zonder die markering is er geen verschil\n * te zien tussen \"ik kijk naar de live site\" en \"ik bewerk maar heb nog niets veranderd\", en dat\n * verschil is precies waar deze balk over gaat.\n */\nexport function AdminModes({\n actief,\n voorSwitch,\n}: {\n actief: Bewerkmodus;\n /**\n * Kans voor de editor om iets te zeggen vóór je weggaat — bijvoorbeeld dat er nog wijzigingen\n * klaarstaan die niet live staan. Bewust niet blokkerend: sluiten bewaart je werk, dus een\n * bevestigingsvraag zou alleen maar in de weg zitten. Melden is genoeg, tegenhouden is te veel.\n */\n voorSwitch?: (modus: Bewerkmodus) => void;\n}) {\n const { demo, signOut } = useAuth();\n const { zetModus } = useEditor();\n\n const knop = (modus: Bewerkmodus, label: string, titel: string) => (\n <button\n type=\"button\"\n className={`xicms-editbar__btn${actief === modus ? \" is-actief\" : \"\"}`}\n aria-pressed={actief === modus}\n title={titel}\n onClick={() => {\n if (modus !== actief) voorSwitch?.(modus);\n zetModus(modus);\n }}\n >\n {label}\n </button>\n );\n\n return (\n <>\n {knop(\"live\", \"Live site\", \"De pagina precies zoals een bezoeker hem ziet\")}\n {knop(\"bewerken\", \"Bewerken\", \"Dezelfde pagina, met de werkversie en potlood/tandwiel per blok\")}\n <span className=\"xicms-editbar__sep\" aria-hidden=\"true\" />\n {/* In de demo bestaat er geen /beheer-route; daar bewerk je alleen op de pagina. */}\n {!demo && (\n <a href=\"/beheer\" className=\"xicms-editbar__btn\" title=\"Pagina's, media, instellingen, webwinkel\">\n Beheren\n </a>\n )}\n <button type=\"button\" className=\"xicms-editbar__btn\" onClick={() => void signOut()}>\n Uitloggen\n </button>\n </>\n );\n}\n"],"mappings":";AAwCI,SA8CA,UA7CE,KADF;AAtCJ,SAAS,eAAe;AACxB,SAAS,eAAe;AACxB,SAAS,iBAAmC;AAuBrC,SAAS,UAAU;AAGxB,QAAM,EAAE,MAAM,KAAK,IAAI,QAAQ;AAC/B,QAAM,EAAE,KAAK,IAAI,UAAU;AAM3B,MAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,KAAM,QAAO;AAE5C,SACE,qBAAC,SAAI,WAAU,iBAAgB,MAAK,WAAU,cAAW,UAAS,sBAAmB,IACnF;AAAA,wBAAC,UAAK,WAAU,wBAAuB,mBAAK;AAAA,IAC5C,oBAAC,cAAW,QAAO,QAAO;AAAA,KAC5B;AAEJ;AAUO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AACF,GAQG;AACD,QAAM,EAAE,MAAM,QAAQ,IAAI,QAAQ;AAClC,QAAM,EAAE,SAAS,IAAI,UAAU;AAE/B,QAAM,OAAO,CAAC,OAAoB,OAAe,UAC/C;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAW,qBAAqB,WAAW,QAAQ,eAAe,EAAE;AAAA,MACpE,gBAAc,WAAW;AAAA,MACzB,OAAO;AAAA,MACP,SAAS,MAAM;AACb,YAAI,UAAU,OAAQ,cAAa,KAAK;AACxC,iBAAS,KAAK;AAAA,MAChB;AAAA,MAEC;AAAA;AAAA,EACH;AAGF,SACE,iCACG;AAAA,SAAK,QAAQ,aAAa,+CAA+C;AAAA,IACzE,KAAK,YAAY,YAAY,iEAAiE;AAAA,IAC/F,oBAAC,UAAK,WAAU,sBAAqB,eAAY,QAAO;AAAA,IAEvD,CAAC,QACA,oBAAC,OAAE,MAAK,WAAU,WAAU,sBAAqB,OAAM,4CAA2C,qBAElG;AAAA,IAEF,oBAAC,YAAO,MAAK,UAAS,WAAU,sBAAqB,SAAS,MAAM,KAAK,QAAQ,GAAG,uBAEpF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/barOffset.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\n\n/**\n * Schuif de plakkende elementen van de SITE onder onze balk vandaan.\n *\n * ── WAAROM PADDING NIET GENOEG IS ─────────────────────────────────────────────────────────\n *\n * De beheerbalk staat `fixed` bovenaan en de pagina wordt omlaag geduwd met padding op `:root`.\n * Dat werkt voor alles wat gewoon meestroomt — maar niet voor een header met\n * `position: sticky; top: 0`. Die plakt aan de bovenkant van het VENSTER, niet aan de bovenkant\n * van de pagina, en schuift dus vrolijk onder onze balk door. Op groenhof was het logo en het hele\n * menu daardoor half afgedekt zodra je inlogde.\n *\n * Het is geen randgeval: een plakkende header is zo'n beetje de standaard, en elke klantsite kiest\n * daar zijn eigen klassenaam en eigen CSS voor. Wij kunnen die CSS niet kennen en willen hem al\n * helemaal niet overschrijven met een gok van een selector.\n *\n * Dus meten we het gewoon: welke elementen plakken of hangen vast aan de bovenrand, en zet daar\n * zolang de balk er staat een `top` van de balkhoogte op. Bij het opruimen gaat de oorspronkelijke\n * waarde terug — de site blijft van de klant.\n *\n * Bewust GEEN elementen aanraken die al verder naar beneden hangen (een plakkende zijbalk, een\n * knop rechtsonder): die zitten onze balk niet in de weg en verplaatsen zou ze juist verschuiven.\n */\nexport function useBalkOffset(actief: boolean, hoogte =
|
|
1
|
+
{"version":3,"sources":["../../src/components/barOffset.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\n\n/**\n * Schuif de plakkende elementen van de SITE onder onze balk vandaan.\n *\n * ── WAAROM PADDING NIET GENOEG IS ─────────────────────────────────────────────────────────\n *\n * De beheerbalk staat `fixed` bovenaan en de pagina wordt omlaag geduwd met padding op `:root`.\n * Dat werkt voor alles wat gewoon meestroomt — maar niet voor een header met\n * `position: sticky; top: 0`. Die plakt aan de bovenkant van het VENSTER, niet aan de bovenkant\n * van de pagina, en schuift dus vrolijk onder onze balk door. Op groenhof was het logo en het hele\n * menu daardoor half afgedekt zodra je inlogde.\n *\n * Het is geen randgeval: een plakkende header is zo'n beetje de standaard, en elke klantsite kiest\n * daar zijn eigen klassenaam en eigen CSS voor. Wij kunnen die CSS niet kennen en willen hem al\n * helemaal niet overschrijven met een gok van een selector.\n *\n * Dus meten we het gewoon: welke elementen plakken of hangen vast aan de bovenrand, en zet daar\n * zolang de balk er staat een `top` van de balkhoogte op. Bij het opruimen gaat de oorspronkelijke\n * waarde terug — de site blijft van de klant.\n *\n * Bewust GEEN elementen aanraken die al verder naar beneden hangen (een plakkende zijbalk, een\n * knop rechtsonder): die zitten onze balk niet in de weg en verplaatsen zou ze juist verschuiven.\n */\nexport function useBalkOffset(actief: boolean, hoogte = 56): void {\n useEffect(() => {\n if (!actief || typeof document === \"undefined\") return;\n\n const aangepast: { el: HTMLElement; top: string }[] = [];\n\n const verschuif = () => {\n for (const el of Array.from(document.querySelectorAll<HTMLElement>(\"body *\"))) {\n // Onze eigen gereedschapslaag overslaan: die staat er met opzet bovenop.\n if (el.closest(\"[data-xicms-tooling]\")) continue;\n if (aangepast.some((a) => a.el === el)) continue;\n const cs = getComputedStyle(el);\n if (cs.position !== \"sticky\" && cs.position !== \"fixed\") continue;\n // `top: auto` betekent \"waar je toevallig staat\" — daar valt niets te corrigeren.\n const top = parseFloat(cs.top);\n if (!Number.isFinite(top) || top > 4) continue;\n aangepast.push({ el, top: el.style.top });\n el.style.top = `${top + hoogte}px`;\n }\n };\n\n verschuif();\n // Een site kan later nog iets plakkends tonen (een aankondigingsbalk die pas na consent komt,\n // een menu dat bij het scrollen van gedrag verandert). Eén meting bij het openen zou dat missen.\n //\n // Wel gebundeld per frame: de observer kijkt naar class/style op de HELE boom, en tijdens het\n // typen in een contentEditable-veld komen mutaties in salvo's. Elke mutatie een volledige\n // DOM-scan geven zou het bewerken zelf traag maken — precies het scherm waar deze hook voor\n // bestaat. En: onze eigen `el.style.top` hieronder is zélf een mutatie; zonder bundeling\n // triggert de correctie de observer opnieuw (onschuldig maar zinloos rondpompen).\n let frame = 0;\n const gepland = () => {\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(verschuif);\n };\n const obs = new MutationObserver(gepland);\n obs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [\"class\", \"style\"] });\n\n return () => {\n obs.disconnect();\n cancelAnimationFrame(frame);\n for (const { el, top } of aangepast) el.style.top = top;\n };\n }, [actief, hoogte]);\n}\n"],"mappings":";AAEA,SAAS,iBAAiB;AAwBnB,SAAS,cAAc,QAAiB,SAAS,IAAU;AAChE,YAAU,MAAM;AACd,QAAI,CAAC,UAAU,OAAO,aAAa,YAAa;AAEhD,UAAM,YAAgD,CAAC;AAEvD,UAAM,YAAY,MAAM;AACtB,iBAAW,MAAM,MAAM,KAAK,SAAS,iBAA8B,QAAQ,CAAC,GAAG;AAE7E,YAAI,GAAG,QAAQ,sBAAsB,EAAG;AACxC,YAAI,UAAU,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,EAAG;AACxC,cAAM,KAAK,iBAAiB,EAAE;AAC9B,YAAI,GAAG,aAAa,YAAY,GAAG,aAAa,QAAS;AAEzD,cAAM,MAAM,WAAW,GAAG,GAAG;AAC7B,YAAI,CAAC,OAAO,SAAS,GAAG,KAAK,MAAM,EAAG;AACtC,kBAAU,KAAK,EAAE,IAAI,KAAK,GAAG,MAAM,IAAI,CAAC;AACxC,WAAG,MAAM,MAAM,GAAG,MAAM,MAAM;AAAA,MAChC;AAAA,IACF;AAEA,cAAU;AASV,QAAI,QAAQ;AACZ,UAAM,UAAU,MAAM;AACpB,2BAAqB,KAAK;AAC1B,cAAQ,sBAAsB,SAAS;AAAA,IACzC;AACA,UAAM,MAAM,IAAI,iBAAiB,OAAO;AACxC,QAAI,QAAQ,SAAS,MAAM,EAAE,WAAW,MAAM,SAAS,MAAM,YAAY,MAAM,iBAAiB,CAAC,SAAS,OAAO,EAAE,CAAC;AAEpH,WAAO,MAAM;AACX,UAAI,WAAW;AACf,2BAAqB,KAAK;AAC1B,iBAAW,EAAE,IAAI,IAAI,KAAK,UAAW,IAAG,MAAM,MAAM;AAAA,IACtD;AAAA,EACF,GAAG,CAAC,QAAQ,MAAM,CAAC;AACrB;","names":[]}
|
|
@@ -236,6 +236,10 @@ export declare const SiteSettingsSchema: z.ZodMiniObject<{
|
|
|
236
236
|
enabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
237
237
|
config: z.ZodMiniOptional<z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniUnknown>>;
|
|
238
238
|
}, z.core.$strip>]>>>;
|
|
239
|
+
dashboardLinks: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
|
|
240
|
+
label: z.ZodMiniString<string>;
|
|
241
|
+
href: z.ZodMiniString<string>;
|
|
242
|
+
}, z.core.$strip>>>;
|
|
239
243
|
}, z.core.$strip>;
|
|
240
244
|
export type SiteSettings = z.infer<typeof SiteSettingsSchema>;
|
|
241
245
|
/**
|
package/dist/schema/settings.js
CHANGED
|
@@ -121,7 +121,16 @@ const SiteSettingsSchema = z.object({
|
|
|
121
121
|
seoDefaults: z.optional(SeoSchema),
|
|
122
122
|
forms: z.optional(FormsSenderSchema),
|
|
123
123
|
footer: z.optional(FooterSettingsSchema),
|
|
124
|
-
modules: z.optional(ModulesSettingsSchema)
|
|
124
|
+
modules: z.optional(ModulesSettingsSchema),
|
|
125
|
+
/**
|
|
126
|
+
* Snelkoppelingen op de beheer-startpagina (7.16). Beheer-intern: bewust NIET in de publieke
|
|
127
|
+
* subset. Opslaan loopt via de bestaande saveSiteSettings-callable — die is passthrough met
|
|
128
|
+
* merge, dus dit veld vergt geen functions-wijziging.
|
|
129
|
+
*/
|
|
130
|
+
dashboardLinks: z.optional(z.array(z.object({
|
|
131
|
+
label: z.string().check(z.minLength(1)).check(z.maxLength(80)),
|
|
132
|
+
href: z.string().check(z.minLength(1)).check(z.maxLength(2e3))
|
|
133
|
+
})).check(z.maxLength(12)))
|
|
125
134
|
});
|
|
126
135
|
const PublicSiteSettingsSchema = z.object({
|
|
127
136
|
name: z.optional(z.string().check(z.maxLength(160))),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/schema/settings.ts"],"sourcesContent":["import * as z from \"zod/mini\";\nimport { SeoSchema } from \"./seo\";\n\n/**\n * Eén navigatie-item.\n *\n * `target` is OPTIONEEL en dat is opzet: elk bestaand menu in Git en in Firestore mist dit veld, en\n * moet blijven valideren. Ontbreekt het, dan opent de link in hetzelfde venster — precies wat een\n * gewone `<a href>` altijd al deed.\n *\n * Er staat bewust geen `type: \"page\" | \"extern\"` in. Dat onderscheid is uit de href zelf af te\n * lezen (begint hij met `/` of met `http`), en een apart veld zou een tweede waarheid introduceren\n * die uit de pas kan lopen met de href. Het beheerscherm mag de gebruiker wél die keuze voorleggen;\n * wat er wordt opgeslagen is één ondubbelzinnig adres.\n */\nexport const NavItemSchema = z.object({\n label: z.string().check(z.minLength(1)).check(z.maxLength(80)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n /**\n * `_embed` toont een externe pagina in een kader BINNEN de site, onder de logo/menubalk, alsof\n * hij erbij hoort. Alleen zinvol bij een externe href.\n */\n target: z.optional(z.enum([\"_self\", \"_blank\", \"_embed\"])),\n});\nexport type NavItem = z.infer<typeof NavItemSchema>;\n\n/** Verwijst dit item naar buiten? Afgeleid uit de href, zodat er geen tweede waarheid ontstaat. */\nexport function isExterneLink(href: string): boolean {\n return /^https?:\\/\\//i.test(href);\n}\n\n/** Het pad waarop een ingekaderde externe pagina wordt getoond. */\nexport const EMBED_PAD = \"/extern\";\n\n/**\n * De href waar dit menu-item feitelijk heen linkt.\n *\n * Bij `_embed` is dat niet de externe URL zelf maar de eigen `/extern`-route, die hem in een kader\n * binnen de site toont. De externe URL gaat als parameter mee; de route controleert vervolgens of\n * dat adres écht in het menu staat vóór hij het inlaadt. Zonder die controle zou iedereen een\n * willekeurige pagina in jouw huisstijl kunnen laten verschijnen door de URL te bewerken.\n */\nexport function navHref(item: NavItem): string {\n if (item.target === \"_embed\" && isExterneLink(item.href)) {\n return `${EMBED_PAD}?url=${encodeURIComponent(item.href)}`;\n }\n return item.href;\n}\n\n/** Staat dit adres als ingekaderd menu-item in de navigatie? Zo niet, dan niet tonen. */\nexport function isToegestaneEmbed(url: string, nav: NavItem[] | undefined): boolean {\n return (nav ?? []).some((i) => i.target === \"_embed\" && i.href === url);\n}\n\n/** Huisstijl-tokens (worden CSS custom properties). Alle uitbreidingen zijn optioneel/update-veilig. */\nexport const ThemeSchema = z.object({\n primary: z.string().check(z.maxLength(40)),\n accent: z.optional(z.string().check(z.maxLength(40))),\n /** Logo-URL (absoluut, https) — gebruikt in de mail-header en op de factuur-PDF. */\n logo: z.optional(z.string().check(z.maxLength(500))),\n background: z.optional(z.string().check(z.maxLength(40))),\n foreground: z.optional(z.string().check(z.maxLength(40))),\n fontFamily: z.optional(z.string().check(z.maxLength(200))),\n /** Optioneel apart lettertype voor koppen (display/serif), voor een elegantere typografie. */\n headingFont: z.optional(z.string().check(z.maxLength(200))),\n /** Optionele stylesheet-URL voor het lettertype (bv. een Google Fonts CSS-link). */\n fontUrl: z.optional(z.string().check(z.maxLength(500))),\n /** Afrondingsschaal (sm/md/lg). */\n radius: z.optional(z.object({ sm: z.optional(z.string().check(z.maxLength(20))), md: z.optional(z.string().check(z.maxLength(20))), lg: z.optional(z.string().check(z.maxLength(20))) })),\n /** Ruimteschaal voor block-padding (xs..xl). */\n spacing: z.optional(z.object({\n xs: z.optional(z.string().check(z.maxLength(20))),\n sm: z.optional(z.string().check(z.maxLength(20))),\n md: z.optional(z.string().check(z.maxLength(20))),\n lg: z.optional(z.string().check(z.maxLength(20))),\n xl: z.optional(z.string().check(z.maxLength(20))),\n })),\n /** Maximale container-breedte + horizontale padding. */\n container: z.optional(z.object({ maxWidth: z.optional(z.string().check(z.maxLength(20))), padding: z.optional(z.string().check(z.maxLength(20))) })),\n /** Donkere modus: uit, altijd aan, of meebewegen met het systeem van de bezoeker. */\n dark: z.optional(z.object({\n /**\n * \"auto\" — volgt het besturingssysteem van de bezoeker (light-dark()).\n * \"dark\" — de site is altijd donker, ongeacht de bezoeker.\n * \"light\" — de site is altijd licht (gelijk aan geen donkere modus).\n *\n * Vervangt de oude `auto`-boolean, die alleen \"volg het systeem\" of \"niets\" kon zeggen —\n * \"deze site is gewoon donker\" was daarmee niet uit te drukken. `auto` blijft gelezen worden\n * zodat bestaande site-configs blijven werken (zie themeMode in XiCMSProvider).\n */\n mode: z.optional(z.enum([\"light\", \"dark\", \"auto\"])),\n /**\n * @deprecated Gebruik `mode`. Blijft werken: true → \"auto\", afwezig/false → \"light\".\n * OPTIONEEL (niet `_default(false)`): met een verplichte default draagt elk gemerged\n * dark-object een `auto`, en dan botst een partiële update uit /beheer op het type.\n */\n auto: z.optional(z.boolean()),\n background: z.optional(z.string().check(z.maxLength(40))),\n foreground: z.optional(z.string().check(z.maxLength(40))),\n primary: z.optional(z.string().check(z.maxLength(40))),\n })),\n});\nexport type ThemeTokens = z.infer<typeof ThemeSchema>;\n\n/**\n * Voegt live thema-instellingen (uit `settings/public.theme`, /beheer) per token samen met het\n * statische SiteConfig-thema. Firestore wint per veld waar gezet; geneste schalen (radius/spacing/\n * container/dark) mergen per subveld, zodat een klant die alleen kleuren wijzigt de fonts/radius\n * uit de repo behoudt. Puur (geen React) — gedeeld door provider en `useMergedSiteConfig`.\n */\nexport function mergeThemeTokens(base: ThemeTokens, override?: ThemeTokens): ThemeTokens {\n if (!override) return base;\n const merged: ThemeTokens = { ...base, ...override };\n for (const key of [\"radius\", \"spacing\", \"container\", \"dark\"] as const) {\n if (base[key] && override[key]) {\n merged[key] = { ...base[key], ...override[key] } as never;\n } else if (base[key] && !override[key]) {\n merged[key] = base[key] as never;\n }\n }\n return merged;\n}\n\n/**\n * Afzender-instelling voor formulier-e-mail. Een provider mag alleen versturen vanaf een\n * geverifieerd domein, dus dit is per site instelbaar:\n * - mode \"xicms\": from = verifiedSender (XiCMS-domein), replyTo = inzender/klant. Werkt direct.\n * - mode \"client\": from = het geverifieerde eigen domein van de klant.\n */\nexport const FormsSenderSchema = z.object({\n mode: z._default(z.enum([\"xicms\", \"client\"]), \"xicms\"),\n /** Geverifieerd from-adres (verplicht ongeacht mode; bij \"client\" het klant-domein). */\n fromEmail: z.optional(z.email()),\n fromName: z.optional(z.string().check(z.maxLength(120))),\n /** Waar notificaties heen gaan (meestal het klant-mailadres). */\n notifyTo: z.optional(z.email()),\n});\nexport type FormsSender = z.infer<typeof FormsSenderSchema>;\n\n/** Eén social-link (platform + url). */\nexport const SocialLinkSchema = z.object({\n platform: z.string().check(z.minLength(1)).check(z.maxLength(40)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n});\nexport type SocialLink = z.infer<typeof SocialLinkSchema>;\n\n/** Eén footer-kolom: kop + links. */\nexport const FooterColumnSchema = z.object({\n heading: z.optional(z.string().check(z.maxLength(120))),\n links: z._default(z.array(NavItemSchema).check(z.maxLength(20)), []),\n});\n\n/** Bewerkbare footer (klant past dit aan in /beheer; core levert de unstyled `SiteFooter`). */\nexport const FooterSettingsSchema = z.object({\n columns: z.optional(z.array(FooterColumnSchema).check(z.maxLength(6))),\n social: z.optional(z.array(SocialLinkSchema).check(z.maxLength(12))),\n copyright: z.optional(z.string().check(z.maxLength(300))),\n /** Toon de \"Beheer\"-link in de footer (default: aan). */\n showAdminLink: z.optional(z.boolean()),\n});\nexport type FooterSettings = z.infer<typeof FooterSettingsSchema>;\n\n/** Per-module activatie/instellingen (spiegelt ModulesSiteConfig; permissief gehouden). */\nexport const ModulesSettingsSchema = z.record(\n z.string(),\n z.union([\n z.boolean(),\n z.object({ enabled: z.optional(z.boolean()), config: z.optional(z.record(z.string(), z.unknown())) }),\n ]),\n);\n\n/** Het `sites/{siteId}/settings/site`-document: alle bewerkbare site-instellingen. */\nexport const SiteSettingsSchema = z.object({\n name: z.optional(z.string().check(z.minLength(1)).check(z.maxLength(160))),\n // Zelfde hostname-regel als SiteConfigSchema.domain (geen protocol/pad).\n domain: z.union([z.optional(z.string().check(z.maxLength(255)).check(z.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i, \"alleen een hostname, bv. 'www.klant.nl' (zonder https:// of pad)\"))), z.literal(\"\")]),\n contactEmail: z.optional(z.email()),\n nav: z.optional(z.array(NavItemSchema).check(z.maxLength(50))),\n theme: z.optional(ThemeSchema),\n seoDefaults: z.optional(SeoSchema),\n forms: z.optional(FormsSenderSchema),\n footer: z.optional(FooterSettingsSchema),\n modules: z.optional(ModulesSettingsSchema),\n});\nexport type SiteSettings = z.infer<typeof SiteSettingsSchema>;\n\n/**\n * Publiek-veilige presentatie-instellingen (`sites/{siteId}/settings/public`): wordt door de live\n * site gelezen (footer, thema, nav, modules) zónder de gevoelige velden (e-mailafzender, contact)\n * uit `settings/site` bloot te geven.\n */\nexport const PublicSiteSettingsSchema = z.object({\n name: z.optional(z.string().check(z.maxLength(160))),\n nav: z.optional(z.array(NavItemSchema).check(z.maxLength(50))),\n theme: z.optional(ThemeSchema),\n seoDefaults: z.optional(SeoSchema),\n footer: z.optional(FooterSettingsSchema),\n modules: z.optional(ModulesSettingsSchema),\n});\nexport type PublicSiteSettings = z.infer<typeof PublicSiteSettingsSchema>;\n\n/** Haalt de publiek-veilige subset uit de volledige settings (voor het `settings/public`-doc). */\nexport function toPublicSettings(s: SiteSettings): PublicSiteSettings {\n return {\n name: s.name,\n nav: s.nav,\n theme: s.theme,\n seoDefaults: s.seoDefaults,\n footer: s.footer,\n modules: s.modules,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/schema/settings.ts"],"sourcesContent":["import * as z from \"zod/mini\";\nimport { SeoSchema } from \"./seo\";\n\n/**\n * Eén navigatie-item.\n *\n * `target` is OPTIONEEL en dat is opzet: elk bestaand menu in Git en in Firestore mist dit veld, en\n * moet blijven valideren. Ontbreekt het, dan opent de link in hetzelfde venster — precies wat een\n * gewone `<a href>` altijd al deed.\n *\n * Er staat bewust geen `type: \"page\" | \"extern\"` in. Dat onderscheid is uit de href zelf af te\n * lezen (begint hij met `/` of met `http`), en een apart veld zou een tweede waarheid introduceren\n * die uit de pas kan lopen met de href. Het beheerscherm mag de gebruiker wél die keuze voorleggen;\n * wat er wordt opgeslagen is één ondubbelzinnig adres.\n */\nexport const NavItemSchema = z.object({\n label: z.string().check(z.minLength(1)).check(z.maxLength(80)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n /**\n * `_embed` toont een externe pagina in een kader BINNEN de site, onder de logo/menubalk, alsof\n * hij erbij hoort. Alleen zinvol bij een externe href.\n */\n target: z.optional(z.enum([\"_self\", \"_blank\", \"_embed\"])),\n});\nexport type NavItem = z.infer<typeof NavItemSchema>;\n\n/** Verwijst dit item naar buiten? Afgeleid uit de href, zodat er geen tweede waarheid ontstaat. */\nexport function isExterneLink(href: string): boolean {\n return /^https?:\\/\\//i.test(href);\n}\n\n/** Het pad waarop een ingekaderde externe pagina wordt getoond. */\nexport const EMBED_PAD = \"/extern\";\n\n/**\n * De href waar dit menu-item feitelijk heen linkt.\n *\n * Bij `_embed` is dat niet de externe URL zelf maar de eigen `/extern`-route, die hem in een kader\n * binnen de site toont. De externe URL gaat als parameter mee; de route controleert vervolgens of\n * dat adres écht in het menu staat vóór hij het inlaadt. Zonder die controle zou iedereen een\n * willekeurige pagina in jouw huisstijl kunnen laten verschijnen door de URL te bewerken.\n */\nexport function navHref(item: NavItem): string {\n if (item.target === \"_embed\" && isExterneLink(item.href)) {\n return `${EMBED_PAD}?url=${encodeURIComponent(item.href)}`;\n }\n return item.href;\n}\n\n/** Staat dit adres als ingekaderd menu-item in de navigatie? Zo niet, dan niet tonen. */\nexport function isToegestaneEmbed(url: string, nav: NavItem[] | undefined): boolean {\n return (nav ?? []).some((i) => i.target === \"_embed\" && i.href === url);\n}\n\n/** Huisstijl-tokens (worden CSS custom properties). Alle uitbreidingen zijn optioneel/update-veilig. */\nexport const ThemeSchema = z.object({\n primary: z.string().check(z.maxLength(40)),\n accent: z.optional(z.string().check(z.maxLength(40))),\n /** Logo-URL (absoluut, https) — gebruikt in de mail-header en op de factuur-PDF. */\n logo: z.optional(z.string().check(z.maxLength(500))),\n background: z.optional(z.string().check(z.maxLength(40))),\n foreground: z.optional(z.string().check(z.maxLength(40))),\n fontFamily: z.optional(z.string().check(z.maxLength(200))),\n /** Optioneel apart lettertype voor koppen (display/serif), voor een elegantere typografie. */\n headingFont: z.optional(z.string().check(z.maxLength(200))),\n /** Optionele stylesheet-URL voor het lettertype (bv. een Google Fonts CSS-link). */\n fontUrl: z.optional(z.string().check(z.maxLength(500))),\n /** Afrondingsschaal (sm/md/lg). */\n radius: z.optional(z.object({ sm: z.optional(z.string().check(z.maxLength(20))), md: z.optional(z.string().check(z.maxLength(20))), lg: z.optional(z.string().check(z.maxLength(20))) })),\n /** Ruimteschaal voor block-padding (xs..xl). */\n spacing: z.optional(z.object({\n xs: z.optional(z.string().check(z.maxLength(20))),\n sm: z.optional(z.string().check(z.maxLength(20))),\n md: z.optional(z.string().check(z.maxLength(20))),\n lg: z.optional(z.string().check(z.maxLength(20))),\n xl: z.optional(z.string().check(z.maxLength(20))),\n })),\n /** Maximale container-breedte + horizontale padding. */\n container: z.optional(z.object({ maxWidth: z.optional(z.string().check(z.maxLength(20))), padding: z.optional(z.string().check(z.maxLength(20))) })),\n /** Donkere modus: uit, altijd aan, of meebewegen met het systeem van de bezoeker. */\n dark: z.optional(z.object({\n /**\n * \"auto\" — volgt het besturingssysteem van de bezoeker (light-dark()).\n * \"dark\" — de site is altijd donker, ongeacht de bezoeker.\n * \"light\" — de site is altijd licht (gelijk aan geen donkere modus).\n *\n * Vervangt de oude `auto`-boolean, die alleen \"volg het systeem\" of \"niets\" kon zeggen —\n * \"deze site is gewoon donker\" was daarmee niet uit te drukken. `auto` blijft gelezen worden\n * zodat bestaande site-configs blijven werken (zie themeMode in XiCMSProvider).\n */\n mode: z.optional(z.enum([\"light\", \"dark\", \"auto\"])),\n /**\n * @deprecated Gebruik `mode`. Blijft werken: true → \"auto\", afwezig/false → \"light\".\n * OPTIONEEL (niet `_default(false)`): met een verplichte default draagt elk gemerged\n * dark-object een `auto`, en dan botst een partiële update uit /beheer op het type.\n */\n auto: z.optional(z.boolean()),\n background: z.optional(z.string().check(z.maxLength(40))),\n foreground: z.optional(z.string().check(z.maxLength(40))),\n primary: z.optional(z.string().check(z.maxLength(40))),\n })),\n});\nexport type ThemeTokens = z.infer<typeof ThemeSchema>;\n\n/**\n * Voegt live thema-instellingen (uit `settings/public.theme`, /beheer) per token samen met het\n * statische SiteConfig-thema. Firestore wint per veld waar gezet; geneste schalen (radius/spacing/\n * container/dark) mergen per subveld, zodat een klant die alleen kleuren wijzigt de fonts/radius\n * uit de repo behoudt. Puur (geen React) — gedeeld door provider en `useMergedSiteConfig`.\n */\nexport function mergeThemeTokens(base: ThemeTokens, override?: ThemeTokens): ThemeTokens {\n if (!override) return base;\n const merged: ThemeTokens = { ...base, ...override };\n for (const key of [\"radius\", \"spacing\", \"container\", \"dark\"] as const) {\n if (base[key] && override[key]) {\n merged[key] = { ...base[key], ...override[key] } as never;\n } else if (base[key] && !override[key]) {\n merged[key] = base[key] as never;\n }\n }\n return merged;\n}\n\n/**\n * Afzender-instelling voor formulier-e-mail. Een provider mag alleen versturen vanaf een\n * geverifieerd domein, dus dit is per site instelbaar:\n * - mode \"xicms\": from = verifiedSender (XiCMS-domein), replyTo = inzender/klant. Werkt direct.\n * - mode \"client\": from = het geverifieerde eigen domein van de klant.\n */\nexport const FormsSenderSchema = z.object({\n mode: z._default(z.enum([\"xicms\", \"client\"]), \"xicms\"),\n /** Geverifieerd from-adres (verplicht ongeacht mode; bij \"client\" het klant-domein). */\n fromEmail: z.optional(z.email()),\n fromName: z.optional(z.string().check(z.maxLength(120))),\n /** Waar notificaties heen gaan (meestal het klant-mailadres). */\n notifyTo: z.optional(z.email()),\n});\nexport type FormsSender = z.infer<typeof FormsSenderSchema>;\n\n/** Eén social-link (platform + url). */\nexport const SocialLinkSchema = z.object({\n platform: z.string().check(z.minLength(1)).check(z.maxLength(40)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n});\nexport type SocialLink = z.infer<typeof SocialLinkSchema>;\n\n/** Eén footer-kolom: kop + links. */\nexport const FooterColumnSchema = z.object({\n heading: z.optional(z.string().check(z.maxLength(120))),\n links: z._default(z.array(NavItemSchema).check(z.maxLength(20)), []),\n});\n\n/** Bewerkbare footer (klant past dit aan in /beheer; core levert de unstyled `SiteFooter`). */\nexport const FooterSettingsSchema = z.object({\n columns: z.optional(z.array(FooterColumnSchema).check(z.maxLength(6))),\n social: z.optional(z.array(SocialLinkSchema).check(z.maxLength(12))),\n copyright: z.optional(z.string().check(z.maxLength(300))),\n /** Toon de \"Beheer\"-link in de footer (default: aan). */\n showAdminLink: z.optional(z.boolean()),\n});\nexport type FooterSettings = z.infer<typeof FooterSettingsSchema>;\n\n/** Per-module activatie/instellingen (spiegelt ModulesSiteConfig; permissief gehouden). */\nexport const ModulesSettingsSchema = z.record(\n z.string(),\n z.union([\n z.boolean(),\n z.object({ enabled: z.optional(z.boolean()), config: z.optional(z.record(z.string(), z.unknown())) }),\n ]),\n);\n\n/** Het `sites/{siteId}/settings/site`-document: alle bewerkbare site-instellingen. */\nexport const SiteSettingsSchema = z.object({\n name: z.optional(z.string().check(z.minLength(1)).check(z.maxLength(160))),\n // Zelfde hostname-regel als SiteConfigSchema.domain (geen protocol/pad).\n domain: z.union([z.optional(z.string().check(z.maxLength(255)).check(z.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i, \"alleen een hostname, bv. 'www.klant.nl' (zonder https:// of pad)\"))), z.literal(\"\")]),\n contactEmail: z.optional(z.email()),\n nav: z.optional(z.array(NavItemSchema).check(z.maxLength(50))),\n theme: z.optional(ThemeSchema),\n seoDefaults: z.optional(SeoSchema),\n forms: z.optional(FormsSenderSchema),\n footer: z.optional(FooterSettingsSchema),\n modules: z.optional(ModulesSettingsSchema),\n /**\n * Snelkoppelingen op de beheer-startpagina (7.16). Beheer-intern: bewust NIET in de publieke\n * subset. Opslaan loopt via de bestaande saveSiteSettings-callable — die is passthrough met\n * merge, dus dit veld vergt geen functions-wijziging.\n */\n dashboardLinks: z.optional(z.array(z.object({\n label: z.string().check(z.minLength(1)).check(z.maxLength(80)),\n href: z.string().check(z.minLength(1)).check(z.maxLength(2000)),\n })).check(z.maxLength(12))),\n});\nexport type SiteSettings = z.infer<typeof SiteSettingsSchema>;\n\n/**\n * Publiek-veilige presentatie-instellingen (`sites/{siteId}/settings/public`): wordt door de live\n * site gelezen (footer, thema, nav, modules) zónder de gevoelige velden (e-mailafzender, contact)\n * uit `settings/site` bloot te geven.\n */\nexport const PublicSiteSettingsSchema = z.object({\n name: z.optional(z.string().check(z.maxLength(160))),\n nav: z.optional(z.array(NavItemSchema).check(z.maxLength(50))),\n theme: z.optional(ThemeSchema),\n seoDefaults: z.optional(SeoSchema),\n footer: z.optional(FooterSettingsSchema),\n modules: z.optional(ModulesSettingsSchema),\n});\nexport type PublicSiteSettings = z.infer<typeof PublicSiteSettingsSchema>;\n\n/** Haalt de publiek-veilige subset uit de volledige settings (voor het `settings/public`-doc). */\nexport function toPublicSettings(s: SiteSettings): PublicSiteSettings {\n return {\n name: s.name,\n nav: s.nav,\n theme: s.theme,\n seoDefaults: s.seoDefaults,\n footer: s.footer,\n modules: s.modules,\n };\n}\n"],"mappings":"AAAA,YAAY,OAAO;AACnB,SAAS,iBAAiB;AAcnB,MAAM,gBAAgB,EAAE,OAAO;AAAA,EACpC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AAAA,EAC7D,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,GAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,EAK9D,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,UAAU,QAAQ,CAAC,CAAC;AAC1D,CAAC;AAIM,SAAS,cAAc,MAAuB;AACnD,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAGO,MAAM,YAAY;AAUlB,SAAS,QAAQ,MAAuB;AAC7C,MAAI,KAAK,WAAW,YAAY,cAAc,KAAK,IAAI,GAAG;AACxD,WAAO,GAAG,SAAS,QAAQ,mBAAmB,KAAK,IAAI,CAAC;AAAA,EAC1D;AACA,SAAO,KAAK;AACd;AAGO,SAAS,kBAAkB,KAAa,KAAqC;AAClF,UAAQ,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,WAAW,YAAY,EAAE,SAAS,GAAG;AACxE;AAGO,MAAM,cAAc,EAAE,OAAO;AAAA,EAClC,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AAAA,EACzC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA;AAAA,EAEpD,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC;AAAA,EACnD,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,EACxD,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,EACxD,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC;AAAA;AAAA,EAEzD,aAAa,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC;AAAA;AAAA,EAE1D,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC;AAAA;AAAA,EAEtD,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAAA;AAAA,EAExL,SAAS,EAAE,SAAS,EAAE,OAAO;AAAA,IACzB,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,IAChD,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,IAChD,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,IAChD,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,IAChD,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,EAClD,CAAC,CAAC;AAAA;AAAA,EAEJ,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,GAAG,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAAA;AAAA,EAEnJ,MAAM,EAAE,SAAS,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUtB,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,QAAQ,MAAM,CAAC,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMlD,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;AAAA,IAC5B,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,IACxD,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,IACxD,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,EACvD,CAAC,CAAC;AACN,CAAC;AASM,SAAS,iBAAiB,MAAmB,UAAqC;AACvF,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,SAAsB,EAAE,GAAG,MAAM,GAAG,SAAS;AACnD,aAAW,OAAO,CAAC,UAAU,WAAW,aAAa,MAAM,GAAY;AACrE,QAAI,KAAK,GAAG,KAAK,SAAS,GAAG,GAAG;AAC9B,aAAO,GAAG,IAAI,EAAE,GAAG,KAAK,GAAG,GAAG,GAAG,SAAS,GAAG,EAAE;AAAA,IACjD,WAAW,KAAK,GAAG,KAAK,CAAC,SAAS,GAAG,GAAG;AACtC,aAAO,GAAG,IAAI,KAAK,GAAG;AAAA,IACxB;AAAA,EACF;AACA,SAAO;AACT;AAQO,MAAM,oBAAoB,EAAE,OAAO;AAAA,EACxC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,QAAQ,CAAC,GAAG,OAAO;AAAA;AAAA,EAErD,WAAW,EAAE,SAAS,EAAE,MAAM,CAAC;AAAA,EAC/B,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC;AAAA;AAAA,EAEvD,UAAU,EAAE,SAAS,EAAE,MAAM,CAAC;AAChC,CAAC;AAIM,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AAAA,EAChE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,GAAI,CAAC;AAChE,CAAC;AAIM,MAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC;AAAA,EACtD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;AACrE,CAAC;AAGM,MAAM,uBAAuB,EAAE,OAAO;AAAA,EAC3C,SAAS,EAAE,SAAS,EAAE,MAAM,kBAAkB,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;AAAA,EACrE,QAAQ,EAAE,SAAS,EAAE,MAAM,gBAAgB,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,EACnE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC;AAAA;AAAA,EAExD,eAAe,EAAE,SAAS,EAAE,QAAQ,CAAC;AACvC,CAAC;AAIM,MAAM,wBAAwB,EAAE;AAAA,EACrC,EAAE,OAAO;AAAA,EACT,EAAE,MAAM;AAAA,IACN,EAAE,QAAQ;AAAA,IACV,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,CAAC,GAAG,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC;AAAA,EACtG,CAAC;AACH;AAGO,MAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC;AAAA;AAAA,EAEzE,QAAQ,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,sEAAsE,kEAAkE,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;AAAA,EACxO,cAAc,EAAE,SAAS,EAAE,MAAM,CAAC;AAAA,EAClC,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,EAC7D,OAAO,EAAE,SAAS,WAAW;AAAA,EAC7B,aAAa,EAAE,SAAS,SAAS;AAAA,EACjC,OAAO,EAAE,SAAS,iBAAiB;AAAA,EACnC,QAAQ,EAAE,SAAS,oBAAoB;AAAA,EACvC,SAAS,EAAE,SAAS,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMzC,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO;AAAA,IAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AAAA,IAC7D,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,GAAI,CAAC;AAAA,EAChE,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAC5B,CAAC;AAQM,MAAM,2BAA2B,EAAE,OAAO;AAAA,EAC/C,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC;AAAA,EACnD,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,EAC7D,OAAO,EAAE,SAAS,WAAW;AAAA,EAC7B,aAAa,EAAE,SAAS,SAAS;AAAA,EACjC,QAAQ,EAAE,SAAS,oBAAoB;AAAA,EACvC,SAAS,EAAE,SAAS,qBAAqB;AAC3C,CAAC;AAIM,SAAS,iBAAiB,GAAqC;AACpE,SAAO;AAAA,IACL,MAAM,EAAE;AAAA,IACR,KAAK,EAAE;AAAA,IACP,OAAO,EAAE;AAAA,IACT,aAAa,EAAE;AAAA,IACf,QAAQ,EAAE;AAAA,IACV,SAAS,EAAE;AAAA,EACb;AACF;","names":[]}
|