mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Replace the flat 8-tab Settings page with a cleaner information architecture: a grouped left rail (Account / Workspace) plus a new Overview landing with live status tiles. Related concerns are merged — "Login & security" (password + sessions) and "Fields & tags" (tags + custom fields). - New shell: settings-sections (section model + legacy ?tab= mapping, tags/custom-fields -> fields), settings-rail, settings-overview (live counts + identity), security-panel, fields-and-tags-panel, settings-panel-head, settings-chip, role-meta (shared role chip metadata, de-duped from members-tab). - Restyle the light panels to the mockup; reframe the heavy panels (WhatsApp, Templates) with the shared panel header, internals intact. - Tokens: add --card-2 (light+dark) and @theme mappings for primary-hover/soft/soft-2 so the redesign stays fully tokenized. - ?tab= remains the URL source of truth, keeping existing sidebar/header deep-links working. Overview decouples the slow WhatsApp Meta health-ping from the cheap count queries so a flaky ping never blanks the whole landing; template counts use head-only count queries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
184 lines
5.2 KiB
TypeScript
184 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import { Check, Moon, Palette, SunMoon, Sun } from "lucide-react";
|
|
|
|
import { useTheme } from "@/hooks/use-theme";
|
|
import { MODES, THEMES, type Mode, type ThemeId } from "@/lib/themes";
|
|
import { cn } from "@/lib/utils";
|
|
import { SettingsPanelHead } from "./settings-panel-head";
|
|
|
|
/**
|
|
* Appearance panel — light/dark mode + accent-color picker.
|
|
*
|
|
* Two independent controls: a mode toggle (light / dark) and the
|
|
* accent grid. Either applies + persists immediately. No save button:
|
|
* each change is a single attribute swap on <html>, there's nothing
|
|
* to roll back.
|
|
*
|
|
* Persistence: localStorage only (device-scoped). The boot script in
|
|
* layout.tsx replays both choices before first paint on subsequent
|
|
* loads.
|
|
*/
|
|
export function AppearancePanel() {
|
|
const { theme, setTheme, mode, setMode } = useTheme();
|
|
return (
|
|
<section className="animate-in fade-in-50 duration-200">
|
|
<SettingsPanelHead
|
|
title="Appearance"
|
|
description="Set the mode and accent colour used across the app. Saved to this device — try it, it changes live."
|
|
/>
|
|
|
|
<div className="space-y-4">
|
|
<h3 className="flex items-center gap-2 text-sm font-semibold text-foreground">
|
|
<SunMoon className="size-4 text-muted-foreground" />
|
|
Mode
|
|
</h3>
|
|
|
|
<div
|
|
role="radiogroup"
|
|
aria-label="Color mode"
|
|
className="grid max-w-md grid-cols-2 gap-3"
|
|
>
|
|
{MODES.map((m) => (
|
|
<ModeCard
|
|
key={m}
|
|
mode={m}
|
|
isActive={m === mode}
|
|
onPick={() => setMode(m)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 space-y-4">
|
|
<h3 className="flex items-center gap-2 text-sm font-semibold text-foreground">
|
|
<Palette className="size-4 text-muted-foreground" />
|
|
Accent color
|
|
</h3>
|
|
|
|
<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>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function ModeCard({
|
|
mode,
|
|
isActive,
|
|
onPick,
|
|
}: {
|
|
mode: Mode;
|
|
isActive: boolean;
|
|
onPick: () => void;
|
|
}) {
|
|
const isLight = mode === "light";
|
|
const Icon = isLight ? Sun : Moon;
|
|
return (
|
|
<button
|
|
type="button"
|
|
role="radio"
|
|
onClick={onPick}
|
|
aria-checked={isActive}
|
|
aria-label={`Use ${mode} mode`}
|
|
className={cn(
|
|
"flex items-center gap-3 rounded-lg border bg-card p-4 text-left transition-colors",
|
|
isActive
|
|
? "border-primary/60 ring-2 ring-primary/40"
|
|
: "border-border hover:border-border hover:bg-muted/40",
|
|
)}
|
|
>
|
|
<span
|
|
aria-hidden
|
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-foreground"
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
</span>
|
|
<span className="flex-1 text-sm font-semibold capitalize text-foreground">
|
|
{mode}
|
|
</span>
|
|
{isActive && (
|
|
<span className="inline-flex items-center gap-1 rounded-full bg-primary/15 px-2 py-0.5 text-[11px] font-medium text-primary">
|
|
<Check className="h-3 w-3" />
|
|
Active
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function ThemeCard({
|
|
id,
|
|
name,
|
|
tagline,
|
|
swatch,
|
|
isActive,
|
|
onPick,
|
|
}: {
|
|
id: ThemeId;
|
|
name: string;
|
|
tagline: string;
|
|
swatch: string;
|
|
isActive: boolean;
|
|
onPick: () => void;
|
|
}) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onPick}
|
|
aria-pressed={isActive}
|
|
aria-label={`Use ${name} theme`}
|
|
className={cn(
|
|
"flex flex-col gap-3 rounded-lg border bg-card p-4 text-left transition-colors",
|
|
isActive
|
|
? "border-primary/60 ring-2 ring-primary/40"
|
|
: "border-border hover:border-border hover:bg-muted/40",
|
|
)}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<span
|
|
aria-hidden
|
|
className="h-8 w-8 shrink-0 rounded-full"
|
|
style={{
|
|
background: swatch,
|
|
boxShadow: "inset 0 0 0 1px oklch(1 0 0 / 0.15)",
|
|
}}
|
|
/>
|
|
{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>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-semibold text-foreground">{name}</div>
|
|
<div className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
|
{tagline}
|
|
</div>
|
|
</div>
|
|
<div
|
|
className="mt-1 flex h-2 overflow-hidden rounded-full"
|
|
aria-hidden
|
|
>
|
|
<span className="flex-1" style={{ background: swatch }} />
|
|
<span className="w-3 bg-muted-foreground/60" />
|
|
<span className="w-3 bg-muted" />
|
|
<span className="w-3 bg-card" />
|
|
</div>
|
|
<span className="sr-only">Theme id: {id}</span>
|
|
</button>
|
|
);
|
|
}
|