-
+
{flow.name}
diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/(dashboard)/settings/page.tsx
index 0dc8d5d..4c29725 100644
--- a/src/app/(dashboard)/settings/page.tsx
+++ b/src/app/(dashboard)/settings/page.tsx
@@ -1,7 +1,7 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
-import { Settings, MessageSquare, Tag, User } from 'lucide-react';
+import { Settings, MessageSquare, Tag, User, Palette } from 'lucide-react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { WhatsAppConfig } from '@/components/settings/whatsapp-config';
import { TemplateManager } from '@/components/settings/template-manager';
@@ -9,8 +9,15 @@ import { TagManager } from '@/components/settings/tag-manager';
import { ProfileForm } from '@/components/settings/profile-form';
import { PasswordForm } from '@/components/settings/password-form';
import { SessionsCard } from '@/components/settings/sessions-card';
+import { AppearancePanel } from '@/components/settings/appearance-panel';
-const TAB_VALUES = ['profile', 'whatsapp', 'templates', 'tags'] as const;
+const TAB_VALUES = [
+ 'profile',
+ 'whatsapp',
+ 'templates',
+ 'tags',
+ 'appearance',
+] as const;
type TabValue = (typeof TAB_VALUES)[number];
function isTabValue(v: string | null): v is TabValue {
@@ -48,32 +55,39 @@ export default function SettingsPage() {
Profile
WhatsApp Config
Templates
Tags
+
+
+ Appearance
+
@@ -93,6 +107,10 @@ export default function SettingsPage() {
+
+
+
+
);
diff --git a/src/app/globals.css b/src/app/globals.css
index 25b8e6f..0212fe0 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -48,7 +48,29 @@
--radius-4xl: calc(var(--radius) * 2.6);
}
-:root {
+/* ============================================================
+ * COLOR THEMES
+ *
+ * 5 dark themes sharing structure, swapping only the primary
+ * accent (and a tiny hue rotation on the slate base). Switched
+ * at runtime by setting `document.documentElement.dataset.theme`
+ * — see ThemeProvider + the inline boot script in layout.tsx.
+ *
+ * `:root` mirrors Violet so default visitors render correctly
+ * before JS runs. `[data-theme="violet"]` is included for the
+ * explicit-select path (settings UI sets it after a pick).
+ *
+ * The three `--primary-*` extras (hover / soft / soft-2) are
+ * new — used for hovered primary buttons and tinted-primary
+ * surfaces (sidebar active pill, validation badges). Components
+ * that don't use them remain backward-compatible.
+ *
+ * Token shapes come straight from the design handoff at
+ * project/Color Themes.html — change one, change the other.
+ * ============================================================ */
+
+:root,
+html[data-theme="violet"] {
--background: oklch(0.13 0.01 260);
--foreground: oklch(0.985 0 0);
--card: oklch(0.18 0.01 260);
@@ -57,6 +79,9 @@
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.526 0.247 293);
--primary-foreground: oklch(0.985 0 0);
+ --primary-hover: oklch(0.6 0.22 293);
+ --primary-soft: oklch(0.526 0.247 293 / 0.12);
+ --primary-soft-2: oklch(0.526 0.247 293 / 0.2);
--secondary: oklch(0.22 0.01 260);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.22 0.01 260);
@@ -83,6 +108,154 @@
--sidebar-ring: oklch(0.526 0.247 293);
}
+html[data-theme="emerald"] {
+ --background: oklch(0.135 0.01 200);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.185 0.01 200);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.185 0.01 200);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.62 0.16 162);
+ --primary-foreground: oklch(0.16 0.02 162);
+ --primary-hover: oklch(0.68 0.15 162);
+ --primary-soft: oklch(0.62 0.16 162 / 0.12);
+ --primary-soft-2: oklch(0.62 0.16 162 / 0.22);
+ --secondary: oklch(0.225 0.01 200);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.225 0.01 200);
+ --muted-foreground: oklch(0.65 0.01 200);
+ --accent: oklch(0.225 0.01 200);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.285 0.01 200);
+ --input: oklch(0.285 0.01 200);
+ --ring: oklch(0.62 0.16 162);
+ --chart-1: oklch(0.62 0.16 162);
+ --chart-2: oklch(0.7 0.14 195);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --sidebar: oklch(0.165 0.01 200);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.62 0.16 162);
+ --sidebar-primary-foreground: oklch(0.16 0.02 162);
+ --sidebar-accent: oklch(0.225 0.01 200);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(0.285 0.01 200);
+ --sidebar-ring: oklch(0.62 0.16 162);
+}
+
+html[data-theme="cobalt"] {
+ --background: oklch(0.13 0.015 252);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.18 0.015 252);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.18 0.015 252);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.585 0.2 254);
+ --primary-foreground: oklch(0.985 0 0);
+ --primary-hover: oklch(0.66 0.18 254);
+ --primary-soft: oklch(0.585 0.2 254 / 0.12);
+ --primary-soft-2: oklch(0.585 0.2 254 / 0.22);
+ --secondary: oklch(0.22 0.015 252);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.22 0.015 252);
+ --muted-foreground: oklch(0.65 0.012 252);
+ --accent: oklch(0.22 0.015 252);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.28 0.015 252);
+ --input: oklch(0.28 0.015 252);
+ --ring: oklch(0.585 0.2 254);
+ --chart-1: oklch(0.585 0.2 254);
+ --chart-2: oklch(0.7 0.15 220);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --sidebar: oklch(0.16 0.015 252);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.585 0.2 254);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.22 0.015 252);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(0.28 0.015 252);
+ --sidebar-ring: oklch(0.585 0.2 254);
+}
+
+html[data-theme="amber"] {
+ --background: oklch(0.135 0.012 60);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.185 0.012 60);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.185 0.012 60);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.745 0.16 65);
+ --primary-foreground: oklch(0.18 0.03 65);
+ --primary-hover: oklch(0.8 0.15 65);
+ --primary-soft: oklch(0.745 0.16 65 / 0.12);
+ --primary-soft-2: oklch(0.745 0.16 65 / 0.22);
+ --secondary: oklch(0.225 0.012 60);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.225 0.012 60);
+ --muted-foreground: oklch(0.66 0.01 60);
+ --accent: oklch(0.225 0.012 60);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.285 0.012 60);
+ --input: oklch(0.285 0.012 60);
+ --ring: oklch(0.745 0.16 65);
+ --chart-1: oklch(0.745 0.16 65);
+ --chart-2: oklch(0.7 0.15 35);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --sidebar: oklch(0.165 0.012 60);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.745 0.16 65);
+ --sidebar-primary-foreground: oklch(0.18 0.03 65);
+ --sidebar-accent: oklch(0.225 0.012 60);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(0.285 0.012 60);
+ --sidebar-ring: oklch(0.745 0.16 65);
+}
+
+html[data-theme="rose"] {
+ --background: oklch(0.13 0.012 15);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.18 0.012 15);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.18 0.012 15);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.645 0.22 16);
+ --primary-foreground: oklch(0.985 0 0);
+ --primary-hover: oklch(0.71 0.2 16);
+ --primary-soft: oklch(0.645 0.22 16 / 0.12);
+ --primary-soft-2: oklch(0.645 0.22 16 / 0.22);
+ --secondary: oklch(0.22 0.012 15);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.22 0.012 15);
+ --muted-foreground: oklch(0.65 0.01 15);
+ --accent: oklch(0.22 0.012 15);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.28 0.012 15);
+ --input: oklch(0.28 0.012 15);
+ --ring: oklch(0.645 0.22 16);
+ --chart-1: oklch(0.645 0.22 16);
+ --chart-2: oklch(0.7 0.18 340);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --sidebar: oklch(0.16 0.012 15);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.645 0.22 16);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.22 0.012 15);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(0.28 0.012 15);
+ --sidebar-ring: oklch(0.645 0.22 16);
+}
+
@layer base {
* {
@apply border-border outline-ring/50;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 9e3207d..0956aa5 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,7 +1,10 @@
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
+import Script from "next/script";
import { Toaster } from "sonner";
import "./globals.css";
+import { ThemeProvider } from "@/hooks/use-theme";
+import { DEFAULT_THEME, STORAGE_KEY, THEME_IDS } from "@/lib/themes";
const inter = Inter({
variable: "--font-sans",
@@ -33,26 +36,63 @@ export const viewport: Viewport = {
colorScheme: "dark",
};
+// Inline boot script — runs before React hydrates so the user's
+// chosen theme is on the element before first paint. Without
+// this every page load flashes the default Violet for a frame before
+// the React tree mounts and applies the picked theme.
+//
+// Kept dependency-free (no imports, no JSX) — must be a string the
+// browser can run as a single