Implement Dusk theme system and polish

This commit is contained in:
Janpeter Visser 2026-04-19 03:27:40 +02:00
parent e269a155da
commit 9280939756
38 changed files with 1144 additions and 270 deletions

View file

@ -9,16 +9,18 @@ type AuthNoticeProps = {
const toneStyles = {
error: {
className: "mb-5 border-rose-200 bg-rose-50 text-rose-950 [&_svg]:text-rose-700",
variant: "destructive" as const,
className: "mb-5",
icon: AlertCircleIcon,
},
success: {
className:
"mb-5 border-emerald-200 bg-emerald-50 text-emerald-950 [&_svg]:text-emerald-700",
variant: "success" as const,
className: "mb-5",
icon: CheckCircle2Icon,
},
info: {
className: "mb-5 border-sky-200 bg-sky-50 text-sky-950 [&_svg]:text-sky-700",
variant: "info" as const,
className: "mb-5",
icon: InfoIcon,
},
};
@ -32,7 +34,7 @@ export function AuthNotice({ notice }: AuthNoticeProps) {
const Icon = tone.icon;
return (
<Alert className={cn("rounded-[1.5rem] px-4 py-3", tone.className)}>
<Alert variant={tone.variant} className={cn("px-4 py-3", tone.className)}>
<Icon className="size-4" />
<AlertDescription className="leading-7 text-current">
{notice.text}

View file

@ -21,22 +21,22 @@ export function AuthPanel({
footer,
}: AuthPanelProps) {
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(167,201,87,0.22),_transparent_32%),linear-gradient(180deg,_#f5f4ee_0%,_#eef2e6_100%)] px-6 py-10 text-slate-900 sm:px-8">
<main className="app-page">
<div className="mx-auto grid min-h-[calc(100vh-5rem)] max-w-6xl gap-6 lg:grid-cols-[1.05fr_0.95fr]">
<section className="flex flex-col justify-between rounded-[2rem] border border-black/10 bg-emerald-950 p-7 text-emerald-50 shadow-[0_18px_60px_rgba(6,78,59,0.18)] sm:p-9">
<section className="app-panel-primary flex flex-col justify-between rounded-[var(--radius-4xl)] p-7 sm:p-9">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-200/80">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-primary-foreground/72">
{eyebrow}
</p>
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl leading-tight sm:text-5xl">
{title}
</h1>
<p className="mt-5 max-w-xl text-base leading-8 text-emerald-50/85">
<p className="mt-5 max-w-xl text-base leading-8 text-primary-foreground/85">
{description}
</p>
</div>
<div className="mt-10 rounded-[1.5rem] border border-white/10 bg-white/8 p-5 text-sm leading-7 text-emerald-50/90">
<div className="mt-10 rounded-[var(--radius-2xl)] border border-white/10 bg-white/8 p-5 text-sm leading-7 text-primary-foreground/90">
<p className="font-semibold">Release 1 blijft bewust licht.</p>
<ul className="mt-3 space-y-2">
<li>Wellness-first en alleen voor individuele gebruikers</li>
@ -47,7 +47,7 @@ export function AuthPanel({
</section>
<section className="flex items-center">
<Card className="w-full rounded-[2rem] border border-border/60 bg-card/90 py-0 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur">
<Card elevation="raised" className="w-full rounded-[var(--radius-4xl)] py-0 backdrop-blur">
<CardContent className="p-6 sm:p-8">
<div className="mb-6 flex items-center justify-between gap-3">
<Link

View file

@ -34,12 +34,12 @@ export function CheckInCard({ todayCheckIn }: CheckInCardProps) {
: "Leg je energiestart en slaapkwaliteit van vandaag vast.";
return (
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<Card className="py-0">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Ochtendcheck-in
</p>
<CardTitle className="text-lg text-slate-900">{title}</CardTitle>
<CardTitle className="text-lg text-foreground">{title}</CardTitle>
</CardHeader>
<CardContent className="space-y-4 pb-6">
<CardDescription className="text-sm leading-7 text-muted-foreground">

View file

@ -58,12 +58,12 @@ export function CheckInForm({ todayCheckIn }: CheckInFormProps) {
<input type="hidden" name="energyScore" value={energyScore ?? ""} />
<input type="hidden" name="sleepQuality" value={sleepQuality ?? ""} />
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_18px_60px_rgba(71,85,105,0.1)]">
<Card elevation="raised" className="py-0">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Ochtendcheck-in
</p>
<CardTitle className="font-[family-name:var(--font-display)] text-3xl text-slate-900">
<CardTitle className="font-[family-name:var(--font-display)] text-3xl text-foreground">
Hoe start je vandaag?
</CardTitle>
<CardDescription className="max-w-2xl text-sm leading-7 text-muted-foreground">
@ -74,21 +74,25 @@ export function CheckInForm({ todayCheckIn }: CheckInFormProps) {
<CardContent className="space-y-6 pb-6">
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-sm font-semibold text-slate-900">
<Label id="energy-score-group-label" className="text-sm font-semibold text-foreground">
Energiescore vandaag
</Label>
<p className="text-sm leading-7 text-muted-foreground">
<p className="text-sm leading-7 text-muted-foreground" aria-live="polite">
{getEnergyScorePrompt(energyScore)}
</p>
{predictedBudget ? (
<p className="text-sm leading-7 text-slate-700">
<p className="text-sm leading-7 text-foreground/80" aria-live="polite">
Voor vandaag geeft dit niveau <strong>{formatEnergyLevelLabel(predictedBudget.energyLevel).toLowerCase()}</strong> en een startbudget van{" "}
<strong>{predictedBudget.dailyBudget} punten</strong>.
</p>
) : null}
</div>
<div className="grid grid-cols-5 gap-2 sm:grid-cols-10">
<div
className="grid grid-cols-5 gap-2 sm:grid-cols-10"
role="group"
aria-labelledby="energy-score-group-label"
>
{ENERGY_SCORE_VALUES.map((value) => {
const isSelected = energyScore === value;
@ -98,6 +102,7 @@ export function CheckInForm({ todayCheckIn }: CheckInFormProps) {
type="button"
disabled={isPending}
onClick={() => setEnergyScore(value)}
aria-pressed={isSelected}
className={cn(
buttonVariants({
variant: isSelected ? "default" : "outline",
@ -117,7 +122,7 @@ export function CheckInForm({ todayCheckIn }: CheckInFormProps) {
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-sm font-semibold text-slate-900">
<Label id="sleep-quality-group-label" className="text-sm font-semibold text-foreground">
Hoe voelde je slaap?
</Label>
<p className="text-sm leading-7 text-muted-foreground">
@ -125,7 +130,11 @@ export function CheckInForm({ todayCheckIn }: CheckInFormProps) {
</p>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<div
className="grid gap-3 sm:grid-cols-3"
role="group"
aria-labelledby="sleep-quality-group-label"
>
{SLEEP_QUALITY_OPTIONS.map((option) => {
const isSelected = sleepQuality === option.value;
@ -135,11 +144,12 @@ export function CheckInForm({ todayCheckIn }: CheckInFormProps) {
type="button"
disabled={isPending}
onClick={() => setSleepQuality(option.value)}
aria-pressed={isSelected}
className={cn(
"rounded-[1.25rem] border px-4 py-4 text-left transition",
isSelected
? "border-primary bg-primary text-primary-foreground shadow-[0_12px_30px_rgba(22,58,43,0.18)]"
: "border-border/60 bg-background/80 text-slate-900 hover:border-primary/35",
? "border-primary bg-primary text-primary-foreground shadow-[var(--shadow-2)]"
: "border-border/60 bg-background/80 text-foreground hover:border-primary/35",
isPending && "pointer-events-none opacity-70",
)}
>
@ -163,7 +173,7 @@ export function CheckInForm({ todayCheckIn }: CheckInFormProps) {
</Card>
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-sm leading-7 text-muted-foreground">
<p className="text-sm leading-7 text-muted-foreground" aria-live="polite">
{isPending
? "Je ochtendcheck-in wordt opgeslagen..."
: todayCheckIn

View file

@ -84,7 +84,7 @@ export function OnboardingFlow({ profileBundle }: OnboardingFlowProps) {
const isPending = isCompleting || isSkipping;
const aside = (
<Alert className="rounded-[1.5rem] border-white/10 bg-white/8 text-primary-foreground [&_svg]:text-primary-foreground/80">
<Alert className="rounded-[var(--radius-2xl)] border-white/10 bg-white/8 text-primary-foreground [&_svg]:text-primary-foreground/80">
<AlertDescription className="leading-7 text-current">
<span className="block font-semibold">Release 1 blijft bewust wellness-first.</span>
<span className="mt-2 block">

View file

@ -9,7 +9,7 @@ import {
export function OnboardingStepIntro() {
return (
<div className="space-y-4">
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0">
<Card tone="subtle" className="py-0 shadow-none">
<CardHeader className="pb-0">
<CardTitle className="font-[family-name:var(--font-display)] text-2xl">
Wat je hier wél krijgt
@ -23,7 +23,7 @@ export function OnboardingStepIntro() {
</CardContent>
</Card>
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0">
<Card tone="subtle" className="py-0 shadow-none">
<CardHeader className="pb-0">
<CardTitle className="font-[family-name:var(--font-display)] text-2xl">
Wat deze app niet doet

View file

@ -21,10 +21,10 @@ export function OnboardingStepPreferences({
}: OnboardingStepPreferencesProps) {
return (
<div className="space-y-4">
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0">
<Card tone="subtle" className="py-0 shadow-none">
<CardContent className="flex items-start justify-between gap-4 py-5">
<div className="space-y-1">
<Label className="text-sm font-semibold text-slate-900">
<Label className="text-sm font-semibold text-foreground">
Toon energiebudgetpunten
</Label>
<p className="text-sm leading-7 text-muted-foreground">
@ -39,11 +39,11 @@ export function OnboardingStepPreferences({
</CardContent>
</Card>
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0">
<Card tone="subtle" className="py-0 shadow-none">
<CardContent className="space-y-4 py-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<Label className="text-sm font-semibold text-slate-900">
<Label className="text-sm font-semibold text-foreground">
Zet een lichte ochtendreminder aan
</Label>
<p className="text-sm leading-7 text-muted-foreground">
@ -63,12 +63,12 @@ export function OnboardingStepPreferences({
<>
<Separator />
<div className="space-y-2">
<Label htmlFor="morning-reminder-time" className="text-slate-800">
<Label htmlFor="morning-reminder-time" className="text-foreground">
Tijdstip voor de ochtendreminder
</Label>
<Input
id="morning-reminder-time"
className="h-12 rounded-[1.25rem] bg-white px-4 text-base md:text-base"
className="h-12 rounded-[1.25rem] bg-background/80 px-4 text-base md:text-base"
disabled={disabled}
type="time"
value={draft.morningReminderTime}
@ -82,10 +82,10 @@ export function OnboardingStepPreferences({
</CardContent>
</Card>
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0">
<Card tone="subtle" className="py-0 shadow-none">
<CardContent className="flex items-start justify-between gap-4 py-5">
<div className="space-y-1">
<Label className="text-sm font-semibold text-slate-900">
<Label className="text-sm font-semibold text-foreground">
Sta lichte reflectieprompts toe
</Label>
<p className="text-sm leading-7 text-muted-foreground">

View file

@ -25,7 +25,7 @@ export function OnboardingStepProfile({
return (
<div className="space-y-5">
<div className="space-y-2">
<Label htmlFor="display-name" className="text-slate-800">
<Label htmlFor="display-name" className="text-foreground">
Schermnaam
</Label>
<Input
@ -40,14 +40,14 @@ export function OnboardingStepProfile({
/>
</div>
<Alert className="rounded-[1.5rem] border-sky-200 bg-sky-50 text-sky-950 [&_svg]:text-sky-700">
<Alert variant="info">
<AlertDescription className="leading-7 text-current">
Voertaal voor release 1 staat vast op <strong>Nederlands</strong>.
</AlertDescription>
</Alert>
<div className="space-y-2">
<Label className="text-slate-800">Timezone</Label>
<Label className="text-foreground">Timezone</Label>
<Select
disabled={disabled}
value={draft.timezone}

View file

@ -89,12 +89,12 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
<input type="hidden" name="impactLevel" value={impactLevel} />
<input type="hidden" name="priorityLevel" value={priorityLevel} />
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_18px_60px_rgba(71,85,105,0.1)]">
<Card elevation="raised" className="py-0">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Dagplanning
</p>
<CardTitle className="font-[family-name:var(--font-display)] text-3xl text-slate-900">
<CardTitle className="font-[family-name:var(--font-display)] text-3xl text-foreground">
Plan een activiteit voor vandaag
</CardTitle>
<CardDescription className="max-w-2xl text-sm leading-7 text-muted-foreground">
@ -104,7 +104,7 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
</CardHeader>
<CardContent className="space-y-6 pb-6">
<div className="space-y-2">
<Label htmlFor="activity-name" className="text-slate-800">
<Label htmlFor="activity-name" className="text-foreground">
Naam van de activiteit
</Label>
<Input
@ -121,7 +121,7 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
<div className="grid gap-5 md:grid-cols-2">
<div className="space-y-2">
<Label className="text-slate-800">Categorie</Label>
<Label className="text-foreground">Categorie</Label>
<Select
disabled={isPending}
value={categoryId}
@ -146,7 +146,7 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
</div>
<div className="space-y-2">
<Label htmlFor="duration-minutes" className="text-slate-800">
<Label htmlFor="duration-minutes" className="text-foreground">
Geschatte duur in minuten
</Label>
<Input
@ -162,13 +162,18 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
value={durationMinutes}
onChange={(event) => setDurationMinutes(event.target.value)}
/>
<div className="flex flex-wrap gap-2">
<div
className="flex flex-wrap gap-2"
role="group"
aria-label="Snelle duurkeuzes"
>
{ACTIVITY_DURATION_SUGGESTIONS.map((value) => (
<button
key={value}
type="button"
disabled={isPending}
onClick={() => setDurationMinutes(String(value))}
aria-pressed={durationMinutes === String(value)}
className={cn(
buttonVariants({
variant: durationMinutes === String(value) ? "default" : "outline",
@ -186,24 +191,24 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
<Separator />
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0 shadow-none">
<Card tone="subtle" className="py-0 shadow-none">
<CardContent className="space-y-2 py-5">
<p className="text-sm font-semibold text-slate-900">Vooruitblik op de meter</p>
<p className="text-sm leading-7 text-muted-foreground">
<p className="text-sm font-semibold text-foreground">Vooruitblik op de meter</p>
<p className="text-sm leading-7 text-muted-foreground" aria-live="polite">
{previewPoints === null
? "Kies een geldige duur en impact om te zien hoeveel punten deze activiteit ongeveer toevoegt."
: `Deze activiteit telt voorlopig voor ${previewPoints} punten. Je totaal zou dan uitkomen op ${previewMeter?.plannedPoints ?? currentMeter.plannedPoints} geplande punten.`}
</p>
{dailyBudget !== null && previewMeter ? (
<p className="text-sm leading-7 text-slate-700">
<p className="text-sm leading-7 text-foreground/80" aria-live="polite">
Dat is {previewMeter.dailyBudget} punten budget, met daarna nog{" "}
<strong>{previewMeter.remainingBudget} punten ruimte</strong>.
</p>
) : null}
{previewMeter?.isOverBudget ? (
<Alert className="rounded-[1.25rem] border-amber-300 bg-amber-50 text-amber-950 [&_svg]:text-amber-700">
<Alert variant="warning">
<AlertTitle className="text-sm">Niet-blokkerende waarschuwing</AlertTitle>
<AlertDescription className="leading-7 text-amber-900">
<AlertDescription className="leading-7 text-current">
Met deze activiteit kom je ongeveer{" "}
<strong>{Math.abs(previewMeter.remainingBudget ?? 0)} punten</strong> boven je dagbudget uit.
Je kunt nog steeds opslaan, maar dit is een goed moment om bewust te heroverwegen of te versimpelen.
@ -216,14 +221,18 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
<div className="grid gap-5 md:grid-cols-2">
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-sm font-semibold text-slate-900">
<Label id="impact-group-label" className="text-sm font-semibold text-foreground">
Verwachte impact
</Label>
<p className="text-sm leading-7 text-muted-foreground">
Kies hoe belastend deze activiteit voor jou aanvoelt.
</p>
</div>
<div className="grid gap-3">
<div
className="grid gap-3"
role="group"
aria-labelledby="impact-group-label"
>
{ACTIVITY_IMPACT_OPTIONS.map((option) => {
const isSelected = impactLevel === option.value;
@ -233,11 +242,12 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
type="button"
disabled={isPending}
onClick={() => setImpactLevel(option.value)}
aria-pressed={isSelected}
className={cn(
"rounded-[1.25rem] border px-4 py-4 text-left transition",
isSelected
? "border-primary bg-primary text-primary-foreground shadow-[0_12px_30px_rgba(22,58,43,0.18)]"
: "border-border/60 bg-background/80 text-slate-900 hover:border-primary/35",
? "border-primary bg-primary text-primary-foreground shadow-[var(--shadow-2)]"
: "border-border/60 bg-background/80 text-foreground hover:border-primary/35",
isPending && "pointer-events-none opacity-70",
)}
>
@ -260,14 +270,18 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-sm font-semibold text-slate-900">
<Label id="priority-group-label" className="text-sm font-semibold text-foreground">
Prioriteit voor vandaag
</Label>
<p className="text-sm leading-7 text-muted-foreground">
Dit helpt straks om bewust te herschikken zonder alles te verliezen.
</p>
</div>
<div className="grid gap-3">
<div
className="grid gap-3"
role="group"
aria-labelledby="priority-group-label"
>
{ACTIVITY_PRIORITY_OPTIONS.map((option) => {
const isSelected = priorityLevel === option.value;
@ -277,11 +291,12 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
type="button"
disabled={isPending}
onClick={() => setPriorityLevel(option.value)}
aria-pressed={isSelected}
className={cn(
"rounded-[1.25rem] border px-4 py-4 text-left transition",
isSelected
? "border-primary bg-primary text-primary-foreground shadow-[0_12px_30px_rgba(22,58,43,0.18)]"
: "border-border/60 bg-background/80 text-slate-900 hover:border-primary/35",
? "border-primary bg-primary text-primary-foreground shadow-[var(--shadow-2)]"
: "border-border/60 bg-background/80 text-foreground hover:border-primary/35",
isPending && "pointer-events-none opacity-70",
)}
>
@ -306,7 +321,7 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
</Card>
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-sm leading-7 text-muted-foreground">
<p className="text-sm leading-7 text-muted-foreground" aria-live="polite">
{isPending
? "Je activiteit wordt opgeslagen..."
: "Je activiteit wordt vandaag toegevoegd met status `gepland`, waarna de meter direct opnieuw wordt berekend."}

View file

@ -46,18 +46,16 @@ export function EnergyMeterCard({
meter,
tone = "default",
}: EnergyMeterCardProps) {
const progressValue =
meter.dailyBudget === null ? null : Math.min(100, Math.max(0, meter.progressPercent ?? 0));
return (
<Card
className={cn(
"rounded-[1.75rem] border border-border/60 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]",
tone === "default" ? "bg-card/90" : "bg-white/70",
)}
>
<Card tone={tone === "default" ? "default" : "subtle"} className="py-0">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
EnergyMeter
</p>
<CardTitle className="text-lg text-slate-900">
<CardTitle className="text-lg text-foreground">
{meter.dailyBudget === null
? `${meter.plannedPoints} geplande punten`
: `${meter.plannedPoints} van ${meter.dailyBudget} punten gepland`}
@ -69,17 +67,32 @@ export function EnergyMeterCard({
</CardDescription>
<div className="space-y-2">
<div className="h-3 overflow-hidden rounded-full bg-secondary">
<div
className="h-3 overflow-hidden rounded-full bg-secondary"
role={progressValue === null ? undefined : "progressbar"}
aria-label="Voortgang van je dagbudget"
aria-valuemin={progressValue === null ? undefined : 0}
aria-valuemax={progressValue === null ? undefined : 100}
aria-valuenow={progressValue === null ? undefined : progressValue}
aria-valuetext={
meter.dailyBudget === null
? "Nog geen dagbudget beschikbaar"
: `${meter.plannedPoints} van ${meter.dailyBudget} punten gepland`
}
>
<div
className={cn(
"h-full rounded-full transition-[width]",
meter.isOverBudget ? "bg-amber-500" : "bg-primary",
meter.isOverBudget ? "bg-warning" : "bg-primary",
)}
style={{ width: `${meter.progressPercent ?? 0}%` }}
/>
</div>
<div className="flex flex-wrap items-center justify-between gap-3 text-sm leading-7 text-slate-700">
<div
className="flex flex-wrap items-center justify-between gap-3 text-sm leading-7 text-muted-foreground"
aria-live="polite"
>
<p>
<strong>Activiteiten:</strong> {meter.activityCount}
</p>
@ -92,9 +105,9 @@ export function EnergyMeterCard({
</div>
{meter.dailyBudget !== null && meter.isOverBudget ? (
<Alert className="rounded-[1.25rem] border-amber-300 bg-amber-50 text-amber-950 [&_svg]:text-amber-700">
<Alert variant="warning">
<AlertTitle className="text-sm">Je zit boven je dagbudget</AlertTitle>
<AlertDescription className="leading-7 text-amber-900">
<AlertDescription className="leading-7 text-current">
Je planning komt nu <strong>{Math.abs(meter.remainingBudget ?? 0)} punten</strong> boven het dagbudget uit.
Je kunt nog steeds doorgaan, maar dit is een goed moment om iets te schrappen, te verkorten of later te doen.
</AlertDescription>

View file

@ -46,12 +46,12 @@ export function TodayActivitiesList({
categories,
}: TodayActivitiesListProps) {
return (
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<Card className="py-0">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Vandaag gepland
</p>
<CardTitle className="text-lg text-slate-900">
<CardTitle className="text-lg text-foreground">
{activities.length === 0
? "Nog geen activiteiten gepland"
: `${activities.length} ${activities.length === 1 ? "activiteit" : "activiteiten"}`}
@ -66,11 +66,11 @@ export function TodayActivitiesList({
activities.map((activity) => (
<div
key={activity.id}
className="rounded-[1.25rem] border border-border/60 bg-background/80 px-4 py-4"
className="rounded-[var(--radius-xl)] border border-border/60 bg-background/80 px-4 py-4"
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-sm font-semibold text-slate-900">{activity.name}</p>
<p className="text-sm font-semibold text-foreground">{activity.name}</p>
<p className="mt-1 text-sm leading-7 text-muted-foreground">
{getCategoryLabel(categories, activity.categoryId)}
</p>
@ -80,7 +80,7 @@ export function TodayActivitiesList({
</span>
</div>
<div className="mt-4 grid gap-3 text-sm leading-7 text-slate-700 sm:grid-cols-3">
<div className="mt-4 grid gap-3 text-sm leading-7 text-foreground/80 sm:grid-cols-3">
<p>
<strong>Duur:</strong> {activity.durationMinutes} min
</p>

View file

@ -48,12 +48,12 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
<input type="hidden" name="locale" value={locale} />
<PreferenceHiddenFields draft={draft} />
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_18px_60px_rgba(71,85,105,0.1)]">
<Card elevation="raised" className="py-0">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Account
</p>
<CardTitle className="font-[family-name:var(--font-display)] text-3xl text-slate-900">
<CardTitle className="font-[family-name:var(--font-display)] text-3xl text-foreground">
Basisinstellingen voor jouw account
</CardTitle>
<CardDescription className="max-w-2xl text-sm leading-7 text-muted-foreground">
@ -62,7 +62,7 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
</CardDescription>
</CardHeader>
<CardContent className="pt-1 pb-6">
<Alert className="rounded-[1.5rem] border-sky-200 bg-sky-50 text-sky-950 [&_svg]:text-sky-700">
<Alert variant="info">
<AlertDescription className="leading-7 text-current">
Release 1 draait bewust volledig in het <strong>Nederlands</strong>.
De taalinstelling blijft wel al aanwezig in het accountmodel.
@ -72,7 +72,7 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
</Card>
<section className="grid gap-5 lg:grid-cols-2">
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<Card className="py-0">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Taal en tijd
@ -80,7 +80,7 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
</CardHeader>
<CardContent className="space-y-5 pb-6">
<div className="space-y-2">
<Label className="text-slate-800">Taal</Label>
<Label className="text-foreground">Taal</Label>
<Select
disabled={isPending}
value={locale}
@ -100,7 +100,7 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
</div>
<div className="space-y-2">
<Label className="text-slate-800">Timezone</Label>
<Label className="text-foreground">Timezone</Label>
<Select
disabled={isPending}
value={draft.timezone}
@ -125,17 +125,17 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
</CardContent>
</Card>
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<Card className="py-0">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Interface
</p>
</CardHeader>
<CardContent className="space-y-4 pb-6">
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0 shadow-none">
<Card tone="subtle" className="py-0 shadow-none">
<CardContent className="flex items-start justify-between gap-4 py-5">
<div className="space-y-1">
<Label htmlFor="show-energy-points" className="text-sm font-semibold text-slate-900">
<Label htmlFor="show-energy-points" className="text-sm font-semibold text-foreground">
Toon energiebudgetpunten
</Label>
<p className="text-sm leading-7 text-muted-foreground">
@ -157,18 +157,18 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
</section>
<section className="grid gap-5 lg:grid-cols-2">
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<Card className="py-0">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Reminders
</p>
</CardHeader>
<CardContent className="space-y-4 pb-6">
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0 shadow-none">
<Card tone="subtle" className="py-0 shadow-none">
<CardContent className="space-y-4 py-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<Label htmlFor="morning-reminder-enabled" className="text-sm font-semibold text-slate-900">
<Label htmlFor="morning-reminder-enabled" className="text-sm font-semibold text-foreground">
Ochtendreminder
</Label>
<p className="text-sm leading-7 text-muted-foreground">
@ -189,12 +189,12 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
<>
<Separator />
<div className="space-y-2">
<Label htmlFor="morning-reminder-time" className="text-slate-800">
<Label htmlFor="morning-reminder-time" className="text-foreground">
Tijdstip voor de ochtendreminder
</Label>
<Input
id="morning-reminder-time"
className="h-12 rounded-[1.25rem] bg-white px-4 text-base md:text-base"
className="h-12 rounded-[1.25rem] bg-background/80 px-4 text-base md:text-base"
disabled={isPending}
type="time"
value={draft.morningReminderTime}
@ -208,10 +208,10 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
</CardContent>
</Card>
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0 shadow-none">
<Card tone="subtle" className="py-0 shadow-none">
<CardContent className="flex items-start justify-between gap-4 py-5">
<div className="space-y-1">
<Label htmlFor="reflection-reminder-enabled" className="text-sm font-semibold text-slate-900">
<Label htmlFor="reflection-reminder-enabled" className="text-sm font-semibold text-foreground">
Reflectieprompts toestaan
</Label>
<p className="text-sm leading-7 text-muted-foreground">
@ -231,7 +231,7 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
</CardContent>
</Card>
<Card className="rounded-[1.75rem] border border-primary/15 bg-primary py-0 text-primary-foreground shadow-[0_12px_40px_rgba(22,58,43,0.18)]">
<Card tone="primary" elevation="raised" className="py-0">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-primary-foreground/75">
Bewuste grenzen

View file

@ -0,0 +1,10 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
type ThemeProviderProps = React.ComponentProps<typeof NextThemesProvider>;
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View file

@ -4,13 +4,16 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
"group/alert relative grid w-full gap-0.5 rounded-[var(--radius-xl)] border px-3 py-3 text-left text-sm shadow-[var(--shadow-1)] has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-[1.125rem]",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
default: "border-border/70 bg-card/92 text-card-foreground",
info: "border-primary/15 bg-secondary text-foreground",
success: "border-success/30 bg-success/14 text-foreground",
warning: "border-warning/32 bg-warning/16 text-foreground",
destructive:
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
"border-destructive/32 bg-destructive/12 text-foreground *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-destructive",
},
},
defaultVariants: {
@ -39,7 +42,7 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="alert-title"
className={cn(
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
"font-medium leading-6 group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
className
)}
{...props}
@ -55,7 +58,7 @@ function AlertDescription({
<div
data-slot="alert-description"
className={cn(
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
"text-sm text-balance leading-7 text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className
)}
{...props}

View file

@ -4,32 +4,37 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group/button inline-flex shrink-0 items-center justify-center rounded-[var(--radius-full,9999px)] border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all duration-150 ease-[cubic-bezier(.2,.7,.2,1)] outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[1.125rem]",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
default:
"bg-primary text-primary-foreground shadow-[var(--shadow-1)] hover:bg-primary/90 hover:shadow-[var(--shadow-2)] [a]:hover:bg-primary/90",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
"border-border bg-background/88 hover:bg-muted hover:text-foreground hover:shadow-[var(--shadow-1)] aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
"bg-secondary text-secondary-foreground shadow-[var(--shadow-1)] hover:bg-secondary/85 hover:shadow-[var(--shadow-2)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
success:
"bg-success text-primary-foreground shadow-[var(--shadow-1)] hover:brightness-[0.98] hover:shadow-[var(--shadow-2)]",
warning:
"bg-warning text-foreground shadow-[var(--shadow-1)] hover:brightness-[0.98] hover:shadow-[var(--shadow-2)]",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
"bg-destructive text-primary-foreground shadow-[var(--shadow-1)] hover:brightness-[0.98] hover:shadow-[var(--shadow-2)] focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
xs: "h-6 gap-1 rounded-[var(--radius-sm)] px-2 text-xs in-data-[slot=button-group]:rounded-[var(--radius-sm)] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[var(--radius)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-[var(--radius)] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"size-6 rounded-[var(--radius-sm)] in-data-[slot=button-group]:rounded-[var(--radius-sm)] [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"size-7 rounded-[var(--radius)] in-data-[slot=button-group]:rounded-[var(--radius)]",
"icon-lg": "size-9",
},
},

View file

@ -1,18 +1,44 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const cardVariants = cva(
"group/card flex flex-col gap-4 overflow-hidden rounded-[var(--radius-xl)] py-4 text-sm text-card-foreground ring-1 ring-border/75 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-[var(--radius-xl)] *:[img:last-child]:rounded-b-[var(--radius-xl)]",
{
variants: {
tone: {
default: "bg-card/92",
subtle: "bg-background/78",
primary: "bg-primary text-primary-foreground ring-primary/10",
},
elevation: {
flat: "shadow-[var(--shadow-1)]",
raised: "shadow-[var(--shadow-2)]",
},
},
defaultVariants: {
tone: "default",
elevation: "flat",
},
}
)
function Card({
className,
size = "default",
tone,
elevation,
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
}: React.ComponentProps<"div"> &
{ size?: "default" | "sm" } &
VariantProps<typeof cardVariants>) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
cardVariants({ tone, elevation }),
className
)}
{...props}
@ -25,7 +51,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-[var(--radius-xl)] px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className
)}
{...props}
@ -38,7 +64,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
"font-heading text-base leading-snug font-semibold tracking-[-0.02em] group-data-[size=sm]/card:text-sm",
className
)}
{...props}
@ -84,7 +110,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
"flex items-center rounded-b-[var(--radius-xl)] border-t border-border/65 bg-muted/60 p-4 group-data-[size=sm]/card:p-3",
className
)}
{...props}
@ -100,4 +126,5 @@ export {
CardAction,
CardDescription,
CardContent,
cardVariants,
}

View file

@ -10,7 +10,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-4 aria-invalid:ring-destructive/16 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/24 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className
)}
{...props}

View file

@ -9,7 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
"h-8 w-full min-w-0 rounded-[var(--radius)] border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground hover:border-border/80 focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-4 aria-invalid:ring-destructive/16 md:text-sm dark:bg-input/30 dark:hover:bg-input/45 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/24",
className
)}
{...props}

View file

@ -41,7 +41,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"flex w-fit items-center justify-between gap-1.5 rounded-[var(--radius)] border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none hover:border-border/80 focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-4 aria-invalid:ring-destructive/16 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[var(--radius-sm)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/24 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
@ -83,7 +83,7 @@ function SelectContent({
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-[var(--radius-lg)] bg-popover text-popover-foreground shadow-[var(--shadow-2)] ring-1 ring-border/80 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
>
<SelectScrollUpButton />

View file

@ -7,12 +7,15 @@ import {
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react";
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { resolvedTheme } = useTheme();
return (
<Sonner
theme="light"
theme={resolvedTheme === "dark" ? "dark" : "light"}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
@ -26,6 +29,12 @@ const Toaster = ({ ...props }: ToasterProps) => {
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--success-bg": "var(--success)",
"--success-text": "var(--primary-foreground)",
"--warning-bg": "var(--warning)",
"--warning-text": "var(--foreground)",
"--error-bg": "var(--destructive)",
"--error-text": "var(--primary-foreground)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}

View file

@ -16,7 +16,7 @@ function Switch({
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background aria-invalid:border-destructive aria-invalid:ring-4 aria-invalid:ring-destructive/16 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/24 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}

View file

@ -69,7 +69,7 @@ export function TestWizardFlow() {
}
const aside = (
<Alert className="rounded-[1.5rem] border-white/10 bg-white/8 text-primary-foreground [&_svg]:text-primary-foreground/80">
<Alert className="rounded-[var(--radius-2xl)] border-white/10 bg-white/8 text-primary-foreground [&_svg]:text-primary-foreground/80">
<AlertDescription className="leading-7 text-current">
<span className="block font-semibold">Interne testwizard</span>
<span className="mt-2 block">
@ -129,7 +129,7 @@ export function TestWizardFlow() {
backAction={backAction}
nextAction={nextAction}
>
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0 shadow-none">
<Card tone="subtle" className="py-0 shadow-none">
<CardHeader className="pb-0">
<CardTitle className="font-[family-name:var(--font-display)] text-2xl">
{wizard.currentStep.title}

View file

@ -11,11 +11,15 @@ export function WizardProgress({ current, total }: WizardProgressProps) {
{Array.from({ length: total }, (_, index) => (
<li
key={index}
aria-current={index + 1 === current ? "step" : undefined}
aria-label={`Stap ${index + 1} van ${total}`}
className={cn(
"h-2 flex-1 rounded-full transition-colors",
index < current ? "bg-primary-foreground/85" : "bg-white/15",
)}
/>
>
<span className="sr-only">Stap {index + 1} van {total}</span>
</li>
))}
</ol>
);

View file

@ -28,7 +28,7 @@ export function WizardShell({
}: WizardShellProps) {
return (
<div className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr]">
<section className="rounded-[2rem] border border-primary/15 bg-primary p-7 text-primary-foreground shadow-[0_18px_60px_rgba(22,58,43,0.18)] sm:p-9">
<section className="app-panel-primary rounded-[var(--radius-4xl)] p-7 sm:p-9">
{eyebrow ? (
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-primary-foreground/70">
{eyebrow}
@ -46,7 +46,7 @@ export function WizardShell({
<WizardProgress current={progressCurrent} total={progressTotal} />
</section>
<section className="rounded-[2rem] border border-border/60 bg-card/90 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:p-8">
<section className="rounded-[var(--radius-4xl)] border border-border/70 bg-card/86 p-6 shadow-[var(--shadow-2)] backdrop-blur sm:p-8">
{topAction ? (
<div className="mb-6 flex items-center justify-between gap-3">{topAction}</div>
) : null}