mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 02:53:51 +00:00
Add darkbox palette themes
This commit is contained in:
@@ -117,7 +117,14 @@ const Stats = () => {
|
||||
|
||||
<style jsx>{`
|
||||
.bg-gradient-text {
|
||||
background: linear-gradient(90deg, #fbbf24, #f59e0b, #d97706, #b45309, #f59e0b, #fbbf24);
|
||||
background: linear-gradient(90deg,
|
||||
rgb(var(--color-yellow-bright)),
|
||||
rgb(var(--color-orange-bright)),
|
||||
rgb(var(--color-orange)),
|
||||
rgb(var(--color-yellow)),
|
||||
rgb(var(--color-orange-bright)),
|
||||
rgb(var(--color-yellow-bright))
|
||||
);
|
||||
background-size: 200% auto;
|
||||
color: transparent;
|
||||
background-clip: text;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
|
||||
interface Cell {
|
||||
alive: boolean;
|
||||
next: boolean;
|
||||
@@ -58,9 +59,46 @@ const RIPPLE_SPEED = 0.02; // Speed of ripple propagation
|
||||
const RIPPLE_ELEVATION_FACTOR = 4; // Height of ripple wave
|
||||
const ELEVATION_FACTOR = 8; // Max height for 3D effect - reduced for more subtle effect
|
||||
|
||||
const Background: React.FC<BackgroundProps> = ({
|
||||
const FALLBACK_PALETTE: [number, number, number][] = [
|
||||
[204, 36, 29], [152, 151, 26], [215, 153, 33],
|
||||
[69, 133, 136], [177, 98, 134], [104, 157, 106]
|
||||
];
|
||||
|
||||
// Read palette from current CSS variables
|
||||
function readPaletteFromCSS(): [number, number, number][] {
|
||||
try {
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
const keys = ["--color-red", "--color-green", "--color-yellow", "--color-blue", "--color-purple", "--color-aqua"];
|
||||
const palette: [number, number, number][] = [];
|
||||
for (const key of keys) {
|
||||
const val = style.getPropertyValue(key).trim();
|
||||
if (val) {
|
||||
const parts = val.split(" ").map(Number);
|
||||
if (parts.length === 3 && parts.every((n) => !isNaN(n))) {
|
||||
palette.push([parts[0], parts[1], parts[2]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return palette.length > 0 ? palette : FALLBACK_PALETTE;
|
||||
} catch {
|
||||
return FALLBACK_PALETTE;
|
||||
}
|
||||
}
|
||||
|
||||
function readBgFromCSS(): string {
|
||||
try {
|
||||
const val = getComputedStyle(document.documentElement).getPropertyValue("--color-background").trim();
|
||||
if (val) {
|
||||
const [r, g, b] = val.split(" ");
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
} catch {}
|
||||
return "rgb(0, 0, 0)";
|
||||
}
|
||||
|
||||
const Background: React.FC<BackgroundProps> = ({
|
||||
layout = 'index',
|
||||
position = 'left'
|
||||
position = 'left'
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const gridRef = useRef<Grid>();
|
||||
@@ -68,6 +106,8 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
const lastUpdateTimeRef = useRef<number>(0);
|
||||
const lastCycleTimeRef = useRef<number>(0);
|
||||
const resizeTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const paletteRef = useRef<[number, number, number][]>(FALLBACK_PALETTE);
|
||||
const bgColorRef = useRef<string>("rgb(0, 0, 0)");
|
||||
const mouseRef = useRef<MousePosition>({
|
||||
x: -1000,
|
||||
y: -1000,
|
||||
@@ -78,15 +118,8 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
});
|
||||
|
||||
const randomColor = (): [number, number, number] => {
|
||||
const colors = [
|
||||
[204, 36, 29], // red
|
||||
[152, 151, 26], // green
|
||||
[215, 153, 33], // yellow
|
||||
[69, 133, 136], // blue
|
||||
[177, 98, 134], // purple
|
||||
[104, 157, 106] // aqua
|
||||
];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
const palette = paletteRef.current;
|
||||
return palette[Math.floor(Math.random() * palette.length)];
|
||||
};
|
||||
|
||||
const getCellSize = () => {
|
||||
@@ -515,7 +548,7 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
gridRef.current.cols !== Math.floor(displayWidth / cellSize) ||
|
||||
gridRef.current.rows !== Math.floor(displayHeight / cellSize)) {
|
||||
gridRef.current = initGrid(displayWidth, displayHeight);
|
||||
}
|
||||
}
|
||||
}, 250);
|
||||
};
|
||||
|
||||
@@ -535,6 +568,31 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
window.addEventListener('mousemove', handleMouseMove, { signal });
|
||||
window.addEventListener('mouseup', handleMouseUp, { signal });
|
||||
|
||||
// Read theme colors from CSS variables
|
||||
paletteRef.current = readPaletteFromCSS();
|
||||
bgColorRef.current = readBgFromCSS();
|
||||
|
||||
// Listen for theme changes and update colors
|
||||
const handleThemeChanged = () => {
|
||||
paletteRef.current = readPaletteFromCSS();
|
||||
bgColorRef.current = readBgFromCSS();
|
||||
|
||||
if (gridRef.current) {
|
||||
const grid = gridRef.current;
|
||||
const palette = paletteRef.current;
|
||||
for (let i = 0; i < grid.cols; i++) {
|
||||
for (let j = 0; j < grid.rows; j++) {
|
||||
const cell = grid.cells[i][j];
|
||||
if (cell.alive && cell.opacity > 0.01) {
|
||||
cell.baseColor = palette[Math.floor(Math.random() * palette.length)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("theme-changed", handleThemeChanged, { signal });
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
// Tab is hidden
|
||||
@@ -584,7 +642,7 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
}
|
||||
|
||||
// Draw frame
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.fillStyle = bgColorRef.current;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (gridRef.current) {
|
||||
@@ -701,10 +759,10 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
<div className={getContainerClasses()}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full bg-black"
|
||||
className="w-full h-full bg-background"
|
||||
style={{ cursor: 'default' }} // Changed from cursor-pointer to default
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm pointer-events-none" />
|
||||
<div className="absolute inset-0 bg-background/30 backdrop-blur-sm pointer-events-none" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Links } from "@/components/header/links";
|
||||
|
||||
export default function Header() {
|
||||
export default function Header({ transparent = false }: { transparent?: boolean }) {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [lastScrollY, setLastScrollY] = useState(0);
|
||||
@@ -34,7 +34,7 @@ export default function Header() {
|
||||
return linkHref !== "/" && path.startsWith(linkHref);
|
||||
};
|
||||
|
||||
const isIndexPage = checkIsActive("/");
|
||||
const isIndexPage = transparent || checkIsActive("/");
|
||||
const headerLinks = Links.map((link) => {
|
||||
const isActive = checkIsActive(link.href);
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function Header() {
|
||||
className={`
|
||||
relative inline-block
|
||||
${link.color}
|
||||
${!isIndexPage ? 'bg-black' : ''}
|
||||
${!isIndexPage ? 'bg-background' : ''}
|
||||
`}
|
||||
>
|
||||
<a
|
||||
@@ -94,13 +94,13 @@ export default function Header() {
|
||||
<div className={`
|
||||
w-full flex flex-row items-center justify-center
|
||||
pointer-events-none
|
||||
${!isIndexPage ? 'bg-black md:bg-transparent' : ''}
|
||||
${!isIndexPage ? 'bg-background md:bg-transparent' : ''}
|
||||
`}>
|
||||
<div className={`
|
||||
w-full md:w-auto flex flex-row pt-1 px-2 text-lg lg:text-3xl md:text-2xl
|
||||
items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20 md:py-2
|
||||
pointer-events-none [&_a]:pointer-events-auto
|
||||
${!isIndexPage ? 'bg-black md:px-20' : ''}
|
||||
${!isIndexPage ? 'bg-background md:px-20' : ''}
|
||||
`}>
|
||||
{headerLinks}
|
||||
</div>
|
||||
|
||||
98
src/src/components/theme-switcher/index.tsx
Normal file
98
src/src/components/theme-switcher/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { getStoredThemeId, getNextTheme, applyTheme } from "@/lib/themes/engine";
|
||||
|
||||
const FADE_DURATION = 300;
|
||||
|
||||
const LABELS: Record<string, string> = {
|
||||
darkbox: "classic",
|
||||
"darkbox-retro": "retro",
|
||||
"darkbox-dim": "dim",
|
||||
};
|
||||
|
||||
export default function ThemeSwitcher() {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const [nextLabel, setNextLabel] = useState("");
|
||||
|
||||
const maskRef = useRef<HTMLDivElement>(null);
|
||||
const animatingRef = useRef(false);
|
||||
const committedRef = useRef("");
|
||||
|
||||
useEffect(() => {
|
||||
committedRef.current = getStoredThemeId();
|
||||
setNextLabel(LABELS[getNextTheme(committedRef.current).id] ?? "");
|
||||
|
||||
const handleSwap = () => {
|
||||
const id = getStoredThemeId();
|
||||
applyTheme(id);
|
||||
committedRef.current = id;
|
||||
setNextLabel(LABELS[getNextTheme(id).id] ?? "");
|
||||
};
|
||||
|
||||
document.addEventListener("astro:after-swap", handleSwap);
|
||||
return () => {
|
||||
document.removeEventListener("astro:after-swap", handleSwap);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleClick = () => {
|
||||
if (animatingRef.current) return;
|
||||
animatingRef.current = true;
|
||||
|
||||
const mask = maskRef.current;
|
||||
if (!mask) return;
|
||||
|
||||
const v = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--color-background")
|
||||
.trim();
|
||||
const [r, g, b] = v.split(" ").map(Number);
|
||||
|
||||
mask.style.backgroundColor = `rgb(${r},${g},${b})`;
|
||||
mask.style.opacity = "1";
|
||||
mask.style.visibility = "visible";
|
||||
mask.style.transition = "none";
|
||||
|
||||
const next = getNextTheme(committedRef.current);
|
||||
applyTheme(next.id);
|
||||
committedRef.current = next.id;
|
||||
setNextLabel(LABELS[getNextTheme(next.id).id] ?? "");
|
||||
|
||||
mask.offsetHeight;
|
||||
|
||||
mask.style.transition = `opacity ${FADE_DURATION}ms ease-out`;
|
||||
mask.style.opacity = "0";
|
||||
|
||||
const onEnd = () => {
|
||||
mask.removeEventListener("transitionend", onEnd);
|
||||
mask.style.visibility = "hidden";
|
||||
mask.style.transition = "none";
|
||||
animatingRef.current = false;
|
||||
};
|
||||
|
||||
mask.addEventListener("transitionend", onEnd);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="fixed bottom-4 right-4 z-[101] pointer-events-auto hidden md:block"
|
||||
onMouseEnter={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
onClick={handleClick}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<span
|
||||
className="text-foreground font-bold text-sm select-none transition-opacity duration-200"
|
||||
style={{ opacity: hovering ? 0.8 : 0.15 }}
|
||||
>
|
||||
{nextLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={maskRef}
|
||||
className="fixed inset-0 z-[100] pointer-events-none"
|
||||
style={{ visibility: "hidden", opacity: 0 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { ClientRouter } from "astro:transitions";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import Background from "@/components/background";
|
||||
import ThemeSwitcher from "@/components/theme-switcher";
|
||||
import { THEME_LOADER_SCRIPT, THEME_NAV_SCRIPT } from "@/lib/themes/loader";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
@@ -20,20 +22,16 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
<title>{title}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<!-- OpenGraph -->
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<!-- Basic meta description for search engines -->
|
||||
<meta name="description" content={description} />
|
||||
<!-- Also used in OpenGraph for social media sharing -->
|
||||
<meta property="og:description" content={description} />
|
||||
<link rel="icon" type="image/jpeg" href="/me.jpeg" />
|
||||
<ClientRouter
|
||||
<ClientRouter
|
||||
defaultTransition={false}
|
||||
handleFocus={false}
|
||||
/>
|
||||
@@ -41,7 +39,6 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
::view-transition-new(:root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
::view-transition-old(:root) {
|
||||
animation: 90ms ease-out both fade-out;
|
||||
}
|
||||
@@ -50,6 +47,7 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
to { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
<script is:inline set:html={THEME_LOADER_SCRIPT} />
|
||||
</head>
|
||||
<body class="bg-background text-foreground min-h-screen flex flex-col">
|
||||
<Header client:load />
|
||||
@@ -65,10 +63,7 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
<div class="mt-auto">
|
||||
<Footer client:load transition:persist />
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener("astro:after-navigation", () => {
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
</script>
|
||||
<ThemeSwitcher client:only="react" transition:persist />
|
||||
<script is:inline set:html={`window.scrollTo(0,0);` + THEME_NAV_SCRIPT} />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { ClientRouter } from "astro:transitions";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import Background from "@/components/background";
|
||||
import ThemeSwitcher from "@/components/theme-switcher";
|
||||
import { THEME_LOADER_SCRIPT, THEME_NAV_SCRIPT } from "@/lib/themes/loader";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
@@ -23,28 +25,27 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
<title>{title}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<!-- OpenGraph -->
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<!-- Basic meta description for search engines -->
|
||||
<meta name="description" content={description} />
|
||||
<!-- Also used in OpenGraph for social media sharing -->
|
||||
<meta property="og:description" content={description} />
|
||||
<link rel="icon" type="image/jpeg" href="/me.jpeg" />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<ClientRouter />
|
||||
<script is:inline set:html={THEME_LOADER_SCRIPT} />
|
||||
</head>
|
||||
<body class="bg-background text-foreground">
|
||||
<Header client:load />
|
||||
<Header client:load transparent />
|
||||
<main transition:animate="fade">
|
||||
<Background layout="index" client:only="react" transition:persist />
|
||||
<slot />
|
||||
</main>
|
||||
<Footer client:load transition:persist fixed=true />
|
||||
<ThemeSwitcher client:only="react" transition:persist />
|
||||
<script is:inline set:html={THEME_NAV_SCRIPT} />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,6 +5,8 @@ import { ClientRouter } from "astro:transitions";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import Background from "@/components/background";
|
||||
import ThemeSwitcher from "@/components/theme-switcher";
|
||||
import { THEME_LOADER_SCRIPT, THEME_NAV_SCRIPT } from "@/lib/themes/loader";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
@@ -20,20 +22,16 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
<title>{title}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<!-- OpenGraph -->
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<!-- Basic meta description for search engines -->
|
||||
<meta name="description" content={description} />
|
||||
<!-- Also used in OpenGraph for social media sharing -->
|
||||
<meta property="og:description" content={description} />
|
||||
<link rel="icon" type="image/jpeg" href="/me.jpeg" />
|
||||
<ClientRouter
|
||||
<ClientRouter
|
||||
defaultTransition={false}
|
||||
handleFocus={false}
|
||||
/>
|
||||
@@ -41,7 +39,6 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
::view-transition-new(:root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
::view-transition-old(:root) {
|
||||
animation: 90ms ease-out both fade-out;
|
||||
}
|
||||
@@ -50,6 +47,7 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
to { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
<script is:inline set:html={THEME_LOADER_SCRIPT} />
|
||||
</head>
|
||||
<body class="bg-background text-foreground min-h-screen flex flex-col">
|
||||
<main class="flex-1 flex flex-col">
|
||||
@@ -61,10 +59,7 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
<Background layout="content" position="left" client:only="react" transition:persist />
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
document.addEventListener("astro:after-navigation", () => {
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
</script>
|
||||
<ThemeSwitcher client:only="react" transition:persist />
|
||||
<script is:inline set:html={`window.scrollTo(0,0);` + THEME_NAV_SCRIPT} />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
59
src/src/lib/themes/engine.ts
Normal file
59
src/src/lib/themes/engine.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { THEMES, DEFAULT_THEME_ID } from "./index";
|
||||
import { CSS_PROPS } from "./props";
|
||||
import type { Theme } from "./types";
|
||||
|
||||
export function getStoredThemeId(): string {
|
||||
if (typeof window === "undefined") return DEFAULT_THEME_ID;
|
||||
return localStorage.getItem("theme") || DEFAULT_THEME_ID;
|
||||
}
|
||||
|
||||
export function saveTheme(id: string): void {
|
||||
localStorage.setItem("theme", id);
|
||||
}
|
||||
|
||||
export function getNextTheme(currentId: string): Theme {
|
||||
const list = Object.values(THEMES);
|
||||
const idx = list.findIndex((t) => t.id === currentId);
|
||||
return list[(idx + 1) % list.length];
|
||||
}
|
||||
|
||||
/** Sets CSS vars and notifies canvas, but does NOT persist to localStorage. */
|
||||
export function previewTheme(id: string): void {
|
||||
const theme = THEMES[id];
|
||||
if (!theme) return;
|
||||
|
||||
const root = document.documentElement;
|
||||
for (const [key, prop] of CSS_PROPS) {
|
||||
root.style.setProperty(prop, theme.colors[key]);
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent("theme-changed", { detail: { id } }));
|
||||
}
|
||||
|
||||
export function applyTheme(id: string): void {
|
||||
const theme = THEMES[id];
|
||||
if (!theme) return;
|
||||
|
||||
// Set CSS vars on :root for immediate visual update
|
||||
const root = document.documentElement;
|
||||
for (const [key, prop] of CSS_PROPS) {
|
||||
root.style.setProperty(prop, theme.colors[key]);
|
||||
}
|
||||
|
||||
// Update <style id="theme-vars"> so Astro view transitions don't revert
|
||||
let el = document.getElementById("theme-vars") as HTMLStyleElement | null;
|
||||
if (!el) {
|
||||
el = document.createElement("style");
|
||||
el.id = "theme-vars";
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
let css = ":root{";
|
||||
for (const [key, prop] of CSS_PROPS) {
|
||||
css += `${prop}:${theme.colors[key]};`;
|
||||
}
|
||||
css += "}";
|
||||
el.textContent = css;
|
||||
|
||||
saveTheme(id);
|
||||
document.dispatchEvent(new CustomEvent("theme-changed", { detail: { id } }));
|
||||
}
|
||||
58
src/src/lib/themes/index.ts
Normal file
58
src/src/lib/themes/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { Theme } from "./types";
|
||||
|
||||
export const DEFAULT_THEME_ID = "darkbox";
|
||||
|
||||
function theme(
|
||||
id: string,
|
||||
name: string,
|
||||
type: "dark" | "light",
|
||||
colors: Theme["colors"],
|
||||
palette: [number, number, number][]
|
||||
): Theme {
|
||||
return { id, name, type, colors, canvasPalette: palette };
|
||||
}
|
||||
|
||||
// Three darkbox variants from darkbox.nvim
|
||||
// Classic (vivid) → Retro (muted) → Dim (deep)
|
||||
// Each variant's "bright" is the next level up's base.
|
||||
|
||||
export const THEMES: Record<string, Theme> = {
|
||||
darkbox: theme("darkbox", "Darkbox Classic", "dark", {
|
||||
background: "0 0 0",
|
||||
foreground: "235 219 178",
|
||||
red: "251 73 52", redBright: "255 110 85",
|
||||
orange: "254 128 25", orangeBright: "255 165 65",
|
||||
green: "184 187 38", greenBright: "210 215 70",
|
||||
yellow: "250 189 47", yellowBright: "255 215 85",
|
||||
blue: "131 165 152", blueBright: "165 195 180",
|
||||
purple: "211 134 155", purpleBright: "235 165 180",
|
||||
aqua: "142 192 124", aquaBright: "175 220 160",
|
||||
surface: "60 56 54",
|
||||
}, [[251,73,52],[184,187,38],[250,189,47],[131,165,152],[211,134,155],[142,192,124]]),
|
||||
|
||||
"darkbox-retro": theme("darkbox-retro", "Darkbox Retro", "dark", {
|
||||
background: "0 0 0",
|
||||
foreground: "189 174 147",
|
||||
red: "204 36 29", redBright: "251 73 52",
|
||||
orange: "214 93 14", orangeBright: "254 128 25",
|
||||
green: "152 151 26", greenBright: "184 187 38",
|
||||
yellow: "215 153 33", yellowBright: "250 189 47",
|
||||
blue: "69 133 136", blueBright: "131 165 152",
|
||||
purple: "177 98 134", purpleBright: "211 134 155",
|
||||
aqua: "104 157 106", aquaBright: "142 192 124",
|
||||
surface: "60 56 54",
|
||||
}, [[204,36,29],[152,151,26],[215,153,33],[69,133,136],[177,98,134],[104,157,106]]),
|
||||
|
||||
"darkbox-dim": theme("darkbox-dim", "Darkbox Dim", "dark", {
|
||||
background: "0 0 0",
|
||||
foreground: "168 153 132",
|
||||
red: "157 0 6", redBright: "204 36 29",
|
||||
orange: "175 58 3", orangeBright: "214 93 14",
|
||||
green: "121 116 14", greenBright: "152 151 26",
|
||||
yellow: "181 118 20", yellowBright: "215 153 33",
|
||||
blue: "7 102 120", blueBright: "69 133 136",
|
||||
purple: "143 63 113", purpleBright: "177 98 134",
|
||||
aqua: "66 123 88", aquaBright: "104 157 106",
|
||||
surface: "60 56 54",
|
||||
}, [[157,0,6],[121,116,14],[181,118,20],[7,102,120],[143,63,113],[66,123,88]]),
|
||||
};
|
||||
25
src/src/lib/themes/loader.ts
Normal file
25
src/src/lib/themes/loader.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Generates the inline <script> content for theme loading.
|
||||
* Called at build time in Astro frontmatter.
|
||||
* The script reads "theme" from localStorage, looks up colors, injects a <style> tag.
|
||||
*/
|
||||
import { THEMES } from "./index";
|
||||
import { CSS_PROPS } from "./props";
|
||||
|
||||
// Pre-build a { prop: value } map for each theme at build time
|
||||
const themeVars: Record<string, Record<string, string>> = {};
|
||||
for (const [id, theme] of Object.entries(THEMES)) {
|
||||
const vars: Record<string, string> = {};
|
||||
for (const [key, prop] of CSS_PROPS) {
|
||||
vars[prop] = theme.colors[key];
|
||||
}
|
||||
themeVars[id] = vars;
|
||||
}
|
||||
|
||||
// Sets inline styles on <html> — highest specificity, beats any stylesheet
|
||||
const APPLY = `var v=t[id];if(!v)return;var s=document.documentElement.style;for(var k in v)s.setProperty(k,v[k])`;
|
||||
const LOOKUP = `var id=localStorage.getItem("theme");if(!id)return;var t=${JSON.stringify(themeVars)};`;
|
||||
|
||||
export const THEME_LOADER_SCRIPT = `(function(){${LOOKUP}${APPLY}})();`;
|
||||
|
||||
export const THEME_NAV_SCRIPT = `document.addEventListener("astro:after-navigation",function(){${LOOKUP}${APPLY}});`;
|
||||
21
src/src/lib/themes/props.ts
Normal file
21
src/src/lib/themes/props.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { ThemeColors } from "./types";
|
||||
|
||||
export const CSS_PROPS: [keyof ThemeColors, string][] = [
|
||||
["background", "--color-background"],
|
||||
["foreground", "--color-foreground"],
|
||||
["red", "--color-red"],
|
||||
["redBright", "--color-red-bright"],
|
||||
["orange", "--color-orange"],
|
||||
["orangeBright", "--color-orange-bright"],
|
||||
["green", "--color-green"],
|
||||
["greenBright", "--color-green-bright"],
|
||||
["yellow", "--color-yellow"],
|
||||
["yellowBright", "--color-yellow-bright"],
|
||||
["blue", "--color-blue"],
|
||||
["blueBright", "--color-blue-bright"],
|
||||
["purple", "--color-purple"],
|
||||
["purpleBright", "--color-purple-bright"],
|
||||
["aqua", "--color-aqua"],
|
||||
["aquaBright", "--color-aqua-bright"],
|
||||
["surface", "--color-surface"],
|
||||
];
|
||||
27
src/src/lib/themes/types.ts
Normal file
27
src/src/lib/themes/types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export interface ThemeColors {
|
||||
background: string;
|
||||
foreground: string;
|
||||
red: string;
|
||||
redBright: string;
|
||||
orange: string;
|
||||
orangeBright: string;
|
||||
green: string;
|
||||
greenBright: string;
|
||||
yellow: string;
|
||||
yellowBright: string;
|
||||
blue: string;
|
||||
blueBright: string;
|
||||
purple: string;
|
||||
purpleBright: string;
|
||||
aqua: string;
|
||||
aquaBright: string;
|
||||
surface: string;
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "dark" | "light";
|
||||
colors: ThemeColors;
|
||||
canvasPalette: [number, number, number][];
|
||||
}
|
||||
@@ -1,3 +1,23 @@
|
||||
:root {
|
||||
--color-background: 0 0 0;
|
||||
--color-foreground: 235 219 178;
|
||||
--color-red: 251 73 52;
|
||||
--color-red-bright: 255 110 85;
|
||||
--color-orange: 254 128 25;
|
||||
--color-orange-bright: 255 165 65;
|
||||
--color-green: 184 187 38;
|
||||
--color-green-bright: 210 215 70;
|
||||
--color-yellow: 250 189 47;
|
||||
--color-yellow-bright: 255 215 85;
|
||||
--color-blue: 131 165 152;
|
||||
--color-blue-bright: 165 195 180;
|
||||
--color-purple: 211 134 155;
|
||||
--color-purple-bright: 235 165 180;
|
||||
--color-aqua: 142 192 124;
|
||||
--color-aqua-bright: 175 220 160;
|
||||
--color-surface: 60 56 54;
|
||||
}
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -6,35 +6,35 @@ module.exports = {
|
||||
"comic-code": ["Comic Code", "monospace"],
|
||||
},
|
||||
colors: {
|
||||
background: "#000000",
|
||||
foreground: "#ebdbb2",
|
||||
background: "rgb(var(--color-background) / <alpha-value>)",
|
||||
foreground: "rgb(var(--color-foreground) / <alpha-value>)",
|
||||
red: {
|
||||
DEFAULT: "#cc241d",
|
||||
bright: "#fb4934"
|
||||
DEFAULT: "rgb(var(--color-red) / <alpha-value>)",
|
||||
bright: "rgb(var(--color-red-bright) / <alpha-value>)"
|
||||
},
|
||||
orange: {
|
||||
DEFAULT: "#d65d0e",
|
||||
bright: "#fe8019"
|
||||
DEFAULT: "rgb(var(--color-orange) / <alpha-value>)",
|
||||
bright: "rgb(var(--color-orange-bright) / <alpha-value>)"
|
||||
},
|
||||
green: {
|
||||
DEFAULT: "#98971a",
|
||||
bright: "#b8bb26"
|
||||
DEFAULT: "rgb(var(--color-green) / <alpha-value>)",
|
||||
bright: "rgb(var(--color-green-bright) / <alpha-value>)"
|
||||
},
|
||||
yellow: {
|
||||
DEFAULT: "#d79921",
|
||||
bright: "#fabd2f"
|
||||
DEFAULT: "rgb(var(--color-yellow) / <alpha-value>)",
|
||||
bright: "rgb(var(--color-yellow-bright) / <alpha-value>)"
|
||||
},
|
||||
blue: {
|
||||
DEFAULT: "#458588",
|
||||
bright: "#83a598"
|
||||
DEFAULT: "rgb(var(--color-blue) / <alpha-value>)",
|
||||
bright: "rgb(var(--color-blue-bright) / <alpha-value>)"
|
||||
},
|
||||
purple: {
|
||||
DEFAULT: "#b16286",
|
||||
bright: "#d3869b"
|
||||
DEFAULT: "rgb(var(--color-purple) / <alpha-value>)",
|
||||
bright: "rgb(var(--color-purple-bright) / <alpha-value>)"
|
||||
},
|
||||
aqua: {
|
||||
DEFAULT: "#689d6a",
|
||||
bright: "#8ec07c"
|
||||
DEFAULT: "rgb(var(--color-aqua) / <alpha-value>)",
|
||||
bright: "rgb(var(--color-aqua-bright) / <alpha-value>)"
|
||||
}
|
||||
},
|
||||
keyframes: {
|
||||
@@ -51,86 +51,82 @@ module.exports = {
|
||||
"draw-line": "draw-line 0.6s ease-out forwards",
|
||||
"fade-in": "fade-in 0.3s ease-in-out forwards"
|
||||
},
|
||||
typography: (theme) => ({
|
||||
typography: () => ({
|
||||
DEFAULT: {
|
||||
css: {
|
||||
color: theme("colors.foreground"),
|
||||
"--tw-prose-body": theme("colors.foreground"),
|
||||
"--tw-prose-headings": theme("colors.yellow.bright"),
|
||||
"--tw-prose-links": theme("colors.blue.bright"),
|
||||
"--tw-prose-bold": theme("colors.orange.bright"),
|
||||
"--tw-prose-quotes": theme("colors.green.bright"),
|
||||
"--tw-prose-code": theme("colors.purple.bright"),
|
||||
"--tw-prose-hr": theme("colors.foreground"),
|
||||
"--tw-prose-bullets": theme("colors.foreground"),
|
||||
|
||||
// Base text color
|
||||
color: theme("colors.foreground"),
|
||||
color: "rgb(var(--color-foreground))",
|
||||
"--tw-prose-body": "rgb(var(--color-foreground))",
|
||||
"--tw-prose-headings": "rgb(var(--color-yellow-bright))",
|
||||
"--tw-prose-links": "rgb(var(--color-blue-bright))",
|
||||
"--tw-prose-bold": "rgb(var(--color-orange-bright))",
|
||||
"--tw-prose-quotes": "rgb(var(--color-green-bright))",
|
||||
"--tw-prose-code": "rgb(var(--color-purple-bright))",
|
||||
"--tw-prose-hr": "rgb(var(--color-foreground))",
|
||||
"--tw-prose-bullets": "rgb(var(--color-foreground))",
|
||||
|
||||
// Headings
|
||||
h1: {
|
||||
color: theme("colors.yellow.bright"),
|
||||
color: "rgb(var(--color-yellow-bright))",
|
||||
fontWeight: "700",
|
||||
},
|
||||
h2: {
|
||||
color: theme("colors.yellow.bright"),
|
||||
color: "rgb(var(--color-yellow-bright))",
|
||||
fontWeight: "600",
|
||||
},
|
||||
h3: {
|
||||
color: theme("colors.yellow.bright"),
|
||||
color: "rgb(var(--color-yellow-bright))",
|
||||
fontWeight: "600",
|
||||
},
|
||||
h4: {
|
||||
color: theme("colors.yellow.bright"),
|
||||
color: "rgb(var(--color-yellow-bright))",
|
||||
fontWeight: "600",
|
||||
},
|
||||
|
||||
// Links
|
||||
a: {
|
||||
color: theme("colors.blue.bright"),
|
||||
color: "rgb(var(--color-blue-bright))",
|
||||
"&:hover": {
|
||||
color: theme("colors.blue.DEFAULT"),
|
||||
color: "rgb(var(--color-blue))",
|
||||
},
|
||||
textDecoration: "none",
|
||||
borderBottom: `1px solid ${theme("colors.blue.bright")}`,
|
||||
borderBottom: "1px solid rgb(var(--color-blue-bright))",
|
||||
transition: "all 0.2s ease-in-out",
|
||||
},
|
||||
|
||||
// Code
|
||||
'code:not([data-language])': {
|
||||
color: theme('colors.purple.bright'),
|
||||
backgroundColor: '#282828',
|
||||
color: "rgb(var(--color-purple-bright))",
|
||||
backgroundColor: "rgb(var(--color-surface))",
|
||||
padding: '0',
|
||||
borderRadius: '0.25rem',
|
||||
fontFamily: 'Comic Code, monospace',
|
||||
fontWeight: '400',
|
||||
fontSize: 'inherit', // Match the parent text size
|
||||
fontSize: 'inherit',
|
||||
'&::before': { content: 'none' },
|
||||
'&::after': { content: 'none' },
|
||||
},
|
||||
|
||||
'pre': {
|
||||
backgroundColor: '#282828',
|
||||
color: theme("colors.foreground"),
|
||||
backgroundColor: "rgb(var(--color-surface))",
|
||||
color: "rgb(var(--color-foreground))",
|
||||
borderRadius: '0.5rem',
|
||||
overflow: 'visible', // This allows the copy button to be positioned outside
|
||||
position: 'relative', // For the copy button positioning
|
||||
marginTop: '1.5rem', // Space for the copy button and language label
|
||||
fontSize: 'inherit', // Match the parent font size
|
||||
overflow: 'visible',
|
||||
position: 'relative',
|
||||
marginTop: '1.5rem',
|
||||
fontSize: 'inherit',
|
||||
},
|
||||
|
||||
'pre code': {
|
||||
display: 'block',
|
||||
fontFamily: 'Comic Code, monospace',
|
||||
fontSize: '1em', // This will inherit from the prose-lg setting
|
||||
fontSize: '1em',
|
||||
padding: '0',
|
||||
overflow: 'auto', // Enable horizontal scrolling
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'pre',
|
||||
'&::before': { content: 'none' },
|
||||
'&::after': { content: 'none' },
|
||||
},
|
||||
|
||||
|
||||
'[data-rehype-pretty-code-fragment]:nth-of-type(2) pre': {
|
||||
'[data-line]::before': {
|
||||
content: 'counter(line)',
|
||||
@@ -148,7 +144,7 @@ module.exports = {
|
||||
|
||||
// Bold
|
||||
strong: {
|
||||
color: theme("colors.orange.bright"),
|
||||
color: "rgb(var(--color-orange-bright))",
|
||||
fontWeight: "600",
|
||||
},
|
||||
|
||||
@@ -156,15 +152,15 @@ module.exports = {
|
||||
ul: {
|
||||
li: {
|
||||
"&::before": {
|
||||
backgroundColor: theme("colors.foreground"),
|
||||
backgroundColor: "rgb(var(--color-foreground))",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Blockquotes
|
||||
blockquote: {
|
||||
borderLeftColor: theme("colors.green.bright"),
|
||||
color: theme("colors.green.bright"),
|
||||
borderLeftColor: "rgb(var(--color-green-bright))",
|
||||
color: "rgb(var(--color-green-bright))",
|
||||
fontStyle: "italic",
|
||||
quotes: "\"\\201C\"\"\\201D\"\"\\2018\"\"\\2019\"",
|
||||
p: {
|
||||
@@ -175,21 +171,21 @@ module.exports = {
|
||||
|
||||
// Horizontal rules
|
||||
hr: {
|
||||
borderColor: theme("colors.foreground"),
|
||||
borderColor: "rgb(var(--color-foreground))",
|
||||
opacity: "0.2",
|
||||
},
|
||||
|
||||
// Table
|
||||
table: {
|
||||
thead: {
|
||||
borderBottomColor: theme("colors.foreground"),
|
||||
borderBottomColor: "rgb(var(--color-foreground))",
|
||||
th: {
|
||||
color: theme("colors.yellow.bright"),
|
||||
color: "rgb(var(--color-yellow-bright))",
|
||||
},
|
||||
},
|
||||
tbody: {
|
||||
tr: {
|
||||
borderBottomColor: theme("colors.foreground"),
|
||||
borderBottomColor: "rgb(var(--color-foreground))",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -201,7 +197,7 @@ module.exports = {
|
||||
|
||||
// Figures
|
||||
figcaption: {
|
||||
color: theme("colors.foreground"),
|
||||
color: "rgb(var(--color-foreground))",
|
||||
opacity: "0.8",
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user