From 4aa329b5278d0d7ec9735e39eadbe39a66efc55a Mon Sep 17 00:00:00 2001 From: Arnas Donauskas Date: Tue, 16 Jun 2026 17:26:29 +0300 Subject: [PATCH] feat(theme): add light/dark mode toggle (#243) Introduce a light/dark MODE axis orthogonal to the existing accent themes. Neutral surface tokens now live in html[data-mode="light|dark"] blocks; accent tokens (--primary*, --ring, --chart-1, --sidebar-primary*) stay in html[data-theme] blocks, so any accent works in either mode. - globals.css: split neutrals (mode) from accent (theme); add light palette; dark values unchanged (dark stays the default). - lib/themes.ts: Mode type, DEFAULT_MODE=dark, MODE_STORAGE_KEY, isMode. - use-theme.tsx: provider owns mode/setMode/toggleMode + cross-tab sync. - layout.tsx: boot script applies persisted mode before first paint (no flash); colorScheme "dark light". - mode-toggle.tsx (header sun/moon) + themed-toaster.tsx (mode-aware). - appearance-panel.tsx: light/dark control + updated copy. - Tokenize hardcoded neutral colors (slate/white/gray) -> semantic tokens across the app so every surface flips with mode. Flow canvas and message-bubble timestamps made mode-aware too. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/(auth)/forgot-password/page.tsx | 26 +-- src/app/(auth)/login/page.tsx | 18 +- src/app/(auth)/signup/page.tsx | 38 ++-- .../automations/[id]/logs/page.tsx | 32 ++-- src/app/(dashboard)/automations/page.tsx | 30 +-- src/app/(dashboard)/broadcasts/[id]/page.tsx | 76 ++++---- src/app/(dashboard)/broadcasts/new/page.tsx | 10 +- src/app/(dashboard)/broadcasts/page.tsx | 46 ++--- src/app/(dashboard)/contacts/page.tsx | 92 ++++----- src/app/(dashboard)/dashboard-shell.tsx | 6 +- src/app/(dashboard)/dashboard/page.tsx | 4 +- src/app/(dashboard)/flows/[id]/page.tsx | 4 +- src/app/(dashboard)/flows/[id]/runs/page.tsx | 50 ++--- src/app/(dashboard)/flows/page.tsx | 50 ++--- src/app/(dashboard)/pipelines/page.tsx | 44 ++--- src/app/(dashboard)/settings/page.tsx | 22 +-- src/app/globals.css | 178 +++++++----------- src/app/join/[token]/page.tsx | 44 ++--- src/app/join/layout.tsx | 2 +- src/app/layout.tsx | 71 +++---- .../automations/automation-builder.tsx | 108 +++++------ .../broadcasts/step1-choose-template.tsx | 24 +-- .../broadcasts/step2-select-audience.tsx | 58 +++--- .../broadcasts/step3-personalize.tsx | 38 ++-- .../broadcasts/step4-schedule-send.tsx | 50 ++--- .../contacts/contact-detail-view.tsx | 76 ++++---- src/components/contacts/contact-form.tsx | 36 ++-- .../contacts/custom-fields-manager.tsx | 20 +- src/components/contacts/import-modal.tsx | 74 ++++---- src/components/dashboard/activity-feed.tsx | 28 +-- .../dashboard/conversations-chart.tsx | 24 +-- src/components/dashboard/empty-state.tsx | 8 +- src/components/dashboard/metric-card.tsx | 12 +- src/components/dashboard/pipeline-donut.tsx | 18 +- src/components/dashboard/quick-actions.tsx | 6 +- .../dashboard/response-time-chart.tsx | 14 +- src/components/dashboard/skeleton.tsx | 4 +- src/components/flows/flow-builder.tsx | 54 +++--- src/components/flows/flow-canvas.tsx | 55 +++--- src/components/flows/flow-editor-shell.tsx | 8 +- src/components/flows/forms/fields.tsx | 10 +- .../flows/forms/node-config-form.tsx | 76 ++++---- src/components/flows/header.tsx | 12 +- src/components/flows/shared.tsx | 2 +- src/components/flows/validation-panel.tsx | 10 +- src/components/inbox/contact-sidebar.tsx | 52 ++--- src/components/inbox/conversation-list.tsx | 30 +-- src/components/inbox/message-actions.tsx | 10 +- src/components/inbox/message-bubble.tsx | 39 ++-- src/components/inbox/message-composer.tsx | 8 +- src/components/inbox/message-reactions.tsx | 2 +- src/components/inbox/message-thread.tsx | 54 +++--- src/components/inbox/reply-quote.tsx | 8 +- src/components/inbox/template-picker.tsx | 48 ++--- src/components/layout/header.tsx | 35 ++-- src/components/layout/mode-toggle.tsx | 37 ++++ src/components/layout/sidebar.tsx | 40 ++-- src/components/pipelines/deal-card.tsx | 12 +- src/components/pipelines/deal-form.tsx | 52 ++--- .../pipelines/pipeline-analytics.tsx | 12 +- src/components/pipelines/pipeline-board.tsx | 12 +- .../pipelines/pipeline-settings.tsx | 36 ++-- src/components/settings/appearance-panel.tsx | 143 ++++++++++---- .../settings/custom-fields-settings.tsx | 6 +- src/components/settings/deals-settings.tsx | 12 +- .../settings/invite-member-dialog.tsx | 44 ++--- src/components/settings/members-tab.tsx | 60 +++--- src/components/settings/password-form.tsx | 12 +- src/components/settings/profile-form.tsx | 32 ++-- src/components/settings/sessions-card.tsx | 6 +- src/components/settings/tag-manager.tsx | 44 ++--- src/components/settings/template-manager.tsx | 130 ++++++------- src/components/settings/whatsapp-config.tsx | 120 ++++++------ src/components/themed-toaster.tsx | 30 +++ src/components/tremor/bar-chart.tsx | 32 ++-- src/components/tremor/chart-colors.ts | 16 +- src/components/ui/dialog.tsx | 2 +- src/components/ui/sheet.tsx | 2 +- src/components/ui/switch.tsx | 6 +- src/hooks/use-theme.tsx | 82 ++++++-- src/lib/automations/trigger-meta.ts | 4 +- src/lib/broadcast-status.ts | 4 +- src/lib/template-status.ts | 4 +- src/lib/themes.ts | 26 +++ 84 files changed, 1581 insertions(+), 1391 deletions(-) create mode 100644 src/components/layout/mode-toggle.tsx create mode 100644 src/components/themed-toaster.tsx diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx index 02ff209..e1c5183 100644 --- a/src/app/(auth)/forgot-password/page.tsx +++ b/src/app/(auth)/forgot-password/page.tsx @@ -43,18 +43,18 @@ export default function ForgotPasswordPage() { if (success) { return ( -
- +
+
- + Check your email - + We've sent a password reset link to{" "} - {email}. Please check your + {email}. Please check your inbox.
@@ -62,7 +62,7 @@ export default function ForgotPasswordPage() { @@ -74,14 +74,14 @@ export default function ForgotPasswordPage() { } return ( -
- +
+
- Reset password - + Reset password + Enter your email and we'll send you a reset link
@@ -94,7 +94,7 @@ export default function ForgotPasswordPage() { )}
-
@@ -119,7 +119,7 @@ export default function ForgotPasswordPage() { Back to sign in diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 1a53226..0ffaab2 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -67,8 +67,8 @@ function LoginPageInner() { }; return ( -
- +
+
{inviteToken ? ( @@ -77,10 +77,10 @@ function LoginPageInner() { )}
- + {inviteToken ? "Sign in to accept" : "Welcome back"} - + {inviteToken ? "Sign in and we'll take you to the invitation." : "Sign in to your account"} @@ -95,7 +95,7 @@ function LoginPageInner() { )}
-
-
@@ -141,7 +141,7 @@ function LoginPageInner() { -

+

Don't have an account?{" "} - +

+
- + Check your email - + We've sent a confirmation link to{" "} - {email}. Please check your + {email}. Please check your inbox and click the link to verify your account.
@@ -116,7 +116,7 @@ function SignupPageInner() { > @@ -128,8 +128,8 @@ function SignupPageInner() { } return ( -
- +
+
{inviteToken ? ( @@ -138,10 +138,10 @@ function SignupPageInner() { )}
- + {inviteToken ? "Create account & join" : "Create account"} - + {inviteToken ? "Verify your email, then accept the invitation to join your team." : "Get started with CRM Template for WhatsApp"} @@ -156,7 +156,7 @@ function SignupPageInner() { )}
-
-
-
-
@@ -224,7 +224,7 @@ function SignupPageInner() { -

+

Already have an account?{" "} 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" >

-

{automation.name}

-

Execution logs

+

{automation.name}

+

Execution logs

{logs.length === 0 ? ( -
-

No executions yet

-

+

+

No executions yet

+

Trigger this automation to see runs here.

@@ -112,7 +112,7 @@ export default function AutomationLogsPage({ return (
  • {isOpen && ( -
    +
    {log.error_message && (

    {log.error_message} @@ -150,7 +150,7 @@ export default function AutomationLogsPage({ ))} {(log.steps_executed ?? []).length === 0 && ( -

  • No steps recorded.
  • +
  • No steps recorded.
  • )}
    @@ -196,9 +196,9 @@ function StepRow({ result }: { result: AutomationLogStepResult }) { > {ok ? : } - {result.step_type} + {result.step_type} {result.detail && ( - — {result.detail} + — {result.detail} )} ) diff --git a/src/app/(dashboard)/automations/page.tsx b/src/app/(dashboard)/automations/page.tsx index ac5eaad..28760ae 100644 --- a/src/app/(dashboard)/automations/page.tsx +++ b/src/app/(dashboard)/automations/page.tsx @@ -160,8 +160,8 @@ export default function AutomationsPage() {
    -

    Automations

    -

    +

    Automations

    +

    Build workflows that react to WhatsApp® events automatically.

    @@ -178,7 +178,7 @@ export default function AutomationsPage() { {showTemplates && (
    -

    Quick-start templates

    +

    Quick-start templates

    {TEMPLATE_ORDER.map((slug) => { const t = AUTOMATION_TEMPLATES[slug] @@ -187,13 +187,13 @@ export default function AutomationsPage() { ) })} @@ -202,12 +202,12 @@ export default function AutomationsPage() { )} {automations.length === 0 ? ( -
    +
    -

    No automations yet

    -

    +

    No automations yet

    +

    Pick a template above or create one from scratch.

    @@ -233,7 +233,7 @@ export default function AutomationsPage() { Delete automation This permanently removes{" "} - {pendingDelete?.name} and its execution + {pendingDelete?.name} and its execution history. This cannot be undone. @@ -277,7 +277,7 @@ function AutomationCard({ }) { const meta = triggerMeta(automation.trigger_type) return ( -
  • +
  • - + {automation.name} {automation.is_active && ( @@ -303,9 +303,9 @@ function AutomationCard({ )}
    {automation.description && ( -

    {automation.description}

    +

    {automation.description}

    )} -
    +
    diff --git a/src/app/(dashboard)/broadcasts/[id]/page.tsx b/src/app/(dashboard)/broadcasts/[id]/page.tsx index 7511806..7449d51 100644 --- a/src/app/(dashboard)/broadcasts/[id]/page.tsx +++ b/src/app/(dashboard)/broadcasts/[id]/page.tsx @@ -50,15 +50,15 @@ interface StatCardProps { function StatCard({ label, value, total, icon, color }: StatCardProps) { const pct = total > 0 ? Math.round((value / total) * 100) : 0; return ( -
    +
    {icon}
    - {pct}% + {pct}%
    -

    {value.toLocaleString()}

    -

    {label}

    +

    {value.toLocaleString()}

    +

    {label}

    ); } @@ -77,8 +77,8 @@ interface FunnelStep { function FunnelChart({ steps }: { steps: FunnelStep[] }) { const max = Math.max(...steps.map((s) => s.value), 1); return ( -
    -

    Funnel

    +
    +

    Funnel

    {steps.map((step) => { const pctOfMax = Math.max(5, Math.round((step.value / max) * 100)); @@ -88,17 +88,17 @@ function FunnelChart({ steps }: { steps: FunnelStep[] }) { : 0; return (
    - + {step.label} -
    +
    - + {step.value.toLocaleString()} - + ({pctOfSent}%) @@ -280,20 +280,20 @@ export default function BroadcastDetailPage() { variant="outline" size="icon" onClick={() => router.push('/broadcasts')} - className="border-slate-700" + className="border-border" >
    -

    {broadcast.name}

    +

    {broadcast.name}

    {status.label}
    -
    +
    Template: {broadcast.template_name} - @@ -315,7 +315,7 @@ export default function BroadcastDetailPage() { size="sm" onClick={() => setConfirmDelete(false)} 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 @@ -354,7 +354,7 @@ export default function BroadcastDetailPage() { value={broadcast.total_recipients} total={broadcast.total_recipients} icon={} - color="bg-slate-800 text-slate-300" + color="bg-muted text-muted-foreground" /> {/* Recipients Table */} -
    -
    -

    +
    +
    +

    Recipients ({filteredRecipients.length} {statusFilter !== 'all' ? ` of ${recipients.length}` : ''})

    @@ -409,7 +409,7 @@ export default function BroadcastDetailPage() {