@col/demo-mode
Versie: 1.1.0 Toegevoegd: 2026-08-02
Herkomst
Geëxtraheerd uit supplier-onboarding-vercel/src/components/demo-banner.tsx (259
regels) toen bleek dat een tweede applicatie dezelfde testbalk nodig had — niet
alleen de vorm, maar ook de werking: rolselectie en e-mailconfigurator.
Het origineel vermengde vier lagen: presentatie, client-gedrag, backend-contract en domeindata. Bij de extractie zijn die uit elkaar gehaald en zijn twee bugs uit het origineel verholpen (zie Bewuste keuzes).
Waarvoor
Een balk boven aan de pagina die zichtbaar maakt dat je niet op productie zit, met optioneel twee besturingselementen: van rol wisselen zonder uit te loggen, en schakelen tussen een testinbox en echte e-mailverzending.
Bedoeld voor test- en demo-omgevingen. Op productie rendert de app hem niet — dat is een keuze van de consumer, niet van het item zelf.
Gebruik het niet als algemene notificatiebalk; DemoBar is een shell met een
slot en zou daarvoor werken, maar de tokens en naamgeving zijn op demo gericht.
Wat je app moet leveren
<DemoBar>
| Prop | Type | Betekenis |
|---|---|---|
message |
string |
Tekst in het midden |
maxWidth |
string? |
Tailwind max-width class, default max-w-6xl |
children |
ReactNode? |
Controls, rechts uitgelijnd |
<DemoRoleSwitcher>
| Prop | Type | Betekenis |
|---|---|---|
available |
{ value, label }[] |
Alle kiesbare rollen |
active |
string[] |
Nu actieve rollen |
onChange |
(roles) => void | Promise<void> |
Wordt aangeroepen met de nieuwe set |
minRoles |
number? |
Minimaal actief te houden, default 1 |
disabled |
boolean? |
|
labels |
{ trigger, heading } |
Teksten |
<DemoEmailSwitcher>
Controlled. provider: 'test' \| 'live', recipient: string \| null,
onProviderChange, onRecipientSave, optioneel testInboxUrl, en een
labels-object met twaalf teksten (zie demo-types.ts). De callbacks mogen
false teruggeven om aan te geven dat de actie mislukte; dan toont het component
geen bevestiging.
useDemoEmail(options?)
Geeft { provider, recipient, onProviderChange, onRecipientSave }. Opties:
enabled (default true) en endpoint (default /api/email-provider). Fouten
worden bewust stil geslikt — de balk mag de app nooit blokkeren — maar de
callbacks geven false terug zodat de UI het wél kan tonen.
Backend-contract
GET <endpoint>
-> 200 { provider: 'test' | 'live', recipient: string | null }
-> 404 als demo mode uit staat
-> 401 als niet ingelogd
POST <endpoint> { provider?: 'test' | 'live', recipient?: string | null }
-> 200 { provider, recipient }
-> 400 bij lege body of ongeldige provider
@col/demo-mode-starter levert een implementatie tegen cookies, plus een
sjabloon voor het rolwissel-endpoint (POST { roles: string[] }).
Bestanden
| Bestand | Landt in | Soort |
|---|---|---|
demo-types.ts |
components/demo/ |
beheerd |
roles.ts |
components/demo/ |
beheerd |
demo-bar.tsx |
components/demo/ |
beheerd |
demo-role-switcher.tsx |
components/demo/ |
beheerd |
demo-email-switcher.tsx |
components/demo/ |
beheerd |
use-demo-email.ts |
components/demo/ |
beheerd |
demo-controls.tsx |
components/demo/ |
glue |
email-provider-route.ts |
app/api/email-provider/ |
glue |
switch-role-route.ts |
app/api/auth/switch-role/ |
glue |
Upstream shadcn-afhankelijkheden: button, badge, input, dropdown-menu.
Tokens: zeven --demo-* variabelen, meegeleverd via cssVars.
Gebruikt door
| App | Sinds versie | Opmerkingen |
|---|---|---|
supplier-onboarding-vercel (Onboarding Portal) |
1.0.0 | Consumer #1. Live op productie sinds 3 aug 2026. Glue koppelt aan Role/RoleLabels uit @/types en aan het eigen i18n-systeem. |
floriday-app (Floriday middleware) |
1.0.1 | Consumer #2, sinds 3 aug 2026. Draait op base-ui in plaats van Radix — zie hieronder. Eén rol per gebruiker (UserRole, geen array), rol zit in de JWT dus de glue roept useSession().update({}) aan na een wissel. Geen NEXT_PUBLIC_DEMO_MODE: gate op VERCEL_ENV/VERCEL server-side. |
Wat consumer #2 tegenkwam op base-ui
Twee dingen die de moeite van het weten waard zijn voor de volgende app die dit item niet op Radix draait:
onSelect={(e) => e.preventDefault()}doet niets in base-ui, maar dat is geen probleem:MenuCheckboxItemheeft daarcloseOnClickstandaard al opfalse, dus het menu blijft sowieso open. Het gedrag klopt om een andere reden dan de code suggereert. Geverifieerd in een echte browser. De typecheck zegt hier niets over —onSelectis een geldige DOM-prop op elk element.nextRoles()vervangt niet, maar voegt toe. Met precies één actieve rol levert een klik op een andere rol[huidigeRol, nieuweRol]op. Wie "de eerste pakken" doet, krijgt de ongewijzigde rol terug en ziet niets gebeuren. Een app met één rol per gebruiker moet dus het element pakken dat afwijkt van de huidige rol.
Bewuste keuzes
- De trigger van
DemoRoleSwitcherenDemoEmailSwitcheris metcn(buttonVariants({ … }), …)gestyled, geen<Button asChild>erin (v1.1.0).asChildbestaat niet in Base UI — twee van de vijf consumers draaien daarop — en moest daar bij elke--overwriteopnieuw met de hand gerepareerd worden. Zie regel 5 inAGENTS.md. onSelect={(e) => e.preventDefault()}op de rol-menu-items. Zonder dit sluit het Radix-menu na elke klik en kun je geen tweede rol aanvinken.onKeyDown={(e) => e.stopPropagation()}op het adresveld. Zonder dit kaapt de typeahead van het dropdownmenu je toetsaanslagen.autoComplete="off"plusdata-1p-ignore/data-lpignoreop het adresveld (v1.0.1). Zonder dit vult Chrome het ingetypte adres ook in andere velden op de pagina; in de onboarding-portal belandde het in de zoekbalk van het dashboard.- setState tijdens render in
DemoEmailSwitcheromdraftte synchroniseren met een nieuwerecipient-prop. Bewust geenuseEffect: dat overtreedtreact-hooks/set-state-in-effect. Dit is het door React aanbevolen patroon. DropdownMenuCheckboxItemin plaats van een genesteCheckbox. In het origineel hadden zowel het menu-item als de checkbox een handler, waardoor een klik op de checkbox de rolwissel twee keer uitvoerde — een dubbel POST-verzoek.- Alleen een expliciete
falsetelt als mislukking inhandleSave. Een callback dieundefinedteruggeeft geldt als geslaagd, zodat consumers met een gewone void-callback blijven werken.
Bekende beperkingen
- De balk kent geen dark-mode-schakelaar; de tokens hebben wel een
.dark-variant. useDemoEmailgebruikt geenAbortController. Bij een wisselendendpointloopt het oude verzoek door, al wordt het resultaat genegeerd.- De groene "live"-badge gebruikt nog vaste Tailwind-kleuren in plaats van tokens.