mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
Merge pull request #253 from ArnasDon/feat/light-dark-mode
feat(theme): add light/dark mode toggle (#243)
This commit is contained in:
commit
6c5cfdd377
@ -43,18 +43,18 @@ export default function ForgotPasswordPage() {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
|
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||||
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
|
<Card className="w-full max-w-md border-border bg-card">
|
||||||
<CardHeader className="items-center text-center">
|
<CardHeader className="items-center text-center">
|
||||||
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
||||||
<CheckCircle className="h-6 w-6 text-primary" />
|
<CheckCircle className="h-6 w-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-xl text-white">
|
<CardTitle className="text-xl text-foreground">
|
||||||
Check your email
|
Check your email
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
We've sent a password reset link to{" "}
|
We've sent a password reset link to{" "}
|
||||||
<span className="text-white">{email}</span>. Please check your
|
<span className="text-foreground">{email}</span>. Please check your
|
||||||
inbox.
|
inbox.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -62,7 +62,7 @@ export default function ForgotPasswordPage() {
|
|||||||
<Link href="/login">
|
<Link href="/login">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white"
|
className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
>
|
>
|
||||||
Back to sign in
|
Back to sign in
|
||||||
</Button>
|
</Button>
|
||||||
@ -74,14 +74,14 @@ export default function ForgotPasswordPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
|
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||||
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
|
<Card className="w-full max-w-md border-border bg-card">
|
||||||
<CardHeader className="items-center text-center">
|
<CardHeader className="items-center text-center">
|
||||||
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
||||||
<MessageSquare className="h-6 w-6 text-primary" />
|
<MessageSquare className="h-6 w-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-xl text-white">Reset password</CardTitle>
|
<CardTitle className="text-xl text-foreground">Reset password</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
Enter your email and we'll send you a reset link
|
Enter your email and we'll send you a reset link
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -94,7 +94,7 @@ export default function ForgotPasswordPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="email" className="text-slate-300">
|
<Label htmlFor="email" className="text-muted-foreground">
|
||||||
Email
|
Email
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -104,7 +104,7 @@ export default function ForgotPasswordPage() {
|
|||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20"
|
className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ export default function ForgotPasswordPage() {
|
|||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/login"
|
||||||
className="mt-6 flex items-center justify-center gap-2 text-sm text-slate-400 hover:text-slate-300"
|
className="mt-6 flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Back to sign in
|
Back to sign in
|
||||||
|
|||||||
@ -67,8 +67,8 @@ function LoginPageInner() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
|
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||||
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
|
<Card className="w-full max-w-md border-border bg-card">
|
||||||
<CardHeader className="items-center text-center">
|
<CardHeader className="items-center text-center">
|
||||||
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
||||||
{inviteToken ? (
|
{inviteToken ? (
|
||||||
@ -77,10 +77,10 @@ function LoginPageInner() {
|
|||||||
<MessageSquare className="h-6 w-6 text-primary" />
|
<MessageSquare className="h-6 w-6 text-primary" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-xl text-white">
|
<CardTitle className="text-xl text-foreground">
|
||||||
{inviteToken ? "Sign in to accept" : "Welcome back"}
|
{inviteToken ? "Sign in to accept" : "Welcome back"}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
{inviteToken
|
{inviteToken
|
||||||
? "Sign in and we'll take you to the invitation."
|
? "Sign in and we'll take you to the invitation."
|
||||||
: "Sign in to your account"}
|
: "Sign in to your account"}
|
||||||
@ -95,7 +95,7 @@ function LoginPageInner() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="email" className="text-slate-300">
|
<Label htmlFor="email" className="text-muted-foreground">
|
||||||
Email
|
Email
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -105,13 +105,13 @@ function LoginPageInner() {
|
|||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20"
|
className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label htmlFor="password" className="text-slate-300">
|
<Label htmlFor="password" className="text-muted-foreground">
|
||||||
Password
|
Password
|
||||||
</Label>
|
</Label>
|
||||||
<Link
|
<Link
|
||||||
@ -128,7 +128,7 @@ function LoginPageInner() {
|
|||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20"
|
className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -141,7 +141,7 @@ function LoginPageInner() {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="mt-6 text-center text-sm text-slate-400">
|
<p className="mt-6 text-center text-sm text-muted-foreground">
|
||||||
Don't have an account?{" "}
|
Don't have an account?{" "}
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
|
|||||||
@ -91,18 +91,18 @@ function SignupPageInner() {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
|
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||||
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
|
<Card className="w-full max-w-md border-border bg-card">
|
||||||
<CardHeader className="items-center text-center">
|
<CardHeader className="items-center text-center">
|
||||||
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
||||||
<CheckCircle className="h-6 w-6 text-primary" />
|
<CheckCircle className="h-6 w-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-xl text-white">
|
<CardTitle className="text-xl text-foreground">
|
||||||
Check your email
|
Check your email
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
We've sent a confirmation link to{" "}
|
We've sent a confirmation link to{" "}
|
||||||
<span className="text-white">{email}</span>. Please check your
|
<span className="text-foreground">{email}</span>. Please check your
|
||||||
inbox and click the link to verify your account.
|
inbox and click the link to verify your account.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -116,7 +116,7 @@ function SignupPageInner() {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white"
|
className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
>
|
>
|
||||||
Back to sign in
|
Back to sign in
|
||||||
</Button>
|
</Button>
|
||||||
@ -128,8 +128,8 @@ function SignupPageInner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
|
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||||
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
|
<Card className="w-full max-w-md border-border bg-card">
|
||||||
<CardHeader className="items-center text-center">
|
<CardHeader className="items-center text-center">
|
||||||
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
||||||
{inviteToken ? (
|
{inviteToken ? (
|
||||||
@ -138,10 +138,10 @@ function SignupPageInner() {
|
|||||||
<MessageSquare className="h-6 w-6 text-primary" />
|
<MessageSquare className="h-6 w-6 text-primary" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-xl text-white">
|
<CardTitle className="text-xl text-foreground">
|
||||||
{inviteToken ? "Create account & join" : "Create account"}
|
{inviteToken ? "Create account & join" : "Create account"}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
{inviteToken
|
{inviteToken
|
||||||
? "Verify your email, then accept the invitation to join your team."
|
? "Verify your email, then accept the invitation to join your team."
|
||||||
: "Get started with CRM Template for WhatsApp"}
|
: "Get started with CRM Template for WhatsApp"}
|
||||||
@ -156,7 +156,7 @@ function SignupPageInner() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="fullName" className="text-slate-300">
|
<Label htmlFor="fullName" className="text-muted-foreground">
|
||||||
Full name
|
Full name
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -166,12 +166,12 @@ function SignupPageInner() {
|
|||||||
value={fullName}
|
value={fullName}
|
||||||
onChange={(e) => setFullName(e.target.value)}
|
onChange={(e) => setFullName(e.target.value)}
|
||||||
required
|
required
|
||||||
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20"
|
className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="email" className="text-slate-300">
|
<Label htmlFor="email" className="text-muted-foreground">
|
||||||
Email
|
Email
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -181,12 +181,12 @@ function SignupPageInner() {
|
|||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20"
|
className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="password" className="text-slate-300">
|
<Label htmlFor="password" className="text-muted-foreground">
|
||||||
Password
|
Password
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -196,12 +196,12 @@ function SignupPageInner() {
|
|||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20"
|
className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="confirmPassword" className="text-slate-300">
|
<Label htmlFor="confirmPassword" className="text-muted-foreground">
|
||||||
Confirm password
|
Confirm password
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -211,7 +211,7 @@ function SignupPageInner() {
|
|||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500 focus-visible:border-primary focus-visible:ring-primary/20"
|
className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -224,7 +224,7 @@ function SignupPageInner() {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="mt-6 text-center text-sm text-slate-400">
|
<p className="mt-6 text-center text-sm text-muted-foreground">
|
||||||
Already have an account?{" "}
|
Already have an account?{" "}
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
|
|||||||
@ -87,21 +87,21 @@ export default function AutomationLogsPage({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push("/automations")}
|
onClick={() => router.push("/automations")}
|
||||||
className="flex h-8 w-8 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-slate-800 hover:text-white"
|
className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||||
aria-label="Back"
|
aria-label="Back"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white">{automation.name}</h1>
|
<h1 className="text-2xl font-bold text-foreground">{automation.name}</h1>
|
||||||
<p className="mt-0.5 text-sm text-slate-400">Execution logs</p>
|
<p className="mt-0.5 text-sm text-muted-foreground">Execution logs</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{logs.length === 0 ? (
|
{logs.length === 0 ? (
|
||||||
<div className="flex h-48 flex-col items-center justify-center rounded-xl border border-dashed border-slate-800 bg-slate-900/40">
|
<div className="flex h-48 flex-col items-center justify-center rounded-xl border border-dashed border-border bg-card/40">
|
||||||
<p className="text-sm text-white">No executions yet</p>
|
<p className="text-sm text-foreground">No executions yet</p>
|
||||||
<p className="mt-1 text-xs text-slate-400">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
Trigger this automation to see runs here.
|
Trigger this automation to see runs here.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -112,7 +112,7 @@ export default function AutomationLogsPage({
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={log.id}
|
key={log.id}
|
||||||
className="rounded-xl border border-slate-800 bg-slate-900"
|
className="rounded-xl border border-border bg-card"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -120,26 +120,26 @@ export default function AutomationLogsPage({
|
|||||||
className="flex w-full items-center gap-3 px-4 py-3 text-left"
|
className="flex w-full items-center gap-3 px-4 py-3 text-left"
|
||||||
>
|
>
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<ChevronDown className="h-4 w-4 text-slate-500" />
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-4 w-4 text-slate-500" />
|
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<StatusBadge status={log.status} />
|
<StatusBadge status={log.status} />
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="truncate text-sm font-medium text-white">
|
<div className="truncate text-sm font-medium text-foreground">
|
||||||
{log.contact?.name ?? log.contact?.phone ?? "Unknown contact"}
|
{log.contact?.name ?? log.contact?.phone ?? "Unknown contact"}
|
||||||
</div>
|
</div>
|
||||||
<div className="truncate text-xs text-slate-500">
|
<div className="truncate text-xs text-muted-foreground">
|
||||||
{log.trigger_event} · {log.steps_executed?.length ?? 0} step
|
{log.trigger_event} · {log.steps_executed?.length ?? 0} step
|
||||||
{log.steps_executed?.length === 1 ? "" : "s"}
|
{log.steps_executed?.length === 1 ? "" : "s"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-500">
|
<div className="text-xs text-muted-foreground">
|
||||||
{formatRelative(log.created_at)}
|
{formatRelative(log.created_at)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="border-t border-slate-800 px-4 py-3">
|
<div className="border-t border-border px-4 py-3">
|
||||||
{log.error_message && (
|
{log.error_message && (
|
||||||
<p className="mb-3 rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
|
<p className="mb-3 rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
|
||||||
{log.error_message}
|
{log.error_message}
|
||||||
@ -150,7 +150,7 @@ export default function AutomationLogsPage({
|
|||||||
<StepRow key={i} result={r} />
|
<StepRow key={i} result={r} />
|
||||||
))}
|
))}
|
||||||
{(log.steps_executed ?? []).length === 0 && (
|
{(log.steps_executed ?? []).length === 0 && (
|
||||||
<li className="text-xs text-slate-500">No steps recorded.</li>
|
<li className="text-xs text-muted-foreground">No steps recorded.</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -196,9 +196,9 @@ function StepRow({ result }: { result: AutomationLogStepResult }) {
|
|||||||
>
|
>
|
||||||
{ok ? <Check className="h-3 w-3" /> : <X className="h-3 w-3" />}
|
{ok ? <Check className="h-3 w-3" /> : <X className="h-3 w-3" />}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-slate-300">{result.step_type}</span>
|
<span className="text-muted-foreground">{result.step_type}</span>
|
||||||
{result.detail && (
|
{result.detail && (
|
||||||
<span className="truncate text-slate-500">— {result.detail}</span>
|
<span className="truncate text-muted-foreground">— {result.detail}</span>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -160,8 +160,8 @@ export default function AutomationsPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white">Automations</h1>
|
<h1 className="text-2xl font-bold text-foreground">Automations</h1>
|
||||||
<p className="mt-1 text-sm text-slate-400">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Build workflows that react to WhatsApp® events automatically.
|
Build workflows that react to WhatsApp® events automatically.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -178,7 +178,7 @@ export default function AutomationsPage() {
|
|||||||
|
|
||||||
{showTemplates && (
|
{showTemplates && (
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-3 text-sm font-semibold text-slate-300">Quick-start templates</h2>
|
<h2 className="mb-3 text-sm font-semibold text-muted-foreground">Quick-start templates</h2>
|
||||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||||
{TEMPLATE_ORDER.map((slug) => {
|
{TEMPLATE_ORDER.map((slug) => {
|
||||||
const t = AUTOMATION_TEMPLATES[slug]
|
const t = AUTOMATION_TEMPLATES[slug]
|
||||||
@ -187,13 +187,13 @@ export default function AutomationsPage() {
|
|||||||
<button
|
<button
|
||||||
key={slug}
|
key={slug}
|
||||||
onClick={() => startFromTemplate(slug)}
|
onClick={() => startFromTemplate(slug)}
|
||||||
className="group flex flex-col items-start rounded-xl border border-slate-800 bg-slate-900 p-4 text-left transition-colors hover:border-primary/50 hover:bg-slate-900/80"
|
className="group flex flex-col items-start rounded-xl border border-border bg-card p-4 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary group-hover:bg-primary/15">
|
<div className="mb-3 flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary group-hover:bg-primary/15">
|
||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-semibold text-white">{t.name}</div>
|
<div className="text-sm font-semibold text-foreground">{t.name}</div>
|
||||||
<p className="mt-1 text-xs text-slate-400">{t.description}</p>
|
<p className="mt-1 text-xs text-muted-foreground">{t.description}</p>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -202,12 +202,12 @@ export default function AutomationsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{automations.length === 0 ? (
|
{automations.length === 0 ? (
|
||||||
<div className="flex h-48 flex-col items-center justify-center rounded-xl border border-dashed border-slate-800 bg-slate-900/40">
|
<div className="flex h-48 flex-col items-center justify-center rounded-xl border border-dashed border-border bg-card/40">
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
||||||
<Zap className="h-6 w-6 text-primary" />
|
<Zap className="h-6 w-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-3 text-sm font-medium text-white">No automations yet</p>
|
<p className="mt-3 text-sm font-medium text-foreground">No automations yet</p>
|
||||||
<p className="mt-1 text-xs text-slate-400">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
Pick a template above or create one from scratch.
|
Pick a template above or create one from scratch.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -233,7 +233,7 @@ export default function AutomationsPage() {
|
|||||||
<DialogTitle>Delete automation</DialogTitle>
|
<DialogTitle>Delete automation</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
This permanently removes{" "}
|
This permanently removes{" "}
|
||||||
<span className="text-white">{pendingDelete?.name}</span> and its execution
|
<span className="text-foreground">{pendingDelete?.name}</span> and its execution
|
||||||
history. This cannot be undone.
|
history. This cannot be undone.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -277,7 +277,7 @@ function AutomationCard({
|
|||||||
}) {
|
}) {
|
||||||
const meta = triggerMeta(automation.trigger_type)
|
const meta = triggerMeta(automation.trigger_type)
|
||||||
return (
|
return (
|
||||||
<li className="rounded-xl border border-slate-800 bg-slate-900 transition-colors hover:border-slate-700">
|
<li className="rounded-xl border border-border bg-card transition-colors hover:border-border">
|
||||||
<div className="flex items-center gap-4 p-4">
|
<div className="flex items-center gap-4 p-4">
|
||||||
<div
|
<div
|
||||||
className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-primary/10"
|
className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-primary/10"
|
||||||
@ -292,7 +292,7 @@ function AutomationCard({
|
|||||||
className="min-w-0 flex-1 text-left"
|
className="min-w-0 flex-1 text-left"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="truncate text-sm font-semibold text-white">
|
<span className="truncate text-sm font-semibold text-foreground">
|
||||||
{automation.name}
|
{automation.name}
|
||||||
</span>
|
</span>
|
||||||
{automation.is_active && (
|
{automation.is_active && (
|
||||||
@ -303,9 +303,9 @@ function AutomationCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{automation.description && (
|
{automation.description && (
|
||||||
<p className="mt-0.5 truncate text-xs text-slate-400">{automation.description}</p>
|
<p className="mt-0.5 truncate text-xs text-muted-foreground">{automation.description}</p>
|
||||||
)}
|
)}
|
||||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-slate-500">
|
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium",
|
"inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium",
|
||||||
@ -332,7 +332,7 @@ function AutomationCard({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
aria-label="Open menu"
|
aria-label="Open menu"
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-slate-800 hover:text-white data-[popup-open]:bg-slate-800"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[popup-open]:bg-muted"
|
||||||
>
|
>
|
||||||
<MoreVertical className="h-4 w-4" />
|
<MoreVertical className="h-4 w-4" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|||||||
@ -50,15 +50,15 @@ interface StatCardProps {
|
|||||||
function StatCard({ label, value, total, icon, color }: StatCardProps) {
|
function StatCard({ label, value, total, icon, color }: StatCardProps) {
|
||||||
const pct = total > 0 ? Math.round((value / total) * 100) : 0;
|
const pct = total > 0 ? Math.round((value / total) * 100) : 0;
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
<div className="rounded-xl border border-border bg-card p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className={`flex h-8 w-8 items-center justify-center rounded-lg ${color}`}>
|
<div className={`flex h-8 w-8 items-center justify-center rounded-lg ${color}`}>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-slate-500">{pct}%</span>
|
<span className="text-xs text-muted-foreground">{pct}%</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-3 text-2xl font-bold text-white">{value.toLocaleString()}</p>
|
<p className="mt-3 text-2xl font-bold text-foreground">{value.toLocaleString()}</p>
|
||||||
<p className="text-xs text-slate-400">{label}</p>
|
<p className="text-xs text-muted-foreground">{label}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -77,8 +77,8 @@ interface FunnelStep {
|
|||||||
function FunnelChart({ steps }: { steps: FunnelStep[] }) {
|
function FunnelChart({ steps }: { steps: FunnelStep[] }) {
|
||||||
const max = Math.max(...steps.map((s) => s.value), 1);
|
const max = Math.max(...steps.map((s) => s.value), 1);
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
<div className="rounded-xl border border-border bg-card p-4">
|
||||||
<h3 className="mb-4 text-sm font-medium text-white">Funnel</h3>
|
<h3 className="mb-4 text-sm font-medium text-foreground">Funnel</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{steps.map((step) => {
|
{steps.map((step) => {
|
||||||
const pctOfMax = Math.max(5, Math.round((step.value / max) * 100));
|
const pctOfMax = Math.max(5, Math.round((step.value / max) * 100));
|
||||||
@ -88,17 +88,17 @@ function FunnelChart({ steps }: { steps: FunnelStep[] }) {
|
|||||||
: 0;
|
: 0;
|
||||||
return (
|
return (
|
||||||
<div key={step.label} className="flex items-center gap-3">
|
<div key={step.label} className="flex items-center gap-3">
|
||||||
<span className="w-20 shrink-0 text-xs text-slate-400">
|
<span className="w-20 shrink-0 text-xs text-muted-foreground">
|
||||||
{step.label}
|
{step.label}
|
||||||
</span>
|
</span>
|
||||||
<div className="relative h-7 flex-1 rounded-full bg-slate-800">
|
<div className="relative h-7 flex-1 rounded-full bg-muted">
|
||||||
<div
|
<div
|
||||||
className={`h-7 rounded-full ${step.color} transition-[width] duration-500`}
|
className={`h-7 rounded-full ${step.color} transition-[width] duration-500`}
|
||||||
style={{ width: `${pctOfMax}%` }}
|
style={{ width: `${pctOfMax}%` }}
|
||||||
/>
|
/>
|
||||||
<span className="absolute inset-0 flex items-center px-3 text-xs font-medium text-white">
|
<span className="absolute inset-0 flex items-center px-3 text-xs font-medium text-foreground">
|
||||||
{step.value.toLocaleString()}
|
{step.value.toLocaleString()}
|
||||||
<span className="ml-2 text-slate-300/80">
|
<span className="ml-2 text-muted-foreground/80">
|
||||||
({pctOfSent}%)
|
({pctOfSent}%)
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@ -280,20 +280,20 @@ export default function BroadcastDetailPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => router.push('/broadcasts')}
|
onClick={() => router.push('/broadcasts')}
|
||||||
className="border-slate-700"
|
className="border-border"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h1 className="text-2xl font-bold text-white">{broadcast.name}</h1>
|
<h1 className="text-2xl font-bold text-foreground">{broadcast.name}</h1>
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${status.classes}`}
|
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${status.classes}`}
|
||||||
>
|
>
|
||||||
{status.label}
|
{status.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex items-center gap-3 text-sm text-slate-400">
|
<div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
|
||||||
<span>Template: {broadcast.template_name}</span>
|
<span>Template: {broadcast.template_name}</span>
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
<span>
|
<span>
|
||||||
@ -315,7 +315,7 @@ export default function BroadcastDetailPage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setConfirmDelete(false)}
|
onClick={() => setConfirmDelete(false)}
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
className="h-7 border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800"
|
className="h-7 border-border bg-transparent text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@ -354,7 +354,7 @@ export default function BroadcastDetailPage() {
|
|||||||
value={broadcast.total_recipients}
|
value={broadcast.total_recipients}
|
||||||
total={broadcast.total_recipients}
|
total={broadcast.total_recipients}
|
||||||
icon={<Users className="h-4 w-4" />}
|
icon={<Users className="h-4 w-4" />}
|
||||||
color="bg-slate-800 text-slate-300"
|
color="bg-muted text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Sent"
|
label="Sent"
|
||||||
@ -396,9 +396,9 @@ export default function BroadcastDetailPage() {
|
|||||||
<FunnelChart steps={funnelSteps} />
|
<FunnelChart steps={funnelSteps} />
|
||||||
|
|
||||||
{/* Recipients Table */}
|
{/* Recipients Table */}
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900">
|
<div className="rounded-xl border border-border bg-card">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-800 px-4 py-3">
|
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border px-4 py-3">
|
||||||
<h2 className="text-sm font-medium text-white">
|
<h2 className="text-sm font-medium text-foreground">
|
||||||
Recipients ({filteredRecipients.length}
|
Recipients ({filteredRecipients.length}
|
||||||
{statusFilter !== 'all' ? ` of ${recipients.length}` : ''})
|
{statusFilter !== 'all' ? ` of ${recipients.length}` : ''})
|
||||||
</h2>
|
</h2>
|
||||||
@ -409,7 +409,7 @@ export default function BroadcastDetailPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -419,11 +419,11 @@ export default function BroadcastDetailPage() {
|
|||||||
: getRecipientStatus(statusFilter).label}
|
: getRecipientStatus(statusFilter).label}
|
||||||
<ChevronDown className="h-3 w-3" />
|
<ChevronDown className="h-3 w-3" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="border-slate-700 bg-slate-900">
|
<DropdownMenuContent className="border-border bg-popover">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setStatusFilter('all')}
|
onClick={() => setStatusFilter('all')}
|
||||||
className={
|
className={
|
||||||
statusFilter === 'all' ? 'text-primary' : 'text-slate-300'
|
statusFilter === 'all' ? 'text-primary' : 'text-popover-foreground'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
All statuses
|
All statuses
|
||||||
@ -435,7 +435,7 @@ export default function BroadcastDetailPage() {
|
|||||||
className={
|
className={
|
||||||
statusFilter === s
|
statusFilter === s
|
||||||
? 'text-primary'
|
? 'text-primary'
|
||||||
: 'text-slate-300'
|
: 'text-popover-foreground'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{getRecipientStatus(s).label}
|
{getRecipientStatus(s).label}
|
||||||
@ -449,7 +449,7 @@ export default function BroadcastDetailPage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
disabled={recipients.length === 0}
|
disabled={recipients.length === 0}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
<Download className="h-3.5 w-3.5" />
|
<Download className="h-3.5 w-3.5" />
|
||||||
Export CSV
|
Export CSV
|
||||||
@ -459,7 +459,7 @@ export default function BroadcastDetailPage() {
|
|||||||
|
|
||||||
{filteredRecipients.length === 0 ? (
|
{filteredRecipients.length === 0 ? (
|
||||||
<div className="flex h-32 items-center justify-center">
|
<div className="flex h-32 items-center justify-center">
|
||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm text-muted-foreground">
|
||||||
{recipients.length === 0
|
{recipients.length === 0
|
||||||
? 'No recipients found.'
|
? 'No recipients found.'
|
||||||
: 'No recipients match this filter.'}
|
: 'No recipients match this filter.'}
|
||||||
@ -469,25 +469,25 @@ export default function BroadcastDetailPage() {
|
|||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
<TableRow className="border-border hover:bg-transparent">
|
||||||
<TableHead className="text-slate-400">Contact</TableHead>
|
<TableHead className="text-muted-foreground">Contact</TableHead>
|
||||||
<TableHead className="text-slate-400">Phone</TableHead>
|
<TableHead className="text-muted-foreground">Phone</TableHead>
|
||||||
<TableHead className="text-slate-400">Status</TableHead>
|
<TableHead className="text-muted-foreground">Status</TableHead>
|
||||||
<TableHead className="text-slate-400">Sent</TableHead>
|
<TableHead className="text-muted-foreground">Sent</TableHead>
|
||||||
<TableHead className="text-slate-400">Delivered</TableHead>
|
<TableHead className="text-muted-foreground">Delivered</TableHead>
|
||||||
<TableHead className="text-slate-400">Read</TableHead>
|
<TableHead className="text-muted-foreground">Read</TableHead>
|
||||||
<TableHead className="text-slate-400">Error</TableHead>
|
<TableHead className="text-muted-foreground">Error</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredRecipients.map((recipient) => {
|
{filteredRecipients.map((recipient) => {
|
||||||
const rStatus = getRecipientStatus(recipient.status);
|
const rStatus = getRecipientStatus(recipient.status);
|
||||||
return (
|
return (
|
||||||
<TableRow key={recipient.id} className="border-slate-800">
|
<TableRow key={recipient.id} className="border-border">
|
||||||
<TableCell className="font-medium text-white">
|
<TableCell className="font-medium text-foreground">
|
||||||
{recipient.contact?.name ?? 'Unknown'}
|
{recipient.contact?.name ?? 'Unknown'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-300">
|
<TableCell className="text-muted-foreground">
|
||||||
{recipient.contact?.phone ?? '-'}
|
{recipient.contact?.phone ?? '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -497,17 +497,17 @@ export default function BroadcastDetailPage() {
|
|||||||
{rStatus.label}
|
{rStatus.label}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-400">
|
<TableCell className="text-muted-foreground">
|
||||||
{recipient.sent_at
|
{recipient.sent_at
|
||||||
? new Date(recipient.sent_at).toLocaleString()
|
? new Date(recipient.sent_at).toLocaleString()
|
||||||
: '-'}
|
: '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-400">
|
<TableCell className="text-muted-foreground">
|
||||||
{recipient.delivered_at
|
{recipient.delivered_at
|
||||||
? new Date(recipient.delivered_at).toLocaleString()
|
? new Date(recipient.delivered_at).toLocaleString()
|
||||||
: '-'}
|
: '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-400">
|
<TableCell className="text-muted-foreground">
|
||||||
{recipient.read_at
|
{recipient.read_at
|
||||||
? new Date(recipient.read_at).toLocaleString()
|
? new Date(recipient.read_at).toLocaleString()
|
||||||
: '-'}
|
: '-'}
|
||||||
|
|||||||
@ -129,8 +129,8 @@ export default function NewBroadcastPage() {
|
|||||||
<div className="mx-auto max-w-3xl space-y-8">
|
<div className="mx-auto max-w-3xl space-y-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white">New Broadcast</h1>
|
<h1 className="text-2xl font-bold text-foreground">New Broadcast</h1>
|
||||||
<p className="mt-1 text-sm text-slate-400">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Create and send a broadcast message to your contacts.
|
Create and send a broadcast message to your contacts.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -150,14 +150,14 @@ export default function NewBroadcastPage() {
|
|||||||
? 'bg-primary text-primary-foreground'
|
? 'bg-primary text-primary-foreground'
|
||||||
: isActive
|
: isActive
|
||||||
? 'border-2 border-primary bg-primary/10 text-primary'
|
? 'border-2 border-primary bg-primary/10 text-primary'
|
||||||
: 'border border-slate-700 bg-slate-800 text-slate-500'
|
: 'border border-border bg-muted text-muted-foreground'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isCompleted ? <Check className="h-4 w-4" /> : index + 1}
|
{isCompleted ? <Check className="h-4 w-4" /> : index + 1}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`hidden text-sm font-medium sm:block ${
|
className={`hidden text-sm font-medium sm:block ${
|
||||||
isActive ? 'text-white' : isCompleted ? 'text-primary' : 'text-slate-500'
|
isActive ? 'text-foreground' : isCompleted ? 'text-primary' : 'text-muted-foreground'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{step.label}
|
{step.label}
|
||||||
@ -166,7 +166,7 @@ export default function NewBroadcastPage() {
|
|||||||
{index < steps.length - 1 && (
|
{index < steps.length - 1 && (
|
||||||
<div
|
<div
|
||||||
className={`mx-3 h-px flex-1 ${
|
className={`mx-3 h-px flex-1 ${
|
||||||
index < currentStep ? 'bg-primary' : 'bg-slate-800'
|
index < currentStep ? 'bg-primary' : 'bg-muted'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -43,10 +43,10 @@ function RateCell({
|
|||||||
const pct = percent(value, total);
|
const pct = percent(value, total);
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="w-10 text-right text-xs tabular-nums text-slate-300">
|
<span className="w-10 text-right text-xs tabular-nums text-muted-foreground">
|
||||||
{pct}%
|
{pct}%
|
||||||
</span>
|
</span>
|
||||||
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-slate-800">
|
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-muted">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full ${color}`}
|
className={`h-1.5 rounded-full ${color}`}
|
||||||
style={{ width: `${pct}%` }}
|
style={{ width: `${pct}%` }}
|
||||||
@ -155,7 +155,7 @@ export default function BroadcastsPage() {
|
|||||||
<div
|
<div
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
aria-label="Broadcast in progress"
|
aria-label="Broadcast in progress"
|
||||||
className="broadcast-indeterminate fixed inset-x-0 top-0 z-40 h-0.5 overflow-hidden bg-slate-800"
|
className="broadcast-indeterminate fixed inset-x-0 top-0 z-40 h-0.5 overflow-hidden bg-muted"
|
||||||
>
|
>
|
||||||
<div className="broadcast-indeterminate-bar h-0.5 bg-primary" />
|
<div className="broadcast-indeterminate-bar h-0.5 bg-primary" />
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
@ -179,8 +179,8 @@ export default function BroadcastsPage() {
|
|||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white">Broadcasts</h1>
|
<h1 className="text-2xl font-bold text-foreground">Broadcasts</h1>
|
||||||
<p className="mt-1 text-sm text-slate-400">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Send bulk messages to your contacts using approved templates.
|
Send bulk messages to your contacts using approved templates.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -196,10 +196,10 @@ export default function BroadcastsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{broadcasts.length === 0 ? (
|
{broadcasts.length === 0 ? (
|
||||||
<div className="flex h-64 flex-col items-center justify-center rounded-xl border border-slate-800 bg-slate-900">
|
<div className="flex h-64 flex-col items-center justify-center rounded-xl border border-border bg-card">
|
||||||
<Radio className="mb-3 h-10 w-10 text-slate-600" />
|
<Radio className="mb-3 h-10 w-10 text-muted-foreground" />
|
||||||
<p className="text-sm font-medium text-white">No broadcasts yet</p>
|
<p className="text-sm font-medium text-foreground">No broadcasts yet</p>
|
||||||
<p className="mt-1 text-xs text-slate-400">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
Create your first broadcast to reach your contacts at scale.
|
Create your first broadcast to reach your contacts at scale.
|
||||||
</p>
|
</p>
|
||||||
<GatedButton
|
<GatedButton
|
||||||
@ -213,19 +213,19 @@ export default function BroadcastsPage() {
|
|||||||
</GatedButton>
|
</GatedButton>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto rounded-xl border border-slate-800 bg-slate-900">
|
<div className="overflow-x-auto rounded-xl border border-border bg-card">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
<TableRow className="border-border hover:bg-transparent">
|
||||||
<TableHead className="text-slate-400">Name</TableHead>
|
<TableHead className="text-muted-foreground">Name</TableHead>
|
||||||
<TableHead className="hidden text-slate-400 md:table-cell">Template</TableHead>
|
<TableHead className="hidden text-muted-foreground md:table-cell">Template</TableHead>
|
||||||
<TableHead className="hidden text-right text-slate-400 sm:table-cell">
|
<TableHead className="hidden text-right text-muted-foreground sm:table-cell">
|
||||||
Recipients
|
Recipients
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="hidden text-slate-400 lg:table-cell">Delivery</TableHead>
|
<TableHead className="hidden text-muted-foreground lg:table-cell">Delivery</TableHead>
|
||||||
<TableHead className="hidden text-slate-400 lg:table-cell">Read</TableHead>
|
<TableHead className="hidden text-muted-foreground lg:table-cell">Read</TableHead>
|
||||||
<TableHead className="text-slate-400">Status</TableHead>
|
<TableHead className="text-muted-foreground">Status</TableHead>
|
||||||
<TableHead className="hidden text-slate-400 sm:table-cell">Date</TableHead>
|
<TableHead className="hidden text-muted-foreground sm:table-cell">Date</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -234,16 +234,16 @@ export default function BroadcastsPage() {
|
|||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={broadcast.id}
|
key={broadcast.id}
|
||||||
className="cursor-pointer border-slate-800 hover:bg-slate-800/50"
|
className="cursor-pointer border-border hover:bg-muted/50"
|
||||||
onClick={() => router.push(`/broadcasts/${broadcast.id}`)}
|
onClick={() => router.push(`/broadcasts/${broadcast.id}`)}
|
||||||
>
|
>
|
||||||
<TableCell className="font-medium text-white">
|
<TableCell className="font-medium text-foreground">
|
||||||
{broadcast.name}
|
{broadcast.name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden text-slate-300 md:table-cell">
|
<TableCell className="hidden text-muted-foreground md:table-cell">
|
||||||
{broadcast.template_name}
|
{broadcast.template_name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden text-right text-slate-300 tabular-nums sm:table-cell">
|
<TableCell className="hidden text-right text-muted-foreground tabular-nums sm:table-cell">
|
||||||
{broadcast.total_recipients}
|
{broadcast.total_recipients}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden lg:table-cell">
|
<TableCell className="hidden lg:table-cell">
|
||||||
@ -273,7 +273,7 @@ export default function BroadcastsPage() {
|
|||||||
{status.label}
|
{status.label}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden text-slate-400 sm:table-cell">
|
<TableCell className="hidden text-muted-foreground sm:table-cell">
|
||||||
{new Date(broadcast.created_at).toLocaleDateString()}
|
{new Date(broadcast.created_at).toLocaleDateString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@ -217,8 +217,8 @@ export default function ContactsPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white">Contacts</h1>
|
<h1 className="text-2xl font-bold text-foreground">Contacts</h1>
|
||||||
<p className="text-sm text-slate-400 mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
Manage your contact list. {totalCount > 0 && `${totalCount} total contacts.`}
|
Manage your contact list. {totalCount > 0 && `${totalCount} total contacts.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -227,7 +227,7 @@ export default function ContactsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setCustomFieldsOpen(true)}
|
onClick={() => setCustomFieldsOpen(true)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
<SlidersHorizontal className="size-4" />
|
<SlidersHorizontal className="size-4" />
|
||||||
Custom fields
|
Custom fields
|
||||||
@ -238,7 +238,7 @@ export default function ContactsPage() {
|
|||||||
canAct={canEdit}
|
canAct={canEdit}
|
||||||
gateReason="add or import contacts"
|
gateReason="add or import contacts"
|
||||||
onClick={() => setImportOpen(true)}
|
onClick={() => setImportOpen(true)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
<Upload className="size-4" />
|
<Upload className="size-4" />
|
||||||
Import
|
Import
|
||||||
@ -257,7 +257,7 @@ export default function ContactsPage() {
|
|||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative max-w-sm">
|
<div className="relative max-w-sm">
|
||||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-slate-500" />
|
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -267,40 +267,40 @@ export default function ContactsPage() {
|
|||||||
setPage(0);
|
setPage(0);
|
||||||
}}
|
}}
|
||||||
placeholder="Search by name, phone, or email..."
|
placeholder="Search by name, phone, or email..."
|
||||||
className="pl-8 bg-slate-900 border-slate-700 text-white placeholder:text-slate-500"
|
className="pl-8 bg-card border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="rounded-lg border border-slate-800 overflow-hidden">
|
<div className="rounded-lg border border-border overflow-hidden">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
<TableRow className="border-border hover:bg-transparent">
|
||||||
<TableHead className="text-slate-400">Name</TableHead>
|
<TableHead className="text-muted-foreground">Name</TableHead>
|
||||||
<TableHead className="text-slate-400">Phone</TableHead>
|
<TableHead className="text-muted-foreground">Phone</TableHead>
|
||||||
<TableHead className="text-slate-400 hidden md:table-cell">Email</TableHead>
|
<TableHead className="text-muted-foreground hidden md:table-cell">Email</TableHead>
|
||||||
<TableHead className="text-slate-400 hidden lg:table-cell">Company</TableHead>
|
<TableHead className="text-muted-foreground hidden lg:table-cell">Company</TableHead>
|
||||||
<TableHead className="text-slate-400 hidden md:table-cell">Tags</TableHead>
|
<TableHead className="text-muted-foreground hidden md:table-cell">Tags</TableHead>
|
||||||
<TableHead className="text-slate-400 hidden lg:table-cell">Created</TableHead>
|
<TableHead className="text-muted-foreground hidden lg:table-cell">Created</TableHead>
|
||||||
<TableHead className="text-slate-400 w-12" />
|
<TableHead className="text-muted-foreground w-12" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<TableRow className="border-slate-800">
|
<TableRow className="border-border">
|
||||||
<TableCell colSpan={7} className="text-center py-12">
|
<TableCell colSpan={7} className="text-center py-12">
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<Loader2 className="size-6 animate-spin text-primary" />
|
<Loader2 className="size-6 animate-spin text-primary" />
|
||||||
<p className="text-sm text-slate-500">Loading contacts...</p>
|
<p className="text-sm text-muted-foreground">Loading contacts...</p>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : contacts.length === 0 ? (
|
) : contacts.length === 0 ? (
|
||||||
<TableRow className="border-slate-800">
|
<TableRow className="border-border">
|
||||||
<TableCell colSpan={7} className="text-center py-12">
|
<TableCell colSpan={7} className="text-center py-12">
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<Users className="size-8 text-slate-600" />
|
<Users className="size-8 text-muted-foreground" />
|
||||||
<p className="text-sm text-slate-500">
|
<p className="text-sm text-muted-foreground">
|
||||||
{search ? 'No contacts match your search.' : 'No contacts yet.'}
|
{search ? 'No contacts match your search.' : 'No contacts yet.'}
|
||||||
</p>
|
</p>
|
||||||
{!search && (
|
{!search && (
|
||||||
@ -308,7 +308,7 @@ export default function ContactsPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={openAddForm}
|
onClick={openAddForm}
|
||||||
className="mt-2 border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="mt-2 border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
<Plus className="size-3.5" />
|
<Plus className="size-3.5" />
|
||||||
Add your first contact
|
Add your first contact
|
||||||
@ -321,20 +321,20 @@ export default function ContactsPage() {
|
|||||||
contacts.map((contact) => (
|
contacts.map((contact) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={contact.id}
|
key={contact.id}
|
||||||
className="border-slate-800 hover:bg-slate-900/50 cursor-pointer"
|
className="border-border hover:bg-muted/50 cursor-pointer"
|
||||||
onClick={() => openDetail(contact.id)}
|
onClick={() => openDetail(contact.id)}
|
||||||
>
|
>
|
||||||
<TableCell className="text-white font-medium">
|
<TableCell className="text-foreground font-medium">
|
||||||
{contact.name || <span className="text-slate-500 italic">Unnamed</span>}
|
{contact.name || <span className="text-muted-foreground italic">Unnamed</span>}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-300 font-mono text-xs">
|
<TableCell className="text-muted-foreground font-mono text-xs">
|
||||||
{contact.phone}
|
{contact.phone}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-400 hidden md:table-cell text-sm">
|
<TableCell className="text-muted-foreground hidden md:table-cell text-sm">
|
||||||
{contact.email || <span className="text-slate-600">-</span>}
|
{contact.email || <span className="text-muted-foreground">-</span>}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-400 hidden lg:table-cell text-sm">
|
<TableCell className="text-muted-foreground hidden lg:table-cell text-sm">
|
||||||
{contact.company || <span className="text-slate-600">-</span>}
|
{contact.company || <span className="text-muted-foreground">-</span>}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden md:table-cell">
|
<TableCell className="hidden md:table-cell">
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
@ -352,16 +352,16 @@ export default function ContactsPage() {
|
|||||||
</span>
|
</span>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<span className="text-slate-600 text-xs">-</span>
|
<span className="text-muted-foreground text-xs">-</span>
|
||||||
)}
|
)}
|
||||||
{contact.tags && contact.tags.length > 3 && (
|
{contact.tags && contact.tags.length > 3 && (
|
||||||
<span className="text-[10px] text-slate-500">
|
<span className="text-[10px] text-muted-foreground">
|
||||||
+{contact.tags.length - 3}
|
+{contact.tags.length - 3}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-slate-500 text-xs hidden lg:table-cell">
|
<TableCell className="text-muted-foreground text-xs hidden lg:table-cell">
|
||||||
{new Date(contact.created_at).toLocaleDateString('en-US', {
|
{new Date(contact.created_at).toLocaleDateString('en-US', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@ -375,7 +375,7 @@ export default function ContactsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
className="text-slate-400 hover:text-white"
|
className="text-muted-foreground hover:text-foreground"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -384,19 +384,19 @@ export default function ContactsPage() {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
className="bg-slate-900 border-slate-700"
|
className="bg-popover border-border"
|
||||||
>
|
>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
openEditForm(contact);
|
openEditForm(contact);
|
||||||
}}
|
}}
|
||||||
className="text-slate-300 focus:bg-slate-800 focus:text-white"
|
className="text-popover-foreground focus:bg-muted focus:text-foreground"
|
||||||
>
|
>
|
||||||
<Pencil className="size-4" />
|
<Pencil className="size-4" />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="bg-slate-700" />
|
<DropdownMenuSeparator className="bg-border" />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -420,7 +420,7 @@ export default function ContactsPage() {
|
|||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
Showing {page * PAGE_SIZE + 1}-{Math.min((page + 1) * PAGE_SIZE, totalCount)} of{' '}
|
Showing {page * PAGE_SIZE + 1}-{Math.min((page + 1) * PAGE_SIZE, totalCount)} of{' '}
|
||||||
{totalCount}
|
{totalCount}
|
||||||
</p>
|
</p>
|
||||||
@ -430,11 +430,11 @@ export default function ContactsPage() {
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
disabled={!hasPrev}
|
disabled={!hasPrev}
|
||||||
onClick={() => setPage((p) => p - 1)}
|
onClick={() => setPage((p) => p - 1)}
|
||||||
className="border-slate-700 text-slate-400 hover:bg-slate-800 hover:text-white disabled:opacity-30"
|
className="border-border text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="size-4" />
|
<ChevronLeft className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-xs text-slate-400 px-2">
|
<span className="text-xs text-muted-foreground px-2">
|
||||||
Page {page + 1} of {totalPages}
|
Page {page + 1} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
@ -442,7 +442,7 @@ export default function ContactsPage() {
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
disabled={!hasNext}
|
disabled={!hasNext}
|
||||||
onClick={() => setPage((p) => p + 1)}
|
onClick={() => setPage((p) => p + 1)}
|
||||||
className="border-slate-700 text-slate-400 hover:bg-slate-800 hover:text-white disabled:opacity-30"
|
className="border-border text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<ChevronRight className="size-4" />
|
<ChevronRight className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -491,22 +491,22 @@ export default function ContactsPage() {
|
|||||||
|
|
||||||
{/* Delete Confirmation */}
|
{/* Delete Confirmation */}
|
||||||
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||||
<DialogContent className="bg-slate-900 border-slate-700 text-slate-200 sm:max-w-sm">
|
<DialogContent className="bg-popover border-border text-popover-foreground sm:max-w-sm">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">Delete Contact</DialogTitle>
|
<DialogTitle className="text-popover-foreground">Delete Contact</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
Are you sure you want to delete{' '}
|
Are you sure you want to delete{' '}
|
||||||
<span className="text-slate-200 font-medium">
|
<span className="text-popover-foreground font-medium">
|
||||||
{deleteTarget?.name || deleteTarget?.phone}
|
{deleteTarget?.name || deleteTarget?.phone}
|
||||||
</span>
|
</span>
|
||||||
? This action cannot be undone.
|
? This action cannot be undone.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter className="bg-slate-900 border-slate-700">
|
<DialogFooter className="bg-popover border-border">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setDeleteConfirmOpen(false)}
|
onClick={() => setDeleteConfirmOpen(false)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -27,10 +27,10 @@ function DashboardShellInner({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen items-center justify-center bg-slate-950">
|
<div className="flex h-screen items-center justify-center bg-background">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
<div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||||
<p className="text-sm text-slate-400">Loading...</p>
|
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -39,7 +39,7 @@ function DashboardShellInner({ children }: { children: React.ReactNode }) {
|
|||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-slate-950">
|
<div className="flex h-screen overflow-hidden bg-background">
|
||||||
<Sidebar open={sidebarOpen} onClose={closeSidebar} />
|
<Sidebar open={sidebarOpen} onClose={closeSidebar} />
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
<Header onOpenSidebar={() => setSidebarOpen(true)} />
|
<Header onOpenSidebar={() => setSidebarOpen(true)} />
|
||||||
|
|||||||
@ -122,8 +122,8 @@ export default function DashboardPage() {
|
|||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white">Dashboard</h1>
|
<h1 className="text-2xl font-bold text-foreground">Dashboard</h1>
|
||||||
<p className="mt-1 text-sm text-slate-400">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Live analytics across conversations, contacts, deals, broadcasts, and automations.
|
Live analytics across conversations, contacts, deals, broadcasts, and automations.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -65,14 +65,14 @@ export default function FlowEditorPage() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-slate-500" />
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (notFound || !flow) {
|
if (notFound || !flow) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-center gap-3">
|
<div className="flex h-full flex-col items-center justify-center gap-3">
|
||||||
<p className="text-sm text-slate-400">Flow not found.</p>
|
<p className="text-sm text-muted-foreground">Flow not found.</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push("/flows")}
|
onClick={() => router.push("/flows")}
|
||||||
|
|||||||
@ -68,7 +68,7 @@ const STATUS_META: Record<
|
|||||||
},
|
},
|
||||||
completed: {
|
completed: {
|
||||||
label: "Completed",
|
label: "Completed",
|
||||||
classes: "border-slate-700 bg-slate-800 text-slate-300",
|
classes: "border-border bg-muted text-muted-foreground",
|
||||||
icon: CircleCheck,
|
icon: CircleCheck,
|
||||||
},
|
},
|
||||||
handed_off: {
|
handed_off: {
|
||||||
@ -78,12 +78,12 @@ const STATUS_META: Record<
|
|||||||
},
|
},
|
||||||
timed_out: {
|
timed_out: {
|
||||||
label: "Timed out",
|
label: "Timed out",
|
||||||
classes: "border-slate-700 bg-slate-800/60 text-slate-400",
|
classes: "border-border bg-muted/60 text-muted-foreground",
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
},
|
},
|
||||||
paused_by_agent: {
|
paused_by_agent: {
|
||||||
label: "Paused by agent",
|
label: "Paused by agent",
|
||||||
classes: "border-slate-700 bg-slate-800 text-slate-300",
|
classes: "border-border bg-muted text-muted-foreground",
|
||||||
icon: PauseCircle,
|
icon: PauseCircle,
|
||||||
},
|
},
|
||||||
failed: {
|
failed: {
|
||||||
@ -151,14 +151,14 @@ export default function FlowRunsPage() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-slate-500" />
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (notFound || !flow) {
|
if (notFound || !flow) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-center gap-3">
|
<div className="flex h-full flex-col items-center justify-center gap-3">
|
||||||
<p className="text-sm text-slate-400">Flow not found.</p>
|
<p className="text-sm text-muted-foreground">Flow not found.</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push("/flows")}
|
onClick={() => router.push("/flows")}
|
||||||
@ -175,19 +175,19 @@ export default function FlowRunsPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push(`/flows/${flow.id}`)}
|
onClick={() => router.push(`/flows/${flow.id}`)}
|
||||||
className="mb-2 inline-flex items-center gap-1 text-xs text-slate-500 hover:text-slate-300"
|
className="mb-2 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-3 w-3" />
|
<ArrowLeft className="h-3 w-3" />
|
||||||
{flow.name}
|
{flow.name}
|
||||||
</button>
|
</button>
|
||||||
<h1 className="text-xl font-semibold text-white">Runs</h1>
|
<h1 className="text-xl font-semibold text-foreground">Runs</h1>
|
||||||
<p className="mt-1 text-sm text-slate-400">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
The 50 most recent times this flow ran. Expand a row to see the engine's
|
The 50 most recent times this flow ran. Expand a row to see the engine's
|
||||||
per-step log.
|
per-step log.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{runs.length === 0 ? (
|
{runs.length === 0 ? (
|
||||||
<div className="mt-6 rounded-lg border border-dashed border-slate-700 bg-slate-900/50 px-6 py-12 text-center text-sm text-slate-400">
|
<div className="mt-6 rounded-lg border border-dashed border-border bg-card/50 px-6 py-12 text-center text-sm text-muted-foreground">
|
||||||
No runs yet. Trigger the flow from a personal WhatsApp number to see
|
No runs yet. Trigger the flow from a personal WhatsApp number to see
|
||||||
it appear here.
|
it appear here.
|
||||||
</div>
|
</div>
|
||||||
@ -229,20 +229,20 @@ function RunCard({
|
|||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-slate-800 bg-slate-900">
|
<div className="rounded-lg border border-border bg-card">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
className="flex w-full items-center gap-3 px-4 py-3 text-left"
|
className="flex w-full items-center gap-3 px-4 py-3 text-left"
|
||||||
>
|
>
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<ChevronDown className="h-4 w-4 shrink-0 text-slate-500" />
|
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-4 w-4 shrink-0 text-slate-500" />
|
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="truncate text-sm font-medium text-white">
|
<span className="truncate text-sm font-medium text-foreground">
|
||||||
{contactLabel}
|
{contactLabel}
|
||||||
</span>
|
</span>
|
||||||
<Badge variant="outline" className={cn("gap-1", meta.classes)}>
|
<Badge variant="outline" className={cn("gap-1", meta.classes)}>
|
||||||
@ -250,12 +250,12 @@ function RunCard({
|
|||||||
{meta.label}
|
{meta.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
{run.status === "active" && run.current_node_key && (
|
{run.status === "active" && run.current_node_key && (
|
||||||
<code className="rounded bg-slate-800 px-1.5 py-0.5 text-[10px] text-slate-400">
|
<code className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||||
at {run.current_node_key}
|
at {run.current_node_key}
|
||||||
</code>
|
</code>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-[11px] text-slate-500">
|
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
|
||||||
<span>Started {format(new Date(run.started_at), "PP p")}</span>
|
<span>Started {format(new Date(run.started_at), "PP p")}</span>
|
||||||
{run.reprompt_count > 0 && (
|
{run.reprompt_count > 0 && (
|
||||||
<span>· {run.reprompt_count} re-prompts</span>
|
<span>· {run.reprompt_count} re-prompts</span>
|
||||||
@ -265,20 +265,20 @@ function RunCard({
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="border-t border-slate-800 px-4 py-3">
|
<div className="border-t border-border px-4 py-3">
|
||||||
{Object.keys(run.vars).length > 0 && (
|
{Object.keys(run.vars).length > 0 && (
|
||||||
<details className="mb-3">
|
<details className="mb-3">
|
||||||
<summary className="cursor-pointer text-xs text-slate-400">
|
<summary className="cursor-pointer text-xs text-muted-foreground">
|
||||||
Captured vars ({Object.keys(run.vars).length})
|
Captured vars ({Object.keys(run.vars).length})
|
||||||
</summary>
|
</summary>
|
||||||
<pre className="mt-2 overflow-x-auto rounded-md bg-slate-950 p-2 text-[11px] text-slate-300">
|
<pre className="mt-2 overflow-x-auto rounded-md bg-background p-2 text-[11px] text-muted-foreground">
|
||||||
{JSON.stringify(run.vars, null, 2)}
|
{JSON.stringify(run.vars, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{events.length === 0 ? (
|
{events.length === 0 ? (
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
No events recorded for this run.
|
No events recorded for this run.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
@ -293,33 +293,33 @@ function RunCard({
|
|||||||
|
|
||||||
const EVENT_COLOR: Record<string, string> = {
|
const EVENT_COLOR: Record<string, string> = {
|
||||||
started: "text-emerald-300",
|
started: "text-emerald-300",
|
||||||
node_entered: "text-slate-300",
|
node_entered: "text-muted-foreground",
|
||||||
message_sent: "text-sky-300",
|
message_sent: "text-sky-300",
|
||||||
reply_received: "text-primary",
|
reply_received: "text-primary",
|
||||||
fallback_fired: "text-amber-300",
|
fallback_fired: "text-amber-300",
|
||||||
handoff: "text-amber-300",
|
handoff: "text-amber-300",
|
||||||
timeout: "text-slate-500",
|
timeout: "text-muted-foreground",
|
||||||
error: "text-red-300",
|
error: "text-red-300",
|
||||||
completed: "text-emerald-300",
|
completed: "text-emerald-300",
|
||||||
};
|
};
|
||||||
|
|
||||||
function EventLine({ ev }: { ev: EventRow }) {
|
function EventLine({ ev }: { ev: EventRow }) {
|
||||||
const cls = EVENT_COLOR[ev.event_type] ?? "text-slate-400";
|
const cls = EVENT_COLOR[ev.event_type] ?? "text-muted-foreground";
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-2 rounded-md px-2 py-1 text-xs">
|
<div className="flex items-start gap-2 rounded-md px-2 py-1 text-xs">
|
||||||
<span className="w-32 shrink-0 text-[10px] text-slate-500">
|
<span className="w-32 shrink-0 text-[10px] text-muted-foreground">
|
||||||
{format(new Date(ev.created_at), "HH:mm:ss")}
|
{format(new Date(ev.created_at), "HH:mm:ss")}
|
||||||
</span>
|
</span>
|
||||||
<span className={cn("w-32 shrink-0 font-mono text-[10px]", cls)}>
|
<span className={cn("w-32 shrink-0 font-mono text-[10px]", cls)}>
|
||||||
{ev.event_type}
|
{ev.event_type}
|
||||||
</span>
|
</span>
|
||||||
{ev.node_key && (
|
{ev.node_key && (
|
||||||
<code className="shrink-0 rounded bg-slate-800 px-1 py-0.5 text-[10px] text-slate-400">
|
<code className="shrink-0 rounded bg-muted px-1 py-0.5 text-[10px] text-muted-foreground">
|
||||||
{ev.node_key}
|
{ev.node_key}
|
||||||
</code>
|
</code>
|
||||||
)}
|
)}
|
||||||
{Object.keys(ev.payload).length > 0 && (
|
{Object.keys(ev.payload).length > 0 && (
|
||||||
<span className="min-w-0 truncate text-[10px] text-slate-500">
|
<span className="min-w-0 truncate text-[10px] text-muted-foreground">
|
||||||
{summarizePayload(ev.payload)}
|
{summarizePayload(ev.payload)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -61,9 +61,9 @@ const STATUS_LABELS: Record<FlowRow["status"], string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_COLORS: Record<FlowRow["status"], string> = {
|
const STATUS_COLORS: Record<FlowRow["status"], string> = {
|
||||||
draft: "border-slate-700 bg-slate-800 text-slate-300",
|
draft: "border-border bg-muted text-muted-foreground",
|
||||||
active: "border-emerald-600/40 bg-emerald-500/10 text-emerald-300",
|
active: "border-emerald-600/40 bg-emerald-500/10 text-emerald-300",
|
||||||
archived: "border-slate-700 bg-slate-800/50 text-slate-500",
|
archived: "border-border bg-muted/50 text-muted-foreground",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface TemplateSummary {
|
interface TemplateSummary {
|
||||||
@ -194,7 +194,7 @@ export default function FlowsPage() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-slate-500" />
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -204,12 +204,12 @@ export default function FlowsPage() {
|
|||||||
<header className="flex flex-wrap items-end justify-between gap-3">
|
<header className="flex flex-wrap items-end justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h1 className="text-2xl font-semibold text-white">Flows</h1>
|
<h1 className="text-2xl font-semibold text-foreground">Flows</h1>
|
||||||
<span className="inline-flex items-center rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-300">
|
<span className="inline-flex items-center rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-300">
|
||||||
Beta
|
Beta
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm text-slate-400">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Build branching, button-driven WhatsApp conversations. Useful for
|
Build branching, button-driven WhatsApp conversations. Useful for
|
||||||
menus, FAQs, and triage before a human steps in.
|
menus, FAQs, and triage before a human steps in.
|
||||||
</p>
|
</p>
|
||||||
@ -247,17 +247,17 @@ export default function FlowsPage() {
|
|||||||
`sm:max-w-sm` baked into its default classes. Without the
|
`sm:max-w-sm` baked into its default classes. Without the
|
||||||
sm: prefix our override applies at base only and the
|
sm: prefix our override applies at base only and the
|
||||||
sm-scoped 384px wins at every real desktop breakpoint. */}
|
sm-scoped 384px wins at every real desktop breakpoint. */}
|
||||||
<DialogContent className="sm:max-w-4xl bg-slate-900 text-slate-100">
|
<DialogContent className="sm:max-w-4xl bg-popover text-popover-foreground">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create a new flow</DialogTitle>
|
<DialogTitle>Create a new flow</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
Start from a template or build from scratch.
|
Start from a template or build from scratch.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{templates.length > 0 && (
|
{templates.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-xs uppercase tracking-wide text-slate-500">
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
Start from a template
|
Start from a template
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
@ -269,16 +269,16 @@ export default function FlowsPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleUseTemplate(t.slug)}
|
onClick={() => handleUseTemplate(t.slug)}
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
className="flex flex-col gap-2.5 rounded-lg border border-slate-800 bg-slate-950 p-4 text-left transition-colors hover:border-primary/40 hover:bg-slate-800 disabled:opacity-50"
|
className="flex flex-col gap-2.5 rounded-lg border border-border bg-background p-4 text-left transition-colors hover:border-primary/40 hover:bg-muted disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5 text-primary" />
|
<Icon className="h-5 w-5 text-primary" />
|
||||||
<span className="text-sm font-semibold text-white">
|
<span className="text-sm font-semibold text-popover-foreground">
|
||||||
{t.name}
|
{t.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs leading-relaxed text-slate-400">
|
<span className="text-xs leading-relaxed text-muted-foreground">
|
||||||
{t.description}
|
{t.description}
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-auto border-t border-slate-800 pt-2 text-[11px] text-slate-500">
|
<span className="mt-auto border-t border-border pt-2 text-[11px] text-muted-foreground">
|
||||||
{t.node_count} {t.node_count === 1 ? "node" : "nodes"}
|
{t.node_count} {t.node_count === 1 ? "node" : "nodes"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -288,15 +288,15 @@ export default function FlowsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2 border-t border-slate-800 pt-4">
|
<div className="space-y-2 border-t border-border pt-4">
|
||||||
<p className="text-xs uppercase tracking-wide text-slate-500">
|
<p className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
Or start blank
|
Or start blank
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
value={newName}
|
value={newName}
|
||||||
onChange={(e) => setNewName(e.target.value)}
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
placeholder="e.g. Welcome menu"
|
placeholder="e.g. Welcome menu"
|
||||||
className="bg-slate-800"
|
className="bg-muted"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") handleCreate();
|
if (e.key === "Enter") handleCreate();
|
||||||
}}
|
}}
|
||||||
@ -330,14 +330,14 @@ function EmptyState({
|
|||||||
canCreate: boolean;
|
canCreate: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-slate-700 bg-slate-900/50 px-6 py-16 text-center">
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border bg-card/50 px-6 py-16 text-center">
|
||||||
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-slate-800">
|
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-muted">
|
||||||
<Workflow className="h-6 w-6 text-slate-500" />
|
<Workflow className="h-6 w-6 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="mt-4 text-base font-medium text-white">
|
<h2 className="mt-4 text-base font-medium text-foreground">
|
||||||
No flows yet
|
No flows yet
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-1 max-w-md text-sm text-slate-400">
|
<p className="mt-1 max-w-md text-sm text-muted-foreground">
|
||||||
Build your first conversation — a welcome menu, an order lookup, an FAQ
|
Build your first conversation — a welcome menu, an order lookup, an FAQ
|
||||||
bot. Customers tap buttons; the bot routes them to the right answer (or
|
bot. Customers tap buttons; the bot routes them to the right answer (or
|
||||||
the right agent).
|
the right agent).
|
||||||
@ -372,11 +372,11 @@ function FlowCard({
|
|||||||
? Archive
|
? Archive
|
||||||
: PauseCircle;
|
: PauseCircle;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col rounded-lg border border-slate-800 bg-slate-900 p-4 transition-colors hover:border-slate-700">
|
<div className="flex flex-col rounded-lg border border-border bg-card p-4 transition-colors hover:border-border">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<Workflow className="h-4 w-4 shrink-0 text-primary" />
|
<Workflow className="h-4 w-4 shrink-0 text-primary" />
|
||||||
<h3 className="truncate text-sm font-semibold text-white">
|
<h3 className="truncate text-sm font-semibold text-foreground">
|
||||||
{flow.name}
|
{flow.name}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -392,18 +392,18 @@ function FlowCard({
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-2 line-clamp-2 text-xs text-slate-400">
|
<p className="mt-2 line-clamp-2 text-xs text-muted-foreground">
|
||||||
{flow.description || triggerSummary}
|
{flow.description || triggerSummary}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-4 flex items-center gap-3 text-[11px] text-slate-500">
|
<div className="mt-4 flex items-center gap-3 text-[11px] text-muted-foreground">
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
<MessageSquare className="h-3 w-3" />
|
<MessageSquare className="h-3 w-3" />
|
||||||
{flow.execution_count} {flow.execution_count === 1 ? "run" : "runs"}
|
{flow.execution_count} {flow.execution_count === 1 ? "run" : "runs"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex items-center justify-end gap-2 border-t border-slate-800 pt-3">
|
<div className="mt-4 flex items-center justify-end gap-2 border-t border-border pt-3">
|
||||||
<Button variant="ghost" size="sm" onClick={onEdit}>
|
<Button variant="ghost" size="sm" onClick={onEdit}>
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
Edit
|
Edit
|
||||||
|
|||||||
@ -299,12 +299,12 @@ export default function PipelinesPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="h-8 w-48 animate-pulse rounded bg-slate-800" />
|
<div className="h-8 w-48 animate-pulse rounded bg-muted" />
|
||||||
<div className="h-9 w-28 animate-pulse rounded-lg bg-slate-800" />
|
<div className="h-9 w-28 animate-pulse rounded-lg bg-muted" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{[1, 2, 3, 4, 5].map((i) => (
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
<div key={i} className="h-96 w-72 animate-pulse rounded-xl bg-slate-800/50" />
|
<div key={i} className="h-96 w-72 animate-pulse rounded-xl bg-muted/50" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -319,20 +319,20 @@ export default function PipelinesPage() {
|
|||||||
{/* Pipeline selector dropdown */}
|
{/* Pipeline selector dropdown */}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
className="inline-flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white hover:bg-slate-800 transition-colors data-[popup-open]:bg-slate-800"
|
className="inline-flex items-center gap-2 rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground hover:bg-muted transition-colors data-[popup-open]:bg-muted"
|
||||||
>
|
>
|
||||||
<GitBranch className="h-4 w-4 text-primary" />
|
<GitBranch className="h-4 w-4 text-primary" />
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{selectedPipeline?.name ?? "Select Pipeline"}
|
{selectedPipeline?.name ?? "Select Pipeline"}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="h-4 w-4 text-slate-400" />
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="start"
|
align="start"
|
||||||
className="w-64 border-slate-700 bg-slate-900 text-slate-200"
|
className="w-64 border-border bg-popover text-popover-foreground"
|
||||||
>
|
>
|
||||||
{pipelines.length === 0 && (
|
{pipelines.length === 0 && (
|
||||||
<DropdownMenuItem disabled className="text-slate-500">
|
<DropdownMenuItem disabled className="text-muted-foreground">
|
||||||
No pipelines yet
|
No pipelines yet
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
@ -343,18 +343,18 @@ export default function PipelinesPage() {
|
|||||||
className={
|
className={
|
||||||
p.id === selectedPipelineId
|
p.id === selectedPipelineId
|
||||||
? "text-primary"
|
? "text-primary"
|
||||||
: "text-slate-300"
|
: "text-popover-foreground"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<GitBranch className="mr-2 h-3.5 w-3.5" />
|
<GitBranch className="mr-2 h-3.5 w-3.5" />
|
||||||
{p.name}
|
{p.name}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
<DropdownMenuSeparator className="bg-slate-700" />
|
<DropdownMenuSeparator className="bg-border" />
|
||||||
{selectedPipeline && (
|
{selectedPipeline && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setSettingsOpen(true)}
|
onClick={() => setSettingsOpen(true)}
|
||||||
className="text-slate-300"
|
className="text-popover-foreground"
|
||||||
>
|
>
|
||||||
<Settings className="mr-2 h-3.5 w-3.5" />
|
<Settings className="mr-2 h-3.5 w-3.5" />
|
||||||
Manage Pipelines
|
Manage Pipelines
|
||||||
@ -370,7 +370,7 @@ export default function PipelinesPage() {
|
|||||||
canAct={canEditSettings}
|
canAct={canEditSettings}
|
||||||
gateReason="create pipelines"
|
gateReason="create pipelines"
|
||||||
onClick={() => setNewPipelineOpen(true)}
|
onClick={() => setNewPipelineOpen(true)}
|
||||||
className="border-slate-700 bg-slate-900 text-slate-200 hover:bg-slate-800"
|
className="border-border bg-card text-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-4 w-4" />
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
Add Pipeline
|
Add Pipeline
|
||||||
@ -390,12 +390,12 @@ export default function PipelinesPage() {
|
|||||||
|
|
||||||
{/* Board */}
|
{/* Board */}
|
||||||
{pipelines.length === 0 ? (
|
{pipelines.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-slate-700 py-20">
|
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-border py-20">
|
||||||
<GitBranch className="h-12 w-12 text-slate-600" />
|
<GitBranch className="h-12 w-12 text-muted-foreground" />
|
||||||
<h3 className="mt-4 text-lg font-medium text-white">
|
<h3 className="mt-4 text-lg font-medium text-foreground">
|
||||||
No pipelines yet
|
No pipelines yet
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-2 text-sm text-slate-400">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
Create a pipeline to start tracking deals
|
Create a pipeline to start tracking deals
|
||||||
</p>
|
</p>
|
||||||
<GatedButton
|
<GatedButton
|
||||||
@ -423,30 +423,30 @@ export default function PipelinesPage() {
|
|||||||
|
|
||||||
{/* New Pipeline Dialog */}
|
{/* New Pipeline Dialog */}
|
||||||
<Dialog open={newPipelineOpen} onOpenChange={setNewPipelineOpen}>
|
<Dialog open={newPipelineOpen} onOpenChange={setNewPipelineOpen}>
|
||||||
<DialogContent className="sm:max-w-sm bg-slate-900 border-slate-700">
|
<DialogContent className="sm:max-w-sm bg-popover border-border">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">New Pipeline</DialogTitle>
|
<DialogTitle className="text-popover-foreground">New Pipeline</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<Label className="text-slate-300">Pipeline Name</Label>
|
<Label className="text-muted-foreground">Pipeline Name</Label>
|
||||||
<Input
|
<Input
|
||||||
value={newPipelineName}
|
value={newPipelineName}
|
||||||
onChange={(e) => setNewPipelineName(e.target.value)}
|
onChange={(e) => setNewPipelineName(e.target.value)}
|
||||||
placeholder="e.g., Enterprise Sales"
|
placeholder="e.g., Enterprise Sales"
|
||||||
className="mt-2 bg-slate-800 border-slate-700 text-white"
|
className="mt-2 bg-muted border-border text-foreground"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") handleCreatePipeline();
|
if (e.key === "Enter") handleCreatePipeline();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 text-xs text-slate-400">
|
<p className="mt-2 text-xs text-muted-foreground">
|
||||||
Default stages (New Lead → Won) will be created automatically.
|
Default stages (New Lead → Won) will be created automatically.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="bg-slate-900/50 border-slate-700">
|
<DialogFooter className="bg-popover/50 border-border">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setNewPipelineOpen(false)}
|
onClick={() => setNewPipelineOpen(false)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -69,39 +69,39 @@ export default function SettingsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white">Settings</h1>
|
<h1 className="text-2xl font-bold text-foreground">Settings</h1>
|
||||||
<p className="mt-1 text-sm text-slate-400">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Manage your profile, WhatsApp® integration, message templates, and
|
Manage your profile, WhatsApp® integration, message templates, and
|
||||||
tags.
|
tags.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs value={tab} onValueChange={(v) => onChange(v as TabValue)}>
|
<Tabs value={tab} onValueChange={(v) => onChange(v as TabValue)}>
|
||||||
<TabsList className="border border-slate-700 bg-slate-900">
|
<TabsList className="border border-border bg-card">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="profile"
|
value="profile"
|
||||||
className="data-active:text-primary text-slate-400 data-active:bg-slate-800"
|
className="data-active:text-primary text-muted-foreground data-active:bg-muted"
|
||||||
>
|
>
|
||||||
<User className="size-4" />
|
<User className="size-4" />
|
||||||
Profile
|
Profile
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="whatsapp"
|
value="whatsapp"
|
||||||
className="data-active:text-primary text-slate-400 data-active:bg-slate-800"
|
className="data-active:text-primary text-muted-foreground data-active:bg-muted"
|
||||||
>
|
>
|
||||||
<Settings className="size-4" />
|
<Settings className="size-4" />
|
||||||
WhatsApp Config
|
WhatsApp Config
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="templates"
|
value="templates"
|
||||||
className="data-active:text-primary text-slate-400 data-active:bg-slate-800"
|
className="data-active:text-primary text-muted-foreground data-active:bg-muted"
|
||||||
>
|
>
|
||||||
<MessageSquare className="size-4" />
|
<MessageSquare className="size-4" />
|
||||||
Templates
|
Templates
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="tags"
|
value="tags"
|
||||||
className="data-active:text-primary text-slate-400 data-active:bg-slate-800"
|
className="data-active:text-primary text-muted-foreground data-active:bg-muted"
|
||||||
>
|
>
|
||||||
<Tag className="size-4" />
|
<Tag className="size-4" />
|
||||||
Tags
|
Tags
|
||||||
@ -109,7 +109,7 @@ export default function SettingsPage() {
|
|||||||
{canEditSettings && (
|
{canEditSettings && (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="custom-fields"
|
value="custom-fields"
|
||||||
className="data-active:text-primary text-slate-400 data-active:bg-slate-800"
|
className="data-active:text-primary text-muted-foreground data-active:bg-muted"
|
||||||
>
|
>
|
||||||
<SlidersHorizontal className="size-4" />
|
<SlidersHorizontal className="size-4" />
|
||||||
Custom Fields
|
Custom Fields
|
||||||
@ -117,21 +117,21 @@ export default function SettingsPage() {
|
|||||||
)}
|
)}
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="deals"
|
value="deals"
|
||||||
className="data-active:text-primary text-slate-400 data-active:bg-slate-800"
|
className="data-active:text-primary text-muted-foreground data-active:bg-muted"
|
||||||
>
|
>
|
||||||
<Coins className="size-4" />
|
<Coins className="size-4" />
|
||||||
Deals
|
Deals
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="appearance"
|
value="appearance"
|
||||||
className="data-active:text-primary text-slate-400 data-active:bg-slate-800"
|
className="data-active:text-primary text-muted-foreground data-active:bg-muted"
|
||||||
>
|
>
|
||||||
<Palette className="size-4" />
|
<Palette className="size-4" />
|
||||||
Appearance
|
Appearance
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="members"
|
value="members"
|
||||||
className="data-active:text-primary text-slate-400 data-active:bg-slate-800"
|
className="data-active:text-primary text-muted-foreground data-active:bg-muted"
|
||||||
>
|
>
|
||||||
<UsersRound className="size-4" />
|
<UsersRound className="size-4" />
|
||||||
Members
|
Members
|
||||||
|
|||||||
@ -49,39 +49,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
* COLOR THEMES
|
* THEMING — two orthogonal dimensions
|
||||||
*
|
*
|
||||||
* 5 dark themes sharing structure, swapping only the primary
|
* 1. MODE (light / dark) → neutral surfaces. Selected with
|
||||||
* accent (and a tiny hue rotation on the slate base). Switched
|
* `document.documentElement.dataset.mode`. Defaults to dark.
|
||||||
* at runtime by setting `document.documentElement.dataset.theme`
|
* 2. ACCENT (violet / …) → the primary color only. Selected
|
||||||
* — see ThemeProvider + the inline boot script in layout.tsx.
|
* with `document.documentElement.dataset.theme`.
|
||||||
*
|
*
|
||||||
* `:root` mirrors Violet so default visitors render correctly
|
* The two compose: any accent works in either mode. Neutral
|
||||||
* before JS runs. `[data-theme="violet"]` is included for the
|
* tokens (background / card / border / muted / sidebar surfaces /
|
||||||
* explicit-select path (settings UI sets it after a pick).
|
* neutral charts / radius) live in the MODE blocks below; accent
|
||||||
|
* tokens (--primary*, --ring, --chart-1, --sidebar-primary*,
|
||||||
|
* --sidebar-ring) live in the ACCENT blocks. They set disjoint
|
||||||
|
* variables, so cascade order between them doesn't matter.
|
||||||
*
|
*
|
||||||
* The three `--primary-*` extras (hover / soft / soft-2) are
|
* `:root` carries the dark-mode + violet defaults so a visitor
|
||||||
* new — used for hovered primary buttons and tinted-primary
|
* renders correctly before JS runs; the boot script in
|
||||||
* surfaces (sidebar active pill, validation badges). Components
|
* layout.tsx replays the saved mode + accent before first paint.
|
||||||
* that don't use them remain backward-compatible.
|
*
|
||||||
|
* The three `--primary-*` extras (hover / soft / soft-2) feed
|
||||||
|
* hovered primary buttons and tinted-primary surfaces (sidebar
|
||||||
|
* active pill, validation badges).
|
||||||
*
|
*
|
||||||
* Token shapes come straight from the design handoff at
|
* Token shapes come straight from the design handoff at
|
||||||
* project/Color Themes.html — change one, change the other.
|
* project/Color Themes.html — change one, change the other.
|
||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
|
|
||||||
|
/* ---- MODE: neutral surfaces ---- */
|
||||||
|
|
||||||
:root,
|
:root,
|
||||||
html[data-theme="violet"] {
|
html[data-mode="dark"] {
|
||||||
--background: oklch(0.13 0.01 260);
|
--background: oklch(0.13 0.01 260);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.18 0.01 260);
|
--card: oklch(0.18 0.01 260);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--popover: oklch(0.18 0.01 260);
|
--popover: oklch(0.18 0.01 260);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--primary: oklch(0.526 0.247 293);
|
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
|
||||||
--primary-hover: oklch(0.6 0.22 293);
|
|
||||||
--primary-soft: oklch(0.526 0.247 293 / 0.12);
|
|
||||||
--primary-soft-2: oklch(0.526 0.247 293 / 0.2);
|
|
||||||
--secondary: oklch(0.22 0.01 260);
|
--secondary: oklch(0.22 0.01 260);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: oklch(0.22 0.01 260);
|
--muted: oklch(0.22 0.01 260);
|
||||||
@ -91,8 +94,6 @@ html[data-theme="violet"] {
|
|||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.28 0.01 260);
|
--border: oklch(0.28 0.01 260);
|
||||||
--input: oklch(0.28 0.01 260);
|
--input: oklch(0.28 0.01 260);
|
||||||
--ring: oklch(0.526 0.247 293);
|
|
||||||
--chart-1: oklch(0.526 0.247 293);
|
|
||||||
--chart-2: oklch(0.556 0 0);
|
--chart-2: oklch(0.556 0 0);
|
||||||
--chart-3: oklch(0.439 0 0);
|
--chart-3: oklch(0.439 0 0);
|
||||||
--chart-4: oklch(0.371 0 0);
|
--chart-4: oklch(0.371 0 0);
|
||||||
@ -100,159 +101,108 @@ html[data-theme="violet"] {
|
|||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--sidebar: oklch(0.16 0.01 260);
|
--sidebar: oklch(0.16 0.01 260);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-primary: oklch(0.526 0.247 293);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.22 0.01 260);
|
--sidebar-accent: oklch(0.22 0.01 260);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-border: oklch(0.28 0.01 260);
|
--sidebar-border: oklch(0.28 0.01 260);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-mode="light"] {
|
||||||
|
--background: oklch(0.99 0.002 260);
|
||||||
|
--foreground: oklch(0.21 0.01 260);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.21 0.01 260);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.21 0.01 260);
|
||||||
|
--secondary: oklch(0.967 0.003 260);
|
||||||
|
--secondary-foreground: oklch(0.25 0.01 260);
|
||||||
|
--muted: oklch(0.967 0.003 260);
|
||||||
|
--muted-foreground: oklch(0.52 0.015 260);
|
||||||
|
--accent: oklch(0.96 0.004 260);
|
||||||
|
--accent-foreground: oklch(0.25 0.01 260);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0.004 260);
|
||||||
|
--input: oklch(0.922 0.004 260);
|
||||||
|
--chart-2: oklch(0.6 0.1 260);
|
||||||
|
--chart-3: oklch(0.7 0.06 260);
|
||||||
|
--chart-4: oklch(0.8 0.04 260);
|
||||||
|
--chart-5: oklch(0.87 0.03 260);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0.002 260);
|
||||||
|
--sidebar-foreground: oklch(0.21 0.01 260);
|
||||||
|
--sidebar-accent: oklch(0.96 0.004 260);
|
||||||
|
--sidebar-accent-foreground: oklch(0.25 0.01 260);
|
||||||
|
--sidebar-border: oklch(0.922 0.004 260);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- ACCENT: primary color ---- */
|
||||||
|
|
||||||
|
:root,
|
||||||
|
html[data-theme="violet"] {
|
||||||
|
--primary: oklch(0.526 0.247 293);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--primary-hover: oklch(0.6 0.22 293);
|
||||||
|
--primary-soft: oklch(0.526 0.247 293 / 0.12);
|
||||||
|
--primary-soft-2: oklch(0.526 0.247 293 / 0.2);
|
||||||
|
--ring: oklch(0.526 0.247 293);
|
||||||
|
--chart-1: oklch(0.526 0.247 293);
|
||||||
|
--sidebar-primary: oklch(0.526 0.247 293);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-ring: oklch(0.526 0.247 293);
|
--sidebar-ring: oklch(0.526 0.247 293);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="emerald"] {
|
html[data-theme="emerald"] {
|
||||||
--background: oklch(0.135 0.01 200);
|
|
||||||
--foreground: oklch(0.985 0 0);
|
|
||||||
--card: oklch(0.185 0.01 200);
|
|
||||||
--card-foreground: oklch(0.985 0 0);
|
|
||||||
--popover: oklch(0.185 0.01 200);
|
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
|
||||||
--primary: oklch(0.62 0.16 162);
|
--primary: oklch(0.62 0.16 162);
|
||||||
--primary-foreground: oklch(0.16 0.02 162);
|
--primary-foreground: oklch(0.16 0.02 162);
|
||||||
--primary-hover: oklch(0.68 0.15 162);
|
--primary-hover: oklch(0.68 0.15 162);
|
||||||
--primary-soft: oklch(0.62 0.16 162 / 0.12);
|
--primary-soft: oklch(0.62 0.16 162 / 0.12);
|
||||||
--primary-soft-2: oklch(0.62 0.16 162 / 0.22);
|
--primary-soft-2: oklch(0.62 0.16 162 / 0.22);
|
||||||
--secondary: oklch(0.225 0.01 200);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
|
||||||
--muted: oklch(0.225 0.01 200);
|
|
||||||
--muted-foreground: oklch(0.65 0.01 200);
|
|
||||||
--accent: oklch(0.225 0.01 200);
|
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
|
||||||
--border: oklch(0.285 0.01 200);
|
|
||||||
--input: oklch(0.285 0.01 200);
|
|
||||||
--ring: oklch(0.62 0.16 162);
|
--ring: oklch(0.62 0.16 162);
|
||||||
--chart-1: oklch(0.62 0.16 162);
|
--chart-1: oklch(0.62 0.16 162);
|
||||||
--chart-2: oklch(0.7 0.14 195);
|
--chart-2: oklch(0.7 0.14 195);
|
||||||
--chart-3: oklch(0.439 0 0);
|
|
||||||
--chart-4: oklch(0.371 0 0);
|
|
||||||
--chart-5: oklch(0.269 0 0);
|
|
||||||
--sidebar: oklch(0.165 0.01 200);
|
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-primary: oklch(0.62 0.16 162);
|
--sidebar-primary: oklch(0.62 0.16 162);
|
||||||
--sidebar-primary-foreground: oklch(0.16 0.02 162);
|
--sidebar-primary-foreground: oklch(0.16 0.02 162);
|
||||||
--sidebar-accent: oklch(0.225 0.01 200);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-border: oklch(0.285 0.01 200);
|
|
||||||
--sidebar-ring: oklch(0.62 0.16 162);
|
--sidebar-ring: oklch(0.62 0.16 162);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="cobalt"] {
|
html[data-theme="cobalt"] {
|
||||||
--background: oklch(0.13 0.015 252);
|
|
||||||
--foreground: oklch(0.985 0 0);
|
|
||||||
--card: oklch(0.18 0.015 252);
|
|
||||||
--card-foreground: oklch(0.985 0 0);
|
|
||||||
--popover: oklch(0.18 0.015 252);
|
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
|
||||||
--primary: oklch(0.585 0.2 254);
|
--primary: oklch(0.585 0.2 254);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--primary-hover: oklch(0.66 0.18 254);
|
--primary-hover: oklch(0.66 0.18 254);
|
||||||
--primary-soft: oklch(0.585 0.2 254 / 0.12);
|
--primary-soft: oklch(0.585 0.2 254 / 0.12);
|
||||||
--primary-soft-2: oklch(0.585 0.2 254 / 0.22);
|
--primary-soft-2: oklch(0.585 0.2 254 / 0.22);
|
||||||
--secondary: oklch(0.22 0.015 252);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
|
||||||
--muted: oklch(0.22 0.015 252);
|
|
||||||
--muted-foreground: oklch(0.65 0.012 252);
|
|
||||||
--accent: oklch(0.22 0.015 252);
|
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
|
||||||
--border: oklch(0.28 0.015 252);
|
|
||||||
--input: oklch(0.28 0.015 252);
|
|
||||||
--ring: oklch(0.585 0.2 254);
|
--ring: oklch(0.585 0.2 254);
|
||||||
--chart-1: oklch(0.585 0.2 254);
|
--chart-1: oklch(0.585 0.2 254);
|
||||||
--chart-2: oklch(0.7 0.15 220);
|
--chart-2: oklch(0.7 0.15 220);
|
||||||
--chart-3: oklch(0.439 0 0);
|
|
||||||
--chart-4: oklch(0.371 0 0);
|
|
||||||
--chart-5: oklch(0.269 0 0);
|
|
||||||
--sidebar: oklch(0.16 0.015 252);
|
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-primary: oklch(0.585 0.2 254);
|
--sidebar-primary: oklch(0.585 0.2 254);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: oklch(0.22 0.015 252);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-border: oklch(0.28 0.015 252);
|
|
||||||
--sidebar-ring: oklch(0.585 0.2 254);
|
--sidebar-ring: oklch(0.585 0.2 254);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="amber"] {
|
html[data-theme="amber"] {
|
||||||
--background: oklch(0.135 0.012 60);
|
|
||||||
--foreground: oklch(0.985 0 0);
|
|
||||||
--card: oklch(0.185 0.012 60);
|
|
||||||
--card-foreground: oklch(0.985 0 0);
|
|
||||||
--popover: oklch(0.185 0.012 60);
|
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
|
||||||
--primary: oklch(0.745 0.16 65);
|
--primary: oklch(0.745 0.16 65);
|
||||||
--primary-foreground: oklch(0.18 0.03 65);
|
--primary-foreground: oklch(0.18 0.03 65);
|
||||||
--primary-hover: oklch(0.8 0.15 65);
|
--primary-hover: oklch(0.8 0.15 65);
|
||||||
--primary-soft: oklch(0.745 0.16 65 / 0.12);
|
--primary-soft: oklch(0.745 0.16 65 / 0.12);
|
||||||
--primary-soft-2: oklch(0.745 0.16 65 / 0.22);
|
--primary-soft-2: oklch(0.745 0.16 65 / 0.22);
|
||||||
--secondary: oklch(0.225 0.012 60);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
|
||||||
--muted: oklch(0.225 0.012 60);
|
|
||||||
--muted-foreground: oklch(0.66 0.01 60);
|
|
||||||
--accent: oklch(0.225 0.012 60);
|
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
|
||||||
--border: oklch(0.285 0.012 60);
|
|
||||||
--input: oklch(0.285 0.012 60);
|
|
||||||
--ring: oklch(0.745 0.16 65);
|
--ring: oklch(0.745 0.16 65);
|
||||||
--chart-1: oklch(0.745 0.16 65);
|
--chart-1: oklch(0.745 0.16 65);
|
||||||
--chart-2: oklch(0.7 0.15 35);
|
--chart-2: oklch(0.7 0.15 35);
|
||||||
--chart-3: oklch(0.439 0 0);
|
|
||||||
--chart-4: oklch(0.371 0 0);
|
|
||||||
--chart-5: oklch(0.269 0 0);
|
|
||||||
--sidebar: oklch(0.165 0.012 60);
|
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-primary: oklch(0.745 0.16 65);
|
--sidebar-primary: oklch(0.745 0.16 65);
|
||||||
--sidebar-primary-foreground: oklch(0.18 0.03 65);
|
--sidebar-primary-foreground: oklch(0.18 0.03 65);
|
||||||
--sidebar-accent: oklch(0.225 0.012 60);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-border: oklch(0.285 0.012 60);
|
|
||||||
--sidebar-ring: oklch(0.745 0.16 65);
|
--sidebar-ring: oklch(0.745 0.16 65);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="rose"] {
|
html[data-theme="rose"] {
|
||||||
--background: oklch(0.13 0.012 15);
|
|
||||||
--foreground: oklch(0.985 0 0);
|
|
||||||
--card: oklch(0.18 0.012 15);
|
|
||||||
--card-foreground: oklch(0.985 0 0);
|
|
||||||
--popover: oklch(0.18 0.012 15);
|
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
|
||||||
--primary: oklch(0.645 0.22 16);
|
--primary: oklch(0.645 0.22 16);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--primary-hover: oklch(0.71 0.2 16);
|
--primary-hover: oklch(0.71 0.2 16);
|
||||||
--primary-soft: oklch(0.645 0.22 16 / 0.12);
|
--primary-soft: oklch(0.645 0.22 16 / 0.12);
|
||||||
--primary-soft-2: oklch(0.645 0.22 16 / 0.22);
|
--primary-soft-2: oklch(0.645 0.22 16 / 0.22);
|
||||||
--secondary: oklch(0.22 0.012 15);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
|
||||||
--muted: oklch(0.22 0.012 15);
|
|
||||||
--muted-foreground: oklch(0.65 0.01 15);
|
|
||||||
--accent: oklch(0.22 0.012 15);
|
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
|
||||||
--border: oklch(0.28 0.012 15);
|
|
||||||
--input: oklch(0.28 0.012 15);
|
|
||||||
--ring: oklch(0.645 0.22 16);
|
--ring: oklch(0.645 0.22 16);
|
||||||
--chart-1: oklch(0.645 0.22 16);
|
--chart-1: oklch(0.645 0.22 16);
|
||||||
--chart-2: oklch(0.7 0.18 340);
|
--chart-2: oklch(0.7 0.18 340);
|
||||||
--chart-3: oklch(0.439 0 0);
|
|
||||||
--chart-4: oklch(0.371 0 0);
|
|
||||||
--chart-5: oklch(0.269 0 0);
|
|
||||||
--sidebar: oklch(0.16 0.012 15);
|
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-primary: oklch(0.645 0.22 16);
|
--sidebar-primary: oklch(0.645 0.22 16);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: oklch(0.22 0.012 15);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-border: oklch(0.28 0.012 15);
|
|
||||||
--sidebar-ring: oklch(0.645 0.22 16);
|
--sidebar-ring: oklch(0.645 0.22 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -220,10 +220,10 @@ export default function JoinPage() {
|
|||||||
// ----- Loading state (peek pending OR auth not yet resolved) -----
|
// ----- Loading state (peek pending OR auth not yet resolved) -----
|
||||||
if (peek === null || authedUserId === undefined) {
|
if (peek === null || authedUserId === undefined) {
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
|
<Card className="w-full max-w-md border-border bg-card">
|
||||||
<CardContent className="flex flex-col items-center gap-3 py-12">
|
<CardContent className="flex flex-col items-center gap-3 py-12">
|
||||||
<Loader2 className="size-6 animate-spin text-primary" />
|
<Loader2 className="size-6 animate-spin text-primary" />
|
||||||
<p className="text-sm text-slate-400">Verifying invitation…</p>
|
<p className="text-sm text-muted-foreground">Verifying invitation…</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@ -233,13 +233,13 @@ export default function JoinPage() {
|
|||||||
if (!peek.ok) {
|
if (!peek.ok) {
|
||||||
const copy = FAIL_COPY[peek.reason];
|
const copy = FAIL_COPY[peek.reason];
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
|
<Card className="w-full max-w-md border-border bg-card">
|
||||||
<CardHeader className="items-center text-center">
|
<CardHeader className="items-center text-center">
|
||||||
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-red-500/10">
|
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-red-500/10">
|
||||||
<MailX className="h-6 w-6 text-red-400" />
|
<MailX className="h-6 w-6 text-red-400" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-xl text-white">{copy.title}</CardTitle>
|
<CardTitle className="text-xl text-foreground">{copy.title}</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
{copy.body}
|
{copy.body}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -262,7 +262,7 @@ export default function JoinPage() {
|
|||||||
<Link href="/signup">
|
<Link href="/signup">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white"
|
className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
>
|
>
|
||||||
Create a new account instead
|
Create a new account instead
|
||||||
</Button>
|
</Button>
|
||||||
@ -278,7 +278,7 @@ export default function JoinPage() {
|
|||||||
<Link href="/login">
|
<Link href="/login">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white"
|
className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</Button>
|
</Button>
|
||||||
@ -296,13 +296,13 @@ export default function JoinPage() {
|
|||||||
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
||||||
<UsersRound className="h-6 w-6 text-primary" />
|
<UsersRound className="h-6 w-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-xl text-white">
|
<CardTitle className="text-xl text-foreground">
|
||||||
You're invited to{' '}
|
You're invited to{' '}
|
||||||
<span className="text-primary">{peek.account_name}</span>
|
<span className="text-primary">{peek.account_name}</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
You'll join as{' '}
|
You'll join as{' '}
|
||||||
<span className="inline-flex items-center gap-1 text-white">
|
<span className="inline-flex items-center gap-1 text-foreground">
|
||||||
<ShieldCheck className="size-3.5 text-primary" />
|
<ShieldCheck className="size-3.5 text-primary" />
|
||||||
{ROLE_LABEL[peek.role]}
|
{ROLE_LABEL[peek.role]}
|
||||||
</span>
|
</span>
|
||||||
@ -321,7 +321,7 @@ export default function JoinPage() {
|
|||||||
if (authedUserId) {
|
if (authedUserId) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
|
<Card className="w-full max-w-md border-border bg-card">
|
||||||
{inviteHeader}
|
{inviteHeader}
|
||||||
<CardContent className="flex flex-col gap-3">
|
<CardContent className="flex flex-col gap-3">
|
||||||
<Button
|
<Button
|
||||||
@ -341,9 +341,9 @@ export default function JoinPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-center text-xs text-slate-500">
|
<p className="text-center text-xs text-muted-foreground">
|
||||||
Accepting moves your login into{' '}
|
Accepting moves your login into{' '}
|
||||||
<span className="text-slate-400">{peek.account_name}</span>. Your
|
<span className="text-muted-foreground">{peek.account_name}</span>. Your
|
||||||
empty personal account from signup will be cleaned up.
|
empty personal account from signup will be cleaned up.
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -359,30 +359,30 @@ export default function JoinPage() {
|
|||||||
if (!open) setConflictMessage(null);
|
if (!open) setConflictMessage(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-md">
|
<DialogContent className="bg-popover border-border sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2 text-white">
|
<DialogTitle className="flex items-center gap-2 text-popover-foreground">
|
||||||
<AlertTriangle className="size-4 text-amber-400" />
|
<AlertTriangle className="size-4 text-amber-400" />
|
||||||
Can't join {peek.account_name} with this account
|
Can't join {peek.account_name} with this account
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
{conflictMessage}
|
{conflictMessage}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-2 py-2 text-xs text-slate-500">
|
<div className="space-y-2 py-2 text-xs text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
To join{' '}
|
To join{' '}
|
||||||
<span className="text-slate-300">{peek.account_name}</span>,
|
<span className="text-popover-foreground">{peek.account_name}</span>,
|
||||||
sign out and sign up again with a different email address.
|
sign out and sign up again with a different email address.
|
||||||
The invite link stays valid as long as it hasn't
|
The invite link stays valid as long as it hasn't
|
||||||
expired.
|
expired.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="bg-slate-900 border-slate-700">
|
<DialogFooter className="bg-popover border-border">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setConflictMessage(null)}
|
onClick={() => setConflictMessage(null)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-popover-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Stay signed in
|
Stay signed in
|
||||||
</Button>
|
</Button>
|
||||||
@ -409,7 +409,7 @@ export default function JoinPage() {
|
|||||||
|
|
||||||
// ----- Not authed: prompt to sign up or sign in -----
|
// ----- Not authed: prompt to sign up or sign in -----
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-md border-slate-800 bg-slate-900">
|
<Card className="w-full max-w-md border-border bg-card">
|
||||||
{inviteHeader}
|
{inviteHeader}
|
||||||
<CardContent className="flex flex-col gap-2">
|
<CardContent className="flex flex-col gap-2">
|
||||||
<Link href={`/signup?invite=${encodeURIComponent(token!)}`}>
|
<Link href={`/signup?invite=${encodeURIComponent(token!)}`}>
|
||||||
@ -420,7 +420,7 @@ export default function JoinPage() {
|
|||||||
<Link href={`/login?invite=${encodeURIComponent(token!)}`}>
|
<Link href={`/login?invite=${encodeURIComponent(token!)}`}>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white"
|
className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
>
|
>
|
||||||
I already have an account
|
I already have an account
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function JoinLayout({ children }: { children: ReactNode }) {
|
export default function JoinLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4">
|
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,17 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import { Toaster } from "sonner";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { ThemeProvider } from "@/hooks/use-theme";
|
import { ThemeProvider } from "@/hooks/use-theme";
|
||||||
import { DEFAULT_THEME, STORAGE_KEY, THEME_IDS } from "@/lib/themes";
|
import { ThemedToaster } from "@/components/themed-toaster";
|
||||||
|
import {
|
||||||
|
DEFAULT_MODE,
|
||||||
|
DEFAULT_THEME,
|
||||||
|
MODE_STORAGE_KEY,
|
||||||
|
MODES,
|
||||||
|
STORAGE_KEY,
|
||||||
|
THEME_IDS,
|
||||||
|
} from "@/lib/themes";
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
variable: "--font-sans",
|
variable: "--font-sans",
|
||||||
@ -33,29 +40,37 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
themeColor: "#020617",
|
themeColor: "#020617",
|
||||||
colorScheme: "dark",
|
colorScheme: "dark light",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inline boot script — runs before React hydrates so the user's
|
// Inline boot script — runs before React hydrates so the user's
|
||||||
// chosen theme is on the <html> element before first paint. Without
|
// chosen accent (data-theme) AND mode (data-mode) are on the <html>
|
||||||
// this every page load flashes the default Violet for a frame before
|
// element before first paint. Without this every page load flashes
|
||||||
// the React tree mounts and applies the picked theme.
|
// the server-rendered defaults for a frame before the React tree
|
||||||
|
// mounts and applies the picked values.
|
||||||
//
|
//
|
||||||
// Kept dependency-free (no imports, no JSX) — must be a string the
|
// Kept dependency-free (no imports, no JSX) — must be a string the
|
||||||
// browser can run as a single <script>. Knowledge of valid theme IDs
|
// browser can run as a single <script>. Knowledge of valid ids is
|
||||||
// is sourced from the THEME_IDS constant so adding a theme doesn't
|
// sourced from the THEME_IDS / MODES constants so adding one doesn't
|
||||||
// silently break the boot path.
|
// silently break the boot path.
|
||||||
const THEME_BOOT_SCRIPT = `
|
const THEME_BOOT_SCRIPT = `
|
||||||
(function(){
|
(function(){
|
||||||
|
var d = document.documentElement;
|
||||||
try {
|
try {
|
||||||
var STORAGE_KEY = ${JSON.stringify(STORAGE_KEY)};
|
var THEME_KEY = ${JSON.stringify(STORAGE_KEY)};
|
||||||
var DEFAULT = ${JSON.stringify(DEFAULT_THEME)};
|
var THEME_DEFAULT = ${JSON.stringify(DEFAULT_THEME)};
|
||||||
var ALLOWED = ${JSON.stringify(THEME_IDS)};
|
var THEMES = ${JSON.stringify(THEME_IDS)};
|
||||||
var saved = localStorage.getItem(STORAGE_KEY);
|
var savedTheme = localStorage.getItem(THEME_KEY);
|
||||||
var theme = ALLOWED.indexOf(saved) !== -1 ? saved : DEFAULT;
|
d.dataset.theme = THEMES.indexOf(savedTheme) !== -1 ? savedTheme : THEME_DEFAULT;
|
||||||
document.documentElement.dataset.theme = theme;
|
|
||||||
|
var MODE_KEY = ${JSON.stringify(MODE_STORAGE_KEY)};
|
||||||
|
var MODE_DEFAULT = ${JSON.stringify(DEFAULT_MODE)};
|
||||||
|
var MODES = ${JSON.stringify(MODES)};
|
||||||
|
var savedMode = localStorage.getItem(MODE_KEY);
|
||||||
|
d.dataset.mode = MODES.indexOf(savedMode) !== -1 ? savedMode : MODE_DEFAULT;
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
document.documentElement.dataset.theme = ${JSON.stringify(DEFAULT_THEME)};
|
d.dataset.theme = ${JSON.stringify(DEFAULT_THEME)};
|
||||||
|
d.dataset.mode = ${JSON.stringify(DEFAULT_MODE)};
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
`;
|
`;
|
||||||
@ -69,13 +84,15 @@ export default function RootLayout({
|
|||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
data-theme={DEFAULT_THEME}
|
data-theme={DEFAULT_THEME}
|
||||||
|
data-mode={DEFAULT_MODE}
|
||||||
className={`${inter.variable} h-full antialiased`}
|
className={`${inter.variable} h-full antialiased`}
|
||||||
// The `theme-boot` script below rewrites `data-theme` on <html>
|
// The `theme-boot` script below rewrites `data-theme` and
|
||||||
// from localStorage before React hydrates, so for any non-default
|
// `data-mode` on <html> from localStorage before React hydrates,
|
||||||
// theme the client DOM intentionally differs from the server-
|
// so for any non-default choice the client DOM intentionally
|
||||||
// rendered `DEFAULT_THEME`. suppressHydrationWarning silences the
|
// differs from the server-rendered defaults. suppressHydration-
|
||||||
// expected mismatch — it only applies to this element's own
|
// Warning silences the expected mismatch — it only applies to
|
||||||
// attributes, so genuine mismatches in children still surface.
|
// this element's own attributes, so genuine mismatches in
|
||||||
|
// children still surface.
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<head>
|
<head>
|
||||||
@ -88,17 +105,7 @@ export default function RootLayout({
|
|||||||
<body className="min-h-full bg-background text-foreground font-sans">
|
<body className="min-h-full bg-background text-foreground font-sans">
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
{children}
|
{children}
|
||||||
<Toaster
|
<ThemedToaster />
|
||||||
theme="dark"
|
|
||||||
position="top-right"
|
|
||||||
toastOptions={{
|
|
||||||
style: {
|
|
||||||
background: "rgb(30 41 59)",
|
|
||||||
border: "1px solid rgb(51 65 85)",
|
|
||||||
color: "white",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -95,7 +95,7 @@ const STEP_META: Record<AutomationStepType, StepMeta> = {
|
|||||||
assign_conversation: { label: "Assign Conversation", icon: UserCheck, border: "border-l-primary" },
|
assign_conversation: { label: "Assign Conversation", icon: UserCheck, border: "border-l-primary" },
|
||||||
update_contact_field: { label: "Update Contact Field", icon: PencilLine, border: "border-l-primary" },
|
update_contact_field: { label: "Update Contact Field", icon: PencilLine, border: "border-l-primary" },
|
||||||
create_deal: { label: "Create Deal", icon: Briefcase, border: "border-l-primary" },
|
create_deal: { label: "Create Deal", icon: Briefcase, border: "border-l-primary" },
|
||||||
wait: { label: "Wait", icon: Hourglass, border: "border-l-slate-500" },
|
wait: { label: "Wait", icon: Hourglass, border: "border-l-border" },
|
||||||
condition: { label: "Condition (If/Else)", icon: GitBranch, border: "border-l-amber-500" },
|
condition: { label: "Condition (If/Else)", icon: GitBranch, border: "border-l-amber-500" },
|
||||||
send_webhook: { label: "Send Webhook", icon: Webhook, border: "border-l-primary" },
|
send_webhook: { label: "Send Webhook", icon: Webhook, border: "border-l-primary" },
|
||||||
close_conversation: { label: "Close Conversation", icon: CircleSlash, border: "border-l-primary" },
|
close_conversation: { label: "Close Conversation", icon: CircleSlash, border: "border-l-primary" },
|
||||||
@ -251,7 +251,7 @@ function ResourcesProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SELECT_CLASS =
|
const SELECT_CLASS =
|
||||||
"w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white focus:border-primary focus:outline-none"
|
"w-full rounded-md border border-border bg-muted px-2 py-1.5 text-sm text-foreground focus:border-primary focus:outline-none"
|
||||||
|
|
||||||
/** Tag dropdown by name + color, storing the tag's id. Falls back to a
|
/** Tag dropdown by name + color, storing the tag's id. Falls back to a
|
||||||
* raw id input when no tags exist yet. */
|
* raw id input when no tags exist yet. */
|
||||||
@ -269,7 +269,7 @@ function TagSelect({
|
|||||||
placeholder="Tag id"
|
placeholder="Tag id"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -277,7 +277,7 @@ function TagSelect({
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className="h-3 w-3 shrink-0 rounded-full border border-slate-600"
|
className="h-3 w-3 shrink-0 rounded-full border border-border"
|
||||||
style={{ backgroundColor: selected?.color ?? "transparent" }}
|
style={{ backgroundColor: selected?.color ?? "transparent" }}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
@ -358,7 +358,7 @@ function AgentSelect({
|
|||||||
placeholder="Agent id"
|
placeholder="Agent id"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -405,7 +405,7 @@ function SendTemplateFields({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange({ template_name: e.target.value, language })
|
onChange({ template_name: e.target.value, language })
|
||||||
}
|
}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
<FieldBlock label="Language">
|
<FieldBlock label="Language">
|
||||||
@ -414,7 +414,7 @@ function SendTemplateFields({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange({ template_name: templateName, language: e.target.value })
|
onChange({ template_name: templateName, language: e.target.value })
|
||||||
}
|
}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
</>
|
</>
|
||||||
@ -548,15 +548,15 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex flex-col bg-slate-950">
|
<div className="fixed inset-0 flex flex-col bg-background">
|
||||||
{/* Top bar. At sub-sm widths the "Active" label is hidden and the
|
{/* Top bar. At sub-sm widths the "Active" label is hidden and the
|
||||||
switch moves to the right of the save button, so the name input
|
switch moves to the right of the save button, so the name input
|
||||||
gets maximum width. */}
|
gets maximum width. */}
|
||||||
<header className="flex flex-shrink-0 items-center gap-2 border-b border-slate-800 bg-slate-900/80 px-3 py-3 sm:gap-3 sm:px-4">
|
<header className="flex flex-shrink-0 items-center gap-2 border-b border-border bg-card/80 px-3 py-3 sm:gap-3 sm:px-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push("/automations")}
|
onClick={() => router.push("/automations")}
|
||||||
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-slate-800 hover:text-white"
|
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||||
aria-label="Back to automations"
|
aria-label="Back to automations"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
@ -565,9 +565,9 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) {
|
|||||||
value={state.name}
|
value={state.name}
|
||||||
onChange={(e) => patchTop("name", e.target.value)}
|
onChange={(e) => patchTop("name", e.target.value)}
|
||||||
placeholder="Untitled automation"
|
placeholder="Untitled automation"
|
||||||
className="min-w-0 flex-1 rounded-md bg-transparent px-2 py-1 text-sm font-semibold text-white placeholder:text-slate-500 focus:bg-slate-800 focus:outline-none sm:text-base"
|
className="min-w-0 flex-1 rounded-md bg-transparent px-2 py-1 text-sm font-semibold text-foreground placeholder:text-muted-foreground focus:bg-muted focus:outline-none sm:text-base"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
<span className="hidden sm:inline">Active</span>
|
<span className="hidden sm:inline">Active</span>
|
||||||
<Switch
|
<Switch
|
||||||
checked={state.is_active}
|
checked={state.is_active}
|
||||||
@ -587,7 +587,7 @@ export function AutomationBuilder({ initial }: { initial: BuilderInitial }) {
|
|||||||
|
|
||||||
{/* Canvas */}
|
{/* Canvas */}
|
||||||
<div className="relative flex-1 overflow-y-auto">
|
<div className="relative flex-1 overflow-y-auto">
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle,#1e293b_1px,transparent_1px)] [background-size:20px_20px] pointer-events-none" />
|
<div className="absolute inset-0 bg-[radial-gradient(circle,var(--border)_1px,transparent_1px)] [background-size:20px_20px] pointer-events-none" />
|
||||||
<div className="relative mx-auto flex max-w-2xl flex-col items-center gap-0 px-4 py-10">
|
<div className="relative mx-auto flex max-w-2xl flex-col items-center gap-0 px-4 py-10">
|
||||||
<ResourcesProvider>
|
<ResourcesProvider>
|
||||||
<TriggerCard
|
<TriggerCard
|
||||||
@ -633,7 +633,7 @@ function TriggerCard({
|
|||||||
// Card width: full on mobile, fixed 320px on sm+. The canvas wrapper
|
// Card width: full on mobile, fixed 320px on sm+. The canvas wrapper
|
||||||
// (max-w-2xl + px-4) keeps this tidy on tablet/desktop.
|
// (max-w-2xl + px-4) keeps this tidy on tablet/desktop.
|
||||||
<div className="z-10 w-full max-w-[320px] sm:w-80">
|
<div className="z-10 w-full max-w-[320px] sm:w-80">
|
||||||
<div className="rounded-lg border border-slate-800 border-l-4 border-l-blue-500 bg-slate-900 shadow-lg">
|
<div className="rounded-lg border border-border border-l-4 border-l-blue-500 bg-card shadow-lg">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
@ -644,24 +644,24 @@ function TriggerCard({
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-[11px] uppercase tracking-wide text-blue-300">Trigger</div>
|
<div className="text-[11px] uppercase tracking-wide text-blue-300">Trigger</div>
|
||||||
<div className="truncate text-sm font-medium text-white">
|
<div className="truncate text-sm font-medium text-foreground">
|
||||||
{TRIGGER_OPTIONS.find((o) => o.value === type)?.label ?? type}
|
{TRIGGER_OPTIONS.find((o) => o.value === type)?.label ?? type}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={cn("h-4 w-4 text-slate-400 transition-transform", open && "rotate-180")}
|
className={cn("h-4 w-4 text-muted-foreground transition-transform", open && "rotate-180")}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="space-y-3 border-t border-slate-800 px-4 py-3">
|
<div className="space-y-3 border-t border-border px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs font-medium text-slate-400">
|
<label className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||||
Trigger type
|
Trigger type
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={type}
|
value={type}
|
||||||
onChange={(e) => onTypeChange(e.target.value as AutomationTriggerType)}
|
onChange={(e) => onTypeChange(e.target.value as AutomationTriggerType)}
|
||||||
className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white focus:border-primary focus:outline-none"
|
className="w-full rounded-md border border-border bg-muted px-2 py-1.5 text-sm text-foreground focus:border-primary focus:outline-none"
|
||||||
>
|
>
|
||||||
{TRIGGER_OPTIONS.map((o) => (
|
{TRIGGER_OPTIONS.map((o) => (
|
||||||
<option key={o.value} value={o.value}>
|
<option key={o.value} value={o.value}>
|
||||||
@ -669,7 +669,7 @@ function TriggerCard({
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<p className="mt-1 text-[11px] text-slate-500">
|
<p className="mt-1 text-[11px] text-muted-foreground">
|
||||||
{TRIGGER_OPTIONS.find((o) => o.value === type)?.hint}
|
{TRIGGER_OPTIONS.find((o) => o.value === type)?.hint}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -681,7 +681,7 @@ function TriggerCard({
|
|||||||
)}
|
)}
|
||||||
{type === "tag_added" && (
|
{type === "tag_added" && (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs font-medium text-slate-400">
|
<label className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||||
Tag
|
Tag
|
||||||
</label>
|
</label>
|
||||||
<TagSelect
|
<TagSelect
|
||||||
@ -697,7 +697,7 @@ function TriggerCard({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onConfigChange({ ...config, schedule: e.target.value })
|
onConfigChange({ ...config, schedule: e.target.value })
|
||||||
}
|
}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -735,7 +735,7 @@ function KeywordMatchConfig({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs font-medium text-slate-400">
|
<label className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||||
Keywords (comma-separated)
|
Keywords (comma-separated)
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
@ -749,17 +749,17 @@ function KeywordMatchConfig({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="e.g. pricing, demo request, talk to sales"
|
placeholder="e.g. pricing, demo request, talk to sales"
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs font-medium text-slate-400">
|
<label className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||||
Match type
|
Match type
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={config?.match_type ?? "contains"}
|
value={config?.match_type ?? "contains"}
|
||||||
onChange={(e) => onChange({ ...config, match_type: e.target.value as "exact" | "contains" })}
|
onChange={(e) => onChange({ ...config, match_type: e.target.value as "exact" | "contains" })}
|
||||||
className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white focus:outline-none"
|
className="w-full rounded-md border border-border bg-muted px-2 py-1.5 text-sm text-foreground focus:outline-none"
|
||||||
>
|
>
|
||||||
<option value="contains">Contains</option>
|
<option value="contains">Contains</option>
|
||||||
<option value="exact">Exact</option>
|
<option value="exact">Exact</option>
|
||||||
@ -858,7 +858,7 @@ function StepRenderer({
|
|||||||
<div className={cn("z-10 flex flex-col", width)}>
|
<div className={cn("z-10 flex flex-col", width)}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border border-slate-800 border-l-4 bg-slate-900 shadow-lg",
|
"rounded-lg border border-border border-l-4 bg-card shadow-lg",
|
||||||
meta.border,
|
meta.border,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -867,28 +867,28 @@ function StepRenderer({
|
|||||||
onClick={() => props.setExpandedId(expanded ? null : step.cid)}
|
onClick={() => props.setExpandedId(expanded ? null : step.cid)}
|
||||||
className="flex w-full items-center gap-3 px-4 py-3 text-left"
|
className="flex w-full items-center gap-3 px-4 py-3 text-left"
|
||||||
>
|
>
|
||||||
<GripVertical className="h-4 w-4 flex-shrink-0 text-slate-600" aria-hidden />
|
<GripVertical className="h-4 w-4 flex-shrink-0 text-muted-foreground" aria-hidden />
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-slate-800 text-slate-300">
|
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-[11px] uppercase tracking-wide text-slate-400">
|
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
||||||
{isCondition ? "Condition" : step.step_type === "wait" ? "Wait" : "Action"}
|
{isCondition ? "Condition" : step.step_type === "wait" ? "Wait" : "Action"}
|
||||||
</div>
|
</div>
|
||||||
<div className="truncate text-sm font-medium text-white">{meta.label}</div>
|
<div className="truncate text-sm font-medium text-foreground">{meta.label}</div>
|
||||||
<div className="truncate text-[11px] text-slate-500">{previewFor(step)}</div>
|
<div className="truncate text-[11px] text-muted-foreground">{previewFor(step)}</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={cn("h-4 w-4 text-slate-400 transition-transform", expanded && "rotate-180")}
|
className={cn("h-4 w-4 text-muted-foreground transition-transform", expanded && "rotate-180")}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="border-t border-slate-800 px-4 py-3">
|
<div className="border-t border-border px-4 py-3">
|
||||||
<StepEditor
|
<StepEditor
|
||||||
step={step}
|
step={step}
|
||||||
onChange={(next) => props.updateStep(path, () => next)}
|
onChange={(next) => props.updateStep(path, () => next)}
|
||||||
/>
|
/>
|
||||||
<div className="mt-3 flex items-center justify-between gap-2 border-t border-slate-800 pt-3">
|
<div className="mt-3 flex items-center justify-between gap-2 border-t border-border pt-3">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -990,17 +990,17 @@ function BranchColumn({
|
|||||||
function AddButton({ onPick }: { onPick: (t: AutomationStepType) => void }) {
|
function AddButton({ onPick }: { onPick: (t: AutomationStepType) => void }) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col items-center">
|
<div className="relative flex flex-col items-center">
|
||||||
<div className="h-4 w-[2px] bg-slate-700" aria-hidden />
|
<div className="h-4 w-[2px] bg-border" aria-hidden />
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-dashed border-slate-700 bg-slate-950 text-slate-400 transition-colors hover:border-primary hover:bg-primary/10 hover:text-primary data-[popup-open]:border-primary data-[popup-open]:bg-primary/20 data-[popup-open]:text-primary"
|
className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-dashed border-border bg-background text-muted-foreground transition-colors hover:border-primary hover:bg-primary/10 hover:text-primary data-[popup-open]:border-primary data-[popup-open]:bg-primary/20 data-[popup-open]:text-primary"
|
||||||
aria-label="Add step"
|
aria-label="Add step"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="start"
|
align="start"
|
||||||
className="max-h-80 min-w-56 overflow-y-auto border-slate-700 bg-slate-900"
|
className="max-h-80 min-w-56 overflow-y-auto border-border bg-popover"
|
||||||
>
|
>
|
||||||
{ADDABLE_STEPS.map((t) => {
|
{ADDABLE_STEPS.map((t) => {
|
||||||
const Icon = STEP_META[t].icon
|
const Icon = STEP_META[t].icon
|
||||||
@ -1013,7 +1013,7 @@ function AddButton({ onPick }: { onPick: (t: AutomationStepType) => void }) {
|
|||||||
})}
|
})}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<div className="h-4 w-[2px] bg-slate-700" aria-hidden />
|
<div className="h-4 w-[2px] bg-border" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1041,7 +1041,7 @@ function StepEditor({
|
|||||||
value={(cfg.text as string) ?? ""}
|
value={(cfg.text as string) ?? ""}
|
||||||
onChange={(e) => set({ text: e.target.value })}
|
onChange={(e) => set({ text: e.target.value })}
|
||||||
placeholder="Hi! Thanks for reaching out…"
|
placeholder="Hi! Thanks for reaching out…"
|
||||||
className="min-h-24 bg-slate-800 text-white"
|
className="min-h-24 bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
)
|
)
|
||||||
@ -1070,7 +1070,7 @@ function StepEditor({
|
|||||||
<select
|
<select
|
||||||
value={(cfg.mode as string) ?? "round_robin"}
|
value={(cfg.mode as string) ?? "round_robin"}
|
||||||
onChange={(e) => set({ mode: e.target.value })}
|
onChange={(e) => set({ mode: e.target.value })}
|
||||||
className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white"
|
className="w-full rounded-md border border-border bg-muted px-2 py-1.5 text-sm text-foreground"
|
||||||
>
|
>
|
||||||
<option value="round_robin">Round-robin</option>
|
<option value="round_robin">Round-robin</option>
|
||||||
<option value="specific">Specific agent</option>
|
<option value="specific">Specific agent</option>
|
||||||
@ -1100,7 +1100,7 @@ function StepEditor({
|
|||||||
value={(cfg.value as string) ?? ""}
|
value={(cfg.value as string) ?? ""}
|
||||||
onChange={(e) => set({ value: e.target.value })}
|
onChange={(e) => set({ value: e.target.value })}
|
||||||
placeholder="Text or {{ vars.x }} / {{ message.text }}"
|
placeholder="Text or {{ vars.x }} / {{ message.text }}"
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
</>
|
</>
|
||||||
@ -1112,21 +1112,21 @@ function StepEditor({
|
|||||||
<Input
|
<Input
|
||||||
value={(cfg.pipeline_id as string) ?? ""}
|
value={(cfg.pipeline_id as string) ?? ""}
|
||||||
onChange={(e) => set({ pipeline_id: e.target.value })}
|
onChange={(e) => set({ pipeline_id: e.target.value })}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
<FieldBlock label="Stage id">
|
<FieldBlock label="Stage id">
|
||||||
<Input
|
<Input
|
||||||
value={(cfg.stage_id as string) ?? ""}
|
value={(cfg.stage_id as string) ?? ""}
|
||||||
onChange={(e) => set({ stage_id: e.target.value })}
|
onChange={(e) => set({ stage_id: e.target.value })}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
<FieldBlock label="Title">
|
<FieldBlock label="Title">
|
||||||
<Input
|
<Input
|
||||||
value={(cfg.title as string) ?? ""}
|
value={(cfg.title as string) ?? ""}
|
||||||
onChange={(e) => set({ title: e.target.value })}
|
onChange={(e) => set({ title: e.target.value })}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
<FieldBlock label="Value">
|
<FieldBlock label="Value">
|
||||||
@ -1134,7 +1134,7 @@ function StepEditor({
|
|||||||
type="number"
|
type="number"
|
||||||
value={(cfg.value as number) ?? 0}
|
value={(cfg.value as number) ?? 0}
|
||||||
onChange={(e) => set({ value: Number(e.target.value) })}
|
onChange={(e) => set({ value: Number(e.target.value) })}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
</>
|
</>
|
||||||
@ -1148,14 +1148,14 @@ function StepEditor({
|
|||||||
min={1}
|
min={1}
|
||||||
value={(cfg.amount as number) ?? 1}
|
value={(cfg.amount as number) ?? 1}
|
||||||
onChange={(e) => set({ amount: Math.max(1, Number(e.target.value)) })}
|
onChange={(e) => set({ amount: Math.max(1, Number(e.target.value)) })}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
<FieldBlock label="Unit">
|
<FieldBlock label="Unit">
|
||||||
<select
|
<select
|
||||||
value={(cfg.unit as string) ?? "hours"}
|
value={(cfg.unit as string) ?? "hours"}
|
||||||
onChange={(e) => set({ unit: e.target.value })}
|
onChange={(e) => set({ unit: e.target.value })}
|
||||||
className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white"
|
className="w-full rounded-md border border-border bg-muted px-2 py-1.5 text-sm text-foreground"
|
||||||
>
|
>
|
||||||
<option value="minutes">Minutes</option>
|
<option value="minutes">Minutes</option>
|
||||||
<option value="hours">Hours</option>
|
<option value="hours">Hours</option>
|
||||||
@ -1171,7 +1171,7 @@ function StepEditor({
|
|||||||
<select
|
<select
|
||||||
value={(cfg.subject as string) ?? "tag_presence"}
|
value={(cfg.subject as string) ?? "tag_presence"}
|
||||||
onChange={(e) => set({ subject: e.target.value })}
|
onChange={(e) => set({ subject: e.target.value })}
|
||||||
className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-white"
|
className="w-full rounded-md border border-border bg-muted px-2 py-1.5 text-sm text-foreground"
|
||||||
>
|
>
|
||||||
<option value="tag_presence">Tag presence</option>
|
<option value="tag_presence">Tag presence</option>
|
||||||
<option value="contact_field">Contact field</option>
|
<option value="contact_field">Contact field</option>
|
||||||
@ -1192,7 +1192,7 @@ function StepEditor({
|
|||||||
}
|
}
|
||||||
value={(cfg.operand as string) ?? ""}
|
value={(cfg.operand as string) ?? ""}
|
||||||
onChange={(e) => set({ operand: e.target.value })}
|
onChange={(e) => set({ operand: e.target.value })}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
{(cfg.subject === "contact_field" || cfg.subject === "message_content") && (
|
{(cfg.subject === "contact_field" || cfg.subject === "message_content") && (
|
||||||
@ -1200,7 +1200,7 @@ function StepEditor({
|
|||||||
<Input
|
<Input
|
||||||
value={(cfg.value as string) ?? ""}
|
value={(cfg.value as string) ?? ""}
|
||||||
onChange={(e) => set({ value: e.target.value })}
|
onChange={(e) => set({ value: e.target.value })}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
)}
|
)}
|
||||||
@ -1213,21 +1213,21 @@ function StepEditor({
|
|||||||
<Input
|
<Input
|
||||||
value={(cfg.url as string) ?? ""}
|
value={(cfg.url as string) ?? ""}
|
||||||
onChange={(e) => set({ url: e.target.value })}
|
onChange={(e) => set({ url: e.target.value })}
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
<FieldBlock label="Body template (JSON)">
|
<FieldBlock label="Body template (JSON)">
|
||||||
<Textarea
|
<Textarea
|
||||||
value={(cfg.body_template as string) ?? ""}
|
value={(cfg.body_template as string) ?? ""}
|
||||||
onChange={(e) => set({ body_template: e.target.value })}
|
onChange={(e) => set({ body_template: e.target.value })}
|
||||||
className="min-h-20 bg-slate-800 font-mono text-xs text-white"
|
className="min-h-20 bg-muted font-mono text-xs text-foreground"
|
||||||
/>
|
/>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case "close_conversation":
|
case "close_conversation":
|
||||||
return (
|
return (
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-muted-foreground">
|
||||||
Sets the conversation status to "closed". No configuration needed.
|
Sets the conversation status to "closed". No configuration needed.
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
@ -1245,7 +1245,7 @@ function FieldBlock({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 last:mb-0">
|
<div className="mb-2 last:mb-0">
|
||||||
<label className="mb-1 block text-xs font-medium text-slate-400">{label}</label>
|
<label className="mb-1 block text-xs font-medium text-muted-foreground">{label}</label>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -68,17 +68,17 @@ export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-white">Choose a Template</h2>
|
<h2 className="text-lg font-semibold text-foreground">Choose a Template</h2>
|
||||||
<p className="mt-1 text-sm text-slate-400">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Select an approved message template for your broadcast.
|
Select an approved message template for your broadcast.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{templates.length === 0 ? (
|
{templates.length === 0 ? (
|
||||||
<div className="flex h-48 flex-col items-center justify-center rounded-xl border border-slate-800 bg-slate-900/50">
|
<div className="flex h-48 flex-col items-center justify-center rounded-xl border border-border bg-card/50">
|
||||||
<FileText className="mb-2 h-8 w-8 text-slate-600" />
|
<FileText className="mb-2 h-8 w-8 text-muted-foreground" />
|
||||||
<p className="text-sm text-slate-400">No templates available.</p>
|
<p className="text-sm text-muted-foreground">No templates available.</p>
|
||||||
<p className="mt-1 text-xs text-slate-500">Create a template in Settings first.</p>
|
<p className="mt-1 text-xs text-muted-foreground">Create a template in Settings first.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
@ -93,19 +93,19 @@ export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack
|
|||||||
className={`flex flex-col gap-3 rounded-xl border p-4 text-left transition-all ${
|
className={`flex flex-col gap-3 rounded-xl border p-4 text-left transition-all ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'border-primary bg-primary/5 ring-1 ring-primary/30'
|
? 'border-primary bg-primary/5 ring-1 ring-primary/30'
|
||||||
: 'border-slate-800 bg-slate-900/50 hover:border-slate-700 hover:bg-slate-900'
|
: 'border-border bg-card/50 hover:border-border hover:bg-card'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<h3 className="text-sm font-medium text-white">{template.name}</h3>
|
<h3 className="text-sm font-medium text-foreground">{template.name}</h3>
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium ${catColor}`}
|
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium ${catColor}`}
|
||||||
>
|
>
|
||||||
{template.category}
|
{template.category}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="line-clamp-3 text-xs text-slate-400">{template.body_text}</p>
|
<p className="line-clamp-3 text-xs text-muted-foreground">{template.body_text}</p>
|
||||||
<div className="flex items-center gap-2 text-[10px] text-slate-500">
|
<div className="flex items-center gap-2 text-[10px] text-muted-foreground">
|
||||||
<span>{template.language ?? 'en_US'}</span>
|
<span>{template.language ?? 'en_US'}</span>
|
||||||
{/* Status is omitted on purpose — every template
|
{/* Status is omitted on purpose — every template
|
||||||
shown here is already filtered to APPROVED,
|
shown here is already filtered to APPROVED,
|
||||||
@ -117,8 +117,8 @@ export function Step1ChooseTemplate({ selectedTemplate, onSelect, onNext, onBack
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between border-t border-slate-800 pt-4">
|
<div className="flex items-center justify-between border-t border-border pt-4">
|
||||||
<Button variant="outline" onClick={onBack} className="border-slate-700 text-slate-300">
|
<Button variant="outline" onClick={onBack} className="border-border text-muted-foreground">
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -249,8 +249,8 @@ export function Step2SelectAudience({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-white">Select Audience</h2>
|
<h2 className="text-lg font-semibold text-foreground">Select Audience</h2>
|
||||||
<p className="mt-1 text-sm text-slate-400">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Choose who will receive this broadcast.
|
Choose who will receive this broadcast.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -280,21 +280,21 @@ export function Step2SelectAudience({
|
|||||||
className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${
|
className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'border-primary bg-primary/5 ring-1 ring-primary/30'
|
? 'border-primary bg-primary/5 ring-1 ring-primary/30'
|
||||||
: 'border-slate-800 bg-slate-900/50 hover:border-slate-700'
|
: 'border-border bg-card/50 hover:border-border'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${
|
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-primary/10 text-primary'
|
? 'bg-primary/10 text-primary'
|
||||||
: 'bg-slate-800 text-slate-400'
|
: 'bg-muted text-muted-foreground'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-white">{option.label}</p>
|
<p className="text-sm font-medium text-foreground">{option.label}</p>
|
||||||
<p className="mt-0.5 text-xs text-slate-400">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
{option.description}
|
{option.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -304,12 +304,12 @@ export function Step2SelectAudience({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{audience.type === 'tags' && (
|
{audience.type === 'tags' && (
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
|
<div className="rounded-xl border border-border bg-card/50 p-4">
|
||||||
<p className="mb-3 text-sm font-medium text-white">Select Tags</p>
|
<p className="mb-3 text-sm font-medium text-foreground">Select Tags</p>
|
||||||
{loadingTags ? (
|
{loadingTags ? (
|
||||||
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
||||||
) : tags.length === 0 ? (
|
) : tags.length === 0 ? (
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-muted-foreground">
|
||||||
No tags found. Create tags in Settings.
|
No tags found. Create tags in Settings.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
@ -323,7 +323,7 @@ export function Step2SelectAudience({
|
|||||||
className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-all ${
|
className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-all ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'border-primary/30 bg-primary/10 text-primary'
|
? 'border-primary/30 bg-primary/10 text-primary'
|
||||||
: 'border-slate-700 bg-slate-800 text-slate-300 hover:border-slate-600'
|
: 'border-border bg-muted text-muted-foreground hover:border-border'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -340,12 +340,12 @@ export function Step2SelectAudience({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{audience.type === 'custom_field' && (
|
{audience.type === 'custom_field' && (
|
||||||
<div className="space-y-3 rounded-xl border border-slate-800 bg-slate-900/50 p-4">
|
<div className="space-y-3 rounded-xl border border-border bg-card/50 p-4">
|
||||||
<p className="text-sm font-medium text-white">Custom Field Filter</p>
|
<p className="text-sm font-medium text-foreground">Custom Field Filter</p>
|
||||||
{loadingFields ? (
|
{loadingFields ? (
|
||||||
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
||||||
) : customFields.length === 0 ? (
|
) : customFields.length === 0 ? (
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-muted-foreground">
|
||||||
No custom fields defined. Create one in Settings → Custom Fields.
|
No custom fields defined. Create one in Settings → Custom Fields.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
@ -353,7 +353,7 @@ export function Step2SelectAudience({
|
|||||||
<select
|
<select
|
||||||
value={audience.customField?.fieldId ?? ''}
|
value={audience.customField?.fieldId ?? ''}
|
||||||
onChange={(e) => updateCustomField({ fieldId: e.target.value })}
|
onChange={(e) => updateCustomField({ fieldId: e.target.value })}
|
||||||
className="h-9 rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary focus:ring-1 focus:ring-primary"
|
className="h-9 rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary"
|
||||||
>
|
>
|
||||||
<option value="">Select field…</option>
|
<option value="">Select field…</option>
|
||||||
{customFields.map((f) => (
|
{customFields.map((f) => (
|
||||||
@ -369,7 +369,7 @@ export function Step2SelectAudience({
|
|||||||
operator: e.target.value as CustomFieldOperator,
|
operator: e.target.value as CustomFieldOperator,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="h-9 rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary focus:ring-1 focus:ring-primary"
|
className="h-9 rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary"
|
||||||
>
|
>
|
||||||
{OPERATOR_OPTIONS.map((op) => (
|
{OPERATOR_OPTIONS.map((op) => (
|
||||||
<option key={op.value} value={op.value}>
|
<option key={op.value} value={op.value}>
|
||||||
@ -382,7 +382,7 @@ export function Step2SelectAudience({
|
|||||||
value={audience.customField?.value ?? ''}
|
value={audience.customField?.value ?? ''}
|
||||||
onChange={(e) => updateCustomField({ value: e.target.value })}
|
onChange={(e) => updateCustomField({ value: e.target.value })}
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
className="h-9 rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none placeholder:text-slate-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
className="h-9 rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -390,16 +390,16 @@ export function Step2SelectAudience({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Exclude list — applies regardless of audience type */}
|
{/* Exclude list — applies regardless of audience type */}
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
|
<div className="rounded-xl border border-border bg-card/50 p-4">
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<X className="h-4 w-4 text-red-400" />
|
<X className="h-4 w-4 text-red-400" />
|
||||||
<p className="text-sm font-medium text-white">
|
<p className="text-sm font-medium text-foreground">
|
||||||
Exclude contacts with these tags
|
Exclude contacts with these tags
|
||||||
</p>
|
</p>
|
||||||
<span className="text-xs text-slate-500">(optional)</span>
|
<span className="text-xs text-muted-foreground">(optional)</span>
|
||||||
</div>
|
</div>
|
||||||
{tags.length === 0 ? (
|
{tags.length === 0 ? (
|
||||||
<p className="text-xs text-slate-500">No tags available.</p>
|
<p className="text-xs text-muted-foreground">No tags available.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
@ -411,7 +411,7 @@ export function Step2SelectAudience({
|
|||||||
className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-all ${
|
className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-all ${
|
||||||
isExcluded
|
isExcluded
|
||||||
? 'border-red-500/30 bg-red-500/10 text-red-300'
|
? 'border-red-500/30 bg-red-500/10 text-red-300'
|
||||||
: 'border-slate-700 bg-slate-800 text-slate-300 hover:border-slate-600'
|
: 'border-border bg-muted text-muted-foreground hover:border-border'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -427,33 +427,33 @@ export function Step2SelectAudience({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Audience Summary */}
|
{/* Audience Summary */}
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
|
<div className="rounded-xl border border-border bg-card/50 p-4">
|
||||||
<p className="mb-2 text-sm font-medium text-white">Audience Summary</p>
|
<p className="mb-2 text-sm font-medium text-foreground">Audience Summary</p>
|
||||||
{loadingCount ? (
|
{loadingCount ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-primary" />
|
<Loader2 className="h-4 w-4 animate-spin text-primary" />
|
||||||
<span className="text-xs text-slate-400">Calculating…</span>
|
<span className="text-xs text-muted-foreground">Calculating…</span>
|
||||||
</div>
|
</div>
|
||||||
) : estimatedCount !== null ? (
|
) : estimatedCount !== null ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Users className="h-4 w-4 text-primary" />
|
<Users className="h-4 w-4 text-primary" />
|
||||||
<span className="text-sm text-white">
|
<span className="text-sm text-foreground">
|
||||||
{estimatedCount.toLocaleString()}
|
{estimatedCount.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-slate-400">estimated recipients</span>
|
<span className="text-xs text-muted-foreground">estimated recipients</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
Select an audience type to see the estimate.
|
Select an audience type to see the estimate.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between border-t border-slate-800 pt-4">
|
<div className="flex items-center justify-between border-t border-border pt-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="border-slate-700 text-slate-300"
|
className="border-border text-muted-foreground"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Back
|
Back
|
||||||
|
|||||||
@ -187,16 +187,16 @@ export function Step3Personalize({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-white">Personalize Message</h2>
|
<h2 className="text-lg font-semibold text-foreground">Personalize Message</h2>
|
||||||
<p className="mt-1 text-sm text-slate-400">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Map template variables to contact fields, custom fields, or static
|
Map template variables to contact fields, custom fields, or static
|
||||||
values.
|
values.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{placeholders.length === 0 ? (
|
{placeholders.length === 0 ? (
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-6 text-center">
|
<div className="rounded-xl border border-border bg-card/50 p-6 text-center">
|
||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm text-muted-foreground">
|
||||||
This template has no variables to personalize.
|
This template has no variables to personalize.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -209,7 +209,7 @@ export function Step3Personalize({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={placeholder}
|
key={placeholder}
|
||||||
className="rounded-xl border border-slate-800 bg-slate-900/50 p-4"
|
className="rounded-xl border border-border bg-card/50 p-4"
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<span className="inline-flex items-center rounded-md bg-primary/10 px-2 py-0.5 text-xs font-mono font-medium text-primary">
|
<span className="inline-flex items-center rounded-md bg-primary/10 px-2 py-0.5 text-xs font-mono font-medium text-primary">
|
||||||
@ -219,7 +219,7 @@ export function Step3Personalize({
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-xs font-medium text-slate-400">
|
<label className="mb-1.5 block text-xs font-medium text-muted-foreground">
|
||||||
Mapping Type
|
Mapping Type
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
@ -231,10 +231,10 @@ export function Step3Personalize({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full border-slate-700 bg-slate-800 text-white">
|
<SelectTrigger className="w-full border-border bg-muted text-foreground">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="border-slate-700 bg-slate-800">
|
<SelectContent className="border-border bg-popover">
|
||||||
<SelectItem value="static">Static Value</SelectItem>
|
<SelectItem value="static">Static Value</SelectItem>
|
||||||
<SelectItem value="field">Contact Field</SelectItem>
|
<SelectItem value="field">Contact Field</SelectItem>
|
||||||
<SelectItem value="custom_field">
|
<SelectItem value="custom_field">
|
||||||
@ -245,7 +245,7 @@ export function Step3Personalize({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-xs font-medium text-slate-400">
|
<label className="mb-1.5 block text-xs font-medium text-muted-foreground">
|
||||||
{mapping.type === 'static' ? 'Value' : 'Field'}
|
{mapping.type === 'static' ? 'Value' : 'Field'}
|
||||||
</label>
|
</label>
|
||||||
{mapping.type === 'static' ? (
|
{mapping.type === 'static' ? (
|
||||||
@ -255,7 +255,7 @@ export function Step3Personalize({
|
|||||||
updateVariable(key, { value: e.target.value })
|
updateVariable(key, { value: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder="Enter value..."
|
placeholder="Enter value..."
|
||||||
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500"
|
className="border-border bg-muted text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
) : mapping.type === 'field' ? (
|
) : mapping.type === 'field' ? (
|
||||||
<Select
|
<Select
|
||||||
@ -264,10 +264,10 @@ export function Step3Personalize({
|
|||||||
updateVariable(key, { value: val || '' })
|
updateVariable(key, { value: val || '' })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full border-slate-700 bg-slate-800 text-white">
|
<SelectTrigger className="w-full border-border bg-muted text-foreground">
|
||||||
<SelectValue placeholder="Select field..." />
|
<SelectValue placeholder="Select field..." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="border-slate-700 bg-slate-800">
|
<SelectContent className="border-border bg-popover">
|
||||||
{contactFields.map((field) => (
|
{contactFields.map((field) => (
|
||||||
<SelectItem key={field.value} value={field.value}>
|
<SelectItem key={field.value} value={field.value}>
|
||||||
{field.label}
|
{field.label}
|
||||||
@ -282,7 +282,7 @@ export function Step3Personalize({
|
|||||||
updateVariable(key, { value: val || '' })
|
updateVariable(key, { value: val || '' })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full border-slate-700 bg-slate-800 text-white">
|
<SelectTrigger className="w-full border-border bg-muted text-foreground">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={
|
placeholder={
|
||||||
loadingFields
|
loadingFields
|
||||||
@ -293,7 +293,7 @@ export function Step3Personalize({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="border-slate-700 bg-slate-800">
|
<SelectContent className="border-border bg-popover">
|
||||||
{customFields.map((f) => (
|
{customFields.map((f) => (
|
||||||
<SelectItem key={f.id} value={f.id}>
|
<SelectItem key={f.id} value={f.id}>
|
||||||
{f.field_name}
|
{f.field_name}
|
||||||
@ -312,11 +312,11 @@ export function Step3Personalize({
|
|||||||
|
|
||||||
{/* Live Preview — rendered as a WhatsApp-style bubble so the user
|
{/* Live Preview — rendered as a WhatsApp-style bubble so the user
|
||||||
sees approximately what the recipient will see. */}
|
sees approximately what the recipient will see. */}
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
|
<div className="rounded-xl border border-border bg-card/50 p-4">
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<Eye className="h-4 w-4 text-primary" />
|
<Eye className="h-4 w-4 text-primary" />
|
||||||
<p className="text-sm font-medium text-white">Live Preview</p>
|
<p className="text-sm font-medium text-foreground">Live Preview</p>
|
||||||
<span className="text-xs text-slate-500">({previewLabel})</span>
|
<span className="text-xs text-muted-foreground">({previewLabel})</span>
|
||||||
{loadingPreview && (
|
{loadingPreview && (
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
|
<Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
|
||||||
)}
|
)}
|
||||||
@ -340,11 +340,11 @@ export function Step3Personalize({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between border-t border-slate-800 pt-4">
|
<div className="flex items-center justify-between border-t border-border pt-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="border-slate-700 text-slate-300"
|
className="border-border text-muted-foreground"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Back
|
Back
|
||||||
|
|||||||
@ -93,51 +93,51 @@ export function Step4ScheduleSend({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-white">Review & Send</h2>
|
<h2 className="text-lg font-semibold text-foreground">Review & Send</h2>
|
||||||
<p className="mt-1 text-sm text-slate-400">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Name your broadcast, review the details, and send.
|
Name your broadcast, review the details, and send.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Broadcast Name */}
|
{/* Broadcast Name */}
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-white">Broadcast Name</label>
|
<label className="mb-1.5 block text-sm font-medium text-foreground">Broadcast Name</label>
|
||||||
<Input
|
<Input
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => onNameChange(e.target.value)}
|
onChange={(e) => onNameChange(e.target.value)}
|
||||||
placeholder="e.g. Summer Sale Announcement"
|
placeholder="e.g. Summer Sale Announcement"
|
||||||
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500"
|
className="border-border bg-muted text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary Card */}
|
{/* Summary Card */}
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4 space-y-3">
|
<div className="rounded-xl border border-border bg-card/50 p-4 space-y-3">
|
||||||
<p className="text-sm font-medium text-white">Summary</p>
|
<p className="text-sm font-medium text-foreground">Summary</p>
|
||||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-400">Template</p>
|
<p className="text-xs text-muted-foreground">Template</p>
|
||||||
<p className="text-white">{template.name}</p>
|
<p className="text-foreground">{template.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-400">Audience</p>
|
<p className="text-xs text-muted-foreground">Audience</p>
|
||||||
<p className="text-white">{audienceLabel}</p>
|
<p className="text-foreground">{audienceLabel}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-400">Estimated Reach</p>
|
<p className="text-xs text-muted-foreground">Estimated Reach</p>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{loadingReach ? (
|
{loadingReach ? (
|
||||||
<Loader2 className="h-3 w-3 animate-spin text-primary" />
|
<Loader2 className="h-3 w-3 animate-spin text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Users className="h-3.5 w-3.5 text-primary" />
|
<Users className="h-3.5 w-3.5 text-primary" />
|
||||||
<p className="font-medium text-white">{estimatedReach.toLocaleString()}</p>
|
<p className="font-medium text-foreground">{estimatedReach.toLocaleString()}</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-400">Language</p>
|
<p className="text-xs text-muted-foreground">Language</p>
|
||||||
<p className="text-white">{template.language ?? 'en_US'}</p>
|
<p className="text-foreground">{template.language ?? 'en_US'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -148,11 +148,11 @@ export function Step4ScheduleSend({
|
|||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-primary" />
|
<Loader2 className="h-4 w-4 animate-spin text-primary" />
|
||||||
<p className="text-sm font-medium text-white">Sending broadcast...</p>
|
<p className="text-sm font-medium text-foreground">Sending broadcast...</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-medium text-primary">{progress}%</span>
|
<span className="text-xs font-medium text-primary">{progress}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1.5 w-full rounded-full bg-slate-800">
|
<div className="h-1.5 w-full rounded-full bg-muted">
|
||||||
<div
|
<div
|
||||||
className="h-1.5 rounded-full bg-primary transition-all duration-300"
|
className="h-1.5 rounded-full bg-primary transition-all duration-300"
|
||||||
style={{ width: `${progress}%` }}
|
style={{ width: `${progress}%` }}
|
||||||
@ -161,12 +161,12 @@ export function Step4ScheduleSend({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-slate-800 pt-4">
|
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-border pt-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
className="border-slate-700 text-slate-300"
|
className="border-border text-muted-foreground"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Back
|
Back
|
||||||
@ -178,7 +178,7 @@ export function Step4ScheduleSend({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onSaveDraft}
|
onClick={onSaveDraft}
|
||||||
disabled={!name.trim() || isProcessing}
|
disabled={!name.trim() || isProcessing}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800 disabled:opacity-50"
|
className="border-border text-muted-foreground hover:bg-muted disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Save className="h-4 w-4" />
|
<Save className="h-4 w-4" />
|
||||||
Save as Draft
|
Save as Draft
|
||||||
@ -197,14 +197,14 @@ export function Step4ScheduleSend({
|
|||||||
<Send className="h-4 w-4" />
|
<Send className="h-4 w-4" />
|
||||||
Send Broadcast
|
Send Broadcast
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="border-slate-700 bg-slate-900 sm:max-w-md">
|
<DialogContent className="border-border bg-popover sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">Confirm Broadcast</DialogTitle>
|
<DialogTitle className="text-popover-foreground">Confirm Broadcast</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
You are about to send this broadcast to{' '}
|
You are about to send this broadcast to{' '}
|
||||||
<span className="font-medium text-white">{estimatedReach.toLocaleString()}</span>{' '}
|
<span className="font-medium text-popover-foreground">{estimatedReach.toLocaleString()}</span>{' '}
|
||||||
contacts using the{' '}
|
contacts using the{' '}
|
||||||
<span className="font-medium text-white">{template.name}</span> template.
|
<span className="font-medium text-popover-foreground">{template.name}</span> template.
|
||||||
This action cannot be undone.
|
This action cannot be undone.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -212,7 +212,7 @@ export function Step4ScheduleSend({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setShowConfirm(false)}
|
onClick={() => setShowConfirm(false)}
|
||||||
className="border-slate-700 text-slate-300"
|
className="border-border text-muted-foreground"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -331,7 +331,7 @@ export function ContactDetailView({
|
|||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent
|
<SheetContent
|
||||||
side="right"
|
side="right"
|
||||||
className="bg-slate-900 border-slate-700 text-slate-200 sm:max-w-lg w-full p-0"
|
className="bg-popover border-border text-popover-foreground sm:max-w-lg w-full p-0"
|
||||||
>
|
>
|
||||||
{loading || !contact ? (
|
{loading || !contact ? (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
@ -340,21 +340,21 @@ export function ContactDetailView({
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<SheetHeader className="p-4 border-b border-slate-700/50">
|
<SheetHeader className="p-4 border-b border-border/50">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar className="size-12 bg-slate-800 border border-slate-700">
|
<Avatar className="size-12 bg-muted border border-border">
|
||||||
<AvatarFallback className="bg-primary/10 text-primary text-sm font-medium">
|
<AvatarFallback className="bg-primary/10 text-primary text-sm font-medium">
|
||||||
{getInitials(contact.name)}
|
{getInitials(contact.name)}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<SheetTitle className="text-white truncate">
|
<SheetTitle className="text-popover-foreground truncate">
|
||||||
{contact.name || 'Unknown'}
|
{contact.name || 'Unknown'}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
<SheetDescription className="text-slate-400 text-xs mt-0.5">
|
<SheetDescription className="text-muted-foreground text-xs mt-0.5">
|
||||||
Contact details
|
Contact details
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
<div className="flex flex-wrap items-center gap-3 mt-1.5 text-xs text-slate-400">
|
<div className="flex flex-wrap items-center gap-3 mt-1.5 text-xs text-muted-foreground">
|
||||||
<button
|
<button
|
||||||
onClick={copyPhone}
|
onClick={copyPhone}
|
||||||
className="flex items-center gap-1 hover:text-primary transition-colors cursor-pointer"
|
className="flex items-center gap-1 hover:text-primary transition-colors cursor-pointer"
|
||||||
@ -386,34 +386,34 @@ export function ContactDetailView({
|
|||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<Tabs defaultValue="details" className="flex-1 flex flex-col min-h-0">
|
<Tabs defaultValue="details" className="flex-1 flex flex-col min-h-0">
|
||||||
<TabsList className="bg-slate-800/50 border-b border-slate-700 mx-4 mt-3">
|
<TabsList className="bg-muted/50 border-b border-border mx-4 mt-3">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="details"
|
value="details"
|
||||||
className="data-active:bg-slate-800 data-active:text-primary text-slate-400"
|
className="data-active:bg-muted data-active:text-primary text-muted-foreground"
|
||||||
>
|
>
|
||||||
Details
|
Details
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="tags"
|
value="tags"
|
||||||
className="data-active:bg-slate-800 data-active:text-primary text-slate-400"
|
className="data-active:bg-muted data-active:text-primary text-muted-foreground"
|
||||||
>
|
>
|
||||||
Tags
|
Tags
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="notes"
|
value="notes"
|
||||||
className="data-active:bg-slate-800 data-active:text-primary text-slate-400"
|
className="data-active:bg-muted data-active:text-primary text-muted-foreground"
|
||||||
>
|
>
|
||||||
Notes
|
Notes
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="custom"
|
value="custom"
|
||||||
className="data-active:bg-slate-800 data-active:text-primary text-slate-400"
|
className="data-active:bg-muted data-active:text-primary text-muted-foreground"
|
||||||
>
|
>
|
||||||
Custom Fields
|
Custom Fields
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="deals"
|
value="deals"
|
||||||
className="data-active:bg-slate-800 data-active:text-primary text-slate-400"
|
className="data-active:bg-muted data-active:text-primary text-muted-foreground"
|
||||||
>
|
>
|
||||||
Deals
|
Deals
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@ -423,37 +423,37 @@ export function ContactDetailView({
|
|||||||
<TabsContent value="details" className="flex-1 overflow-y-auto px-4 py-3">
|
<TabsContent value="details" className="flex-1 overflow-y-auto px-4 py-3">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-slate-400 text-xs">Name</Label>
|
<Label className="text-muted-foreground text-xs">Name</Label>
|
||||||
<Input
|
<Input
|
||||||
value={editName}
|
value={editName}
|
||||||
onChange={(e) => setEditName(e.target.value)}
|
onChange={(e) => setEditName(e.target.value)}
|
||||||
className="bg-slate-800 border-slate-700 text-white h-8 text-sm"
|
className="bg-muted border-border text-foreground h-8 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-slate-400 text-xs">
|
<Label className="text-muted-foreground text-xs">
|
||||||
Phone <span className="text-red-400">*</span>
|
Phone <span className="text-red-400">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
value={editPhone}
|
value={editPhone}
|
||||||
onChange={(e) => setEditPhone(e.target.value)}
|
onChange={(e) => setEditPhone(e.target.value)}
|
||||||
className="bg-slate-800 border-slate-700 text-white h-8 text-sm"
|
className="bg-muted border-border text-foreground h-8 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-slate-400 text-xs">Email</Label>
|
<Label className="text-muted-foreground text-xs">Email</Label>
|
||||||
<Input
|
<Input
|
||||||
value={editEmail}
|
value={editEmail}
|
||||||
onChange={(e) => setEditEmail(e.target.value)}
|
onChange={(e) => setEditEmail(e.target.value)}
|
||||||
className="bg-slate-800 border-slate-700 text-white h-8 text-sm"
|
className="bg-muted border-border text-foreground h-8 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-slate-400 text-xs">Company</Label>
|
<Label className="text-muted-foreground text-xs">Company</Label>
|
||||||
<Input
|
<Input
|
||||||
value={editCompany}
|
value={editCompany}
|
||||||
onChange={(e) => setEditCompany(e.target.value)}
|
onChange={(e) => setEditCompany(e.target.value)}
|
||||||
className="bg-slate-800 border-slate-700 text-white h-8 text-sm"
|
className="bg-muted border-border text-foreground h-8 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@ -475,11 +475,11 @@ export function ContactDetailView({
|
|||||||
{/* Tags Tab */}
|
{/* Tags Tab */}
|
||||||
<TabsContent value="tags" className="flex-1 overflow-y-auto px-4 py-3">
|
<TabsContent value="tags" className="flex-1 overflow-y-auto px-4 py-3">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-muted-foreground">
|
||||||
Click a tag to add or remove it from this contact.
|
Click a tag to add or remove it from this contact.
|
||||||
</p>
|
</p>
|
||||||
{allTags.length === 0 ? (
|
{allTags.length === 0 ? (
|
||||||
<p className="text-sm text-slate-500">
|
<p className="text-sm text-muted-foreground">
|
||||||
No tags available. Create tags in Settings.
|
No tags available. Create tags in Settings.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
@ -493,7 +493,7 @@ export function ContactDetailView({
|
|||||||
disabled={savingTags}
|
disabled={savingTags}
|
||||||
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium transition-all cursor-pointer ${
|
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium transition-all cursor-pointer ${
|
||||||
selected
|
selected
|
||||||
? 'ring-2 ring-primary ring-offset-1 ring-offset-slate-900'
|
? 'ring-2 ring-primary ring-offset-1 ring-offset-border'
|
||||||
: 'opacity-50 hover:opacity-80'
|
: 'opacity-50 hover:opacity-80'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@ -518,7 +518,7 @@ export function ContactDetailView({
|
|||||||
value={newNote}
|
value={newNote}
|
||||||
onChange={(e) => setNewNote(e.target.value)}
|
onChange={(e) => setNewNote(e.target.value)}
|
||||||
placeholder="Write a note..."
|
placeholder="Write a note..."
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 min-h-[60px] text-sm resize-none"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground min-h-[60px] text-sm resize-none"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={addNote}
|
onClick={addNote}
|
||||||
@ -538,30 +538,30 @@ export function ContactDetailView({
|
|||||||
<div className="flex-1 overflow-y-auto space-y-2">
|
<div className="flex-1 overflow-y-auto space-y-2">
|
||||||
{loadingNotes ? (
|
{loadingNotes ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Loader2 className="size-5 animate-spin text-slate-500" />
|
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
) : notes.length === 0 ? (
|
) : notes.length === 0 ? (
|
||||||
<p className="text-sm text-slate-500 text-center py-8">
|
<p className="text-sm text-muted-foreground text-center py-8">
|
||||||
No notes yet.
|
No notes yet.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
notes.map((note) => (
|
notes.map((note) => (
|
||||||
<div
|
<div
|
||||||
key={note.id}
|
key={note.id}
|
||||||
className="rounded-lg bg-slate-800/50 border border-slate-700/50 p-3 group"
|
className="rounded-lg bg-muted/50 border border-border/50 p-3 group"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<p className="text-sm text-slate-300 whitespace-pre-wrap flex-1">
|
<p className="text-sm text-muted-foreground whitespace-pre-wrap flex-1">
|
||||||
{note.note_text}
|
{note.note_text}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => deleteNote(note.id)}
|
onClick={() => deleteNote(note.id)}
|
||||||
className="opacity-0 group-hover:opacity-100 text-slate-500 hover:text-red-400 transition-all cursor-pointer shrink-0"
|
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-red-400 transition-all cursor-pointer shrink-0"
|
||||||
>
|
>
|
||||||
<Trash2 className="size-3.5" />
|
<Trash2 className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-500 mt-1.5">
|
<p className="text-xs text-muted-foreground mt-1.5">
|
||||||
{new Date(note.created_at).toLocaleDateString('en-US', {
|
{new Date(note.created_at).toLocaleDateString('en-US', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@ -580,17 +580,17 @@ export function ContactDetailView({
|
|||||||
<TabsContent value="custom" className="flex-1 overflow-y-auto px-4 py-3">
|
<TabsContent value="custom" className="flex-1 overflow-y-auto px-4 py-3">
|
||||||
{loadingCustom ? (
|
{loadingCustom ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Loader2 className="size-5 animate-spin text-slate-500" />
|
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
) : customFields.length === 0 ? (
|
) : customFields.length === 0 ? (
|
||||||
<p className="text-sm text-slate-500 text-center py-8">
|
<p className="text-sm text-muted-foreground text-center py-8">
|
||||||
No custom fields defined. Create them in Settings.
|
No custom fields defined. Create them in Settings.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{customFields.map((field) => (
|
{customFields.map((field) => (
|
||||||
<div key={field.id} className="space-y-1.5">
|
<div key={field.id} className="space-y-1.5">
|
||||||
<Label className="text-slate-400 text-xs capitalize">
|
<Label className="text-muted-foreground text-xs capitalize">
|
||||||
{field.field_name}
|
{field.field_name}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -602,7 +602,7 @@ export function ContactDetailView({
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
placeholder={`Enter ${field.field_name}...`}
|
placeholder={`Enter ${field.field_name}...`}
|
||||||
className="bg-slate-800 border-slate-700 text-white h-8 text-sm placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground h-8 text-sm placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -630,16 +630,16 @@ export function ContactDetailView({
|
|||||||
<Loader2 className="size-5 animate-spin text-primary" />
|
<Loader2 className="size-5 animate-spin text-primary" />
|
||||||
</div>
|
</div>
|
||||||
) : deals.length === 0 ? (
|
) : deals.length === 0 ? (
|
||||||
<p className="text-xs text-slate-500">No deals yet</p>
|
<p className="text-xs text-muted-foreground">No deals yet</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{deals.map((deal) => (
|
{deals.map((deal) => (
|
||||||
<div
|
<div
|
||||||
key={deal.id}
|
key={deal.id}
|
||||||
className="rounded-lg border border-slate-700 bg-slate-800/50 p-3"
|
className="rounded-lg border border-border bg-muted/50 p-3"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<p className="text-sm font-medium text-white">
|
<p className="text-sm font-medium text-foreground">
|
||||||
{deal.title}
|
{deal.title}
|
||||||
</p>
|
</p>
|
||||||
{deal.stage && (
|
{deal.stage && (
|
||||||
@ -654,7 +654,7 @@ export function ContactDetailView({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1.5 flex items-center justify-between text-xs text-slate-400">
|
<div className="mt-1.5 flex items-center justify-between text-xs text-muted-foreground">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<DollarSign className="size-3" />
|
<DollarSign className="size-3" />
|
||||||
{formatCurrency(
|
{formatCurrency(
|
||||||
|
|||||||
@ -223,12 +223,12 @@ export function ContactForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="bg-slate-900 border-slate-700 text-slate-200 sm:max-w-md">
|
<DialogContent className="bg-popover border-border text-popover-foreground sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">
|
<DialogTitle className="text-popover-foreground">
|
||||||
{isEdit ? 'Edit Contact' : 'Add Contact'}
|
{isEdit ? 'Edit Contact' : 'Add Contact'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
{isEdit
|
{isEdit
|
||||||
? 'Update the contact details below.'
|
? 'Update the contact details below.'
|
||||||
: 'Fill in the details to create a new contact.'}
|
: 'Fill in the details to create a new contact.'}
|
||||||
@ -237,7 +237,7 @@ export function ContactForm({
|
|||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="cf-name" className="text-slate-300">
|
<Label htmlFor="cf-name" className="text-muted-foreground">
|
||||||
Name
|
Name
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -245,12 +245,12 @@ export function ContactForm({
|
|||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="John Doe"
|
placeholder="John Doe"
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="cf-phone" className="text-slate-300">
|
<Label htmlFor="cf-phone" className="text-muted-foreground">
|
||||||
Phone <span className="text-red-400">*</span>
|
Phone <span className="text-red-400">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -262,7 +262,7 @@ export function ContactForm({
|
|||||||
}}
|
}}
|
||||||
onBlur={checkDuplicate}
|
onBlur={checkDuplicate}
|
||||||
placeholder="+1 234 567 8900"
|
placeholder="+1 234 567 8900"
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
{dupMatch ? (
|
{dupMatch ? (
|
||||||
<div
|
<div
|
||||||
@ -291,14 +291,14 @@ export function ContactForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
Include country code, e.g. +1 for US
|
Include country code, e.g. +1 for US
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="cf-email" className="text-slate-300">
|
<Label htmlFor="cf-email" className="text-muted-foreground">
|
||||||
Email
|
Email
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -307,12 +307,12 @@ export function ContactForm({
|
|||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="john@example.com"
|
placeholder="john@example.com"
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="cf-company" className="text-slate-300">
|
<Label htmlFor="cf-company" className="text-muted-foreground">
|
||||||
Company
|
Company
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -320,19 +320,19 @@ export function ContactForm({
|
|||||||
value={company}
|
value={company}
|
||||||
onChange={(e) => setCompany(e.target.value)}
|
onChange={(e) => setCompany(e.target.value)}
|
||||||
placeholder="Acme Inc."
|
placeholder="Acme Inc."
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Tags</Label>
|
<Label className="text-muted-foreground">Tags</Label>
|
||||||
{loadingTags ? (
|
{loadingTags ? (
|
||||||
<div className="flex items-center gap-2 text-slate-500 text-sm">
|
<div className="flex items-center gap-2 text-muted-foreground text-sm">
|
||||||
<Loader2 className="size-3 animate-spin" />
|
<Loader2 className="size-3 animate-spin" />
|
||||||
Loading tags...
|
Loading tags...
|
||||||
</div>
|
</div>
|
||||||
) : tags.length === 0 ? (
|
) : tags.length === 0 ? (
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
No tags available. Create tags in Settings.
|
No tags available. Create tags in Settings.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
@ -346,7 +346,7 @@ export function ContactForm({
|
|||||||
onClick={() => toggleTag(tag.id)}
|
onClick={() => toggleTag(tag.id)}
|
||||||
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors cursor-pointer ${
|
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors cursor-pointer ${
|
||||||
selected
|
selected
|
||||||
? 'ring-2 ring-primary ring-offset-1 ring-offset-slate-900'
|
? 'ring-2 ring-primary ring-offset-1 ring-offset-border'
|
||||||
: 'opacity-60 hover:opacity-100'
|
: 'opacity-60 hover:opacity-100'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@ -363,12 +363,12 @@ export function ContactForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="bg-slate-900 border-slate-700">
|
<DialogFooter className="bg-popover border-border">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -33,10 +33,10 @@ export function CustomFieldsManager({
|
|||||||
}: CustomFieldsManagerProps) {
|
}: CustomFieldsManagerProps) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="border-slate-700 bg-slate-900 text-slate-200 sm:max-w-md">
|
<DialogContent className="border-border bg-popover text-popover-foreground sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">Custom fields</DialogTitle>
|
<DialogTitle className="text-popover-foreground">Custom fields</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
Define extra contact fields (e.g. ZIP code, lead source). They
|
Define extra contact fields (e.g. ZIP code, lead source). They
|
||||||
appear on every contact and in the “Update Contact Field” automation
|
appear on every contact and in the “Update Contact Field” automation
|
||||||
action.
|
action.
|
||||||
@ -185,7 +185,7 @@ export function CustomFieldsPanel() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="New field name…"
|
placeholder="New field name…"
|
||||||
className="bg-slate-800 text-white"
|
className="bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
@ -202,18 +202,18 @@ export function CustomFieldsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* List */}
|
{/* List */}
|
||||||
<div className="max-h-72 overflow-y-auto rounded-md border border-slate-800">
|
<div className="max-h-72 overflow-y-auto rounded-md border border-border">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center gap-2 py-8 text-sm text-slate-500">
|
<div className="flex items-center justify-center gap-2 py-8 text-sm text-muted-foreground">
|
||||||
<Loader2 className="size-4 animate-spin" />
|
<Loader2 className="size-4 animate-spin" />
|
||||||
Loading…
|
Loading…
|
||||||
</div>
|
</div>
|
||||||
) : fields.length === 0 ? (
|
) : fields.length === 0 ? (
|
||||||
<p className="py-8 text-center text-sm text-slate-500">
|
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||||
No custom fields yet.
|
No custom fields yet.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="divide-y divide-slate-800">
|
<ul className="divide-y divide-border">
|
||||||
{fields.map((field) => (
|
{fields.map((field) => (
|
||||||
<FieldRow
|
<FieldRow
|
||||||
key={field.id}
|
key={field.id}
|
||||||
@ -265,7 +265,7 @@ function FieldRow({
|
|||||||
if (e.key === 'Enter') e.currentTarget.blur();
|
if (e.key === 'Enter') e.currentTarget.blur();
|
||||||
}}
|
}}
|
||||||
aria-label={`Rename ${field.field_name}`}
|
aria-label={`Rename ${field.field_name}`}
|
||||||
className="focus:border-primary h-8 border-transparent bg-transparent text-white hover:border-slate-700"
|
className="focus:border-primary h-8 border-transparent bg-transparent text-foreground hover:border-border"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -273,7 +273,7 @@ function FieldRow({
|
|||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => onDelete(field)}
|
onClick={() => onDelete(field)}
|
||||||
title="Delete field"
|
title="Delete field"
|
||||||
className="shrink-0 text-slate-400 hover:text-red-400"
|
className="shrink-0 text-muted-foreground hover:text-red-400"
|
||||||
>
|
>
|
||||||
{busy ? (
|
{busy ? (
|
||||||
<Loader2 className="size-4 animate-spin" />
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
|||||||
@ -80,7 +80,7 @@ function ImportPreviewTags({
|
|||||||
tagColorByKey: Map<string, string>;
|
tagColorByKey: Map<string, string>;
|
||||||
}) {
|
}) {
|
||||||
if (tagNames.length === 0) {
|
if (tagNames.length === 0) {
|
||||||
return <span className="text-slate-600">—</span>;
|
return <span className="text-muted-foreground">—</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -398,31 +398,31 @@ export function ImportModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="flex max-h-[min(90vh,720px)] flex-col gap-0 overflow-hidden border-slate-700/80 bg-slate-900 p-0 text-slate-200 sm:max-w-2xl">
|
<DialogContent className="flex max-h-[min(90vh,720px)] flex-col gap-0 overflow-hidden border-border/80 bg-popover p-0 text-popover-foreground sm:max-w-2xl">
|
||||||
<div className="shrink-0 space-y-4 border-b border-slate-800/80 px-6 pt-6 pb-5">
|
<div className="shrink-0 space-y-4 border-b border-border/80 px-6 pt-6 pb-5">
|
||||||
<DialogHeader className="gap-1.5">
|
<DialogHeader className="gap-1.5">
|
||||||
<DialogTitle className="text-lg text-white">
|
<DialogTitle className="text-lg text-popover-foreground">
|
||||||
Import Contacts
|
Import Contacts
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="leading-relaxed text-slate-400">
|
<DialogDescription className="leading-relaxed text-muted-foreground">
|
||||||
Upload a CSV with a required{' '}
|
Upload a CSV with a required{' '}
|
||||||
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300">
|
<code className="rounded bg-muted px-1 py-0.5 text-[11px] text-muted-foreground">
|
||||||
phone
|
phone
|
||||||
</code>{' '}
|
</code>{' '}
|
||||||
column. Optional:{' '}
|
column. Optional:{' '}
|
||||||
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300">
|
<code className="rounded bg-muted px-1 py-0.5 text-[11px] text-muted-foreground">
|
||||||
name
|
name
|
||||||
</code>
|
</code>
|
||||||
,{' '}
|
,{' '}
|
||||||
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300">
|
<code className="rounded bg-muted px-1 py-0.5 text-[11px] text-muted-foreground">
|
||||||
email
|
email
|
||||||
</code>
|
</code>
|
||||||
,{' '}
|
,{' '}
|
||||||
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300">
|
<code className="rounded bg-muted px-1 py-0.5 text-[11px] text-muted-foreground">
|
||||||
company
|
company
|
||||||
</code>
|
</code>
|
||||||
,{' '}
|
,{' '}
|
||||||
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300">
|
<code className="rounded bg-muted px-1 py-0.5 text-[11px] text-muted-foreground">
|
||||||
tags
|
tags
|
||||||
</code>{' '}
|
</code>{' '}
|
||||||
(comma-separated; quote multi-tag cells).
|
(comma-separated; quote multi-tag cells).
|
||||||
@ -441,7 +441,7 @@ export function ImportModal({
|
|||||||
'group flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border border-dashed p-5 transition-all',
|
'group flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border border-dashed p-5 transition-all',
|
||||||
file
|
file
|
||||||
? 'border-primary/35 bg-primary/[0.04]'
|
? 'border-primary/35 bg-primary/[0.04]'
|
||||||
: 'hover:border-primary/40 border-slate-700/80 bg-slate-950/40 hover:bg-slate-950/70'
|
: 'hover:border-primary/40 border-border/80 bg-background/40 hover:bg-background/70'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{file ? (
|
{file ? (
|
||||||
@ -450,25 +450,25 @@ export function ImportModal({
|
|||||||
<FileText className="text-primary size-5" />
|
<FileText className="text-primary size-5" />
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
className="max-w-full truncate px-2 text-sm font-medium text-slate-200"
|
className="max-w-full truncate px-2 text-sm font-medium text-popover-foreground"
|
||||||
title={file.name}
|
title={file.name}
|
||||||
>
|
>
|
||||||
{truncateFilename(file.name)}
|
{truncateFilename(file.name)}
|
||||||
</p>
|
</p>
|
||||||
<span className="rounded-full bg-slate-800 px-2.5 py-0.5 text-[11px] font-medium text-slate-400">
|
<span className="rounded-full bg-muted px-2.5 py-0.5 text-[11px] font-medium text-muted-foreground">
|
||||||
{parsedRows.length} row{parsedRows.length !== 1 ? 's' : ''}{' '}
|
{parsedRows.length} row{parsedRows.length !== 1 ? 's' : ''}{' '}
|
||||||
ready
|
ready
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex size-10 items-center justify-center rounded-lg bg-slate-800/80 ring-1 ring-slate-700/80 transition-colors group-hover:bg-slate-800">
|
<div className="flex size-10 items-center justify-center rounded-lg bg-muted/80 ring-1 ring-border/80 transition-colors group-hover:bg-muted">
|
||||||
<Upload className="size-5 text-slate-500 group-hover:text-slate-400" />
|
<Upload className="size-5 text-muted-foreground group-hover:text-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm text-muted-foreground">
|
||||||
Click to choose a CSV file
|
Click to choose a CSV file
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[11px] text-slate-600">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
.csv up to your browser limit
|
.csv up to your browser limit
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
@ -488,12 +488,12 @@ export function ImportModal({
|
|||||||
{preview.length > 0 && !result && (
|
{preview.length > 0 && !result && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<p className="text-[11px] font-semibold tracking-[0.14em] text-slate-500 uppercase">
|
<p className="text-[11px] font-semibold tracking-[0.14em] text-muted-foreground uppercase">
|
||||||
Preview · first {preview.length}
|
Preview · first {preview.length}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
{tagStats.rowsWithTags > 0 && (
|
{tagStats.rowsWithTags > 0 && (
|
||||||
<span className="inline-flex items-center gap-1 rounded-md bg-slate-800/90 px-2 py-0.5 text-[11px] text-slate-400">
|
<span className="inline-flex items-center gap-1 rounded-md bg-muted/90 px-2 py-0.5 text-[11px] text-muted-foreground">
|
||||||
<Tag className="text-primary/80 size-3" />
|
<Tag className="text-primary/80 size-3" />
|
||||||
{tagStats.unique} tag{tagStats.unique !== 1 ? 's' : ''} ·{' '}
|
{tagStats.unique} tag{tagStats.unique !== 1 ? 's' : ''} ·{' '}
|
||||||
{tagStats.rowsWithTags} contact
|
{tagStats.rowsWithTags} contact
|
||||||
@ -503,59 +503,59 @@ export function ImportModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-hidden rounded-xl border border-slate-800 ring-1 ring-slate-800/50">
|
<div className="overflow-hidden rounded-xl border border-border ring-1 ring-border/50">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full min-w-[32rem] text-xs">
|
<table className="w-full min-w-[32rem] text-xs">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-slate-800 bg-slate-950/60">
|
<tr className="border-b border-border bg-background/60">
|
||||||
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-slate-500">
|
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-muted-foreground">
|
||||||
Phone
|
Phone
|
||||||
</th>
|
</th>
|
||||||
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-slate-500">
|
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-muted-foreground">
|
||||||
Name
|
Name
|
||||||
</th>
|
</th>
|
||||||
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-slate-500">
|
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-muted-foreground">
|
||||||
Email
|
Email
|
||||||
</th>
|
</th>
|
||||||
{previewHasCompany && (
|
{previewHasCompany && (
|
||||||
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-slate-500">
|
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-muted-foreground">
|
||||||
Company
|
Company
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
{previewHasTags && (
|
{previewHasTags && (
|
||||||
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-slate-500">
|
<th className="px-3 py-2 text-left font-medium whitespace-nowrap text-muted-foreground">
|
||||||
Tags
|
Tags
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-800/70">
|
<tbody className="divide-y divide-border/70">
|
||||||
{preview.map((row, i) => (
|
{preview.map((row, i) => (
|
||||||
<tr
|
<tr
|
||||||
key={i}
|
key={i}
|
||||||
className="bg-slate-900/40 transition-colors hover:bg-slate-800/30"
|
className="bg-popover/40 transition-colors hover:bg-muted/30"
|
||||||
>
|
>
|
||||||
<td className="px-3 py-2 whitespace-nowrap text-slate-300">
|
<td className="px-3 py-2 whitespace-nowrap text-muted-foreground">
|
||||||
<PreviewCell
|
<PreviewCell
|
||||||
value={row.phone}
|
value={row.phone}
|
||||||
mono
|
mono
|
||||||
maxWidth="max-w-[7.5rem]"
|
maxWidth="max-w-[7.5rem]"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-slate-200">
|
<td className="px-3 py-2 text-popover-foreground">
|
||||||
<PreviewCell
|
<PreviewCell
|
||||||
value={row.name || '—'}
|
value={row.name || '—'}
|
||||||
maxWidth="max-w-[8.5rem]"
|
maxWidth="max-w-[8.5rem]"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-slate-400">
|
<td className="px-3 py-2 text-muted-foreground">
|
||||||
<PreviewCell
|
<PreviewCell
|
||||||
value={row.email || '—'}
|
value={row.email || '—'}
|
||||||
maxWidth="max-w-[10rem]"
|
maxWidth="max-w-[10rem]"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{previewHasCompany && (
|
{previewHasCompany && (
|
||||||
<td className="px-3 py-2 text-slate-400">
|
<td className="px-3 py-2 text-muted-foreground">
|
||||||
<PreviewCell
|
<PreviewCell
|
||||||
value={row.company || '—'}
|
value={row.company || '—'}
|
||||||
maxWidth="max-w-[7rem]"
|
maxWidth="max-w-[7rem]"
|
||||||
@ -578,7 +578,7 @@ export function ImportModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{parsedRows.length > PREVIEW_LIMIT && (
|
{parsedRows.length > PREVIEW_LIMIT && (
|
||||||
<p className="text-center text-[11px] text-slate-600">
|
<p className="text-center text-[11px] text-muted-foreground">
|
||||||
+ {parsedRows.length - PREVIEW_LIMIT} more row
|
+ {parsedRows.length - PREVIEW_LIMIT} more row
|
||||||
{parsedRows.length - PREVIEW_LIMIT !== 1 ? 's' : ''} not shown
|
{parsedRows.length - PREVIEW_LIMIT !== 1 ? 's' : ''} not shown
|
||||||
</p>
|
</p>
|
||||||
@ -587,8 +587,8 @@ export function ImportModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{result && (
|
{result && (
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-950/50 p-4">
|
<div className="rounded-xl border border-border bg-background/50 p-4">
|
||||||
<p className="text-sm font-medium text-white">Import complete</p>
|
<p className="text-sm font-medium text-popover-foreground">Import complete</p>
|
||||||
<div className="mt-3 flex flex-wrap gap-3">
|
<div className="mt-3 flex flex-wrap gap-3">
|
||||||
{result.imported > 0 && (
|
{result.imported > 0 && (
|
||||||
<div className="text-primary flex items-center gap-1.5 text-sm">
|
<div className="text-primary flex items-center gap-1.5 text-sm">
|
||||||
@ -620,12 +620,12 @@ export function ImportModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="mt-0 shrink-0 gap-2 border-t border-slate-800/80 bg-slate-950/50 px-6 py-4 sm:justify-end">
|
<DialogFooter className="mt-0 shrink-0 gap-2 border-t border-border/80 bg-background/50 px-6 py-4 sm:justify-end">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleOpenChange(false)}
|
onClick={() => handleOpenChange(false)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
{result ? 'Close' : 'Cancel'}
|
{result ? 'Close' : 'Cancel'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -54,9 +54,9 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
|
|||||||
i === 0 || totalLoaded > PAGE_SIZES[i - 1]
|
i === 0 || totalLoaded > PAGE_SIZES[i - 1]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-xl border border-slate-800 bg-slate-900">
|
<section className="rounded-xl border border-border bg-card">
|
||||||
<header className="flex items-center justify-between border-b border-slate-800 px-5 py-4">
|
<header className="flex items-center justify-between border-b border-border px-5 py-4">
|
||||||
<h2 className="text-sm font-semibold text-white">Recent Activity</h2>
|
<h2 className="text-sm font-semibold text-foreground">Recent Activity</h2>
|
||||||
<Link
|
<Link
|
||||||
href="/inbox"
|
href="/inbox"
|
||||||
className="text-xs font-medium text-primary hover:text-primary/80"
|
className="text-xs font-medium text-primary hover:text-primary/80"
|
||||||
@ -81,13 +81,14 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ul className="divide-y divide-slate-800">
|
<ul className="divide-y divide-border">
|
||||||
{visible.map((it, i) => {
|
{visible.map((it, i) => {
|
||||||
const theme = KIND_THEME[it.kind]
|
const theme = KIND_THEME[it.kind]
|
||||||
const Icon = theme.icon
|
const Icon = theme.icon
|
||||||
// Alternating row background for scanability — dark-theme
|
// Alternating row background for scanability. bg-muted/40
|
||||||
// translation of the spec's white / #f9fafb stripes.
|
// keeps the stripe visible in both light and dark modes
|
||||||
const stripe = i % 2 === 0 ? 'bg-transparent' : 'bg-slate-900/40'
|
// (bg-card/40 vanishes against a white card surface in light).
|
||||||
|
const stripe = i % 2 === 0 ? 'bg-transparent' : 'bg-muted/40'
|
||||||
const row = (
|
const row = (
|
||||||
<div className="flex items-center gap-3 px-5 py-2.5">
|
<div className="flex items-center gap-3 px-5 py-2.5">
|
||||||
<span
|
<span
|
||||||
@ -98,16 +99,16 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
|
|||||||
>
|
>
|
||||||
<Icon className="h-3.5 w-3.5" />
|
<Icon className="h-3.5 w-3.5" />
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0 flex-1 truncate text-sm text-slate-200">
|
<span className="min-w-0 flex-1 truncate text-sm text-foreground">
|
||||||
{it.text}
|
{it.text}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex-shrink-0 text-xs text-slate-500 tabular-nums">
|
<span className="flex-shrink-0 text-xs text-muted-foreground tabular-nums">
|
||||||
{relativeTime(it.at)}
|
{relativeTime(it.at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<li key={it.id} className={cn(stripe, 'transition-colors hover:bg-slate-800/40')}>
|
<li key={it.id} className={cn(stripe, 'transition-colors hover:bg-muted/40')}>
|
||||||
{it.href ? (
|
{it.href ? (
|
||||||
<Link href={it.href} className="block">
|
<Link href={it.href} className="block">
|
||||||
{row}
|
{row}
|
||||||
@ -119,13 +120,13 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
<footer className="flex items-center justify-between border-t border-slate-800 px-5 py-3 text-xs">
|
<footer className="flex items-center justify-between border-t border-border px-5 py-3 text-xs">
|
||||||
<span className="text-slate-500 tabular-nums">
|
<span className="text-muted-foreground tabular-nums">
|
||||||
Showing {visible.length} of {totalLoaded}
|
Showing {visible.length} of {totalLoaded}
|
||||||
{totalLoaded === 50 ? '+' : ''}
|
{totalLoaded === 50 ? '+' : ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="mr-1 text-slate-500">Show</span>
|
<span className="mr-1 text-muted-foreground">Show</span>
|
||||||
{PAGE_SIZES.map((size, i) => {
|
{PAGE_SIZES.map((size, i) => {
|
||||||
const disabled = !isSizeUseful(size, i)
|
const disabled = !isSizeUseful(size, i)
|
||||||
return (
|
return (
|
||||||
@ -137,9 +138,9 @@ export function ActivityFeed({ items, loading }: ActivityFeedProps) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md px-2 py-1 font-medium tabular-nums transition-colors',
|
'rounded-md px-2 py-1 font-medium tabular-nums transition-colors',
|
||||||
pageSize === size
|
pageSize === size
|
||||||
? 'bg-slate-700 text-white'
|
? 'bg-secondary text-secondary-foreground'
|
||||||
: 'text-slate-400 hover:bg-slate-800 hover:text-white',
|
: 'text-muted-foreground hover:bg-muted hover:text-foreground',
|
||||||
disabled && 'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-slate-400',
|
disabled && 'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-muted-foreground',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{size}
|
{size}
|
||||||
|
|||||||
@ -46,13 +46,13 @@ export function ConversationsChart({ series, loading, range, onRangeChange }: Co
|
|||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex h-full flex-col rounded-xl border border-slate-800 bg-slate-900">
|
<section className="flex h-full flex-col rounded-xl border border-border bg-card">
|
||||||
<header className="flex items-center justify-between border-b border-slate-800 px-5 py-4">
|
<header className="flex items-center justify-between border-b border-border px-5 py-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-sm font-semibold text-white">Conversations Over Time</h2>
|
<h2 className="text-sm font-semibold text-foreground">Conversations Over Time</h2>
|
||||||
<p className="mt-0.5 text-xs text-slate-500">Daily message volume by direction</p>
|
<p className="mt-0.5 text-xs text-muted-foreground">Daily message volume by direction</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 rounded-lg bg-slate-800/60 p-1">
|
<div className="flex items-center gap-1 rounded-lg bg-muted/60 p-1">
|
||||||
{[7, 30, 90].map((r) => (
|
{[7, 30, 90].map((r) => (
|
||||||
<button
|
<button
|
||||||
key={r}
|
key={r}
|
||||||
@ -61,8 +61,8 @@ export function ConversationsChart({ series, loading, range, onRangeChange }: Co
|
|||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
|
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
|
||||||
range === r
|
range === r
|
||||||
? 'bg-slate-700 text-white'
|
? 'bg-secondary text-secondary-foreground'
|
||||||
: 'text-slate-400 hover:text-white',
|
: 'text-muted-foreground hover:text-foreground',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{r} days
|
{r} days
|
||||||
@ -85,7 +85,7 @@ export function ConversationsChart({ series, loading, range, onRangeChange }: Co
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className="flex items-center gap-4 border-t border-slate-800 px-5 py-3 text-xs text-slate-400">
|
<footer className="flex items-center gap-4 border-t border-border px-5 py-3 text-xs text-muted-foreground">
|
||||||
<LegendDot color="#3b82f6" label="Incoming" />
|
<LegendDot color="#3b82f6" label="Incoming" />
|
||||||
<LegendDot color="#7c3aed" label="Outgoing" />
|
<LegendDot color="#7c3aed" label="Outgoing" />
|
||||||
</footer>
|
</footer>
|
||||||
@ -207,7 +207,7 @@ function LineSvg({
|
|||||||
x2={VB_W - PADDING.right}
|
x2={VB_W - PADDING.right}
|
||||||
y1={y}
|
y1={y}
|
||||||
y2={y}
|
y2={y}
|
||||||
stroke="rgb(30 41 59)"
|
stroke="var(--border)"
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
@ -215,7 +215,7 @@ function LineSvg({
|
|||||||
y={y}
|
y={y}
|
||||||
textAnchor="end"
|
textAnchor="end"
|
||||||
dominantBaseline="middle"
|
dominantBaseline="middle"
|
||||||
className="fill-slate-500 text-[10px]"
|
className="fill-muted-foreground text-[10px]"
|
||||||
>
|
>
|
||||||
{t}
|
{t}
|
||||||
</text>
|
</text>
|
||||||
@ -231,7 +231,7 @@ function LineSvg({
|
|||||||
x={xFor(i)}
|
x={xFor(i)}
|
||||||
y={VB_H - 8}
|
y={VB_H - 8}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
className="fill-slate-500 text-[10px]"
|
className="fill-muted-foreground text-[10px]"
|
||||||
>
|
>
|
||||||
{shortDayLabel(p.day)}
|
{shortDayLabel(p.day)}
|
||||||
</text>
|
</text>
|
||||||
@ -265,7 +265,7 @@ function LineSvg({
|
|||||||
x2={hoverX}
|
x2={hoverX}
|
||||||
y1={PADDING.top}
|
y1={PADDING.top}
|
||||||
y2={PADDING.top + chartH}
|
y2={PADDING.top + chartH}
|
||||||
stroke="rgb(71 85 105)"
|
stroke="var(--muted-foreground)"
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
/>
|
/>
|
||||||
<circle cx={hoverX} cy={yFor(data[hover.idx].incoming)} r={3.5} fill="#3b82f6" />
|
<circle cx={hoverX} cy={yFor(data[hover.idx].incoming)} r={3.5} fill="#3b82f6" />
|
||||||
@ -280,10 +280,10 @@ function LineSvg({
|
|||||||
letterboxed viewBox percentage. */}
|
letterboxed viewBox percentage. */}
|
||||||
{hovered && hover !== null && (
|
{hovered && hover !== null && (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none absolute top-0 z-10 -translate-x-1/2 rounded-md border border-slate-700 bg-slate-950 px-2.5 py-1.5 text-[11px] shadow-lg"
|
className="pointer-events-none absolute top-0 z-10 -translate-x-1/2 rounded-md border border-border bg-popover px-2.5 py-1.5 text-[11px] shadow-lg"
|
||||||
style={{ left: `${hover.tooltipLeftPx}px` }}
|
style={{ left: `${hover.tooltipLeftPx}px` }}
|
||||||
>
|
>
|
||||||
<div className="font-medium text-white">{longDayLabel(hovered.day)}</div>
|
<div className="font-medium text-popover-foreground">{longDayLabel(hovered.day)}</div>
|
||||||
<div className="mt-1 flex flex-col gap-0.5">
|
<div className="mt-1 flex flex-col gap-0.5">
|
||||||
<span className="flex items-center gap-1.5 text-blue-300">
|
<span className="flex items-center gap-1.5 text-blue-300">
|
||||||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-blue-500" />
|
<span className="inline-block h-1.5 w-1.5 rounded-full bg-blue-500" />
|
||||||
|
|||||||
@ -22,15 +22,15 @@ export function EmptyState({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-full min-h-40 flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-slate-800 bg-slate-900/40 px-4 py-6 text-center',
|
'flex h-full min-h-40 flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border bg-card/40 px-4 py-6 text-center',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-slate-800 text-slate-500">
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-medium text-slate-300">{title}</p>
|
<p className="text-sm font-medium text-muted-foreground">{title}</p>
|
||||||
{hint && <p className="max-w-xs text-xs text-slate-500">{hint}</p>}
|
{hint && <p className="max-w-xs text-xs text-muted-foreground">{hint}</p>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,18 +23,18 @@ interface MetricCardProps {
|
|||||||
|
|
||||||
export function MetricCard({ title, value, icon: Icon, delta, subtitle }: MetricCardProps) {
|
export function MetricCard({ title, value, icon: Icon, delta, subtitle }: MetricCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-5">
|
<div className="rounded-xl border border-border bg-card p-5">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<p className="text-sm font-medium text-slate-400">{title}</p>
|
<p className="text-sm font-medium text-muted-foreground">{title}</p>
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-800 text-slate-500">
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-3 text-[28px] leading-none font-bold tabular-nums text-white">
|
<p className="mt-3 text-[28px] leading-none font-bold tabular-nums text-foreground">
|
||||||
{value}
|
{value}
|
||||||
</p>
|
</p>
|
||||||
{delta ? <DeltaRow sign={delta.sign} label={delta.label} /> : subtitle ? (
|
{delta ? <DeltaRow sign={delta.sign} label={delta.label} /> : subtitle ? (
|
||||||
<p className="mt-2 text-sm text-slate-500">{subtitle}</p>
|
<p className="mt-2 text-sm text-muted-foreground">{subtitle}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -46,7 +46,7 @@ function DeltaRow({ sign, label }: { sign: number; label: string }) {
|
|||||||
? 'text-primary'
|
? 'text-primary'
|
||||||
: sign < 0
|
: sign < 0
|
||||||
? 'text-red-400'
|
? 'text-red-400'
|
||||||
: 'text-slate-500'
|
: 'text-muted-foreground'
|
||||||
const Arrow = sign > 0 ? ArrowUp : sign < 0 ? ArrowDown : Minus
|
const Arrow = sign > 0 ? ArrowUp : sign < 0 ? ArrowDown : Minus
|
||||||
return (
|
return (
|
||||||
<div className={cn('mt-2 flex items-center gap-1 text-sm', tone)}>
|
<div className={cn('mt-2 flex items-center gap-1 text-sm', tone)}>
|
||||||
|
|||||||
@ -15,10 +15,10 @@ interface PipelineDonutProps {
|
|||||||
|
|
||||||
export function PipelineDonut({ data, loading, currency }: PipelineDonutProps) {
|
export function PipelineDonut({ data, loading, currency }: PipelineDonutProps) {
|
||||||
return (
|
return (
|
||||||
<section className="flex h-full flex-col rounded-xl border border-slate-800 bg-slate-900">
|
<section className="flex h-full flex-col rounded-xl border border-border bg-card">
|
||||||
<header className="border-b border-slate-800 px-5 py-4">
|
<header className="border-b border-border px-5 py-4">
|
||||||
<h2 className="text-sm font-semibold text-white">Pipeline Value</h2>
|
<h2 className="text-sm font-semibold text-foreground">Pipeline Value</h2>
|
||||||
<p className="mt-0.5 text-xs text-slate-500">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
Open deals by stage
|
Open deals by stage
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
@ -43,11 +43,11 @@ export function PipelineDonut({ data, loading, currency }: PipelineDonutProps) {
|
|||||||
style={{ background: s.color }}
|
style={{ background: s.color }}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<span className="flex-1 truncate text-slate-300">{s.name}</span>
|
<span className="flex-1 truncate text-muted-foreground">{s.name}</span>
|
||||||
<span className="text-slate-500 tabular-nums">
|
<span className="text-muted-foreground tabular-nums">
|
||||||
{s.dealCount} deal{s.dealCount === 1 ? '' : 's'}
|
{s.dealCount} deal{s.dealCount === 1 ? '' : 's'}
|
||||||
</span>
|
</span>
|
||||||
<span className="w-20 text-right text-slate-300 tabular-nums">
|
<span className="w-20 text-right text-muted-foreground tabular-nums">
|
||||||
{formatCurrencyShort(s.totalValue, currency)}
|
{formatCurrencyShort(s.totalValue, currency)}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
@ -98,7 +98,7 @@ function Donut({ data, currency }: { data: PipelineDonutData; currency: string }
|
|||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<svg viewBox={`0 0 ${size} ${size}`} className="h-48 w-48" role="img" aria-label="Pipeline value by stage">
|
<svg viewBox={`0 0 ${size} ${size}`} className="h-48 w-48" role="img" aria-label="Pipeline value by stage">
|
||||||
{/* background ring */}
|
{/* background ring */}
|
||||||
<circle cx={cx} cy={cy} r={r} fill="none" stroke="rgb(30 41 59)" strokeWidth={ringWidth} />
|
<circle cx={cx} cy={cy} r={r} fill="none" stroke="var(--muted)" strokeWidth={ringWidth} />
|
||||||
{segments.map((seg) => (
|
{segments.map((seg) => (
|
||||||
<path
|
<path
|
||||||
key={seg.id}
|
key={seg.id}
|
||||||
@ -114,7 +114,7 @@ function Donut({ data, currency }: { data: PipelineDonutData; currency: string }
|
|||||||
x={cx}
|
x={cx}
|
||||||
y={cy - 6}
|
y={cy - 6}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
className="fill-slate-500 text-[11px]"
|
className="fill-muted-foreground text-[11px]"
|
||||||
>
|
>
|
||||||
Total
|
Total
|
||||||
</text>
|
</text>
|
||||||
@ -122,7 +122,7 @@ function Donut({ data, currency }: { data: PipelineDonutData; currency: string }
|
|||||||
x={cx}
|
x={cx}
|
||||||
y={cy + 14}
|
y={cy + 14}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
className="fill-white text-[18px] font-semibold tabular-nums"
|
className="fill-foreground text-[18px] font-semibold tabular-nums"
|
||||||
>
|
>
|
||||||
{formatCurrencyShort(data.totalValue, currency)}
|
{formatCurrencyShort(data.totalValue, currency)}
|
||||||
</text>
|
</text>
|
||||||
|
|||||||
@ -31,12 +31,12 @@ export function QuickActions() {
|
|||||||
<Link
|
<Link
|
||||||
key={a.href}
|
key={a.href}
|
||||||
href={a.href}
|
href={a.href}
|
||||||
className="group flex items-center gap-3 rounded-xl border border-slate-800 bg-slate-900 px-4 py-3 transition-colors hover:border-slate-700 hover:bg-slate-800/60"
|
className="group flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 transition-colors hover:border-border hover:bg-muted/60"
|
||||||
>
|
>
|
||||||
<div className={`flex h-9 w-9 items-center justify-center rounded-lg bg-slate-800 ${a.tint}`}>
|
<div className={`flex h-9 w-9 items-center justify-center rounded-lg bg-muted ${a.tint}`}>
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-white">{a.label}</span>
|
<span className="text-sm font-medium text-foreground">{a.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -44,13 +44,13 @@ export function ResponseTimeChart({
|
|||||||
})) ?? []
|
})) ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-xl border border-slate-800 bg-slate-900">
|
<section className="rounded-xl border border-border bg-card">
|
||||||
<header className="flex items-center justify-between gap-3 border-b border-slate-800 px-5 py-4">
|
<header className="flex items-center justify-between gap-3 border-b border-border px-5 py-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-sm font-semibold text-white">
|
<h2 className="text-sm font-semibold text-foreground">
|
||||||
Average First Response Time
|
Average First Response Time
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-0.5 text-xs text-slate-500">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
Minutes to reply to a customer's first unreplied message, by
|
Minutes to reply to a customer's first unreplied message, by
|
||||||
weekday
|
weekday
|
||||||
</p>
|
</p>
|
||||||
@ -63,13 +63,13 @@ export function ResponseTimeChart({
|
|||||||
)}
|
)}
|
||||||
{data && (data.thisWeekAvg != null || data.lastWeekAvg != null) && (
|
{data && (data.thisWeekAvg != null || data.lastWeekAvg != null) && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-slate-400">
|
<div className="text-muted-foreground">
|
||||||
This week:{' '}
|
This week:{' '}
|
||||||
<span className="font-medium text-white tabular-nums">
|
<span className="font-medium text-foreground tabular-nums">
|
||||||
{fmt(data.thisWeekAvg)}
|
{fmt(data.thisWeekAvg)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-slate-500">
|
<div className="text-muted-foreground">
|
||||||
Last week:{' '}
|
Last week:{' '}
|
||||||
<span className="tabular-nums">{fmt(data.lastWeekAvg)}</span>
|
<span className="tabular-nums">{fmt(data.lastWeekAvg)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,14 +6,14 @@ import { cn } from '@/lib/utils'
|
|||||||
* its data fetches.
|
* its data fetches.
|
||||||
*/
|
*/
|
||||||
export function Skeleton({ className }: { className?: string }) {
|
export function Skeleton({ className }: { className?: string }) {
|
||||||
return <div className={cn('animate-pulse rounded-md bg-slate-800', className)} />
|
return <div className={cn('animate-pulse rounded-md bg-muted', className)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SkeletonCard({ className }: { className?: string }) {
|
export function SkeletonCard({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-xl border border-slate-800 bg-slate-900 p-5',
|
'rounded-xl border border-border bg-card p-5',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -161,14 +161,14 @@ export function FlowBuilder() {
|
|||||||
|
|
||||||
<section className="flex flex-col gap-3">
|
<section className="flex flex-col gap-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-sm font-semibold text-white">
|
<h2 className="text-sm font-semibold text-foreground">
|
||||||
Nodes ({state.nodes.length})
|
Nodes ({state.nodes.length})
|
||||||
</h2>
|
</h2>
|
||||||
<AddNodeButton onAdd={addNode} />
|
<AddNodeButton onAdd={addNode} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{state.nodes.length === 0 ? (
|
{state.nodes.length === 0 ? (
|
||||||
<div className="rounded-lg border border-dashed border-slate-700 bg-slate-900/50 p-8 text-center text-sm text-slate-400">
|
<div className="rounded-lg border border-dashed border-border bg-card/50 p-8 text-center text-sm text-muted-foreground">
|
||||||
Add a <strong>Start</strong> node, then a <strong>Send buttons</strong>
|
Add a <strong>Start</strong> node, then a <strong>Send buttons</strong>
|
||||||
{" "}node, then a <strong>Handoff</strong> — that's the welcome-menu
|
{" "}node, then a <strong>Handoff</strong> — that's the welcome-menu
|
||||||
shape from the brief.
|
shape from the brief.
|
||||||
@ -246,7 +246,7 @@ function KeywordsInput({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="support, help, hi"
|
placeholder="support, help, hi"
|
||||||
className="bg-slate-800"
|
className="bg-muted"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -265,11 +265,11 @@ function TriggerPanel({
|
|||||||
triggerIssues: ValidationIssue[];
|
triggerIssues: ValidationIssue[];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="rounded-lg border border-slate-800 bg-slate-900 p-4">
|
<section className="rounded-lg border border-border bg-card p-4">
|
||||||
<h2 className="mb-3 text-sm font-semibold text-white">Trigger</h2>
|
<h2 className="mb-3 text-sm font-semibold text-foreground">Trigger</h2>
|
||||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">When…</label>
|
<label className="mb-1 block text-xs text-muted-foreground">When…</label>
|
||||||
<Select
|
<Select
|
||||||
value={state.trigger_type}
|
value={state.trigger_type}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
@ -281,7 +281,7 @@ function TriggerPanel({
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-slate-800">
|
<SelectTrigger className="bg-muted">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -299,7 +299,7 @@ function TriggerPanel({
|
|||||||
</div>
|
</div>
|
||||||
{state.trigger_type === "keyword" && (
|
{state.trigger_type === "keyword" && (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">
|
<label className="mb-1 block text-xs text-muted-foreground">
|
||||||
Keywords (comma-separated)
|
Keywords (comma-separated)
|
||||||
</label>
|
</label>
|
||||||
<KeywordsInput
|
<KeywordsInput
|
||||||
@ -342,9 +342,9 @@ function EntryPicker({
|
|||||||
}) {
|
}) {
|
||||||
if (state.nodes.length === 0) return null;
|
if (state.nodes.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<section className="flex items-center gap-3 rounded-lg border border-slate-800 bg-slate-900 p-3">
|
<section className="flex items-center gap-3 rounded-lg border border-border bg-card p-3">
|
||||||
<CornerDownRight className="h-4 w-4 shrink-0 text-primary" />
|
<CornerDownRight className="h-4 w-4 shrink-0 text-primary" />
|
||||||
<span className="text-xs text-slate-400">Entry node:</span>
|
<span className="text-xs text-muted-foreground">Entry node:</span>
|
||||||
<NodeKeySelect
|
<NodeKeySelect
|
||||||
value={state.entry_node_id}
|
value={state.entry_node_id}
|
||||||
nodes={state.nodes}
|
nodes={state.nodes}
|
||||||
@ -396,14 +396,14 @@ function NodeCard({
|
|||||||
<div
|
<div
|
||||||
ref={cardRef}
|
ref={cardRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border bg-slate-900 transition-shadow duration-500",
|
"rounded-lg border bg-card transition-shadow duration-500",
|
||||||
hasError
|
hasError
|
||||||
? "border-red-500/40"
|
? "border-red-500/40"
|
||||||
: isEntry
|
: isEntry
|
||||||
? "border-primary/50"
|
? "border-primary/50"
|
||||||
: "border-slate-800",
|
: "border-border",
|
||||||
isFlashed &&
|
isFlashed &&
|
||||||
"ring-2 ring-primary ring-offset-2 ring-offset-slate-950",
|
"ring-2 ring-primary ring-offset-2 ring-offset-background",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -414,10 +414,10 @@ function NodeCard({
|
|||||||
<meta.icon className={cn("h-4 w-4 shrink-0", meta.color)} />
|
<meta.icon className={cn("h-4 w-4 shrink-0", meta.color)} />
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="truncate text-sm font-medium text-white">
|
<span className="truncate text-sm font-medium text-foreground">
|
||||||
{meta.label}
|
{meta.label}
|
||||||
</span>
|
</span>
|
||||||
<code className="rounded bg-slate-800 px-1.5 py-0.5 text-[10px] text-slate-400">
|
<code className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||||
{node.node_key}
|
{node.node_key}
|
||||||
</code>
|
</code>
|
||||||
{isEntry && (
|
{isEntry && (
|
||||||
@ -430,7 +430,7 @@ function NodeCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!expanded && preview && (
|
{!expanded && preview && (
|
||||||
<p className="mt-0.5 truncate text-xs text-slate-500">
|
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||||
{preview}
|
{preview}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -439,20 +439,20 @@ function NodeCard({
|
|||||||
<CircleAlert className="h-3.5 w-3.5 shrink-0 text-red-400" />
|
<CircleAlert className="h-3.5 w-3.5 shrink-0 text-red-400" />
|
||||||
)}
|
)}
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<ChevronUp className="h-4 w-4 text-slate-500" />
|
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronDown className="h-4 w-4 text-slate-500" />
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="border-t border-slate-800 px-4 py-4">
|
<div className="border-t border-border px-4 py-4">
|
||||||
<NodeConfigWithAdvanced
|
<NodeConfigWithAdvanced
|
||||||
node={node}
|
node={node}
|
||||||
allNodes={allNodes}
|
allNodes={allNodes}
|
||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
onUpdateConfig={onUpdateConfig}
|
onUpdateConfig={onUpdateConfig}
|
||||||
/>
|
/>
|
||||||
<div className="mt-4 flex items-center justify-between border-t border-slate-800 pt-3">
|
<div className="mt-4 flex items-center justify-between border-t border-border pt-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!isEntry && (
|
{!isEntry && (
|
||||||
<Button variant="ghost" size="sm" onClick={onSetEntry}>
|
<Button variant="ghost" size="sm" onClick={onSetEntry}>
|
||||||
@ -511,11 +511,11 @@ function NodeConfigWithAdvanced({
|
|||||||
showAdvanced={showAdvanced}
|
showAdvanced={showAdvanced}
|
||||||
onUpdateConfig={onUpdateConfig}
|
onUpdateConfig={onUpdateConfig}
|
||||||
/>
|
/>
|
||||||
<div className="border-t border-slate-800 pt-3">
|
<div className="border-t border-border pt-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowAdvanced((v) => !v)}
|
onClick={() => setShowAdvanced((v) => !v)}
|
||||||
className="inline-flex items-center gap-1 text-xs text-slate-500 hover:text-slate-300"
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
{showAdvanced ? (
|
{showAdvanced ? (
|
||||||
<ChevronUp className="h-3 w-3" />
|
<ChevronUp className="h-3 w-3" />
|
||||||
@ -527,7 +527,7 @@ function NodeConfigWithAdvanced({
|
|||||||
{showAdvanced && (
|
{showAdvanced && (
|
||||||
<div className="mt-3 flex flex-col gap-3">
|
<div className="mt-3 flex flex-col gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">
|
<label className="mb-1 block text-xs text-muted-foreground">
|
||||||
Node key (internal identifier — keep stable for analytics)
|
Node key (internal identifier — keep stable for analytics)
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
@ -535,11 +535,11 @@ function NodeConfigWithAdvanced({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onUpdate({ node_key: slugify(e.target.value, node.node_key) })
|
onUpdate({ node_key: slugify(e.target.value, node.node_key) })
|
||||||
}
|
}
|
||||||
className="bg-slate-800 font-mono text-xs"
|
className="bg-muted font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{hasReplyIds && (
|
{hasReplyIds && (
|
||||||
<p className="text-[10px] text-slate-500">
|
<p className="text-[10px] text-muted-foreground">
|
||||||
Reply IDs for each option are shown inline above. They're
|
Reply IDs for each option are shown inline above. They're
|
||||||
returned by WhatsApp when a customer taps; you usually don't
|
returned by WhatsApp when a customer taps; you usually don't
|
||||||
need to touch them.
|
need to touch them.
|
||||||
@ -573,13 +573,13 @@ function AddNodeButton({ onAdd }: { onAdd: (type: NodeType) => void }) {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
className="inline-flex items-center gap-1.5 rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-xs font-medium text-slate-200 transition-colors hover:bg-slate-800"
|
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-muted"
|
||||||
aria-label="Add node"
|
aria-label="Add node"
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5" />
|
<Plus className="h-3.5 w-3.5" />
|
||||||
Add node
|
Add node
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="border-slate-700 bg-slate-900">
|
<DropdownMenuContent align="end" className="border-border bg-popover">
|
||||||
{types.map((t) => {
|
{types.map((t) => {
|
||||||
const meta = NODE_META[t];
|
const meta = NODE_META[t];
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -128,10 +128,10 @@ function FlowNodeCard({ data, selected }: NodeProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative min-w-[220px] max-w-[260px] rounded-lg border bg-slate-900/95 px-3 py-2 text-left shadow-lg backdrop-blur transition-colors",
|
"relative min-w-[220px] max-w-[260px] rounded-lg border bg-card/95 px-3 py-2 text-left shadow-lg backdrop-blur transition-colors",
|
||||||
selected
|
selected
|
||||||
? "border-primary ring-1 ring-primary/40"
|
? "border-primary ring-1 ring-primary/40"
|
||||||
: "border-slate-700 hover:border-slate-600",
|
: "border-border hover:border-border",
|
||||||
// Flash overrides hover/selected colors briefly. Tailwind's
|
// Flash overrides hover/selected colors briefly. Tailwind's
|
||||||
// built-in `animate-pulse` is too gentle; a ring with the
|
// built-in `animate-pulse` is too gentle; a ring with the
|
||||||
// amber accent matches the list view's flash semantics.
|
// amber accent matches the list view's flash semantics.
|
||||||
@ -142,13 +142,13 @@ function FlowNodeCard({ data, selected }: NodeProps) {
|
|||||||
<Handle
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className="!h-2.5 !w-2.5 !border-slate-600 !bg-slate-700"
|
className="!h-2.5 !w-2.5 !border-border !bg-muted"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Icon className={cn("h-3.5 w-3.5 shrink-0", meta.color)} />
|
<Icon className={cn("h-3.5 w-3.5 shrink-0", meta.color)} />
|
||||||
<span className="truncate text-[11px] font-medium uppercase tracking-wide text-slate-400">
|
<span className="truncate text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
{meta.label}
|
{meta.label}
|
||||||
</span>
|
</span>
|
||||||
{isEntry && (
|
{isEntry && (
|
||||||
@ -157,21 +157,21 @@ function FlowNodeCard({ data, selected }: NodeProps) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 truncate font-mono text-[11px] text-slate-300">
|
<div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
|
||||||
{node.node_key}
|
{node.node_key}
|
||||||
</div>
|
</div>
|
||||||
{summary && (
|
{summary && (
|
||||||
<div className="mt-1 line-clamp-2 text-xs text-slate-400">
|
<div className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||||
{summary}
|
{summary}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isMultiSlot && (
|
{isMultiSlot && (
|
||||||
<div className="mt-2 flex flex-col gap-1 border-t border-slate-800 pt-2">
|
<div className="mt-2 flex flex-col gap-1 border-t border-border pt-2">
|
||||||
{slots.map((slot) => (
|
{slots.map((slot) => (
|
||||||
<div
|
<div
|
||||||
key={slot.id}
|
key={slot.id}
|
||||||
className="relative flex items-center justify-between gap-2 rounded px-1 py-0.5 text-[11px] text-slate-300"
|
className="relative flex items-center justify-between gap-2 rounded px-1 py-0.5 text-[11px] text-muted-foreground"
|
||||||
>
|
>
|
||||||
<span className="truncate" title={slot.label}>
|
<span className="truncate" title={slot.label}>
|
||||||
{slot.label}
|
{slot.label}
|
||||||
@ -184,7 +184,7 @@ function FlowNodeCard({ data, selected }: NodeProps) {
|
|||||||
// sits flush with the right edge of the card instead
|
// sits flush with the right edge of the card instead
|
||||||
// of floating at vertical center. The negative offset
|
// of floating at vertical center. The negative offset
|
||||||
// matches the card's px-3 + the handle's own radius.
|
// matches the card's px-3 + the handle's own radius.
|
||||||
className="!relative !right-auto !top-auto !h-2.5 !w-2.5 !translate-x-[12px] !transform-none !border-slate-600 !bg-slate-700"
|
className="!relative !right-auto !top-auto !h-2.5 !w-2.5 !translate-x-[12px] !transform-none !border-border !bg-muted"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -196,7 +196,7 @@ function FlowNodeCard({ data, selected }: NodeProps) {
|
|||||||
type="source"
|
type="source"
|
||||||
id={slots[0].id}
|
id={slots[0].id}
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className="!h-2.5 !w-2.5 !border-slate-600 !bg-slate-700"
|
className="!h-2.5 !w-2.5 !border-border !bg-muted"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -319,11 +319,12 @@ function FlowCanvasInner() {
|
|||||||
target: e.target,
|
target: e.target,
|
||||||
sourceHandle: e.sourceHandle,
|
sourceHandle: e.sourceHandle,
|
||||||
label: e.label,
|
label: e.label,
|
||||||
labelStyle: { fill: "#cbd5e1", fontSize: 11 },
|
// Mode-aware via CSS tokens so edge chrome flips with light/dark.
|
||||||
labelBgStyle: { fill: "#0f172a" },
|
labelStyle: { fill: "var(--muted-foreground)", fontSize: 11 },
|
||||||
|
labelBgStyle: { fill: "var(--card)" },
|
||||||
labelBgPadding: [4, 2] as [number, number],
|
labelBgPadding: [4, 2] as [number, number],
|
||||||
labelBgBorderRadius: 4,
|
labelBgBorderRadius: 4,
|
||||||
style: { stroke: "#475569", strokeWidth: 1.5 },
|
style: { stroke: "var(--border)", strokeWidth: 1.5 },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return rfEdges;
|
return rfEdges;
|
||||||
@ -456,7 +457,7 @@ function FlowCanvasInner() {
|
|||||||
|
|
||||||
if (rfNodes.length === 0) {
|
if (rfNodes.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[60vh] flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-slate-700 bg-slate-950 text-sm text-slate-500">
|
<div className="flex h-[60vh] flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-background text-sm text-muted-foreground">
|
||||||
<p>No nodes yet.</p>
|
<p>No nodes yet.</p>
|
||||||
<CanvasAddNodeButton />
|
<CanvasAddNodeButton />
|
||||||
</div>
|
</div>
|
||||||
@ -465,7 +466,7 @@ function FlowCanvasInner() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-[70vh] w-full overflow-hidden rounded-lg border border-slate-800 bg-slate-950">
|
<div className="h-[70vh] w-full overflow-hidden rounded-lg border border-border bg-background">
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={rfNodes}
|
nodes={rfNodes}
|
||||||
edges={rfEdges}
|
edges={rfEdges}
|
||||||
@ -491,17 +492,17 @@ function FlowCanvasInner() {
|
|||||||
minZoom={0.2}
|
minZoom={0.2}
|
||||||
maxZoom={1.5}
|
maxZoom={1.5}
|
||||||
>
|
>
|
||||||
<Background gap={24} size={1} color="#1e293b" />
|
<Background gap={24} size={1} color="var(--border)" />
|
||||||
<Controls
|
<Controls
|
||||||
className="!border-slate-700 !bg-slate-900 [&_button]:!border-slate-700 [&_button]:!bg-slate-900 [&_button:hover]:!bg-slate-800"
|
className="!border-border !bg-card [&_button]:!border-border [&_button]:!bg-card [&_button:hover]:!bg-muted [&_button_svg]:!fill-foreground"
|
||||||
showInteractive={false}
|
showInteractive={false}
|
||||||
/>
|
/>
|
||||||
<MiniMap
|
<MiniMap
|
||||||
pannable
|
pannable
|
||||||
zoomable
|
zoomable
|
||||||
nodeColor="#334155"
|
nodeColor="var(--muted-foreground)"
|
||||||
maskColor="rgba(15, 23, 42, 0.7)"
|
maskColor="color-mix(in oklch, var(--background) 70%, transparent)"
|
||||||
className="!border !border-slate-700 !bg-slate-900"
|
className="!border !border-border !bg-card"
|
||||||
/>
|
/>
|
||||||
<Panel position="bottom-right" className="!bottom-4 !right-4">
|
<Panel position="bottom-right" className="!bottom-4 !right-4">
|
||||||
<CanvasAddNodeButton />
|
<CanvasAddNodeButton />
|
||||||
@ -561,10 +562,10 @@ function NodeEditSheet({
|
|||||||
<Sheet open={open} onOpenChange={(v) => !v && onClose()}>
|
<Sheet open={open} onOpenChange={(v) => !v && onClose()}>
|
||||||
<SheetContent
|
<SheetContent
|
||||||
side="right"
|
side="right"
|
||||||
className="flex w-full flex-col gap-0 border-l border-slate-800 bg-slate-950 p-0 sm:max-w-md"
|
className="flex w-full flex-col gap-0 border-l border-border bg-popover p-0 sm:max-w-md"
|
||||||
>
|
>
|
||||||
<SheetHeader className="border-b border-slate-800 px-5 py-4">
|
<SheetHeader className="border-b border-border px-5 py-4">
|
||||||
<SheetTitle className="flex items-center gap-2 text-slate-100">
|
<SheetTitle className="flex items-center gap-2 text-popover-foreground">
|
||||||
<Icon className={cn("h-4 w-4 shrink-0", meta.color)} />
|
<Icon className={cn("h-4 w-4 shrink-0", meta.color)} />
|
||||||
<span>{meta.label}</span>
|
<span>{meta.label}</span>
|
||||||
{isEntry && (
|
{isEntry && (
|
||||||
@ -573,7 +574,7 @@ function NodeEditSheet({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
<SheetDescription className="font-mono text-[11px] text-slate-400">
|
<SheetDescription className="font-mono text-[11px] text-muted-foreground">
|
||||||
{node.node_key}
|
{node.node_key}
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
@ -587,7 +588,7 @@ function NodeEditSheet({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SheetFooter className="border-t border-slate-800 px-5 py-3 sm:flex-row sm:justify-between">
|
<SheetFooter className="border-t border-border px-5 py-3 sm:flex-row sm:justify-between">
|
||||||
{!isEntry ? (
|
{!isEntry ? (
|
||||||
<Button variant="ghost" size="sm" onClick={onSetEntry}>
|
<Button variant="ghost" size="sm" onClick={onSetEntry}>
|
||||||
Set as entry
|
Set as entry
|
||||||
@ -657,13 +658,13 @@ function CanvasAddNodeButton() {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
className="inline-flex items-center gap-1.5 rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-xs font-medium text-slate-200 shadow-lg transition-colors hover:bg-slate-800"
|
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 text-xs font-medium text-foreground shadow-lg transition-colors hover:bg-muted"
|
||||||
aria-label="Add node"
|
aria-label="Add node"
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5" />
|
<Plus className="h-3.5 w-3.5" />
|
||||||
Add node
|
Add node
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="border-slate-700 bg-slate-900">
|
<DropdownMenuContent align="end" className="border-border bg-popover">
|
||||||
{ADD_NODE_TYPES.map((t) => {
|
{ADD_NODE_TYPES.map((t) => {
|
||||||
const meta = NODE_META[t];
|
const meta = NODE_META[t];
|
||||||
const Icon = meta.icon;
|
const Icon = meta.icon;
|
||||||
|
|||||||
@ -85,7 +85,7 @@ export function FlowEditorShell({ initialFlow, initialNodes }: Props) {
|
|||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
aria-label="Editor view"
|
aria-label="Editor view"
|
||||||
className="inline-flex items-center gap-1 rounded-md border border-slate-700 bg-slate-900 p-0.5 text-xs"
|
className="inline-flex items-center gap-1 rounded-md border border-border bg-card p-0.5 text-xs"
|
||||||
>
|
>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
active={effectiveView === "canvas"}
|
active={effectiveView === "canvas"}
|
||||||
@ -108,7 +108,7 @@ export function FlowEditorShell({ initialFlow, initialNodes }: Props) {
|
|||||||
{/* Sticky-bottom validation panel mirrors the placement used
|
{/* Sticky-bottom validation panel mirrors the placement used
|
||||||
when this lived inside FlowBuilder — the activate-readiness
|
when this lived inside FlowBuilder — the activate-readiness
|
||||||
status follows the user as they scroll, in either view. */}
|
status follows the user as they scroll, in either view. */}
|
||||||
<div className="sticky bottom-4 z-10 shadow-xl shadow-slate-950/60">
|
<div className="sticky bottom-4 z-10 shadow-xl shadow-background/60">
|
||||||
<ValidationPanel />
|
<ValidationPanel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -157,8 +157,8 @@ function ToggleButton({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-1.5 rounded px-2 py-1 transition-colors",
|
"inline-flex items-center gap-1.5 rounded px-2 py-1 transition-colors",
|
||||||
active
|
active
|
||||||
? "bg-slate-700 text-slate-100"
|
? "bg-secondary text-secondary-foreground"
|
||||||
: "text-slate-400 hover:bg-slate-800 hover:text-slate-200",
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
|
|||||||
@ -43,19 +43,19 @@ export function TextRow({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">{label}</label>
|
<label className="mb-1 block text-xs text-muted-foreground">{label}</label>
|
||||||
{rows > 1 ? (
|
{rows > 1 ? (
|
||||||
<Textarea
|
<Textarea
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
className="bg-slate-800"
|
className="bg-muted"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className="bg-slate-800"
|
className="bg-muted"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -77,7 +77,7 @@ export function NextNodeRow({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">{label}</label>
|
<label className="mb-1 block text-xs text-muted-foreground">{label}</label>
|
||||||
<NodeKeySelect
|
<NodeKeySelect
|
||||||
value={value || null}
|
value={value || null}
|
||||||
nodes={allNodes}
|
nodes={allNodes}
|
||||||
@ -110,7 +110,7 @@ export function NodeKeySelect({
|
|||||||
value={value ?? "__none__"}
|
value={value ?? "__none__"}
|
||||||
onValueChange={(v) => onChange(v === "__none__" ? null : v)}
|
onValueChange={(v) => onChange(v === "__none__" ? null : v)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className={cn("bg-slate-800", className)}>
|
<SelectTrigger className={cn("bg-muted", className)}>
|
||||||
<SelectValue placeholder={placeholder ?? "—"} />
|
<SelectValue placeholder={placeholder ?? "—"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@ -135,7 +135,7 @@ export function NodeConfigForm({
|
|||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">
|
<label className="mb-1 block text-xs text-muted-foreground">
|
||||||
Variable key (stored in flow_runs.vars; alphanumeric + underscore)
|
Variable key (stored in flow_runs.vars; alphanumeric + underscore)
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
@ -146,11 +146,11 @@ export function NodeConfigForm({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder="e.g. name, email, company"
|
placeholder="e.g. name, email, company"
|
||||||
className="bg-slate-800 font-mono text-xs"
|
className="bg-muted font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-[10px] text-slate-500">
|
<p className="mt-1 text-[10px] text-muted-foreground">
|
||||||
Interpolate in downstream prompts and handoff notes with{" "}
|
Interpolate in downstream prompts and handoff notes with{" "}
|
||||||
<code className="rounded bg-slate-800 px-1">
|
<code className="rounded bg-muted px-1">
|
||||||
{"{{vars."}
|
{"{{vars."}
|
||||||
{(cfg as { var_key?: string }).var_key || "name"}
|
{(cfg as { var_key?: string }).var_key || "name"}
|
||||||
{"}}"}
|
{"}}"}
|
||||||
@ -200,7 +200,7 @@ export function NodeConfigForm({
|
|||||||
|
|
||||||
case "end":
|
case "end":
|
||||||
return (
|
return (
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
Terminal node. When the runner reaches this node the run is marked
|
Terminal node. When the runner reaches this node the run is marked
|
||||||
complete. No config needed.
|
complete. No config needed.
|
||||||
</p>
|
</p>
|
||||||
@ -269,7 +269,7 @@ function SendButtonsForm({
|
|||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<label className="text-xs text-slate-400">
|
<label className="text-xs text-muted-foreground">
|
||||||
Buttons (1–3) — each one routes to a different next node
|
Buttons (1–3) — each one routes to a different next node
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -278,7 +278,7 @@ function SendButtonsForm({
|
|||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid grid-cols-1 gap-2 rounded-md border border-slate-800 bg-slate-800/40 p-3",
|
"grid grid-cols-1 gap-2 rounded-md border border-border bg-muted/40 p-3",
|
||||||
showAdvanced
|
showAdvanced
|
||||||
? "md:grid-cols-[1fr_2fr_2fr_auto]"
|
? "md:grid-cols-[1fr_2fr_2fr_auto]"
|
||||||
: "md:grid-cols-[2fr_2fr_auto]",
|
: "md:grid-cols-[2fr_2fr_auto]",
|
||||||
@ -293,14 +293,14 @@ function SendButtonsForm({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder="reply_id"
|
placeholder="reply_id"
|
||||||
className="bg-slate-800 font-mono text-xs"
|
className="bg-muted font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
value={b.title}
|
value={b.title}
|
||||||
onChange={(e) => updateButton(i, { title: e.target.value })}
|
onChange={(e) => updateButton(i, { title: e.target.value })}
|
||||||
placeholder="Visible title (≤20 chars)"
|
placeholder="Visible title (≤20 chars)"
|
||||||
className="bg-slate-800"
|
className="bg-muted"
|
||||||
maxLength={20}
|
maxLength={20}
|
||||||
/>
|
/>
|
||||||
<NodeKeySelect
|
<NodeKeySelect
|
||||||
@ -465,13 +465,13 @@ function SendListForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<label className="mb-2 block text-xs text-slate-400">
|
<label className="mb-2 block text-xs text-muted-foreground">
|
||||||
Rows (1–10 total across all sections)
|
Rows (1–10 total across all sections)
|
||||||
</label>
|
</label>
|
||||||
{sections.map((section, sIdx) => (
|
{sections.map((section, sIdx) => (
|
||||||
<div
|
<div
|
||||||
key={sIdx}
|
key={sIdx}
|
||||||
className="mb-3 rounded-md border border-slate-800 bg-slate-800/40 p-3"
|
className="mb-3 rounded-md border border-border bg-muted/40 p-3"
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
@ -480,7 +480,7 @@ function SendListForm({
|
|||||||
updateSection(sIdx, { title: e.target.value })
|
updateSection(sIdx, { title: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder={`Section ${sIdx + 1} title (optional)`}
|
placeholder={`Section ${sIdx + 1} title (optional)`}
|
||||||
className="bg-slate-800 text-xs"
|
className="bg-muted text-xs"
|
||||||
/>
|
/>
|
||||||
{sections.length > 1 && (
|
{sections.length > 1 && (
|
||||||
<Button
|
<Button
|
||||||
@ -516,7 +516,7 @@ function SendListForm({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder="reply_id"
|
placeholder="reply_id"
|
||||||
className="bg-slate-800 font-mono text-xs"
|
className="bg-muted font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
@ -525,7 +525,7 @@ function SendListForm({
|
|||||||
updateRow(sIdx, rIdx, { title: e.target.value })
|
updateRow(sIdx, rIdx, { title: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder="Row title (≤24)"
|
placeholder="Row title (≤24)"
|
||||||
className="bg-slate-800"
|
className="bg-muted"
|
||||||
maxLength={24}
|
maxLength={24}
|
||||||
/>
|
/>
|
||||||
<NodeKeySelect
|
<NodeKeySelect
|
||||||
@ -614,14 +614,14 @@ function ConditionForm({
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">If</label>
|
<label className="mb-1 block text-xs text-muted-foreground">If</label>
|
||||||
<Select
|
<Select
|
||||||
value={subject}
|
value={subject}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
onUpdateConfig({ subject: v as ConditionCfg["subject"] })
|
onUpdateConfig({ subject: v as ConditionCfg["subject"] })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-slate-800">
|
<SelectTrigger className="bg-muted">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -632,7 +632,7 @@ function ConditionForm({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<label className="mb-1 block text-xs text-slate-400">
|
<label className="mb-1 block text-xs text-muted-foreground">
|
||||||
{subject === "var"
|
{subject === "var"
|
||||||
? "var name"
|
? "var name"
|
||||||
: subject === "tag"
|
: subject === "tag"
|
||||||
@ -644,7 +644,7 @@ function ConditionForm({
|
|||||||
value={cfg.subject_key ?? ""}
|
value={cfg.subject_key ?? ""}
|
||||||
onValueChange={(v) => onUpdateConfig({ subject_key: v })}
|
onValueChange={(v) => onUpdateConfig({ subject_key: v })}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-slate-800">
|
<SelectTrigger className="bg-muted">
|
||||||
<SelectValue placeholder="Pick a tag…" />
|
<SelectValue placeholder="Pick a tag…" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -660,7 +660,7 @@ function ConditionForm({
|
|||||||
value={cfg.subject_key ?? ""}
|
value={cfg.subject_key ?? ""}
|
||||||
onValueChange={(v) => onUpdateConfig({ subject_key: v })}
|
onValueChange={(v) => onUpdateConfig({ subject_key: v })}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-slate-800">
|
<SelectTrigger className="bg-muted">
|
||||||
<SelectValue placeholder="Pick a field…" />
|
<SelectValue placeholder="Pick a field…" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -677,7 +677,7 @@ function ConditionForm({
|
|||||||
onUpdateConfig({ subject_key: e.target.value })
|
onUpdateConfig({ subject_key: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder={subject === "var" ? "e.g. email" : "tag UUID"}
|
placeholder={subject === "var" ? "e.g. email" : "tag UUID"}
|
||||||
className="bg-slate-800 font-mono text-xs"
|
className="bg-muted font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -690,14 +690,14 @@ function ConditionForm({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">Operator</label>
|
<label className="mb-1 block text-xs text-muted-foreground">Operator</label>
|
||||||
<Select
|
<Select
|
||||||
value={operator}
|
value={operator}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
onUpdateConfig({ operator: v as ConditionCfg["operator"] })
|
onUpdateConfig({ operator: v as ConditionCfg["operator"] })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-slate-800">
|
<SelectTrigger className="bg-muted">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -710,11 +710,11 @@ function ConditionForm({
|
|||||||
</div>
|
</div>
|
||||||
{showValue && (
|
{showValue && (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">Value</label>
|
<label className="mb-1 block text-xs text-muted-foreground">Value</label>
|
||||||
<Input
|
<Input
|
||||||
value={cfg.value ?? ""}
|
value={cfg.value ?? ""}
|
||||||
onChange={(e) => onUpdateConfig({ value: e.target.value })}
|
onChange={(e) => onUpdateConfig({ value: e.target.value })}
|
||||||
className="bg-slate-800"
|
className="bg-muted"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -767,14 +767,14 @@ function SetTagForm({
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">Action</label>
|
<label className="mb-1 block text-xs text-muted-foreground">Action</label>
|
||||||
<Select
|
<Select
|
||||||
value={cfg.mode ?? "add"}
|
value={cfg.mode ?? "add"}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
onUpdateConfig({ mode: v as SetTagCfg["mode"] })
|
onUpdateConfig({ mode: v as SetTagCfg["mode"] })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-slate-800">
|
<SelectTrigger className="bg-muted">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -784,13 +784,13 @@ function SetTagForm({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">Tag</label>
|
<label className="mb-1 block text-xs text-muted-foreground">Tag</label>
|
||||||
{tags.length > 0 ? (
|
{tags.length > 0 ? (
|
||||||
<Select
|
<Select
|
||||||
value={cfg.tag_id ?? ""}
|
value={cfg.tag_id ?? ""}
|
||||||
onValueChange={(v) => onUpdateConfig({ tag_id: v })}
|
onValueChange={(v) => onUpdateConfig({ tag_id: v })}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-slate-800">
|
<SelectTrigger className="bg-muted">
|
||||||
<SelectValue placeholder="Pick a tag…" />
|
<SelectValue placeholder="Pick a tag…" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -806,7 +806,7 @@ function SetTagForm({
|
|||||||
value={cfg.tag_id ?? ""}
|
value={cfg.tag_id ?? ""}
|
||||||
onChange={(e) => onUpdateConfig({ tag_id: e.target.value })}
|
onChange={(e) => onUpdateConfig({ tag_id: e.target.value })}
|
||||||
placeholder="Tag UUID"
|
placeholder="Tag UUID"
|
||||||
className="bg-slate-800 font-mono text-xs"
|
className="bg-muted font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -972,7 +972,7 @@ function SendMediaForm({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">Media type</label>
|
<label className="mb-1 block text-xs text-muted-foreground">Media type</label>
|
||||||
<Select
|
<Select
|
||||||
value={mediaType}
|
value={mediaType}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
@ -986,7 +986,7 @@ function SendMediaForm({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-slate-800">
|
<SelectTrigger className="bg-muted">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -1000,15 +1000,15 @@ function SendMediaForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">File</label>
|
<label className="mb-1 block text-xs text-muted-foreground">File</label>
|
||||||
{cfg.media_url ? (
|
{cfg.media_url ? (
|
||||||
<div className="flex items-center gap-2 rounded-md border border-slate-700 bg-slate-800 px-3 py-2 text-xs">
|
<div className="flex items-center gap-2 rounded-md border border-border bg-muted px-3 py-2 text-xs">
|
||||||
<Paperclip className="h-3.5 w-3.5 shrink-0 text-cyan-400" />
|
<Paperclip className="h-3.5 w-3.5 shrink-0 text-cyan-400" />
|
||||||
<a
|
<a
|
||||||
href={cfg.media_url}
|
href={cfg.media_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="min-w-0 flex-1 truncate text-slate-200 hover:text-cyan-300"
|
className="min-w-0 flex-1 truncate text-foreground hover:text-cyan-300"
|
||||||
title={displayName || cfg.media_url}
|
title={displayName || cfg.media_url}
|
||||||
>
|
>
|
||||||
{displayName || cfg.media_url}
|
{displayName || cfg.media_url}
|
||||||
@ -1016,7 +1016,7 @@ function SendMediaForm({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
className="rounded p-1 text-slate-400 hover:bg-slate-700 hover:text-slate-200"
|
className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
aria-label="Remove file"
|
aria-label="Remove file"
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
>
|
>
|
||||||
@ -1028,7 +1028,7 @@ function SendMediaForm({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-md border border-dashed border-slate-700 bg-slate-900 px-3 py-4 text-xs text-slate-400 transition-colors hover:border-slate-600 hover:bg-slate-800 hover:text-slate-200 disabled:cursor-not-allowed disabled:opacity-60"
|
className="flex w-full items-center justify-center gap-2 rounded-md border border-dashed border-border bg-card px-3 py-4 text-xs text-muted-foreground transition-colors hover:border-border hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
>
|
>
|
||||||
{uploading ? (
|
{uploading ? (
|
||||||
<>
|
<>
|
||||||
@ -1066,14 +1066,14 @@ function SendMediaForm({
|
|||||||
|
|
||||||
{isDocument && (
|
{isDocument && (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-slate-400">
|
<label className="mb-1 block text-xs text-muted-foreground">
|
||||||
Filename shown to the customer (documents only)
|
Filename shown to the customer (documents only)
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
value={cfg.filename ?? ""}
|
value={cfg.filename ?? ""}
|
||||||
onChange={(e) => onUpdateConfig({ filename: e.target.value })}
|
onChange={(e) => onUpdateConfig({ filename: e.target.value })}
|
||||||
placeholder="invoice.pdf"
|
placeholder="invoice.pdf"
|
||||||
className="bg-slate-800 text-xs"
|
className="bg-muted text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -53,11 +53,11 @@ export function EditorHeader() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center gap-2 text-xs text-slate-500">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push("/flows")}
|
onClick={() => router.push("/flows")}
|
||||||
className="inline-flex items-center gap-1 hover:text-slate-300"
|
className="inline-flex items-center gap-1 hover:text-foreground"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-3 w-3" />
|
<ArrowLeft className="h-3 w-3" />
|
||||||
Flows
|
Flows
|
||||||
@ -72,7 +72,7 @@ export function EditorHeader() {
|
|||||||
setState((s) => ({ ...s, name: e.target.value }))
|
setState((s) => ({ ...s, name: e.target.value }))
|
||||||
}
|
}
|
||||||
placeholder="Flow name"
|
placeholder="Flow name"
|
||||||
className="max-w-md bg-slate-900 text-lg font-semibold"
|
className="max-w-md bg-card text-lg font-semibold"
|
||||||
/>
|
/>
|
||||||
<StatusBadge status={state.status} />
|
<StatusBadge status={state.status} />
|
||||||
{dirty && (
|
{dirty && (
|
||||||
@ -154,7 +154,7 @@ export function EditorHeader() {
|
|||||||
setState((s) => ({ ...s, description: e.target.value }))
|
setState((s) => ({ ...s, description: e.target.value }))
|
||||||
}
|
}
|
||||||
placeholder="Optional description (internal — customers don't see this)"
|
placeholder="Optional description (internal — customers don't see this)"
|
||||||
className="bg-slate-900 text-sm"
|
className="bg-card text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -162,9 +162,9 @@ export function EditorHeader() {
|
|||||||
|
|
||||||
function StatusBadge({ status }: { status: BuilderState["status"] }) {
|
function StatusBadge({ status }: { status: BuilderState["status"] }) {
|
||||||
const cls = {
|
const cls = {
|
||||||
draft: "border-slate-700 bg-slate-800 text-slate-300",
|
draft: "border-border bg-muted text-muted-foreground",
|
||||||
active: "border-emerald-600/40 bg-emerald-500/10 text-emerald-300",
|
active: "border-emerald-600/40 bg-emerald-500/10 text-emerald-300",
|
||||||
archived: "border-slate-700 bg-slate-800/50 text-slate-500",
|
archived: "border-border bg-muted/50 text-muted-foreground",
|
||||||
}[status];
|
}[status];
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className={cn("shrink-0", cls)}>
|
<Badge variant="outline" className={cn("shrink-0", cls)}>
|
||||||
|
|||||||
@ -110,7 +110,7 @@ export const NODE_META: Record<
|
|||||||
icon: UserPlus,
|
icon: UserPlus,
|
||||||
color: "text-amber-400",
|
color: "text-amber-400",
|
||||||
},
|
},
|
||||||
end: { label: "End", icon: Flag, color: "text-slate-400" },
|
end: { label: "End", icon: Flag, color: "text-muted-foreground" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export function ValidationPanel() {
|
|||||||
// sticky-positioned over scrolled-behind node cards (a translucent
|
// sticky-positioned over scrolled-behind node cards (a translucent
|
||||||
// bg-emerald-500/10 would bleed through ugly).
|
// bg-emerald-500/10 would bleed through ugly).
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 rounded-lg border border-emerald-600/50 bg-slate-950 p-3 text-sm font-medium text-emerald-300">
|
<div className="flex items-center gap-2 rounded-lg border border-emerald-600/50 bg-background p-3 text-sm font-medium text-emerald-300">
|
||||||
<CircleCheck className="h-4 w-4 shrink-0" />
|
<CircleCheck className="h-4 w-4 shrink-0" />
|
||||||
No issues. Ready to activate.
|
No issues. Ready to activate.
|
||||||
</div>
|
</div>
|
||||||
@ -41,11 +41,11 @@ export function ValidationPanel() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border bg-slate-950 p-3",
|
"rounded-lg border bg-background p-3",
|
||||||
errors.length > 0 ? "border-red-500/40" : "border-amber-500/40",
|
errors.length > 0 ? "border-red-500/40" : "border-amber-500/40",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-center gap-2 text-xs text-slate-400">
|
<div className="mb-2 flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
{errors.length > 0 ? (
|
{errors.length > 0 ? (
|
||||||
<CircleAlert className="h-4 w-4 text-red-400" />
|
<CircleAlert className="h-4 w-4 text-red-400" />
|
||||||
) : (
|
) : (
|
||||||
@ -85,7 +85,7 @@ export function IssueLine({
|
|||||||
<CircleAlert className={cn("mt-0.5 h-3 w-3 shrink-0", iconTone)} />
|
<CircleAlert className={cn("mt-0.5 h-3 w-3 shrink-0", iconTone)} />
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
{issue.node_key && (
|
{issue.node_key && (
|
||||||
<code className="mr-1 rounded bg-slate-800 px-1 py-0.5 text-[10px] text-slate-400">
|
<code className="mr-1 rounded bg-muted px-1 py-0.5 text-[10px] text-muted-foreground">
|
||||||
{issue.node_key}
|
{issue.node_key}
|
||||||
</code>
|
</code>
|
||||||
)}
|
)}
|
||||||
@ -103,7 +103,7 @@ export function IssueLine({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onJump(issue.node_key!)}
|
onClick={() => onJump(issue.node_key!)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-start gap-2 rounded-md px-2 py-1 text-left text-xs transition-colors hover:bg-slate-800/60",
|
"flex w-full items-start gap-2 rounded-md px-2 py-1 text-left text-xs transition-colors hover:bg-muted/60",
|
||||||
tone,
|
tone,
|
||||||
)}
|
)}
|
||||||
aria-label={`Jump to node ${issue.node_key}`}
|
aria-label={`Jump to node ${issue.node_key}`}
|
||||||
|
|||||||
@ -117,8 +117,8 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
|
|||||||
|
|
||||||
if (!contact) {
|
if (!contact) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-70 items-center justify-center border-l border-slate-800 bg-slate-900">
|
<div className="flex h-full w-70 items-center justify-center border-l border-border bg-card">
|
||||||
<p className="text-sm text-slate-500">Select a conversation</p>
|
<p className="text-sm text-muted-foreground">Select a conversation</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -127,12 +127,12 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
|
|||||||
const initials = displayName.charAt(0).toUpperCase();
|
const initials = displayName.charAt(0).toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-70 flex-col border-l border-slate-800 bg-slate-900">
|
<div className="flex h-full w-70 flex-col border-l border-border bg-card">
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{/* Contact Info */}
|
{/* Contact Info */}
|
||||||
<div className="flex flex-col items-center text-center">
|
<div className="flex flex-col items-center text-center">
|
||||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-slate-700 text-lg font-semibold text-white">
|
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted text-lg font-semibold text-foreground">
|
||||||
{contact.avatar_url ? (
|
{contact.avatar_url ? (
|
||||||
<img
|
<img
|
||||||
src={contact.avatar_url}
|
src={contact.avatar_url}
|
||||||
@ -143,11 +143,11 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
|
|||||||
initials
|
initials
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mt-3 text-sm font-semibold text-white">
|
<h3 className="mt-3 text-sm font-semibold text-foreground">
|
||||||
{displayName}
|
{displayName}
|
||||||
</h3>
|
</h3>
|
||||||
{contact.company && (
|
{contact.company && (
|
||||||
<p className="text-xs text-slate-400">{contact.company}</p>
|
<p className="text-xs text-muted-foreground">{contact.company}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -155,37 +155,37 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
|
|||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleCopyPhone}
|
onClick={handleCopyPhone}
|
||||||
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-slate-300 transition-colors hover:bg-slate-800"
|
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted"
|
||||||
>
|
>
|
||||||
<Phone className="h-4 w-4 text-slate-500" />
|
<Phone className="h-4 w-4 text-muted-foreground" />
|
||||||
<span className="flex-1 text-left">{contact.phone}</span>
|
<span className="flex-1 text-left">{contact.phone}</span>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<Check className="h-3 w-3 text-primary" />
|
<Check className="h-3 w-3 text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="h-3 w-3 text-slate-600" />
|
<Copy className="h-3 w-3 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{contact.email && (
|
{contact.email && (
|
||||||
<div className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-slate-300">
|
<div className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-muted-foreground">
|
||||||
<Mail className="h-4 w-4 text-slate-500" />
|
<Mail className="h-4 w-4 text-muted-foreground" />
|
||||||
<span className="truncate">{contact.email}</span>
|
<span className="truncate">{contact.email}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="my-4 border-t border-slate-800" />
|
<div className="my-4 border-t border-border" />
|
||||||
|
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 px-1 text-xs font-medium uppercase tracking-wider text-slate-500">
|
<div className="flex items-center gap-2 px-1 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||||
<TagIcon className="h-3 w-3" />
|
<TagIcon className="h-3 w-3" />
|
||||||
Tags
|
Tags
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex flex-wrap gap-1">
|
<div className="mt-2 flex flex-wrap gap-1">
|
||||||
{tags.length === 0 ? (
|
{tags.length === 0 ? (
|
||||||
<p className="px-1 text-xs text-slate-600">No tags</p>
|
<p className="px-1 text-xs text-muted-foreground">No tags</p>
|
||||||
) : (
|
) : (
|
||||||
tags.map((tag) => (
|
tags.map((tag) => (
|
||||||
<span
|
<span
|
||||||
@ -204,27 +204,27 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="my-4 border-t border-slate-800" />
|
<div className="my-4 border-t border-border" />
|
||||||
|
|
||||||
{/* Active Deals */}
|
{/* Active Deals */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 px-1 text-xs font-medium uppercase tracking-wider text-slate-500">
|
<div className="flex items-center gap-2 px-1 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||||
<DollarSign className="h-3 w-3" />
|
<DollarSign className="h-3 w-3" />
|
||||||
Active Deals
|
Active Deals
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
{deals.length === 0 ? (
|
{deals.length === 0 ? (
|
||||||
<p className="px-1 text-xs text-slate-600">No deals</p>
|
<p className="px-1 text-xs text-muted-foreground">No deals</p>
|
||||||
) : (
|
) : (
|
||||||
deals.map((deal) => (
|
deals.map((deal) => (
|
||||||
<div
|
<div
|
||||||
key={deal.id}
|
key={deal.id}
|
||||||
className="rounded-lg bg-slate-800 px-3 py-2"
|
className="rounded-lg bg-muted px-3 py-2"
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium text-white">
|
<p className="text-sm font-medium text-foreground">
|
||||||
{deal.title}
|
{deal.title}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-1 flex items-center justify-between text-xs text-slate-400">
|
<div className="mt-1 flex items-center justify-between text-xs text-muted-foreground">
|
||||||
<span>
|
<span>
|
||||||
{deal.currency ?? "$"}
|
{deal.currency ?? "$"}
|
||||||
{deal.value.toLocaleString()}
|
{deal.value.toLocaleString()}
|
||||||
@ -248,11 +248,11 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="my-4 border-t border-slate-800" />
|
<div className="my-4 border-t border-border" />
|
||||||
|
|
||||||
{/* Notes */}
|
{/* Notes */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 px-1 text-xs font-medium uppercase tracking-wider text-slate-500">
|
<div className="flex items-center gap-2 px-1 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||||
<StickyNote className="h-3 w-3" />
|
<StickyNote className="h-3 w-3" />
|
||||||
Notes
|
Notes
|
||||||
</div>
|
</div>
|
||||||
@ -263,7 +263,7 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
|
|||||||
onChange={(e) => setNewNote(e.target.value)}
|
onChange={(e) => setNewNote(e.target.value)}
|
||||||
placeholder="Add a note..."
|
placeholder="Add a note..."
|
||||||
rows={2}
|
rows={2}
|
||||||
className="flex-1 resize-none rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 text-xs text-white placeholder-slate-500 outline-none focus:border-primary/50"
|
className="flex-1 resize-none rounded-lg border border-border bg-muted px-3 py-2 text-xs text-foreground placeholder-muted-foreground outline-none focus:border-primary/50"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -279,12 +279,12 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
|
|||||||
{notes.map((note) => (
|
{notes.map((note) => (
|
||||||
<div
|
<div
|
||||||
key={note.id}
|
key={note.id}
|
||||||
className="rounded-lg bg-slate-800 px-3 py-2"
|
className="rounded-lg bg-muted px-3 py-2"
|
||||||
>
|
>
|
||||||
<p className="whitespace-pre-wrap text-xs text-slate-300">
|
<p className="whitespace-pre-wrap text-xs text-muted-foreground">
|
||||||
{note.note_text}
|
{note.note_text}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-[10px] text-slate-600">
|
<p className="mt-1 text-[10px] text-muted-foreground">
|
||||||
{format(new Date(note.created_at), "MMM d, yyyy HH:mm")}
|
{format(new Date(note.created_at), "MMM d, yyyy HH:mm")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -33,7 +33,7 @@ interface ConversationListProps {
|
|||||||
const STATUS_COLORS: Record<ConversationStatus, string> = {
|
const STATUS_COLORS: Record<ConversationStatus, string> = {
|
||||||
open: "bg-primary",
|
open: "bg-primary",
|
||||||
pending: "bg-amber-500",
|
pending: "bg-amber-500",
|
||||||
closed: "bg-slate-500",
|
closed: "bg-muted-foreground",
|
||||||
};
|
};
|
||||||
|
|
||||||
const FILTER_OPTIONS: { label: string; value: ConversationStatus | "all" }[] = [
|
const FILTER_OPTIONS: { label: string; value: ConversationStatus | "all" }[] = [
|
||||||
@ -147,27 +147,27 @@ export function ConversationList({
|
|||||||
// w-full on mobile so the list occupies the whole viewport when it's
|
// w-full on mobile so the list occupies the whole viewport when it's
|
||||||
// the single pane showing; fixed 320px on desktop where it shares the
|
// the single pane showing; fixed 320px on desktop where it shares the
|
||||||
// row with the thread + contact sidebar.
|
// row with the thread + contact sidebar.
|
||||||
<div className="flex h-full w-full flex-col border-r border-slate-800 bg-slate-900 lg:w-80">
|
<div className="flex h-full w-full flex-col border-r border-border bg-card lg:w-80">
|
||||||
{/* Search + Filter */}
|
{/* Search + Filter */}
|
||||||
<div className="space-y-2 border-b border-slate-800 p-3">
|
<div className="space-y-2 border-b border-border p-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500" />
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
value={search}
|
value={search}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
placeholder="Search conversations..."
|
placeholder="Search conversations..."
|
||||||
className="border-slate-700 bg-slate-800 pl-9 text-sm text-white placeholder-slate-500 focus:border-primary/50"
|
className="border-border bg-muted pl-9 text-sm text-foreground placeholder-muted-foreground focus:border-primary/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger className="inline-flex items-center justify-center h-7 gap-1 px-2 text-xs text-slate-400 hover:text-white rounded-md hover:bg-slate-800">
|
<DropdownMenuTrigger className="inline-flex items-center justify-center h-7 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground rounded-md hover:bg-muted">
|
||||||
{activeFilter?.label ?? "All"}
|
{activeFilter?.label ?? "All"}
|
||||||
<ChevronDown className="h-3 w-3" />
|
<ChevronDown className="h-3 w-3" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="start"
|
align="start"
|
||||||
className="border-slate-700 bg-slate-800"
|
className="border-border bg-popover"
|
||||||
>
|
>
|
||||||
{FILTER_OPTIONS.map((opt) => (
|
{FILTER_OPTIONS.map((opt) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@ -177,7 +177,7 @@ export function ConversationList({
|
|||||||
"text-sm",
|
"text-sm",
|
||||||
filter === opt.value
|
filter === opt.value
|
||||||
? "text-primary"
|
? "text-primary"
|
||||||
: "text-slate-300"
|
: "text-popover-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
@ -200,7 +200,7 @@ export function ConversationList({
|
|||||||
</div>
|
</div>
|
||||||
) : filtered.length === 0 ? (
|
) : filtered.length === 0 ? (
|
||||||
<div className="px-4 py-12 text-center">
|
<div className="px-4 py-12 text-center">
|
||||||
<p className="text-sm text-slate-500">No conversations found</p>
|
<p className="text-sm text-muted-foreground">No conversations found</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@ -248,12 +248,12 @@ function ConversationItem({
|
|||||||
<button
|
<button
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-start gap-3 px-3 py-3 text-left transition-colors hover:bg-slate-800/50",
|
"flex w-full items-start gap-3 px-3 py-3 text-left transition-colors hover:bg-muted/50",
|
||||||
isActive && "border-l-2 border-primary bg-slate-800/70"
|
isActive && "border-l-2 border-primary bg-muted/70"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-slate-700 text-sm font-medium text-white">
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-muted text-sm font-medium text-foreground">
|
||||||
{contact?.avatar_url ? (
|
{contact?.avatar_url ? (
|
||||||
<img
|
<img
|
||||||
src={contact.avatar_url}
|
src={contact.avatar_url}
|
||||||
@ -268,13 +268,13 @@ function ConversationItem({
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="truncate text-sm font-medium text-white">
|
<span className="truncate text-sm font-medium text-foreground">
|
||||||
{displayName}
|
{displayName}
|
||||||
</span>
|
</span>
|
||||||
<span className="shrink-0 text-[10px] text-slate-500">{timeAgo}</span>
|
<span className="shrink-0 text-[10px] text-muted-foreground">{timeAgo}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 flex items-center justify-between gap-2">
|
<div className="mt-0.5 flex items-center justify-between gap-2">
|
||||||
<p className="truncate text-xs text-slate-400">
|
<p className="truncate text-xs text-muted-foreground">
|
||||||
{conversation.last_message_text || "No messages yet"}
|
{conversation.last_message_text || "No messages yet"}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex shrink-0 items-center gap-1.5">
|
<div className="flex shrink-0 items-center gap-1.5">
|
||||||
|
|||||||
@ -95,7 +95,7 @@ export function MessageActions({
|
|||||||
<div
|
<div
|
||||||
data-touch-open={touchOpen || pickerOpen ? "true" : undefined}
|
data-touch-open={touchOpen || pickerOpen ? "true" : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute -top-3 z-10 flex h-7 items-center gap-0.5 rounded-full border border-slate-700 bg-slate-900/95 px-1 shadow-md backdrop-blur-sm transition-opacity",
|
"absolute -top-3 z-10 flex h-7 items-center gap-0.5 rounded-full border border-border bg-popover/95 px-1 shadow-md backdrop-blur-sm transition-opacity",
|
||||||
"opacity-0 group-hover/actions:opacity-100 group-focus-within/actions:opacity-100",
|
"opacity-0 group-hover/actions:opacity-100 group-focus-within/actions:opacity-100",
|
||||||
"data-[touch-open=true]:opacity-100",
|
"data-[touch-open=true]:opacity-100",
|
||||||
isAgent ? "right-3" : "left-3",
|
isAgent ? "right-3" : "left-3",
|
||||||
@ -103,7 +103,7 @@ export function MessageActions({
|
|||||||
>
|
>
|
||||||
<Popover open={pickerOpen} onOpenChange={setPickerOpen}>
|
<Popover open={pickerOpen} onOpenChange={setPickerOpen}>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
className="flex h-5 w-5 items-center justify-center rounded-full text-slate-300 hover:bg-slate-700 hover:text-white"
|
className="flex h-5 w-5 items-center justify-center rounded-full text-popover-foreground hover:bg-muted hover:text-foreground"
|
||||||
aria-label="React"
|
aria-label="React"
|
||||||
>
|
>
|
||||||
<SmilePlus className="h-3.5 w-3.5" />
|
<SmilePlus className="h-3.5 w-3.5" />
|
||||||
@ -117,7 +117,7 @@ export function MessageActions({
|
|||||||
key={e}
|
key={e}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handlePickEmoji(e)}
|
onClick={() => handlePickEmoji(e)}
|
||||||
className="flex h-8 w-8 items-center justify-center rounded-full text-lg leading-none transition-transform hover:scale-125 hover:bg-slate-700"
|
className="flex h-8 w-8 items-center justify-center rounded-full text-lg leading-none transition-transform hover:scale-125 hover:bg-muted"
|
||||||
aria-label={`React with ${e}`}
|
aria-label={`React with ${e}`}
|
||||||
>
|
>
|
||||||
{e}
|
{e}
|
||||||
@ -128,7 +128,7 @@ export function MessageActions({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleReply}
|
onClick={handleReply}
|
||||||
className="flex h-5 w-5 items-center justify-center rounded-full text-slate-300 hover:bg-slate-700 hover:text-white"
|
className="flex h-5 w-5 items-center justify-center rounded-full text-popover-foreground hover:bg-muted hover:text-foreground"
|
||||||
aria-label="Reply"
|
aria-label="Reply"
|
||||||
>
|
>
|
||||||
<CornerUpLeft className="h-3.5 w-3.5" />
|
<CornerUpLeft className="h-3.5 w-3.5" />
|
||||||
@ -136,7 +136,7 @@ export function MessageActions({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="flex h-5 w-5 items-center justify-center rounded-full text-slate-300 hover:bg-slate-700 hover:text-white"
|
className="flex h-5 w-5 items-center justify-center rounded-full text-popover-foreground hover:bg-muted hover:text-foreground"
|
||||||
aria-label="Copy"
|
aria-label="Copy"
|
||||||
>
|
>
|
||||||
<Copy className="h-3.5 w-3.5" />
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
|||||||
@ -30,11 +30,11 @@ interface MessageBubbleProps {
|
|||||||
function StatusIcon({ status }: { status: Message["status"] }) {
|
function StatusIcon({ status }: { status: Message["status"] }) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "sending":
|
case "sending":
|
||||||
return <Clock className="h-3 w-3 text-slate-400" />;
|
return <Clock className="h-3 w-3 text-muted-foreground" />;
|
||||||
case "sent":
|
case "sent":
|
||||||
return <Check className="h-3 w-3 text-slate-400" />;
|
return <Check className="h-3 w-3 text-muted-foreground" />;
|
||||||
case "delivered":
|
case "delivered":
|
||||||
return <CheckCheck className="h-3 w-3 text-slate-400" />;
|
return <CheckCheck className="h-3 w-3 text-muted-foreground" />;
|
||||||
case "read":
|
case "read":
|
||||||
return <CheckCheck className="h-3 w-3 text-blue-400" />;
|
return <CheckCheck className="h-3 w-3 text-blue-400" />;
|
||||||
case "failed":
|
case "failed":
|
||||||
@ -46,8 +46,8 @@ function StatusIcon({ status }: { status: Message["status"] }) {
|
|||||||
|
|
||||||
function MediaUnavailable({ label }: { label: string }) {
|
function MediaUnavailable({ label }: { label: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 rounded-lg bg-slate-700/40 px-3 py-2 text-xs text-slate-300">
|
<div className="flex items-center gap-2 rounded-lg bg-muted/40 px-3 py-2 text-xs text-muted-foreground">
|
||||||
<ImageOff className="h-4 w-4 shrink-0 text-slate-500" />
|
<ImageOff className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
<span>{label} unavailable</span>
|
<span>{label} unavailable</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -92,15 +92,15 @@ function MediaImage({ url, alt }: { url: string; alt: string }) {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-40 w-60 items-center justify-center rounded-lg bg-slate-700">
|
<div className="flex h-40 w-60 items-center justify-center rounded-lg bg-muted">
|
||||||
<ImageOff className="h-8 w-8 text-slate-500" />
|
<ImageOff className="h-8 w-8 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-40 w-60 items-center justify-center rounded-lg bg-slate-700">
|
<div className="flex h-40 w-60 items-center justify-center rounded-lg bg-muted">
|
||||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -181,9 +181,9 @@ function MessageContent({ message }: { message: Message }) {
|
|||||||
href={message.media_url}
|
href={message.media_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-2 rounded-lg bg-slate-700/50 px-3 py-2 text-sm hover:bg-slate-700"
|
className="flex items-center gap-2 rounded-lg bg-muted/50 px-3 py-2 text-sm hover:bg-muted"
|
||||||
>
|
>
|
||||||
<FileText className="h-5 w-5 shrink-0 text-slate-400" />
|
<FileText className="h-5 w-5 shrink-0 text-muted-foreground" />
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{message.content_text || "Document"}
|
{message.content_text || "Document"}
|
||||||
</span>
|
</span>
|
||||||
@ -208,7 +208,7 @@ function MessageContent({ message }: { message: Message }) {
|
|||||||
case "location":
|
case "location":
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<MapPin className="h-4 w-4 shrink-0 text-slate-400" />
|
<MapPin className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
<span>{message.content_text || "Location shared"}</span>
|
<span>{message.content_text || "Location shared"}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -221,7 +221,7 @@ function MessageContent({ message }: { message: Message }) {
|
|||||||
// tap rather than the customer typing the same words.
|
// tap rather than the customer typing the same words.
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<span className="inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-slate-400">
|
<span className="inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
<CornerDownLeft className="h-3 w-3" />
|
<CornerDownLeft className="h-3 w-3" />
|
||||||
Button reply
|
Button reply
|
||||||
</span>
|
</span>
|
||||||
@ -265,11 +265,15 @@ export function MessageBubble({
|
|||||||
"relative rounded-2xl px-3 py-2",
|
"relative rounded-2xl px-3 py-2",
|
||||||
isAgent
|
isAgent
|
||||||
? "rounded-br-md bg-primary text-primary-foreground"
|
? "rounded-br-md bg-primary text-primary-foreground"
|
||||||
: "rounded-bl-md bg-slate-800 text-slate-100",
|
: "rounded-bl-md bg-muted text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{reply && (
|
{reply && (
|
||||||
<ReplyQuote authorLabel={reply.authorLabel} preview={reply.preview} />
|
<ReplyQuote
|
||||||
|
authorLabel={reply.authorLabel}
|
||||||
|
preview={reply.preview}
|
||||||
|
onPrimary={isAgent}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<MessageContent message={message} />
|
<MessageContent message={message} />
|
||||||
<div
|
<div
|
||||||
@ -278,7 +282,18 @@ export function MessageBubble({
|
|||||||
isAgent ? "justify-end" : "justify-start",
|
isAgent ? "justify-end" : "justify-start",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="text-[10px] text-white/60">{time}</span>
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-[10px]",
|
||||||
|
// Outbound bubbles sit on the primary fill, so the
|
||||||
|
// timestamp must read against that (not the neutral
|
||||||
|
// foreground) — otherwise it goes low-contrast in light
|
||||||
|
// mode. Inbound bubbles use the muted surface.
|
||||||
|
isAgent ? "text-primary-foreground/70" : "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{time}
|
||||||
|
</span>
|
||||||
{isAgent && <StatusIcon status={message.status} />}
|
{isAgent && <StatusIcon status={message.status} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -84,7 +84,7 @@ export function MessageComposer({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-slate-800 bg-slate-900 p-3">
|
<div className="border-t border-border bg-card p-3">
|
||||||
{replyTo && (
|
{replyTo && (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<ReplyQuote
|
<ReplyQuote
|
||||||
@ -118,7 +118,7 @@ export function MessageComposer({
|
|||||||
canAct={!readOnly}
|
canAct={!readOnly}
|
||||||
gateReason="send messages"
|
gateReason="send messages"
|
||||||
title={readOnly ? undefined : "Send template"}
|
title={readOnly ? undefined : "Send template"}
|
||||||
className="h-9 w-9 shrink-0 p-0 text-slate-400 hover:text-white"
|
className="h-9 w-9 shrink-0 p-0 text-muted-foreground hover:text-foreground"
|
||||||
onClick={onOpenTemplates}
|
onClick={onOpenTemplates}
|
||||||
>
|
>
|
||||||
<LayoutTemplate className="h-4 w-4" />
|
<LayoutTemplate className="h-4 w-4" />
|
||||||
@ -143,7 +143,7 @@ export function MessageComposer({
|
|||||||
// The placeholder text also surfaces the read-only state.
|
// The placeholder text also surfaces the read-only state.
|
||||||
title={readOnly ? "Read-only — your role can't send messages" : undefined}
|
title={readOnly ? "Read-only — your role can't send messages" : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 resize-none rounded-xl border border-slate-700 bg-slate-800 px-4 py-2.5 text-sm text-white placeholder-slate-500 outline-none transition-colors focus:border-primary/50",
|
"flex-1 resize-none rounded-xl border border-border bg-muted px-4 py-2.5 text-sm text-foreground placeholder-muted-foreground outline-none transition-colors focus:border-primary/50",
|
||||||
(sessionExpired || readOnly) && "cursor-not-allowed opacity-50"
|
(sessionExpired || readOnly) && "cursor-not-allowed opacity-50"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -163,7 +163,7 @@ export function MessageComposer({
|
|||||||
{/* Hint sits outside the flex row so its height doesn't push
|
{/* Hint sits outside the flex row so its height doesn't push
|
||||||
`items-end` buttons below the textarea. Indented to line up
|
`items-end` buttons below the textarea. Indented to line up
|
||||||
under the textarea left edge (w-9 button + gap-2 = 44px). */}
|
under the textarea left edge (w-9 button + gap-2 = 44px). */}
|
||||||
<p className="mt-1 pl-11 text-[10px] text-slate-600">
|
<p className="mt-1 pl-11 text-[10px] text-muted-foreground">
|
||||||
Type '/' for quick replies
|
Type '/' for quick replies
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export function MessageReactions({
|
|||||||
"inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[11px] leading-none transition-colors",
|
"inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[11px] leading-none transition-colors",
|
||||||
g.byCurrentUser
|
g.byCurrentUser
|
||||||
? "border-primary/60 bg-primary/15 text-primary hover:bg-primary/25"
|
? "border-primary/60 bg-primary/15 text-primary hover:bg-primary/25"
|
||||||
: "border-slate-700 bg-slate-800/80 text-slate-200 hover:bg-slate-700",
|
: "border-border bg-muted/80 text-foreground hover:bg-muted",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="text-sm leading-none">{g.emoji}</span>
|
<span className="text-sm leading-none">{g.emoji}</span>
|
||||||
|
|||||||
@ -116,7 +116,7 @@ function groupMessagesByDate(messages: Message[]) {
|
|||||||
const STATUS_OPTIONS: { label: string; value: ConversationStatus; color: string }[] = [
|
const STATUS_OPTIONS: { label: string; value: ConversationStatus; color: string }[] = [
|
||||||
{ label: "Open", value: "open", color: "text-primary" },
|
{ label: "Open", value: "open", color: "text-primary" },
|
||||||
{ label: "Pending", value: "pending", color: "text-amber-400" },
|
{ label: "Pending", value: "pending", color: "text-amber-400" },
|
||||||
{ label: "Closed", value: "closed", color: "text-slate-400" },
|
{ label: "Closed", value: "closed", color: "text-muted-foreground" },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,7 +129,7 @@ const STATUS_OPTIONS: { label: string; value: ConversationStatus; color: string
|
|||||||
* if we ever switch the asset, both spots update together.
|
* if we ever switch the asset, both spots update together.
|
||||||
*/
|
*/
|
||||||
const DOODLE_BG_CLASSES =
|
const DOODLE_BG_CLASSES =
|
||||||
"bg-slate-950 bg-[url('/inbox-doodle.svg')] bg-repeat";
|
"bg-background bg-[url('/inbox-doodle.svg')] bg-repeat";
|
||||||
|
|
||||||
export function MessageThread({
|
export function MessageThread({
|
||||||
conversation,
|
conversation,
|
||||||
@ -696,13 +696,13 @@ export function MessageThread({
|
|||||||
if (!conversation || !contact) {
|
if (!conversation || !contact) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-1 flex-col items-center justify-center", DOODLE_BG_CLASSES)}>
|
<div className={cn("flex flex-1 flex-col items-center justify-center", DOODLE_BG_CLASSES)}>
|
||||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-slate-800">
|
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
||||||
<MessageSquare className="h-8 w-8 text-slate-600" />
|
<MessageSquare className="h-8 w-8 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mt-4 text-sm font-medium text-slate-400">
|
<h3 className="mt-4 text-sm font-medium text-muted-foreground">
|
||||||
Select a conversation
|
Select a conversation
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 text-xs text-slate-600">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
Choose a conversation from the left to start messaging
|
Choose a conversation from the left to start messaging
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -722,9 +722,9 @@ export function MessageThread({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-1 flex-col", DOODLE_BG_CLASSES)}>
|
<div className={cn("flex flex-1 flex-col", DOODLE_BG_CLASSES)}>
|
||||||
{/* Header — solid bg-slate-900 sits on top of the doodle so the
|
{/* Header — solid card surface sits on top of the doodle so the
|
||||||
name/avatar/dropdowns stay legible. */}
|
name/avatar/dropdowns stay legible. */}
|
||||||
<div className="flex items-center justify-between gap-2 border-b border-slate-800 bg-slate-900 px-3 py-3 sm:px-4">
|
<div className="flex items-center justify-between gap-2 border-b border-border bg-card px-3 py-3 sm:px-4">
|
||||||
<div className="flex min-w-0 items-center gap-2 sm:gap-3">
|
<div className="flex min-w-0 items-center gap-2 sm:gap-3">
|
||||||
{/* Back-to-list button — mobile only. Hidden on lg+ where the
|
{/* Back-to-list button — mobile only. Hidden on lg+ where the
|
||||||
conversation list is always visible next to the thread. */}
|
conversation list is always visible next to the thread. */}
|
||||||
@ -733,24 +733,24 @@ export function MessageThread({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
aria-label="Back to conversations"
|
aria-label="Back to conversations"
|
||||||
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md text-slate-300 hover:bg-slate-800 hover:text-white lg:hidden"
|
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-5 w-5" />
|
<ArrowLeft className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-slate-700 text-sm font-medium text-white">
|
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-muted text-sm font-medium text-foreground">
|
||||||
{displayName.charAt(0).toUpperCase()}
|
{displayName.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h2 className="truncate text-sm font-semibold text-white">{displayName}</h2>
|
<h2 className="truncate text-sm font-semibold text-foreground">{displayName}</h2>
|
||||||
<p className="truncate text-xs text-slate-400">{contact.phone}</p>
|
<p className="truncate text-xs text-muted-foreground">{contact.phone}</p>
|
||||||
</div>
|
</div>
|
||||||
{/* Session timer badge — hidden on the narrowest phones so
|
{/* Session timer badge — hidden on the narrowest phones so
|
||||||
the name + back arrow keep their room. */}
|
the name + back arrow keep their room. */}
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-1 hidden gap-1 border-slate-700 text-[10px] sm:inline-flex sm:ml-2",
|
"ml-1 hidden gap-1 border-border text-[10px] sm:inline-flex sm:ml-2",
|
||||||
sessionInfo.expired ? "text-red-400" : "text-primary"
|
sessionInfo.expired ? "text-red-400" : "text-primary"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -773,7 +773,7 @@ export function MessageThread({
|
|||||||
aria-label="Refresh conversation"
|
aria-label="Refresh conversation"
|
||||||
title="Refresh"
|
title="Refresh"
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-7 w-7 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-slate-800 hover:text-white disabled:opacity-60",
|
"inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-60",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
@ -785,15 +785,15 @@ export function MessageThread({
|
|||||||
{/* Status dropdown */}
|
{/* Status dropdown */}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger className={cn(
|
<DropdownMenuTrigger className={cn(
|
||||||
"inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-slate-800",
|
"inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-muted",
|
||||||
currentStatus?.color ?? "text-slate-400"
|
currentStatus?.color ?? "text-muted-foreground"
|
||||||
)}>
|
)}>
|
||||||
{currentStatus?.label ?? "Status"}
|
{currentStatus?.label ?? "Status"}
|
||||||
<ChevronDown className="h-3 w-3" />
|
<ChevronDown className="h-3 w-3" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
className="border-slate-700 bg-slate-800"
|
className="border-border bg-popover"
|
||||||
>
|
>
|
||||||
{STATUS_OPTIONS.map((opt) => (
|
{STATUS_OPTIONS.map((opt) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@ -811,8 +811,8 @@ export function MessageThread({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-slate-800",
|
"inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-muted",
|
||||||
assignedAgentId ? "text-primary" : "text-slate-400"
|
assignedAgentId ? "text-primary" : "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<UserPlus className="h-3 w-3" />
|
<UserPlus className="h-3 w-3" />
|
||||||
@ -821,10 +821,10 @@ export function MessageThread({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
className="border-slate-700 bg-slate-800"
|
className="border-border bg-popover"
|
||||||
>
|
>
|
||||||
{profiles.length === 0 ? (
|
{profiles.length === 0 ? (
|
||||||
<DropdownMenuItem disabled className="text-sm text-slate-500">
|
<DropdownMenuItem disabled className="text-sm text-muted-foreground">
|
||||||
No teammates available
|
No teammates available
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : (
|
) : (
|
||||||
@ -836,7 +836,7 @@ export function MessageThread({
|
|||||||
onClick={() => handleAssignChange(p.user_id)}
|
onClick={() => handleAssignChange(p.user_id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm",
|
"text-sm",
|
||||||
isSelected ? "text-primary" : "text-slate-300"
|
isSelected ? "text-primary" : "text-popover-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="flex-1">
|
<span className="flex-1">
|
||||||
@ -850,10 +850,10 @@ export function MessageThread({
|
|||||||
)}
|
)}
|
||||||
{assignedAgentId && (
|
{assignedAgentId && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator className="bg-slate-700" />
|
<DropdownMenuSeparator className="bg-border" />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleAssignChange(null)}
|
onClick={() => handleAssignChange(null)}
|
||||||
className="text-sm text-slate-400"
|
className="text-sm text-muted-foreground"
|
||||||
>
|
>
|
||||||
Unassign
|
Unassign
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -872,8 +872,8 @@ export function MessageThread({
|
|||||||
</div>
|
</div>
|
||||||
) : messages.length === 0 ? (
|
) : messages.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-12">
|
<div className="flex flex-col items-center justify-center py-12">
|
||||||
<p className="text-sm text-slate-500">No messages yet</p>
|
<p className="text-sm text-muted-foreground">No messages yet</p>
|
||||||
<p className="text-xs text-slate-600">
|
<p className="text-xs text-muted-foreground">
|
||||||
Send a template to start the conversation
|
Send a template to start the conversation
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -883,7 +883,7 @@ export function MessageThread({
|
|||||||
<div key={group.date}>
|
<div key={group.date}>
|
||||||
{/* Date separator */}
|
{/* Date separator */}
|
||||||
<div className="mb-4 flex items-center justify-center">
|
<div className="mb-4 flex items-center justify-center">
|
||||||
<span className="rounded-full bg-slate-800 px-3 py-1 text-[10px] font-medium text-slate-400">
|
<span className="rounded-full bg-muted px-3 py-1 text-[10px] font-medium text-muted-foreground">
|
||||||
{formatDateSeparator(group.date)}
|
{formatDateSeparator(group.date)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,25 +14,38 @@ interface ReplyQuoteProps {
|
|||||||
/** Present → renders the composer-chip variant with an X button. Absent →
|
/** Present → renders the composer-chip variant with an X button. Absent →
|
||||||
* renders the embedded-in-bubble variant. */
|
* renders the embedded-in-bubble variant. */
|
||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
|
/** True when embedded inside an outbound (primary-filled) bubble, so the
|
||||||
|
* quote must read against the primary surface rather than the neutral
|
||||||
|
* foreground — otherwise it goes low-contrast in light mode. */
|
||||||
|
onPrimary?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReplyQuote({
|
export function ReplyQuote({
|
||||||
authorLabel,
|
authorLabel,
|
||||||
preview,
|
preview,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
|
onPrimary = false,
|
||||||
}: ReplyQuoteProps) {
|
}: ReplyQuoteProps) {
|
||||||
const isChip = !!onDismiss;
|
const isChip = !!onDismiss;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-start gap-2 border-l-2 border-primary px-2 py-1",
|
"flex items-start gap-2 border-l-2 px-2 py-1",
|
||||||
|
onPrimary ? "border-primary-foreground/50" : "border-primary",
|
||||||
isChip
|
isChip
|
||||||
? "rounded-md bg-slate-800/80"
|
? "rounded-md bg-muted/80"
|
||||||
: "mb-1.5 rounded-md bg-black/20",
|
: onPrimary
|
||||||
|
? "mb-1.5 rounded-md bg-primary-foreground/15"
|
||||||
|
: "mb-1.5 rounded-md bg-background/20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="min-w-0 flex-1 overflow-hidden">
|
<div className="min-w-0 flex-1 overflow-hidden">
|
||||||
<div className="truncate text-[11px] font-medium text-primary">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"truncate text-[11px] font-medium",
|
||||||
|
onPrimary ? "text-primary-foreground" : "text-primary",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{authorLabel}
|
{authorLabel}
|
||||||
</div>
|
</div>
|
||||||
{/* Wrap the preview instead of truncating to a single line.
|
{/* Wrap the preview instead of truncating to a single line.
|
||||||
@ -42,7 +55,7 @@ export function ReplyQuote({
|
|||||||
* layout wider, shoving the contact sidebar off-screen.
|
* layout wider, shoving the contact sidebar off-screen.
|
||||||
* `break-words` also wraps long URLs that have no whitespace
|
* `break-words` also wraps long URLs that have no whitespace
|
||||||
* to break on. Issue #165. */}
|
* to break on. Issue #165. */}
|
||||||
<div className="whitespace-pre-wrap break-words text-xs text-slate-200/80">
|
<div className="whitespace-pre-wrap break-words text-xs text-foreground/80">
|
||||||
{preview}
|
{preview}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -51,7 +64,7 @@ export function ReplyQuote({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onDismiss}
|
onClick={onDismiss}
|
||||||
aria-label="Cancel reply"
|
aria-label="Cancel reply"
|
||||||
className="flex h-6 w-6 shrink-0 items-center justify-center rounded text-slate-400 hover:bg-slate-700 hover:text-white"
|
className="flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
>
|
>
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -183,13 +183,13 @@ export function TemplatePicker({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="border-slate-700 bg-slate-900 sm:max-w-lg">
|
<DialogContent className="border-border bg-popover sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2 text-white">
|
<DialogTitle className="flex items-center gap-2 text-popover-foreground">
|
||||||
<LayoutTemplate className="h-4 w-4 text-primary" />
|
<LayoutTemplate className="h-4 w-4 text-primary" />
|
||||||
{selected ? selected.name : "Send template"}
|
{selected ? selected.name : "Send template"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
{selected
|
{selected
|
||||||
? "Fill in the placeholders to render this template. Meta requires every variable to be set."
|
? "Fill in the placeholders to render this template. Meta requires every variable to be set."
|
||||||
: "Pick an approved WhatsApp template to send to this contact."}
|
: "Pick an approved WhatsApp template to send to this contact."}
|
||||||
@ -203,9 +203,9 @@ export function TemplatePicker({
|
|||||||
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
||||||
</div>
|
</div>
|
||||||
) : templates.length === 0 ? (
|
) : templates.length === 0 ? (
|
||||||
<div className="rounded-md border border-slate-800 bg-slate-950/50 p-6 text-center">
|
<div className="rounded-md border border-border bg-background/50 p-6 text-center">
|
||||||
<p className="text-sm text-slate-300">No approved templates</p>
|
<p className="text-sm text-popover-foreground">No approved templates</p>
|
||||||
<p className="mt-1 text-xs text-slate-500">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
Approve a template in Meta WhatsApp Manager, then sync it
|
Approve a template in Meta WhatsApp Manager, then sync it
|
||||||
from Settings → Templates.
|
from Settings → Templates.
|
||||||
</p>
|
</p>
|
||||||
@ -216,28 +216,28 @@ export function TemplatePicker({
|
|||||||
key={t.id}
|
key={t.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => pickTemplate(t)}
|
onClick={() => pickTemplate(t)}
|
||||||
className="w-full rounded-md border border-slate-800 bg-slate-950/50 p-3 text-left transition-colors hover:border-primary/40 hover:bg-slate-900"
|
className="w-full rounded-md border border-border bg-background/50 p-3 text-left transition-colors hover:border-primary/40 hover:bg-popover"
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<p className="truncate text-sm font-medium text-white">
|
<p className="truncate text-sm font-medium text-popover-foreground">
|
||||||
{t.name}
|
{t.name}
|
||||||
</p>
|
</p>
|
||||||
<Badge className="border border-primary/30 bg-primary/20 text-[10px] text-primary">
|
<Badge className="border border-primary/30 bg-primary/20 text-[10px] text-primary">
|
||||||
{t.category}
|
{t.category}
|
||||||
</Badge>
|
</Badge>
|
||||||
{t.language && (
|
{t.language && (
|
||||||
<span className="text-[10px] uppercase text-slate-500">
|
<span className="text-[10px] uppercase text-muted-foreground">
|
||||||
{t.language}
|
{t.language}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 line-clamp-2 text-xs text-slate-400">
|
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||||
{t.body_text}
|
{t.body_text}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="h-4 w-4 flex-shrink-0 text-slate-500" />
|
<ChevronRight className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))
|
))
|
||||||
@ -245,33 +245,33 @@ export function TemplatePicker({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="rounded-md border border-slate-800 bg-slate-950/50 p-3">
|
<div className="rounded-md border border-border bg-background/50 p-3">
|
||||||
<p className="mb-1 text-xs text-slate-400">Preview</p>
|
<p className="mb-1 text-xs text-muted-foreground">Preview</p>
|
||||||
<p className="whitespace-pre-wrap text-sm text-slate-200">
|
<p className="whitespace-pre-wrap text-sm text-popover-foreground">
|
||||||
{renderBodyPreview(selected.body_text, params)}
|
{renderBodyPreview(selected.body_text, params)}
|
||||||
</p>
|
</p>
|
||||||
{selected.footer_text && (
|
{selected.footer_text && (
|
||||||
<p className="mt-2 text-xs italic text-slate-500">
|
<p className="mt-2 text-xs italic text-muted-foreground">
|
||||||
{selected.footer_text}
|
{selected.footer_text}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{slots && slots.headerVarCount > 0 && (
|
{slots && slots.headerVarCount > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs text-slate-300">
|
<Label className="text-xs text-popover-foreground">
|
||||||
{`Header {{1}}`}
|
{`Header {{1}}`}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
value={headerText}
|
value={headerText}
|
||||||
onChange={(e) => setHeaderText(e.target.value)}
|
onChange={(e) => setHeaderText(e.target.value)}
|
||||||
placeholder="Value for the header variable"
|
placeholder="Value for the header variable"
|
||||||
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500"
|
className="border-border bg-muted text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{slots?.bodyVars.map((v, i) => (
|
{slots?.bodyVars.map((v, i) => (
|
||||||
<div key={v} className="space-y-1">
|
<div key={v} className="space-y-1">
|
||||||
<Label className="text-xs text-slate-300">{`Body {{${v}}}`}</Label>
|
<Label className="text-xs text-popover-foreground">{`Body {{${v}}}`}</Label>
|
||||||
<Input
|
<Input
|
||||||
value={params[i] ?? ""}
|
value={params[i] ?? ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -280,13 +280,13 @@ export function TemplatePicker({
|
|||||||
setParams(next);
|
setParams(next);
|
||||||
}}
|
}}
|
||||||
placeholder={`Value for {{${v}}}`}
|
placeholder={`Value for {{${v}}}`}
|
||||||
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500"
|
className="border-border bg-muted text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{slots?.urlButtonSlots.map((slot) => (
|
{slots?.urlButtonSlots.map((slot) => (
|
||||||
<div key={slot.index} className="space-y-1">
|
<div key={slot.index} className="space-y-1">
|
||||||
<Label className="text-xs text-slate-300">
|
<Label className="text-xs text-popover-foreground">
|
||||||
{`URL button "${slot.text}" — value for `}{`{{1}}`}
|
{`URL button "${slot.text}" — value for `}{`{{1}}`}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -298,9 +298,9 @@ export function TemplatePicker({
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
placeholder="URL suffix value"
|
placeholder="URL suffix value"
|
||||||
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500"
|
className="border-border bg-muted text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] text-slate-500 break-all">
|
<p className="text-[10px] text-muted-foreground break-all">
|
||||||
Final URL: {slot.url.replace(/\{\{1\}\}/g, buttonParams[slot.index] || "{{1}}")}
|
Final URL: {slot.url.replace(/\{\{1\}\}/g, buttonParams[slot.index] || "{{1}}")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -314,7 +314,7 @@ export function TemplatePicker({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={resetSelection}
|
onClick={resetSelection}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-popover-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
Back
|
Back
|
||||||
@ -331,7 +331,7 @@ export function TemplatePicker({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleOpenChange(false)}
|
onClick={() => handleOpenChange(false)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-popover-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { ModeToggle } from "@/components/layout/mode-toggle";
|
||||||
|
|
||||||
const pageTitles: Record<string, string> = {
|
const pageTitles: Record<string, string> = {
|
||||||
"/dashboard": "Dashboard",
|
"/dashboard": "Dashboard",
|
||||||
@ -52,25 +53,28 @@ export function Header({ onOpenSidebar }: HeaderProps) {
|
|||||||
"U";
|
"U";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="flex h-14 shrink-0 items-center justify-between gap-3 border-b border-slate-800 bg-slate-950 px-4 lg:px-6">
|
<header className="flex h-14 shrink-0 items-center justify-between gap-3 border-b border-border bg-background px-4 lg:px-6">
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
{/* Hamburger — mobile only. 44×44 hit target per Apple HIG. */}
|
{/* Hamburger — mobile only. 44×44 hit target per Apple HIG. */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onOpenSidebar}
|
onClick={onOpenSidebar}
|
||||||
aria-label="Open menu"
|
aria-label="Open menu"
|
||||||
className="flex h-10 w-10 items-center justify-center rounded-md text-slate-300 transition-colors hover:bg-slate-800 hover:text-white lg:hidden"
|
className="flex h-10 w-10 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground lg:hidden"
|
||||||
>
|
>
|
||||||
<Menu className="h-5 w-5" />
|
<Menu className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
<h1 className="truncate text-base font-semibold text-white sm:text-lg">
|
<h1 className="truncate text-base font-semibold text-foreground sm:text-lg">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenu>
|
<div className="flex items-center gap-1 sm:gap-2">
|
||||||
|
<ModeToggle />
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
className="flex items-center gap-2 rounded-md px-1 py-1 transition-colors hover:bg-slate-800/70 focus:bg-slate-800/70 focus:outline-none data-popup-open:bg-slate-800/70 sm:gap-3 sm:pl-1 sm:pr-3"
|
className="flex items-center gap-2 rounded-md px-1 py-1 transition-colors hover:bg-muted/70 focus:bg-muted/70 focus:outline-none data-popup-open:bg-muted/70 sm:gap-3 sm:pl-1 sm:pr-3"
|
||||||
aria-label="Open account menu"
|
aria-label="Open account menu"
|
||||||
>
|
>
|
||||||
<Avatar className="size-8">
|
<Avatar className="size-8">
|
||||||
@ -84,29 +88,29 @@ export function Header({ onOpenSidebar }: HeaderProps) {
|
|||||||
{initial}
|
{initial}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<span className="hidden text-sm font-medium text-white sm:inline">
|
<span className="hidden text-sm font-medium text-foreground sm:inline">
|
||||||
{profile?.full_name ?? "User"}
|
{profile?.full_name ?? "User"}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={6}
|
sideOffset={6}
|
||||||
className="min-w-56 bg-slate-900 text-slate-100 ring-slate-700"
|
className="min-w-56 bg-popover text-popover-foreground ring-border"
|
||||||
>
|
>
|
||||||
<div className="px-2 py-1.5">
|
<div className="px-2 py-1.5">
|
||||||
<p className="truncate text-sm font-medium text-white">
|
<p className="truncate text-sm font-medium text-foreground">
|
||||||
{profile?.full_name ?? "User"}
|
{profile?.full_name ?? "User"}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-xs text-slate-400">
|
<p className="truncate text-xs text-muted-foreground">
|
||||||
{profile?.email ?? ""}
|
{profile?.email ?? ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator className="bg-slate-800" />
|
<DropdownMenuSeparator className="bg-border" />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
render={
|
render={
|
||||||
<Link
|
<Link
|
||||||
href="/settings?tab=profile"
|
href="/settings?tab=profile"
|
||||||
className="text-slate-200 focus:bg-slate-800 focus:text-white"
|
className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -117,23 +121,24 @@ export function Header({ onOpenSidebar }: HeaderProps) {
|
|||||||
render={
|
render={
|
||||||
<Link
|
<Link
|
||||||
href="/settings?tab=whatsapp"
|
href="/settings?tab=whatsapp"
|
||||||
className="text-slate-200 focus:bg-slate-800 focus:text-white"
|
className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SettingsIcon className="size-4" />
|
<SettingsIcon className="size-4" />
|
||||||
Settings
|
Settings
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="bg-slate-800" />
|
<DropdownMenuSeparator className="bg-border" />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={signOut}
|
onClick={signOut}
|
||||||
className="text-slate-200 focus:bg-slate-800 focus:text-white"
|
className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
|
||||||
>
|
>
|
||||||
<LogOut className="size-4" />
|
<LogOut className="size-4" />
|
||||||
Sign out
|
Sign out
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/components/layout/mode-toggle.tsx
Normal file
37
src/components/layout/mode-toggle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -52,14 +52,14 @@ const ROLE_CHIP: Record<
|
|||||||
label: "Agent",
|
label: "Agent",
|
||||||
// Neutral slate: the operational default.
|
// Neutral slate: the operational default.
|
||||||
className:
|
className:
|
||||||
"border-slate-700 bg-slate-800 text-slate-300",
|
"border-border bg-muted text-foreground",
|
||||||
},
|
},
|
||||||
viewer: {
|
viewer: {
|
||||||
icon: User,
|
icon: User,
|
||||||
label: "Viewer",
|
label: "Viewer",
|
||||||
// Muted slate: read-only role; visually quieter than agent.
|
// Muted slate: read-only role; visually quieter than agent.
|
||||||
className:
|
className:
|
||||||
"border-slate-800 bg-slate-900 text-slate-500",
|
"border-border bg-card text-muted-foreground",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
import {
|
import {
|
||||||
@ -157,7 +157,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
aria-label="Close menu"
|
aria-label="Close menu"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-30 bg-slate-950/70 backdrop-blur-sm transition-opacity lg:hidden",
|
"fixed inset-0 z-30 bg-background/70 backdrop-blur-sm transition-opacity lg:hidden",
|
||||||
open
|
open
|
||||||
? "pointer-events-auto opacity-100"
|
? "pointer-events-auto opacity-100"
|
||||||
: "pointer-events-none opacity-0",
|
: "pointer-events-none opacity-0",
|
||||||
@ -167,7 +167,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
// Mobile: fixed drawer that slides in from the left.
|
// Mobile: fixed drawer that slides in from the left.
|
||||||
"fixed inset-y-0 left-0 z-40 flex h-full w-64 flex-col border-r border-slate-800 bg-slate-900",
|
"fixed inset-y-0 left-0 z-40 flex h-full w-64 flex-col border-r border-border bg-card",
|
||||||
"transition-transform duration-200 ease-out will-change-transform",
|
"transition-transform duration-200 ease-out will-change-transform",
|
||||||
open ? "translate-x-0" : "-translate-x-full",
|
open ? "translate-x-0" : "-translate-x-full",
|
||||||
// Desktop: static, always visible — reset all the mobile framing.
|
// Desktop: static, always visible — reset all the mobile framing.
|
||||||
@ -177,12 +177,12 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
>
|
>
|
||||||
{/* Logo row. On mobile we put a close button here; on desktop the
|
{/* Logo row. On mobile we put a close button here; on desktop the
|
||||||
close button is hidden since the sidebar is always-visible. */}
|
close button is hidden since the sidebar is always-visible. */}
|
||||||
<div className="flex h-14 shrink-0 items-center justify-between gap-2 border-b border-slate-800 px-4">
|
<div className="flex h-14 shrink-0 items-center justify-between gap-2 border-b border-border px-4">
|
||||||
<Link href="/dashboard" className="flex items-center gap-2">
|
<Link href="/dashboard" className="flex items-center gap-2">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
||||||
<MessageSquare className="h-4 w-4" />
|
<MessageSquare className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-semibold text-white">
|
<span className="text-sm font-semibold text-foreground">
|
||||||
CRM Template for WhatsApp
|
CRM Template for WhatsApp
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
@ -190,7 +190,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close menu"
|
aria-label="Close menu"
|
||||||
className="flex h-9 w-9 items-center justify-center rounded-md text-slate-400 hover:bg-slate-800 hover:text-white lg:hidden"
|
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden"
|
||||||
>
|
>
|
||||||
<X className="h-5 w-5" />
|
<X className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
@ -216,7 +216,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2",
|
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2",
|
||||||
isActive
|
isActive
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary/10 text-primary"
|
||||||
: "text-slate-400 hover:bg-slate-800 hover:text-white",
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<item.icon className="h-4 w-4" />
|
<item.icon className="h-4 w-4" />
|
||||||
@ -244,7 +244,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div className="my-4 border-t border-slate-800" />
|
<div className="my-4 border-t border-border" />
|
||||||
|
|
||||||
<ul className="flex flex-col gap-1">
|
<ul className="flex flex-col gap-1">
|
||||||
{bottomNavItems.map((item) => {
|
{bottomNavItems.map((item) => {
|
||||||
@ -257,7 +257,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2",
|
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors lg:py-2",
|
||||||
isActive
|
isActive
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary/10 text-primary"
|
||||||
: "text-slate-400 hover:bg-slate-800 hover:text-white",
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<item.icon className="h-4 w-4" />
|
<item.icon className="h-4 w-4" />
|
||||||
@ -270,7 +270,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* User section */}
|
{/* User section */}
|
||||||
<div className="shrink-0 border-t border-slate-800 p-3">
|
<div className="shrink-0 border-t border-border p-3">
|
||||||
{/* Account name display — surfaced only when the account
|
{/* Account name display — surfaced only when the account
|
||||||
name differs from the user's own name (see
|
name differs from the user's own name (see
|
||||||
`showAccountStrip`). For a default solo account the two
|
`showAccountStrip`). For a default solo account the two
|
||||||
@ -278,7 +278,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
below; for renamed or shared accounts it tells the user
|
below; for renamed or shared accounts it tells the user
|
||||||
which account they're acting in. */}
|
which account they're acting in. */}
|
||||||
{showAccountStrip && account?.name ? (
|
{showAccountStrip && account?.name ? (
|
||||||
<div className="mb-2 flex items-center gap-2 px-3 text-xs text-slate-500">
|
<div className="mb-2 flex items-center gap-2 px-3 text-xs text-muted-foreground">
|
||||||
<UsersRound className="size-3.5 shrink-0" />
|
<UsersRound className="size-3.5 shrink-0" />
|
||||||
{/* `title=` exposes the full name on hover when it
|
{/* `title=` exposes the full name on hover when it
|
||||||
gets truncated (long account names + narrow
|
gets truncated (long account names + narrow
|
||||||
@ -307,7 +307,7 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-slate-800/60 focus:bg-slate-800/60 focus:outline-none data-popup-open:bg-slate-800/60">
|
<DropdownMenuTrigger className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-muted/60 focus:bg-muted/60 focus:outline-none data-popup-open:bg-muted/60">
|
||||||
<Avatar className="size-8 shrink-0">
|
<Avatar className="size-8 shrink-0">
|
||||||
{profile?.avatar_url ? (
|
{profile?.avatar_url ? (
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
@ -322,10 +322,10 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate text-sm font-medium text-white">
|
<p className="truncate text-sm font-medium text-foreground">
|
||||||
{profile?.full_name ?? "User"}
|
{profile?.full_name ?? "User"}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-xs text-slate-400">
|
<p className="truncate text-xs text-muted-foreground">
|
||||||
{profile?.email ?? ""}
|
{profile?.email ?? ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -334,14 +334,14 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
align="end"
|
align="end"
|
||||||
side="top"
|
side="top"
|
||||||
sideOffset={6}
|
sideOffset={6}
|
||||||
className="min-w-56 bg-slate-900 text-slate-100 ring-slate-700"
|
className="min-w-56 bg-popover text-popover-foreground ring-border"
|
||||||
>
|
>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
render={
|
render={
|
||||||
<Link
|
<Link
|
||||||
href="/settings?tab=profile"
|
href="/settings?tab=profile"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-slate-200 focus:bg-slate-800 focus:text-white"
|
className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -353,17 +353,17 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
<Link
|
<Link
|
||||||
href="/settings?tab=whatsapp"
|
href="/settings?tab=whatsapp"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-slate-200 focus:bg-slate-800 focus:text-white"
|
className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Settings className="size-4" />
|
<Settings className="size-4" />
|
||||||
Settings
|
Settings
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="bg-slate-800" />
|
<DropdownMenuSeparator className="bg-border" />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={signOut}
|
onClick={signOut}
|
||||||
className="text-slate-200 focus:bg-slate-800 focus:text-white"
|
className="text-popover-foreground focus:bg-accent focus:text-accent-foreground"
|
||||||
>
|
>
|
||||||
<LogOut className="size-4" />
|
<LogOut className="size-4" />
|
||||||
Sign out
|
Sign out
|
||||||
|
|||||||
@ -39,10 +39,10 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onEdit(deal);
|
onEdit(deal);
|
||||||
}}
|
}}
|
||||||
className={`group relative w-full cursor-pointer rounded-xl border border-slate-700/50 bg-slate-800/70 pl-4 pr-3 py-3 text-left shadow-sm transition-all ${
|
className={`group relative w-full cursor-pointer rounded-xl border border-border/50 bg-muted/70 pl-4 pr-3 py-3 text-left shadow-sm transition-all ${
|
||||||
isOverlay
|
isOverlay
|
||||||
? "shadow-xl"
|
? "shadow-xl"
|
||||||
: "hover:-translate-y-0.5 hover:border-slate-600 hover:bg-slate-800 hover:shadow-lg"
|
: "hover:-translate-y-0.5 hover:border-border hover:bg-muted hover:shadow-lg"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* 4px left accent bar using stage color */}
|
{/* 4px left accent bar using stage color */}
|
||||||
@ -53,7 +53,7 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<h4 className="flex-1 text-sm font-semibold leading-snug text-white break-words">
|
<h4 className="flex-1 text-sm font-semibold leading-snug text-foreground break-words">
|
||||||
{deal.title}
|
{deal.title}
|
||||||
</h4>
|
</h4>
|
||||||
{deal.status === "won" && (
|
{deal.status === "won" && (
|
||||||
@ -72,10 +72,10 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
|
|||||||
|
|
||||||
{/* Contact row */}
|
{/* Contact row */}
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-slate-700 text-[10px] font-semibold text-slate-200">
|
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-muted text-[10px] font-semibold text-foreground">
|
||||||
{initials(deal.contact?.name, deal.contact?.phone)}
|
{initials(deal.contact?.name, deal.contact?.phone)}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate text-xs text-slate-400">{contactLabel}</span>
|
<span className="truncate text-xs text-muted-foreground">{contactLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 flex items-center justify-between">
|
<div className="mt-2 flex items-center justify-between">
|
||||||
@ -83,7 +83,7 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
|
|||||||
{formatCurrency(deal.value, deal.currency)}
|
{formatCurrency(deal.value, deal.currency)}
|
||||||
</span>
|
</span>
|
||||||
{deal.expected_close_date && (
|
{deal.expected_close_date && (
|
||||||
<span className="flex items-center gap-1 text-[11px] text-slate-500">
|
<span className="flex items-center gap-1 text-[11px] text-muted-foreground">
|
||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
{formatDate(deal.expected_close_date)}
|
{formatDate(deal.expected_close_date)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -247,32 +247,32 @@ export function DealForm({
|
|||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent
|
<SheetContent
|
||||||
side="right"
|
side="right"
|
||||||
className="bg-slate-900 border-slate-700 text-slate-200 sm:max-w-lg w-full p-0"
|
className="bg-popover border-border text-popover-foreground sm:max-w-lg w-full p-0"
|
||||||
>
|
>
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<SheetHeader className="border-b border-slate-700/50 p-4">
|
<SheetHeader className="border-b border-border/50 p-4">
|
||||||
<SheetTitle className="text-white">
|
<SheetTitle className="text-popover-foreground">
|
||||||
{deal ? "Edit Deal" : "New Deal"}
|
{deal ? "Edit Deal" : "New Deal"}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-slate-300">Title</Label>
|
<Label className="text-muted-foreground">Title</Label>
|
||||||
<Input
|
<Input
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
placeholder="Deal title"
|
placeholder="Deal title"
|
||||||
className="border-slate-700 bg-slate-800 text-white"
|
className="border-border bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-slate-300">Contact</Label>
|
<Label className="text-muted-foreground">Contact</Label>
|
||||||
<select
|
<select
|
||||||
value={contactId}
|
value={contactId}
|
||||||
onChange={(e) => setContactId(e.target.value)}
|
onChange={(e) => setContactId(e.target.value)}
|
||||||
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary focus:ring-1 focus:ring-primary"
|
className="h-9 w-full rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary"
|
||||||
>
|
>
|
||||||
<option value="">Select a contact</option>
|
<option value="">Select a contact</option>
|
||||||
{contacts.map((c) => (
|
{contacts.map((c) => (
|
||||||
@ -295,24 +295,24 @@ export function DealForm({
|
|||||||
|
|
||||||
<div className="grid grid-cols-[1fr_110px] gap-3">
|
<div className="grid grid-cols-[1fr_110px] gap-3">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-slate-300">Value</Label>
|
<Label className="text-muted-foreground">Value</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<DollarSign className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-500" />
|
<DollarSign className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
className="border-slate-700 bg-slate-800 pl-7 text-white"
|
className="border-border bg-muted pl-7 text-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-slate-300">Currency</Label>
|
<Label className="text-muted-foreground">Currency</Label>
|
||||||
<select
|
<select
|
||||||
value={currency}
|
value={currency}
|
||||||
onChange={(e) => setCurrency(e.target.value)}
|
onChange={(e) => setCurrency(e.target.value)}
|
||||||
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary"
|
className="h-9 w-full rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary"
|
||||||
>
|
>
|
||||||
{CURRENCIES.map((c) => (
|
{CURRENCIES.map((c) => (
|
||||||
<option key={c.code} value={c.code}>
|
<option key={c.code} value={c.code}>
|
||||||
@ -324,21 +324,21 @@ export function DealForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-slate-300">Expected Close Date</Label>
|
<Label className="text-muted-foreground">Expected Close Date</Label>
|
||||||
<Input
|
<Input
|
||||||
type="date"
|
type="date"
|
||||||
value={expectedCloseDate}
|
value={expectedCloseDate}
|
||||||
onChange={(e) => setExpectedCloseDate(e.target.value)}
|
onChange={(e) => setExpectedCloseDate(e.target.value)}
|
||||||
className="border-slate-700 bg-slate-800 text-white"
|
className="border-border bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-slate-300">Stage</Label>
|
<Label className="text-muted-foreground">Stage</Label>
|
||||||
<select
|
<select
|
||||||
value={stageId}
|
value={stageId}
|
||||||
onChange={(e) => setStageId(e.target.value)}
|
onChange={(e) => setStageId(e.target.value)}
|
||||||
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary"
|
className="h-9 w-full rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary"
|
||||||
>
|
>
|
||||||
{stages.map((s) => (
|
{stages.map((s) => (
|
||||||
<option key={s.id} value={s.id}>
|
<option key={s.id} value={s.id}>
|
||||||
@ -349,11 +349,11 @@ export function DealForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-slate-300">Assigned To</Label>
|
<Label className="text-muted-foreground">Assigned To</Label>
|
||||||
<select
|
<select
|
||||||
value={assignedTo}
|
value={assignedTo}
|
||||||
onChange={(e) => setAssignedTo(e.target.value)}
|
onChange={(e) => setAssignedTo(e.target.value)}
|
||||||
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary"
|
className="h-9 w-full rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary"
|
||||||
>
|
>
|
||||||
<option value="">Unassigned</option>
|
<option value="">Unassigned</option>
|
||||||
{profiles.map((p) => (
|
{profiles.map((p) => (
|
||||||
@ -365,18 +365,18 @@ export function DealForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-slate-300">Notes</Label>
|
<Label className="text-muted-foreground">Notes</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={notes}
|
value={notes}
|
||||||
onChange={(e) => setNotes(e.target.value)}
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
placeholder="Add notes..."
|
placeholder="Add notes..."
|
||||||
className="min-h-[100px] border-slate-700 bg-slate-800 text-white"
|
className="min-h-[100px] border-border bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{deal && (
|
{deal && (
|
||||||
<div className="space-y-2 rounded-lg border border-slate-700 bg-slate-900/50 p-3">
|
<div className="space-y-2 rounded-lg border border-border bg-muted/50 p-3">
|
||||||
<p className="text-xs font-medium uppercase tracking-wider text-slate-400">
|
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||||
Status
|
Status
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@ -417,7 +417,7 @@ export function DealForm({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => handleStatusChange("open")}
|
onClick={() => handleStatusChange("open")}
|
||||||
disabled={!!statusAction}
|
disabled={!!statusAction}
|
||||||
className="w-full text-slate-400 hover:text-white"
|
className="w-full text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
Reopen deal
|
Reopen deal
|
||||||
</Button>
|
</Button>
|
||||||
@ -426,12 +426,12 @@ export function DealForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-slate-700/50 bg-slate-900/80 p-4">
|
<div className="border-t border-border/50 bg-popover/80 p-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
className="flex-1 border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800"
|
className="flex-1 border-border bg-transparent text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@ -453,7 +453,7 @@ export function DealForm({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setConfirmDelete(false)}
|
onClick={() => setConfirmDelete(false)}
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
className="rounded px-2 py-1 text-slate-300 hover:bg-slate-800"
|
className="rounded px-2 py-1 text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -93,9 +93,9 @@ export function PipelineAnalytics({ stages, deals }: PipelineAnalyticsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="grid grid-cols-2 gap-3 rounded-xl border border-slate-800 bg-slate-900/60 p-4 sm:grid-cols-3 xl:grid-cols-6">
|
<div className="grid grid-cols-2 gap-3 rounded-xl border border-border bg-card/60 p-4 sm:grid-cols-3 xl:grid-cols-6">
|
||||||
<Metric
|
<Metric
|
||||||
icon={<BarChart3 className="h-4 w-4 text-slate-400" />}
|
icon={<BarChart3 className="h-4 w-4 text-muted-foreground" />}
|
||||||
label="Total Deals"
|
label="Total Deals"
|
||||||
value={String(stats.totalCount)}
|
value={String(stats.totalCount)}
|
||||||
tooltip="Count of every deal in this pipeline that isn't marked as Lost. Won deals are still included."
|
tooltip="Count of every deal in this pipeline that isn't marked as Lost. Won deals are still included."
|
||||||
@ -147,8 +147,8 @@ function Metric({
|
|||||||
tooltip: string;
|
tooltip: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg bg-slate-800/50 p-3">
|
<div className="rounded-lg bg-muted/50 p-3">
|
||||||
<div className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-slate-400">
|
<div className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
||||||
{icon}
|
{icon}
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@ -157,7 +157,7 @@ function Metric({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`How ${label} is calculated`}
|
aria-label={`How ${label} is calculated`}
|
||||||
className="ml-auto text-slate-500 hover:text-slate-300 focus:outline-none"
|
className="ml-auto text-muted-foreground hover:text-foreground focus:outline-none"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -168,7 +168,7 @@ function Metric({
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-base font-semibold text-white">{value}</p>
|
<p className="mt-1 text-base font-semibold text-foreground">{value}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -164,7 +164,7 @@ export function PipelineBoard({
|
|||||||
@media (hover: hover) and (pointer: fine) {
|
@media (hover: hover) and (pointer: fine) {
|
||||||
.pipeline-scroll {
|
.pipeline-scroll {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgb(51 65 85) transparent;
|
scrollbar-color: var(--border) transparent;
|
||||||
}
|
}
|
||||||
.pipeline-scroll::-webkit-scrollbar {
|
.pipeline-scroll::-webkit-scrollbar {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@ -173,11 +173,11 @@ export function PipelineBoard({
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
.pipeline-scroll::-webkit-scrollbar-thumb {
|
.pipeline-scroll::-webkit-scrollbar-thumb {
|
||||||
background-color: rgb(51 65 85);
|
background-color: var(--border);
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
}
|
}
|
||||||
.pipeline-scroll::-webkit-scrollbar-thumb:hover {
|
.pipeline-scroll::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: rgb(71 85 105);
|
background-color: var(--muted-foreground);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
@ -209,21 +209,21 @@ function StageColumn({
|
|||||||
// restore the flex-1 share-the-row behavior. The droppable ref is
|
// restore the flex-1 share-the-row behavior. The droppable ref is
|
||||||
// on the inner messages region below — intentionally NOT here, so
|
// on the inner messages region below — intentionally NOT here, so
|
||||||
// a drag over the column header doesn't highlight the whole column.
|
// a drag over the column header doesn't highlight the whole column.
|
||||||
<div className="flex w-[85vw] min-w-[260px] max-w-[320px] shrink-0 snap-start flex-col rounded-xl border border-slate-800 bg-slate-900/60 p-4 lg:w-auto lg:max-w-none lg:flex-1 lg:basis-[260px] lg:shrink lg:snap-none">
|
<div className="flex w-[85vw] min-w-[260px] max-w-[320px] shrink-0 snap-start flex-col rounded-xl border border-border bg-card/60 p-4 lg:w-auto lg:max-w-none lg:flex-1 lg:basis-[260px] lg:shrink lg:snap-none">
|
||||||
{/* 3px colored top border — sits above the column's padding */}
|
{/* 3px colored top border — sits above the column's padding */}
|
||||||
<div
|
<div
|
||||||
className="-mx-4 -mt-4 h-[3px] rounded-t-xl"
|
className="-mx-4 -mt-4 h-[3px] rounded-t-xl"
|
||||||
style={{ backgroundColor: stage.color }}
|
style={{ backgroundColor: stage.color }}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between pt-3">
|
<div className="flex items-center justify-between pt-3">
|
||||||
<h3 className="truncate text-sm font-semibold text-white">
|
<h3 className="truncate text-sm font-semibold text-foreground">
|
||||||
{stage.name}
|
{stage.name}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="shrink-0 rounded-full bg-slate-800 px-2 py-0.5 text-[11px] font-medium text-slate-300">
|
<span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
||||||
{deals.length}
|
{deals.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-muted-foreground">
|
||||||
{formatCurrency(totalValue, currency)}
|
{formatCurrency(totalValue, currency)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -236,7 +236,7 @@ function StageColumn({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{deals.length === 0 ? (
|
{deals.length === 0 ? (
|
||||||
<div className="flex flex-1 items-center justify-center rounded-lg border-2 border-dashed border-slate-700 py-10 text-xs text-slate-500">
|
<div className="flex flex-1 items-center justify-center rounded-lg border-2 border-dashed border-border py-10 text-xs text-muted-foreground">
|
||||||
Drop a deal here
|
Drop a deal here
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -255,7 +255,7 @@ function StageColumn({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onAddDeal(stage.id)}
|
onClick={() => onAddDeal(stage.id)}
|
||||||
className="mt-3 w-full justify-start border border-dashed border-slate-700 bg-transparent text-slate-400 hover:border-slate-600 hover:bg-slate-800 hover:text-white"
|
className="mt-3 w-full justify-start border border-dashed border-border bg-transparent text-muted-foreground hover:border-border hover:bg-muted hover:text-foreground"
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-3 w-3" />
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
Add Deal
|
Add Deal
|
||||||
|
|||||||
@ -199,9 +199,9 @@ export function PipelineSettings({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md bg-slate-900 border-slate-700 max-h-[85vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-md bg-popover border-border max-h-[85vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">Manage Pipeline</DialogTitle>
|
<DialogTitle className="text-popover-foreground">Manage Pipeline</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{showDeleteConfirm ? (
|
{showDeleteConfirm ? (
|
||||||
@ -212,7 +212,7 @@ export function PipelineSettings({
|
|||||||
<p className="text-sm font-medium text-red-400">
|
<p className="text-sm font-medium text-red-400">
|
||||||
Delete Pipeline
|
Delete Pipeline
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-xs text-slate-400">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
This will archive all deals in this pipeline. This cannot be
|
This will archive all deals in this pipeline. This cannot be
|
||||||
undone.
|
undone.
|
||||||
</p>
|
</p>
|
||||||
@ -222,7 +222,7 @@ export function PipelineSettings({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setShowDeleteConfirm(false)}
|
onClick={() => setShowDeleteConfirm(false)}
|
||||||
className="border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800"
|
className="border-border bg-transparent text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@ -239,16 +239,16 @@ export function PipelineSettings({
|
|||||||
<>
|
<>
|
||||||
<div className="grid gap-4 py-2">
|
<div className="grid gap-4 py-2">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-slate-300">Pipeline Name</Label>
|
<Label className="text-muted-foreground">Pipeline Name</Label>
|
||||||
<Input
|
<Input
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
className="border-slate-700 bg-slate-800 text-white"
|
className="border-border bg-muted text-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-slate-300">Stages</Label>
|
<Label className="text-muted-foreground">Stages</Label>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
@ -292,7 +292,9 @@ export function PipelineSettings({
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
borderColor:
|
borderColor:
|
||||||
newStageColor === color ? "white" : "transparent",
|
newStageColor === color
|
||||||
|
? "var(--foreground)"
|
||||||
|
: "transparent",
|
||||||
}}
|
}}
|
||||||
aria-label={`Pick color ${color}`}
|
aria-label={`Pick color ${color}`}
|
||||||
/>
|
/>
|
||||||
@ -303,7 +305,7 @@ export function PipelineSettings({
|
|||||||
value={newStageName}
|
value={newStageName}
|
||||||
onChange={(e) => setNewStageName(e.target.value)}
|
onChange={(e) => setNewStageName(e.target.value)}
|
||||||
placeholder="New stage name"
|
placeholder="New stage name"
|
||||||
className="border-slate-700 bg-slate-800 text-sm text-white"
|
className="border-border bg-muted text-sm text-foreground"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") handleAddStage();
|
if (e.key === "Enter") handleAddStage();
|
||||||
}}
|
}}
|
||||||
@ -313,7 +315,7 @@ export function PipelineSettings({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleAddStage}
|
onClick={handleAddStage}
|
||||||
disabled={!newStageName.trim()}
|
disabled={!newStageName.trim()}
|
||||||
className="shrink-0 border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800"
|
className="shrink-0 border-border bg-transparent text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-3 w-3" />
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
Add
|
Add
|
||||||
@ -324,14 +326,14 @@ export function PipelineSettings({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onCreateNewPipeline}
|
onClick={onCreateNewPipeline}
|
||||||
className="w-full border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800"
|
className="w-full border-border bg-transparent text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-3 w-3" />
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
Create a new pipeline
|
Create a new pipeline
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="border-slate-700 bg-slate-900/50">
|
<DialogFooter className="border-border bg-popover/50">
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => setShowDeleteConfirm(true)}
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
@ -342,7 +344,7 @@ export function PipelineSettings({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
className="border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800"
|
className="border-border bg-transparent text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@ -387,13 +389,13 @@ function SortableStageRow({
|
|||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
className="flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-800 p-2"
|
className="flex items-center gap-2 rounded-lg border border-border bg-muted p-2"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
className="cursor-grab touch-none text-slate-500 hover:text-slate-300 active:cursor-grabbing"
|
className="cursor-grab touch-none text-muted-foreground hover:text-foreground active:cursor-grabbing"
|
||||||
aria-label="Drag to reorder"
|
aria-label="Drag to reorder"
|
||||||
>
|
>
|
||||||
<GripVertical className="h-4 w-4" />
|
<GripVertical className="h-4 w-4" />
|
||||||
@ -402,13 +404,13 @@ function SortableStageRow({
|
|||||||
<Input
|
<Input
|
||||||
value={stage.name}
|
value={stage.name}
|
||||||
onChange={(e) => onNameChange(e.target.value)}
|
onChange={(e) => onNameChange(e.target.value)}
|
||||||
className="h-7 flex-1 border-transparent bg-transparent text-sm text-white focus:border-slate-600"
|
className="h-7 flex-1 border-transparent bg-transparent text-sm text-foreground focus:border-border"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
className="text-slate-400 hover:text-red-400"
|
className="text-muted-foreground hover:text-red-400"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -431,14 +433,14 @@ function ColorSwatch({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
className="h-4 w-4 rounded-full border border-slate-600"
|
className="h-4 w-4 rounded-full border border-border"
|
||||||
style={{ backgroundColor: value }}
|
style={{ backgroundColor: value }}
|
||||||
aria-label="Change color"
|
aria-label="Change color"
|
||||||
/>
|
/>
|
||||||
{open && (
|
{open && (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
|
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
|
||||||
<div className="absolute left-0 top-6 z-20 flex flex-wrap gap-1 rounded-lg border border-slate-700 bg-slate-900 p-2 shadow-lg w-36">
|
<div className="absolute left-0 top-6 z-20 flex flex-wrap gap-1 rounded-lg border border-border bg-popover p-2 shadow-lg w-36">
|
||||||
{colors.map((c) => (
|
{colors.map((c) => (
|
||||||
<button
|
<button
|
||||||
key={c}
|
key={c}
|
||||||
@ -450,7 +452,8 @@ function ColorSwatch({
|
|||||||
className="h-5 w-5 rounded-full border-2 transition-transform hover:scale-110"
|
className="h-5 w-5 rounded-full border-2 transition-transform hover:scale-110"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: c,
|
backgroundColor: c,
|
||||||
borderColor: c === value ? "white" : "transparent",
|
borderColor:
|
||||||
|
c === value ? "var(--foreground)" : "transparent",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,53 +1,126 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Check } from "lucide-react";
|
import { Check, Moon, Sun } from "lucide-react";
|
||||||
|
|
||||||
import { useTheme } from "@/hooks/use-theme";
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
import { THEMES, type ThemeId } from "@/lib/themes";
|
import { MODES, THEMES, type Mode, type ThemeId } from "@/lib/themes";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appearance panel — color-theme picker.
|
* Appearance panel — light/dark mode + accent-color picker.
|
||||||
*
|
*
|
||||||
* Click a card → applies + persists immediately. No save button:
|
* Two independent controls: a mode toggle (light / dark) and the
|
||||||
* the whole change is a single CSS-variable swap on <html>, there's
|
* accent grid. Either applies + persists immediately. No save button:
|
||||||
* nothing to roll back. The active card carries a check chip + a
|
* each change is a single attribute swap on <html>, there's nothing
|
||||||
* primary-tinted border so the current pick is obvious.
|
* to roll back.
|
||||||
*
|
*
|
||||||
* Persistence: localStorage only (device-scoped). The boot script in
|
* Persistence: localStorage only (device-scoped). The boot script in
|
||||||
* layout.tsx replays the choice before first paint on subsequent
|
* layout.tsx replays both choices before first paint on subsequent
|
||||||
* loads.
|
* loads.
|
||||||
*/
|
*/
|
||||||
export function AppearancePanel() {
|
export function AppearancePanel() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme, mode, setMode } = useTheme();
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="space-y-8">
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<h2 className="text-lg font-semibold text-white">Color theme</h2>
|
<div>
|
||||||
<p className="mt-1 text-sm text-slate-400">
|
<h2 className="text-lg font-semibold text-foreground">Mode</h2>
|
||||||
Pick the accent color used across the app. All themes stay
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
dark — only the primary color (buttons, active nav, badges)
|
Choose light or dark. Light is easier on the eyes in bright
|
||||||
changes. Saved to this device.
|
rooms; dark is the original look. Works with any accent
|
||||||
</p>
|
color below. Saved to this device.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label="Color mode"
|
||||||
|
className="grid max-w-md grid-cols-2 gap-3"
|
||||||
|
>
|
||||||
|
{MODES.map((m) => (
|
||||||
|
<ModeCard
|
||||||
|
key={m}
|
||||||
|
mode={m}
|
||||||
|
isActive={m === mode}
|
||||||
|
onPick={() => setMode(m)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="space-y-4">
|
||||||
{THEMES.map((t) => (
|
<div>
|
||||||
<ThemeCard
|
<h2 className="text-lg font-semibold text-foreground">
|
||||||
key={t.id}
|
Accent color
|
||||||
id={t.id}
|
</h2>
|
||||||
name={t.name}
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
tagline={t.tagline}
|
Pick the accent color used across the app — buttons, active
|
||||||
swatch={t.swatch}
|
nav, and badges. Saved to this device.
|
||||||
isActive={t.id === theme}
|
</p>
|
||||||
onPick={() => setTheme(t.id)}
|
</div>
|
||||||
/>
|
|
||||||
))}
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{THEMES.map((t) => (
|
||||||
|
<ThemeCard
|
||||||
|
key={t.id}
|
||||||
|
id={t.id}
|
||||||
|
name={t.name}
|
||||||
|
tagline={t.tagline}
|
||||||
|
swatch={t.swatch}
|
||||||
|
isActive={t.id === theme}
|
||||||
|
onPick={() => setTheme(t.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ModeCard({
|
||||||
|
mode,
|
||||||
|
isActive,
|
||||||
|
onPick,
|
||||||
|
}: {
|
||||||
|
mode: Mode;
|
||||||
|
isActive: boolean;
|
||||||
|
onPick: () => void;
|
||||||
|
}) {
|
||||||
|
const isLight = mode === "light";
|
||||||
|
const Icon = isLight ? Sun : Moon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
onClick={onPick}
|
||||||
|
aria-checked={isActive}
|
||||||
|
aria-label={`Use ${mode} mode`}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 rounded-lg border bg-card p-4 text-left transition-colors",
|
||||||
|
isActive
|
||||||
|
? "border-primary/60 ring-2 ring-primary/40"
|
||||||
|
: "border-border hover:border-border hover:bg-muted/40",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-foreground"
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-sm font-semibold capitalize text-foreground">
|
||||||
|
{mode}
|
||||||
|
</span>
|
||||||
|
{isActive && (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-primary/15 px-2 py-0.5 text-[11px] font-medium text-primary">
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ThemeCard({
|
function ThemeCard({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@ -73,7 +146,7 @@ function ThemeCard({
|
|||||||
"flex flex-col gap-3 rounded-lg border bg-card p-4 text-left transition-colors",
|
"flex flex-col gap-3 rounded-lg border bg-card p-4 text-left transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? "border-primary/60 ring-2 ring-primary/40"
|
? "border-primary/60 ring-2 ring-primary/40"
|
||||||
: "border-slate-800 hover:border-slate-700 hover:bg-slate-800/40",
|
: "border-border hover:border-border hover:bg-muted/40",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -93,8 +166,8 @@ function ThemeCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-semibold text-white">{name}</div>
|
<div className="text-sm font-semibold text-foreground">{name}</div>
|
||||||
<div className="mt-1 text-xs leading-relaxed text-slate-400">
|
<div className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
||||||
{tagline}
|
{tagline}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -103,9 +176,9 @@ function ThemeCard({
|
|||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
<span className="flex-1" style={{ background: swatch }} />
|
<span className="flex-1" style={{ background: swatch }} />
|
||||||
<span className="w-3 bg-slate-700" />
|
<span className="w-3 bg-muted-foreground/60" />
|
||||||
<span className="w-3 bg-slate-800" />
|
<span className="w-3 bg-muted" />
|
||||||
<span className="w-3 bg-slate-900" />
|
<span className="w-3 bg-card" />
|
||||||
</div>
|
</div>
|
||||||
<span className="sr-only">Theme id: {id}</span>
|
<span className="sr-only">Theme id: {id}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -12,14 +12,14 @@ export function CustomFieldsSettings() {
|
|||||||
return (
|
return (
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-white">Custom fields</h2>
|
<h2 className="text-lg font-semibold text-foreground">Custom fields</h2>
|
||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm text-muted-foreground">
|
||||||
Define extra contact fields (e.g. ZIP code, lead source). They appear
|
Define extra contact fields (e.g. ZIP code, lead source). They appear
|
||||||
on every contact and in the “Update Contact Field” automation action.
|
on every contact and in the “Update Contact Field” automation action.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="border-slate-700 bg-slate-900 ring-0 ring-transparent">
|
<Card className="border-border bg-card ring-0 ring-transparent">
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4">
|
||||||
<CustomFieldsPanel />
|
<CustomFieldsPanel />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -68,13 +68,13 @@ export function DealsSettings() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mt-4 space-y-4">
|
<section className="mt-4 space-y-4">
|
||||||
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent">
|
<Card className="bg-card border-border ring-0 ring-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-white">
|
<CardTitle className="flex items-center gap-2 text-foreground">
|
||||||
<Coins className="size-4 text-primary" />
|
<Coins className="size-4 text-primary" />
|
||||||
Default currency
|
Default currency
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
New deals default to this currency, and pipeline and
|
New deals default to this currency, and pipeline and
|
||||||
dashboard totals are shown in it. Existing deals keep the
|
dashboard totals are shown in it. Existing deals keep the
|
||||||
currency they were saved with.
|
currency they were saved with.
|
||||||
@ -82,12 +82,12 @@ export function DealsSettings() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="grid gap-2 sm:max-w-xs">
|
<div className="grid gap-2 sm:max-w-xs">
|
||||||
<Label className="text-slate-300">Currency</Label>
|
<Label className="text-muted-foreground">Currency</Label>
|
||||||
<select
|
<select
|
||||||
value={selected}
|
value={selected}
|
||||||
onChange={(e) => setSelected(e.target.value)}
|
onChange={(e) => setSelected(e.target.value)}
|
||||||
disabled={!canEditSettings || profileLoading}
|
disabled={!canEditSettings || profileLoading}
|
||||||
className="h-9 w-full rounded-lg border border-slate-700 bg-slate-800 px-2.5 text-sm text-white outline-none focus:border-primary focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-60"
|
className="h-9 w-full rounded-lg border border-border bg-muted px-2.5 text-sm text-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
>
|
>
|
||||||
{CURRENCIES.map((c) => (
|
{CURRENCIES.map((c) => (
|
||||||
<option key={c.code} value={c.code}>
|
<option key={c.code} value={c.code}>
|
||||||
@ -96,7 +96,7 @@ export function DealsSettings() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{!canEditSettings && (
|
{!canEditSettings && (
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
Only account admins can change the default currency.
|
Only account admins can change the default currency.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -185,20 +185,20 @@ export function InviteMemberDialog({
|
|||||||
onOpenChange(next);
|
onOpenChange(next);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-md">
|
<DialogContent className="bg-popover border-border sm:max-w-md">
|
||||||
{result ? (
|
{result ? (
|
||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2 text-white">
|
<DialogTitle className="flex items-center gap-2 text-popover-foreground">
|
||||||
<Sparkles className="size-4 text-primary" />
|
<Sparkles className="size-4 text-primary" />
|
||||||
Invite created
|
Invite created
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
Share this link with your new teammate. They'll be able
|
Share this link with your new teammate. They'll be able
|
||||||
to sign up (or sign in) and join the account as{' '}
|
to sign up (or sign in) and join the account as{' '}
|
||||||
<span className="font-medium text-slate-300">{result.role}</span>
|
<span className="font-medium text-muted-foreground">{result.role}</span>
|
||||||
. The link is valid for{' '}
|
. The link is valid for{' '}
|
||||||
<span className="font-medium text-slate-300">
|
<span className="font-medium text-muted-foreground">
|
||||||
{result.expiresInDays} day{result.expiresInDays === 1 ? '' : 's'}
|
{result.expiresInDays} day{result.expiresInDays === 1 ? '' : 's'}
|
||||||
</span>
|
</span>
|
||||||
.
|
.
|
||||||
@ -206,12 +206,12 @@ export function InviteMemberDialog({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-3 py-2">
|
<div className="space-y-3 py-2">
|
||||||
<Label className="text-slate-300">Invite link</Label>
|
<Label className="text-muted-foreground">Invite link</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
readOnly
|
readOnly
|
||||||
value={result.url}
|
value={result.url}
|
||||||
className="bg-slate-800 border-slate-700 text-white font-mono text-xs"
|
className="bg-muted border-border text-foreground font-mono text-xs"
|
||||||
onFocus={(e) => e.currentTarget.select()}
|
onFocus={(e) => e.currentTarget.select()}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@ -250,7 +250,7 @@ export function InviteMemberDialog({
|
|||||||
className={buttonVariants({
|
className={buttonVariants({
|
||||||
variant: 'outline',
|
variant: 'outline',
|
||||||
className:
|
className:
|
||||||
'w-full border-slate-700 text-slate-300 hover:bg-slate-800',
|
'w-full border-border text-muted-foreground hover:bg-muted',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<MessageCircle className="size-4" />
|
<MessageCircle className="size-4" />
|
||||||
@ -258,7 +258,7 @@ export function InviteMemberDialog({
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="bg-slate-900 border-slate-700">
|
<DialogFooter className="bg-popover border-border">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
className="bg-primary hover:bg-primary/90 text-primary-foreground"
|
className="bg-primary hover:bg-primary/90 text-primary-foreground"
|
||||||
@ -270,8 +270,8 @@ export function InviteMemberDialog({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">Invite a teammate</DialogTitle>
|
<DialogTitle className="text-popover-foreground">Invite a teammate</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
Generate a one-time invite link. Share it via WhatsApp,
|
Generate a one-time invite link. Share it via WhatsApp,
|
||||||
Slack, or any channel you like — no email service required.
|
Slack, or any channel you like — no email service required.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
@ -279,12 +279,12 @@ export function InviteMemberDialog({
|
|||||||
|
|
||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Role</Label>
|
<Label className="text-muted-foreground">Role</Label>
|
||||||
<Select
|
<Select
|
||||||
value={role}
|
value={role}
|
||||||
onValueChange={(v) => v && setRole(v as InviteRole)}
|
onValueChange={(v) => v && setRole(v as InviteRole)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full bg-slate-800 border-slate-700 text-white">
|
<SelectTrigger className="w-full bg-muted border-border text-foreground">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -293,18 +293,18 @@ export function InviteMemberDialog({
|
|||||||
<SelectItem value="viewer">Viewer</SelectItem>
|
<SelectItem value="viewer">Viewer</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
{ROLE_DESCRIPTIONS[role]}
|
{ROLE_DESCRIPTIONS[role]}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Link valid for</Label>
|
<Label className="text-muted-foreground">Link valid for</Label>
|
||||||
<Select
|
<Select
|
||||||
value={expiry}
|
value={expiry}
|
||||||
onValueChange={(v) => v && setExpiry(v)}
|
onValueChange={(v) => v && setExpiry(v)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full bg-slate-800 border-slate-700 text-white">
|
<SelectTrigger className="w-full bg-muted border-border text-foreground">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -318,29 +318,29 @@ export function InviteMemberDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">
|
<Label className="text-muted-foreground">
|
||||||
Label{' '}
|
Label{' '}
|
||||||
<span className="text-xs text-slate-500">(optional)</span>
|
<span className="text-xs text-muted-foreground">(optional)</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="e.g. Sara — support team"
|
placeholder="e.g. Sara — support team"
|
||||||
value={label}
|
value={label}
|
||||||
onChange={(e) => setLabel(e.target.value)}
|
onChange={(e) => setLabel(e.target.value)}
|
||||||
maxLength={MAX_LABEL_LEN}
|
maxLength={MAX_LABEL_LEN}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
Helps you remember who you sent the link to in the pending
|
Helps you remember who you sent the link to in the pending
|
||||||
list below.
|
list below.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="bg-slate-900 border-slate-700">
|
<DialogFooter className="bg-popover border-border">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -110,12 +110,14 @@ const ROLE_CHIP: Record<
|
|||||||
agent: {
|
agent: {
|
||||||
icon: UserCog,
|
icon: UserCog,
|
||||||
label: 'Agent',
|
label: 'Agent',
|
||||||
className: 'border-slate-700 bg-slate-800 text-slate-300',
|
className: 'border-border bg-muted text-muted-foreground',
|
||||||
},
|
},
|
||||||
viewer: {
|
viewer: {
|
||||||
icon: UserIcon,
|
icon: UserIcon,
|
||||||
label: 'Viewer',
|
label: 'Viewer',
|
||||||
className: 'border-slate-800 bg-slate-900 text-slate-500',
|
// Outline-only so it stays quieter than the filled Agent chip in
|
||||||
|
// both modes — bg-card would blend into a card surface in light mode.
|
||||||
|
className: 'border-border bg-transparent text-muted-foreground',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -296,8 +298,8 @@ export function MembersTab() {
|
|||||||
{/* Header + invite button */}
|
{/* Header + invite button */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-white">Account members</h2>
|
<h2 className="text-lg font-semibold text-foreground">Account members</h2>
|
||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm text-muted-foreground">
|
||||||
People with access to this account. Roles control what each
|
People with access to this account. Roles control what each
|
||||||
teammate can do.
|
teammate can do.
|
||||||
</p>
|
</p>
|
||||||
@ -314,9 +316,9 @@ export function MembersTab() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Roster */}
|
{/* Roster */}
|
||||||
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent">
|
<Card className="bg-card border-border ring-0 ring-transparent">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<ul className="divide-y divide-slate-800">
|
<ul className="divide-y divide-border">
|
||||||
{members.map((member) => {
|
{members.map((member) => {
|
||||||
const roleMeta = ROLE_CHIP[member.role];
|
const roleMeta = ROLE_CHIP[member.role];
|
||||||
const RoleIcon = roleMeta.icon;
|
const RoleIcon = roleMeta.icon;
|
||||||
@ -351,17 +353,17 @@ export function MembersTab() {
|
|||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="truncate text-sm font-medium text-white">
|
<span className="truncate text-sm font-medium text-foreground">
|
||||||
{member.full_name || 'Unnamed'}
|
{member.full_name || 'Unnamed'}
|
||||||
</span>
|
</span>
|
||||||
{isSelf && (
|
{isSelf && (
|
||||||
<Badge className="bg-slate-800 text-slate-300 border-slate-700 text-[10px] uppercase tracking-wide">
|
<Badge className="bg-muted text-muted-foreground border-border text-[10px] uppercase tracking-wide">
|
||||||
You
|
You
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{member.email && (
|
{member.email && (
|
||||||
<p className="truncate text-xs text-slate-500">
|
<p className="truncate text-xs text-muted-foreground">
|
||||||
{member.email}
|
{member.email}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -370,7 +372,7 @@ export function MembersTab() {
|
|||||||
|
|
||||||
{/* Joined date stays desktop-only. The mobile row's
|
{/* Joined date stays desktop-only. The mobile row's
|
||||||
vertical density makes the joined date noise. */}
|
vertical density makes the joined date noise. */}
|
||||||
<div className="hidden sm:block text-right text-xs text-slate-500">
|
<div className="hidden sm:block text-right text-xs text-muted-foreground">
|
||||||
Joined {fmtDate(member.joined_at)}
|
Joined {fmtDate(member.joined_at)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -394,7 +396,7 @@ export function MembersTab() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className="w-32 bg-slate-800 border-slate-700 text-slate-200"
|
className="w-32 bg-muted border-border text-foreground"
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
>
|
>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
@ -446,11 +448,11 @@ export function MembersTab() {
|
|||||||
<RequireRole min="admin">
|
<RequireRole min="admin">
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<UsersRound className="size-4 text-slate-400" />
|
<UsersRound className="size-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold text-white">
|
<h3 className="text-sm font-semibold text-foreground">
|
||||||
Pending invitations
|
Pending invitations
|
||||||
</h3>
|
</h3>
|
||||||
<Badge className="bg-slate-800 text-slate-400 border-slate-700">
|
<Badge className="bg-muted text-muted-foreground border-border">
|
||||||
{invitations.length}
|
{invitations.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@ -460,7 +462,7 @@ export function MembersTab() {
|
|||||||
front (rather than letting the user discover it by
|
front (rather than letting the user discover it by
|
||||||
looking for a button) keeps it from feeling like a bug. */}
|
looking for a button) keeps it from feeling like a bug. */}
|
||||||
{invitations.length > 0 ? (
|
{invitations.length > 0 ? (
|
||||||
<p className="mb-3 text-xs text-slate-500">
|
<p className="mb-3 text-xs text-muted-foreground">
|
||||||
The plaintext invite URL is only shown once at creation
|
The plaintext invite URL is only shown once at creation
|
||||||
for security — to re-share, revoke the invite below and
|
for security — to re-share, revoke the invite below and
|
||||||
create a new one.
|
create a new one.
|
||||||
@ -468,22 +470,22 @@ export function MembersTab() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{invitations.length === 0 ? (
|
{invitations.length === 0 ? (
|
||||||
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent">
|
<Card className="bg-card border-border ring-0 ring-transparent">
|
||||||
<CardContent className="flex flex-col items-center justify-center py-8 text-center">
|
<CardContent className="flex flex-col items-center justify-center py-8 text-center">
|
||||||
<Mail className="size-6 text-slate-600" />
|
<Mail className="size-6 text-muted-foreground" />
|
||||||
<p className="mt-2 text-sm text-slate-400">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
No pending invitations.
|
No pending invitations.
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-xs text-slate-500">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
Click <span className="text-slate-300">Invite member</span>{' '}
|
Click <span className="text-muted-foreground">Invite member</span>{' '}
|
||||||
above to generate a shareable link.
|
above to generate a shareable link.
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent">
|
<Card className="bg-card border-border ring-0 ring-transparent">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<ul className="divide-y divide-slate-800">
|
<ul className="divide-y divide-border">
|
||||||
{invitations.map((inv) => {
|
{invitations.map((inv) => {
|
||||||
const inviteRoleMeta = ROLE_CHIP[inv.role];
|
const inviteRoleMeta = ROLE_CHIP[inv.role];
|
||||||
const InviteRoleIcon = inviteRoleMeta.icon;
|
const InviteRoleIcon = inviteRoleMeta.icon;
|
||||||
@ -494,7 +496,7 @@ export function MembersTab() {
|
|||||||
>
|
>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm font-medium text-white">
|
<span className="text-sm font-medium text-foreground">
|
||||||
{inv.label || 'Untitled invite'}
|
{inv.label || 'Untitled invite'}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@ -504,7 +506,7 @@ export function MembersTab() {
|
|||||||
{inviteRoleMeta.label}
|
{inviteRoleMeta.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-0.5 text-xs text-slate-500">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
Created {fmtDate(inv.created_at)} · {fmtExpiresIn(inv.expires_at)}
|
Created {fmtDate(inv.created_at)} · {fmtExpiresIn(inv.expires_at)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -544,15 +546,15 @@ export function MembersTab() {
|
|||||||
if (!open) setRemovingMember(null);
|
if (!open) setRemovingMember(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-sm">
|
<DialogContent className="bg-popover border-border sm:max-w-sm">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2 text-white">
|
<DialogTitle className="flex items-center gap-2 text-popover-foreground">
|
||||||
<AlertTriangle className="size-4 text-amber-400" />
|
<AlertTriangle className="size-4 text-amber-400" />
|
||||||
Remove member
|
Remove member
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
Remove{' '}
|
Remove{' '}
|
||||||
<span className="font-medium text-slate-300">
|
<span className="font-medium text-muted-foreground">
|
||||||
{removingMember?.full_name || 'this teammate'}
|
{removingMember?.full_name || 'this teammate'}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
from the account? They'll be signed out of this account
|
from the account? They'll be signed out of this account
|
||||||
@ -560,11 +562,11 @@ export function MembersTab() {
|
|||||||
login isn't deleted.
|
login isn't deleted.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter className="bg-slate-900 border-slate-700">
|
<DialogFooter className="bg-popover border-border">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setRemovingMember(null)}
|
onClick={() => setRemovingMember(null)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -81,13 +81,13 @@ export function PasswordForm() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-slate-900/40 border-slate-800">
|
<Card className="bg-card/40 border-border">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-white">
|
<CardTitle className="flex items-center gap-2 text-foreground">
|
||||||
<KeyRound className="size-4 text-primary" />
|
<KeyRound className="size-4 text-primary" />
|
||||||
Password
|
Password
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
Use at least {MIN_PASSWORD} characters. You will stay signed in on
|
Use at least {MIN_PASSWORD} characters. You will stay signed in on
|
||||||
this device after changing it.
|
this device after changing it.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@ -96,7 +96,7 @@ export function PasswordForm() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={onSubmit} className="space-y-4">
|
<form onSubmit={onSubmit} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="current-password" className="text-slate-200">
|
<Label htmlFor="current-password" className="text-foreground">
|
||||||
Current password
|
Current password
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -112,7 +112,7 @@ export function PasswordForm() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="new-password" className="text-slate-200">
|
<Label htmlFor="new-password" className="text-foreground">
|
||||||
New password
|
New password
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -127,7 +127,7 @@ export function PasswordForm() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="confirm-password" className="text-slate-200">
|
<Label htmlFor="confirm-password" className="text-foreground">
|
||||||
Confirm new password
|
Confirm new password
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@ -210,10 +210,10 @@ export function ProfileForm() {
|
|||||||
: '—';
|
: '—';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-slate-900/40 border-slate-800">
|
<Card className="bg-card/40 border-border">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-white">Profile</CardTitle>
|
<CardTitle className="text-foreground">Profile</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
How you show up across the app. Your avatar and name appear in the
|
How you show up across the app. Your avatar and name appear in the
|
||||||
header, sidebar, and anywhere your teammates see you.
|
header, sidebar, and anywhere your teammates see you.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@ -255,13 +255,13 @@ export function ProfileForm() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={onRemoveAvatar}
|
onClick={onRemoveAvatar}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="text-slate-400 hover:text-white"
|
className="text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<p className="w-full text-xs text-slate-500">
|
<p className="w-full text-xs text-muted-foreground">
|
||||||
PNG, JPG, WebP, or GIF. Up to 2 MB.
|
PNG, JPG, WebP, or GIF. Up to 2 MB.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -269,7 +269,7 @@ export function ProfileForm() {
|
|||||||
|
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="profile-full-name" className="text-slate-200">
|
<Label htmlFor="profile-full-name" className="text-foreground">
|
||||||
Display name
|
Display name
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -285,7 +285,7 @@ export function ProfileForm() {
|
|||||||
|
|
||||||
{/* Email */}
|
{/* Email */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="profile-email" className="text-slate-200">
|
<Label htmlFor="profile-email" className="text-foreground">
|
||||||
Email
|
Email
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -309,24 +309,24 @@ export function ProfileForm() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Read-only block */}
|
{/* Read-only block */}
|
||||||
<div className="rounded-lg border border-slate-800 bg-slate-900/60 p-4">
|
<div className="rounded-lg border border-border bg-card/60 p-4">
|
||||||
<p className="mb-3 text-xs font-semibold uppercase tracking-wider text-slate-500">
|
<p className="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
Account details
|
Account details
|
||||||
</p>
|
</p>
|
||||||
<dl className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
|
<dl className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-slate-500">Role</dt>
|
<dt className="text-muted-foreground">Role</dt>
|
||||||
<dd className="mt-0.5 font-mono text-slate-200">
|
<dd className="mt-0.5 font-mono text-foreground">
|
||||||
{profile?.role ?? 'user'}
|
{profile?.role ?? 'user'}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-slate-500">Joined</dt>
|
<dt className="text-muted-foreground">Joined</dt>
|
||||||
<dd className="mt-0.5 text-slate-200">{joined}</dd>
|
<dd className="mt-0.5 text-foreground">{joined}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<dt className="text-slate-500">User ID</dt>
|
<dt className="text-muted-foreground">User ID</dt>
|
||||||
<dd className="mt-0.5 break-all font-mono text-xs text-slate-400">
|
<dd className="mt-0.5 break-all font-mono text-xs text-muted-foreground">
|
||||||
{user?.id ?? '—'}
|
{user?.id ?? '—'}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@ -334,7 +334,7 @@ export function ProfileForm() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!profile && (
|
{!profile && (
|
||||||
<p className="flex items-center gap-2 text-sm text-slate-400">
|
<p className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<CircleAlert className="size-4" />
|
<CircleAlert className="size-4" />
|
||||||
Loading your profile…
|
Loading your profile…
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -49,13 +49,13 @@ export function SessionsCard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="bg-slate-900/40 border-slate-800">
|
<Card className="bg-card/40 border-border">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-white">
|
<CardTitle className="flex items-center gap-2 text-foreground">
|
||||||
<LogOut className="size-4 text-primary" />
|
<LogOut className="size-4 text-primary" />
|
||||||
Active sessions
|
Active sessions
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
Sign out of every device where you're logged in — including
|
Sign out of every device where you're logged in — including
|
||||||
this one. Useful if you lost a laptop or shared your password.
|
this one. Useful if you lost a laptop or shared your password.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
|||||||
@ -152,8 +152,8 @@ export function TagManager() {
|
|||||||
<div className="space-y-4 mt-4">
|
<div className="space-y-4 mt-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-white">Tags</h2>
|
<h2 className="text-lg font-semibold text-foreground">Tags</h2>
|
||||||
<p className="text-sm text-slate-400">Organize your contacts with color-coded tags.</p>
|
<p className="text-sm text-muted-foreground">Organize your contacts with color-coded tags.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -169,14 +169,14 @@ export function TagManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tags.length === 0 ? (
|
{tags.length === 0 ? (
|
||||||
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent">
|
<Card className="bg-card border-border ring-0 ring-transparent">
|
||||||
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
<p className="text-slate-400 text-sm">No tags yet.</p>
|
<p className="text-muted-foreground text-sm">No tags yet.</p>
|
||||||
<p className="text-slate-500 text-xs mt-1">Create tags to categorize your contacts.</p>
|
<p className="text-muted-foreground text-xs mt-1">Create tags to categorize your contacts.</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent">
|
<Card className="bg-card border-border ring-0 ring-transparent">
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
@ -196,7 +196,7 @@ export function TagManager() {
|
|||||||
{tag.name}
|
{tag.name}
|
||||||
<button
|
<button
|
||||||
onClick={() => confirmDelete(tag)}
|
onClick={() => confirmDelete(tag)}
|
||||||
className="ml-0.5 rounded-full p-0.5 opacity-0 group-hover:opacity-100 transition-opacity hover:bg-white/10"
|
className="ml-0.5 rounded-full p-0.5 opacity-0 group-hover:opacity-100 transition-opacity hover:bg-muted"
|
||||||
>
|
>
|
||||||
<X className="size-3" />
|
<X className="size-3" />
|
||||||
</button>
|
</button>
|
||||||
@ -209,22 +209,22 @@ export function TagManager() {
|
|||||||
|
|
||||||
{/* New Tag Dialog */}
|
{/* New Tag Dialog */}
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-sm">
|
<DialogContent className="bg-popover border-border sm:max-w-sm">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">New Tag</DialogTitle>
|
<DialogTitle className="text-popover-foreground">New Tag</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
Create a new tag with a name and color.
|
Create a new tag with a name and color.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Tag Name</Label>
|
<Label className="text-muted-foreground">Tag Name</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="e.g. VIP Customer"
|
placeholder="e.g. VIP Customer"
|
||||||
value={newTagName}
|
value={newTagName}
|
||||||
onChange={(e) => setNewTagName(e.target.value)}
|
onChange={(e) => setNewTagName(e.target.value)}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') handleCreate();
|
if (e.key === 'Enter') handleCreate();
|
||||||
}}
|
}}
|
||||||
@ -232,16 +232,16 @@ export function TagManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Color</Label>
|
<Label className="text-muted-foreground">Color</Label>
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{PRESET_COLORS.map((color) => (
|
{PRESET_COLORS.map((color) => (
|
||||||
<button
|
<button
|
||||||
key={color.value}
|
key={color.value}
|
||||||
onClick={() => setSelectedColor(color.value)}
|
onClick={() => setSelectedColor(color.value)}
|
||||||
className="relative size-8 rounded-full transition-transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-900"
|
className="relative size-8 rounded-full transition-transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-popover"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: color.value,
|
backgroundColor: color.value,
|
||||||
boxShadow: selectedColor === color.value ? `0 0 0 2px rgb(15 23 42), 0 0 0 4px ${color.value}` : 'none',
|
boxShadow: selectedColor === color.value ? `0 0 0 2px var(--popover), 0 0 0 4px ${color.value}` : 'none',
|
||||||
}}
|
}}
|
||||||
title={color.name}
|
title={color.name}
|
||||||
/>
|
/>
|
||||||
@ -251,7 +251,7 @@ export function TagManager() {
|
|||||||
|
|
||||||
{/* Preview */}
|
{/* Preview */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Preview</Label>
|
<Label className="text-muted-foreground">Preview</Label>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-medium"
|
className="inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-medium"
|
||||||
@ -271,11 +271,11 @@ export function TagManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="bg-slate-900 border-slate-700">
|
<DialogFooter className="bg-popover border-border">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setDialogOpen(false)}
|
onClick={() => setDialogOpen(false)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@ -299,19 +299,19 @@ export function TagManager() {
|
|||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
{/* Delete Confirmation Dialog */}
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-sm">
|
<DialogContent className="bg-popover border-border sm:max-w-sm">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">Delete Tag</DialogTitle>
|
<DialogTitle className="text-popover-foreground">Delete Tag</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
Are you sure you want to delete the tag "{tagToDelete?.name}"? This will remove
|
Are you sure you want to delete the tag "{tagToDelete?.name}"? This will remove
|
||||||
it from all contacts. This action cannot be undone.
|
it from all contacts. This action cannot be undone.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter className="bg-slate-900 border-slate-700">
|
<DialogFooter className="bg-popover border-border">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setDeleteDialogOpen(false)}
|
onClick={() => setDeleteDialogOpen(false)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -449,8 +449,8 @@ export function TemplateManager() {
|
|||||||
<div className="space-y-4 mt-4">
|
<div className="space-y-4 mt-4">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-white">Message Templates</h2>
|
<h2 className="text-lg font-semibold text-foreground">Message Templates</h2>
|
||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm text-muted-foreground">
|
||||||
Create message templates and submit them to Meta for approval. Use
|
Create message templates and submit them to Meta for approval. Use
|
||||||
"Sync from Meta" to pull templates approved elsewhere.
|
"Sync from Meta" to pull templates approved elsewhere.
|
||||||
</p>
|
</p>
|
||||||
@ -460,7 +460,7 @@ export function TemplateManager() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleSyncFromMeta}
|
onClick={handleSyncFromMeta}
|
||||||
disabled={syncing}
|
disabled={syncing}
|
||||||
className="border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800"
|
className="border-border bg-transparent text-muted-foreground hover:bg-muted"
|
||||||
title="Pull approved templates from your Meta WhatsApp Business Account"
|
title="Pull approved templates from your Meta WhatsApp Business Account"
|
||||||
>
|
>
|
||||||
<RefreshCw className={`size-4 ${syncing ? 'animate-spin' : ''}`} />
|
<RefreshCw className={`size-4 ${syncing ? 'animate-spin' : ''}`} />
|
||||||
@ -477,10 +477,10 @@ export function TemplateManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{templates.length === 0 ? (
|
{templates.length === 0 ? (
|
||||||
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent">
|
<Card className="bg-card border-border ring-0 ring-transparent">
|
||||||
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
<p className="text-slate-400 text-sm">No templates yet.</p>
|
<p className="text-muted-foreground text-sm">No templates yet.</p>
|
||||||
<p className="text-slate-500 text-xs mt-1">
|
<p className="text-muted-foreground text-xs mt-1">
|
||||||
Create your first message template to get started.
|
Create your first message template to get started.
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -493,12 +493,12 @@ export function TemplateManager() {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={template.id}
|
key={template.id}
|
||||||
className="bg-slate-900 border-slate-700 ring-0 ring-transparent"
|
className="bg-card border-border ring-0 ring-transparent"
|
||||||
>
|
>
|
||||||
<CardContent className="flex items-start justify-between pt-4">
|
<CardContent className="flex items-start justify-between pt-4">
|
||||||
<div className="space-y-2 min-w-0 flex-1">
|
<div className="space-y-2 min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<h3 className="font-medium text-white">{template.name}</h3>
|
<h3 className="font-medium text-foreground">{template.name}</h3>
|
||||||
<Badge
|
<Badge
|
||||||
className={`text-xs border ${categoryColors[template.category] || ''}`}
|
className={`text-xs border ${categoryColors[template.category] || ''}`}
|
||||||
>
|
>
|
||||||
@ -508,7 +508,7 @@ export function TemplateManager() {
|
|||||||
{status.label}
|
{status.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
{template.language && (
|
{template.language && (
|
||||||
<span className="text-xs text-slate-500 uppercase">
|
<span className="text-xs text-muted-foreground uppercase">
|
||||||
{template.language}
|
{template.language}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -527,11 +527,11 @@ export function TemplateManager() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-400 line-clamp-2">
|
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||||
{template.body_text}
|
{template.body_text}
|
||||||
</p>
|
</p>
|
||||||
{template.footer_text && (
|
{template.footer_text && (
|
||||||
<p className="text-xs text-slate-500 italic">
|
<p className="text-xs text-muted-foreground italic">
|
||||||
{template.footer_text}
|
{template.footer_text}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -552,7 +552,7 @@ export function TemplateManager() {
|
|||||||
onClick={() => openEdit(template)}
|
onClick={() => openEdit(template)}
|
||||||
title="Editing triggers Meta re-review — status flips to PENDING."
|
title="Editing triggers Meta re-review — status flips to PENDING."
|
||||||
aria-label="Edit template"
|
aria-label="Edit template"
|
||||||
className="text-slate-300 hover:text-primary hover:bg-primary/10 h-8 px-2"
|
className="text-muted-foreground hover:text-primary hover:bg-primary/10 h-8 px-2"
|
||||||
>
|
>
|
||||||
<Pencil className="size-3.5" />
|
<Pencil className="size-3.5" />
|
||||||
Edit
|
Edit
|
||||||
@ -565,7 +565,7 @@ export function TemplateManager() {
|
|||||||
onClick={() => openEdit(template)}
|
onClick={() => openEdit(template)}
|
||||||
title="Edit the template and resubmit to Meta for review."
|
title="Edit the template and resubmit to Meta for review."
|
||||||
aria-label="Edit and resubmit template"
|
aria-label="Edit and resubmit template"
|
||||||
className="text-slate-300 hover:text-primary hover:bg-primary/10 h-8 px-2"
|
className="text-muted-foreground hover:text-primary hover:bg-primary/10 h-8 px-2"
|
||||||
>
|
>
|
||||||
<RotateCcw className="size-3.5" />
|
<RotateCcw className="size-3.5" />
|
||||||
Resubmit
|
Resubmit
|
||||||
@ -586,7 +586,7 @@ export function TemplateManager() {
|
|||||||
? 'Delete from Meta and locally'
|
? 'Delete from Meta and locally'
|
||||||
: 'Delete locally'
|
: 'Delete locally'
|
||||||
}
|
}
|
||||||
className="text-slate-400 hover:text-red-400 hover:bg-red-950/30 h-8 w-8"
|
className="text-muted-foreground hover:text-red-400 hover:bg-red-950/30 h-8 w-8"
|
||||||
>
|
>
|
||||||
{deletingId === template.id ? (
|
{deletingId === template.id ? (
|
||||||
<Loader2 className="size-4 animate-spin" />
|
<Loader2 className="size-4 animate-spin" />
|
||||||
@ -612,12 +612,12 @@ export function TemplateManager() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="bg-popover border-border sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">
|
<DialogTitle className="text-popover-foreground">
|
||||||
{editingId ? 'Edit Message Template' : 'New Message Template'}
|
{editingId ? 'Edit Message Template' : 'New Message Template'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
{editingId
|
{editingId
|
||||||
? 'Save your changes to re-submit to Meta. Status will flip back to PENDING during review.'
|
? 'Save your changes to re-submit to Meta. Status will flip back to PENDING during review.'
|
||||||
: 'Build a template and submit it to Meta for approval. Once approved, you can use it in broadcasts and the inbox.'}
|
: 'Build a template and submit it to Meta for approval. Once approved, you can use it in broadcasts and the inbox.'}
|
||||||
@ -638,15 +638,15 @@ export function TemplateManager() {
|
|||||||
|
|
||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Template Name</Label>
|
<Label className="text-muted-foreground">Template Name</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="e.g. order_confirmation"
|
placeholder="e.g. order_confirmation"
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
disabled={editingId !== null}
|
disabled={editingId !== null}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 disabled:opacity-60 disabled:cursor-not-allowed"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-slate-500">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
{editingId
|
{editingId
|
||||||
? 'Name is fixed once a template exists on Meta — create a new template to change it.'
|
? 'Name is fixed once a template exists on Meta — create a new template to change it.'
|
||||||
: 'Lowercase letters, digits, and underscores only.'}
|
: 'Lowercase letters, digits, and underscores only.'}
|
||||||
@ -655,7 +655,7 @@ export function TemplateManager() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Category</Label>
|
<Label className="text-muted-foreground">Category</Label>
|
||||||
<Select
|
<Select
|
||||||
value={form.category}
|
value={form.category}
|
||||||
onValueChange={(val) =>
|
onValueChange={(val) =>
|
||||||
@ -665,15 +665,15 @@ export function TemplateManager() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full bg-slate-800 border-slate-700 text-white">
|
<SelectTrigger className="w-full bg-muted border-border text-foreground">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="bg-slate-800 border-slate-700">
|
<SelectContent className="bg-popover border-border">
|
||||||
{CATEGORIES.map((cat) => (
|
{CATEGORIES.map((cat) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={cat}
|
key={cat}
|
||||||
value={cat}
|
value={cat}
|
||||||
className="text-white focus:bg-slate-700 focus:text-white"
|
className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
|
||||||
>
|
>
|
||||||
{cat}
|
{cat}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@ -683,7 +683,7 @@ export function TemplateManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Language</Label>
|
<Label className="text-muted-foreground">Language</Label>
|
||||||
<Input
|
<Input
|
||||||
list="template-language-codes"
|
list="template-language-codes"
|
||||||
placeholder="en_US"
|
placeholder="en_US"
|
||||||
@ -692,14 +692,14 @@ export function TemplateManager() {
|
|||||||
setForm({ ...form, language: e.target.value })
|
setForm({ ...form, language: e.target.value })
|
||||||
}
|
}
|
||||||
disabled={editingId !== null}
|
disabled={editingId !== null}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 disabled:opacity-60 disabled:cursor-not-allowed"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
/>
|
/>
|
||||||
<datalist id="template-language-codes">
|
<datalist id="template-language-codes">
|
||||||
{COMMON_LANGUAGE_CODES.map((code) => (
|
{COMMON_LANGUAGE_CODES.map((code) => (
|
||||||
<option key={code} value={code} />
|
<option key={code} value={code} />
|
||||||
))}
|
))}
|
||||||
</datalist>
|
</datalist>
|
||||||
<p className="text-[11px] text-slate-500">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
{editingId
|
{editingId
|
||||||
? 'Language is fixed once a template exists on Meta.'
|
? 'Language is fixed once a template exists on Meta.'
|
||||||
: (
|
: (
|
||||||
@ -713,7 +713,7 @@ export function TemplateManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Header</Label>
|
<Label className="text-muted-foreground">Header</Label>
|
||||||
<Select
|
<Select
|
||||||
value={form.header_format}
|
value={form.header_format}
|
||||||
onValueChange={(val) =>
|
onValueChange={(val) =>
|
||||||
@ -729,15 +729,15 @@ export function TemplateManager() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full bg-slate-800 border-slate-700 text-white">
|
<SelectTrigger className="w-full bg-muted border-border text-foreground">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="bg-slate-800 border-slate-700">
|
<SelectContent className="bg-popover border-border">
|
||||||
{HEADER_FORMATS.map((type) => (
|
{HEADER_FORMATS.map((type) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={type}
|
key={type}
|
||||||
value={type}
|
value={type}
|
||||||
className="text-white focus:bg-slate-700 focus:text-white"
|
className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
|
||||||
>
|
>
|
||||||
{type === 'none'
|
{type === 'none'
|
||||||
? 'None'
|
? 'None'
|
||||||
@ -758,7 +758,7 @@ export function TemplateManager() {
|
|||||||
setForm({ ...form, header_content: e.target.value })
|
setForm({ ...form, header_content: e.target.value })
|
||||||
}
|
}
|
||||||
maxLength={TEMPLATE_LIMITS.headerTextMaxLength}
|
maxLength={TEMPLATE_LIMITS.headerTextMaxLength}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
{headerVarCount > 0 && (
|
{headerVarCount > 0 && (
|
||||||
<Input
|
<Input
|
||||||
@ -769,7 +769,7 @@ export function TemplateManager() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm({ ...form, header_sample: e.target.value })
|
setForm({ ...form, header_sample: e.target.value })
|
||||||
}
|
}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -783,9 +783,9 @@ export function TemplateManager() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm({ ...form, header_media_url: e.target.value })
|
setForm({ ...form, header_media_url: e.target.value })
|
||||||
}
|
}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-slate-500 leading-relaxed">
|
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||||
Must be publicly accessible HTTPS. Meta fetches it once
|
Must be publicly accessible HTTPS. Meta fetches it once
|
||||||
during review, so the file needs to stay live for ~24 hrs.
|
during review, so the file needs to stay live for ~24 hrs.
|
||||||
{form.header_format === 'image' &&
|
{form.header_format === 'image' &&
|
||||||
@ -801,7 +801,7 @@ export function TemplateManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Body Text</Label>
|
<Label className="text-muted-foreground">Body Text</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Hello {{1}}, your order {{2}} is confirmed."
|
placeholder="Hello {{1}}, your order {{2}} is confirmed."
|
||||||
value={form.body_text}
|
value={form.body_text}
|
||||||
@ -810,16 +810,16 @@ export function TemplateManager() {
|
|||||||
}
|
}
|
||||||
rows={4}
|
rows={4}
|
||||||
maxLength={TEMPLATE_LIMITS.bodyMaxLength}
|
maxLength={TEMPLATE_LIMITS.bodyMaxLength}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 resize-none"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground resize-none"
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-slate-500">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
Use {`{{1}}`}, {`{{2}}`} for variables (must be contiguous
|
Use {`{{1}}`}, {`{{2}}`} for variables (must be contiguous
|
||||||
starting at {`{{1}}`}).
|
starting at {`{{1}}`}).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{bodyVarCount > 0 && (
|
{bodyVarCount > 0 && (
|
||||||
<div className="space-y-1.5 pt-1">
|
<div className="space-y-1.5 pt-1">
|
||||||
<Label className="text-[11px] text-slate-400">
|
<Label className="text-[11px] text-muted-foreground">
|
||||||
Sample values (Meta uses these to review your template)
|
Sample values (Meta uses these to review your template)
|
||||||
</Label>
|
</Label>
|
||||||
{form.body_samples.map((val, i) => {
|
{form.body_samples.map((val, i) => {
|
||||||
@ -836,7 +836,7 @@ export function TemplateManager() {
|
|||||||
next[i] = e.target.value;
|
next[i] = e.target.value;
|
||||||
setForm({ ...form, body_samples: next });
|
setForm({ ...form, body_samples: next });
|
||||||
}}
|
}}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -845,7 +845,7 @@ export function TemplateManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Footer (optional)</Label>
|
<Label className="text-muted-foreground">Footer (optional)</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Optional footer text (max 60 chars)"
|
placeholder="Optional footer text (max 60 chars)"
|
||||||
value={form.footer_text}
|
value={form.footer_text}
|
||||||
@ -853,27 +853,27 @@ export function TemplateManager() {
|
|||||||
setForm({ ...form, footer_text: e.target.value })
|
setForm({ ...form, footer_text: e.target.value })
|
||||||
}
|
}
|
||||||
maxLength={TEMPLATE_LIMITS.footerMaxLength}
|
maxLength={TEMPLATE_LIMITS.footerMaxLength}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-slate-300">Buttons (optional)</Label>
|
<Label className="text-muted-foreground">Buttons (optional)</Label>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={addButton}
|
onClick={addButton}
|
||||||
disabled={form.buttons.length >= TEMPLATE_LIMITS.maxButtonsTotal}
|
disabled={form.buttons.length >= TEMPLATE_LIMITS.maxButtonsTotal}
|
||||||
className="border-slate-700 bg-transparent text-slate-300 hover:bg-slate-800 h-7 text-xs"
|
className="border-border bg-transparent text-muted-foreground hover:bg-muted h-7 text-xs"
|
||||||
>
|
>
|
||||||
<Plus className="size-3" />
|
<Plus className="size-3" />
|
||||||
Add Button
|
Add Button
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{form.buttons.length === 0 ? (
|
{form.buttons.length === 0 ? (
|
||||||
<p className="text-[11px] text-slate-500">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
Up to {TEMPLATE_LIMITS.maxButtonsTotal} buttons. QUICK_REPLY
|
Up to {TEMPLATE_LIMITS.maxButtonsTotal} buttons. QUICK_REPLY
|
||||||
buttons must come before URL / phone / copy-code buttons.
|
buttons must come before URL / phone / copy-code buttons.
|
||||||
</p>
|
</p>
|
||||||
@ -882,7 +882,7 @@ export function TemplateManager() {
|
|||||||
{form.buttons.map((btn, i) => (
|
{form.buttons.map((btn, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="space-y-2 rounded border border-slate-700 bg-slate-800/50 p-2"
|
className="space-y-2 rounded border border-border bg-muted/50 p-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Select
|
<Select
|
||||||
@ -895,31 +895,31 @@ export function TemplateManager() {
|
|||||||
changeButtonType(i, val as TemplateButton['type']);
|
changeButtonType(i, val as TemplateButton['type']);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-40 bg-slate-800 border-slate-700 text-white h-8 text-xs">
|
<SelectTrigger className="w-40 bg-muted border-border text-foreground h-8 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="bg-slate-800 border-slate-700">
|
<SelectContent className="bg-popover border-border">
|
||||||
<SelectItem
|
<SelectItem
|
||||||
value="QUICK_REPLY"
|
value="QUICK_REPLY"
|
||||||
className="text-white focus:bg-slate-700 focus:text-white"
|
className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
|
||||||
>
|
>
|
||||||
Quick Reply
|
Quick Reply
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem
|
<SelectItem
|
||||||
value="URL"
|
value="URL"
|
||||||
className="text-white focus:bg-slate-700 focus:text-white"
|
className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
|
||||||
>
|
>
|
||||||
URL
|
URL
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem
|
<SelectItem
|
||||||
value="PHONE_NUMBER"
|
value="PHONE_NUMBER"
|
||||||
className="text-white focus:bg-slate-700 focus:text-white"
|
className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
|
||||||
>
|
>
|
||||||
Phone
|
Phone
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem
|
<SelectItem
|
||||||
value="COPY_CODE"
|
value="COPY_CODE"
|
||||||
className="text-white focus:bg-slate-700 focus:text-white"
|
className="text-popover-foreground focus:bg-muted focus:text-popover-foreground"
|
||||||
>
|
>
|
||||||
Copy Code
|
Copy Code
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@ -932,14 +932,14 @@ export function TemplateManager() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateButton(i, { text: e.target.value })
|
updateButton(i, { text: e.target.value })
|
||||||
}
|
}
|
||||||
className="flex-1 bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 h-8 text-xs"
|
className="flex-1 bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => removeButton(i)}
|
onClick={() => removeButton(i)}
|
||||||
className="text-slate-400 hover:text-red-400 hover:bg-red-950/30 size-7"
|
className="text-muted-foreground hover:text-red-400 hover:bg-red-950/30 size-7"
|
||||||
>
|
>
|
||||||
<X className="size-3.5" />
|
<X className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -952,7 +952,7 @@ export function TemplateManager() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateButton(i, { url: e.target.value })
|
updateButton(i, { url: e.target.value })
|
||||||
}
|
}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 h-8 text-xs"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
{extractVariableIndices(btn.url).length > 0 && (
|
{extractVariableIndices(btn.url).length > 0 && (
|
||||||
<Input
|
<Input
|
||||||
@ -961,7 +961,7 @@ export function TemplateManager() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateButton(i, { example: e.target.value })
|
updateButton(i, { example: e.target.value })
|
||||||
}
|
}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 h-8 text-xs"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -973,7 +973,7 @@ export function TemplateManager() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateButton(i, { phone_number: e.target.value })
|
updateButton(i, { phone_number: e.target.value })
|
||||||
}
|
}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 h-8 text-xs"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{btn.type === 'COPY_CODE' && (
|
{btn.type === 'COPY_CODE' && (
|
||||||
@ -983,7 +983,7 @@ export function TemplateManager() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateButton(i, { example: e.target.value })
|
updateButton(i, { example: e.target.value })
|
||||||
}
|
}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 h-8 text-xs"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -993,11 +993,11 @@ export function TemplateManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="bg-slate-900 border-slate-700">
|
<DialogFooter className="bg-popover border-border">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setDialogOpen(false)}
|
onClick={() => setDialogOpen(false)}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@ -1030,21 +1030,21 @@ export function TemplateManager() {
|
|||||||
if (!open) setTemplateToDelete(null);
|
if (!open) setTemplateToDelete(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="bg-slate-900 border-slate-700 sm:max-w-sm">
|
<DialogContent className="bg-popover border-border sm:max-w-sm">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">Delete template?</DialogTitle>
|
<DialogTitle className="text-popover-foreground">Delete template?</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-muted-foreground">
|
||||||
{templateToDelete?.meta_template_id
|
{templateToDelete?.meta_template_id
|
||||||
? `"${templateToDelete?.name}" will be deleted from Meta and from wacrm. Active broadcasts using this template will start failing on their next send. This can't be undone.`
|
? `"${templateToDelete?.name}" will be deleted from Meta and from wacrm. Active broadcasts using this template will start failing on their next send. This can't be undone.`
|
||||||
: `"${templateToDelete?.name}" will be deleted from wacrm. It was never submitted to Meta, so no remote cleanup is needed.`}
|
: `"${templateToDelete?.name}" will be deleted from wacrm. It was never submitted to Meta, so no remote cleanup is needed.`}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter className="bg-slate-900 border-slate-700">
|
<DialogFooter className="bg-popover border-border">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setTemplateToDelete(null)}
|
onClick={() => setTemplateToDelete(null)}
|
||||||
disabled={deletingId !== null}
|
disabled={deletingId !== null}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -408,18 +408,18 @@ export function WhatsAppConfig() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Connection Status */}
|
{/* Connection Status */}
|
||||||
<Alert className="bg-slate-900 border-slate-700">
|
<Alert className="bg-card border-border">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{connectionStatus === 'connected' ? (
|
{connectionStatus === 'connected' ? (
|
||||||
<CheckCircle2 className="size-4 text-primary" />
|
<CheckCircle2 className="size-4 text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<XCircle className="size-4 text-red-500" />
|
<XCircle className="size-4 text-red-500" />
|
||||||
)}
|
)}
|
||||||
<AlertTitle className="text-white mb-0">
|
<AlertTitle className="text-foreground mb-0">
|
||||||
{connectionStatus === 'connected' ? 'Credentials valid' : 'Not Connected'}
|
{connectionStatus === 'connected' ? 'Credentials valid' : 'Not Connected'}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
</div>
|
</div>
|
||||||
<AlertDescription className="text-slate-400">
|
<AlertDescription className="text-muted-foreground">
|
||||||
{connectionStatus === 'connected'
|
{connectionStatus === 'connected'
|
||||||
? 'Your access token authenticates with Meta. See Registration status below for whether webhooks are actually wired.'
|
? 'Your access token authenticates with Meta. See Registration status below for whether webhooks are actually wired.'
|
||||||
: statusMessage ||
|
: statusMessage ||
|
||||||
@ -462,7 +462,7 @@ export function WhatsAppConfig() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleVerifyRegistration}
|
onClick={handleVerifyRegistration}
|
||||||
disabled={verifyingRegistration}
|
disabled={verifyingRegistration}
|
||||||
className="border-slate-700 bg-transparent text-slate-200 hover:bg-slate-800 h-7"
|
className="border-border bg-transparent text-foreground hover:bg-muted h-7"
|
||||||
>
|
>
|
||||||
{verifyingRegistration ? (
|
{verifyingRegistration ? (
|
||||||
<Loader2 className="size-3.5 animate-spin" />
|
<Loader2 className="size-3.5 animate-spin" />
|
||||||
@ -472,7 +472,7 @@ export function WhatsAppConfig() {
|
|||||||
Verify with Meta
|
Verify with Meta
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<AlertDescription className="text-slate-400 mt-2 text-xs leading-relaxed">
|
<AlertDescription className="text-muted-foreground mt-2 text-xs leading-relaxed">
|
||||||
{isRegistered ? (
|
{isRegistered ? (
|
||||||
<>
|
<>
|
||||||
Subscribed since{' '}
|
Subscribed since{' '}
|
||||||
@ -502,14 +502,14 @@ export function WhatsAppConfig() {
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
|
||||||
{registrationProbe && (
|
{registrationProbe && (
|
||||||
<div className="mt-3 rounded border border-slate-700 bg-slate-900/60 px-3 py-2 space-y-1.5 text-[11px]">
|
<div className="mt-3 rounded border border-border bg-card/60 px-3 py-2 space-y-1.5 text-[11px]">
|
||||||
<p className="font-medium text-slate-200">
|
<p className="font-medium text-foreground">
|
||||||
Diagnostic — last run: {' '}
|
Diagnostic — last run: {' '}
|
||||||
<span className={registrationProbe.live ? 'text-emerald-400' : 'text-amber-400'}>
|
<span className={registrationProbe.live ? 'text-emerald-400' : 'text-amber-400'}>
|
||||||
{registrationProbe.live ? 'live' : 'not live'}
|
{registrationProbe.live ? 'live' : 'not live'}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<ul className="space-y-0.5 text-slate-400">
|
<ul className="space-y-0.5 text-muted-foreground">
|
||||||
{Object.entries(registrationProbe.checks).map(([k, v]) => (
|
{Object.entries(registrationProbe.checks).map(([k, v]) => (
|
||||||
<li key={k} className="flex items-center gap-1.5">
|
<li key={k} className="flex items-center gap-1.5">
|
||||||
{v === true ? (
|
{v === true ? (
|
||||||
@ -517,9 +517,9 @@ export function WhatsAppConfig() {
|
|||||||
) : v === false ? (
|
) : v === false ? (
|
||||||
<XCircle className="size-3 text-red-400 shrink-0" />
|
<XCircle className="size-3 text-red-400 shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<span className="size-3 rounded-full border border-slate-600 shrink-0" />
|
<span className="size-3 rounded-full border border-border shrink-0" />
|
||||||
)}
|
)}
|
||||||
<code className="text-slate-300">{k}</code>
|
<code className="text-muted-foreground">{k}</code>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -536,36 +536,36 @@ export function WhatsAppConfig() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* API Credentials */}
|
{/* API Credentials */}
|
||||||
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent">
|
<Card className="bg-card border-border ring-0 ring-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-white">API Credentials</CardTitle>
|
<CardTitle className="text-foreground">API Credentials</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
Enter your Meta WhatsApp Business API credentials.
|
Enter your Meta WhatsApp Business API credentials.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Phone Number ID</Label>
|
<Label className="text-muted-foreground">Phone Number ID</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="e.g. 100234567890123"
|
placeholder="e.g. 100234567890123"
|
||||||
value={phoneNumberId}
|
value={phoneNumberId}
|
||||||
onChange={(e) => setPhoneNumberId(e.target.value)}
|
onChange={(e) => setPhoneNumberId(e.target.value)}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">WhatsApp Business Account ID</Label>
|
<Label className="text-muted-foreground">WhatsApp Business Account ID</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="e.g. 100234567890456"
|
placeholder="e.g. 100234567890456"
|
||||||
value={wabaId}
|
value={wabaId}
|
||||||
onChange={(e) => setWabaId(e.target.value)}
|
onChange={(e) => setWabaId(e.target.value)}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Permanent Access Token</Label>
|
<Label className="text-muted-foreground">Permanent Access Token</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={showToken ? 'text' : 'password'}
|
type={showToken ? 'text' : 'password'}
|
||||||
@ -581,40 +581,40 @@ export function WhatsAppConfig() {
|
|||||||
setTokenEdited(true);
|
setTokenEdited(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 pr-10"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground pr-10"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowToken(!showToken)}
|
onClick={() => setShowToken(!showToken)}
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white transition-colors"
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
{showToken ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
{showToken ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{config && !tokenEdited && (
|
{config && !tokenEdited && (
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
Token is hidden for security. Re-enter it to update configuration.
|
Token is hidden for security. Re-enter it to update configuration.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Webhook Verify Token</Label>
|
<Label className="text-muted-foreground">Webhook Verify Token</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Create a custom verify token"
|
placeholder="Create a custom verify token"
|
||||||
value={verifyToken}
|
value={verifyToken}
|
||||||
onChange={(e) => setVerifyToken(e.target.value)}
|
onChange={(e) => setVerifyToken(e.target.value)}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
A custom string you create. Must match the token you set in Meta webhook settings.
|
A custom string you create. Must match the token you set in Meta webhook settings.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">
|
<Label className="text-muted-foreground">
|
||||||
Two-step verification PIN
|
Two-step verification PIN
|
||||||
<span className="ml-1 text-slate-500">(optional)</span>
|
<span className="ml-1 text-muted-foreground">(optional)</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
@ -625,12 +625,12 @@ export function WhatsAppConfig() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setPin(e.target.value.replace(/\D/g, '').slice(0, 6))
|
setPin(e.target.value.replace(/\D/g, '').slice(0, 6))
|
||||||
}
|
}
|
||||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 tracking-widest"
|
className="bg-muted border-border text-foreground placeholder:text-muted-foreground tracking-widest"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500 leading-relaxed">
|
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||||
Needed only to wire <strong className="text-slate-300">inbound</strong> messages
|
Needed only to wire <strong className="text-muted-foreground">inbound</strong> messages
|
||||||
for a <strong className="text-slate-300">production</strong> number. Set it in{' '}
|
for a <strong className="text-muted-foreground">production</strong> number. Set it in{' '}
|
||||||
<strong className="text-slate-300">
|
<strong className="text-muted-foreground">
|
||||||
Meta Business Manager → WhatsApp Accounts → Phone
|
Meta Business Manager → WhatsApp Accounts → Phone
|
||||||
Numbers → Two-step verification
|
Numbers → Two-step verification
|
||||||
</strong>
|
</strong>
|
||||||
@ -638,7 +638,7 @@ export function WhatsAppConfig() {
|
|||||||
otherwise Meta routes inbound events to whichever app
|
otherwise Meta routes inbound events to whichever app
|
||||||
last claimed it (the symptom that hits second numbers
|
last claimed it (the symptom that hits second numbers
|
||||||
under a shared WABA).{' '}
|
under a shared WABA).{' '}
|
||||||
<strong className="text-slate-300">Meta test numbers</strong> have no
|
<strong className="text-muted-foreground">Meta test numbers</strong> have no
|
||||||
PIN and are pre-registered — leave this blank for them.
|
PIN and are pre-registered — leave this blank for them.
|
||||||
Leaving it blank also keeps an existing registration
|
Leaving it blank also keeps an existing registration
|
||||||
untouched.
|
untouched.
|
||||||
@ -648,27 +648,27 @@ export function WhatsAppConfig() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Webhook URL */}
|
{/* Webhook URL */}
|
||||||
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent">
|
<Card className="bg-card border-border ring-0 ring-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-white">Webhook Configuration</CardTitle>
|
<CardTitle className="text-foreground">Webhook Configuration</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
Use this URL as your webhook callback in the Meta App Dashboard.
|
Use this URL as your webhook callback in the Meta App Dashboard.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-slate-300">Webhook Callback URL</Label>
|
<Label className="text-muted-foreground">Webhook Callback URL</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
readOnly
|
readOnly
|
||||||
value={webhookUrl}
|
value={webhookUrl}
|
||||||
className="bg-slate-800 border-slate-700 text-slate-300 font-mono text-sm"
|
className="bg-muted border-border text-muted-foreground font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleCopyWebhookUrl}
|
onClick={handleCopyWebhookUrl}
|
||||||
className="shrink-0 border-slate-700 text-slate-300 hover:text-white hover:bg-slate-800"
|
className="shrink-0 border-border text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
<Copy className="size-4" />
|
<Copy className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -697,7 +697,7 @@ export function WhatsAppConfig() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleTestConnection}
|
onClick={handleTestConnection}
|
||||||
disabled={testing || !config}
|
disabled={testing || !config}
|
||||||
className="border-slate-700 text-slate-300 hover:text-white hover:bg-slate-800"
|
className="border-border text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||||
>
|
>
|
||||||
{testing ? (
|
{testing ? (
|
||||||
<>
|
<>
|
||||||
@ -736,23 +736,23 @@ export function WhatsAppConfig() {
|
|||||||
|
|
||||||
{/* Setup Instructions Sidebar */}
|
{/* Setup Instructions Sidebar */}
|
||||||
<div>
|
<div>
|
||||||
<Card className="bg-slate-900 border-slate-700 ring-0 ring-transparent">
|
<Card className="bg-card border-border ring-0 ring-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-white text-base">Setup Instructions</CardTitle>
|
<CardTitle className="text-foreground text-base">Setup Instructions</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-muted-foreground">
|
||||||
Follow these steps to connect your WhatsApp Business API.
|
Follow these steps to connect your WhatsApp Business API.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<AccordionItem className="border-slate-700">
|
<AccordionItem className="border-border">
|
||||||
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
|
<AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">1</span>
|
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">1</span>
|
||||||
Create a Meta App
|
Create a Meta App
|
||||||
</span>
|
</span>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="text-slate-400">
|
<AccordionContent className="text-muted-foreground">
|
||||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||||
<li>Go to <span className="text-primary">developers.facebook.com</span></li>
|
<li>Go to <span className="text-primary">developers.facebook.com</span></li>
|
||||||
<li>Click "My Apps" and then "Create App"</li>
|
<li>Click "My Apps" and then "Create App"</li>
|
||||||
@ -762,14 +762,14 @@ export function WhatsAppConfig() {
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem className="border-slate-700">
|
<AccordionItem className="border-border">
|
||||||
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
|
<AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">2</span>
|
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">2</span>
|
||||||
Add WhatsApp Product
|
Add WhatsApp Product
|
||||||
</span>
|
</span>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="text-slate-400">
|
<AccordionContent className="text-muted-foreground">
|
||||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||||
<li>In your app dashboard, click "Add Product"</li>
|
<li>In your app dashboard, click "Add Product"</li>
|
||||||
<li>Find "WhatsApp" and click "Set Up"</li>
|
<li>Find "WhatsApp" and click "Set Up"</li>
|
||||||
@ -778,43 +778,43 @@ export function WhatsAppConfig() {
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem className="border-slate-700">
|
<AccordionItem className="border-border">
|
||||||
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
|
<AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">3</span>
|
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">3</span>
|
||||||
Get API Credentials
|
Get API Credentials
|
||||||
</span>
|
</span>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="text-slate-400">
|
<AccordionContent className="text-muted-foreground">
|
||||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||||
<li>Go to WhatsApp > API Setup</li>
|
<li>Go to WhatsApp > API Setup</li>
|
||||||
<li>Copy your <strong className="text-slate-200">Phone Number ID</strong></li>
|
<li>Copy your <strong className="text-foreground">Phone Number ID</strong></li>
|
||||||
<li>Copy your <strong className="text-slate-200">WhatsApp Business Account ID</strong></li>
|
<li>Copy your <strong className="text-foreground">WhatsApp Business Account ID</strong></li>
|
||||||
<li>Generate a <strong className="text-slate-200">Permanent Access Token</strong> from Business Settings > System Users</li>
|
<li>Generate a <strong className="text-foreground">Permanent Access Token</strong> from Business Settings > System Users</li>
|
||||||
</ol>
|
</ol>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem className="border-slate-700">
|
<AccordionItem className="border-border">
|
||||||
<AccordionTrigger className="text-slate-300 hover:text-white hover:no-underline">
|
<AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">4</span>
|
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">4</span>
|
||||||
Configure Webhooks
|
Configure Webhooks
|
||||||
</span>
|
</span>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="text-slate-400">
|
<AccordionContent className="text-muted-foreground">
|
||||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||||
<li>Go to WhatsApp > Configuration</li>
|
<li>Go to WhatsApp > Configuration</li>
|
||||||
<li>Click "Edit" on the Webhook section</li>
|
<li>Click "Edit" on the Webhook section</li>
|
||||||
<li>Paste the <strong className="text-slate-200">Webhook Callback URL</strong> from above</li>
|
<li>Paste the <strong className="text-foreground">Webhook Callback URL</strong> from above</li>
|
||||||
<li>Enter the same <strong className="text-slate-200">Verify Token</strong> you set here</li>
|
<li>Enter the same <strong className="text-foreground">Verify Token</strong> you set here</li>
|
||||||
<li>Subscribe to "messages" webhook field</li>
|
<li>Subscribe to "messages" webhook field</li>
|
||||||
</ol>
|
</ol>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<div className="mt-4 pt-4 border-t border-slate-700">
|
<div className="mt-4 pt-4 border-t border-border">
|
||||||
<a
|
<a
|
||||||
href="https://developers.facebook.com/docs/whatsapp/cloud-api/get-started"
|
href="https://developers.facebook.com/docs/whatsapp/cloud-api/get-started"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
51
src/components/themed-toaster.tsx
Normal file
51
src/components/themed-toaster.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSyncExternalStore } from "react";
|
||||||
|
import { Toaster } from "sonner";
|
||||||
|
|
||||||
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
|
import { DEFAULT_MODE } from "@/lib/themes";
|
||||||
|
|
||||||
|
// Returns false during SSR and the first hydration render, true after —
|
||||||
|
// the sanctioned (warning-free, no setState-in-effect) way to diverge
|
||||||
|
// server vs client. Lets us match the server-rendered default on first
|
||||||
|
// paint, then adopt the real mode.
|
||||||
|
const noopSubscribe = () => () => {};
|
||||||
|
function useIsClient() {
|
||||||
|
return useSyncExternalStore(
|
||||||
|
noopSubscribe,
|
||||||
|
() => true,
|
||||||
|
() => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* The theme is gated behind `useIsClient`: the server renders
|
||||||
|
* DEFAULT_MODE, so first client paint must too, otherwise a light-mode
|
||||||
|
* user hydrates with a different sonner `theme` attribute than the
|
||||||
|
* server emitted and React logs a hydration mismatch.
|
||||||
|
*/
|
||||||
|
export function ThemedToaster() {
|
||||||
|
const { mode } = useTheme();
|
||||||
|
const isClient = useIsClient();
|
||||||
|
return (
|
||||||
|
<Toaster
|
||||||
|
theme={isClient ? mode : DEFAULT_MODE}
|
||||||
|
position="top-right"
|
||||||
|
toastOptions={{
|
||||||
|
style: {
|
||||||
|
background: "var(--popover)",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
color: "var(--popover-foreground)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -128,7 +128,7 @@ const LegendItem = ({
|
|||||||
className={cx(
|
className={cx(
|
||||||
"group inline-flex flex-nowrap items-center gap-1.5 rounded-sm px-2 py-1 whitespace-nowrap transition",
|
"group inline-flex flex-nowrap items-center gap-1.5 rounded-sm px-2 py-1 whitespace-nowrap transition",
|
||||||
hasOnValueChange
|
hasOnValueChange
|
||||||
? "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
? "cursor-pointer hover:bg-muted"
|
||||||
: "cursor-default",
|
: "cursor-default",
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -147,9 +147,9 @@ const LegendItem = ({
|
|||||||
<p
|
<p
|
||||||
className={cx(
|
className={cx(
|
||||||
"truncate text-xs whitespace-nowrap",
|
"truncate text-xs whitespace-nowrap",
|
||||||
"text-gray-700 dark:text-gray-300",
|
"text-muted-foreground",
|
||||||
hasOnValueChange &&
|
hasOnValueChange &&
|
||||||
"group-hover:text-gray-900 dark:group-hover:text-gray-50",
|
"group-hover:text-foreground",
|
||||||
activeLegend && activeLegend !== name ? "opacity-40" : "opacity-100",
|
activeLegend && activeLegend !== name ? "opacity-40" : "opacity-100",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -194,8 +194,8 @@ const ScrollButton = ({ icon, onClick, disabled }: ScrollButtonProps) => {
|
|||||||
className={cx(
|
className={cx(
|
||||||
"group inline-flex size-5 items-center truncate rounded-sm transition",
|
"group inline-flex size-5 items-center truncate rounded-sm transition",
|
||||||
disabled
|
disabled
|
||||||
? "cursor-not-allowed text-gray-400 dark:text-gray-600"
|
? "cursor-not-allowed text-muted-foreground"
|
||||||
: "cursor-pointer text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-50",
|
: "cursor-pointer text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -358,7 +358,7 @@ const Legend = React.forwardRef<HTMLOListElement, LegendProps>((props, ref) => {
|
|||||||
ref={scrollButtonsRef}
|
ref={scrollButtonsRef}
|
||||||
className={cx(
|
className={cx(
|
||||||
"absolute top-0 right-0 bottom-0 flex h-full items-center justify-center pr-1",
|
"absolute top-0 right-0 bottom-0 flex h-full items-center justify-center pr-1",
|
||||||
"bg-white dark:bg-gray-950",
|
"bg-background",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ScrollButton
|
<ScrollButton
|
||||||
@ -470,13 +470,13 @@ const ChartTooltip = ({
|
|||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"rounded-md border text-sm shadow-md",
|
"rounded-md border text-sm shadow-md",
|
||||||
"border-gray-200 dark:border-gray-800",
|
"border-border",
|
||||||
"bg-white dark:bg-gray-950",
|
"bg-popover",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={cx("border-b border-inherit px-4 py-2")}>
|
<div className={cx("border-b border-inherit px-4 py-2")}>
|
||||||
<p
|
<p
|
||||||
className={cx("font-medium", "text-gray-900 dark:text-gray-50")}
|
className={cx("font-medium", "text-popover-foreground")}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
@ -498,7 +498,7 @@ const ChartTooltip = ({
|
|||||||
<p
|
<p
|
||||||
className={cx(
|
className={cx(
|
||||||
"text-right whitespace-nowrap",
|
"text-right whitespace-nowrap",
|
||||||
"text-gray-700 dark:text-gray-300",
|
"text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{category}
|
{category}
|
||||||
@ -507,7 +507,7 @@ const ChartTooltip = ({
|
|||||||
<p
|
<p
|
||||||
className={cx(
|
className={cx(
|
||||||
"text-right font-medium whitespace-nowrap tabular-nums",
|
"text-right font-medium whitespace-nowrap tabular-nums",
|
||||||
"text-gray-900 dark:text-gray-50",
|
"text-popover-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{valueFormatter(value)}
|
{valueFormatter(value)}
|
||||||
@ -685,7 +685,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
|
|||||||
>
|
>
|
||||||
{showGridLines ? (
|
{showGridLines ? (
|
||||||
<CartesianGrid
|
<CartesianGrid
|
||||||
className={cx("stroke-gray-200 stroke-1 dark:stroke-gray-800")}
|
className={cx("stroke-border stroke-1")}
|
||||||
horizontal={layout !== "vertical"}
|
horizontal={layout !== "vertical"}
|
||||||
vertical={layout === "vertical"}
|
vertical={layout === "vertical"}
|
||||||
/>
|
/>
|
||||||
@ -700,7 +700,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
|
|||||||
stroke=""
|
stroke=""
|
||||||
className={cx(
|
className={cx(
|
||||||
"text-xs",
|
"text-xs",
|
||||||
"fill-gray-500 dark:fill-gray-500",
|
"fill-muted-foreground",
|
||||||
{ "mt-4": layout !== "vertical" },
|
{ "mt-4": layout !== "vertical" },
|
||||||
)}
|
)}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
@ -730,7 +730,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
|
|||||||
<Label
|
<Label
|
||||||
position="insideBottom"
|
position="insideBottom"
|
||||||
offset={-20}
|
offset={-20}
|
||||||
className="fill-gray-800 text-sm font-medium dark:fill-gray-200"
|
className="fill-foreground text-sm font-medium"
|
||||||
>
|
>
|
||||||
{xAxisLabel}
|
{xAxisLabel}
|
||||||
</Label>
|
</Label>
|
||||||
@ -745,7 +745,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
|
|||||||
stroke=""
|
stroke=""
|
||||||
className={cx(
|
className={cx(
|
||||||
"text-xs",
|
"text-xs",
|
||||||
"fill-gray-500 dark:fill-gray-500",
|
"fill-muted-foreground",
|
||||||
)}
|
)}
|
||||||
tick={{
|
tick={{
|
||||||
transform:
|
transform:
|
||||||
@ -776,7 +776,7 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
|
|||||||
style={{ textAnchor: "middle" }}
|
style={{ textAnchor: "middle" }}
|
||||||
angle={-90}
|
angle={-90}
|
||||||
offset={-15}
|
offset={-15}
|
||||||
className="fill-gray-800 text-sm font-medium dark:fill-gray-200"
|
className="fill-foreground text-sm font-medium"
|
||||||
>
|
>
|
||||||
{yAxisLabel}
|
{yAxisLabel}
|
||||||
</Label>
|
</Label>
|
||||||
|
|||||||
@ -39,10 +39,10 @@ export const chartColors = {
|
|||||||
text: "text-amber-500",
|
text: "text-amber-500",
|
||||||
},
|
},
|
||||||
gray: {
|
gray: {
|
||||||
bg: "bg-gray-500",
|
bg: "bg-muted",
|
||||||
stroke: "stroke-gray-500",
|
stroke: "stroke-muted-foreground",
|
||||||
fill: "fill-gray-500",
|
fill: "fill-muted-foreground",
|
||||||
text: "text-gray-500",
|
text: "text-muted-foreground",
|
||||||
},
|
},
|
||||||
cyan: {
|
cyan: {
|
||||||
bg: "bg-cyan-500",
|
bg: "bg-cyan-500",
|
||||||
@ -96,10 +96,10 @@ export const getColorClassName = (
|
|||||||
type: ColorUtility,
|
type: ColorUtility,
|
||||||
): string => {
|
): string => {
|
||||||
const fallbackColor = {
|
const fallbackColor = {
|
||||||
bg: "bg-gray-500",
|
bg: "bg-muted",
|
||||||
stroke: "stroke-gray-500",
|
stroke: "stroke-muted-foreground",
|
||||||
fill: "fill-gray-500",
|
fill: "fill-muted-foreground",
|
||||||
text: "text-gray-500",
|
text: "text-muted-foreground",
|
||||||
}
|
}
|
||||||
return chartColors[color]?.[type] ?? fallbackColor[type]
|
return chartColors[color]?.[type] ?? fallbackColor[type]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ function DialogOverlay({
|
|||||||
<DialogPrimitive.Backdrop
|
<DialogPrimitive.Backdrop
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
"fixed inset-0 isolate z-50 bg-background/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -28,7 +28,7 @@ function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
|
|||||||
<SheetPrimitive.Backdrop
|
<SheetPrimitive.Backdrop
|
||||||
data-slot="sheet-overlay"
|
data-slot="sheet-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
|
"fixed inset-0 z-50 bg-background/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -16,9 +16,9 @@ function Switch({
|
|||||||
data-slot="switch"
|
data-slot="switch"
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors",
|
"inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors",
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950",
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
||||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
"data-[checked]:bg-primary data-[unchecked]:bg-slate-700",
|
"data-[checked]:bg-primary data-[unchecked]:bg-muted",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -26,7 +26,7 @@ function Switch({
|
|||||||
<SwitchPrimitive.Thumb
|
<SwitchPrimitive.Thumb
|
||||||
data-slot="switch-thumb"
|
data-slot="switch-thumb"
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm ring-0 transition-transform",
|
"pointer-events-none block h-4 w-4 rounded-full bg-card shadow-sm ring-0 transition-transform",
|
||||||
"data-[checked]:translate-x-4 data-[unchecked]:translate-x-0",
|
"data-[checked]:translate-x-4 data-[unchecked]:translate-x-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -10,20 +10,26 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_MODE,
|
||||||
DEFAULT_THEME,
|
DEFAULT_THEME,
|
||||||
|
MODE_STORAGE_KEY,
|
||||||
STORAGE_KEY,
|
STORAGE_KEY,
|
||||||
|
isMode,
|
||||||
isThemeId,
|
isThemeId,
|
||||||
|
type Mode,
|
||||||
type ThemeId,
|
type ThemeId,
|
||||||
} from "@/lib/themes";
|
} from "@/lib/themes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ThemeProvider — wraps the whole app, owns the active theme state.
|
* ThemeProvider — wraps the whole app, owns the two theming axes:
|
||||||
|
* • `theme` — the accent color (`data-theme` on <html>)
|
||||||
|
* • `mode` — light / dark (`data-mode` on <html>)
|
||||||
|
* The two are independent, so any accent renders in either mode.
|
||||||
*
|
*
|
||||||
* The boot script in `src/app/layout.tsx` has already applied
|
* The boot script in `src/app/layout.tsx` has already applied both
|
||||||
* `document.documentElement.dataset.theme` before React hydrates, so
|
* `data-theme` and `data-mode` before React hydrates, so by the time
|
||||||
* by the time this Provider mounts the page is already painted in
|
* this Provider mounts the page is already painted correctly. We just
|
||||||
* the right colors. We just have to read what's there and keep it
|
* read what's there and keep it in sync going forward.
|
||||||
* in sync going forward.
|
|
||||||
*
|
*
|
||||||
* Persistence is localStorage only (device-scoped). A future
|
* Persistence is localStorage only (device-scoped). A future
|
||||||
* follow-up could mirror to `profiles.preferences` for cross-device
|
* follow-up could mirror to `profiles.preferences` for cross-device
|
||||||
@ -34,6 +40,9 @@ import {
|
|||||||
interface ThemeContextValue {
|
interface ThemeContextValue {
|
||||||
theme: ThemeId;
|
theme: ThemeId;
|
||||||
setTheme: (next: ThemeId) => void;
|
setTheme: (next: ThemeId) => void;
|
||||||
|
mode: Mode;
|
||||||
|
setMode: (next: Mode) => void;
|
||||||
|
toggleMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||||
@ -54,8 +63,22 @@ function readInitialTheme(): ThemeId {
|
|||||||
return DEFAULT_THEME;
|
return DEFAULT_THEME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readInitialMode(): Mode {
|
||||||
|
if (typeof window === "undefined") return DEFAULT_MODE;
|
||||||
|
const fromAttr = document.documentElement.dataset.mode;
|
||||||
|
if (isMode(fromAttr)) return fromAttr;
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(MODE_STORAGE_KEY);
|
||||||
|
if (isMode(stored)) return stored;
|
||||||
|
} catch {
|
||||||
|
// localStorage can throw in private-browsing / sandboxed contexts.
|
||||||
|
}
|
||||||
|
return DEFAULT_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
const [theme, setThemeState] = useState<ThemeId>(readInitialTheme);
|
const [theme, setThemeState] = useState<ThemeId>(readInitialTheme);
|
||||||
|
const [mode, setModeState] = useState<Mode>(readInitialMode);
|
||||||
|
|
||||||
const setTheme = useCallback((next: ThemeId) => {
|
const setTheme = useCallback((next: ThemeId) => {
|
||||||
setThemeState(next);
|
setThemeState(next);
|
||||||
@ -70,22 +93,46 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Sync from other tabs — if you change your theme in tab A, tab B
|
const setMode = useCallback((next: Mode) => {
|
||||||
|
setModeState(next);
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.documentElement.dataset.mode = next;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
localStorage.setItem(MODE_STORAGE_KEY, next);
|
||||||
|
} catch {
|
||||||
|
// Same private-browsing edge case as above.
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleMode = useCallback(() => {
|
||||||
|
setMode(mode === "dark" ? "light" : "dark");
|
||||||
|
}, [mode, setMode]);
|
||||||
|
|
||||||
|
// Sync from other tabs — change theme or mode in tab A, tab B
|
||||||
// catches up without a refresh.
|
// catches up without a refresh.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onStorage(e: StorageEvent) {
|
function onStorage(e: StorageEvent) {
|
||||||
if (e.key !== STORAGE_KEY) return;
|
if (e.key === STORAGE_KEY) {
|
||||||
if (isThemeId(e.newValue) && e.newValue !== theme) {
|
if (isThemeId(e.newValue) && e.newValue !== theme) {
|
||||||
setThemeState(e.newValue);
|
setThemeState(e.newValue);
|
||||||
document.documentElement.dataset.theme = e.newValue;
|
document.documentElement.dataset.theme = e.newValue;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === MODE_STORAGE_KEY) {
|
||||||
|
if (isMode(e.newValue) && e.newValue !== mode) {
|
||||||
|
setModeState(e.newValue);
|
||||||
|
document.documentElement.dataset.mode = e.newValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener("storage", onStorage);
|
window.addEventListener("storage", onStorage);
|
||||||
return () => window.removeEventListener("storage", onStorage);
|
return () => window.removeEventListener("storage", onStorage);
|
||||||
}, [theme]);
|
}, [theme, mode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
<ThemeContext.Provider value={{ theme, setTheme, mode, setMode, toggleMode }}>
|
||||||
{children}
|
{children}
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
@ -94,12 +141,15 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|||||||
export function useTheme(): ThemeContextValue {
|
export function useTheme(): ThemeContextValue {
|
||||||
const ctx = useContext(ThemeContext);
|
const ctx = useContext(ThemeContext);
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
// Fallback for components rendered outside the provider — return a
|
// Fallback for components rendered outside the provider — return
|
||||||
// no-op setter so callers don't crash. The boot script still
|
// no-op setters so callers don't crash. The boot script still
|
||||||
// applied the right CSS attribute, so visually the page is fine.
|
// applied the right CSS attributes, so visually the page is fine.
|
||||||
return {
|
return {
|
||||||
theme: DEFAULT_THEME,
|
theme: DEFAULT_THEME,
|
||||||
setTheme: () => {},
|
setTheme: () => {},
|
||||||
|
mode: DEFAULT_MODE,
|
||||||
|
setMode: () => {},
|
||||||
|
toggleMode: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return ctx;
|
return ctx;
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export const TRIGGER_META: Record<AutomationTriggerType, TriggerMeta> = {
|
|||||||
},
|
},
|
||||||
time_based: {
|
time_based: {
|
||||||
label: 'Time-Based',
|
label: 'Time-Based',
|
||||||
pillClass: 'border-slate-500/30 bg-slate-500/10 text-slate-300',
|
pillClass: 'border-slate-500/30 bg-slate-500/10 text-muted-foreground',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ export function triggerMeta(t: AutomationTriggerType | string): TriggerMeta {
|
|||||||
return (
|
return (
|
||||||
TRIGGER_META[t as AutomationTriggerType] ?? {
|
TRIGGER_META[t as AutomationTriggerType] ?? {
|
||||||
label: t,
|
label: t,
|
||||||
pillClass: 'border-slate-500/30 bg-slate-500/10 text-slate-300',
|
pillClass: 'border-slate-500/30 bg-slate-500/10 text-muted-foreground',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,10 @@
|
|||||||
* /broadcasts/page.tsx and /broadcasts/[id]/page.tsx with slight
|
* /broadcasts/page.tsx and /broadcasts/[id]/page.tsx with slight
|
||||||
* drift risk. One source of truth now.
|
* drift risk. One source of truth now.
|
||||||
*
|
*
|
||||||
* Dark-theme only — bg-*-500/10 + text-*-400 + border-*-500/20.
|
* Badge shape: bg-*-500/10 + text-*-400 + border-*-500/20. The
|
||||||
|
* translucent fills sit fine on both light and dark surfaces; neutral
|
||||||
|
* statuses use text-muted-foreground so the label stays legible in
|
||||||
|
* light mode (a solid slate-400 would be too faint on white).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { BroadcastStatus, RecipientStatus } from "@/types";
|
import type { BroadcastStatus, RecipientStatus } from "@/types";
|
||||||
@ -23,7 +26,7 @@ export interface StatusDisplay {
|
|||||||
export const broadcastStatusConfig: Record<BroadcastStatus, StatusDisplay> = {
|
export const broadcastStatusConfig: Record<BroadcastStatus, StatusDisplay> = {
|
||||||
draft: {
|
draft: {
|
||||||
label: "Draft",
|
label: "Draft",
|
||||||
classes: "bg-slate-500/10 text-slate-400 border-slate-500/20",
|
classes: "bg-slate-500/10 text-muted-foreground border-slate-500/20",
|
||||||
},
|
},
|
||||||
scheduled: {
|
scheduled: {
|
||||||
label: "Scheduled",
|
label: "Scheduled",
|
||||||
@ -47,7 +50,7 @@ export const broadcastStatusConfig: Record<BroadcastStatus, StatusDisplay> = {
|
|||||||
export const recipientStatusConfig: Record<RecipientStatus, StatusDisplay> = {
|
export const recipientStatusConfig: Record<RecipientStatus, StatusDisplay> = {
|
||||||
pending: {
|
pending: {
|
||||||
label: "Pending",
|
label: "Pending",
|
||||||
classes: "bg-slate-500/10 text-slate-400 border-slate-500/20",
|
classes: "bg-slate-500/10 text-muted-foreground border-slate-500/20",
|
||||||
},
|
},
|
||||||
sent: {
|
sent: {
|
||||||
label: "Sent",
|
label: "Sent",
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export const templateStatusConfig: Record<
|
|||||||
> = {
|
> = {
|
||||||
DRAFT: {
|
DRAFT: {
|
||||||
label: 'Draft',
|
label: 'Draft',
|
||||||
classes: 'bg-slate-600/20 text-slate-400 border-slate-600/30',
|
classes: 'bg-slate-600/20 text-muted-foreground border-slate-600/30',
|
||||||
},
|
},
|
||||||
PENDING: {
|
PENDING: {
|
||||||
label: 'Pending',
|
label: 'Pending',
|
||||||
@ -48,6 +48,6 @@ export const templateStatusConfig: Record<
|
|||||||
},
|
},
|
||||||
PENDING_DELETION: {
|
PENDING_DELETION: {
|
||||||
label: 'Pending Deletion',
|
label: 'Pending Deletion',
|
||||||
classes: 'bg-slate-700/30 text-slate-500 border-slate-700/40',
|
classes: 'bg-slate-700/30 text-muted-foreground border-slate-700/40',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -27,6 +27,32 @@ export const DEFAULT_THEME: ThemeId = "violet";
|
|||||||
|
|
||||||
export const STORAGE_KEY = "wacrm.theme";
|
export const STORAGE_KEY = "wacrm.theme";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MODE — the light/dark dimension, orthogonal to the accent theme.
|
||||||
|
*
|
||||||
|
* The CSS variables live in `src/app/globals.css` under
|
||||||
|
* `html[data-mode="..."]` blocks (neutral surfaces only). Applied
|
||||||
|
* at runtime via `document.documentElement.dataset.mode`. Dark is
|
||||||
|
* the historical default and stays the app's identity; light is the
|
||||||
|
* opt-in eye-strain-friendly alternative.
|
||||||
|
*
|
||||||
|
* Persisted under its own localStorage key so it composes freely
|
||||||
|
* with the accent choice (you can run Violet-light or Violet-dark).
|
||||||
|
*/
|
||||||
|
export const MODES = ["light", "dark"] as const;
|
||||||
|
|
||||||
|
export type Mode = (typeof MODES)[number];
|
||||||
|
|
||||||
|
export const DEFAULT_MODE: Mode = "dark";
|
||||||
|
|
||||||
|
export const MODE_STORAGE_KEY = "wacrm.mode";
|
||||||
|
|
||||||
|
export function isMode(value: unknown): value is Mode {
|
||||||
|
return (
|
||||||
|
typeof value === "string" && (MODES as ReadonlyArray<string>).includes(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface ThemeMeta {
|
export interface ThemeMeta {
|
||||||
id: ThemeId;
|
id: ThemeId;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user