diff --git a/src/app/(dashboard)/dashboard-shell.tsx b/src/app/(dashboard)/dashboard-shell.tsx index 5606f99..0ad55b0 100644 --- a/src/app/(dashboard)/dashboard-shell.tsx +++ b/src/app/(dashboard)/dashboard-shell.tsx @@ -29,7 +29,7 @@ function DashboardShellInner({ children }: { children: React.ReactNode }) { return (
-
+

Loading...

diff --git a/src/app/(dashboard)/flows/[id]/page.tsx b/src/app/(dashboard)/flows/[id]/page.tsx index 2d25bee..3a8de76 100644 --- a/src/app/(dashboard)/flows/[id]/page.tsx +++ b/src/app/(dashboard)/flows/[id]/page.tsx @@ -88,7 +88,7 @@ export default function FlowEditorPage() { diff --git a/src/app/(dashboard)/flows/[id]/runs/page.tsx b/src/app/(dashboard)/flows/[id]/runs/page.tsx index da6dc2b..78d7ced 100644 --- a/src/app/(dashboard)/flows/[id]/runs/page.tsx +++ b/src/app/(dashboard)/flows/[id]/runs/page.tsx @@ -173,7 +173,7 @@ export default function FlowRunsPage() { diff --git a/src/app/(dashboard)/flows/page.tsx b/src/app/(dashboard)/flows/page.tsx index e037bd5..0476901 100644 --- a/src/app/(dashboard)/flows/page.tsx +++ b/src/app/(dashboard)/flows/page.tsx @@ -271,9 +271,9 @@ export default function FlowsPage() { type="button" onClick={() => handleUseTemplate(t.slug)} disabled={creating} - className="flex flex-col gap-2.5 rounded-lg border border-slate-800 bg-slate-950 p-4 text-left transition-colors hover:border-violet-500/40 hover:bg-slate-800 disabled:opacity-50" + 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" > - + {t.name} @@ -366,7 +366,7 @@ function FlowCard({
- +

{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