Repository navigation
feat(web): switch skins automatically with the system or the sun - #49
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
"Follow the sun" needs a location before first paint, without asking. tzdata's zone.tab names each zone's principal city; tools/gen-tz-coords.mjs turns it, plus backward's legacy link names (which Chrome reports, e.g. Asia/Calcutta), into a 549-zone table from tzdata 2026e, about 7 KB gzipped. A zone the table can't place falls back to the equator at the longitude its UTC offset implies. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
skinPrefs.ts reads and writes the four new localStorage keys (mode, the light and dark slots, a typed sun location) next to the existing knot.skin, each surviving a missing or throwing storage, and decides the skin to show: the hand-picked one, or the light or dark slot by the system's appearance or by whether the sun is up. A slot holding a skin of the other scheme counts as unset. readInitialSkin moves here as readManualSkin; ui.ts keeps the old name as an alias. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`skin` is now the skin showing, resolved from the mode, the hand-picked skin, the light and dark slots and the sun location. Going into an automatic mode seeds the slot of the skin showing if it has no pick yet; going back to By hand keeps the skin showing. refreshSkin and adoptStoredSkinPrefs never write storage and leave <html> alone when the skin showing is unchanged, for the watcher that comes next. main.tsx stamps the resolved skin before first paint. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Follow system switches live with the OS appearance and survives a reload. Follow the sun, with the time zone pinned and the clock faked, shows the light skin by day and the dark skin after sunset, live and from first paint, and a typed location wins over the time zone. The changelog gains the entry under Unreleased, Added. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An unplaced zone's longitude now wraps to the exact meridian of its UTC offset instead of clamping at 180, so UTC+14 lands at 150° W rather than on the antimeridian, an hour off. ICU reports America/Coral_Harbour for Atikokan, Ontario, while tzdata's backward file links it to America/Panama. The generator now overrides that name with Atikokan's zone.tab coordinates. A typed coordinate also accepts the minus sign (U+2212). The size-budget test reads the table next to itself, wherever vitest runs from. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The sunrise and sunset times use the locale's short time style, and the unplaced-zone sentence writes its 06:00 and 18:00 on the same clock. A failed Save marks every field that is wrong, each with its own message, and moves focus to the first, so a repeated failure is read out again. The edit button is described by the times and location lines, so landing on it after Save, Cancel or "Use time zone" reads the result. In forced-colours mode the chosen segment of the mode control takes the system highlight, and its keyboard focus shows as an outline instead of a box-shadow ring. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
main.tsx stamps the skin the store already resolved on load instead of resolving it a second time, so first paint and the store can't disagree. resolveInitialSkin goes; a fresh-module test pins the store's initial skin for sun mode after sunset. setSunLocation turns away a location that storage wouldn't read back (non-finite or out of range), so the store never holds one a reload would drop. A new test pins that sun mode without a typed location places the sun by the browser's time zone, re-read on every call. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The daytime check now runs at 19:00 UTC, when only the time zone's table entry for Europe/Berlin has the sun still up; the equator fallback would already be in the night. Every reload assertion also checks data-theme. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The changelog entry says the skin is now synced across open tabs, including one picked by hand. The plan no longer names a local scratch directory for tzdata, and sunEvent's comment says which polar verdict a sun that never crosses the sunrise altitude gives, and that sunrise's is checked first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Skins can now switch on their own. Settings → Appearance offers three choices:
The automatic modes use one light skin and one dark skin, picked in the existing card grid. This is frontend only and stored per browser. There's no server, API or migration change and no new dependency.
Design:
docs/superpowers/specs/2026-10-24-auto-skin-switching-design.md. Plan:docs/superpowers/plans/2026-10-24-auto-skin-switching.md.knot.skinModemeans By hand, andknot.skinkeeps its meaning, including the legacyknot.themefallback.Asia/Calcutta.UTC,Etc/*) use the UTC offset's meridian at the equator, which gives a day of roughly 06:00–18:00.Evidence
pnpm test: 474 passed.After:
pnpm test: 64 files, 572 tests passed.tscandeslint --max-warnings 0are clean. The new tests cover:matchMediastub and storage events;flows/skin-auto.spec.ts: 3 passed in Chromium.emulateMediaflips the OS appearance, and the skin follows without a reload.page.clockwithtimezoneId: Europe/Berlin. It expects light at 19:00Z, which only the time-zone placement gives, then dark after the 19:33Z sunset, both live and after a reload.flows/skin.spec.ts: 2 passed, unchanged.tree-reorder.spec.ts"nest", which fails the same way onmain.Merge Danger
Door: two-way
The new preferences live in new
localStoragekeys. A revert leaves them unused andknot.skinuntouched, so everyone falls back to their hand-picked skin.Blast Radius: every page load
The pre-paint skin stamp on every page, including login, setup and public share pages, now goes through the resolver. The tz table (about 7 KB gzipped) joins the main bundle. Syncing a hand-picked skin across tabs is new behaviour.
🤖 Generated with Claude Code