wacrm/src/app/layout.tsx
Arnas Donauskas 4aa329b527
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) <noreply@anthropic.com>
2026-06-16 17:29:46 +03:00

114 lines
3.3 KiB
TypeScript

import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import Script from "next/script";
import "./globals.css";
import { ThemeProvider } from "@/hooks/use-theme";
import { ThemedToaster } from "@/components/themed-toaster";
import {
DEFAULT_MODE,
DEFAULT_THEME,
MODE_STORAGE_KEY,
MODES,
STORAGE_KEY,
THEME_IDS,
} from "@/lib/themes";
const inter = Inter({
variable: "--font-sans",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: {
default: "wacrm",
template: "%s — wacrm",
},
description: "Self-hostable CRM template for WhatsApp.",
robots: {
index: false,
follow: false,
},
icons: {
icon: [{ url: "/icon" }],
},
formatDetection: {
email: false,
address: false,
telephone: false,
},
};
export const viewport: Viewport = {
themeColor: "#020617",
colorScheme: "dark light",
};
// Inline boot script — runs before React hydrates so the user's
// chosen accent (data-theme) AND mode (data-mode) are on the <html>
// element before first paint. Without this every page load flashes
// the server-rendered defaults for a frame before the React tree
// mounts and applies the picked values.
//
// Kept dependency-free (no imports, no JSX) — must be a string the
// browser can run as a single <script>. Knowledge of valid ids is
// sourced from the THEME_IDS / MODES constants so adding one doesn't
// silently break the boot path.
const THEME_BOOT_SCRIPT = `
(function(){
var d = document.documentElement;
try {
var THEME_KEY = ${JSON.stringify(STORAGE_KEY)};
var THEME_DEFAULT = ${JSON.stringify(DEFAULT_THEME)};
var THEMES = ${JSON.stringify(THEME_IDS)};
var savedTheme = localStorage.getItem(THEME_KEY);
d.dataset.theme = THEMES.indexOf(savedTheme) !== -1 ? savedTheme : THEME_DEFAULT;
var MODE_KEY = ${JSON.stringify(MODE_STORAGE_KEY)};
var MODE_DEFAULT = ${JSON.stringify(DEFAULT_MODE)};
var MODES = ${JSON.stringify(MODES)};
var savedMode = localStorage.getItem(MODE_KEY);
d.dataset.mode = MODES.indexOf(savedMode) !== -1 ? savedMode : MODE_DEFAULT;
} catch (_e) {
d.dataset.theme = ${JSON.stringify(DEFAULT_THEME)};
d.dataset.mode = ${JSON.stringify(DEFAULT_MODE)};
}
})();
`;
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
data-theme={DEFAULT_THEME}
data-mode={DEFAULT_MODE}
className={`${inter.variable} h-full antialiased`}
// The `theme-boot` script below rewrites `data-theme` and
// `data-mode` on <html> from localStorage before React hydrates,
// so for any non-default choice the client DOM intentionally
// differs from the server-rendered defaults. suppressHydration-
// Warning silences the expected mismatch — it only applies to
// this element's own attributes, so genuine mismatches in
// children still surface.
suppressHydrationWarning
>
<head>
<Script
id="theme-boot"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ __html: THEME_BOOT_SCRIPT }}
/>
</head>
<body className="min-h-full bg-background text-foreground font-sans">
<ThemeProvider>
{children}
<ThemedToaster />
</ThemeProvider>
</body>
</html>
);
}