Implement Dusk theme system and polish
This commit is contained in:
parent
e269a155da
commit
9280939756
38 changed files with 1144 additions and 270 deletions
|
|
@ -28,6 +28,7 @@ product, niet als medisch hulpmiddel. Release 1 blijft smal:
|
|||
- korte onboardingflow voor eerste voorkeuren
|
||||
- instellingen voor taal, timezone, reminders en zichtbaarheid van energiepunten
|
||||
- `shadcn/ui` foundation voor knoppen, formulieren, kaarten en meldingen
|
||||
- `Dusk`-theme met dark-mode prioriteit, semantische oppervlakken en verbeterde focus-/toegankelijkheidsstijlen
|
||||
|
||||
## Stack
|
||||
|
||||
|
|
@ -94,6 +95,10 @@ knoppen, formulieren, kaarten en meldingen. De theme tokens staan centraal in
|
|||
Voor feedback na redirects of server actions krijgt de app nu standaard de voorkeur
|
||||
voor `sonner`-toasts boven losse inline statusmeldingen.
|
||||
|
||||
De actuele visuele richting is `Dusk`: warme paper-achtergronden, gedempte indigo
|
||||
als primaire kleur, dark mode als standaard en semantische `success`/`warning`
|
||||
tokens voor rustige, niet-medische feedback.
|
||||
|
||||
## Interne wizard-test
|
||||
|
||||
Er is een interne testwizard beschikbaar op `/wizard-test` om een toekomstige
|
||||
|
|
@ -109,6 +114,7 @@ zichtbaar als `NEXT_PUBLIC_ENABLE_TEST_WIZARD=true` staat.
|
|||
## Documentatie
|
||||
|
||||
- Hoofdset specificaties en plannen: [docs/README.md](/Users/janpetervisser/Development/third/docs/README.md)
|
||||
- Dusk theme-specificatie: [inspannings-monitor-09-dusk-theme-specificatie-v01.md](/Users/janpetervisser/Development/third/docs/inspannings-monitor-09-dusk-theme-specificatie-v01.md)
|
||||
- Technische architectuur: [inspannings-monitor-05-technische-architectuur-en-implementatie-v01.docx](/Users/janpetervisser/Development/third/docs/inspannings-monitor-05-technische-architectuur-en-implementatie-v01.docx)
|
||||
- Implementatieplan en backlog: [inspannings-monitor-06-implementatieplan-en-backlog-v01.docx](/Users/janpetervisser/Development/third/docs/inspannings-monitor-06-implementatieplan-en-backlog-v01.docx)
|
||||
|
||||
|
|
|
|||
|
|
@ -55,14 +55,14 @@ export default async function CheckInPage({ searchParams }: CheckInPageProps) {
|
|||
);
|
||||
|
||||
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 flex max-w-6xl flex-col gap-8">
|
||||
<StatusToastBridge toast={statusToast} paramKeys={["error", "status"]} />
|
||||
|
||||
<header className="flex flex-col gap-5 rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:flex-row sm:items-start sm:justify-between sm:p-8">
|
||||
<header className="app-page-header">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
|
||||
<Link href="/dashboard" className="transition hover:text-slate-900">
|
||||
<div className="app-page-breadcrumb">
|
||||
<Link href="/dashboard" className="app-page-link">
|
||||
Dashboard
|
||||
</Link>
|
||||
<span>/</span>
|
||||
|
|
@ -71,7 +71,7 @@ export default async function CheckInPage({ searchParams }: CheckInPageProps) {
|
|||
<h1 className="mt-3 font-[family-name:var(--font-display)] text-4xl leading-tight">
|
||||
Ochtendcheck-in van vandaag
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-base leading-8 text-slate-700">
|
||||
<p className="app-page-copy">
|
||||
Houd je start rustig en klein. Je legt alleen een energiescore en een
|
||||
globale slaapindruk vast voor vandaag.
|
||||
</p>
|
||||
|
|
@ -99,12 +99,12 @@ export default async function CheckInPage({ searchParams }: CheckInPageProps) {
|
|||
<CheckInForm todayCheckIn={checkInStatus?.todayCheckIn ?? null} />
|
||||
|
||||
<aside className="space-y-5">
|
||||
<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
|
||||
</p>
|
||||
<CardTitle className="text-lg text-slate-900">
|
||||
<CardTitle className="text-lg text-foreground">
|
||||
{checkInStatus?.todayCheckIn ? "Check-in staat al klaar" : "Nog geen check-in"}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -123,7 +123,7 @@ export default async function CheckInPage({ searchParams }: CheckInPageProps) {
|
|||
</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 grens
|
||||
|
|
|
|||
|
|
@ -77,19 +77,19 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
|
|||
);
|
||||
|
||||
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 flex max-w-6xl flex-col gap-8">
|
||||
<StatusToastBridge toast={statusToast} />
|
||||
|
||||
<header className="flex flex-col gap-5 rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:flex-row sm:items-start sm:justify-between sm:p-8">
|
||||
<header className="app-page-header">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
||||
Protected route
|
||||
</p>
|
||||
<h1 className="mt-3 font-[family-name:var(--font-display)] text-4xl leading-tight">
|
||||
Dashboard placeholder voor release 1
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-base leading-8 text-slate-700">
|
||||
<p className="app-page-copy">
|
||||
Je sessie is server-side gevalideerd en het minimale profielbundle is
|
||||
nu beschikbaar. Daarmee staat de fundering voor onboarding, settings
|
||||
en de eerste energieflows klaar.
|
||||
|
|
@ -135,12 +135,12 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
|
|||
</header>
|
||||
|
||||
<section className="grid gap-5 md:grid-cols-3">
|
||||
<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">
|
||||
Auth
|
||||
</p>
|
||||
<CardTitle className="text-lg text-slate-900">Cookie-based sessie actief</CardTitle>
|
||||
<CardTitle className="text-lg text-foreground">Cookie-based sessie actief</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-6">
|
||||
<CardDescription className="text-sm leading-7 text-muted-foreground">
|
||||
|
|
@ -149,12 +149,12 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
|
|||
</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">
|
||||
Profiel
|
||||
</p>
|
||||
<CardTitle className="text-lg text-slate-900">{profileTitle}</CardTitle>
|
||||
<CardTitle className="text-lg text-foreground">{profileTitle}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-6">
|
||||
<CardDescription className="text-sm leading-7 text-muted-foreground">
|
||||
|
|
@ -164,12 +164,12 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
|
|||
</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">
|
||||
Onboarding
|
||||
</p>
|
||||
<CardTitle className="text-lg text-slate-900">{onboardingState}</CardTitle>
|
||||
<CardTitle className="text-lg text-foreground">{onboardingState}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-6">
|
||||
<CardDescription className="text-sm leading-7 text-muted-foreground">
|
||||
|
|
@ -179,12 +179,12 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
|
|||
</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">
|
||||
Instellingen
|
||||
</p>
|
||||
<CardTitle className="text-lg text-slate-900">
|
||||
<CardTitle className="text-lg text-foreground">
|
||||
Punten {formatToggleState(settings.showEnergyPoints, "zichtbaar", "verborgen")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -198,12 +198,12 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
|
|||
|
||||
<CheckInCard todayCheckIn={checkInStatus?.todayCheckIn ?? null} />
|
||||
|
||||
<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">
|
||||
Dagplanning
|
||||
</p>
|
||||
<CardTitle className="text-lg text-slate-900">
|
||||
<CardTitle className="text-lg text-foreground">
|
||||
{planningStatus?.activities.length
|
||||
? `${planningStatus.activities.length} activiteiten voor vandaag`
|
||||
: "Nog niets gepland voor vandaag"}
|
||||
|
|
@ -230,12 +230,12 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
|
|||
<EnergyMeterCard meter={planningMeter} tone="subtle" />
|
||||
|
||||
{isTestWizardEnabled() ? (
|
||||
<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">
|
||||
Wizard core
|
||||
</p>
|
||||
<CardTitle className="text-lg text-slate-900">Interne testwizard actief</CardTitle>
|
||||
<CardTitle className="text-lg text-foreground">Interne testwizard actief</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-6">
|
||||
<CardDescription className="text-sm leading-7 text-muted-foreground">
|
||||
|
|
@ -247,11 +247,11 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
|
|||
</section>
|
||||
|
||||
{!profile.onboardingCompleted ? (
|
||||
<Card className="rounded-[1.75rem] border border-amber-900/15 bg-amber-50 py-0 text-amber-950 shadow-[0_12px_40px_rgba(146,64,14,0.08)]">
|
||||
<Card className="border-warning/32 bg-warning/16 py-0 text-foreground shadow-[var(--shadow-1)]">
|
||||
<CardContent className="flex flex-col gap-4 px-6 py-5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="font-semibold">Je onboarding is nog niet afgerond.</p>
|
||||
<p className="mt-1 max-w-2xl text-sm leading-7 text-amber-900">
|
||||
<p className="mt-1 max-w-2xl text-sm leading-7 text-foreground/82">
|
||||
Je kunt de korte flow later alsnog afronden om je basisinstellingen
|
||||
en eerste voorkeuren vast te leggen.
|
||||
</p>
|
||||
|
|
@ -259,8 +259,8 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
|
|||
<Link
|
||||
href="/onboarding"
|
||||
className={cn(
|
||||
buttonVariants({ size: "lg" }),
|
||||
"h-11 shrink-0 rounded-full bg-amber-950 px-5 text-amber-50 hover:bg-amber-900",
|
||||
buttonVariants({ variant: "warning", size: "lg" }),
|
||||
"h-11 shrink-0 rounded-full px-5",
|
||||
)}
|
||||
>
|
||||
Rond onboarding af
|
||||
|
|
@ -268,7 +268,7 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
|
|||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="rounded-[1.75rem] border border-primary/10 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">
|
||||
<CardContent className="flex flex-col gap-4 px-6 py-5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="font-semibold">Je instellingen kun je nu ook los beheren.</p>
|
||||
|
|
|
|||
266
app/globals.css
266
app/globals.css
|
|
@ -5,42 +5,52 @@
|
|||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--font-display: "Iowan Old Style", "Palatino Linotype", "URW Palladio L",
|
||||
Palatino, Georgia, serif;
|
||||
--font-body: "Inter", "Aptos", "Segoe UI", "Helvetica Neue", Arial,
|
||||
sans-serif;
|
||||
--background: #f5f4ee;
|
||||
--foreground: #0f172a;
|
||||
--card: rgb(255 255 255 / 0.84);
|
||||
--card-foreground: #0f172a;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #0f172a;
|
||||
--primary: #163a2b;
|
||||
--primary-foreground: #effaf3;
|
||||
--secondary: #e5ecde;
|
||||
--secondary-foreground: #163a2b;
|
||||
--muted: #eef2e6;
|
||||
--muted-foreground: #51606f;
|
||||
--accent: #dbe7d1;
|
||||
--accent-foreground: #163a2b;
|
||||
--destructive: #b91c1c;
|
||||
--border: rgb(15 23 42 / 0.1);
|
||||
--input: rgb(15 23 42 / 0.12);
|
||||
--ring: #5d8a67;
|
||||
--chart-1: #163a2b;
|
||||
--chart-2: #5d8a67;
|
||||
--chart-3: #90a955;
|
||||
--chart-4: #b7c5a4;
|
||||
--chart-5: #d7dfce;
|
||||
--radius: 1rem;
|
||||
--sidebar: #fbfaf5;
|
||||
--sidebar-foreground: #0f172a;
|
||||
--sidebar-primary: #163a2b;
|
||||
--sidebar-primary-foreground: #effaf3;
|
||||
--sidebar-accent: #dbe7d1;
|
||||
--sidebar-accent-foreground: #163a2b;
|
||||
--sidebar-border: rgb(15 23 42 / 0.08);
|
||||
--sidebar-ring: #5d8a67;
|
||||
--font-body: var(--font-inter-tight), ui-sans-serif, system-ui, sans-serif;
|
||||
--font-display: var(--font-body);
|
||||
--font-mono: var(--font-plex-mono), ui-monospace, monospace;
|
||||
|
||||
--background: oklch(97% 0.008 80);
|
||||
--foreground: oklch(22% 0.03 262);
|
||||
--card: oklch(99% 0.004 80);
|
||||
--card-foreground: oklch(22% 0.03 262);
|
||||
--popover: oklch(100% 0 0);
|
||||
--popover-foreground: oklch(22% 0.03 262);
|
||||
|
||||
--primary: oklch(44% 0.11 262);
|
||||
--primary-foreground: oklch(98% 0.01 262);
|
||||
--secondary: oklch(92% 0.03 262);
|
||||
--secondary-foreground: oklch(44% 0.11 262);
|
||||
--muted: oklch(95% 0.012 82);
|
||||
--muted-foreground: oklch(58% 0.015 262);
|
||||
--accent: oklch(92% 0.03 262);
|
||||
--accent-foreground: oklch(44% 0.11 262);
|
||||
|
||||
--destructive: oklch(58% 0.16 25);
|
||||
--success: oklch(62% 0.09 155);
|
||||
--warning: oklch(72% 0.1 70);
|
||||
|
||||
--border: oklch(22% 0.03 262 / 0.1);
|
||||
--input: oklch(22% 0.03 262 / 0.12);
|
||||
--ring: oklch(44% 0.11 262);
|
||||
|
||||
--chart-1: oklch(44% 0.11 262);
|
||||
--chart-2: oklch(60% 0.09 262);
|
||||
--chart-3: oklch(70% 0.1 50);
|
||||
--chart-4: oklch(62% 0.09 155);
|
||||
--chart-5: oklch(80% 0.03 262);
|
||||
|
||||
--radius: 14px;
|
||||
--shadow-1: 0 1px 3px oklch(22% 0.03 262 / 0.1), 0 10px 30px oklch(22% 0.03 262 / 0.06);
|
||||
--shadow-2: 0 4px 16px oklch(22% 0.03 262 / 0.14), 0 18px 45px oklch(22% 0.03 262 / 0.12);
|
||||
--shadow-3: 0 24px 60px oklch(22% 0.03 262 / 0.18);
|
||||
--sidebar: oklch(96.5% 0.01 80);
|
||||
--sidebar-foreground: oklch(22% 0.03 262);
|
||||
--sidebar-primary: oklch(44% 0.11 262);
|
||||
--sidebar-primary-foreground: oklch(98% 0.01 262);
|
||||
--sidebar-accent: oklch(92% 0.03 262);
|
||||
--sidebar-accent-foreground: oklch(44% 0.11 262);
|
||||
--sidebar-border: oklch(22% 0.03 262 / 0.08);
|
||||
--sidebar-ring: oklch(44% 0.11 262);
|
||||
}
|
||||
|
||||
* {
|
||||
|
|
@ -79,6 +89,8 @@ a {
|
|||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-warning: var(--warning);
|
||||
--color-success: var(--success);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
|
|
@ -97,47 +109,60 @@ a {
|
|||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--font-mono: var(--font-plex-mono), ui-monospace, monospace;
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 11px;
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
--radius-xl: 21px;
|
||||
--radius-2xl: 25px;
|
||||
--radius-3xl: 32px;
|
||||
--radius-4xl: 40px;
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #111927;
|
||||
--foreground: #eef6f0;
|
||||
--card: rgb(17 25 39 / 0.84);
|
||||
--card-foreground: #eef6f0;
|
||||
--popover: #152131;
|
||||
--popover-foreground: #eef6f0;
|
||||
--primary: #d9f2de;
|
||||
--primary-foreground: #133225;
|
||||
--secondary: #243244;
|
||||
--secondary-foreground: #eef6f0;
|
||||
--muted: #243244;
|
||||
--muted-foreground: #b1bec8;
|
||||
--accent: #31485b;
|
||||
--accent-foreground: #eef6f0;
|
||||
--destructive: #ef4444;
|
||||
--border: rgb(255 255 255 / 0.12);
|
||||
--input: rgb(255 255 255 / 0.14);
|
||||
--ring: #88b593;
|
||||
--chart-1: #d9f2de;
|
||||
--chart-2: #88b593;
|
||||
--chart-3: #90a955;
|
||||
--chart-4: #51606f;
|
||||
--chart-5: #243244;
|
||||
--sidebar: #152131;
|
||||
--sidebar-foreground: #eef6f0;
|
||||
--sidebar-primary: #d9f2de;
|
||||
--sidebar-primary-foreground: #133225;
|
||||
--sidebar-accent: #243244;
|
||||
--sidebar-accent-foreground: #eef6f0;
|
||||
--sidebar-border: rgb(255 255 255 / 0.12);
|
||||
--sidebar-ring: #88b593;
|
||||
--background: oklch(17% 0.02 262);
|
||||
--foreground: oklch(96% 0.008 80);
|
||||
--card: oklch(22% 0.025 262);
|
||||
--card-foreground: oklch(96% 0.008 80);
|
||||
--popover: oklch(22% 0.025 262);
|
||||
--popover-foreground: oklch(96% 0.008 80);
|
||||
|
||||
--primary: oklch(78% 0.08 262);
|
||||
--primary-foreground: oklch(20% 0.03 262);
|
||||
--secondary: oklch(28% 0.03 262);
|
||||
--secondary-foreground: oklch(96% 0.008 80);
|
||||
--muted: oklch(26% 0.025 262);
|
||||
--muted-foreground: oklch(70% 0.015 262);
|
||||
--accent: oklch(30% 0.04 262);
|
||||
--accent-foreground: oklch(96% 0.008 80);
|
||||
|
||||
--destructive: oklch(70% 0.16 25);
|
||||
--success: oklch(74% 0.09 155);
|
||||
--warning: oklch(80% 0.1 70);
|
||||
|
||||
--border: oklch(100% 0 0 / 0.1);
|
||||
--input: oklch(100% 0 0 / 0.1);
|
||||
--ring: oklch(78% 0.08 262);
|
||||
|
||||
--chart-1: oklch(78% 0.08 262);
|
||||
--chart-2: oklch(60% 0.09 262);
|
||||
--chart-3: oklch(80% 0.1 70);
|
||||
--chart-4: oklch(74% 0.09 155);
|
||||
--chart-5: oklch(36% 0.03 262);
|
||||
|
||||
--shadow-1: 0 1px 3px oklch(0% 0 0 / 0.24), 0 10px 30px oklch(0% 0 0 / 0.2);
|
||||
--shadow-2: 0 4px 16px oklch(0% 0 0 / 0.28), 0 18px 45px oklch(0% 0 0 / 0.24);
|
||||
--shadow-3: 0 24px 60px oklch(0% 0 0 / 0.32);
|
||||
|
||||
--sidebar: oklch(20% 0.022 262);
|
||||
--sidebar-foreground: oklch(96% 0.008 80);
|
||||
--sidebar-primary: oklch(78% 0.08 262);
|
||||
--sidebar-primary-foreground: oklch(20% 0.03 262);
|
||||
--sidebar-accent: oklch(28% 0.03 262);
|
||||
--sidebar-accent-foreground: oklch(96% 0.008 80);
|
||||
--sidebar-border: oklch(100% 0 0 / 0.1);
|
||||
--sidebar-ring: oklch(78% 0.08 262);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
|
@ -152,12 +177,107 @@ a {
|
|||
@apply font-sans;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
color-scheme: light;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: var(--font-body), sans-serif;
|
||||
line-height: 1.7;
|
||||
font-variant-numeric: tabular-nums;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
h1 {
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.08;
|
||||
}
|
||||
h2 {
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
h3 {
|
||||
line-height: 1.3;
|
||||
}
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: var(--font-mono), monospace;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.app-page {
|
||||
@apply min-h-screen px-6 py-10 text-foreground sm:px-8;
|
||||
background-color: var(--background);
|
||||
background-image:
|
||||
radial-gradient(
|
||||
circle at top,
|
||||
color-mix(in oklab, var(--primary) 16%, transparent) 0%,
|
||||
transparent 34%
|
||||
),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in oklab, var(--background) 96%, var(--secondary) 4%) 0%,
|
||||
color-mix(in oklab, var(--background) 86%, var(--secondary) 14%) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.dark .app-page {
|
||||
background-image:
|
||||
radial-gradient(
|
||||
circle at top,
|
||||
color-mix(in oklab, var(--primary) 22%, transparent) 0%,
|
||||
transparent 38%
|
||||
),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in oklab, var(--background) 96%, var(--secondary) 4%) 0%,
|
||||
color-mix(in oklab, var(--background) 88%, var(--secondary) 12%) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.app-page-header {
|
||||
@apply flex flex-col gap-5 rounded-[var(--radius-4xl)] p-6 sm:flex-row sm:items-start sm:justify-between sm:p-8;
|
||||
border: 1px solid color-mix(in oklab, var(--border) 88%, transparent);
|
||||
background: color-mix(in oklab, var(--card) 84%, transparent);
|
||||
box-shadow: var(--shadow-2);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.app-page-breadcrumb {
|
||||
@apply flex flex-wrap items-center gap-3 text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground;
|
||||
}
|
||||
|
||||
.app-page-link {
|
||||
@apply transition hover:text-foreground;
|
||||
}
|
||||
|
||||
.app-page-copy {
|
||||
@apply mt-4 max-w-2xl text-base leading-8 text-muted-foreground;
|
||||
}
|
||||
|
||||
.app-panel-primary {
|
||||
@apply text-primary-foreground;
|
||||
border: 1px solid color-mix(in oklab, var(--primary) 16%, transparent);
|
||||
background: var(--primary);
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,22 @@
|
|||
import type { Metadata } from "next";
|
||||
import { IBM_Plex_Mono, Inter_Tight } from "next/font/google";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import "./globals.css";
|
||||
|
||||
const fontBody = Inter_Tight({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter-tight",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const fontMono = IBM_Plex_Mono({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500"],
|
||||
variable: "--font-plex-mono",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Inspannings Monitor",
|
||||
description:
|
||||
|
|
@ -14,10 +29,21 @@ export default function RootLayout({
|
|||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="nl">
|
||||
<body className="min-h-screen">
|
||||
{children}
|
||||
<Toaster position="top-right" richColors closeButton />
|
||||
<html
|
||||
lang="nl"
|
||||
suppressHydrationWarning
|
||||
className={`${fontBody.variable} ${fontMono.variable}`}
|
||||
>
|
||||
<body className="min-h-screen antialiased">
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
<Toaster position="top-right" richColors closeButton />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
|
|||
footer={
|
||||
<p>
|
||||
Nog geen account?{" "}
|
||||
<Link href={signUpHref} className="font-semibold text-emerald-900">
|
||||
<Link href={signUpHref} className="font-semibold text-primary">
|
||||
Maak er een aan
|
||||
</Link>
|
||||
</p>
|
||||
|
|
@ -50,7 +50,7 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
|
|||
<StatusToastBridge toast={statusToast} paramKeys={["error", "status"]} />
|
||||
|
||||
{!authState.isConfigured ? (
|
||||
<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">
|
||||
Voeg eerst je Supabase-gegevens toe in `.env.local` op basis van `.env.example`.
|
||||
</AlertDescription>
|
||||
|
|
@ -60,7 +60,7 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
|
|||
<input type="hidden" name="next" value={next} />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-slate-800">
|
||||
<Label htmlFor="email" className="text-foreground">
|
||||
E-mailadres
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -74,7 +74,7 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
|
|||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="text-slate-800">
|
||||
<Label htmlFor="password" className="text-foreground">
|
||||
Wachtwoord
|
||||
</Label>
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export default async function OnboardingPage({ searchParams }: OnboardingPagePro
|
|||
);
|
||||
|
||||
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 flex max-w-6xl flex-col gap-8">
|
||||
<StatusToastBridge toast={statusToast} paramKeys={["error", "status"]} />
|
||||
<OnboardingFlow profileBundle={profileBundle} />
|
||||
|
|
|
|||
29
app/page.tsx
29
app/page.tsx
|
|
@ -51,11 +51,11 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|||
);
|
||||
|
||||
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%)] text-slate-900">
|
||||
<main className="app-page">
|
||||
<div className="mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-10 sm:px-8 lg:px-10">
|
||||
<header className="mb-10 flex items-center justify-between border-b border-black/10 pb-5">
|
||||
<header className="mb-10 flex items-center justify-between border-b border-border/70 pb-5">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-600">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
||||
Inspannings Monitor
|
||||
</p>
|
||||
<h1 className="font-[family-name:var(--font-display)] text-3xl leading-tight sm:text-5xl">
|
||||
|
|
@ -104,7 +104,7 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|||
</>
|
||||
)
|
||||
) : (
|
||||
<span className="rounded-full border border-amber-900/15 bg-amber-50 px-4 py-2 text-sm font-medium text-amber-900 shadow-sm">
|
||||
<span className="rounded-full border border-warning/30 bg-warning/14 px-4 py-2 text-sm font-medium text-foreground shadow-[var(--shadow-1)]">
|
||||
Supabase nog niet geconfigureerd
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -114,9 +114,9 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|||
<StatusToastBridge toast={statusToast} paramKeys={["error", "status"]} />
|
||||
|
||||
<section className="grid gap-6 lg:grid-cols-[1.35fr_0.95fr]">
|
||||
<Card className="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="rounded-[var(--radius-4xl)] py-0 backdrop-blur">
|
||||
<CardContent className="p-6 sm:p-8">
|
||||
<p className="mb-4 max-w-2xl text-lg leading-8 text-slate-700">
|
||||
<p className="mb-4 max-w-2xl text-lg leading-8 text-muted-foreground">
|
||||
De projectbasis staat nu, inclusief de eerste auth-laag via Supabase.
|
||||
Release 1 blijft bewust smal: publieke landing, aparte login/signup
|
||||
routes en een eerste protected dashboard als basis voor de volgende stories.
|
||||
|
|
@ -125,7 +125,8 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|||
{loopSteps.map((step, index) => (
|
||||
<Card
|
||||
key={step.title}
|
||||
className="rounded-[1.5rem] border border-border/50 bg-background/80 py-0 shadow-none"
|
||||
tone="subtle"
|
||||
className="rounded-[var(--radius-2xl)] py-0 shadow-none"
|
||||
>
|
||||
<CardHeader className="pb-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-muted-foreground">
|
||||
|
|
@ -146,7 +147,7 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="rounded-[2rem] border border-primary/10 bg-primary py-0 text-primary-foreground shadow-[0_18px_60px_rgba(22,58,43,0.18)]">
|
||||
<Card tone="primary" elevation="raised" className="rounded-[var(--radius-4xl)] py-0">
|
||||
<CardHeader className="px-6 pt-7 sm:px-8">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-primary-foreground/75">
|
||||
Release 1 focus
|
||||
|
|
@ -156,7 +157,7 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|||
{releaseFocus.map((item) => (
|
||||
<Card
|
||||
key={item}
|
||||
className="rounded-[1.5rem] border border-white/10 bg-white/8 py-0 text-primary-foreground shadow-none"
|
||||
className="rounded-[var(--radius-2xl)] border-white/10 bg-white/8 py-0 text-primary-foreground shadow-none"
|
||||
>
|
||||
<CardContent className="px-4 py-3 text-sm leading-7">{item}</CardContent>
|
||||
</Card>
|
||||
|
|
@ -174,31 +175,31 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|||
</Card>
|
||||
</section>
|
||||
|
||||
<Card className="mt-8 rounded-[2rem] border border-border/60 bg-card/80 py-0 shadow-[0_10px_45px_rgba(71,85,105,0.08)] backdrop-blur">
|
||||
<Card tone="subtle" className="mt-8 rounded-[var(--radius-4xl)] py-0 backdrop-blur">
|
||||
<CardContent className="grid gap-5 p-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
||||
Volgende story
|
||||
</p>
|
||||
<p className="mt-2 font-semibold text-slate-900">ST-201 Ochtendcheck-in</p>
|
||||
<p className="mt-2 font-semibold text-foreground">ST-201 Ochtendcheck-in</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
||||
Doelgroep
|
||||
</p>
|
||||
<p className="mt-2 font-semibold text-slate-900">Volwassen individuele gebruikers</p>
|
||||
<p className="mt-2 font-semibold text-foreground">Volwassen individuele gebruikers</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
||||
Positionering
|
||||
</p>
|
||||
<p className="mt-2 font-semibold text-slate-900">Wellness / self-management</p>
|
||||
<p className="mt-2 font-semibold text-foreground">Wellness / self-management</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
||||
Status
|
||||
</p>
|
||||
<p className="mt-2 font-semibold text-slate-900">Auth, onboarding en settings actief</p>
|
||||
<p className="mt-2 font-semibold text-foreground">Auth, onboarding en settings actief</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -70,14 +70,14 @@ export default async function PlanningPage({ searchParams }: PlanningPageProps)
|
|||
);
|
||||
|
||||
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 flex max-w-6xl flex-col gap-8">
|
||||
<StatusToastBridge toast={statusToast} paramKeys={["error", "status"]} />
|
||||
|
||||
<header className="flex flex-col gap-5 rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:flex-row sm:items-start sm:justify-between sm:p-8">
|
||||
<header className="app-page-header">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
|
||||
<Link href="/dashboard" className="transition hover:text-slate-900">
|
||||
<div className="app-page-breadcrumb">
|
||||
<Link href="/dashboard" className="app-page-link">
|
||||
Dashboard
|
||||
</Link>
|
||||
<span>/</span>
|
||||
|
|
@ -86,7 +86,7 @@ export default async function PlanningPage({ searchParams }: PlanningPageProps)
|
|||
<h1 className="mt-3 font-[family-name:var(--font-display)] text-4xl leading-tight">
|
||||
Plan vandaag bewust klein
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-base leading-8 text-slate-700">
|
||||
<p className="app-page-copy">
|
||||
Voeg alleen activiteiten toe die vandaag echt relevant zijn. Houd de lijst licht,
|
||||
zodat je later goed kunt bijsturen zonder druk op te bouwen.
|
||||
</p>
|
||||
|
|
@ -118,12 +118,12 @@ export default async function PlanningPage({ searchParams }: PlanningPageProps)
|
|||
/>
|
||||
|
||||
<aside className="space-y-5">
|
||||
<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
|
||||
</p>
|
||||
<CardTitle className="text-lg text-slate-900">
|
||||
<CardTitle className="text-lg text-foreground">
|
||||
{planningPageData.activities.length === 0
|
||||
? "Start met een eerste activiteit"
|
||||
: `${planningPageData.activities.length} activiteiten ingepland`}
|
||||
|
|
@ -149,7 +149,7 @@ export default async function PlanningPage({ searchParams }: PlanningPageProps)
|
|||
|
||||
<EnergyMeterCard meter={planningMeter} />
|
||||
|
||||
<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 grens
|
||||
|
|
|
|||
|
|
@ -57,14 +57,14 @@ export default async function SettingsPage({ searchParams }: SettingsPageProps)
|
|||
"Ingelogde gebruiker";
|
||||
|
||||
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 flex max-w-6xl flex-col gap-8">
|
||||
<StatusToastBridge toast={statusToast} paramKeys={["error", "status"]} />
|
||||
|
||||
<header className="flex flex-col gap-5 rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:flex-row sm:items-start sm:justify-between sm:p-8">
|
||||
<header className="app-page-header">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
|
||||
<Link href="/dashboard" className="transition hover:text-slate-900">
|
||||
<div className="app-page-breadcrumb">
|
||||
<Link href="/dashboard" className="app-page-link">
|
||||
Dashboard
|
||||
</Link>
|
||||
<span>/</span>
|
||||
|
|
@ -73,7 +73,7 @@ export default async function SettingsPage({ searchParams }: SettingsPageProps)
|
|||
<h1 className="mt-3 font-[family-name:var(--font-display)] text-4xl leading-tight">
|
||||
Instellingen
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-base leading-8 text-slate-700">
|
||||
<p className="app-page-copy">
|
||||
Pas je basisvoorkeuren rustig aan. Alles blijft beperkt tot jouw eigen
|
||||
account en de wellness-first scope van release 1.
|
||||
</p>
|
||||
|
|
@ -101,12 +101,12 @@ export default async function SettingsPage({ searchParams }: SettingsPageProps)
|
|||
<SettingsForm profileBundle={profileBundle} />
|
||||
|
||||
<aside className="space-y-5">
|
||||
<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">
|
||||
Account
|
||||
</p>
|
||||
<CardTitle className="text-lg text-slate-900">{profileTitle}</CardTitle>
|
||||
<CardTitle className="text-lg text-foreground">{profileTitle}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-6">
|
||||
<CardDescription className="text-sm leading-7 text-muted-foreground">
|
||||
|
|
@ -115,12 +115,12 @@ export default async function SettingsPage({ searchParams }: SettingsPageProps)
|
|||
</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">
|
||||
Huidige status
|
||||
</p>
|
||||
<CardTitle className="text-lg text-slate-900">
|
||||
<CardTitle className="text-lg text-foreground">
|
||||
Onboarding {profileBundle.profile.onboardingCompleted ? "afgerond" : "later afronden"}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export default async function SignUpPage({ searchParams }: SignUpPageProps) {
|
|||
footer={
|
||||
<p>
|
||||
Heb je al een account?{" "}
|
||||
<Link href={loginHref} className="font-semibold text-emerald-900">
|
||||
<Link href={loginHref} className="font-semibold text-primary">
|
||||
Log dan in
|
||||
</Link>
|
||||
</p>
|
||||
|
|
@ -50,7 +50,7 @@ export default async function SignUpPage({ searchParams }: SignUpPageProps) {
|
|||
<StatusToastBridge toast={statusToast} paramKeys={["error", "status"]} />
|
||||
|
||||
{!authState.isConfigured ? (
|
||||
<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">
|
||||
Voeg eerst je Supabase-gegevens toe in `.env.local` op basis van `.env.example`.
|
||||
</AlertDescription>
|
||||
|
|
@ -60,7 +60,7 @@ export default async function SignUpPage({ searchParams }: SignUpPageProps) {
|
|||
<input type="hidden" name="next" value={next} />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-slate-800">
|
||||
<Label htmlFor="email" className="text-foreground">
|
||||
E-mailadres
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -74,7 +74,7 @@ export default async function SignUpPage({ searchParams }: SignUpPageProps) {
|
|||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="text-slate-800">
|
||||
<Label htmlFor="password" className="text-foreground">
|
||||
Wachtwoord
|
||||
</Label>
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default async function WizardTestPage() {
|
|||
}
|
||||
|
||||
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 flex max-w-6xl flex-col gap-8">
|
||||
<TestWizardFlow />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
10
components/theme-provider.tsx
Normal file
10
components/theme-provider.tsx
Normal 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>;
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ Deze map bevat de vernieuwde documentatie voor de gekozen `wellness/self-managem
|
|||
- Database: `Supabase PostgreSQL`
|
||||
- Authenticatie: `Supabase Auth`
|
||||
- UI foundation in de app: `Tailwind CSS + shadcn/ui`
|
||||
- Visueel thema in de app: `Dusk` met dark-mode prioriteit en semantische statuskleuren
|
||||
|
||||
## Actuele app-status
|
||||
|
||||
|
|
@ -59,6 +60,8 @@ Deze map bevat de vernieuwde documentatie voor de gekozen `wellness/self-managem
|
|||
Legt de actuele operationele en security-keuzes vast rond repositorybescherming, Vercel-deploys en secretbeheer.
|
||||
- [gpt-instructies.md](./gpt-instructies.md)
|
||||
Bundelt de inhoudelijke instructies en expliciete keuzes die in deze context zijn gegeven als compacte bron voor vervolgwerk.
|
||||
- [inspannings-monitor-09-dusk-theme-specificatie-v01.md](./inspannings-monitor-09-dusk-theme-specificatie-v01.md)
|
||||
Legt het leidende Dusk-thema vast voor kleur, typografie, iconografie, motion en toegankelijkheidsregels in de app.
|
||||
|
||||
## Backlog en Linear
|
||||
|
||||
|
|
|
|||
620
docs/inspannings-monitor-09-dusk-theme-specificatie-v01.md
Normal file
620
docs/inspannings-monitor-09-dusk-theme-specificatie-v01.md
Normal file
|
|
@ -0,0 +1,620 @@
|
|||
# Inspannings Monitor — 09 · Dusk Theme Specificatie
|
||||
|
||||
**Versie:** v0.1 · **Datum:** 2026-04-19 · **Status:** Normatief, basis nu in code doorgevoerd
|
||||
**Scope:** UI-thema voor Next.js 16 + shadcn/ui + Tailwind v4 codebase
|
||||
**Variant:** Dusk · **Dark mode:** Prioriteit · **Icon-set:** Lucide
|
||||
|
||||
Bij conflict met eerdere documenten wint deze specificatie voor alles wat
|
||||
visueel design betreft. Functioneel gedrag blijft geregeld in
|
||||
`02-functionele-specificatie-mvp`.
|
||||
|
||||
## 0 · Implementatiestatus
|
||||
|
||||
Per 2026-04-19 is de basis van dit thema in de app doorgevoerd:
|
||||
|
||||
- centrale `oklch()`-tokens in `app/globals.css`
|
||||
- `Inter Tight` + `IBM Plex Mono` via `next/font`
|
||||
- dark mode als standaard via `next-themes`
|
||||
- semantische componentvarianten voor cards, alerts en buttons
|
||||
- Dusk-shell op landing, dashboard, auth, onboarding, check-in, planning en settings
|
||||
- accessibility-polish voor focus, keuzegroepen en de EnergyMeter-progressbar
|
||||
|
||||
Nog bewust iteratief:
|
||||
|
||||
- fijnslijpen van iconmapping per view
|
||||
- laatste micro-spacing en copy-patronen in nieuwe features
|
||||
- eventuele theme toggle als productkeuze voor later
|
||||
|
||||
---
|
||||
|
||||
## 1 · Rationale
|
||||
|
||||
Inspannings Monitor richt zich op volwassenen die rust zoeken in het
|
||||
plannen van hun energie. Dusk is gekozen omdat:
|
||||
|
||||
- **Warme papertone** (ivory, L 97%) vermindert klinische feel — de app
|
||||
is expliciet *wellness*, geen medisch hulpmiddel.
|
||||
- **Gedempte indigo primary** (hue 262, chroma 0.11) voelt kalm en
|
||||
"avondlicht" aan. Past bij de dagelijkse lus (ochtendcheck-in →
|
||||
dagplanning → avondreflectie) die doorloopt tot de avond.
|
||||
- **Whisper-quiet contrast** (nooit echt zwart-op-wit) voorkomt
|
||||
schermmoeheid en matcht de rustige copywriting.
|
||||
- **Dark mode als spiegel** in dezelfde hue, iets hogere chroma op ink —
|
||||
identiteit blijft behouden zonder saai navy te worden.
|
||||
|
||||
### Niet-doelen
|
||||
|
||||
- Geen medische signalen (geen rood-groen triage-semantiek).
|
||||
- Geen "motivational" UI (geen grote cijfers, geen gamification-kleur).
|
||||
- Geen hoge-energie accenten (chroma > 0.13 uitgesloten).
|
||||
|
||||
---
|
||||
|
||||
## 2 · Kleurpalet
|
||||
|
||||
Alle kleuren in **oklch()**. Namen volgen shadcn/ui-conventies zodat alle
|
||||
bestaande componenten zonder aanpassing werken.
|
||||
|
||||
### 2.1 · Light mode
|
||||
|
||||
| Token | Waarde | Gebruik |
|
||||
| --- | --- | --- |
|
||||
| `--background` | `oklch(97% 0.008 80)` | Paper |
|
||||
| `--muted` | `oklch(95% 0.012 82)` | Paper-soft (rail, rustvlak) |
|
||||
| `--card` | `oklch(99% 0.004 80)` | Kaartoppervlak |
|
||||
| `--popover` | `oklch(100% 0 0)` | Popovers, toasts, sheets |
|
||||
| `--foreground` | `oklch(22% 0.03 262)` | Primaire tekst (contrast 14.2:1) |
|
||||
| `--muted-foreground` | `oklch(58% 0.015 262)` | Secundaire tekst (AA 4.5:1) |
|
||||
| `--primary` | `oklch(44% 0.11 262)` | Muted indigo |
|
||||
| `--primary-foreground` | `oklch(98% 0.01 262)` | Tekst op primary |
|
||||
| `--secondary` / `--accent` | `oklch(92% 0.03 262)` | Primary-soft vlak |
|
||||
| `--border` | `oklch(22% 0.03 262 / 0.10)` | Hairline |
|
||||
| `--input` | `oklch(22% 0.03 262 / 0.12)` | Form border |
|
||||
| `--ring` | `oklch(44% 0.11 262)` | Focus |
|
||||
| `--destructive` | `oklch(58% 0.16 25)` | Alleen echte destructieve acties |
|
||||
| `--success` | `oklch(62% 0.09 155)` | Succes, voltooid |
|
||||
| `--warning` | `oklch(72% 0.10 70)` | Budget-overschrijding |
|
||||
|
||||
### 2.2 · Dark mode
|
||||
|
||||
| Token | Waarde |
|
||||
| --- | --- |
|
||||
| `--background` | `oklch(17% 0.02 262)` |
|
||||
| `--card` | `oklch(22% 0.025 262)` |
|
||||
| `--popover` | `oklch(22% 0.025 262)` |
|
||||
| `--foreground` | `oklch(96% 0.008 80)` |
|
||||
| `--muted` | `oklch(26% 0.025 262)` |
|
||||
| `--muted-foreground` | `oklch(70% 0.015 262)` |
|
||||
| `--primary` | `oklch(78% 0.08 262)` |
|
||||
| `--primary-foreground` | `oklch(20% 0.03 262)` |
|
||||
| `--secondary` | `oklch(28% 0.03 262)` |
|
||||
| `--accent` | `oklch(30% 0.04 262)` |
|
||||
| `--border` | `oklch(100% 0 0 / 0.10)` |
|
||||
| `--ring` | `oklch(78% 0.08 262)` |
|
||||
| `--destructive` | `oklch(70% 0.16 25)` |
|
||||
| `--success` | `oklch(74% 0.09 155)` |
|
||||
| `--warning` | `oklch(80% 0.10 70)` |
|
||||
|
||||
### 2.3 · Charts
|
||||
|
||||
| Token | Waarde | Gebruik |
|
||||
| --- | --- | --- |
|
||||
| `--chart-1` | `oklch(44% 0.11 262)` | Vandaag / primair |
|
||||
| `--chart-2` | `oklch(60% 0.09 262)` | Gemiddelde / trend |
|
||||
| `--chart-3` | `oklch(70% 0.10 50)` | Accent amber — alleen voor overschrijding |
|
||||
| `--chart-4` | `oklch(62% 0.09 155)` | Succestint |
|
||||
| `--chart-5` | `oklch(80% 0.03 262)` | Overige dagen / achtergrond |
|
||||
|
||||
> **Regel:** rood (`--destructive`) nooit gebruiken voor budget — dat is
|
||||
> `--warning` (amber). Rood is gereserveerd voor "verwijder account",
|
||||
> "annuleer abonnement" en vergelijkbaar.
|
||||
|
||||
---
|
||||
|
||||
## 3 · Typografie
|
||||
|
||||
Eén UI-font: **Inter Tight**. Monospace voor cijfers en timestamps in
|
||||
ondersteunende posities: **IBM Plex Mono**. De huidige display-serif
|
||||
stack wordt verwijderd.
|
||||
|
||||
### 3.1 · Schaal
|
||||
|
||||
| Rol | Grootte | Gewicht | Letterspacing | Line-height |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| H1 | 42 | 500 | -0.03em | 1.08 |
|
||||
| H2 | 22 | 600 | -0.02em | 1.2 |
|
||||
| H3 | 15 | 600 | 0 | 1.3 |
|
||||
| Body | 15 | 400 | 0 | 1.7 |
|
||||
| Micro | 12 | 400 | 0 | 1.5 |
|
||||
| Eyebrow | 11 | 600 | 0.16em uppercase | 1.4 |
|
||||
| Number | variabel | 500 | -0.02em | 1.2 |
|
||||
|
||||
### 3.2 · Regels
|
||||
|
||||
- Alle cijfers in UI krijgen `font-variant-numeric: tabular-nums`. Dit
|
||||
staat globaal op `body` en wordt niet ongedaan gemaakt.
|
||||
- Letterspacing op headings negatief (-0.02 / -0.03) — geen condensed
|
||||
gevoel.
|
||||
- Body line-height 1.65–1.7.
|
||||
- Geen font-weight onder 400 — zwakke tekst is onleesbaar op low-contrast
|
||||
paper.
|
||||
- Geen serif in UI meer. Serif eventueel alleen in gegenereerde PDF's.
|
||||
|
||||
---
|
||||
|
||||
## 4 · Iconografie — Lucide
|
||||
|
||||
Set: **lucide-react** (al in shadcn-stack). Alle iconen met
|
||||
`strokeWidth={1.5}`, `strokeLinecap="round"`, `strokeLinejoin="round"`.
|
||||
Default grootte **18px** in UI, **20px** in cards, **16px** inline. Kleur
|
||||
erft via `currentColor` — nooit hardcoden.
|
||||
|
||||
### 4.1 · Voorgeschreven mapping
|
||||
|
||||
| Icoon | Doel |
|
||||
| --- | --- |
|
||||
| `Sun` | Ochtendcheck-in |
|
||||
| `Moon` | Avondreflectie |
|
||||
| `Zap` | Energie / budget |
|
||||
| `Calendar` | Dagplanning |
|
||||
| `Clock` | Tijdstip / duur |
|
||||
| `Heart` | Welzijn / herstel |
|
||||
| `Pencil` | Notitie bewerken |
|
||||
| `Settings` | Instellingen |
|
||||
| `Plus` | Activiteit toevoegen |
|
||||
| `Check` | Klaar / voltooid |
|
||||
| `AlertCircle` | Buiten budget (warning) |
|
||||
| `LogOut` | Uitloggen |
|
||||
|
||||
### 4.2 · Regels
|
||||
|
||||
- Nooit twee verschillende icoon-sets mixen in één view.
|
||||
- Rood (`text-destructive`) alleen op echte destructieve acties, nooit
|
||||
voor "over budget" — daar `text-[--warning]` (amber) gebruiken.
|
||||
- Iconen zijn stil: geen hover-animatie, geen kleurwissel behalve de
|
||||
natuurlijke `hover:text-primary` van het parent-element.
|
||||
- Iconen met `aria-hidden` tenzij ze de enige labeldrager zijn.
|
||||
|
||||
---
|
||||
|
||||
## 5 · Radius & elevatie
|
||||
|
||||
| Token | Waarde | Gebruik |
|
||||
| --- | --- | --- |
|
||||
| `--radius` | `14px` | Kaarten, inputs (basis) |
|
||||
| `--radius-sm` | `8px` | Chips, dense items |
|
||||
| `--radius-lg` | `21px` | Hero cards, dialogs |
|
||||
| `--radius-xl` | `25px` | Sheet / drawer |
|
||||
| `--radius-full` | `9999px` | Buttons (pill), avatars |
|
||||
| `--shadow-1` | 1px + 3px drop | Flat kaarten |
|
||||
| `--shadow-2` | 4/16px drop | Hover, verhoogde kaarten |
|
||||
| `--shadow-3` | 24/60px drop | Popovers, toasts, modals |
|
||||
|
||||
Alle schaduwen zijn indigo-getint (niet neutraal grijs) zodat ze
|
||||
harmoniseren met paper en ink.
|
||||
|
||||
---
|
||||
|
||||
## 6 · Motion
|
||||
|
||||
- Standaard transitieduur **160 ms**, easing
|
||||
`cubic-bezier(.2, .7, .2, 1)`.
|
||||
- Meter- en ring-animaties: **400 ms** enter, geen loop.
|
||||
- Modals / popovers: **180 ms** fade + 6 px translate-Y.
|
||||
- `prefers-reduced-motion: reduce` schakelt alles naar **0.01 ms**.
|
||||
- Geen parallax, geen scroll-linked animaties.
|
||||
|
||||
---
|
||||
|
||||
## 7 · Toegankelijkheid
|
||||
|
||||
- Body-tekst minimaal WCAG AA (4.5:1). Primary-knop AAA in beide modes.
|
||||
- Focus-ring: `2px solid var(--ring)` met `outline-offset: 2px` —
|
||||
**nooit** `outline: none` zonder vervanging.
|
||||
- Hit-targets minimaal 40×40 desktop, **44×44 op touch**.
|
||||
- Scale-buttons (1–10 in check-in): `aria-label="Energie {n} van 10"` +
|
||||
`aria-pressed`.
|
||||
- Alle getalwaarden krijgen een leesbare `aria-label`
|
||||
(bijv. "8 komma 1 punten van 14").
|
||||
- Kleur is nooit de enige signaaldrager — "binnen budget" krijgt altijd
|
||||
ook tekst of een icoon.
|
||||
- Nederlands is de primaire taal. Screen-reader-output moet in het
|
||||
Nederlands kloppen (decimale komma, niet punt).
|
||||
|
||||
---
|
||||
|
||||
## 8 · Implementatie
|
||||
|
||||
### 8.1 · `app/globals.css`
|
||||
|
||||
Vervang de inhoud van `:root` en voeg `.dark` toe. Namen blijven
|
||||
shadcn-compatibel; alleen waarden wijzigen.
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
/* Type */
|
||||
--font-body: var(--font-inter-tight), ui-sans-serif, system-ui, sans-serif;
|
||||
--font-display: var(--font-body);
|
||||
--font-mono: var(--font-plex-mono), ui-monospace, monospace;
|
||||
|
||||
/* Surfaces */
|
||||
--background: oklch(97% 0.008 80);
|
||||
--foreground: oklch(22% 0.03 262);
|
||||
--card: oklch(99% 0.004 80);
|
||||
--card-foreground: oklch(22% 0.03 262);
|
||||
--popover: oklch(100% 0 0);
|
||||
--popover-foreground: oklch(22% 0.03 262);
|
||||
|
||||
/* Primary — muted indigo */
|
||||
--primary: oklch(44% 0.11 262);
|
||||
--primary-foreground: oklch(98% 0.01 262);
|
||||
--secondary: oklch(92% 0.03 262);
|
||||
--secondary-foreground: oklch(44% 0.11 262);
|
||||
|
||||
/* Muted & accent */
|
||||
--muted: oklch(95% 0.012 82);
|
||||
--muted-foreground: oklch(58% 0.015 262);
|
||||
--accent: oklch(92% 0.03 262);
|
||||
--accent-foreground: oklch(44% 0.11 262);
|
||||
|
||||
/* Status */
|
||||
--destructive: oklch(58% 0.16 25);
|
||||
--success: oklch(62% 0.09 155);
|
||||
--warning: oklch(72% 0.10 70);
|
||||
|
||||
/* Lijnen & focus */
|
||||
--border: oklch(22% 0.03 262 / 0.10);
|
||||
--input: oklch(22% 0.03 262 / 0.12);
|
||||
--ring: oklch(44% 0.11 262);
|
||||
|
||||
/* Charts */
|
||||
--chart-1: oklch(44% 0.11 262);
|
||||
--chart-2: oklch(60% 0.09 262);
|
||||
--chart-3: oklch(70% 0.10 50);
|
||||
--chart-4: oklch(62% 0.09 155);
|
||||
--chart-5: oklch(80% 0.03 262);
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar: oklch(95% 0.012 82);
|
||||
--sidebar-foreground: oklch(22% 0.03 262);
|
||||
--sidebar-primary: oklch(44% 0.11 262);
|
||||
--sidebar-primary-foreground: oklch(98% 0.01 262);
|
||||
--sidebar-accent: oklch(92% 0.03 262);
|
||||
--sidebar-accent-foreground: oklch(44% 0.11 262);
|
||||
--sidebar-border: oklch(22% 0.03 262 / 0.08);
|
||||
--sidebar-ring: oklch(44% 0.11 262);
|
||||
|
||||
/* Radius & elevatie */
|
||||
--radius: 14px;
|
||||
--shadow-1: 0 1px 2px oklch(25% 0.03 262 / 0.06), 0 1px 3px oklch(25% 0.03 262 / 0.04);
|
||||
--shadow-2: 0 4px 16px oklch(25% 0.03 262 / 0.08), 0 1px 2px oklch(25% 0.03 262 / 0.04);
|
||||
--shadow-3: 0 24px 60px oklch(25% 0.03 262 / 0.10), 0 2px 6px oklch(25% 0.03 262 / 0.04);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(17% 0.02 262);
|
||||
--foreground: oklch(96% 0.008 80);
|
||||
--card: oklch(22% 0.025 262);
|
||||
--card-foreground: oklch(96% 0.008 80);
|
||||
--popover: oklch(22% 0.025 262);
|
||||
--popover-foreground: oklch(96% 0.008 80);
|
||||
|
||||
--primary: oklch(78% 0.08 262);
|
||||
--primary-foreground: oklch(20% 0.03 262);
|
||||
--secondary: oklch(28% 0.03 262);
|
||||
--secondary-foreground: oklch(92% 0.02 262);
|
||||
|
||||
--muted: oklch(26% 0.025 262);
|
||||
--muted-foreground: oklch(70% 0.015 262);
|
||||
--accent: oklch(30% 0.04 262);
|
||||
--accent-foreground: oklch(92% 0.02 262);
|
||||
|
||||
--destructive: oklch(70% 0.16 25);
|
||||
--success: oklch(74% 0.09 155);
|
||||
--warning: oklch(80% 0.10 70);
|
||||
|
||||
--border: oklch(100% 0 0 / 0.10);
|
||||
--input: oklch(100% 0 0 / 0.12);
|
||||
--ring: oklch(78% 0.08 262);
|
||||
|
||||
--chart-1: oklch(78% 0.08 262);
|
||||
--chart-2: oklch(60% 0.09 262);
|
||||
--chart-3: oklch(74% 0.10 50);
|
||||
--chart-4: oklch(74% 0.09 155);
|
||||
--chart-5: oklch(45% 0.03 262);
|
||||
|
||||
--sidebar: oklch(20% 0.025 262);
|
||||
--sidebar-foreground: oklch(96% 0.008 80);
|
||||
--sidebar-primary: oklch(78% 0.08 262);
|
||||
--sidebar-primary-foreground: oklch(20% 0.03 262);
|
||||
--sidebar-accent: oklch(28% 0.03 262);
|
||||
--sidebar-accent-foreground: oklch(92% 0.02 262);
|
||||
--sidebar-border: oklch(100% 0 0 / 0.08);
|
||||
--sidebar-ring: oklch(78% 0.08 262);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-body);
|
||||
--font-mono: var(--font-mono);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* { @apply border-border outline-ring/50; }
|
||||
html {
|
||||
color-scheme: light dark;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: var(--font-body);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
button:not(:disabled), [role="button"]:not(:disabled) { cursor: pointer; }
|
||||
|
||||
:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 · `app/layout.tsx` — fonts via `next/font`
|
||||
|
||||
```tsx
|
||||
import { Inter_Tight, IBM_Plex_Mono } from "next/font/google";
|
||||
|
||||
const interTight = Inter_Tight({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter-tight",
|
||||
display: "swap",
|
||||
weight: ["400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
const plexMono = IBM_Plex_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-plex-mono",
|
||||
display: "swap",
|
||||
weight: ["400", "500"],
|
||||
});
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html
|
||||
lang="nl"
|
||||
suppressHydrationWarning
|
||||
className={`${interTight.variable} ${plexMono.variable}`}
|
||||
>
|
||||
<body>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Verwijder de bestaande `--font-display` (serif) declaratie en alle
|
||||
`font-[family-name:var(--font-display)]` classes uit views.
|
||||
|
||||
### 8.3 · Dark mode toggle via `next-themes`
|
||||
|
||||
```bash
|
||||
npm i next-themes
|
||||
```
|
||||
|
||||
```tsx
|
||||
// app/theme-provider.tsx
|
||||
"use client";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 · `components/ui/theme-toggle.tsx`
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button } from "./button";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={isDark ? "Schakel naar licht" : "Schakel naar donker"}
|
||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||
>
|
||||
{isDark
|
||||
? <Sun className="size-[18px]" strokeWidth={1.5} />
|
||||
: <Moon className="size-[18px]" strokeWidth={1.5} />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 8.5 · Class-cleanup tabel
|
||||
|
||||
Hardcoded Tailwind-kleuren ondermijnen het thema. Per view zoek-en-vervang:
|
||||
|
||||
| Oud | Nieuw |
|
||||
| --- | --- |
|
||||
| `bg-white/75 backdrop-blur` | `bg-card` |
|
||||
| `bg-white` | `bg-card` of `bg-popover` |
|
||||
| `text-slate-900` | `text-foreground` |
|
||||
| `text-slate-700` / `text-slate-600` / `text-slate-500` | `text-muted-foreground` |
|
||||
| `border-black/10` | `border-border` |
|
||||
| `rounded-[2rem]` / `rounded-[1.75rem]` | `rounded-xl` (= 1.4× `--radius`) |
|
||||
| `shadow-[0_18px_60px_...]` | `shadow-[var(--shadow-3)]` |
|
||||
| `shadow-[0_12px_40px_...]` | `shadow-[var(--shadow-2)]` |
|
||||
| `bg-[radial-gradient(...)]` hero | verwijderen — paper is al warm |
|
||||
| `font-[family-name:var(--font-display)]` | verwijderen — één font |
|
||||
| `bg-amber-50 text-amber-950` (budget warning) | `bg-muted text-foreground` + `border-[color:var(--warning)]` |
|
||||
|
||||
### 8.6 · Voorbeeld-diff — `app/dashboard/page.tsx`
|
||||
|
||||
```diff
|
||||
- <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="min-h-screen bg-background px-6 py-10 text-foreground sm:px-8">
|
||||
|
||||
- <header className="... rounded-[2rem] border border-black/10 bg-white/75 ... shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur ...">
|
||||
+ <header className="... rounded-xl border border-border bg-card shadow-[var(--shadow-2)] ...">
|
||||
|
||||
- <CardTitle className="text-lg text-slate-900">Cookie-based sessie actief</CardTitle>
|
||||
+ <CardTitle className="text-lg text-foreground">Cookie-based sessie actief</CardTitle>
|
||||
```
|
||||
|
||||
### 8.7 · Iconen in componenten
|
||||
|
||||
```tsx
|
||||
import { Sun, Calendar, Zap, Plus } from "lucide-react";
|
||||
|
||||
// CheckInCard header:
|
||||
<Sun className="size-[18px] text-primary" strokeWidth={1.5} aria-hidden />
|
||||
|
||||
// PlanningPage primaire actie:
|
||||
<Button>
|
||||
<Plus className="size-4" strokeWidth={1.5} aria-hidden />
|
||||
Activiteit
|
||||
</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9 · Chart-specificatie
|
||||
|
||||
De week-grafiek op het dashboard gebruikt `--chart-1` voor "vandaag" en
|
||||
`--chart-5` voor overige dagen. `--chart-3` (amber) markeert
|
||||
overschrijdingen. **Nooit** `--destructive` (rood) voor budget — dat is
|
||||
`--warning`.
|
||||
|
||||
---
|
||||
|
||||
## 10 · Rollout-plan
|
||||
|
||||
| PR | Scope | Omvang |
|
||||
| --- | --- | --- |
|
||||
| 1 | `globals.css` vervangen (§8.1) | klein |
|
||||
| 2 | Fonts via `next/font` + cleanup `--font-display` (§8.2) | klein |
|
||||
| 3 | Dark mode — `next-themes`, `ThemeToggle` in dashboard + settings (§8.3, §8.4) | middel |
|
||||
| 4 | View cleanup — één commit per route: landing, dashboard, check-in, planning, settings, onboarding (§8.5, §8.6) | groot |
|
||||
| 5 | Lucide-standaardisatie — alle iconen 1.5 stroke, `size-[18px]` default (§4) | klein |
|
||||
| 6 | `/styleguide` route achter `NEXT_PUBLIC_ENABLE_STYLEGUIDE` vlag | klein |
|
||||
|
||||
Richtlijn: maximaal één PR per dag in productie — de grote view-cleanup
|
||||
splitsen over meerdere dagen zodat visual regression per route
|
||||
beoordeeld kan worden.
|
||||
|
||||
---
|
||||
|
||||
## 11 · Acceptatiecriteria
|
||||
|
||||
- [ ] Landing, dashboard, ochtendcheck-in, dagplanning, instellingen en
|
||||
onboarding renderen correct in zowel light als dark.
|
||||
- [ ] Er zijn geen hardcoded `text-slate-*`, `bg-white*`, `border-black/*`
|
||||
of hex-kleurwaarden meer in `app/**/*.tsx` of `components/**/*.tsx`.
|
||||
- [ ] Theme-toggle werkt zonder flash; SSR levert de juiste kleur bij
|
||||
eerste render (dankzij `suppressHydrationWarning` + `next-themes`).
|
||||
- [ ] Lighthouse a11y-score ≥ 95 op dashboard in beide modes.
|
||||
- [ ] `prefers-reduced-motion` getest — meter-animaties zijn uit.
|
||||
- [ ] Alle pt-waarden (check-in, planning, dagbudget) gebruiken
|
||||
`tabular-nums`.
|
||||
- [ ] Focus-ring zichtbaar op alle tab-reachable elementen.
|
||||
- [ ] `docs/inspannings-monitor-09-dusk-theme-specificatie-v01.md`
|
||||
gelinkt vanuit `docs/README.md`.
|
||||
|
||||
---
|
||||
|
||||
## 12 · Bestanden die wijzigen
|
||||
|
||||
| Bestand | Wijziging |
|
||||
| --- | --- |
|
||||
| `app/globals.css` | Vervangen (§8.1) |
|
||||
| `app/layout.tsx` | Fonts + Providers wrap (§8.2, §8.3) |
|
||||
| `app/theme-provider.tsx` | Nieuw (§8.3) |
|
||||
| `components/ui/theme-toggle.tsx` | Nieuw (§8.4) |
|
||||
| `app/page.tsx` | Class cleanup (§8.5, §8.6) |
|
||||
| `app/dashboard/page.tsx` | Cleanup + `ThemeToggle` |
|
||||
| `app/check-in/**/*.tsx` | Cleanup + Lucide `Sun` icon |
|
||||
| `app/planning/**/*.tsx` | Cleanup + Lucide `Calendar` / `Plus` |
|
||||
| `app/settings/page.tsx` | Cleanup + `ThemeToggle` |
|
||||
| `app/onboarding/**/*.tsx` | Cleanup |
|
||||
| `components/check-in/*.tsx` | Tokens + `aria-label` op scale-buttons |
|
||||
| `components/planning/energy-meter-card.tsx` | Tokens + `tabular-nums` + reduced-motion |
|
||||
| `app/styleguide/page.tsx` | Nieuw — primitives preview (flag-gated) |
|
||||
| `docs/README.md` | Link naar deze spec toevoegen |
|
||||
|
||||
---
|
||||
|
||||
## 13 · Wijzigingslog
|
||||
|
||||
| Versie | Datum | Wijziging |
|
||||
| --- | --- | --- |
|
||||
| v0.1 | 2026-04-19 | Initiële Dusk-specificatie. Variant gekozen na exploratie van 4 blue-leaning opties (Dusk, Harbor, Linen, Meridian). |
|
||||
|
||||
---
|
||||
|
||||
*Dit document is de normatieve bron voor het Dusk-thema. Wijzigingen
|
||||
vereisen versiebump en een corresponderende entry in §13.*
|
||||
Loading…
Add table
Add a link
Reference in a new issue