feat: unify Guanghu channel workspace
This commit is contained in:
parent
9bb8ae1e55
commit
2e6f52a5a8
@ -181,7 +181,7 @@ flowchart TD
|
|||||||
LC --> TM["See You Tomorrow Channel"]
|
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
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
|
|||||||
21
docs/adr/0154-channel-owned-workspace-shell.md
Normal file
21
docs/adr/0154-channel-owned-workspace-shell.md
Normal 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.
|
||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "tolaria",
|
"name": "tolaria",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -5547,7 +5547,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tolaria"
|
name = "tolaria"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tolaria"
|
name = "tolaria"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
description = "Personal knowledge and life management app"
|
description = "Personal knowledge and life management app"
|
||||||
authors = ["Luca Rossi"]
|
authors = ["Luca Rossi"]
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "HoloLake Era",
|
"productName": "HoloLake Era",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"identifier": "com.guanghu.desktop",
|
"identifier": "com.guanghu.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
25
src/App.css
25
src/App.css
@ -20,12 +20,26 @@
|
|||||||
.hololake-bar__library:hover { background: var(--state-hover-subtle); color: var(--text-primary); }
|
.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 { 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-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
|
/* 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. */
|
product mark and title in a separate, stable lane so they never collide. */
|
||||||
body.mac-chrome .hololake-bar { padding-left: 92px; }
|
body.mac-chrome .hololake-bar { padding-left: 92px; }
|
||||||
.hololake-bar__brand { min-width: max-content; white-space: nowrap; }
|
.hololake-bar__brand { min-width: max-content; white-space: nowrap; }
|
||||||
.hololake-bar__context { 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__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__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__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 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-card__status { margin-top: 20px; color: var(--text-muted); font-size: 12px; }
|
||||||
.channel-stage__tools { display: flex; gap: 10px; margin-top: 18px; }
|
.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 { 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 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; }
|
.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 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__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; }
|
.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: 1000px) { .server-grid { grid-template-columns: 1fr; } }
|
||||||
@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: 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; } }
|
@media (max-width: 700px) { body.mac-chrome .hololake-bar { padding-left: 84px; }.hololake-bar__line { display: none; } }
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
|
|||||||
15
src/App.tsx
15
src/App.tsx
@ -1635,12 +1635,19 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
|
|||||||
<header ref={dragRegionRef} className="hololake-bar" data-tauri-drag-region>
|
<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>
|
<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__line" />
|
||||||
<span className="hololake-bar__context">光湖语言人格驱动操作系统</span>
|
<span className="hololake-bar__context">{showHoloLakeHome ? '光湖语言人格驱动操作系统' : '永恒湖心系统 · 知识湖模块'}</span>
|
||||||
<button className="hololake-bar__library" onClick={() => setShowHoloLakeHome(false)}>知识库</button>
|
|
||||||
<button className="hololake-bar__theme" onClick={cycleTheme} title="切换光湖配套主题">主题:{GUANGHU_THEME_LABELS[guanghuTheme]}</button>
|
<button className="hololake-bar__theme" onClick={cycleTheme} title="切换光湖配套主题">主题:{GUANGHU_THEME_LABELS[guanghuTheme]}</button>
|
||||||
</header>
|
</header>
|
||||||
<div className="app">
|
<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 }}>
|
<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} />
|
<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} />
|
<ResizeHandle onResize={layout.handleSidebarResize} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{noteListVisible && (
|
{noteListVisible && !showHoloLakeHome && (
|
||||||
<>
|
<>
|
||||||
<div className={`app__note-list${aiActivity.highlightElement === 'notelist' ? ' ai-highlight' : ''}`} style={{ width: layout.noteListWidth }}>
|
<div className={`app__note-list${aiActivity.highlightElement === 'notelist' ? ' ai-highlight' : ''}`} style={{ width: layout.noteListWidth }}>
|
||||||
{effectiveSelection.kind === 'filter' && effectiveSelection.filter === 'pulse' ? (
|
{effectiveSelection.kind === 'filter' && effectiveSelection.filter === 'pulse' ? (
|
||||||
|
|||||||
@ -17,21 +17,21 @@ describe('HoloLakeHome', () => {
|
|||||||
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} />)
|
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} />)
|
||||||
|
|
||||||
expect(screen.getByRole('heading', { name: '零点原核频道' })).toBeInTheDocument()
|
expect(screen.getByRole('heading', { name: '零点原核频道' })).toBeInTheDocument()
|
||||||
expect(screen.queryByText('爱之核心子系统')).not.toBeInTheDocument()
|
expect(screen.queryByRole('heading', { name: '爱之核心子系统' })).not.toBeInTheDocument()
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /进入第五域/ }))
|
fireEvent.click(screen.getByRole('button', { name: /进入第五域/ }))
|
||||||
|
|
||||||
expect(screen.getByRole('heading', { name: '第五域' })).toBeInTheDocument()
|
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.queryByText('爱之核心子系统')).not.toBeInTheDocument()
|
expect(screen.queryByRole('heading', { name: '爱之核心子系统' })).not.toBeInTheDocument()
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /进入永恒湖心系统/ }))
|
fireEvent.click(screen.getByRole('button', { name: /进入永恒湖心系统/ }))
|
||||||
|
|
||||||
expect(screen.getByRole('heading', { name: '永恒湖心系统' })).toBeInTheDocument()
|
expect(screen.getByRole('heading', { name: '永恒湖心系统' })).toBeInTheDocument()
|
||||||
expect(screen.getByText('光之湖子系统')).toBeInTheDocument()
|
expect(screen.getByRole('heading', { name: '光之湖子系统' })).toBeInTheDocument()
|
||||||
expect(screen.getByText('心跳核心频道')).toBeInTheDocument()
|
expect(screen.getByRole('heading', { name: '心跳核心频道' })).toBeInTheDocument()
|
||||||
expect(screen.getByText('爱之核心子系统')).toBeInTheDocument()
|
expect(screen.getByRole('heading', { name: '爱之核心子系统' })).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps Zhizhi inside Eternal Lake Heart and routes to Tomorrow channel', () => {
|
it('keeps Zhizhi inside Eternal Lake Heart and routes to Tomorrow channel', () => {
|
||||||
@ -69,6 +69,17 @@ describe('HoloLakeHome', () => {
|
|||||||
expect(screen.getByText('霜砚人格系统路径')).toBeInTheDocument()
|
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', () => {
|
it('opens the public domestic development repository', () => {
|
||||||
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} />)
|
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} />)
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ type HoloLakeHomeProps = {
|
|||||||
onEnterKnowledgeBase: () => void
|
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 = {
|
type RouteCardProps = {
|
||||||
action?: string
|
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>
|
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) {
|
export function HoloLakeHome({ locale, onEnterKnowledgeBase }: HoloLakeHomeProps) {
|
||||||
const [route, setRoute] = useState<ChannelRoute>('zero-core')
|
const [route, setRoute] = useState<ChannelRoute>('zero-core')
|
||||||
const [architectureOpen, setArchitectureOpen] = useState(false)
|
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 (
|
return (
|
||||||
<section className="channel-stage">
|
<section className="channel-stage">
|
||||||
<h1>{t('hololake.channel.heartbeatTitle')}</h1>
|
<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 · 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="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="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>
|
||||||
<div className="channel-stage__tools">
|
<div className="channel-stage__tools">
|
||||||
<Button variant="outline" onClick={openArchitecture}>{t('hololake.home.openArchitecture')}</Button>
|
<Button variant="outline" onClick={openArchitecture}>{t('hololake.home.openArchitecture')}</Button>
|
||||||
@ -162,13 +190,28 @@ export function HoloLakeHome({ locale, onEnterKnowledgeBase }: HoloLakeHomeProps
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="hololake-home" aria-label={t('hololake.home.ariaLabel')}>
|
<main className="hololake-home" aria-label={t('hololake.home.ariaLabel')}>
|
||||||
<nav className="channel-breadcrumb" aria-label={t('hololake.channel.breadcrumbLabel')}>
|
<aside className="channel-rail" aria-label="光湖频道导航">
|
||||||
<Button variant="ghost" size="sm" onClick={() => navigate('zero-core')}>{t('hololake.channel.zeroCoreTitle')}</Button>
|
<div className="channel-rail__heading"><span>GUANGHU</span><strong>频道工作区</strong><small>统一控制知识、模块与节点</small></div>
|
||||||
{route !== 'zero-core' && <><span>›</span><Button variant="ghost" size="sm" onClick={() => navigate('fifth-domain')}>{t('hololake.channel.fifthDomainTitle')}</Button></>}
|
<nav>
|
||||||
{!['zero-core', 'fifth-domain'].includes(route) && <><span>›</span><Button variant="ghost" size="sm" onClick={() => navigate('eternal-lake-heart')}>{t('hololake.channel.eternalLakeTitle')}</Button></>}
|
<Button data-active={route === 'zero-core'} variant="ghost" onClick={() => navigate('zero-core')}><i data-color="cyan" />{t('hololake.channel.zeroCoreTitle')}</Button>
|
||||||
</nav>
|
<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>
|
||||||
|
|
||||||
{renderRoute()}
|
<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}>
|
<Dialog open={architectureOpen} onOpenChange={setArchitectureOpen}>
|
||||||
<DialogContent className="guanghu-architecture" aria-label={t('hololake.architecture.title')}>
|
<DialogContent className="guanghu-architecture" aria-label={t('hololake.architecture.title')}>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ function applyGuanghuTheme(theme: GuanghuTheme): void {
|
|||||||
document.documentElement.setAttribute('data-guanghu-theme', theme)
|
document.documentElement.setAttribute('data-guanghu-theme', theme)
|
||||||
if (theme === 'native') return
|
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)
|
applyThemeModeToDocument(document, mode)
|
||||||
writeStoredThemeMode(window.localStorage, mode)
|
writeStoredThemeMode(window.localStorage, mode)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -400,6 +400,70 @@
|
|||||||
--border-focus: #8bc9e5;
|
--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 --- */
|
/* --- Tailwind v4 theme inline: register colors + radii --- */
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
|
|
||||||
describe('guanghuTheme', () => {
|
describe('guanghuTheme', () => {
|
||||||
it('keeps the companion theme choices explicit and stable', () => {
|
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', () => {
|
it('falls back to the native Tolaria theme for unknown values', () => {
|
||||||
@ -17,5 +17,7 @@ describe('guanghuTheme', () => {
|
|||||||
it('accepts every supported companion theme', () => {
|
it('accepts every supported companion theme', () => {
|
||||||
expect(normalizeGuanghuTheme('lake-light')).toBe('lake-light')
|
expect(normalizeGuanghuTheme('lake-light')).toBe('lake-light')
|
||||||
expect(normalizeGuanghuTheme('lake-dark')).toBe('lake-dark')
|
expect(normalizeGuanghuTheme('lake-dark')).toBe('lake-dark')
|
||||||
|
expect(normalizeGuanghuTheme('yaoming-purple')).toBe('yaoming-purple')
|
||||||
|
expect(normalizeGuanghuTheme('dawn-gold')).toBe('dawn-gold')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
export const GUANGHU_THEME_STORAGE_KEY = 'guanghu-theme'
|
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 type GuanghuTheme = typeof GUANGHU_THEMES[number]
|
||||||
|
|
||||||
export const GUANGHU_THEME_LABELS: Record<GuanghuTheme, string> = {
|
export const GUANGHU_THEME_LABELS: Record<GuanghuTheme, string> = {
|
||||||
native: '跟随原生',
|
native: '跟随原生',
|
||||||
'lake-light': '湖光浅色(亮)',
|
'lake-light': '湖光浅色(亮)',
|
||||||
'lake-dark': '湖心深色(暗)',
|
'lake-dark': '湖心深色(暗)',
|
||||||
|
'aurora-dark': '极光深海',
|
||||||
|
'yaoming-purple': '曜冥紫',
|
||||||
|
'dawn-gold': '晨曦金',
|
||||||
|
'tundra-green': '苔原绿',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeGuanghuTheme(value: unknown): GuanghuTheme {
|
export function normalizeGuanghuTheme(value: unknown): GuanghuTheme {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user