mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
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>
114 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|