Full light/dark theme toggle; brand-aware connector lines #29
+17
-12
@@ -11,23 +11,28 @@
|
|||||||
--font-serif: var(--font-fraunces), Georgia, "Times New Roman", ui-serif, serif;
|
--font-serif: var(--font-fraunces), Georgia, "Times New Roman", ui-serif, serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adaptive tokens — ink/paper flip for light/dark; bronze + paper are constant. */
|
/* Adaptive tokens — ink/paper flip for light/dark; bronze + paper are constant.
|
||||||
|
Theme is class-based (.dark on <html>) so it can be toggled manually; an inline
|
||||||
|
script in the root layout sets it pre-paint from the saved choice or the OS. */
|
||||||
:root {
|
:root {
|
||||||
--background: #f7f3ec;
|
--background: #f7f3ec;
|
||||||
--foreground: #1a1a17;
|
--foreground: #1a1a17;
|
||||||
--muted: #6b6862;
|
--muted: #6b6862;
|
||||||
--surface: #fffdf9;
|
--surface: #fffdf9;
|
||||||
--border: #e6ddcc;
|
--border: #e6ddcc;
|
||||||
|
/* Connector "lines between people" (pedigree + tree chart). Derived from Ink
|
||||||
|
(the brand mark color): a dark line on light, light on dark. */
|
||||||
|
--line: color-mix(in srgb, var(--foreground) 55%, transparent);
|
||||||
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
.dark {
|
||||||
:root {
|
--background: #161410;
|
||||||
--background: #161410;
|
--foreground: #f2eee6;
|
||||||
--foreground: #f2eee6;
|
--muted: #9a968e;
|
||||||
--muted: #9a968e;
|
--surface: #211d17;
|
||||||
--surface: #211d17;
|
--border: #353029;
|
||||||
--border: #353029;
|
color-scheme: dark;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -78,7 +83,7 @@ h3,
|
|||||||
left: -2.5rem;
|
left: -2.5rem;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.ped-leaf {
|
.ped-leaf {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -90,7 +95,7 @@ h3,
|
|||||||
left: 0;
|
left: 0;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.ped-leaf::after {
|
.ped-leaf::after {
|
||||||
content: "";
|
content: "";
|
||||||
@@ -98,7 +103,7 @@ h3,
|
|||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
border-left: 1px solid var(--border);
|
border-left: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.ped-leaf:first-child::after {
|
.ped-leaf:first-child::after {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
|||||||
@@ -18,9 +18,16 @@ export const metadata: Metadata = {
|
|||||||
icons: { icon: "/favicon.svg" },
|
icons: { icon: "/favicon.svg" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sets the theme class before first paint to avoid a flash; reads the saved
|
||||||
|
// choice ("light"/"dark"/"system") or falls back to the OS preference.
|
||||||
|
const themeScript = `(function(){try{var t=localStorage.getItem("theme");var d=t==="dark"||((!t||t==="system")&&window.matchMedia("(prefers-color-scheme: dark)").matches);document.documentElement.classList.toggle("dark",d);}catch(e){}})();`;
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={`${serif.variable} ${sans.variable}`}>
|
<html lang="en" className={`${serif.variable} ${sans.variable}`} suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||||
|
</head>
|
||||||
<body className="min-h-screen antialiased">{children}</body>
|
<body className="min-h-screen antialiased">{children}</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -530,6 +530,11 @@
|
|||||||
|
|
||||||
.f3 .link {
|
.f3 .link {
|
||||||
transition: stroke-width 0.2s ease-in-out;
|
transition: stroke-width 0.2s ease-in-out;
|
||||||
|
/* Brand-aware connector lines: dark on the light paper, light on dark. The
|
||||||
|
library's default white stroke is invisible on the light theme. */
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--line);
|
||||||
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.f3 .link.f3-path-to-main {
|
.f3 .link.f3-path-to-main {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
|
|
||||||
import { api } from "@/lib/api/client";
|
import { api } from "@/lib/api/client";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
|
|
||||||
export function AppSidebar({ onNavigate }: { onNavigate?: () => void }) {
|
export function AppSidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -136,7 +137,11 @@ export function AppSidebar({ onNavigate }: { onNavigate?: () => void }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div ref={menuRef} className="relative mt-auto">
|
<div className="mt-auto">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={menuRef} className="relative">
|
||||||
{menuOpen && (
|
{menuOpen && (
|
||||||
<div className="absolute bottom-full left-0 mb-2 w-full overflow-hidden rounded-lg border border-[var(--border)] bg-[var(--surface)] shadow-lg">
|
<div className="absolute bottom-full left-0 mb-2 w-full overflow-hidden rounded-lg border border-[var(--border)] bg-[var(--surface)] shadow-lg">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export function FanChart({
|
|||||||
<path
|
<path
|
||||||
d={sector(r0 + 1, r1 - 1, a0 + 0.004, a1 - 0.004)}
|
d={sector(r0 + 1, r1 - 1, a0 + 0.004, a1 - 0.004)}
|
||||||
fill={id ? "var(--surface)" : "transparent"}
|
fill={id ? "var(--surface)" : "transparent"}
|
||||||
stroke="var(--border)"
|
stroke="var(--line)"
|
||||||
/>
|
/>
|
||||||
{id && (
|
{id && (
|
||||||
<text
|
<text
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Monitor, Moon, Sun } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type Theme = "light" | "dark" | "system";
|
||||||
|
const ORDER: Theme[] = ["system", "light", "dark"];
|
||||||
|
const ICON = { system: Monitor, light: Sun, dark: Moon };
|
||||||
|
const LABEL = { system: "System", light: "Light", dark: "Dark" };
|
||||||
|
|
||||||
|
function apply(theme: Theme) {
|
||||||
|
const dark =
|
||||||
|
theme === "dark" ||
|
||||||
|
(theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||||
|
document.documentElement.classList.toggle("dark", dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cycles theme System → Light → Dark, persisting the choice in localStorage. */
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const [theme, setTheme] = useState<Theme>("system");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = (localStorage.getItem("theme") as Theme) || "system";
|
||||||
|
setTheme(saved);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Keep "system" in sync if the OS preference changes while selected.
|
||||||
|
useEffect(() => {
|
||||||
|
if (theme !== "system") return;
|
||||||
|
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
const onChange = () => apply("system");
|
||||||
|
mq.addEventListener("change", onChange);
|
||||||
|
return () => mq.removeEventListener("change", onChange);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
function cycle() {
|
||||||
|
const next = ORDER[(ORDER.indexOf(theme) + 1) % ORDER.length];
|
||||||
|
localStorage.setItem("theme", next);
|
||||||
|
setTheme(next);
|
||||||
|
apply(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = ICON[theme];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={cycle}
|
||||||
|
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-[var(--muted)] transition-colors hover:bg-bronze/[0.07] hover:text-[var(--foreground)]"
|
||||||
|
title={`Theme: ${LABEL[theme]} (click to change)`}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4 shrink-0" />
|
||||||
|
Theme: {LABEL[theme]}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user