Add dark mode toggle, PWA support, remove unused PdfViewer
This commit is contained in:
parent
287debeafc
commit
8f2c5ac074
@ -8,6 +8,10 @@
|
|||||||
# Compression
|
# Compression
|
||||||
encode gzip
|
encode gzip
|
||||||
|
|
||||||
|
# Service worker must not be cached
|
||||||
|
@sw path /sw.js
|
||||||
|
header @sw Cache-Control "no-cache, no-store, must-revalidate"
|
||||||
|
|
||||||
# Cache static assets
|
# Cache static assets
|
||||||
@static {
|
@static {
|
||||||
path *.jpg *.jpeg *.png *.gif *.ico *.css *.js *.pdf *.svg *.woff *.woff2
|
path *.jpg *.jpeg *.png *.gif *.ico *.css *.js *.pdf *.svg *.woff *.woff2
|
||||||
|
|||||||
@ -22,7 +22,6 @@
|
|||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-pdf": "^10.3.0",
|
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18"
|
"tailwindcss": "^4.1.18"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
site/public/icons/apple-touch-icon.png
Normal file
BIN
site/public/icons/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
site/public/icons/icon-192.png
Normal file
BIN
site/public/icons/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
site/public/icons/icon-512-maskable.png
Normal file
BIN
site/public/icons/icon-512-maskable.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
site/public/icons/icon-512.png
Normal file
BIN
site/public/icons/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
32
site/public/manifest.json
Normal file
32
site/public/manifest.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "Electronics Reference Library",
|
||||||
|
"short_name": "Mims Library",
|
||||||
|
"description": "Classic electronics reference notebooks from Forrest M. Mims III",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#f7f3ee",
|
||||||
|
"theme_color": "#3b5998",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-512-maskable.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/favicon.svg",
|
||||||
|
"sizes": "any",
|
||||||
|
"type": "image/svg+xml"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
66
site/public/offline.html
Normal file
66
site/public/offline.html
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Offline | Electronics Reference Library</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f7f3ee;
|
||||||
|
color: #2c3e5a;
|
||||||
|
font-family: Georgia, 'Times New Roman', serif;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
color: #6b7f99;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
max-width: 28rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
border: 1px solid #c5cdd8;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: white;
|
||||||
|
color: #2c3e5a;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #eef1f5;
|
||||||
|
border-color: #a0aec0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#3b5998" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
|
||||||
|
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1>You're offline</h1>
|
||||||
|
<p>Previously viewed pages and downloaded PDFs may still be available. Check your connection and try again.</p>
|
||||||
|
<button onclick="location.reload()">Try again</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
75
site/public/sw.js
Normal file
75
site/public/sw.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
const CACHE_NAME = 'mims-library-v1';
|
||||||
|
const PRECACHE_URLS = [
|
||||||
|
'/',
|
||||||
|
'/offline.html',
|
||||||
|
'/favicon.svg',
|
||||||
|
'/manifest.json'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Install: precache essential resources
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activate: clean old caches
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((keys) =>
|
||||||
|
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch: tiered caching strategy
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
const { request } = event;
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
// Only handle same-origin
|
||||||
|
if (url.origin !== location.origin) return;
|
||||||
|
|
||||||
|
// Static assets: cache-first
|
||||||
|
if (/\.(css|js|jpg|jpeg|png|gif|svg|woff|woff2|ico)$/.test(url.pathname)) {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(request).then((cached) =>
|
||||||
|
cached || fetch(request).then((response) => {
|
||||||
|
const clone = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDFs: network-first, cache on success
|
||||||
|
if (/\.pdf$/.test(url.pathname)) {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(request)
|
||||||
|
.then((response) => {
|
||||||
|
const clone = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => caches.match(request))
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML pages: network-first with offline fallback
|
||||||
|
event.respondWith(
|
||||||
|
fetch(request)
|
||||||
|
.then((response) => {
|
||||||
|
const clone = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() =>
|
||||||
|
caches.match(request).then((cached) => cached || caches.match('/offline.html'))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -1,106 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
|
|
||||||
interface PdfViewerProps {
|
|
||||||
pdfUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PdfViewer({ pdfUrl }: PdfViewerProps) {
|
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{/* Controls */}
|
|
||||||
<div className="flex items-center justify-between p-3 border-b border-border bg-muted/30">
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
||||||
<polyline points="14 2 14 8 20 8"/>
|
|
||||||
</svg>
|
|
||||||
<span>PDF Document</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsFullscreen(!isFullscreen)}
|
|
||||||
className="h-8"
|
|
||||||
>
|
|
||||||
{isFullscreen ? (
|
|
||||||
<>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-1.5">
|
|
||||||
<path d="M8 3v3a2 2 0 0 1-2 2H3"/>
|
|
||||||
<path d="M21 8h-3a2 2 0 0 1-2-2V3"/>
|
|
||||||
<path d="M3 16h3a2 2 0 0 1 2 2v3"/>
|
|
||||||
<path d="M16 21v-3a2 2 0 0 1 2-2h3"/>
|
|
||||||
</svg>
|
|
||||||
<span className="hidden sm:inline">Compact</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-1.5">
|
|
||||||
<path d="M8 3H5a2 2 0 0 0-2 2v3"/>
|
|
||||||
<path d="M21 8V5a2 2 0 0 0-2-2h-3"/>
|
|
||||||
<path d="M3 16v3a2 2 0 0 0 2 2h3"/>
|
|
||||||
<path d="M16 21h3a2 2 0 0 0 2-2v-3"/>
|
|
||||||
</svg>
|
|
||||||
<span className="hidden sm:inline">Expand</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href={pdfUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm border border-border rounded-md hover:bg-muted transition-colors"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
||||||
<polyline points="15 3 21 3 21 9"/>
|
|
||||||
<line x1="10" x2="21" y1="14" y2="3"/>
|
|
||||||
</svg>
|
|
||||||
<span className="hidden sm:inline">Open in new tab</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href={pdfUrl}
|
|
||||||
download
|
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
||||||
<polyline points="7 10 12 15 17 10"/>
|
|
||||||
<line x1="12" x2="12" y1="15" y2="3"/>
|
|
||||||
</svg>
|
|
||||||
<span className="hidden sm:inline">Download</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* PDF Embed */}
|
|
||||||
<div className={`bg-neutral-200 dark:bg-neutral-800 ${isFullscreen ? 'h-[85vh]' : 'h-[600px]'} transition-all duration-300`}>
|
|
||||||
<iframe
|
|
||||||
src={`${pdfUrl}#toolbar=1&navpanes=1&scrollbar=1`}
|
|
||||||
className="w-full h-full border-0"
|
|
||||||
title="PDF Viewer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Fallback message */}
|
|
||||||
<div className="p-4 text-center text-sm text-muted-foreground bg-muted/30 border-t border-border">
|
|
||||||
<p>
|
|
||||||
PDF not displaying? Try{' '}
|
|
||||||
<a href={pdfUrl} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
|
||||||
opening directly
|
|
||||||
</a>{' '}
|
|
||||||
or{' '}
|
|
||||||
<a href={pdfUrl} download className="text-primary hover:underline">
|
|
||||||
downloading the file
|
|
||||||
</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
85
site/src/components/ThemeToggle.tsx
Normal file
85
site/src/components/ThemeToggle.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Sun, Moon, Monitor } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark' | 'system';
|
||||||
|
|
||||||
|
export default function ThemeToggle() {
|
||||||
|
const [theme, setTheme] = useState<Theme>('system');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem('theme') as Theme | null;
|
||||||
|
if (stored) {
|
||||||
|
setTheme(stored);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
function applyTheme(t: Theme) {
|
||||||
|
if (t === 'dark') {
|
||||||
|
root.classList.add('dark');
|
||||||
|
} else if (t === 'light') {
|
||||||
|
root.classList.remove('dark');
|
||||||
|
} else {
|
||||||
|
// system
|
||||||
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
root.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
root.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTheme(theme);
|
||||||
|
|
||||||
|
if (theme === 'system') {
|
||||||
|
localStorage.removeItem('theme');
|
||||||
|
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const handler = () => applyTheme('system');
|
||||||
|
mq.addEventListener('change', handler);
|
||||||
|
return () => mq.removeEventListener('change', handler);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const icon = theme === 'dark' ? (
|
||||||
|
<Moon className="h-4 w-4" />
|
||||||
|
) : theme === 'light' ? (
|
||||||
|
<Sun className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Monitor className="h-4 w-4" />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8" aria-label="Toggle theme">
|
||||||
|
{icon}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||||
|
<Sun className="h-4 w-4" />
|
||||||
|
Light
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||||
|
<Moon className="h-4 w-4" />
|
||||||
|
Dark
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||||
|
<Monitor className="h-4 w-4" />
|
||||||
|
System
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import '@/styles/global.css';
|
import '@/styles/global.css';
|
||||||
|
import ThemeToggle from '@/components/ThemeToggle';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@ -16,7 +17,19 @@ const { title, description = "Classic electronics reference notebooks from Forre
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<meta name="theme-color" content="#3b5998" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
||||||
<title>{title} | Electronics Reference Library</title>
|
<title>{title} | Electronics Reference Library</title>
|
||||||
|
<script is:inline>
|
||||||
|
(function() {
|
||||||
|
const theme = localStorage.getItem('theme');
|
||||||
|
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen graph-paper-large">
|
<body class="min-h-screen graph-paper-large">
|
||||||
<header class="border-b border-border bg-card/80 backdrop-blur-sm sticky top-0 z-50">
|
<header class="border-b border-border bg-card/80 backdrop-blur-sm sticky top-0 z-50">
|
||||||
@ -48,6 +61,7 @@ const { title, description = "Classic electronics reference notebooks from Forre
|
|||||||
>
|
>
|
||||||
Ugly's
|
Ugly's
|
||||||
</a>
|
</a>
|
||||||
|
<ThemeToggle client:load />
|
||||||
<a
|
<a
|
||||||
href="https://archive.org"
|
href="https://archive.org"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -82,5 +96,12 @@ const { title, description = "Classic electronics reference notebooks from Forre
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
<script is:inline>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
navigator.serviceWorker.register('/sw.js');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -194,3 +194,35 @@
|
|||||||
background: oklch(0.55 0.15 145);
|
background: oklch(0.55 0.15 145);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark mode adjustments */
|
||||||
|
.dark .graph-paper-large {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, oklch(0.3 0.01 230 / 0.15) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, oklch(0.3 0.01 230 / 0.15) 1px, transparent 1px),
|
||||||
|
linear-gradient(to right, oklch(0.35 0.02 230 / 0.25) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, oklch(0.35 0.02 230 / 0.25) 1px, transparent 1px);
|
||||||
|
background-size: 10px 10px, 10px 10px, 50px 50px, 50px 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .book-card:hover {
|
||||||
|
box-shadow: 0 12px 24px -8px oklch(0 0 0 / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .book-cover {
|
||||||
|
border-color: oklch(1 0 0 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .circuit-border {
|
||||||
|
border-color: oklch(0.65 0.15 145);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .circuit-border::before,
|
||||||
|
.dark .circuit-border::after {
|
||||||
|
background: oklch(0.65 0.15 145);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth theme transition */
|
||||||
|
html {
|
||||||
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user