feat: initial commit

This commit is contained in:
Janpeter Visser 2026-04-18 14:18:26 +02:00
commit 7d443a004a
76 changed files with 15704 additions and 0 deletions

View file

@ -0,0 +1,25 @@
import { type AuthNotice } from "@/lib/auth/messages";
type AuthNoticeProps = {
notice: AuthNotice | null;
};
const toneStyles = {
error: "border-rose-200 bg-rose-50 text-rose-900",
success: "border-emerald-200 bg-emerald-50 text-emerald-900",
info: "border-sky-200 bg-sky-50 text-sky-900",
};
export function AuthNotice({ notice }: AuthNoticeProps) {
if (!notice) {
return null;
}
return (
<div
className={`mb-5 rounded-2xl border px-4 py-3 text-sm leading-7 ${toneStyles[notice.tone]}`}
>
{notice.text}
</div>
);
}

View file

@ -0,0 +1,64 @@
import Link from "next/link";
import type { ReactNode } from "react";
type AuthPanelProps = {
eyebrow: string;
title: string;
description: string;
children: ReactNode;
footer: ReactNode;
};
export function AuthPanel({
eyebrow,
title,
description,
children,
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">
<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">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-200/80">
{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">
{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">
<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>
<li>Geen zorgverlenerstoegang, sharing of AI in deze fase</li>
<li>Authenticatie via Supabase met cookie-based sessies</li>
</ul>
</div>
</section>
<section className="flex items-center">
<div className="w-full rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:p-8">
<div className="mb-6 flex items-center justify-between gap-3">
<Link
href="/"
className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500 transition hover:text-slate-900"
>
Terug naar landing
</Link>
</div>
{children}
<div className="mt-6 border-t border-black/10 pt-5 text-sm text-slate-600">
{footer}
</div>
</div>
</section>
</div>
</main>
);
}

View file

@ -0,0 +1,292 @@
"use client";
import type { MouseEvent } from "react";
import { useState } from "react";
import { completeOnboardingAction, skipOnboardingAction } from "@/app/onboarding/actions";
import { ONBOARDING_TIMEZONE_OPTIONS } from "@/lib/onboarding/options";
import type { ProfileBundle } from "@/lib/profile/types";
type OnboardingFlowProps = {
profileBundle: ProfileBundle;
};
const steps = [
{
eyebrow: "Stap 1",
title: "Zo gebruiken we Inspannings Monitor",
description:
"De app helpt je om je dag rustiger te plannen en terug te kijken zonder medische claims of zorgverlenerfuncties.",
},
{
eyebrow: "Stap 2",
title: "Basisprofiel",
description:
"Kies hoe de app je mag aanspreken en welke timezone het best bij je dagindeling past.",
},
{
eyebrow: "Stap 3",
title: "Startvoorkeuren",
description:
"Kies rustig hoe zichtbaar je energiebudget is en of je lichte reminders wilt ontvangen.",
},
] as const;
export function OnboardingFlow({ profileBundle }: OnboardingFlowProps) {
const [currentStep, setCurrentStep] = useState(0);
const [displayName, setDisplayName] = useState(profileBundle.profile.displayName ?? "");
const [timezone, setTimezone] = useState(profileBundle.profile.timezone);
const [showEnergyPoints, setShowEnergyPoints] = useState(
profileBundle.settings.showEnergyPoints,
);
const [morningReminderEnabled, setMorningReminderEnabled] = useState(
profileBundle.settings.morningReminderEnabled,
);
const [morningReminderTime, setMorningReminderTime] = useState(
profileBundle.settings.morningReminderTime ?? "08:30",
);
const [reflectionReminderEnabled, setReflectionReminderEnabled] = useState(
profileBundle.settings.reflectionReminderEnabled,
);
const step = steps[currentStep];
const isFirstStep = currentStep === 0;
const isLastStep = currentStep === steps.length - 1;
function goToPreviousStep() {
setCurrentStep((stepIndex) => Math.max(0, stepIndex - 1));
}
function goToNextStep(event: MouseEvent<HTMLButtonElement>) {
// This button lives inside the onboarding form. By preventing the default
// click action and rendering a keyed replacement, we avoid an accidental
// form submit when the final step button appears after the state update.
event.preventDefault();
setCurrentStep((stepIndex) => Math.min(steps.length - 1, stepIndex + 1));
}
return (
<div className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr]">
<section className="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">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-200/80">
{step.eyebrow}
</p>
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl leading-tight sm:text-5xl">
{step.title}
</h1>
<p className="mt-5 max-w-xl text-base leading-8 text-emerald-50/85">
{step.description}
</p>
<div className="mt-10 rounded-[1.5rem] border border-white/10 bg-white/8 p-5 text-sm leading-7 text-emerald-50/90">
<p className="font-semibold">Release 1 blijft bewust wellness-first.</p>
<ul className="mt-3 space-y-2">
<li>Alleen voor individuele gebruikers, zonder delen of zorgverlenerstoegang.</li>
<li>De app geeft geen diagnose, behandeling of medisch advies.</li>
<li>Bij acute of snel verslechterende klachten hoort directe hulp via arts, huisartsenpost of 112 buiten deze app.</li>
</ul>
</div>
<ol className="mt-8 flex gap-3">
{steps.map((item, index) => (
<li
key={item.title}
className={`h-2 flex-1 rounded-full ${
index <= currentStep ? "bg-emerald-200" : "bg-white/15"
}`}
/>
))}
</ol>
</section>
<section className="rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:p-8">
<div className="mb-6 flex items-center justify-between gap-3">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Korte onboarding
</p>
<form action={skipOnboardingAction}>
<button
type="submit"
className="rounded-full border border-black/10 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:-translate-y-0.5 hover:text-slate-950"
>
Nu overslaan
</button>
</form>
</div>
<form action={completeOnboardingAction} className="space-y-6">
<input type="hidden" name="displayName" value={displayName} />
<input type="hidden" name="timezone" value={timezone} />
<input
type="hidden"
name="showEnergyPoints"
value={showEnergyPoints ? "true" : "false"}
/>
<input
type="hidden"
name="morningReminderEnabled"
value={morningReminderEnabled ? "true" : "false"}
/>
<input type="hidden" name="morningReminderTime" value={morningReminderTime} />
<input
type="hidden"
name="reflectionReminderEnabled"
value={reflectionReminderEnabled ? "true" : "false"}
/>
{currentStep === 0 ? (
<div className="space-y-4">
<article className="rounded-[1.5rem] border border-black/10 bg-stone-50 p-5">
<h2 className="font-[family-name:var(--font-display)] text-2xl text-slate-900">
Wat je hier wél krijgt
</h2>
<p className="mt-3 text-sm leading-7 text-slate-700">
Een rustige plan-doe-evalueer flow met energiebudgetten, zonder
druk, score-oordeel of medische terminologie.
</p>
</article>
<article className="rounded-[1.5rem] border border-black/10 bg-stone-50 p-5">
<h2 className="font-[family-name:var(--font-display)] text-2xl text-slate-900">
Wat deze app niet doet
</h2>
<p className="mt-3 text-sm leading-7 text-slate-700">
Geen diagnose, geen behandeling, geen medische triage en geen
automatisch delen met derden.
</p>
</article>
</div>
) : null}
{currentStep === 1 ? (
<div className="space-y-5">
<label className="block text-sm font-medium text-slate-800">
Schermnaam
<input
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
type="text"
value={displayName}
onChange={(event) => setDisplayName(event.target.value)}
placeholder="Optioneel, bijvoorbeeld Jan"
maxLength={40}
/>
</label>
<div className="rounded-[1.5rem] border border-sky-200 bg-sky-50 px-4 py-4 text-sm leading-7 text-sky-900">
Voertaal voor release 1 staat vast op <strong>Nederlands</strong>.
</div>
<label className="block text-sm font-medium text-slate-800">
Timezone
<select
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
value={timezone}
onChange={(event) => setTimezone(event.target.value)}
>
{ONBOARDING_TIMEZONE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
) : null}
{currentStep === 2 ? (
<div className="space-y-4">
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={showEnergyPoints}
onChange={(event) => setShowEnergyPoints(event.target.checked)}
/>
<span>
<span className="block text-sm font-semibold text-slate-900">
Toon energiebudgetpunten
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Laat geplande en resterende punten zichtbaar zien in de interface.
</span>
</span>
</label>
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={morningReminderEnabled}
onChange={(event) => setMorningReminderEnabled(event.target.checked)}
/>
<span className="flex-1">
<span className="block text-sm font-semibold text-slate-900">
Zet een lichte ochtendreminder aan
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Handig als je later een korte check-in wilt doen zonder extra druk.
</span>
{morningReminderEnabled ? (
<input
className="mt-3 w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-base outline-none transition focus:border-emerald-600"
type="time"
value={morningReminderTime}
onChange={(event) => setMorningReminderTime(event.target.value)}
/>
) : null}
</span>
</label>
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={reflectionReminderEnabled}
onChange={(event) => setReflectionReminderEnabled(event.target.checked)}
/>
<span>
<span className="block text-sm font-semibold text-slate-900">
Sta lichte reflectieprompts toe
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Optionele terugblikprompts kunnen later helpen om rustiger patronen te zien.
</span>
</span>
</label>
</div>
) : null}
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-black/10 pt-6">
<button
type="button"
onClick={goToPreviousStep}
disabled={isFirstStep}
className="rounded-full border border-black/10 bg-white px-5 py-3 text-sm font-medium text-slate-700 transition hover:-translate-y-0.5 hover:text-slate-950 disabled:cursor-not-allowed disabled:opacity-45"
>
Vorige
</button>
{isLastStep ? (
<button
key="complete-onboarding"
type="submit"
className="rounded-full bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
Rond onboarding af
</button>
) : (
<button
key={`next-step-${currentStep}`}
type="button"
onClick={goToNextStep}
className="rounded-full bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
Ga verder
</button>
)}
</div>
</form>
</section>
</div>
);
}

View file

@ -0,0 +1,217 @@
"use client";
import { useState } from "react";
import { saveSettingsAction } from "@/app/settings/actions";
import { ONBOARDING_TIMEZONE_OPTIONS } from "@/lib/onboarding/options";
import type { ProfileBundle } from "@/lib/profile/types";
type SettingsFormProps = {
profileBundle: ProfileBundle;
};
const LOCALE_OPTIONS = [
{
value: "nl-NL",
label: "Nederlands",
},
] as const;
export function SettingsForm({ profileBundle }: SettingsFormProps) {
const [locale, setLocale] = useState(profileBundle.profile.locale);
const [timezone, setTimezone] = useState(profileBundle.profile.timezone);
const [showEnergyPoints, setShowEnergyPoints] = useState(
profileBundle.settings.showEnergyPoints,
);
const [morningReminderEnabled, setMorningReminderEnabled] = useState(
profileBundle.settings.morningReminderEnabled,
);
const [morningReminderTime, setMorningReminderTime] = useState(
profileBundle.settings.morningReminderTime ?? "08:30",
);
const [reflectionReminderEnabled, setReflectionReminderEnabled] = useState(
profileBundle.settings.reflectionReminderEnabled,
);
return (
<form action={saveSettingsAction} className="space-y-6">
<input type="hidden" name="locale" value={locale} />
<input type="hidden" name="timezone" value={timezone} />
<input
type="hidden"
name="showEnergyPoints"
value={showEnergyPoints ? "true" : "false"}
/>
<input
type="hidden"
name="morningReminderEnabled"
value={morningReminderEnabled ? "true" : "false"}
/>
<input type="hidden" name="morningReminderTime" value={morningReminderTime} />
<input
type="hidden"
name="reflectionReminderEnabled"
value={reflectionReminderEnabled ? "true" : "false"}
/>
<section className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Account
</p>
<h2 className="mt-3 font-[family-name:var(--font-display)] text-3xl text-slate-900">
Basisinstellingen voor jouw account
</h2>
<p className="mt-3 max-w-2xl text-sm leading-7 text-slate-700">
Je past hier alleen je wellness-first voorkeuren aan. Er zijn in release 1
geen medische velden, deelinstellingen of zorgverlenerrollen.
</p>
</section>
<section className="grid gap-5 lg:grid-cols-2">
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Taal en tijd
</p>
<div className="mt-5 space-y-5">
<label className="block text-sm font-medium text-slate-800">
Taal
<select
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
value={locale}
onChange={(event) => setLocale(event.target.value)}
>
{LOCALE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<div className="rounded-[1.5rem] border border-sky-200 bg-sky-50 px-4 py-4 text-sm leading-7 text-sky-900">
Release 1 draait bewust volledig in het Nederlands. De taalinstelling
blijft al wel aanwezig in het accountmodel.
</div>
<label className="block text-sm font-medium text-slate-800">
Timezone
<select
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
value={timezone}
onChange={(event) => setTimezone(event.target.value)}
>
{ONBOARDING_TIMEZONE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
</article>
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Interface
</p>
<div className="mt-5 space-y-4">
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={showEnergyPoints}
onChange={(event) => setShowEnergyPoints(event.target.checked)}
/>
<span>
<span className="block text-sm font-semibold text-slate-900">
Toon energiebudgetpunten
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Laat budgetpunten zichtbaar zien in het dashboard en latere dagflows.
</span>
</span>
</label>
</div>
</article>
</section>
<section className="grid gap-5 lg:grid-cols-2">
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Reminders
</p>
<div className="mt-5 space-y-4">
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={morningReminderEnabled}
onChange={(event) => setMorningReminderEnabled(event.target.checked)}
/>
<span className="flex-1">
<span className="block text-sm font-semibold text-slate-900">
Ochtendreminder
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Zet een lichte reminder aan voor een rustige start van je check-in.
</span>
{morningReminderEnabled ? (
<input
className="mt-3 w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-base outline-none transition focus:border-emerald-600"
type="time"
value={morningReminderTime}
onChange={(event) => setMorningReminderTime(event.target.value)}
/>
) : null}
</span>
</label>
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={reflectionReminderEnabled}
onChange={(event) => setReflectionReminderEnabled(event.target.checked)}
/>
<span>
<span className="block text-sm font-semibold text-slate-900">
Reflectieprompts toestaan
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Maak alvast de opt-in klaar voor lichte terugblikprompts in een latere story.
</span>
</span>
</label>
</div>
</article>
<article className="rounded-[1.75rem] border border-emerald-950/10 bg-emerald-950 p-6 text-emerald-50 shadow-[0_12px_40px_rgba(6,78,59,0.18)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-200/80">
Bewuste grenzen
</p>
<ul className="mt-4 space-y-3 text-sm leading-7 text-emerald-50/90">
<li>Geen medische drempels, diagnoses of behandelinstellingen.</li>
<li>Geen delen met zorgverleners of naasten in release 1.</li>
<li>Alle instellingen blijven gekoppeld aan alleen jouw account.</li>
</ul>
</article>
</section>
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-black/10 pt-6">
<p className="text-sm leading-7 text-slate-600">
Wijzigingen zijn direct van toepassing op jouw account en volgende sessies.
</p>
<button
type="submit"
className="rounded-full bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
Instellingen opslaan
</button>
</div>
</form>
);
}

58
components/ui/button.tsx Normal file
View file

@ -0,0 +1,58 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button"
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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
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",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
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",
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",
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",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }