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) {
return (
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center">
<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" />
</div>
<CardTitle className="text-xl text-white">
<CardTitle className="text-xl text-foreground">
Check your email
</CardTitle>
<CardDescription className="text-slate-400">
<CardDescription className="text-muted-foreground">
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.
</CardDescription>
</CardHeader>
@ -62,7 +62,7 @@ export default function ForgotPasswordPage() {
<Link href="/login">
<Button
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
</Button>
@ -74,14 +74,14 @@ export default function ForgotPasswordPage() {
}
return (
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center">
<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" />
</div>
<CardTitle className="text-xl text-white">Reset password</CardTitle>
<CardDescription className="text-slate-400">
<CardTitle className="text-xl text-foreground">Reset password</CardTitle>
<CardDescription className="text-muted-foreground">
Enter your email and we&apos;ll send you a reset link
</CardDescription>
</CardHeader>
@ -94,7 +94,7 @@ export default function ForgotPasswordPage() {
)}
<div className="flex flex-col gap-2">
<Label htmlFor="email" className="text-slate-300">
<Label htmlFor="email" className="text-muted-foreground">
Email
</Label>
<Input
@ -104,7 +104,7 @@ export default function ForgotPasswordPage() {
value={email}
onChange={(e) => setEmail(e.target.value)}
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>
@ -119,7 +119,7 @@ export default function ForgotPasswordPage() {
<Link
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" />
Back to sign in

View File

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

View File

@ -91,18 +91,18 @@ function SignupPageInner() {
if (success) {
return (
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center">
<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" />
</div>
<CardTitle className="text-xl text-white">
<CardTitle className="text-xl text-foreground">
Check your email
</CardTitle>
<CardDescription className="text-slate-400">
<CardDescription className="text-muted-foreground">
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.
</CardDescription>
</CardHeader>
@ -116,7 +116,7 @@ function SignupPageInner() {
>
<Button
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
</Button>
@ -128,8 +128,8 @@ function SignupPageInner() {
}
return (
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<Card className="w-full max-w-md border-border bg-card">
<CardHeader className="items-center text-center">
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
{inviteToken ? (
@ -138,10 +138,10 @@ function SignupPageInner() {
<MessageSquare className="h-6 w-6 text-primary" />
)}
</div>
<CardTitle className="text-xl text-white">
<CardTitle className="text-xl text-foreground">
{inviteToken ? "Create account & join" : "Create account"}
</CardTitle>
<CardDescription className="text-slate-400">
<CardDescription className="text-muted-foreground">
{inviteToken
? "Verify your email, then accept the invitation to join your team."
: "Get started with CRM Template for WhatsApp"}
@ -156,7 +156,7 @@ function SignupPageInner() {
)}
<div className="flex flex-col gap-2">
<Label htmlFor="fullName" className="text-slate-300">
<Label htmlFor="fullName" className="text-muted-foreground">
Full name
</Label>
<Input
@ -166,12 +166,12 @@ function SignupPageInner() {
value={fullName}
onChange={(e) => setFullName(e.target.value)}
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 className="flex flex-col gap-2">
<Label htmlFor="email" className="text-slate-300">
<Label htmlFor="email" className="text-muted-foreground">
Email
</Label>
<Input
@ -181,12 +181,12 @@ function SignupPageInner() {
value={email}
onChange={(e) => setEmail(e.target.value)}
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 className="flex flex-col gap-2">
<Label htmlFor="password" className="text-slate-300">
<Label htmlFor="password" className="text-muted-foreground">
Password
</Label>
<Input
@ -196,12 +196,12 @@ function SignupPageInner() {
value={password}
onChange={(e) => setPassword(e.target.value)}
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 className="flex flex-col gap-2">
<Label htmlFor="confirmPassword" className="text-slate-300">
<Label htmlFor="confirmPassword" className="text-muted-foreground">
Confirm password
</Label>
<Input
@ -211,7 +211,7 @@ function SignupPageInner() {
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
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>
@ -224,7 +224,7 @@ function SignupPageInner() {
</Button>
</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?{" "}
<Link
href={

View File

@ -87,21 +87,21 @@ export default function AutomationLogsPage({
<button
type="button"
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"
>
<ArrowLeft className="h-4 w-4" />
</button>
<div>
<h1 className="text-2xl font-bold text-white">{automation.name}</h1>
<p className="mt-0.5 text-sm text-slate-400">Execution logs</p>
<h1 className="text-2xl font-bold text-foreground">{automation.name}</h1>
<p className="mt-0.5 text-sm text-muted-foreground">Execution logs</p>
</div>
</div>
{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">
<p className="text-sm text-white">No executions yet</p>
<p className="mt-1 text-xs text-slate-400">
<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-foreground">No executions yet</p>
<p className="mt-1 text-xs text-muted-foreground">
Trigger this automation to see runs here.
</p>
</div>
@ -112,7 +112,7 @@ export default function AutomationLogsPage({
return (
<li
key={log.id}
className="rounded-xl border border-slate-800 bg-slate-900"
className="rounded-xl border border-border bg-card"
>
<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"
>
{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} />
<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"}
</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.steps_executed?.length === 1 ? "" : "s"}
</div>
</div>
<div className="text-xs text-slate-500">
<div className="text-xs text-muted-foreground">
{formatRelative(log.created_at)}
</div>
</button>
{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 && (
<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}
@ -150,7 +150,7 @@ export default function AutomationLogsPage({
<StepRow key={i} result={r} />
))}
{(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>
</div>
@ -196,9 +196,9 @@ function StepRow({ result }: { result: AutomationLogStepResult }) {
>
{ok ? <Check className="h-3 w-3" /> : <X className="h-3 w-3" />}
</span>
<span className="text-slate-300">{result.step_type}</span>
<span className="text-muted-foreground">{result.step_type}</span>
{result.detail && (
<span className="truncate text-slate-500">— {result.detail}</span>
<span className="truncate text-muted-foreground">— {result.detail}</span>
)}
</li>
)

View File

@ -160,8 +160,8 @@ export default function AutomationsPage() {
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">Automations</h1>
<p className="mt-1 text-sm text-slate-400">
<h1 className="text-2xl font-bold text-foreground">Automations</h1>
<p className="mt-1 text-sm text-muted-foreground">
Build workflows that react to WhatsApp® events automatically.
</p>
</div>
@ -178,7 +178,7 @@ export default function AutomationsPage() {
{showTemplates && (
<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">
{TEMPLATE_ORDER.map((slug) => {
const t = AUTOMATION_TEMPLATES[slug]
@ -187,13 +187,13 @@ export default function AutomationsPage() {
<button
key={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">
<Icon className="h-5 w-5" />
</div>
<div className="text-sm font-semibold text-white">{t.name}</div>
<p className="mt-1 text-xs text-slate-400">{t.description}</p>
<div className="text-sm font-semibold text-foreground">{t.name}</div>
<p className="mt-1 text-xs text-muted-foreground">{t.description}</p>
</button>
)
})}
@ -202,12 +202,12 @@ export default function AutomationsPage() {
)}
{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">
<Zap className="h-6 w-6 text-primary" />
</div>
<p className="mt-3 text-sm font-medium text-white">No automations yet</p>
<p className="mt-1 text-xs text-slate-400">
<p className="mt-3 text-sm font-medium text-foreground">No automations yet</p>
<p className="mt-1 text-xs text-muted-foreground">
Pick a template above or create one from scratch.
</p>
</div>
@ -233,7 +233,7 @@ export default function AutomationsPage() {
<DialogTitle>Delete automation</DialogTitle>
<DialogDescription>
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.
</DialogDescription>
</DialogHeader>
@ -277,7 +277,7 @@ function AutomationCard({
}) {
const meta = triggerMeta(automation.trigger_type)
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 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"
>
<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}
</span>
{automation.is_active && (
@ -303,9 +303,9 @@ function AutomationCard({
)}
</div>
{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
className={cn(
"inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium",
@ -332,7 +332,7 @@ function AutomationCard({
<DropdownMenu>
<DropdownMenuTrigger
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" />
</DropdownMenuTrigger>

View File

@ -50,15 +50,15 @@ interface StatCardProps {
function StatCard({ label, value, total, icon, color }: StatCardProps) {
const pct = total > 0 ? Math.round((value / total) * 100) : 0;
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 h-8 w-8 items-center justify-center rounded-lg ${color}`}>
{icon}
</div>
<span className="text-xs text-slate-500">{pct}%</span>
<span className="text-xs text-muted-foreground">{pct}%</span>
</div>
<p className="mt-3 text-2xl font-bold text-white">{value.toLocaleString()}</p>
<p className="text-xs text-slate-400">{label}</p>
<p className="mt-3 text-2xl font-bold text-foreground">{value.toLocaleString()}</p>
<p className="text-xs text-muted-foreground">{label}</p>
</div>
);
}
@ -77,8 +77,8 @@ interface FunnelStep {
function FunnelChart({ steps }: { steps: FunnelStep[] }) {
const max = Math.max(...steps.map((s) => s.value), 1);
return (
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
<h3 className="mb-4 text-sm font-medium text-white">Funnel</h3>
<div className="rounded-xl border border-border bg-card p-4">
<h3 className="mb-4 text-sm font-medium text-foreground">Funnel</h3>
<div className="space-y-2">
{steps.map((step) => {
const pctOfMax = Math.max(5, Math.round((step.value / max) * 100));
@ -88,17 +88,17 @@ function FunnelChart({ steps }: { steps: FunnelStep[] }) {
: 0;
return (
<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}
</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
className={`h-7 rounded-full ${step.color} transition-[width] duration-500`}
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()}
<span className="ml-2 text-slate-300/80">
<span className="ml-2 text-muted-foreground/80">
({pctOfSent}%)
</span>
</span>
@ -280,20 +280,20 @@ export default function BroadcastDetailPage() {
variant="outline"
size="icon"
onClick={() => router.push('/broadcasts')}
className="border-slate-700"
className="border-border"
>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<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
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${status.classes}`}
>
{status.label}
</span>
</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>-</span>
<span>
@ -315,7 +315,7 @@ export default function BroadcastDetailPage() {
size="sm"
onClick={() => setConfirmDelete(false)}
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
</Button>
@ -354,7 +354,7 @@ export default function BroadcastDetailPage() {
value={broadcast.total_recipients}
total={broadcast.total_recipients}
icon={<Users className="h-4 w-4" />}
color="bg-slate-800 text-slate-300"
color="bg-muted text-muted-foreground"
/>
<StatCard
label="Sent"
@ -396,9 +396,9 @@ export default function BroadcastDetailPage() {
<FunnelChart steps={funnelSteps} />
{/* Recipients Table */}
<div className="rounded-xl border border-slate-800 bg-slate-900">
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-800 px-4 py-3">
<h2 className="text-sm font-medium text-white">
<div className="rounded-xl border border-border bg-card">
<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-foreground">
Recipients ({filteredRecipients.length}
{statusFilter !== 'all' ? ` of ${recipients.length}` : ''})
</h2>
@ -409,7 +409,7 @@ export default function BroadcastDetailPage() {
<Button
variant="outline"
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}
<ChevronDown className="h-3 w-3" />
</DropdownMenuTrigger>
<DropdownMenuContent className="border-slate-700 bg-slate-900">
<DropdownMenuContent className="border-border bg-popover">
<DropdownMenuItem
onClick={() => setStatusFilter('all')}
className={
statusFilter === 'all' ? 'text-primary' : 'text-slate-300'
statusFilter === 'all' ? 'text-primary' : 'text-popover-foreground'
}
>
All statuses
@ -435,7 +435,7 @@ export default function BroadcastDetailPage() {
className={
statusFilter === s
? 'text-primary'
: 'text-slate-300'
: 'text-popover-foreground'
}
>
{getRecipientStatus(s).label}
@ -449,7 +449,7 @@ export default function BroadcastDetailPage() {
size="sm"
onClick={handleExport}
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" />
Export CSV
@ -459,7 +459,7 @@ export default function BroadcastDetailPage() {
{filteredRecipients.length === 0 ? (
<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
? 'No recipients found.'
: 'No recipients match this filter.'}
@ -469,25 +469,25 @@ export default function BroadcastDetailPage() {
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent">
<TableHead className="text-slate-400">Contact</TableHead>
<TableHead className="text-slate-400">Phone</TableHead>
<TableHead className="text-slate-400">Status</TableHead>
<TableHead className="text-slate-400">Sent</TableHead>
<TableHead className="text-slate-400">Delivered</TableHead>
<TableHead className="text-slate-400">Read</TableHead>
<TableHead className="text-slate-400">Error</TableHead>
<TableRow className="border-border hover:bg-transparent">
<TableHead className="text-muted-foreground">Contact</TableHead>
<TableHead className="text-muted-foreground">Phone</TableHead>
<TableHead className="text-muted-foreground">Status</TableHead>
<TableHead className="text-muted-foreground">Sent</TableHead>
<TableHead className="text-muted-foreground">Delivered</TableHead>
<TableHead className="text-muted-foreground">Read</TableHead>
<TableHead className="text-muted-foreground">Error</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRecipients.map((recipient) => {
const rStatus = getRecipientStatus(recipient.status);
return (
<TableRow key={recipient.id} className="border-slate-800">
<TableCell className="font-medium text-white">
<TableRow key={recipient.id} className="border-border">
<TableCell className="font-medium text-foreground">
{recipient.contact?.name ?? 'Unknown'}
</TableCell>
<TableCell className="text-slate-300">
<TableCell className="text-muted-foreground">
{recipient.contact?.phone ?? '-'}
</TableCell>
<TableCell>
@ -497,17 +497,17 @@ export default function BroadcastDetailPage() {
{rStatus.label}
</span>
</TableCell>
<TableCell className="text-slate-400">
<TableCell className="text-muted-foreground">
{recipient.sent_at
? new Date(recipient.sent_at).toLocaleString()
: '-'}
</TableCell>
<TableCell className="text-slate-400">
<TableCell className="text-muted-foreground">
{recipient.delivered_at
? new Date(recipient.delivered_at).toLocaleString()
: '-'}
</TableCell>
<TableCell className="text-slate-400">
<TableCell className="text-muted-foreground">
{recipient.read_at
? 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">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-white">New Broadcast</h1>
<p className="mt-1 text-sm text-slate-400">
<h1 className="text-2xl font-bold text-foreground">New Broadcast</h1>
<p className="mt-1 text-sm text-muted-foreground">
Create and send a broadcast message to your contacts.
</p>
</div>
@ -150,14 +150,14 @@ export default function NewBroadcastPage() {
? 'bg-primary text-primary-foreground'
: isActive
? '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}
</div>
<span
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}
@ -166,7 +166,7 @@ export default function NewBroadcastPage() {
{index < steps.length - 1 && (
<div
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);
return (
<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}%
</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
className={`h-1.5 rounded-full ${color}`}
style={{ width: `${pct}%` }}
@ -155,7 +155,7 @@ export default function BroadcastsPage() {
<div
role="progressbar"
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" />
<style jsx>{`
@ -179,8 +179,8 @@ export default function BroadcastsPage() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">Broadcasts</h1>
<p className="mt-1 text-sm text-slate-400">
<h1 className="text-2xl font-bold text-foreground">Broadcasts</h1>
<p className="mt-1 text-sm text-muted-foreground">
Send bulk messages to your contacts using approved templates.
</p>
</div>
@ -196,10 +196,10 @@ export default function BroadcastsPage() {
</div>
{broadcasts.length === 0 ? (
<div className="flex h-64 flex-col items-center justify-center rounded-xl border border-slate-800 bg-slate-900">
<Radio className="mb-3 h-10 w-10 text-slate-600" />
<p className="text-sm font-medium text-white">No broadcasts yet</p>
<p className="mt-1 text-xs text-slate-400">
<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-muted-foreground" />
<p className="text-sm font-medium text-foreground">No broadcasts yet</p>
<p className="mt-1 text-xs text-muted-foreground">
Create your first broadcast to reach your contacts at scale.
</p>
<GatedButton
@ -213,19 +213,19 @@ export default function BroadcastsPage() {
</GatedButton>
</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>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent">
<TableHead className="text-slate-400">Name</TableHead>
<TableHead className="hidden text-slate-400 md:table-cell">Template</TableHead>
<TableHead className="hidden text-right text-slate-400 sm:table-cell">
<TableRow className="border-border hover:bg-transparent">
<TableHead className="text-muted-foreground">Name</TableHead>
<TableHead className="hidden text-muted-foreground md:table-cell">Template</TableHead>
<TableHead className="hidden text-right text-muted-foreground sm:table-cell">
Recipients
</TableHead>
<TableHead className="hidden text-slate-400 lg:table-cell">Delivery</TableHead>
<TableHead className="hidden text-slate-400 lg:table-cell">Read</TableHead>
<TableHead className="text-slate-400">Status</TableHead>
<TableHead className="hidden text-slate-400 sm:table-cell">Date</TableHead>
<TableHead className="hidden text-muted-foreground lg:table-cell">Delivery</TableHead>
<TableHead className="hidden text-muted-foreground lg:table-cell">Read</TableHead>
<TableHead className="text-muted-foreground">Status</TableHead>
<TableHead className="hidden text-muted-foreground sm:table-cell">Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@ -234,16 +234,16 @@ export default function BroadcastsPage() {
return (
<TableRow
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}`)}
>
<TableCell className="font-medium text-white">
<TableCell className="font-medium text-foreground">
{broadcast.name}
</TableCell>
<TableCell className="hidden text-slate-300 md:table-cell">
<TableCell className="hidden text-muted-foreground md:table-cell">
{broadcast.template_name}
</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}
</TableCell>
<TableCell className="hidden lg:table-cell">
@ -273,7 +273,7 @@ export default function BroadcastsPage() {
{status.label}
</span>
</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()}
</TableCell>
</TableRow>

View File

@ -217,8 +217,8 @@ export default function ContactsPage() {
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-white">Contacts</h1>
<p className="text-sm text-slate-400 mt-1">
<h1 className="text-2xl font-bold text-foreground">Contacts</h1>
<p className="text-sm text-muted-foreground mt-1">
Manage your contact list. {totalCount > 0 && `${totalCount} total contacts.`}
</p>
</div>
@ -227,7 +227,7 @@ export default function ContactsPage() {
<Button
variant="outline"
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" />
Custom fields
@ -238,7 +238,7 @@ export default function ContactsPage() {
canAct={canEdit}
gateReason="add or import contacts"
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" />
Import
@ -257,7 +257,7 @@ export default function ContactsPage() {
{/* Search */}
<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
value={search}
onChange={(e) => {
@ -267,40 +267,40 @@ export default function ContactsPage() {
setPage(0);
}}
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>
{/* Table */}
<div className="rounded-lg border border-slate-800 overflow-hidden">
<div className="rounded-lg border border-border overflow-hidden">
<Table>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent">
<TableHead className="text-slate-400">Name</TableHead>
<TableHead className="text-slate-400">Phone</TableHead>
<TableHead className="text-slate-400 hidden md:table-cell">Email</TableHead>
<TableHead className="text-slate-400 hidden lg:table-cell">Company</TableHead>
<TableHead className="text-slate-400 hidden md:table-cell">Tags</TableHead>
<TableHead className="text-slate-400 hidden lg:table-cell">Created</TableHead>
<TableHead className="text-slate-400 w-12" />
<TableRow className="border-border hover:bg-transparent">
<TableHead className="text-muted-foreground">Name</TableHead>
<TableHead className="text-muted-foreground">Phone</TableHead>
<TableHead className="text-muted-foreground hidden md:table-cell">Email</TableHead>
<TableHead className="text-muted-foreground hidden lg:table-cell">Company</TableHead>
<TableHead className="text-muted-foreground hidden md:table-cell">Tags</TableHead>
<TableHead className="text-muted-foreground hidden lg:table-cell">Created</TableHead>
<TableHead className="text-muted-foreground w-12" />
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow className="border-slate-800">
<TableRow className="border-border">
<TableCell colSpan={7} className="text-center py-12">
<div className="flex flex-col items-center gap-2">
<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>
</TableCell>
</TableRow>
) : contacts.length === 0 ? (
<TableRow className="border-slate-800">
<TableRow className="border-border">
<TableCell colSpan={7} className="text-center py-12">
<div className="flex flex-col items-center gap-2">
<Users className="size-8 text-slate-600" />
<p className="text-sm text-slate-500">
<Users className="size-8 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
{search ? 'No contacts match your search.' : 'No contacts yet.'}
</p>
{!search && (
@ -308,7 +308,7 @@ export default function ContactsPage() {
variant="outline"
size="sm"
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" />
Add your first contact
@ -321,20 +321,20 @@ export default function ContactsPage() {
contacts.map((contact) => (
<TableRow
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)}
>
<TableCell className="text-white font-medium">
{contact.name || <span className="text-slate-500 italic">Unnamed</span>}
<TableCell className="text-foreground font-medium">
{contact.name || <span className="text-muted-foreground italic">Unnamed</span>}
</TableCell>
<TableCell className="text-slate-300 font-mono text-xs">
<TableCell className="text-muted-foreground font-mono text-xs">
{contact.phone}
</TableCell>
<TableCell className="text-slate-400 hidden md:table-cell text-sm">
{contact.email || <span className="text-slate-600">-</span>}
<TableCell className="text-muted-foreground hidden md:table-cell text-sm">
{contact.email || <span className="text-muted-foreground">-</span>}
</TableCell>
<TableCell className="text-slate-400 hidden lg:table-cell text-sm">
{contact.company || <span className="text-slate-600">-</span>}
<TableCell className="text-muted-foreground hidden lg:table-cell text-sm">
{contact.company || <span className="text-muted-foreground">-</span>}
</TableCell>
<TableCell className="hidden md:table-cell">
<div className="flex flex-wrap gap-1">
@ -352,16 +352,16 @@ export default function ContactsPage() {
</span>
))
) : (
<span className="text-slate-600 text-xs">-</span>
<span className="text-muted-foreground text-xs">-</span>
)}
{contact.tags && contact.tags.length > 3 && (
<span className="text-[10px] text-slate-500">
<span className="text-[10px] text-muted-foreground">
+{contact.tags.length - 3}
</span>
)}
</div>
</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', {
month: 'short',
day: 'numeric',
@ -375,7 +375,7 @@ export default function ContactsPage() {
<Button
variant="ghost"
size="icon-sm"
className="text-slate-400 hover:text-white"
className="text-muted-foreground hover:text-foreground"
onClick={(e) => e.stopPropagation()}
/>
}
@ -384,19 +384,19 @@ export default function ContactsPage() {
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="bg-slate-900 border-slate-700"
className="bg-popover border-border"
>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
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" />
Edit
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-slate-700" />
<DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem
variant="destructive"
onClick={(e) => {
@ -420,7 +420,7 @@ export default function ContactsPage() {
{/* Pagination */}
{totalPages > 1 && (
<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{' '}
{totalCount}
</p>
@ -430,11 +430,11 @@ export default function ContactsPage() {
size="icon-sm"
disabled={!hasPrev}
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" />
</Button>
<span className="text-xs text-slate-400 px-2">
<span className="text-xs text-muted-foreground px-2">
Page {page + 1} of {totalPages}
</span>
<Button
@ -442,7 +442,7 @@ export default function ContactsPage() {
size="icon-sm"
disabled={!hasNext}
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" />
</Button>
@ -491,22 +491,22 @@ export default function ContactsPage() {
{/* Delete Confirmation */}
<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>
<DialogTitle className="text-white">Delete Contact</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogTitle className="text-popover-foreground">Delete Contact</DialogTitle>
<DialogDescription className="text-muted-foreground">
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}
</span>
? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter className="bg-slate-900 border-slate-700">
<DialogFooter className="bg-popover border-border">
<Button
variant="outline"
onClick={() => setDeleteConfirmOpen(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800"
className="border-border text-muted-foreground hover:bg-muted"
>
Cancel
</Button>

View File

@ -27,10 +27,10 @@ function DashboardShellInner({ children }: { children: React.ReactNode }) {
if (loading) {
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="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>
);
@ -39,7 +39,7 @@ function DashboardShellInner({ children }: { children: React.ReactNode }) {
if (!user) return null;
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} />
<div className="flex flex-1 flex-col overflow-hidden">
<Header onOpenSidebar={() => setSidebarOpen(true)} />

View File

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

View File

@ -65,14 +65,14 @@ export default function FlowEditorPage() {
if (loading) {
return (
<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>
);
}
if (notFound || !flow) {
return (
<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
type="button"
onClick={() => router.push("/flows")}

View File

@ -68,7 +68,7 @@ const STATUS_META: Record<
},
completed: {
label: "Completed",
classes: "border-slate-700 bg-slate-800 text-slate-300",
classes: "border-border bg-muted text-muted-foreground",
icon: CircleCheck,
},
handed_off: {
@ -78,12 +78,12 @@ const STATUS_META: Record<
},
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,
},
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,
},
failed: {
@ -151,14 +151,14 @@ export default function FlowRunsPage() {
if (loading) {
return (
<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>
);
}
if (notFound || !flow) {
return (
<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
type="button"
onClick={() => router.push("/flows")}
@ -175,19 +175,19 @@ export default function FlowRunsPage() {
<button
type="button"
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" />
{flow.name}
</button>
<h1 className="text-xl font-semibold text-white">Runs</h1>
<p className="mt-1 text-sm text-slate-400">
<h1 className="text-xl font-semibold text-foreground">Runs</h1>
<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
per-step log.
</p>
{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
it appear here.
</div>
@ -229,20 +229,20 @@ function RunCard({
})
: null;
return (
<div className="rounded-lg border border-slate-800 bg-slate-900">
<div className="rounded-lg border border-border bg-card">
<button
type="button"
onClick={onToggle}
className="flex w-full items-center gap-3 px-4 py-3 text-left"
>
{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="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}
</span>
<Badge variant="outline" className={cn("gap-1", meta.classes)}>
@ -250,12 +250,12 @@ function RunCard({
{meta.label}
</Badge>
{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}
</code>
)}
</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>
{run.reprompt_count > 0 && (
<span>· {run.reprompt_count} re-prompts</span>
@ -265,20 +265,20 @@ function RunCard({
</div>
</button>
{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 && (
<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})
</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)}
</pre>
</details>
)}
<div className="flex flex-col gap-1">
{events.length === 0 ? (
<p className="text-xs text-slate-500">
<p className="text-xs text-muted-foreground">
No events recorded for this run.
</p>
) : (
@ -293,33 +293,33 @@ function RunCard({
const EVENT_COLOR: Record<string, string> = {
started: "text-emerald-300",
node_entered: "text-slate-300",
node_entered: "text-muted-foreground",
message_sent: "text-sky-300",
reply_received: "text-primary",
fallback_fired: "text-amber-300",
handoff: "text-amber-300",
timeout: "text-slate-500",
timeout: "text-muted-foreground",
error: "text-red-300",
completed: "text-emerald-300",
};
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 (
<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")}
</span>
<span className={cn("w-32 shrink-0 font-mono text-[10px]", cls)}>
{ev.event_type}
</span>
{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}
</code>
)}
{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)}
</span>
)}

View File

@ -61,9 +61,9 @@ const STATUS_LABELS: 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",
archived: "border-slate-700 bg-slate-800/50 text-slate-500",
archived: "border-border bg-muted/50 text-muted-foreground",
};
interface TemplateSummary {
@ -194,7 +194,7 @@ export default function FlowsPage() {
if (loading) {
return (
<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>
);
}
@ -204,12 +204,12 @@ export default function FlowsPage() {
<header className="flex flex-wrap items-end justify-between gap-3">
<div>
<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">
Beta
</span>
</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
menus, FAQs, and triage before a human steps in.
</p>
@ -247,17 +247,17 @@ export default function FlowsPage() {
`sm:max-w-sm` baked into its default classes. Without the
sm: prefix our override applies at base only and the
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>
<DialogTitle>Create a new flow</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogDescription className="text-muted-foreground">
Start from a template or build from scratch.
</DialogDescription>
</DialogHeader>
{templates.length > 0 && (
<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
</p>
<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"
onClick={() => handleUseTemplate(t.slug)}
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" />
<span className="text-sm font-semibold text-white">
<span className="text-sm font-semibold text-popover-foreground">
{t.name}
</span>
<span className="text-xs leading-relaxed text-slate-400">
<span className="text-xs leading-relaxed text-muted-foreground">
{t.description}
</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"}
</span>
</button>
@ -288,15 +288,15 @@ export default function FlowsPage() {
</div>
)}
<div className="space-y-2 border-t border-slate-800 pt-4">
<p className="text-xs uppercase tracking-wide text-slate-500">
<div className="space-y-2 border-t border-border pt-4">
<p className="text-xs uppercase tracking-wide text-muted-foreground">
Or start blank
</p>
<Input
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="e.g. Welcome menu"
className="bg-slate-800"
className="bg-muted"
onKeyDown={(e) => {
if (e.key === "Enter") handleCreate();
}}
@ -330,14 +330,14 @@ function EmptyState({
canCreate: boolean;
}) {
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 h-14 w-14 items-center justify-center rounded-full bg-slate-800">
<Workflow className="h-6 w-6 text-slate-500" />
<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-muted">
<Workflow className="h-6 w-6 text-muted-foreground" />
</div>
<h2 className="mt-4 text-base font-medium text-white">
<h2 className="mt-4 text-base font-medium text-foreground">
No flows yet
</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
bot. Customers tap buttons; the bot routes them to the right answer (or
the right agent).
@ -372,11 +372,11 @@ function FlowCard({
? Archive
: PauseCircle;
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 min-w-0 items-center gap-2">
<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}
</h3>
</div>
@ -392,18 +392,18 @@ function FlowCard({
</Badge>
</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}
</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">
<MessageSquare className="h-3 w-3" />
{flow.execution_count} {flow.execution_count === 1 ? "run" : "runs"}
</span>
</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}>
<Pencil className="h-3.5 w-3.5" />
Edit

View File

@ -299,12 +299,12 @@ export default function PipelinesPage() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="h-8 w-48 animate-pulse rounded bg-slate-800" />
<div className="h-9 w-28 animate-pulse rounded-lg 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-muted" />
</div>
<div className="flex gap-3">
{[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>
@ -319,20 +319,20 @@ export default function PipelinesPage() {
{/* Pipeline selector dropdown */}
<DropdownMenu>
<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" />
<span className="font-semibold">
{selectedPipeline?.name ?? "Select Pipeline"}
</span>
<ChevronDown className="h-4 w-4 text-slate-400" />
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent
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 && (
<DropdownMenuItem disabled className="text-slate-500">
<DropdownMenuItem disabled className="text-muted-foreground">
No pipelines yet
</DropdownMenuItem>
)}
@ -343,18 +343,18 @@ export default function PipelinesPage() {
className={
p.id === selectedPipelineId
? "text-primary"
: "text-slate-300"
: "text-popover-foreground"
}
>
<GitBranch className="mr-2 h-3.5 w-3.5" />
{p.name}
</DropdownMenuItem>
))}
<DropdownMenuSeparator className="bg-slate-700" />
<DropdownMenuSeparator className="bg-border" />
{selectedPipeline && (
<DropdownMenuItem
onClick={() => setSettingsOpen(true)}
className="text-slate-300"
className="text-popover-foreground"
>
<Settings className="mr-2 h-3.5 w-3.5" />
Manage Pipelines
@ -370,7 +370,7 @@ export default function PipelinesPage() {
canAct={canEditSettings}
gateReason="create pipelines"
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" />
Add Pipeline
@ -390,12 +390,12 @@ export default function PipelinesPage() {
{/* Board */}
{pipelines.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-slate-700 py-20">
<GitBranch className="h-12 w-12 text-slate-600" />
<h3 className="mt-4 text-lg font-medium text-white">
<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-muted-foreground" />
<h3 className="mt-4 text-lg font-medium text-foreground">
No pipelines yet
</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
</p>
<GatedButton
@ -423,30 +423,30 @@ export default function PipelinesPage() {
{/* New Pipeline Dialog */}
<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>
<DialogTitle className="text-white">New Pipeline</DialogTitle>
<DialogTitle className="text-popover-foreground">New Pipeline</DialogTitle>
</DialogHeader>
<div className="py-2">
<Label className="text-slate-300">Pipeline Name</Label>
<Label className="text-muted-foreground">Pipeline Name</Label>
<Input
value={newPipelineName}
onChange={(e) => setNewPipelineName(e.target.value)}
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) => {
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.
</p>
</div>
<DialogFooter className="bg-slate-900/50 border-slate-700">
<DialogFooter className="bg-popover/50 border-border">
<Button
variant="outline"
onClick={() => setNewPipelineOpen(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800"
className="border-border text-muted-foreground hover:bg-muted"
>
Cancel
</Button>

View File

@ -69,39 +69,39 @@ export default function SettingsPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-white">Settings</h1>
<p className="mt-1 text-sm text-slate-400">
<h1 className="text-2xl font-bold text-foreground">Settings</h1>
<p className="mt-1 text-sm text-muted-foreground">
Manage your profile, WhatsApp® integration, message templates, and
tags.
</p>
</div>
<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
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" />
Profile
</TabsTrigger>
<TabsTrigger
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" />
WhatsApp Config
</TabsTrigger>
<TabsTrigger
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" />
Templates
</TabsTrigger>
<TabsTrigger
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" />
Tags
@ -109,7 +109,7 @@ export default function SettingsPage() {
{canEditSettings && (
<TabsTrigger
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" />
Custom Fields
@ -117,21 +117,21 @@ export default function SettingsPage() {
)}
<TabsTrigger
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" />
Deals
</TabsTrigger>
<TabsTrigger
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" />
Appearance
</TabsTrigger>
<TabsTrigger
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" />
Members

View File

@ -49,39 +49,42 @@
}
/* ============================================================
* COLOR THEMES
* THEMING — two orthogonal dimensions
*
* 5 dark themes sharing structure, swapping only the primary
* accent (and a tiny hue rotation on the slate base). Switched
* at runtime by setting `document.documentElement.dataset.theme`
* — see ThemeProvider + the inline boot script in layout.tsx.
* 1. MODE (light / dark) → neutral surfaces. Selected with
* `document.documentElement.dataset.mode`. Defaults to dark.
* 2. ACCENT (violet / …) → the primary color only. Selected
* with `document.documentElement.dataset.theme`.
*
* `:root` mirrors Violet so default visitors render correctly
* before JS runs. `[data-theme="violet"]` is included for the
* explicit-select path (settings UI sets it after a pick).
* The two compose: any accent works in either mode. Neutral
* tokens (background / card / border / muted / sidebar surfaces /
* 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
* new — used for hovered primary buttons and tinted-primary
* surfaces (sidebar active pill, validation badges). Components
* that don't use them remain backward-compatible.
* `:root` carries the dark-mode + violet defaults so a visitor
* renders correctly before JS runs; the boot script in
* layout.tsx replays the saved mode + accent before first paint.
*
* 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
* project/Color Themes.html — change one, change the other.
* ============================================================ */
/* ---- MODE: neutral surfaces ---- */
:root,
html[data-theme="violet"] {
html[data-mode="dark"] {
--background: oklch(0.13 0.01 260);
--foreground: oklch(0.985 0 0);
--card: oklch(0.18 0.01 260);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.18 0.01 260);
--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-foreground: oklch(0.985 0 0);
--muted: oklch(0.22 0.01 260);
@ -91,8 +94,6 @@ html[data-theme="violet"] {
--destructive: oklch(0.577 0.245 27.325);
--border: 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-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
@ -100,159 +101,108 @@ html[data-theme="violet"] {
--radius: 0.625rem;
--sidebar: oklch(0.16 0.01 260);
--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-foreground: oklch(0.985 0 0);
--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);
}
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-foreground: oklch(0.16 0.02 162);
--primary-hover: oklch(0.68 0.15 162);
--primary-soft: oklch(0.62 0.16 162 / 0.12);
--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);
--chart-1: oklch(0.62 0.16 162);
--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-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);
}
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-foreground: oklch(0.985 0 0);
--primary-hover: oklch(0.66 0.18 254);
--primary-soft: oklch(0.585 0.2 254 / 0.12);
--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);
--chart-1: oklch(0.585 0.2 254);
--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-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);
}
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-foreground: oklch(0.18 0.03 65);
--primary-hover: oklch(0.8 0.15 65);
--primary-soft: oklch(0.745 0.16 65 / 0.12);
--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);
--chart-1: oklch(0.745 0.16 65);
--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-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);
}
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-foreground: oklch(0.985 0 0);
--primary-hover: oklch(0.71 0.2 16);
--primary-soft: oklch(0.645 0.22 16 / 0.12);
--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);
--chart-1: oklch(0.645 0.22 16);
--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-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);
}

View File

@ -220,10 +220,10 @@ export default function JoinPage() {
// ----- Loading state (peek pending OR auth not yet resolved) -----
if (peek === null || authedUserId === undefined) {
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">
<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>
</Card>
);
@ -233,13 +233,13 @@ export default function JoinPage() {
if (!peek.ok) {
const copy = FAIL_COPY[peek.reason];
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">
<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" />
</div>
<CardTitle className="text-xl text-white">{copy.title}</CardTitle>
<CardDescription className="text-slate-400">
<CardTitle className="text-xl text-foreground">{copy.title}</CardTitle>
<CardDescription className="text-muted-foreground">
{copy.body}
</CardDescription>
</CardHeader>
@ -262,7 +262,7 @@ export default function JoinPage() {
<Link href="/signup">
<Button
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
</Button>
@ -278,7 +278,7 @@ export default function JoinPage() {
<Link href="/login">
<Button
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
</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">
<UsersRound className="h-6 w-6 text-primary" />
</div>
<CardTitle className="text-xl text-white">
<CardTitle className="text-xl text-foreground">
You&apos;re invited to{' '}
<span className="text-primary">{peek.account_name}</span>
</CardTitle>
<CardDescription className="text-slate-400">
<CardDescription className="text-muted-foreground">
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" />
{ROLE_LABEL[peek.role]}
</span>
@ -321,7 +321,7 @@ export default function JoinPage() {
if (authedUserId) {
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}
<CardContent className="flex flex-col gap-3">
<Button
@ -341,9 +341,9 @@ export default function JoinPage() {
</>
)}
</Button>
<p className="text-center text-xs text-slate-500">
<p className="text-center text-xs text-muted-foreground">
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.
</p>
</CardContent>
@ -359,30 +359,30 @@ export default function JoinPage() {
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>
<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" />
Can&apos;t join {peek.account_name} with this account
</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogDescription className="text-muted-foreground">
{conflictMessage}
</DialogDescription>
</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>
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.
The invite link stays valid as long as it hasn&apos;t
expired.
</p>
</div>
<DialogFooter className="bg-slate-900 border-slate-700">
<DialogFooter className="bg-popover border-border">
<Button
variant="outline"
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
</Button>
@ -409,7 +409,7 @@ export default function JoinPage() {
// ----- Not authed: prompt to sign up or sign in -----
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}
<CardContent className="flex flex-col gap-2">
<Link href={`/signup?invite=${encodeURIComponent(token!)}`}>
@ -420,7 +420,7 @@ export default function JoinPage() {
<Link href={`/login?invite=${encodeURIComponent(token!)}`}>
<Button
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
</Button>

View File

@ -37,7 +37,7 @@ export const metadata: Metadata = {
export default function JoinLayout({ children }: { children: ReactNode }) {
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}
</div>
);

View File

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

View File

@ -95,7 +95,7 @@ const STEP_META: Record<AutomationStepType, StepMeta> = {
assign_conversation: { label: "Assign Conversation", icon: UserCheck, 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" },
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" },
send_webhook: { label: "Send Webhook", icon: Webhook, 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 =
"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
* raw id input when no tags exist yet. */
@ -269,7 +269,7 @@ function TagSelect({
placeholder="Tag id"
value={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 (
<div className="flex items-center gap-2">
<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" }}
aria-hidden
/>
@ -358,7 +358,7 @@ function AgentSelect({
placeholder="Agent id"
value={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({ template_name: e.target.value, language })
}
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
</FieldBlock>
<FieldBlock label="Language">
@ -414,7 +414,7 @@ function SendTemplateFields({
onChange={(e) =>
onChange({ template_name: templateName, language: e.target.value })
}
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
</FieldBlock>
</>
@ -548,15 +548,15 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) {
}
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
switch moves to the right of the save button, so the name input
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
type="button"
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"
>
<ArrowLeft className="h-4 w-4" />
@ -565,9 +565,9 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) {
value={state.name}
onChange={(e) => patchTop("name", e.target.value)}
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>
<Switch
checked={state.is_active}
@ -633,7 +633,7 @@ function TriggerCard({
// Card width: full on mobile, fixed 320px on sm+. The canvas wrapper
// (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="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
type="button"
onClick={() => setOpen((v) => !v)}
@ -644,24 +644,24 @@ function TriggerCard({
</div>
<div className="min-w-0 flex-1">
<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}
</div>
</div>
<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>
{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>
<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
</label>
<select
value={type}
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) => (
<option key={o.value} value={o.value}>
@ -669,7 +669,7 @@ function TriggerCard({
</option>
))}
</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}
</p>
</div>
@ -681,7 +681,7 @@ function TriggerCard({
)}
{type === "tag_added" && (
<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
</label>
<TagSelect
@ -697,7 +697,7 @@ function TriggerCard({
onChange={(e) =>
onConfigChange({ ...config, schedule: e.target.value })
}
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
)}
</div>
@ -735,7 +735,7 @@ function KeywordMatchConfig({
return (
<div className="space-y-2">
<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)
</label>
<Input
@ -749,17 +749,17 @@ function KeywordMatchConfig({
}
}}
placeholder="e.g. pricing, demo request, talk to sales"
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
</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
</label>
<select
value={config?.match_type ?? "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="exact">Exact</option>
@ -858,7 +858,7 @@ function StepRenderer({
<div className={cn("z-10 flex flex-col", width)}>
<div
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,
)}
>
@ -867,28 +867,28 @@ function StepRenderer({
onClick={() => props.setExpandedId(expanded ? null : step.cid)}
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 />
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-slate-800 text-slate-300">
<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-muted text-muted-foreground">
<Icon className="h-4 w-4" />
</div>
<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"}
</div>
<div className="truncate text-sm font-medium text-white">{meta.label}</div>
<div className="truncate text-[11px] text-slate-500">{previewFor(step)}</div>
<div className="truncate text-sm font-medium text-foreground">{meta.label}</div>
<div className="truncate text-[11px] text-muted-foreground">{previewFor(step)}</div>
</div>
<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>
{expanded && (
<div className="border-t border-slate-800 px-4 py-3">
<div className="border-t border-border px-4 py-3">
<StepEditor
step={step}
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">
<Button
variant="ghost"
@ -990,17 +990,17 @@ function BranchColumn({
function AddButton({ onPick }: { onPick: (t: AutomationStepType) => void }) {
return (
<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>
<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"
>
<Plus className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
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) => {
const Icon = STEP_META[t].icon
@ -1013,7 +1013,7 @@ function AddButton({ onPick }: { onPick: (t: AutomationStepType) => void }) {
})}
</DropdownMenuContent>
</DropdownMenu>
<div className="h-4 w-[2px] bg-slate-700" aria-hidden />
<div className="h-4 w-[2px] bg-border" aria-hidden />
</div>
)
}
@ -1041,7 +1041,7 @@ function StepEditor({
value={(cfg.text as string) ?? ""}
onChange={(e) => set({ text: e.target.value })}
placeholder="Hi! Thanks for reaching out…"
className="min-h-24 bg-slate-800 text-white"
className="min-h-24 bg-muted text-foreground"
/>
</FieldBlock>
)
@ -1070,7 +1070,7 @@ function StepEditor({
<select
value={(cfg.mode as string) ?? "round_robin"}
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="specific">Specific agent</option>
@ -1100,7 +1100,7 @@ function StepEditor({
value={(cfg.value as string) ?? ""}
onChange={(e) => set({ value: e.target.value })}
placeholder="Text or {{ vars.x }} / {{ message.text }}"
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
</FieldBlock>
</>
@ -1112,21 +1112,21 @@ function StepEditor({
<Input
value={(cfg.pipeline_id as string) ?? ""}
onChange={(e) => set({ pipeline_id: e.target.value })}
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
</FieldBlock>
<FieldBlock label="Stage id">
<Input
value={(cfg.stage_id as string) ?? ""}
onChange={(e) => set({ stage_id: e.target.value })}
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
</FieldBlock>
<FieldBlock label="Title">
<Input
value={(cfg.title as string) ?? ""}
onChange={(e) => set({ title: e.target.value })}
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
</FieldBlock>
<FieldBlock label="Value">
@ -1134,7 +1134,7 @@ function StepEditor({
type="number"
value={(cfg.value as number) ?? 0}
onChange={(e) => set({ value: Number(e.target.value) })}
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
</FieldBlock>
</>
@ -1148,14 +1148,14 @@ function StepEditor({
min={1}
value={(cfg.amount as number) ?? 1}
onChange={(e) => set({ amount: Math.max(1, Number(e.target.value)) })}
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
</FieldBlock>
<FieldBlock label="Unit">
<select
value={(cfg.unit as string) ?? "hours"}
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="hours">Hours</option>
@ -1171,7 +1171,7 @@ function StepEditor({
<select
value={(cfg.subject as string) ?? "tag_presence"}
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="contact_field">Contact field</option>
@ -1192,7 +1192,7 @@ function StepEditor({
}
value={(cfg.operand as string) ?? ""}
onChange={(e) => set({ operand: e.target.value })}
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
</FieldBlock>
{(cfg.subject === "contact_field" || cfg.subject === "message_content") && (
@ -1200,7 +1200,7 @@ function StepEditor({
<Input
value={(cfg.value as string) ?? ""}
onChange={(e) => set({ value: e.target.value })}
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
</FieldBlock>
)}
@ -1213,21 +1213,21 @@ function StepEditor({
<Input
value={(cfg.url as string) ?? ""}
onChange={(e) => set({ url: e.target.value })}
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
</FieldBlock>
<FieldBlock label="Body template (JSON)">
<Textarea
value={(cfg.body_template as string) ?? ""}
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>
</>
)
case "close_conversation":
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.
</p>
)
@ -1245,7 +1245,7 @@ function FieldBlock({
}) {
return (
<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}
</div>
)

View File

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

View File

@ -249,8 +249,8 @@ export function Step2SelectAudience({
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-white">Select Audience</h2>
<p className="mt-1 text-sm text-slate-400">
<h2 className="text-lg font-semibold text-foreground">Select Audience</h2>
<p className="mt-1 text-sm text-muted-foreground">
Choose who will receive this broadcast.
</p>
</div>
@ -280,21 +280,21 @@ export function Step2SelectAudience({
className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${
isSelected
? '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
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${
isSelected
? 'bg-primary/10 text-primary'
: 'bg-slate-800 text-slate-400'
: 'bg-muted text-muted-foreground'
}`}
>
<Icon className="h-4 w-4" />
</div>
<div>
<p className="text-sm font-medium text-white">{option.label}</p>
<p className="mt-0.5 text-xs text-slate-400">
<p className="text-sm font-medium text-foreground">{option.label}</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{option.description}
</p>
</div>
@ -304,12 +304,12 @@ export function Step2SelectAudience({
</div>
{audience.type === 'tags' && (
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
<p className="mb-3 text-sm font-medium text-white">Select Tags</p>
<div className="rounded-xl border border-border bg-card/50 p-4">
<p className="mb-3 text-sm font-medium text-foreground">Select Tags</p>
{loadingTags ? (
<Loader2 className="h-5 w-5 animate-spin text-primary" />
) : tags.length === 0 ? (
<p className="text-xs text-slate-400">
<p className="text-xs text-muted-foreground">
No tags found. Create tags in Settings.
</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 ${
isSelected
? '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
@ -340,12 +340,12 @@ export function Step2SelectAudience({
)}
{audience.type === 'custom_field' && (
<div className="space-y-3 rounded-xl border border-slate-800 bg-slate-900/50 p-4">
<p className="text-sm font-medium text-white">Custom Field Filter</p>
<div className="space-y-3 rounded-xl border border-border bg-card/50 p-4">
<p className="text-sm font-medium text-foreground">Custom Field Filter</p>
{loadingFields ? (
<Loader2 className="h-5 w-5 animate-spin text-primary" />
) : 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.
</p>
) : (
@ -353,7 +353,7 @@ export function Step2SelectAudience({
<select
value={audience.customField?.fieldId ?? ''}
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>
{customFields.map((f) => (
@ -369,7 +369,7 @@ export function Step2SelectAudience({
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) => (
<option key={op.value} value={op.value}>
@ -382,7 +382,7 @@ export function Step2SelectAudience({
value={audience.customField?.value ?? ''}
onChange={(e) => updateCustomField({ value: e.target.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>
)}
@ -390,16 +390,16 @@ export function Step2SelectAudience({
)}
{/* 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">
<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
</p>
<span className="text-xs text-slate-500">(optional)</span>
<span className="text-xs text-muted-foreground">(optional)</span>
</div>
{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">
{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 ${
isExcluded
? '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
@ -427,33 +427,33 @@ export function Step2SelectAudience({
</div>
{/* Audience Summary */}
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
<p className="mb-2 text-sm font-medium text-white">Audience Summary</p>
<div className="rounded-xl border border-border bg-card/50 p-4">
<p className="mb-2 text-sm font-medium text-foreground">Audience Summary</p>
{loadingCount ? (
<div className="flex items-center gap-2">
<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>
) : estimatedCount !== null ? (
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-primary" />
<span className="text-sm text-white">
<span className="text-sm text-foreground">
{estimatedCount.toLocaleString()}
</span>
<span className="text-xs text-slate-400">estimated recipients</span>
<span className="text-xs text-muted-foreground">estimated recipients</span>
</div>
) : (
<p className="text-xs text-slate-500">
<p className="text-xs text-muted-foreground">
Select an audience type to see the estimate.
</p>
)}
</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"
className="border-border text-muted-foreground"
>
<ArrowLeft className="h-4 w-4" />
Back

View File

@ -187,16 +187,16 @@ export function Step3Personalize({
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-white">Personalize Message</h2>
<p className="mt-1 text-sm text-slate-400">
<h2 className="text-lg font-semibold text-foreground">Personalize Message</h2>
<p className="mt-1 text-sm text-muted-foreground">
Map template variables to contact fields, custom fields, or static
values.
</p>
</div>
{placeholders.length === 0 ? (
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-6 text-center">
<p className="text-sm text-slate-400">
<div className="rounded-xl border border-border bg-card/50 p-6 text-center">
<p className="text-sm text-muted-foreground">
This template has no variables to personalize.
</p>
</div>
@ -209,7 +209,7 @@ export function Step3Personalize({
return (
<div
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">
<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>
<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
</label>
<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 />
</SelectTrigger>
<SelectContent className="border-slate-700 bg-slate-800">
<SelectContent className="border-border bg-popover">
<SelectItem value="static">Static Value</SelectItem>
<SelectItem value="field">Contact Field</SelectItem>
<SelectItem value="custom_field">
@ -245,7 +245,7 @@ export function Step3Personalize({
</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'}
</label>
{mapping.type === 'static' ? (
@ -255,7 +255,7 @@ export function Step3Personalize({
updateVariable(key, { value: e.target.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' ? (
<Select
@ -264,10 +264,10 @@ export function Step3Personalize({
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..." />
</SelectTrigger>
<SelectContent className="border-slate-700 bg-slate-800">
<SelectContent className="border-border bg-popover">
{contactFields.map((field) => (
<SelectItem key={field.value} value={field.value}>
{field.label}
@ -282,7 +282,7 @@ export function Step3Personalize({
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={
loadingFields
@ -293,7 +293,7 @@ export function Step3Personalize({
}
/>
</SelectTrigger>
<SelectContent className="border-slate-700 bg-slate-800">
<SelectContent className="border-border bg-popover">
{customFields.map((f) => (
<SelectItem key={f.id} value={f.id}>
{f.field_name}
@ -312,11 +312,11 @@ export function Step3Personalize({
{/* Live Preview — rendered as a WhatsApp-style bubble so the user
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">
<Eye className="h-4 w-4 text-primary" />
<p className="text-sm font-medium text-white">Live Preview</p>
<span className="text-xs text-slate-500">({previewLabel})</span>
<p className="text-sm font-medium text-foreground">Live Preview</p>
<span className="text-xs text-muted-foreground">({previewLabel})</span>
{loadingPreview && (
<Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
)}
@ -340,11 +340,11 @@ export function Step3Personalize({
</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"
className="border-border text-muted-foreground"
>
<ArrowLeft className="h-4 w-4" />
Back

View File

@ -93,51 +93,51 @@ export function Step4ScheduleSend({
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-white">Review & Send</h2>
<p className="mt-1 text-sm text-slate-400">
<h2 className="text-lg font-semibold text-foreground">Review & Send</h2>
<p className="mt-1 text-sm text-muted-foreground">
Name your broadcast, review the details, and send.
</p>
</div>
{/* Broadcast Name */}
<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
value={name}
onChange={(e) => onNameChange(e.target.value)}
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>
{/* Summary Card */}
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4 space-y-3">
<p className="text-sm font-medium text-white">Summary</p>
<div className="rounded-xl border border-border bg-card/50 p-4 space-y-3">
<p className="text-sm font-medium text-foreground">Summary</p>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<p className="text-xs text-slate-400">Template</p>
<p className="text-white">{template.name}</p>
<p className="text-xs text-muted-foreground">Template</p>
<p className="text-foreground">{template.name}</p>
</div>
<div>
<p className="text-xs text-slate-400">Audience</p>
<p className="text-white">{audienceLabel}</p>
<p className="text-xs text-muted-foreground">Audience</p>
<p className="text-foreground">{audienceLabel}</p>
</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">
{loadingReach ? (
<Loader2 className="h-3 w-3 animate-spin 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>
<p className="text-xs text-slate-400">Language</p>
<p className="text-white">{template.language ?? 'en_US'}</p>
<p className="text-xs text-muted-foreground">Language</p>
<p className="text-foreground">{template.language ?? 'en_US'}</p>
</div>
</div>
</div>
@ -148,11 +148,11 @@ export function Step4ScheduleSend({
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<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>
<span className="text-xs font-medium text-primary">{progress}%</span>
</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
className="h-1.5 rounded-full bg-primary transition-all duration-300"
style={{ width: `${progress}%` }}
@ -161,12 +161,12 @@ export function Step4ScheduleSend({
</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
variant="outline"
onClick={onBack}
disabled={isProcessing}
className="border-slate-700 text-slate-300"
className="border-border text-muted-foreground"
>
<ArrowLeft className="h-4 w-4" />
Back
@ -178,7 +178,7 @@ export function Step4ScheduleSend({
variant="outline"
onClick={onSaveDraft}
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 as Draft
@ -197,14 +197,14 @@ export function Step4ScheduleSend({
<Send className="h-4 w-4" />
Send Broadcast
</DialogTrigger>
<DialogContent className="border-slate-700 bg-slate-900 sm:max-w-md">
<DialogContent className="border-border bg-popover sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-white">Confirm Broadcast</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogTitle className="text-popover-foreground">Confirm Broadcast</DialogTitle>
<DialogDescription className="text-muted-foreground">
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{' '}
<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.
</DialogDescription>
</DialogHeader>
@ -212,7 +212,7 @@ export function Step4ScheduleSend({
<Button
variant="outline"
onClick={() => setShowConfirm(false)}
className="border-slate-700 text-slate-300"
className="border-border text-muted-foreground"
>
Cancel
</Button>

View File

@ -331,7 +331,7 @@ export function ContactDetailView({
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
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 ? (
<div className="flex items-center justify-center h-full">
@ -340,21 +340,21 @@ export function ContactDetailView({
) : (
<div className="flex flex-col h-full">
{/* 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">
<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">
{getInitials(contact.name)}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<SheetTitle className="text-white truncate">
<SheetTitle className="text-popover-foreground truncate">
{contact.name || 'Unknown'}
</SheetTitle>
<SheetDescription className="text-slate-400 text-xs mt-0.5">
<SheetDescription className="text-muted-foreground text-xs mt-0.5">
Contact details
</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
onClick={copyPhone}
className="flex items-center gap-1 hover:text-primary transition-colors cursor-pointer"
@ -386,34 +386,34 @@ export function ContactDetailView({
{/* Tabs */}
<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
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
</TabsTrigger>
<TabsTrigger
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
</TabsTrigger>
<TabsTrigger
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
</TabsTrigger>
<TabsTrigger
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
</TabsTrigger>
<TabsTrigger
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
</TabsTrigger>
@ -423,37 +423,37 @@ export function ContactDetailView({
<TabsContent value="details" className="flex-1 overflow-y-auto px-4 py-3">
<div className="space-y-3">
<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
value={editName}
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 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>
</Label>
<Input
value={editPhone}
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 className="space-y-1.5">
<Label className="text-slate-400 text-xs">Email</Label>
<Label className="text-muted-foreground text-xs">Email</Label>
<Input
value={editEmail}
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 className="space-y-1.5">
<Label className="text-slate-400 text-xs">Company</Label>
<Label className="text-muted-foreground text-xs">Company</Label>
<Input
value={editCompany}
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>
<Button
@ -475,11 +475,11 @@ export function ContactDetailView({
{/* Tags Tab */}
<TabsContent value="tags" className="flex-1 overflow-y-auto px-4 py-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.
</p>
{allTags.length === 0 ? (
<p className="text-sm text-slate-500">
<p className="text-sm text-muted-foreground">
No tags available. Create tags in Settings.
</p>
) : (
@ -493,7 +493,7 @@ export function ContactDetailView({
disabled={savingTags}
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium transition-all cursor-pointer ${
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'
}`}
style={{
@ -518,7 +518,7 @@ export function ContactDetailView({
value={newNote}
onChange={(e) => setNewNote(e.target.value)}
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
onClick={addNote}
@ -538,30 +538,30 @@ export function ContactDetailView({
<div className="flex-1 overflow-y-auto space-y-2">
{loadingNotes ? (
<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>
) : 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.
</p>
) : (
notes.map((note) => (
<div
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">
<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}
</p>
<button
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" />
</button>
</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', {
month: 'short',
day: 'numeric',
@ -580,17 +580,17 @@ export function ContactDetailView({
<TabsContent value="custom" className="flex-1 overflow-y-auto px-4 py-3">
{loadingCustom ? (
<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>
) : 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.
</p>
) : (
<div className="space-y-3">
{customFields.map((field) => (
<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}
</Label>
<Input
@ -602,7 +602,7 @@ export function ContactDetailView({
}))
}
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>
))}
@ -630,16 +630,16 @@ export function ContactDetailView({
<Loader2 className="size-5 animate-spin text-primary" />
</div>
) : 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">
{deals.map((deal) => (
<div
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">
<p className="text-sm font-medium text-white">
<p className="text-sm font-medium text-foreground">
{deal.title}
</p>
{deal.stage && (
@ -654,7 +654,7 @@ export function ContactDetailView({
</span>
)}
</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">
<DollarSign className="size-3" />
{formatCurrency(

View File

@ -223,12 +223,12 @@ export function ContactForm({
return (
<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>
<DialogTitle className="text-white">
<DialogTitle className="text-popover-foreground">
{isEdit ? 'Edit Contact' : 'Add Contact'}
</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogDescription className="text-muted-foreground">
{isEdit
? 'Update the contact details below.'
: 'Fill in the details to create a new contact.'}
@ -237,7 +237,7 @@ export function ContactForm({
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="cf-name" className="text-slate-300">
<Label htmlFor="cf-name" className="text-muted-foreground">
Name
</Label>
<Input
@ -245,12 +245,12 @@ export function ContactForm({
value={name}
onChange={(e) => setName(e.target.value)}
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 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>
</Label>
<Input
@ -262,7 +262,7 @@ export function ContactForm({
}}
onBlur={checkDuplicate}
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 ? (
<div
@ -291,14 +291,14 @@ export function ContactForm({
</div>
</div>
) : (
<p className="text-xs text-slate-500">
<p className="text-xs text-muted-foreground">
Include country code, e.g. +1 for US
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="cf-email" className="text-slate-300">
<Label htmlFor="cf-email" className="text-muted-foreground">
Email
</Label>
<Input
@ -307,12 +307,12 @@ export function ContactForm({
value={email}
onChange={(e) => setEmail(e.target.value)}
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 className="space-y-2">
<Label htmlFor="cf-company" className="text-slate-300">
<Label htmlFor="cf-company" className="text-muted-foreground">
Company
</Label>
<Input
@ -320,19 +320,19 @@ export function ContactForm({
value={company}
onChange={(e) => setCompany(e.target.value)}
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 className="space-y-2">
<Label className="text-slate-300">Tags</Label>
<Label className="text-muted-foreground">Tags</Label>
{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" />
Loading tags...
</div>
) : tags.length === 0 ? (
<p className="text-xs text-slate-500">
<p className="text-xs text-muted-foreground">
No tags available. Create tags in Settings.
</p>
) : (
@ -346,7 +346,7 @@ export function ContactForm({
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 ${
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'
}`}
style={{
@ -363,12 +363,12 @@ export function ContactForm({
)}
</div>
<DialogFooter className="bg-slate-900 border-slate-700">
<DialogFooter className="bg-popover border-border">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800"
className="border-border text-muted-foreground hover:bg-muted"
>
Cancel
</Button>

View File

@ -33,10 +33,10 @@ export function CustomFieldsManager({
}: CustomFieldsManagerProps) {
return (
<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>
<DialogTitle className="text-white">Custom fields</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogTitle className="text-popover-foreground">Custom fields</DialogTitle>
<DialogDescription className="text-muted-foreground">
Define extra contact fields (e.g. ZIP code, lead source). They
appear on every contact and in the “Update Contact Field” automation
action.
@ -185,7 +185,7 @@ export function CustomFieldsPanel() {
}
}}
placeholder="New field name…"
className="bg-slate-800 text-white"
className="bg-muted text-foreground"
/>
<Button
onClick={handleCreate}
@ -202,18 +202,18 @@ export function CustomFieldsPanel() {
</div>
{/* 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 ? (
<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" />
Loading…
</div>
) : 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.
</p>
) : (
<ul className="divide-y divide-slate-800">
<ul className="divide-y divide-border">
{fields.map((field) => (
<FieldRow
key={field.id}
@ -265,7 +265,7 @@ function FieldRow({
if (e.key === 'Enter') e.currentTarget.blur();
}}
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
variant="ghost"
@ -273,7 +273,7 @@ function FieldRow({
disabled={busy}
onClick={() => onDelete(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 ? (
<Loader2 className="size-4 animate-spin" />

View File

@ -80,7 +80,7 @@ function ImportPreviewTags({
tagColorByKey: Map<string, string>;
}) {
if (tagNames.length === 0) {
return <span className="text-slate-600">—</span>;
return <span className="text-muted-foreground">—</span>;
}
return (
@ -398,31 +398,31 @@ export function ImportModal({
return (
<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">
<div className="shrink-0 space-y-4 border-b border-slate-800/80 px-6 pt-6 pb-5">
<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-border/80 px-6 pt-6 pb-5">
<DialogHeader className="gap-1.5">
<DialogTitle className="text-lg text-white">
<DialogTitle className="text-lg text-popover-foreground">
Import Contacts
</DialogTitle>
<DialogDescription className="leading-relaxed text-slate-400">
<DialogDescription className="leading-relaxed text-muted-foreground">
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
</code>{' '}
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
</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
</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
</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
</code>{' '}
(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',
file
? '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 ? (
@ -450,25 +450,25 @@ export function ImportModal({
<FileText className="text-primary size-5" />
</div>
<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}
>
{truncateFilename(file.name)}
</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' : ''}{' '}
ready
</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">
<Upload className="size-5 text-slate-500 group-hover:text-slate-400" />
<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-muted-foreground group-hover:text-muted-foreground" />
</div>
<p className="text-sm text-slate-400">
<p className="text-sm text-muted-foreground">
Click to choose a CSV file
</p>
<p className="text-[11px] text-slate-600">
<p className="text-[11px] text-muted-foreground">
.csv up to your browser limit
</p>
</>
@ -488,12 +488,12 @@ export function ImportModal({
{preview.length > 0 && !result && (
<div className="space-y-3">
<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}
</p>
<div className="flex flex-wrap items-center gap-1.5">
{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" />
{tagStats.unique} tag{tagStats.unique !== 1 ? 's' : ''} ·{' '}
{tagStats.rowsWithTags} contact
@ -503,59 +503,59 @@ export function ImportModal({
</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">
<table className="w-full min-w-[32rem] text-xs">
<thead>
<tr className="border-b border-slate-800 bg-slate-950/60">
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-slate-500">
<tr className="border-b border-border bg-background/60">
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-muted-foreground">
Phone
</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
</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
</th>
{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
</th>
)}
{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
</th>
)}
</tr>
</thead>
<tbody className="divide-y divide-slate-800/70">
<tbody className="divide-y divide-border/70">
{preview.map((row, i) => (
<tr
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
value={row.phone}
mono
maxWidth="max-w-[7.5rem]"
/>
</td>
<td className="px-3 py-2 text-slate-200">
<td className="px-3 py-2 text-popover-foreground">
<PreviewCell
value={row.name || '—'}
maxWidth="max-w-[8.5rem]"
/>
</td>
<td className="px-3 py-2 text-slate-400">
<td className="px-3 py-2 text-muted-foreground">
<PreviewCell
value={row.email || '—'}
maxWidth="max-w-[10rem]"
/>
</td>
{previewHasCompany && (
<td className="px-3 py-2 text-slate-400">
<td className="px-3 py-2 text-muted-foreground">
<PreviewCell
value={row.company || '—'}
maxWidth="max-w-[7rem]"
@ -578,7 +578,7 @@ export function ImportModal({
</div>
{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 !== 1 ? 's' : ''} not shown
</p>
@ -587,8 +587,8 @@ export function ImportModal({
)}
{result && (
<div className="rounded-xl border border-slate-800 bg-slate-950/50 p-4">
<p className="text-sm font-medium text-white">Import complete</p>
<div className="rounded-xl border border-border bg-background/50 p-4">
<p className="text-sm font-medium text-popover-foreground">Import complete</p>
<div className="mt-3 flex flex-wrap gap-3">
{result.imported > 0 && (
<div className="text-primary flex items-center gap-1.5 text-sm">
@ -620,12 +620,12 @@ export function ImportModal({
)}
</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
type="button"
variant="outline"
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'}
</Button>

View File

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

View File

@ -46,13 +46,13 @@ export function ConversationsChart({ series, loading, range, onRangeChange }: Co
}, [data])
return (
<section className="flex h-full flex-col rounded-xl border border-slate-800 bg-slate-900">
<header className="flex items-center justify-between border-b border-slate-800 px-5 py-4">
<section className="flex h-full flex-col rounded-xl border border-border bg-card">
<header className="flex items-center justify-between border-b border-border px-5 py-4">
<div>
<h2 className="text-sm font-semibold text-white">Conversations Over Time</h2>
<p className="mt-0.5 text-xs text-slate-500">Daily message volume by direction</p>
<h2 className="text-sm font-semibold text-foreground">Conversations Over Time</h2>
<p className="mt-0.5 text-xs text-muted-foreground">Daily message volume by direction</p>
</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) => (
<button
key={r}
@ -61,8 +61,8 @@ export function ConversationsChart({ series, loading, range, onRangeChange }: Co
className={cn(
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
range === r
? 'bg-slate-700 text-white'
: 'text-slate-400 hover:text-white',
? 'bg-secondary text-secondary-foreground'
: 'text-muted-foreground hover:text-foreground',
)}
>
{r} days
@ -85,7 +85,7 @@ export function ConversationsChart({ series, loading, range, onRangeChange }: Co
)}
</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="#7c3aed" label="Outgoing" />
</footer>
@ -215,7 +215,7 @@ function LineSvg({
y={y}
textAnchor="end"
dominantBaseline="middle"
className="fill-slate-500 text-[10px]"
className="fill-muted-foreground text-[10px]"
>
{t}
</text>
@ -231,7 +231,7 @@ function LineSvg({
x={xFor(i)}
y={VB_H - 8}
textAnchor="middle"
className="fill-slate-500 text-[10px]"
className="fill-muted-foreground text-[10px]"
>
{shortDayLabel(p.day)}
</text>
@ -280,10 +280,10 @@ function LineSvg({
letterboxed viewBox percentage. */}
{hovered && hover !== null && (
<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` }}
>
<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">
<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" />

View File

@ -22,15 +22,15 @@ export function EmptyState({
return (
<div
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,
)}
>
<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" />
</div>
<p className="text-sm font-medium text-slate-300">{title}</p>
{hint && <p className="max-w-xs text-xs text-slate-500">{hint}</p>}
<p className="text-sm font-medium text-muted-foreground">{title}</p>
{hint && <p className="max-w-xs text-xs text-muted-foreground">{hint}</p>}
</div>
)
}

View File

@ -23,18 +23,18 @@ interface MetricCardProps {
export function MetricCard({ title, value, icon: Icon, delta, subtitle }: MetricCardProps) {
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">
<p className="text-sm font-medium text-slate-400">{title}</p>
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-800 text-slate-500">
<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-muted text-muted-foreground">
<Icon className="h-4 w-4" />
</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}
</p>
{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}
</div>
)
@ -46,7 +46,7 @@ function DeltaRow({ sign, label }: { sign: number; label: string }) {
? 'text-primary'
: sign < 0
? 'text-red-400'
: 'text-slate-500'
: 'text-muted-foreground'
const Arrow = sign > 0 ? ArrowUp : sign < 0 ? ArrowDown : Minus
return (
<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) {
return (
<section className="flex h-full flex-col rounded-xl border border-slate-800 bg-slate-900">
<header className="border-b border-slate-800 px-5 py-4">
<h2 className="text-sm font-semibold text-white">Pipeline Value</h2>
<p className="mt-0.5 text-xs text-slate-500">
<section className="flex h-full flex-col rounded-xl border border-border bg-card">
<header className="border-b border-border px-5 py-4">
<h2 className="text-sm font-semibold text-foreground">Pipeline Value</h2>
<p className="mt-0.5 text-xs text-muted-foreground">
Open deals by stage
</p>
</header>
@ -43,11 +43,11 @@ export function PipelineDonut({ data, loading, currency }: PipelineDonutProps) {
style={{ background: s.color }}
aria-hidden
/>
<span className="flex-1 truncate text-slate-300">{s.name}</span>
<span className="text-slate-500 tabular-nums">
<span className="flex-1 truncate text-muted-foreground">{s.name}</span>
<span className="text-muted-foreground tabular-nums">
{s.dealCount} deal{s.dealCount === 1 ? '' : 's'}
</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)}
</span>
</li>
@ -114,7 +114,7 @@ function Donut({ data, currency }: { data: PipelineDonutData; currency: string }
x={cx}
y={cy - 6}
textAnchor="middle"
className="fill-slate-500 text-[11px]"
className="fill-muted-foreground text-[11px]"
>
Total
</text>
@ -122,7 +122,7 @@ function Donut({ data, currency }: { data: PipelineDonutData; currency: string }
x={cx}
y={cy + 14}
textAnchor="middle"
className="fill-white text-[18px] font-semibold tabular-nums"
className="fill-foreground text-[18px] font-semibold tabular-nums"
>
{formatCurrencyShort(data.totalValue, currency)}
</text>

View File

@ -31,12 +31,12 @@ export function QuickActions() {
<Link
key={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" />
</div>
<span className="text-sm font-medium text-white">{a.label}</span>
<span className="text-sm font-medium text-foreground">{a.label}</span>
</Link>
)
})}

View File

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

View File

@ -6,14 +6,14 @@ import { cn } from '@/lib/utils'
* its data fetches.
*/
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 }) {
return (
<div
className={cn(
'rounded-xl border border-slate-800 bg-slate-900 p-5',
'rounded-xl border border-border bg-card p-5',
className,
)}
>

View File

@ -161,14 +161,14 @@ export function FlowBuilder() {
<section className="flex flex-col gap-3">
<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})
</h2>
<AddNodeButton onAdd={addNode} />
</div>
{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>
{" "}node, then a <strong>Handoff</strong> — that&apos;s the welcome-menu
shape from the brief.
@ -246,7 +246,7 @@ function KeywordsInput({
}
}}
placeholder="support, help, hi"
className="bg-slate-800"
className="bg-muted"
/>
);
}
@ -265,11 +265,11 @@ function TriggerPanel({
triggerIssues: ValidationIssue[];
}) {
return (
<section className="rounded-lg border border-slate-800 bg-slate-900 p-4">
<h2 className="mb-3 text-sm font-semibold text-white">Trigger</h2>
<section className="rounded-lg border border-border bg-card p-4">
<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>
<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
value={state.trigger_type}
onValueChange={(v) =>
@ -281,7 +281,7 @@ function TriggerPanel({
}))
}
>
<SelectTrigger className="bg-slate-800">
<SelectTrigger className="bg-muted">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -299,7 +299,7 @@ function TriggerPanel({
</div>
{state.trigger_type === "keyword" && (
<div>
<label className="mb-1 block text-xs text-slate-400">
<label className="mb-1 block text-xs text-muted-foreground">
Keywords (comma-separated)
</label>
<KeywordsInput
@ -342,9 +342,9 @@ function EntryPicker({
}) {
if (state.nodes.length === 0) return null;
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" />
<span className="text-xs text-slate-400">Entry node:</span>
<span className="text-xs text-muted-foreground">Entry node:</span>
<NodeKeySelect
value={state.entry_node_id}
nodes={state.nodes}
@ -396,14 +396,14 @@ function NodeCard({
<div
ref={cardRef}
className={cn(
"rounded-lg border bg-slate-900 transition-shadow duration-500",
"rounded-lg border bg-card transition-shadow duration-500",
hasError
? "border-red-500/40"
: isEntry
? "border-primary/50"
: "border-slate-800",
: "border-border",
isFlashed &&
"ring-2 ring-primary ring-offset-2 ring-offset-slate-950",
"ring-2 ring-primary ring-offset-2 ring-offset-background",
)}
>
<button
@ -414,10 +414,10 @@ function NodeCard({
<meta.icon className={cn("h-4 w-4 shrink-0", meta.color)} />
<div className="min-w-0 flex-1">
<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}
</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}
</code>
{isEntry && (
@ -430,7 +430,7 @@ function NodeCard({
)}
</div>
{!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}
</p>
)}
@ -439,20 +439,20 @@ function NodeCard({
<CircleAlert className="h-3.5 w-3.5 shrink-0 text-red-400" />
)}
{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>
{expanded && (
<div className="border-t border-slate-800 px-4 py-4">
<div className="border-t border-border px-4 py-4">
<NodeConfigWithAdvanced
node={node}
allNodes={allNodes}
onUpdate={onUpdate}
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">
{!isEntry && (
<Button variant="ghost" size="sm" onClick={onSetEntry}>
@ -511,11 +511,11 @@ function NodeConfigWithAdvanced({
showAdvanced={showAdvanced}
onUpdateConfig={onUpdateConfig}
/>
<div className="border-t border-slate-800 pt-3">
<div className="border-t border-border pt-3">
<button
type="button"
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 ? (
<ChevronUp className="h-3 w-3" />
@ -527,7 +527,7 @@ function NodeConfigWithAdvanced({
{showAdvanced && (
<div className="mt-3 flex flex-col gap-3">
<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)
</label>
<Input
@ -535,11 +535,11 @@ function NodeConfigWithAdvanced({
onChange={(e) =>
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>
{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
returned by WhatsApp when a customer taps; you usually don&apos;t
need to touch them.
@ -573,13 +573,13 @@ function AddNodeButton({ onAdd }: { onAdd: (type: NodeType) => void }) {
return (
<DropdownMenu>
<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"
>
<Plus className="h-3.5 w-3.5" />
Add node
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="border-slate-700 bg-slate-900">
<DropdownMenuContent align="end" className="border-border bg-popover">
{types.map((t) => {
const meta = NODE_META[t];
return (

View File

@ -128,10 +128,10 @@ function FlowNodeCard({ data, selected }: NodeProps) {
return (
<div
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
? "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
// built-in `animate-pulse` is too gentle; a ring with the
// amber accent matches the list view's flash semantics.
@ -142,13 +142,13 @@ function FlowNodeCard({ data, selected }: NodeProps) {
<Handle
type="target"
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">
<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}
</span>
{isEntry && (
@ -157,21 +157,21 @@ function FlowNodeCard({ data, selected }: NodeProps) {
</span>
)}
</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}
</div>
{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}
</div>
)}
{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) => (
<div
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}>
{slot.label}
@ -184,7 +184,7 @@ function FlowNodeCard({ data, selected }: NodeProps) {
// sits flush with the right edge of the card instead
// of floating at vertical center. The negative offset
// 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>
))}
@ -196,7 +196,7 @@ function FlowNodeCard({ data, selected }: NodeProps) {
type="source"
id={slots[0].id}
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>
@ -319,11 +319,12 @@ function FlowCanvasInner() {
target: e.target,
sourceHandle: e.sourceHandle,
label: e.label,
labelStyle: { fill: "#cbd5e1", fontSize: 11 },
labelBgStyle: { fill: "#0f172a" },
// Mode-aware via CSS tokens so edge chrome flips with light/dark.
labelStyle: { fill: "var(--muted-foreground)", fontSize: 11 },
labelBgStyle: { fill: "var(--card)" },
labelBgPadding: [4, 2] as [number, number],
labelBgBorderRadius: 4,
style: { stroke: "#475569", strokeWidth: 1.5 },
style: { stroke: "var(--border)", strokeWidth: 1.5 },
}));
return rfEdges;
@ -456,7 +457,7 @@ function FlowCanvasInner() {
if (rfNodes.length === 0) {
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>
<CanvasAddNodeButton />
</div>
@ -465,7 +466,7 @@ function FlowCanvasInner() {
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
nodes={rfNodes}
edges={rfEdges}
@ -491,17 +492,17 @@ function FlowCanvasInner() {
minZoom={0.2}
maxZoom={1.5}
>
<Background gap={24} size={1} color="#1e293b" />
<Background gap={24} size={1} color="var(--border)" />
<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}
/>
<MiniMap
pannable
zoomable
nodeColor="#334155"
maskColor="rgba(15, 23, 42, 0.7)"
className="!border !border-slate-700 !bg-slate-900"
nodeColor="var(--muted-foreground)"
maskColor="color-mix(in oklch, var(--background) 70%, transparent)"
className="!border !border-border !bg-card"
/>
<Panel position="bottom-right" className="!bottom-4 !right-4">
<CanvasAddNodeButton />
@ -561,10 +562,10 @@ function NodeEditSheet({
<Sheet open={open} onOpenChange={(v) => !v && onClose()}>
<SheetContent
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">
<SheetTitle className="flex items-center gap-2 text-slate-100">
<SheetHeader className="border-b border-border px-5 py-4">
<SheetTitle className="flex items-center gap-2 text-popover-foreground">
<Icon className={cn("h-4 w-4 shrink-0", meta.color)} />
<span>{meta.label}</span>
{isEntry && (
@ -573,7 +574,7 @@ function NodeEditSheet({
</span>
)}
</SheetTitle>
<SheetDescription className="font-mono text-[11px] text-slate-400">
<SheetDescription className="font-mono text-[11px] text-muted-foreground">
{node.node_key}
</SheetDescription>
</SheetHeader>
@ -587,7 +588,7 @@ function NodeEditSheet({
/>
</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 ? (
<Button variant="ghost" size="sm" onClick={onSetEntry}>
Set as entry
@ -657,13 +658,13 @@ function CanvasAddNodeButton() {
return (
<DropdownMenu>
<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"
>
<Plus className="h-3.5 w-3.5" />
Add node
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="border-slate-700 bg-slate-900">
<DropdownMenuContent align="end" className="border-border bg-popover">
{ADD_NODE_TYPES.map((t) => {
const meta = NODE_META[t];
const Icon = meta.icon;

View File

@ -85,7 +85,7 @@ export function FlowEditorShell({ initialFlow, initialNodes }: Props) {
<div
role="group"
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
active={effectiveView === "canvas"}
@ -108,7 +108,7 @@ export function FlowEditorShell({ initialFlow, initialNodes }: Props) {
{/* Sticky-bottom validation panel mirrors the placement used
when this lived inside FlowBuilder — the activate-readiness
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 />
</div>
</div>
@ -157,8 +157,8 @@ function ToggleButton({
className={cn(
"inline-flex items-center gap-1.5 rounded px-2 py-1 transition-colors",
active
? "bg-slate-700 text-slate-100"
: "text-slate-400 hover:bg-slate-800 hover:text-slate-200",
? "bg-secondary text-secondary-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
{icon}

View File

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

View File

@ -135,7 +135,7 @@ export function NodeConfigForm({
rows={2}
/>
<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)
</label>
<Input
@ -146,11 +146,11 @@ export function NodeConfigForm({
})
}
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{" "}
<code className="rounded bg-slate-800 px-1">
<code className="rounded bg-muted px-1">
{"{{vars."}
{(cfg as { var_key?: string }).var_key || "name"}
{"}}"}
@ -200,7 +200,7 @@ export function NodeConfigForm({
case "end":
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
complete. No config needed.
</p>
@ -269,7 +269,7 @@ function SendButtonsForm({
/>
<div>
<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
</label>
</div>
@ -278,7 +278,7 @@ function SendButtonsForm({
<div
key={i}
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
? "md:grid-cols-[1fr_2fr_2fr_auto]"
: "md:grid-cols-[2fr_2fr_auto]",
@ -293,14 +293,14 @@ function SendButtonsForm({
})
}
placeholder="reply_id"
className="bg-slate-800 font-mono text-xs"
className="bg-muted font-mono text-xs"
/>
)}
<Input
value={b.title}
onChange={(e) => updateButton(i, { title: e.target.value })}
placeholder="Visible title (≤20 chars)"
className="bg-slate-800"
className="bg-muted"
maxLength={20}
/>
<NodeKeySelect
@ -465,13 +465,13 @@ function SendListForm({
</div>
<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)
</label>
{sections.map((section, sIdx) => (
<div
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">
<Input
@ -480,7 +480,7 @@ function SendListForm({
updateSection(sIdx, { title: e.target.value })
}
placeholder={`Section ${sIdx + 1} title (optional)`}
className="bg-slate-800 text-xs"
className="bg-muted text-xs"
/>
{sections.length > 1 && (
<Button
@ -516,7 +516,7 @@ function SendListForm({
})
}
placeholder="reply_id"
className="bg-slate-800 font-mono text-xs"
className="bg-muted font-mono text-xs"
/>
)}
<Input
@ -525,7 +525,7 @@ function SendListForm({
updateRow(sIdx, rIdx, { title: e.target.value })
}
placeholder="Row title (≤24)"
className="bg-slate-800"
className="bg-muted"
maxLength={24}
/>
<NodeKeySelect
@ -614,14 +614,14 @@ function ConditionForm({
<>
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
<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
value={subject}
onValueChange={(v) =>
onUpdateConfig({ subject: v as ConditionCfg["subject"] })
}
>
<SelectTrigger className="bg-slate-800">
<SelectTrigger className="bg-muted">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -632,7 +632,7 @@ function ConditionForm({
</Select>
</div>
<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"
? "var name"
: subject === "tag"
@ -644,7 +644,7 @@ function ConditionForm({
value={cfg.subject_key ?? ""}
onValueChange={(v) => onUpdateConfig({ subject_key: v })}
>
<SelectTrigger className="bg-slate-800">
<SelectTrigger className="bg-muted">
<SelectValue placeholder="Pick a tag…" />
</SelectTrigger>
<SelectContent>
@ -660,7 +660,7 @@ function ConditionForm({
value={cfg.subject_key ?? ""}
onValueChange={(v) => onUpdateConfig({ subject_key: v })}
>
<SelectTrigger className="bg-slate-800">
<SelectTrigger className="bg-muted">
<SelectValue placeholder="Pick a field…" />
</SelectTrigger>
<SelectContent>
@ -677,7 +677,7 @@ function ConditionForm({
onUpdateConfig({ subject_key: e.target.value })
}
placeholder={subject === "var" ? "e.g. email" : "tag UUID"}
className="bg-slate-800 font-mono text-xs"
className="bg-muted font-mono text-xs"
/>
)}
</div>
@ -690,14 +690,14 @@ function ConditionForm({
)}
>
<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
value={operator}
onValueChange={(v) =>
onUpdateConfig({ operator: v as ConditionCfg["operator"] })
}
>
<SelectTrigger className="bg-slate-800">
<SelectTrigger className="bg-muted">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -710,11 +710,11 @@ function ConditionForm({
</div>
{showValue && (
<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
value={cfg.value ?? ""}
onChange={(e) => onUpdateConfig({ value: e.target.value })}
className="bg-slate-800"
className="bg-muted"
/>
</div>
)}
@ -767,14 +767,14 @@ function SetTagForm({
<>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<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
value={cfg.mode ?? "add"}
onValueChange={(v) =>
onUpdateConfig({ mode: v as SetTagCfg["mode"] })
}
>
<SelectTrigger className="bg-slate-800">
<SelectTrigger className="bg-muted">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -784,13 +784,13 @@ function SetTagForm({
</Select>
</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 ? (
<Select
value={cfg.tag_id ?? ""}
onValueChange={(v) => onUpdateConfig({ tag_id: v })}
>
<SelectTrigger className="bg-slate-800">
<SelectTrigger className="bg-muted">
<SelectValue placeholder="Pick a tag…" />
</SelectTrigger>
<SelectContent>
@ -806,7 +806,7 @@ function SetTagForm({
value={cfg.tag_id ?? ""}
onChange={(e) => onUpdateConfig({ tag_id: e.target.value })}
placeholder="Tag UUID"
className="bg-slate-800 font-mono text-xs"
className="bg-muted font-mono text-xs"
/>
)}
</div>
@ -972,7 +972,7 @@ function SendMediaForm({
return (
<>
<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
value={mediaType}
onValueChange={(v) => {
@ -986,7 +986,7 @@ function SendMediaForm({
});
}}
>
<SelectTrigger className="bg-slate-800">
<SelectTrigger className="bg-muted">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -1000,15 +1000,15 @@ function SendMediaForm({
</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 ? (
<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" />
<a
href={cfg.media_url}
target="_blank"
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}
>
{displayName || cfg.media_url}
@ -1016,7 +1016,7 @@ function SendMediaForm({
<button
type="button"
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"
disabled={uploading}
>
@ -1028,7 +1028,7 @@ function SendMediaForm({
type="button"
onClick={() => fileInputRef.current?.click()}
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 ? (
<>
@ -1066,14 +1066,14 @@ function SendMediaForm({
{isDocument && (
<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)
</label>
<Input
value={cfg.filename ?? ""}
onChange={(e) => onUpdateConfig({ filename: e.target.value })}
placeholder="invoice.pdf"
className="bg-slate-800 text-xs"
className="bg-muted text-xs"
/>
</div>
)}

View File

@ -53,11 +53,11 @@ export function EditorHeader() {
return (
<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
type="button"
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" />
Flows
@ -72,7 +72,7 @@ export function EditorHeader() {
setState((s) => ({ ...s, name: e.target.value }))
}
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} />
{dirty && (
@ -154,7 +154,7 @@ export function EditorHeader() {
setState((s) => ({ ...s, description: e.target.value }))
}
placeholder="Optional description (internal — customers don't see this)"
className="bg-slate-900 text-sm"
className="bg-card text-sm"
/>
</div>
);
@ -162,9 +162,9 @@ export function EditorHeader() {
function StatusBadge({ status }: { status: BuilderState["status"] }) {
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",
archived: "border-slate-700 bg-slate-800/50 text-slate-500",
archived: "border-border bg-muted/50 text-muted-foreground",
}[status];
return (
<Badge variant="outline" className={cn("shrink-0", cls)}>

View File

@ -110,7 +110,7 @@ export const NODE_META: Record<
icon: UserPlus,
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
// bg-emerald-500/10 would bleed through ugly).
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" />
No issues. Ready to activate.
</div>
@ -41,11 +41,11 @@ export function ValidationPanel() {
return (
<div
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",
)}
>
<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 ? (
<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)} />
<span className="min-w-0 flex-1">
{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}
</code>
)}
@ -103,7 +103,7 @@ export function IssueLine({
type="button"
onClick={() => onJump(issue.node_key!)}
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,
)}
aria-label={`Jump to node ${issue.node_key}`}

View File

@ -117,8 +117,8 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
if (!contact) {
return (
<div className="flex h-full w-70 items-center justify-center border-l border-slate-800 bg-slate-900">
<p className="text-sm text-slate-500">Select a conversation</p>
<div className="flex h-full w-70 items-center justify-center border-l border-border bg-card">
<p className="text-sm text-muted-foreground">Select a conversation</p>
</div>
);
}
@ -127,12 +127,12 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
const initials = displayName.charAt(0).toUpperCase();
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">
<div className="p-4">
{/* Contact Info */}
<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 ? (
<img
src={contact.avatar_url}
@ -143,11 +143,11 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
initials
)}
</div>
<h3 className="mt-3 text-sm font-semibold text-white">
<h3 className="mt-3 text-sm font-semibold text-foreground">
{displayName}
</h3>
{contact.company && (
<p className="text-xs text-slate-400">{contact.company}</p>
<p className="text-xs text-muted-foreground">{contact.company}</p>
)}
</div>
@ -155,37 +155,37 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
<div className="mt-4 space-y-2">
<button
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>
{copied ? (
<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>
{contact.email && (
<div className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-slate-300">
<Mail className="h-4 w-4 text-slate-500" />
<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-muted-foreground" />
<span className="truncate">{contact.email}</span>
</div>
)}
</div>
{/* Divider */}
<div className="my-4 border-t border-slate-800" />
<div className="my-4 border-t border-border" />
{/* Tags */}
<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" />
Tags
</div>
<div className="mt-2 flex flex-wrap gap-1">
{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) => (
<span
@ -204,27 +204,27 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
</div>
{/* Divider */}
<div className="my-4 border-t border-slate-800" />
<div className="my-4 border-t border-border" />
{/* Active Deals */}
<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" />
Active Deals
</div>
<div className="mt-2 space-y-2">
{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) => (
<div
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}
</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>
{deal.currency ?? "$"}
{deal.value.toLocaleString()}
@ -248,11 +248,11 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
</div>
{/* Divider */}
<div className="my-4 border-t border-slate-800" />
<div className="my-4 border-t border-border" />
{/* Notes */}
<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" />
Notes
</div>
@ -263,7 +263,7 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
onChange={(e) => setNewNote(e.target.value)}
placeholder="Add a note..."
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
size="sm"
@ -279,12 +279,12 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
{notes.map((note) => (
<div
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}
</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")}
</p>
</div>

View File

@ -33,7 +33,7 @@ interface ConversationListProps {
const STATUS_COLORS: Record<ConversationStatus, string> = {
open: "bg-primary",
pending: "bg-amber-500",
closed: "bg-slate-500",
closed: "bg-muted-foreground",
};
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
// the single pane showing; fixed 320px on desktop where it shares the
// 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 */}
<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">
<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
value={search}
onChange={handleSearchChange}
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>
<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"}
<ChevronDown className="h-3 w-3" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="border-slate-700 bg-slate-800"
className="border-border bg-popover"
>
{FILTER_OPTIONS.map((opt) => (
<DropdownMenuItem
@ -177,7 +177,7 @@ export function ConversationList({
"text-sm",
filter === opt.value
? "text-primary"
: "text-slate-300"
: "text-popover-foreground"
)}
>
{opt.label}
@ -200,7 +200,7 @@ export function ConversationList({
</div>
) : filtered.length === 0 ? (
<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 className="flex flex-col">
@ -248,12 +248,12 @@ function ConversationItem({
<button
onClick={handleClick}
className={cn(
"flex w-full items-start gap-3 px-3 py-3 text-left transition-colors hover:bg-slate-800/50",
isActive && "border-l-2 border-primary bg-slate-800/70"
"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-muted/70"
)}
>
{/* 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 ? (
<img
src={contact.avatar_url}
@ -268,13 +268,13 @@ function ConversationItem({
{/* Content */}
<div className="min-w-0 flex-1">
<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}
</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 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"}
</p>
<div className="flex shrink-0 items-center gap-1.5">

View File

@ -95,7 +95,7 @@ export function MessageActions({
<div
data-touch-open={touchOpen || pickerOpen ? "true" : undefined}
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",
"data-[touch-open=true]:opacity-100",
isAgent ? "right-3" : "left-3",
@ -103,7 +103,7 @@ export function MessageActions({
>
<Popover open={pickerOpen} onOpenChange={setPickerOpen}>
<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"
>
<SmilePlus className="h-3.5 w-3.5" />
@ -117,7 +117,7 @@ export function MessageActions({
key={e}
type="button"
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}`}
>
{e}
@ -128,7 +128,7 @@ export function MessageActions({
<button
type="button"
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"
>
<CornerUpLeft className="h-3.5 w-3.5" />
@ -136,7 +136,7 @@ export function MessageActions({
<button
type="button"
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"
>
<Copy className="h-3.5 w-3.5" />

View File

@ -30,11 +30,11 @@ interface MessageBubbleProps {
function StatusIcon({ status }: { status: Message["status"] }) {
switch (status) {
case "sending":
return <Clock className="h-3 w-3 text-slate-400" />;
return <Clock className="h-3 w-3 text-muted-foreground" />;
case "sent":
return <Check className="h-3 w-3 text-slate-400" />;
return <Check className="h-3 w-3 text-muted-foreground" />;
case "delivered":
return <CheckCheck className="h-3 w-3 text-slate-400" />;
return <CheckCheck className="h-3 w-3 text-muted-foreground" />;
case "read":
return <CheckCheck className="h-3 w-3 text-blue-400" />;
case "failed":
@ -46,8 +46,8 @@ function StatusIcon({ status }: { status: Message["status"] }) {
function MediaUnavailable({ label }: { label: string }) {
return (
<div className="flex items-center gap-2 rounded-lg bg-slate-700/40 px-3 py-2 text-xs text-slate-300">
<ImageOff className="h-4 w-4 shrink-0 text-slate-500" />
<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-muted-foreground" />
<span>{label} unavailable</span>
</div>
);
@ -92,15 +92,15 @@ function MediaImage({ url, alt }: { url: string; alt: string }) {
if (error) {
return (
<div className="flex h-40 w-60 items-center justify-center rounded-lg bg-slate-700">
<ImageOff className="h-8 w-8 text-slate-500" />
<div className="flex h-40 w-60 items-center justify-center rounded-lg bg-muted">
<ImageOff className="h-8 w-8 text-muted-foreground" />
</div>
);
}
if (loading) {
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>
);
@ -181,9 +181,9 @@ function MessageContent({ message }: { message: Message }) {
href={message.media_url}
target="_blank"
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">
{message.content_text || "Document"}
</span>
@ -208,7 +208,7 @@ function MessageContent({ message }: { message: Message }) {
case "location":
return (
<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>
</div>
);
@ -221,7 +221,7 @@ function MessageContent({ message }: { message: Message }) {
// tap rather than the customer typing the same words.
return (
<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" />
Button reply
</span>
@ -265,7 +265,7 @@ export function MessageBubble({
"relative rounded-2xl px-3 py-2",
isAgent
? "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 && (
@ -278,7 +278,18 @@ export function MessageBubble({
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} />}
</div>
</div>

View File

@ -84,7 +84,7 @@ export function MessageComposer({
);
return (
<div className="border-t border-slate-800 bg-slate-900 p-3">
<div className="border-t border-border bg-card p-3">
{replyTo && (
<div className="mb-2">
<ReplyQuote
@ -118,7 +118,7 @@ export function MessageComposer({
canAct={!readOnly}
gateReason="send messages"
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}
>
<LayoutTemplate className="h-4 w-4" />
@ -143,7 +143,7 @@ export function MessageComposer({
// The placeholder text also surfaces the read-only state.
title={readOnly ? "Read-only — your role can't send messages" : undefined}
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"
)}
/>
@ -163,7 +163,7 @@ export function MessageComposer({
{/* Hint sits outside the flex row so its height doesn't push
`items-end` buttons below the textarea. Indented to line up
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
</p>
</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",
g.byCurrentUser
? "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>

View File

@ -116,7 +116,7 @@ function groupMessagesByDate(messages: Message[]) {
const STATUS_OPTIONS: { label: string; value: ConversationStatus; color: string }[] = [
{ label: "Open", value: "open", color: "text-primary" },
{ 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.
*/
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({
conversation,
@ -696,13 +696,13 @@ export function MessageThread({
if (!conversation || !contact) {
return (
<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">
<MessageSquare className="h-8 w-8 text-slate-600" />
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
<MessageSquare className="h-8 w-8 text-muted-foreground" />
</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
</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
</p>
</div>
@ -722,9 +722,9 @@ export function MessageThread({
return (
<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. */}
<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">
{/* Back-to-list button — mobile only. Hidden on lg+ where the
conversation list is always visible next to the thread. */}
@ -733,24 +733,24 @@ export function MessageThread({
type="button"
onClick={onBack}
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" />
</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()}
</div>
<div className="min-w-0">
<h2 className="truncate text-sm font-semibold text-white">{displayName}</h2>
<p className="truncate text-xs text-slate-400">{contact.phone}</p>
<h2 className="truncate text-sm font-semibold text-foreground">{displayName}</h2>
<p className="truncate text-xs text-muted-foreground">{contact.phone}</p>
</div>
{/* Session timer badge — hidden on the narrowest phones so
the name + back arrow keep their room. */}
<Badge
variant="outline"
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"
)}
>
@ -773,7 +773,7 @@ export function MessageThread({
aria-label="Refresh conversation"
title="Refresh"
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
@ -785,15 +785,15 @@ export function MessageThread({
{/* Status dropdown */}
<DropdownMenu>
<DropdownMenuTrigger className={cn(
"inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-slate-800",
currentStatus?.color ?? "text-slate-400"
"inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-muted",
currentStatus?.color ?? "text-muted-foreground"
)}>
{currentStatus?.label ?? "Status"}
<ChevronDown className="h-3 w-3" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="border-slate-700 bg-slate-800"
className="border-border bg-popover"
>
{STATUS_OPTIONS.map((opt) => (
<DropdownMenuItem
@ -811,8 +811,8 @@ export function MessageThread({
<DropdownMenu>
<DropdownMenuTrigger
className={cn(
"inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-slate-800",
assignedAgentId ? "text-primary" : "text-slate-400"
"inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-muted",
assignedAgentId ? "text-primary" : "text-muted-foreground"
)}
>
<UserPlus className="h-3 w-3" />
@ -821,10 +821,10 @@ export function MessageThread({
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="border-slate-700 bg-slate-800"
className="border-border bg-popover"
>
{profiles.length === 0 ? (
<DropdownMenuItem disabled className="text-sm text-slate-500">
<DropdownMenuItem disabled className="text-sm text-muted-foreground">
No teammates available
</DropdownMenuItem>
) : (
@ -836,7 +836,7 @@ export function MessageThread({
onClick={() => handleAssignChange(p.user_id)}
className={cn(
"text-sm",
isSelected ? "text-primary" : "text-slate-300"
isSelected ? "text-primary" : "text-popover-foreground"
)}
>
<span className="flex-1">
@ -850,10 +850,10 @@ export function MessageThread({
)}
{assignedAgentId && (
<>
<DropdownMenuSeparator className="bg-slate-700" />
<DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem
onClick={() => handleAssignChange(null)}
className="text-sm text-slate-400"
className="text-sm text-muted-foreground"
>
Unassign
</DropdownMenuItem>
@ -872,8 +872,8 @@ export function MessageThread({
</div>
) : messages.length === 0 ? (
<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-xs text-slate-600">
<p className="text-sm text-muted-foreground">No messages yet</p>
<p className="text-xs text-muted-foreground">
Send a template to start the conversation
</p>
</div>
@ -883,7 +883,7 @@ export function MessageThread({
<div key={group.date}>
{/* Date separator */}
<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)}
</span>
</div>

View File

@ -27,8 +27,8 @@ export function ReplyQuote({
className={cn(
"flex items-start gap-2 border-l-2 border-primary px-2 py-1",
isChip
? "rounded-md bg-slate-800/80"
: "mb-1.5 rounded-md bg-black/20",
? "rounded-md bg-muted/80"
: "mb-1.5 rounded-md bg-background/20",
)}
>
<div className="min-w-0 flex-1 overflow-hidden">
@ -42,7 +42,7 @@ export function ReplyQuote({
* layout wider, shoving the contact sidebar off-screen.
* `break-words` also wraps long URLs that have no whitespace
* 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}
</div>
</div>
@ -51,7 +51,7 @@ export function ReplyQuote({
type="button"
onClick={onDismiss}
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" />
</button>

View File

@ -183,13 +183,13 @@ export function TemplatePicker({
return (
<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>
<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" />
{selected ? selected.name : "Send template"}
</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogDescription className="text-muted-foreground">
{selected
? "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."}
@ -203,9 +203,9 @@ export function TemplatePicker({
<Loader2 className="h-5 w-5 animate-spin text-primary" />
</div>
) : templates.length === 0 ? (
<div className="rounded-md border border-slate-800 bg-slate-950/50 p-6 text-center">
<p className="text-sm text-slate-300">No approved templates</p>
<p className="mt-1 text-xs text-slate-500">
<div className="rounded-md border border-border bg-background/50 p-6 text-center">
<p className="text-sm text-popover-foreground">No approved templates</p>
<p className="mt-1 text-xs text-muted-foreground">
Approve a template in Meta WhatsApp Manager, then sync it
from Settings → Templates.
</p>
@ -216,28 +216,28 @@ export function TemplatePicker({
key={t.id}
type="button"
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="min-w-0 flex-1">
<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}
</p>
<Badge className="border border-primary/30 bg-primary/20 text-[10px] text-primary">
{t.category}
</Badge>
{t.language && (
<span className="text-[10px] uppercase text-slate-500">
<span className="text-[10px] uppercase text-muted-foreground">
{t.language}
</span>
)}
</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}
</p>
</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>
</button>
))
@ -245,33 +245,33 @@ export function TemplatePicker({
</div>
) : (
<div className="space-y-3">
<div className="rounded-md border border-slate-800 bg-slate-950/50 p-3">
<p className="mb-1 text-xs text-slate-400">Preview</p>
<p className="whitespace-pre-wrap text-sm text-slate-200">
<div className="rounded-md border border-border bg-background/50 p-3">
<p className="mb-1 text-xs text-muted-foreground">Preview</p>
<p className="whitespace-pre-wrap text-sm text-popover-foreground">
{renderBodyPreview(selected.body_text, params)}
</p>
{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}
</p>
)}
</div>
{slots && slots.headerVarCount > 0 && (
<div className="space-y-1">
<Label className="text-xs text-slate-300">
<Label className="text-xs text-popover-foreground">
{`Header {{1}}`}
</Label>
<Input
value={headerText}
onChange={(e) => setHeaderText(e.target.value)}
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>
)}
{slots?.bodyVars.map((v, i) => (
<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
value={params[i] ?? ""}
onChange={(e) => {
@ -280,13 +280,13 @@ export function TemplatePicker({
setParams(next);
}}
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>
))}
{slots?.urlButtonSlots.map((slot) => (
<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}}`}
</Label>
<Input
@ -298,9 +298,9 @@ export function TemplatePicker({
}))
}
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}}")}
</p>
</div>
@ -314,7 +314,7 @@ export function TemplatePicker({
<Button
variant="outline"
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" />
Back
@ -331,7 +331,7 @@ export function TemplatePicker({
<Button
variant="outline"
onClick={() => handleOpenChange(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800"
className="border-border text-popover-foreground hover:bg-muted"
>
Cancel
</Button>

View File

@ -16,6 +16,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ModeToggle } from "@/components/layout/mode-toggle";
const pageTitles: Record<string, string> = {
"/dashboard": "Dashboard",
@ -52,25 +53,28 @@ export function Header({ onOpenSidebar }: HeaderProps) {
"U";
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">
{/* Hamburger — mobile only. 44×44 hit target per Apple HIG. */}
<button
type="button"
onClick={onOpenSidebar}
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" />
</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}
</h1>
</div>
<div className="flex items-center gap-1 sm:gap-2">
<ModeToggle />
<DropdownMenu>
<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"
>
<Avatar className="size-8">
@ -84,29 +88,29 @@ export function Header({ onOpenSidebar }: HeaderProps) {
{initial}
</AvatarFallback>
</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"}
</span>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
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">
<p className="truncate text-sm font-medium text-white">
<p className="truncate text-sm font-medium text-foreground">
{profile?.full_name ?? "User"}
</p>
<p className="truncate text-xs text-slate-400">
<p className="truncate text-xs text-muted-foreground">
{profile?.email ?? ""}
</p>
</div>
<DropdownMenuSeparator className="bg-slate-800" />
<DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem
render={
<Link
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={
<Link
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" />
Settings
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-slate-800" />
<DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem
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" />
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</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",
// Neutral slate: the operational default.
className:
"border-slate-700 bg-slate-800 text-slate-300",
"border-border bg-muted text-foreground",
},
viewer: {
icon: User,
label: "Viewer",
// Muted slate: read-only role; visually quieter than agent.
className:
"border-slate-800 bg-slate-900 text-slate-500",
"border-border bg-card text-muted-foreground",
},
};
import {
@ -157,7 +157,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
aria-label="Close menu"
onClick={onClose}
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
? "pointer-events-auto opacity-100"
: "pointer-events-none opacity-0",
@ -167,7 +167,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
<aside
className={cn(
// 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",
open ? "translate-x-0" : "-translate-x-full",
// 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
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">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<MessageSquare className="h-4 w-4" />
</div>
<span className="text-sm font-semibold text-white">
<span className="text-sm font-semibold text-foreground">
CRM Template for WhatsApp
</span>
</Link>
@ -190,7 +190,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
type="button"
onClick={onClose}
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" />
</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",
isActive
? "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" />
@ -244,7 +244,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
})}
</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">
{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",
isActive
? "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" />
@ -270,7 +270,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
</nav>
{/* 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
name differs from the user's own name (see
`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
which account they're acting in. */}
{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" />
{/* `title=` exposes the full name on hover when it
gets truncated (long account names + narrow
@ -307,7 +307,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
</div>
) : null}
<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">
{profile?.avatar_url ? (
<AvatarImage
@ -322,10 +322,10 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
</AvatarFallback>
</Avatar>
<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"}
</p>
<p className="truncate text-xs text-slate-400">
<p className="truncate text-xs text-muted-foreground">
{profile?.email ?? ""}
</p>
</div>
@ -334,14 +334,14 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
align="end"
side="top"
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
render={
<Link
href="/settings?tab=profile"
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
href="/settings?tab=whatsapp"
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
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-slate-800" />
<DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem
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" />
Sign out

View File

@ -39,10 +39,10 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
e.stopPropagation();
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
? "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 */}
@ -53,7 +53,7 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
/>
<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}
</h4>
{deal.status === "won" && (
@ -72,10 +72,10 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
{/* Contact row */}
<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)}
</span>
<span className="truncate text-xs text-slate-400">{contactLabel}</span>
<span className="truncate text-xs text-muted-foreground">{contactLabel}</span>
</div>
<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)}
</span>
{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" />
{formatDate(deal.expected_close_date)}
</span>

View File

@ -247,32 +247,32 @@ export function DealForm({
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
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">
<SheetHeader className="border-b border-slate-700/50 p-4">
<SheetTitle className="text-white">
<SheetHeader className="border-b border-border/50 p-4">
<SheetTitle className="text-popover-foreground">
{deal ? "Edit Deal" : "New Deal"}
</SheetTitle>
</SheetHeader>
<div className="flex-1 overflow-y-auto p-4 space-y-4">
<div className="grid gap-2">
<Label className="text-slate-300">Title</Label>
<Label className="text-muted-foreground">Title</Label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Deal title"
className="border-slate-700 bg-slate-800 text-white"
className="border-border bg-muted text-foreground"
/>
</div>
<div className="grid gap-2">
<Label className="text-slate-300">Contact</Label>
<Label className="text-muted-foreground">Contact</Label>
<select
value={contactId}
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>
{contacts.map((c) => (
@ -295,24 +295,24 @@ export function DealForm({
<div className="grid grid-cols-[1fr_110px] gap-3">
<div className="grid gap-2">
<Label className="text-slate-300">Value</Label>
<Label className="text-muted-foreground">Value</Label>
<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
type="number"
value={value}
onChange={(e) => setValue(e.target.value)}
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 className="grid gap-2">
<Label className="text-slate-300">Currency</Label>
<Label className="text-muted-foreground">Currency</Label>
<select
value={currency}
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) => (
<option key={c.code} value={c.code}>
@ -324,21 +324,21 @@ export function DealForm({
</div>
<div className="grid gap-2">
<Label className="text-slate-300">Expected Close Date</Label>
<Label className="text-muted-foreground">Expected Close Date</Label>
<Input
type="date"
value={expectedCloseDate}
onChange={(e) => setExpectedCloseDate(e.target.value)}
className="border-slate-700 bg-slate-800 text-white"
className="border-border bg-muted text-foreground"
/>
</div>
<div className="grid gap-2">
<Label className="text-slate-300">Stage</Label>
<Label className="text-muted-foreground">Stage</Label>
<select
value={stageId}
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) => (
<option key={s.id} value={s.id}>
@ -349,11 +349,11 @@ export function DealForm({
</div>
<div className="grid gap-2">
<Label className="text-slate-300">Assigned To</Label>
<Label className="text-muted-foreground">Assigned To</Label>
<select
value={assignedTo}
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>
{profiles.map((p) => (
@ -365,18 +365,18 @@ export function DealForm({
</div>
<div className="grid gap-2">
<Label className="text-slate-300">Notes</Label>
<Label className="text-muted-foreground">Notes</Label>
<Textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
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>
{deal && (
<div className="space-y-2 rounded-lg border border-slate-700 bg-slate-900/50 p-3">
<p className="text-xs font-medium uppercase tracking-wider text-slate-400">
<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-muted-foreground">
Status
</p>
<div className="flex gap-2">
@ -417,7 +417,7 @@ export function DealForm({
variant="ghost"
onClick={() => handleStatusChange("open")}
disabled={!!statusAction}
className="w-full text-slate-400 hover:text-white"
className="w-full text-muted-foreground hover:text-foreground"
>
Reopen deal
</Button>
@ -426,12 +426,12 @@ export function DealForm({
)}
</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">
<Button
variant="outline"
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
</Button>
@ -453,7 +453,7 @@ export function DealForm({
type="button"
onClick={() => setConfirmDelete(false)}
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
</button>

View File

@ -93,9 +93,9 @@ export function PipelineAnalytics({ stages, deals }: PipelineAnalyticsProps) {
return (
<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
icon={<BarChart3 className="h-4 w-4 text-slate-400" />}
icon={<BarChart3 className="h-4 w-4 text-muted-foreground" />}
label="Total Deals"
value={String(stats.totalCount)}
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;
}) {
return (
<div className="rounded-lg bg-slate-800/50 p-3">
<div className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-slate-400">
<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-muted-foreground">
{icon}
<span>{label}</span>
<Tooltip>
@ -157,7 +157,7 @@ function Metric({
<button
type="button"
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>
</Tooltip>
</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>
);
}

View File

@ -209,21 +209,21 @@ function StageColumn({
// restore the flex-1 share-the-row behavior. The droppable ref is
// on the inner messages region below — intentionally NOT here, so
// 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 */}
<div
className="-mx-4 -mt-4 h-[3px] rounded-t-xl"
style={{ backgroundColor: stage.color }}
/>
<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}
</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}
</span>
</div>
<p className="text-xs text-slate-400">
<p className="text-xs text-muted-foreground">
{formatCurrency(totalValue, currency)}
</p>
@ -236,7 +236,7 @@ function StageColumn({
}`}
>
{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
</div>
) : (
@ -255,7 +255,7 @@ function StageColumn({
variant="ghost"
size="sm"
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" />
Add Deal

View File

@ -199,9 +199,9 @@ export function PipelineSettings({
return (
<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>
<DialogTitle className="text-white">Manage Pipeline</DialogTitle>
<DialogTitle className="text-popover-foreground">Manage Pipeline</DialogTitle>
</DialogHeader>
{showDeleteConfirm ? (
@ -212,7 +212,7 @@ export function PipelineSettings({
<p className="text-sm font-medium text-red-400">
Delete Pipeline
</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
undone.
</p>
@ -222,7 +222,7 @@ export function PipelineSettings({
<Button
variant="outline"
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
</Button>
@ -239,16 +239,16 @@ export function PipelineSettings({
<>
<div className="grid gap-4 py-2">
<div className="grid gap-2">
<Label className="text-slate-300">Pipeline Name</Label>
<Label className="text-muted-foreground">Pipeline Name</Label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
className="border-slate-700 bg-slate-800 text-white"
className="border-border bg-muted text-foreground"
/>
</div>
<div className="grid gap-2">
<Label className="text-slate-300">Stages</Label>
<Label className="text-muted-foreground">Stages</Label>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
@ -303,7 +303,7 @@ export function PipelineSettings({
value={newStageName}
onChange={(e) => setNewStageName(e.target.value)}
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) => {
if (e.key === "Enter") handleAddStage();
}}
@ -313,7 +313,7 @@ export function PipelineSettings({
size="sm"
onClick={handleAddStage}
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" />
Add
@ -324,14 +324,14 @@ export function PipelineSettings({
<Button
variant="outline"
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" />
Create a new pipeline
</Button>
</div>
<DialogFooter className="border-slate-700 bg-slate-900/50">
<DialogFooter className="border-border bg-popover/50">
<Button
variant="destructive"
onClick={() => setShowDeleteConfirm(true)}
@ -342,7 +342,7 @@ export function PipelineSettings({
<Button
variant="outline"
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
</Button>
@ -387,13 +387,13 @@ function SortableStageRow({
<div
ref={setNodeRef}
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
type="button"
{...attributes}
{...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"
>
<GripVertical className="h-4 w-4" />
@ -402,13 +402,13 @@ function SortableStageRow({
<Input
value={stage.name}
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
variant="ghost"
size="icon-xs"
onClick={onRemove}
className="text-slate-400 hover:text-red-400"
className="text-muted-foreground hover:text-red-400"
>
<Trash2 className="h-3 w-3" />
</Button>
@ -431,14 +431,14 @@ function ColorSwatch({
<button
type="button"
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 }}
aria-label="Change color"
/>
{open && (
<>
<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) => (
<button
key={c}

View File

@ -1,33 +1,61 @@
"use client";
import { Check } from "lucide-react";
import { Check, Moon, Sun } from "lucide-react";
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";
/**
* Appearance panel — color-theme picker.
* Appearance panel — light/dark mode + accent-color picker.
*
* Click a card → applies + persists immediately. No save button:
* the whole change is a single CSS-variable swap on <html>, there's
* nothing to roll back. The active card carries a check chip + a
* primary-tinted border so the current pick is obvious.
* Two independent controls: a mode toggle (light / dark) and the
* accent grid. Either applies + persists immediately. No save button:
* each change is a single attribute swap on <html>, there's nothing
* to roll back.
*
* 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.
*/
export function AppearancePanel() {
const { theme, setTheme } = useTheme();
const { theme, setTheme, mode, setMode } = useTheme();
return (
<section className="space-y-4">
<section className="space-y-8">
<div className="space-y-4">
<div>
<h2 className="text-lg font-semibold text-white">Color theme</h2>
<p className="mt-1 text-sm text-slate-400">
Pick the accent color used across the app. All themes stay
dark — only the primary color (buttons, active nav, badges)
changes. Saved to this device.
<h2 className="text-lg font-semibold text-foreground">Mode</h2>
<p className="mt-1 text-sm text-muted-foreground">
Choose light or dark. Light is easier on the eyes in bright
rooms; dark is the original look. Works with any accent
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 className="space-y-4">
<div>
<h2 className="text-lg font-semibold text-foreground">
Accent color
</h2>
<p className="mt-1 text-sm text-muted-foreground">
Pick the accent color used across the app — buttons, active
nav, and badges. Saved to this device.
</p>
</div>
@ -44,10 +72,55 @@ export function AppearancePanel() {
/>
))}
</div>
</div>
</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({
id,
name,
@ -73,7 +146,7 @@ function ThemeCard({
"flex flex-col gap-3 rounded-lg border bg-card p-4 text-left transition-colors",
isActive
? "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">
@ -93,8 +166,8 @@ function ThemeCard({
)}
</div>
<div>
<div className="text-sm font-semibold text-white">{name}</div>
<div className="mt-1 text-xs leading-relaxed text-slate-400">
<div className="text-sm font-semibold text-foreground">{name}</div>
<div className="mt-1 text-xs leading-relaxed text-muted-foreground">
{tagline}
</div>
</div>
@ -103,9 +176,9 @@ function ThemeCard({
aria-hidden
>
<span className="flex-1" style={{ background: swatch }} />
<span className="w-3 bg-slate-700" />
<span className="w-3 bg-slate-800" />
<span className="w-3 bg-slate-900" />
<span className="w-3 bg-muted-foreground/60" />
<span className="w-3 bg-muted" />
<span className="w-3 bg-card" />
</div>
<span className="sr-only">Theme id: {id}</span>
</button>

View File

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

View File

@ -68,13 +68,13 @@ export function DealsSettings() {
return (
<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>
<CardTitle className="flex items-center gap-2 text-white">
<CardTitle className="flex items-center gap-2 text-foreground">
<Coins className="size-4 text-primary" />
Default currency
</CardTitle>
<CardDescription className="text-slate-400">
<CardDescription className="text-muted-foreground">
New deals default to this currency, and pipeline and
dashboard totals are shown in it. Existing deals keep the
currency they were saved with.
@ -82,12 +82,12 @@ export function DealsSettings() {
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-2 sm:max-w-xs">
<Label className="text-slate-300">Currency</Label>
<Label className="text-muted-foreground">Currency</Label>
<select
value={selected}
onChange={(e) => setSelected(e.target.value)}
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) => (
<option key={c.code} value={c.code}>
@ -96,7 +96,7 @@ export function DealsSettings() {
))}
</select>
{!canEditSettings && (
<p className="text-xs text-slate-500">
<p className="text-xs text-muted-foreground">
Only account admins can change the default currency.
</p>
)}

View File

@ -185,20 +185,20 @@ export function InviteMemberDialog({
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 ? (
<>
<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" />
Invite created
</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogDescription className="text-muted-foreground">
Share this link with your new teammate. They&apos;ll be able
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{' '}
<span className="font-medium text-slate-300">
<span className="font-medium text-muted-foreground">
{result.expiresInDays} day{result.expiresInDays === 1 ? '' : 's'}
</span>
.
@ -206,12 +206,12 @@ export function InviteMemberDialog({
</DialogHeader>
<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">
<Input
readOnly
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()}
/>
<Button
@ -250,7 +250,7 @@ export function InviteMemberDialog({
className={buttonVariants({
variant: 'outline',
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" />
@ -258,7 +258,7 @@ export function InviteMemberDialog({
</a>
</div>
<DialogFooter className="bg-slate-900 border-slate-700">
<DialogFooter className="bg-popover border-border">
<Button
onClick={() => onOpenChange(false)}
className="bg-primary hover:bg-primary/90 text-primary-foreground"
@ -270,8 +270,8 @@ export function InviteMemberDialog({
) : (
<>
<DialogHeader>
<DialogTitle className="text-white">Invite a teammate</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogTitle className="text-popover-foreground">Invite a teammate</DialogTitle>
<DialogDescription className="text-muted-foreground">
Generate a one-time invite link. Share it via WhatsApp,
Slack, or any channel you like — no email service required.
</DialogDescription>
@ -279,12 +279,12 @@ export function InviteMemberDialog({
<div className="space-y-4 py-2">
<div className="space-y-2">
<Label className="text-slate-300">Role</Label>
<Label className="text-muted-foreground">Role</Label>
<Select
value={role}
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 />
</SelectTrigger>
<SelectContent>
@ -293,18 +293,18 @@ export function InviteMemberDialog({
<SelectItem value="viewer">Viewer</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-slate-500">
<p className="text-xs text-muted-foreground">
{ROLE_DESCRIPTIONS[role]}
</p>
</div>
<div className="space-y-2">
<Label className="text-slate-300">Link valid for</Label>
<Label className="text-muted-foreground">Link valid for</Label>
<Select
value={expiry}
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 />
</SelectTrigger>
<SelectContent>
@ -318,29 +318,29 @@ export function InviteMemberDialog({
</div>
<div className="space-y-2">
<Label className="text-slate-300">
<Label className="text-muted-foreground">
Label{' '}
<span className="text-xs text-slate-500">(optional)</span>
<span className="text-xs text-muted-foreground">(optional)</span>
</Label>
<Input
placeholder="e.g. Sara — support team"
value={label}
onChange={(e) => setLabel(e.target.value)}
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
list below.
</p>
</div>
</div>
<DialogFooter className="bg-slate-900 border-slate-700">
<DialogFooter className="bg-popover border-border">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800"
className="border-border text-muted-foreground hover:bg-muted"
>
Cancel
</Button>

View File

@ -110,12 +110,12 @@ const ROLE_CHIP: Record<
agent: {
icon: UserCog,
label: 'Agent',
className: 'border-slate-700 bg-slate-800 text-slate-300',
className: 'border-border bg-muted text-muted-foreground',
},
viewer: {
icon: UserIcon,
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 */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-white">Account members</h2>
<p className="text-sm text-slate-400">
<h2 className="text-lg font-semibold text-foreground">Account members</h2>
<p className="text-sm text-muted-foreground">
People with access to this account. Roles control what each
teammate can do.
</p>
@ -314,9 +314,9 @@ export function MembersTab() {
</div>
{/* 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">
<ul className="divide-y divide-slate-800">
<ul className="divide-y divide-border">
{members.map((member) => {
const roleMeta = ROLE_CHIP[member.role];
const RoleIcon = roleMeta.icon;
@ -351,17 +351,17 @@ export function MembersTab() {
<div className="min-w-0 flex-1">
<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'}
</span>
{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
</Badge>
)}
</div>
{member.email && (
<p className="truncate text-xs text-slate-500">
<p className="truncate text-xs text-muted-foreground">
{member.email}
</p>
)}
@ -370,7 +370,7 @@ export function MembersTab() {
{/* Joined date stays desktop-only. The mobile row's
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)}
</div>
@ -394,7 +394,7 @@ export function MembersTab() {
}
>
<SelectTrigger
className="w-32 bg-slate-800 border-slate-700 text-slate-200"
className="w-32 bg-muted border-border text-foreground"
disabled={isBusy}
>
<SelectValue />
@ -446,11 +446,11 @@ export function MembersTab() {
<RequireRole min="admin">
<div>
<div className="mb-2 flex items-center gap-2">
<UsersRound className="size-4 text-slate-400" />
<h3 className="text-sm font-semibold text-white">
<UsersRound className="size-4 text-muted-foreground" />
<h3 className="text-sm font-semibold text-foreground">
Pending invitations
</h3>
<Badge className="bg-slate-800 text-slate-400 border-slate-700">
<Badge className="bg-muted text-muted-foreground border-border">
{invitations.length}
</Badge>
</div>
@ -460,7 +460,7 @@ export function MembersTab() {
front (rather than letting the user discover it by
looking for a button) keeps it from feeling like a bug. */}
{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
for security — to re-share, revoke the invite below and
create a new one.
@ -468,22 +468,22 @@ export function MembersTab() {
) : null}
{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">
<Mail className="size-6 text-slate-600" />
<p className="mt-2 text-sm text-slate-400">
<Mail className="size-6 text-muted-foreground" />
<p className="mt-2 text-sm text-muted-foreground">
No pending invitations.
</p>
<p className="mt-1 text-xs text-slate-500">
Click <span className="text-slate-300">Invite member</span>{' '}
<p className="mt-1 text-xs text-muted-foreground">
Click <span className="text-muted-foreground">Invite member</span>{' '}
above to generate a shareable link.
</p>
</CardContent>
</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">
<ul className="divide-y divide-slate-800">
<ul className="divide-y divide-border">
{invitations.map((inv) => {
const inviteRoleMeta = ROLE_CHIP[inv.role];
const InviteRoleIcon = inviteRoleMeta.icon;
@ -494,7 +494,7 @@ export function MembersTab() {
>
<div className="min-w-0 flex-1">
<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'}
</span>
<span
@ -504,7 +504,7 @@ export function MembersTab() {
{inviteRoleMeta.label}
</span>
</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)}
</p>
</div>
@ -544,15 +544,15 @@ export function MembersTab() {
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>
<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" />
Remove member
</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogDescription className="text-muted-foreground">
Remove{' '}
<span className="font-medium text-slate-300">
<span className="font-medium text-muted-foreground">
{removingMember?.full_name || 'this teammate'}
</span>{' '}
from the account? They&apos;ll be signed out of this account
@ -560,11 +560,11 @@ export function MembersTab() {
login isn&apos;t deleted.
</DialogDescription>
</DialogHeader>
<DialogFooter className="bg-slate-900 border-slate-700">
<DialogFooter className="bg-popover border-border">
<Button
variant="outline"
onClick={() => setRemovingMember(null)}
className="border-slate-700 text-slate-300 hover:bg-slate-800"
className="border-border text-muted-foreground hover:bg-muted"
>
Cancel
</Button>

View File

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

View File

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

View File

@ -49,13 +49,13 @@ export function SessionsCard() {
return (
<>
<Card className="bg-slate-900/40 border-slate-800">
<Card className="bg-card/40 border-border">
<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" />
Active sessions
</CardTitle>
<CardDescription className="text-slate-400">
<CardDescription className="text-muted-foreground">
Sign out of every device where you&apos;re logged in — including
this one. Useful if you lost a laptop or shared your password.
</CardDescription>

View File

@ -152,8 +152,8 @@ export function TagManager() {
<div className="space-y-4 mt-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-white">Tags</h2>
<p className="text-sm text-slate-400">Organize your contacts with color-coded tags.</p>
<h2 className="text-lg font-semibold text-foreground">Tags</h2>
<p className="text-sm text-muted-foreground">Organize your contacts with color-coded tags.</p>
</div>
<Button
onClick={() => {
@ -169,14 +169,14 @@ export function TagManager() {
</div>
{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">
<p className="text-slate-400 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-sm">No tags yet.</p>
<p className="text-muted-foreground text-xs mt-1">Create tags to categorize your contacts.</p>
</CardContent>
</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">
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
@ -196,7 +196,7 @@ export function TagManager() {
{tag.name}
<button
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" />
</button>
@ -209,22 +209,22 @@ export function TagManager() {
{/* New Tag Dialog */}
<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>
<DialogTitle className="text-white">New Tag</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogTitle className="text-popover-foreground">New Tag</DialogTitle>
<DialogDescription className="text-muted-foreground">
Create a new tag with a name and color.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-2">
<Label className="text-slate-300">Tag Name</Label>
<Label className="text-muted-foreground">Tag Name</Label>
<Input
placeholder="e.g. VIP Customer"
value={newTagName}
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) => {
if (e.key === 'Enter') handleCreate();
}}
@ -232,13 +232,13 @@ export function TagManager() {
</div>
<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">
{PRESET_COLORS.map((color) => (
<button
key={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={{
backgroundColor: color.value,
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 */}
<div className="space-y-2">
<Label className="text-slate-300">Preview</Label>
<Label className="text-muted-foreground">Preview</Label>
<div>
<span
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>
<DialogFooter className="bg-slate-900 border-slate-700">
<DialogFooter className="bg-popover border-border">
<Button
variant="outline"
onClick={() => setDialogOpen(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800"
className="border-border text-muted-foreground hover:bg-muted"
>
Cancel
</Button>
@ -299,19 +299,19 @@ export function TagManager() {
{/* Delete Confirmation Dialog */}
<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>
<DialogTitle className="text-white">Delete Tag</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogTitle className="text-popover-foreground">Delete Tag</DialogTitle>
<DialogDescription className="text-muted-foreground">
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.
</DialogDescription>
</DialogHeader>
<DialogFooter className="bg-slate-900 border-slate-700">
<DialogFooter className="bg-popover border-border">
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800"
className="border-border text-muted-foreground hover:bg-muted"
>
Cancel
</Button>

View File

@ -449,8 +449,8 @@ export function TemplateManager() {
<div className="space-y-4 mt-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<h2 className="text-lg font-semibold text-white">Message Templates</h2>
<p className="text-sm text-slate-400">
<h2 className="text-lg font-semibold text-foreground">Message Templates</h2>
<p className="text-sm text-muted-foreground">
Create message templates and submit them to Meta for approval. Use
&quot;Sync from Meta&quot; to pull templates approved elsewhere.
</p>
@ -460,7 +460,7 @@ export function TemplateManager() {
variant="outline"
onClick={handleSyncFromMeta}
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"
>
<RefreshCw className={`size-4 ${syncing ? 'animate-spin' : ''}`} />
@ -477,10 +477,10 @@ export function TemplateManager() {
</div>
{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">
<p className="text-slate-400 text-sm">No templates yet.</p>
<p className="text-slate-500 text-xs mt-1">
<p className="text-muted-foreground text-sm">No templates yet.</p>
<p className="text-muted-foreground text-xs mt-1">
Create your first message template to get started.
</p>
</CardContent>
@ -493,12 +493,12 @@ export function TemplateManager() {
return (
<Card
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">
<div className="space-y-2 min-w-0 flex-1">
<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
className={`text-xs border ${categoryColors[template.category] || ''}`}
>
@ -508,7 +508,7 @@ export function TemplateManager() {
{status.label}
</Badge>
{template.language && (
<span className="text-xs text-slate-500 uppercase">
<span className="text-xs text-muted-foreground uppercase">
{template.language}
</span>
)}
@ -527,11 +527,11 @@ export function TemplateManager() {
</span>
)}
</div>
<p className="text-sm text-slate-400 line-clamp-2">
<p className="text-sm text-muted-foreground line-clamp-2">
{template.body_text}
</p>
{template.footer_text && (
<p className="text-xs text-slate-500 italic">
<p className="text-xs text-muted-foreground italic">
{template.footer_text}
</p>
)}
@ -552,7 +552,7 @@ export function TemplateManager() {
onClick={() => openEdit(template)}
title="Editing triggers Meta re-review — status flips to PENDING."
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" />
Edit
@ -565,7 +565,7 @@ export function TemplateManager() {
onClick={() => openEdit(template)}
title="Edit the template and resubmit to Meta for review."
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" />
Resubmit
@ -586,7 +586,7 @@ export function TemplateManager() {
? 'Delete from Meta and 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 ? (
<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>
<DialogTitle className="text-white">
<DialogTitle className="text-popover-foreground">
{editingId ? 'Edit Message Template' : 'New Message Template'}
</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogDescription className="text-muted-foreground">
{editingId
? '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.'}
@ -638,15 +638,15 @@ export function TemplateManager() {
<div className="space-y-4 py-2">
<div className="space-y-2">
<Label className="text-slate-300">Template Name</Label>
<Label className="text-muted-foreground">Template Name</Label>
<Input
placeholder="e.g. order_confirmation"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
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
? 'Name is fixed once a template exists on Meta — create a new template to change it.'
: 'Lowercase letters, digits, and underscores only.'}
@ -655,7 +655,7 @@ export function TemplateManager() {
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-slate-300">Category</Label>
<Label className="text-muted-foreground">Category</Label>
<Select
value={form.category}
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 />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectContent className="bg-popover border-border">
{CATEGORIES.map((cat) => (
<SelectItem
key={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}
</SelectItem>
@ -683,7 +683,7 @@ export function TemplateManager() {
</div>
<div className="space-y-2">
<Label className="text-slate-300">Language</Label>
<Label className="text-muted-foreground">Language</Label>
<Input
list="template-language-codes"
placeholder="en_US"
@ -692,14 +692,14 @@ export function TemplateManager() {
setForm({ ...form, language: e.target.value })
}
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">
{COMMON_LANGUAGE_CODES.map((code) => (
<option key={code} value={code} />
))}
</datalist>
<p className="text-[11px] text-slate-500">
<p className="text-[11px] text-muted-foreground">
{editingId
? 'Language is fixed once a template exists on Meta.'
: (
@ -713,7 +713,7 @@ export function TemplateManager() {
</div>
<div className="space-y-2">
<Label className="text-slate-300">Header</Label>
<Label className="text-muted-foreground">Header</Label>
<Select
value={form.header_format}
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 />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectContent className="bg-popover border-border">
{HEADER_FORMATS.map((type) => (
<SelectItem
key={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'
? 'None'
@ -758,7 +758,7 @@ export function TemplateManager() {
setForm({ ...form, header_content: e.target.value })
}
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 && (
<Input
@ -769,7 +769,7 @@ export function TemplateManager() {
onChange={(e) =>
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>
@ -783,9 +783,9 @@ export function TemplateManager() {
onChange={(e) =>
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
during review, so the file needs to stay live for ~24 hrs.
{form.header_format === 'image' &&
@ -801,7 +801,7 @@ export function TemplateManager() {
</div>
<div className="space-y-2">
<Label className="text-slate-300">Body Text</Label>
<Label className="text-muted-foreground">Body Text</Label>
<Textarea
placeholder="Hello {{1}}, your order {{2}} is confirmed."
value={form.body_text}
@ -810,16 +810,16 @@ export function TemplateManager() {
}
rows={4}
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
starting at {`{{1}}`}).
</p>
{bodyVarCount > 0 && (
<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)
</Label>
{form.body_samples.map((val, i) => {
@ -836,7 +836,7 @@ export function TemplateManager() {
next[i] = e.target.value;
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 className="space-y-2">
<Label className="text-slate-300">Footer (optional)</Label>
<Label className="text-muted-foreground">Footer (optional)</Label>
<Input
placeholder="Optional footer text (max 60 chars)"
value={form.footer_text}
@ -853,27 +853,27 @@ export function TemplateManager() {
setForm({ ...form, footer_text: e.target.value })
}
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 className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-slate-300">Buttons (optional)</Label>
<Label className="text-muted-foreground">Buttons (optional)</Label>
<Button
type="button"
variant="outline"
size="sm"
onClick={addButton}
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" />
Add Button
</Button>
</div>
{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
buttons must come before URL / phone / copy-code buttons.
</p>
@ -882,7 +882,7 @@ export function TemplateManager() {
{form.buttons.map((btn, i) => (
<div
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">
<Select
@ -895,31 +895,31 @@ export function TemplateManager() {
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 />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectContent className="bg-popover border-border">
<SelectItem
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
</SelectItem>
<SelectItem
value="URL"
className="text-white focus:bg-slate-700 focus:text-white"
className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
>
URL
</SelectItem>
<SelectItem
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
</SelectItem>
<SelectItem
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
</SelectItem>
@ -932,14 +932,14 @@ export function TemplateManager() {
onChange={(e) =>
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
type="button"
variant="ghost"
size="icon"
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" />
</Button>
@ -952,7 +952,7 @@ export function TemplateManager() {
onChange={(e) =>
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 && (
<Input
@ -961,7 +961,7 @@ export function TemplateManager() {
onChange={(e) =>
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>
@ -973,7 +973,7 @@ export function TemplateManager() {
onChange={(e) =>
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' && (
@ -983,7 +983,7 @@ export function TemplateManager() {
onChange={(e) =>
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>
@ -993,11 +993,11 @@ export function TemplateManager() {
</div>
</div>
<DialogFooter className="bg-slate-900 border-slate-700">
<DialogFooter className="bg-popover border-border">
<Button
variant="outline"
onClick={() => setDialogOpen(false)}
className="border-slate-700 text-slate-300 hover:bg-slate-800"
className="border-border text-muted-foreground hover:bg-muted"
>
Cancel
</Button>
@ -1030,21 +1030,21 @@ export function TemplateManager() {
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>
<DialogTitle className="text-white">Delete template?</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogTitle className="text-popover-foreground">Delete template?</DialogTitle>
<DialogDescription className="text-muted-foreground">
{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 wacrm. It was never submitted to Meta, so no remote cleanup is needed.`}
</DialogDescription>
</DialogHeader>
<DialogFooter className="bg-slate-900 border-slate-700">
<DialogFooter className="bg-popover border-border">
<Button
variant="outline"
onClick={() => setTemplateToDelete(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
</Button>

View File

@ -408,18 +408,18 @@ export function WhatsAppConfig() {
)}
{/* Connection Status */}
<Alert className="bg-slate-900 border-slate-700">
<Alert className="bg-card border-border">
<div className="flex items-center gap-2">
{connectionStatus === 'connected' ? (
<CheckCircle2 className="size-4 text-primary" />
) : (
<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'}
</AlertTitle>
</div>
<AlertDescription className="text-slate-400">
<AlertDescription className="text-muted-foreground">
{connectionStatus === 'connected'
? 'Your access token authenticates with Meta. See Registration status below for whether webhooks are actually wired.'
: statusMessage ||
@ -462,7 +462,7 @@ export function WhatsAppConfig() {
size="sm"
onClick={handleVerifyRegistration}
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 ? (
<Loader2 className="size-3.5 animate-spin" />
@ -472,7 +472,7 @@ export function WhatsAppConfig() {
Verify with Meta
</Button>
</div>
<AlertDescription className="text-slate-400 mt-2 text-xs leading-relaxed">
<AlertDescription className="text-muted-foreground mt-2 text-xs leading-relaxed">
{isRegistered ? (
<>
Subscribed since{' '}
@ -502,14 +502,14 @@ export function WhatsAppConfig() {
</AlertDescription>
{registrationProbe && (
<div className="mt-3 rounded border border-slate-700 bg-slate-900/60 px-3 py-2 space-y-1.5 text-[11px]">
<p className="font-medium text-slate-200">
<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-foreground">
Diagnostic — last run: {' '}
<span className={registrationProbe.live ? 'text-emerald-400' : 'text-amber-400'}>
{registrationProbe.live ? 'live' : 'not live'}
</span>
</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]) => (
<li key={k} className="flex items-center gap-1.5">
{v === true ? (
@ -517,9 +517,9 @@ export function WhatsAppConfig() {
) : v === false ? (
<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>
))}
</ul>
@ -536,36 +536,36 @@ export function WhatsAppConfig() {
)}
{/* 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>
<CardTitle className="text-white">API Credentials</CardTitle>
<CardDescription className="text-slate-400">
<CardTitle className="text-foreground">API Credentials</CardTitle>
<CardDescription className="text-muted-foreground">
Enter your Meta WhatsApp Business API credentials.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="text-slate-300">Phone Number ID</Label>
<Label className="text-muted-foreground">Phone Number ID</Label>
<Input
placeholder="e.g. 100234567890123"
value={phoneNumberId}
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 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
placeholder="e.g. 100234567890456"
value={wabaId}
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 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">
<Input
type={showToken ? 'text' : 'password'}
@ -581,40 +581,40 @@ export function WhatsAppConfig() {
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
type="button"
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" />}
</button>
</div>
{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.
</p>
)}
</div>
<div className="space-y-2">
<Label className="text-slate-300">Webhook Verify Token</Label>
<Label className="text-muted-foreground">Webhook Verify Token</Label>
<Input
placeholder="Create a custom verify token"
value={verifyToken}
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.
</p>
</div>
<div className="space-y-2">
<Label className="text-slate-300">
<Label className="text-muted-foreground">
Two-step verification PIN
<span className="ml-1 text-slate-500">(optional)</span>
<span className="ml-1 text-muted-foreground">(optional)</span>
</Label>
<Input
type="text"
@ -625,12 +625,12 @@ export function WhatsAppConfig() {
onChange={(e) =>
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">
Needed only to wire <strong className="text-slate-300">inbound</strong> messages
for a <strong className="text-slate-300">production</strong> number. Set it in{' '}
<strong className="text-slate-300">
<p className="text-xs text-muted-foreground leading-relaxed">
Needed only to wire <strong className="text-muted-foreground">inbound</strong> messages
for a <strong className="text-muted-foreground">production</strong> number. Set it in{' '}
<strong className="text-muted-foreground">
Meta Business Manager → WhatsApp Accounts → Phone
Numbers → Two-step verification
</strong>
@ -638,7 +638,7 @@ export function WhatsAppConfig() {
otherwise Meta routes inbound events to whichever app
last claimed it (the symptom that hits second numbers
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.
Leaving it blank also keeps an existing registration
untouched.
@ -648,27 +648,27 @@ export function WhatsAppConfig() {
</Card>
{/* 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>
<CardTitle className="text-white">Webhook Configuration</CardTitle>
<CardDescription className="text-slate-400">
<CardTitle className="text-foreground">Webhook Configuration</CardTitle>
<CardDescription className="text-muted-foreground">
Use this URL as your webhook callback in the Meta App Dashboard.
</CardDescription>
</CardHeader>
<CardContent>
<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">
<Input
readOnly
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
variant="outline"
size="icon"
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" />
</Button>
@ -697,7 +697,7 @@ export function WhatsAppConfig() {
variant="outline"
onClick={handleTestConnection}
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 ? (
<>
@ -736,23 +736,23 @@ export function WhatsAppConfig() {
{/* Setup Instructions Sidebar */}
<div>
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent">
<Card className="bg-card border-border ring-0 ring-transparent">
<CardHeader>
<CardTitle className="text-white text-base">Setup Instructions</CardTitle>
<CardDescription className="text-slate-400">
<CardTitle className="text-foreground text-base">Setup Instructions</CardTitle>
<CardDescription className="text-muted-foreground">
Follow these steps to connect your WhatsApp Business API.
</CardDescription>
</CardHeader>
<CardContent>
<Accordion>
<AccordionItem className="border-slate-700">
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
<AccordionItem className="border-border">
<AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
<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>
Create a Meta App
</span>
</AccordionTrigger>
<AccordionContent className="text-slate-400">
<AccordionContent className="text-muted-foreground">
<ol className="list-decimal list-inside space-y-1 text-sm">
<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>
@ -762,14 +762,14 @@ export function WhatsAppConfig() {
</AccordionContent>
</AccordionItem>
<AccordionItem className="border-slate-700">
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
<AccordionItem className="border-border">
<AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
<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>
Add WhatsApp Product
</span>
</AccordionTrigger>
<AccordionContent className="text-slate-400">
<AccordionContent className="text-muted-foreground">
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>In your app dashboard, click &quot;Add Product&quot;</li>
<li>Find &quot;WhatsApp&quot; and click &quot;Set Up&quot;</li>
@ -778,43 +778,43 @@ export function WhatsAppConfig() {
</AccordionContent>
</AccordionItem>
<AccordionItem className="border-slate-700">
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
<AccordionItem className="border-border">
<AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
<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>
Get API Credentials
</span>
</AccordionTrigger>
<AccordionContent className="text-slate-400">
<AccordionContent className="text-muted-foreground">
<ol className="list-decimal list-inside space-y-1 text-sm">
<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-slate-200">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>Copy your <strong className="text-foreground">Phone Number ID</strong></li>
<li>Copy your <strong className="text-foreground">WhatsApp Business Account ID</strong></li>
<li>Generate a <strong className="text-foreground">Permanent Access Token</strong> from Business Settings &gt; System Users</li>
</ol>
</AccordionContent>
</AccordionItem>
<AccordionItem className="border-slate-700">
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
<AccordionItem className="border-border">
<AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
<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>
Configure Webhooks
</span>
</AccordionTrigger>
<AccordionContent className="text-slate-400">
<AccordionContent className="text-muted-foreground">
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to WhatsApp &gt; Configuration</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>Enter the same <strong className="text-slate-200">Verify Token</strong> you set here</li>
<li>Paste the <strong className="text-foreground">Webhook Callback URL</strong> from above</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>
</ol>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="mt-4 pt-4 border-t border-slate-700">
<div className="mt-4 pt-4 border-t border-border">
<a
href="https://developers.facebook.com/docs/whatsapp/cloud-api/get-started"
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(
"group inline-flex flex-nowrap items-center gap-1.5 rounded-sm px-2 py-1 whitespace-nowrap transition",
hasOnValueChange
? "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
? "cursor-pointer hover:bg-muted"
: "cursor-default",
)}
onClick={(e) => {
@ -147,9 +147,9 @@ const LegendItem = ({
<p
className={cx(
"truncate text-xs whitespace-nowrap",
"text-gray-700 dark:text-gray-300",
"text-muted-foreground",
hasOnValueChange &&
"group-hover:text-gray-900 dark:group-hover:text-gray-50",
"group-hover:text-foreground",
activeLegend && activeLegend !== name ? "opacity-40" : "opacity-100",
)}
>
@ -194,8 +194,8 @@ const ScrollButton = ({ icon, onClick, disabled }: ScrollButtonProps) => {
className={cx(
"group inline-flex size-5 items-center truncate rounded-sm transition",
disabled
? "cursor-not-allowed text-gray-400 dark:text-gray-600"
: "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-not-allowed text-muted-foreground"
: "cursor-pointer text-muted-foreground hover:bg-muted hover:text-foreground",
)}
disabled={disabled}
onClick={(e) => {
@ -358,7 +358,7 @@ const Legend = React.forwardRef<HTMLOListElement, LegendProps>((props, ref) => {
ref={scrollButtonsRef}
className={cx(
"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
@ -470,13 +470,13 @@ const ChartTooltip = ({
<div
className={cx(
"rounded-md border text-sm shadow-md",
"border-gray-200 dark:border-gray-800",
"bg-white dark:bg-gray-950",
"border-border",
"bg-popover",
)}
>
<div className={cx("border-b border-inherit px-4 py-2")}>
<p
className={cx("font-medium", "text-gray-900 dark:text-gray-50")}
className={cx("font-medium", "text-popover-foreground")}
>
{label}
</p>
@ -498,7 +498,7 @@ const ChartTooltip = ({
<p
className={cx(
"text-right whitespace-nowrap",
"text-gray-700 dark:text-gray-300",
"text-muted-foreground",
)}
>
{category}
@ -507,7 +507,7 @@ const ChartTooltip = ({
<p
className={cx(
"text-right font-medium whitespace-nowrap tabular-nums",
"text-gray-900 dark:text-gray-50",
"text-popover-foreground",
)}
>
{valueFormatter(value)}
@ -685,7 +685,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
>
{showGridLines ? (
<CartesianGrid
className={cx("stroke-gray-200 stroke-1 dark:stroke-gray-800")}
className={cx("stroke-border stroke-1")}
horizontal={layout !== "vertical"}
vertical={layout === "vertical"}
/>
@ -700,7 +700,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
stroke=""
className={cx(
"text-xs",
"fill-gray-500 dark:fill-gray-500",
"fill-muted-foreground",
{ "mt-4": layout !== "vertical" },
)}
tickLine={false}
@ -730,7 +730,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
<Label
position="insideBottom"
offset={-20}
className="fill-gray-800 text-sm font-medium dark:fill-gray-200"
className="fill-foreground text-sm font-medium"
>
{xAxisLabel}
</Label>
@ -745,7 +745,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
stroke=""
className={cx(
"text-xs",
"fill-gray-500 dark:fill-gray-500",
"fill-muted-foreground",
)}
tick={{
transform:
@ -776,7 +776,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
style={{ textAnchor: "middle" }}
angle={-90}
offset={-15}
className="fill-gray-800 text-sm font-medium dark:fill-gray-200"
className="fill-foreground text-sm font-medium"
>
{yAxisLabel}
</Label>

View File

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

View File

@ -31,7 +31,7 @@ function DialogOverlay({
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
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
)}
{...props}

View File

@ -28,7 +28,7 @@ function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
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
)}
{...props}

View File

@ -16,9 +16,9 @@ function Switch({
data-slot="switch"
className={cn(
"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",
"data-[checked]:bg-primary data-[unchecked]:bg-slate-700",
"data-[checked]:bg-primary data-[unchecked]:bg-muted",
className,
)}
{...props}
@ -26,7 +26,7 @@ function Switch({
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
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",
)}
/>

View File

@ -10,20 +10,26 @@ import {
} from "react";
import {
DEFAULT_MODE,
DEFAULT_THEME,
MODE_STORAGE_KEY,
STORAGE_KEY,
isMode,
isThemeId,
type Mode,
type ThemeId,
} 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
* `document.documentElement.dataset.theme` before React hydrates, so
* by the time this Provider mounts the page is already painted in
* the right colors. We just have to read what's there and keep it
* in sync going forward.
* The boot script in `src/app/layout.tsx` has already applied both
* `data-theme` and `data-mode` before React hydrates, so by the time
* this Provider mounts the page is already painted correctly. We just
* read what's there and keep it in sync going forward.
*
* Persistence is localStorage only (device-scoped). A future
* follow-up could mirror to `profiles.preferences` for cross-device
@ -34,6 +40,9 @@ import {
interface ThemeContextValue {
theme: ThemeId;
setTheme: (next: ThemeId) => void;
mode: Mode;
setMode: (next: Mode) => void;
toggleMode: () => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
@ -54,8 +63,22 @@ function readInitialTheme(): ThemeId {
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 }) {
const [theme, setThemeState] = useState<ThemeId>(readInitialTheme);
const [mode, setModeState] = useState<Mode>(readInitialMode);
const setTheme = useCallback((next: ThemeId) => {
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.
useEffect(() => {
function onStorage(e: StorageEvent) {
if (e.key !== STORAGE_KEY) return;
if (e.key === STORAGE_KEY) {
if (isThemeId(e.newValue) && e.newValue !== theme) {
setThemeState(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);
return () => window.removeEventListener("storage", onStorage);
}, [theme]);
}, [theme, mode]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<ThemeContext.Provider value={{ theme, setTheme, mode, setMode, toggleMode }}>
{children}
</ThemeContext.Provider>
);
@ -94,12 +141,15 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
export function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) {
// Fallback for components rendered outside the provider — return a
// no-op setter so callers don't crash. The boot script still
// applied the right CSS attribute, so visually the page is fine.
// Fallback for components rendered outside the provider — return
// no-op setters so callers don't crash. The boot script still
// applied the right CSS attributes, so visually the page is fine.
return {
theme: DEFAULT_THEME,
setTheme: () => {},
mode: DEFAULT_MODE,
setMode: () => {},
toggleMode: () => {},
};
}
return ctx;

View File

@ -33,7 +33,7 @@ export const TRIGGER_META: Record<AutomationTriggerType, TriggerMeta> = {
},
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 (
TRIGGER_META[t as AutomationTriggerType] ?? {
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> = {
draft: {
label: "Draft",
classes: "bg-slate-500/10 text-slate-400 border-slate-500/20",
classes: "bg-muted text-muted-foreground border-border",
},
scheduled: {
label: "Scheduled",
@ -47,7 +47,7 @@ export const broadcastStatusConfig: Record<BroadcastStatus, StatusDisplay> = {
export const recipientStatusConfig: Record<RecipientStatus, StatusDisplay> = {
pending: {
label: "Pending",
classes: "bg-slate-500/10 text-slate-400 border-slate-500/20",
classes: "bg-muted text-muted-foreground border-border",
},
sent: {
label: "Sent",

View File

@ -20,7 +20,7 @@ export const templateStatusConfig: Record<
> = {
DRAFT: {
label: 'Draft',
classes: 'bg-slate-600/20 text-slate-400 border-slate-600/30',
classes: 'bg-muted text-muted-foreground border-border',
},
PENDING: {
label: 'Pending',
@ -48,6 +48,6 @@ export const templateStatusConfig: Record<
},
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";
/**
* 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 {
id: ThemeId;
name: string;