feat(theme): add light/dark mode toggle (#243)

Introduce a light/dark MODE axis orthogonal to the existing accent
themes. Neutral surface tokens now live in html[data-mode="light|dark"]
blocks; accent tokens (--primary*, --ring, --chart-1, --sidebar-primary*)
stay in html[data-theme] blocks, so any accent works in either mode.

- globals.css: split neutrals (mode) from accent (theme); add light
  palette; dark values unchanged (dark stays the default).
- lib/themes.ts: Mode type, DEFAULT_MODE=dark, MODE_STORAGE_KEY, isMode.
- use-theme.tsx: provider owns mode/setMode/toggleMode + cross-tab sync.
- layout.tsx: boot script applies persisted mode before first paint
  (no flash); colorScheme "dark light".
- mode-toggle.tsx (header sun/moon) + themed-toaster.tsx (mode-aware).
- appearance-panel.tsx: light/dark control + updated copy.
- Tokenize hardcoded neutral colors (slate/white/gray) -> semantic
  tokens across the app so every surface flips with mode. Flow canvas
  and message-bubble timestamps made mode-aware too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arnas Donauskas 2026-06-16 17:26:29 +03:00
parent e1ab7b2b58
commit 4aa329b527
No known key found for this signature in database
84 changed files with 1581 additions and 1391 deletions

View File

@ -43,18 +43,18 @@ export default function ForgotPasswordPage() {
if (success) { if (success) {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4"> <div className="flex min-h-screen items-center justify-center bg-background px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center"> <CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10"> <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<CheckCircle className="h-6 w-6 text-primary" /> <CheckCircle className="h-6 w-6 text-primary" />
</div> </div>
<CardTitle className="text-xl text-white"> <CardTitle className="text-xl text-foreground">
Check your email Check your email
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
We&apos;ve sent a password reset link to{" "} We&apos;ve sent a password reset link to{" "}
<span className="text-white">{email}</span>. Please check your <span className="text-foreground">{email}</span>. Please check your
inbox. inbox.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
@ -62,7 +62,7 @@ export default function ForgotPasswordPage() {
<Link href="/login"> <Link href="/login">
<Button <Button
variant="outline" variant="outline"
className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white" className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
> >
Back to sign in Back to sign in
</Button> </Button>
@ -74,14 +74,14 @@ export default function ForgotPasswordPage() {
} }
return ( return (
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4"> <div className="flex min-h-screen items-center justify-center bg-background px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center"> <CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10"> <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<MessageSquare className="h-6 w-6 text-primary" /> <MessageSquare className="h-6 w-6 text-primary" />
</div> </div>
<CardTitle className="text-xl text-white">Reset password</CardTitle> <CardTitle className="text-xl text-foreground">Reset password</CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
Enter your email and we&apos;ll send you a reset link Enter your email and we&apos;ll send you a reset link
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
@ -94,7 +94,7 @@ export default function ForgotPasswordPage() {
)} )}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="email" className="text-slate-300"> <Label htmlFor="email" className="text-muted-foreground">
Email Email
</Label> </Label>
<Input <Input
@ -104,7 +104,7 @@ export default function ForgotPasswordPage() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20" className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
/> />
</div> </div>
@ -119,7 +119,7 @@ export default function ForgotPasswordPage() {
<Link <Link
href="/login" href="/login"
className="mt-6 flex items-center justify-center gap-2 text-sm text-slate-400 hover:text-slate-300" className="mt-6 flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-muted-foreground"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
Back to sign in Back to sign in

View File

@ -67,8 +67,8 @@ function LoginPageInner() {
}; };
return ( return (
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4"> <div className="flex min-h-screen items-center justify-center bg-background px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center"> <CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10"> <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
{inviteToken ? ( {inviteToken ? (
@ -77,10 +77,10 @@ function LoginPageInner() {
<MessageSquare className="h-6 w-6 text-primary" /> <MessageSquare className="h-6 w-6 text-primary" />
)} )}
</div> </div>
<CardTitle className="text-xl text-white"> <CardTitle className="text-xl text-foreground">
{inviteToken ? "Sign in to accept" : "Welcome back"} {inviteToken ? "Sign in to accept" : "Welcome back"}
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
{inviteToken {inviteToken
? "Sign in and we'll take you to the invitation." ? "Sign in and we'll take you to the invitation."
: "Sign in to your account"} : "Sign in to your account"}
@ -95,7 +95,7 @@ function LoginPageInner() {
)} )}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="email" className="text-slate-300"> <Label htmlFor="email" className="text-muted-foreground">
Email Email
</Label> </Label>
<Input <Input
@ -105,13 +105,13 @@ function LoginPageInner() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20" className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
/> />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label htmlFor="password" className="text-slate-300"> <Label htmlFor="password" className="text-muted-foreground">
Password Password
</Label> </Label>
<Link <Link
@ -128,7 +128,7 @@ function LoginPageInner() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20" className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
/> />
</div> </div>
@ -141,7 +141,7 @@ function LoginPageInner() {
</Button> </Button>
</form> </form>
<p className="mt-6 text-center text-sm text-slate-400"> <p className="mt-6 text-center text-sm text-muted-foreground">
Don&apos;t have an account?{" "} Don&apos;t have an account?{" "}
<Link <Link
href={ href={

View File

@ -91,18 +91,18 @@ function SignupPageInner() {
if (success) { if (success) {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4"> <div className="flex min-h-screen items-center justify-center bg-background px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center"> <CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10"> <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<CheckCircle className="h-6 w-6 text-primary" /> <CheckCircle className="h-6 w-6 text-primary" />
</div> </div>
<CardTitle className="text-xl text-white"> <CardTitle className="text-xl text-foreground">
Check your email Check your email
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
We&apos;ve sent a confirmation link to{" "} We&apos;ve sent a confirmation link to{" "}
<span className="text-white">{email}</span>. Please check your <span className="text-foreground">{email}</span>. Please check your
inbox and click the link to verify your account. inbox and click the link to verify your account.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
@ -116,7 +116,7 @@ function SignupPageInner() {
> >
<Button <Button
variant="outline" variant="outline"
className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white" className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
> >
Back to sign in Back to sign in
</Button> </Button>
@ -128,8 +128,8 @@ function SignupPageInner() {
} }
return ( return (
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4"> <div className="flex min-h-screen items-center justify-center bg-background px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center"> <CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10"> <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
{inviteToken ? ( {inviteToken ? (
@ -138,10 +138,10 @@ function SignupPageInner() {
<MessageSquare className="h-6 w-6 text-primary" /> <MessageSquare className="h-6 w-6 text-primary" />
)} )}
</div> </div>
<CardTitle className="text-xl text-white"> <CardTitle className="text-xl text-foreground">
{inviteToken ? "Create account & join" : "Create account"} {inviteToken ? "Create account & join" : "Create account"}
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
{inviteToken {inviteToken
? "Verify your email, then accept the invitation to join your team." ? "Verify your email, then accept the invitation to join your team."
: "Get started with CRM Template for WhatsApp"} : "Get started with CRM Template for WhatsApp"}
@ -156,7 +156,7 @@ function SignupPageInner() {
)} )}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="fullName" className="text-slate-300"> <Label htmlFor="fullName" className="text-muted-foreground">
Full name Full name
</Label> </Label>
<Input <Input
@ -166,12 +166,12 @@ function SignupPageInner() {
value={fullName} value={fullName}
onChange={(e) => setFullName(e.target.value)} onChange={(e) => setFullName(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20" className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
/> />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="email" className="text-slate-300"> <Label htmlFor="email" className="text-muted-foreground">
Email Email
</Label> </Label>
<Input <Input
@ -181,12 +181,12 @@ function SignupPageInner() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20" className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
/> />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="password" className="text-slate-300"> <Label htmlFor="password" className="text-muted-foreground">
Password Password
</Label> </Label>
<Input <Input
@ -196,12 +196,12 @@ function SignupPageInner() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20" className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
/> />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="confirmPassword" className="text-slate-300"> <Label htmlFor="confirmPassword" className="text-muted-foreground">
Confirm password Confirm password
</Label> </Label>
<Input <Input
@ -211,7 +211,7 @@ function SignupPageInner() {
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
required required
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20" className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
/> />
</div> </div>
@ -224,7 +224,7 @@ function SignupPageInner() {
</Button> </Button>
</form> </form>
<p className="mt-6 text-center text-sm text-slate-400"> <p className="mt-6 text-center text-sm text-muted-foreground">
Already have an account?{" "} Already have an account?{" "}
<Link <Link
href={ href={

View File

@ -87,21 +87,21 @@ export default function AutomationLogsPage({
<button <button
type="button" type="button"
onClick={() => router.push("/automations")} onClick={() => router.push("/automations")}
className="flex h-8 w-8 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-slate-800 hover:text-white" className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
aria-label="Back" aria-label="Back"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
</button> </button>
<div> <div>
<h1 className="text-2xl font-bold text-white">{automation.name}</h1> <h1 className="text-2xl font-bold text-foreground">{automation.name}</h1>
<p className="mt-0.5 text-sm text-slate-400">Execution logs</p> <p className="mt-0.5 text-sm text-muted-foreground">Execution logs</p>
</div> </div>
</div> </div>
{logs.length === 0 ? ( {logs.length === 0 ? (
<div className="flex h-48 flex-col items-center justify-center rounded-xl border border-dashed border-slate-800 bg-slate-900/40"> <div className="flex h-48 flex-col items-center justify-center rounded-xl border border-dashed border-border bg-card/40">
<p className="text-sm text-white">No executions yet</p> <p className="text-sm text-foreground">No executions yet</p>
<p className="mt-1 text-xs text-slate-400"> <p className="mt-1 text-xs text-muted-foreground">
Trigger this automation to see runs here. Trigger this automation to see runs here.
</p> </p>
</div> </div>
@ -112,7 +112,7 @@ export default function AutomationLogsPage({
return ( return (
<li <li
key={log.id} key={log.id}
className="rounded-xl border border-slate-800 bg-slate-900" className="rounded-xl border border-border bg-card"
> >
<button <button
type="button" type="button"
@ -120,26 +120,26 @@ export default function AutomationLogsPage({
className="flex w-full items-center gap-3 px-4 py-3 text-left" className="flex w-full items-center gap-3 px-4 py-3 text-left"
> >
{isOpen ? ( {isOpen ? (
<ChevronDown className="h-4 w-4 text-slate-500" /> <ChevronDown className="h-4 w-4 text-muted-foreground" />
) : ( ) : (
<ChevronRight className="h-4 w-4 text-slate-500" /> <ChevronRight className="h-4 w-4 text-muted-foreground" />
)} )}
<StatusBadge status={log.status} /> <StatusBadge status={log.status} />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-white"> <div className="truncate text-sm font-medium text-foreground">
{log.contact?.name ?? log.contact?.phone ?? "Unknown contact"} {log.contact?.name ?? log.contact?.phone ?? "Unknown contact"}
</div> </div>
<div className="truncate text-xs text-slate-500"> <div className="truncate text-xs text-muted-foreground">
{log.trigger_event} · {log.steps_executed?.length ?? 0} step {log.trigger_event} · {log.steps_executed?.length ?? 0} step
{log.steps_executed?.length === 1 ? "" : "s"} {log.steps_executed?.length === 1 ? "" : "s"}
</div> </div>
</div> </div>
<div className="text-xs text-slate-500"> <div className="text-xs text-muted-foreground">
{formatRelative(log.created_at)} {formatRelative(log.created_at)}
</div> </div>
</button> </button>
{isOpen && ( {isOpen && (
<div className="border-t border-slate-800 px-4 py-3"> <div className="border-t border-border px-4 py-3">
{log.error_message && ( {log.error_message && (
<p className="mb-3 rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300"> <p className="mb-3 rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
{log.error_message} {log.error_message}
@ -150,7 +150,7 @@ export default function AutomationLogsPage({
<StepRow key={i} result={r} /> <StepRow key={i} result={r} />
))} ))}
{(log.steps_executed ?? []).length === 0 && ( {(log.steps_executed ?? []).length === 0 && (
<li className="text-xs text-slate-500">No steps recorded.</li> <li className="text-xs text-muted-foreground">No steps recorded.</li>
)} )}
</ul> </ul>
</div> </div>
@ -196,9 +196,9 @@ function StepRow({ result }: { result: AutomationLogStepResult }) {
> >
{ok ? <Check className="h-3 w-3" /> : <X className="h-3 w-3" />} {ok ? <Check className="h-3 w-3" /> : <X className="h-3 w-3" />}
</span> </span>
<span className="text-slate-300">{result.step_type}</span> <span className="text-muted-foreground">{result.step_type}</span>
{result.detail && ( {result.detail && (
<span className="truncate text-slate-500">— {result.detail}</span> <span className="truncate text-muted-foreground">— {result.detail}</span>
)} )}
</li> </li>
) )

View File

@ -160,8 +160,8 @@ export default function AutomationsPage() {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-white">Automations</h1> <h1 className="text-2xl font-bold text-foreground">Automations</h1>
<p className="mt-1 text-sm text-slate-400"> <p className="mt-1 text-sm text-muted-foreground">
Build workflows that react to WhatsApp® events automatically. Build workflows that react to WhatsApp® events automatically.
</p> </p>
</div> </div>
@ -178,7 +178,7 @@ export default function AutomationsPage() {
{showTemplates && ( {showTemplates && (
<section> <section>
<h2 className="mb-3 text-sm font-semibold text-slate-300">Quick-start templates</h2> <h2 className="mb-3 text-sm font-semibold text-muted-foreground">Quick-start templates</h2>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4"> <div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
{TEMPLATE_ORDER.map((slug) => { {TEMPLATE_ORDER.map((slug) => {
const t = AUTOMATION_TEMPLATES[slug] const t = AUTOMATION_TEMPLATES[slug]
@ -187,13 +187,13 @@ export default function AutomationsPage() {
<button <button
key={slug} key={slug}
onClick={() => startFromTemplate(slug)} onClick={() => startFromTemplate(slug)}
className="group flex flex-col items-start rounded-xl border border-slate-800 bg-slate-900 p-4 text-left transition-colors hover:border-primary/50 hover:bg-slate-900/80" className="group flex flex-col items-start rounded-xl border border-border bg-card p-4 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
> >
<div className="mb-3 flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary group-hover:bg-primary/15"> <div className="mb-3 flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary group-hover:bg-primary/15">
<Icon className="h-5 w-5" /> <Icon className="h-5 w-5" />
</div> </div>
<div className="text-sm font-semibold text-white">{t.name}</div> <div className="text-sm font-semibold text-foreground">{t.name}</div>
<p className="mt-1 text-xs text-slate-400">{t.description}</p> <p className="mt-1 text-xs text-muted-foreground">{t.description}</p>
</button> </button>
) )
})} })}
@ -202,12 +202,12 @@ export default function AutomationsPage() {
)} )}
{automations.length === 0 ? ( {automations.length === 0 ? (
<div className="flex h-48 flex-col items-center justify-center rounded-xl border border-dashed border-slate-800 bg-slate-900/40"> <div className="flex h-48 flex-col items-center justify-center rounded-xl border border-dashed border-border bg-card/40">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10"> <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<Zap className="h-6 w-6 text-primary" /> <Zap className="h-6 w-6 text-primary" />
</div> </div>
<p className="mt-3 text-sm font-medium text-white">No automations yet</p> <p className="mt-3 text-sm font-medium text-foreground">No automations yet</p>
<p className="mt-1 text-xs text-slate-400"> <p className="mt-1 text-xs text-muted-foreground">
Pick a template above or create one from scratch. Pick a template above or create one from scratch.
</p> </p>
</div> </div>
@ -233,7 +233,7 @@ export default function AutomationsPage() {
<DialogTitle>Delete automation</DialogTitle> <DialogTitle>Delete automation</DialogTitle>
<DialogDescription> <DialogDescription>
This permanently removes{" "} This permanently removes{" "}
<span className="text-white">{pendingDelete?.name}</span> and its execution <span className="text-foreground">{pendingDelete?.name}</span> and its execution
history. This cannot be undone. history. This cannot be undone.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@ -277,7 +277,7 @@ function AutomationCard({
}) { }) {
const meta = triggerMeta(automation.trigger_type) const meta = triggerMeta(automation.trigger_type)
return ( return (
<li className="rounded-xl border border-slate-800 bg-slate-900 transition-colors hover:border-slate-700"> <li className="rounded-xl border border-border bg-card transition-colors hover:border-border">
<div className="flex items-center gap-4 p-4"> <div className="flex items-center gap-4 p-4">
<div <div
className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-primary/10" className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-primary/10"
@ -292,7 +292,7 @@ function AutomationCard({
className="min-w-0 flex-1 text-left" className="min-w-0 flex-1 text-left"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="truncate text-sm font-semibold text-white"> <span className="truncate text-sm font-semibold text-foreground">
{automation.name} {automation.name}
</span> </span>
{automation.is_active && ( {automation.is_active && (
@ -303,9 +303,9 @@ function AutomationCard({
)} )}
</div> </div>
{automation.description && ( {automation.description && (
<p className="mt-0.5 truncate text-xs text-slate-400">{automation.description}</p> <p className="mt-0.5 truncate text-xs text-muted-foreground">{automation.description}</p>
)} )}
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-slate-500"> <div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span <span
className={cn( className={cn(
"inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium", "inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium",
@ -332,7 +332,7 @@ function AutomationCard({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
aria-label="Open menu" aria-label="Open menu"
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-slate-800 hover:text-white data-[popup-open]:bg-slate-800" className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[popup-open]:bg-muted"
> >
<MoreVertical className="h-4 w-4" /> <MoreVertical className="h-4 w-4" />
</DropdownMenuTrigger> </DropdownMenuTrigger>

View File

@ -50,15 +50,15 @@ interface StatCardProps {
function StatCard({ label, value, total, icon, color }: StatCardProps) { function StatCard({ label, value, total, icon, color }: StatCardProps) {
const pct = total > 0 ? Math.round((value / total) * 100) : 0; const pct = total > 0 ? Math.round((value / total) * 100) : 0;
return ( return (
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4"> <div className="rounded-xl border border-border bg-card p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className={`flex h-8 w-8 items-center justify-center rounded-lg ${color}`}> <div className={`flex h-8 w-8 items-center justify-center rounded-lg ${color}`}>
{icon} {icon}
</div> </div>
<span className="text-xs text-slate-500">{pct}%</span> <span className="text-xs text-muted-foreground">{pct}%</span>
</div> </div>
<p className="mt-3 text-2xl font-bold text-white">{value.toLocaleString()}</p> <p className="mt-3 text-2xl font-bold text-foreground">{value.toLocaleString()}</p>
<p className="text-xs text-slate-400">{label}</p> <p className="text-xs text-muted-foreground">{label}</p>
</div> </div>
); );
} }
@ -77,8 +77,8 @@ interface FunnelStep {
function FunnelChart({ steps }: { steps: FunnelStep[] }) { function FunnelChart({ steps }: { steps: FunnelStep[] }) {
const max = Math.max(...steps.map((s) => s.value), 1); const max = Math.max(...steps.map((s) => s.value), 1);
return ( return (
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4"> <div className="rounded-xl border border-border bg-card p-4">
<h3 className="mb-4 text-sm font-medium text-white">Funnel</h3> <h3 className="mb-4 text-sm font-medium text-foreground">Funnel</h3>
<div className="space-y-2"> <div className="space-y-2">
{steps.map((step) => { {steps.map((step) => {
const pctOfMax = Math.max(5, Math.round((step.value / max) * 100)); const pctOfMax = Math.max(5, Math.round((step.value / max) * 100));
@ -88,17 +88,17 @@ function FunnelChart({ steps }: { steps: FunnelStep[] }) {
: 0; : 0;
return ( return (
<div key={step.label} className="flex items-center gap-3"> <div key={step.label} className="flex items-center gap-3">
<span className="w-20 shrink-0 text-xs text-slate-400"> <span className="w-20 shrink-0 text-xs text-muted-foreground">
{step.label} {step.label}
</span> </span>
<div className="relative h-7 flex-1 rounded-full bg-slate-800"> <div className="relative h-7 flex-1 rounded-full bg-muted">
<div <div
className={`h-7 rounded-full ${step.color} transition-[width] duration-500`} className={`h-7 rounded-full ${step.color} transition-[width] duration-500`}
style={{ width: `${pctOfMax}%` }} style={{ width: `${pctOfMax}%` }}
/> />
<span className="absolute inset-0 flex items-center px-3 text-xs font-medium text-white"> <span className="absolute inset-0 flex items-center px-3 text-xs font-medium text-foreground">
{step.value.toLocaleString()} {step.value.toLocaleString()}
<span className="ml-2 text-slate-300/80"> <span className="ml-2 text-muted-foreground/80">
({pctOfSent}%) ({pctOfSent}%)
</span> </span>
</span> </span>
@ -280,20 +280,20 @@ export default function BroadcastDetailPage() {
variant="outline" variant="outline"
size="icon" size="icon"
onClick={() => router.push('/broadcasts')} onClick={() => router.push('/broadcasts')}
className="border-slate-700" className="border-border"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
</Button> </Button>
<div> <div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-white">{broadcast.name}</h1> <h1 className="text-2xl font-bold text-foreground">{broadcast.name}</h1>
<span <span
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${status.classes}`} className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${status.classes}`}
> >
{status.label} {status.label}
</span> </span>
</div> </div>
<div className="mt-1 flex items-center gap-3 text-sm text-slate-400"> <div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
<span>Template: {broadcast.template_name}</span> <span>Template: {broadcast.template_name}</span>
<span>-</span> <span>-</span>
<span> <span>
@ -315,7 +315,7 @@ export default function BroadcastDetailPage() {
size="sm" size="sm"
onClick={() => setConfirmDelete(false)} onClick={() => setConfirmDelete(false)}
disabled={deleting} disabled={deleting}
className="h-7 border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800" className="h-7 border-border bg-transparent text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>
@ -354,7 +354,7 @@ export default function BroadcastDetailPage() {
value={broadcast.total_recipients} value={broadcast.total_recipients}
total={broadcast.total_recipients} total={broadcast.total_recipients}
icon={<Users className="h-4 w-4" />} icon={<Users className="h-4 w-4" />}
color="bg-slate-800 text-slate-300" color="bg-muted text-muted-foreground"
/> />
<StatCard <StatCard
label="Sent" label="Sent"
@ -396,9 +396,9 @@ export default function BroadcastDetailPage() {
<FunnelChart steps={funnelSteps} /> <FunnelChart steps={funnelSteps} />
{/* Recipients Table */} {/* Recipients Table */}
<div className="rounded-xl border border-slate-800 bg-slate-900"> <div className="rounded-xl border border-border bg-card">
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-800 px-4 py-3"> <div className="flex flex-wrap items-center justify-between gap-2 border-b border-border px-4 py-3">
<h2 className="text-sm font-medium text-white"> <h2 className="text-sm font-medium text-foreground">
Recipients ({filteredRecipients.length} Recipients ({filteredRecipients.length}
{statusFilter !== 'all' ? ` of ${recipients.length}` : ''}) {statusFilter !== 'all' ? ` of ${recipients.length}` : ''})
</h2> </h2>
@ -409,7 +409,7 @@ export default function BroadcastDetailPage() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
/> />
} }
> >
@ -419,11 +419,11 @@ export default function BroadcastDetailPage() {
: getRecipientStatus(statusFilter).label} : getRecipientStatus(statusFilter).label}
<ChevronDown className="h-3 w-3" /> <ChevronDown className="h-3 w-3" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="border-slate-700 bg-slate-900"> <DropdownMenuContent className="border-border bg-popover">
<DropdownMenuItem <DropdownMenuItem
onClick={() => setStatusFilter('all')} onClick={() => setStatusFilter('all')}
className={ className={
statusFilter === 'all' ? 'text-primary' : 'text-slate-300' statusFilter === 'all' ? 'text-primary' : 'text-popover-foreground'
} }
> >
All statuses All statuses
@ -435,7 +435,7 @@ export default function BroadcastDetailPage() {
className={ className={
statusFilter === s statusFilter === s
? 'text-primary' ? 'text-primary'
: 'text-slate-300' : 'text-popover-foreground'
} }
> >
{getRecipientStatus(s).label} {getRecipientStatus(s).label}
@ -449,7 +449,7 @@ export default function BroadcastDetailPage() {
size="sm" size="sm"
onClick={handleExport} onClick={handleExport}
disabled={recipients.length === 0} disabled={recipients.length === 0}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
<Download className="h-3.5 w-3.5" /> <Download className="h-3.5 w-3.5" />
Export CSV Export CSV
@ -459,7 +459,7 @@ export default function BroadcastDetailPage() {
{filteredRecipients.length === 0 ? ( {filteredRecipients.length === 0 ? (
<div className="flex h-32 items-center justify-center"> <div className="flex h-32 items-center justify-center">
<p className="text-sm text-slate-400"> <p className="text-sm text-muted-foreground">
{recipients.length === 0 {recipients.length === 0
? 'No recipients found.' ? 'No recipients found.'
: 'No recipients match this filter.'} : 'No recipients match this filter.'}
@ -469,25 +469,25 @@ export default function BroadcastDetailPage() {
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent"> <TableRow className="border-border hover:bg-transparent">
<TableHead className="text-slate-400">Contact</TableHead> <TableHead className="text-muted-foreground">Contact</TableHead>
<TableHead className="text-slate-400">Phone</TableHead> <TableHead className="text-muted-foreground">Phone</TableHead>
<TableHead className="text-slate-400">Status</TableHead> <TableHead className="text-muted-foreground">Status</TableHead>
<TableHead className="text-slate-400">Sent</TableHead> <TableHead className="text-muted-foreground">Sent</TableHead>
<TableHead className="text-slate-400">Delivered</TableHead> <TableHead className="text-muted-foreground">Delivered</TableHead>
<TableHead className="text-slate-400">Read</TableHead> <TableHead className="text-muted-foreground">Read</TableHead>
<TableHead className="text-slate-400">Error</TableHead> <TableHead className="text-muted-foreground">Error</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredRecipients.map((recipient) => { {filteredRecipients.map((recipient) => {
const rStatus = getRecipientStatus(recipient.status); const rStatus = getRecipientStatus(recipient.status);
return ( return (
<TableRow key={recipient.id} className="border-slate-800"> <TableRow key={recipient.id} className="border-border">
<TableCell className="font-medium text-white"> <TableCell className="font-medium text-foreground">
{recipient.contact?.name ?? 'Unknown'} {recipient.contact?.name ?? 'Unknown'}
</TableCell> </TableCell>
<TableCell className="text-slate-300"> <TableCell className="text-muted-foreground">
{recipient.contact?.phone ?? '-'} {recipient.contact?.phone ?? '-'}
</TableCell> </TableCell>
<TableCell> <TableCell>
@ -497,17 +497,17 @@ export default function BroadcastDetailPage() {
{rStatus.label} {rStatus.label}
</span> </span>
</TableCell> </TableCell>
<TableCell className="text-slate-400"> <TableCell className="text-muted-foreground">
{recipient.sent_at {recipient.sent_at
? new Date(recipient.sent_at).toLocaleString() ? new Date(recipient.sent_at).toLocaleString()
: '-'} : '-'}
</TableCell> </TableCell>
<TableCell className="text-slate-400"> <TableCell className="text-muted-foreground">
{recipient.delivered_at {recipient.delivered_at
? new Date(recipient.delivered_at).toLocaleString() ? new Date(recipient.delivered_at).toLocaleString()
: '-'} : '-'}
</TableCell> </TableCell>
<TableCell className="text-slate-400"> <TableCell className="text-muted-foreground">
{recipient.read_at {recipient.read_at
? new Date(recipient.read_at).toLocaleString() ? new Date(recipient.read_at).toLocaleString()
: '-'} : '-'}

View File

@ -129,8 +129,8 @@ export default function NewBroadcastPage() {
<div className="mx-auto max-w-3xl space-y-8"> <div className="mx-auto max-w-3xl space-y-8">
{/* Header */} {/* Header */}
<div> <div>
<h1 className="text-2xl font-bold text-white">New Broadcast</h1> <h1 className="text-2xl font-bold text-foreground">New Broadcast</h1>
<p className="mt-1 text-sm text-slate-400"> <p className="mt-1 text-sm text-muted-foreground">
Create and send a broadcast message to your contacts. Create and send a broadcast message to your contacts.
</p> </p>
</div> </div>
@ -150,14 +150,14 @@ export default function NewBroadcastPage() {
? 'bg-primary text-primary-foreground' ? 'bg-primary text-primary-foreground'
: isActive : isActive
? 'border-2 border-primary bg-primary/10 text-primary' ? 'border-2 border-primary bg-primary/10 text-primary'
: 'border border-slate-700 bg-slate-800 text-slate-500' : 'border border-border bg-muted text-muted-foreground'
}`} }`}
> >
{isCompleted ? <Check className="h-4 w-4" /> : index + 1} {isCompleted ? <Check className="h-4 w-4" /> : index + 1}
</div> </div>
<span <span
className={`hidden text-sm font-medium sm:block ${ className={`hidden text-sm font-medium sm:block ${
isActive ? 'text-white' : isCompleted ? 'text-primary' : 'text-slate-500' isActive ? 'text-foreground' : isCompleted ? 'text-primary' : 'text-muted-foreground'
}`} }`}
> >
{step.label} {step.label}
@ -166,7 +166,7 @@ export default function NewBroadcastPage() {
{index < steps.length - 1 && ( {index < steps.length - 1 && (
<div <div
className={`mx-3 h-px flex-1 ${ className={`mx-3 h-px flex-1 ${
index < currentStep ? 'bg-primary' : 'bg-slate-800' index < currentStep ? 'bg-primary' : 'bg-muted'
}`} }`}
/> />
)} )}

View File

@ -43,10 +43,10 @@ function RateCell({
const pct = percent(value, total); const pct = percent(value, total);
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="w-10 text-right text-xs tabular-nums text-slate-300"> <span className="w-10 text-right text-xs tabular-nums text-muted-foreground">
{pct}% {pct}%
</span> </span>
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-slate-800"> <div className="h-1.5 w-20 overflow-hidden rounded-full bg-muted">
<div <div
className={`h-1.5 rounded-full ${color}`} className={`h-1.5 rounded-full ${color}`}
style={{ width: `${pct}%` }} style={{ width: `${pct}%` }}
@ -155,7 +155,7 @@ export default function BroadcastsPage() {
<div <div
role="progressbar" role="progressbar"
aria-label="Broadcast in progress" aria-label="Broadcast in progress"
className="broadcast-indeterminate fixed inset-x-0 top-0 z-40 h-0.5 overflow-hidden bg-slate-800" className="broadcast-indeterminate fixed inset-x-0 top-0 z-40 h-0.5 overflow-hidden bg-muted"
> >
<div className="broadcast-indeterminate-bar h-0.5 bg-primary" /> <div className="broadcast-indeterminate-bar h-0.5 bg-primary" />
<style jsx>{` <style jsx>{`
@ -179,8 +179,8 @@ export default function BroadcastsPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-white">Broadcasts</h1> <h1 className="text-2xl font-bold text-foreground">Broadcasts</h1>
<p className="mt-1 text-sm text-slate-400"> <p className="mt-1 text-sm text-muted-foreground">
Send bulk messages to your contacts using approved templates. Send bulk messages to your contacts using approved templates.
</p> </p>
</div> </div>
@ -196,10 +196,10 @@ export default function BroadcastsPage() {
</div> </div>
{broadcasts.length === 0 ? ( {broadcasts.length === 0 ? (
<div className="flex h-64 flex-col items-center justify-center rounded-xl border border-slate-800 bg-slate-900"> <div className="flex h-64 flex-col items-center justify-center rounded-xl border border-border bg-card">
<Radio className="mb-3 h-10 w-10 text-slate-600" /> <Radio className="mb-3 h-10 w-10 text-muted-foreground" />
<p className="text-sm font-medium text-white">No broadcasts yet</p> <p className="text-sm font-medium text-foreground">No broadcasts yet</p>
<p className="mt-1 text-xs text-slate-400"> <p className="mt-1 text-xs text-muted-foreground">
Create your first broadcast to reach your contacts at scale. Create your first broadcast to reach your contacts at scale.
</p> </p>
<GatedButton <GatedButton
@ -213,19 +213,19 @@ export default function BroadcastsPage() {
</GatedButton> </GatedButton>
</div> </div>
) : ( ) : (
<div className="overflow-x-auto rounded-xl border border-slate-800 bg-slate-900"> <div className="overflow-x-auto rounded-xl border border-border bg-card">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent"> <TableRow className="border-border hover:bg-transparent">
<TableHead className="text-slate-400">Name</TableHead> <TableHead className="text-muted-foreground">Name</TableHead>
<TableHead className="hidden text-slate-400 md:table-cell">Template</TableHead> <TableHead className="hidden text-muted-foreground md:table-cell">Template</TableHead>
<TableHead className="hidden text-right text-slate-400 sm:table-cell"> <TableHead className="hidden text-right text-muted-foreground sm:table-cell">
Recipients Recipients
</TableHead> </TableHead>
<TableHead className="hidden text-slate-400 lg:table-cell">Delivery</TableHead> <TableHead className="hidden text-muted-foreground lg:table-cell">Delivery</TableHead>
<TableHead className="hidden text-slate-400 lg:table-cell">Read</TableHead> <TableHead className="hidden text-muted-foreground lg:table-cell">Read</TableHead>
<TableHead className="text-slate-400">Status</TableHead> <TableHead className="text-muted-foreground">Status</TableHead>
<TableHead className="hidden text-slate-400 sm:table-cell">Date</TableHead> <TableHead className="hidden text-muted-foreground sm:table-cell">Date</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@ -234,16 +234,16 @@ export default function BroadcastsPage() {
return ( return (
<TableRow <TableRow
key={broadcast.id} key={broadcast.id}
className="cursor-pointer border-slate-800 hover:bg-slate-800/50" className="cursor-pointer border-border hover:bg-muted/50"
onClick={() => router.push(`/broadcasts/${broadcast.id}`)} onClick={() => router.push(`/broadcasts/${broadcast.id}`)}
> >
<TableCell className="font-medium text-white"> <TableCell className="font-medium text-foreground">
{broadcast.name} {broadcast.name}
</TableCell> </TableCell>
<TableCell className="hidden text-slate-300 md:table-cell"> <TableCell className="hidden text-muted-foreground md:table-cell">
{broadcast.template_name} {broadcast.template_name}
</TableCell> </TableCell>
<TableCell className="hidden text-right text-slate-300 tabular-nums sm:table-cell"> <TableCell className="hidden text-right text-muted-foreground tabular-nums sm:table-cell">
{broadcast.total_recipients} {broadcast.total_recipients}
</TableCell> </TableCell>
<TableCell className="hidden lg:table-cell"> <TableCell className="hidden lg:table-cell">
@ -273,7 +273,7 @@ export default function BroadcastsPage() {
{status.label} {status.label}
</span> </span>
</TableCell> </TableCell>
<TableCell className="hidden text-slate-400 sm:table-cell"> <TableCell className="hidden text-muted-foreground sm:table-cell">
{new Date(broadcast.created_at).toLocaleDateString()} {new Date(broadcast.created_at).toLocaleDateString()}
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@ -217,8 +217,8 @@ export default function ContactsPage() {
{/* Header */} {/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<h1 className="text-2xl font-bold text-white">Contacts</h1> <h1 className="text-2xl font-bold text-foreground">Contacts</h1>
<p className="text-sm text-slate-400 mt-1"> <p className="text-sm text-muted-foreground mt-1">
Manage your contact list. {totalCount > 0 && `${totalCount} total contacts.`} Manage your contact list. {totalCount > 0 && `${totalCount} total contacts.`}
</p> </p>
</div> </div>
@ -227,7 +227,7 @@ export default function ContactsPage() {
<Button <Button
variant="outline" variant="outline"
onClick={() => setCustomFieldsOpen(true)} onClick={() => setCustomFieldsOpen(true)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
<SlidersHorizontal className="size-4" /> <SlidersHorizontal className="size-4" />
Custom fields Custom fields
@ -238,7 +238,7 @@ export default function ContactsPage() {
canAct={canEdit} canAct={canEdit}
gateReason="add or import contacts" gateReason="add or import contacts"
onClick={() => setImportOpen(true)} onClick={() => setImportOpen(true)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
<Upload className="size-4" /> <Upload className="size-4" />
Import Import
@ -257,7 +257,7 @@ export default function ContactsPage() {
{/* Search */} {/* Search */}
<div className="relative max-w-sm"> <div className="relative max-w-sm">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-slate-500" /> <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input <Input
value={search} value={search}
onChange={(e) => { onChange={(e) => {
@ -267,40 +267,40 @@ export default function ContactsPage() {
setPage(0); setPage(0);
}} }}
placeholder="Search by name, phone, or email..." placeholder="Search by name, phone, or email..."
className="pl-8 bg-slate-900 border-slate-700 text-white placeholder:text-slate-500" className="pl-8 bg-card border-border text-foreground placeholder:text-muted-foreground"
/> />
</div> </div>
{/* Table */} {/* Table */}
<div className="rounded-lg border border-slate-800 overflow-hidden"> <div className="rounded-lg border border-border overflow-hidden">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent"> <TableRow className="border-border hover:bg-transparent">
<TableHead className="text-slate-400">Name</TableHead> <TableHead className="text-muted-foreground">Name</TableHead>
<TableHead className="text-slate-400">Phone</TableHead> <TableHead className="text-muted-foreground">Phone</TableHead>
<TableHead className="text-slate-400 hidden md:table-cell">Email</TableHead> <TableHead className="text-muted-foreground hidden md:table-cell">Email</TableHead>
<TableHead className="text-slate-400 hidden lg:table-cell">Company</TableHead> <TableHead className="text-muted-foreground hidden lg:table-cell">Company</TableHead>
<TableHead className="text-slate-400 hidden md:table-cell">Tags</TableHead> <TableHead className="text-muted-foreground hidden md:table-cell">Tags</TableHead>
<TableHead className="text-slate-400 hidden lg:table-cell">Created</TableHead> <TableHead className="text-muted-foreground hidden lg:table-cell">Created</TableHead>
<TableHead className="text-slate-400 w-12" /> <TableHead className="text-muted-foreground w-12" />
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow className="border-slate-800"> <TableRow className="border-border">
<TableCell colSpan={7} className="text-center py-12"> <TableCell colSpan={7} className="text-center py-12">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<Loader2 className="size-6 animate-spin text-primary" /> <Loader2 className="size-6 animate-spin text-primary" />
<p className="text-sm text-slate-500">Loading contacts...</p> <p className="text-sm text-muted-foreground">Loading contacts...</p>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
) : contacts.length === 0 ? ( ) : contacts.length === 0 ? (
<TableRow className="border-slate-800"> <TableRow className="border-border">
<TableCell colSpan={7} className="text-center py-12"> <TableCell colSpan={7} className="text-center py-12">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<Users className="size-8 text-slate-600" /> <Users className="size-8 text-muted-foreground" />
<p className="text-sm text-slate-500"> <p className="text-sm text-muted-foreground">
{search ? 'No contacts match your search.' : 'No contacts yet.'} {search ? 'No contacts match your search.' : 'No contacts yet.'}
</p> </p>
{!search && ( {!search && (
@ -308,7 +308,7 @@ export default function ContactsPage() {
variant="outline" variant="outline"
size="sm" size="sm"
onClick={openAddForm} onClick={openAddForm}
className="mt-2 border-slate-700 text-slate-300 hover:bg-slate-800" className="mt-2 border-border text-muted-foreground hover:bg-muted"
> >
<Plus className="size-3.5" /> <Plus className="size-3.5" />
Add your first contact Add your first contact
@ -321,20 +321,20 @@ export default function ContactsPage() {
contacts.map((contact) => ( contacts.map((contact) => (
<TableRow <TableRow
key={contact.id} key={contact.id}
className="border-slate-800 hover:bg-slate-900/50 cursor-pointer" className="border-border hover:bg-muted/50 cursor-pointer"
onClick={() => openDetail(contact.id)} onClick={() => openDetail(contact.id)}
> >
<TableCell className="text-white font-medium"> <TableCell className="text-foreground font-medium">
{contact.name || <span className="text-slate-500 italic">Unnamed</span>} {contact.name || <span className="text-muted-foreground italic">Unnamed</span>}
</TableCell> </TableCell>
<TableCell className="text-slate-300 font-mono text-xs"> <TableCell className="text-muted-foreground font-mono text-xs">
{contact.phone} {contact.phone}
</TableCell> </TableCell>
<TableCell className="text-slate-400 hidden md:table-cell text-sm"> <TableCell className="text-muted-foreground hidden md:table-cell text-sm">
{contact.email || <span className="text-slate-600">-</span>} {contact.email || <span className="text-muted-foreground">-</span>}
</TableCell> </TableCell>
<TableCell className="text-slate-400 hidden lg:table-cell text-sm"> <TableCell className="text-muted-foreground hidden lg:table-cell text-sm">
{contact.company || <span className="text-slate-600">-</span>} {contact.company || <span className="text-muted-foreground">-</span>}
</TableCell> </TableCell>
<TableCell className="hidden md:table-cell"> <TableCell className="hidden md:table-cell">
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
@ -352,16 +352,16 @@ export default function ContactsPage() {
</span> </span>
)) ))
) : ( ) : (
<span className="text-slate-600 text-xs">-</span> <span className="text-muted-foreground text-xs">-</span>
)} )}
{contact.tags && contact.tags.length > 3 && ( {contact.tags && contact.tags.length > 3 && (
<span className="text-[10px] text-slate-500"> <span className="text-[10px] text-muted-foreground">
+{contact.tags.length - 3} +{contact.tags.length - 3}
</span> </span>
)} )}
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-slate-500 text-xs hidden lg:table-cell"> <TableCell className="text-muted-foreground text-xs hidden lg:table-cell">
{new Date(contact.created_at).toLocaleDateString('en-US', { {new Date(contact.created_at).toLocaleDateString('en-US', {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
@ -375,7 +375,7 @@ export default function ContactsPage() {
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
className="text-slate-400 hover:text-white" className="text-muted-foreground hover:text-foreground"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
/> />
} }
@ -384,19 +384,19 @@ export default function ContactsPage() {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
className="bg-slate-900 border-slate-700" className="bg-popover border-border"
> >
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
openEditForm(contact); openEditForm(contact);
}} }}
className="text-slate-300 focus:bg-slate-800 focus:text-white" className="text-popover-foreground focus:bg-muted focus:text-foreground"
> >
<Pencil className="size-4" /> <Pencil className="size-4" />
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className="bg-slate-700" /> <DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem <DropdownMenuItem
variant="destructive" variant="destructive"
onClick={(e) => { onClick={(e) => {
@ -420,7 +420,7 @@ export default function ContactsPage() {
{/* Pagination */} {/* Pagination */}
{totalPages > 1 && ( {totalPages > 1 && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs text-slate-500"> <p className="text-xs text-muted-foreground">
Showing {page * PAGE_SIZE + 1}-{Math.min((page + 1) * PAGE_SIZE, totalCount)} of{' '} Showing {page * PAGE_SIZE + 1}-{Math.min((page + 1) * PAGE_SIZE, totalCount)} of{' '}
{totalCount} {totalCount}
</p> </p>
@ -430,11 +430,11 @@ export default function ContactsPage() {
size="icon-sm" size="icon-sm"
disabled={!hasPrev} disabled={!hasPrev}
onClick={() => setPage((p) => p - 1)} onClick={() => setPage((p) => p - 1)}
className="border-slate-700 text-slate-400 hover:bg-slate-800 hover:text-white disabled:opacity-30" className="border-border text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-30"
> >
<ChevronLeft className="size-4" /> <ChevronLeft className="size-4" />
</Button> </Button>
<span className="text-xs text-slate-400 px-2"> <span className="text-xs text-muted-foreground px-2">
Page {page + 1} of {totalPages} Page {page + 1} of {totalPages}
</span> </span>
<Button <Button
@ -442,7 +442,7 @@ export default function ContactsPage() {
size="icon-sm" size="icon-sm"
disabled={!hasNext} disabled={!hasNext}
onClick={() => setPage((p) => p + 1)} onClick={() => setPage((p) => p + 1)}
className="border-slate-700 text-slate-400 hover:bg-slate-800 hover:text-white disabled:opacity-30" className="border-border text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-30"
> >
<ChevronRight className="size-4" /> <ChevronRight className="size-4" />
</Button> </Button>
@ -491,22 +491,22 @@ export default function ContactsPage() {
{/* Delete Confirmation */} {/* Delete Confirmation */}
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}> <Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
<DialogContent className="bg-slate-900 border-slate-700 text-slate-200 sm:max-w-sm"> <DialogContent className="bg-popover border-border text-popover-foreground sm:max-w-sm">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white">Delete Contact</DialogTitle> <DialogTitle className="text-popover-foreground">Delete Contact</DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
Are you sure you want to delete{' '} Are you sure you want to delete{' '}
<span className="text-slate-200 font-medium"> <span className="text-popover-foreground font-medium">
{deleteTarget?.name || deleteTarget?.phone} {deleteTarget?.name || deleteTarget?.phone}
</span> </span>
? This action cannot be undone. ? This action cannot be undone.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter className="bg-slate-900 border-slate-700"> <DialogFooter className="bg-popover border-border">
<Button <Button
variant="outline" variant="outline"
onClick={() => setDeleteConfirmOpen(false)} onClick={() => setDeleteConfirmOpen(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>

View File

@ -27,10 +27,10 @@ function DashboardShellInner({ children }: { children: React.ReactNode }) {
if (loading) { if (loading) {
return ( return (
<div className="flex h-screen items-center justify-center bg-slate-950"> <div className="flex h-screen items-center justify-center bg-background">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent" /> <div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<p className="text-sm text-slate-400">Loading...</p> <p className="text-sm text-muted-foreground">Loading...</p>
</div> </div>
</div> </div>
); );
@ -39,7 +39,7 @@ function DashboardShellInner({ children }: { children: React.ReactNode }) {
if (!user) return null; if (!user) return null;
return ( return (
<div className="flex h-screen overflow-hidden bg-slate-950"> <div className="flex h-screen overflow-hidden bg-background">
<Sidebar open={sidebarOpen} onClose={closeSidebar} /> <Sidebar open={sidebarOpen} onClose={closeSidebar} />
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">
<Header onOpenSidebar={() => setSidebarOpen(true)} /> <Header onOpenSidebar={() => setSidebarOpen(true)} />

View File

@ -122,8 +122,8 @@ export default function DashboardPage() {
<div className="space-y-5"> <div className="space-y-5">
{/* Header */} {/* Header */}
<div> <div>
<h1 className="text-2xl font-bold text-white">Dashboard</h1> <h1 className="text-2xl font-bold text-foreground">Dashboard</h1>
<p className="mt-1 text-sm text-slate-400"> <p className="mt-1 text-sm text-muted-foreground">
Live analytics across conversations, contacts, deals, broadcasts, and automations. Live analytics across conversations, contacts, deals, broadcasts, and automations.
</p> </p>
</div> </div>

View File

@ -65,14 +65,14 @@ export default function FlowEditorPage() {
if (loading) { if (loading) {
return ( return (
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-slate-500" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
); );
} }
if (notFound || !flow) { if (notFound || !flow) {
return ( return (
<div className="flex h-full flex-col items-center justify-center gap-3"> <div className="flex h-full flex-col items-center justify-center gap-3">
<p className="text-sm text-slate-400">Flow not found.</p> <p className="text-sm text-muted-foreground">Flow not found.</p>
<button <button
type="button" type="button"
onClick={() => router.push("/flows")} onClick={() => router.push("/flows")}

View File

@ -68,7 +68,7 @@ const STATUS_META: Record<
}, },
completed: { completed: {
label: "Completed", label: "Completed",
classes: "border-slate-700 bg-slate-800 text-slate-300", classes: "border-border bg-muted text-muted-foreground",
icon: CircleCheck, icon: CircleCheck,
}, },
handed_off: { handed_off: {
@ -78,12 +78,12 @@ const STATUS_META: Record<
}, },
timed_out: { timed_out: {
label: "Timed out", label: "Timed out",
classes: "border-slate-700 bg-slate-800/60 text-slate-400", classes: "border-border bg-muted/60 text-muted-foreground",
icon: Clock, icon: Clock,
}, },
paused_by_agent: { paused_by_agent: {
label: "Paused by agent", label: "Paused by agent",
classes: "border-slate-700 bg-slate-800 text-slate-300", classes: "border-border bg-muted text-muted-foreground",
icon: PauseCircle, icon: PauseCircle,
}, },
failed: { failed: {
@ -151,14 +151,14 @@ export default function FlowRunsPage() {
if (loading) { if (loading) {
return ( return (
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-slate-500" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
); );
} }
if (notFound || !flow) { if (notFound || !flow) {
return ( return (
<div className="flex h-full flex-col items-center justify-center gap-3"> <div className="flex h-full flex-col items-center justify-center gap-3">
<p className="text-sm text-slate-400">Flow not found.</p> <p className="text-sm text-muted-foreground">Flow not found.</p>
<button <button
type="button" type="button"
onClick={() => router.push("/flows")} onClick={() => router.push("/flows")}
@ -175,19 +175,19 @@ export default function FlowRunsPage() {
<button <button
type="button" type="button"
onClick={() => router.push(`/flows/${flow.id}`)} onClick={() => router.push(`/flows/${flow.id}`)}
className="mb-2 inline-flex items-center gap-1 text-xs text-slate-500 hover:text-slate-300" className="mb-2 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
> >
<ArrowLeft className="h-3 w-3" /> <ArrowLeft className="h-3 w-3" />
{flow.name} {flow.name}
</button> </button>
<h1 className="text-xl font-semibold text-white">Runs</h1> <h1 className="text-xl font-semibold text-foreground">Runs</h1>
<p className="mt-1 text-sm text-slate-400"> <p className="mt-1 text-sm text-muted-foreground">
The 50 most recent times this flow ran. Expand a row to see the engine&apos;s The 50 most recent times this flow ran. Expand a row to see the engine&apos;s
per-step log. per-step log.
</p> </p>
{runs.length === 0 ? ( {runs.length === 0 ? (
<div className="mt-6 rounded-lg border border-dashed border-slate-700 bg-slate-900/50 px-6 py-12 text-center text-sm text-slate-400"> <div className="mt-6 rounded-lg border border-dashed border-border bg-card/50 px-6 py-12 text-center text-sm text-muted-foreground">
No runs yet. Trigger the flow from a personal WhatsApp number to see No runs yet. Trigger the flow from a personal WhatsApp number to see
it appear here. it appear here.
</div> </div>
@ -229,20 +229,20 @@ function RunCard({
}) })
: null; : null;
return ( return (
<div className="rounded-lg border border-slate-800 bg-slate-900"> <div className="rounded-lg border border-border bg-card">
<button <button
type="button" type="button"
onClick={onToggle} onClick={onToggle}
className="flex w-full items-center gap-3 px-4 py-3 text-left" className="flex w-full items-center gap-3 px-4 py-3 text-left"
> >
{expanded ? ( {expanded ? (
<ChevronDown className="h-4 w-4 shrink-0 text-slate-500" /> <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
) : ( ) : (
<ChevronRight className="h-4 w-4 shrink-0 text-slate-500" /> <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
)} )}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<span className="truncate text-sm font-medium text-white"> <span className="truncate text-sm font-medium text-foreground">
{contactLabel} {contactLabel}
</span> </span>
<Badge variant="outline" className={cn("gap-1", meta.classes)}> <Badge variant="outline" className={cn("gap-1", meta.classes)}>
@ -250,12 +250,12 @@ function RunCard({
{meta.label} {meta.label}
</Badge> </Badge>
{run.status === "active" && run.current_node_key && ( {run.status === "active" && run.current_node_key && (
<code className="rounded bg-slate-800 px-1.5 py-0.5 text-[10px] text-slate-400"> <code className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
at {run.current_node_key} at {run.current_node_key}
</code> </code>
)} )}
</div> </div>
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-[11px] text-slate-500"> <div className="mt-0.5 flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
<span>Started {format(new Date(run.started_at), "PP p")}</span> <span>Started {format(new Date(run.started_at), "PP p")}</span>
{run.reprompt_count > 0 && ( {run.reprompt_count > 0 && (
<span>· {run.reprompt_count} re-prompts</span> <span>· {run.reprompt_count} re-prompts</span>
@ -265,20 +265,20 @@ function RunCard({
</div> </div>
</button> </button>
{expanded && ( {expanded && (
<div className="border-t border-slate-800 px-4 py-3"> <div className="border-t border-border px-4 py-3">
{Object.keys(run.vars).length > 0 && ( {Object.keys(run.vars).length > 0 && (
<details className="mb-3"> <details className="mb-3">
<summary className="cursor-pointer text-xs text-slate-400"> <summary className="cursor-pointer text-xs text-muted-foreground">
Captured vars ({Object.keys(run.vars).length}) Captured vars ({Object.keys(run.vars).length})
</summary> </summary>
<pre className="mt-2 overflow-x-auto rounded-md bg-slate-950 p-2 text-[11px] text-slate-300"> <pre className="mt-2 overflow-x-auto rounded-md bg-background p-2 text-[11px] text-muted-foreground">
{JSON.stringify(run.vars, null, 2)} {JSON.stringify(run.vars, null, 2)}
</pre> </pre>
</details> </details>
)} )}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{events.length === 0 ? ( {events.length === 0 ? (
<p className="text-xs text-slate-500"> <p className="text-xs text-muted-foreground">
No events recorded for this run. No events recorded for this run.
</p> </p>
) : ( ) : (
@ -293,33 +293,33 @@ function RunCard({
const EVENT_COLOR: Record<string, string> = { const EVENT_COLOR: Record<string, string> = {
started: "text-emerald-300", started: "text-emerald-300",
node_entered: "text-slate-300", node_entered: "text-muted-foreground",
message_sent: "text-sky-300", message_sent: "text-sky-300",
reply_received: "text-primary", reply_received: "text-primary",
fallback_fired: "text-amber-300", fallback_fired: "text-amber-300",
handoff: "text-amber-300", handoff: "text-amber-300",
timeout: "text-slate-500", timeout: "text-muted-foreground",
error: "text-red-300", error: "text-red-300",
completed: "text-emerald-300", completed: "text-emerald-300",
}; };
function EventLine({ ev }: { ev: EventRow }) { function EventLine({ ev }: { ev: EventRow }) {
const cls = EVENT_COLOR[ev.event_type] ?? "text-slate-400"; const cls = EVENT_COLOR[ev.event_type] ?? "text-muted-foreground";
return ( return (
<div className="flex items-start gap-2 rounded-md px-2 py-1 text-xs"> <div className="flex items-start gap-2 rounded-md px-2 py-1 text-xs">
<span className="w-32 shrink-0 text-[10px] text-slate-500"> <span className="w-32 shrink-0 text-[10px] text-muted-foreground">
{format(new Date(ev.created_at), "HH:mm:ss")} {format(new Date(ev.created_at), "HH:mm:ss")}
</span> </span>
<span className={cn("w-32 shrink-0 font-mono text-[10px]", cls)}> <span className={cn("w-32 shrink-0 font-mono text-[10px]", cls)}>
{ev.event_type} {ev.event_type}
</span> </span>
{ev.node_key && ( {ev.node_key && (
<code className="shrink-0 rounded bg-slate-800 px-1 py-0.5 text-[10px] text-slate-400"> <code className="shrink-0 rounded bg-muted px-1 py-0.5 text-[10px] text-muted-foreground">
{ev.node_key} {ev.node_key}
</code> </code>
)} )}
{Object.keys(ev.payload).length > 0 && ( {Object.keys(ev.payload).length > 0 && (
<span className="min-w-0 truncate text-[10px] text-slate-500"> <span className="min-w-0 truncate text-[10px] text-muted-foreground">
{summarizePayload(ev.payload)} {summarizePayload(ev.payload)}
</span> </span>
)} )}

View File

@ -61,9 +61,9 @@ const STATUS_LABELS: Record<FlowRow["status"], string> = {
}; };
const STATUS_COLORS: Record<FlowRow["status"], string> = { const STATUS_COLORS: Record<FlowRow["status"], string> = {
draft: "border-slate-700 bg-slate-800 text-slate-300", draft: "border-border bg-muted text-muted-foreground",
active: "border-emerald-600/40 bg-emerald-500/10 text-emerald-300", active: "border-emerald-600/40 bg-emerald-500/10 text-emerald-300",
archived: "border-slate-700 bg-slate-800/50 text-slate-500", archived: "border-border bg-muted/50 text-muted-foreground",
}; };
interface TemplateSummary { interface TemplateSummary {
@ -194,7 +194,7 @@ export default function FlowsPage() {
if (loading) { if (loading) {
return ( return (
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-slate-500" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
); );
} }
@ -204,12 +204,12 @@ export default function FlowsPage() {
<header className="flex flex-wrap items-end justify-between gap-3"> <header className="flex flex-wrap items-end justify-between gap-3">
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h1 className="text-2xl font-semibold text-white">Flows</h1> <h1 className="text-2xl font-semibold text-foreground">Flows</h1>
<span className="inline-flex items-center rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-300"> <span className="inline-flex items-center rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-300">
Beta Beta
</span> </span>
</div> </div>
<p className="mt-1 text-sm text-slate-400"> <p className="mt-1 text-sm text-muted-foreground">
Build branching, button-driven WhatsApp conversations. Useful for Build branching, button-driven WhatsApp conversations. Useful for
menus, FAQs, and triage before a human steps in. menus, FAQs, and triage before a human steps in.
</p> </p>
@ -247,17 +247,17 @@ export default function FlowsPage() {
`sm:max-w-sm` baked into its default classes. Without the `sm:max-w-sm` baked into its default classes. Without the
sm: prefix our override applies at base only and the sm: prefix our override applies at base only and the
sm-scoped 384px wins at every real desktop breakpoint. */} sm-scoped 384px wins at every real desktop breakpoint. */}
<DialogContent className="sm:max-w-4xl bg-slate-900 text-slate-100"> <DialogContent className="sm:max-w-4xl bg-popover text-popover-foreground">
<DialogHeader> <DialogHeader>
<DialogTitle>Create a new flow</DialogTitle> <DialogTitle>Create a new flow</DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
Start from a template or build from scratch. Start from a template or build from scratch.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{templates.length > 0 && ( {templates.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-xs uppercase tracking-wide text-slate-500"> <p className="text-xs uppercase tracking-wide text-muted-foreground">
Start from a template Start from a template
</p> </p>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
@ -269,16 +269,16 @@ export default function FlowsPage() {
type="button" type="button"
onClick={() => handleUseTemplate(t.slug)} onClick={() => handleUseTemplate(t.slug)}
disabled={creating} disabled={creating}
className="flex flex-col gap-2.5 rounded-lg border border-slate-800 bg-slate-950 p-4 text-left transition-colors hover:border-primary/40 hover:bg-slate-800 disabled:opacity-50" className="flex flex-col gap-2.5 rounded-lg border border-border bg-background p-4 text-left transition-colors hover:border-primary/40 hover:bg-muted disabled:opacity-50"
> >
<Icon className="h-5 w-5 text-primary" /> <Icon className="h-5 w-5 text-primary" />
<span className="text-sm font-semibold text-white"> <span className="text-sm font-semibold text-popover-foreground">
{t.name} {t.name}
</span> </span>
<span className="text-xs leading-relaxed text-slate-400"> <span className="text-xs leading-relaxed text-muted-foreground">
{t.description} {t.description}
</span> </span>
<span className="mt-auto border-t border-slate-800 pt-2 text-[11px] text-slate-500"> <span className="mt-auto border-t border-border pt-2 text-[11px] text-muted-foreground">
{t.node_count} {t.node_count === 1 ? "node" : "nodes"} {t.node_count} {t.node_count === 1 ? "node" : "nodes"}
</span> </span>
</button> </button>
@ -288,15 +288,15 @@ export default function FlowsPage() {
</div> </div>
)} )}
<div className="space-y-2 border-t border-slate-800 pt-4"> <div className="space-y-2 border-t border-border pt-4">
<p className="text-xs uppercase tracking-wide text-slate-500"> <p className="text-xs uppercase tracking-wide text-muted-foreground">
Or start blank Or start blank
</p> </p>
<Input <Input
value={newName} value={newName}
onChange={(e) => setNewName(e.target.value)} onChange={(e) => setNewName(e.target.value)}
placeholder="e.g. Welcome menu" placeholder="e.g. Welcome menu"
className="bg-slate-800" className="bg-muted"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") handleCreate(); if (e.key === "Enter") handleCreate();
}} }}
@ -330,14 +330,14 @@ function EmptyState({
canCreate: boolean; canCreate: boolean;
}) { }) {
return ( return (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-slate-700 bg-slate-900/50 px-6 py-16 text-center"> <div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border bg-card/50 px-6 py-16 text-center">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-slate-800"> <div className="flex h-14 w-14 items-center justify-center rounded-full bg-muted">
<Workflow className="h-6 w-6 text-slate-500" /> <Workflow className="h-6 w-6 text-muted-foreground" />
</div> </div>
<h2 className="mt-4 text-base font-medium text-white"> <h2 className="mt-4 text-base font-medium text-foreground">
No flows yet No flows yet
</h2> </h2>
<p className="mt-1 max-w-md text-sm text-slate-400"> <p className="mt-1 max-w-md text-sm text-muted-foreground">
Build your first conversation — a welcome menu, an order lookup, an FAQ Build your first conversation — a welcome menu, an order lookup, an FAQ
bot. Customers tap buttons; the bot routes them to the right answer (or bot. Customers tap buttons; the bot routes them to the right answer (or
the right agent). the right agent).
@ -372,11 +372,11 @@ function FlowCard({
? Archive ? Archive
: PauseCircle; : PauseCircle;
return ( return (
<div className="flex flex-col rounded-lg border border-slate-800 bg-slate-900 p-4 transition-colors hover:border-slate-700"> <div className="flex flex-col rounded-lg border border-border bg-card p-4 transition-colors hover:border-border">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<Workflow className="h-4 w-4 shrink-0 text-primary" /> <Workflow className="h-4 w-4 shrink-0 text-primary" />
<h3 className="truncate text-sm font-semibold text-white"> <h3 className="truncate text-sm font-semibold text-foreground">
{flow.name} {flow.name}
</h3> </h3>
</div> </div>
@ -392,18 +392,18 @@ function FlowCard({
</Badge> </Badge>
</div> </div>
<p className="mt-2 line-clamp-2 text-xs text-slate-400"> <p className="mt-2 line-clamp-2 text-xs text-muted-foreground">
{flow.description || triggerSummary} {flow.description || triggerSummary}
</p> </p>
<div className="mt-4 flex items-center gap-3 text-[11px] text-slate-500"> <div className="mt-4 flex items-center gap-3 text-[11px] text-muted-foreground">
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
<MessageSquare className="h-3 w-3" /> <MessageSquare className="h-3 w-3" />
{flow.execution_count} {flow.execution_count === 1 ? "run" : "runs"} {flow.execution_count} {flow.execution_count === 1 ? "run" : "runs"}
</span> </span>
</div> </div>
<div className="mt-4 flex items-center justify-end gap-2 border-t border-slate-800 pt-3"> <div className="mt-4 flex items-center justify-end gap-2 border-t border-border pt-3">
<Button variant="ghost" size="sm" onClick={onEdit}> <Button variant="ghost" size="sm" onClick={onEdit}>
<Pencil className="h-3.5 w-3.5" /> <Pencil className="h-3.5 w-3.5" />
Edit Edit

View File

@ -299,12 +299,12 @@ export default function PipelinesPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="h-8 w-48 animate-pulse rounded bg-slate-800" /> <div className="h-8 w-48 animate-pulse rounded bg-muted" />
<div className="h-9 w-28 animate-pulse rounded-lg bg-slate-800" /> <div className="h-9 w-28 animate-pulse rounded-lg bg-muted" />
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
{[1, 2, 3, 4, 5].map((i) => ( {[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="h-96 w-72 animate-pulse rounded-xl bg-slate-800/50" /> <div key={i} className="h-96 w-72 animate-pulse rounded-xl bg-muted/50" />
))} ))}
</div> </div>
</div> </div>
@ -319,20 +319,20 @@ export default function PipelinesPage() {
{/* Pipeline selector dropdown */} {/* Pipeline selector dropdown */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
className="inline-flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white hover:bg-slate-800 transition-colors data-[popup-open]:bg-slate-800" className="inline-flex items-center gap-2 rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground hover:bg-muted transition-colors data-[popup-open]:bg-muted"
> >
<GitBranch className="h-4 w-4 text-primary" /> <GitBranch className="h-4 w-4 text-primary" />
<span className="font-semibold"> <span className="font-semibold">
{selectedPipeline?.name ?? "Select Pipeline"} {selectedPipeline?.name ?? "Select Pipeline"}
</span> </span>
<ChevronDown className="h-4 w-4 text-slate-400" /> <ChevronDown className="h-4 w-4 text-muted-foreground" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="start" align="start"
className="w-64 border-slate-700 bg-slate-900 text-slate-200" className="w-64 border-border bg-popover text-popover-foreground"
> >
{pipelines.length === 0 && ( {pipelines.length === 0 && (
<DropdownMenuItem disabled className="text-slate-500"> <DropdownMenuItem disabled className="text-muted-foreground">
No pipelines yet No pipelines yet
</DropdownMenuItem> </DropdownMenuItem>
)} )}
@ -343,18 +343,18 @@ export default function PipelinesPage() {
className={ className={
p.id === selectedPipelineId p.id === selectedPipelineId
? "text-primary" ? "text-primary"
: "text-slate-300" : "text-popover-foreground"
} }
> >
<GitBranch className="mr-2 h-3.5 w-3.5" /> <GitBranch className="mr-2 h-3.5 w-3.5" />
{p.name} {p.name}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
<DropdownMenuSeparator className="bg-slate-700" /> <DropdownMenuSeparator className="bg-border" />
{selectedPipeline && ( {selectedPipeline && (
<DropdownMenuItem <DropdownMenuItem
onClick={() => setSettingsOpen(true)} onClick={() => setSettingsOpen(true)}
className="text-slate-300" className="text-popover-foreground"
> >
<Settings className="mr-2 h-3.5 w-3.5" /> <Settings className="mr-2 h-3.5 w-3.5" />
Manage Pipelines Manage Pipelines
@ -370,7 +370,7 @@ export default function PipelinesPage() {
canAct={canEditSettings} canAct={canEditSettings}
gateReason="create pipelines" gateReason="create pipelines"
onClick={() => setNewPipelineOpen(true)} onClick={() => setNewPipelineOpen(true)}
className="border-slate-700 bg-slate-900 text-slate-200 hover:bg-slate-800" className="border-border bg-card text-foreground hover:bg-muted"
> >
<Plus className="mr-1 h-4 w-4" /> <Plus className="mr-1 h-4 w-4" />
Add Pipeline Add Pipeline
@ -390,12 +390,12 @@ export default function PipelinesPage() {
{/* Board */} {/* Board */}
{pipelines.length === 0 ? ( {pipelines.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-slate-700 py-20"> <div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-border py-20">
<GitBranch className="h-12 w-12 text-slate-600" /> <GitBranch className="h-12 w-12 text-muted-foreground" />
<h3 className="mt-4 text-lg font-medium text-white"> <h3 className="mt-4 text-lg font-medium text-foreground">
No pipelines yet No pipelines yet
</h3> </h3>
<p className="mt-2 text-sm text-slate-400"> <p className="mt-2 text-sm text-muted-foreground">
Create a pipeline to start tracking deals Create a pipeline to start tracking deals
</p> </p>
<GatedButton <GatedButton
@ -423,30 +423,30 @@ export default function PipelinesPage() {
{/* New Pipeline Dialog */} {/* New Pipeline Dialog */}
<Dialog open={newPipelineOpen} onOpenChange={setNewPipelineOpen}> <Dialog open={newPipelineOpen} onOpenChange={setNewPipelineOpen}>
<DialogContent className="sm:max-w-sm bg-slate-900 border-slate-700"> <DialogContent className="sm:max-w-sm bg-popover border-border">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white">New Pipeline</DialogTitle> <DialogTitle className="text-popover-foreground">New Pipeline</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="py-2"> <div className="py-2">
<Label className="text-slate-300">Pipeline Name</Label> <Label className="text-muted-foreground">Pipeline Name</Label>
<Input <Input
value={newPipelineName} value={newPipelineName}
onChange={(e) => setNewPipelineName(e.target.value)} onChange={(e) => setNewPipelineName(e.target.value)}
placeholder="e.g., Enterprise Sales" placeholder="e.g., Enterprise Sales"
className="mt-2 bg-slate-800 border-slate-700 text-white" className="mt-2 bg-muted border-border text-foreground"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") handleCreatePipeline(); if (e.key === "Enter") handleCreatePipeline();
}} }}
/> />
<p className="mt-2 text-xs text-slate-400"> <p className="mt-2 text-xs text-muted-foreground">
Default stages (New Lead → Won) will be created automatically. Default stages (New Lead → Won) will be created automatically.
</p> </p>
</div> </div>
<DialogFooter className="bg-slate-900/50 border-slate-700"> <DialogFooter className="bg-popover/50 border-border">
<Button <Button
variant="outline" variant="outline"
onClick={() => setNewPipelineOpen(false)} onClick={() => setNewPipelineOpen(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>

View File

@ -69,39 +69,39 @@ export default function SettingsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h1 className="text-2xl font-bold text-white">Settings</h1> <h1 className="text-2xl font-bold text-foreground">Settings</h1>
<p className="mt-1 text-sm text-slate-400"> <p className="mt-1 text-sm text-muted-foreground">
Manage your profile, WhatsApp® integration, message templates, and Manage your profile, WhatsApp® integration, message templates, and
tags. tags.
</p> </p>
</div> </div>
<Tabs value={tab} onValueChange={(v) => onChange(v as TabValue)}> <Tabs value={tab} onValueChange={(v) => onChange(v as TabValue)}>
<TabsList className="border border-slate-700 bg-slate-900"> <TabsList className="border border-border bg-card">
<TabsTrigger <TabsTrigger
value="profile" value="profile"
className="data-active:text-primary text-slate-400 data-active:bg-slate-800" className="data-active:text-primary text-muted-foreground data-active:bg-muted"
> >
<User className="size-4" /> <User className="size-4" />
Profile Profile
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="whatsapp" value="whatsapp"
className="data-active:text-primary text-slate-400 data-active:bg-slate-800" className="data-active:text-primary text-muted-foreground data-active:bg-muted"
> >
<Settings className="size-4" /> <Settings className="size-4" />
WhatsApp Config WhatsApp Config
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="templates" value="templates"
className="data-active:text-primary text-slate-400 data-active:bg-slate-800" className="data-active:text-primary text-muted-foreground data-active:bg-muted"
> >
<MessageSquare className="size-4" /> <MessageSquare className="size-4" />
Templates Templates
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="tags" value="tags"
className="data-active:text-primary text-slate-400 data-active:bg-slate-800" className="data-active:text-primary text-muted-foreground data-active:bg-muted"
> >
<Tag className="size-4" /> <Tag className="size-4" />
Tags Tags
@ -109,7 +109,7 @@ export default function SettingsPage() {
{canEditSettings && ( {canEditSettings && (
<TabsTrigger <TabsTrigger
value="custom-fields" value="custom-fields"
className="data-active:text-primary text-slate-400 data-active:bg-slate-800" className="data-active:text-primary text-muted-foreground data-active:bg-muted"
> >
<SlidersHorizontal className="size-4" /> <SlidersHorizontal className="size-4" />
Custom Fields Custom Fields
@ -117,21 +117,21 @@ export default function SettingsPage() {
)} )}
<TabsTrigger <TabsTrigger
value="deals" value="deals"
className="data-active:text-primary text-slate-400 data-active:bg-slate-800" className="data-active:text-primary text-muted-foreground data-active:bg-muted"
> >
<Coins className="size-4" /> <Coins className="size-4" />
Deals Deals
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="appearance" value="appearance"
className="data-active:text-primary text-slate-400 data-active:bg-slate-800" className="data-active:text-primary text-muted-foreground data-active:bg-muted"
> >
<Palette className="size-4" /> <Palette className="size-4" />
Appearance Appearance
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="members" value="members"
className="data-active:text-primary text-slate-400 data-active:bg-slate-800" className="data-active:text-primary text-muted-foreground data-active:bg-muted"
> >
<UsersRound className="size-4" /> <UsersRound className="size-4" />
Members Members

View File

@ -49,39 +49,42 @@
} }
/* ============================================================ /* ============================================================
* COLOR THEMES * THEMING — two orthogonal dimensions
* *
* 5 dark themes sharing structure, swapping only the primary * 1. MODE (light / dark) → neutral surfaces. Selected with
* accent (and a tiny hue rotation on the slate base). Switched * `document.documentElement.dataset.mode`. Defaults to dark.
* at runtime by setting `document.documentElement.dataset.theme` * 2. ACCENT (violet / …) → the primary color only. Selected
* — see ThemeProvider + the inline boot script in layout.tsx. * with `document.documentElement.dataset.theme`.
* *
* `:root` mirrors Violet so default visitors render correctly * The two compose: any accent works in either mode. Neutral
* before JS runs. `[data-theme="violet"]` is included for the * tokens (background / card / border / muted / sidebar surfaces /
* explicit-select path (settings UI sets it after a pick). * neutral charts / radius) live in the MODE blocks below; accent
* tokens (--primary*, --ring, --chart-1, --sidebar-primary*,
* --sidebar-ring) live in the ACCENT blocks. They set disjoint
* variables, so cascade order between them doesn't matter.
* *
* The three `--primary-*` extras (hover / soft / soft-2) are * `:root` carries the dark-mode + violet defaults so a visitor
* new — used for hovered primary buttons and tinted-primary * renders correctly before JS runs; the boot script in
* surfaces (sidebar active pill, validation badges). Components * layout.tsx replays the saved mode + accent before first paint.
* that don't use them remain backward-compatible. *
* The three `--primary-*` extras (hover / soft / soft-2) feed
* hovered primary buttons and tinted-primary surfaces (sidebar
* active pill, validation badges).
* *
* Token shapes come straight from the design handoff at * Token shapes come straight from the design handoff at
* project/Color Themes.html — change one, change the other. * project/Color Themes.html — change one, change the other.
* ============================================================ */ * ============================================================ */
/* ---- MODE: neutral surfaces ---- */
:root, :root,
html[data-theme="violet"] { html[data-mode="dark"] {
--background: oklch(0.13 0.01 260); --background: oklch(0.13 0.01 260);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.18 0.01 260); --card: oklch(0.18 0.01 260);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.18 0.01 260); --popover: oklch(0.18 0.01 260);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.526 0.247 293);
--primary-foreground: oklch(0.985 0 0);
--primary-hover: oklch(0.6 0.22 293);
--primary-soft: oklch(0.526 0.247 293 / 0.12);
--primary-soft-2: oklch(0.526 0.247 293 / 0.2);
--secondary: oklch(0.22 0.01 260); --secondary: oklch(0.22 0.01 260);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.22 0.01 260); --muted: oklch(0.22 0.01 260);
@ -91,8 +94,6 @@ html[data-theme="violet"] {
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.28 0.01 260); --border: oklch(0.28 0.01 260);
--input: oklch(0.28 0.01 260); --input: oklch(0.28 0.01 260);
--ring: oklch(0.526 0.247 293);
--chart-1: oklch(0.526 0.247 293);
--chart-2: oklch(0.556 0 0); --chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0); --chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0); --chart-4: oklch(0.371 0 0);
@ -100,159 +101,108 @@ html[data-theme="violet"] {
--radius: 0.625rem; --radius: 0.625rem;
--sidebar: oklch(0.16 0.01 260); --sidebar: oklch(0.16 0.01 260);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.526 0.247 293);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.22 0.01 260); --sidebar-accent: oklch(0.22 0.01 260);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.28 0.01 260); --sidebar-border: oklch(0.28 0.01 260);
}
html[data-mode="light"] {
--background: oklch(0.99 0.002 260);
--foreground: oklch(0.21 0.01 260);
--card: oklch(1 0 0);
--card-foreground: oklch(0.21 0.01 260);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.21 0.01 260);
--secondary: oklch(0.967 0.003 260);
--secondary-foreground: oklch(0.25 0.01 260);
--muted: oklch(0.967 0.003 260);
--muted-foreground: oklch(0.52 0.015 260);
--accent: oklch(0.96 0.004 260);
--accent-foreground: oklch(0.25 0.01 260);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0.004 260);
--input: oklch(0.922 0.004 260);
--chart-2: oklch(0.6 0.1 260);
--chart-3: oklch(0.7 0.06 260);
--chart-4: oklch(0.8 0.04 260);
--chart-5: oklch(0.87 0.03 260);
--radius: 0.625rem;
--sidebar: oklch(0.985 0.002 260);
--sidebar-foreground: oklch(0.21 0.01 260);
--sidebar-accent: oklch(0.96 0.004 260);
--sidebar-accent-foreground: oklch(0.25 0.01 260);
--sidebar-border: oklch(0.922 0.004 260);
}
/* ---- ACCENT: primary color ---- */
:root,
html[data-theme="violet"] {
--primary: oklch(0.526 0.247 293);
--primary-foreground: oklch(0.985 0 0);
--primary-hover: oklch(0.6 0.22 293);
--primary-soft: oklch(0.526 0.247 293 / 0.12);
--primary-soft-2: oklch(0.526 0.247 293 / 0.2);
--ring: oklch(0.526 0.247 293);
--chart-1: oklch(0.526 0.247 293);
--sidebar-primary: oklch(0.526 0.247 293);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-ring: oklch(0.526 0.247 293); --sidebar-ring: oklch(0.526 0.247 293);
} }
html[data-theme="emerald"] { html[data-theme="emerald"] {
--background: oklch(0.135 0.01 200);
--foreground: oklch(0.985 0 0);
--card: oklch(0.185 0.01 200);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.185 0.01 200);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.62 0.16 162); --primary: oklch(0.62 0.16 162);
--primary-foreground: oklch(0.16 0.02 162); --primary-foreground: oklch(0.16 0.02 162);
--primary-hover: oklch(0.68 0.15 162); --primary-hover: oklch(0.68 0.15 162);
--primary-soft: oklch(0.62 0.16 162 / 0.12); --primary-soft: oklch(0.62 0.16 162 / 0.12);
--primary-soft-2: oklch(0.62 0.16 162 / 0.22); --primary-soft-2: oklch(0.62 0.16 162 / 0.22);
--secondary: oklch(0.225 0.01 200);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.225 0.01 200);
--muted-foreground: oklch(0.65 0.01 200);
--accent: oklch(0.225 0.01 200);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.285 0.01 200);
--input: oklch(0.285 0.01 200);
--ring: oklch(0.62 0.16 162); --ring: oklch(0.62 0.16 162);
--chart-1: oklch(0.62 0.16 162); --chart-1: oklch(0.62 0.16 162);
--chart-2: oklch(0.7 0.14 195); --chart-2: oklch(0.7 0.14 195);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.165 0.01 200);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.62 0.16 162); --sidebar-primary: oklch(0.62 0.16 162);
--sidebar-primary-foreground: oklch(0.16 0.02 162); --sidebar-primary-foreground: oklch(0.16 0.02 162);
--sidebar-accent: oklch(0.225 0.01 200);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.285 0.01 200);
--sidebar-ring: oklch(0.62 0.16 162); --sidebar-ring: oklch(0.62 0.16 162);
} }
html[data-theme="cobalt"] { html[data-theme="cobalt"] {
--background: oklch(0.13 0.015 252);
--foreground: oklch(0.985 0 0);
--card: oklch(0.18 0.015 252);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.18 0.015 252);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.585 0.2 254); --primary: oklch(0.585 0.2 254);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(0.985 0 0);
--primary-hover: oklch(0.66 0.18 254); --primary-hover: oklch(0.66 0.18 254);
--primary-soft: oklch(0.585 0.2 254 / 0.12); --primary-soft: oklch(0.585 0.2 254 / 0.12);
--primary-soft-2: oklch(0.585 0.2 254 / 0.22); --primary-soft-2: oklch(0.585 0.2 254 / 0.22);
--secondary: oklch(0.22 0.015 252);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.22 0.015 252);
--muted-foreground: oklch(0.65 0.012 252);
--accent: oklch(0.22 0.015 252);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.28 0.015 252);
--input: oklch(0.28 0.015 252);
--ring: oklch(0.585 0.2 254); --ring: oklch(0.585 0.2 254);
--chart-1: oklch(0.585 0.2 254); --chart-1: oklch(0.585 0.2 254);
--chart-2: oklch(0.7 0.15 220); --chart-2: oklch(0.7 0.15 220);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.16 0.015 252);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.585 0.2 254); --sidebar-primary: oklch(0.585 0.2 254);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.22 0.015 252);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.28 0.015 252);
--sidebar-ring: oklch(0.585 0.2 254); --sidebar-ring: oklch(0.585 0.2 254);
} }
html[data-theme="amber"] { html[data-theme="amber"] {
--background: oklch(0.135 0.012 60);
--foreground: oklch(0.985 0 0);
--card: oklch(0.185 0.012 60);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.185 0.012 60);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.745 0.16 65); --primary: oklch(0.745 0.16 65);
--primary-foreground: oklch(0.18 0.03 65); --primary-foreground: oklch(0.18 0.03 65);
--primary-hover: oklch(0.8 0.15 65); --primary-hover: oklch(0.8 0.15 65);
--primary-soft: oklch(0.745 0.16 65 / 0.12); --primary-soft: oklch(0.745 0.16 65 / 0.12);
--primary-soft-2: oklch(0.745 0.16 65 / 0.22); --primary-soft-2: oklch(0.745 0.16 65 / 0.22);
--secondary: oklch(0.225 0.012 60);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.225 0.012 60);
--muted-foreground: oklch(0.66 0.01 60);
--accent: oklch(0.225 0.012 60);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.285 0.012 60);
--input: oklch(0.285 0.012 60);
--ring: oklch(0.745 0.16 65); --ring: oklch(0.745 0.16 65);
--chart-1: oklch(0.745 0.16 65); --chart-1: oklch(0.745 0.16 65);
--chart-2: oklch(0.7 0.15 35); --chart-2: oklch(0.7 0.15 35);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.165 0.012 60);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.745 0.16 65); --sidebar-primary: oklch(0.745 0.16 65);
--sidebar-primary-foreground: oklch(0.18 0.03 65); --sidebar-primary-foreground: oklch(0.18 0.03 65);
--sidebar-accent: oklch(0.225 0.012 60);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.285 0.012 60);
--sidebar-ring: oklch(0.745 0.16 65); --sidebar-ring: oklch(0.745 0.16 65);
} }
html[data-theme="rose"] { html[data-theme="rose"] {
--background: oklch(0.13 0.012 15);
--foreground: oklch(0.985 0 0);
--card: oklch(0.18 0.012 15);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.18 0.012 15);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.645 0.22 16); --primary: oklch(0.645 0.22 16);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(0.985 0 0);
--primary-hover: oklch(0.71 0.2 16); --primary-hover: oklch(0.71 0.2 16);
--primary-soft: oklch(0.645 0.22 16 / 0.12); --primary-soft: oklch(0.645 0.22 16 / 0.12);
--primary-soft-2: oklch(0.645 0.22 16 / 0.22); --primary-soft-2: oklch(0.645 0.22 16 / 0.22);
--secondary: oklch(0.22 0.012 15);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.22 0.012 15);
--muted-foreground: oklch(0.65 0.01 15);
--accent: oklch(0.22 0.012 15);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.28 0.012 15);
--input: oklch(0.28 0.012 15);
--ring: oklch(0.645 0.22 16); --ring: oklch(0.645 0.22 16);
--chart-1: oklch(0.645 0.22 16); --chart-1: oklch(0.645 0.22 16);
--chart-2: oklch(0.7 0.18 340); --chart-2: oklch(0.7 0.18 340);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.16 0.012 15);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.645 0.22 16); --sidebar-primary: oklch(0.645 0.22 16);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.22 0.012 15);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.28 0.012 15);
--sidebar-ring: oklch(0.645 0.22 16); --sidebar-ring: oklch(0.645 0.22 16);
} }

View File

@ -220,10 +220,10 @@ export default function JoinPage() {
// ----- Loading state (peek pending OR auth not yet resolved) ----- // ----- Loading state (peek pending OR auth not yet resolved) -----
if (peek === null || authedUserId === undefined) { if (peek === null || authedUserId === undefined) {
return ( return (
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-border bg-card">
<CardContent className="flex flex-col items-center gap-3 py-12"> <CardContent className="flex flex-col items-center gap-3 py-12">
<Loader2 className="size-6 animate-spin text-primary" /> <Loader2 className="size-6 animate-spin text-primary" />
<p className="text-sm text-slate-400">Verifying invitation…</p> <p className="text-sm text-muted-foreground">Verifying invitation…</p>
</CardContent> </CardContent>
</Card> </Card>
); );
@ -233,13 +233,13 @@ export default function JoinPage() {
if (!peek.ok) { if (!peek.ok) {
const copy = FAIL_COPY[peek.reason]; const copy = FAIL_COPY[peek.reason];
return ( return (
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center"> <CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-red-500/10"> <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-red-500/10">
<MailX className="h-6 w-6 text-red-400" /> <MailX className="h-6 w-6 text-red-400" />
</div> </div>
<CardTitle className="text-xl text-white">{copy.title}</CardTitle> <CardTitle className="text-xl text-foreground">{copy.title}</CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
{copy.body} {copy.body}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
@ -262,7 +262,7 @@ export default function JoinPage() {
<Link href="/signup"> <Link href="/signup">
<Button <Button
variant="outline" variant="outline"
className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white" className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
> >
Create a new account instead Create a new account instead
</Button> </Button>
@ -278,7 +278,7 @@ export default function JoinPage() {
<Link href="/login"> <Link href="/login">
<Button <Button
variant="outline" variant="outline"
className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white" className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
> >
Sign in Sign in
</Button> </Button>
@ -296,13 +296,13 @@ export default function JoinPage() {
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10"> <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<UsersRound className="h-6 w-6 text-primary" /> <UsersRound className="h-6 w-6 text-primary" />
</div> </div>
<CardTitle className="text-xl text-white"> <CardTitle className="text-xl text-foreground">
You&apos;re invited to{' '} You&apos;re invited to{' '}
<span className="text-primary">{peek.account_name}</span> <span className="text-primary">{peek.account_name}</span>
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
You&apos;ll join as{' '} You&apos;ll join as{' '}
<span className="inline-flex items-center gap-1 text-white"> <span className="inline-flex items-center gap-1 text-foreground">
<ShieldCheck className="size-3.5 text-primary" /> <ShieldCheck className="size-3.5 text-primary" />
{ROLE_LABEL[peek.role]} {ROLE_LABEL[peek.role]}
</span> </span>
@ -321,7 +321,7 @@ export default function JoinPage() {
if (authedUserId) { if (authedUserId) {
return ( return (
<> <>
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-border bg-card">
{inviteHeader} {inviteHeader}
<CardContent className="flex flex-col gap-3"> <CardContent className="flex flex-col gap-3">
<Button <Button
@ -341,9 +341,9 @@ export default function JoinPage() {
</> </>
)} )}
</Button> </Button>
<p className="text-center text-xs text-slate-500"> <p className="text-center text-xs text-muted-foreground">
Accepting moves your login into{' '} Accepting moves your login into{' '}
<span className="text-slate-400">{peek.account_name}</span>. Your <span className="text-muted-foreground">{peek.account_name}</span>. Your
empty personal account from signup will be cleaned up. empty personal account from signup will be cleaned up.
</p> </p>
</CardContent> </CardContent>
@ -359,30 +359,30 @@ export default function JoinPage() {
if (!open) setConflictMessage(null); if (!open) setConflictMessage(null);
}} }}
> >
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-md"> <DialogContent className="bg-popover border-border sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-white"> <DialogTitle className="flex items-center gap-2 text-popover-foreground">
<AlertTriangle className="size-4 text-amber-400" /> <AlertTriangle className="size-4 text-amber-400" />
Can&apos;t join {peek.account_name} with this account Can&apos;t join {peek.account_name} with this account
</DialogTitle> </DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
{conflictMessage} {conflictMessage}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-2 py-2 text-xs text-slate-500"> <div className="space-y-2 py-2 text-xs text-muted-foreground">
<p> <p>
To join{' '} To join{' '}
<span className="text-slate-300">{peek.account_name}</span>, <span className="text-popover-foreground">{peek.account_name}</span>,
sign out and sign up again with a different email address. sign out and sign up again with a different email address.
The invite link stays valid as long as it hasn&apos;t The invite link stays valid as long as it hasn&apos;t
expired. expired.
</p> </p>
</div> </div>
<DialogFooter className="bg-slate-900 border-slate-700"> <DialogFooter className="bg-popover border-border">
<Button <Button
variant="outline" variant="outline"
onClick={() => setConflictMessage(null)} onClick={() => setConflictMessage(null)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-popover-foreground hover:bg-muted"
> >
Stay signed in Stay signed in
</Button> </Button>
@ -409,7 +409,7 @@ export default function JoinPage() {
// ----- Not authed: prompt to sign up or sign in ----- // ----- Not authed: prompt to sign up or sign in -----
return ( return (
<Card className="w-full max-w-md border-slate-800 bg-slate-900"> <Card className="w-full max-w-md border-border bg-card">
{inviteHeader} {inviteHeader}
<CardContent className="flex flex-col gap-2"> <CardContent className="flex flex-col gap-2">
<Link href={`/signup?invite=${encodeURIComponent(token!)}`}> <Link href={`/signup?invite=${encodeURIComponent(token!)}`}>
@ -420,7 +420,7 @@ export default function JoinPage() {
<Link href={`/login?invite=${encodeURIComponent(token!)}`}> <Link href={`/login?invite=${encodeURIComponent(token!)}`}>
<Button <Button
variant="outline" variant="outline"
className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white" className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
> >
I already have an account I already have an account
</Button> </Button>

View File

@ -37,7 +37,7 @@ export const metadata: Metadata = {
export default function JoinLayout({ children }: { children: ReactNode }) { export default function JoinLayout({ children }: { children: ReactNode }) {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4"> <div className="flex min-h-screen items-center justify-center bg-background px-4">
{children} {children}
</div> </div>
); );

View File

@ -1,10 +1,17 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import Script from "next/script"; import Script from "next/script";
import { Toaster } from "sonner";
import "./globals.css"; import "./globals.css";
import { ThemeProvider } from "@/hooks/use-theme"; import { ThemeProvider } from "@/hooks/use-theme";
import { DEFAULT_THEME, STORAGE_KEY, THEME_IDS } from "@/lib/themes"; import { ThemedToaster } from "@/components/themed-toaster";
import {
DEFAULT_MODE,
DEFAULT_THEME,
MODE_STORAGE_KEY,
MODES,
STORAGE_KEY,
THEME_IDS,
} from "@/lib/themes";
const inter = Inter({ const inter = Inter({
variable: "--font-sans", variable: "--font-sans",
@ -33,29 +40,37 @@ export const metadata: Metadata = {
export const viewport: Viewport = { export const viewport: Viewport = {
themeColor: "#020617", themeColor: "#020617",
colorScheme: "dark", colorScheme: "dark light",
}; };
// Inline boot script — runs before React hydrates so the user's // Inline boot script — runs before React hydrates so the user's
// chosen theme is on the <html> element before first paint. Without // chosen accent (data-theme) AND mode (data-mode) are on the <html>
// this every page load flashes the default Violet for a frame before // element before first paint. Without this every page load flashes
// the React tree mounts and applies the picked theme. // the server-rendered defaults for a frame before the React tree
// mounts and applies the picked values.
// //
// Kept dependency-free (no imports, no JSX) — must be a string the // Kept dependency-free (no imports, no JSX) — must be a string the
// browser can run as a single <script>. Knowledge of valid theme IDs // browser can run as a single <script>. Knowledge of valid ids is
// is sourced from the THEME_IDS constant so adding a theme doesn't // sourced from the THEME_IDS / MODES constants so adding one doesn't
// silently break the boot path. // silently break the boot path.
const THEME_BOOT_SCRIPT = ` const THEME_BOOT_SCRIPT = `
(function(){ (function(){
var d = document.documentElement;
try { try {
var STORAGE_KEY = ${JSON.stringify(STORAGE_KEY)}; var THEME_KEY = ${JSON.stringify(STORAGE_KEY)};
var DEFAULT = ${JSON.stringify(DEFAULT_THEME)}; var THEME_DEFAULT = ${JSON.stringify(DEFAULT_THEME)};
var ALLOWED = ${JSON.stringify(THEME_IDS)}; var THEMES = ${JSON.stringify(THEME_IDS)};
var saved = localStorage.getItem(STORAGE_KEY); var savedTheme = localStorage.getItem(THEME_KEY);
var theme = ALLOWED.indexOf(saved) !== -1 ? saved : DEFAULT; d.dataset.theme = THEMES.indexOf(savedTheme) !== -1 ? savedTheme : THEME_DEFAULT;
document.documentElement.dataset.theme = theme;
var MODE_KEY = ${JSON.stringify(MODE_STORAGE_KEY)};
var MODE_DEFAULT = ${JSON.stringify(DEFAULT_MODE)};
var MODES = ${JSON.stringify(MODES)};
var savedMode = localStorage.getItem(MODE_KEY);
d.dataset.mode = MODES.indexOf(savedMode) !== -1 ? savedMode : MODE_DEFAULT;
} catch (_e) { } catch (_e) {
document.documentElement.dataset.theme = ${JSON.stringify(DEFAULT_THEME)}; d.dataset.theme = ${JSON.stringify(DEFAULT_THEME)};
d.dataset.mode = ${JSON.stringify(DEFAULT_MODE)};
} }
})(); })();
`; `;
@ -69,13 +84,15 @@ export default function RootLayout({
<html <html
lang="en" lang="en"
data-theme={DEFAULT_THEME} data-theme={DEFAULT_THEME}
data-mode={DEFAULT_MODE}
className={`${inter.variable} h-full antialiased`} className={`${inter.variable} h-full antialiased`}
// The `theme-boot` script below rewrites `data-theme` on <html> // The `theme-boot` script below rewrites `data-theme` and
// from localStorage before React hydrates, so for any non-default // `data-mode` on <html> from localStorage before React hydrates,
// theme the client DOM intentionally differs from the server- // so for any non-default choice the client DOM intentionally
// rendered `DEFAULT_THEME`. suppressHydrationWarning silences the // differs from the server-rendered defaults. suppressHydration-
// expected mismatch — it only applies to this element's own // Warning silences the expected mismatch — it only applies to
// attributes, so genuine mismatches in children still surface. // this element's own attributes, so genuine mismatches in
// children still surface.
suppressHydrationWarning suppressHydrationWarning
> >
<head> <head>
@ -88,17 +105,7 @@ export default function RootLayout({
<body className="min-h-full bg-background text-foreground font-sans"> <body className="min-h-full bg-background text-foreground font-sans">
<ThemeProvider> <ThemeProvider>
{children} {children}
<Toaster <ThemedToaster />
theme="dark"
position="top-right"
toastOptions={{
style: {
background: "rgb(30 41 59)",
border: "1px solid rgb(51 65 85)",
color: "white",
},
}}
/>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>

View File

@ -95,7 +95,7 @@ const STEP_META: Record<AutomationStepType, StepMeta> = {
assign_conversation: { label: "Assign Conversation", icon: UserCheck, border: "border-l-primary" }, assign_conversation: { label: "Assign Conversation", icon: UserCheck, border: "border-l-primary" },
update_contact_field: { label: "Update Contact Field", icon: PencilLine, border: "border-l-primary" }, update_contact_field: { label: "Update Contact Field", icon: PencilLine, border: "border-l-primary" },
create_deal: { label: "Create Deal", icon: Briefcase, border: "border-l-primary" }, create_deal: { label: "Create Deal", icon: Briefcase, border: "border-l-primary" },
wait: { label: "Wait", icon: Hourglass, border: "border-l-slate-500" }, wait: { label: "Wait", icon: Hourglass, border: "border-l-border" },
condition: { label: "Condition (If/Else)", icon: GitBranch, border: "border-l-amber-500" }, condition: { label: "Condition (If/Else)", icon: GitBranch, border: "border-l-amber-500" },
send_webhook: { label: "Send Webhook", icon: Webhook, border: "border-l-primary" }, send_webhook: { label: "Send Webhook", icon: Webhook, border: "border-l-primary" },
close_conversation: { label: "Close Conversation", icon: CircleSlash, border: "border-l-primary" }, close_conversation: { label: "Close Conversation", icon: CircleSlash, border: "border-l-primary" },
@ -251,7 +251,7 @@ function ResourcesProvider({ children }: { children: ReactNode }) {
} }
const SELECT_CLASS = const SELECT_CLASS =
"w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white focus:border-primary focus:outline-none" "w-full rounded-md border border-border bg-muted px-2 py-1.5 text-sm text-foreground focus:border-primary focus:outline-none"
/** Tag dropdown by name + color, storing the tag's id. Falls back to a /** Tag dropdown by name + color, storing the tag's id. Falls back to a
* raw id input when no tags exist yet. */ * raw id input when no tags exist yet. */
@ -269,7 +269,7 @@ function TagSelect({
placeholder="Tag id" placeholder="Tag id"
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
) )
} }
@ -277,7 +277,7 @@ function TagSelect({
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
className="h-3 w-3 shrink-0 rounded-full border border-slate-600" className="h-3 w-3 shrink-0 rounded-full border border-border"
style={{ backgroundColor: selected?.color ?? "transparent" }} style={{ backgroundColor: selected?.color ?? "transparent" }}
aria-hidden aria-hidden
/> />
@ -358,7 +358,7 @@ function AgentSelect({
placeholder="Agent id" placeholder="Agent id"
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
) )
} }
@ -405,7 +405,7 @@ function SendTemplateFields({
onChange={(e) => onChange={(e) =>
onChange({ template_name: e.target.value, language }) onChange({ template_name: e.target.value, language })
} }
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
</FieldBlock> </FieldBlock>
<FieldBlock label="Language"> <FieldBlock label="Language">
@ -414,7 +414,7 @@ function SendTemplateFields({
onChange={(e) => onChange={(e) =>
onChange({ template_name: templateName, language: e.target.value }) onChange({ template_name: templateName, language: e.target.value })
} }
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
</FieldBlock> </FieldBlock>
</> </>
@ -548,15 +548,15 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) {
} }
return ( return (
<div className="fixed inset-0 flex flex-col bg-slate-950"> <div className="fixed inset-0 flex flex-col bg-background">
{/* Top bar. At sub-sm widths the "Active" label is hidden and the {/* Top bar. At sub-sm widths the "Active" label is hidden and the
switch moves to the right of the save button, so the name input switch moves to the right of the save button, so the name input
gets maximum width. */} gets maximum width. */}
<header className="flex flex-shrink-0 items-center gap-2 border-b border-slate-800 bg-slate-900/80 px-3 py-3 sm:gap-3 sm:px-4"> <header className="flex flex-shrink-0 items-center gap-2 border-b border-border bg-card/80 px-3 py-3 sm:gap-3 sm:px-4">
<button <button
type="button" type="button"
onClick={() => router.push("/automations")} onClick={() => router.push("/automations")}
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-slate-800 hover:text-white" className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
aria-label="Back to automations" aria-label="Back to automations"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
@ -565,9 +565,9 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) {
value={state.name} value={state.name}
onChange={(e) => patchTop("name", e.target.value)} onChange={(e) => patchTop("name", e.target.value)}
placeholder="Untitled automation" placeholder="Untitled automation"
className="min-w-0 flex-1 rounded-md bg-transparent px-2 py-1 text-sm font-semibold text-white placeholder:text-slate-500 focus:bg-slate-800 focus:outline-none sm:text-base" className="min-w-0 flex-1 rounded-md bg-transparent px-2 py-1 text-sm font-semibold text-foreground placeholder:text-muted-foreground focus:bg-muted focus:outline-none sm:text-base"
/> />
<div className="flex items-center gap-2 text-xs text-slate-400"> <div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className="hidden sm:inline">Active</span> <span className="hidden sm:inline">Active</span>
<Switch <Switch
checked={state.is_active} checked={state.is_active}
@ -633,7 +633,7 @@ function TriggerCard({
// Card width: full on mobile, fixed 320px on sm+. The canvas wrapper // Card width: full on mobile, fixed 320px on sm+. The canvas wrapper
// (max-w-2xl + px-4) keeps this tidy on tablet/desktop. // (max-w-2xl + px-4) keeps this tidy on tablet/desktop.
<div className="z-10 w-full max-w-[320px] sm:w-80"> <div className="z-10 w-full max-w-[320px] sm:w-80">
<div className="rounded-lg border border-slate-800 border-l-4 border-l-blue-500 bg-slate-900 shadow-lg"> <div className="rounded-lg border border-border border-l-4 border-l-blue-500 bg-card shadow-lg">
<button <button
type="button" type="button"
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
@ -644,24 +644,24 @@ function TriggerCard({
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="text-[11px] uppercase tracking-wide text-blue-300">Trigger</div> <div className="text-[11px] uppercase tracking-wide text-blue-300">Trigger</div>
<div className="truncate text-sm font-medium text-white"> <div className="truncate text-sm font-medium text-foreground">
{TRIGGER_OPTIONS.find((o) => o.value === type)?.label ?? type} {TRIGGER_OPTIONS.find((o) => o.value === type)?.label ?? type}
</div> </div>
</div> </div>
<ChevronDown <ChevronDown
className={cn("h-4 w-4 text-slate-400 transition-transform", open && "rotate-180")} className={cn("h-4 w-4 text-muted-foreground transition-transform", open && "rotate-180")}
/> />
</button> </button>
{open && ( {open && (
<div className="space-y-3 border-t border-slate-800 px-4 py-3"> <div className="space-y-3 border-t border-border px-4 py-3">
<div> <div>
<label className="mb-1 block text-xs font-medium text-slate-400"> <label className="mb-1 block text-xs font-medium text-muted-foreground">
Trigger type Trigger type
</label> </label>
<select <select
value={type} value={type}
onChange={(e) => onTypeChange(e.target.value as AutomationTriggerType)} onChange={(e) => onTypeChange(e.target.value as AutomationTriggerType)}
className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white focus:border-primary focus:outline-none" className="w-full rounded-md border border-border bg-muted px-2 py-1.5 text-sm text-foreground focus:border-primary focus:outline-none"
> >
{TRIGGER_OPTIONS.map((o) => ( {TRIGGER_OPTIONS.map((o) => (
<option key={o.value} value={o.value}> <option key={o.value} value={o.value}>
@ -669,7 +669,7 @@ function TriggerCard({
</option> </option>
))} ))}
</select> </select>
<p className="mt-1 text-[11px] text-slate-500"> <p className="mt-1 text-[11px] text-muted-foreground">
{TRIGGER_OPTIONS.find((o) => o.value === type)?.hint} {TRIGGER_OPTIONS.find((o) => o.value === type)?.hint}
</p> </p>
</div> </div>
@ -681,7 +681,7 @@ function TriggerCard({
)} )}
{type === "tag_added" && ( {type === "tag_added" && (
<div> <div>
<label className="mb-1 block text-xs font-medium text-slate-400"> <label className="mb-1 block text-xs font-medium text-muted-foreground">
Tag Tag
</label> </label>
<TagSelect <TagSelect
@ -697,7 +697,7 @@ function TriggerCard({
onChange={(e) => onChange={(e) =>
onConfigChange({ ...config, schedule: e.target.value }) onConfigChange({ ...config, schedule: e.target.value })
} }
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
)} )}
</div> </div>
@ -735,7 +735,7 @@ function KeywordMatchConfig({
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div> <div>
<label className="mb-1 block text-xs font-medium text-slate-400"> <label className="mb-1 block text-xs font-medium text-muted-foreground">
Keywords (comma-separated) Keywords (comma-separated)
</label> </label>
<Input <Input
@ -749,17 +749,17 @@ function KeywordMatchConfig({
} }
}} }}
placeholder="e.g. pricing, demo request, talk to sales" placeholder="e.g. pricing, demo request, talk to sales"
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
</div> </div>
<div> <div>
<label className="mb-1 block text-xs font-medium text-slate-400"> <label className="mb-1 block text-xs font-medium text-muted-foreground">
Match type Match type
</label> </label>
<select <select
value={config?.match_type ?? "contains"} value={config?.match_type ?? "contains"}
onChange={(e) => onChange({ ...config, match_type: e.target.value as "exact" | "contains" })} onChange={(e) => onChange({ ...config, match_type: e.target.value as "exact" | "contains" })}
className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white focus:outline-none" className="w-full rounded-md border border-border bg-muted px-2 py-1.5 text-sm text-foreground focus:outline-none"
> >
<option value="contains">Contains</option> <option value="contains">Contains</option>
<option value="exact">Exact</option> <option value="exact">Exact</option>
@ -858,7 +858,7 @@ function StepRenderer({
<div className={cn("z-10 flex flex-col", width)}> <div className={cn("z-10 flex flex-col", width)}>
<div <div
className={cn( className={cn(
"rounded-lg border border-slate-800 border-l-4 bg-slate-900 shadow-lg", "rounded-lg border border-border border-l-4 bg-card shadow-lg",
meta.border, meta.border,
)} )}
> >
@ -867,28 +867,28 @@ function StepRenderer({
onClick={() => props.setExpandedId(expanded ? null : step.cid)} onClick={() => props.setExpandedId(expanded ? null : step.cid)}
className="flex w-full items-center gap-3 px-4 py-3 text-left" className="flex w-full items-center gap-3 px-4 py-3 text-left"
> >
<GripVertical className="h-4 w-4 flex-shrink-0 text-slate-600" aria-hidden /> <GripVertical className="h-4 w-4 flex-shrink-0 text-muted-foreground" aria-hidden />
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-slate-800 text-slate-300"> <div className="flex h-8 w-8 items-center justify-center rounded-md bg-muted text-muted-foreground">
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="text-[11px] uppercase tracking-wide text-slate-400"> <div className="text-[11px] uppercase tracking-wide text-muted-foreground">
{isCondition ? "Condition" : step.step_type === "wait" ? "Wait" : "Action"} {isCondition ? "Condition" : step.step_type === "wait" ? "Wait" : "Action"}
</div> </div>
<div className="truncate text-sm font-medium text-white">{meta.label}</div> <div className="truncate text-sm font-medium text-foreground">{meta.label}</div>
<div className="truncate text-[11px] text-slate-500">{previewFor(step)}</div> <div className="truncate text-[11px] text-muted-foreground">{previewFor(step)}</div>
</div> </div>
<ChevronDown <ChevronDown
className={cn("h-4 w-4 text-slate-400 transition-transform", expanded && "rotate-180")} className={cn("h-4 w-4 text-muted-foreground transition-transform", expanded && "rotate-180")}
/> />
</button> </button>
{expanded && ( {expanded && (
<div className="border-t border-slate-800 px-4 py-3"> <div className="border-t border-border px-4 py-3">
<StepEditor <StepEditor
step={step} step={step}
onChange={(next) => props.updateStep(path, () => next)} onChange={(next) => props.updateStep(path, () => next)}
/> />
<div className="mt-3 flex items-center justify-between gap-2 border-t border-slate-800 pt-3"> <div className="mt-3 flex items-center justify-between gap-2 border-t border-border pt-3">
<div className="flex gap-1"> <div className="flex gap-1">
<Button <Button
variant="ghost" variant="ghost"
@ -990,17 +990,17 @@ function BranchColumn({
function AddButton({ onPick }: { onPick: (t: AutomationStepType) => void }) { function AddButton({ onPick }: { onPick: (t: AutomationStepType) => void }) {
return ( return (
<div className="relative flex flex-col items-center"> <div className="relative flex flex-col items-center">
<div className="h-4 w-[2px] bg-slate-700" aria-hidden /> <div className="h-4 w-[2px] bg-border" aria-hidden />
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-dashed border-slate-700 bg-slate-950 text-slate-400 transition-colors hover:border-primary hover:bg-primary/10 hover:text-primary data-[popup-open]:border-primary data-[popup-open]:bg-primary/20 data-[popup-open]:text-primary" className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-dashed border-border bg-background text-muted-foreground transition-colors hover:border-primary hover:bg-primary/10 hover:text-primary data-[popup-open]:border-primary data-[popup-open]:bg-primary/20 data-[popup-open]:text-primary"
aria-label="Add step" aria-label="Add step"
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="start" align="start"
className="max-h-80 min-w-56 overflow-y-auto border-slate-700 bg-slate-900" className="max-h-80 min-w-56 overflow-y-auto border-border bg-popover"
> >
{ADDABLE_STEPS.map((t) => { {ADDABLE_STEPS.map((t) => {
const Icon = STEP_META[t].icon const Icon = STEP_META[t].icon
@ -1013,7 +1013,7 @@ function AddButton({ onPick }: { onPick: (t: AutomationStepType) => void }) {
})} })}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<div className="h-4 w-[2px] bg-slate-700" aria-hidden /> <div className="h-4 w-[2px] bg-border" aria-hidden />
</div> </div>
) )
} }
@ -1041,7 +1041,7 @@ function StepEditor({
value={(cfg.text as string) ?? ""} value={(cfg.text as string) ?? ""}
onChange={(e) => set({ text: e.target.value })} onChange={(e) => set({ text: e.target.value })}
placeholder="Hi! Thanks for reaching out…" placeholder="Hi! Thanks for reaching out…"
className="min-h-24 bg-slate-800 text-white" className="min-h-24 bg-muted text-foreground"
/> />
</FieldBlock> </FieldBlock>
) )
@ -1070,7 +1070,7 @@ function StepEditor({
<select <select
value={(cfg.mode as string) ?? "round_robin"} value={(cfg.mode as string) ?? "round_robin"}
onChange={(e) => set({ mode: e.target.value })} onChange={(e) => set({ mode: e.target.value })}
className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white" className="w-full rounded-md border border-border bg-muted px-2 py-1.5 text-sm text-foreground"
> >
<option value="round_robin">Round-robin</option> <option value="round_robin">Round-robin</option>
<option value="specific">Specific agent</option> <option value="specific">Specific agent</option>
@ -1100,7 +1100,7 @@ function StepEditor({
value={(cfg.value as string) ?? ""} value={(cfg.value as string) ?? ""}
onChange={(e) => set({ value: e.target.value })} onChange={(e) => set({ value: e.target.value })}
placeholder="Text or {{ vars.x }} / {{ message.text }}" placeholder="Text or {{ vars.x }} / {{ message.text }}"
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
</FieldBlock> </FieldBlock>
</> </>
@ -1112,21 +1112,21 @@ function StepEditor({
<Input <Input
value={(cfg.pipeline_id as string) ?? ""} value={(cfg.pipeline_id as string) ?? ""}
onChange={(e) => set({ pipeline_id: e.target.value })} onChange={(e) => set({ pipeline_id: e.target.value })}
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
</FieldBlock> </FieldBlock>
<FieldBlock label="Stage id"> <FieldBlock label="Stage id">
<Input <Input
value={(cfg.stage_id as string) ?? ""} value={(cfg.stage_id as string) ?? ""}
onChange={(e) => set({ stage_id: e.target.value })} onChange={(e) => set({ stage_id: e.target.value })}
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
</FieldBlock> </FieldBlock>
<FieldBlock label="Title"> <FieldBlock label="Title">
<Input <Input
value={(cfg.title as string) ?? ""} value={(cfg.title as string) ?? ""}
onChange={(e) => set({ title: e.target.value })} onChange={(e) => set({ title: e.target.value })}
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
</FieldBlock> </FieldBlock>
<FieldBlock label="Value"> <FieldBlock label="Value">
@ -1134,7 +1134,7 @@ function StepEditor({
type="number" type="number"
value={(cfg.value as number) ?? 0} value={(cfg.value as number) ?? 0}
onChange={(e) => set({ value: Number(e.target.value) })} onChange={(e) => set({ value: Number(e.target.value) })}
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
</FieldBlock> </FieldBlock>
</> </>
@ -1148,14 +1148,14 @@ function StepEditor({
min={1} min={1}
value={(cfg.amount as number) ?? 1} value={(cfg.amount as number) ?? 1}
onChange={(e) => set({ amount: Math.max(1, Number(e.target.value)) })} onChange={(e) => set({ amount: Math.max(1, Number(e.target.value)) })}
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
</FieldBlock> </FieldBlock>
<FieldBlock label="Unit"> <FieldBlock label="Unit">
<select <select
value={(cfg.unit as string) ?? "hours"} value={(cfg.unit as string) ?? "hours"}
onChange={(e) => set({ unit: e.target.value })} onChange={(e) => set({ unit: e.target.value })}
className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white" className="w-full rounded-md border border-border bg-muted px-2 py-1.5 text-sm text-foreground"
> >
<option value="minutes">Minutes</option> <option value="minutes">Minutes</option>
<option value="hours">Hours</option> <option value="hours">Hours</option>
@ -1171,7 +1171,7 @@ function StepEditor({
<select <select
value={(cfg.subject as string) ?? "tag_presence"} value={(cfg.subject as string) ?? "tag_presence"}
onChange={(e) => set({ subject: e.target.value })} onChange={(e) => set({ subject: e.target.value })}
className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white" className="w-full rounded-md border border-border bg-muted px-2 py-1.5 text-sm text-foreground"
> >
<option value="tag_presence">Tag presence</option> <option value="tag_presence">Tag presence</option>
<option value="contact_field">Contact field</option> <option value="contact_field">Contact field</option>
@ -1192,7 +1192,7 @@ function StepEditor({
} }
value={(cfg.operand as string) ?? ""} value={(cfg.operand as string) ?? ""}
onChange={(e) => set({ operand: e.target.value })} onChange={(e) => set({ operand: e.target.value })}
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
</FieldBlock> </FieldBlock>
{(cfg.subject === "contact_field" || cfg.subject === "message_content") && ( {(cfg.subject === "contact_field" || cfg.subject === "message_content") && (
@ -1200,7 +1200,7 @@ function StepEditor({
<Input <Input
value={(cfg.value as string) ?? ""} value={(cfg.value as string) ?? ""}
onChange={(e) => set({ value: e.target.value })} onChange={(e) => set({ value: e.target.value })}
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
</FieldBlock> </FieldBlock>
)} )}
@ -1213,21 +1213,21 @@ function StepEditor({
<Input <Input
value={(cfg.url as string) ?? ""} value={(cfg.url as string) ?? ""}
onChange={(e) => set({ url: e.target.value })} onChange={(e) => set({ url: e.target.value })}
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
</FieldBlock> </FieldBlock>
<FieldBlock label="Body template (JSON)"> <FieldBlock label="Body template (JSON)">
<Textarea <Textarea
value={(cfg.body_template as string) ?? ""} value={(cfg.body_template as string) ?? ""}
onChange={(e) => set({ body_template: e.target.value })} onChange={(e) => set({ body_template: e.target.value })}
className="min-h-20 bg-slate-800 font-mono text-xs text-white" className="min-h-20 bg-muted font-mono text-xs text-foreground"
/> />
</FieldBlock> </FieldBlock>
</> </>
) )
case "close_conversation": case "close_conversation":
return ( return (
<p className="text-xs text-slate-400"> <p className="text-xs text-muted-foreground">
Sets the conversation status to &quot;closed&quot;. No configuration needed. Sets the conversation status to &quot;closed&quot;. No configuration needed.
</p> </p>
) )
@ -1245,7 +1245,7 @@ function FieldBlock({
}) { }) {
return ( return (
<div className="mb-2 last:mb-0"> <div className="mb-2 last:mb-0">
<label className="mb-1 block text-xs font-medium text-slate-400">{label}</label> <label className="mb-1 block text-xs font-medium text-muted-foreground">{label}</label>
{children} {children}
</div> </div>
) )

View File

@ -68,17 +68,17 @@ export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h2 className="text-lg font-semibold text-white">Choose a Template</h2> <h2 className="text-lg font-semibold text-foreground">Choose a Template</h2>
<p className="mt-1 text-sm text-slate-400"> <p className="mt-1 text-sm text-muted-foreground">
Select an approved message template for your broadcast. Select an approved message template for your broadcast.
</p> </p>
</div> </div>
{templates.length === 0 ? ( {templates.length === 0 ? (
<div className="flex h-48 flex-col items-center justify-center rounded-xl border border-slate-800 bg-slate-900/50"> <div className="flex h-48 flex-col items-center justify-center rounded-xl border border-border bg-card/50">
<FileText className="mb-2 h-8 w-8 text-slate-600" /> <FileText className="mb-2 h-8 w-8 text-muted-foreground" />
<p className="text-sm text-slate-400">No templates available.</p> <p className="text-sm text-muted-foreground">No templates available.</p>
<p className="mt-1 text-xs text-slate-500">Create a template in Settings first.</p> <p className="mt-1 text-xs text-muted-foreground">Create a template in Settings first.</p>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
@ -93,19 +93,19 @@ export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack
className={`flex flex-col gap-3 rounded-xl border p-4 text-left transition-all ${ className={`flex flex-col gap-3 rounded-xl border p-4 text-left transition-all ${
isSelected isSelected
? 'border-primary bg-primary/5 ring-1 ring-primary/30' ? 'border-primary bg-primary/5 ring-1 ring-primary/30'
: 'border-slate-800 bg-slate-900/50 hover:border-slate-700 hover:bg-slate-900' : 'border-border bg-card/50 hover:border-border hover:bg-card'
}`} }`}
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<h3 className="text-sm font-medium text-white">{template.name}</h3> <h3 className="text-sm font-medium text-foreground">{template.name}</h3>
<span <span
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium ${catColor}`} className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium ${catColor}`}
> >
{template.category} {template.category}
</span> </span>
</div> </div>
<p className="line-clamp-3 text-xs text-slate-400">{template.body_text}</p> <p className="line-clamp-3 text-xs text-muted-foreground">{template.body_text}</p>
<div className="flex items-center gap-2 text-[10px] text-slate-500"> <div className="flex items-center gap-2 text-[10px] text-muted-foreground">
<span>{template.language ?? 'en_US'}</span> <span>{template.language ?? 'en_US'}</span>
{/* Status is omitted on purpose — every template {/* Status is omitted on purpose — every template
shown here is already filtered to APPROVED, shown here is already filtered to APPROVED,
@ -117,8 +117,8 @@ export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack
</div> </div>
)} )}
<div className="flex items-center justify-between border-t border-slate-800 pt-4"> <div className="flex items-center justify-between border-t border-border pt-4">
<Button variant="outline" onClick={onBack} className="border-slate-700 text-slate-300"> <Button variant="outline" onClick={onBack} className="border-border text-muted-foreground">
Back Back
</Button> </Button>
<Button <Button

View File

@ -249,8 +249,8 @@ export function Step2SelectAudience({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h2 className="text-lg font-semibold text-white">Select Audience</h2> <h2 className="text-lg font-semibold text-foreground">Select Audience</h2>
<p className="mt-1 text-sm text-slate-400"> <p className="mt-1 text-sm text-muted-foreground">
Choose who will receive this broadcast. Choose who will receive this broadcast.
</p> </p>
</div> </div>
@ -280,21 +280,21 @@ export function Step2SelectAudience({
className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${ className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${
isSelected isSelected
? 'border-primary bg-primary/5 ring-1 ring-primary/30' ? 'border-primary bg-primary/5 ring-1 ring-primary/30'
: 'border-slate-800 bg-slate-900/50 hover:border-slate-700' : 'border-border bg-card/50 hover:border-border'
}`} }`}
> >
<div <div
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${ className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${
isSelected isSelected
? 'bg-primary/10 text-primary' ? 'bg-primary/10 text-primary'
: 'bg-slate-800 text-slate-400' : 'bg-muted text-muted-foreground'
}`} }`}
> >
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
</div> </div>
<div> <div>
<p className="text-sm font-medium text-white">{option.label}</p> <p className="text-sm font-medium text-foreground">{option.label}</p>
<p className="mt-0.5 text-xs text-slate-400"> <p className="mt-0.5 text-xs text-muted-foreground">
{option.description} {option.description}
</p> </p>
</div> </div>
@ -304,12 +304,12 @@ export function Step2SelectAudience({
</div> </div>
{audience.type === 'tags' && ( {audience.type === 'tags' && (
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4"> <div className="rounded-xl border border-border bg-card/50 p-4">
<p className="mb-3 text-sm font-medium text-white">Select Tags</p> <p className="mb-3 text-sm font-medium text-foreground">Select Tags</p>
{loadingTags ? ( {loadingTags ? (
<Loader2 className="h-5 w-5 animate-spin text-primary" /> <Loader2 className="h-5 w-5 animate-spin text-primary" />
) : tags.length === 0 ? ( ) : tags.length === 0 ? (
<p className="text-xs text-slate-400"> <p className="text-xs text-muted-foreground">
No tags found. Create tags in Settings. No tags found. Create tags in Settings.
</p> </p>
) : ( ) : (
@ -323,7 +323,7 @@ export function Step2SelectAudience({
className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-all ${ className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-all ${
isSelected isSelected
? 'border-primary/30 bg-primary/10 text-primary' ? 'border-primary/30 bg-primary/10 text-primary'
: 'border-slate-700 bg-slate-800 text-slate-300 hover:border-slate-600' : 'border-border bg-muted text-muted-foreground hover:border-border'
}`} }`}
> >
<span <span
@ -340,12 +340,12 @@ export function Step2SelectAudience({
)} )}
{audience.type === 'custom_field' && ( {audience.type === 'custom_field' && (
<div className="space-y-3 rounded-xl border border-slate-800 bg-slate-900/50 p-4"> <div className="space-y-3 rounded-xl border border-border bg-card/50 p-4">
<p className="text-sm font-medium text-white">Custom Field Filter</p> <p className="text-sm font-medium text-foreground">Custom Field Filter</p>
{loadingFields ? ( {loadingFields ? (
<Loader2 className="h-5 w-5 animate-spin text-primary" /> <Loader2 className="h-5 w-5 animate-spin text-primary" />
) : customFields.length === 0 ? ( ) : customFields.length === 0 ? (
<p className="text-xs text-slate-400"> <p className="text-xs text-muted-foreground">
No custom fields defined. Create one in Settings → Custom Fields. No custom fields defined. Create one in Settings → Custom Fields.
</p> </p>
) : ( ) : (
@ -353,7 +353,7 @@ export function Step2SelectAudience({
<select <select
value={audience.customField?.fieldId ?? ''} value={audience.customField?.fieldId ?? ''}
onChange={(e) => updateCustomField({ fieldId: e.target.value })} onChange={(e) => updateCustomField({ fieldId: e.target.value })}
className="h-9 rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary focus:ring-1 focus:ring-primary" className="h-9 rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary"
> >
<option value="">Select field…</option> <option value="">Select field…</option>
{customFields.map((f) => ( {customFields.map((f) => (
@ -369,7 +369,7 @@ export function Step2SelectAudience({
operator: e.target.value as CustomFieldOperator, operator: e.target.value as CustomFieldOperator,
}) })
} }
className="h-9 rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary focus:ring-1 focus:ring-primary" className="h-9 rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary"
> >
{OPERATOR_OPTIONS.map((op) => ( {OPERATOR_OPTIONS.map((op) => (
<option key={op.value} value={op.value}> <option key={op.value} value={op.value}>
@ -382,7 +382,7 @@ export function Step2SelectAudience({
value={audience.customField?.value ?? ''} value={audience.customField?.value ?? ''}
onChange={(e) => updateCustomField({ value: e.target.value })} onChange={(e) => updateCustomField({ value: e.target.value })}
placeholder="Value" placeholder="Value"
className="h-9 rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none placeholder:text-slate-500 focus:border-primary focus:ring-1 focus:ring-primary" className="h-9 rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary"
/> />
</div> </div>
)} )}
@ -390,16 +390,16 @@ export function Step2SelectAudience({
)} )}
{/* Exclude list — applies regardless of audience type */} {/* Exclude list — applies regardless of audience type */}
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4"> <div className="rounded-xl border border-border bg-card/50 p-4">
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<X className="h-4 w-4 text-red-400" /> <X className="h-4 w-4 text-red-400" />
<p className="text-sm font-medium text-white"> <p className="text-sm font-medium text-foreground">
Exclude contacts with these tags Exclude contacts with these tags
</p> </p>
<span className="text-xs text-slate-500">(optional)</span> <span className="text-xs text-muted-foreground">(optional)</span>
</div> </div>
{tags.length === 0 ? ( {tags.length === 0 ? (
<p className="text-xs text-slate-500">No tags available.</p> <p className="text-xs text-muted-foreground">No tags available.</p>
) : ( ) : (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{tags.map((tag) => { {tags.map((tag) => {
@ -411,7 +411,7 @@ export function Step2SelectAudience({
className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-all ${ className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-all ${
isExcluded isExcluded
? 'border-red-500/30 bg-red-500/10 text-red-300' ? 'border-red-500/30 bg-red-500/10 text-red-300'
: 'border-slate-700 bg-slate-800 text-slate-300 hover:border-slate-600' : 'border-border bg-muted text-muted-foreground hover:border-border'
}`} }`}
> >
<span <span
@ -427,33 +427,33 @@ export function Step2SelectAudience({
</div> </div>
{/* Audience Summary */} {/* Audience Summary */}
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4"> <div className="rounded-xl border border-border bg-card/50 p-4">
<p className="mb-2 text-sm font-medium text-white">Audience Summary</p> <p className="mb-2 text-sm font-medium text-foreground">Audience Summary</p>
{loadingCount ? ( {loadingCount ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-primary" /> <Loader2 className="h-4 w-4 animate-spin text-primary" />
<span className="text-xs text-slate-400">Calculating…</span> <span className="text-xs text-muted-foreground">Calculating…</span>
</div> </div>
) : estimatedCount !== null ? ( ) : estimatedCount !== null ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Users className="h-4 w-4 text-primary" /> <Users className="h-4 w-4 text-primary" />
<span className="text-sm text-white"> <span className="text-sm text-foreground">
{estimatedCount.toLocaleString()} {estimatedCount.toLocaleString()}
</span> </span>
<span className="text-xs text-slate-400">estimated recipients</span> <span className="text-xs text-muted-foreground">estimated recipients</span>
</div> </div>
) : ( ) : (
<p className="text-xs text-slate-500"> <p className="text-xs text-muted-foreground">
Select an audience type to see the estimate. Select an audience type to see the estimate.
</p> </p>
)} )}
</div> </div>
<div className="flex items-center justify-between border-t border-slate-800 pt-4"> <div className="flex items-center justify-between border-t border-border pt-4">
<Button <Button
variant="outline" variant="outline"
onClick={onBack} onClick={onBack}
className="border-slate-700 text-slate-300" className="border-border text-muted-foreground"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
Back Back

View File

@ -187,16 +187,16 @@ export function Step3Personalize({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h2 className="text-lg font-semibold text-white">Personalize Message</h2> <h2 className="text-lg font-semibold text-foreground">Personalize Message</h2>
<p className="mt-1 text-sm text-slate-400"> <p className="mt-1 text-sm text-muted-foreground">
Map template variables to contact fields, custom fields, or static Map template variables to contact fields, custom fields, or static
values. values.
</p> </p>
</div> </div>
{placeholders.length === 0 ? ( {placeholders.length === 0 ? (
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-6 text-center"> <div className="rounded-xl border border-border bg-card/50 p-6 text-center">
<p className="text-sm text-slate-400"> <p className="text-sm text-muted-foreground">
This template has no variables to personalize. This template has no variables to personalize.
</p> </p>
</div> </div>
@ -209,7 +209,7 @@ export function Step3Personalize({
return ( return (
<div <div
key={placeholder} key={placeholder}
className="rounded-xl border border-slate-800 bg-slate-900/50 p-4" className="rounded-xl border border-border bg-card/50 p-4"
> >
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<span className="inline-flex items-center rounded-md bg-primary/10 px-2 py-0.5 text-xs font-mono font-medium text-primary"> <span className="inline-flex items-center rounded-md bg-primary/10 px-2 py-0.5 text-xs font-mono font-medium text-primary">
@ -219,7 +219,7 @@ export function Step3Personalize({
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div> <div>
<label className="mb-1.5 block text-xs font-medium text-slate-400"> <label className="mb-1.5 block text-xs font-medium text-muted-foreground">
Mapping Type Mapping Type
</label> </label>
<Select <Select
@ -231,10 +231,10 @@ export function Step3Personalize({
}) })
} }
> >
<SelectTrigger className="w-full border-slate-700 bg-slate-800 text-white"> <SelectTrigger className="w-full border-border bg-muted text-foreground">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent className="border-slate-700 bg-slate-800"> <SelectContent className="border-border bg-popover">
<SelectItem value="static">Static Value</SelectItem> <SelectItem value="static">Static Value</SelectItem>
<SelectItem value="field">Contact Field</SelectItem> <SelectItem value="field">Contact Field</SelectItem>
<SelectItem value="custom_field"> <SelectItem value="custom_field">
@ -245,7 +245,7 @@ export function Step3Personalize({
</div> </div>
<div> <div>
<label className="mb-1.5 block text-xs font-medium text-slate-400"> <label className="mb-1.5 block text-xs font-medium text-muted-foreground">
{mapping.type === 'static' ? 'Value' : 'Field'} {mapping.type === 'static' ? 'Value' : 'Field'}
</label> </label>
{mapping.type === 'static' ? ( {mapping.type === 'static' ? (
@ -255,7 +255,7 @@ export function Step3Personalize({
updateVariable(key, { value: e.target.value }) updateVariable(key, { value: e.target.value })
} }
placeholder="Enter value..." placeholder="Enter value..."
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500" className="border-border bg-muted text-foreground placeholder:text-muted-foreground"
/> />
) : mapping.type === 'field' ? ( ) : mapping.type === 'field' ? (
<Select <Select
@ -264,10 +264,10 @@ export function Step3Personalize({
updateVariable(key, { value: val || '' }) updateVariable(key, { value: val || '' })
} }
> >
<SelectTrigger className="w-full border-slate-700 bg-slate-800 text-white"> <SelectTrigger className="w-full border-border bg-muted text-foreground">
<SelectValue placeholder="Select field..." /> <SelectValue placeholder="Select field..." />
</SelectTrigger> </SelectTrigger>
<SelectContent className="border-slate-700 bg-slate-800"> <SelectContent className="border-border bg-popover">
{contactFields.map((field) => ( {contactFields.map((field) => (
<SelectItem key={field.value} value={field.value}> <SelectItem key={field.value} value={field.value}>
{field.label} {field.label}
@ -282,7 +282,7 @@ export function Step3Personalize({
updateVariable(key, { value: val || '' }) updateVariable(key, { value: val || '' })
} }
> >
<SelectTrigger className="w-full border-slate-700 bg-slate-800 text-white"> <SelectTrigger className="w-full border-border bg-muted text-foreground">
<SelectValue <SelectValue
placeholder={ placeholder={
loadingFields loadingFields
@ -293,7 +293,7 @@ export function Step3Personalize({
} }
/> />
</SelectTrigger> </SelectTrigger>
<SelectContent className="border-slate-700 bg-slate-800"> <SelectContent className="border-border bg-popover">
{customFields.map((f) => ( {customFields.map((f) => (
<SelectItem key={f.id} value={f.id}> <SelectItem key={f.id} value={f.id}>
{f.field_name} {f.field_name}
@ -312,11 +312,11 @@ export function Step3Personalize({
{/* Live Preview — rendered as a WhatsApp-style bubble so the user {/* Live Preview — rendered as a WhatsApp-style bubble so the user
sees approximately what the recipient will see. */} sees approximately what the recipient will see. */}
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4"> <div className="rounded-xl border border-border bg-card/50 p-4">
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<Eye className="h-4 w-4 text-primary" /> <Eye className="h-4 w-4 text-primary" />
<p className="text-sm font-medium text-white">Live Preview</p> <p className="text-sm font-medium text-foreground">Live Preview</p>
<span className="text-xs text-slate-500">({previewLabel})</span> <span className="text-xs text-muted-foreground">({previewLabel})</span>
{loadingPreview && ( {loadingPreview && (
<Loader2 className="h-3.5 w-3.5 animate-spin text-primary" /> <Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
)} )}
@ -340,11 +340,11 @@ export function Step3Personalize({
</div> </div>
)} )}
<div className="flex items-center justify-between border-t border-slate-800 pt-4"> <div className="flex items-center justify-between border-t border-border pt-4">
<Button <Button
variant="outline" variant="outline"
onClick={onBack} onClick={onBack}
className="border-slate-700 text-slate-300" className="border-border text-muted-foreground"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
Back Back

View File

@ -93,51 +93,51 @@ export function Step4ScheduleSend({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h2 className="text-lg font-semibold text-white">Review & Send</h2> <h2 className="text-lg font-semibold text-foreground">Review & Send</h2>
<p className="mt-1 text-sm text-slate-400"> <p className="mt-1 text-sm text-muted-foreground">
Name your broadcast, review the details, and send. Name your broadcast, review the details, and send.
</p> </p>
</div> </div>
{/* Broadcast Name */} {/* Broadcast Name */}
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-white">Broadcast Name</label> <label className="mb-1.5 block text-sm font-medium text-foreground">Broadcast Name</label>
<Input <Input
value={name} value={name}
onChange={(e) => onNameChange(e.target.value)} onChange={(e) => onNameChange(e.target.value)}
placeholder="e.g. Summer Sale Announcement" placeholder="e.g. Summer Sale Announcement"
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500" className="border-border bg-muted text-foreground placeholder:text-muted-foreground"
/> />
</div> </div>
{/* Summary Card */} {/* Summary Card */}
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4 space-y-3"> <div className="rounded-xl border border-border bg-card/50 p-4 space-y-3">
<p className="text-sm font-medium text-white">Summary</p> <p className="text-sm font-medium text-foreground">Summary</p>
<div className="grid grid-cols-2 gap-3 text-sm"> <div className="grid grid-cols-2 gap-3 text-sm">
<div> <div>
<p className="text-xs text-slate-400">Template</p> <p className="text-xs text-muted-foreground">Template</p>
<p className="text-white">{template.name}</p> <p className="text-foreground">{template.name}</p>
</div> </div>
<div> <div>
<p className="text-xs text-slate-400">Audience</p> <p className="text-xs text-muted-foreground">Audience</p>
<p className="text-white">{audienceLabel}</p> <p className="text-foreground">{audienceLabel}</p>
</div> </div>
<div> <div>
<p className="text-xs text-slate-400">Estimated Reach</p> <p className="text-xs text-muted-foreground">Estimated Reach</p>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{loadingReach ? ( {loadingReach ? (
<Loader2 className="h-3 w-3 animate-spin text-primary" /> <Loader2 className="h-3 w-3 animate-spin text-primary" />
) : ( ) : (
<> <>
<Users className="h-3.5 w-3.5 text-primary" /> <Users className="h-3.5 w-3.5 text-primary" />
<p className="font-medium text-white">{estimatedReach.toLocaleString()}</p> <p className="font-medium text-foreground">{estimatedReach.toLocaleString()}</p>
</> </>
)} )}
</div> </div>
</div> </div>
<div> <div>
<p className="text-xs text-slate-400">Language</p> <p className="text-xs text-muted-foreground">Language</p>
<p className="text-white">{template.language ?? 'en_US'}</p> <p className="text-foreground">{template.language ?? 'en_US'}</p>
</div> </div>
</div> </div>
</div> </div>
@ -148,11 +148,11 @@ export function Step4ScheduleSend({
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-primary" /> <Loader2 className="h-4 w-4 animate-spin text-primary" />
<p className="text-sm font-medium text-white">Sending broadcast...</p> <p className="text-sm font-medium text-foreground">Sending broadcast...</p>
</div> </div>
<span className="text-xs font-medium text-primary">{progress}%</span> <span className="text-xs font-medium text-primary">{progress}%</span>
</div> </div>
<div className="h-1.5 w-full rounded-full bg-slate-800"> <div className="h-1.5 w-full rounded-full bg-muted">
<div <div
className="h-1.5 rounded-full bg-primary transition-all duration-300" className="h-1.5 rounded-full bg-primary transition-all duration-300"
style={{ width: `${progress}%` }} style={{ width: `${progress}%` }}
@ -161,12 +161,12 @@ export function Step4ScheduleSend({
</div> </div>
)} )}
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-slate-800 pt-4"> <div className="flex flex-wrap items-center justify-between gap-2 border-t border-border pt-4">
<Button <Button
variant="outline" variant="outline"
onClick={onBack} onClick={onBack}
disabled={isProcessing} disabled={isProcessing}
className="border-slate-700 text-slate-300" className="border-border text-muted-foreground"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
Back Back
@ -178,7 +178,7 @@ export function Step4ScheduleSend({
variant="outline" variant="outline"
onClick={onSaveDraft} onClick={onSaveDraft}
disabled={!name.trim() || isProcessing} disabled={!name.trim() || isProcessing}
className="border-slate-700 text-slate-300 hover:bg-slate-800 disabled:opacity-50" className="border-border text-muted-foreground hover:bg-muted disabled:opacity-50"
> >
<Save className="h-4 w-4" /> <Save className="h-4 w-4" />
Save as Draft Save as Draft
@ -197,14 +197,14 @@ export function Step4ScheduleSend({
<Send className="h-4 w-4" /> <Send className="h-4 w-4" />
Send Broadcast Send Broadcast
</DialogTrigger> </DialogTrigger>
<DialogContent className="border-slate-700 bg-slate-900 sm:max-w-md"> <DialogContent className="border-border bg-popover sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white">Confirm Broadcast</DialogTitle> <DialogTitle className="text-popover-foreground">Confirm Broadcast</DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
You are about to send this broadcast to{' '} You are about to send this broadcast to{' '}
<span className="font-medium text-white">{estimatedReach.toLocaleString()}</span>{' '} <span className="font-medium text-popover-foreground">{estimatedReach.toLocaleString()}</span>{' '}
contacts using the{' '} contacts using the{' '}
<span className="font-medium text-white">{template.name}</span> template. <span className="font-medium text-popover-foreground">{template.name}</span> template.
This action cannot be undone. This action cannot be undone.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@ -212,7 +212,7 @@ export function Step4ScheduleSend({
<Button <Button
variant="outline" variant="outline"
onClick={() => setShowConfirm(false)} onClick={() => setShowConfirm(false)}
className="border-slate-700 text-slate-300" className="border-border text-muted-foreground"
> >
Cancel Cancel
</Button> </Button>

View File

@ -331,7 +331,7 @@ export function ContactDetailView({
<Sheet open={open} onOpenChange={onOpenChange}> <Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent <SheetContent
side="right" side="right"
className="bg-slate-900 border-slate-700 text-slate-200 sm:max-w-lg w-full p-0" className="bg-popover border-border text-popover-foreground sm:max-w-lg w-full p-0"
> >
{loading || !contact ? ( {loading || !contact ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
@ -340,21 +340,21 @@ export function ContactDetailView({
) : ( ) : (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* Header */} {/* Header */}
<SheetHeader className="p-4 border-b border-slate-700/50"> <SheetHeader className="p-4 border-b border-border/50">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar className="size-12 bg-slate-800 border border-slate-700"> <Avatar className="size-12 bg-muted border border-border">
<AvatarFallback className="bg-primary/10 text-primary text-sm font-medium"> <AvatarFallback className="bg-primary/10 text-primary text-sm font-medium">
{getInitials(contact.name)} {getInitials(contact.name)}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<SheetTitle className="text-white truncate"> <SheetTitle className="text-popover-foreground truncate">
{contact.name || 'Unknown'} {contact.name || 'Unknown'}
</SheetTitle> </SheetTitle>
<SheetDescription className="text-slate-400 text-xs mt-0.5"> <SheetDescription className="text-muted-foreground text-xs mt-0.5">
Contact details Contact details
</SheetDescription> </SheetDescription>
<div className="flex flex-wrap items-center gap-3 mt-1.5 text-xs text-slate-400"> <div className="flex flex-wrap items-center gap-3 mt-1.5 text-xs text-muted-foreground">
<button <button
onClick={copyPhone} onClick={copyPhone}
className="flex items-center gap-1 hover:text-primary transition-colors cursor-pointer" className="flex items-center gap-1 hover:text-primary transition-colors cursor-pointer"
@ -386,34 +386,34 @@ export function ContactDetailView({
{/* Tabs */} {/* Tabs */}
<Tabs defaultValue="details" className="flex-1 flex flex-col min-h-0"> <Tabs defaultValue="details" className="flex-1 flex flex-col min-h-0">
<TabsList className="bg-slate-800/50 border-b border-slate-700 mx-4 mt-3"> <TabsList className="bg-muted/50 border-b border-border mx-4 mt-3">
<TabsTrigger <TabsTrigger
value="details" value="details"
className="data-active:bg-slate-800 data-active:text-primary text-slate-400" className="data-active:bg-muted data-active:text-primary text-muted-foreground"
> >
Details Details
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="tags" value="tags"
className="data-active:bg-slate-800 data-active:text-primary text-slate-400" className="data-active:bg-muted data-active:text-primary text-muted-foreground"
> >
Tags Tags
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="notes" value="notes"
className="data-active:bg-slate-800 data-active:text-primary text-slate-400" className="data-active:bg-muted data-active:text-primary text-muted-foreground"
> >
Notes Notes
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="custom" value="custom"
className="data-active:bg-slate-800 data-active:text-primary text-slate-400" className="data-active:bg-muted data-active:text-primary text-muted-foreground"
> >
Custom Fields Custom Fields
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="deals" value="deals"
className="data-active:bg-slate-800 data-active:text-primary text-slate-400" className="data-active:bg-muted data-active:text-primary text-muted-foreground"
> >
Deals Deals
</TabsTrigger> </TabsTrigger>
@ -423,37 +423,37 @@ export function ContactDetailView({
<TabsContent value="details" className="flex-1 overflow-y-auto px-4 py-3"> <TabsContent value="details" className="flex-1 overflow-y-auto px-4 py-3">
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-slate-400 text-xs">Name</Label> <Label className="text-muted-foreground text-xs">Name</Label>
<Input <Input
value={editName} value={editName}
onChange={(e) => setEditName(e.target.value)} onChange={(e) => setEditName(e.target.value)}
className="bg-slate-800 border-slate-700 text-white h-8 text-sm" className="bg-muted border-border text-foreground h-8 text-sm"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-slate-400 text-xs"> <Label className="text-muted-foreground text-xs">
Phone <span className="text-red-400">*</span> Phone <span className="text-red-400">*</span>
</Label> </Label>
<Input <Input
value={editPhone} value={editPhone}
onChange={(e) => setEditPhone(e.target.value)} onChange={(e) => setEditPhone(e.target.value)}
className="bg-slate-800 border-slate-700 text-white h-8 text-sm" className="bg-muted border-border text-foreground h-8 text-sm"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-slate-400 text-xs">Email</Label> <Label className="text-muted-foreground text-xs">Email</Label>
<Input <Input
value={editEmail} value={editEmail}
onChange={(e) => setEditEmail(e.target.value)} onChange={(e) => setEditEmail(e.target.value)}
className="bg-slate-800 border-slate-700 text-white h-8 text-sm" className="bg-muted border-border text-foreground h-8 text-sm"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-slate-400 text-xs">Company</Label> <Label className="text-muted-foreground text-xs">Company</Label>
<Input <Input
value={editCompany} value={editCompany}
onChange={(e) => setEditCompany(e.target.value)} onChange={(e) => setEditCompany(e.target.value)}
className="bg-slate-800 border-slate-700 text-white h-8 text-sm" className="bg-muted border-border text-foreground h-8 text-sm"
/> />
</div> </div>
<Button <Button
@ -475,11 +475,11 @@ export function ContactDetailView({
{/* Tags Tab */} {/* Tags Tab */}
<TabsContent value="tags" className="flex-1 overflow-y-auto px-4 py-3"> <TabsContent value="tags" className="flex-1 overflow-y-auto px-4 py-3">
<div className="space-y-3"> <div className="space-y-3">
<p className="text-xs text-slate-400"> <p className="text-xs text-muted-foreground">
Click a tag to add or remove it from this contact. Click a tag to add or remove it from this contact.
</p> </p>
{allTags.length === 0 ? ( {allTags.length === 0 ? (
<p className="text-sm text-slate-500"> <p className="text-sm text-muted-foreground">
No tags available. Create tags in Settings. No tags available. Create tags in Settings.
</p> </p>
) : ( ) : (
@ -493,7 +493,7 @@ export function ContactDetailView({
disabled={savingTags} disabled={savingTags}
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium transition-all cursor-pointer ${ className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium transition-all cursor-pointer ${
selected selected
? 'ring-2 ring-primary ring-offset-1 ring-offset-slate-900' ? 'ring-2 ring-primary ring-offset-1 ring-offset-border'
: 'opacity-50 hover:opacity-80' : 'opacity-50 hover:opacity-80'
}`} }`}
style={{ style={{
@ -518,7 +518,7 @@ export function ContactDetailView({
value={newNote} value={newNote}
onChange={(e) => setNewNote(e.target.value)} onChange={(e) => setNewNote(e.target.value)}
placeholder="Write a note..." placeholder="Write a note..."
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 min-h-[60px] text-sm resize-none" className="bg-muted border-border text-foreground placeholder:text-muted-foreground min-h-[60px] text-sm resize-none"
/> />
<Button <Button
onClick={addNote} onClick={addNote}
@ -538,30 +538,30 @@ export function ContactDetailView({
<div className="flex-1 overflow-y-auto space-y-2"> <div className="flex-1 overflow-y-auto space-y-2">
{loadingNotes ? ( {loadingNotes ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="size-5 animate-spin text-slate-500" /> <Loader2 className="size-5 animate-spin text-muted-foreground" />
</div> </div>
) : notes.length === 0 ? ( ) : notes.length === 0 ? (
<p className="text-sm text-slate-500 text-center py-8"> <p className="text-sm text-muted-foreground text-center py-8">
No notes yet. No notes yet.
</p> </p>
) : ( ) : (
notes.map((note) => ( notes.map((note) => (
<div <div
key={note.id} key={note.id}
className="rounded-lg bg-slate-800/50 border border-slate-700/50 p-3 group" className="rounded-lg bg-muted/50 border border-border/50 p-3 group"
> >
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<p className="text-sm text-slate-300 whitespace-pre-wrap flex-1"> <p className="text-sm text-muted-foreground whitespace-pre-wrap flex-1">
{note.note_text} {note.note_text}
</p> </p>
<button <button
onClick={() => deleteNote(note.id)} onClick={() => deleteNote(note.id)}
className="opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all cursor-pointer shrink-0" className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-red-400 transition-all cursor-pointer shrink-0"
> >
<Trash2 className="size-3.5" /> <Trash2 className="size-3.5" />
</button> </button>
</div> </div>
<p className="text-xs text-slate-500 mt-1.5"> <p className="text-xs text-muted-foreground mt-1.5">
{new Date(note.created_at).toLocaleDateString('en-US', { {new Date(note.created_at).toLocaleDateString('en-US', {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
@ -580,17 +580,17 @@ export function ContactDetailView({
<TabsContent value="custom" className="flex-1 overflow-y-auto px-4 py-3"> <TabsContent value="custom" className="flex-1 overflow-y-auto px-4 py-3">
{loadingCustom ? ( {loadingCustom ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="size-5 animate-spin text-slate-500" /> <Loader2 className="size-5 animate-spin text-muted-foreground" />
</div> </div>
) : customFields.length === 0 ? ( ) : customFields.length === 0 ? (
<p className="text-sm text-slate-500 text-center py-8"> <p className="text-sm text-muted-foreground text-center py-8">
No custom fields defined. Create them in Settings. No custom fields defined. Create them in Settings.
</p> </p>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{customFields.map((field) => ( {customFields.map((field) => (
<div key={field.id} className="space-y-1.5"> <div key={field.id} className="space-y-1.5">
<Label className="text-slate-400 text-xs capitalize"> <Label className="text-muted-foreground text-xs capitalize">
{field.field_name} {field.field_name}
</Label> </Label>
<Input <Input
@ -602,7 +602,7 @@ export function ContactDetailView({
})) }))
} }
placeholder={`Enter ${field.field_name}...`} placeholder={`Enter ${field.field_name}...`}
className="bg-slate-800 border-slate-700 text-white h-8 text-sm placeholder:text-slate-500" className="bg-muted border-border text-foreground h-8 text-sm placeholder:text-muted-foreground"
/> />
</div> </div>
))} ))}
@ -630,16 +630,16 @@ export function ContactDetailView({
<Loader2 className="size-5 animate-spin text-primary" /> <Loader2 className="size-5 animate-spin text-primary" />
</div> </div>
) : deals.length === 0 ? ( ) : deals.length === 0 ? (
<p className="text-xs text-slate-500">No deals yet</p> <p className="text-xs text-muted-foreground">No deals yet</p>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{deals.map((deal) => ( {deals.map((deal) => (
<div <div
key={deal.id} key={deal.id}
className="rounded-lg border border-slate-700 bg-slate-800/50 p-3" className="rounded-lg border border-border bg-muted/50 p-3"
> >
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<p className="text-sm font-medium text-white"> <p className="text-sm font-medium text-foreground">
{deal.title} {deal.title}
</p> </p>
{deal.stage && ( {deal.stage && (
@ -654,7 +654,7 @@ export function ContactDetailView({
</span> </span>
)} )}
</div> </div>
<div className="mt-1.5 flex items-center justify-between text-xs text-slate-400"> <div className="mt-1.5 flex items-center justify-between text-xs text-muted-foreground">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<DollarSign className="size-3" /> <DollarSign className="size-3" />
{formatCurrency( {formatCurrency(

View File

@ -223,12 +223,12 @@ export function ContactForm({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-slate-900 border-slate-700 text-slate-200 sm:max-w-md"> <DialogContent className="bg-popover border-border text-popover-foreground sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white"> <DialogTitle className="text-popover-foreground">
{isEdit ? 'Edit Contact' : 'Add Contact'} {isEdit ? 'Edit Contact' : 'Add Contact'}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
{isEdit {isEdit
? 'Update the contact details below.' ? 'Update the contact details below.'
: 'Fill in the details to create a new contact.'} : 'Fill in the details to create a new contact.'}
@ -237,7 +237,7 @@ export function ContactForm({
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cf-name" className="text-slate-300"> <Label htmlFor="cf-name" className="text-muted-foreground">
Name Name
</Label> </Label>
<Input <Input
@ -245,12 +245,12 @@ export function ContactForm({
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="John Doe" placeholder="John Doe"
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cf-phone" className="text-slate-300"> <Label htmlFor="cf-phone" className="text-muted-foreground">
Phone <span className="text-red-400">*</span> Phone <span className="text-red-400">*</span>
</Label> </Label>
<Input <Input
@ -262,7 +262,7 @@ export function ContactForm({
}} }}
onBlur={checkDuplicate} onBlur={checkDuplicate}
placeholder="+1 234 567 8900" placeholder="+1 234 567 8900"
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
{dupMatch ? ( {dupMatch ? (
<div <div
@ -291,14 +291,14 @@ export function ContactForm({
</div> </div>
</div> </div>
) : ( ) : (
<p className="text-xs text-slate-500"> <p className="text-xs text-muted-foreground">
Include country code, e.g. +1 for US Include country code, e.g. +1 for US
</p> </p>
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cf-email" className="text-slate-300"> <Label htmlFor="cf-email" className="text-muted-foreground">
Email Email
</Label> </Label>
<Input <Input
@ -307,12 +307,12 @@ export function ContactForm({
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="john@example.com" placeholder="john@example.com"
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cf-company" className="text-slate-300"> <Label htmlFor="cf-company" className="text-muted-foreground">
Company Company
</Label> </Label>
<Input <Input
@ -320,19 +320,19 @@ export function ContactForm({
value={company} value={company}
onChange={(e) => setCompany(e.target.value)} onChange={(e) => setCompany(e.target.value)}
placeholder="Acme Inc." placeholder="Acme Inc."
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Tags</Label> <Label className="text-muted-foreground">Tags</Label>
{loadingTags ? ( {loadingTags ? (
<div className="flex items-center gap-2 text-slate-500 text-sm"> <div className="flex items-center gap-2 text-muted-foreground text-sm">
<Loader2 className="size-3 animate-spin" /> <Loader2 className="size-3 animate-spin" />
Loading tags... Loading tags...
</div> </div>
) : tags.length === 0 ? ( ) : tags.length === 0 ? (
<p className="text-xs text-slate-500"> <p className="text-xs text-muted-foreground">
No tags available. Create tags in Settings. No tags available. Create tags in Settings.
</p> </p>
) : ( ) : (
@ -346,7 +346,7 @@ export function ContactForm({
onClick={() => toggleTag(tag.id)} onClick={() => toggleTag(tag.id)}
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors cursor-pointer ${ className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors cursor-pointer ${
selected selected
? 'ring-2 ring-primary ring-offset-1 ring-offset-slate-900' ? 'ring-2 ring-primary ring-offset-1 ring-offset-border'
: 'opacity-60 hover:opacity-100' : 'opacity-60 hover:opacity-100'
}`} }`}
style={{ style={{
@ -363,12 +363,12 @@ export function ContactForm({
)} )}
</div> </div>
<DialogFooter className="bg-slate-900 border-slate-700"> <DialogFooter className="bg-popover border-border">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>

View File

@ -33,10 +33,10 @@ export function CustomFieldsManager({
}: CustomFieldsManagerProps) { }: CustomFieldsManagerProps) {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="border-slate-700 bg-slate-900 text-slate-200 sm:max-w-md"> <DialogContent className="border-border bg-popover text-popover-foreground sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white">Custom fields</DialogTitle> <DialogTitle className="text-popover-foreground">Custom fields</DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
Define extra contact fields (e.g. ZIP code, lead source). They Define extra contact fields (e.g. ZIP code, lead source). They
appear on every contact and in the “Update Contact Field” automation appear on every contact and in the “Update Contact Field” automation
action. action.
@ -185,7 +185,7 @@ export function CustomFieldsPanel() {
} }
}} }}
placeholder="New field name…" placeholder="New field name…"
className="bg-slate-800 text-white" className="bg-muted text-foreground"
/> />
<Button <Button
onClick={handleCreate} onClick={handleCreate}
@ -202,18 +202,18 @@ export function CustomFieldsPanel() {
</div> </div>
{/* List */} {/* List */}
<div className="max-h-72 overflow-y-auto rounded-md border border-slate-800"> <div className="max-h-72 overflow-y-auto rounded-md border border-border">
{loading ? ( {loading ? (
<div className="flex items-center justify-center gap-2 py-8 text-sm text-slate-500"> <div className="flex items-center justify-center gap-2 py-8 text-sm text-muted-foreground">
<Loader2 className="size-4 animate-spin" /> <Loader2 className="size-4 animate-spin" />
Loading… Loading…
</div> </div>
) : fields.length === 0 ? ( ) : fields.length === 0 ? (
<p className="py-8 text-center text-sm text-slate-500"> <p className="py-8 text-center text-sm text-muted-foreground">
No custom fields yet. No custom fields yet.
</p> </p>
) : ( ) : (
<ul className="divide-y divide-slate-800"> <ul className="divide-y divide-border">
{fields.map((field) => ( {fields.map((field) => (
<FieldRow <FieldRow
key={field.id} key={field.id}
@ -265,7 +265,7 @@ function FieldRow({
if (e.key === 'Enter') e.currentTarget.blur(); if (e.key === 'Enter') e.currentTarget.blur();
}} }}
aria-label={`Rename ${field.field_name}`} aria-label={`Rename ${field.field_name}`}
className="focus:border-primary h-8 border-transparent bg-transparent text-white hover:border-slate-700" className="focus:border-primary h-8 border-transparent bg-transparent text-foreground hover:border-border"
/> />
<Button <Button
variant="ghost" variant="ghost"
@ -273,7 +273,7 @@ function FieldRow({
disabled={busy} disabled={busy}
onClick={() => onDelete(field)} onClick={() => onDelete(field)}
title="Delete field" title="Delete field"
className="shrink-0 text-slate-400 hover:text-red-400" className="shrink-0 text-muted-foreground hover:text-red-400"
> >
{busy ? ( {busy ? (
<Loader2 className="size-4 animate-spin" /> <Loader2 className="size-4 animate-spin" />

View File

@ -80,7 +80,7 @@ function ImportPreviewTags({
tagColorByKey: Map<string, string>; tagColorByKey: Map<string, string>;
}) { }) {
if (tagNames.length === 0) { if (tagNames.length === 0) {
return <span className="text-slate-600">—</span>; return <span className="text-muted-foreground">—</span>;
} }
return ( return (
@ -398,31 +398,31 @@ export function ImportModal({
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="flex max-h-[min(90vh,720px)] flex-col gap-0 overflow-hidden border-slate-700/80 bg-slate-900 p-0 text-slate-200 sm:max-w-2xl"> <DialogContent className="flex max-h-[min(90vh,720px)] flex-col gap-0 overflow-hidden border-border/80 bg-popover p-0 text-popover-foreground sm:max-w-2xl">
<div className="shrink-0 space-y-4 border-b border-slate-800/80 px-6 pt-6 pb-5"> <div className="shrink-0 space-y-4 border-b border-border/80 px-6 pt-6 pb-5">
<DialogHeader className="gap-1.5"> <DialogHeader className="gap-1.5">
<DialogTitle className="text-lg text-white"> <DialogTitle className="text-lg text-popover-foreground">
Import Contacts Import Contacts
</DialogTitle> </DialogTitle>
<DialogDescription className="leading-relaxed text-slate-400"> <DialogDescription className="leading-relaxed text-muted-foreground">
Upload a CSV with a required{' '} Upload a CSV with a required{' '}
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300"> <code className="rounded bg-muted px-1 py-0.5 text-[11px] text-muted-foreground">
phone phone
</code>{' '} </code>{' '}
column. Optional:{' '} column. Optional:{' '}
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300"> <code className="rounded bg-muted px-1 py-0.5 text-[11px] text-muted-foreground">
name name
</code> </code>
,{' '} ,{' '}
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300"> <code className="rounded bg-muted px-1 py-0.5 text-[11px] text-muted-foreground">
email email
</code> </code>
,{' '} ,{' '}
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300"> <code className="rounded bg-muted px-1 py-0.5 text-[11px] text-muted-foreground">
company company
</code> </code>
,{' '} ,{' '}
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300"> <code className="rounded bg-muted px-1 py-0.5 text-[11px] text-muted-foreground">
tags tags
</code>{' '} </code>{' '}
(comma-separated; quote multi-tag cells). (comma-separated; quote multi-tag cells).
@ -441,7 +441,7 @@ export function ImportModal({
'group flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border border-dashed p-5 transition-all', 'group flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border border-dashed p-5 transition-all',
file file
? 'border-primary/35 bg-primary/[0.04]' ? 'border-primary/35 bg-primary/[0.04]'
: 'hover:border-primary/40 border-slate-700/80 bg-slate-950/40 hover:bg-slate-950/70' : 'hover:border-primary/40 border-border/80 bg-background/40 hover:bg-background/70'
)} )}
> >
{file ? ( {file ? (
@ -450,25 +450,25 @@ export function ImportModal({
<FileText className="text-primary size-5" /> <FileText className="text-primary size-5" />
</div> </div>
<p <p
className="max-w-full truncate px-2 text-sm font-medium text-slate-200" className="max-w-full truncate px-2 text-sm font-medium text-popover-foreground"
title={file.name} title={file.name}
> >
{truncateFilename(file.name)} {truncateFilename(file.name)}
</p> </p>
<span className="rounded-full bg-slate-800 px-2.5 py-0.5 text-[11px] font-medium text-slate-400"> <span className="rounded-full bg-muted px-2.5 py-0.5 text-[11px] font-medium text-muted-foreground">
{parsedRows.length} row{parsedRows.length !== 1 ? 's' : ''}{' '} {parsedRows.length} row{parsedRows.length !== 1 ? 's' : ''}{' '}
ready ready
</span> </span>
</> </>
) : ( ) : (
<> <>
<div className="flex size-10 items-center justify-center rounded-lg bg-slate-800/80 ring-1 ring-slate-700/80 transition-colors group-hover:bg-slate-800"> <div className="flex size-10 items-center justify-center rounded-lg bg-muted/80 ring-1 ring-border/80 transition-colors group-hover:bg-muted">
<Upload className="size-5 text-slate-500 group-hover:text-slate-400" /> <Upload className="size-5 text-muted-foreground group-hover:text-muted-foreground" />
</div> </div>
<p className="text-sm text-slate-400"> <p className="text-sm text-muted-foreground">
Click to choose a CSV file Click to choose a CSV file
</p> </p>
<p className="text-[11px] text-slate-600"> <p className="text-[11px] text-muted-foreground">
.csv up to your browser limit .csv up to your browser limit
</p> </p>
</> </>
@ -488,12 +488,12 @@ export function ImportModal({
{preview.length > 0 && !result && ( {preview.length > 0 && !result && (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-[11px] font-semibold tracking-[0.14em] text-slate-500 uppercase"> <p className="text-[11px] font-semibold tracking-[0.14em] text-muted-foreground uppercase">
Preview · first {preview.length} Preview · first {preview.length}
</p> </p>
<div className="flex flex-wrap items-center gap-1.5"> <div className="flex flex-wrap items-center gap-1.5">
{tagStats.rowsWithTags > 0 && ( {tagStats.rowsWithTags > 0 && (
<span className="inline-flex items-center gap-1 rounded-md bg-slate-800/90 px-2 py-0.5 text-[11px] text-slate-400"> <span className="inline-flex items-center gap-1 rounded-md bg-muted/90 px-2 py-0.5 text-[11px] text-muted-foreground">
<Tag className="text-primary/80 size-3" /> <Tag className="text-primary/80 size-3" />
{tagStats.unique} tag{tagStats.unique !== 1 ? 's' : ''} ·{' '} {tagStats.unique} tag{tagStats.unique !== 1 ? 's' : ''} ·{' '}
{tagStats.rowsWithTags} contact {tagStats.rowsWithTags} contact
@ -503,59 +503,59 @@ export function ImportModal({
</div> </div>
</div> </div>
<div className="overflow-hidden rounded-xl border border-slate-800 ring-1 ring-slate-800/50"> <div className="overflow-hidden rounded-xl border border-border ring-1 ring-border/50">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full min-w-[32rem] text-xs"> <table className="w-full min-w-[32rem] text-xs">
<thead> <thead>
<tr className="border-b border-slate-800 bg-slate-950/60"> <tr className="border-b border-border bg-background/60">
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-slate-500"> <th className="px-3 py-2 text-left font-medium whitespace-nowrap text-muted-foreground">
Phone Phone
</th> </th>
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-slate-500"> <th className="px-3 py-2 text-left font-medium whitespace-nowrap text-muted-foreground">
Name Name
</th> </th>
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-slate-500"> <th className="px-3 py-2 text-left font-medium whitespace-nowrap text-muted-foreground">
Email Email
</th> </th>
{previewHasCompany && ( {previewHasCompany && (
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-slate-500"> <th className="px-3 py-2 text-left font-medium whitespace-nowrap text-muted-foreground">
Company Company
</th> </th>
)} )}
{previewHasTags && ( {previewHasTags && (
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-slate-500"> <th className="px-3 py-2 text-left font-medium whitespace-nowrap text-muted-foreground">
Tags Tags
</th> </th>
)} )}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-800/70"> <tbody className="divide-y divide-border/70">
{preview.map((row, i) => ( {preview.map((row, i) => (
<tr <tr
key={i} key={i}
className="bg-slate-900/40 transition-colors hover:bg-slate-800/30" className="bg-popover/40 transition-colors hover:bg-muted/30"
> >
<td className="px-3 py-2 whitespace-nowrap text-slate-300"> <td className="px-3 py-2 whitespace-nowrap text-muted-foreground">
<PreviewCell <PreviewCell
value={row.phone} value={row.phone}
mono mono
maxWidth="max-w-[7.5rem]" maxWidth="max-w-[7.5rem]"
/> />
</td> </td>
<td className="px-3 py-2 text-slate-200"> <td className="px-3 py-2 text-popover-foreground">
<PreviewCell <PreviewCell
value={row.name || '—'} value={row.name || '—'}
maxWidth="max-w-[8.5rem]" maxWidth="max-w-[8.5rem]"
/> />
</td> </td>
<td className="px-3 py-2 text-slate-400"> <td className="px-3 py-2 text-muted-foreground">
<PreviewCell <PreviewCell
value={row.email || '—'} value={row.email || '—'}
maxWidth="max-w-[10rem]" maxWidth="max-w-[10rem]"
/> />
</td> </td>
{previewHasCompany && ( {previewHasCompany && (
<td className="px-3 py-2 text-slate-400"> <td className="px-3 py-2 text-muted-foreground">
<PreviewCell <PreviewCell
value={row.company || '—'} value={row.company || '—'}
maxWidth="max-w-[7rem]" maxWidth="max-w-[7rem]"
@ -578,7 +578,7 @@ export function ImportModal({
</div> </div>
{parsedRows.length > PREVIEW_LIMIT && ( {parsedRows.length > PREVIEW_LIMIT && (
<p className="text-center text-[11px] text-slate-600"> <p className="text-center text-[11px] text-muted-foreground">
+ {parsedRows.length - PREVIEW_LIMIT} more row + {parsedRows.length - PREVIEW_LIMIT} more row
{parsedRows.length - PREVIEW_LIMIT !== 1 ? 's' : ''} not shown {parsedRows.length - PREVIEW_LIMIT !== 1 ? 's' : ''} not shown
</p> </p>
@ -587,8 +587,8 @@ export function ImportModal({
)} )}
{result && ( {result && (
<div className="rounded-xl border border-slate-800 bg-slate-950/50 p-4"> <div className="rounded-xl border border-border bg-background/50 p-4">
<p className="text-sm font-medium text-white">Import complete</p> <p className="text-sm font-medium text-popover-foreground">Import complete</p>
<div className="mt-3 flex flex-wrap gap-3"> <div className="mt-3 flex flex-wrap gap-3">
{result.imported > 0 && ( {result.imported > 0 && (
<div className="text-primary flex items-center gap-1.5 text-sm"> <div className="text-primary flex items-center gap-1.5 text-sm">
@ -620,12 +620,12 @@ export function ImportModal({
)} )}
</div> </div>
<DialogFooter className="mt-0 shrink-0 gap-2 border-t border-slate-800/80 bg-slate-950/50 px-6 py-4 sm:justify-end"> <DialogFooter className="mt-0 shrink-0 gap-2 border-t border-border/80 bg-background/50 px-6 py-4 sm:justify-end">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={() => handleOpenChange(false)} onClick={() => handleOpenChange(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
{result ? 'Close' : 'Cancel'} {result ? 'Close' : 'Cancel'}
</Button> </Button>

View File

@ -54,9 +54,9 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
i === 0 || totalLoaded > PAGE_SIZES[i - 1] i === 0 || totalLoaded > PAGE_SIZES[i - 1]
return ( return (
<section className="rounded-xl border border-slate-800 bg-slate-900"> <section className="rounded-xl border border-border bg-card">
<header className="flex items-center justify-between border-b border-slate-800 px-5 py-4"> <header className="flex items-center justify-between border-b border-border px-5 py-4">
<h2 className="text-sm font-semibold text-white">Recent Activity</h2> <h2 className="text-sm font-semibold text-foreground">Recent Activity</h2>
<Link <Link
href="/inbox" href="/inbox"
className="text-xs font-medium text-primary hover:text-primary/80" className="text-xs font-medium text-primary hover:text-primary/80"
@ -81,13 +81,13 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
</div> </div>
) : ( ) : (
<> <>
<ul className="divide-y divide-slate-800"> <ul className="divide-y divide-border">
{visible.map((it, i) => { {visible.map((it, i) => {
const theme = KIND_THEME[it.kind] const theme = KIND_THEME[it.kind]
const Icon = theme.icon const Icon = theme.icon
// Alternating row background for scanability — dark-theme // Alternating row background for scanability — dark-theme
// translation of the spec's white / #f9fafb stripes. // translation of the spec's white / #f9fafb stripes.
const stripe = i % 2 === 0 ? 'bg-transparent' : 'bg-slate-900/40' const stripe = i % 2 === 0 ? 'bg-transparent' : 'bg-card/40'
const row = ( const row = (
<div className="flex items-center gap-3 px-5 py-2.5"> <div className="flex items-center gap-3 px-5 py-2.5">
<span <span
@ -98,16 +98,16 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
> >
<Icon className="h-3.5 w-3.5" /> <Icon className="h-3.5 w-3.5" />
</span> </span>
<span className="min-w-0 flex-1 truncate text-sm text-slate-200"> <span className="min-w-0 flex-1 truncate text-sm text-foreground">
{it.text} {it.text}
</span> </span>
<span className="flex-shrink-0 text-xs text-slate-500 tabular-nums"> <span className="flex-shrink-0 text-xs text-muted-foreground tabular-nums">
{relativeTime(it.at)} {relativeTime(it.at)}
</span> </span>
</div> </div>
) )
return ( return (
<li key={it.id} className={cn(stripe, 'transition-colors hover:bg-slate-800/40')}> <li key={it.id} className={cn(stripe, 'transition-colors hover:bg-muted/40')}>
{it.href ? ( {it.href ? (
<Link href={it.href} className="block"> <Link href={it.href} className="block">
{row} {row}
@ -119,13 +119,13 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
) )
})} })}
</ul> </ul>
<footer className="flex items-center justify-between border-t border-slate-800 px-5 py-3 text-xs"> <footer className="flex items-center justify-between border-t border-border px-5 py-3 text-xs">
<span className="text-slate-500 tabular-nums"> <span className="text-muted-foreground tabular-nums">
Showing {visible.length} of {totalLoaded} Showing {visible.length} of {totalLoaded}
{totalLoaded === 50 ? '+' : ''} {totalLoaded === 50 ? '+' : ''}
</span> </span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="mr-1 text-slate-500">Show</span> <span className="mr-1 text-muted-foreground">Show</span>
{PAGE_SIZES.map((size, i) => { {PAGE_SIZES.map((size, i) => {
const disabled = !isSizeUseful(size, i) const disabled = !isSizeUseful(size, i)
return ( return (
@ -137,9 +137,9 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
className={cn( className={cn(
'rounded-md px-2 py-1 font-medium tabular-nums transition-colors', 'rounded-md px-2 py-1 font-medium tabular-nums transition-colors',
pageSize === size pageSize === size
? 'bg-slate-700 text-white' ? 'bg-secondary text-secondary-foreground'
: 'text-slate-400 hover:bg-slate-800 hover:text-white', : 'text-muted-foreground hover:bg-muted hover:text-foreground',
disabled && 'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-slate-400', disabled && 'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-muted-foreground',
)} )}
> >
{size} {size}

View File

@ -46,13 +46,13 @@ export function ConversationsChart({ series, loading, range, onRangeChange }: Co
}, [data]) }, [data])
return ( return (
<section className="flex h-full flex-col rounded-xl border border-slate-800 bg-slate-900"> <section className="flex h-full flex-col rounded-xl border border-border bg-card">
<header className="flex items-center justify-between border-b border-slate-800 px-5 py-4"> <header className="flex items-center justify-between border-b border-border px-5 py-4">
<div> <div>
<h2 className="text-sm font-semibold text-white">Conversations Over Time</h2> <h2 className="text-sm font-semibold text-foreground">Conversations Over Time</h2>
<p className="mt-0.5 text-xs text-slate-500">Daily message volume by direction</p> <p className="mt-0.5 text-xs text-muted-foreground">Daily message volume by direction</p>
</div> </div>
<div className="flex items-center gap-1 rounded-lg bg-slate-800/60 p-1"> <div className="flex items-center gap-1 rounded-lg bg-muted/60 p-1">
{[7, 30, 90].map((r) => ( {[7, 30, 90].map((r) => (
<button <button
key={r} key={r}
@ -61,8 +61,8 @@ export function ConversationsChart({ series, loading, range, onRangeChange }: Co
className={cn( className={cn(
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors', 'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
range === r range === r
? 'bg-slate-700 text-white' ? 'bg-secondary text-secondary-foreground'
: 'text-slate-400 hover:text-white', : 'text-muted-foreground hover:text-foreground',
)} )}
> >
{r} days {r} days
@ -85,7 +85,7 @@ export function ConversationsChart({ series, loading, range, onRangeChange }: Co
)} )}
</div> </div>
<footer className="flex items-center gap-4 border-t border-slate-800 px-5 py-3 text-xs text-slate-400"> <footer className="flex items-center gap-4 border-t border-border px-5 py-3 text-xs text-muted-foreground">
<LegendDot color="#3b82f6" label="Incoming" /> <LegendDot color="#3b82f6" label="Incoming" />
<LegendDot color="#7c3aed" label="Outgoing" /> <LegendDot color="#7c3aed" label="Outgoing" />
</footer> </footer>
@ -215,7 +215,7 @@ function LineSvg({
y={y} y={y}
textAnchor="end" textAnchor="end"
dominantBaseline="middle" dominantBaseline="middle"
className="fill-slate-500 text-[10px]" className="fill-muted-foreground text-[10px]"
> >
{t} {t}
</text> </text>
@ -231,7 +231,7 @@ function LineSvg({
x={xFor(i)} x={xFor(i)}
y={VB_H - 8} y={VB_H - 8}
textAnchor="middle" textAnchor="middle"
className="fill-slate-500 text-[10px]" className="fill-muted-foreground text-[10px]"
> >
{shortDayLabel(p.day)} {shortDayLabel(p.day)}
</text> </text>
@ -280,10 +280,10 @@ function LineSvg({
letterboxed viewBox percentage. */} letterboxed viewBox percentage. */}
{hovered && hover !== null && ( {hovered && hover !== null && (
<div <div
className="pointer-events-none absolute top-0 z-10 -translate-x-1/2 rounded-md border border-slate-700 bg-slate-950 px-2.5 py-1.5 text-[11px] shadow-lg" className="pointer-events-none absolute top-0 z-10 -translate-x-1/2 rounded-md border border-border bg-popover px-2.5 py-1.5 text-[11px] shadow-lg"
style={{ left: `${hover.tooltipLeftPx}px` }} style={{ left: `${hover.tooltipLeftPx}px` }}
> >
<div className="font-medium text-white">{longDayLabel(hovered.day)}</div> <div className="font-medium text-popover-foreground">{longDayLabel(hovered.day)}</div>
<div className="mt-1 flex flex-col gap-0.5"> <div className="mt-1 flex flex-col gap-0.5">
<span className="flex items-center gap-1.5 text-blue-300"> <span className="flex items-center gap-1.5 text-blue-300">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-blue-500" /> <span className="inline-block h-1.5 w-1.5 rounded-full bg-blue-500" />

View File

@ -22,15 +22,15 @@ export function EmptyState({
return ( return (
<div <div
className={cn( className={cn(
'flex h-full min-h-40 flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-slate-800 bg-slate-900/40 px-4 py-6 text-center', 'flex h-full min-h-40 flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border bg-card/40 px-4 py-6 text-center',
className, className,
)} )}
> >
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-slate-800 text-slate-500"> <div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted text-muted-foreground">
<Icon className="h-5 w-5" /> <Icon className="h-5 w-5" />
</div> </div>
<p className="text-sm font-medium text-slate-300">{title}</p> <p className="text-sm font-medium text-muted-foreground">{title}</p>
{hint && <p className="max-w-xs text-xs text-slate-500">{hint}</p>} {hint && <p className="max-w-xs text-xs text-muted-foreground">{hint}</p>}
</div> </div>
) )
} }

View File

@ -23,18 +23,18 @@ interface MetricCardProps {
export function MetricCard({ title, value, icon: Icon, delta, subtitle }: MetricCardProps) { export function MetricCard({ title, value, icon: Icon, delta, subtitle }: MetricCardProps) {
return ( return (
<div className="rounded-xl border border-slate-800 bg-slate-900 p-5"> <div className="rounded-xl border border-border bg-card p-5">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<p className="text-sm font-medium text-slate-400">{title}</p> <p className="text-sm font-medium text-muted-foreground">{title}</p>
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-800 text-slate-500"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-muted text-muted-foreground">
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
</div> </div>
</div> </div>
<p className="mt-3 text-[28px] leading-none font-bold tabular-nums text-white"> <p className="mt-3 text-[28px] leading-none font-bold tabular-nums text-foreground">
{value} {value}
</p> </p>
{delta ? <DeltaRow sign={delta.sign} label={delta.label} /> : subtitle ? ( {delta ? <DeltaRow sign={delta.sign} label={delta.label} /> : subtitle ? (
<p className="mt-2 text-sm text-slate-500">{subtitle}</p> <p className="mt-2 text-sm text-muted-foreground">{subtitle}</p>
) : null} ) : null}
</div> </div>
) )
@ -46,7 +46,7 @@ function DeltaRow({ sign, label }: { sign: number; label: string }) {
? 'text-primary' ? 'text-primary'
: sign < 0 : sign < 0
? 'text-red-400' ? 'text-red-400'
: 'text-slate-500' : 'text-muted-foreground'
const Arrow = sign > 0 ? ArrowUp : sign < 0 ? ArrowDown : Minus const Arrow = sign > 0 ? ArrowUp : sign < 0 ? ArrowDown : Minus
return ( return (
<div className={cn('mt-2 flex items-center gap-1 text-sm', tone)}> <div className={cn('mt-2 flex items-center gap-1 text-sm', tone)}>

View File

@ -15,10 +15,10 @@ interface PipelineDonutProps {
export function PipelineDonut({ data, loading, currency }: PipelineDonutProps) { export function PipelineDonut({ data, loading, currency }: PipelineDonutProps) {
return ( return (
<section className="flex h-full flex-col rounded-xl border border-slate-800 bg-slate-900"> <section className="flex h-full flex-col rounded-xl border border-border bg-card">
<header className="border-b border-slate-800 px-5 py-4"> <header className="border-b border-border px-5 py-4">
<h2 className="text-sm font-semibold text-white">Pipeline Value</h2> <h2 className="text-sm font-semibold text-foreground">Pipeline Value</h2>
<p className="mt-0.5 text-xs text-slate-500"> <p className="mt-0.5 text-xs text-muted-foreground">
Open deals by stage Open deals by stage
</p> </p>
</header> </header>
@ -43,11 +43,11 @@ export function PipelineDonut({ data, loading, currency }: PipelineDonutProps) {
style={{ background: s.color }} style={{ background: s.color }}
aria-hidden aria-hidden
/> />
<span className="flex-1 truncate text-slate-300">{s.name}</span> <span className="flex-1 truncate text-muted-foreground">{s.name}</span>
<span className="text-slate-500 tabular-nums"> <span className="text-muted-foreground tabular-nums">
{s.dealCount} deal{s.dealCount === 1 ? '' : 's'} {s.dealCount} deal{s.dealCount === 1 ? '' : 's'}
</span> </span>
<span className="w-20 text-right text-slate-300 tabular-nums"> <span className="w-20 text-right text-muted-foreground tabular-nums">
{formatCurrencyShort(s.totalValue, currency)} {formatCurrencyShort(s.totalValue, currency)}
</span> </span>
</li> </li>
@ -114,7 +114,7 @@ function Donut({ data, currency }: { data: PipelineDonutData; currency: string }
x={cx} x={cx}
y={cy - 6} y={cy - 6}
textAnchor="middle" textAnchor="middle"
className="fill-slate-500 text-[11px]" className="fill-muted-foreground text-[11px]"
> >
Total Total
</text> </text>
@ -122,7 +122,7 @@ function Donut({ data, currency }: { data: PipelineDonutData; currency: string }
x={cx} x={cx}
y={cy + 14} y={cy + 14}
textAnchor="middle" textAnchor="middle"
className="fill-white text-[18px] font-semibold tabular-nums" className="fill-foreground text-[18px] font-semibold tabular-nums"
> >
{formatCurrencyShort(data.totalValue, currency)} {formatCurrencyShort(data.totalValue, currency)}
</text> </text>

View File

@ -31,12 +31,12 @@ export function QuickActions() {
<Link <Link
key={a.href} key={a.href}
href={a.href} href={a.href}
className="group flex items-center gap-3 rounded-xl border border-slate-800 bg-slate-900 px-4 py-3 transition-colors hover:border-slate-700 hover:bg-slate-800/60" className="group flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 transition-colors hover:border-border hover:bg-muted/60"
> >
<div className={`flex h-9 w-9 items-center justify-center rounded-lg bg-slate-800 ${a.tint}`}> <div className={`flex h-9 w-9 items-center justify-center rounded-lg bg-muted ${a.tint}`}>
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
</div> </div>
<span className="text-sm font-medium text-white">{a.label}</span> <span className="text-sm font-medium text-foreground">{a.label}</span>
</Link> </Link>
) )
})} })}

View File

@ -44,13 +44,13 @@ export function ResponseTimeChart({
})) ?? [] })) ?? []
return ( return (
<section className="rounded-xl border border-slate-800 bg-slate-900"> <section className="rounded-xl border border-border bg-card">
<header className="flex items-center justify-between gap-3 border-b border-slate-800 px-5 py-4"> <header className="flex items-center justify-between gap-3 border-b border-border px-5 py-4">
<div> <div>
<h2 className="text-sm font-semibold text-white"> <h2 className="text-sm font-semibold text-foreground">
Average First Response Time Average First Response Time
</h2> </h2>
<p className="mt-0.5 text-xs text-slate-500"> <p className="mt-0.5 text-xs text-muted-foreground">
Minutes to reply to a customer&apos;s first unreplied message, by Minutes to reply to a customer&apos;s first unreplied message, by
weekday weekday
</p> </p>
@ -63,13 +63,13 @@ export function ResponseTimeChart({
)} )}
{data && (data.thisWeekAvg != null || data.lastWeekAvg != null) && ( {data && (data.thisWeekAvg != null || data.lastWeekAvg != null) && (
<div> <div>
<div className="text-slate-400"> <div className="text-muted-foreground">
This week:{' '} This week:{' '}
<span className="font-medium text-white tabular-nums"> <span className="font-medium text-foreground tabular-nums">
{fmt(data.thisWeekAvg)} {fmt(data.thisWeekAvg)}
</span> </span>
</div> </div>
<div className="text-slate-500"> <div className="text-muted-foreground">
Last week:{' '} Last week:{' '}
<span className="tabular-nums">{fmt(data.lastWeekAvg)}</span> <span className="tabular-nums">{fmt(data.lastWeekAvg)}</span>
</div> </div>

View File

@ -6,14 +6,14 @@ import { cn } from '@/lib/utils'
* its data fetches. * its data fetches.
*/ */
export function Skeleton({ className }: { className?: string }) { export function Skeleton({ className }: { className?: string }) {
return <div className={cn('animate-pulse rounded-md bg-slate-800', className)} /> return <div className={cn('animate-pulse rounded-md bg-muted', className)} />
} }
export function SkeletonCard({ className }: { className?: string }) { export function SkeletonCard({ className }: { className?: string }) {
return ( return (
<div <div
className={cn( className={cn(
'rounded-xl border border-slate-800 bg-slate-900 p-5', 'rounded-xl border border-border bg-card p-5',
className, className,
)} )}
> >

View File

@ -161,14 +161,14 @@ export function FlowBuilder() {
<section className="flex flex-col gap-3"> <section className="flex flex-col gap-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-sm font-semibold text-white"> <h2 className="text-sm font-semibold text-foreground">
Nodes ({state.nodes.length}) Nodes ({state.nodes.length})
</h2> </h2>
<AddNodeButton onAdd={addNode} /> <AddNodeButton onAdd={addNode} />
</div> </div>
{state.nodes.length === 0 ? ( {state.nodes.length === 0 ? (
<div className="rounded-lg border border-dashed border-slate-700 bg-slate-900/50 p-8 text-center text-sm text-slate-400"> <div className="rounded-lg border border-dashed border-border bg-card/50 p-8 text-center text-sm text-muted-foreground">
Add a <strong>Start</strong> node, then a <strong>Send buttons</strong> Add a <strong>Start</strong> node, then a <strong>Send buttons</strong>
{" "}node, then a <strong>Handoff</strong> — that&apos;s the welcome-menu {" "}node, then a <strong>Handoff</strong> — that&apos;s the welcome-menu
shape from the brief. shape from the brief.
@ -246,7 +246,7 @@ function KeywordsInput({
} }
}} }}
placeholder="support, help, hi" placeholder="support, help, hi"
className="bg-slate-800" className="bg-muted"
/> />
); );
} }
@ -265,11 +265,11 @@ function TriggerPanel({
triggerIssues: ValidationIssue[]; triggerIssues: ValidationIssue[];
}) { }) {
return ( return (
<section className="rounded-lg border border-slate-800 bg-slate-900 p-4"> <section className="rounded-lg border border-border bg-card p-4">
<h2 className="mb-3 text-sm font-semibold text-white">Trigger</h2> <h2 className="mb-3 text-sm font-semibold text-foreground">Trigger</h2>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2"> <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div> <div>
<label className="mb-1 block text-xs text-slate-400">When…</label> <label className="mb-1 block text-xs text-muted-foreground">When…</label>
<Select <Select
value={state.trigger_type} value={state.trigger_type}
onValueChange={(v) => onValueChange={(v) =>
@ -281,7 +281,7 @@ function TriggerPanel({
})) }))
} }
> >
<SelectTrigger className="bg-slate-800"> <SelectTrigger className="bg-muted">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -299,7 +299,7 @@ function TriggerPanel({
</div> </div>
{state.trigger_type === "keyword" && ( {state.trigger_type === "keyword" && (
<div> <div>
<label className="mb-1 block text-xs text-slate-400"> <label className="mb-1 block text-xs text-muted-foreground">
Keywords (comma-separated) Keywords (comma-separated)
</label> </label>
<KeywordsInput <KeywordsInput
@ -342,9 +342,9 @@ function EntryPicker({
}) { }) {
if (state.nodes.length === 0) return null; if (state.nodes.length === 0) return null;
return ( return (
<section className="flex items-center gap-3 rounded-lg border border-slate-800 bg-slate-900 p-3"> <section className="flex items-center gap-3 rounded-lg border border-border bg-card p-3">
<CornerDownRight className="h-4 w-4 shrink-0 text-primary" /> <CornerDownRight className="h-4 w-4 shrink-0 text-primary" />
<span className="text-xs text-slate-400">Entry node:</span> <span className="text-xs text-muted-foreground">Entry node:</span>
<NodeKeySelect <NodeKeySelect
value={state.entry_node_id} value={state.entry_node_id}
nodes={state.nodes} nodes={state.nodes}
@ -396,14 +396,14 @@ function NodeCard({
<div <div
ref={cardRef} ref={cardRef}
className={cn( className={cn(
"rounded-lg border bg-slate-900 transition-shadow duration-500", "rounded-lg border bg-card transition-shadow duration-500",
hasError hasError
? "border-red-500/40" ? "border-red-500/40"
: isEntry : isEntry
? "border-primary/50" ? "border-primary/50"
: "border-slate-800", : "border-border",
isFlashed && isFlashed &&
"ring-2 ring-primary ring-offset-2 ring-offset-slate-950", "ring-2 ring-primary ring-offset-2 ring-offset-background",
)} )}
> >
<button <button
@ -414,10 +414,10 @@ function NodeCard({
<meta.icon className={cn("h-4 w-4 shrink-0", meta.color)} /> <meta.icon className={cn("h-4 w-4 shrink-0", meta.color)} />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="truncate text-sm font-medium text-white"> <span className="truncate text-sm font-medium text-foreground">
{meta.label} {meta.label}
</span> </span>
<code className="rounded bg-slate-800 px-1.5 py-0.5 text-[10px] text-slate-400"> <code className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
{node.node_key} {node.node_key}
</code> </code>
{isEntry && ( {isEntry && (
@ -430,7 +430,7 @@ function NodeCard({
)} )}
</div> </div>
{!expanded && preview && ( {!expanded && preview && (
<p className="mt-0.5 truncate text-xs text-slate-500"> <p className="mt-0.5 truncate text-xs text-muted-foreground">
{preview} {preview}
</p> </p>
)} )}
@ -439,20 +439,20 @@ function NodeCard({
<CircleAlert className="h-3.5 w-3.5 shrink-0 text-red-400" /> <CircleAlert className="h-3.5 w-3.5 shrink-0 text-red-400" />
)} )}
{expanded ? ( {expanded ? (
<ChevronUp className="h-4 w-4 text-slate-500" /> <ChevronUp className="h-4 w-4 text-muted-foreground" />
) : ( ) : (
<ChevronDown className="h-4 w-4 text-slate-500" /> <ChevronDown className="h-4 w-4 text-muted-foreground" />
)} )}
</button> </button>
{expanded && ( {expanded && (
<div className="border-t border-slate-800 px-4 py-4"> <div className="border-t border-border px-4 py-4">
<NodeConfigWithAdvanced <NodeConfigWithAdvanced
node={node} node={node}
allNodes={allNodes} allNodes={allNodes}
onUpdate={onUpdate} onUpdate={onUpdate}
onUpdateConfig={onUpdateConfig} onUpdateConfig={onUpdateConfig}
/> />
<div className="mt-4 flex items-center justify-between border-t border-slate-800 pt-3"> <div className="mt-4 flex items-center justify-between border-t border-border pt-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{!isEntry && ( {!isEntry && (
<Button variant="ghost" size="sm" onClick={onSetEntry}> <Button variant="ghost" size="sm" onClick={onSetEntry}>
@ -511,11 +511,11 @@ function NodeConfigWithAdvanced({
showAdvanced={showAdvanced} showAdvanced={showAdvanced}
onUpdateConfig={onUpdateConfig} onUpdateConfig={onUpdateConfig}
/> />
<div className="border-t border-slate-800 pt-3"> <div className="border-t border-border pt-3">
<button <button
type="button" type="button"
onClick={() => setShowAdvanced((v) => !v)} onClick={() => setShowAdvanced((v) => !v)}
className="inline-flex items-center gap-1 text-xs text-slate-500 hover:text-slate-300" className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
> >
{showAdvanced ? ( {showAdvanced ? (
<ChevronUp className="h-3 w-3" /> <ChevronUp className="h-3 w-3" />
@ -527,7 +527,7 @@ function NodeConfigWithAdvanced({
{showAdvanced && ( {showAdvanced && (
<div className="mt-3 flex flex-col gap-3"> <div className="mt-3 flex flex-col gap-3">
<div> <div>
<label className="mb-1 block text-xs text-slate-400"> <label className="mb-1 block text-xs text-muted-foreground">
Node key (internal identifier — keep stable for analytics) Node key (internal identifier — keep stable for analytics)
</label> </label>
<Input <Input
@ -535,11 +535,11 @@ function NodeConfigWithAdvanced({
onChange={(e) => onChange={(e) =>
onUpdate({ node_key: slugify(e.target.value, node.node_key) }) onUpdate({ node_key: slugify(e.target.value, node.node_key) })
} }
className="bg-slate-800 font-mono text-xs" className="bg-muted font-mono text-xs"
/> />
</div> </div>
{hasReplyIds && ( {hasReplyIds && (
<p className="text-[10px] text-slate-500"> <p className="text-[10px] text-muted-foreground">
Reply IDs for each option are shown inline above. They&apos;re Reply IDs for each option are shown inline above. They&apos;re
returned by WhatsApp when a customer taps; you usually don&apos;t returned by WhatsApp when a customer taps; you usually don&apos;t
need to touch them. need to touch them.
@ -573,13 +573,13 @@ function AddNodeButton({ onAdd }: { onAdd: (type: NodeType) => void }) {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
className="inline-flex items-center gap-1.5 rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-xs font-medium text-slate-200 transition-colors hover:bg-slate-800" className="inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-muted"
aria-label="Add node" aria-label="Add node"
> >
<Plus className="h-3.5 w-3.5" /> <Plus className="h-3.5 w-3.5" />
Add node Add node
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="border-slate-700 bg-slate-900"> <DropdownMenuContent align="end" className="border-border bg-popover">
{types.map((t) => { {types.map((t) => {
const meta = NODE_META[t]; const meta = NODE_META[t];
return ( return (

View File

@ -128,10 +128,10 @@ function FlowNodeCard({ data, selected }: NodeProps) {
return ( return (
<div <div
className={cn( className={cn(
"relative min-w-[220px] max-w-[260px] rounded-lg border bg-slate-900/95 px-3 py-2 text-left shadow-lg backdrop-blur transition-colors", "relative min-w-[220px] max-w-[260px] rounded-lg border bg-card/95 px-3 py-2 text-left shadow-lg backdrop-blur transition-colors",
selected selected
? "border-primary ring-1 ring-primary/40" ? "border-primary ring-1 ring-primary/40"
: "border-slate-700 hover:border-slate-600", : "border-border hover:border-border",
// Flash overrides hover/selected colors briefly. Tailwind's // Flash overrides hover/selected colors briefly. Tailwind's
// built-in `animate-pulse` is too gentle; a ring with the // built-in `animate-pulse` is too gentle; a ring with the
// amber accent matches the list view's flash semantics. // amber accent matches the list view's flash semantics.
@ -142,13 +142,13 @@ function FlowNodeCard({ data, selected }: NodeProps) {
<Handle <Handle
type="target" type="target"
position={Position.Left} position={Position.Left}
className="!h-2.5 !w-2.5 !border-slate-600 !bg-slate-700" className="!h-2.5 !w-2.5 !border-border !bg-muted"
/> />
)} )}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Icon className={cn("h-3.5 w-3.5 shrink-0", meta.color)} /> <Icon className={cn("h-3.5 w-3.5 shrink-0", meta.color)} />
<span className="truncate text-[11px] font-medium uppercase tracking-wide text-slate-400"> <span className="truncate text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
{meta.label} {meta.label}
</span> </span>
{isEntry && ( {isEntry && (
@ -157,21 +157,21 @@ function FlowNodeCard({ data, selected }: NodeProps) {
</span> </span>
)} )}
</div> </div>
<div className="mt-1 truncate font-mono text-[11px] text-slate-300"> <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
{node.node_key} {node.node_key}
</div> </div>
{summary && ( {summary && (
<div className="mt-1 line-clamp-2 text-xs text-slate-400"> <div className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{summary} {summary}
</div> </div>
)} )}
{isMultiSlot && ( {isMultiSlot && (
<div className="mt-2 flex flex-col gap-1 border-t border-slate-800 pt-2"> <div className="mt-2 flex flex-col gap-1 border-t border-border pt-2">
{slots.map((slot) => ( {slots.map((slot) => (
<div <div
key={slot.id} key={slot.id}
className="relative flex items-center justify-between gap-2 rounded px-1 py-0.5 text-[11px] text-slate-300" className="relative flex items-center justify-between gap-2 rounded px-1 py-0.5 text-[11px] text-muted-foreground"
> >
<span className="truncate" title={slot.label}> <span className="truncate" title={slot.label}>
{slot.label} {slot.label}
@ -184,7 +184,7 @@ function FlowNodeCard({ data, selected }: NodeProps) {
// sits flush with the right edge of the card instead // sits flush with the right edge of the card instead
// of floating at vertical center. The negative offset // of floating at vertical center. The negative offset
// matches the card's px-3 + the handle's own radius. // matches the card's px-3 + the handle's own radius.
className="!relative !right-auto !top-auto !h-2.5 !w-2.5 !translate-x-[12px] !transform-none !border-slate-600 !bg-slate-700" className="!relative !right-auto !top-auto !h-2.5 !w-2.5 !translate-x-[12px] !transform-none !border-border !bg-muted"
/> />
</div> </div>
))} ))}
@ -196,7 +196,7 @@ function FlowNodeCard({ data, selected }: NodeProps) {
type="source" type="source"
id={slots[0].id} id={slots[0].id}
position={Position.Right} position={Position.Right}
className="!h-2.5 !w-2.5 !border-slate-600 !bg-slate-700" className="!h-2.5 !w-2.5 !border-border !bg-muted"
/> />
)} )}
</div> </div>
@ -319,11 +319,12 @@ function FlowCanvasInner() {
target: e.target, target: e.target,
sourceHandle: e.sourceHandle, sourceHandle: e.sourceHandle,
label: e.label, label: e.label,
labelStyle: { fill: "#cbd5e1", fontSize: 11 }, // Mode-aware via CSS tokens so edge chrome flips with light/dark.
labelBgStyle: { fill: "#0f172a" }, labelStyle: { fill: "var(--muted-foreground)", fontSize: 11 },
labelBgStyle: { fill: "var(--card)" },
labelBgPadding: [4, 2] as [number, number], labelBgPadding: [4, 2] as [number, number],
labelBgBorderRadius: 4, labelBgBorderRadius: 4,
style: { stroke: "#475569", strokeWidth: 1.5 }, style: { stroke: "var(--border)", strokeWidth: 1.5 },
})); }));
return rfEdges; return rfEdges;
@ -456,7 +457,7 @@ function FlowCanvasInner() {
if (rfNodes.length === 0) { if (rfNodes.length === 0) {
return ( return (
<div className="flex h-[60vh] flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-slate-700 bg-slate-950 text-sm text-slate-500"> <div className="flex h-[60vh] flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-background text-sm text-muted-foreground">
<p>No nodes yet.</p> <p>No nodes yet.</p>
<CanvasAddNodeButton /> <CanvasAddNodeButton />
</div> </div>
@ -465,7 +466,7 @@ function FlowCanvasInner() {
return ( return (
<> <>
<div className="h-[70vh] w-full overflow-hidden rounded-lg border border-slate-800 bg-slate-950"> <div className="h-[70vh] w-full overflow-hidden rounded-lg border border-border bg-background">
<ReactFlow <ReactFlow
nodes={rfNodes} nodes={rfNodes}
edges={rfEdges} edges={rfEdges}
@ -491,17 +492,17 @@ function FlowCanvasInner() {
minZoom={0.2} minZoom={0.2}
maxZoom={1.5} maxZoom={1.5}
> >
<Background gap={24} size={1} color="#1e293b" /> <Background gap={24} size={1} color="var(--border)" />
<Controls <Controls
className="!border-slate-700 !bg-slate-900 [&_button]:!border-slate-700 [&_button]:!bg-slate-900 [&_button:hover]:!bg-slate-800" className="!border-border !bg-card [&_button]:!border-border [&_button]:!bg-card [&_button:hover]:!bg-muted [&_button_svg]:!fill-foreground"
showInteractive={false} showInteractive={false}
/> />
<MiniMap <MiniMap
pannable pannable
zoomable zoomable
nodeColor="#334155" nodeColor="var(--muted-foreground)"
maskColor="rgba(15, 23, 42, 0.7)" maskColor="color-mix(in oklch, var(--background) 70%, transparent)"
className="!border !border-slate-700 !bg-slate-900" className="!border !border-border !bg-card"
/> />
<Panel position="bottom-right" className="!bottom-4 !right-4"> <Panel position="bottom-right" className="!bottom-4 !right-4">
<CanvasAddNodeButton /> <CanvasAddNodeButton />
@ -561,10 +562,10 @@ function NodeEditSheet({
<Sheet open={open} onOpenChange={(v) => !v && onClose()}> <Sheet open={open} onOpenChange={(v) => !v && onClose()}>
<SheetContent <SheetContent
side="right" side="right"
className="flex w-full flex-col gap-0 border-l border-slate-800 bg-slate-950 p-0 sm:max-w-md" className="flex w-full flex-col gap-0 border-l border-border bg-popover p-0 sm:max-w-md"
> >
<SheetHeader className="border-b border-slate-800 px-5 py-4"> <SheetHeader className="border-b border-border px-5 py-4">
<SheetTitle className="flex items-center gap-2 text-slate-100"> <SheetTitle className="flex items-center gap-2 text-popover-foreground">
<Icon className={cn("h-4 w-4 shrink-0", meta.color)} /> <Icon className={cn("h-4 w-4 shrink-0", meta.color)} />
<span>{meta.label}</span> <span>{meta.label}</span>
{isEntry && ( {isEntry && (
@ -573,7 +574,7 @@ function NodeEditSheet({
</span> </span>
)} )}
</SheetTitle> </SheetTitle>
<SheetDescription className="font-mono text-[11px] text-slate-400"> <SheetDescription className="font-mono text-[11px] text-muted-foreground">
{node.node_key} {node.node_key}
</SheetDescription> </SheetDescription>
</SheetHeader> </SheetHeader>
@ -587,7 +588,7 @@ function NodeEditSheet({
/> />
</div> </div>
<SheetFooter className="border-t border-slate-800 px-5 py-3 sm:flex-row sm:justify-between"> <SheetFooter className="border-t border-border px-5 py-3 sm:flex-row sm:justify-between">
{!isEntry ? ( {!isEntry ? (
<Button variant="ghost" size="sm" onClick={onSetEntry}> <Button variant="ghost" size="sm" onClick={onSetEntry}>
Set as entry Set as entry
@ -657,13 +658,13 @@ function CanvasAddNodeButton() {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
className="inline-flex items-center gap-1.5 rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-xs font-medium text-slate-200 shadow-lg transition-colors hover:bg-slate-800" className="inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 text-xs font-medium text-foreground shadow-lg transition-colors hover:bg-muted"
aria-label="Add node" aria-label="Add node"
> >
<Plus className="h-3.5 w-3.5" /> <Plus className="h-3.5 w-3.5" />
Add node Add node
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="border-slate-700 bg-slate-900"> <DropdownMenuContent align="end" className="border-border bg-popover">
{ADD_NODE_TYPES.map((t) => { {ADD_NODE_TYPES.map((t) => {
const meta = NODE_META[t]; const meta = NODE_META[t];
const Icon = meta.icon; const Icon = meta.icon;

View File

@ -85,7 +85,7 @@ export function FlowEditorShell({ initialFlow, initialNodes }: Props) {
<div <div
role="group" role="group"
aria-label="Editor view" aria-label="Editor view"
className="inline-flex items-center gap-1 rounded-md border border-slate-700 bg-slate-900 p-0.5 text-xs" className="inline-flex items-center gap-1 rounded-md border border-border bg-card p-0.5 text-xs"
> >
<ToggleButton <ToggleButton
active={effectiveView === "canvas"} active={effectiveView === "canvas"}
@ -108,7 +108,7 @@ export function FlowEditorShell({ initialFlow, initialNodes }: Props) {
{/* Sticky-bottom validation panel mirrors the placement used {/* Sticky-bottom validation panel mirrors the placement used
when this lived inside FlowBuilder — the activate-readiness when this lived inside FlowBuilder — the activate-readiness
status follows the user as they scroll, in either view. */} status follows the user as they scroll, in either view. */}
<div className="sticky bottom-4 z-10 shadow-xl shadow-slate-950/60"> <div className="sticky bottom-4 z-10 shadow-xl shadow-background/60">
<ValidationPanel /> <ValidationPanel />
</div> </div>
</div> </div>
@ -157,8 +157,8 @@ function ToggleButton({
className={cn( className={cn(
"inline-flex items-center gap-1.5 rounded px-2 py-1 transition-colors", "inline-flex items-center gap-1.5 rounded px-2 py-1 transition-colors",
active active
? "bg-slate-700 text-slate-100" ? "bg-secondary text-secondary-foreground"
: "text-slate-400 hover:bg-slate-800 hover:text-slate-200", : "text-muted-foreground hover:bg-muted hover:text-foreground",
)} )}
> >
{icon} {icon}

View File

@ -43,19 +43,19 @@ export function TextRow({
}) { }) {
return ( return (
<div> <div>
<label className="mb-1 block text-xs text-slate-400">{label}</label> <label className="mb-1 block text-xs text-muted-foreground">{label}</label>
{rows > 1 ? ( {rows > 1 ? (
<Textarea <Textarea
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
rows={rows} rows={rows}
className="bg-slate-800" className="bg-muted"
/> />
) : ( ) : (
<Input <Input
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
className="bg-slate-800" className="bg-muted"
/> />
)} )}
</div> </div>
@ -77,7 +77,7 @@ export function NextNodeRow({
}) { }) {
return ( return (
<div> <div>
<label className="mb-1 block text-xs text-slate-400">{label}</label> <label className="mb-1 block text-xs text-muted-foreground">{label}</label>
<NodeKeySelect <NodeKeySelect
value={value || null} value={value || null}
nodes={allNodes} nodes={allNodes}
@ -110,7 +110,7 @@ export function NodeKeySelect({
value={value ?? "__none__"} value={value ?? "__none__"}
onValueChange={(v) => onChange(v === "__none__" ? null : v)} onValueChange={(v) => onChange(v === "__none__" ? null : v)}
> >
<SelectTrigger className={cn("bg-slate-800", className)}> <SelectTrigger className={cn("bg-muted", className)}>
<SelectValue placeholder={placeholder ?? "—"} /> <SelectValue placeholder={placeholder ?? "—"} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@ -135,7 +135,7 @@ export function NodeConfigForm({
rows={2} rows={2}
/> />
<div> <div>
<label className="mb-1 block text-xs text-slate-400"> <label className="mb-1 block text-xs text-muted-foreground">
Variable key (stored in flow_runs.vars; alphanumeric + underscore) Variable key (stored in flow_runs.vars; alphanumeric + underscore)
</label> </label>
<Input <Input
@ -146,11 +146,11 @@ export function NodeConfigForm({
}) })
} }
placeholder="e.g. name, email, company" placeholder="e.g. name, email, company"
className="bg-slate-800 font-mono text-xs" className="bg-muted font-mono text-xs"
/> />
<p className="mt-1 text-[10px] text-slate-500"> <p className="mt-1 text-[10px] text-muted-foreground">
Interpolate in downstream prompts and handoff notes with{" "} Interpolate in downstream prompts and handoff notes with{" "}
<code className="rounded bg-slate-800 px-1"> <code className="rounded bg-muted px-1">
{"{{vars."} {"{{vars."}
{(cfg as { var_key?: string }).var_key || "name"} {(cfg as { var_key?: string }).var_key || "name"}
{"}}"} {"}}"}
@ -200,7 +200,7 @@ export function NodeConfigForm({
case "end": case "end":
return ( return (
<p className="text-xs text-slate-500"> <p className="text-xs text-muted-foreground">
Terminal node. When the runner reaches this node the run is marked Terminal node. When the runner reaches this node the run is marked
complete. No config needed. complete. No config needed.
</p> </p>
@ -269,7 +269,7 @@ function SendButtonsForm({
/> />
<div> <div>
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<label className="text-xs text-slate-400"> <label className="text-xs text-muted-foreground">
Buttons (1–3) — each one routes to a different next node Buttons (1–3) — each one routes to a different next node
</label> </label>
</div> </div>
@ -278,7 +278,7 @@ function SendButtonsForm({
<div <div
key={i} key={i}
className={cn( className={cn(
"grid grid-cols-1 gap-2 rounded-md border border-slate-800 bg-slate-800/40 p-3", "grid grid-cols-1 gap-2 rounded-md border border-border bg-muted/40 p-3",
showAdvanced showAdvanced
? "md:grid-cols-[1fr_2fr_2fr_auto]" ? "md:grid-cols-[1fr_2fr_2fr_auto]"
: "md:grid-cols-[2fr_2fr_auto]", : "md:grid-cols-[2fr_2fr_auto]",
@ -293,14 +293,14 @@ function SendButtonsForm({
}) })
} }
placeholder="reply_id" placeholder="reply_id"
className="bg-slate-800 font-mono text-xs" className="bg-muted font-mono text-xs"
/> />
)} )}
<Input <Input
value={b.title} value={b.title}
onChange={(e) => updateButton(i, { title: e.target.value })} onChange={(e) => updateButton(i, { title: e.target.value })}
placeholder="Visible title (≤20 chars)" placeholder="Visible title (≤20 chars)"
className="bg-slate-800" className="bg-muted"
maxLength={20} maxLength={20}
/> />
<NodeKeySelect <NodeKeySelect
@ -465,13 +465,13 @@ function SendListForm({
</div> </div>
<div className="mt-2"> <div className="mt-2">
<label className="mb-2 block text-xs text-slate-400"> <label className="mb-2 block text-xs text-muted-foreground">
Rows (1–10 total across all sections) Rows (1–10 total across all sections)
</label> </label>
{sections.map((section, sIdx) => ( {sections.map((section, sIdx) => (
<div <div
key={sIdx} key={sIdx}
className="mb-3 rounded-md border border-slate-800 bg-slate-800/40 p-3" className="mb-3 rounded-md border border-border bg-muted/40 p-3"
> >
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">
<Input <Input
@ -480,7 +480,7 @@ function SendListForm({
updateSection(sIdx, { title: e.target.value }) updateSection(sIdx, { title: e.target.value })
} }
placeholder={`Section ${sIdx + 1} title (optional)`} placeholder={`Section ${sIdx + 1} title (optional)`}
className="bg-slate-800 text-xs" className="bg-muted text-xs"
/> />
{sections.length > 1 && ( {sections.length > 1 && (
<Button <Button
@ -516,7 +516,7 @@ function SendListForm({
}) })
} }
placeholder="reply_id" placeholder="reply_id"
className="bg-slate-800 font-mono text-xs" className="bg-muted font-mono text-xs"
/> />
)} )}
<Input <Input
@ -525,7 +525,7 @@ function SendListForm({
updateRow(sIdx, rIdx, { title: e.target.value }) updateRow(sIdx, rIdx, { title: e.target.value })
} }
placeholder="Row title (≤24)" placeholder="Row title (≤24)"
className="bg-slate-800" className="bg-muted"
maxLength={24} maxLength={24}
/> />
<NodeKeySelect <NodeKeySelect
@ -614,14 +614,14 @@ function ConditionForm({
<> <>
<div className="grid grid-cols-1 gap-3 md:grid-cols-3"> <div className="grid grid-cols-1 gap-3 md:grid-cols-3">
<div> <div>
<label className="mb-1 block text-xs text-slate-400">If</label> <label className="mb-1 block text-xs text-muted-foreground">If</label>
<Select <Select
value={subject} value={subject}
onValueChange={(v) => onValueChange={(v) =>
onUpdateConfig({ subject: v as ConditionCfg["subject"] }) onUpdateConfig({ subject: v as ConditionCfg["subject"] })
} }
> >
<SelectTrigger className="bg-slate-800"> <SelectTrigger className="bg-muted">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -632,7 +632,7 @@ function ConditionForm({
</Select> </Select>
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="mb-1 block text-xs text-slate-400"> <label className="mb-1 block text-xs text-muted-foreground">
{subject === "var" {subject === "var"
? "var name" ? "var name"
: subject === "tag" : subject === "tag"
@ -644,7 +644,7 @@ function ConditionForm({
value={cfg.subject_key ?? ""} value={cfg.subject_key ?? ""}
onValueChange={(v) => onUpdateConfig({ subject_key: v })} onValueChange={(v) => onUpdateConfig({ subject_key: v })}
> >
<SelectTrigger className="bg-slate-800"> <SelectTrigger className="bg-muted">
<SelectValue placeholder="Pick a tag…" /> <SelectValue placeholder="Pick a tag…" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -660,7 +660,7 @@ function ConditionForm({
value={cfg.subject_key ?? ""} value={cfg.subject_key ?? ""}
onValueChange={(v) => onUpdateConfig({ subject_key: v })} onValueChange={(v) => onUpdateConfig({ subject_key: v })}
> >
<SelectTrigger className="bg-slate-800"> <SelectTrigger className="bg-muted">
<SelectValue placeholder="Pick a field…" /> <SelectValue placeholder="Pick a field…" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -677,7 +677,7 @@ function ConditionForm({
onUpdateConfig({ subject_key: e.target.value }) onUpdateConfig({ subject_key: e.target.value })
} }
placeholder={subject === "var" ? "e.g. email" : "tag UUID"} placeholder={subject === "var" ? "e.g. email" : "tag UUID"}
className="bg-slate-800 font-mono text-xs" className="bg-muted font-mono text-xs"
/> />
)} )}
</div> </div>
@ -690,14 +690,14 @@ function ConditionForm({
)} )}
> >
<div> <div>
<label className="mb-1 block text-xs text-slate-400">Operator</label> <label className="mb-1 block text-xs text-muted-foreground">Operator</label>
<Select <Select
value={operator} value={operator}
onValueChange={(v) => onValueChange={(v) =>
onUpdateConfig({ operator: v as ConditionCfg["operator"] }) onUpdateConfig({ operator: v as ConditionCfg["operator"] })
} }
> >
<SelectTrigger className="bg-slate-800"> <SelectTrigger className="bg-muted">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -710,11 +710,11 @@ function ConditionForm({
</div> </div>
{showValue && ( {showValue && (
<div> <div>
<label className="mb-1 block text-xs text-slate-400">Value</label> <label className="mb-1 block text-xs text-muted-foreground">Value</label>
<Input <Input
value={cfg.value ?? ""} value={cfg.value ?? ""}
onChange={(e) => onUpdateConfig({ value: e.target.value })} onChange={(e) => onUpdateConfig({ value: e.target.value })}
className="bg-slate-800" className="bg-muted"
/> />
</div> </div>
)} )}
@ -767,14 +767,14 @@ function SetTagForm({
<> <>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2"> <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div> <div>
<label className="mb-1 block text-xs text-slate-400">Action</label> <label className="mb-1 block text-xs text-muted-foreground">Action</label>
<Select <Select
value={cfg.mode ?? "add"} value={cfg.mode ?? "add"}
onValueChange={(v) => onValueChange={(v) =>
onUpdateConfig({ mode: v as SetTagCfg["mode"] }) onUpdateConfig({ mode: v as SetTagCfg["mode"] })
} }
> >
<SelectTrigger className="bg-slate-800"> <SelectTrigger className="bg-muted">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -784,13 +784,13 @@ function SetTagForm({
</Select> </Select>
</div> </div>
<div> <div>
<label className="mb-1 block text-xs text-slate-400">Tag</label> <label className="mb-1 block text-xs text-muted-foreground">Tag</label>
{tags.length > 0 ? ( {tags.length > 0 ? (
<Select <Select
value={cfg.tag_id ?? ""} value={cfg.tag_id ?? ""}
onValueChange={(v) => onUpdateConfig({ tag_id: v })} onValueChange={(v) => onUpdateConfig({ tag_id: v })}
> >
<SelectTrigger className="bg-slate-800"> <SelectTrigger className="bg-muted">
<SelectValue placeholder="Pick a tag…" /> <SelectValue placeholder="Pick a tag…" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -806,7 +806,7 @@ function SetTagForm({
value={cfg.tag_id ?? ""} value={cfg.tag_id ?? ""}
onChange={(e) => onUpdateConfig({ tag_id: e.target.value })} onChange={(e) => onUpdateConfig({ tag_id: e.target.value })}
placeholder="Tag UUID" placeholder="Tag UUID"
className="bg-slate-800 font-mono text-xs" className="bg-muted font-mono text-xs"
/> />
)} )}
</div> </div>
@ -972,7 +972,7 @@ function SendMediaForm({
return ( return (
<> <>
<div> <div>
<label className="mb-1 block text-xs text-slate-400">Media type</label> <label className="mb-1 block text-xs text-muted-foreground">Media type</label>
<Select <Select
value={mediaType} value={mediaType}
onValueChange={(v) => { onValueChange={(v) => {
@ -986,7 +986,7 @@ function SendMediaForm({
}); });
}} }}
> >
<SelectTrigger className="bg-slate-800"> <SelectTrigger className="bg-muted">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -1000,15 +1000,15 @@ function SendMediaForm({
</div> </div>
<div> <div>
<label className="mb-1 block text-xs text-slate-400">File</label> <label className="mb-1 block text-xs text-muted-foreground">File</label>
{cfg.media_url ? ( {cfg.media_url ? (
<div className="flex items-center gap-2 rounded-md border border-slate-700 bg-slate-800 px-3 py-2 text-xs"> <div className="flex items-center gap-2 rounded-md border border-border bg-muted px-3 py-2 text-xs">
<Paperclip className="h-3.5 w-3.5 shrink-0 text-cyan-400" /> <Paperclip className="h-3.5 w-3.5 shrink-0 text-cyan-400" />
<a <a
href={cfg.media_url} href={cfg.media_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="min-w-0 flex-1 truncate text-slate-200 hover:text-cyan-300" className="min-w-0 flex-1 truncate text-foreground hover:text-cyan-300"
title={displayName || cfg.media_url} title={displayName || cfg.media_url}
> >
{displayName || cfg.media_url} {displayName || cfg.media_url}
@ -1016,7 +1016,7 @@ function SendMediaForm({
<button <button
type="button" type="button"
onClick={handleClear} onClick={handleClear}
className="rounded p-1 text-slate-400 hover:bg-slate-700 hover:text-slate-200" className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
aria-label="Remove file" aria-label="Remove file"
disabled={uploading} disabled={uploading}
> >
@ -1028,7 +1028,7 @@ function SendMediaForm({
type="button" type="button"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
disabled={uploading} disabled={uploading}
className="flex w-full items-center justify-center gap-2 rounded-md border border-dashed border-slate-700 bg-slate-900 px-3 py-4 text-xs text-slate-400 transition-colors hover:border-slate-600 hover:bg-slate-800 hover:text-slate-200 disabled:cursor-not-allowed disabled:opacity-60" className="flex w-full items-center justify-center gap-2 rounded-md border border-dashed border-border bg-card px-3 py-4 text-xs text-muted-foreground transition-colors hover:border-border hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60"
> >
{uploading ? ( {uploading ? (
<> <>
@ -1066,14 +1066,14 @@ function SendMediaForm({
{isDocument && ( {isDocument && (
<div> <div>
<label className="mb-1 block text-xs text-slate-400"> <label className="mb-1 block text-xs text-muted-foreground">
Filename shown to the customer (documents only) Filename shown to the customer (documents only)
</label> </label>
<Input <Input
value={cfg.filename ?? ""} value={cfg.filename ?? ""}
onChange={(e) => onUpdateConfig({ filename: e.target.value })} onChange={(e) => onUpdateConfig({ filename: e.target.value })}
placeholder="invoice.pdf" placeholder="invoice.pdf"
className="bg-slate-800 text-xs" className="bg-muted text-xs"
/> />
</div> </div>
)} )}

View File

@ -53,11 +53,11 @@ export function EditorHeader() {
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex items-center gap-2 text-xs text-slate-500"> <div className="flex items-center gap-2 text-xs text-muted-foreground">
<button <button
type="button" type="button"
onClick={() => router.push("/flows")} onClick={() => router.push("/flows")}
className="inline-flex items-center gap-1 hover:text-slate-300" className="inline-flex items-center gap-1 hover:text-foreground"
> >
<ArrowLeft className="h-3 w-3" /> <ArrowLeft className="h-3 w-3" />
Flows Flows
@ -72,7 +72,7 @@ export function EditorHeader() {
setState((s) => ({ ...s, name: e.target.value })) setState((s) => ({ ...s, name: e.target.value }))
} }
placeholder="Flow name" placeholder="Flow name"
className="max-w-md bg-slate-900 text-lg font-semibold" className="max-w-md bg-card text-lg font-semibold"
/> />
<StatusBadge status={state.status} /> <StatusBadge status={state.status} />
{dirty && ( {dirty && (
@ -154,7 +154,7 @@ export function EditorHeader() {
setState((s) => ({ ...s, description: e.target.value })) setState((s) => ({ ...s, description: e.target.value }))
} }
placeholder="Optional description (internal — customers don't see this)" placeholder="Optional description (internal — customers don't see this)"
className="bg-slate-900 text-sm" className="bg-card text-sm"
/> />
</div> </div>
); );
@ -162,9 +162,9 @@ export function EditorHeader() {
function StatusBadge({ status }: { status: BuilderState["status"] }) { function StatusBadge({ status }: { status: BuilderState["status"] }) {
const cls = { const cls = {
draft: "border-slate-700 bg-slate-800 text-slate-300", draft: "border-border bg-muted text-muted-foreground",
active: "border-emerald-600/40 bg-emerald-500/10 text-emerald-300", active: "border-emerald-600/40 bg-emerald-500/10 text-emerald-300",
archived: "border-slate-700 bg-slate-800/50 text-slate-500", archived: "border-border bg-muted/50 text-muted-foreground",
}[status]; }[status];
return ( return (
<Badge variant="outline" className={cn("shrink-0", cls)}> <Badge variant="outline" className={cn("shrink-0", cls)}>

View File

@ -110,7 +110,7 @@ export const NODE_META: Record<
icon: UserPlus, icon: UserPlus,
color: "text-amber-400", color: "text-amber-400",
}, },
end: { label: "End", icon: Flag, color: "text-slate-400" }, end: { label: "End", icon: Flag, color: "text-muted-foreground" },
}; };
// ============================================================ // ============================================================

View File

@ -30,7 +30,7 @@ export function ValidationPanel() {
// sticky-positioned over scrolled-behind node cards (a translucent // sticky-positioned over scrolled-behind node cards (a translucent
// bg-emerald-500/10 would bleed through ugly). // bg-emerald-500/10 would bleed through ugly).
return ( return (
<div className="flex items-center gap-2 rounded-lg border border-emerald-600/50 bg-slate-950 p-3 text-sm font-medium text-emerald-300"> <div className="flex items-center gap-2 rounded-lg border border-emerald-600/50 bg-background p-3 text-sm font-medium text-emerald-300">
<CircleCheck className="h-4 w-4 shrink-0" /> <CircleCheck className="h-4 w-4 shrink-0" />
No issues. Ready to activate. No issues. Ready to activate.
</div> </div>
@ -41,11 +41,11 @@ export function ValidationPanel() {
return ( return (
<div <div
className={cn( className={cn(
"rounded-lg border bg-slate-950 p-3", "rounded-lg border bg-background p-3",
errors.length > 0 ? "border-red-500/40" : "border-amber-500/40", errors.length > 0 ? "border-red-500/40" : "border-amber-500/40",
)} )}
> >
<div className="mb-2 flex items-center gap-2 text-xs text-slate-400"> <div className="mb-2 flex items-center gap-2 text-xs text-muted-foreground">
{errors.length > 0 ? ( {errors.length > 0 ? (
<CircleAlert className="h-4 w-4 text-red-400" /> <CircleAlert className="h-4 w-4 text-red-400" />
) : ( ) : (
@ -85,7 +85,7 @@ export function IssueLine({
<CircleAlert className={cn("mt-0.5 h-3 w-3 shrink-0", iconTone)} /> <CircleAlert className={cn("mt-0.5 h-3 w-3 shrink-0", iconTone)} />
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
{issue.node_key && ( {issue.node_key && (
<code className="mr-1 rounded bg-slate-800 px-1 py-0.5 text-[10px] text-slate-400"> <code className="mr-1 rounded bg-muted px-1 py-0.5 text-[10px] text-muted-foreground">
{issue.node_key} {issue.node_key}
</code> </code>
)} )}
@ -103,7 +103,7 @@ export function IssueLine({
type="button" type="button"
onClick={() => onJump(issue.node_key!)} onClick={() => onJump(issue.node_key!)}
className={cn( className={cn(
"flex w-full items-start gap-2 rounded-md px-2 py-1 text-left text-xs transition-colors hover:bg-slate-800/60", "flex w-full items-start gap-2 rounded-md px-2 py-1 text-left text-xs transition-colors hover:bg-muted/60",
tone, tone,
)} )}
aria-label={`Jump to node ${issue.node_key}`} aria-label={`Jump to node ${issue.node_key}`}

View File

@ -117,8 +117,8 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
if (!contact) { if (!contact) {
return ( return (
<div className="flex h-full w-70 items-center justify-center border-l border-slate-800 bg-slate-900"> <div className="flex h-full w-70 items-center justify-center border-l border-border bg-card">
<p className="text-sm text-slate-500">Select a conversation</p> <p className="text-sm text-muted-foreground">Select a conversation</p>
</div> </div>
); );
} }
@ -127,12 +127,12 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
const initials = displayName.charAt(0).toUpperCase(); const initials = displayName.charAt(0).toUpperCase();
return ( return (
<div className="flex h-full w-70 flex-col border-l border-slate-800 bg-slate-900"> <div className="flex h-full w-70 flex-col border-l border-border bg-card">
<ScrollArea className="flex-1"> <ScrollArea className="flex-1">
<div className="p-4"> <div className="p-4">
{/* Contact Info */} {/* Contact Info */}
<div className="flex flex-col items-center text-center"> <div className="flex flex-col items-center text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-slate-700 text-lg font-semibold text-white"> <div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted text-lg font-semibold text-foreground">
{contact.avatar_url ? ( {contact.avatar_url ? (
<img <img
src={contact.avatar_url} src={contact.avatar_url}
@ -143,11 +143,11 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
initials initials
)} )}
</div> </div>
<h3 className="mt-3 text-sm font-semibold text-white"> <h3 className="mt-3 text-sm font-semibold text-foreground">
{displayName} {displayName}
</h3> </h3>
{contact.company && ( {contact.company && (
<p className="text-xs text-slate-400">{contact.company}</p> <p className="text-xs text-muted-foreground">{contact.company}</p>
)} )}
</div> </div>
@ -155,37 +155,37 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<button <button
onClick={handleCopyPhone} onClick={handleCopyPhone}
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-slate-300 transition-colors hover:bg-slate-800" className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted"
> >
<Phone className="h-4 w-4 text-slate-500" /> <Phone className="h-4 w-4 text-muted-foreground" />
<span className="flex-1 text-left">{contact.phone}</span> <span className="flex-1 text-left">{contact.phone}</span>
{copied ? ( {copied ? (
<Check className="h-3 w-3 text-primary" /> <Check className="h-3 w-3 text-primary" />
) : ( ) : (
<Copy className="h-3 w-3 text-slate-600" /> <Copy className="h-3 w-3 text-muted-foreground" />
)} )}
</button> </button>
{contact.email && ( {contact.email && (
<div className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-slate-300"> <div className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-muted-foreground">
<Mail className="h-4 w-4 text-slate-500" /> <Mail className="h-4 w-4 text-muted-foreground" />
<span className="truncate">{contact.email}</span> <span className="truncate">{contact.email}</span>
</div> </div>
)} )}
</div> </div>
{/* Divider */} {/* Divider */}
<div className="my-4 border-t border-slate-800" /> <div className="my-4 border-t border-border" />
{/* Tags */} {/* Tags */}
<div> <div>
<div className="flex items-center gap-2 px-1 text-xs font-medium uppercase tracking-wider text-slate-500"> <div className="flex items-center gap-2 px-1 text-xs font-medium uppercase tracking-wider text-muted-foreground">
<TagIcon className="h-3 w-3" /> <TagIcon className="h-3 w-3" />
Tags Tags
</div> </div>
<div className="mt-2 flex flex-wrap gap-1"> <div className="mt-2 flex flex-wrap gap-1">
{tags.length === 0 ? ( {tags.length === 0 ? (
<p className="px-1 text-xs text-slate-600">No tags</p> <p className="px-1 text-xs text-muted-foreground">No tags</p>
) : ( ) : (
tags.map((tag) => ( tags.map((tag) => (
<span <span
@ -204,27 +204,27 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
</div> </div>
{/* Divider */} {/* Divider */}
<div className="my-4 border-t border-slate-800" /> <div className="my-4 border-t border-border" />
{/* Active Deals */} {/* Active Deals */}
<div> <div>
<div className="flex items-center gap-2 px-1 text-xs font-medium uppercase tracking-wider text-slate-500"> <div className="flex items-center gap-2 px-1 text-xs font-medium uppercase tracking-wider text-muted-foreground">
<DollarSign className="h-3 w-3" /> <DollarSign className="h-3 w-3" />
Active Deals Active Deals
</div> </div>
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
{deals.length === 0 ? ( {deals.length === 0 ? (
<p className="px-1 text-xs text-slate-600">No deals</p> <p className="px-1 text-xs text-muted-foreground">No deals</p>
) : ( ) : (
deals.map((deal) => ( deals.map((deal) => (
<div <div
key={deal.id} key={deal.id}
className="rounded-lg bg-slate-800 px-3 py-2" className="rounded-lg bg-muted px-3 py-2"
> >
<p className="text-sm font-medium text-white"> <p className="text-sm font-medium text-foreground">
{deal.title} {deal.title}
</p> </p>
<div className="mt-1 flex items-center justify-between text-xs text-slate-400"> <div className="mt-1 flex items-center justify-between text-xs text-muted-foreground">
<span> <span>
{deal.currency ?? "$"} {deal.currency ?? "$"}
{deal.value.toLocaleString()} {deal.value.toLocaleString()}
@ -248,11 +248,11 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
</div> </div>
{/* Divider */} {/* Divider */}
<div className="my-4 border-t border-slate-800" /> <div className="my-4 border-t border-border" />
{/* Notes */} {/* Notes */}
<div> <div>
<div className="flex items-center gap-2 px-1 text-xs font-medium uppercase tracking-wider text-slate-500"> <div className="flex items-center gap-2 px-1 text-xs font-medium uppercase tracking-wider text-muted-foreground">
<StickyNote className="h-3 w-3" /> <StickyNote className="h-3 w-3" />
Notes Notes
</div> </div>
@ -263,7 +263,7 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
onChange={(e) => setNewNote(e.target.value)} onChange={(e) => setNewNote(e.target.value)}
placeholder="Add a note..." placeholder="Add a note..."
rows={2} rows={2}
className="flex-1 resize-none rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 text-xs text-white placeholder-slate-500 outline-none focus:border-primary/50" className="flex-1 resize-none rounded-lg border border-border bg-muted px-3 py-2 text-xs text-foreground placeholder-muted-foreground outline-none focus:border-primary/50"
/> />
<Button <Button
size="sm" size="sm"
@ -279,12 +279,12 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
{notes.map((note) => ( {notes.map((note) => (
<div <div
key={note.id} key={note.id}
className="rounded-lg bg-slate-800 px-3 py-2" className="rounded-lg bg-muted px-3 py-2"
> >
<p className="whitespace-pre-wrap text-xs text-slate-300"> <p className="whitespace-pre-wrap text-xs text-muted-foreground">
{note.note_text} {note.note_text}
</p> </p>
<p className="mt-1 text-[10px] text-slate-600"> <p className="mt-1 text-[10px] text-muted-foreground">
{format(new Date(note.created_at), "MMM d, yyyy HH:mm")} {format(new Date(note.created_at), "MMM d, yyyy HH:mm")}
</p> </p>
</div> </div>

View File

@ -33,7 +33,7 @@ interface ConversationListProps {
const STATUS_COLORS: Record<ConversationStatus, string> = { const STATUS_COLORS: Record<ConversationStatus, string> = {
open: "bg-primary", open: "bg-primary",
pending: "bg-amber-500", pending: "bg-amber-500",
closed: "bg-slate-500", closed: "bg-muted-foreground",
}; };
const FILTER_OPTIONS: { label: string; value: ConversationStatus | "all" }[] = [ const FILTER_OPTIONS: { label: string; value: ConversationStatus | "all" }[] = [
@ -147,27 +147,27 @@ export function ConversationList({
// w-full on mobile so the list occupies the whole viewport when it's // w-full on mobile so the list occupies the whole viewport when it's
// the single pane showing; fixed 320px on desktop where it shares the // the single pane showing; fixed 320px on desktop where it shares the
// row with the thread + contact sidebar. // row with the thread + contact sidebar.
<div className="flex h-full w-full flex-col border-r border-slate-800 bg-slate-900 lg:w-80"> <div className="flex h-full w-full flex-col border-r border-border bg-card lg:w-80">
{/* Search + Filter */} {/* Search + Filter */}
<div className="space-y-2 border-b border-slate-800 p-3"> <div className="space-y-2 border-b border-border p-3">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500" /> <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
value={search} value={search}
onChange={handleSearchChange} onChange={handleSearchChange}
placeholder="Search conversations..." placeholder="Search conversations..."
className="border-slate-700 bg-slate-800 pl-9 text-sm text-white placeholder-slate-500 focus:border-primary/50" className="border-border bg-muted pl-9 text-sm text-foreground placeholder-muted-foreground focus:border-primary/50"
/> />
</div> </div>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger className="inline-flex items-center justify-center h-7 gap-1 px-2 text-xs text-slate-400 hover:text-white rounded-md hover:bg-slate-800"> <DropdownMenuTrigger className="inline-flex items-center justify-center h-7 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground rounded-md hover:bg-muted">
{activeFilter?.label ?? "All"} {activeFilter?.label ?? "All"}
<ChevronDown className="h-3 w-3" /> <ChevronDown className="h-3 w-3" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="start" align="start"
className="border-slate-700 bg-slate-800" className="border-border bg-popover"
> >
{FILTER_OPTIONS.map((opt) => ( {FILTER_OPTIONS.map((opt) => (
<DropdownMenuItem <DropdownMenuItem
@ -177,7 +177,7 @@ export function ConversationList({
"text-sm", "text-sm",
filter === opt.value filter === opt.value
? "text-primary" ? "text-primary"
: "text-slate-300" : "text-popover-foreground"
)} )}
> >
{opt.label} {opt.label}
@ -200,7 +200,7 @@ export function ConversationList({
</div> </div>
) : filtered.length === 0 ? ( ) : filtered.length === 0 ? (
<div className="px-4 py-12 text-center"> <div className="px-4 py-12 text-center">
<p className="text-sm text-slate-500">No conversations found</p> <p className="text-sm text-muted-foreground">No conversations found</p>
</div> </div>
) : ( ) : (
<div className="flex flex-col"> <div className="flex flex-col">
@ -248,12 +248,12 @@ function ConversationItem({
<button <button
onClick={handleClick} onClick={handleClick}
className={cn( className={cn(
"flex w-full items-start gap-3 px-3 py-3 text-left transition-colors hover:bg-slate-800/50", "flex w-full items-start gap-3 px-3 py-3 text-left transition-colors hover:bg-muted/50",
isActive && "border-l-2 border-primary bg-slate-800/70" isActive && "border-l-2 border-primary bg-muted/70"
)} )}
> >
{/* Avatar */} {/* Avatar */}
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-slate-700 text-sm font-medium text-white"> <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-muted text-sm font-medium text-foreground">
{contact?.avatar_url ? ( {contact?.avatar_url ? (
<img <img
src={contact.avatar_url} src={contact.avatar_url}
@ -268,13 +268,13 @@ function ConversationItem({
{/* Content */} {/* Content */}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="truncate text-sm font-medium text-white"> <span className="truncate text-sm font-medium text-foreground">
{displayName} {displayName}
</span> </span>
<span className="shrink-0 text-[10px] text-slate-500">{timeAgo}</span> <span className="shrink-0 text-[10px] text-muted-foreground">{timeAgo}</span>
</div> </div>
<div className="mt-0.5 flex items-center justify-between gap-2"> <div className="mt-0.5 flex items-center justify-between gap-2">
<p className="truncate text-xs text-slate-400"> <p className="truncate text-xs text-muted-foreground">
{conversation.last_message_text || "No messages yet"} {conversation.last_message_text || "No messages yet"}
</p> </p>
<div className="flex shrink-0 items-center gap-1.5"> <div className="flex shrink-0 items-center gap-1.5">

View File

@ -95,7 +95,7 @@ export function MessageActions({
<div <div
data-touch-open={touchOpen || pickerOpen ? "true" : undefined} data-touch-open={touchOpen || pickerOpen ? "true" : undefined}
className={cn( className={cn(
"absolute -top-3 z-10 flex h-7 items-center gap-0.5 rounded-full border border-slate-700 bg-slate-900/95 px-1 shadow-md backdrop-blur-sm transition-opacity", "absolute -top-3 z-10 flex h-7 items-center gap-0.5 rounded-full border border-border bg-popover/95 px-1 shadow-md backdrop-blur-sm transition-opacity",
"opacity-0 group-hover/actions:opacity-100 group-focus-within/actions:opacity-100", "opacity-0 group-hover/actions:opacity-100 group-focus-within/actions:opacity-100",
"data-[touch-open=true]:opacity-100", "data-[touch-open=true]:opacity-100",
isAgent ? "right-3" : "left-3", isAgent ? "right-3" : "left-3",
@ -103,7 +103,7 @@ export function MessageActions({
> >
<Popover open={pickerOpen} onOpenChange={setPickerOpen}> <Popover open={pickerOpen} onOpenChange={setPickerOpen}>
<PopoverTrigger <PopoverTrigger
className="flex h-5 w-5 items-center justify-center rounded-full text-slate-300 hover:bg-slate-700 hover:text-white" className="flex h-5 w-5 items-center justify-center rounded-full text-popover-foreground hover:bg-muted hover:text-foreground"
aria-label="React" aria-label="React"
> >
<SmilePlus className="h-3.5 w-3.5" /> <SmilePlus className="h-3.5 w-3.5" />
@ -117,7 +117,7 @@ export function MessageActions({
key={e} key={e}
type="button" type="button"
onClick={() => handlePickEmoji(e)} onClick={() => handlePickEmoji(e)}
className="flex h-8 w-8 items-center justify-center rounded-full text-lg leading-none transition-transform hover:scale-125 hover:bg-slate-700" className="flex h-8 w-8 items-center justify-center rounded-full text-lg leading-none transition-transform hover:scale-125 hover:bg-muted"
aria-label={`React with ${e}`} aria-label={`React with ${e}`}
> >
{e} {e}
@ -128,7 +128,7 @@ export function MessageActions({
<button <button
type="button" type="button"
onClick={handleReply} onClick={handleReply}
className="flex h-5 w-5 items-center justify-center rounded-full text-slate-300 hover:bg-slate-700 hover:text-white" className="flex h-5 w-5 items-center justify-center rounded-full text-popover-foreground hover:bg-muted hover:text-foreground"
aria-label="Reply" aria-label="Reply"
> >
<CornerUpLeft className="h-3.5 w-3.5" /> <CornerUpLeft className="h-3.5 w-3.5" />
@ -136,7 +136,7 @@ export function MessageActions({
<button <button
type="button" type="button"
onClick={handleCopy} onClick={handleCopy}
className="flex h-5 w-5 items-center justify-center rounded-full text-slate-300 hover:bg-slate-700 hover:text-white" className="flex h-5 w-5 items-center justify-center rounded-full text-popover-foreground hover:bg-muted hover:text-foreground"
aria-label="Copy" aria-label="Copy"
> >
<Copy className="h-3.5 w-3.5" /> <Copy className="h-3.5 w-3.5" />

View File

@ -30,11 +30,11 @@ interface MessageBubbleProps {
function StatusIcon({ status }: { status: Message["status"] }) { function StatusIcon({ status }: { status: Message["status"] }) {
switch (status) { switch (status) {
case "sending": case "sending":
return <Clock className="h-3 w-3 text-slate-400" />; return <Clock className="h-3 w-3 text-muted-foreground" />;
case "sent": case "sent":
return <Check className="h-3 w-3 text-slate-400" />; return <Check className="h-3 w-3 text-muted-foreground" />;
case "delivered": case "delivered":
return <CheckCheck className="h-3 w-3 text-slate-400" />; return <CheckCheck className="h-3 w-3 text-muted-foreground" />;
case "read": case "read":
return <CheckCheck className="h-3 w-3 text-blue-400" />; return <CheckCheck className="h-3 w-3 text-blue-400" />;
case "failed": case "failed":
@ -46,8 +46,8 @@ function StatusIcon({ status }: { status: Message["status"] }) {
function MediaUnavailable({ label }: { label: string }) { function MediaUnavailable({ label }: { label: string }) {
return ( return (
<div className="flex items-center gap-2 rounded-lg bg-slate-700/40 px-3 py-2 text-xs text-slate-300"> <div className="flex items-center gap-2 rounded-lg bg-muted/40 px-3 py-2 text-xs text-muted-foreground">
<ImageOff className="h-4 w-4 shrink-0 text-slate-500" /> <ImageOff className="h-4 w-4 shrink-0 text-muted-foreground" />
<span>{label} unavailable</span> <span>{label} unavailable</span>
</div> </div>
); );
@ -92,15 +92,15 @@ function MediaImage({ url, alt }: { url: string; alt: string }) {
if (error) { if (error) {
return ( return (
<div className="flex h-40 w-60 items-center justify-center rounded-lg bg-slate-700"> <div className="flex h-40 w-60 items-center justify-center rounded-lg bg-muted">
<ImageOff className="h-8 w-8 text-slate-500" /> <ImageOff className="h-8 w-8 text-muted-foreground" />
</div> </div>
); );
} }
if (loading) { if (loading) {
return ( return (
<div className="flex h-40 w-60 items-center justify-center rounded-lg bg-slate-700"> <div className="flex h-40 w-60 items-center justify-center rounded-lg bg-muted">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" /> <div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
</div> </div>
); );
@ -181,9 +181,9 @@ function MessageContent({ message }: { message: Message }) {
href={message.media_url} href={message.media_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-2 rounded-lg bg-slate-700/50 px-3 py-2 text-sm hover:bg-slate-700" className="flex items-center gap-2 rounded-lg bg-muted/50 px-3 py-2 text-sm hover:bg-muted"
> >
<FileText className="h-5 w-5 shrink-0 text-slate-400" /> <FileText className="h-5 w-5 shrink-0 text-muted-foreground" />
<span className="truncate"> <span className="truncate">
{message.content_text || "Document"} {message.content_text || "Document"}
</span> </span>
@ -208,7 +208,7 @@ function MessageContent({ message }: { message: Message }) {
case "location": case "location":
return ( return (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<MapPin className="h-4 w-4 shrink-0 text-slate-400" /> <MapPin className="h-4 w-4 shrink-0 text-muted-foreground" />
<span>{message.content_text || "Location shared"}</span> <span>{message.content_text || "Location shared"}</span>
</div> </div>
); );
@ -221,7 +221,7 @@ function MessageContent({ message }: { message: Message }) {
// tap rather than the customer typing the same words. // tap rather than the customer typing the same words.
return ( return (
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<span className="inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-slate-400"> <span className="inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
<CornerDownLeft className="h-3 w-3" /> <CornerDownLeft className="h-3 w-3" />
Button reply Button reply
</span> </span>
@ -265,7 +265,7 @@ export function MessageBubble({
"relative rounded-2xl px-3 py-2", "relative rounded-2xl px-3 py-2",
isAgent isAgent
? "rounded-br-md bg-primary text-primary-foreground" ? "rounded-br-md bg-primary text-primary-foreground"
: "rounded-bl-md bg-slate-800 text-slate-100", : "rounded-bl-md bg-muted text-foreground",
)} )}
> >
{reply && ( {reply && (
@ -278,7 +278,18 @@ export function MessageBubble({
isAgent ? "justify-end" : "justify-start", isAgent ? "justify-end" : "justify-start",
)} )}
> >
<span className="text-[10px] text-white/60">{time}</span> <span
className={cn(
"text-[10px]",
// Outbound bubbles sit on the primary fill, so the
// timestamp must read against that (not the neutral
// foreground) — otherwise it goes low-contrast in light
// mode. Inbound bubbles use the muted surface.
isAgent ? "text-primary-foreground/70" : "text-muted-foreground",
)}
>
{time}
</span>
{isAgent && <StatusIcon status={message.status} />} {isAgent && <StatusIcon status={message.status} />}
</div> </div>
</div> </div>

View File

@ -84,7 +84,7 @@ export function MessageComposer({
); );
return ( return (
<div className="border-t border-slate-800 bg-slate-900 p-3"> <div className="border-t border-border bg-card p-3">
{replyTo && ( {replyTo && (
<div className="mb-2"> <div className="mb-2">
<ReplyQuote <ReplyQuote
@ -118,7 +118,7 @@ export function MessageComposer({
canAct={!readOnly} canAct={!readOnly}
gateReason="send messages" gateReason="send messages"
title={readOnly ? undefined : "Send template"} title={readOnly ? undefined : "Send template"}
className="h-9 w-9 shrink-0 p-0 text-slate-400 hover:text-white" className="h-9 w-9 shrink-0 p-0 text-muted-foreground hover:text-foreground"
onClick={onOpenTemplates} onClick={onOpenTemplates}
> >
<LayoutTemplate className="h-4 w-4" /> <LayoutTemplate className="h-4 w-4" />
@ -143,7 +143,7 @@ export function MessageComposer({
// The placeholder text also surfaces the read-only state. // The placeholder text also surfaces the read-only state.
title={readOnly ? "Read-only — your role can't send messages" : undefined} title={readOnly ? "Read-only — your role can't send messages" : undefined}
className={cn( className={cn(
"flex-1 resize-none rounded-xl border border-slate-700 bg-slate-800 px-4 py-2.5 text-sm text-white placeholder-slate-500 outline-none transition-colors focus:border-primary/50", "flex-1 resize-none rounded-xl border border-border bg-muted px-4 py-2.5 text-sm text-foreground placeholder-muted-foreground outline-none transition-colors focus:border-primary/50",
(sessionExpired || readOnly) && "cursor-not-allowed opacity-50" (sessionExpired || readOnly) && "cursor-not-allowed opacity-50"
)} )}
/> />
@ -163,7 +163,7 @@ export function MessageComposer({
{/* Hint sits outside the flex row so its height doesn't push {/* Hint sits outside the flex row so its height doesn't push
`items-end` buttons below the textarea. Indented to line up `items-end` buttons below the textarea. Indented to line up
under the textarea left edge (w-9 button + gap-2 = 44px). */} under the textarea left edge (w-9 button + gap-2 = 44px). */}
<p className="mt-1 pl-11 text-[10px] text-slate-600"> <p className="mt-1 pl-11 text-[10px] text-muted-foreground">
Type &apos;/&apos; for quick replies Type &apos;/&apos; for quick replies
</p> </p>
</div> </div>

View File

@ -63,7 +63,7 @@ export function MessageReactions({
"inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[11px] leading-none transition-colors", "inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[11px] leading-none transition-colors",
g.byCurrentUser g.byCurrentUser
? "border-primary/60 bg-primary/15 text-primary hover:bg-primary/25" ? "border-primary/60 bg-primary/15 text-primary hover:bg-primary/25"
: "border-slate-700 bg-slate-800/80 text-slate-200 hover:bg-slate-700", : "border-border bg-muted/80 text-foreground hover:bg-muted",
)} )}
> >
<span className="text-sm leading-none">{g.emoji}</span> <span className="text-sm leading-none">{g.emoji}</span>

View File

@ -116,7 +116,7 @@ function groupMessagesByDate(messages: Message[]) {
const STATUS_OPTIONS: { label: string; value: ConversationStatus; color: string }[] = [ const STATUS_OPTIONS: { label: string; value: ConversationStatus; color: string }[] = [
{ label: "Open", value: "open", color: "text-primary" }, { label: "Open", value: "open", color: "text-primary" },
{ label: "Pending", value: "pending", color: "text-amber-400" }, { label: "Pending", value: "pending", color: "text-amber-400" },
{ label: "Closed", value: "closed", color: "text-slate-400" }, { label: "Closed", value: "closed", color: "text-muted-foreground" },
]; ];
/** /**
@ -129,7 +129,7 @@ const STATUS_OPTIONS: { label: string; value: ConversationStatus; color: string
* if we ever switch the asset, both spots update together. * if we ever switch the asset, both spots update together.
*/ */
const DOODLE_BG_CLASSES = const DOODLE_BG_CLASSES =
"bg-slate-950 bg-[url('/inbox-doodle.svg')] bg-repeat"; "bg-background bg-[url('/inbox-doodle.svg')] bg-repeat";
export function MessageThread({ export function MessageThread({
conversation, conversation,
@ -696,13 +696,13 @@ export function MessageThread({
if (!conversation || !contact) { if (!conversation || !contact) {
return ( return (
<div className={cn("flex flex-1 flex-col items-center justify-center", DOODLE_BG_CLASSES)}> <div className={cn("flex flex-1 flex-col items-center justify-center", DOODLE_BG_CLASSES)}>
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-slate-800"> <div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
<MessageSquare className="h-8 w-8 text-slate-600" /> <MessageSquare className="h-8 w-8 text-muted-foreground" />
</div> </div>
<h3 className="mt-4 text-sm font-medium text-slate-400"> <h3 className="mt-4 text-sm font-medium text-muted-foreground">
Select a conversation Select a conversation
</h3> </h3>
<p className="mt-1 text-xs text-slate-600"> <p className="mt-1 text-xs text-muted-foreground">
Choose a conversation from the left to start messaging Choose a conversation from the left to start messaging
</p> </p>
</div> </div>
@ -722,9 +722,9 @@ export function MessageThread({
return ( return (
<div className={cn("flex flex-1 flex-col", DOODLE_BG_CLASSES)}> <div className={cn("flex flex-1 flex-col", DOODLE_BG_CLASSES)}>
{/* Header — solid bg-slate-900 sits on top of the doodle so the {/* Header — solid card surface sits on top of the doodle so the
name/avatar/dropdowns stay legible. */} name/avatar/dropdowns stay legible. */}
<div className="flex items-center justify-between gap-2 border-b border-slate-800 bg-slate-900 px-3 py-3 sm:px-4"> <div className="flex items-center justify-between gap-2 border-b border-border bg-card px-3 py-3 sm:px-4">
<div className="flex min-w-0 items-center gap-2 sm:gap-3"> <div className="flex min-w-0 items-center gap-2 sm:gap-3">
{/* Back-to-list button — mobile only. Hidden on lg+ where the {/* Back-to-list button — mobile only. Hidden on lg+ where the
conversation list is always visible next to the thread. */} conversation list is always visible next to the thread. */}
@ -733,24 +733,24 @@ export function MessageThread({
type="button" type="button"
onClick={onBack} onClick={onBack}
aria-label="Back to conversations" aria-label="Back to conversations"
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md text-slate-300 hover:bg-slate-800 hover:text-white lg:hidden" className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden"
> >
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
</button> </button>
)} )}
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-slate-700 text-sm font-medium text-white"> <div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-muted text-sm font-medium text-foreground">
{displayName.charAt(0).toUpperCase()} {displayName.charAt(0).toUpperCase()}
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<h2 className="truncate text-sm font-semibold text-white">{displayName}</h2> <h2 className="truncate text-sm font-semibold text-foreground">{displayName}</h2>
<p className="truncate text-xs text-slate-400">{contact.phone}</p> <p className="truncate text-xs text-muted-foreground">{contact.phone}</p>
</div> </div>
{/* Session timer badge — hidden on the narrowest phones so {/* Session timer badge — hidden on the narrowest phones so
the name + back arrow keep their room. */} the name + back arrow keep their room. */}
<Badge <Badge
variant="outline" variant="outline"
className={cn( className={cn(
"ml-1 hidden gap-1 border-slate-700 text-[10px] sm:inline-flex sm:ml-2", "ml-1 hidden gap-1 border-border text-[10px] sm:inline-flex sm:ml-2",
sessionInfo.expired ? "text-red-400" : "text-primary" sessionInfo.expired ? "text-red-400" : "text-primary"
)} )}
> >
@ -773,7 +773,7 @@ export function MessageThread({
aria-label="Refresh conversation" aria-label="Refresh conversation"
title="Refresh" title="Refresh"
className={cn( className={cn(
"inline-flex h-7 w-7 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-slate-800 hover:text-white disabled:opacity-60", "inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-60",
)} )}
> >
<RefreshCw <RefreshCw
@ -785,15 +785,15 @@ export function MessageThread({
{/* Status dropdown */} {/* Status dropdown */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger className={cn( <DropdownMenuTrigger className={cn(
"inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-slate-800", "inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-muted",
currentStatus?.color ?? "text-slate-400" currentStatus?.color ?? "text-muted-foreground"
)}> )}>
{currentStatus?.label ?? "Status"} {currentStatus?.label ?? "Status"}
<ChevronDown className="h-3 w-3" /> <ChevronDown className="h-3 w-3" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
className="border-slate-700 bg-slate-800" className="border-border bg-popover"
> >
{STATUS_OPTIONS.map((opt) => ( {STATUS_OPTIONS.map((opt) => (
<DropdownMenuItem <DropdownMenuItem
@ -811,8 +811,8 @@ export function MessageThread({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
className={cn( className={cn(
"inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-slate-800", "inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-muted",
assignedAgentId ? "text-primary" : "text-slate-400" assignedAgentId ? "text-primary" : "text-muted-foreground"
)} )}
> >
<UserPlus className="h-3 w-3" /> <UserPlus className="h-3 w-3" />
@ -821,10 +821,10 @@ export function MessageThread({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
className="border-slate-700 bg-slate-800" className="border-border bg-popover"
> >
{profiles.length === 0 ? ( {profiles.length === 0 ? (
<DropdownMenuItem disabled className="text-sm text-slate-500"> <DropdownMenuItem disabled className="text-sm text-muted-foreground">
No teammates available No teammates available
</DropdownMenuItem> </DropdownMenuItem>
) : ( ) : (
@ -836,7 +836,7 @@ export function MessageThread({
onClick={() => handleAssignChange(p.user_id)} onClick={() => handleAssignChange(p.user_id)}
className={cn( className={cn(
"text-sm", "text-sm",
isSelected ? "text-primary" : "text-slate-300" isSelected ? "text-primary" : "text-popover-foreground"
)} )}
> >
<span className="flex-1"> <span className="flex-1">
@ -850,10 +850,10 @@ export function MessageThread({
)} )}
{assignedAgentId && ( {assignedAgentId && (
<> <>
<DropdownMenuSeparator className="bg-slate-700" /> <DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleAssignChange(null)} onClick={() => handleAssignChange(null)}
className="text-sm text-slate-400" className="text-sm text-muted-foreground"
> >
Unassign Unassign
</DropdownMenuItem> </DropdownMenuItem>
@ -872,8 +872,8 @@ export function MessageThread({
</div> </div>
) : messages.length === 0 ? ( ) : messages.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12"> <div className="flex flex-col items-center justify-center py-12">
<p className="text-sm text-slate-500">No messages yet</p> <p className="text-sm text-muted-foreground">No messages yet</p>
<p className="text-xs text-slate-600"> <p className="text-xs text-muted-foreground">
Send a template to start the conversation Send a template to start the conversation
</p> </p>
</div> </div>
@ -883,7 +883,7 @@ export function MessageThread({
<div key={group.date}> <div key={group.date}>
{/* Date separator */} {/* Date separator */}
<div className="mb-4 flex items-center justify-center"> <div className="mb-4 flex items-center justify-center">
<span className="rounded-full bg-slate-800 px-3 py-1 text-[10px] font-medium text-slate-400"> <span className="rounded-full bg-muted px-3 py-1 text-[10px] font-medium text-muted-foreground">
{formatDateSeparator(group.date)} {formatDateSeparator(group.date)}
</span> </span>
</div> </div>

View File

@ -27,8 +27,8 @@ export function ReplyQuote({
className={cn( className={cn(
"flex items-start gap-2 border-l-2 border-primary px-2 py-1", "flex items-start gap-2 border-l-2 border-primary px-2 py-1",
isChip isChip
? "rounded-md bg-slate-800/80" ? "rounded-md bg-muted/80"
: "mb-1.5 rounded-md bg-black/20", : "mb-1.5 rounded-md bg-background/20",
)} )}
> >
<div className="min-w-0 flex-1 overflow-hidden"> <div className="min-w-0 flex-1 overflow-hidden">
@ -42,7 +42,7 @@ export function ReplyQuote({
* layout wider, shoving the contact sidebar off-screen. * layout wider, shoving the contact sidebar off-screen.
* `break-words` also wraps long URLs that have no whitespace * `break-words` also wraps long URLs that have no whitespace
* to break on. Issue #165. */} * to break on. Issue #165. */}
<div className="whitespace-pre-wrap break-words text-xs text-slate-200/80"> <div className="whitespace-pre-wrap break-words text-xs text-foreground/80">
{preview} {preview}
</div> </div>
</div> </div>
@ -51,7 +51,7 @@ export function ReplyQuote({
type="button" type="button"
onClick={onDismiss} onClick={onDismiss}
aria-label="Cancel reply" aria-label="Cancel reply"
className="flex h-6 w-6 shrink-0 items-center justify-center rounded text-slate-400 hover:bg-slate-700 hover:text-white" className="flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground"
> >
<X className="h-3.5 w-3.5" /> <X className="h-3.5 w-3.5" />
</button> </button>

View File

@ -183,13 +183,13 @@ export function TemplatePicker({
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="border-slate-700 bg-slate-900 sm:max-w-lg"> <DialogContent className="border-border bg-popover sm:max-w-lg">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-white"> <DialogTitle className="flex items-center gap-2 text-popover-foreground">
<LayoutTemplate className="h-4 w-4 text-primary" /> <LayoutTemplate className="h-4 w-4 text-primary" />
{selected ? selected.name : "Send template"} {selected ? selected.name : "Send template"}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
{selected {selected
? "Fill in the placeholders to render this template. Meta requires every variable to be set." ? "Fill in the placeholders to render this template. Meta requires every variable to be set."
: "Pick an approved WhatsApp template to send to this contact."} : "Pick an approved WhatsApp template to send to this contact."}
@ -203,9 +203,9 @@ export function TemplatePicker({
<Loader2 className="h-5 w-5 animate-spin text-primary" /> <Loader2 className="h-5 w-5 animate-spin text-primary" />
</div> </div>
) : templates.length === 0 ? ( ) : templates.length === 0 ? (
<div className="rounded-md border border-slate-800 bg-slate-950/50 p-6 text-center"> <div className="rounded-md border border-border bg-background/50 p-6 text-center">
<p className="text-sm text-slate-300">No approved templates</p> <p className="text-sm text-popover-foreground">No approved templates</p>
<p className="mt-1 text-xs text-slate-500"> <p className="mt-1 text-xs text-muted-foreground">
Approve a template in Meta WhatsApp Manager, then sync it Approve a template in Meta WhatsApp Manager, then sync it
from Settings → Templates. from Settings → Templates.
</p> </p>
@ -216,28 +216,28 @@ export function TemplatePicker({
key={t.id} key={t.id}
type="button" type="button"
onClick={() => pickTemplate(t)} onClick={() => pickTemplate(t)}
className="w-full rounded-md border border-slate-800 bg-slate-950/50 p-3 text-left transition-colors hover:border-primary/40 hover:bg-slate-900" className="w-full rounded-md border border-border bg-background/50 p-3 text-left transition-colors hover:border-primary/40 hover:bg-popover"
> >
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<p className="truncate text-sm font-medium text-white"> <p className="truncate text-sm font-medium text-popover-foreground">
{t.name} {t.name}
</p> </p>
<Badge className="border border-primary/30 bg-primary/20 text-[10px] text-primary"> <Badge className="border border-primary/30 bg-primary/20 text-[10px] text-primary">
{t.category} {t.category}
</Badge> </Badge>
{t.language && ( {t.language && (
<span className="text-[10px] uppercase text-slate-500"> <span className="text-[10px] uppercase text-muted-foreground">
{t.language} {t.language}
</span> </span>
)} )}
</div> </div>
<p className="mt-1 line-clamp-2 text-xs text-slate-400"> <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{t.body_text} {t.body_text}
</p> </p>
</div> </div>
<ChevronRight className="h-4 w-4 flex-shrink-0 text-slate-500" /> <ChevronRight className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
</div> </div>
</button> </button>
)) ))
@ -245,33 +245,33 @@ export function TemplatePicker({
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
<div className="rounded-md border border-slate-800 bg-slate-950/50 p-3"> <div className="rounded-md border border-border bg-background/50 p-3">
<p className="mb-1 text-xs text-slate-400">Preview</p> <p className="mb-1 text-xs text-muted-foreground">Preview</p>
<p className="whitespace-pre-wrap text-sm text-slate-200"> <p className="whitespace-pre-wrap text-sm text-popover-foreground">
{renderBodyPreview(selected.body_text, params)} {renderBodyPreview(selected.body_text, params)}
</p> </p>
{selected.footer_text && ( {selected.footer_text && (
<p className="mt-2 text-xs italic text-slate-500"> <p className="mt-2 text-xs italic text-muted-foreground">
{selected.footer_text} {selected.footer_text}
</p> </p>
)} )}
</div> </div>
{slots && slots.headerVarCount > 0 && ( {slots && slots.headerVarCount > 0 && (
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-xs text-slate-300"> <Label className="text-xs text-popover-foreground">
{`Header {{1}}`} {`Header {{1}}`}
</Label> </Label>
<Input <Input
value={headerText} value={headerText}
onChange={(e) => setHeaderText(e.target.value)} onChange={(e) => setHeaderText(e.target.value)}
placeholder="Value for the header variable" placeholder="Value for the header variable"
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500" className="border-border bg-muted text-foreground placeholder:text-muted-foreground"
/> />
</div> </div>
)} )}
{slots?.bodyVars.map((v, i) => ( {slots?.bodyVars.map((v, i) => (
<div key={v} className="space-y-1"> <div key={v} className="space-y-1">
<Label className="text-xs text-slate-300">{`Body {{${v}}}`}</Label> <Label className="text-xs text-popover-foreground">{`Body {{${v}}}`}</Label>
<Input <Input
value={params[i] ?? ""} value={params[i] ?? ""}
onChange={(e) => { onChange={(e) => {
@ -280,13 +280,13 @@ export function TemplatePicker({
setParams(next); setParams(next);
}} }}
placeholder={`Value for {{${v}}}`} placeholder={`Value for {{${v}}}`}
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500" className="border-border bg-muted text-foreground placeholder:text-muted-foreground"
/> />
</div> </div>
))} ))}
{slots?.urlButtonSlots.map((slot) => ( {slots?.urlButtonSlots.map((slot) => (
<div key={slot.index} className="space-y-1"> <div key={slot.index} className="space-y-1">
<Label className="text-xs text-slate-300"> <Label className="text-xs text-popover-foreground">
{`URL button "${slot.text}" — value for `}{`{{1}}`} {`URL button "${slot.text}" — value for `}{`{{1}}`}
</Label> </Label>
<Input <Input
@ -298,9 +298,9 @@ export function TemplatePicker({
})) }))
} }
placeholder="URL suffix value" placeholder="URL suffix value"
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500" className="border-border bg-muted text-foreground placeholder:text-muted-foreground"
/> />
<p className="text-[10px] text-slate-500 break-all"> <p className="text-[10px] text-muted-foreground break-all">
Final URL: {slot.url.replace(/\{\{1\}\}/g, buttonParams[slot.index] || "{{1}}")} Final URL: {slot.url.replace(/\{\{1\}\}/g, buttonParams[slot.index] || "{{1}}")}
</p> </p>
</div> </div>
@ -314,7 +314,7 @@ export function TemplatePicker({
<Button <Button
variant="outline" variant="outline"
onClick={resetSelection} onClick={resetSelection}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-popover-foreground hover:bg-muted"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
Back Back
@ -331,7 +331,7 @@ export function TemplatePicker({
<Button <Button
variant="outline" variant="outline"
onClick={() => handleOpenChange(false)} onClick={() => handleOpenChange(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-popover-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>

View File

@ -16,6 +16,7 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { ModeToggle } from "@/components/layout/mode-toggle";
const pageTitles: Record<string, string> = { const pageTitles: Record<string, string> = {
"/dashboard": "Dashboard", "/dashboard": "Dashboard",
@ -52,25 +53,28 @@ export function Header({ onOpenSidebar }: HeaderProps) {
"U"; "U";
return ( return (
<header className="flex h-14 shrink-0 items-center justify-between gap-3 border-b border-slate-800 bg-slate-950 px-4 lg:px-6"> <header className="flex h-14 shrink-0 items-center justify-between gap-3 border-b border-border bg-background px-4 lg:px-6">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
{/* Hamburger — mobile only. 44×44 hit target per Apple HIG. */} {/* Hamburger — mobile only. 44×44 hit target per Apple HIG. */}
<button <button
type="button" type="button"
onClick={onOpenSidebar} onClick={onOpenSidebar}
aria-label="Open menu" aria-label="Open menu"
className="flex h-10 w-10 items-center justify-center rounded-md text-slate-300 transition-colors hover:bg-slate-800 hover:text-white lg:hidden" className="flex h-10 w-10 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground lg:hidden"
> >
<Menu className="h-5 w-5" /> <Menu className="h-5 w-5" />
</button> </button>
<h1 className="truncate text-base font-semibold text-white sm:text-lg"> <h1 className="truncate text-base font-semibold text-foreground sm:text-lg">
{title} {title}
</h1> </h1>
</div> </div>
<DropdownMenu> <div className="flex items-center gap-1 sm:gap-2">
<ModeToggle />
<DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
className="flex items-center gap-2 rounded-md px-1 py-1 transition-colors hover:bg-slate-800/70 focus:bg-slate-800/70 focus:outline-none data-popup-open:bg-slate-800/70 sm:gap-3 sm:pl-1 sm:pr-3" className="flex items-center gap-2 rounded-md px-1 py-1 transition-colors hover:bg-muted/70 focus:bg-muted/70 focus:outline-none data-popup-open:bg-muted/70 sm:gap-3 sm:pl-1 sm:pr-3"
aria-label="Open account menu" aria-label="Open account menu"
> >
<Avatar className="size-8"> <Avatar className="size-8">
@ -84,29 +88,29 @@ export function Header({ onOpenSidebar }: HeaderProps) {
{initial} {initial}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<span className="hidden text-sm font-medium text-white sm:inline"> <span className="hidden text-sm font-medium text-foreground sm:inline">
{profile?.full_name ?? "User"} {profile?.full_name ?? "User"}
</span> </span>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
sideOffset={6} sideOffset={6}
className="min-w-56 bg-slate-900 text-slate-100 ring-slate-700" className="min-w-56 bg-popover text-popover-foreground ring-border"
> >
<div className="px-2 py-1.5"> <div className="px-2 py-1.5">
<p className="truncate text-sm font-medium text-white"> <p className="truncate text-sm font-medium text-foreground">
{profile?.full_name ?? "User"} {profile?.full_name ?? "User"}
</p> </p>
<p className="truncate text-xs text-slate-400"> <p className="truncate text-xs text-muted-foreground">
{profile?.email ?? ""} {profile?.email ?? ""}
</p> </p>
</div> </div>
<DropdownMenuSeparator className="bg-slate-800" /> <DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem <DropdownMenuItem
render={ render={
<Link <Link
href="/settings?tab=profile" href="/settings?tab=profile"
className="text-slate-200 focus:bg-slate-800 focus:text-white" className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
/> />
} }
> >
@ -117,23 +121,24 @@ export function Header({ onOpenSidebar }: HeaderProps) {
render={ render={
<Link <Link
href="/settings?tab=whatsapp" href="/settings?tab=whatsapp"
className="text-slate-200 focus:bg-slate-800 focus:text-white" className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
/> />
} }
> >
<SettingsIcon className="size-4" /> <SettingsIcon className="size-4" />
Settings Settings
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className="bg-slate-800" /> <DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem <DropdownMenuItem
onClick={signOut} onClick={signOut}
className="text-slate-200 focus:bg-slate-800 focus:text-white" className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
> >
<LogOut className="size-4" /> <LogOut className="size-4" />
Sign out Sign out
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div>
</header> </header>
); );
} }

View File

@ -0,0 +1,37 @@
"use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "@/hooks/use-theme";
import { cn } from "@/lib/utils";
/**
* Light/dark mode toggle — a single icon button that flips the app
* between the two modes. Sun shows in light mode (click → go dark),
* moon shows in dark mode (click → go light); the label always names
* the destination so screen-reader users hear what the click does.
*
* 40×40 hit target to match the header's other touch controls.
*/
export function ModeToggle({ className }: { className?: string }) {
const { mode, toggleMode } = useTheme();
const goingTo = mode === "dark" ? "light" : "dark";
return (
<button
type="button"
onClick={toggleMode}
aria-label={`Switch to ${goingTo} mode`}
title={`Switch to ${goingTo} mode`}
className={cn(
"flex h-10 w-10 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
className,
)}
>
{mode === "dark" ? (
<Moon className="h-5 w-5" />
) : (
<Sun className="h-5 w-5" />
)}
</button>
);
}

View File

@ -52,14 +52,14 @@ const ROLE_CHIP: Record<
label: "Agent", label: "Agent",
// Neutral slate: the operational default. // Neutral slate: the operational default.
className: className:
"border-slate-700 bg-slate-800 text-slate-300", "border-border bg-muted text-foreground",
}, },
viewer: { viewer: {
icon: User, icon: User,
label: "Viewer", label: "Viewer",
// Muted slate: read-only role; visually quieter than agent. // Muted slate: read-only role; visually quieter than agent.
className: className:
"border-slate-800 bg-slate-900 text-slate-500", "border-border bg-card text-muted-foreground",
}, },
}; };
import { import {
@ -157,7 +157,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
aria-label="Close menu" aria-label="Close menu"
onClick={onClose} onClick={onClose}
className={cn( className={cn(
"fixed inset-0 z-30 bg-slate-950/70 backdrop-blur-sm transition-opacity lg:hidden", "fixed inset-0 z-30 bg-background/70 backdrop-blur-sm transition-opacity lg:hidden",
open open
? "pointer-events-auto opacity-100" ? "pointer-events-auto opacity-100"
: "pointer-events-none opacity-0", : "pointer-events-none opacity-0",
@ -167,7 +167,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
<aside <aside
className={cn( className={cn(
// Mobile: fixed drawer that slides in from the left. // Mobile: fixed drawer that slides in from the left.
"fixed inset-y-0 left-0 z-40 flex h-full w-64 flex-col border-r border-slate-800 bg-slate-900", "fixed inset-y-0 left-0 z-40 flex h-full w-64 flex-col border-r border-border bg-card",
"transition-transform duration-200 ease-out will-change-transform", "transition-transform duration-200 ease-out will-change-transform",
open ? "translate-x-0" : "-translate-x-full", open ? "translate-x-0" : "-translate-x-full",
// Desktop: static, always visible — reset all the mobile framing. // Desktop: static, always visible — reset all the mobile framing.
@ -177,12 +177,12 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
> >
{/* Logo row. On mobile we put a close button here; on desktop the {/* Logo row. On mobile we put a close button here; on desktop the
close button is hidden since the sidebar is always-visible. */} close button is hidden since the sidebar is always-visible. */}
<div className="flex h-14 shrink-0 items-center justify-between gap-2 border-b border-slate-800 px-4"> <div className="flex h-14 shrink-0 items-center justify-between gap-2 border-b border-border px-4">
<Link href="/dashboard" className="flex items-center gap-2"> <Link href="/dashboard" className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<MessageSquare className="h-4 w-4" /> <MessageSquare className="h-4 w-4" />
</div> </div>
<span className="text-sm font-semibold text-white"> <span className="text-sm font-semibold text-foreground">
CRM Template for WhatsApp CRM Template for WhatsApp
</span> </span>
</Link> </Link>
@ -190,7 +190,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
type="button" type="button"
onClick={onClose} onClick={onClose}
aria-label="Close menu" aria-label="Close menu"
className="flex h-9 w-9 items-center justify-center rounded-md text-slate-400 hover:bg-slate-800 hover:text-white lg:hidden" className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden"
> >
<X className="h-5 w-5" /> <X className="h-5 w-5" />
</button> </button>
@ -216,7 +216,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2", "flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2",
isActive isActive
? "bg-primary/10 text-primary" ? "bg-primary/10 text-primary"
: "text-slate-400 hover:bg-slate-800 hover:text-white", : "text-muted-foreground hover:bg-muted hover:text-foreground",
)} )}
> >
<item.icon className="h-4 w-4" /> <item.icon className="h-4 w-4" />
@ -244,7 +244,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
})} })}
</ul> </ul>
<div className="my-4 border-t border-slate-800" /> <div className="my-4 border-t border-border" />
<ul className="flex flex-col gap-1"> <ul className="flex flex-col gap-1">
{bottomNavItems.map((item) => { {bottomNavItems.map((item) => {
@ -257,7 +257,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2", "flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2",
isActive isActive
? "bg-primary/10 text-primary" ? "bg-primary/10 text-primary"
: "text-slate-400 hover:bg-slate-800 hover:text-white", : "text-muted-foreground hover:bg-muted hover:text-foreground",
)} )}
> >
<item.icon className="h-4 w-4" /> <item.icon className="h-4 w-4" />
@ -270,7 +270,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
</nav> </nav>
{/* User section */} {/* User section */}
<div className="shrink-0 border-t border-slate-800 p-3"> <div className="shrink-0 border-t border-border p-3">
{/* Account name display — surfaced only when the account {/* Account name display — surfaced only when the account
name differs from the user's own name (see name differs from the user's own name (see
`showAccountStrip`). For a default solo account the two `showAccountStrip`). For a default solo account the two
@ -278,7 +278,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
below; for renamed or shared accounts it tells the user below; for renamed or shared accounts it tells the user
which account they're acting in. */} which account they're acting in. */}
{showAccountStrip && account?.name ? ( {showAccountStrip && account?.name ? (
<div className="mb-2 flex items-center gap-2 px-3 text-xs text-slate-500"> <div className="mb-2 flex items-center gap-2 px-3 text-xs text-muted-foreground">
<UsersRound className="size-3.5 shrink-0" /> <UsersRound className="size-3.5 shrink-0" />
{/* `title=` exposes the full name on hover when it {/* `title=` exposes the full name on hover when it
gets truncated (long account names + narrow gets truncated (long account names + narrow
@ -307,7 +307,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
</div> </div>
) : null} ) : null}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-slate-800/60 focus:bg-slate-800/60 focus:outline-none data-popup-open:bg-slate-800/60"> <DropdownMenuTrigger className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-muted/60 focus:bg-muted/60 focus:outline-none data-popup-open:bg-muted/60">
<Avatar className="size-8 shrink-0"> <Avatar className="size-8 shrink-0">
{profile?.avatar_url ? ( {profile?.avatar_url ? (
<AvatarImage <AvatarImage
@ -322,10 +322,10 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-white"> <p className="truncate text-sm font-medium text-foreground">
{profile?.full_name ?? "User"} {profile?.full_name ?? "User"}
</p> </p>
<p className="truncate text-xs text-slate-400"> <p className="truncate text-xs text-muted-foreground">
{profile?.email ?? ""} {profile?.email ?? ""}
</p> </p>
</div> </div>
@ -334,14 +334,14 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
align="end" align="end"
side="top" side="top"
sideOffset={6} sideOffset={6}
className="min-w-56 bg-slate-900 text-slate-100 ring-slate-700" className="min-w-56 bg-popover text-popover-foreground ring-border"
> >
<DropdownMenuItem <DropdownMenuItem
render={ render={
<Link <Link
href="/settings?tab=profile" href="/settings?tab=profile"
onClick={onClose} onClick={onClose}
className="text-slate-200 focus:bg-slate-800 focus:text-white" className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
/> />
} }
> >
@ -353,17 +353,17 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
<Link <Link
href="/settings?tab=whatsapp" href="/settings?tab=whatsapp"
onClick={onClose} onClick={onClose}
className="text-slate-200 focus:bg-slate-800 focus:text-white" className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
/> />
} }
> >
<Settings className="size-4" /> <Settings className="size-4" />
Settings Settings
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className="bg-slate-800" /> <DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem <DropdownMenuItem
onClick={signOut} onClick={signOut}
className="text-slate-200 focus:bg-slate-800 focus:text-white" className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
> >
<LogOut className="size-4" /> <LogOut className="size-4" />
Sign out Sign out

View File

@ -39,10 +39,10 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
e.stopPropagation(); e.stopPropagation();
onEdit(deal); onEdit(deal);
}} }}
className={`group relative w-full cursor-pointer rounded-xl border border-slate-700/50 bg-slate-800/70 pl-4 pr-3 py-3 text-left shadow-sm transition-all ${ className={`group relative w-full cursor-pointer rounded-xl border border-border/50 bg-muted/70 pl-4 pr-3 py-3 text-left shadow-sm transition-all ${
isOverlay isOverlay
? "shadow-xl" ? "shadow-xl"
: "hover:-translate-y-0.5 hover:border-slate-600 hover:bg-slate-800 hover:shadow-lg" : "hover:-translate-y-0.5 hover:border-border hover:bg-muted hover:shadow-lg"
}`} }`}
> >
{/* 4px left accent bar using stage color */} {/* 4px left accent bar using stage color */}
@ -53,7 +53,7 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
/> />
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<h4 className="flex-1 text-sm font-semibold leading-snug text-white break-words"> <h4 className="flex-1 text-sm font-semibold leading-snug text-foreground break-words">
{deal.title} {deal.title}
</h4> </h4>
{deal.status === "won" && ( {deal.status === "won" && (
@ -72,10 +72,10 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
{/* Contact row */} {/* Contact row */}
<div className="mt-2 flex items-center gap-2"> <div className="mt-2 flex items-center gap-2">
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-slate-700 text-[10px] font-semibold text-slate-200"> <span className="flex h-5 w-5 items-center justify-center rounded-full bg-muted text-[10px] font-semibold text-foreground">
{initials(deal.contact?.name, deal.contact?.phone)} {initials(deal.contact?.name, deal.contact?.phone)}
</span> </span>
<span className="truncate text-xs text-slate-400">{contactLabel}</span> <span className="truncate text-xs text-muted-foreground">{contactLabel}</span>
</div> </div>
<div className="mt-2 flex items-center justify-between"> <div className="mt-2 flex items-center justify-between">
@ -83,7 +83,7 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
{formatCurrency(deal.value, deal.currency)} {formatCurrency(deal.value, deal.currency)}
</span> </span>
{deal.expected_close_date && ( {deal.expected_close_date && (
<span className="flex items-center gap-1 text-[11px] text-slate-500"> <span className="flex items-center gap-1 text-[11px] text-muted-foreground">
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3" />
{formatDate(deal.expected_close_date)} {formatDate(deal.expected_close_date)}
</span> </span>

View File

@ -247,32 +247,32 @@ export function DealForm({
<Sheet open={open} onOpenChange={onOpenChange}> <Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent <SheetContent
side="right" side="right"
className="bg-slate-900 border-slate-700 text-slate-200 sm:max-w-lg w-full p-0" className="bg-popover border-border text-popover-foreground sm:max-w-lg w-full p-0"
> >
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<SheetHeader className="border-b border-slate-700/50 p-4"> <SheetHeader className="border-b border-border/50 p-4">
<SheetTitle className="text-white"> <SheetTitle className="text-popover-foreground">
{deal ? "Edit Deal" : "New Deal"} {deal ? "Edit Deal" : "New Deal"}
</SheetTitle> </SheetTitle>
</SheetHeader> </SheetHeader>
<div className="flex-1 overflow-y-auto p-4 space-y-4"> <div className="flex-1 overflow-y-auto p-4 space-y-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-slate-300">Title</Label> <Label className="text-muted-foreground">Title</Label>
<Input <Input
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
placeholder="Deal title" placeholder="Deal title"
className="border-slate-700 bg-slate-800 text-white" className="border-border bg-muted text-foreground"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-slate-300">Contact</Label> <Label className="text-muted-foreground">Contact</Label>
<select <select
value={contactId} value={contactId}
onChange={(e) => setContactId(e.target.value)} onChange={(e) => setContactId(e.target.value)}
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary focus:ring-1 focus:ring-primary" className="h-9 w-full rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary"
> >
<option value="">Select a contact</option> <option value="">Select a contact</option>
{contacts.map((c) => ( {contacts.map((c) => (
@ -295,24 +295,24 @@ export function DealForm({
<div className="grid grid-cols-[1fr_110px] gap-3"> <div className="grid grid-cols-[1fr_110px] gap-3">
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-slate-300">Value</Label> <Label className="text-muted-foreground">Value</Label>
<div className="relative"> <div className="relative">
<DollarSign className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-500" /> <DollarSign className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input <Input
type="number" type="number"
value={value} value={value}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
placeholder="0" placeholder="0"
className="border-slate-700 bg-slate-800 pl-7 text-white" className="border-border bg-muted pl-7 text-foreground"
/> />
</div> </div>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-slate-300">Currency</Label> <Label className="text-muted-foreground">Currency</Label>
<select <select
value={currency} value={currency}
onChange={(e) => setCurrency(e.target.value)} onChange={(e) => setCurrency(e.target.value)}
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary" className="h-9 w-full rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary"
> >
{CURRENCIES.map((c) => ( {CURRENCIES.map((c) => (
<option key={c.code} value={c.code}> <option key={c.code} value={c.code}>
@ -324,21 +324,21 @@ export function DealForm({
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-slate-300">Expected Close Date</Label> <Label className="text-muted-foreground">Expected Close Date</Label>
<Input <Input
type="date" type="date"
value={expectedCloseDate} value={expectedCloseDate}
onChange={(e) => setExpectedCloseDate(e.target.value)} onChange={(e) => setExpectedCloseDate(e.target.value)}
className="border-slate-700 bg-slate-800 text-white" className="border-border bg-muted text-foreground"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-slate-300">Stage</Label> <Label className="text-muted-foreground">Stage</Label>
<select <select
value={stageId} value={stageId}
onChange={(e) => setStageId(e.target.value)} onChange={(e) => setStageId(e.target.value)}
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary" className="h-9 w-full rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary"
> >
{stages.map((s) => ( {stages.map((s) => (
<option key={s.id} value={s.id}> <option key={s.id} value={s.id}>
@ -349,11 +349,11 @@ export function DealForm({
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-slate-300">Assigned To</Label> <Label className="text-muted-foreground">Assigned To</Label>
<select <select
value={assignedTo} value={assignedTo}
onChange={(e) => setAssignedTo(e.target.value)} onChange={(e) => setAssignedTo(e.target.value)}
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary" className="h-9 w-full rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary"
> >
<option value="">Unassigned</option> <option value="">Unassigned</option>
{profiles.map((p) => ( {profiles.map((p) => (
@ -365,18 +365,18 @@ export function DealForm({
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-slate-300">Notes</Label> <Label className="text-muted-foreground">Notes</Label>
<Textarea <Textarea
value={notes} value={notes}
onChange={(e) => setNotes(e.target.value)} onChange={(e) => setNotes(e.target.value)}
placeholder="Add notes..." placeholder="Add notes..."
className="min-h-[100px] border-slate-700 bg-slate-800 text-white" className="min-h-[100px] border-border bg-muted text-foreground"
/> />
</div> </div>
{deal && ( {deal && (
<div className="space-y-2 rounded-lg border border-slate-700 bg-slate-900/50 p-3"> <div className="space-y-2 rounded-lg border border-border bg-muted/50 p-3">
<p className="text-xs font-medium uppercase tracking-wider text-slate-400"> <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Status Status
</p> </p>
<div className="flex gap-2"> <div className="flex gap-2">
@ -417,7 +417,7 @@ export function DealForm({
variant="ghost" variant="ghost"
onClick={() => handleStatusChange("open")} onClick={() => handleStatusChange("open")}
disabled={!!statusAction} disabled={!!statusAction}
className="w-full text-slate-400 hover:text-white" className="w-full text-muted-foreground hover:text-foreground"
> >
Reopen deal Reopen deal
</Button> </Button>
@ -426,12 +426,12 @@ export function DealForm({
)} )}
</div> </div>
<div className="border-t border-slate-700/50 bg-slate-900/80 p-4"> <div className="border-t border-border/50 bg-popover/80 p-4">
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
variant="outline" variant="outline"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="flex-1 border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800" className="flex-1 border-border bg-transparent text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>
@ -453,7 +453,7 @@ export function DealForm({
type="button" type="button"
onClick={() => setConfirmDelete(false)} onClick={() => setConfirmDelete(false)}
disabled={deleting} disabled={deleting}
className="rounded px-2 py-1 text-slate-300 hover:bg-slate-800" className="rounded px-2 py-1 text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</button> </button>

View File

@ -93,9 +93,9 @@ export function PipelineAnalytics({ stages, deals }: PipelineAnalyticsProps) {
return ( return (
<TooltipProvider> <TooltipProvider>
<div className="grid grid-cols-2 gap-3 rounded-xl border border-slate-800 bg-slate-900/60 p-4 sm:grid-cols-3 xl:grid-cols-6"> <div className="grid grid-cols-2 gap-3 rounded-xl border border-border bg-card/60 p-4 sm:grid-cols-3 xl:grid-cols-6">
<Metric <Metric
icon={<BarChart3 className="h-4 w-4 text-slate-400" />} icon={<BarChart3 className="h-4 w-4 text-muted-foreground" />}
label="Total Deals" label="Total Deals"
value={String(stats.totalCount)} value={String(stats.totalCount)}
tooltip="Count of every deal in this pipeline that isn't marked as Lost. Won deals are still included." tooltip="Count of every deal in this pipeline that isn't marked as Lost. Won deals are still included."
@ -147,8 +147,8 @@ function Metric({
tooltip: string; tooltip: string;
}) { }) {
return ( return (
<div className="rounded-lg bg-slate-800/50 p-3"> <div className="rounded-lg bg-muted/50 p-3">
<div className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-slate-400"> <div className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
{icon} {icon}
<span>{label}</span> <span>{label}</span>
<Tooltip> <Tooltip>
@ -157,7 +157,7 @@ function Metric({
<button <button
type="button" type="button"
aria-label={`How ${label} is calculated`} aria-label={`How ${label} is calculated`}
className="ml-auto text-slate-500 hover:text-slate-300 focus:outline-none" className="ml-auto text-muted-foreground hover:text-foreground focus:outline-none"
/> />
} }
> >
@ -168,7 +168,7 @@ function Metric({
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<p className="mt-1 text-base font-semibold text-white">{value}</p> <p className="mt-1 text-base font-semibold text-foreground">{value}</p>
</div> </div>
); );
} }

View File

@ -209,21 +209,21 @@ function StageColumn({
// restore the flex-1 share-the-row behavior. The droppable ref is // restore the flex-1 share-the-row behavior. The droppable ref is
// on the inner messages region below — intentionally NOT here, so // on the inner messages region below — intentionally NOT here, so
// a drag over the column header doesn't highlight the whole column. // a drag over the column header doesn't highlight the whole column.
<div className="flex w-[85vw] min-w-[260px] max-w-[320px] shrink-0 snap-start flex-col rounded-xl border border-slate-800 bg-slate-900/60 p-4 lg:w-auto lg:max-w-none lg:flex-1 lg:basis-[260px] lg:shrink lg:snap-none"> <div className="flex w-[85vw] min-w-[260px] max-w-[320px] shrink-0 snap-start flex-col rounded-xl border border-border bg-card/60 p-4 lg:w-auto lg:max-w-none lg:flex-1 lg:basis-[260px] lg:shrink lg:snap-none">
{/* 3px colored top border — sits above the column's padding */} {/* 3px colored top border — sits above the column's padding */}
<div <div
className="-mx-4 -mt-4 h-[3px] rounded-t-xl" className="-mx-4 -mt-4 h-[3px] rounded-t-xl"
style={{ backgroundColor: stage.color }} style={{ backgroundColor: stage.color }}
/> />
<div className="flex items-center justify-between pt-3"> <div className="flex items-center justify-between pt-3">
<h3 className="truncate text-sm font-semibold text-white"> <h3 className="truncate text-sm font-semibold text-foreground">
{stage.name} {stage.name}
</h3> </h3>
<span className="shrink-0 rounded-full bg-slate-800 px-2 py-0.5 text-[11px] font-medium text-slate-300"> <span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
{deals.length} {deals.length}
</span> </span>
</div> </div>
<p className="text-xs text-slate-400"> <p className="text-xs text-muted-foreground">
{formatCurrency(totalValue, currency)} {formatCurrency(totalValue, currency)}
</p> </p>
@ -236,7 +236,7 @@ function StageColumn({
}`} }`}
> >
{deals.length === 0 ? ( {deals.length === 0 ? (
<div className="flex flex-1 items-center justify-center rounded-lg border-2 border-dashed border-slate-700 py-10 text-xs text-slate-500"> <div className="flex flex-1 items-center justify-center rounded-lg border-2 border-dashed border-border py-10 text-xs text-muted-foreground">
Drop a deal here Drop a deal here
</div> </div>
) : ( ) : (
@ -255,7 +255,7 @@ function StageColumn({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onAddDeal(stage.id)} onClick={() => onAddDeal(stage.id)}
className="mt-3 w-full justify-start border border-dashed border-slate-700 bg-transparent text-slate-400 hover:border-slate-600 hover:bg-slate-800 hover:text-white" className="mt-3 w-full justify-start border border-dashed border-border bg-transparent text-muted-foreground hover:border-border hover:bg-muted hover:text-foreground"
> >
<Plus className="mr-1 h-3 w-3" /> <Plus className="mr-1 h-3 w-3" />
Add Deal Add Deal

View File

@ -199,9 +199,9 @@ export function PipelineSettings({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md bg-slate-900 border-slate-700 max-h-[85vh] overflow-y-auto"> <DialogContent className="sm:max-w-md bg-popover border-border max-h-[85vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white">Manage Pipeline</DialogTitle> <DialogTitle className="text-popover-foreground">Manage Pipeline</DialogTitle>
</DialogHeader> </DialogHeader>
{showDeleteConfirm ? ( {showDeleteConfirm ? (
@ -212,7 +212,7 @@ export function PipelineSettings({
<p className="text-sm font-medium text-red-400"> <p className="text-sm font-medium text-red-400">
Delete Pipeline Delete Pipeline
</p> </p>
<p className="mt-1 text-xs text-slate-400"> <p className="mt-1 text-xs text-muted-foreground">
This will archive all deals in this pipeline. This cannot be This will archive all deals in this pipeline. This cannot be
undone. undone.
</p> </p>
@ -222,7 +222,7 @@ export function PipelineSettings({
<Button <Button
variant="outline" variant="outline"
onClick={() => setShowDeleteConfirm(false)} onClick={() => setShowDeleteConfirm(false)}
className="border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800" className="border-border bg-transparent text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>
@ -239,16 +239,16 @@ export function PipelineSettings({
<> <>
<div className="grid gap-4 py-2"> <div className="grid gap-4 py-2">
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-slate-300">Pipeline Name</Label> <Label className="text-muted-foreground">Pipeline Name</Label>
<Input <Input
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
className="border-slate-700 bg-slate-800 text-white" className="border-border bg-muted text-foreground"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-slate-300">Stages</Label> <Label className="text-muted-foreground">Stages</Label>
<DndContext <DndContext
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
@ -303,7 +303,7 @@ export function PipelineSettings({
value={newStageName} value={newStageName}
onChange={(e) => setNewStageName(e.target.value)} onChange={(e) => setNewStageName(e.target.value)}
placeholder="New stage name" placeholder="New stage name"
className="border-slate-700 bg-slate-800 text-sm text-white" className="border-border bg-muted text-sm text-foreground"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") handleAddStage(); if (e.key === "Enter") handleAddStage();
}} }}
@ -313,7 +313,7 @@ export function PipelineSettings({
size="sm" size="sm"
onClick={handleAddStage} onClick={handleAddStage}
disabled={!newStageName.trim()} disabled={!newStageName.trim()}
className="shrink-0 border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800" className="shrink-0 border-border bg-transparent text-muted-foreground hover:bg-muted"
> >
<Plus className="mr-1 h-3 w-3" /> <Plus className="mr-1 h-3 w-3" />
Add Add
@ -324,14 +324,14 @@ export function PipelineSettings({
<Button <Button
variant="outline" variant="outline"
onClick={onCreateNewPipeline} onClick={onCreateNewPipeline}
className="w-full border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800" className="w-full border-border bg-transparent text-muted-foreground hover:bg-muted"
> >
<Plus className="mr-1 h-3 w-3" /> <Plus className="mr-1 h-3 w-3" />
Create a new pipeline Create a new pipeline
</Button> </Button>
</div> </div>
<DialogFooter className="border-slate-700 bg-slate-900/50"> <DialogFooter className="border-border bg-popover/50">
<Button <Button
variant="destructive" variant="destructive"
onClick={() => setShowDeleteConfirm(true)} onClick={() => setShowDeleteConfirm(true)}
@ -342,7 +342,7 @@ export function PipelineSettings({
<Button <Button
variant="outline" variant="outline"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800" className="border-border bg-transparent text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>
@ -387,13 +387,13 @@ function SortableStageRow({
<div <div
ref={setNodeRef} ref={setNodeRef}
style={style} style={style}
className="flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-800 p-2" className="flex items-center gap-2 rounded-lg border border-border bg-muted p-2"
> >
<button <button
type="button" type="button"
{...attributes} {...attributes}
{...listeners} {...listeners}
className="cursor-grab touch-none text-slate-500 hover:text-slate-300 active:cursor-grabbing" className="cursor-grab touch-none text-muted-foreground hover:text-foreground active:cursor-grabbing"
aria-label="Drag to reorder" aria-label="Drag to reorder"
> >
<GripVertical className="h-4 w-4" /> <GripVertical className="h-4 w-4" />
@ -402,13 +402,13 @@ function SortableStageRow({
<Input <Input
value={stage.name} value={stage.name}
onChange={(e) => onNameChange(e.target.value)} onChange={(e) => onNameChange(e.target.value)}
className="h-7 flex-1 border-transparent bg-transparent text-sm text-white focus:border-slate-600" className="h-7 flex-1 border-transparent bg-transparent text-sm text-foreground focus:border-border"
/> />
<Button <Button
variant="ghost" variant="ghost"
size="icon-xs" size="icon-xs"
onClick={onRemove} onClick={onRemove}
className="text-slate-400 hover:text-red-400" className="text-muted-foreground hover:text-red-400"
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
@ -431,14 +431,14 @@ function ColorSwatch({
<button <button
type="button" type="button"
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
className="h-4 w-4 rounded-full border border-slate-600" className="h-4 w-4 rounded-full border border-border"
style={{ backgroundColor: value }} style={{ backgroundColor: value }}
aria-label="Change color" aria-label="Change color"
/> />
{open && ( {open && (
<> <>
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} /> <div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
<div className="absolute left-0 top-6 z-20 flex flex-wrap gap-1 rounded-lg border border-slate-700 bg-slate-900 p-2 shadow-lg w-36"> <div className="absolute left-0 top-6 z-20 flex flex-wrap gap-1 rounded-lg border border-border bg-popover p-2 shadow-lg w-36">
{colors.map((c) => ( {colors.map((c) => (
<button <button
key={c} key={c}

View File

@ -1,53 +1,126 @@
"use client"; "use client";
import { Check } from "lucide-react"; import { Check, Moon, Sun } from "lucide-react";
import { useTheme } from "@/hooks/use-theme"; import { useTheme } from "@/hooks/use-theme";
import { THEMES, type ThemeId } from "@/lib/themes"; import { MODES, THEMES, type Mode, type ThemeId } from "@/lib/themes";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
/** /**
* Appearance panel — color-theme picker. * Appearance panel — light/dark mode + accent-color picker.
* *
* Click a card → applies + persists immediately. No save button: * Two independent controls: a mode toggle (light / dark) and the
* the whole change is a single CSS-variable swap on <html>, there's * accent grid. Either applies + persists immediately. No save button:
* nothing to roll back. The active card carries a check chip + a * each change is a single attribute swap on <html>, there's nothing
* primary-tinted border so the current pick is obvious. * to roll back.
* *
* Persistence: localStorage only (device-scoped). The boot script in * Persistence: localStorage only (device-scoped). The boot script in
* layout.tsx replays the choice before first paint on subsequent * layout.tsx replays both choices before first paint on subsequent
* loads. * loads.
*/ */
export function AppearancePanel() { export function AppearancePanel() {
const { theme, setTheme } = useTheme(); const { theme, setTheme, mode, setMode } = useTheme();
return ( return (
<section className="space-y-4"> <section className="space-y-8">
<div> <div className="space-y-4">
<h2 className="text-lg font-semibold text-white">Color theme</h2> <div>
<p className="mt-1 text-sm text-slate-400"> <h2 className="text-lg font-semibold text-foreground">Mode</h2>
Pick the accent color used across the app. All themes stay <p className="mt-1 text-sm text-muted-foreground">
dark — only the primary color (buttons, active nav, badges) Choose light or dark. Light is easier on the eyes in bright
changes. Saved to this device. rooms; dark is the original look. Works with any accent
</p> color below. Saved to this device.
</p>
</div>
<div
role="radiogroup"
aria-label="Color mode"
className="grid max-w-md grid-cols-2 gap-3"
>
{MODES.map((m) => (
<ModeCard
key={m}
mode={m}
isActive={m === mode}
onPick={() => setMode(m)}
/>
))}
</div>
</div> </div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"> <div className="space-y-4">
{THEMES.map((t) => ( <div>
<ThemeCard <h2 className="text-lg font-semibold text-foreground">
key={t.id} Accent color
id={t.id} </h2>
name={t.name} <p className="mt-1 text-sm text-muted-foreground">
tagline={t.tagline} Pick the accent color used across the app — buttons, active
swatch={t.swatch} nav, and badges. Saved to this device.
isActive={t.id === theme} </p>
onPick={() => setTheme(t.id)} </div>
/>
))} <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{THEMES.map((t) => (
<ThemeCard
key={t.id}
id={t.id}
name={t.name}
tagline={t.tagline}
swatch={t.swatch}
isActive={t.id === theme}
onPick={() => setTheme(t.id)}
/>
))}
</div>
</div> </div>
</section> </section>
); );
} }
function ModeCard({
mode,
isActive,
onPick,
}: {
mode: Mode;
isActive: boolean;
onPick: () => void;
}) {
const isLight = mode === "light";
const Icon = isLight ? Sun : Moon;
return (
<button
type="button"
role="radio"
onClick={onPick}
aria-checked={isActive}
aria-label={`Use ${mode} mode`}
className={cn(
"flex items-center gap-3 rounded-lg border bg-card p-4 text-left transition-colors",
isActive
? "border-primary/60 ring-2 ring-primary/40"
: "border-border hover:border-border hover:bg-muted/40",
)}
>
<span
aria-hidden
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-foreground"
>
<Icon className="h-4 w-4" />
</span>
<span className="flex-1 text-sm font-semibold capitalize text-foreground">
{mode}
</span>
{isActive && (
<span className="inline-flex items-center gap-1 rounded-full bg-primary/15 px-2 py-0.5 text-[11px] font-medium text-primary">
<Check className="h-3 w-3" />
Active
</span>
)}
</button>
);
}
function ThemeCard({ function ThemeCard({
id, id,
name, name,
@ -73,7 +146,7 @@ function ThemeCard({
"flex flex-col gap-3 rounded-lg border bg-card p-4 text-left transition-colors", "flex flex-col gap-3 rounded-lg border bg-card p-4 text-left transition-colors",
isActive isActive
? "border-primary/60 ring-2 ring-primary/40" ? "border-primary/60 ring-2 ring-primary/40"
: "border-slate-800 hover:border-slate-700 hover:bg-slate-800/40", : "border-border hover:border-border hover:bg-muted/40",
)} )}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -93,8 +166,8 @@ function ThemeCard({
)} )}
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-white">{name}</div> <div className="text-sm font-semibold text-foreground">{name}</div>
<div className="mt-1 text-xs leading-relaxed text-slate-400"> <div className="mt-1 text-xs leading-relaxed text-muted-foreground">
{tagline} {tagline}
</div> </div>
</div> </div>
@ -103,9 +176,9 @@ function ThemeCard({
aria-hidden aria-hidden
> >
<span className="flex-1" style={{ background: swatch }} /> <span className="flex-1" style={{ background: swatch }} />
<span className="w-3 bg-slate-700" /> <span className="w-3 bg-muted-foreground/60" />
<span className="w-3 bg-slate-800" /> <span className="w-3 bg-muted" />
<span className="w-3 bg-slate-900" /> <span className="w-3 bg-card" />
</div> </div>
<span className="sr-only">Theme id: {id}</span> <span className="sr-only">Theme id: {id}</span>
</button> </button>

View File

@ -12,14 +12,14 @@ export function CustomFieldsSettings() {
return ( return (
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">
<div> <div>
<h2 className="text-lg font-semibold text-white">Custom fields</h2> <h2 className="text-lg font-semibold text-foreground">Custom fields</h2>
<p className="text-sm text-slate-400"> <p className="text-sm text-muted-foreground">
Define extra contact fields (e.g. ZIP code, lead source). They appear Define extra contact fields (e.g. ZIP code, lead source). They appear
on every contact and in the “Update Contact Field” automation action. on every contact and in the “Update Contact Field” automation action.
</p> </p>
</div> </div>
<Card className="border-slate-700 bg-slate-900 ring-0 ring-transparent"> <Card className="border-border bg-card ring-0 ring-transparent">
<CardContent className="pt-4"> <CardContent className="pt-4">
<CustomFieldsPanel /> <CustomFieldsPanel />
</CardContent> </CardContent>

View File

@ -68,13 +68,13 @@ export function DealsSettings() {
return ( return (
<section className="mt-4 space-y-4"> <section className="mt-4 space-y-4">
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent"> <Card className="bg-card border-border ring-0 ring-transparent">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-white"> <CardTitle className="flex items-center gap-2 text-foreground">
<Coins className="size-4 text-primary" /> <Coins className="size-4 text-primary" />
Default currency Default currency
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
New deals default to this currency, and pipeline and New deals default to this currency, and pipeline and
dashboard totals are shown in it. Existing deals keep the dashboard totals are shown in it. Existing deals keep the
currency they were saved with. currency they were saved with.
@ -82,12 +82,12 @@ export function DealsSettings() {
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid gap-2 sm:max-w-xs"> <div className="grid gap-2 sm:max-w-xs">
<Label className="text-slate-300">Currency</Label> <Label className="text-muted-foreground">Currency</Label>
<select <select
value={selected} value={selected}
onChange={(e) => setSelected(e.target.value)} onChange={(e) => setSelected(e.target.value)}
disabled={!canEditSettings || profileLoading} disabled={!canEditSettings || profileLoading}
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-60" className="h-9 w-full rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-60"
> >
{CURRENCIES.map((c) => ( {CURRENCIES.map((c) => (
<option key={c.code} value={c.code}> <option key={c.code} value={c.code}>
@ -96,7 +96,7 @@ export function DealsSettings() {
))} ))}
</select> </select>
{!canEditSettings && ( {!canEditSettings && (
<p className="text-xs text-slate-500"> <p className="text-xs text-muted-foreground">
Only account admins can change the default currency. Only account admins can change the default currency.
</p> </p>
)} )}

View File

@ -185,20 +185,20 @@ export function InviteMemberDialog({
onOpenChange(next); onOpenChange(next);
}} }}
> >
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-md"> <DialogContent className="bg-popover border-border sm:max-w-md">
{result ? ( {result ? (
<> <>
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-white"> <DialogTitle className="flex items-center gap-2 text-popover-foreground">
<Sparkles className="size-4 text-primary" /> <Sparkles className="size-4 text-primary" />
Invite created Invite created
</DialogTitle> </DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
Share this link with your new teammate. They&apos;ll be able Share this link with your new teammate. They&apos;ll be able
to sign up (or sign in) and join the account as{' '} to sign up (or sign in) and join the account as{' '}
<span className="font-medium text-slate-300">{result.role}</span> <span className="font-medium text-muted-foreground">{result.role}</span>
. The link is valid for{' '} . The link is valid for{' '}
<span className="font-medium text-slate-300"> <span className="font-medium text-muted-foreground">
{result.expiresInDays} day{result.expiresInDays === 1 ? '' : 's'} {result.expiresInDays} day{result.expiresInDays === 1 ? '' : 's'}
</span> </span>
. .
@ -206,12 +206,12 @@ export function InviteMemberDialog({
</DialogHeader> </DialogHeader>
<div className="space-y-3 py-2"> <div className="space-y-3 py-2">
<Label className="text-slate-300">Invite link</Label> <Label className="text-muted-foreground">Invite link</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
readOnly readOnly
value={result.url} value={result.url}
className="bg-slate-800 border-slate-700 text-white font-mono text-xs" className="bg-muted border-border text-foreground font-mono text-xs"
onFocus={(e) => e.currentTarget.select()} onFocus={(e) => e.currentTarget.select()}
/> />
<Button <Button
@ -250,7 +250,7 @@ export function InviteMemberDialog({
className={buttonVariants({ className={buttonVariants({
variant: 'outline', variant: 'outline',
className: className:
'w-full border-slate-700 text-slate-300 hover:bg-slate-800', 'w-full border-border text-muted-foreground hover:bg-muted',
})} })}
> >
<MessageCircle className="size-4" /> <MessageCircle className="size-4" />
@ -258,7 +258,7 @@ export function InviteMemberDialog({
</a> </a>
</div> </div>
<DialogFooter className="bg-slate-900 border-slate-700"> <DialogFooter className="bg-popover border-border">
<Button <Button
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="bg-primary hover:bg-primary/90 text-primary-foreground" className="bg-primary hover:bg-primary/90 text-primary-foreground"
@ -270,8 +270,8 @@ export function InviteMemberDialog({
) : ( ) : (
<> <>
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white">Invite a teammate</DialogTitle> <DialogTitle className="text-popover-foreground">Invite a teammate</DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
Generate a one-time invite link. Share it via WhatsApp, Generate a one-time invite link. Share it via WhatsApp,
Slack, or any channel you like — no email service required. Slack, or any channel you like — no email service required.
</DialogDescription> </DialogDescription>
@ -279,12 +279,12 @@ export function InviteMemberDialog({
<div className="space-y-4 py-2"> <div className="space-y-4 py-2">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Role</Label> <Label className="text-muted-foreground">Role</Label>
<Select <Select
value={role} value={role}
onValueChange={(v) => v && setRole(v as InviteRole)} onValueChange={(v) => v && setRole(v as InviteRole)}
> >
<SelectTrigger className="w-full bg-slate-800 border-slate-700 text-white"> <SelectTrigger className="w-full bg-muted border-border text-foreground">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -293,18 +293,18 @@ export function InviteMemberDialog({
<SelectItem value="viewer">Viewer</SelectItem> <SelectItem value="viewer">Viewer</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<p className="text-xs text-slate-500"> <p className="text-xs text-muted-foreground">
{ROLE_DESCRIPTIONS[role]} {ROLE_DESCRIPTIONS[role]}
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Link valid for</Label> <Label className="text-muted-foreground">Link valid for</Label>
<Select <Select
value={expiry} value={expiry}
onValueChange={(v) => v && setExpiry(v)} onValueChange={(v) => v && setExpiry(v)}
> >
<SelectTrigger className="w-full bg-slate-800 border-slate-700 text-white"> <SelectTrigger className="w-full bg-muted border-border text-foreground">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -318,29 +318,29 @@ export function InviteMemberDialog({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300"> <Label className="text-muted-foreground">
Label{' '} Label{' '}
<span className="text-xs text-slate-500">(optional)</span> <span className="text-xs text-muted-foreground">(optional)</span>
</Label> </Label>
<Input <Input
placeholder="e.g. Sara — support team" placeholder="e.g. Sara — support team"
value={label} value={label}
onChange={(e) => setLabel(e.target.value)} onChange={(e) => setLabel(e.target.value)}
maxLength={MAX_LABEL_LEN} maxLength={MAX_LABEL_LEN}
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
<p className="text-xs text-slate-500"> <p className="text-xs text-muted-foreground">
Helps you remember who you sent the link to in the pending Helps you remember who you sent the link to in the pending
list below. list below.
</p> </p>
</div> </div>
</div> </div>
<DialogFooter className="bg-slate-900 border-slate-700"> <DialogFooter className="bg-popover border-border">
<Button <Button
variant="outline" variant="outline"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>

View File

@ -110,12 +110,12 @@ const ROLE_CHIP: Record<
agent: { agent: {
icon: UserCog, icon: UserCog,
label: 'Agent', label: 'Agent',
className: 'border-slate-700 bg-slate-800 text-slate-300', className: 'border-border bg-muted text-muted-foreground',
}, },
viewer: { viewer: {
icon: UserIcon, icon: UserIcon,
label: 'Viewer', label: 'Viewer',
className: 'border-slate-800 bg-slate-900 text-slate-500', className: 'border-border bg-card text-muted-foreground',
}, },
}; };
@ -296,8 +296,8 @@ export function MembersTab() {
{/* Header + invite button */} {/* Header + invite button */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-lg font-semibold text-white">Account members</h2> <h2 className="text-lg font-semibold text-foreground">Account members</h2>
<p className="text-sm text-slate-400"> <p className="text-sm text-muted-foreground">
People with access to this account. Roles control what each People with access to this account. Roles control what each
teammate can do. teammate can do.
</p> </p>
@ -314,9 +314,9 @@ export function MembersTab() {
</div> </div>
{/* Roster */} {/* Roster */}
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent"> <Card className="bg-card border-border ring-0 ring-transparent">
<CardContent className="p-0"> <CardContent className="p-0">
<ul className="divide-y divide-slate-800"> <ul className="divide-y divide-border">
{members.map((member) => { {members.map((member) => {
const roleMeta = ROLE_CHIP[member.role]; const roleMeta = ROLE_CHIP[member.role];
const RoleIcon = roleMeta.icon; const RoleIcon = roleMeta.icon;
@ -351,17 +351,17 @@ export function MembersTab() {
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="truncate text-sm font-medium text-white"> <span className="truncate text-sm font-medium text-foreground">
{member.full_name || 'Unnamed'} {member.full_name || 'Unnamed'}
</span> </span>
{isSelf && ( {isSelf && (
<Badge className="bg-slate-800 text-slate-300 border-slate-700 text-[10px] uppercase tracking-wide"> <Badge className="bg-muted text-muted-foreground border-border text-[10px] uppercase tracking-wide">
You You
</Badge> </Badge>
)} )}
</div> </div>
{member.email && ( {member.email && (
<p className="truncate text-xs text-slate-500"> <p className="truncate text-xs text-muted-foreground">
{member.email} {member.email}
</p> </p>
)} )}
@ -370,7 +370,7 @@ export function MembersTab() {
{/* Joined date stays desktop-only. The mobile row's {/* Joined date stays desktop-only. The mobile row's
vertical density makes the joined date noise. */} vertical density makes the joined date noise. */}
<div className="hidden sm:block text-right text-xs text-slate-500"> <div className="hidden sm:block text-right text-xs text-muted-foreground">
Joined {fmtDate(member.joined_at)} Joined {fmtDate(member.joined_at)}
</div> </div>
@ -394,7 +394,7 @@ export function MembersTab() {
} }
> >
<SelectTrigger <SelectTrigger
className="w-32 bg-slate-800 border-slate-700 text-slate-200" className="w-32 bg-muted border-border text-foreground"
disabled={isBusy} disabled={isBusy}
> >
<SelectValue /> <SelectValue />
@ -446,11 +446,11 @@ export function MembersTab() {
<RequireRole min="admin"> <RequireRole min="admin">
<div> <div>
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">
<UsersRound className="size-4 text-slate-400" /> <UsersRound className="size-4 text-muted-foreground" />
<h3 className="text-sm font-semibold text-white"> <h3 className="text-sm font-semibold text-foreground">
Pending invitations Pending invitations
</h3> </h3>
<Badge className="bg-slate-800 text-slate-400 border-slate-700"> <Badge className="bg-muted text-muted-foreground border-border">
{invitations.length} {invitations.length}
</Badge> </Badge>
</div> </div>
@ -460,7 +460,7 @@ export function MembersTab() {
front (rather than letting the user discover it by front (rather than letting the user discover it by
looking for a button) keeps it from feeling like a bug. */} looking for a button) keeps it from feeling like a bug. */}
{invitations.length > 0 ? ( {invitations.length > 0 ? (
<p className="mb-3 text-xs text-slate-500"> <p className="mb-3 text-xs text-muted-foreground">
The plaintext invite URL is only shown once at creation The plaintext invite URL is only shown once at creation
for security — to re-share, revoke the invite below and for security — to re-share, revoke the invite below and
create a new one. create a new one.
@ -468,22 +468,22 @@ export function MembersTab() {
) : null} ) : null}
{invitations.length === 0 ? ( {invitations.length === 0 ? (
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent"> <Card className="bg-card border-border ring-0 ring-transparent">
<CardContent className="flex flex-col items-center justify-center py-8 text-center"> <CardContent className="flex flex-col items-center justify-center py-8 text-center">
<Mail className="size-6 text-slate-600" /> <Mail className="size-6 text-muted-foreground" />
<p className="mt-2 text-sm text-slate-400"> <p className="mt-2 text-sm text-muted-foreground">
No pending invitations. No pending invitations.
</p> </p>
<p className="mt-1 text-xs text-slate-500"> <p className="mt-1 text-xs text-muted-foreground">
Click <span className="text-slate-300">Invite member</span>{' '} Click <span className="text-muted-foreground">Invite member</span>{' '}
above to generate a shareable link. above to generate a shareable link.
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent"> <Card className="bg-card border-border ring-0 ring-transparent">
<CardContent className="p-0"> <CardContent className="p-0">
<ul className="divide-y divide-slate-800"> <ul className="divide-y divide-border">
{invitations.map((inv) => { {invitations.map((inv) => {
const inviteRoleMeta = ROLE_CHIP[inv.role]; const inviteRoleMeta = ROLE_CHIP[inv.role];
const InviteRoleIcon = inviteRoleMeta.icon; const InviteRoleIcon = inviteRoleMeta.icon;
@ -494,7 +494,7 @@ export function MembersTab() {
> >
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-medium text-white"> <span className="text-sm font-medium text-foreground">
{inv.label || 'Untitled invite'} {inv.label || 'Untitled invite'}
</span> </span>
<span <span
@ -504,7 +504,7 @@ export function MembersTab() {
{inviteRoleMeta.label} {inviteRoleMeta.label}
</span> </span>
</div> </div>
<p className="mt-0.5 text-xs text-slate-500"> <p className="mt-0.5 text-xs text-muted-foreground">
Created {fmtDate(inv.created_at)} · {fmtExpiresIn(inv.expires_at)} Created {fmtDate(inv.created_at)} · {fmtExpiresIn(inv.expires_at)}
</p> </p>
</div> </div>
@ -544,15 +544,15 @@ export function MembersTab() {
if (!open) setRemovingMember(null); if (!open) setRemovingMember(null);
}} }}
> >
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-sm"> <DialogContent className="bg-popover border-border sm:max-w-sm">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-white"> <DialogTitle className="flex items-center gap-2 text-popover-foreground">
<AlertTriangle className="size-4 text-amber-400" /> <AlertTriangle className="size-4 text-amber-400" />
Remove member Remove member
</DialogTitle> </DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
Remove{' '} Remove{' '}
<span className="font-medium text-slate-300"> <span className="font-medium text-muted-foreground">
{removingMember?.full_name || 'this teammate'} {removingMember?.full_name || 'this teammate'}
</span>{' '} </span>{' '}
from the account? They&apos;ll be signed out of this account from the account? They&apos;ll be signed out of this account
@ -560,11 +560,11 @@ export function MembersTab() {
login isn&apos;t deleted. login isn&apos;t deleted.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter className="bg-slate-900 border-slate-700"> <DialogFooter className="bg-popover border-border">
<Button <Button
variant="outline" variant="outline"
onClick={() => setRemovingMember(null)} onClick={() => setRemovingMember(null)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>

View File

@ -81,13 +81,13 @@ export function PasswordForm() {
}; };
return ( return (
<Card className="bg-slate-900/40 border-slate-800"> <Card className="bg-card/40 border-border">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-white"> <CardTitle className="flex items-center gap-2 text-foreground">
<KeyRound className="size-4 text-primary" /> <KeyRound className="size-4 text-primary" />
Password Password
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
Use at least {MIN_PASSWORD} characters. You will stay signed in on Use at least {MIN_PASSWORD} characters. You will stay signed in on
this device after changing it. this device after changing it.
</CardDescription> </CardDescription>
@ -96,7 +96,7 @@ export function PasswordForm() {
<CardContent> <CardContent>
<form onSubmit={onSubmit} className="space-y-4"> <form onSubmit={onSubmit} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="current-password" className="text-slate-200"> <Label htmlFor="current-password" className="text-foreground">
Current password Current password
</Label> </Label>
<Input <Input
@ -112,7 +112,7 @@ export function PasswordForm() {
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="new-password" className="text-slate-200"> <Label htmlFor="new-password" className="text-foreground">
New password New password
</Label> </Label>
<Input <Input
@ -127,7 +127,7 @@ export function PasswordForm() {
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="confirm-password" className="text-slate-200"> <Label htmlFor="confirm-password" className="text-foreground">
Confirm new password Confirm new password
</Label> </Label>
<Input <Input

View File

@ -210,10 +210,10 @@ export function ProfileForm() {
: '—'; : '—';
return ( return (
<Card className="bg-slate-900/40 border-slate-800"> <Card className="bg-card/40 border-border">
<CardHeader> <CardHeader>
<CardTitle className="text-white">Profile</CardTitle> <CardTitle className="text-foreground">Profile</CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
How you show up across the app. Your avatar and name appear in the How you show up across the app. Your avatar and name appear in the
header, sidebar, and anywhere your teammates see you. header, sidebar, and anywhere your teammates see you.
</CardDescription> </CardDescription>
@ -255,13 +255,13 @@ export function ProfileForm() {
variant="ghost" variant="ghost"
onClick={onRemoveAvatar} onClick={onRemoveAvatar}
disabled={saving} disabled={saving}
className="text-slate-400 hover:text-white" className="text-muted-foreground hover:text-foreground"
> >
<Trash2 className="size-4" /> <Trash2 className="size-4" />
Remove Remove
</Button> </Button>
)} )}
<p className="w-full text-xs text-slate-500"> <p className="w-full text-xs text-muted-foreground">
PNG, JPG, WebP, or GIF. Up to 2 MB. PNG, JPG, WebP, or GIF. Up to 2 MB.
</p> </p>
</div> </div>
@ -269,7 +269,7 @@ export function ProfileForm() {
{/* Name */} {/* Name */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="profile-full-name" className="text-slate-200"> <Label htmlFor="profile-full-name" className="text-foreground">
Display name Display name
</Label> </Label>
<Input <Input
@ -285,7 +285,7 @@ export function ProfileForm() {
{/* Email */} {/* Email */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="profile-email" className="text-slate-200"> <Label htmlFor="profile-email" className="text-foreground">
Email Email
</Label> </Label>
<Input <Input
@ -309,24 +309,24 @@ export function ProfileForm() {
</div> </div>
{/* Read-only block */} {/* Read-only block */}
<div className="rounded-lg border border-slate-800 bg-slate-900/60 p-4"> <div className="rounded-lg border border-border bg-card/60 p-4">
<p className="mb-3 text-xs font-semibold uppercase tracking-wider text-slate-500"> <p className="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Account details Account details
</p> </p>
<dl className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2"> <dl className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
<div> <div>
<dt className="text-slate-500">Role</dt> <dt className="text-muted-foreground">Role</dt>
<dd className="mt-0.5 font-mono text-slate-200"> <dd className="mt-0.5 font-mono text-foreground">
{profile?.role ?? 'user'} {profile?.role ?? 'user'}
</dd> </dd>
</div> </div>
<div> <div>
<dt className="text-slate-500">Joined</dt> <dt className="text-muted-foreground">Joined</dt>
<dd className="mt-0.5 text-slate-200">{joined}</dd> <dd className="mt-0.5 text-foreground">{joined}</dd>
</div> </div>
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<dt className="text-slate-500">User ID</dt> <dt className="text-muted-foreground">User ID</dt>
<dd className="mt-0.5 break-all font-mono text-xs text-slate-400"> <dd className="mt-0.5 break-all font-mono text-xs text-muted-foreground">
{user?.id ?? '—'} {user?.id ?? '—'}
</dd> </dd>
</div> </div>
@ -334,7 +334,7 @@ export function ProfileForm() {
</div> </div>
{!profile && ( {!profile && (
<p className="flex items-center gap-2 text-sm text-slate-400"> <p className="flex items-center gap-2 text-sm text-muted-foreground">
<CircleAlert className="size-4" /> <CircleAlert className="size-4" />
Loading your profile… Loading your profile…
</p> </p>

View File

@ -49,13 +49,13 @@ export function SessionsCard() {
return ( return (
<> <>
<Card className="bg-slate-900/40 border-slate-800"> <Card className="bg-card/40 border-border">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-white"> <CardTitle className="flex items-center gap-2 text-foreground">
<LogOut className="size-4 text-primary" /> <LogOut className="size-4 text-primary" />
Active sessions Active sessions
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
Sign out of every device where you&apos;re logged in — including Sign out of every device where you&apos;re logged in — including
this one. Useful if you lost a laptop or shared your password. this one. Useful if you lost a laptop or shared your password.
</CardDescription> </CardDescription>

View File

@ -152,8 +152,8 @@ export function TagManager() {
<div className="space-y-4 mt-4"> <div className="space-y-4 mt-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-lg font-semibold text-white">Tags</h2> <h2 className="text-lg font-semibold text-foreground">Tags</h2>
<p className="text-sm text-slate-400">Organize your contacts with color-coded tags.</p> <p className="text-sm text-muted-foreground">Organize your contacts with color-coded tags.</p>
</div> </div>
<Button <Button
onClick={() => { onClick={() => {
@ -169,14 +169,14 @@ export function TagManager() {
</div> </div>
{tags.length === 0 ? ( {tags.length === 0 ? (
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent"> <Card className="bg-card border-border ring-0 ring-transparent">
<CardContent className="flex flex-col items-center justify-center py-12 text-center"> <CardContent className="flex flex-col items-center justify-center py-12 text-center">
<p className="text-slate-400 text-sm">No tags yet.</p> <p className="text-muted-foreground text-sm">No tags yet.</p>
<p className="text-slate-500 text-xs mt-1">Create tags to categorize your contacts.</p> <p className="text-muted-foreground text-xs mt-1">Create tags to categorize your contacts.</p>
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent"> <Card className="bg-card border-border ring-0 ring-transparent">
<CardContent className="pt-4"> <CardContent className="pt-4">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{tags.map((tag) => ( {tags.map((tag) => (
@ -196,7 +196,7 @@ export function TagManager() {
{tag.name} {tag.name}
<button <button
onClick={() => confirmDelete(tag)} onClick={() => confirmDelete(tag)}
className="ml-0.5 rounded-full p-0.5 opacity-0 group-hover:opacity-100 transition-opacity hover:bg-white/10" className="ml-0.5 rounded-full p-0.5 opacity-0 group-hover:opacity-100 transition-opacity hover:bg-muted"
> >
<X className="size-3" /> <X className="size-3" />
</button> </button>
@ -209,22 +209,22 @@ export function TagManager() {
{/* New Tag Dialog */} {/* New Tag Dialog */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}> <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-sm"> <DialogContent className="bg-popover border-border sm:max-w-sm">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white">New Tag</DialogTitle> <DialogTitle className="text-popover-foreground">New Tag</DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
Create a new tag with a name and color. Create a new tag with a name and color.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-2"> <div className="space-y-4 py-2">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Tag Name</Label> <Label className="text-muted-foreground">Tag Name</Label>
<Input <Input
placeholder="e.g. VIP Customer" placeholder="e.g. VIP Customer"
value={newTagName} value={newTagName}
onChange={(e) => setNewTagName(e.target.value)} onChange={(e) => setNewTagName(e.target.value)}
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') handleCreate(); if (e.key === 'Enter') handleCreate();
}} }}
@ -232,13 +232,13 @@ export function TagManager() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Color</Label> <Label className="text-muted-foreground">Color</Label>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{PRESET_COLORS.map((color) => ( {PRESET_COLORS.map((color) => (
<button <button
key={color.value} key={color.value}
onClick={() => setSelectedColor(color.value)} onClick={() => setSelectedColor(color.value)}
className="relative size-8 rounded-full transition-transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-900" className="relative size-8 rounded-full transition-transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-popover"
style={{ style={{
backgroundColor: color.value, backgroundColor: color.value,
boxShadow: selectedColor === color.value ? `0 0 0 2px rgb(15 23 42), 0 0 0 4px ${color.value}` : 'none', boxShadow: selectedColor === color.value ? `0 0 0 2px rgb(15 23 42), 0 0 0 4px ${color.value}` : 'none',
@ -251,7 +251,7 @@ export function TagManager() {
{/* Preview */} {/* Preview */}
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Preview</Label> <Label className="text-muted-foreground">Preview</Label>
<div> <div>
<span <span
className="inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-medium" className="inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-medium"
@ -271,11 +271,11 @@ export function TagManager() {
</div> </div>
</div> </div>
<DialogFooter className="bg-slate-900 border-slate-700"> <DialogFooter className="bg-popover border-border">
<Button <Button
variant="outline" variant="outline"
onClick={() => setDialogOpen(false)} onClick={() => setDialogOpen(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>
@ -299,19 +299,19 @@ export function TagManager() {
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-sm"> <DialogContent className="bg-popover border-border sm:max-w-sm">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white">Delete Tag</DialogTitle> <DialogTitle className="text-popover-foreground">Delete Tag</DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
Are you sure you want to delete the tag &quot;{tagToDelete?.name}&quot;? This will remove Are you sure you want to delete the tag &quot;{tagToDelete?.name}&quot;? This will remove
it from all contacts. This action cannot be undone. it from all contacts. This action cannot be undone.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter className="bg-slate-900 border-slate-700"> <DialogFooter className="bg-popover border-border">
<Button <Button
variant="outline" variant="outline"
onClick={() => setDeleteDialogOpen(false)} onClick={() => setDeleteDialogOpen(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>

View File

@ -449,8 +449,8 @@ export function TemplateManager() {
<div className="space-y-4 mt-4"> <div className="space-y-4 mt-4">
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<div> <div>
<h2 className="text-lg font-semibold text-white">Message Templates</h2> <h2 className="text-lg font-semibold text-foreground">Message Templates</h2>
<p className="text-sm text-slate-400"> <p className="text-sm text-muted-foreground">
Create message templates and submit them to Meta for approval. Use Create message templates and submit them to Meta for approval. Use
&quot;Sync from Meta&quot; to pull templates approved elsewhere. &quot;Sync from Meta&quot; to pull templates approved elsewhere.
</p> </p>
@ -460,7 +460,7 @@ export function TemplateManager() {
variant="outline" variant="outline"
onClick={handleSyncFromMeta} onClick={handleSyncFromMeta}
disabled={syncing} disabled={syncing}
className="border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800" className="border-border bg-transparent text-muted-foreground hover:bg-muted"
title="Pull approved templates from your Meta WhatsApp Business Account" title="Pull approved templates from your Meta WhatsApp Business Account"
> >
<RefreshCw className={`size-4 ${syncing ? 'animate-spin' : ''}`} /> <RefreshCw className={`size-4 ${syncing ? 'animate-spin' : ''}`} />
@ -477,10 +477,10 @@ export function TemplateManager() {
</div> </div>
{templates.length === 0 ? ( {templates.length === 0 ? (
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent"> <Card className="bg-card border-border ring-0 ring-transparent">
<CardContent className="flex flex-col items-center justify-center py-12 text-center"> <CardContent className="flex flex-col items-center justify-center py-12 text-center">
<p className="text-slate-400 text-sm">No templates yet.</p> <p className="text-muted-foreground text-sm">No templates yet.</p>
<p className="text-slate-500 text-xs mt-1"> <p className="text-muted-foreground text-xs mt-1">
Create your first message template to get started. Create your first message template to get started.
</p> </p>
</CardContent> </CardContent>
@ -493,12 +493,12 @@ export function TemplateManager() {
return ( return (
<Card <Card
key={template.id} key={template.id}
className="bg-slate-900 border-slate-700 ring-0 ring-transparent" className="bg-card border-border ring-0 ring-transparent"
> >
<CardContent className="flex items-start justify-between pt-4"> <CardContent className="flex items-start justify-between pt-4">
<div className="space-y-2 min-w-0 flex-1"> <div className="space-y-2 min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<h3 className="font-medium text-white">{template.name}</h3> <h3 className="font-medium text-foreground">{template.name}</h3>
<Badge <Badge
className={`text-xs border ${categoryColors[template.category] || ''}`} className={`text-xs border ${categoryColors[template.category] || ''}`}
> >
@ -508,7 +508,7 @@ export function TemplateManager() {
{status.label} {status.label}
</Badge> </Badge>
{template.language && ( {template.language && (
<span className="text-xs text-slate-500 uppercase"> <span className="text-xs text-muted-foreground uppercase">
{template.language} {template.language}
</span> </span>
)} )}
@ -527,11 +527,11 @@ export function TemplateManager() {
</span> </span>
)} )}
</div> </div>
<p className="text-sm text-slate-400 line-clamp-2"> <p className="text-sm text-muted-foreground line-clamp-2">
{template.body_text} {template.body_text}
</p> </p>
{template.footer_text && ( {template.footer_text && (
<p className="text-xs text-slate-500 italic"> <p className="text-xs text-muted-foreground italic">
{template.footer_text} {template.footer_text}
</p> </p>
)} )}
@ -552,7 +552,7 @@ export function TemplateManager() {
onClick={() => openEdit(template)} onClick={() => openEdit(template)}
title="Editing triggers Meta re-review — status flips to PENDING." title="Editing triggers Meta re-review — status flips to PENDING."
aria-label="Edit template" aria-label="Edit template"
className="text-slate-300 hover:text-primary hover:bg-primary/10 h-8 px-2" className="text-muted-foreground hover:text-primary hover:bg-primary/10 h-8 px-2"
> >
<Pencil className="size-3.5" /> <Pencil className="size-3.5" />
Edit Edit
@ -565,7 +565,7 @@ export function TemplateManager() {
onClick={() => openEdit(template)} onClick={() => openEdit(template)}
title="Edit the template and resubmit to Meta for review." title="Edit the template and resubmit to Meta for review."
aria-label="Edit and resubmit template" aria-label="Edit and resubmit template"
className="text-slate-300 hover:text-primary hover:bg-primary/10 h-8 px-2" className="text-muted-foreground hover:text-primary hover:bg-primary/10 h-8 px-2"
> >
<RotateCcw className="size-3.5" /> <RotateCcw className="size-3.5" />
Resubmit Resubmit
@ -586,7 +586,7 @@ export function TemplateManager() {
? 'Delete from Meta and locally' ? 'Delete from Meta and locally'
: 'Delete locally' : 'Delete locally'
} }
className="text-slate-400 hover:text-red-400 hover:bg-red-950/30 h-8 w-8" className="text-muted-foreground hover:text-red-400 hover:bg-red-950/30 h-8 w-8"
> >
{deletingId === template.id ? ( {deletingId === template.id ? (
<Loader2 className="size-4 animate-spin" /> <Loader2 className="size-4 animate-spin" />
@ -612,12 +612,12 @@ export function TemplateManager() {
} }
}} }}
> >
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-2xl max-h-[90vh] overflow-y-auto"> <DialogContent className="bg-popover border-border sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white"> <DialogTitle className="text-popover-foreground">
{editingId ? 'Edit Message Template' : 'New Message Template'} {editingId ? 'Edit Message Template' : 'New Message Template'}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
{editingId {editingId
? 'Save your changes to re-submit to Meta. Status will flip back to PENDING during review.' ? 'Save your changes to re-submit to Meta. Status will flip back to PENDING during review.'
: 'Build a template and submit it to Meta for approval. Once approved, you can use it in broadcasts and the inbox.'} : 'Build a template and submit it to Meta for approval. Once approved, you can use it in broadcasts and the inbox.'}
@ -638,15 +638,15 @@ export function TemplateManager() {
<div className="space-y-4 py-2"> <div className="space-y-4 py-2">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Template Name</Label> <Label className="text-muted-foreground">Template Name</Label>
<Input <Input
placeholder="e.g. order_confirmation" placeholder="e.g. order_confirmation"
value={form.name} value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })} onChange={(e) => setForm({ ...form, name: e.target.value })}
disabled={editingId !== null} disabled={editingId !== null}
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 disabled:opacity-60 disabled:cursor-not-allowed" className="bg-muted border-border text-foreground placeholder:text-muted-foreground disabled:opacity-60 disabled:cursor-not-allowed"
/> />
<p className="text-[11px] text-slate-500"> <p className="text-[11px] text-muted-foreground">
{editingId {editingId
? 'Name is fixed once a template exists on Meta — create a new template to change it.' ? 'Name is fixed once a template exists on Meta — create a new template to change it.'
: 'Lowercase letters, digits, and underscores only.'} : 'Lowercase letters, digits, and underscores only.'}
@ -655,7 +655,7 @@ export function TemplateManager() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Category</Label> <Label className="text-muted-foreground">Category</Label>
<Select <Select
value={form.category} value={form.category}
onValueChange={(val) => onValueChange={(val) =>
@ -665,15 +665,15 @@ export function TemplateManager() {
}) })
} }
> >
<SelectTrigger className="w-full bg-slate-800 border-slate-700 text-white"> <SelectTrigger className="w-full bg-muted border-border text-foreground">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700"> <SelectContent className="bg-popover border-border">
{CATEGORIES.map((cat) => ( {CATEGORIES.map((cat) => (
<SelectItem <SelectItem
key={cat} key={cat}
value={cat} value={cat}
className="text-white focus:bg-slate-700 focus:text-white" className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
> >
{cat} {cat}
</SelectItem> </SelectItem>
@ -683,7 +683,7 @@ export function TemplateManager() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Language</Label> <Label className="text-muted-foreground">Language</Label>
<Input <Input
list="template-language-codes" list="template-language-codes"
placeholder="en_US" placeholder="en_US"
@ -692,14 +692,14 @@ export function TemplateManager() {
setForm({ ...form, language: e.target.value }) setForm({ ...form, language: e.target.value })
} }
disabled={editingId !== null} disabled={editingId !== null}
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 disabled:opacity-60 disabled:cursor-not-allowed" className="bg-muted border-border text-foreground placeholder:text-muted-foreground disabled:opacity-60 disabled:cursor-not-allowed"
/> />
<datalist id="template-language-codes"> <datalist id="template-language-codes">
{COMMON_LANGUAGE_CODES.map((code) => ( {COMMON_LANGUAGE_CODES.map((code) => (
<option key={code} value={code} /> <option key={code} value={code} />
))} ))}
</datalist> </datalist>
<p className="text-[11px] text-slate-500"> <p className="text-[11px] text-muted-foreground">
{editingId {editingId
? 'Language is fixed once a template exists on Meta.' ? 'Language is fixed once a template exists on Meta.'
: ( : (
@ -713,7 +713,7 @@ export function TemplateManager() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Header</Label> <Label className="text-muted-foreground">Header</Label>
<Select <Select
value={form.header_format} value={form.header_format}
onValueChange={(val) => onValueChange={(val) =>
@ -729,15 +729,15 @@ export function TemplateManager() {
}) })
} }
> >
<SelectTrigger className="w-full bg-slate-800 border-slate-700 text-white"> <SelectTrigger className="w-full bg-muted border-border text-foreground">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700"> <SelectContent className="bg-popover border-border">
{HEADER_FORMATS.map((type) => ( {HEADER_FORMATS.map((type) => (
<SelectItem <SelectItem
key={type} key={type}
value={type} value={type}
className="text-white focus:bg-slate-700 focus:text-white" className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
> >
{type === 'none' {type === 'none'
? 'None' ? 'None'
@ -758,7 +758,7 @@ export function TemplateManager() {
setForm({ ...form, header_content: e.target.value }) setForm({ ...form, header_content: e.target.value })
} }
maxLength={TEMPLATE_LIMITS.headerTextMaxLength} maxLength={TEMPLATE_LIMITS.headerTextMaxLength}
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
{headerVarCount > 0 && ( {headerVarCount > 0 && (
<Input <Input
@ -769,7 +769,7 @@ export function TemplateManager() {
onChange={(e) => onChange={(e) =>
setForm({ ...form, header_sample: e.target.value }) setForm({ ...form, header_sample: e.target.value })
} }
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
)} )}
</div> </div>
@ -783,9 +783,9 @@ export function TemplateManager() {
onChange={(e) => onChange={(e) =>
setForm({ ...form, header_media_url: e.target.value }) setForm({ ...form, header_media_url: e.target.value })
} }
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
<p className="text-[11px] text-slate-500 leading-relaxed"> <p className="text-[11px] text-muted-foreground leading-relaxed">
Must be publicly accessible HTTPS. Meta fetches it once Must be publicly accessible HTTPS. Meta fetches it once
during review, so the file needs to stay live for ~24 hrs. during review, so the file needs to stay live for ~24 hrs.
{form.header_format === 'image' && {form.header_format === 'image' &&
@ -801,7 +801,7 @@ export function TemplateManager() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Body Text</Label> <Label className="text-muted-foreground">Body Text</Label>
<Textarea <Textarea
placeholder="Hello {{1}}, your order {{2}} is confirmed." placeholder="Hello {{1}}, your order {{2}} is confirmed."
value={form.body_text} value={form.body_text}
@ -810,16 +810,16 @@ export function TemplateManager() {
} }
rows={4} rows={4}
maxLength={TEMPLATE_LIMITS.bodyMaxLength} maxLength={TEMPLATE_LIMITS.bodyMaxLength}
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 resize-none" className="bg-muted border-border text-foreground placeholder:text-muted-foreground resize-none"
/> />
<p className="text-[11px] text-slate-500"> <p className="text-[11px] text-muted-foreground">
Use {`{{1}}`}, {`{{2}}`} for variables (must be contiguous Use {`{{1}}`}, {`{{2}}`} for variables (must be contiguous
starting at {`{{1}}`}). starting at {`{{1}}`}).
</p> </p>
{bodyVarCount > 0 && ( {bodyVarCount > 0 && (
<div className="space-y-1.5 pt-1"> <div className="space-y-1.5 pt-1">
<Label className="text-[11px] text-slate-400"> <Label className="text-[11px] text-muted-foreground">
Sample values (Meta uses these to review your template) Sample values (Meta uses these to review your template)
</Label> </Label>
{form.body_samples.map((val, i) => { {form.body_samples.map((val, i) => {
@ -836,7 +836,7 @@ export function TemplateManager() {
next[i] = e.target.value; next[i] = e.target.value;
setForm({ ...form, body_samples: next }); setForm({ ...form, body_samples: next });
}} }}
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
); );
})} })}
@ -845,7 +845,7 @@ export function TemplateManager() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Footer (optional)</Label> <Label className="text-muted-foreground">Footer (optional)</Label>
<Input <Input
placeholder="Optional footer text (max 60 chars)" placeholder="Optional footer text (max 60 chars)"
value={form.footer_text} value={form.footer_text}
@ -853,27 +853,27 @@ export function TemplateManager() {
setForm({ ...form, footer_text: e.target.value }) setForm({ ...form, footer_text: e.target.value })
} }
maxLength={TEMPLATE_LIMITS.footerMaxLength} maxLength={TEMPLATE_LIMITS.footerMaxLength}
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label className="text-slate-300">Buttons (optional)</Label> <Label className="text-muted-foreground">Buttons (optional)</Label>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={addButton} onClick={addButton}
disabled={form.buttons.length >= TEMPLATE_LIMITS.maxButtonsTotal} disabled={form.buttons.length >= TEMPLATE_LIMITS.maxButtonsTotal}
className="border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800 h-7 text-xs" className="border-border bg-transparent text-muted-foreground hover:bg-muted h-7 text-xs"
> >
<Plus className="size-3" /> <Plus className="size-3" />
Add Button Add Button
</Button> </Button>
</div> </div>
{form.buttons.length === 0 ? ( {form.buttons.length === 0 ? (
<p className="text-[11px] text-slate-500"> <p className="text-[11px] text-muted-foreground">
Up to {TEMPLATE_LIMITS.maxButtonsTotal} buttons. QUICK_REPLY Up to {TEMPLATE_LIMITS.maxButtonsTotal} buttons. QUICK_REPLY
buttons must come before URL / phone / copy-code buttons. buttons must come before URL / phone / copy-code buttons.
</p> </p>
@ -882,7 +882,7 @@ export function TemplateManager() {
{form.buttons.map((btn, i) => ( {form.buttons.map((btn, i) => (
<div <div
key={i} key={i}
className="space-y-2 rounded border border-slate-700 bg-slate-800/50 p-2" className="space-y-2 rounded border border-border bg-muted/50 p-2"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Select <Select
@ -895,31 +895,31 @@ export function TemplateManager() {
changeButtonType(i, val as TemplateButton['type']); changeButtonType(i, val as TemplateButton['type']);
}} }}
> >
<SelectTrigger className="w-40 bg-slate-800 border-slate-700 text-white h-8 text-xs"> <SelectTrigger className="w-40 bg-muted border-border text-foreground h-8 text-xs">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700"> <SelectContent className="bg-popover border-border">
<SelectItem <SelectItem
value="QUICK_REPLY" value="QUICK_REPLY"
className="text-white focus:bg-slate-700 focus:text-white" className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
> >
Quick Reply Quick Reply
</SelectItem> </SelectItem>
<SelectItem <SelectItem
value="URL" value="URL"
className="text-white focus:bg-slate-700 focus:text-white" className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
> >
URL URL
</SelectItem> </SelectItem>
<SelectItem <SelectItem
value="PHONE_NUMBER" value="PHONE_NUMBER"
className="text-white focus:bg-slate-700 focus:text-white" className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
> >
Phone Phone
</SelectItem> </SelectItem>
<SelectItem <SelectItem
value="COPY_CODE" value="COPY_CODE"
className="text-white focus:bg-slate-700 focus:text-white" className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
> >
Copy Code Copy Code
</SelectItem> </SelectItem>
@ -932,14 +932,14 @@ export function TemplateManager() {
onChange={(e) => onChange={(e) =>
updateButton(i, { text: e.target.value }) updateButton(i, { text: e.target.value })
} }
className="flex-1 bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 h-8 text-xs" className="flex-1 bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
/> />
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => removeButton(i)} onClick={() => removeButton(i)}
className="text-slate-400 hover:text-red-400 hover:bg-red-950/30 size-7" className="text-muted-foreground hover:text-red-400 hover:bg-red-950/30 size-7"
> >
<X className="size-3.5" /> <X className="size-3.5" />
</Button> </Button>
@ -952,7 +952,7 @@ export function TemplateManager() {
onChange={(e) => onChange={(e) =>
updateButton(i, { url: e.target.value }) updateButton(i, { url: e.target.value })
} }
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 h-8 text-xs" className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
/> />
{extractVariableIndices(btn.url).length > 0 && ( {extractVariableIndices(btn.url).length > 0 && (
<Input <Input
@ -961,7 +961,7 @@ export function TemplateManager() {
onChange={(e) => onChange={(e) =>
updateButton(i, { example: e.target.value }) updateButton(i, { example: e.target.value })
} }
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 h-8 text-xs" className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
/> />
)} )}
</div> </div>
@ -973,7 +973,7 @@ export function TemplateManager() {
onChange={(e) => onChange={(e) =>
updateButton(i, { phone_number: e.target.value }) updateButton(i, { phone_number: e.target.value })
} }
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 h-8 text-xs" className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
/> />
)} )}
{btn.type === 'COPY_CODE' && ( {btn.type === 'COPY_CODE' && (
@ -983,7 +983,7 @@ export function TemplateManager() {
onChange={(e) => onChange={(e) =>
updateButton(i, { example: e.target.value }) updateButton(i, { example: e.target.value })
} }
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 h-8 text-xs" className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
/> />
)} )}
</div> </div>
@ -993,11 +993,11 @@ export function TemplateManager() {
</div> </div>
</div> </div>
<DialogFooter className="bg-slate-900 border-slate-700"> <DialogFooter className="bg-popover border-border">
<Button <Button
variant="outline" variant="outline"
onClick={() => setDialogOpen(false)} onClick={() => setDialogOpen(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>
@ -1030,21 +1030,21 @@ export function TemplateManager() {
if (!open) setTemplateToDelete(null); if (!open) setTemplateToDelete(null);
}} }}
> >
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-sm"> <DialogContent className="bg-popover border-border sm:max-w-sm">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white">Delete template?</DialogTitle> <DialogTitle className="text-popover-foreground">Delete template?</DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-muted-foreground">
{templateToDelete?.meta_template_id {templateToDelete?.meta_template_id
? `"${templateToDelete?.name}" will be deleted from Meta and from wacrm. Active broadcasts using this template will start failing on their next send. This can't be undone.` ? `"${templateToDelete?.name}" will be deleted from Meta and from wacrm. Active broadcasts using this template will start failing on their next send. This can't be undone.`
: `"${templateToDelete?.name}" will be deleted from wacrm. It was never submitted to Meta, so no remote cleanup is needed.`} : `"${templateToDelete?.name}" will be deleted from wacrm. It was never submitted to Meta, so no remote cleanup is needed.`}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter className="bg-slate-900 border-slate-700"> <DialogFooter className="bg-popover border-border">
<Button <Button
variant="outline" variant="outline"
onClick={() => setTemplateToDelete(null)} onClick={() => setTemplateToDelete(null)}
disabled={deletingId !== null} disabled={deletingId !== null}
className="border-slate-700 text-slate-300 hover:bg-slate-800" className="border-border text-muted-foreground hover:bg-muted"
> >
Cancel Cancel
</Button> </Button>

View File

@ -408,18 +408,18 @@ export function WhatsAppConfig() {
)} )}
{/* Connection Status */} {/* Connection Status */}
<Alert className="bg-slate-900 border-slate-700"> <Alert className="bg-card border-border">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{connectionStatus === 'connected' ? ( {connectionStatus === 'connected' ? (
<CheckCircle2 className="size-4 text-primary" /> <CheckCircle2 className="size-4 text-primary" />
) : ( ) : (
<XCircle className="size-4 text-red-500" /> <XCircle className="size-4 text-red-500" />
)} )}
<AlertTitle className="text-white mb-0"> <AlertTitle className="text-foreground mb-0">
{connectionStatus === 'connected' ? 'Credentials valid' : 'Not Connected'} {connectionStatus === 'connected' ? 'Credentials valid' : 'Not Connected'}
</AlertTitle> </AlertTitle>
</div> </div>
<AlertDescription className="text-slate-400"> <AlertDescription className="text-muted-foreground">
{connectionStatus === 'connected' {connectionStatus === 'connected'
? 'Your access token authenticates with Meta. See Registration status below for whether webhooks are actually wired.' ? 'Your access token authenticates with Meta. See Registration status below for whether webhooks are actually wired.'
: statusMessage || : statusMessage ||
@ -462,7 +462,7 @@ export function WhatsAppConfig() {
size="sm" size="sm"
onClick={handleVerifyRegistration} onClick={handleVerifyRegistration}
disabled={verifyingRegistration} disabled={verifyingRegistration}
className="border-slate-700 bg-transparent text-slate-200 hover:bg-slate-800 h-7" className="border-border bg-transparent text-foreground hover:bg-muted h-7"
> >
{verifyingRegistration ? ( {verifyingRegistration ? (
<Loader2 className="size-3.5 animate-spin" /> <Loader2 className="size-3.5 animate-spin" />
@ -472,7 +472,7 @@ export function WhatsAppConfig() {
Verify with Meta Verify with Meta
</Button> </Button>
</div> </div>
<AlertDescription className="text-slate-400 mt-2 text-xs leading-relaxed"> <AlertDescription className="text-muted-foreground mt-2 text-xs leading-relaxed">
{isRegistered ? ( {isRegistered ? (
<> <>
Subscribed since{' '} Subscribed since{' '}
@ -502,14 +502,14 @@ export function WhatsAppConfig() {
</AlertDescription> </AlertDescription>
{registrationProbe && ( {registrationProbe && (
<div className="mt-3 rounded border border-slate-700 bg-slate-900/60 px-3 py-2 space-y-1.5 text-[11px]"> <div className="mt-3 rounded border border-border bg-card/60 px-3 py-2 space-y-1.5 text-[11px]">
<p className="font-medium text-slate-200"> <p className="font-medium text-foreground">
Diagnostic — last run: {' '} Diagnostic — last run: {' '}
<span className={registrationProbe.live ? 'text-emerald-400' : 'text-amber-400'}> <span className={registrationProbe.live ? 'text-emerald-400' : 'text-amber-400'}>
{registrationProbe.live ? 'live' : 'not live'} {registrationProbe.live ? 'live' : 'not live'}
</span> </span>
</p> </p>
<ul className="space-y-0.5 text-slate-400"> <ul className="space-y-0.5 text-muted-foreground">
{Object.entries(registrationProbe.checks).map(([k, v]) => ( {Object.entries(registrationProbe.checks).map(([k, v]) => (
<li key={k} className="flex items-center gap-1.5"> <li key={k} className="flex items-center gap-1.5">
{v === true ? ( {v === true ? (
@ -517,9 +517,9 @@ export function WhatsAppConfig() {
) : v === false ? ( ) : v === false ? (
<XCircle className="size-3 text-red-400 shrink-0" /> <XCircle className="size-3 text-red-400 shrink-0" />
) : ( ) : (
<span className="size-3 rounded-full border border-slate-600 shrink-0" /> <span className="size-3 rounded-full border border-border shrink-0" />
)} )}
<code className="text-slate-300">{k}</code> <code className="text-muted-foreground">{k}</code>
</li> </li>
))} ))}
</ul> </ul>
@ -536,36 +536,36 @@ export function WhatsAppConfig() {
)} )}
{/* API Credentials */} {/* API Credentials */}
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent"> <Card className="bg-card border-border ring-0 ring-transparent">
<CardHeader> <CardHeader>
<CardTitle className="text-white">API Credentials</CardTitle> <CardTitle className="text-foreground">API Credentials</CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
Enter your Meta WhatsApp Business API credentials. Enter your Meta WhatsApp Business API credentials.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Phone Number ID</Label> <Label className="text-muted-foreground">Phone Number ID</Label>
<Input <Input
placeholder="e.g. 100234567890123" placeholder="e.g. 100234567890123"
value={phoneNumberId} value={phoneNumberId}
onChange={(e) => setPhoneNumberId(e.target.value)} onChange={(e) => setPhoneNumberId(e.target.value)}
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">WhatsApp Business Account ID</Label> <Label className="text-muted-foreground">WhatsApp Business Account ID</Label>
<Input <Input
placeholder="e.g. 100234567890456" placeholder="e.g. 100234567890456"
value={wabaId} value={wabaId}
onChange={(e) => setWabaId(e.target.value)} onChange={(e) => setWabaId(e.target.value)}
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Permanent Access Token</Label> <Label className="text-muted-foreground">Permanent Access Token</Label>
<div className="relative"> <div className="relative">
<Input <Input
type={showToken ? 'text' : 'password'} type={showToken ? 'text' : 'password'}
@ -581,40 +581,40 @@ export function WhatsAppConfig() {
setTokenEdited(true); setTokenEdited(true);
} }
}} }}
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 pr-10" className="bg-muted border-border text-foreground placeholder:text-muted-foreground pr-10"
/> />
<button <button
type="button" type="button"
onClick={() => setShowToken(!showToken)} onClick={() => setShowToken(!showToken)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white transition-colors" className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
> >
{showToken ? <EyeOff className="size-4" /> : <Eye className="size-4" />} {showToken ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
</button> </button>
</div> </div>
{config && !tokenEdited && ( {config && !tokenEdited && (
<p className="text-xs text-slate-500"> <p className="text-xs text-muted-foreground">
Token is hidden for security. Re-enter it to update configuration. Token is hidden for security. Re-enter it to update configuration.
</p> </p>
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Webhook Verify Token</Label> <Label className="text-muted-foreground">Webhook Verify Token</Label>
<Input <Input
placeholder="Create a custom verify token" placeholder="Create a custom verify token"
value={verifyToken} value={verifyToken}
onChange={(e) => setVerifyToken(e.target.value)} onChange={(e) => setVerifyToken(e.target.value)}
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
<p className="text-xs text-slate-500"> <p className="text-xs text-muted-foreground">
A custom string you create. Must match the token you set in Meta webhook settings. A custom string you create. Must match the token you set in Meta webhook settings.
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300"> <Label className="text-muted-foreground">
Two-step verification PIN Two-step verification PIN
<span className="ml-1 text-slate-500">(optional)</span> <span className="ml-1 text-muted-foreground">(optional)</span>
</Label> </Label>
<Input <Input
type="text" type="text"
@ -625,12 +625,12 @@ export function WhatsAppConfig() {
onChange={(e) => onChange={(e) =>
setPin(e.target.value.replace(/\D/g, '').slice(0, 6)) setPin(e.target.value.replace(/\D/g, '').slice(0, 6))
} }
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 tracking-widest" className="bg-muted border-border text-foreground placeholder:text-muted-foreground tracking-widest"
/> />
<p className="text-xs text-slate-500 leading-relaxed"> <p className="text-xs text-muted-foreground leading-relaxed">
Needed only to wire <strong className="text-slate-300">inbound</strong> messages Needed only to wire <strong className="text-muted-foreground">inbound</strong> messages
for a <strong className="text-slate-300">production</strong> number. Set it in{' '} for a <strong className="text-muted-foreground">production</strong> number. Set it in{' '}
<strong className="text-slate-300"> <strong className="text-muted-foreground">
Meta Business Manager → WhatsApp Accounts → Phone Meta Business Manager → WhatsApp Accounts → Phone
Numbers → Two-step verification Numbers → Two-step verification
</strong> </strong>
@ -638,7 +638,7 @@ export function WhatsAppConfig() {
otherwise Meta routes inbound events to whichever app otherwise Meta routes inbound events to whichever app
last claimed it (the symptom that hits second numbers last claimed it (the symptom that hits second numbers
under a shared WABA).{' '} under a shared WABA).{' '}
<strong className="text-slate-300">Meta test numbers</strong> have no <strong className="text-muted-foreground">Meta test numbers</strong> have no
PIN and are pre-registered — leave this blank for them. PIN and are pre-registered — leave this blank for them.
Leaving it blank also keeps an existing registration Leaving it blank also keeps an existing registration
untouched. untouched.
@ -648,27 +648,27 @@ export function WhatsAppConfig() {
</Card> </Card>
{/* Webhook URL */} {/* Webhook URL */}
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent"> <Card className="bg-card border-border ring-0 ring-transparent">
<CardHeader> <CardHeader>
<CardTitle className="text-white">Webhook Configuration</CardTitle> <CardTitle className="text-foreground">Webhook Configuration</CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
Use this URL as your webhook callback in the Meta App Dashboard. Use this URL as your webhook callback in the Meta App Dashboard.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-300">Webhook Callback URL</Label> <Label className="text-muted-foreground">Webhook Callback URL</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
readOnly readOnly
value={webhookUrl} value={webhookUrl}
className="bg-slate-800 border-slate-700 text-slate-300 font-mono text-sm" className="bg-muted border-border text-muted-foreground font-mono text-sm"
/> />
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
onClick={handleCopyWebhookUrl} onClick={handleCopyWebhookUrl}
className="shrink-0 border-slate-700 text-slate-300 hover:text-white hover:bg-slate-800" className="shrink-0 border-border text-muted-foreground hover:text-foreground hover:bg-muted"
> >
<Copy className="size-4" /> <Copy className="size-4" />
</Button> </Button>
@ -697,7 +697,7 @@ export function WhatsAppConfig() {
variant="outline" variant="outline"
onClick={handleTestConnection} onClick={handleTestConnection}
disabled={testing || !config} disabled={testing || !config}
className="border-slate-700 text-slate-300 hover:text-white hover:bg-slate-800" className="border-border text-muted-foreground hover:text-foreground hover:bg-muted"
> >
{testing ? ( {testing ? (
<> <>
@ -736,23 +736,23 @@ export function WhatsAppConfig() {
{/* Setup Instructions Sidebar */} {/* Setup Instructions Sidebar */}
<div> <div>
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent"> <Card className="bg-card border-border ring-0 ring-transparent">
<CardHeader> <CardHeader>
<CardTitle className="text-white text-base">Setup Instructions</CardTitle> <CardTitle className="text-foreground text-base">Setup Instructions</CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-muted-foreground">
Follow these steps to connect your WhatsApp Business API. Follow these steps to connect your WhatsApp Business API.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Accordion> <Accordion>
<AccordionItem className="border-slate-700"> <AccordionItem className="border-border">
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline"> <AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">1</span> <span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">1</span>
Create a Meta App Create a Meta App
</span> </span>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="text-slate-400"> <AccordionContent className="text-muted-foreground">
<ol className="list-decimal list-inside space-y-1 text-sm"> <ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to <span className="text-primary">developers.facebook.com</span></li> <li>Go to <span className="text-primary">developers.facebook.com</span></li>
<li>Click &quot;My Apps&quot; and then &quot;Create App&quot;</li> <li>Click &quot;My Apps&quot; and then &quot;Create App&quot;</li>
@ -762,14 +762,14 @@ export function WhatsAppConfig() {
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
<AccordionItem className="border-slate-700"> <AccordionItem className="border-border">
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline"> <AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">2</span> <span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">2</span>
Add WhatsApp Product Add WhatsApp Product
</span> </span>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="text-slate-400"> <AccordionContent className="text-muted-foreground">
<ol className="list-decimal list-inside space-y-1 text-sm"> <ol className="list-decimal list-inside space-y-1 text-sm">
<li>In your app dashboard, click &quot;Add Product&quot;</li> <li>In your app dashboard, click &quot;Add Product&quot;</li>
<li>Find &quot;WhatsApp&quot; and click &quot;Set Up&quot;</li> <li>Find &quot;WhatsApp&quot; and click &quot;Set Up&quot;</li>
@ -778,43 +778,43 @@ export function WhatsAppConfig() {
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
<AccordionItem className="border-slate-700"> <AccordionItem className="border-border">
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline"> <AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">3</span> <span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">3</span>
Get API Credentials Get API Credentials
</span> </span>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="text-slate-400"> <AccordionContent className="text-muted-foreground">
<ol className="list-decimal list-inside space-y-1 text-sm"> <ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to WhatsApp &gt; API Setup</li> <li>Go to WhatsApp &gt; API Setup</li>
<li>Copy your <strong className="text-slate-200">Phone Number ID</strong></li> <li>Copy your <strong className="text-foreground">Phone Number ID</strong></li>
<li>Copy your <strong className="text-slate-200">WhatsApp Business Account ID</strong></li> <li>Copy your <strong className="text-foreground">WhatsApp Business Account ID</strong></li>
<li>Generate a <strong className="text-slate-200">Permanent Access Token</strong> from Business Settings &gt; System Users</li> <li>Generate a <strong className="text-foreground">Permanent Access Token</strong> from Business Settings &gt; System Users</li>
</ol> </ol>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
<AccordionItem className="border-slate-700"> <AccordionItem className="border-border">
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline"> <AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">4</span> <span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">4</span>
Configure Webhooks Configure Webhooks
</span> </span>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="text-slate-400"> <AccordionContent className="text-muted-foreground">
<ol className="list-decimal list-inside space-y-1 text-sm"> <ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to WhatsApp &gt; Configuration</li> <li>Go to WhatsApp &gt; Configuration</li>
<li>Click &quot;Edit&quot; on the Webhook section</li> <li>Click &quot;Edit&quot; on the Webhook section</li>
<li>Paste the <strong className="text-slate-200">Webhook Callback URL</strong> from above</li> <li>Paste the <strong className="text-foreground">Webhook Callback URL</strong> from above</li>
<li>Enter the same <strong className="text-slate-200">Verify Token</strong> you set here</li> <li>Enter the same <strong className="text-foreground">Verify Token</strong> you set here</li>
<li>Subscribe to &quot;messages&quot; webhook field</li> <li>Subscribe to &quot;messages&quot; webhook field</li>
</ol> </ol>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
<div className="mt-4 pt-4 border-t border-slate-700"> <div className="mt-4 pt-4 border-t border-border">
<a <a
href="https://developers.facebook.com/docs/whatsapp/cloud-api/get-started" href="https://developers.facebook.com/docs/whatsapp/cloud-api/get-started"
target="_blank" target="_blank"

View File

@ -0,0 +1,30 @@
"use client";
import { Toaster } from "sonner";
import { useTheme } from "@/hooks/use-theme";
/**
* Toaster wrapper that tracks the active light/dark mode.
*
* Lives inside <ThemeProvider> (see layout.tsx) so it can read the
* current mode and hand it to sonner. Colors are driven off the same
* CSS tokens as the rest of the app, so a toast looks at home in
* either mode without a second palette to maintain.
*/
export function ThemedToaster() {
const { mode } = useTheme();
return (
<Toaster
theme={mode}
position="top-right"
toastOptions={{
style: {
background: "var(--popover)",
border: "1px solid var(--border)",
color: "var(--popover-foreground)",
},
}}
/>
);
}

View File

@ -128,7 +128,7 @@ const LegendItem = ({
className={cx( className={cx(
"group inline-flex flex-nowrap items-center gap-1.5 rounded-sm px-2 py-1 whitespace-nowrap transition", "group inline-flex flex-nowrap items-center gap-1.5 rounded-sm px-2 py-1 whitespace-nowrap transition",
hasOnValueChange hasOnValueChange
? "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800" ? "cursor-pointer hover:bg-muted"
: "cursor-default", : "cursor-default",
)} )}
onClick={(e) => { onClick={(e) => {
@ -147,9 +147,9 @@ const LegendItem = ({
<p <p
className={cx( className={cx(
"truncate text-xs whitespace-nowrap", "truncate text-xs whitespace-nowrap",
"text-gray-700 dark:text-gray-300", "text-muted-foreground",
hasOnValueChange && hasOnValueChange &&
"group-hover:text-gray-900 dark:group-hover:text-gray-50", "group-hover:text-foreground",
activeLegend && activeLegend !== name ? "opacity-40" : "opacity-100", activeLegend && activeLegend !== name ? "opacity-40" : "opacity-100",
)} )}
> >
@ -194,8 +194,8 @@ const ScrollButton = ({ icon, onClick, disabled }: ScrollButtonProps) => {
className={cx( className={cx(
"group inline-flex size-5 items-center truncate rounded-sm transition", "group inline-flex size-5 items-center truncate rounded-sm transition",
disabled disabled
? "cursor-not-allowed text-gray-400 dark:text-gray-600" ? "cursor-not-allowed text-muted-foreground"
: "cursor-pointer text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-50", : "cursor-pointer text-muted-foreground hover:bg-muted hover:text-foreground",
)} )}
disabled={disabled} disabled={disabled}
onClick={(e) => { onClick={(e) => {
@ -358,7 +358,7 @@ const Legend = React.forwardRef<HTMLOListElement, LegendProps>((props, ref) => {
ref={scrollButtonsRef} ref={scrollButtonsRef}
className={cx( className={cx(
"absolute top-0 right-0 bottom-0 flex h-full items-center justify-center pr-1", "absolute top-0 right-0 bottom-0 flex h-full items-center justify-center pr-1",
"bg-white dark:bg-gray-950", "bg-background",
)} )}
> >
<ScrollButton <ScrollButton
@ -470,13 +470,13 @@ const ChartTooltip = ({
<div <div
className={cx( className={cx(
"rounded-md border text-sm shadow-md", "rounded-md border text-sm shadow-md",
"border-gray-200 dark:border-gray-800", "border-border",
"bg-white dark:bg-gray-950", "bg-popover",
)} )}
> >
<div className={cx("border-b border-inherit px-4 py-2")}> <div className={cx("border-b border-inherit px-4 py-2")}>
<p <p
className={cx("font-medium", "text-gray-900 dark:text-gray-50")} className={cx("font-medium", "text-popover-foreground")}
> >
{label} {label}
</p> </p>
@ -498,7 +498,7 @@ const ChartTooltip = ({
<p <p
className={cx( className={cx(
"text-right whitespace-nowrap", "text-right whitespace-nowrap",
"text-gray-700 dark:text-gray-300", "text-muted-foreground",
)} )}
> >
{category} {category}
@ -507,7 +507,7 @@ const ChartTooltip = ({
<p <p
className={cx( className={cx(
"text-right font-medium whitespace-nowrap tabular-nums", "text-right font-medium whitespace-nowrap tabular-nums",
"text-gray-900 dark:text-gray-50", "text-popover-foreground",
)} )}
> >
{valueFormatter(value)} {valueFormatter(value)}
@ -685,7 +685,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
> >
{showGridLines ? ( {showGridLines ? (
<CartesianGrid <CartesianGrid
className={cx("stroke-gray-200 stroke-1 dark:stroke-gray-800")} className={cx("stroke-border stroke-1")}
horizontal={layout !== "vertical"} horizontal={layout !== "vertical"}
vertical={layout === "vertical"} vertical={layout === "vertical"}
/> />
@ -700,7 +700,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
stroke="" stroke=""
className={cx( className={cx(
"text-xs", "text-xs",
"fill-gray-500 dark:fill-gray-500", "fill-muted-foreground",
{ "mt-4": layout !== "vertical" }, { "mt-4": layout !== "vertical" },
)} )}
tickLine={false} tickLine={false}
@ -730,7 +730,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
<Label <Label
position="insideBottom" position="insideBottom"
offset={-20} offset={-20}
className="fill-gray-800 text-sm font-medium dark:fill-gray-200" className="fill-foreground text-sm font-medium"
> >
{xAxisLabel} {xAxisLabel}
</Label> </Label>
@ -745,7 +745,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
stroke="" stroke=""
className={cx( className={cx(
"text-xs", "text-xs",
"fill-gray-500 dark:fill-gray-500", "fill-muted-foreground",
)} )}
tick={{ tick={{
transform: transform:
@ -776,7 +776,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
style={{ textAnchor: "middle" }} style={{ textAnchor: "middle" }}
angle={-90} angle={-90}
offset={-15} offset={-15}
className="fill-gray-800 text-sm font-medium dark:fill-gray-200" className="fill-foreground text-sm font-medium"
> >
{yAxisLabel} {yAxisLabel}
</Label> </Label>

View File

@ -39,10 +39,10 @@ export const chartColors = {
text: "text-amber-500", text: "text-amber-500",
}, },
gray: { gray: {
bg: "bg-gray-500", bg: "bg-muted",
stroke: "stroke-gray-500", stroke: "stroke-muted-foreground",
fill: "fill-gray-500", fill: "fill-muted-foreground",
text: "text-gray-500", text: "text-muted-foreground",
}, },
cyan: { cyan: {
bg: "bg-cyan-500", bg: "bg-cyan-500",
@ -96,10 +96,10 @@ export const getColorClassName = (
type: ColorUtility, type: ColorUtility,
): string => { ): string => {
const fallbackColor = { const fallbackColor = {
bg: "bg-gray-500", bg: "bg-muted",
stroke: "stroke-gray-500", stroke: "stroke-muted-foreground",
fill: "fill-gray-500", fill: "fill-muted-foreground",
text: "text-gray-500", text: "text-muted-foreground",
} }
return chartColors[color]?.[type] ?? fallbackColor[type] return chartColors[color]?.[type] ?? fallbackColor[type]
} }

View File

@ -31,7 +31,7 @@ function DialogOverlay({
<DialogPrimitive.Backdrop <DialogPrimitive.Backdrop
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0", "fixed inset-0 isolate z-50 bg-background/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className className
)} )}
{...props} {...props}

View File

@ -28,7 +28,7 @@ function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
<SheetPrimitive.Backdrop <SheetPrimitive.Backdrop
data-slot="sheet-overlay" data-slot="sheet-overlay"
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs", "fixed inset-0 z-50 bg-background/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
className className
)} )}
{...props} {...props}

View File

@ -16,9 +16,9 @@ function Switch({
data-slot="switch" data-slot="switch"
className={cn( className={cn(
"inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors", "inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"disabled:cursor-not-allowed disabled:opacity-50", "disabled:cursor-not-allowed disabled:opacity-50",
"data-[checked]:bg-primary data-[unchecked]:bg-slate-700", "data-[checked]:bg-primary data-[unchecked]:bg-muted",
className, className,
)} )}
{...props} {...props}
@ -26,7 +26,7 @@ function Switch({
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot="switch-thumb"
className={cn( className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm ring-0 transition-transform", "pointer-events-none block h-4 w-4 rounded-full bg-card shadow-sm ring-0 transition-transform",
"data-[checked]:translate-x-4 data-[unchecked]:translate-x-0", "data-[checked]:translate-x-4 data-[unchecked]:translate-x-0",
)} )}
/> />

View File

@ -10,20 +10,26 @@ import {
} from "react"; } from "react";
import { import {
DEFAULT_MODE,
DEFAULT_THEME, DEFAULT_THEME,
MODE_STORAGE_KEY,
STORAGE_KEY, STORAGE_KEY,
isMode,
isThemeId, isThemeId,
type Mode,
type ThemeId, type ThemeId,
} from "@/lib/themes"; } from "@/lib/themes";
/** /**
* ThemeProvider — wraps the whole app, owns the active theme state. * ThemeProvider — wraps the whole app, owns the two theming axes:
* • `theme` — the accent color (`data-theme` on <html>)
* • `mode` — light / dark (`data-mode` on <html>)
* The two are independent, so any accent renders in either mode.
* *
* The boot script in `src/app/layout.tsx` has already applied * The boot script in `src/app/layout.tsx` has already applied both
* `document.documentElement.dataset.theme` before React hydrates, so * `data-theme` and `data-mode` before React hydrates, so by the time
* by the time this Provider mounts the page is already painted in * this Provider mounts the page is already painted correctly. We just
* the right colors. We just have to read what's there and keep it * read what's there and keep it in sync going forward.
* in sync going forward.
* *
* Persistence is localStorage only (device-scoped). A future * Persistence is localStorage only (device-scoped). A future
* follow-up could mirror to `profiles.preferences` for cross-device * follow-up could mirror to `profiles.preferences` for cross-device
@ -34,6 +40,9 @@ import {
interface ThemeContextValue { interface ThemeContextValue {
theme: ThemeId; theme: ThemeId;
setTheme: (next: ThemeId) => void; setTheme: (next: ThemeId) => void;
mode: Mode;
setMode: (next: Mode) => void;
toggleMode: () => void;
} }
const ThemeContext = createContext<ThemeContextValue | null>(null); const ThemeContext = createContext<ThemeContextValue | null>(null);
@ -54,8 +63,22 @@ function readInitialTheme(): ThemeId {
return DEFAULT_THEME; return DEFAULT_THEME;
} }
function readInitialMode(): Mode {
if (typeof window === "undefined") return DEFAULT_MODE;
const fromAttr = document.documentElement.dataset.mode;
if (isMode(fromAttr)) return fromAttr;
try {
const stored = localStorage.getItem(MODE_STORAGE_KEY);
if (isMode(stored)) return stored;
} catch {
// localStorage can throw in private-browsing / sandboxed contexts.
}
return DEFAULT_MODE;
}
export function ThemeProvider({ children }: { children: ReactNode }) { export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<ThemeId>(readInitialTheme); const [theme, setThemeState] = useState<ThemeId>(readInitialTheme);
const [mode, setModeState] = useState<Mode>(readInitialMode);
const setTheme = useCallback((next: ThemeId) => { const setTheme = useCallback((next: ThemeId) => {
setThemeState(next); setThemeState(next);
@ -70,22 +93,46 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
} }
}, []); }, []);
// Sync from other tabs — if you change your theme in tab A, tab B const setMode = useCallback((next: Mode) => {
setModeState(next);
if (typeof document !== "undefined") {
document.documentElement.dataset.mode = next;
}
try {
localStorage.setItem(MODE_STORAGE_KEY, next);
} catch {
// Same private-browsing edge case as above.
}
}, []);
const toggleMode = useCallback(() => {
setMode(mode === "dark" ? "light" : "dark");
}, [mode, setMode]);
// Sync from other tabs — change theme or mode in tab A, tab B
// catches up without a refresh. // catches up without a refresh.
useEffect(() => { useEffect(() => {
function onStorage(e: StorageEvent) { function onStorage(e: StorageEvent) {
if (e.key !== STORAGE_KEY) return; if (e.key === STORAGE_KEY) {
if (isThemeId(e.newValue) && e.newValue !== theme) { if (isThemeId(e.newValue) && e.newValue !== theme) {
setThemeState(e.newValue); setThemeState(e.newValue);
document.documentElement.dataset.theme = e.newValue; document.documentElement.dataset.theme = e.newValue;
}
return;
}
if (e.key === MODE_STORAGE_KEY) {
if (isMode(e.newValue) && e.newValue !== mode) {
setModeState(e.newValue);
document.documentElement.dataset.mode = e.newValue;
}
} }
} }
window.addEventListener("storage", onStorage); window.addEventListener("storage", onStorage);
return () => window.removeEventListener("storage", onStorage); return () => window.removeEventListener("storage", onStorage);
}, [theme]); }, [theme, mode]);
return ( return (
<ThemeContext.Provider value={{ theme, setTheme }}> <ThemeContext.Provider value={{ theme, setTheme, mode, setMode, toggleMode }}>
{children} {children}
</ThemeContext.Provider> </ThemeContext.Provider>
); );
@ -94,12 +141,15 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
export function useTheme(): ThemeContextValue { export function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext); const ctx = useContext(ThemeContext);
if (!ctx) { if (!ctx) {
// Fallback for components rendered outside the provider — return a // Fallback for components rendered outside the provider — return
// no-op setter so callers don't crash. The boot script still // no-op setters so callers don't crash. The boot script still
// applied the right CSS attribute, so visually the page is fine. // applied the right CSS attributes, so visually the page is fine.
return { return {
theme: DEFAULT_THEME, theme: DEFAULT_THEME,
setTheme: () => {}, setTheme: () => {},
mode: DEFAULT_MODE,
setMode: () => {},
toggleMode: () => {},
}; };
} }
return ctx; return ctx;

View File

@ -33,7 +33,7 @@ export const TRIGGER_META: Record<AutomationTriggerType, TriggerMeta> = {
}, },
time_based: { time_based: {
label: 'Time-Based', label: 'Time-Based',
pillClass: 'border-slate-500/30 bg-slate-500/10 text-slate-300', pillClass: 'border-border bg-muted text-muted-foreground',
}, },
} }
@ -41,7 +41,7 @@ export function triggerMeta(t: AutomationTriggerType | string): TriggerMeta {
return ( return (
TRIGGER_META[t as AutomationTriggerType] ?? { TRIGGER_META[t as AutomationTriggerType] ?? {
label: t, label: t,
pillClass: 'border-slate-500/30 bg-slate-500/10 text-slate-300', pillClass: 'border-border bg-muted text-muted-foreground',
} }
) )
} }

View File

@ -23,7 +23,7 @@ export interface StatusDisplay {
export const broadcastStatusConfig: Record<BroadcastStatus, StatusDisplay> = { export const broadcastStatusConfig: Record<BroadcastStatus, StatusDisplay> = {
draft: { draft: {
label: "Draft", label: "Draft",
classes: "bg-slate-500/10 text-slate-400 border-slate-500/20", classes: "bg-muted text-muted-foreground border-border",
}, },
scheduled: { scheduled: {
label: "Scheduled", label: "Scheduled",
@ -47,7 +47,7 @@ export const broadcastStatusConfig: Record<BroadcastStatus, StatusDisplay> = {
export const recipientStatusConfig: Record<RecipientStatus, StatusDisplay> = { export const recipientStatusConfig: Record<RecipientStatus, StatusDisplay> = {
pending: { pending: {
label: "Pending", label: "Pending",
classes: "bg-slate-500/10 text-slate-400 border-slate-500/20", classes: "bg-muted text-muted-foreground border-border",
}, },
sent: { sent: {
label: "Sent", label: "Sent",

View File

@ -20,7 +20,7 @@ export const templateStatusConfig: Record<
> = { > = {
DRAFT: { DRAFT: {
label: 'Draft', label: 'Draft',
classes: 'bg-slate-600/20 text-slate-400 border-slate-600/30', classes: 'bg-muted text-muted-foreground border-border',
}, },
PENDING: { PENDING: {
label: 'Pending', label: 'Pending',
@ -48,6 +48,6 @@ export const templateStatusConfig: Record<
}, },
PENDING_DELETION: { PENDING_DELETION: {
label: 'Pending Deletion', label: 'Pending Deletion',
classes: 'bg-slate-700/30 text-slate-500 border-slate-700/40', classes: 'bg-muted text-muted-foreground border-border',
}, },
}; };

View File

@ -27,6 +27,32 @@ export const DEFAULT_THEME: ThemeId = "violet";
export const STORAGE_KEY = "wacrm.theme"; export const STORAGE_KEY = "wacrm.theme";
/**
* MODE — the light/dark dimension, orthogonal to the accent theme.
*
* The CSS variables live in `src/app/globals.css` under
* `html[data-mode="..."]` blocks (neutral surfaces only). Applied
* at runtime via `document.documentElement.dataset.mode`. Dark is
* the historical default and stays the app's identity; light is the
* opt-in eye-strain-friendly alternative.
*
* Persisted under its own localStorage key so it composes freely
* with the accent choice (you can run Violet-light or Violet-dark).
*/
export const MODES = ["light", "dark"] as const;
export type Mode = (typeof MODES)[number];
export const DEFAULT_MODE: Mode = "dark";
export const MODE_STORAGE_KEY = "wacrm.mode";
export function isMode(value: unknown): value is Mode {
return (
typeof value === "string" && (MODES as ReadonlyArray<string>).includes(value)
);
}
export interface ThemeMeta { export interface ThemeMeta {
id: ThemeId; id: ThemeId;
name: string; name: string;