feat: unify Guanghu channel workspace

This commit is contained in:
冰朔 2026-07-18 18:25:32 +08:00
parent 9bb8ae1e55
commit 2e6f52a5a8
14 changed files with 199 additions and 28 deletions

View File

@ -181,7 +181,7 @@ flowchart TD
LC --> TM["See You Tomorrow Channel"]
```
`HoloLakeHome` owns only transient navigation state. It does not persist system ownership, server addresses, or credentials. The existing vault remains the filesystem source of truth, and entering the knowledge module returns to the normal Tolaria workspace. A real server connection may later back a system card without changing that card's logical parent.
`HoloLakeHome` owns only transient navigation state. It does not persist system ownership, server addresses, or credentials. The existing vault remains the filesystem source of truth. From 0.1.7 onward, channel mode owns the full application body; entering Knowledge Lake reveals Tolaria's sidebar and note list as navigation inside that module while a Guanghu module dock remains present. Server registration cards expose stable node identities and future monitoring boundaries without presenting placeholder telemetry as live state. See [ADR 0154](adr/0154-channel-owned-workspace-shell.md).
```mermaid
flowchart TD

View File

@ -0,0 +1,21 @@
# ADR 0154: Channel-owned workspace shell
## Status
Accepted for the 0.1.7 internal prototype.
## Context
The first Fifth Domain prototype rendered channel navigation inside Tolaria's editor while leaving Tolaria's sidebar and note list visible. That made the knowledge base and Guanghu channels appear to be two independent products.
## Decision
- Channel mode owns the full application body and hides Tolaria's knowledge navigation.
- The channel rail is the primary navigation for domains, human systems, persona paths, modules, and registered servers.
- Opening Knowledge Lake changes the workspace module while preserving a Guanghu module dock. Tolaria's sidebar and note list are subordinate navigation inside that module.
- Server cards expose stable identity and monitoring interface boundaries before live probes are connected.
- Guanghu companion themes expand through semantic color tokens rather than component-local colors.
## Consequences
The 0.1.7 prototype establishes ownership and extension points, but it does not yet provide live server telemetry or a fully embedded video application. Those modules must attach to the same workspace contract rather than create parallel shells.

View File

@ -2,7 +2,7 @@
"name": "tolaria",
"private": true,
"license": "AGPL-3.0-or-later",
"version": "0.1.6",
"version": "0.1.7",
"type": "module",
"scripts": {
"dev": "vite",

2
src-tauri/Cargo.lock generated
View File

@ -5547,7 +5547,7 @@ dependencies = [
[[package]]
name = "tolaria"
version = "0.1.6"
version = "0.1.7"
dependencies = [
"base64 0.22.1",
"chrono",

View File

@ -1,6 +1,6 @@
[package]
name = "tolaria"
version = "0.1.6"
version = "0.1.7"
description = "Personal knowledge and life management app"
authors = ["Luca Rossi"]
license = "AGPL-3.0-or-later"

View File

@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "HoloLake Era",
"version": "0.1.6",
"version": "0.1.7",
"identifier": "com.guanghu.desktop",
"build": {
"frontendDist": "../dist",

View File

@ -20,12 +20,26 @@
.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); }
.channel-dock { display: flex; width: 58px; flex: 0 0 58px; flex-direction: column; align-items: center; gap: 8px; padding: 10px 7px; border-right: 1px solid var(--border-default); background: color-mix(in srgb, var(--surface-sidebar) 92%, var(--accent-blue)); }
.channel-dock button { display: grid; width: 44px; min-height: 48px; place-items: center; gap: 2px; padding: 5px 2px; border: 1px solid transparent; border-radius: 9px; background: transparent; color: var(--text-secondary); cursor: pointer; }.channel-dock button:hover, .channel-dock button[data-active="true"] { border-color: var(--border-default); background: var(--state-selected); color: var(--text-primary); }.channel-dock button:disabled { opacity: .45; cursor: default; }.channel-dock span { font-family: Georgia, serif; font-size: 16px; }.channel-dock small { font-size: 9px; }
/* Tauri's macOS traffic lights are overlaid on the title bar. Keep the
product mark and title in a separate, stable lane so they never collide. */
body.mac-chrome .hololake-bar { padding-left: 92px; }
.hololake-bar__brand { min-width: max-content; white-space: nowrap; }
.hololake-bar__context { white-space: nowrap; }
.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 { position: relative; z-index: 2; display: flex; width: 100%; min-width: 0; overflow: hidden; color: var(--text-primary); background: var(--surface-app); }
.channel-workspace { min-width: 0; flex: 1; overflow: auto; padding: clamp(28px, 5vw, 64px); }
.channel-rail { display: flex; width: 236px; flex: 0 0 236px; flex-direction: column; gap: 22px; overflow-y: auto; padding: 22px 14px; border-right: 1px solid var(--border-default); background: color-mix(in srgb, var(--surface-sidebar) 94%, var(--accent-blue)); }
.channel-rail__heading { display: grid; gap: 5px; padding: 4px 10px 14px; border-bottom: 1px solid var(--border-default); }
.channel-rail__heading span { color: var(--accent-blue); font: 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .18em; }
.channel-rail__heading strong { font-size: 17px; }.channel-rail__heading small { color: var(--text-muted); font-size: 11px; line-height: 1.5; }
.channel-rail nav { display: grid; gap: 4px; }
.channel-rail nav button { justify-content: flex-start; width: 100%; height: 36px; padding-inline: 10px; color: var(--text-secondary); }
.channel-rail nav button[data-active="true"] { background: var(--state-selected); color: var(--text-primary); }
.channel-rail nav i { width: 8px; height: 8px; border-radius: 999px; background: var(--tag-color); box-shadow: 0 0 10px color-mix(in srgb, var(--tag-color) 55%, transparent); }
.channel-rail nav i[data-color="cyan"] { --tag-color: #4fd1c5; }.channel-rail nav i[data-color="blue"] { --tag-color: #60a5fa; }.channel-rail nav i[data-color="violet"] { --tag-color: #a78bfa; }.channel-rail nav i[data-color="amber"] { --tag-color: #fbbf24; }.channel-rail nav i[data-color="rose"] { --tag-color: #fb7185; }.channel-rail nav i[data-color="pink"] { --tag-color: #f472b6; }.channel-rail nav i[data-color="green"] { --tag-color: #4ade80; }
.channel-rail__modules { display: grid; gap: 8px; margin-top: auto; padding: 14px 10px; border: 1px solid var(--border-default); border-radius: 10px; background: var(--surface-card); }
.channel-rail__modules span { color: var(--text-muted); font-size: 10px; letter-spacing: .12em; }.channel-rail__modules b { font-size: 12px; font-weight: 550; }
.hololake-home__hero { position: relative; max-width: 740px; padding: 24px 0 56px; overflow: hidden; }
.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; }
@ -60,6 +74,10 @@ body.mac-chrome .hololake-bar { padding-left: 92px; }
.channel-card button { align-self: flex-start; margin-top: 20px; padding: 0; color: var(--accent-blue); }
.channel-card__status { margin-top: 20px; color: var(--text-muted); font-size: 12px; }
.channel-stage__tools { display: flex; gap: 10px; margin-top: 18px; }
.server-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.server-card { padding: 20px; border: 1px solid var(--border-default); border-radius: 12px; background: var(--surface-card); }
.server-card__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }.server-card__head code { color: var(--accent-blue); font-size: 11px; }.server-card__head span { padding: 3px 7px; border-radius: 999px; background: var(--surface-panel); color: var(--text-muted); font-size: 10px; }.server-card__head span[data-tone="online"] { background: color-mix(in srgb, var(--accent-green) 18%, transparent); color: var(--accent-green); }
.server-card h2 { margin: 28px 0 6px; font-size: 18px; }.server-card > p { margin: 0 0 22px; color: var(--text-secondary); font-size: 12px; }.server-card dl { display: grid; gap: 8px; margin: 0 0 20px; }.server-card dl div { display: flex; justify-content: space-between; gap: 12px; font-size: 11px; }.server-card dt { color: var(--text-muted); }.server-card dd { margin: 0; color: var(--text-secondary); }.server-card button { width: 100%; font-size: 11px; }
.persona-route-list { display: grid; gap: 12px; max-width: 820px; margin: 0; padding: 0; list-style: none; }
.persona-route-list li { display: grid; grid-template-columns: 140px 1fr auto; align-items: center; gap: 18px; padding: 20px; border: 1px solid var(--border-default); border-radius: 10px; background: var(--surface-card); }
.persona-route-list code { color: var(--accent-blue); font-size: 11px; }
@ -72,8 +90,9 @@ body.mac-chrome .hololake-bar { padding-left: 92px; }
.guanghu-architecture__routes code { align-self: start; color: var(--accent-blue); font-size: 11px; }
.guanghu-architecture__routes strong { font-size: 14px; }.guanghu-architecture__routes p { margin: 5px 0 0; color: var(--text-secondary); font-size: 12px; line-height: 1.65; }
.guanghu-architecture__route { overflow-x: auto; margin: 2px 0 0; padding: 12px; border-radius: 6px; color: var(--text-secondary); background: var(--surface-app); font: 11px/1.6 monospace; white-space: nowrap; }
@media (max-width: 900px) { .channel-grid--three { grid-template-columns: 1fr; }.channel-card { min-height: 190px; } }
@media (max-width: 780px) { .hololake-home__signal-grid, .hololake-home__module-grid, .channel-grid { grid-template-columns: 1fr; }.hololake-bar__context { display: none; }.hololake-home { padding: 28px; }.channel-breadcrumb { overflow-x: auto; }.channel-stage h1 { font-size: 42px; } }
@media (max-width: 1000px) { .server-grid { grid-template-columns: 1fr; } }
@media (max-width: 900px) { .channel-grid--three { grid-template-columns: 1fr; }.channel-card { min-height: 190px; }.channel-rail { width: 190px; flex-basis: 190px; } }
@media (max-width: 780px) { .hololake-home__signal-grid, .hololake-home__module-grid, .channel-grid { grid-template-columns: 1fr; }.hololake-bar__context { display: none; }.channel-workspace { padding: 28px; }.channel-rail { display: none; }.channel-breadcrumb { overflow-x: auto; }.channel-stage h1 { font-size: 42px; } }
@media (max-width: 700px) { body.mac-chrome .hololake-bar { padding-left: 84px; }.hololake-bar__line { display: none; } }
.app {

View File

@ -1635,12 +1635,19 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
<header ref={dragRegionRef} className="hololake-bar" data-tauri-drag-region>
<button className="hololake-bar__brand" onClick={() => setShowHoloLakeHome(true)}><img className="hololake-bar__mark" src={guanghuLogoLight} alt="光湖" /><span>HoloLake <b>Era</b></span></button>
<span className="hololake-bar__line" />
<span className="hololake-bar__context"></span>
<button className="hololake-bar__library" onClick={() => setShowHoloLakeHome(false)}></button>
<span className="hololake-bar__context">{showHoloLakeHome ? '光湖语言人格驱动操作系统' : '永恒湖心系统 · 知识湖模块'}</span>
<button className="hololake-bar__theme" onClick={cycleTheme} title="切换光湖配套主题">{GUANGHU_THEME_LABELS[guanghuTheme]}</button>
</header>
<div className="app">
{sidebarVisible && (
{!showHoloLakeHome && noteWindowParams === null && (
<nav className="channel-dock" aria-label="频道模块控制栏">
<button onClick={() => setShowHoloLakeHome(true)} title="返回心跳核心频道"><span></span><small></small></button>
<button data-active="true" title="当前模块:知识湖"><span></span><small></small></button>
<button disabled title="短剧视频 AI 接口已预留"><span></span><small></small></button>
<button disabled title="服务器状态接口已预留"><span></span><small></small></button>
</nav>
)}
{sidebarVisible && !showHoloLakeHome && (
<>
<div className="app__sidebar" style={{ width: layout.sidebarWidth }}>
<Sidebar entries={visibleEntries} folders={vault.folders} views={vault.views} selection={effectiveSelection} onSelect={handleSetSelection} onSelectNote={notes.handleSelectNote} onSelectFavorite={handleOpenFavorite} onReorderFavorites={entryActions.handleReorderFavorites} onCreateType={notes.handleCreateNoteImmediate} onCreateNewType={dialogs.openCreateType} onCustomizeType={entryActions.handleCustomizeType} onUpdateTypeTemplate={entryActions.handleUpdateTypeTemplate} onReorderSections={entryActions.handleReorderSections} onRenameSection={entryActions.handleRenameSection} onDeleteType={handleDeleteType} onToggleTypeVisibility={entryActions.handleToggleTypeVisibility} onCreateFolder={handleCreateFolder} onRenameFolder={folderActions.renameFolder} onDeleteFolder={folderActions.requestDeleteFolder} folderFileActions={fileActions.folderActions} renamingFolderPath={folderActions.renamingFolderPath} onStartRenameFolder={folderActions.startFolderRename} onCancelRenameFolder={folderActions.cancelFolderRename} onCreateView={dialogs.openCreateView} onEditView={handleEditView} onDeleteView={handleDeleteView} onUpdateViewDefinition={handleSidebarUpdateViewDefinition} onReorderViews={canReorderSavedViews ? viewOrdering.onReorderViews : undefined} showInbox={explicitOrganizationEnabled} inboxCount={inboxCount} allNotesFileVisibility={allNotesFileVisibility} pluralizeTypeLabels={settings.sidebar_type_pluralization_enabled ?? true} onCollapse={handleCollapseSidebar} onGoBack={handleGoBack} onGoForward={handleGoForward} canGoBack={canGoBack} canGoForward={canGoForward} locale={appLocale} loading={isVaultContentLoading} vaultRootPath={resolvedPath} workspaceOrder={vaultWorkspaceOrder} />
@ -1648,7 +1655,7 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
<ResizeHandle onResize={layout.handleSidebarResize} />
</>
)}
{noteListVisible && (
{noteListVisible && !showHoloLakeHome && (
<>
<div className={`app__note-list${aiActivity.highlightElement === 'notelist' ? ' ai-highlight' : ''}`} style={{ width: layout.noteListWidth }}>
{effectiveSelection.kind === 'filter' && effectiveSelection.filter === 'pulse' ? (

View File

@ -17,21 +17,21 @@ describe('HoloLakeHome', () => {
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} />)
expect(screen.getByRole('heading', { name: '零点原核频道' })).toBeInTheDocument()
expect(screen.queryByText('爱之核心子系统')).not.toBeInTheDocument()
expect(screen.queryByRole('heading', { name: '爱之核心子系统' })).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /进入第五域/ }))
expect(screen.getByRole('heading', { name: '第五域' })).toBeInTheDocument()
expect(screen.getByText('胖头鱼语言子系统')).toBeInTheDocument()
expect(screen.getByText('永恒湖心系统')).toBeInTheDocument()
expect(screen.queryByText('爱之核心子系统')).not.toBeInTheDocument()
expect(screen.getByRole('heading', { name: '永恒湖心系统' })).toBeInTheDocument()
expect(screen.queryByRole('heading', { name: '爱之核心子系统' })).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /进入永恒湖心系统/ }))
expect(screen.getByRole('heading', { name: '永恒湖心系统' })).toBeInTheDocument()
expect(screen.getByText('光之湖子系统')).toBeInTheDocument()
expect(screen.getByText('心跳核心频道')).toBeInTheDocument()
expect(screen.getByText('爱之核心子系统')).toBeInTheDocument()
expect(screen.getByRole('heading', { name: '光之湖子系统' })).toBeInTheDocument()
expect(screen.getByRole('heading', { name: '心跳核心频道' })).toBeInTheDocument()
expect(screen.getByRole('heading', { name: '爱之核心子系统' })).toBeInTheDocument()
})
it('keeps Zhizhi inside Eternal Lake Heart and routes to Tomorrow channel', () => {
@ -69,6 +69,17 @@ describe('HoloLakeHome', () => {
expect(screen.getByText('霜砚人格系统路径')).toBeInTheDocument()
})
it('keeps a server registry interface inside the Heartbeat Core workspace', () => {
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} />)
fireEvent.click(screen.getByRole('button', { name: '服务器与节点' }))
expect(screen.getByRole('heading', { name: '服务器与灯塔节点' })).toBeInTheDocument()
expect(screen.getByText('BS-SG-001')).toBeInTheDocument()
expect(screen.getByText('JD-FD-PRIMARY')).toBeInTheDocument()
expect(screen.getAllByRole('button', { name: /接口已预留/ })).toHaveLength(3)
})
it('opens the public domestic development repository', () => {
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} />)

View File

@ -20,7 +20,7 @@ type HoloLakeHomeProps = {
onEnterKnowledgeBase: () => void
}
type ChannelRoute = 'zero-core' | 'fifth-domain' | 'eternal-lake-heart' | 'light-lake' | 'heartbeat-core' | 'love-core'
type ChannelRoute = 'zero-core' | 'fifth-domain' | 'eternal-lake-heart' | 'light-lake' | 'heartbeat-core' | 'love-core' | 'servers'
type RouteCardProps = {
action?: string
@ -56,6 +56,12 @@ function PersonaRoute({ id, title }: { id: string; title: string }) {
return <li><code>{id}</code><strong>{title}</strong><span></span></li>
}
const registeredServers = [
{ id: 'BS-SG-001', label: '新加坡大脑服务器', owner: '冰朔 · 永恒湖心', status: '已接入', tone: 'online' },
{ id: 'BS-GZ-001', label: '广州个人服务器', owner: '冰朔 · 心跳核心', status: '接口预留', tone: 'pending' },
{ id: 'JD-FD-PRIMARY', label: '第五域企业主节点', owner: '第五域 · 零点原核', status: '接口预留', tone: 'pending' },
] as const
export function HoloLakeHome({ locale, onEnterKnowledgeBase }: HoloLakeHomeProps) {
const [route, setRoute] = useState<ChannelRoute>('zero-core')
const [architectureOpen, setArchitectureOpen] = useState(false)
@ -143,6 +149,27 @@ export function HoloLakeHome({ locale, onEnterKnowledgeBase }: HoloLakeHomeProps
)
}
if (route === 'servers') {
return (
<section className="channel-stage">
<p className="channel-stage__eyebrow">SERVER REGISTRY · READ ONLY</p>
<h1></h1>
<p className="channel-stage__lead"></p>
<div className="server-grid">
{registeredServers.map(server => (
<article className="server-card" key={server.id}>
<div className="server-card__head"><code>{server.id}</code><span data-tone={server.tone}>{server.status}</span></div>
<h2>{server.label}</h2>
<p>{server.owner}</p>
<dl><div><dt></dt><dd></dd></div><div><dt></dt><dd></dd></div></dl>
<Button variant="outline" disabled> · </Button>
</article>
))}
</div>
</section>
)
}
return (
<section className="channel-stage">
<h1>{t('hololake.channel.heartbeatTitle')}</h1>
@ -151,6 +178,7 @@ export function HoloLakeHome({ locale, onEnterKnowledgeBase }: HoloLakeHomeProps
<RouteCard eyebrow="MODULE · KNOWLEDGE" title={t('hololake.home.moduleKnowledgeTitle')} description={t('hololake.home.moduleKnowledgeDescription')} action={t('hololake.channel.openKnowledge')} onOpen={openKnowledgeBase} />
<RouteCard eyebrow="MODULE · VIDEO AI" title={t('hololake.channel.videoAiTitle')} description={t('hololake.channel.videoAiDescription')} status={t('hololake.channel.prototypeMounted')} />
<RouteCard eyebrow="SYSTEM · ORIGIN HEARTBEAT" title={t('hololake.channel.bottleBabyTitle')} description={t('hololake.channel.bottleBabyDescription')} status={t('hololake.channel.yaomingOrigin')} />
<RouteCard eyebrow="INFRASTRUCTURE · SERVERS" title="服务器与灯塔节点" description="查看频道登记的服务器编号、归属与真实监控接口状态。" action="打开服务器登记" onOpen={() => navigate('servers')} />
</div>
<div className="channel-stage__tools">
<Button variant="outline" onClick={openArchitecture}>{t('hololake.home.openArchitecture')}</Button>
@ -162,13 +190,28 @@ export function HoloLakeHome({ locale, onEnterKnowledgeBase }: HoloLakeHomeProps
return (
<main className="hololake-home" aria-label={t('hololake.home.ariaLabel')}>
<aside className="channel-rail" aria-label="光湖频道导航">
<div className="channel-rail__heading"><span>GUANGHU</span><strong></strong><small></small></div>
<nav>
<Button data-active={route === 'zero-core'} variant="ghost" onClick={() => navigate('zero-core')}><i data-color="cyan" />{t('hololake.channel.zeroCoreTitle')}</Button>
<Button data-active={route === 'fifth-domain'} variant="ghost" onClick={() => navigate('fifth-domain')}><i data-color="blue" />{t('hololake.channel.fifthDomainTitle')}</Button>
<Button data-active={['eternal-lake-heart', 'light-lake', 'heartbeat-core', 'love-core', 'servers'].includes(route)} variant="ghost" onClick={() => navigate('eternal-lake-heart')}><i data-color="violet" />{t('hololake.channel.eternalLakeTitle')}</Button>
<Button data-active={route === 'light-lake'} variant="ghost" onClick={() => navigate('light-lake')}><i data-color="amber" />{t('hololake.channel.lightLakeTitle')}</Button>
<Button data-active={route === 'heartbeat-core'} variant="ghost" onClick={() => navigate('heartbeat-core')}><i data-color="rose" />{t('hololake.channel.heartbeatTitle')}</Button>
<Button data-active={route === 'love-core'} variant="ghost" onClick={() => navigate('love-core')}><i data-color="pink" />{t('hololake.channel.loveCoreTitle')}</Button>
<Button data-active={route === 'servers'} variant="ghost" onClick={() => navigate('servers')}><i data-color="green" /></Button>
</nav>
<div className="channel-rail__modules"><span></span><b></b><b> AI</b><b></b></div>
</aside>
<div className="channel-workspace">
<nav className="channel-breadcrumb" aria-label={t('hololake.channel.breadcrumbLabel')}>
<Button variant="ghost" size="sm" onClick={() => navigate('zero-core')}>{t('hololake.channel.zeroCoreTitle')}</Button>
{route !== 'zero-core' && <><span></span><Button variant="ghost" size="sm" onClick={() => navigate('fifth-domain')}>{t('hololake.channel.fifthDomainTitle')}</Button></>}
{!['zero-core', 'fifth-domain'].includes(route) && <><span></span><Button variant="ghost" size="sm" onClick={() => navigate('eternal-lake-heart')}>{t('hololake.channel.eternalLakeTitle')}</Button></>}
</nav>
{renderRoute()}
</div>
<Dialog open={architectureOpen} onOpenChange={setArchitectureOpen}>
<DialogContent className="guanghu-architecture" aria-label={t('hololake.architecture.title')}>

View File

@ -12,7 +12,7 @@ function applyGuanghuTheme(theme: GuanghuTheme): void {
document.documentElement.setAttribute('data-guanghu-theme', theme)
if (theme === 'native') return
const mode = theme === 'lake-light' ? 'light' : 'dark'
const mode = ['lake-light', 'dawn-gold', 'tundra-green'].includes(theme) ? 'light' : 'dark'
applyThemeModeToDocument(document, mode)
writeStoredThemeMode(window.localStorage, mode)
}

View File

@ -400,6 +400,70 @@
--border-focus: #8bc9e5;
}
[data-guanghu-theme="aurora-dark"][data-theme="dark"] {
--surface-app: #07151b;
--surface-sidebar: #0b2028;
--surface-panel: #102b34;
--surface-card: #143640;
--surface-popover: #183f49;
--surface-editor: #07151b;
--text-primary: #e6fbfb;
--text-secondary: #9fc7c9;
--accent-blue: #4ee2d0;
--accent-blue-hover: #80f1e4;
--accent-blue-bg: rgba(78, 226, 208, .15);
--state-selected: #16484d;
--state-selected-strong: #1d5a5f;
--border-focus: #4ee2d0;
}
[data-guanghu-theme="yaoming-purple"][data-theme="dark"] {
--surface-app: #15101d;
--surface-sidebar: #1d1629;
--surface-panel: #251c33;
--surface-card: #2d2340;
--surface-popover: #362a4b;
--surface-editor: #15101d;
--text-primary: #f2eaff;
--text-secondary: #bdacd3;
--accent-blue: #b88cff;
--accent-blue-hover: #d0b2ff;
--accent-blue-bg: rgba(184, 140, 255, .16);
--state-selected: #44315f;
--state-selected-strong: #563c78;
--border-focus: #b88cff;
}
[data-guanghu-theme="dawn-gold"][data-theme="light"] {
--surface-app: #fffaf0;
--surface-sidebar: #f8f0df;
--surface-panel: #fffdf8;
--surface-card: #fff9eb;
--surface-popover: #fffdf8;
--surface-editor: #fffdf8;
--accent-blue: #a66a16;
--accent-blue-hover: #7f4f0d;
--accent-blue-bg: rgba(196, 132, 37, .14);
--state-selected: #f5e4bd;
--state-selected-strong: #ecd39c;
--border-focus: #b6781d;
}
[data-guanghu-theme="tundra-green"][data-theme="light"] {
--surface-app: #f4f8f1;
--surface-sidebar: #eaf1e5;
--surface-panel: #fbfdf9;
--surface-card: #f7fbf4;
--surface-popover: #fbfdf9;
--surface-editor: #fbfdf9;
--accent-blue: #4f7b48;
--accent-blue-hover: #385f33;
--accent-blue-bg: rgba(79, 123, 72, .14);
--state-selected: #dcebd6;
--state-selected-strong: #cbe0c3;
--border-focus: #4f7b48;
}
/* --- Tailwind v4 theme inline: register colors + radii --- */
@theme inline {
--color-background: var(--background);

View File

@ -6,7 +6,7 @@ import {
describe('guanghuTheme', () => {
it('keeps the companion theme choices explicit and stable', () => {
expect(GUANGHU_THEMES).toEqual(['native', 'lake-light', 'lake-dark'])
expect(GUANGHU_THEMES).toEqual(['native', 'lake-light', 'lake-dark', 'aurora-dark', 'yaoming-purple', 'dawn-gold', 'tundra-green'])
})
it('falls back to the native Tolaria theme for unknown values', () => {
@ -17,5 +17,7 @@ describe('guanghuTheme', () => {
it('accepts every supported companion theme', () => {
expect(normalizeGuanghuTheme('lake-light')).toBe('lake-light')
expect(normalizeGuanghuTheme('lake-dark')).toBe('lake-dark')
expect(normalizeGuanghuTheme('yaoming-purple')).toBe('yaoming-purple')
expect(normalizeGuanghuTheme('dawn-gold')).toBe('dawn-gold')
})
})

View File

@ -1,12 +1,16 @@
export const GUANGHU_THEME_STORAGE_KEY = 'guanghu-theme'
export const GUANGHU_THEMES = ['native', 'lake-light', 'lake-dark'] as const
export const GUANGHU_THEMES = ['native', 'lake-light', 'lake-dark', 'aurora-dark', 'yaoming-purple', 'dawn-gold', 'tundra-green'] as const
export type GuanghuTheme = typeof GUANGHU_THEMES[number]
export const GUANGHU_THEME_LABELS: Record<GuanghuTheme, string> = {
native: '跟随原生',
'lake-light': '湖光浅色(亮)',
'lake-dark': '湖心深色(暗)',
'aurora-dark': '极光深海',
'yaoming-purple': '曜冥紫',
'dawn-gold': '晨曦金',
'tundra-green': '苔原绿',
}
export function normalizeGuanghuTheme(value: unknown): GuanghuTheme {