Skip to content

Commit 3975c2f

Browse files
feat(web): global day/night toggle via an inverse-colour overlay
Light mode applies filter: invert(1) hue-rotate(180deg) to <html> (re-inverting real media), toggled by a sun/moon button in the navbar and the editor header, persisted to localStorage with an anti-flash inline script in the layout.
1 parent 6c674e4 commit 3975c2f

5 files changed

Lines changed: 53 additions & 0 deletions

File tree

web/src/app/globals.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ html, body {
1010
overflow: hidden;
1111
}
1212

13+
/* Light mode: a global inverse-colour overlay. Invert the whole UI, then
14+
* re-invert real media so photos/diagrams keep their true colours. */
15+
html.light {
16+
filter: invert(1) hue-rotate(180deg);
17+
}
18+
html.light img,
19+
html.light video,
20+
html.light .no-invert {
21+
filter: invert(1) hue-rotate(180deg);
22+
}
23+
1324
/* ============================================
1425
* Astrolabe Color System CSS Variables
1526
* Synchronized with src/lib/colors.ts

web/src/app/layout.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ export default function RootLayout({
1818
}) {
1919
return (
2020
<html lang="en" className="h-full overflow-hidden">
21+
<head>
22+
<script
23+
dangerouslySetInnerHTML={{
24+
__html: "try{if(localStorage.getItem('theme')==='light')document.documentElement.classList.add('light')}catch(e){}",
25+
}}
26+
/>
27+
</head>
2128
<body className="h-full overflow-hidden">{children}</body>
2229
</html>
2330
);

web/src/app/local/edit/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import '@/lib/errorSuppression'
44
import { Suspense, useCallback, useEffect, useRef, useState } from 'react'
55
import { Bars3BottomLeftIcon, MinusIcon, PlusIcon, HomeIcon } from '@heroicons/react/24/outline'
66
import { API_BASE } from '@/lib/apiBase'
7+
import { ThemeToggle } from '@/components/ThemeToggle'
78
import { useViewStore } from '@/stores/viewStore'
89
import { useSearchParams, useRouter } from 'next/navigation'
910
import { Panel, PanelGroup, PanelResizeHandle, type ImperativePanelHandle } from 'react-resizable-panels'
@@ -84,6 +85,7 @@ function EditorPage() {
8485
<button onClick={() => useViewStore.getState().increaseFontSize()} className="p-1 text-white/30 hover:text-white/70 transition-colors rounded hover:bg-white/5" title="Increase font size">
8586
<PlusIcon className="w-3 h-3" />
8687
</button>
88+
<ThemeToggle className="p-1 ml-1 text-white/30 hover:text-white/70 transition-colors rounded hover:bg-white/5" />
8789
</div>
8890
</div>
8991

web/src/components/Navbar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Link from 'next/link'
2+
import { ThemeToggle } from './ThemeToggle'
23

34
/** Top navigation bar — gives the site a normal-website feel. */
45
export function Navbar() {
@@ -25,6 +26,7 @@ export function Navbar() {
2526
>
2627
GitHub
2728
</a>
29+
<ThemeToggle className="hover:text-white/75 transition-colors" />
2830
</div>
2931
</nav>
3032
)

web/src/components/ThemeToggle.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use client'
2+
3+
import { useEffect, useState } from 'react'
4+
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline'
5+
6+
/** Global day/night toggle. Light mode is a CSS inverse-colour overlay applied to
7+
* <html> (see globals.css); the choice is persisted to localStorage. */
8+
export function ThemeToggle({ className }: { className?: string }) {
9+
const [light, setLight] = useState(false)
10+
11+
useEffect(() => {
12+
setLight(document.documentElement.classList.contains('light'))
13+
}, [])
14+
15+
const toggle = () => {
16+
const next = !document.documentElement.classList.contains('light')
17+
document.documentElement.classList.toggle('light', next)
18+
try { localStorage.setItem('theme', next ? 'light' : 'dark') } catch { /* ignore */ }
19+
setLight(next)
20+
}
21+
22+
return (
23+
<button
24+
onClick={toggle}
25+
title={light ? 'Switch to dark mode' : 'Switch to light mode'}
26+
className={className ?? 'hover:text-white/75 transition-colors'}
27+
>
28+
{light ? <MoonIcon className="w-4 h-4" /> : <SunIcon className="w-4 h-4" />}
29+
</button>
30+
)
31+
}

0 commit comments

Comments
 (0)