diff --git a/README.md b/README.md index 46e378c..d10b0dd 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,10 @@ npm run build # static export in out/ `./deploy-shanaty.sh` builds and publishes the site to shanaty.ru (needs the ssh key `~/.ssh/MyGitea`). Only needed after changing code or styles. +## Themes + +The page is **dark by default** (a night version of the viewport: graphite backdrop, brighter axis colours, and a light closing contact band). A button in the top bar switches to the light theme; the choice is remembered in the browser and applied before the first paint, so there is no flash. Colours live in the tokens at the top of `app/globals.css` (`:root` for light, `:root[data-theme='dark']` for dark); components only use those tokens, so a new theme is a new block of values. + ## Design The page is laid out like a 3D-editor viewport, a nod to the game-engine work it presents: a studio backdrop with a perspective floor whose red X and blue Z axes match the axis colours of Blender and Unity, and an orientation gizmo in the corner that orbits as the page scrolls (press it to go back to the top). Projects without a demo image get a deterministic isometric voxel sculpture generated from the repo name. diff --git a/app/globals.css b/app/globals.css index a8872ec..3af2eef 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,16 +1,59 @@ /* Design tokens: a studio-lit 3D viewport. Axis colours (X red, Y green, Z blue) are marks and lines, not text. */ :root { - --studio: #e6e9ed; - --studio-deep: #cdd3db; + color-scheme: light; + + /* Palette. The page is a 3D viewport: a studio backdrop and the three axis colours (X red, Y green, Z blue), + which are marks and lines rather than text. Dark is the default theme; see :root[data-theme='dark']. */ --paper: #f4f6f8; --ink: #15181d; --muted: #5b6571; - --line: rgba(21, 24, 29, 0.14); --x: #e0384b; --y: #5aa300; --z: #2c63e0; --z-text: #2455c9; + /* Surfaces */ + --hero-top: #f5f7fa; + --hero-bottom: #e6e9ee; + --stage-top: #d8dde4; + --stage-bottom: #cdd3db; + --plate-a: #f3f5f8; + --plate-b: #e6e9ed; + --plate-c: #d3d9e0; + --pill-bg: rgba(255, 255, 255, 0.72); + --disc-bg: rgba(244, 246, 248, 0.62); + --topbar-bg: rgba(244, 246, 248, 0.86); + --grid: rgba(21, 24, 29, 0.2); + --img-shadow: rgba(21, 24, 29, 0.28); + --dot-glow: rgba(90, 163, 0, 0.2); + + /* Voxel covers of projects without a demo image */ + --cube-top: #f9fafb; + --cube-left: #d6dbe2; + --cube-right: #b8c0cb; + --cube-edge: rgba(21, 24, 29, 0.16); + --cover-grid: rgba(21, 24, 29, 0.09); + + /* The closing contact band is the inverse of the page */ + --band-bg: #15181d; + --band-ink: #f4f6f8; + + /* Light type on a dark ground looks heavier, so the display weight is eased off there */ + --name-weight: 800; + + /* Derived from the palette above, so they follow the theme */ + --line: color-mix(in srgb, var(--ink) 14%, transparent); + --ring: color-mix(in srgb, var(--ink) 9%, transparent); + --horizon: color-mix(in srgb, var(--ink) 22%, transparent); + --chip: color-mix(in srgb, var(--ink) 7%, transparent); + --ink-2: color-mix(in srgb, var(--ink) 86%, transparent); + --ink-3: color-mix(in srgb, var(--ink) 76%, transparent); + --ink-line: color-mix(in srgb, var(--ink) 32%, transparent); + --band-2: color-mix(in srgb, var(--band-ink) 70%, transparent); + --band-3: color-mix(in srgb, var(--band-ink) 58%, transparent); + --band-line: color-mix(in srgb, var(--band-ink) 22%, transparent); + --band-underline: color-mix(in srgb, var(--band-ink) 38%, transparent); + --radius-plate: 18px; --radius-control: 10px; --shell: min(1240px, calc(100% - 48px)); @@ -21,6 +64,42 @@ --font-head: var(--font-display), 'Segoe UI', system-ui, sans-serif; } +:root[data-theme='dark'] { + color-scheme: dark; + --paper: #1c2026; + --ink: #e9edf2; + --muted: #9aa5b1; + --x: #ff5a6e; + --y: #86d62c; + --z: #5b8dff; + --z-text: #8db0ff; + + --hero-top: #282d35; + --hero-bottom: #1c2026; + --stage-top: #14171b; + --stage-bottom: #0f1114; + --plate-a: #363c46; + --plate-b: #2a2f37; + --plate-c: #1f2329; + --pill-bg: rgba(255, 255, 255, 0.07); + --disc-bg: rgba(40, 45, 53, 0.72); + --topbar-bg: rgba(28, 32, 38, 0.86); + --grid: rgba(255, 255, 255, 0.15); + --img-shadow: rgba(0, 0, 0, 0.55); + --dot-glow: rgba(134, 214, 44, 0.24); + + --cube-top: #e9edf2; + --cube-left: #9aa5b3; + --cube-right: #69737f; + --cube-edge: rgba(0, 0, 0, 0.4); + --cover-grid: rgba(255, 255, 255, 0.09); + + --band-bg: #e8ebef; + --band-ink: #15181d; + + --name-weight: 730; +} + * { box-sizing: border-box; } html { background: var(--paper); scroll-behavior: smooth; scroll-padding-top: 88px; } body { diff --git a/app/layout.tsx b/app/layout.tsx index 40a057a..534ad2a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,9 +12,15 @@ export const metadata: Metadata = { authors: [{ name: 'Emil Shanayev' }], } +// Dark is the default theme; a stored choice of "light" is applied before the first paint so the page never flashes. +const THEME_SCRIPT = "try{if(localStorage.getItem('theme')==='light')document.documentElement.dataset.theme='light'}catch(e){}" + export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( - + + +