Add darkbox palette themes

This commit is contained in:
2026-03-30 17:17:52 -07:00
parent 2c5f64a769
commit 16902f00f4
14 changed files with 465 additions and 105 deletions

View File

@@ -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;

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef } from "react";
interface Cell {
alive: boolean;
next: boolean;
@@ -58,6 +59,43 @@ 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 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'
@@ -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>
);
};

View File

@@ -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>

View 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 }}
/>
</>
);
}

View File

@@ -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,17 +22,13 @@ 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
@@ -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>

View File

@@ -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>

View File

@@ -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,17 +22,13 @@ 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
@@ -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>

View 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 } }));
}

View 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]]),
};

View 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}});`;

View 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"],
];

View 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][];
}

View File

@@ -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;

View File

@@ -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",
},
},