From 0e134e9c6c2e0ad01315085c28dba6467ff664aa Mon Sep 17 00:00:00 2001 From: Yaowei Zheng Date: Tue, 21 Jul 2026 18:00:11 +0800 Subject: [PATCH] Share language and theme across the sites; prepare the 0.1.0 release (#13) Co-authored-by: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 +- .../2026-07-20-landing-site.md | 0 .../2026-07-20-models-and-credentials.md | 0 .../2026-07-20-readme-blog-and-docs.md | 0 .../{0.0.2 => 0.1.0}/2026-07-20-skills.md | 0 .../{0.0.2 => 0.1.0}/2026-07-20-tooling.md | 2 +- .../{0.0.2 => 0.1.0}/2026-07-20-web-app.md | 0 ...-07-21-model-reference-and-review-fixes.md | 0 .../0.1.0/2026-07-21-site-preferences-sync.md | 11 ++++ changelog/{0.0.2 => 0.1.0}/README.md | 6 +- package.json | 2 +- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- packages/core/src/index.ts | 2 +- packages/docs/index.html | 10 ++- packages/docs/package.json | 2 +- packages/docs/src/state/locale.tsx | 12 ++-- packages/docs/src/state/site-prefs.ts | 66 +++++++++++++++++++ packages/docs/src/state/theme.tsx | 12 ++-- packages/landing/index.html | 13 ++++ packages/landing/package.json | 2 +- packages/landing/src/state/locale.tsx | 12 ++-- packages/landing/src/state/site-prefs.ts | 66 +++++++++++++++++++ packages/landing/src/state/theme.tsx | 12 ++-- packages/server/package.json | 2 +- packages/skills/package.json | 2 +- packages/web/package.json | 2 +- 27 files changed, 206 insertions(+), 36 deletions(-) rename changelog/{0.0.2 => 0.1.0}/2026-07-20-landing-site.md (100%) rename changelog/{0.0.2 => 0.1.0}/2026-07-20-models-and-credentials.md (100%) rename changelog/{0.0.2 => 0.1.0}/2026-07-20-readme-blog-and-docs.md (100%) rename changelog/{0.0.2 => 0.1.0}/2026-07-20-skills.md (100%) rename changelog/{0.0.2 => 0.1.0}/2026-07-20-tooling.md (99%) rename changelog/{0.0.2 => 0.1.0}/2026-07-20-web-app.md (100%) rename changelog/{0.0.2 => 0.1.0}/2026-07-21-model-reference-and-review-fixes.md (100%) create mode 100644 changelog/0.1.0/2026-07-21-site-preferences-sync.md rename changelog/{0.0.2 => 0.1.0}/README.md (85%) create mode 100644 packages/docs/src/state/site-prefs.ts create mode 100644 packages/landing/src/state/site-prefs.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 83dc026..e95e09f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,5 +2,5 @@ One brief line per release. Per-release detail lives in [`changelog//`](changelog/). -- **0.0.2** — unreleased. ([details](changelog/0.0.2/README.md)) +- **0.1.0** — 2026-07-21. First feature release: the Web App, landing site and docs, the model catalog, and the self-improvement skills. ([details](changelog/0.1.0/README.md)) - **0.0.1** — 2026-07-19. First tagged release; changelog history starts after this tag. diff --git a/changelog/0.0.2/2026-07-20-landing-site.md b/changelog/0.1.0/2026-07-20-landing-site.md similarity index 100% rename from changelog/0.0.2/2026-07-20-landing-site.md rename to changelog/0.1.0/2026-07-20-landing-site.md diff --git a/changelog/0.0.2/2026-07-20-models-and-credentials.md b/changelog/0.1.0/2026-07-20-models-and-credentials.md similarity index 100% rename from changelog/0.0.2/2026-07-20-models-and-credentials.md rename to changelog/0.1.0/2026-07-20-models-and-credentials.md diff --git a/changelog/0.0.2/2026-07-20-readme-blog-and-docs.md b/changelog/0.1.0/2026-07-20-readme-blog-and-docs.md similarity index 100% rename from changelog/0.0.2/2026-07-20-readme-blog-and-docs.md rename to changelog/0.1.0/2026-07-20-readme-blog-and-docs.md diff --git a/changelog/0.0.2/2026-07-20-skills.md b/changelog/0.1.0/2026-07-20-skills.md similarity index 100% rename from changelog/0.0.2/2026-07-20-skills.md rename to changelog/0.1.0/2026-07-20-skills.md diff --git a/changelog/0.0.2/2026-07-20-tooling.md b/changelog/0.1.0/2026-07-20-tooling.md similarity index 99% rename from changelog/0.0.2/2026-07-20-tooling.md rename to changelog/0.1.0/2026-07-20-tooling.md index 9d2ed87..1c06590 100644 --- a/changelog/0.0.2/2026-07-20-tooling.md +++ b/changelog/0.1.0/2026-07-20-tooling.md @@ -41,7 +41,7 @@ one-line-per-change index, and detail files live inside the folder (mirrors agen ## Details - `changelog//README.md` is the release summary: one line per change with its - title and one-sentence summary, linking the detail file relatively; `0.0.2/README.md` + title and one-sentence summary, linking the detail file relatively; `0.1.0/README.md` absorbs the section previously kept in the top-level index. - `changelog/README.md` now documents only the layout and the entry conventions (no per-file listing). diff --git a/changelog/0.0.2/2026-07-20-web-app.md b/changelog/0.1.0/2026-07-20-web-app.md similarity index 100% rename from changelog/0.0.2/2026-07-20-web-app.md rename to changelog/0.1.0/2026-07-20-web-app.md diff --git a/changelog/0.0.2/2026-07-21-model-reference-and-review-fixes.md b/changelog/0.1.0/2026-07-21-model-reference-and-review-fixes.md similarity index 100% rename from changelog/0.0.2/2026-07-21-model-reference-and-review-fixes.md rename to changelog/0.1.0/2026-07-21-model-reference-and-review-fixes.md diff --git a/changelog/0.1.0/2026-07-21-site-preferences-sync.md b/changelog/0.1.0/2026-07-21-site-preferences-sync.md new file mode 100644 index 0000000..634530a --- /dev/null +++ b/changelog/0.1.0/2026-07-21-site-preferences-sync.md @@ -0,0 +1,11 @@ +# Language and theme carry across the landing page and the docs site + +The two sites are separate SPAs deployed to one origin — penguin.ooo and penguin.ooo/docs/ — but each persisted its preferences under its own `localStorage` key (`penguin-landing.*` and `penguin-docs.*`). Picking dark mode or Chinese on one and clicking through to the other therefore dropped the visitor back to the system default, which read as the setting being ignored. + +- Both sites now read and write one shared pair of keys, `penguin-site.theme` and `penguin-site.lang`, via a small `state/site-prefs.ts` module duplicated in each package (they share no package, exactly as `theme.tsx` and `locale.tsx` already are). The keys must stay identical on both sides; if they drift, the sync stops silently. +- The retired per-site keys are still read as a fallback, so a returning visitor keeps the choice they made before the change instead of being reset once. +- A `storage` listener covers the case where both sites are open at once: switching theme or language in one tab now updates the other without a reload. The site-to-site hop is a full page load and is served by the read on mount. +- The docs site's pre-paint theme script reads the shared key too, and the landing page — which had no such script — gained the same one. Without it, arriving at the landing page from a dark docs page flashed a white background before React booted. +- Reading `localStorage` is wrapped throughout: it throws outright in cookie-blocked and partitioned contexts, and a preference is not worth taking the page down for. + +Verified against the assembled site (landing at `/`, docs under `/docs/`, one origin): a dark + Chinese choice made on the landing page survives the hop to the docs site and back, a change made on the docs side propagates the other way, and a profile carrying only the retired `penguin-docs.*` keys still renders dark and Chinese. diff --git a/changelog/0.0.2/README.md b/changelog/0.1.0/README.md similarity index 85% rename from changelog/0.0.2/README.md rename to changelog/0.1.0/README.md index cd6ebb9..4b0d0e6 100644 --- a/changelog/0.0.2/README.md +++ b/changelog/0.1.0/README.md @@ -1,6 +1,8 @@ -# Version 0.0.2 +# Version 0.1.0 -Unreleased. +Released on 2026-07-21. + +- [2026-07-21] Language and theme are shared between the landing page and the docs site, so a choice made on one carries across to the other. ([details](2026-07-21-site-preferences-sync.md)) - [2026-07-21] A model is always referenced by an explicit `(provider, model_id)` pair — the provider is never inferred, guessed, or defaulted — together with the fixes from a full review of the release branch, refreshed benchmark results, and the demo videos on the landing page. ([details](2026-07-21-model-reference-and-review-fixes.md)) diff --git a/package.json b/package.json index f4961cd..8bec10c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "penguin-harness", - "version": "0.0.1", + "version": "0.1.0", "private": true, "type": "module", "description": "PenguinHarness — TypeScript AI Agent (SDK + CLI).", diff --git a/packages/cli/package.json b/packages/cli/package.json index 401b8dc..fa1107f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@prismshadow/penguin-cli", - "version": "0.0.1", + "version": "0.1.0", "type": "module", "description": "PenguinHarness CLI: interactive REPL and single-task runner over @prismshadow/penguin-core.", "license": "Apache-2.0", diff --git a/packages/core/package.json b/packages/core/package.json index abd4c28..2b88b58 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@prismshadow/penguin-core", - "version": "0.0.1", + "version": "0.1.0", "type": "module", "description": "PenguinHarness core SDK: context_engine, OmniMessage protocol, LLM/Environment interfaces.", "license": "Apache-2.0", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fdbd62f..fef74c0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -48,4 +48,4 @@ export { Agent, createAgent } from "./agent.js"; export type { CreateAgentOptions, CreateSessionOptions, ResumeSessionOptions } from "./agent.js"; /** SDK version number. */ -export const VERSION = "0.0.1"; +export const VERSION = "0.1.0"; diff --git a/packages/docs/index.html b/packages/docs/index.html index 98be81d..952b819 100644 --- a/packages/docs/index.html +++ b/packages/docs/index.html @@ -18,11 +18,15 @@ PenguinHarness Docs - +
diff --git a/packages/landing/package.json b/packages/landing/package.json index f60b76d..a3be8ef 100644 --- a/packages/landing/package.json +++ b/packages/landing/package.json @@ -1,6 +1,6 @@ { "name": "@prismshadow/penguin-landing", - "version": "0.0.1", + "version": "0.1.0", "private": true, "type": "module", "description": "PenguinHarness landing page (React + Vite + Tailwind CSS): multilingual, light/dark themes, benchmark showcase, and a local Markdown blog, deployed to GitHub Pages via GitHub Actions.", diff --git a/packages/landing/src/state/locale.tsx b/packages/landing/src/state/locale.tsx index 1c48269..ea9d68c 100644 --- a/packages/landing/src/state/locale.tsx +++ b/packages/landing/src/state/locale.tsx @@ -14,13 +14,14 @@ import { useState, } from "react"; import type { ReactNode } from "react"; +import { LANG_KEY, readPref, subscribePref, writePref } from "./site-prefs"; import { setActiveStrings, zh } from "../lib/strings"; import { en } from "../lib/strings-en"; export type LangPref = "zh" | "en" | "system"; export type Locale = "zh" | "en"; -const STORAGE_KEY = "penguin-landing.lang"; +const LANG_PREFS = ["zh", "en", "system"] as const; interface LocaleContextValue { lang: LangPref; @@ -44,9 +45,7 @@ function resolve(lang: LangPref): Locale { } function initialLang(): LangPref { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored === "zh" || stored === "en" || stored === "system") return stored; - return "system"; + return readPref(LANG_KEY, LANG_PREFS) ?? "system"; } export function LocaleProvider({ children }: { children: ReactNode }) { @@ -71,10 +70,13 @@ export function LocaleProvider({ children }: { children: ReactNode }) { }, [lang]); const setLang = useCallback((next: LangPref) => { - localStorage.setItem(STORAGE_KEY, next); + writePref(LANG_KEY, next); setLangState(next); }, []); + // The sibling site (landing <-> docs) writing the shared key in another tab. + useEffect(() => subscribePref(LANG_KEY, LANG_PREFS, setLangState), []); + return ( {children} ); diff --git a/packages/landing/src/state/site-prefs.ts b/packages/landing/src/state/site-prefs.ts new file mode 100644 index 0000000..6d56cf8 --- /dev/null +++ b/packages/landing/src/state/site-prefs.ts @@ -0,0 +1,66 @@ +/** + * Preferences shared by the landing page and the docs site. + * + * The two are separate SPAs but ship to one origin (penguin.ooo and penguin.ooo/docs/), + * so a single localStorage key is all it takes for a language or theme choice to survive + * the hop between them. They previously wrote `penguin-landing.*` and `penguin-docs.*`, + * which is why picking dark mode on the landing page and clicking through to the docs + * dropped you back into light. + * + * This module is duplicated verbatim in packages/docs (as theme.tsx and locale.tsx + * already are — the two sites share no package). The KEYS below must stay identical on + * both sides; if they drift, the sync silently stops working with nothing failing. + */ + +export const THEME_KEY = "penguin-site.theme"; +export const LANG_KEY = "penguin-site.lang"; + +/** Pre-unification keys, still read once so an existing visitor keeps their choice. */ +const LEGACY_KEYS: Record = { + [THEME_KEY]: ["penguin-landing.theme", "penguin-docs.theme"], + [LANG_KEY]: ["penguin-landing.lang", "penguin-docs.lang"], +}; + +/** + * Reads a stored preference, falling back to the retired per-site keys. Everything is + * wrapped: localStorage throws outright in a cookie-blocked or partitioned context, and + * a preference is never worth taking the page down for. + */ +export function readPref(key: string, allowed: readonly T[]): T | null { + try { + for (const k of [key, ...(LEGACY_KEYS[key] ?? [])]) { + const stored = localStorage.getItem(k); + if (stored && (allowed as readonly string[]).includes(stored)) return stored as T; + } + } catch { + // Storage unavailable — fall through to the system default. + } + return null; +} + +export function writePref(key: string, value: string): void { + try { + localStorage.setItem(key, value); + } catch { + // Non-persistent session: the choice still applies for this page. + } +} + +/** + * Live sync for the both-tabs-open case. The storage event fires in every *other* tab on + * the origin, so switching theme on the landing page updates an already-open docs tab + * without a reload. Navigating between the two sites is a full page load and is covered + * by the read on mount instead. + */ +export function subscribePref( + key: string, + allowed: readonly T[], + onChange: (value: T) => void, +): () => void { + const handler = (e: StorageEvent) => { + if (e.key !== key || e.newValue === null) return; + if ((allowed as readonly string[]).includes(e.newValue)) onChange(e.newValue as T); + }; + window.addEventListener("storage", handler); + return () => window.removeEventListener("storage", handler); +} diff --git a/packages/landing/src/state/theme.tsx b/packages/landing/src/state/theme.tsx index 4f3a8cd..53f978c 100644 --- a/packages/landing/src/state/theme.tsx +++ b/packages/landing/src/state/theme.tsx @@ -5,10 +5,11 @@ */ import { createContext, useCallback, useContext, useEffect, useState } from "react"; import type { ReactNode } from "react"; +import { THEME_KEY, readPref, subscribePref, writePref } from "./site-prefs"; export type ThemeMode = "light" | "dark" | "system"; -const MODE_KEY = "penguin-landing.theme"; +const THEME_MODES = ["light", "dark", "system"] as const; interface ThemeContextValue { mode: ThemeMode; @@ -20,9 +21,7 @@ interface ThemeContextValue { const ThemeContext = createContext(null); function initialMode(): ThemeMode { - const stored = localStorage.getItem(MODE_KEY); - if (stored === "light" || stored === "dark" || stored === "system") return stored; - return "system"; + return readPref(THEME_KEY, THEME_MODES) ?? "system"; } function systemDark(): boolean { @@ -49,10 +48,13 @@ export function ThemeProvider({ children }: { children: ReactNode }) { }, [mode]); const setMode = useCallback((next: ThemeMode) => { - localStorage.setItem(MODE_KEY, next); + writePref(THEME_KEY, next); setModeState(next); }, []); + // The sibling site (landing <-> docs) writing the shared key in another tab. + useEffect(() => subscribePref(THEME_KEY, THEME_MODES, setModeState), []); + return {children}; } diff --git a/packages/server/package.json b/packages/server/package.json index e4e7261..fd605f5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@prismshadow/penguin-server", - "version": "0.0.1", + "version": "0.1.0", "type": "module", "description": "PenguinHarness web server: multi-user authentication and authorization, Session execution with SSE streaming, and usage accounting, built on @prismshadow/penguin-core.", "license": "Apache-2.0", diff --git a/packages/skills/package.json b/packages/skills/package.json index ff3ce3d..ec88e2f 100644 --- a/packages/skills/package.json +++ b/packages/skills/package.json @@ -1,6 +1,6 @@ { "name": "@prismshadow/penguin-skills", - "version": "0.0.1", + "version": "0.1.0", "type": "module", "description": "PenguinHarness skill library: built-in SKILL.md documents and skill groups, decoupled from core.", "license": "Apache-2.0", diff --git a/packages/web/package.json b/packages/web/package.json index c6caee2..b2dc43f 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@prismshadow/penguin-web", - "version": "0.0.1", + "version": "0.1.0", "private": true, "type": "module", "description": "PenguinHarness web frontend SPA: React + Vite + Tailwind CSS, rendering conversations streamed over the OmniMessage protocol, plus Agent configuration, model configuration, usage stats, and Traces.",