diff --git a/scripts/editor-performance-benchmark.mjs b/scripts/editor-performance-benchmark.mjs index 1629c5e..b6ece51 100644 --- a/scripts/editor-performance-benchmark.mjs +++ b/scripts/editor-performance-benchmark.mjs @@ -468,7 +468,12 @@ function summarizeScenario(scenarioName, scenario, runs) { } async function runBenchmarks(baseUrl) { - const browser = await chromium.launch({ headless: !options.headful }) + const browser = await chromium.launch({ + headless: !options.headful, + ...(process.env.PLAYWRIGHT_EXECUTABLE_PATH + ? { executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH } + : {}), + }) const summaries = {} try { for (const scenarioName of options.scenarioNames) { diff --git a/src/App.css b/src/App.css index d0dc2b3..5336edd 100644 --- a/src/App.css +++ b/src/App.css @@ -1,73 +1,42 @@ /* App layout - minimal CSS, most styling via Tailwind */ .app-shell { - /* HoloLake Era visual system: the inherited Tolaria dark palette is warm - charcoal; override semantic tokens here so every existing panel speaks - the same deep-lake blue language as the world entrance. */ - --surface-app: #071126; - --surface-sidebar: #0a1730; - --surface-panel: #0d1c38; - --surface-card: #102343; - --surface-popover: #12284c; - --surface-input: #0a1730; - --surface-button: #172e55; - --surface-dialog: #0d1d3a; - --surface-editor: #09162d; - --surface-overlay: rgba(2, 7, 18, .68); - --text-primary: #edf4ff; - --text-secondary: #b9c9e2; - --text-tertiary: #8ea4c6; - --text-muted: #7188ac; - --text-heading: #f3f7ff; - --border-default: #1d3a67; - --border-subtle: #142b4d; - --border-strong: #315588; - --border-input: #274a78; - --sidebar-border: #17365f; - --state-hover: #142b4e; - --state-hover-subtle: #0e213f; - --state-selected: #173d70; - --state-selected-strong: #24538d; - --state-active: #1d477e; - --accent-blue: #8dbbff; - --accent-blue-bg: rgba(105, 165, 255, .23); - --accent-blue-light: rgba(105, 165, 255, .16); display: flex; flex-direction: column; height: 100%; width: 100%; overflow: hidden; - background: - radial-gradient(58rem 35rem at 82% -16%, var(--guanghu-aurora), transparent 62%), - radial-gradient(42rem 30rem at -12% 112%, var(--guanghu-glow), transparent 65%), - var(--surface-app); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border-primary) 82%, var(--accent-blue)); + background: var(--surface-app); + box-shadow: inset 0 0 0 1px var(--border-primary); } -.hololake-bar { height: 52px; flex: 0 0 52px; display: flex; align-items: center; gap: 14px; padding: 0 20px; color: #eef4ff; background: linear-gradient(100deg, #08122b, #172d58 52%, #0b1731); border-bottom: 1px solid rgba(153, 189, 255, .28); letter-spacing: .02em; } +.hololake-bar { height: 44px; flex: 0 0 44px; display: flex; align-items: center; gap: 12px; padding: 0 16px; color: var(--text-primary); background: var(--surface-panel); border-bottom: 1px solid var(--border-default); letter-spacing: .02em; } .hololake-bar__brand, .hololake-bar__library { border: 0; background: transparent; color: inherit; cursor: pointer; } -.hololake-bar__brand { display: inline-flex; align-items: center; gap: 9px; font-size: 16px; font-weight: 650; letter-spacing: .04em; } -.hololake-bar__brand b { color: #9cc3ff; font-weight: 500; } -.hololake-bar__mark { display: grid; place-items: center; width: 24px; height: 24px; color: #dceaff; border: 1px solid rgba(205, 225, 255, .48); border-radius: 50%; box-shadow: 0 0 22px rgba(96, 165, 250, .65); } -.hololake-bar__line { width: 1px; height: 17px; background: rgba(196, 218, 255, .35); } -.hololake-bar__context { color: #b7c8e7; font-size: 12px; } -.hololake-bar__library { margin-left: auto; padding: 6px 10px; border: 1px solid rgba(185, 211, 255, .36); border-radius: 999px; color: #dceaff; font-size: 12px; } -.hololake-home { position: relative; z-index: 2; width: 100%; overflow: auto; padding: clamp(32px, 6vw, 84px); color: #ecf4ff; background: radial-gradient(circle at 75% 10%, rgba(75, 135, 255, .25), transparent 28%), radial-gradient(circle at 12% 92%, rgba(83, 220, 255, .15), transparent 34%), #071126; } +.hololake-bar__brand { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 650; letter-spacing: .02em; } +.hololake-bar__brand b { color: var(--accent-blue); font-weight: 600; } +.hololake-bar__mark { display: grid; place-items: center; width: 20px; height: 20px; color: var(--accent-blue); border: 1px solid var(--border-strong); border-radius: 50%; } +.hololake-bar__line { width: 1px; height: 16px; background: var(--border-default); } +.hololake-bar__context { color: var(--text-secondary); font-size: 12px; } +.hololake-bar__library { margin-left: auto; padding: 5px 9px; border: 1px solid var(--border-default); border-radius: 6px; color: var(--text-secondary); font-size: 12px; } +.hololake-bar__library:hover { background: var(--state-hover-subtle); color: var(--text-primary); } +.hololake-bar__theme { padding: 5px 9px; border: 1px solid var(--border-default); border-radius: 6px; background: transparent; color: var(--text-secondary); font-size: 12px; cursor: pointer; } +.hololake-bar__theme:hover { background: var(--state-hover-subtle); color: var(--text-primary); } +.hololake-home { position: relative; z-index: 2; width: 100%; overflow: auto; padding: clamp(28px, 5vw, 64px); color: var(--text-primary); background: var(--surface-app); } .hololake-home__hero { position: relative; max-width: 740px; padding: 24px 0 56px; overflow: hidden; } -.hololake-home__halo { position: absolute; right: -110px; top: -180px; width: 390px; aspect-ratio: 1; border: 1px solid rgba(166, 205, 255, .28); border-radius: 50%; box-shadow: 0 0 0 45px rgba(103, 165, 255, .05), 0 0 0 105px rgba(103, 165, 255, .035), 0 0 100px rgba(77, 143, 255, .4); } -.hololake-home__eyebrow, .hololake-home__section-head span { color: #8db5f7; font-size: 12px; letter-spacing: .16em; text-transform: uppercase; } +.hololake-home__halo { position: absolute; right: -80px; top: -140px; width: 300px; aspect-ratio: 1; border: 1px solid var(--border-default); border-radius: 50%; opacity: .45; } +.hololake-home__eyebrow, .hololake-home__section-head span { color: var(--accent-blue); font-size: 12px; letter-spacing: .12em; text-transform: uppercase; } .hololake-home h1 { position: relative; margin: 18px 0 0; font-family: Georgia, 'Times New Roman', serif; font-size: clamp(52px, 7vw, 92px); font-weight: 500; line-height: .95; letter-spacing: -.06em; } .hololake-home h1 em { color: #9cc7ff; font-style: italic; font-weight: 400; } .hololake-home__lead { margin: 20px 0 0; font-size: 18px; letter-spacing: .16em; } -.hololake-home__copy { max-width: 560px; margin: 16px 0 0; color: #b9c8df; font-size: 15px; line-height: 1.9; } +.hololake-home__copy { max-width: 560px; margin: 16px 0 0; color: var(--text-secondary); font-size: 15px; line-height: 1.8; } .hololake-home__actions { display: flex; align-items: center; gap: 18px; margin-top: 30px; } -.hololake-home__primary { padding: 12px 18px; border: 1px solid #b9d5ff; border-radius: 8px; background: linear-gradient(135deg, #c8dcff, #6093e8); color: #061126; font-weight: 700; cursor: pointer; } +.hololake-home__primary { padding: 10px 16px; border: 1px solid var(--border-strong); border-radius: 6px; background: var(--accent-blue); color: var(--text-inverse); font-weight: 650; cursor: pointer; } .hololake-home__primary span, .hololake-module button span { margin-left: 12px; } -.hololake-home__status { color: #9eb3d4; font-size: 12px; }.hololake-home__status i { display: inline-block; width: 7px; height: 7px; margin-right: 7px; border-radius: 50%; background: #67e8b6; box-shadow: 0 0 12px #67e8b6; } +.hololake-home__status { color: var(--text-secondary); font-size: 12px; }.hololake-home__status i { display: inline-block; width: 7px; height: 7px; margin-right: 7px; border-radius: 50%; background: var(--accent-green); } .hololake-home__signal-grid, .hololake-home__module-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; } -.hololake-home__signal-grid article, .hololake-module { border: 1px solid rgba(157, 192, 244, .2); border-radius: 14px; background: linear-gradient(145deg, rgba(30, 57, 105, .55), rgba(12, 25, 51, .78)); box-shadow: inset 0 1px rgba(255,255,255,.06); } -.hololake-home__signal-grid article { padding: 18px; }.hololake-home__signal-grid span, .hololake-home__signal-grid small { display: block; color: #91a9ce; font-size: 11px; }.hololake-home__signal-grid strong { display: block; margin: 10px 0 6px; font-size: 15px; } -.hololake-home__modules { margin-top: 52px; }.hololake-home__section-head { display: flex; align-items: end; justify-content: space-between; margin-bottom: 14px; }.hololake-home__section-head p { margin: 0; color: #91a9ce; font-size: 13px; } -.hololake-module { min-height: 215px; padding: 22px; display: flex; flex-direction: column; }.hololake-module__number { color: #78aaf7; font-family: monospace; font-size: 12px; }.hololake-module h2 { margin: auto 0 8px; font-size: 22px; font-weight: 600; }.hololake-module p { margin: 0; color: #aebed7; font-size: 13px; line-height: 1.7; }.hololake-module button { margin-top: 19px; padding: 0; border: 0; color: #c8dcff; background: transparent; text-align: left; font: inherit; cursor: pointer; }.hololake-module__soon { margin-top: 19px; color: #7185a8; font-size: 12px; } +.hololake-home__signal-grid article, .hololake-module { border: 1px solid var(--border-default); border-radius: 8px; background: var(--surface-card); } +.hololake-home__signal-grid article { padding: 18px; }.hololake-home__signal-grid span, .hololake-home__signal-grid small { display: block; color: var(--text-secondary); font-size: 11px; }.hololake-home__signal-grid strong { display: block; margin: 10px 0 6px; font-size: 15px; } +.hololake-home__modules { margin-top: 44px; }.hololake-home__section-head { display: flex; align-items: end; justify-content: space-between; margin-bottom: 14px; }.hololake-home__section-head p { margin: 0; color: var(--text-secondary); font-size: 13px; } +.hololake-module { min-height: 185px; padding: 20px; display: flex; flex-direction: column; }.hololake-module__number { color: var(--accent-blue); font-family: monospace; font-size: 12px; }.hololake-module h2 { margin: auto 0 8px; font-size: 20px; font-weight: 600; }.hololake-module p { margin: 0; color: var(--text-secondary); font-size: 13px; line-height: 1.7; }.hololake-module button { margin-top: 19px; padding: 0; border: 0; color: var(--accent-blue); background: transparent; text-align: left; font: inherit; cursor: pointer; }.hololake-module__soon { margin-top: 19px; color: var(--text-muted); font-size: 12px; } @media (max-width: 780px) { .hololake-home__signal-grid, .hololake-home__module-grid { grid-template-columns: 1fr; }.hololake-bar__context { display: none; }.hololake-home { padding: 28px; } } .app { @@ -82,7 +51,7 @@ min-width: 220px; display: flex; flex-direction: column; - background: linear-gradient(180deg, #10254a, var(--surface-sidebar) 40%, #08152c); + background: var(--surface-sidebar); } .app__sidebar > * { @@ -100,8 +69,7 @@ flex-direction: column; position: relative; z-index: 20; - background: linear-gradient(180deg, #0d2040, #091730 48%, #08152c); - box-shadow: 14px 0 30px rgba(1, 7, 20, .36); + background: var(--surface-sidebar); } .app__note-list > * { @@ -118,26 +86,7 @@ flex-direction: column; container-type: inline-size; container-name: editor; - position: relative; - background: - linear-gradient(var(--guanghu-grid) 1px, transparent 1px), - linear-gradient(90deg, var(--guanghu-grid) 1px, transparent 1px), - color-mix(in srgb, var(--surface-editor) 94%, var(--guanghu-glow)); - background-size: 32px 32px, 32px 32px, auto; -} - -.app__editor::before { - content: ''; - position: absolute; - inset: 0; - pointer-events: none; - background: linear-gradient(180deg, color-mix(in srgb, var(--surface-editor) 28%, transparent), transparent 13rem); - z-index: 0; -} - -.app__editor > * { - position: relative; - z-index: 1; + background: var(--surface-editor); } .app__editor > * { diff --git a/src/App.tsx b/src/App.tsx index c8bc0fa..78ac6ad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,7 @@ import { McpSetupDialog } from './components/McpSetupDialog' import { NoteRetargetingDialogs } from './components/note-retargeting/NoteRetargetingDialogs' import { StartupScreen } from './components/StartupScreen' import { HoloLakeHome } from './components/HoloLakeHome' +import { GUANGHU_THEME_LABELS } from './lib/guanghuTheme' import { useAiAgentsOnboarding } from './hooks/useAiAgentsOnboarding' import { useAiAgentsStatus } from './hooks/useAiAgentsStatus' import { useVaultAiGuidanceStatus } from './hooks/useVaultAiGuidanceStatus' @@ -42,6 +43,7 @@ import { useVaultSwitcher } from './hooks/useVaultSwitcher' import { useGitHistory } from './hooks/useGitHistory' import { useUpdater, restartApp } from './hooks/useUpdater' import { useAutoSync } from './hooks/useAutoSync' +import { useGuanghuTheme } from './hooks/useGuanghuTheme' import { useConflictResolver } from './hooks/useConflictResolver' import { useVaultConfig } from './hooks/useVaultConfig' import { useOnboarding } from './hooks/useOnboarding' @@ -166,7 +168,8 @@ function App() { function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | null }) { const aiWorkspaceWindow = false - const [showHoloLakeHome, setShowHoloLakeHome] = useState(true) + const [showHoloLakeHome, setShowHoloLakeHome] = useState(false) + const { theme: guanghuTheme, cycleTheme } = useGuanghuTheme() const [selection, setSelection] = useState(DEFAULT_SELECTION) const [noteListFilter, setNoteListFilter] = useState('open') const [pendingNoteListPdfExportPath, setPendingNoteListPdfExportPath] = useState(null) @@ -1628,6 +1631,7 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu 光湖语言人格驱动操作系统 +
{sidebarVisible && ( diff --git a/src/components/Editor.css b/src/components/Editor.css index 132fc3f..24f0738 100644 --- a/src/components/Editor.css +++ b/src/components/Editor.css @@ -24,36 +24,23 @@ .guanghu-portal-card { width: 100%; - margin: 1.1rem 0; - padding: 1.15rem 1.25rem 1.2rem; - border: 1px solid color-mix(in srgb, var(--colors-accent) 46%, var(--colors-border)); - border-radius: 1rem; - background: - linear-gradient(135deg, color-mix(in srgb, var(--colors-accent) 16%, transparent), transparent 42%), - radial-gradient(circle at 92% 16%, color-mix(in srgb, var(--colors-accent) 25%, transparent), transparent 36%), - var(--guanghu-card-highlight, var(--colors-surface)); - box-shadow: - 0 0.8rem 2.2rem color-mix(in srgb, black 14%, transparent), - inset 0 1px 0 color-mix(in srgb, white 28%, transparent); - overflow: hidden; - position: relative; - transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease; + margin: .75rem 0; + padding: 1rem 1.1rem; + border: 1px solid color-mix(in srgb, var(--colors-accent) 32%, var(--colors-border)); + border-radius: .65rem; + background: var(--colors-surface); } .guanghu-portal-card::before { content: ''; position: absolute; inset: 0 auto 0 0; - width: 3px; - background: linear-gradient(var(--colors-accent), color-mix(in srgb, var(--colors-accent) 12%, transparent)); + width: 2px; + background: var(--colors-accent); } .guanghu-portal-card:hover { - border-color: color-mix(in srgb, var(--colors-accent) 72%, var(--colors-border)); - box-shadow: - 0 1rem 2.6rem color-mix(in srgb, var(--colors-accent) 18%, transparent), - inset 0 1px 0 color-mix(in srgb, white 32%, transparent); - transform: translateY(-1px); + border-color: color-mix(in srgb, var(--colors-accent) 58%, var(--colors-border)); } .guanghu-portal-card__eyebrow { @@ -215,23 +202,19 @@ body.mac-chrome .app:not(:has(.app__sidebar)):not(:has(.app__note-list)) .breadc .editor__blocknote-container .bn-editor { width: 100%; - padding: 36px 0 72px; + padding: 20px 0 48px; } .editor__blocknote-container .bn-editor > .bn-block-group { - padding: 1.25rem clamp(1rem, 3vw, 2.25rem) 3rem; - border: 1px solid color-mix(in srgb, var(--border-primary) 82%, transparent); - border-radius: 1.25rem; - background: color-mix(in srgb, var(--surface-editor) 86%, transparent); - box-shadow: 0 1.3rem 3.5rem color-mix(in srgb, black 8%, transparent); - backdrop-filter: blur(16px); + padding: 0 clamp(1rem, 3vw, 2.25rem) 2rem; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + backdrop-filter: none; } @media (max-width: 720px) { - .app__editor { - background-size: 22px 22px, 22px 22px, auto; - } - .editor__blocknote-container .bn-editor > .bn-block-group { border-inline: 0; border-radius: 0; diff --git a/src/hooks/useGuanghuTheme.ts b/src/hooks/useGuanghuTheme.ts new file mode 100644 index 0000000..2c42f2d --- /dev/null +++ b/src/hooks/useGuanghuTheme.ts @@ -0,0 +1,30 @@ +import { useCallback, useEffect, useState } from 'react' +import { + GUANGHU_THEMES, + GUANGHU_THEME_STORAGE_KEY, + readGuanghuTheme, + writeGuanghuTheme, + type GuanghuTheme, +} from '../lib/guanghuTheme' + +export function useGuanghuTheme(): { theme: GuanghuTheme; cycleTheme: () => void } { + const [theme, setTheme] = useState(() => ( + typeof window === 'undefined' ? 'native' : readGuanghuTheme(window.localStorage) + )) + + useEffect(() => { + document.documentElement.setAttribute('data-guanghu-theme', theme) + writeGuanghuTheme(window.localStorage, theme) + }, [theme]) + + const cycleTheme = useCallback(() => { + setTheme(current => { + const index = GUANGHU_THEMES.indexOf(current) + return GUANGHU_THEMES[(index + 1) % GUANGHU_THEMES.length] + }) + }, []) + + return { theme, cycleTheme } +} + +export { GUANGHU_THEME_STORAGE_KEY } diff --git a/src/index.css b/src/index.css index 7bbaa4c..30a010c 100644 --- a/src/index.css +++ b/src/index.css @@ -180,11 +180,10 @@ --link-color: var(--accent-blue); --link-hover: var(--accent-blue-hover); - /* Guanghu visual layer — an app-owned, ice-lake palette. */ - --guanghu-aurora: rgba(91, 181, 255, 0.2); - --guanghu-glow: rgba(38, 131, 235, 0.15); - --guanghu-grid: rgba(42, 113, 188, 0.06); - --guanghu-card-highlight: rgba(255, 255, 255, 0.76); + /* Guanghu theme extension: palettes only; layout stays Tolaria-native. */ + --guanghu-aurora: transparent; + --guanghu-glow: transparent; + --guanghu-grid: transparent; } :root.dark, @@ -337,11 +336,68 @@ --link-color: var(--accent-blue); --link-hover: var(--accent-blue-hover); - /* Guanghu visual layer — deep lake, not a flat black canvas. */ - --guanghu-aurora: rgba(76, 174, 255, 0.2); - --guanghu-glow: rgba(20, 118, 214, 0.24); - --guanghu-grid: rgba(126, 202, 255, 0.055); - --guanghu-card-highlight: rgba(28, 54, 80, 0.72); + /* Guanghu theme extension: palettes only; layout stays Tolaria-native. */ + --guanghu-aurora: transparent; + --guanghu-glow: transparent; + --guanghu-grid: transparent; +} + +/* Guanghu companion presets. They change semantic colors only; native + Tolaria layout, editor surfaces and interaction density remain intact. */ +[data-guanghu-theme="lake-light"][data-theme="light"] { + --surface-app: #f6fbff; + --surface-sidebar: #f0f6fa; + --surface-panel: #ffffff; + --surface-card: #ffffff; + --surface-popover: #ffffff; + --surface-editor: #ffffff; + --accent-blue: #2b6ea6; + --accent-blue-hover: #21577f; + --accent-blue-bg: rgba(43, 110, 166, .12); + --accent-blue-light: rgba(43, 110, 166, .08); + --state-selected: #e4f1f8; + --state-selected-strong: #d7eaf4; + --state-active: #e4f1f8; + --border-focus: #2b6ea6; +} + +[data-guanghu-theme="lake-dark"][data-theme="dark"] { + --surface-app: #15191d; + --surface-sidebar: #1b2025; + --surface-panel: #20262c; + --surface-card: #242c33; + --surface-popover: #292f35; + --surface-editor: #15191d; + --text-primary: #e6edf2; + --text-secondary: #aab8c2; + --text-heading: #f1f6f8; + --accent-blue: #72b7d6; + --accent-blue-hover: #98cee3; + --accent-blue-bg: rgba(114, 183, 214, .16); + --accent-blue-light: rgba(114, 183, 214, .1); + --state-hover: #2a333b; + --state-hover-subtle: #252d34; + --state-selected: #29414d; + --state-selected-strong: #315565; + --state-active: #29414d; + --border-focus: #72b7d6; +} + +[data-guanghu-theme="lake-light"][data-theme="dark"] { + --surface-app: #17212a; + --surface-sidebar: #1b2833; + --surface-panel: #21313d; + --surface-card: #263a48; + --surface-popover: #2c4352; + --surface-editor: #17212a; + --accent-blue: #8bc9e5; + --accent-blue-hover: #b0def0; + --accent-blue-bg: rgba(139, 201, 229, .16); + --accent-blue-light: rgba(139, 201, 229, .1); + --state-selected: #294858; + --state-selected-strong: #31586b; + --state-active: #294858; + --border-focus: #8bc9e5; } /* --- Tailwind v4 theme inline: register colors + radii --- */ diff --git a/src/lib/guanghuTheme.test.ts b/src/lib/guanghuTheme.test.ts new file mode 100644 index 0000000..6843fd2 --- /dev/null +++ b/src/lib/guanghuTheme.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' +import { + GUANGHU_THEMES, + normalizeGuanghuTheme, +} from './guanghuTheme' + +describe('guanghuTheme', () => { + it('keeps the companion theme choices explicit and stable', () => { + expect(GUANGHU_THEMES).toEqual(['native', 'lake-light', 'lake-dark']) + }) + + it('falls back to the native Tolaria theme for unknown values', () => { + expect(normalizeGuanghuTheme('old-blue-skin')).toBe('native') + expect(normalizeGuanghuTheme(null)).toBe('native') + }) + + it('accepts every supported companion theme', () => { + expect(normalizeGuanghuTheme('lake-light')).toBe('lake-light') + expect(normalizeGuanghuTheme('lake-dark')).toBe('lake-dark') + }) +}) diff --git a/src/lib/guanghuTheme.ts b/src/lib/guanghuTheme.ts new file mode 100644 index 0000000..711a589 --- /dev/null +++ b/src/lib/guanghuTheme.ts @@ -0,0 +1,32 @@ +export const GUANGHU_THEME_STORAGE_KEY = 'guanghu-theme' + +export const GUANGHU_THEMES = ['native', 'lake-light', 'lake-dark'] as const +export type GuanghuTheme = typeof GUANGHU_THEMES[number] + +export const GUANGHU_THEME_LABELS: Record = { + native: '原生', + 'lake-light': '湖光浅色', + 'lake-dark': '湖心深色', +} + +export function normalizeGuanghuTheme(value: unknown): GuanghuTheme { + return typeof value === 'string' && (GUANGHU_THEMES as readonly string[]).includes(value) + ? value as GuanghuTheme + : 'native' +} + +export function readGuanghuTheme(storage: Pick): GuanghuTheme { + try { + return normalizeGuanghuTheme(storage.getItem(GUANGHU_THEME_STORAGE_KEY)) + } catch { + return 'native' + } +} + +export function writeGuanghuTheme(storage: Pick, theme: GuanghuTheme): void { + try { + storage.setItem(GUANGHU_THEME_STORAGE_KEY, theme) + } catch { + // Storage may be unavailable in restricted browser contexts. + } +}