# HG changeset patch # User MrJuneJune # Date 1785887341 25200 # Node ID 30c2196d03d4f118334ff90812f23fb55dfd7ddd # Parent 5ec271d612ae34b4b2524cc9302f646a03d74bba [site] Integrate Zenbu themes and components Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> diff -r 5ec271d612ae -r 30c2196d03d4 .claude/skills/zenbu-design-system/SKILL.md --- a/.claude/skills/zenbu-design-system/SKILL.md Tue Aug 04 15:12:18 2026 -0700 +++ b/.claude/skills/zenbu-design-system/SKILL.md Tue Aug 04 16:49:01 2026 -0700 @@ -40,6 +40,9 @@ widths and reject horizontal page overflow. - Preserve the warm, restrained MUJI-like visual character: paper neutrals, natural material colors, strong typography, and deliberate color accents. +- Treat themes as semantic-token compositions. Preserve the built-in Paper, + Ink, and Playful themes plus automatic system light/dark behavior; components + must not branch on theme names. - Use semantic `--zen-color-*` aliases in component CSS. Use primitive ramps or the ten data tokens only when a semantic role does not fit; never encode meaning through color alone. diff -r 5ec271d612ae -r 30c2196d03d4 .claude/skills/zenbu-personal-site/SKILL.md --- a/.claude/skills/zenbu-personal-site/SKILL.md Tue Aug 04 15:12:18 2026 -0700 +++ b/.claude/skills/zenbu-personal-site/SKILL.md Tue Aug 04 16:49:01 2026 -0700 @@ -13,6 +13,9 @@ - `mrjunejune/src/`: static site source files and HTML templates. - `mrjunejune/src/parts/`: shared HTML includes used by `{{...}}` template placeholders. - `mrjunejune/src/public/`: public assets, PWA files, media, generated JS, icons, resume PDF. +- Zenbu UI assets are copied from `//design_system:components` and + `//design_system:styles` into `src/public/design-system/` by Bazel. Do not + hand-copy or fork shared component files. - `mrjunejune/test/`: integration tests and snapshots. - `mrjunejune/BUILD`: Bazel build, bundle, asset movement, and test-visible filegroups. - `mrjunejune/.config` and `.config.development`: server configuration. Do not commit secrets. @@ -68,4 +71,7 @@ - For binary responses, set `content-length` and avoid string-only operations on body bytes. - If a route is part of the public website, add or update snapshot coverage in `mrjunejune/test`. - Keep PWA changes consistent across `manifest.json`, `sw.js`, icons, and `PWA_SETUP.md`. +- Site themes use `data-zen-theme` with Auto, Paper, Ink, and Playful states. + Preserve the More Sugar font aliases and keep theme-specific values in shared + design-system tokens rather than branching inside components. - Reuse `seobeo`, `dowa`, `deita`, `s3`, and `markdown_converter` instead of adding parallel implementations. diff -r 5ec271d612ae -r 30c2196d03d4 design_system/README.md --- a/design_system/README.md Tue Aug 04 15:12:18 2026 -0700 +++ b/design_system/README.md Tue Aug 04 16:49:01 2026 -0700 @@ -57,6 +57,8 @@ Every visual token is a `--zen-*` custom property and can be overridden by the application. See [`COLORS.md`](COLORS.md) for the complete warm color ramps, material aliases, semantic roles, dark-theme behavior, and data palette. +Named [`themes`](THEMES.md) compose those tokens as Paper, Ink, and Playful +experiences in addition to automatic system light/dark behavior. Use [`zen-icon`](ICONS.md) for all interface iconography. Design-system source does not use emoji, ad hoc SVG, icon fonts, or third-party icon packages. diff -r 5ec271d612ae -r 30c2196d03d4 design_system/THEMES.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/THEMES.md Tue Aug 04 16:49:01 2026 -0700 @@ -0,0 +1,28 @@ +# Zenbu UI themes + +Themes are named token compositions, not separate component stylesheets. +Applications select one with `data-zen-theme` on the document root: + +```html + +``` + +Built-in themes: + +- `paper`: warm neutral surfaces, restrained natural accents, editorial type; +- `ink`: dark charcoal surfaces with paper-like text and softer status colors; +- `playful`: brighter clay, persimmon, violet, and rounded shapes; +- no attribute: follow the operating-system light/dark preference. + +Applications can provide their own fonts without changing component CSS: + +```css +:root { + --zen-font-editorial: "More Thin", sans-serif; + --zen-font-playful: "More", sans-serif; +} +``` + +Import `themes.css` after `tokens.css` and before component CSS. Components +continue to consume semantic tokens, so every built-in and application theme +inherits the same accessibility, motion, and native-HTML behavior. diff -r 5ec271d612ae -r 30c2196d03d4 design_system/src/index.html --- a/design_system/src/index.html Tue Aug 04 15:12:18 2026 -0700 +++ b/design_system/src/index.html Tue Aug 04 16:49:01 2026 -0700 @@ -6,6 +6,7 @@ Zenbu UI + diff -r 5ec271d612ae -r 30c2196d03d4 design_system/src/styles/themes.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/src/styles/themes.css Tue Aug 04 16:49:01 2026 -0700 @@ -0,0 +1,95 @@ +:root[data-zen-theme="paper"] { + color-scheme: light; + --zen-color-canvas: var(--zen-color-neutral-100); + --zen-color-surface: var(--zen-color-neutral-50); + --zen-color-surface-raised: var(--zen-color-neutral-50); + --zen-color-surface-muted: var(--zen-color-neutral-100); + --zen-color-surface-sunken: var(--zen-color-neutral-200); + --zen-color-text: var(--zen-color-neutral-950); + --zen-color-text-muted: var(--zen-color-neutral-600); + --zen-color-text-subtle: var(--zen-color-neutral-500); + --zen-color-border: var(--zen-color-neutral-800); + --zen-color-border-muted: var(--zen-color-neutral-300); + --zen-color-accent: var(--zen-color-amber-400); + --zen-color-accent-hover: var(--zen-color-amber-300); + --zen-color-accent-muted: var(--zen-color-amber-100); + --zen-color-info: var(--zen-color-blue-600); + --zen-color-success: var(--zen-color-green-600); + --zen-color-warning: var(--zen-color-orange-600); + --zen-color-danger: var(--zen-color-red-600); + --zen-color-focus: var(--zen-color-violet-600); + --zen-font-sans: var( + --zen-font-editorial, + ui-rounded, + system-ui, + sans-serif + ); +} + +:root[data-zen-theme="ink"] { + color-scheme: dark; + --zen-color-canvas: var(--zen-color-neutral-950); + --zen-color-surface: var(--zen-color-neutral-900); + --zen-color-surface-raised: var(--zen-color-neutral-800); + --zen-color-surface-muted: var(--zen-color-neutral-800); + --zen-color-surface-sunken: var(--zen-color-neutral-950); + --zen-color-text: var(--zen-color-neutral-50); + --zen-color-text-muted: var(--zen-color-neutral-300); + --zen-color-text-subtle: var(--zen-color-neutral-400); + --zen-color-border: var(--zen-color-neutral-200); + --zen-color-border-muted: var(--zen-color-neutral-700); + --zen-color-accent: var(--zen-color-amber-300); + --zen-color-accent-hover: var(--zen-color-amber-200); + --zen-color-accent-muted: var(--zen-color-amber-800); + --zen-color-info: var(--zen-color-blue-300); + --zen-color-success: var(--zen-color-green-300); + --zen-color-warning: var(--zen-color-orange-300); + --zen-color-danger: var(--zen-color-red-300); + --zen-color-on-info: var(--zen-color-neutral-950); + --zen-color-on-success: var(--zen-color-neutral-950); + --zen-color-on-warning: var(--zen-color-neutral-950); + --zen-color-on-danger: var(--zen-color-neutral-950); + --zen-color-focus: var(--zen-color-violet-300); + --zen-color-on-accent: var(--zen-color-neutral-950); + --zen-color-text-inverse: var(--zen-color-neutral-950); + --zen-font-sans: var( + --zen-font-editorial, + ui-rounded, + system-ui, + sans-serif + ); +} + +:root[data-zen-theme="playful"] { + color-scheme: light; + --zen-color-canvas: var(--zen-color-rose-100); + --zen-color-surface: var(--zen-color-amber-50); + --zen-color-surface-raised: var(--zen-color-neutral-50); + --zen-color-surface-muted: var(--zen-color-orange-100); + --zen-color-surface-sunken: var(--zen-color-violet-100); + --zen-color-text: var(--zen-color-blue-900); + --zen-color-text-muted: var(--zen-color-violet-700); + --zen-color-text-subtle: var(--zen-color-blue-600); + --zen-color-border: var(--zen-color-violet-700); + --zen-color-border-muted: var(--zen-color-rose-300); + --zen-color-accent: var(--zen-color-orange-300); + --zen-color-accent-hover: var(--zen-color-amber-300); + --zen-color-accent-muted: var(--zen-color-orange-100); + --zen-color-info: var(--zen-color-blue-600); + --zen-color-success: var(--zen-color-green-600); + --zen-color-warning: var(--zen-color-amber-600); + --zen-color-danger: var(--zen-color-rose-600); + --zen-color-on-success: var(--zen-color-neutral-50); + --zen-color-on-warning: var(--zen-color-neutral-50); + --zen-color-focus: var(--zen-color-violet-500); + --zen-color-on-accent: var(--zen-color-blue-950); + --zen-font-sans: var( + --zen-font-playful, + ui-rounded, + system-ui, + sans-serif + ); + --zen-radius-sm: 0.65rem; + --zen-radius-md: 1rem; + --zen-radius-lg: 1.5rem; +} diff -r 5ec271d612ae -r 30c2196d03d4 design_system/src/styles/tokens.css --- a/design_system/src/styles/tokens.css Tue Aug 04 15:12:18 2026 -0700 +++ b/design_system/src/styles/tokens.css Tue Aug 04 16:49:01 2026 -0700 @@ -268,7 +268,7 @@ } @media (prefers-color-scheme: dark) { - :root:not([data-zen-theme="light"]) { + :root:not([data-zen-theme]) { color-scheme: dark; --zen-color-canvas: #101218; --zen-color-surface: #181b23; diff -r 5ec271d612ae -r 30c2196d03d4 design_system/test/storybook_test.js --- a/design_system/test/storybook_test.js Tue Aug 04 15:12:18 2026 -0700 +++ b/design_system/test/storybook_test.js Tue Aug 04 16:49:01 2026 -0700 @@ -138,6 +138,7 @@ } for (const asset of [ '/styles/tokens.css', + '/styles/themes.css', '/styles/components.css', '/styles/shadcn.css', '/components/index.js', @@ -480,6 +481,52 @@ { length: 10 }, (_, index) => value(`data-${index + 1}`), ); + const themes = ['paper', 'ink', 'playful'].map(theme => { + root.dataset.zenTheme = theme; + const channels = value => value.match(/\d+(?:\.\d+)?/g) + .slice(0, 3) + .map(Number); + const luminance = value => { + const converted = channels(value).map(channel => { + const normalized = channel / 255; + return normalized <= 0.04045 + ? normalized / 12.92 + : ((normalized + 0.055) / 1.055) ** 2.4; + }); + return converted[0] * 0.2126 + + converted[1] * 0.7152 + + converted[2] * 0.0722; + }; + const contrasts = [ + 'info', + 'success', + 'warning', + 'danger', + ].map(tone => { + const probe = document.createElement('span'); + probe.style.background = `var(--zen-color-${tone})`; + probe.style.color = `var(--zen-color-on-${tone})`; + document.body.append(probe); + const probeStyle = getComputedStyle(probe); + const foreground = luminance(probeStyle.color); + const background = luminance(probeStyle.backgroundColor); + const contrast = (Math.max(foreground, background) + 0.05) / + (Math.min(foreground, background) + 0.05); + probe.remove(); + return contrast; + }); + return { + canvas: value('canvas'), + contrast: Math.min(...contrasts), + font: getComputedStyle(root) + .getPropertyValue('--zen-font-sans') + .trim(), + name: theme, + radius: getComputedStyle(root) + .getPropertyValue('--zen-radius-lg') + .trim(), + }; + }); delete root.dataset.zenTheme; return { darkData, @@ -488,6 +535,7 @@ lightMuted, materials, rampsComplete, + themes, }; }); assert.equal(colorTokenState.rampsComplete, true); @@ -495,6 +543,18 @@ assert.equal(new Set(colorTokenState.data).size, 10); assert.equal(new Set(colorTokenState.darkData).size, 10); assert.notDeepEqual(colorTokenState.data, colorTokenState.darkData); + assert.equal( + new Set(colorTokenState.themes.map(theme => theme.canvas)).size, + 3, + ); + assert.notEqual( + colorTokenState.themes.find(theme => theme.name === 'paper').radius, + colorTokenState.themes.find(theme => theme.name === 'playful').radius, + ); + assert.ok( + colorTokenState.themes.every(theme => theme.contrast >= 4.5), + JSON.stringify(colorTokenState.themes), + ); assert.notEqual( colorTokenState.lightMuted, colorTokenState.darkMuted, diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/BUILD --- a/mrjunejune/BUILD Tue Aug 04 15:12:18 2026 -0700 +++ b/mrjunejune/BUILD Tue Aug 04 16:49:01 2026 -0700 @@ -134,6 +134,18 @@ ) move_files_into_dir( + name = "design_system_components", + srcs = ["//design_system:components"], + dest = "src/public/design-system/components", +) + +move_files_into_dir( + name = "design_system_styles", + srcs = ["//design_system:styles"], + dest = "src/public/design-system/styles", +) + +move_files_into_dir( name = "icons", srcs = [ "//assets:icons", @@ -161,7 +173,7 @@ srcs = glob( ["src/**"], exclude = ["src/**/*.png"], - ) + [":react_pages", ":shared_js_non_public", ":shared_js_file", ":rich_editor_js", ":icons", ":generated_public_webp", ":hlsjs_runtime"], + ) + [":react_pages", ":shared_js_non_public", ":shared_js_file", ":rich_editor_js", ":design_system_components", ":design_system_styles", ":icons", ":generated_public_webp", ":hlsjs_runtime"], visibility = ["//mrjunejune/test:__pkg__"], ) diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/src/base.css --- a/mrjunejune/src/base.css Tue Aug 04 15:12:18 2026 -0700 +++ b/mrjunejune/src/base.css Tue Aug 04 16:49:01 2026 -0700 @@ -74,7 +74,7 @@ 0 -8px -24px rgba(159, 140, 96, 33%), 0 -16px -32px rgba(159, 140, 96, 33%); } @media (prefers-color-scheme: dark) { - :root:not(.light-mode) { + :root:not([data-zen-theme]) { --white: hsl(224, 10%, 10%); --black: hsl(0, 0%, 90%); --darkgray: #cccccc; @@ -98,6 +98,39 @@ } } +:root { + --zen-font-editorial: "More Thin", "Roboto", sans-serif; + --zen-font-playful: "More", "More Thin", sans-serif; + --zen-font-sans: var(--zen-font-editorial); +} + +:root:not([data-zen-theme]), +:root[data-zen-theme] { + --white: var(--zen-color-surface); + --black: var(--zen-color-text); + --darkgray: var(--zen-color-border-strong); + --lightgray: var(--zen-color-border-muted); + --gray-light: var(--zen-color-border-muted); + --green: var(--zen-color-success); + --orange: var(--zen-color-warning); + --purple: var(--zen-color-violet-400); + --red: var(--zen-color-danger); + --blue: var(--zen-color-info); + --darktext: var(--zen-color-text-muted); + --text: var(--zen-color-text); + --graytext: var(--zen-color-text-muted); + --lighttext: var(--zen-color-text-subtle); + --awesome: var(--zen-color-persimmon); + --accent: var(--zen-color-info); + --accent-dark: var(--zen-color-indigo); + --gray-dark: var(--zen-color-text); + --gray-gradient: linear-gradient( + var(--zen-color-canvas), + var(--zen-color-surface) + ); + --box-shadow: var(--zen-shadow-md); +} + /* --- fonts --- */ /* @font-face { @@ -141,7 +174,7 @@ } body { - font-family: "More Thin", sans-serif; + font-family: var(--zen-font-sans, "More Thin", sans-serif); margin: 0; padding: 0; text-align: left; @@ -149,7 +182,7 @@ background-size: 100% 600px; word-wrap: break-word; overflow-wrap: break-word; - color: rgb(var(--gray-dark)); + color: var(--gray-dark); font-size: 20px; line-height: 1.7; -webkit-font-smoothing: antialiased; @@ -170,7 +203,7 @@ h5, h6 { margin: 0 0 0.5rem 0; - color: rgb(var(--black)); + color: var(--black); line-height: 1.2; } @@ -228,7 +261,7 @@ code { padding: 2px 5px; - background-color: rgb(var(--gray-light)); + background-color: var(--gray-light); border-radius: 2px; } @@ -250,7 +283,7 @@ hr { border: none; - border-top: 1px solid rgb(var(--gray-light)); + border-top: 1px solid var(--gray-light); } .center { @@ -395,4 +428,3 @@ /* Icon color = button text color */ background-color: currentColor; } - diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/src/index.html --- a/mrjunejune/src/index.html Tue Aug 04 15:12:18 2026 -0700 +++ b/mrjunejune/src/index.html Tue Aug 04 16:49:01 2026 -0700 @@ -64,6 +64,28 @@ z-index: 101; cursor: pointer; } + + .site-link-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + gap: var(--zen-space-4); + } + + .site-link-grid > a { + color: inherit; + text-decoration: none; + } + + .site-link-grid zen-card, + .site-link-grid article { + height: 100%; + } + + .site-link-grid p { + margin: 0; + color: var(--zen-color-text-muted); + font-size: 0.9rem; + } @@ -80,12 +102,28 @@

Links

- + diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/src/index.js --- a/mrjunejune/src/index.js Tue Aug 04 15:12:18 2026 -0700 +++ b/mrjunejune/src/index.js Tue Aug 04 16:49:01 2026 -0700 @@ -1,50 +1,74 @@ const THEME_STORAGE_KEY = 'theme-preference'; -// Get stored preference or detect system preference -function getThemePreference() -{ - const stored = localStorage.getItem(THEME_STORAGE_KEY); - if (stored) { - return stored; - } - return 'auto'; +const THEMES = ['auto', 'paper', 'ink', 'playful']; +const THEME_LABELS = { + auto: 'Auto', + paper: 'Paper', + ink: 'Ink', + playful: 'Playful', +}; + +function normalizeTheme(value) { + if (value === 'light') return 'paper'; + if (value === 'dark') return 'ink'; + return THEMES.includes(value) ? value : 'auto'; +} + +function getThemePreference() { + return normalizeTheme(localStorage.getItem(THEME_STORAGE_KEY)); +} + +function updateThemeColor() { + const probe = document.createElement('span'); + probe.style.background = 'var(--zen-color-canvas)'; + document.body.append(probe); + const color = getComputedStyle(probe).backgroundColor; + probe.remove(); + document.querySelector('meta[name="theme-color"]') + ?.setAttribute('content', color); } -function applyTheme(preference) -{ - const root = document.documentElement; - - if (preference === 'light') { - root.classList.remove('dark', 'auto'); - root.classList.add('light-mode'); - } else if (preference === 'dark') { - root.classList.remove('light-mode', 'auto'); - root.classList.add('dark'); - } else { - // Auto - remove manual overrides and let CSS media query handle it - root.classList.remove('light-mode', 'dark'); - root.classList.add('auto'); - } +function updateThemeControl(preference) { + const toggle = document.querySelector('#themeToggle'); + const label = document.querySelector('#themeName'); + if (label) label.textContent = THEME_LABELS[preference]; + toggle?.setAttribute( + 'aria-label', + `Change site theme. Current theme: ${THEME_LABELS[preference]}`, + ); } -(function() { - const currentPreference = getThemePreference(); - applyTheme(currentPreference); -})(); - -/* Toggle lights auto and dark */ -const toggle = document.querySelector('#themeToggle'); -toggle.addEventListener('click', function() { - let preference = getThemePreference(); +function applyTheme(value, persist = false) { + const preference = normalizeTheme(value); + const root = document.documentElement; + root.classList.remove('light-mode', 'dark', 'auto'); + if (preference === 'auto') delete root.dataset.zenTheme; + else root.dataset.zenTheme = preference; + if (persist) localStorage.setItem(THEME_STORAGE_KEY, preference); + updateThemeControl(preference); + requestAnimationFrame(updateThemeColor); + document.dispatchEvent(new CustomEvent('zen-theme-change', { + detail: { theme: preference }, + })); + return preference; +} - // Cycle through: auto -> light -> dark -> auto - if (preference === 'auto') { - preference = 'light'; - } else if (preference === 'light') { - preference = 'dark'; - } else { - preference = 'auto'; - } +const currentPreference = applyTheme(getThemePreference()); +if (localStorage.getItem(THEME_STORAGE_KEY) !== currentPreference) { + localStorage.setItem(THEME_STORAGE_KEY, currentPreference); +} - localStorage.setItem(THEME_STORAGE_KEY, preference); - applyTheme(preference); +document.querySelector('#themeToggle')?.addEventListener('click', () => { + const current = getThemePreference(); + const next = THEMES[(THEMES.indexOf(current) + 1) % THEMES.length]; + applyTheme(next, true); }); + +matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + if (getThemePreference() === 'auto') updateThemeColor(); +}); + +window.MrJuneJuneTheme = Object.freeze({ + apply: theme => applyTheme(theme, true), + current: getThemePreference, + themes: [...THEMES], +}); diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/src/parts/base_head.html --- a/mrjunejune/src/parts/base_head.html Tue Aug 04 15:12:18 2026 -0700 +++ b/mrjunejune/src/parts/base_head.html Tue Aug 04 16:49:01 2026 -0700 @@ -1,6 +1,6 @@ - + @@ -16,8 +16,14 @@ + + + + - diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/src/parts/header.html --- a/mrjunejune/src/parts/header.html Tue Aug 04 15:12:18 2026 -0700 +++ b/mrjunejune/src/parts/header.html Tue Aug 04 16:49:01 2026 -0700 @@ -1,35 +1,41 @@ -
- -
+ + +

MrJuneJune

- diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/src/public/dog-game.js --- a/mrjunejune/src/public/dog-game.js Tue Aug 04 15:12:18 2026 -0700 +++ b/mrjunejune/src/public/dog-game.js Tue Aug 04 16:49:01 2026 -0700 @@ -51,8 +51,8 @@ function isDarkMode() { const root = document.documentElement; - if (root.classList.contains("dark")) return true; - if (root.classList.contains("light-mode")) return false; + if (root.dataset.zenTheme === "ink") return true; + if (root.dataset.zenTheme) return false; return darkModePreference.matches; } @@ -324,8 +324,9 @@ window.addEventListener('resize', resizeCanvas); new MutationObserver(updateStarPalette).observe( document.documentElement, - { attributes: true, attributeFilter: ["class"] } + { attributes: true, attributeFilter: ["data-zen-theme"] } ); +document.addEventListener("zen-theme-change", updateStarPalette); darkModePreference.addEventListener("change", updateStarPalette); window.onload = () => { diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/src/public/pwa-register.js --- a/mrjunejune/src/public/pwa-register.js Tue Aug 04 15:12:18 2026 -0700 +++ b/mrjunejune/src/public/pwa-register.js Tue Aug 04 16:49:01 2026 -0700 @@ -2,7 +2,7 @@ if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker - .register('/public/sw.js') + .register('/sw.js', { scope: '/' }) .then((registration) => { // console.log('[PWA] Service Worker registered:', registration.scope); diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/src/public/sw.js --- a/mrjunejune/src/public/sw.js Tue Aug 04 15:12:18 2026 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,143 +0,0 @@ -// Service Worker for MrJuneJune PWA -const CACHE_VERSION = 'v4-hlsjs'; -const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`; - -// Files to cache immediately on install -const STATIC_CACHE = [ - '/', - '/base.css', - '/public/epi_all_colors.svg', - '/public/fonts/Roboto-Regular.ttf', - '/public/fonts/Roboto-Thin.ttf', - '/public/fonts/more-sugar.regular.otf', - '/public/fonts/more-sugar.thin.otf', -]; - -// Install event - cache static assets -self.addEventListener('install', (event) => { - console.log('[SW] Installing service worker...'); - - event.waitUntil( - caches.open(CACHE_NAME).then((cache) => { - console.log('[SW] Caching static assets'); - return cache.addAll(STATIC_CACHE); - }).then(() => { - console.log('[SW] Skip waiting'); - return self.skipWaiting(); - }) - ); -}); - -// Activate event - clean up old caches -self.addEventListener('activate', (event) => { - console.log('[SW] Activating service worker...'); - - event.waitUntil( - caches.keys().then((cacheNames) => { - return Promise.all( - cacheNames.map((cacheName) => { - if (cacheName !== CACHE_NAME) { - console.log('[SW] Deleting old cache:', cacheName); - return caches.delete(cacheName); - } - }) - ); - }).then(() => { - console.log('[SW] Claiming clients'); - return self.clients.claim(); - }) - ); -}); - -// Fetch event - serve from cache, fallback to network -self.addEventListener('fetch', (event) => { - const { request } = event; - const url = new URL(request.url); - - // Skip non-GET requests - if (request.method !== 'GET') { - return; - } - - // Skip chrome-extension and other non-http(s) requests - if (!url.protocol.startsWith('http')) { - return; - } - - // Skip API calls and media uploads (always go to network) - if (url.pathname.startsWith('/api/')) { - return; - } - - // The HLS tool and media must always reflect the current player version. - if (url.pathname.startsWith('/tools/hls_player') || - url.pathname.startsWith('/public/hls-sample/')) { - return; - } - - event.respondWith( - caches.match(request).then((cachedResponse) => { - if (cachedResponse) { - console.log('[SW] Serving from cache:', url.pathname); - return cachedResponse; - } - - // Not in cache, fetch from network - return fetch(request).then((networkResponse) => { - // Only cache successful responses - if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'error') { - return networkResponse; - } - - // Cache specific file types - const shouldCache = - url.pathname.endsWith('.css') || - url.pathname.endsWith('.js') || - url.pathname.endsWith('.svg') || - url.pathname.endsWith('.jpg') || - url.pathname.endsWith('.webp') || - url.pathname.endsWith('.woff') || - url.pathname.endsWith('.woff2') || - url.pathname.endsWith('.ttf') || - url.pathname.endsWith('.otf') || - url.pathname.startsWith('/blog/') || - url.pathname.startsWith('/notes/') || - url.pathname === '/'; - - if (shouldCache) { - const responseToCache = networkResponse.clone(); - caches.open(CACHE_NAME).then((cache) => { - console.log('[SW] Caching new resource:', url.pathname); - cache.put(request, responseToCache); - }); - } - - return networkResponse; - }).catch((error) => { - console.log('[SW] Fetch failed:', error); - - // Return offline page for HTML requests - if (request.headers.get('Accept').includes('text/html')) { - return caches.match('/offline.html'); - } - }); - }) - ); -}); - -// Handle messages from the client -self.addEventListener('message', (event) => { - if (event.data && event.data.type === 'SKIP_WAITING') { - self.skipWaiting(); - } - - if (event.data && event.data.type === 'CLEAR_CACHE') { - event.waitUntil( - caches.keys().then((cacheNames) => { - return Promise.all( - cacheNames.map((cacheName) => caches.delete(cacheName)) - ); - }) - ); - } -}); diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/src/sw.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/sw.js Tue Aug 04 16:49:01 2026 -0700 @@ -0,0 +1,162 @@ +// Root-scoped Service Worker for MrJuneJune PWA +const CACHE_VERSION = 'v5-zenbu-themes'; +const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`; + +// Files to cache immediately on install +const STATIC_CACHE = [ + '/', + '/index.js', + '/base.css', + '/public/design-system/styles/tokens.css', + '/public/design-system/styles/themes.css', + '/public/design-system/styles/components.css', + '/public/design-system/styles/shadcn.css', + '/public/design-system/components/index.js', + '/public/design-system/components/alert.js', + '/public/design-system/components/button.js', + '/public/design-system/components/card.js', + '/public/design-system/components/collections.js', + '/public/design-system/components/disclosure.js', + '/public/design-system/components/field.js', + '/public/design-system/components/forms.js', + '/public/design-system/components/icon.js', + '/public/design-system/components/notifications.js', + '/public/design-system/components/overlays.js', + '/public/design-system/components/primitives.js', + '/public/design-system/components/stack.js', + '/public/design-system/components/story.js', + '/public/epi_all_colors.svg', + '/public/fonts/Roboto-Regular.ttf', + '/public/fonts/Roboto-Thin.ttf', + '/public/fonts/more-sugar.regular.otf', + '/public/fonts/more-sugar.thin.otf', +]; + +// Install event - cache static assets +self.addEventListener('install', (event) => { + console.log('[SW] Installing service worker...'); + + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + console.log('[SW] Caching static assets'); + return cache.addAll(STATIC_CACHE); + }).then(() => { + console.log('[SW] Skip waiting'); + return self.skipWaiting(); + }) + ); +}); + +// Activate event - clean up old caches +self.addEventListener('activate', (event) => { + console.log('[SW] Activating service worker...'); + + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + console.log('[SW] Deleting old cache:', cacheName); + return caches.delete(cacheName); + } + }) + ); + }).then(() => { + console.log('[SW] Claiming clients'); + return self.clients.claim(); + }) + ); +}); + +// Fetch event - serve from cache, fallback to network +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + // Skip non-GET requests + if (request.method !== 'GET') { + return; + } + + // Skip chrome-extension and other non-http(s) requests + if (!url.protocol.startsWith('http')) { + return; + } + + // Skip API calls and media uploads (always go to network) + if (url.pathname.startsWith('/api/')) { + return; + } + + // The HLS tool and media must always reflect the current player version. + if (url.pathname.startsWith('/tools/hls_player') || + url.pathname.startsWith('/public/hls-sample/')) { + return; + } + + event.respondWith( + caches.match(request).then((cachedResponse) => { + if (cachedResponse) { + console.log('[SW] Serving from cache:', url.pathname); + return cachedResponse; + } + + // Not in cache, fetch from network + return fetch(request).then((networkResponse) => { + // Only cache successful responses + if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'error') { + return networkResponse; + } + + // Cache specific file types + const shouldCache = + url.pathname.endsWith('.css') || + url.pathname.endsWith('.js') || + url.pathname.endsWith('.svg') || + url.pathname.endsWith('.jpg') || + url.pathname.endsWith('.webp') || + url.pathname.endsWith('.woff') || + url.pathname.endsWith('.woff2') || + url.pathname.endsWith('.ttf') || + url.pathname.endsWith('.otf') || + url.pathname.startsWith('/blog/') || + url.pathname.startsWith('/notes/') || + url.pathname === '/'; + + if (shouldCache) { + const responseToCache = networkResponse.clone(); + caches.open(CACHE_NAME).then((cache) => { + console.log('[SW] Caching new resource:', url.pathname); + cache.put(request, responseToCache); + }); + } + + return networkResponse; + }).catch((error) => { + console.log('[SW] Fetch failed:', error); + + // Return offline page for HTML requests + if (request.headers.get('Accept').includes('text/html')) { + return caches.match('/offline.html'); + } + }); + }) + ); +}); + +// Handle messages from the client +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } + + if (event.data && event.data.type === 'CLEAR_CACHE') { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => caches.delete(cacheName)) + ); + }) + ); + } +}); diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/src/tools/hls_player/index.css --- a/mrjunejune/src/tools/hls_player/index.css Tue Aug 04 15:12:18 2026 -0700 +++ b/mrjunejune/src/tools/hls_player/index.css Tue Aug 04 16:49:01 2026 -0700 @@ -23,7 +23,7 @@ .hls-url-row input { width: 100%; padding: 0.65rem 0.8rem; - color: rgb(var(--gray-dark)); + color: var(--gray-dark); background: var(--white); border: 1px solid var(--darkgray); border-radius: 6px; diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/src/tools/markdown_to_html/index.css --- a/mrjunejune/src/tools/markdown_to_html/index.css Tue Aug 04 15:12:18 2026 -0700 +++ b/mrjunejune/src/tools/markdown_to_html/index.css Tue Aug 04 16:49:01 2026 -0700 @@ -17,7 +17,7 @@ } .header h1 { - color: rgb(var(--gray-dark)); + color: var(--gray-dark); margin-bottom: 10px; } @@ -49,7 +49,7 @@ .label { font-weight: 600; - color: rgb(var(--gray-dark)); + color: var(--gray-dark); font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px; @@ -75,8 +75,8 @@ flex: 1; width: 100%; padding: 15px; - background: rgb(var(--gray-light)); - color: rgb(var(--gray-dark)); + background: var(--gray-light); + color: var(--gray-dark); border: 1px solid rgba(var(--gray), 0.3); border-radius: 4px; font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; @@ -97,7 +97,7 @@ border: 1px solid rgba(var(--gray), 0.3); border-radius: 4px; background: var(--white); - color: rgb(var(--gray-dark)); + color: var(--gray-dark); overflow-y: auto; font-size: 15px; } @@ -105,7 +105,7 @@ /* Markdown output styles */ #output h1, #output h2, #output h3, #output h4, #output h5, #output h6 { margin: 1em 0 0.5em 0; - color: rgb(var(--gray-dark)); + color: var(--gray-dark); line-height: 1.3; } @@ -132,7 +132,7 @@ } #output code { - background: rgb(var(--gray-light)); + background: var(--gray-light); padding: 2px 6px; border-radius: 3px; font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; @@ -160,7 +160,7 @@ border-left: 4px solid var(--accent); padding: 10px 15px; margin: 1em 0; - background: rgb(var(--gray-light)); + background: var(--gray-light); color: rgb(var(--gray)); font-style: italic; } @@ -176,7 +176,7 @@ #output hr { border: none; - border-top: 1px solid rgb(var(--gray-light)); + border-top: 1px solid var(--gray-light); margin: 1.5em 0; } diff -r 5ec271d612ae -r 30c2196d03d4 mrjunejune/test/snapshots/blog.snapshot --- a/mrjunejune/test/snapshots/blog.snapshot Tue Aug 04 15:12:18 2026 -0700 +++ b/mrjunejune/test/snapshots/blog.snapshot Tue Aug 04 16:49:01 2026 -0700 @@ -3,7 +3,10 @@ + + + @@ -16,44 +19,58 @@ + + + + + -
- -
+ + +

MrJuneJune

-

Blogs