feat(web): play a random track from the Frutiger Aero SoundCloud playlist in aero mode (v2.2.22)

This commit is contained in:
Martin Dvořák 2026-10-16 00:20:14 +02:00
commit be33317d54
12 changed files with 387 additions and 83 deletions

View file

@ -56,7 +56,7 @@ frosted-glass **web** site, and an alternate KDE/Plasma **desktop simulation**
utilities in the components (Tailwind is present via PostCSS but the look is utilities in the components (Tailwind is present via PostCSS but the look is
hand-written CSS). hand-written CSS).
- Path alias: `@/*` → `src/*`. - Path alias: `@/*` → `src/*`.
- Version: `2.2.21` (see `package.json`). - Version: `2.2.22` (see `package.json`).
### Scripts ### Scripts
```bash ```bash
@ -103,6 +103,7 @@ src/app/
components/ components/
ThemeRoot.tsx → picks which shell to mount (the router of the UI) ThemeRoot.tsx → picks which shell to mount (the router of the UI)
WebLayout.tsx → "web" theme (default): sticky sidebar + glass content WebLayout.tsx → "web" theme (default): sticky sidebar + glass content
AeroMusic.tsx → Frutiger Aero soundtrack (random SoundCloud playlist track)
FeatureWindow.tsx → decorative CSS "windows" (game / browser / forge); used in the FeatureWindow.tsx → decorative CSS "windows" (game / browser / forge); used in the
web feature panels and the desktop app windows web feature panels and the desktop app windows
WindowsDesktop.tsx → "skeuo" theme, desktop: icons, windows, taskbar WindowsDesktop.tsx → "skeuo" theme, desktop: icons, windows, taskbar
@ -181,7 +182,11 @@ Themes: `web` = default frosted-glass website (sidebar + content, light/dark),
bubbles (`.web-aero-bubbles` / `.web-aero-bubble`) rise across the backdrop, and bubbles (`.web-aero-bubbles` / `.web-aero-bubble`) rise across the backdrop, and
all `.glass` panels and interactive controls become crystal-clear 3D bevel-rimmed all `.glass` panels and interactive controls become crystal-clear 3D bevel-rimmed
liquid glass (the `.web-ctl-scheme` light/dark switch is hidden in aero mode — liquid glass (the `.web-ctl-scheme` light/dark switch is hidden in aero mode —
it's a fixed bright-sky look). The two remaining feature panels (`.web-split`) stretch it's a fixed bright-sky look). While aero is on, `AeroMusic.tsx` autoplays a
random track from the *Frutiger Aero* SoundCloud playlist (a hidden SoundCloud
iframe driven through their widget API: it picks a random index from `getSounds`
and re-randomises on every `PLAY` event so the set never plays in order; the chip
label is synced from `getCurrentSound`, not from what we asked for). The two remaining feature panels (`.web-split`) stretch
to equal height and share a fixed `16 / 10` `.fw` window (the `FeatureWindow` to equal height and share a fixed `16 / 10` `.fw` window (the `FeatureWindow`
component) — decorative, **asset-free CSS mock-ups** rather than screenshots: component) — decorative, **asset-free CSS mock-ups** rather than screenshots:
Revival Inc. is a little necromancy game scene (night sky, moon, tombstones, Revival Inc. is a little necromancy game scene (night sky, moon, tombstones,

View file

@ -47,8 +47,10 @@ more.
`freezmo_aero.png` and transforms the web shell into a Frutiger Aero theme: `freezmo_aero.png` and transforms the web shell into a Frutiger Aero theme:
`frutigerbackground.png` as the full-screen backdrop, floating glossy water/soap bubbles `frutigerbackground.png` as the full-screen backdrop, floating glossy water/soap bubbles
across the viewport, and crystal-clear 3D bevel-rimmed liquid-glass panels and buttons across the viewport, and crystal-clear 3D bevel-rimmed liquid-glass panels and buttons
with subtle cyan refractive rims. The light/dark switch is hidden while the mode is with subtle cyan refractive rims. A random track from the *Frutiger Aero* SoundCloud
active (it's a fixed bright-sky look), and the desktop mode is left untouched. playlist plays while the mode is on (driven through SoundCloud's widget API), with a
small glass "now playing" chip to pause it. The light/dark switch is hidden while the
mode is active (it's a fixed bright-sky look), and the desktop mode is left untouched.
- **Draggable desktop icons** — free placement that snaps to a measured grid, one icon per - **Draggable desktop icons** — free placement that snaps to a measured grid, one icon per
cell, with a follow-the-cursor drag ghost. Icons shrink on cramped viewports so they never cell, with a follow-the-cursor drag ghost. Icons shrink on cramped viewports so they never
collide. collide.
@ -93,7 +95,7 @@ more.
icons (`public/freezmo.png`, `public/freezmo_aero.png`), the Frutiger Aero wallpaper icons (`public/freezmo.png`, `public/freezmo_aero.png`), the Frutiger Aero wallpaper
(`public/frutigerbackground.png`), the Joker easter egg and the Gentoo mark; all cards, (`public/frutigerbackground.png`), the Joker easter egg and the Gentoo mark; all cards,
feature/app previews and the desktop wallpaper are CSS. feature/app previews and the desktop wallpaper are CSS.
- **Version:** `2.2.21` (see `package.json`) - **Version:** `2.2.22` (see `package.json`)
- **Dependencies:** `npm audit` reports **0 vulnerabilities**. `next` is kept at a patched - **Dependencies:** `npm audit` reports **0 vulnerabilities**. `next` is kept at a patched
release (`>=16.3.6`) for the `next/og` RCE, and `brace-expansion` is force-upgraded via release (`>=16.3.6`) for the `next/og` RCE, and `brace-expansion` is force-upgraded via
`overrides`. `braces` (a transitive ESLint/`fast-glob` dependency) has **no upstream fix**, `overrides`. `braces` (a transitive ESLint/`fast-glob` dependency) has **no upstream fix**,
@ -215,6 +217,7 @@ src/app/
useTheme.ts → theme state via useSyncExternalStore useTheme.ts → theme state via useSyncExternalStore
useColorScheme.ts → light/dark state via useSyncExternalStore useColorScheme.ts → light/dark state via useSyncExternalStore
WebLayout.tsx → default web shell: sidebar + glass content WebLayout.tsx → default web shell: sidebar + glass content
AeroMusic.tsx → Frutiger Aero soundtrack (random SoundCloud playlist track)
FeatureWindow.tsx → decorative CSS windows for the two feature panels FeatureWindow.tsx → decorative CSS windows for the two feature panels
WindowsDesktop.tsx → desktop shell: icons, windows, taskbar, Start menu WindowsDesktop.tsx → desktop shell: icons, windows, taskbar, Start menu
WinWindow.tsx → one draggable/resizable window WinWindow.tsx → one draggable/resizable window

View file

@ -6,16 +6,11 @@
"6": "braces Character Constants", "6": "braces Character Constants",
"7": "Next Config & API Routes", "7": "Next Config & API Routes",
"8": "TypeScript Compiler Config", "8": "TypeScript Compiler Config",
"9": "Root Package Dependencies",
"10": "Root Dev Dependencies", "10": "Root Dev Dependencies",
"12": "Debian Dependency Installer", "12": "Debian Dependency Installer",
"13": "brace-expansion Overrides",
"14": "npm Scripts",
"15": "Freezmo Icon Visual Identity", "15": "Freezmo Icon Visual Identity",
"16": "braces Public API Methods", "16": "braces Public API Methods",
"17": "Eye Logo & Favicon Assets", "17": "Eye Logo & Favicon Assets",
"18": "ESLint Configuration",
"19": "Runtime Dependencies",
"20": "Discord Icon Asset", "20": "Discord Icon Asset",
"22": "Instagram Icon Asset", "22": "Instagram Icon Asset",
"23": "Gentoo Logo Asset", "23": "Gentoo Logo Asset",
@ -23,7 +18,6 @@
"25": "Revival Inc. Game Favicon", "25": "Revival Inc. Game Favicon",
"26": "TikTok Icon Asset", "26": "TikTok Icon Asset",
"27": "System Service Installer", "27": "System Service Installer",
"28": "Package allowScripts Overrides",
"29": "PostCSS Configuration", "29": "PostCSS Configuration",
"30": "GitHub Icon Asset", "30": "GitHub Icon Asset",
"31": "itch.io Icon Asset", "31": "itch.io Icon Asset",

View file

@ -1,17 +1,17 @@
# Graph Report - dvorakmortonxd.dev (2026-10-15) # Graph Report - dvorakmortonxd.dev (2026-10-16)
## Corpus Check ## Corpus Check
- 43 files · ~319,284 words - 44 files · ~320,188 words
- Verdict: corpus is large enough that graph structure adds value. - Verdict: corpus is large enough that graph structure adds value.
- Unclassified: 19 file(s) not represented in the graph (top: (none) 6, .ttf 3, .css 3) - Unclassified: 19 file(s) not represented in the graph (top: (none) 6, .ttf 3, .css 3)
## Summary ## Summary
- 424 nodes · 592 edges · 40 communities (25 shown, 15 thin omitted) - 434 nodes · 607 edges · 37 communities (23 shown, 14 thin omitted)
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 44 edges (avg confidence: 0.89) - Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 44 edges (avg confidence: 0.89)
- Token cost: 0 input · 0 output - Token cost: 0 input · 0 output
## Graph Freshness ## Graph Freshness
- Built from commit: `15ef4db7` - Built from commit: `e4530afd`
- Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost). - Run `graphify update .` after code changes (no API cost).
@ -19,22 +19,20 @@
- Desktop App Catalog - Desktop App Catalog
- Deployment & Ops Docs - Deployment & Ops Docs
- braces Package Metadata - braces Package Metadata
- WindowsDesktop.tsx - useTheme.ts
- braces Parsing Engine - braces Parsing Engine
- apps.tsx - apps.tsx
- braces Character Constants - braces Character Constants
- Next Config & API Routes - Next Config & API Routes
- TypeScript Compiler Config - TypeScript Compiler Config
- Root Package Dependencies - package.json
- Root Dev Dependencies - Root Dev Dependencies
- ProjectIndex.tsx
- Debian Dependency Installer - Debian Dependency Installer
- brace-expansion Overrides - dev-server.mjs
- npm Scripts
- Freezmo Icon Visual Identity - Freezmo Icon Visual Identity
- braces Public API Methods - braces Public API Methods
- Eye Logo & Favicon Assets - Eye Logo & Favicon Assets
- ESLint Configuration
- Runtime Dependencies
- Discord Icon Asset - Discord Icon Asset
- AGENTS.md — cold-boot notes - AGENTS.md — cold-boot notes
- Instagram Icon Asset - Instagram Icon Asset
@ -43,7 +41,6 @@
- Revival Inc. Game Favicon - Revival Inc. Game Favicon
- TikTok Icon Asset - TikTok Icon Asset
- System Service Installer - System Service Installer
- Package allowScripts Overrides
- PostCSS Configuration - PostCSS Configuration
- GitHub Icon Asset - GitHub Icon Asset
- itch.io Icon Asset - itch.io Icon Asset
@ -55,9 +52,9 @@
- Twitter/X Icon Asset - Twitter/X Icon Asset
## God Nodes (most connected - your core abstractions) ## God Nodes (most connected - your core abstractions)
1. `WebLayout()` - 18 edges 1. `WebLayout()` - 19 edges
2. `compilerOptions` - 16 edges 2. `react` - 16 edges
3. `react` - 15 edges 3. `compilerOptions` - 16 edges
4. `next` - 12 edges 4. `next` - 12 edges
5. `FeatureWindow()` - 12 edges 5. `FeatureWindow()` - 12 edges
6. `useTheme()` - 12 edges 6. `useTheme()` - 12 edges
@ -86,11 +83,11 @@
- **** — public_revival_inc_favicon_png_image, public_revival_inc_favicon_png_revival_inc_game, public_revival_inc_favicon_png_game_controller_mark [INFERRED 0.75] - **** — public_revival_inc_favicon_png_image, public_revival_inc_favicon_png_revival_inc_game, public_revival_inc_favicon_png_game_controller_mark [INFERRED 0.75]
- **Site identity: eye logo used as app icon and in-page logo** — src_app_icon_png_image, src_app_icon_png_eye_logo, src_app_icon_png_favicon, src_app_components_jokerlogo_tsx_logo [INFERRED 0.85] - **Site identity: eye logo used as app icon and in-page logo** — src_app_icon_png_image, src_app_icon_png_eye_logo, src_app_icon_png_favicon, src_app_components_jokerlogo_tsx_logo [INFERRED 0.85]
## Communities (40 total, 15 thin omitted) ## Communities (37 total, 14 thin omitted)
### Community 0 - "Desktop App Catalog" ### Community 0 - "Desktop App Catalog"
Cohesion: 0.11 Cohesion: 0.08
Nodes (26): Web theme + light/dark (v2.2 rework), react, ContributionsApp(), Clock(), getTime(), subscribe(), Activity, ActivityPayload (+18 more) Nodes (35): Web theme + light/dark (v2.2 rework), react, AeroMusic(), loadWidgetApi(), PLACEHOLDER, SoundCloudGlobal, SoundCloudSound, SoundCloudWidget (+27 more)
### Community 1 - "Deployment & Ops Docs" ### Community 1 - "Deployment & Ops Docs"
Cohesion: 0.09 Cohesion: 0.09
@ -100,17 +97,17 @@ Nodes (30): Autostart on Boot (systemd / OpenRC), Cloudflare Tunnel Reverse Prox
Cohesion: 0.05 Cohesion: 0.05
Nodes (37): ansi-colors, bash-path, gulp-format-md, mocha, author, bugs, url, contributors (+29 more) Nodes (37): ansi-colors, bash-path, gulp-format-md, mocha, author, bugs, url, contributors (+29 more)
### Community 3 - "WindowsDesktop.tsx" ### Community 3 - "useTheme.ts"
Cohesion: 0.08 Cohesion: 0.19
Nodes (33): Conventions & things that will bite you, How UI selection works, APP_BY_ID, AppDef, APPS, createWebsiteApp(), EmbeddedSite(), WebsiteApp() (+25 more) Nodes (13): How UI selection works, WelcomeApp(), ThemeRoot(), DEFAULT_THEME, getServerSnapshot(), getSnapshot(), setTheme(), subscribe() (+5 more)
### Community 4 - "braces Parsing Engine" ### Community 4 - "braces Parsing Engine"
Cohesion: 0.10 Cohesion: 0.10
Nodes (17): compile, expand, parse, stringify, compile(), fill, utils, append() (+9 more) Nodes (17): compile, expand, parse, stringify, compile(), fill, utils, append() (+9 more)
### Community 5 - "apps.tsx" ### Community 5 - "apps.tsx"
Cohesion: 0.07 Cohesion: 0.06
Nodes (36): ALACRITTY_APP, FriendsApp(), GitdxdApp(), HardwareApp(), NEOFETCH, ProjectsApp(), RevivalApp(), RustyWolfApp() (+28 more) Nodes (36): Conventions & things that will bite you, ALACRITTY_APP, APP_BY_ID, AppDef, APPS, createWebsiteApp(), EmbeddedSite(), FriendsApp() (+28 more)
### Community 6 - "braces Character Constants" ### Community 6 - "braces Character Constants"
Cohesion: 0.11 Cohesion: 0.11
@ -133,32 +130,32 @@ Nodes (3): {
}, parse(), stringify }, parse(), stringify
### Community 7 - "Next Config & API Routes" ### Community 7 - "Next Config & API Routes"
Cohesion: 0.10 Cohesion: 0.15
Nodes (20): nextConfig, next, app, hasForwardedHeaders(), isDevDebugRequest(), isLocalDebugRequest(), isLoopback(), port (+12 more) Nodes (13): nextConfig, next, ALLOWED_HOSTS, clickShim(), fail(), GET(), isAllowedHost(), ALLOWED_HOSTS (+5 more)
### Community 8 - "TypeScript Compiler Config" ### Community 8 - "TypeScript Compiler Config"
Cohesion: 0.11 Cohesion: 0.11
Nodes (18): compilerOptions, allowJs, esModuleInterop, incremental, isolatedModules, jsx, lib, module (+10 more) Nodes (18): compilerOptions, allowJs, esModuleInterop, incremental, isolatedModules, jsx, lib, module (+10 more)
### Community 9 - "Root Package Dependencies" ### Community 9 - "package.json"
Cohesion: 0.13 Cohesion: 0.06
Nodes (14): fill-range, name, private, version, autoprefixer, postcss, react-dom, @svgr/webpack (+6 more) Nodes (35): eslintConfig, allowScripts, unrs-resolver@1.12.2, dependencies, next, react, react-dom, fill-range (+27 more)
### Community 10 - "Root Dev Dependencies" ### Community 10 - "Root Dev Dependencies"
Cohesion: 0.15 Cohesion: 0.15
Nodes (13): devDependencies, autoprefixer, eslint, eslint-config-next, fill-range, postcss, @svgr/webpack, tailwindcss (+5 more) Nodes (13): devDependencies, autoprefixer, eslint, eslint-config-next, fill-range, postcss, @svgr/webpack, tailwindcss (+5 more)
### Community 11 - "ProjectIndex.tsx"
Cohesion: 0.14
Nodes (20): HardwareApp(), ProjectsApp(), CodingProjectIndex(), CodingRow(), Group(), HARDWARE_GROUPS, HardwareProjectIndex(), HardwareRow() (+12 more)
### Community 12 - "Debian Dependency Installer" ### Community 12 - "Debian Dependency Installer"
Cohesion: 0.43 Cohesion: 0.43
Nodes (6): die(), log(), node_ok(), install-dependencies-deb.sh script, usage(), warn() Nodes (6): die(), log(), node_ok(), install-dependencies-deb.sh script, usage(), warn()
### Community 13 - "brace-expansion Overrides" ### Community 13 - "dev-server.mjs"
Cohesion: 0.33 Cohesion: 0.29
Nodes (6): brace-expansion, brace-expansion, overrides, braces, minimatch@^10.0.0, minimatch@^3.0.0 Nodes (7): app, hasForwardedHeaders(), isDevDebugRequest(), isLocalDebugRequest(), isLoopback(), port, server
### Community 14 - "npm Scripts"
Cohesion: 0.33
Nodes (6): scripts, build, dev, dev:next, lint, start
### Community 15 - "Freezmo Icon Visual Identity" ### Community 15 - "Freezmo Icon Visual Identity"
Cohesion: 0.53 Cohesion: 0.53
@ -168,14 +165,6 @@ Nodes (5): Dark Navy Rounded-Square (Squircle) Frame, Freeze / Preserve State, F
Cohesion: 0.50 Cohesion: 0.50
Nodes (4): JokerLogo Component Logo Image, Eye Logo Mark (amber iris and pupil), App Icon - Macro Photograph of a Human Eye, All-Seeing Eye / Surveillance Motif Nodes (4): JokerLogo Component Logo Image, Eye Logo Mark (amber iris and pupil), App Icon - Macro Photograph of a Human Eye, All-Seeing Eye / Surveillance Motif
### Community 18 - "ESLint Configuration"
Cohesion: 0.50
Nodes (3): eslintConfig, eslint, eslint-config-next
### Community 19 - "Runtime Dependencies"
Cohesion: 0.50
Nodes (4): dependencies, next, react, react-dom
### Community 20 - "Discord Icon Asset" ### Community 20 - "Discord Icon Asset"
Cohesion: 0.50 Cohesion: 0.50
Nodes (4): Discord (chat platform brand mark), Discord Icon (SVG social link asset), Profile social link icon, SVG Repo (icon asset source) Nodes (4): Discord (chat platform brand mark), Discord Icon (SVG social link asset), Profile social link icon, SVG Repo (icon asset source)
@ -206,23 +195,23 @@ Nodes (3): TikTok, TikTok Logo Icon, SVG Repo
## Knowledge Gaps ## Knowledge Gaps
- **54 isolated node(s):** `react-dom`, `@svgr/webpack`, `@tailwindcss/postcss`, `@types/node`, `@types/react` (+49 more) - **54 isolated node(s):** `react-dom`, `@svgr/webpack`, `@tailwindcss/postcss`, `@types/node`, `@types/react` (+49 more)
These have ≤1 connection - possible missing edges or undocumented components. (Counts symbols only; 227 node(s) total have ≤1 connection when file, concept and rationale nodes are included.) These have ≤1 connection - possible missing edges or undocumented components. (Counts symbols only; 233 node(s) total have ≤1 connection when file, concept and rationale nodes are included.)
- **15 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. - **14 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions ## Suggested Questions
_Questions this graph is uniquely positioned to answer:_ _Questions this graph is uniquely positioned to answer:_
- **Why does `Conventions & things that will bite you` connect `WindowsDesktop.tsx` to `braces Public API Methods`, `AGENTS.md — cold-boot notes`?** - **Why does `Conventions & things that will bite you` connect `apps.tsx` to `braces Public API Methods`, `AGENTS.md — cold-boot notes`?**
_High betweenness centrality (0.183) - this node is a cross-community bridge._ _High betweenness centrality (0.181) - this node is a cross-community bridge._
- **Are the 2 inferred relationships involving `WebLayout()` (e.g. with `How UI selection works` and `Web theme + light/dark (v2.2 rework)`) actually correct?** - **Are the 2 inferred relationships involving `WebLayout()` (e.g. with `How UI selection works` and `Web theme + light/dark (v2.2 rework)`) actually correct?**
_`WebLayout()` has 2 INFERRED edges - model-reasoned connections that need verification._ _`WebLayout()` has 2 INFERRED edges - model-reasoned connections that need verification._
- **What connects `react-dom`, `@svgr/webpack`, `@tailwindcss/postcss` to the rest of the system?** - **What connects `react-dom`, `@svgr/webpack`, `@tailwindcss/postcss` to the rest of the system?**
_54 weakly-connected nodes found - possible documentation gaps or missing edges._ _54 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `Desktop App Catalog` be split into smaller, more focused modules?** - **Should `Desktop App Catalog` be split into smaller, more focused modules?**
_Cohesion score 0.10873440285204991 - nodes in this community are weakly interconnected._ _Cohesion score 0.080338266384778 - nodes in this community are weakly interconnected._
- **Why does `braces()` connect `braces Public API Methods` to `WindowsDesktop.tsx`, `braces Parsing Engine`?** - **Why does `braces()` connect `braces Public API Methods` to `braces Parsing Engine`, `apps.tsx`?**
_High betweenness centrality (0.150) - this node is a cross-community bridge._ _High betweenness centrality (0.149) - this node is a cross-community bridge._
- **Should `Deployment & Ops Docs` be split into smaller, more focused modules?** - **Should `Deployment & Ops Docs` be split into smaller, more focused modules?**
_Cohesion score 0.08735632183908046 - nodes in this community are weakly interconnected._ _Cohesion score 0.08735632183908046 - nodes in this community are weakly interconnected._
- **Why does `next` connect `Next Config & API Routes` to `Desktop App Catalog`, `Root Package Dependencies`, `WindowsDesktop.tsx`, `apps.tsx`?** - **Why does `react` connect `Desktop App Catalog` to `useTheme.ts`, `apps.tsx`, `Next Config & API Routes`, `package.json`, `ProjectIndex.tsx`?**
_High betweenness centrality (0.126) - this node is a cross-community bridge._ _High betweenness centrality (0.136) - this node is a cross-community bridge._

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -24,9 +24,9 @@
"semantic_hash": "03388a201be8750ddcddb14dfbc34411" "semantic_hash": "03388a201be8750ddcddb14dfbc34411"
}, },
"package.json": { "package.json": {
"mtime": 1792100566.4931674, "mtime": 1792102753.0037653,
"seen": 1792100581.439811, "seen": 1792102795.132633,
"ast_hash": "fceb2aeaea53cfa26e74f36ec2cf4e7c", "ast_hash": "5b8d59a5fd4c3052fa6fb1dd3616838e",
"semantic_hash": "" "semantic_hash": ""
}, },
"postcss.config.mjs": { "postcss.config.mjs": {
@ -114,9 +114,9 @@
"semantic_hash": "20e25c727b4bb982e5a22d65efece143" "semantic_hash": "20e25c727b4bb982e5a22d65efece143"
}, },
"src/app/components/WebLayout.tsx": { "src/app/components/WebLayout.tsx": {
"mtime": 1792100566.4931674, "mtime": 1792102753.0127654,
"seen": 1792100581.4398491, "seen": 1792102795.1326752,
"ast_hash": "4c5f105a49587f804c8a59d17029e263", "ast_hash": "19ff085bedc94e6cea703bfccfacd52d",
"semantic_hash": "" "semantic_hash": ""
}, },
"src/app/components/WinWindow.tsx": { "src/app/components/WinWindow.tsx": {
@ -240,9 +240,9 @@
"semantic_hash": "b81dea2175efa2b0b6c38451ab5a4bbd" "semantic_hash": "b81dea2175efa2b0b6c38451ab5a4bbd"
}, },
"README.md": { "README.md": {
"mtime": 1792100566.5121677, "mtime": 1792102753.0337658,
"seen": 1792100581.439904, "seen": 1792102795.1327267,
"ast_hash": "374e4ca7cda87fbbe327cd53df378839", "ast_hash": "d42062ffe4714c5f5c00a7a7849bcb80",
"semantic_hash": "" "semantic_hash": ""
}, },
"public/llms.txt": { "public/llms.txt": {
@ -366,9 +366,9 @@
"semantic_hash": "6f69e16823f10d9c7af1625eff2b32e8" "semantic_hash": "6f69e16823f10d9c7af1625eff2b32e8"
}, },
"AGENTS.md": { "AGENTS.md": {
"mtime": 1792100572.9662693, "mtime": 1792102785.3782775,
"seen": 1792100581.439899, "seen": 1792102795.1327217,
"ast_hash": "13b85d5f3f9e7e69a7c3ec4862c4326d", "ast_hash": "594842bd5f504ca50401180b54e743b8",
"semantic_hash": "" "semantic_hash": ""
}, },
"public/freezmo_aero.png": { "public/freezmo_aero.png": {
@ -382,5 +382,11 @@
"seen": 1792099263.7671065, "seen": 1792099263.7671065,
"ast_hash": "78344ac481fd50f9366c30f041c8e135", "ast_hash": "78344ac481fd50f9366c30f041c8e135",
"semantic_hash": "" "semantic_hash": ""
},
"src/app/components/AeroMusic.tsx": {
"mtime": 1792102680.4096167,
"seen": 1792102795.1326532,
"ast_hash": "9e5d7d4591477a92d78eebf144627856",
"semantic_hash": ""
} }
} }

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "dvorakmortonxd", "name": "dvorakmortonxd",
"version": "2.2.21", "version": "2.2.22",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "dvorakmortonxd", "name": "dvorakmortonxd",
"version": "2.2.21", "version": "2.2.22",
"dependencies": { "dependencies": {
"next": "^16.3.8", "next": "^16.3.8",
"react": "^19.3.0", "react": "^19.3.0",

View file

@ -1,6 +1,6 @@
{ {
"name": "dvorakmortonxd", "name": "dvorakmortonxd",
"version": "2.2.21", "version": "2.2.22",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "node scripts/dev-server.mjs", "dev": "node scripts/dev-server.mjs",

View file

@ -0,0 +1,211 @@
"use client";
import { useEffect, useRef, useState } from "react";
/* Frutiger Aero soundtrack: a random track from the "Frutiger Aero" playlist on
SoundCloud. A hidden SoundCloud iframe is driven through their Widget API so
the playlist autoplays, jumps to a random track and re-randomises whenever
playback moves on; a small glass chip shows what's playing and gives the
visitor a play/pause control. The iframe is only mounted by `WebLayout` while
aero mode is active, so unmounting it stops the music. */
const PLAYLIST_URL = "https://soundcloud.com/rustie-gamer/sets/frutiger-aero";
const API_SRC = "https://w.soundcloud.com/player/api.js";
const VOLUME = 35;
/* Ignore PLAY events for this long after we pick a track: a `skip()` can still
report the previous index for a moment, which would otherwise bounce us into
an endless re-pick. */
const PICK_GUARD_MS = 3000;
type SoundCloudSound = {
title?: string;
user?: { username?: string };
};
type SoundCloudWidget = {
play: () => void;
pause: () => void;
skip: (index: number) => void;
setVolume: (volume: number) => void;
getSounds: (callback: (sounds: SoundCloudSound[]) => void) => void;
getCurrentSound: (callback: (sound: SoundCloudSound | null) => void) => void;
getCurrentSoundIndex: (callback: (index: number) => void) => void;
bind: (event: string, listener: () => void) => void;
};
type SoundCloudGlobal = {
Widget: ((element: HTMLIFrameElement) => SoundCloudWidget) & {
Events: { READY: string; PLAY: string; FINISH: string };
};
};
type Track = { title: string; artist: string };
const PLACEHOLDER: Track = { title: "Frutiger Aero", artist: "SoundCloud playlist" };
declare global {
interface Window {
SC?: SoundCloudGlobal;
}
}
const WIDGET_SRC =
"https://w.soundcloud.com/player/?url=" +
encodeURIComponent(PLAYLIST_URL) +
"&color=%2364e3ff&auto_play=true&hide_related=true&show_comments=false" +
"&show_user=false&show_reposts=false&show_teaser=false&visual=false";
let apiPromise: Promise<SoundCloudGlobal> | null = null;
/** Loads SoundCloud's Widget API once; later mounts reuse the same promise. */
function loadWidgetApi(): Promise<SoundCloudGlobal> {
if (window.SC) return Promise.resolve(window.SC);
apiPromise ??= new Promise<SoundCloudGlobal>((resolve, reject) => {
const script = document.createElement("script");
script.dataset.soundcloudApi = "";
script.src = API_SRC;
script.async = true;
script.addEventListener("load", () => {
if (window.SC) resolve(window.SC);
else reject(new Error("SoundCloud widget API did not initialise"));
});
script.addEventListener("error", () =>
reject(new Error("SoundCloud widget API failed to load")),
);
document.head.appendChild(script);
});
return apiPromise;
}
function toTrack(sound: SoundCloudSound | null | undefined): Track {
if (!sound) return PLACEHOLDER;
return {
title: sound.title || PLACEHOLDER.title,
artist: sound.user?.username || "SoundCloud",
};
}
export default function AeroMusic() {
const frameRef = useRef<HTMLIFrameElement>(null);
const widgetRef = useRef<SoundCloudWidget | null>(null);
const [track, setTrack] = useState<Track>(PLACEHOLDER);
const [playing, setPlaying] = useState(true);
useEffect(() => {
let cancelled = false;
/* The index we deliberately jumped to, and when — see PICK_GUARD_MS. */
let chosen = -1;
let pickedAt = 0;
let syncTimer = 0;
loadWidgetApi()
.then((SC) => {
const frame = frameRef.current;
if (cancelled || !frame) return;
const widget = SC.Widget(frame);
widgetRef.current = widget;
/* The chip mirrors what the player is actually on, so a skip that gets
ignored can't leave it lying. Partial reads are ignored rather than
blanking the label. */
const syncTrack = () => {
widget.getCurrentSound((sound) => {
if (cancelled || !sound || !sound.title) return;
setTrack({ title: sound.title, artist: sound.user?.username || "SoundCloud" });
});
};
const playRandomTrack = () => {
widget.getSounds((sounds) => {
if (cancelled || !sounds || sounds.length === 0) return;
let index = Math.floor(Math.random() * sounds.length);
/* Don't just replay whatever is already on. */
if (sounds.length > 1 && index === chosen) index = (index + 1) % sounds.length;
chosen = index;
pickedAt = Date.now();
widget.skip(index);
widget.play();
/* Show it straight away, then settle on the truth once the player
has caught up. */
setTrack(toTrack(sounds[index]));
window.clearTimeout(syncTimer);
syncTimer = window.setTimeout(syncTrack, 900);
});
};
widget.bind(SC.Widget.Events.READY, () => {
widget.setVolume(VOLUME);
playRandomTrack();
});
/* The set plays on in its own order — grab every chance to re-randomise
instead of following the playlist track by track. */
widget.bind(SC.Widget.Events.PLAY, () => {
setPlaying(true);
syncTrack();
if (Date.now() - pickedAt < PICK_GUARD_MS) return;
widget.getCurrentSoundIndex((index) => {
if (cancelled || index === chosen) return;
playRandomTrack();
});
});
/* Fires at the end of the set (or of the single selected track). */
widget.bind(SC.Widget.Events.FINISH, playRandomTrack);
/* Cover the case where READY already fired before we bound it. */
widget.setVolume(VOLUME);
playRandomTrack();
})
.catch(() => {
/* Offline, blocked or SoundCloud is down — the theme just stays silent. */
});
return () => {
cancelled = true;
window.clearTimeout(syncTimer);
widgetRef.current?.pause();
widgetRef.current = null;
};
}, []);
const toggle = () => {
const widget = widgetRef.current;
if (!widget) return;
if (playing) {
widget.pause();
setPlaying(false);
} else {
widget.play();
setPlaying(true);
}
};
return (
<>
<iframe
ref={frameRef}
className="web-aero-music-frame"
src={WIDGET_SRC}
title={`Frutiger Aero playlist on SoundCloud — ${track.title} by ${track.artist}`}
allow="autoplay"
loading="eager"
/>
<button
type="button"
className="web-aero-music"
onClick={toggle}
aria-pressed={playing}
title={playing ? "Pause the Frutiger Aero soundtrack" : "Play the Frutiger Aero soundtrack"}
>
<span className="web-aero-music-icon" aria-hidden="true">
{playing ? "❚❚" : "▶"}
</span>
<span className="web-aero-music-text">
<span className="web-aero-music-title">{track.title}</span>
<span className="web-aero-music-artist">{track.artist} · SoundCloud</span>
</span>
</button>
</>
);
}

View file

@ -8,6 +8,7 @@ import ContributionHeatmap from "./ContributionHeatmap";
import FeatureWindow from "./FeatureWindow"; import FeatureWindow from "./FeatureWindow";
import { CodingProjectIndex, HardwareProjectIndex } from "./ProjectIndex"; import { CodingProjectIndex, HardwareProjectIndex } from "./ProjectIndex";
import Clock from "./Clock"; import Clock from "./Clock";
import AeroMusic from "./AeroMusic";
import { useTheme } from "./useTheme"; import { useTheme } from "./useTheme";
import { useColorScheme } from "./useColorScheme"; import { useColorScheme } from "./useColorScheme";
import { socials, friends } from "../data"; import { socials, friends } from "../data";
@ -421,12 +422,14 @@ export default function WebLayout() {
</Section> </Section>
<footer className="web-footer glass"> <footer className="web-footer glass">
<span className="web-footer-label">v2.2.21</span> <span className="web-footer-label">v2.2.22</span>
© 2025-2026 Martin Dvořák. All rights reserved (not really I dont care, just © 2025-2026 Martin Dvořák. All rights reserved (not really I dont care, just
follow the GPL3.0 License) follow the GPL3.0 License)
</footer> </footer>
</main> </main>
</div> </div>
{aero && <AeroMusic />}
</> </>
); );
} }

View file

@ -2189,6 +2189,7 @@ html[data-theme="web"][data-aero="true"] .web-freezmo-ghost,
html[data-theme="web"][data-aero="true"] .web-freezmo-badge, html[data-theme="web"][data-aero="true"] .web-freezmo-badge,
html[data-theme="web"][data-aero="true"] .web-switch, html[data-theme="web"][data-aero="true"] .web-switch,
html[data-theme="web"][data-aero="true"] .web-footer-label, html[data-theme="web"][data-aero="true"] .web-footer-label,
html[data-theme="web"][data-aero="true"] .web-aero-music,
html[data-theme="web"][data-aero="true"] .fw, html[data-theme="web"][data-aero="true"] .fw,
html[data-theme="web"][data-aero="true"] .fw-url, html[data-theme="web"][data-aero="true"] .fw-url,
html[data-theme="web"][data-aero="true"] .fw-search, html[data-theme="web"][data-aero="true"] .fw-search,
@ -2248,6 +2249,89 @@ html[data-theme="web"][data-aero="true"] .social-icon {
opacity: 0.95; opacity: 0.95;
} }
/* ---------- Frutiger Aero soundtrack ("Gelwave" by skeuoss) ----------
The SoundCloud iframe is park in the corner at 1px and fully transparent —
it is only there to play audio; the glass chip below is the real control. */
html[data-theme="web"][data-aero="true"] .web-aero-music-frame {
position: fixed;
left: -8px;
bottom: -8px;
width: 1px;
height: 1px;
border: 0;
opacity: 0;
pointer-events: none;
}
html[data-theme="web"][data-aero="true"] .web-aero-music {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 40;
display: inline-flex;
align-items: center;
gap: 11px;
padding: 8px 16px 8px 11px;
border-radius: 999px;
font: inherit;
font-size: 0.78rem;
text-align: left;
cursor: pointer;
animation: aero-chip-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
}
html[data-theme="web"][data-aero="true"] .web-aero-music-icon {
display: grid;
place-items: center;
flex: none;
width: 28px;
height: 28px;
border-radius: 50%;
font-size: 0.58rem;
line-height: 1;
letter-spacing: 0.03em;
background: rgba(8, 28, 62, 0.3);
box-shadow:
inset 0 1px 0 rgba(190, 240, 255, 0.45),
inset 0 -2px 5px rgba(0, 140, 220, 0.25);
}
html[data-theme="web"][data-aero="true"] .web-aero-music-text {
display: flex;
flex-direction: column;
min-width: 0;
max-width: 42vw;
line-height: 1.3;
}
html[data-theme="web"][data-aero="true"] .web-aero-music-title {
overflow: hidden;
font-size: 0.8rem;
font-weight: 700;
white-space: nowrap;
text-overflow: ellipsis;
}
html[data-theme="web"][data-aero="true"] .web-aero-music-artist {
overflow: hidden;
font-size: 0.64rem;
font-weight: 600;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 0.82;
}
@keyframes aero-chip-in {
from {
opacity: 0;
transform: translateY(14px) scale(0.94);
}
to {
opacity: 1;
transform: none;
}
}
html[data-theme="web"][data-aero="true"] .heatmap-cell[data-level="1"], html[data-theme="web"][data-aero="true"] .heatmap-cell[data-level="1"],
html[data-theme="web"][data-aero="true"] .heatmap-legend-cell[data-level="1"] { html[data-theme="web"][data-aero="true"] .heatmap-legend-cell[data-level="1"] {
background-color: rgba(255, 255, 255, 0.26); background-color: rgba(255, 255, 255, 0.26);
@ -2412,6 +2496,15 @@ html[data-theme="web"][data-aero="true"] .heatmap-legend-cell[data-level="4"] {
.web-side-foot { .web-side-foot {
display: none; display: none;
} }
/* Keep the Aero soundtrack chip out of the way on a phone — icon + title only. */
html[data-theme="web"][data-aero="true"] .web-aero-music {
right: 12px;
bottom: 12px;
padding: 7px 13px 7px 9px;
}
html[data-theme="web"][data-aero="true"] .web-aero-music-artist {
display: none;
}
} }
@media (max-width: 400px) { @media (max-width: 400px) {