add dark mode to website using prefers-color-scheme + .dark class

Inline script in <head> applies .dark class on <html> based on OS preference
and listens for live changes. All liveline page colors now use CSS variables
with dark overrides in .dark .liveline-page block.

- liveline.css: new vars (--ll-text-muted, --ll-text-hover, --ll-divider,
  --ll-code-line-nr, --ll-btn-bg/shadow, --ll-fade-*) + full dark overrides
- liveline-prism.css: GitHub Dark syntax highlighting theme under .dark
- liveline.tsx: ~30 hardcoded rgba/hex colors replaced with CSS vars
- root.tsx: anti-FOUC script toggling .dark from prefers-color-scheme
This commit is contained in:
Tommy D. Rossi
2026-02-19 18:02:07 +01:00
parent 35c885b199
commit 4dad886f14
4 changed files with 207 additions and 34 deletions
+10 -1
View File
@@ -11,10 +11,19 @@ import {
export function Layout({ children }: { children: React.ReactNode }) { export function Layout({ children }: { children: React.ReactNode }) {
return ( return (
<html className="" lang="en"> <html className="" lang="en" suppressHydrationWarning>
<head> <head>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
{/*
Apply .dark class based on OS prefers-color-scheme.
Inline in <head> to prevent FOUC. Listener handles live OS changes.
*/}
<script
dangerouslySetInnerHTML={{
__html: `(function(){var d=document.documentElement.classList,m=matchMedia('(prefers-color-scheme:dark)');function a(){d.toggle('dark',m.matches)}a();m.addEventListener('change',a)})()`,
}}
/>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link <link
rel="preconnect" rel="preconnect"
+21 -20
View File
@@ -15,6 +15,7 @@ import "prismjs/components/prism-bash";
import "website/src/styles/liveline.css"; import "website/src/styles/liveline.css";
import "website/src/styles/liveline-prism.css"; import "website/src/styles/liveline-prism.css";
/* ========================================================================= /* =========================================================================
TOC sidebar (fixed left) TOC sidebar (fixed left)
========================================================================= */ ========================================================================= */
@@ -109,15 +110,15 @@ function TableOfContents() {
lineHeight: "20px", lineHeight: "20px",
letterSpacing: "-0.09px", letterSpacing: "-0.09px",
padding: "4px 0", padding: "4px 0",
color: "rgba(0, 0, 0, 0.4)", color: "var(--ll-text-secondary)",
fontFamily: "var(--ll-font-primary)", fontFamily: "var(--ll-font-primary)",
marginBottom: "8px", marginBottom: "8px",
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.color = "rgba(0, 0, 0, 0.7)"; e.currentTarget.style.color = "var(--ll-text-hover)";
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.color = "rgba(0, 0, 0, 0.4)"; e.currentTarget.style.color = "var(--ll-text-secondary)";
}} }}
> >
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
@@ -133,7 +134,7 @@ function TableOfContents() {
</a> </a>
{tocItems.map((item) => { {tocItems.map((item) => {
const isActive = `#${activeId}` === item.href; const isActive = `#${activeId}` === item.href;
const defaultColor = isActive ? "rgba(0, 0, 0, 0.9)" : "rgba(0, 0, 0, 0.4)"; const defaultColor = isActive ? "var(--ll-text-primary)" : "var(--ll-text-secondary)";
return ( return (
<a <a
key={item.href} key={item.href}
@@ -151,7 +152,7 @@ function TableOfContents() {
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!isActive) { if (!isActive) {
e.currentTarget.style.color = "rgba(0, 0, 0, 0.7)"; e.currentTarget.style.color = "var(--ll-text-hover)";
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
@@ -175,19 +176,19 @@ function BackButton() {
return ( return (
<a <a
href="/" href="/"
className="fixed top-5 right-5 z-[100000] flex items-center justify-center w-10 h-10 rounded-full bg-white no-underline" className="fixed top-5 right-5 z-[100000] flex items-center justify-center w-10 h-10 rounded-full no-underline"
style={{ style={{
color: "rgba(0, 0, 0, 0.4)", background: "var(--ll-btn-bg)",
boxShadow: color: "var(--ll-text-secondary)",
"rgba(0, 0, 0, 0.08) 0px 2px 8px, rgba(0, 0, 0, 0.04) 0px 4px 16px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px inset", boxShadow: "var(--ll-btn-shadow)",
transition: "color 0.15s, transform 0.15s", transition: "color 0.15s, transform 0.15s",
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.color = "rgba(0, 0, 0, 0.7)"; e.currentTarget.style.color = "var(--ll-text-hover)";
e.currentTarget.style.transform = "scale(1.05)"; e.currentTarget.style.transform = "scale(1.05)";
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.color = "rgba(0, 0, 0, 0.4)"; e.currentTarget.style.color = "var(--ll-text-secondary)";
e.currentTarget.style.transform = "scale(1)"; e.currentTarget.style.transform = "scale(1)";
}} }}
onMouseDown={(e) => { onMouseDown={(e) => {
@@ -264,7 +265,7 @@ function Caption({ children }: { children: React.ReactNode }) {
fontWeight: 475, fontWeight: 475,
lineHeight: "20px", lineHeight: "20px",
letterSpacing: "-0.09px", letterSpacing: "-0.09px",
color: "rgba(0, 0, 0, 0.4)", color: "var(--ll-text-secondary)",
margin: 0, margin: 0,
}} }}
> >
@@ -278,7 +279,7 @@ function Divider() {
<div <div
style={{ style={{
height: "1px", height: "1px",
background: "rgb(242, 242, 242)", background: "var(--ll-divider)",
margin: 0, margin: 0,
}} }}
/> />
@@ -358,7 +359,7 @@ function CodeBlock({ children, lang = "jsx" }: { children: string; lang?: string
className="select-none shrink-0" className="select-none shrink-0"
aria-hidden="true" aria-hidden="true"
style={{ style={{
color: "rgba(0, 0, 0, 0.15)", color: "var(--ll-code-line-nr)",
textAlign: "right", textAlign: "right",
paddingRight: "20px", paddingRight: "20px",
minWidth: "28px", minWidth: "28px",
@@ -487,7 +488,7 @@ function PropsTable({
fontFamily: "var(--ll-font-primary)", fontFamily: "var(--ll-font-primary)",
fontSize: "11px", fontSize: "11px",
fontWeight: 400, fontWeight: 400,
color: "rgba(0, 0, 0, 0.35)", color: "var(--ll-text-muted)",
textTransform: "uppercase", textTransform: "uppercase",
letterSpacing: "0.02em", letterSpacing: "0.02em",
padding: "0 0 6px", padding: "0 0 6px",
@@ -515,7 +516,7 @@ function PropsTable({
fontSize: "11px", fontSize: "11px",
fontWeight: 400, fontWeight: 400,
fontFamily: "var(--ll-font-primary)", fontFamily: "var(--ll-font-primary)",
color: "rgba(0, 0, 0, 0.35)", color: "var(--ll-text-muted)",
borderBottom: "1px solid var(--ll-border)", borderBottom: "1px solid var(--ll-border)",
}} }}
> >
@@ -548,7 +549,7 @@ function PropsTable({
fontSize: "11px", fontSize: "11px",
fontWeight: 400, fontWeight: 400,
fontFamily: "var(--ll-font-code)", fontFamily: "var(--ll-font-code)",
color: "rgba(0, 0, 0, 0.35)", color: "var(--ll-text-muted)",
borderBottom: "1px solid var(--ll-border)", borderBottom: "1px solid var(--ll-border)",
whiteSpace: "nowrap", whiteSpace: "nowrap",
}} }}
@@ -561,7 +562,7 @@ function PropsTable({
fontSize: "11px", fontSize: "11px",
fontWeight: 400, fontWeight: 400,
fontFamily: "var(--ll-font-code)", fontFamily: "var(--ll-font-code)",
color: "rgba(0, 0, 0, 0.35)", color: "var(--ll-text-muted)",
borderBottom: "1px solid var(--ll-border)", borderBottom: "1px solid var(--ll-border)",
}} }}
> >
@@ -608,7 +609,7 @@ export default function LivelinePage() {
fontWeight: 475, fontWeight: 475,
lineHeight: "15.6px", lineHeight: "15.6px",
letterSpacing: "-0.04px", letterSpacing: "-0.04px",
color: "rgba(18, 18, 18, 0.4)", color: "var(--ll-text-secondary)",
margin: 0, margin: 0,
padding: "0 0 16px", padding: "0 0 16px",
}} }}
@@ -622,7 +623,7 @@ export default function LivelinePage() {
fontWeight: 400, fontWeight: 400,
lineHeight: "15.6px", lineHeight: "15.6px",
letterSpacing: "-0.04px", letterSpacing: "-0.04px",
color: "rgba(18, 18, 18, 0.25)", color: "var(--ll-text-tertiary)",
}} }}
> >
2025 2025
+91
View File
@@ -133,3 +133,94 @@ pre[class*="language-"] {
.token { .token {
text-shadow: none; text-shadow: none;
} }
/* ======================================================================
Dark mode - GitHub Dark inspired palette
====================================================================== */
.dark code[class*="language-"],
.dark pre[class*="language-"] {
color: #e6edf3;
}
.dark .token.keyword,
.dark .token.module,
.dark .token.imports,
.dark .token.control-flow {
color: #ff7b72;
}
.dark .token.string,
.dark .token.template-string,
.dark .token.attr-value {
color: #a5d6ff;
}
.dark .token.function,
.dark .token.class-name {
color: #d2a8ff;
}
.dark .token.property,
.dark .token.parameter {
color: #ffa657;
}
.dark .token.tag .token.tag {
color: #7ee787;
}
.dark .token.tag .token.class-name {
color: #7ee787;
}
.dark .token.tag .token.attr-name,
.dark .token.attr-name {
color: #79c0ff;
}
.dark .token.number {
color: #79c0ff;
}
.dark .token.operator {
color: #ff7b72;
}
.dark .token.punctuation {
color: #e6edf3;
}
.dark .token.boolean,
.dark .token.constant,
.dark .token.builtin {
color: #79c0ff;
}
.dark .token.comment,
.dark .token.prolog,
.dark .token.doctype,
.dark .token.cdata {
color: #8b949e;
font-style: italic;
}
.dark .token.tag .token.punctuation {
color: #e6edf3;
}
.dark .token.spread {
color: #ff7b72;
}
.dark .token.plain-text {
color: #e6edf3;
}
.dark .token.regex {
color: #a5d6ff;
}
.dark .language-bash .token.function {
color: #79c0ff;
}
+85 -13
View File
@@ -46,11 +46,21 @@
--ll-text-primary: rgb(17, 17, 17); --ll-text-primary: rgb(17, 17, 17);
--ll-text-secondary: rgba(0, 0, 0, 0.4); --ll-text-secondary: rgba(0, 0, 0, 0.4);
--ll-text-tertiary: rgba(0, 0, 0, 0.25); --ll-text-tertiary: rgba(0, 0, 0, 0.25);
--ll-text-muted: rgba(0, 0, 0, 0.35);
--ll-text-hover: rgba(0, 0, 0, 0.7);
--ll-bg: #fff; --ll-bg: #fff;
--ll-border: #e3e3e3; --ll-border: #e3e3e3;
--ll-divider: rgb(242, 242, 242);
--ll-code-bg: rgba(0, 0, 0, 0.02); --ll-code-bg: rgba(0, 0, 0, 0.02);
--ll-code-line-nr: rgba(0, 0, 0, 0.15);
--ll-selection-bg: rgba(0, 0, 0, 0.08); --ll-selection-bg: rgba(0, 0, 0, 0.08);
/* Button / back button */
--ll-btn-bg: #fff;
--ll-btn-shadow: rgba(0, 0, 0, 0.08) 0px 2px 8px,
rgba(0, 0, 0, 0.04) 0px 4px 16px,
rgba(0, 0, 0, 0.06) 0px 0px 0px 1px inset;
/* P3 accent colors (wider gamut) */ /* P3 accent colors (wider gamut) */
--ll-primary: #3e9fff; --ll-primary: #3e9fff;
--ll-secondary: #f09637; --ll-secondary: #f09637;
@@ -65,6 +75,68 @@
0 0.3125rem 0.5rem rgba(0, 0, 0, 0.02), 0 0.3125rem 0.5rem rgba(0, 0, 0, 0.02),
0 0.125rem 0.25rem rgba(0, 0, 0, 0.016), 0 0.125rem 0.25rem rgba(0, 0, 0, 0.016),
0 0 0.125rem rgba(0, 0, 0, 0.01); 0 0 0.125rem rgba(0, 0, 0, 0.01);
/* Header fade gradient stops (white) */
--ll-fade-0: rgb(255, 255, 255);
--ll-fade-1: rgba(255, 255, 255, 0.737);
--ll-fade-2: rgba(255, 255, 255, 0.541);
--ll-fade-3: rgba(255, 255, 255, 0.382);
--ll-fade-4: rgba(255, 255, 255, 0.278);
--ll-fade-5: rgba(255, 255, 255, 0.194);
--ll-fade-6: rgba(255, 255, 255, 0.126);
--ll-fade-7: rgba(255, 255, 255, 0.075);
--ll-fade-8: rgba(255, 255, 255, 0.042);
--ll-fade-9: rgba(255, 255, 255, 0.021);
--ll-fade-10: rgba(255, 255, 255, 0.008);
--ll-fade-11: rgba(255, 255, 255, 0.002);
--ll-fade-12: rgba(255, 255, 255, 0);
}
/* Dark mode overrides */
.dark .liveline-page {
--ll-text-primary: rgba(255, 255, 255, 0.9);
--ll-text-secondary: rgba(255, 255, 255, 0.45);
--ll-text-tertiary: rgba(255, 255, 255, 0.25);
--ll-text-muted: rgba(255, 255, 255, 0.35);
--ll-text-hover: rgba(255, 255, 255, 0.7);
--ll-bg: rgb(17, 17, 17);
--ll-border: rgba(255, 255, 255, 0.1);
--ll-divider: rgba(255, 255, 255, 0.06);
--ll-code-bg: rgba(255, 255, 255, 0.05);
--ll-code-line-nr: rgba(255, 255, 255, 0.15);
--ll-selection-bg: rgba(255, 255, 255, 0.1);
--ll-btn-bg: rgb(30, 30, 30);
--ll-btn-shadow: rgba(255, 255, 255, 0.05) 0px 2px 8px,
rgba(255, 255, 255, 0.02) 0px 4px 16px,
rgba(255, 255, 255, 0.06) 0px 0px 0px 1px inset;
--ll-primary: #60a5fa;
--ll-secondary: #f5a623;
--ll-overlay-bg: hsla(0, 0%, 7%, 0.8);
--ll-overlay-shadow: 0 0 0 1px rgba(255, 255, 255, 0.06),
0 1.625rem 3.375rem rgba(0, 0, 0, 0.3),
0 1rem 2rem rgba(0, 0, 0, 0.2),
0 0.625rem 1rem rgba(0, 0, 0, 0.15),
0 0.3125rem 0.5rem rgba(0, 0, 0, 0.1),
0 0.125rem 0.25rem rgba(0, 0, 0, 0.08),
0 0 0.125rem rgba(0, 0, 0, 0.05);
/* Header fade gradient stops (dark) */
--ll-fade-0: rgb(17, 17, 17);
--ll-fade-1: rgba(17, 17, 17, 0.737);
--ll-fade-2: rgba(17, 17, 17, 0.541);
--ll-fade-3: rgba(17, 17, 17, 0.382);
--ll-fade-4: rgba(17, 17, 17, 0.278);
--ll-fade-5: rgba(17, 17, 17, 0.194);
--ll-fade-6: rgba(17, 17, 17, 0.126);
--ll-fade-7: rgba(17, 17, 17, 0.075);
--ll-fade-8: rgba(17, 17, 17, 0.042);
--ll-fade-9: rgba(17, 17, 17, 0.021);
--ll-fade-10: rgba(17, 17, 17, 0.008);
--ll-fade-11: rgba(17, 17, 17, 0.002);
--ll-fade-12: rgba(17, 17, 17, 0);
} }
@supports (color: color(display-p3 1 1 1)) { @supports (color: color(display-p3 1 1 1)) {
@@ -130,18 +202,18 @@
right: 0; right: 0;
height: 8rem; height: 8rem;
background: linear-gradient( background: linear-gradient(
rgb(255, 255, 255) 0px, var(--ll-fade-0) 0px,
rgba(255, 255, 255, 0.737) 19%, var(--ll-fade-1) 19%,
rgba(255, 255, 255, 0.541) 34%, var(--ll-fade-2) 34%,
rgba(255, 255, 255, 0.382) 47%, var(--ll-fade-3) 47%,
rgba(255, 255, 255, 0.278) 56.5%, var(--ll-fade-4) 56.5%,
rgba(255, 255, 255, 0.194) 65%, var(--ll-fade-5) 65%,
rgba(255, 255, 255, 0.126) 73%, var(--ll-fade-6) 73%,
rgba(255, 255, 255, 0.075) 80.2%, var(--ll-fade-7) 80.2%,
rgba(255, 255, 255, 0.042) 86.1%, var(--ll-fade-8) 86.1%,
rgba(255, 255, 255, 0.021) 91%, var(--ll-fade-9) 91%,
rgba(255, 255, 255, 0.008) 95.2%, var(--ll-fade-10) 95.2%,
rgba(255, 255, 255, 0.002) 98.2%, var(--ll-fade-11) 98.2%,
rgba(255, 255, 255, 0) 100% var(--ll-fade-12) 100%
); );
} }