default to dark mode, refresh Right now text, CSS barbell gym icons

This commit is contained in:
Martin Dvořák 2026-09-30 21:37:56 +00:00
commit 4a8cac689b
10 changed files with 101 additions and 66 deletions

View file

@ -8,7 +8,7 @@ from the saved theme, all from the same content:
- **`web`** (default) — a light, frosted-glass **website**: a sticky full-height sidebar
(section nav, light/dark switch, desktop switch) beside a column of glass cards — hero with
a socials row, quick tiles, live services, activity heatmap, projects and hardware
(a masonry gallery), features and friends. Light by default with a persisted dark mode,
(a masonry gallery), features and friends. Dark by default with a persisted light mode,
and fully responsive, so phones and desktops get the same shell.
- **`skeuo`** — an alternate KDE Plasma / Windows-style **desktop simulation** (desktop
only): draggable snap-to-grid icons, resizable/floating windows, a Start menu, taskbar and
@ -26,7 +26,7 @@ more.
- **Two themes / shells** — a default frosted-glass **web** shell and an alternate KDE-style
**desktop** shell, toggled from either UI and persisted in `localStorage` (applied pre-paint
to avoid a flash). Phones always get the web shell.
- **Light / dark mode** — the web shell has a light/dark switch (light by default, remembered
- **Light / dark mode** — the web shell has a light/dark switch (dark by default, remembered
in `localStorage`, applied pre-paint). The desktop shell keeps its own Breeze Dark look.
- **Glassmorphic web layout** — a sticky sidebar with section navigation beside a column of
frosted-glass panels (subtle gradients + `backdrop-filter` blur), reorganised into

View file

@ -109,7 +109,8 @@ scripts/
1. `ThemeScript` runs before paint. It reads `localStorage.theme` and sets
`data-theme` (`web` | `skeuo`) on `<html>`, defaulting to `web`; it also reads
`localStorage.scheme` and sets `data-scheme` (`light` | `dark`), defaulting to
`light`. There is no device sniff anymore.
`dark` (only an explicit stored `light` overrides it). There is no device
sniff anymore.
2. `useTheme()` (in `useTheme.ts`) reads `data-theme` via `useSyncExternalStore`
and dispatches a `theme-change` event on toggle. `useColorScheme()` does the
same for `data-scheme` via a `scheme-change` event.
@ -192,6 +193,14 @@ Themes: `web` = default frosted-glass website (sidebar + content, light/dark),
`.web-tile-note` is centered. Every other panel title is centered too:
`.web-section-head` (the eyebrow + `.web-section-title`) and `.web-tile-title`
("Right now" / "Gym PRs"). Desktop keeps the left-aligned headings.
- The gym PR icons (`.gym-icon--squat` / `--bench` / `--deadlift`) are **pure
CSS barbells**, not SVGs: a bar (`::before`) plus plates drawn as layered
`linear-gradient` backgrounds on `::after`, all in `currentColor` so they
adapt to light/dark and the desktop app surface. More plates per side = a
heavier lift (bench 1, squat 2, deadlift 3). Don't swap them back to images.
- `.web-quick` is top-aligned (`align-items: start`) so the shorter "Right now"
tile isn't stretched to the Gym tile's height (it used to leave a big empty
gap under the text).
- Under `max-width: 640px` the year-grid heatmap (`.heatmap-container`) and its
legend/hover row (`.heatmap-status-row`) are `display: none` — the grid is
unreadable at phone widths. The contribution total and the commit history

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<g id="bench-press-6" transform="translate(-2 -2)">
<path id="primary" d="M6,21V15H18v6" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<path id="primary-2" data-name="primary" d="M4,3V9M3,4V8M20,3V9m1-5V8M4,6H20M16,4V15M8,15V4m9,17h2M5,21H7" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 625 B

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<g id="deadlift" transform="translate(-2 -2)">
<circle id="secondary" fill="#2ca9bc" cx="2" cy="2" r="2" transform="translate(10 3)"/>
<path id="primary" d="M8,15V10a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2v5" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<path id="primary-2" data-name="primary" d="M14,5a2,2,0,1,1-2-2A2,2,0,0,1,14,5Zm0,9v7m-4,0V14M3,14H20m0-2v4m1-3v2M4,12v4M3,13v2" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 762 B

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 256 256" xml:space="preserve">
<g>
<circle cx="127" cy="28.4" r="26.6"/>
<path d="M213.4,171.2h-49.8V97.3c0-1.8,1.5-3.3,3.3-3.3c0.6,0,1.2,0.2,1.7,0.5l30.1,17.3l-20.8,36c-3.2,5.6-1.3,12.7,4.3,15.9
c5.6,3.2,12.7,1.3,15.9-4.3l26.6-46c3.2-5.6,1.3-12.7-4.3-15.9l-51.6-29.8c-4.7-3.3-10-5.4-15.5-6L127,61.6h0h0l-26.3,0.1
c-5.5,0.6-10.8,2.7-15.5,6L33.6,97.5c-5.6,3.2-7.5,10.3-4.3,15.9l26.6,46c3.2,5.6,10.3,7.5,15.9,4.3c5.6-3.2,7.5-10.3,4.3-15.9
l-20.8-36l30.1-17.3c0.5-0.3,1.1-0.5,1.7-0.5c1.8,0,3.3,1.5,3.3,3.3v24.1v49.8H40.6c-9.2,0-16.6,7.4-16.6,16.6
c0,4.6,1.8,8.7,4.8,11.7l49.7,49.7c6.5,6.5,17,6.5,23.5,0c6.5-6.5,6.5-17,0-23.5l-21.3-21.3l46.2,0l0-13.3c0,0,0,0,0,0s0,0,0,0
l0,13.3l46.2,0l-21.3,21.3c-6.5,6.5-6.5,17,0,23.5c6.5,6.5,17,6.5,23.5,0l49.7-49.7c3-3,4.8-7.1,4.8-11.7
C230,178.7,222.6,171.2,213.4,171.2z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -7,8 +7,9 @@ import { useServerInsertedHTML } from "next/navigation";
// are applied without a flash. It is injected with useServerInsertedHTML so the
// <script> lives in the SSR HTML stream instead of inside the React tree —
// React 19 logs an error for script tags rendered by components on the client.
// Anything unrecognised falls back to the defaults: web + light.
const INIT_SCRIPT = `(function(){try{var t=localStorage.getItem('theme');document.documentElement.setAttribute('data-theme',t==='skeuo'?'skeuo':'web');var s=localStorage.getItem('scheme');document.documentElement.setAttribute('data-scheme',s==='dark'?'dark':'light');}catch(e){}})();`;
// Anything unrecognised falls back to the defaults: web + dark. Dark is the
// default scheme; only an explicit stored "light" switches it off.
const INIT_SCRIPT = `(function(){try{var t=localStorage.getItem('theme');document.documentElement.setAttribute('data-theme',t==='skeuo'?'skeuo':'web');var s=localStorage.getItem('scheme');document.documentElement.setAttribute('data-scheme',s==='light'?'light':'dark');}catch(e){}})();`;
export default function ThemeScript() {
// useServerInsertedHTML can fire several times during a single render pass;

View file

@ -121,15 +121,15 @@ function Services() {
function Gym() {
const lifts = [
{ icon: "/squat.svg", label: "SQUAT", weight: "125kg", date: "5.1.2025" },
{ icon: "/bench.svg", label: "BENCH", weight: "111kg", date: "29.5.2026" },
{ icon: "/deadlift.svg", label: "DEADLIFT", weight: "165kg", date: "17.12.2024" },
{ icon: "squat", label: "SQUAT", weight: "125kg", date: "5.1.2025" },
{ icon: "bench", label: "BENCH", weight: "111kg", date: "29.5.2026" },
{ icon: "deadlift", label: "DEADLIFT", weight: "165kg", date: "17.12.2024" },
];
return (
<div className="gym-grid">
{lifts.map((l) => (
<div key={l.label} className="gym-item">
<Image src={l.icon} alt={l.label} width={48} height={48} className="gym-icon" />
<span className={`gym-icon gym-icon--${l.icon}`} aria-hidden="true" />
<div className="gym-label">{l.label}</div>
<div className="gym-weight">{l.weight}</div>
<div className="gym-date">{l.date}</div>
@ -238,11 +238,9 @@ export default function WebLayout() {
<article id="now" className="web-tile glass">
<h3 className="web-tile-title">Right now</h3>
<p className="web-tile-text">
I&#39;m currently hosting an SMP (dm me on discord). Also I&#39;m pretty much
cooking daily, because im bulking through out the non-summer seasons. And me
and my pal <a href="https://skopec.dev">Pepa</a> are working on an education
app so we can study more efficiently, It&#39;s called{" "}
<a href="https://git.dvorakmortonxd.dev/edu/maturita.c">maturita.c</a>!
I left the maturita.c project to focus on my proxmox server (just popped in
another 1.7TB hdd upgrade, 3 small hdds from ewaste laptops and I just
ziplocked it into place and connected it lmao)
</p>
</article>

View file

@ -301,25 +301,19 @@ function GymApp() {
<div className="app-pad">
<div className="gym-grid">
<div className="gym-item">
<Image src="/squat.svg" alt="Squat" width={48} height={48} className="gym-icon" />
<span className="gym-icon gym-icon--squat" aria-hidden="true" />
<div className="gym-label">SQUAT</div>
<div className="gym-weight">125kg</div>
<div className="gym-date">5.1.2025</div>
</div>
<div className="gym-item">
<Image src="/bench.svg" alt="Bench" width={48} height={48} className="gym-icon" />
<span className="gym-icon gym-icon--bench" aria-hidden="true" />
<div className="gym-label">BENCH</div>
<div className="gym-weight">111kg</div>
<div className="gym-date">29.5.2026</div>
</div>
<div className="gym-item">
<Image
src="/deadlift.svg"
alt="Deadlift"
width={48}
height={48}
className="gym-icon"
/>
<span className="gym-icon gym-icon--deadlift" aria-hidden="true" />
<div className="gym-label">DEADLIFT</div>
<div className="gym-weight">165kg</div>
<div className="gym-date">17.12.2024</div>

View file

@ -4,13 +4,13 @@ import { useSyncExternalStore } from "react";
/**
* Light/dark appearance for the web theme. Persisted in `localStorage.scheme`
* and applied pre-paint as `<html data-scheme>` so there is no flash. Light is
* the default.
* and applied pre-paint as `<html data-scheme>` so there is no flash. Dark is
* the default; the user's choice is remembered once they toggle.
*/
export type ColorScheme = "light" | "dark";
const EVENT = "scheme-change";
export const DEFAULT_SCHEME: ColorScheme = "light";
export const DEFAULT_SCHEME: ColorScheme = "dark";
function subscribe(callback: () => void) {
window.addEventListener(EVENT, callback);
@ -22,9 +22,9 @@ function subscribe(callback: () => void) {
}
function getSnapshot(): ColorScheme {
return document.documentElement.getAttribute("data-scheme") === "dark"
? "dark"
: "light";
return document.documentElement.getAttribute("data-scheme") === "light"
? "light"
: "dark";
}
function getServerSnapshot(): ColorScheme {

View file

@ -357,7 +357,9 @@ html[data-theme="web"] body {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 20px;
align-items: stretch;
/* Don't stretch the shorter "Right now" tile to the taller Gym tile's
height — that left a big empty gap under its text. */
align-items: start;
}
.web-tile {
display: flex;
@ -373,7 +375,8 @@ html[data-theme="web"] body {
color: var(--accent);
}
.web-tile-text {
font-size: 0.9rem;
font-size: 1.12rem;
line-height: 1.7;
color: var(--web-muted);
}
.web-tile-note {
@ -728,10 +731,72 @@ html[data-theme="web"][data-scheme="dark"] .social-icon {
border-color: var(--accent-line);
transform: translateY(-2px);
}
/* Custom CSS barbell icons (the squat/bench/deadlift SVGs are gone). Same
barbell, drawn from currentColor so it adapts to the theme; more plates per
side = heavier lift (bench 1, squat 2, deadlift 3). */
.gym-icon {
width: 100%;
max-width: 42px;
height: auto;
position: relative;
display: block;
width: 54px;
height: 30px;
}
.gym-icon::before {
/* the bar */
content: "";
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 3px;
border-radius: 2px;
background: currentColor;
opacity: 0.55;
transform: translateY(-50%);
}
.gym-icon::after {
/* the weight plates */
content: "";
position: absolute;
inset: 0;
background-repeat: no-repeat;
opacity: 0.85;
}
.gym-icon--bench::after {
background-image:
linear-gradient(currentColor, currentColor),
linear-gradient(currentColor, currentColor);
background-size: 7px 24px, 7px 24px;
background-position: 9px center, calc(100% - 16px) center;
}
.gym-icon--squat::after {
background-image:
linear-gradient(currentColor, currentColor),
linear-gradient(currentColor, currentColor),
linear-gradient(currentColor, currentColor),
linear-gradient(currentColor, currentColor);
background-size: 6px 18px, 7px 26px, 6px 18px, 7px 26px;
background-position:
5px center,
12px center,
calc(100% - 11px) center,
calc(100% - 19px) center;
}
.gym-icon--deadlift::after {
background-image:
linear-gradient(currentColor, currentColor),
linear-gradient(currentColor, currentColor),
linear-gradient(currentColor, currentColor),
linear-gradient(currentColor, currentColor),
linear-gradient(currentColor, currentColor),
linear-gradient(currentColor, currentColor);
background-size: 6px 16px, 6px 22px, 7px 28px, 6px 16px, 6px 22px, 7px 28px;
background-position:
3px center,
9px center,
16px center,
calc(100% - 9px) center,
calc(100% - 15px) center,
calc(100% - 23px) center;
}
.gym-label {
font-size: 0.66rem;