mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Adds a new "Appearance" tab in Settings with 5 dark color themes (Violet, Emerald, Cobalt, Amber, Rose). Pick a theme → applied immediately and persisted to localStorage. Boot script in the root layout replays the pick before first paint so there's no flash-of-default on subsequent loads. Implementation, ordered the way it's read: 1. `globals.css` — 5 `html[data-theme="..."]` blocks extracted verbatim from the design handoff at project/Color Themes.html. `:root` mirrors Violet so SSR / first-paint renders correctly even before the boot script runs. Three new tokens (`--primary-hover`, `--primary-soft`, `--primary-soft-2`) added to all themes — used by tinted-primary surfaces. 2. `src/lib/themes.ts` — single source of truth for the catalog (id, name, tagline, swatch). Adding a theme = append a CSS block + append an entry here. 3. `src/hooks/use-theme.tsx` — ThemeProvider client component. Reads what the boot script already applied, syncs across tabs via the `storage` event, persists to localStorage. Falls back to a no-op setter when used outside the provider. 4. `src/app/layout.tsx` — inline `beforeInteractive` script that reads localStorage and sets `documentElement.dataset.theme` before React hydrates. Knowledge of valid theme IDs comes from the THEME_IDS constant so adding a theme doesn't silently break the boot path. Wraps the tree in ThemeProvider. 5. `src/components/settings/appearance-panel.tsx` — picker UI. 5 cards, swatch + name + tagline + a 4-tone preview strip. Active card carries a primary-tinted ring + "Active" chip. 6. `src/app/(dashboard)/settings/page.tsx` — new "appearance" tab alongside profile/whatsapp/templates/tags. 7. Key-surface tokenization — replaced hard-coded `violet-*` with `primary` tokens on the high-visibility surfaces so the theme switch feels real: sidebar logo + active nav + unread dot + avatar fallback, header avatar, dashboard-shell loading spinner, Flows header icon + entry-node picker icon + entry badge + flash ring, Flows list page (template card hover, FlowCard icon), Flows editor + runs "back" links, Settings tabs active state. NOT touched (intentional follow-ups): - Per-node-type colors in flow-builder.tsx NODE_META — those are a deliberate per-type rainbow, not a brand accent. - The inbox/contacts/pipelines surfaces — many `violet-*` usages there. Sweep is a contained follow-up; the user explicitly scoped this PR to "tokens + tab + key surfaces". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
140 lines
4.5 KiB
TypeScript
140 lines
4.5 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { usePathname } from "next/navigation";
|
||
import { useAuth } from "@/hooks/use-auth";
|
||
import { LogOut, Menu, Settings as SettingsIcon, User } from "lucide-react";
|
||
import {
|
||
Avatar,
|
||
AvatarFallback,
|
||
AvatarImage,
|
||
} from "@/components/ui/avatar";
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuSeparator,
|
||
DropdownMenuTrigger,
|
||
} from "@/components/ui/dropdown-menu";
|
||
|
||
const pageTitles: Record<string, string> = {
|
||
"/dashboard": "Dashboard",
|
||
"/inbox": "Inbox",
|
||
"/contacts": "Contacts",
|
||
"/pipelines": "Pipelines",
|
||
"/broadcasts": "Broadcasts",
|
||
"/automations": "Automations",
|
||
"/settings": "Settings",
|
||
};
|
||
|
||
function getPageTitle(pathname: string): string {
|
||
if (pageTitles[pathname]) return pageTitles[pathname];
|
||
const match = Object.entries(pageTitles).find(([path]) =>
|
||
pathname.startsWith(path),
|
||
);
|
||
return match ? match[1] : "Dashboard";
|
||
}
|
||
|
||
interface HeaderProps {
|
||
/** Wired to the shell's drawer state. Used only on mobile — the
|
||
* hamburger button is hidden on lg+. */
|
||
onOpenSidebar?: () => void;
|
||
}
|
||
|
||
export function Header({ onOpenSidebar }: HeaderProps) {
|
||
const pathname = usePathname();
|
||
const { profile, signOut } = useAuth();
|
||
const title = getPageTitle(pathname);
|
||
|
||
const initial =
|
||
profile?.full_name?.charAt(0)?.toUpperCase() ??
|
||
profile?.email?.charAt(0)?.toUpperCase() ??
|
||
"U";
|
||
|
||
return (
|
||
<header className="flex h-14 shrink-0 items-center justify-between gap-3 border-b border-slate-800 bg-slate-950 px-4 lg:px-6">
|
||
<div className="flex min-w-0 items-center gap-2">
|
||
{/* Hamburger — mobile only. 44×44 hit target per Apple HIG. */}
|
||
<button
|
||
type="button"
|
||
onClick={onOpenSidebar}
|
||
aria-label="Open menu"
|
||
className="flex h-10 w-10 items-center justify-center rounded-md text-slate-300 transition-colors hover:bg-slate-800 hover:text-white lg:hidden"
|
||
>
|
||
<Menu className="h-5 w-5" />
|
||
</button>
|
||
<h1 className="truncate text-base font-semibold text-white sm:text-lg">
|
||
{title}
|
||
</h1>
|
||
</div>
|
||
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger
|
||
className="flex items-center gap-2 rounded-md px-1 py-1 transition-colors hover:bg-slate-800/70 focus:bg-slate-800/70 focus:outline-none data-popup-open:bg-slate-800/70 sm:gap-3 sm:pl-1 sm:pr-3"
|
||
aria-label="Open account menu"
|
||
>
|
||
<Avatar className="size-8">
|
||
{profile?.avatar_url ? (
|
||
<AvatarImage
|
||
src={profile.avatar_url}
|
||
alt={profile.full_name ?? "Avatar"}
|
||
/>
|
||
) : null}
|
||
<AvatarFallback className="bg-primary/10 text-sm font-medium text-primary">
|
||
{initial}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<span className="hidden text-sm font-medium text-white sm:inline">
|
||
{profile?.full_name ?? "User"}
|
||
</span>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent
|
||
align="end"
|
||
sideOffset={6}
|
||
className="min-w-56 bg-slate-900 text-slate-100 ring-slate-700"
|
||
>
|
||
<div className="px-2 py-1.5">
|
||
<p className="truncate text-sm font-medium text-white">
|
||
{profile?.full_name ?? "User"}
|
||
</p>
|
||
<p className="truncate text-xs text-slate-400">
|
||
{profile?.email ?? ""}
|
||
</p>
|
||
</div>
|
||
<DropdownMenuSeparator className="bg-slate-800" />
|
||
<DropdownMenuItem
|
||
render={
|
||
<Link
|
||
href="/settings?tab=profile"
|
||
className="text-slate-200 focus:bg-slate-800 focus:text-white"
|
||
/>
|
||
}
|
||
>
|
||
<User className="size-4" />
|
||
Profile
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem
|
||
render={
|
||
<Link
|
||
href="/settings?tab=whatsapp"
|
||
className="text-slate-200 focus:bg-slate-800 focus:text-white"
|
||
/>
|
||
}
|
||
>
|
||
<SettingsIcon className="size-4" />
|
||
Settings
|
||
</DropdownMenuItem>
|
||
<DropdownMenuSeparator className="bg-slate-800" />
|
||
<DropdownMenuItem
|
||
onClick={signOut}
|
||
className="text-slate-200 focus:bg-slate-800 focus:text-white"
|
||
>
|
||
<LogOut className="size-4" />
|
||
Sign out
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</header>
|
||
);
|
||
}
|