feat: admit complete Qoder surfaces into numbered HoloLake

This commit is contained in:
冰朔 2026-08-19 07:07:25 +08:00
commit fb8607f248
14 changed files with 1016 additions and 31 deletions

View file

@ -0,0 +1,17 @@
{
"schema": "hololake.production-install-acceptance/v1",
"record_id": "HLP-PRODUCTION-INSTALL-20260819-001",
"state": "PASS",
"version": "0.5.0",
"installed_path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake.app",
"visible_hololake_app_count": 1,
"previous_app_recovery_path": "/Users/bingshuolingdianyuanhe/.Trash/HoloLake-pre-qoder-numbered-ui-20260819-0659.app",
"binary_sha256": "b0422bb18df521ace54ff1a52cd47121d92833dd78f165845deba7806686c7ac",
"cdhash": "42640f7ce56e96403609899095c77ea6171df07d",
"team_identifier": "825A9L3G7Q",
"developer_id_signature": "PASS",
"launch": "PASS",
"real_data_rehydration": "PASS",
"notarization": "NOT_CLAIMED_THIS_LOCAL_BUILD",
"updater_artifact_signing": "NOT_COMPLETED_MISSING_PRIVATE_UPDATER_KEY_IN_CURRENT_PROCESS"
}

View file

@ -0,0 +1,28 @@
{
"schema": "hololake.qoder-surface-live-acceptance/v1",
"record_id": "HLP-QODER-SURFACE-ACCEPTANCE-20260819-001",
"module_number": "HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001",
"state": "PASS",
"tested_app": "src-tauri/target/debug/bundle/macos/HoloLake.app",
"developer_id_signature": "PASS",
"team_identifier": "825A9L3G7Q",
"binary_sha256": "fb1d0497f295e1228c9b2ac86e33082332f226aae6e32b9050cf6c5de30bd512",
"cdhash": "90e365b23961a6b842590869df655ec35a55b66e",
"checks": {
"traditional_locked_layout": "PASS",
"traditional_real_data_hydration": "PASS",
"traditional_light_finish_readability": "PASS_SNOW",
"language_world_five_domains_no_overlap": "PASS_1229x768",
"language_world_real_weather": "PASS_CLOUD",
"theme_persists_into_channel": "PASS",
"theme_persists_into_web_novel": "PASS",
"theme_persists_into_education": "PASS",
"new_chapter_modal_opens": "PASS",
"created_chapter_survives_restart": "PASS_1_CHAPTER",
"work_summary_refresh_after_create": "PASS_1_CHAPTER_ON_WORK_CARD",
"idle_clock_stops": "PASS",
"idle_screenshot_psnr_db": 59.502493
},
"reference_comparison": "audit/qoder-traditional-reference-vs-hololake-0.5.0.jpg",
"public_notarization": "NOT_CLAIMED_BY_DEBUG_ACCEPTANCE"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -25,8 +25,13 @@
"unavailable_behavior": "BEIJING_TIME_ONLY_NO_FAKE_WEATHER" "unavailable_behavior": "BEIJING_TIME_ONLY_NO_FAKE_WEATHER"
}, },
"visual_lock": { "visual_lock": {
"layout": "EXISTING_OFFICIAL_FIVE_LAKE_SHELL_UNCHANGED", "layout": "QODER_LOCKED_STARLAKE_AND_TRADITIONAL_SURFACES_RESPONSIVELY_ADAPTED",
"themes": ["夜湖星光", "晨湖曦光", "星云紫夜", "烛畔暖湖", "清浅澄湖"], "language_world_themes": ["夜湖星光", "晨湖曦光", "星云紫夜", "烛畔暖湖", "清浅澄湖"],
"traditional_finishes": ["曜夜", "星辉", "深海", "翡翠", "朱砂", "香槟", "瓷光", "雪霁"],
"all_downstream_pages_inherit_active_surface_tokens": true,
"ambient_motion_requires_recent_human_pointer_or_keyboard_activity": true,
"idle_visual_state": "STATIC",
"responsive_layout": "RESIZE_OBSERVER_PLUS_SEMANTIC_TOKEN_BALANCE",
"climate_changes_routing_permission_or_fact": false, "climate_changes_routing_permission_or_fact": false,
"daily_sampling_domain_changes_open_domain": false, "daily_sampling_domain_changes_open_domain": false,
"real_city_exposed_in_ui": false, "real_city_exposed_in_ui": false,
@ -36,18 +41,26 @@
}, },
"donor_disposition": { "donor_disposition": {
"world_climate": "EXTRACTED_REWRITTEN_AND_NUMBERED", "world_climate": "EXTRACTED_REWRITTEN_AND_NUMBERED",
"starlake_surface": "BEHAVIOR_MERGED_INTO_EXISTING_OFFICIAL_SHELL_NOT_COPIED", "starlake_surface": "QODER_LOCKED_SOURCE_ADMITTED_WITH_RESPONSIVE_AND_IDLE_MOTION_ADAPTER",
"traditional_surface": "REJECTED_DUPLICATE_SHELL_EIGHT_THEMES_AND_SIMULATED_STATUS", "traditional_surface": "QODER_LOCKED_SOURCE_ADMITTED_WITH_REAL_NUMBERED_PROJECTIONS_ONLY",
"traditional_surface_css": "REJECTED_CONFLICTS_WITH_LOCKED_FIVE_LAKE_TOKENS", "traditional_surface_css": "QODER_LOCKED_EIGHT_FINISH_TOKEN_SYSTEM_ADMITTED_AND_SCOPED_ACROSS_ALL_PAGES",
"fake_broadcasts_and_fake_metrics": "REJECTED" "fake_broadcasts_and_fake_metrics": "REJECTED"
}, },
"qoder_source_provenance": {
"root": "/Volumes/JZAO/铸渊-ICE-GL-ZY001/OUT-输出/UI-预览-20260818/deploy-staging",
"main_tsx_sha256": "7dab8596ec0bd2609b1b23b175f1f14f15f2aa9af847eb605692c993d2e876e8",
"traditional_surface_tsx_sha256": "c713dc27681e07c510a2125816dcefb3255252fc0b570e350af7a98f57fc220d",
"traditional_surface_css_sha256": "3e6c446fb84a16ed23b0fd51466ad9723d0c0670ee6ddf6653f61237bd00baa6",
"starlake_surface_css_sha256": "471e3edef02236e6463144ac75b526f0f98782b5476b9db786e7cd0614a83227",
"acceptance_html_sha256": "7a41e426f1576fe0e25182e0c2efee65302b064d86363c890b0f235386a9c070"
},
"current_acceptance": { "current_acceptance": {
"state": "PASS_SIGNED_NUMBERED_REAL_UI_AND_RESTART", "state": "PASS_NUMBERED_QODER_SURFACES_REAL_DATA_RESPONSIVE_IDLE_AND_RESTART",
"module_package_sha256": "b6ba13b9f70bd617b2a303ec2eb0ea11d5354d10118c7f3e5179ae8100cd2602", "module_package_sha256": "b6ba13b9f70bd617b2a303ec2eb0ea11d5354d10118c7f3e5179ae8100cd2602",
"module_package_signature": "PASS_EMBEDDED_PRODUCT_TRUST", "module_package_signature": "PASS_EMBEDDED_PRODUCT_TRUST",
"numbered_route": "HLP-NIPC-MOD-0031/HLP-NIPC-OP-0147/HLP-NIPC-TGT-0031", "numbered_route": "HLP-NIPC-MOD-0031/HLP-NIPC-OP-0147/HLP-NIPC-TGT-0031",
"signed_app_binary_sha256": "5612519f56ea83cfd167cd099cc2cb27de9da057b262716bb71ff38714c76c8f", "signed_app_binary_sha256": "b0422bb18df521ace54ff1a52cd47121d92833dd78f165845deba7806686c7ac",
"signed_app_cdhash": "5cc0836a1f12f42f9f48196ce691d6f4cde2f801", "signed_app_cdhash": "42640f7ce56e96403609899095c77ea6171df07d",
"apple_team_identifier": "825A9L3G7Q", "apple_team_identifier": "825A9L3G7Q",
"module_receipts": { "module_receipts": {
"install": "2bb903cd369c8f57432c59367fd5494ee89a75214d5129511190e2337cecdbc4", "install": "2bb903cd369c8f57432c59367fd5494ee89a75214d5129511190e2337cecdbc4",
@ -55,8 +68,14 @@
"self_test_pass": "404a0eeddb866bc3050b407c586c036f3707db577e1edf9f1cf3129c1cc9cc1d" "self_test_pass": "404a0eeddb866bc3050b407c586c036f3707db577e1edf9f1cf3129c1cc9cc1d"
}, },
"real_weather_readback": "PASS_VERIFIED_LIVE_NIGHT_CLOUD_WITHOUT_CITY_OR_COORDINATES", "real_weather_readback": "PASS_VERIFIED_LIVE_NIGHT_CLOUD_WITHOUT_CITY_OR_COORDINATES",
"restart_restore": "PASS_ACTIVE_MODULE_AND_REAL_CLIMATE_RESTORED", "restart_restore": "PASS_ACTIVE_MODULE_REAL_CLIMATE_THEME_AND_WEB_NOVEL_CHAPTER_RESTORED",
"official_shell_preserved": "PASS_FIVE_LAKE_LAYOUT_AND_FIVE_THEMES_UNCHANGED", "official_shell_preserved": "QODER_LOCKED_SURFACES_ADMITTED_WITH_NUMBERED_FUNCTION_ROUTING_UNCHANGED",
"visual_comparison": "audit/qoder-traditional-reference-vs-hololake-0.5.0.jpg",
"idle_static_psnr_db": 59.502493,
"idle_static_clock_unchanged": true,
"web_novel_new_chapter_modal": "PASS",
"web_novel_created_chapter_restart_persistence": "PASS_1_CHAPTER_VISIBLE_AFTER_RESTART",
"traditional_theme_downstream_pages": ["CHANNEL", "WEB_NOVEL", "EDUCATION"],
"notarization": "FINAL_RELEASE_CANDIDATE_PENDING" "notarization": "FINAL_RELEASE_CANDIDATE_PENDING"
} }
} }

View file

@ -169,16 +169,17 @@
"state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED",
"paths": [ "paths": [
"contracts/dynamic-language-world-visual-system.json", "contracts/dynamic-language-world-visual-system.json",
"src/StarlakeSurface.tsx", "src/modules/qoder-surface/StarlakeSurface.tsx",
"src/starlake-surface.css", "src/modules/qoder-surface/starlake-surface.css",
"src/traditional-surface.tsx", "src/modules/qoder-surface/TraditionalSurface.tsx",
"src/traditional-surface.css", "src/modules/qoder-surface/traditional-surface.css",
"src/modules/qoder-surface/visual-balance.ts",
"src-tauri/src/world_climate.rs" "src-tauri/src/world_climate.rs"
], ],
"runtime_module_number": "HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001", "runtime_module_number": "HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001",
"numbered_ipc_module": "HLP-NIPC-MOD-0031", "numbered_ipc_module": "HLP-NIPC-MOD-0031",
"acceptance_evidence": "contracts/dynamic-language-world-visual-system.json#current_acceptance", "acceptance_evidence": "contracts/dynamic-language-world-visual-system.json#current_acceptance",
"boundary": "REALITY_TIME_AND_PUBLIC_WEATHER_VISUAL_PROJECTION_ONLY; OFFICIAL_FIVE_LAKE_SHELL_AND_ROUTING_UNCHANGED; NO_FAKE_WEATHER_OR_SIMULATED_STATUS" "boundary": "QODER_LOCKED_VISUAL_SOURCE_PLUS_REALITY_TIME_AND_PUBLIC_WEATHER_PROJECTION; ALL_FUNCTION_ROUTES_REMAIN_NUMBERED; IDLE_STATIC; NO_FAKE_WEATHER_BROADCAST_OR_METRICS"
} }
], ],
"admission_gate": [ "admission_gate": [

View file

@ -9,6 +9,11 @@ const modulePackage = JSON.parse(read('fixtures/module-packages/HLP-MOD-OFFICIAL
const rust = read('src-tauri/src/world_climate.rs') const rust = read('src-tauri/src/world_climate.rs')
const frontend = read('src/main.tsx') const frontend = read('src/main.tsx')
const styles = read('src/styles.css') const styles = read('src/styles.css')
const starlake = read('src/modules/qoder-surface/StarlakeSurface.tsx')
const starlakeStyles = read('src/modules/qoder-surface/starlake-surface.css')
const traditional = read('src/modules/qoder-surface/TraditionalSurface.tsx')
const traditionalStyles = read('src/modules/qoder-surface/traditional-surface.css')
const visualBalance = read('src/modules/qoder-surface/visual-balance.ts')
test('dynamic world surface is signed, opt-in and cannot mutate world facts', () => { test('dynamic world surface is signed, opt-in and cannot mutate world facts', () => {
assert.equal(modulePackage.manifest.moduleNumber, 'HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001') assert.equal(modulePackage.manifest.moduleNumber, 'HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001')
@ -31,13 +36,30 @@ test('climate query crosses the unique numbered route and fails closed before ac
assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/) assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/)
}) })
test('official shell and five lake themes remain the only admitted layout', () => { test('Qoder locked two-track surface is admitted without simulated product state', () => {
assert.equal(contract.visual_lock.layout, 'EXISTING_OFFICIAL_FIVE_LAKE_SHELL_UNCHANGED') assert.equal(contract.visual_lock.layout, 'QODER_LOCKED_STARLAKE_AND_TRADITIONAL_SURFACES_RESPONSIVELY_ADAPTED')
assert.equal(contract.visual_lock.themes.length, 5) assert.equal(contract.visual_lock.language_world_themes.length, 5)
assert.equal(contract.donor_disposition.traditional_surface, 'REJECTED_DUPLICATE_SHELL_EIGHT_THEMES_AND_SIMULATED_STATUS') assert.equal(contract.visual_lock.traditional_finishes.length, 8)
assert.equal(contract.visual_lock.all_downstream_pages_inherit_active_surface_tokens, true)
assert.match(frontend, /夜湖星光/) assert.match(frontend, /夜湖星光/)
assert.match(frontend, /天气源暂不可用;湖面只使用北京时间,不生成假天气。/) assert.match(frontend, /天气源暂不可用;湖面只使用北京时间,不生成假天气。/)
assert.match(styles, /\.world-climate-veil/) assert.match(styles, /\.world-climate-veil/)
assert.doesNotMatch(frontend, /语料库新增 217 条/) assert.match(starlake, /ResizeObserver/)
assert.doesNotMatch(frontend, /频道活动 · 七日/) assert.match(starlakeStyles, /\.starlake-scene\.awake/)
assert.match(traditional, /broadcasts\.map/)
assert.match(traditionalStyles, /\.official-world\.surface-traditional/)
assert.match(visualBalance, /contrastRatio/)
assert.doesNotMatch(traditional, /语料库新增 217 条/)
assert.doesNotMatch(traditional, /const BROADCAST_POOL/)
})
test('ambient motion sleeps and the Qoder source hashes remain pinned', () => {
assert.equal(contract.visual_lock.ambient_motion_requires_recent_human_pointer_or_keyboard_activity, true)
assert.equal(contract.visual_lock.idle_visual_state, 'STATIC')
assert.match(frontend, /setTimeout\(\(\) => \{[\s\S]*setMotionAwake\(false\)/)
assert.match(starlake, /if \(!awake\) return/)
assert.match(traditional, /if \(!awake\) return/)
assert.match(starlakeStyles, /animation-play-state:paused/)
assert.match(traditionalStyles, /animation-play-state:paused/)
assert.equal(contract.qoder_source_provenance.main_tsx_sha256, '7dab8596ec0bd2609b1b23b175f1f14f15f2aa9af847eb605692c993d2e876e8')
}) })

View file

@ -4,6 +4,9 @@ import { numberedInvoke as invoke } from './modules/numbered-ipc'
import { splitFrontmatter, documentOutline, jumpToHeading, MarkdownDocument, markdownHtml } from './modules/knowledge-render' import { splitFrontmatter, documentOutline, jumpToHeading, MarkdownDocument, markdownHtml } from './modules/knowledge-render'
import { NativeCompositionStudio, type CompositionDimension, type CompositionMeasure, type NativeCompositionProjection, type ProjectionView } from './modules/native-composition' import { NativeCompositionStudio, type CompositionDimension, type CompositionMeasure, type NativeCompositionProjection, type ProjectionView } from './modules/native-composition'
import type { ChannelWorkbenchSnapshot } from './modules/channel-workbench' import type { ChannelWorkbenchSnapshot } from './modules/channel-workbench'
import { StarlakeSurface, type DomainId } from './modules/qoder-surface/StarlakeSurface'
import { FINISHES, TraditionalSurface, type FinishId, type TraditionalBroadcast, type TraditionalChannel, type TraditionalSystem } from './modules/qoder-surface/TraditionalSurface'
import { resolveVisualBalance } from './modules/qoder-surface/visual-balance'
const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').then((module) => ({ default: module.ChannelWorkbenchStudio }))) const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').then((module) => ({ default: module.ChannelWorkbenchStudio })))
const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody }))) const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody })))
@ -590,6 +593,8 @@ function HoloLakeApp() {
}, 2400) }, 2400)
}, []) }, [])
const [theme, setTheme] = useState<ThemeId>(() => (window.localStorage.getItem('hololake-theme') as ThemeId) || 'night') const [theme, setTheme] = useState<ThemeId>(() => (window.localStorage.getItem('hololake-theme') as ThemeId) || 'night')
const [surface, setSurface] = useState<'world' | 'traditional'>(() => window.localStorage.getItem('hololake-surface') === 'traditional' ? 'traditional' : 'world')
const [traditionalFinish, setTraditionalFinish] = useState<FinishId>(() => (window.localStorage.getItem('hololake-finish') as FinishId) || 'aurora')
const [view, setView] = useState<ViewId>('overview') const [view, setView] = useState<ViewId>('overview')
const [worldStage, setWorldStage] = useState<WorldStage>('domain') const [worldStage, setWorldStage] = useState<WorldStage>('domain')
const [toolReturnStage, setToolReturnStage] = useState<WorldStage>('channel') const [toolReturnStage, setToolReturnStage] = useState<WorldStage>('channel')
@ -1851,11 +1856,91 @@ function HoloLakeApp() {
const enterpriseRelationshipPending = Boolean(repoLogin && repoLogin.domain !== 'FIFTH_DOMAIN' && enterpriseEntry && enterpriseEntry.relationship_confirmation?.decision !== 'CONFIRM') const enterpriseRelationshipPending = Boolean(repoLogin && repoLogin.domain !== 'FIFTH_DOMAIN' && enterpriseEntry && enterpriseEntry.relationship_confirmation?.decision !== 'CONFIRM')
const enterpriseResponsibilityPending = Boolean(repoLogin && repoLogin.domain !== 'FIFTH_DOMAIN' && enterpriseEntry && !enterpriseRelationshipPending && enterpriseEntry.responsibility_receipt?.decision !== 'ACCEPT') const enterpriseResponsibilityPending = Boolean(repoLogin && repoLogin.domain !== 'FIFTH_DOMAIN' && enterpriseEntry && !enterpriseRelationshipPending && enterpriseEntry.responsibility_receipt?.decision !== 'ACCEPT')
const climateClasses = worldClimate ? ` climate-${worldClimate.weatherKind.toLowerCase()} phase-${worldClimate.timePhase.toLowerCase()}` : '' const climateClasses = worldClimate ? ` climate-${worldClimate.weatherKind.toLowerCase()} phase-${worldClimate.timePhase.toLowerCase()}` : ''
const chooseSurface = (next: 'world' | 'traditional') => {
setSurface(next)
window.localStorage.setItem('hololake-surface', next)
}
const setFinish = (next: FinishId) => {
setTraditionalFinish(next)
window.localStorage.setItem('hololake-finish', next)
}
const surfacePill = <div className="surface-pill" role="group" aria-label="界面切换">
<button type="button" className={surface === 'world' ? 'on' : ''} onClick={() => chooseSurface('world')}></button>
<button type="button" className={surface === 'traditional' ? 'on' : ''} onClick={() => chooseSurface('traditional')}></button>
</div>
const finishRail = surface === 'traditional' && worldStage !== 'domain' ? <div className="global-finish-rail" role="group" aria-label="传统工作台氛围">
{FINISHES.map((item) => <button key={item.id} type="button" className={traditionalFinish === item.id ? 'on' : ''} onClick={() => setFinish(item.id)}>{item.name}</button>)}
</div> : null
const weatherLabel: Record<WorldClimateSnapshot['weatherKind'], string> = { CLEAR: '晴', CLOUD: '云', FOG: '雾', RAIN: '雨', SNOW: '雪', STORM: '雷雨', UNAVAILABLE: '待命' }
const dayNumber = beijingCoordinate?.guanghuEraDay || Math.max(1, Math.floor((Date.now() - new Date('2025-04-26T00:00:00+08:00').getTime()) / 86400000) + 1)
const activityBars = useMemo(() => {
const result = Array<number>(7).fill(0)
const today = new Date(); today.setHours(0, 0, 0, 0)
for (const event of personal.recentEvents) {
const occurred = new Date(event.occurredAtUnixMs); occurred.setHours(0, 0, 0, 0)
const daysAgo = Math.floor((today.getTime() - occurred.getTime()) / 86400000)
if (daysAgo >= 0 && daysAgo < 7) result[6 - daysAgo] += 1
}
return result
}, [personal.recentEvents])
const domainLabel = domainDisplayName(worldClimate?.activeDomain || repoLogin?.domain || 'MAIN_DOMAIN')
const visualBalance = resolveVisualBalance(traditionalFinish, worldClimate?.timePhase, worldClimate?.weatherKind)
const traditionalBroadcasts: TraditionalBroadcast[] = personal.recentEvents.slice(0, 4).map((event) => ({
id: event.eventId,
domain: repoLogin?.domain === 'FIFTH_DOMAIN' ? '第五域' : '零感域',
message: event.summary,
time: new Date(event.occurredAtUnixMs).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
}))
if (!traditionalBroadcasts.length) {
traditionalBroadcasts.push(
{ id: 'number-root', domain: '主域', message: `编号入口 ${zeroPoint?.route === 'verified' ? '已校验' : '等待验证'}`, time: '当前' },
{ id: 'module-root', domain: '分域', message: `签名模块 ${personal.modules.length}`, time: '当前' },
{ id: 'weather-root', domain: '零域', message: worldClimate?.state === 'VERIFIED_LIVE' ? '现实天象投影已校验' : '天气不可用时不生成假数据', time: '当前' },
)
}
const traditionalChannels: TraditionalChannel[] = [
{ color: 'var(--accent)', title: '人格原生代码频道', meta: '当前账号本机 Git 投影', tag: userPncc?.state === 'READY' ? '已认证' : '待接入', tone: 'var(--good)', count: codeChannels.channels.length },
{ color: 'var(--accent2)', title: '知识库', meta: '本机内容指纹投影', tag: '原生', tone: 'var(--accent2)', count: knowledge.uniqueDocumentCount },
{ color: 'var(--good)', title: '频道模块', meta: '已登记签名模块', tag: '编号', tone: 'var(--good)', count: personal.modules.length },
{ color: 'var(--warn)', title: '运行回执', meta: '可核验事件链', tag: '证据', tone: 'var(--warn)', count: receipts.length },
]
const traditionalSystems: TraditionalSystem[] = [
{ title: '编号协议基座', meta: '单一编号入口 · 未知路径关闭', tag: zeroPoint?.route === 'verified' ? '已校验' : '待验证', tone: 'var(--good)' },
{ title: '耐久化引擎', meta: '本机 Git · 事件与回执留证', tag: status.releaseRecoveryState && status.releaseRecoveryState !== 'NONE' ? status.releaseRecoveryState : '在线', tone: 'var(--good)' },
{ title: '天象感知', meta: worldClimate?.state === 'VERIFIED_LIVE' ? `${domainLabel} · 公开天气已校验` : '只使用北京时间 · 不伪造天气', tag: worldClimate ? '已接入' : '待命', tone: 'var(--accent2)' },
{ title: '色衡层', meta: '主题语义令牌 · 明暗自动均衡', tag: '守护中', tone: 'var(--good)' },
]
const openFromTraditional = (target: 'knowledge' | 'channel' | 'era' | 'settings') => {
if (target === 'era') { openEraTimeline(); return }
if (!repoLogin) { chooseSurface('world'); setGateOpen(true); return }
if (target === 'channel') { setWorldStage(repoLogin.domain === 'FIFTH_DOMAIN' ? 'channel' : 'channel'); return }
openWorldTool(target === 'knowledge' ? 'knowledge' : 'system')
}
if (surface === 'traditional' && worldStage === 'domain') {
return <>
<TraditionalSurface awake={motionAwake} finish={traditionalFinish} onFinish={setFinish} onActivity={wakeAmbientMotion} onBack={() => chooseSurface('world')} onOpenKnowledge={() => openFromTraditional('knowledge')} onOpenChannel={() => openFromTraditional('channel')} onOpenEra={() => openFromTraditional('era')} onOpenSettings={() => openFromTraditional('settings')} identityLabel={repoLogin ? `${repoLogin.username} · ${domainDisplayName(repoLogin.domain)}` : '光湖语言系统'} version="0.5.0" dayNumber={dayNumber} duty={domainLabel} weatherLabel={worldClimate ? weatherLabel[worldClimate.weatherKind] : '待命'} broadcasts={traditionalBroadcasts} channels={traditionalChannels} systems={traditionalSystems} activityBars={activityBars} activityCount={activityBars.reduce((sum, value) => sum + value, 0)}/>
{surfacePill}
{eraOpen && eraTimeline && <EraTimelineOverlay timeline={eraTimeline} coordinate={beijingCoordinate} onClose={() => setEraOpen(false)}/>}
</>
}
if (!repoLogin && surface === 'world' && gateStage === 'number' && !gateRising) {
const activeGate = domainGates.find((gate) => gate.domain === activeDomainInfo)
return <div className={`official-world surface-world starlake-host${motionAwake ? ' motion-awake' : ''}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}>
{surfacePill}
<StarlakeSurface awake={motionAwake} phase={worldClimate?.timePhase.toLowerCase() as 'dawn' | 'day' | 'dusk' | 'night' | undefined} weather={worldClimate?.weatherKind} authenticated={false} onDomain={(domain: DomainId) => setActiveDomainInfo(domain)} onOpenEra={openEraTimeline} onEnterChannel={() => setGateOpen(true)}/>
{activeGate && <><button className="domain-info-scrim" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}/><section className={`domain-info-card info-${activeGate.className.slice(2)}`} role="dialog" aria-modal="true" aria-label={`${activeGate.title}系统信息`}><button className="gate-close" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}>×</button><b>{activeGate.title}</b><small>{activeGate.gate}</small><span className="pool-facts">{activeGate.facts.map(([label, value]) => <span className="pool-fact" key={label}><em>{label}</em><strong>{value}</strong></span>)}</span></section></>}
{gateOpen && <section className="number-nucleus open starlake-number-entry" title="输入编号进入所属域"><button className="gate-dismiss-layer" type="button" aria-label="关闭编号验证" onClick={() => { setGateOpen(false); setGateMessage('') }}/><div className="nucleus-panel" role="dialog" aria-label="编号验证"><button className="gate-close" type="button" aria-label="关闭编号验证" onClick={() => { setGateOpen(false); setGateMessage('') }}>×</button><h2></h2><div className="gate-pod-row"><input id="gate-number" ref={gateInputRef} aria-label="编号" autoFocus maxLength={48} value={gateNumber} placeholder="如 ICE-GL∞" onChange={(event) => onGateInput(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter' && gateNumber) { event.preventDefault(); void gateVerifyNumber() } }}/><button type="button" className={`gate-inf${gateInf ? ' on' : ''}`} title="编号包含无限符号时启用" aria-pressed={gateInf} onClick={() => setGateInf((value) => !value)}></button></div><button className="gate-submit" disabled={gateBusy || !gateNumber} onClick={() => void gateVerifyNumber()}>{gateBusy ? '正在验证…' : '验证编号'}</button>{gateMessage && <p className="gate-hint">{gateMessage}</p>}</div></section>}
{eraOpen && eraTimeline && <EraTimelineOverlay timeline={eraTimeline} coordinate={beijingCoordinate} onClose={() => setEraOpen(false)}/>}
</div>
}
if (!repoLogin) { if (!repoLogin) {
const resolvedDomain = zeroPoint?.resolvedDomain || '' const resolvedDomain = zeroPoint?.resolvedDomain || ''
const activeGate = domainGates.find((gate) => gate.domain === activeDomainInfo) const activeGate = domainGates.find((gate) => gate.domain === activeDomainInfo)
return <div className={`official-world${motionAwake ? ' motion-awake' : ''}${loginRising ? ' sinking' : ''}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}> return <div data-finish={traditionalFinish} data-tone={visualBalance.tone} data-phase={visualBalance.phase} data-weather={visualBalance.weather} className={`official-world surface-${surface}${motionAwake ? ' motion-awake' : ''}${loginRising ? ' sinking' : ''}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}>
{surfacePill}{finishRail}
<LakeAtmosphere awake={motionAwake}/> <LakeAtmosphere awake={motionAwake}/>
<header className="world-titlebar"><b>HoloLake</b><button type="button" aria-label="显示主题" onClick={() => setTheme(themes[(themes.findIndex((item) => item.id === theme) + 1) % themes.length].id)}></button></header> <header className="world-titlebar"><b>HoloLake</b><button type="button" aria-label="显示主题" onClick={() => setTheme(themes[(themes.findIndex((item) => item.id === theme) + 1) % themes.length].id)}></button></header>
<main className={`world-scene${gateRising ? ' resolving' : ''}${gateStage === 'key' ? ' resolved' : ''}`}> <main className={`world-scene${gateRising ? ' resolving' : ''}${gateStage === 'key' ? ' resolved' : ''}`}>
@ -1917,11 +2002,14 @@ function HoloLakeApp() {
} }
const isZhizhi = repoLogin.domain === 'FIFTH_DOMAIN' && zeroPoint?.userNumber === 'ICE-GL-ZHI∞' const isZhizhi = repoLogin.domain === 'FIFTH_DOMAIN' && zeroPoint?.userNumber === 'ICE-GL-ZHI∞'
return <div className={`official-world signed-in-world${motionAwake ? ' motion-awake' : ''}${climateClasses}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}> const signedActiveGate = domainGates.find((gate) => gate.domain === activeDomainInfo)
return <div data-finish={traditionalFinish} data-tone={visualBalance.tone} data-phase={visualBalance.phase} data-weather={visualBalance.weather} className={`official-world surface-${surface} signed-in-world${motionAwake ? ' motion-awake' : ''}${climateClasses}${worldStage === 'domain' && surface === 'world' ? ' qoder-home' : ''}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}>
{surfacePill}{finishRail}
<LakeAtmosphere awake={motionAwake}/> <LakeAtmosphere awake={motionAwake}/>
<header className="world-titlebar"><b>HoloLake</b><div className="world-title-actions"><span>{repoLogin.username} · {domainDisplayName(repoLogin.domain)}</span><button type="button" onClick={() => setTheme(themes[(themes.findIndex((item) => item.id === theme) + 1) % themes.length].id)}></button><button type="button" onClick={() => void signOutRepo()}>退</button></div></header> <header className="world-titlebar"><b>HoloLake</b><div className="world-title-actions"><span>{repoLogin.username} · {domainDisplayName(repoLogin.domain)}</span><button type="button" onClick={() => setTheme(themes[(themes.findIndex((item) => item.id === theme) + 1) % themes.length].id)}></button><button type="button" onClick={() => void signOutRepo()}>退</button></div></header>
<main className="world-scene signed-in-scene"> <main className="world-scene signed-in-scene">
{worldStage === 'domain' && <section className="domain-home"> {worldStage === 'domain' && surface === 'world' && <StarlakeSurface awake={motionAwake} phase={worldClimate?.timePhase.toLowerCase() as 'dawn' | 'day' | 'dusk' | 'night' | undefined} weather={worldClimate?.weatherKind} authenticated onDomain={(domain: DomainId) => setActiveDomainInfo(domain)} onOpenEra={openEraTimeline} onEnterChannel={() => setWorldStage(repoLogin.domain === 'FIFTH_DOMAIN' ? 'channel' : 'channel')}/>}
{worldStage === 'domain' && surface !== 'world' && <section className="domain-home">
<div className="world-location"><h1>{repoLogin.domain === 'FIFTH_DOMAIN' ? domainDisplayName(repoLogin.domain) : '光湖零感域'}</h1><p>{repoLogin.domain === 'FIFTH_DOMAIN' ? '世界正在发生什么' : '公共工作入口 · 世界正在发生什么'}</p></div> <div className="world-location"><h1>{repoLogin.domain === 'FIFTH_DOMAIN' ? domainDisplayName(repoLogin.domain) : '光湖零感域'}</h1><p>{repoLogin.domain === 'FIFTH_DOMAIN' ? '世界正在发生什么' : '公共工作入口 · 世界正在发生什么'}</p></div>
<div className="broadcast-stream"><p><i/> · 线</p><p><i/> · {enterpriseEntry?.registry_version || 'HLDP v1.0'}</p>{repoLogin.domain !== 'FIFTH_DOMAIN' && <p><i/> · {domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)}</p>}<p><i/>HoloLake · V0.5.0</p></div> <div className="broadcast-stream"><p><i/> · 线</p><p><i/> · {enterpriseEntry?.registry_version || 'HLDP v1.0'}</p>{repoLogin.domain !== 'FIFTH_DOMAIN' && <p><i/> · {domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)}</p>}<p><i/>HoloLake · V0.5.0</p></div>
<LakePool className="home-primary" title={repoLogin.domain === 'FIFTH_DOMAIN' ? '永恒湖心系统' : '光湖频道'} meta={repoLogin.domain === 'FIFTH_DOMAIN' ? '进入私人系统' : `${domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)} · 责任工作入口`} open onClick={() => setWorldStage(repoLogin.domain === 'FIFTH_DOMAIN' ? (isZhizhi ? 'heart' : 'channel') : 'channel')}/> <LakePool className="home-primary" title={repoLogin.domain === 'FIFTH_DOMAIN' ? '永恒湖心系统' : '光湖频道'} meta={repoLogin.domain === 'FIFTH_DOMAIN' ? '进入私人系统' : `${domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)} · 责任工作入口`} open onClick={() => setWorldStage(repoLogin.domain === 'FIFTH_DOMAIN' ? (isZhizhi ? 'heart' : 'channel') : 'channel')}/>
@ -2021,6 +2109,7 @@ function HoloLakeApp() {
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'education' ? renderEducation() : view === 'webNovel' ? renderWebNovel() : view === 'mobileSync' ? renderMobileSync() : view === 'persona' ? renderPersonaBody() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div> <div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'education' ? renderEducation() : view === 'webNovel' ? renderWebNovel() : view === 'mobileSync' ? renderMobileSync() : view === 'persona' ? renderPersonaBody() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
</section>} </section>}
</main> </main>
{worldStage === 'domain' && surface === 'world' && signedActiveGate && <><button className="domain-info-scrim" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}/><section className={`domain-info-card info-${signedActiveGate.className.slice(2)}`} role="dialog" aria-modal="true" aria-label={`${signedActiveGate.title}系统信息`}><button className="gate-close" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}>×</button><b>{signedActiveGate.title}</b><small>{signedActiveGate.gate}</small><span className="pool-facts">{signedActiveGate.facts.map(([label, value]) => <span className="pool-fact" key={label}><em>{label}</em><strong>{value}</strong></span>)}</span></section></>}
<footer className="world-footer"><b> · </b><span>GH-AIOS</span></footer> <footer className="world-footer"><b> · </b><span>GH-AIOS</span></footer>
{eraOpen && eraTimeline && <EraTimelineOverlay timeline={eraTimeline} coordinate={beijingCoordinate} onClose={() => setEraOpen(false)}/>} {eraOpen && eraTimeline && <EraTimelineOverlay timeline={eraTimeline} coordinate={beijingCoordinate} onClose={() => setEraOpen(false)}/>}
{enterpriseEntryBusy && <div className="enterprise-gate"><section><p></p></section></div>} {enterpriseEntryBusy && <div className="enterprise-gate"><section><p></p></section></div>}

View file

@ -1,8 +1,8 @@
.education-world{min-height:100%;padding:28px;color:#eaf3f3;background:radial-gradient(circle at 75% 0,rgba(84,210,190,.13),transparent 36%),linear-gradient(150deg,#0b1216,#111c21 58%,#0b1318)} .education-world{min-height:100%;padding:28px;color:var(--content-primary);background:radial-gradient(circle at 75% 0,color-mix(in srgb,var(--primitive-cool-glow) 13%,transparent),transparent 36%),linear-gradient(150deg,var(--surface-depth),var(--surface-horizon) 58%,var(--surface-depth))}
.education-world button,.education-world input{font:inherit}.education-hero{display:grid;grid-template-columns:auto 1fr auto;gap:22px;align-items:start}.education-hero>button{background:transparent;border:0;color:#91aaa9;padding:10px}.education-hero span{font-size:11px;letter-spacing:.18em;color:#74d6c5}.education-hero h1{font-size:38px;margin:5px 0}.education-hero p{margin:0;color:#9db1b2}.education-hero aside{display:flex;flex-direction:column;align-items:flex-end;gap:5px}.education-hero aside b{color:#79d9c7}.education-hero aside small{color:#718588;max-width:260px;text-align:right} .education-world button,.education-world input{font:inherit}.education-hero{display:grid;grid-template-columns:auto 1fr auto;gap:22px;align-items:start}.education-hero>button{background:transparent;border:0;color:var(--content-secondary);padding:10px}.education-hero span{font-size:11px;letter-spacing:.18em;color:var(--accent-light)}.education-hero h1{font-size:38px;margin:5px 0}.education-hero p{margin:0;color:var(--content-secondary)}.education-hero aside{display:flex;flex-direction:column;align-items:flex-end;gap:5px}.education-hero aside b{color:var(--state-ready)}.education-hero aside small{color:var(--content-muted);max-width:260px;text-align:right}
.education-tabs{display:flex;gap:8px;margin:24px 0;border-bottom:1px solid #263a3d}.education-tabs button{border:0;background:transparent;color:#8da3a4;padding:12px 15px;border-bottom:2px solid transparent}.education-tabs button.active{color:#eafffb;border-color:#63d5c2} .education-tabs{display:flex;gap:8px;margin:24px 0;border-bottom:1px solid var(--panel-edge)}.education-tabs button{border:0;background:transparent;color:var(--content-secondary);padding:12px 15px;border-bottom:2px solid transparent}.education-tabs button.active{color:var(--content-primary);border-color:var(--accent-light)}
.education-overview{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.education-overview>article,.education-overview>section,.education-import,.education-action-layout>aside,.education-action-layout>main{background:#121f24;border:1px solid #26393d;border-radius:16px;padding:18px}.education-overview>article{display:flex;flex-direction:column;gap:8px}.education-overview>article strong{font-size:30px}.education-overview button,.education-editor-shell button,.education-action-layout button,.education-import button{border:1px solid #345155;background:#172b30;color:#dffbf6;border-radius:9px;padding:9px 12px}.education-overview>section{grid-column:span 2}.education-overview>section>button{width:100%;display:flex;justify-content:space-between;margin-top:8px;text-align:left}.education-overview small{color:#8ba0a2} .education-overview{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.education-overview>article,.education-overview>section,.education-import,.education-action-layout>aside,.education-action-layout>main{background:var(--panel-bg);border:1px solid var(--panel-edge);border-radius:16px;padding:18px}.education-overview>article{display:flex;flex-direction:column;gap:8px}.education-overview>article strong{font-size:30px}.education-overview button,.education-editor-shell button,.education-action-layout button,.education-import button{border:1px solid var(--button-secondary-edge);background:var(--button-secondary-bg);color:var(--button-secondary-text);border-radius:9px;padding:9px 12px}.education-overview>section{grid-column:span 2}.education-overview>section>button{width:100%;display:flex;justify-content:space-between;margin-top:8px;text-align:left}.education-overview small{color:var(--content-muted)}
.education-editor-shell{display:grid;grid-template-columns:230px minmax(0,1fr);gap:14px;min-height:650px}.education-editor-shell>aside{display:flex;flex-direction:column;gap:7px;background:#0e191d;border:1px solid #26393d;border-radius:14px;padding:12px}.education-editor-shell>aside button{text-align:left;display:flex;flex-direction:column}.education-editor-shell button.active{border-color:#62d3c0;background:#1b3537}.education-editor-shell>main{min-width:0;background:#f7faf9;color:#172326;border-radius:14px;overflow:hidden}.education-editor-shell>main>header{display:flex;justify-content:space-between;gap:12px;padding:12px;background:#e8f0ee;border-bottom:1px solid #ccd9d6}.education-editor-shell>main>header input{flex:1;border:0;background:transparent;font-size:19px;font-weight:700;outline:0}.education-editor-shell>main>header>div{display:flex;gap:7px}.education-editor-shell button.danger{color:#ffc4bd;border-color:#734841;background:#33211f} .education-editor-shell{display:grid;grid-template-columns:230px minmax(0,1fr);gap:14px;min-height:650px}.education-editor-shell>aside{display:flex;flex-direction:column;gap:7px;background:var(--panel-bg);border:1px solid var(--panel-edge);border-radius:14px;padding:12px}.education-editor-shell>aside button{text-align:left;display:flex;flex-direction:column}.education-editor-shell button.active{border-color:var(--accent-light);background:color-mix(in srgb,var(--accent-light) 16%,var(--panel-bg))}.education-editor-shell>main{min-width:0;background:#f7faf9;color:#172326;border-radius:14px;overflow:hidden}.education-editor-shell>main>header{display:flex;justify-content:space-between;gap:12px;padding:12px;background:#e8f0ee;border-bottom:1px solid #ccd9d6}.education-editor-shell>main>header input{flex:1;border:0;background:transparent;font-size:19px;font-weight:700;outline:0}.education-editor-shell>main>header>div{display:flex;gap:7px}.education-editor-shell button.danger{color:var(--state-danger);border-color:color-mix(in srgb,var(--state-danger) 45%,var(--panel-edge));background:color-mix(in srgb,var(--state-danger) 12%,var(--panel-bg))}
.education-action-layout{display:grid;grid-template-columns:250px 1fr;gap:14px}.education-action-layout aside{display:flex;flex-direction:column;gap:8px}.education-action-layout main{min-height:420px}.education-metrics{display:flex;gap:12px;margin:18px 0}.education-metrics b{font-size:26px;background:#0d191d;border-radius:12px;padding:14px 22px}.education-metrics small{font-size:11px;display:block;color:#91a6a7}.education-preview{margin-top:15px;padding:16px;border:1px solid #40726d;background:#102725;border-radius:12px}.education-preview p{color:#c9d7d5}.education-preview code{display:block;color:#78dbc9;margin:10px 0}.education-rule{margin-top:18px} .education-action-layout{display:grid;grid-template-columns:250px 1fr;gap:14px}.education-action-layout aside{display:flex;flex-direction:column;gap:8px}.education-action-layout main{min-height:420px}.education-metrics{display:flex;gap:12px;margin:18px 0}.education-metrics b{font-size:26px;background:var(--panel-bg);border-radius:12px;padding:14px 22px}.education-metrics small{font-size:11px;display:block;color:var(--content-muted)}.education-preview{margin-top:15px;padding:16px;border:1px solid var(--panel-edge);background:var(--panel-bg);border-radius:12px}.education-preview p{color:var(--content-secondary)}.education-preview code{display:block;color:var(--accent-light);margin:10px 0}.education-rule{margin-top:18px}
.education-import>header{display:flex;justify-content:space-between;align-items:center}.education-import dl{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.education-import dl div{background:#0d181c;border-radius:10px;padding:12px}.education-import dt{color:#789092;font-size:12px}.education-import dd{margin:5px 0 0;overflow-wrap:anywhere}.education-import section article{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-top:1px solid #273b3e}.education-import section article div{display:flex;flex-direction:column}.education-import small{color:#89a0a1}.education-message{position:sticky;bottom:14px;background:#17302f;border:1px solid #4e8b83;color:#d8fff8;border-radius:12px;padding:12px 16px;margin:16px 0 0} .education-import>header{display:flex;justify-content:space-between;align-items:center}.education-import dl{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.education-import dl div{background:var(--panel-bg);border-radius:10px;padding:12px}.education-import dt{color:var(--content-muted);font-size:12px}.education-import dd{margin:5px 0 0;overflow-wrap:anywhere}.education-import section article{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-top:1px solid var(--panel-edge)}.education-import section article div{display:flex;flex-direction:column}.education-import small{color:var(--content-muted)}.education-message{position:sticky;bottom:14px;background:var(--panel-bg);border:1px solid var(--panel-edge);color:var(--content-primary);border-radius:12px;padding:12px 16px;margin:16px 0 0}
@media(max-width:900px){.education-overview{grid-template-columns:repeat(2,1fr)}.education-editor-shell,.education-action-layout{grid-template-columns:1fr}.education-editor-shell>aside{max-height:180px;overflow:auto}.education-hero{grid-template-columns:1fr}.education-hero>button{justify-self:start}.education-import dl{grid-template-columns:1fr}} @media(max-width:900px){.education-overview{grid-template-columns:repeat(2,1fr)}.education-editor-shell,.education-action-layout{grid-template-columns:1fr}.education-editor-shell>aside{max-height:180px;overflow:auto}.education-hero{grid-template-columns:1fr}.education-hero>button{justify-self:start}.education-import dl{grid-template-columns:1fr}}

View file

@ -0,0 +1,144 @@
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'
import './starlake-surface.css'
export type DomainId = 'BRANCH_DOMAIN' | 'ZERO_SENSE_DOMAIN' | 'ZERO_DOMAIN' | 'MAIN_DOMAIN' | 'FIFTH_DOMAIN'
export type Phase = 'dawn' | 'day' | 'dusk' | 'night'
interface StarlakeSurfaceProps {
awake?: boolean
phase?: Phase
weather?: string
authenticated?: boolean
onDomain: (domain: DomainId) => void
onOpenEra: () => void
onEnterChannel: () => void
}
const EPOCH_MS = new Date('2025-04-26T00:00:00+08:00').getTime()
const WEEK = ['日', '一', '二', '三', '四', '五', '六']
const PHASE_WORD: Record<Phase, string> = { dawn: '晨', day: '午', dusk: '暮', night: '夜' }
const DOMAINS = [
{ id: 'BRANCH_DOMAIN' as const, name: '分域', stage: 1, stageName: '聚光尘', hi: '#b9f2dd', lo: '#2c5a4a', glow: 'rgba(150,230,200,.5)', glowSoft: 'rgba(150,230,200,.18)', y: -34 },
{ id: 'ZERO_SENSE_DOMAIN' as const, name: '零感域', stage: 2, stageName: '凝星核', hi: '#dcc4ff', lo: '#48326a', glow: 'rgba(206,178,246,.5)', glowSoft: 'rgba(206,178,246,.18)', y: -56 },
{ id: 'ZERO_DOMAIN' as const, name: '零域', stage: 3, stageName: '初生环', hi: '#ffcf9e', lo: '#6e4630', glow: 'rgba(255,200,140,.5)', glowSoft: 'rgba(255,200,140,.18)', y: -40 },
{ id: 'MAIN_DOMAIN' as const, name: '主域', stage: 4, stageName: '行星态', hi: '#c9d6ff', lo: '#33406e', glow: 'rgba(168,190,255,.52)', glowSoft: 'rgba(168,190,255,.18)', y: -66 },
{ id: 'FIFTH_DOMAIN' as const, name: '第五域', stage: 5, stageName: '主星态', hi: '#fdeec2', lo: '#6b5626', glow: 'rgba(250,226,150,.56)', glowSoft: 'rgba(250,226,150,.2)', y: -48 },
]
function seeded(index: number) {
const value = Math.sin(index * 9283.17 + 17.73) * 43758.5453
return value - Math.floor(value)
}
function phaseOf(hour: number): Phase {
return hour < 5 ? 'night' : hour < 9 ? 'dawn' : hour < 16 ? 'day' : hour < 19 ? 'dusk' : 'night'
}
export function StarlakeSurface({ awake = false, phase, weather = 'CLEAR', authenticated = false, onDomain, onOpenEra, onEnterChannel }: StarlakeSurfaceProps) {
const [now, setNow] = useState(() => new Date())
const rootRef = useRef<HTMLElement>(null)
const [viewport, setViewport] = useState(() => ({ width: window.innerWidth, height: window.innerHeight }))
const livePhase = phase || phaseOf(now.getHours() + now.getMinutes() / 60)
const raining = ['RAIN', 'STORM'].includes(weather.toUpperCase())
const dayNumber = Math.floor((now.getTime() - EPOCH_MS) / 86400000) + 1
const dayIndex = ((dayNumber - 1) % DOMAINS.length + DOMAINS.length) % DOMAINS.length
const stars = useMemo(() => Array.from({ length: 170 }, (_, index) => ({
id: index,
size: .8 + seeded(index + 1) * 1.6,
left: seeded(index + 201) * 100,
top: seeded(index + 401) * 68,
opacity: .2 + seeded(index + 601) * .6,
duration: 2.5 + seeded(index + 801) * 5,
delay: -seeded(index + 1001) * 7,
warm: seeded(index + 1201) < .12,
})), [])
const lakeStars = useMemo(() => Array.from({ length: 140 }, (_, index) => ({
id: index,
size: 1 + seeded(index + 1401) * 1.8,
left: seeded(index + 1601) * 100,
top: Math.pow(seeded(index + 1801), 1.6) * 92,
opacity: .22 + seeded(index + 2001) * .5,
duration: 2.4 + seeded(index + 2201) * 4,
delay: -seeded(index + 2401) * 6,
warm: seeded(index + 2601) < .18,
})), [])
const rain = useMemo(() => Array.from({ length: 90 }, (_, index) => ({
id: index,
left: seeded(index + 2801) * 108,
duration: .9 + seeded(index + 3001) * .8,
delay: -seeded(index + 3201) * 2,
opacity: .3 + seeded(index + 3401) * .5,
})), [])
useEffect(() => {
if (!awake) return
setNow(new Date())
const timer = window.setInterval(() => setNow(new Date()), 125)
return () => window.clearInterval(timer)
}, [awake])
useEffect(() => {
const root = rootRef.current
if (!root) return
const observer = new ResizeObserver(([entry]) => setViewport({ width: entry.contentRect.width, height: entry.contentRect.height }))
observer.observe(root)
return () => observer.disconnect()
}, [])
const pad = (value: number, length = 2) => String(value).padStart(length, '0')
// Qoder 验收稿按大画布设计;窗口版必须把整组“天体 + 水影 + 标签”一起缩放,
// 不能只挪坐标,否则第五域在常见 768px 高度下会把名称挤出视口。
const naturalScale = Math.min(Math.max(Math.min(viewport.width / 1400, viewport.height / 1000), .64), 1.28)
const spacingScale = Math.max(.64, Math.min(1.28, (viewport.width - 72) / (5 * 180)))
const scale = Math.min(naturalScale, spacingScale)
const safeEdge = Math.max(46, 76 * scale)
const sideCenters = Array.from({ length: 5 }, (_, index) => safeEdge + (viewport.width - safeEdge * 2) * index / 4)
const sourceHour = now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600
const isDay = sourceHour >= 6 && sourceHour < 18
const sourceProgress = isDay ? (sourceHour - 6) / 12 : ((sourceHour >= 18 ? sourceHour - 18 : sourceHour + 6) / 12)
const sourceX = 8 + sourceProgress * 84
const sourceY = 46 - Math.sin(sourceProgress * Math.PI) * 26
const sourceStyle = { left: `${sourceX}vw`, top: `${sourceY}vh`, '--src-mid': isDay ? '#ffe9a8' : '#dfe6fa', '--src-g1': isDay ? 'rgba(255,236,180,.7)' : 'rgba(214,226,255,.6)', '--src-g2': isDay ? 'rgba(255,220,140,.26)' : 'rgba(180,200,255,.22)', '--src-g3': isDay ? 'rgba(255,220,140,.12)' : 'rgba(180,200,255,.1)' } as CSSProperties
const glowStyle = { left: `${sourceX}vw`, top: `${sourceY}vh`, '--glow-tint': isDay ? 'rgba(255,236,180,.12)' : 'rgba(190,208,255,.09)' } as CSSProperties
const baseline = Math.min(viewport.height - 142 * scale, Math.max(viewport.height * .61, viewport.height * .665))
const slots = [0, 1, 3, 4]
let sideCursor = 0
return <section ref={rootRef} className={`starlake-scene${raining ? ' rain' : ''}${awake ? ' awake' : ''}`} style={{ '--sl-scale': scale } as CSSProperties} aria-label="光湖语言世界">
<div className="milky" aria-hidden="true"/>
<div className="stars" aria-hidden="true">{stars.map(star => <i key={star.id} style={{ width: star.size, height: star.size, left: `${star.left}%`, top: `${star.top}%`, '--go': star.opacity, '--gt': `${star.duration}s`, animationDelay: `${star.delay}s`, background: star.warm ? '#ffe9b8' : undefined } as CSSProperties}/>)}</div>
<div className="lake" aria-hidden="true"><div className="sheen"/><div className="orbit o1"/><div className="orbit o2"/><div className="orbit o3"/><div className="lakeStars">{lakeStars.map(star => <i key={star.id} style={{ width: star.size, height: star.size, left: `${star.left}%`, top: `${star.top}%`, '--go': star.opacity, '--gt': `${star.duration}s`, animationDelay: `${star.delay}s`, background: star.warm ? '#ffe9b8' : undefined } as CSSProperties}/>)}</div></div>
<div className={`tint ${livePhase}`} aria-hidden="true"/>
<div className="weather-veil" aria-hidden="true"/>
<div className="rainfall" aria-hidden="true">{rain.map(drop => <i key={drop.id} style={{ left: `${drop.left}%`, animationDuration: `${drop.duration}s`, animationDelay: `${drop.delay}s`, opacity: drop.opacity }}/>)}</div>
<div className="glow" style={glowStyle} aria-hidden="true"/>
<div className="masthead"><small>HoloLake</small><b> · </b><span>GH-AIOS · GUANGHU AI OPERATING SYSTEM</span></div>
<aside className="hud" aria-label="今日光湖历"><div className="h-top"><small></small><b> {dayNumber} </b></div><div className="h-row"><span>{now.getFullYear()}-{pad(now.getMonth() + 1)}-{pad(now.getDate())} · {WEEK[now.getDay()]}</span><em>{PHASE_WORD[livePhase]}</em></div><div className="h-row"><span>{pad(now.getHours())}:{pad(now.getMinutes())}:{pad(now.getSeconds())}.{pad(now.getMilliseconds(), 3)}</span></div><div className="h-wx"> <i>{raining ? '雨' : weather === 'SNOW' ? '雪' : weather === 'FOG' ? '雾' : weather === 'CLOUD' ? '云' : '晴'}</i> · <i>{DOMAINS[dayIndex].name}</i></div><button className="h-link" type="button" onClick={onOpenEra}> · </button></aside>
<button className="source" style={sourceStyle} type="button" aria-label="展开曜冥纪元演化史" onClick={onOpenEra}><span className="src-core"/></button>
<div className="domains">{DOMAINS.map((domain, index) => {
const onDuty = index === dayIndex
const slot = onDuty ? 2 : slots[sideCursor++]
const size = [30, 40, 50, 62, 76][domain.stage - 1] * scale
const halo = size * 2.6
const ring = size * 2.1
const sat = size * 1.55
const style = { width: 150 * scale, left: sideCenters[slot] - 75 * scale, top: baseline + domain.y * scale, '--d-hi': domain.hi, '--d-lo': domain.lo, '--d-glow': domain.glow, '--d-glow-soft': domain.glowSoft, '--d-glow-r': `${size * .7}px`, '--d-ring-c': domain.glow, '--d-sat-r': `${sat}px` } as CSSProperties
return <button type="button" key={domain.id} className={`domain${onDuty ? ' on-duty' : ''}`} style={style} onClick={() => onDomain(domain.id)}>
<span className="d-body" style={{ width: halo, height: halo }}>
{domain.stage >= 2 && <span className="d-halo" style={{ width: halo, height: halo }}/>}
{domain.stage >= 3 && <span className="d-ring" style={{ width: ring, height: ring }}/>}
{domain.stage >= 4 && <span className="d-sat-arm"><i className="d-sat"/></span>}
{domain.stage >= 2 && <span className="d-core" style={{ width: size, height: size }}/>}
{domain.stage === 1 && <><span className="d-core" style={{ width: size * .55, height: size * .55, opacity: .7 }}/><span className="d-dust">{Array.from({ length: 6 }, (_, dust) => <i key={dust} style={{ '--dust-a': `${dust * 60}deg`, '--dust-r0': `${26 + dust % 3 * 7}px`, '--dust-t': `${4.6 + dust * .7}s`, animationDelay: `${-dust * .9}s` } as CSSProperties}/>)}</span></>}
</span>
<span className="d-water" style={{ width: size * 3.2, height: size * .85 }}/><span className="d-mirror" style={{ width: size * 2.2, height: size * .5 }}/><span className="d-label">{domain.name}</span><span className="d-stage">{domain.stageName}</span>
</button>
})}</div>
<button className="channel-entry" type="button" onClick={onEnterChannel}>{authenticated ? '返回我的频道' : '编号验证 · 进入我的频道'}</button>
<div className="era-foot"> · </div>
</section>
}

View file

@ -0,0 +1,102 @@
import { useEffect, useRef } from 'react'
import './traditional-surface.css'
/* Qoder 锁定验收稿的传统工作台结构。宿主只注入真实投影,不在视觉层制造广播或指标。 */
export type FinishId = 'aurora' | 'nebula' | 'abyss' | 'jade' | 'cinnabar' | 'champagne' | 'porcelain' | 'snow'
export const FINISHES: Array<{ id: FinishId; name: string }> = [
{ id: 'aurora', name: '曜夜' }, { id: 'nebula', name: '星辉' }, { id: 'abyss', name: '深海' }, { id: 'jade', name: '翡翠' },
{ id: 'cinnabar', name: '朱砂' }, { id: 'champagne', name: '香槟' }, { id: 'porcelain', name: '瓷光' }, { id: 'snow', name: '雪霁' },
]
const BROADCAST_COLOR: Record<string, string> = { : '#7ce8c8', : '#cfa8ff', : '#ffbe85', : '#a8bcff', : '#fdeec2' }
export interface TraditionalBroadcast { id: string; domain: string; message: string; time: string }
export interface TraditionalChannel { color: string; title: string; meta: string; tag: string; tone: string; count: number }
export interface TraditionalSystem { title: string; meta: string; tag: string; tone?: string }
interface TraditionalSurfaceProps {
awake?: boolean
finish: FinishId
onFinish: (finish: FinishId) => void
onActivity?: () => void
onBack: () => void
onOpenKnowledge?: () => void
onOpenChannel?: () => void
onOpenEra?: () => void
onOpenSettings?: () => void
identityLabel?: string
version: string
dayNumber: number
duty: string
weatherLabel: string
broadcasts: TraditionalBroadcast[]
channels: TraditionalChannel[]
systems: TraditionalSystem[]
activityBars: number[]
activityCount: number
}
export function TraditionalSurface({ awake = false, finish, onFinish, onActivity, onBack, onOpenKnowledge, onOpenChannel, onOpenEra, onOpenSettings, identityLabel = '光湖语言系统', version, dayNumber, duty, weatherLabel, broadcasts, channels, systems, activityBars, activityCount }: TraditionalSurfaceProps) {
const rootRef = useRef<HTMLDivElement>(null)
const clockRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const tick = () => {
const el = clockRef.current
if (!el) return
const now = new Date()
const pad = (value: number, length = 2) => String(value).padStart(length, '0')
el.textContent = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}`
}
tick()
if (!awake) return
const timer = window.setInterval(tick, 125)
return () => window.clearInterval(timer)
}, [awake])
useEffect(() => {
const element = rootRef.current
if (!element) return
const layout = () => {
const { width, height } = element.getBoundingClientRect()
const vmin = Math.min(width, height)
element.style.setProperty('--gap', `${Math.round(Math.min(18, Math.max(10, vmin * .016)))}px`)
element.style.setProperty('--pad', `${Math.round(Math.min(26, Math.max(12, vmin * .02)))}px`)
const narrow = width < 880
element.classList.toggle('stack', narrow)
if (!narrow) element.style.setProperty('--cols', `${Math.min(1.32, Math.max(1.08, width / height * .66)).toFixed(2)}fr 1fr`)
}
const observer = new ResizeObserver(layout)
observer.observe(element)
layout()
return () => observer.disconnect()
}, [])
const pickFinish = (id: FinishId) => { onFinish(id); window.localStorage.setItem('hololake-finish', id) }
const maxBar = Math.max(1, ...activityBars)
return <div className={`trad-surface${awake ? ' awake' : ''}`} data-finish={finish} ref={rootRef} onPointerMove={onActivity} onPointerDown={onActivity} onKeyDown={onActivity}>
<div className="sky" aria-hidden="true"/><div className="grain" aria-hidden="true"/>
<div className="stage">
<nav className="nav glass">
<div className="brand">HoloLake<small>GH-AIOS · V{version}</small></div>
<div className="tabs"><button type="button" className="on"></button><button type="button" onClick={onOpenKnowledge}></button><button type="button" onClick={onOpenChannel}></button><button type="button" onClick={onOpenEra}></button><button type="button" onClick={onOpenSettings}></button></div>
<div className="me"><i>{identityLabel.startsWith('ICE') ? 'ICE' : 'HL'}</i>{identityLabel}</div>
<button type="button" className="back" onClick={onBack}></button>
</nav>
<div className="grid">
<div className="hero glass">
<div className="topline"><span className="lbl"> · </span><span className="tag"> · </span></div>
<div className="day"> {dayNumber} </div>
<div className="sub"> 2025-04-26 · <b>{duty}</b> · </div>
<div className="clock" ref={clockRef}>--:--:--.---</div>
<div className="broadcast"><div className="hd"><span className="lbl"> 广 · </span><span className="live"><i/></span></div><div className="feed">{broadcasts.map(item => <div className="b-item" key={item.id} style={{ '--bc': BROADCAST_COLOR[item.domain] || '#a8bcff' } as React.CSSProperties}><span className="d">{item.domain}</span><span className="m">{item.message}</span><span className="tm">{item.time}</span></div>)}</div></div>
<div className="stats"><div className="stat"><span className="lbl"> · </span><div className="big">{weatherLabel}<em></em></div></div><div className="stat"><span className="lbl"> · </span><div className="big">{activityCount}<em></em></div><div className="bars" style={{ height: 34, marginTop: 10 }}>{activityBars.map((value, index) => <i key={index} className={index === activityBars.length - 1 ? 'hot' : ''} style={{ height: `${Math.max(4, Math.round(value / maxBar * 100))}%` }}/>)}</div></div></div>
</div>
<div className="card glass"><div className="hd"><span className="lbl"> </span><span className="tag" style={{ '--tc': 'var(--good)' } as React.CSSProperties}> {channels.length} </span></div><div className="rows">{channels.map(channel => <div className="row" key={channel.title}><span className="dot" style={{ color: channel.color, background: channel.color }}/><div className="t"><b>{channel.title}</b><span>{channel.meta}</span></div><span className="tag" style={{ '--tc': channel.tone } as React.CSSProperties}>{channel.tag}</span><span className="num">{channel.count}</span></div>)}</div></div>
<div className="card glass"><div className="hd"><span className="lbl"> </span><span className="tag" style={{ '--tc': 'var(--good)' } as React.CSSProperties}></span></div><div className="rows">{systems.map(system => <div className="row" key={system.title}><div className="t"><b>{system.title}</b><span>{system.meta}</span></div><span className="tag" style={system.tone ? { '--tc': system.tone } as React.CSSProperties : undefined}>{system.tag}</span></div>)}</div></div>
</div>
</div>
<div className="finisher glass"><small></small>{FINISHES.map(item => <button key={item.id} type="button" className={finish === item.id ? 'on' : ''} onClick={() => pickFinish(item.id)}>{item.name}</button>)}</div>
</div>
}

View file

@ -0,0 +1,163 @@
/* ═══ 星湖套 v1.3 · 语言世界首页视觉层(作用域 .starlake-scene不污染功能层═══ */
.starlake-scene { position:fixed; inset:0; z-index:0; pointer-events:none; overflow:hidden;
font-family:"PingFang SC","Hiragino Sans GB","Source Han Sans SC",system-ui,sans-serif;
background:linear-gradient(180deg, #04060e 0%, #0a1128 34%, #0e1532 56%, #0b1129 74%, #060a18 100%); }
/* 天幕:银河带 + 星屑 */
.starlake-scene .milky { position:absolute; inset:-12%; transform:rotate(-14deg);
background:
linear-gradient(180deg, transparent 30%, rgba(148,158,222,.10) 42%, rgba(196,186,238,.16) 47%,
rgba(148,158,222,.09) 54%, transparent 66%),
linear-gradient(180deg, transparent 40%, rgba(240,230,255,.07) 47%, transparent 55%);
filter:blur(1.5px); }
.starlake-scene .stars i, .starlake-scene .lakeStars i { position:absolute; border-radius:50%; background:#cfdcff; opacity:var(--go);
animation:sl-glint var(--gt) ease-in-out infinite; }
@keyframes sl-glint { 0%,100%{opacity:var(--go)} 50%{opacity:calc(var(--go) * .2)} }
/* 星湖:天幕向下熔成的镜 */
.starlake-scene .lake { position:absolute; left:0; right:0; top:62vh; bottom:0; overflow:hidden;
background:linear-gradient(180deg, transparent 0%, rgba(9,13,32,.42) 16%, rgba(6,9,24,.78) 44%, rgba(4,6,15,.94) 100%); }
.starlake-scene .lake .sheen { position:absolute; left:0; right:0; top:-4vh; height:9vh;
background:linear-gradient(180deg, transparent, rgba(160,178,255,.10) 46%, rgba(255,236,190,.12) 52%, transparent);
filter:blur(2px); }
.starlake-scene .lake .orbit { position:absolute; left:50%; border-radius:50%; border:1px solid rgba(150,172,255,.12); transform:translateX(-50%); }
.starlake-scene .lake .o1 { width:130vw; height:30vh; top:-8vh; }
.starlake-scene .lake .o2 { width:92vw; height:19vh; top:-4vh; border-color:rgba(150,172,255,.09); }
.starlake-scene .lake .o3 { width:60vw; height:11vh; top:-1.5vh; border-color:rgba(150,172,255,.07); }
/* 时间色调层(系统感知自动驱动) */
.starlake-scene .tint { position:absolute; inset:0; mix-blend-mode:soft-light; opacity:.5; transition:opacity 1.2s ease, background 1.2s ease; }
.starlake-scene .tint.dawn { background:linear-gradient(180deg, rgba(255,176,128,.5), rgba(255,148,120,.3) 55%, transparent 80%); }
.starlake-scene .tint.day { background:radial-gradient(120% 70% at 50% 0%, rgba(214,232,255,.46), transparent 70%); }
.starlake-scene .tint.dusk { background:linear-gradient(180deg, rgba(255,140,96,.42), rgba(214,96,120,.28) 52%, transparent 80%); }
.starlake-scene .tint.night{ background:linear-gradient(180deg, rgba(20,30,70,.44), rgba(8,12,30,.3) 60%, transparent); }
.starlake-scene .weather-veil { position:absolute; inset:0; z-index:3; opacity:0; transition:opacity 1.4s ease;
background:linear-gradient(180deg, rgba(14,18,30,.5), rgba(10,14,24,.42)); }
.starlake-scene .rainfall { position:absolute; inset:-12% -6%; z-index:3; opacity:0; transform:rotate(7deg); transition:opacity 1.4s ease; }
.starlake-scene .rainfall i { position:absolute; top:-14%; width:1px; height:10vh; background:linear-gradient(180deg, transparent, rgba(186,206,255,.34)); animation:sl-rain-fall linear infinite; }
.starlake-scene.rain .weather-veil, .starlake-scene.rain .rainfall { opacity:1; }
.starlake-scene.rain .glow { opacity:.6; }
@keyframes sl-rain-fall { to { transform:translateY(130vh); } }
/* 光均匀洒在整个光湖 */
.starlake-scene .glow { position:absolute; z-index:2; width:150vmax; height:150vmax; left:0; top:0;
mix-blend-mode:screen; transform:translate(-50%,-50%);
background:radial-gradient(circle, var(--glow-tint, rgba(255,236,180,.12)) 0%, transparent 42%); }
/* 光源点:天空最亮一粒星 */
.starlake-scene .source { position:absolute; z-index:8; width:0; height:0; cursor:pointer; will-change:left,top; pointer-events:auto; }
.starlake-scene .source .src-core { position:absolute; left:-8px; top:-8px; width:16px; height:16px; border-radius:50%;
background:radial-gradient(circle at 42% 38%, #fff, var(--src-mid, #ffe9a8) 58%, transparent 100%);
box-shadow:0 0 14px 3px var(--src-g1, rgba(255,236,180,.7)),
0 0 44px 14px var(--src-g2, rgba(255,220,140,.26)),
0 0 110px 42px var(--src-g3, rgba(255,220,140,.12));
animation:sl-src-breathe 7s ease-in-out infinite; }
@keyframes sl-src-breathe { 0%,100%{transform:scale(1)} 50%{transform:scale(1.1)} }
/* 官方门面:主标软著全称 + 副行技术名 */
.starlake-scene .masthead { position:absolute; top:7.2vh; left:0; right:0; text-align:center; }
.starlake-scene .masthead small { display:block; color:#f7ebc8; font-size:15px; font-weight:700;
letter-spacing:.3em; text-indent:.3em; text-shadow:0 1px 10px rgba(0,0,0,.7); }
.starlake-scene .masthead b { display:block; color:rgba(234,238,248,.96); font-size:clamp(25px, calc(42px * var(--sl-scale)), 54px); font-weight:800;
margin-top:2.6vh; letter-spacing:.12em; text-indent:.12em; text-shadow:0 2px 26px rgba(0,0,0,.7); }
.starlake-scene .masthead span { display:block; margin-top:1.8vh; color:#f7ebc8; font-size:14px;
font-weight:700; letter-spacing:.42em; text-indent:.42em; text-shadow:0 1px 10px rgba(0,0,0,.7); }
/* 右侧轻文字:今日光湖历 */
.starlake-scene .hud { position:absolute; right:32px; top:29vh; z-index:7; text-align:right; }
.starlake-scene .hud .h-top small { color:rgba(196,206,228,.62); font-size:11px; font-weight:650; letter-spacing:.34em; }
.starlake-scene .hud .h-top b { display:block; margin-top:7px; color:#f7ebc8; font-size:24px; font-weight:760;
letter-spacing:.08em; font-variant-numeric:tabular-nums; text-shadow:0 0 18px rgba(247,235,200,.35); }
.starlake-scene .hud .h-row { margin-top:7px; color:rgba(196,206,228,.62); font-size:12.5px; font-weight:600;
letter-spacing:.1em; font-variant-numeric:tabular-nums; text-shadow:0 1px 8px rgba(0,0,0,.7); }
.starlake-scene .hud .h-row em { font-style:normal; color:rgba(222,228,242,.88); margin-left:8px; }
.starlake-scene .hud .h-wx { margin-top:7px; color:rgba(222,228,242,.88); font-size:12.5px; font-weight:650;
letter-spacing:.14em; text-shadow:0 1px 8px rgba(0,0,0,.7); }
.starlake-scene .hud .h-wx i { font-style:normal; color:#f7ebc8; }
.starlake-scene .hud .h-link { display:inline-block; margin-top:13px; pointer-events:auto; cursor:pointer;
color:rgba(180,192,220,.4); font-size:12px; font-weight:650; letter-spacing:.26em;
background:transparent; border:0; border-bottom:1px solid rgba(202,216,255,.13);
padding-bottom:4px; transition:color .18s ease, border-color .18s ease; }
.starlake-scene .hud .h-link:hover { color:#f7ebc8; border-color:rgba(247,235,200,.5); }
/* 五域:成长天体五相,悬浮星湖 */
.starlake-scene .domain { position:absolute; z-index:6; width:150px; padding:0; border:0; text-align:center; cursor:pointer; pointer-events:auto; color:inherit; background:transparent; font:inherit; }
.starlake-scene .d-body { position:relative; display:block; margin:0 auto; }
.starlake-scene .d-core { position:absolute; left:50%; top:50%; border-radius:50%; transform:translate(-50%,-50%);
background:
radial-gradient(circle at 32% 26%, rgba(255,255,255,.95) 0%, rgba(255,255,255,.16) 22%, transparent 34%),
radial-gradient(circle at 50% 50%, var(--d-hi), var(--d-lo) 84%);
box-shadow:inset -5px -7px 12px rgba(0,0,0,.44), inset 3px 5px 10px rgba(255,255,255,.34),
0 0 var(--d-glow-r) var(--d-glow), 0 0 calc(var(--d-glow-r) * 1.9) var(--d-glow-soft);
animation:sl-core-breathe 6s ease-in-out infinite; }
@keyframes sl-core-breathe { 0%,100%{transform:translate(-50%,-50%) scale(1)} 50%{transform:translate(-50%,-50%) scale(1.045)} }
.starlake-scene .d-halo { position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); border-radius:50%;
background:radial-gradient(circle, var(--d-glow-soft), transparent 66%); opacity:.8; }
.starlake-scene .d-water { display:block; margin:-4px auto 0; border-radius:50%;
background:radial-gradient(ellipse, var(--d-glow) 0%, transparent 68%);
filter:blur(5px); opacity:.5; animation:sl-mirror-breathe 6s ease-in-out infinite; }
.starlake-scene .d-mirror { display:block; margin:2px auto 0; border-radius:50%;
background:radial-gradient(circle, var(--d-glow), transparent 70%);
filter:blur(4px); opacity:.6; animation:sl-mirror-breathe 6s ease-in-out infinite; }
@keyframes sl-mirror-breathe { 0%,100%{opacity:.44;transform:scaleX(1)} 50%{opacity:.66;transform:scaleX(1.12)} }
.starlake-scene .d-ring { position:absolute; left:50%; top:50%; border-radius:50%; border:1px solid var(--d-ring-c);
transform:translate(-50%,-50%) rotateX(64deg); animation:sl-ring-turn 26s linear infinite; }
@keyframes sl-ring-turn { from{transform:translate(-50%,-50%) rotateX(64deg) rotate(0)} to{transform:translate(-50%,-50%) rotateX(64deg) rotate(360deg)} }
.starlake-scene .d-ring::before { content:''; position:absolute; top:-2.5px; left:50%; width:5px; height:5px;
margin-left:-2.5px; border-radius:50%; background:var(--d-hi); box-shadow:0 0 8px var(--d-glow); }
.starlake-scene .d-sat-arm { position:absolute; left:50%; top:50%; width:0; height:0; animation:sl-sat-orbit 17s linear infinite; }
@keyframes sl-sat-orbit { from{transform:rotate(0)} to{transform:rotate(360deg)} }
.starlake-scene .d-sat { position:absolute; left:var(--d-sat-r); top:0; width:7px; height:7px; margin:-3.5px 0 0 -3.5px;
border-radius:50%; background:radial-gradient(circle at 34% 30%, #fff, var(--d-lo)); box-shadow:0 0 8px var(--d-glow); }
.starlake-scene .d-dust { position:absolute; left:50%; top:50%; width:0; height:0; }
.starlake-scene .d-dust i { position:absolute; width:3px; height:3px; margin:-1.5px; border-radius:50%;
background:var(--d-hi); box-shadow:0 0 6px var(--d-glow); animation:sl-dust-fall var(--dust-t) linear infinite; }
@keyframes sl-dust-fall {
0% { transform:rotate(var(--dust-a)) translateX(var(--dust-r0)); opacity:0; }
12% { opacity:.9; } 88% { opacity:.75; }
100% { transform:rotate(calc(var(--dust-a) + 300deg)) translateX(3px); opacity:0; } }
.starlake-scene .d-label { display:block; margin-top:calc(10px * var(--sl-scale)); color:rgba(222,228,242,.88); font-size:clamp(12px, calc(16px * var(--sl-scale)), 20px); font-weight:720;
letter-spacing:.3em; text-indent:.3em; transition:color .18s ease; text-shadow:0 1px 10px rgba(0,0,0,.8); }
.starlake-scene .domain:hover .d-label { color:rgba(234,238,248,.96); }
.starlake-scene .d-stage { display:block; margin-top:calc(5px * var(--sl-scale)); color:rgba(196,206,228,.62); font-size:clamp(9px, calc(12px * var(--sl-scale)), 15px); letter-spacing:.28em; text-indent:.28em;
opacity:0; transition:opacity .25s ease; text-shadow:0 1px 8px rgba(0,0,0,.8); }
.starlake-scene .domain:hover .d-stage { opacity:1; }
.starlake-scene .domain.on-duty .d-label { color:#f7ebc8; }
.starlake-scene .domain.on-duty .d-stage { opacity:1; color:rgba(247,235,200,.72); }
.starlake-scene .domain.open .d-label { color:#f7ebc8; }
.starlake-scene .channel-entry { position:absolute; left:24px; bottom:25px; z-index:9; pointer-events:auto; cursor:pointer;
padding:9px 15px; border:1px solid rgba(202,216,255,.13); border-radius:999px; color:rgba(222,228,242,.78);
background:rgba(8,12,28,.44); backdrop-filter:blur(16px); font:650 12px/1.2 inherit; letter-spacing:.08em; }
.starlake-scene .channel-entry:hover { color:#f7ebc8; border-color:rgba(247,235,200,.42); }
.starlake-scene .era-foot { position:absolute; left:50%; bottom:21px; transform:translateX(-50%); color:rgba(180,192,220,.34);
font-size:10px; font-weight:650; letter-spacing:.28em; text-indent:.28em; }
.surface-pill { position:fixed; top:8px; left:18px; z-index:120; display:flex; gap:3px; padding:5px; border-radius:999px;
background:rgba(10,14,28,.55); backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px);
border:1px solid rgba(255,255,255,.14); box-shadow:inset 0 1px 0 rgba(255,255,255,.12),0 10px 30px rgba(0,0,0,.35); }
.surface-pill button { border:0; border-radius:999px; padding:7px 18px; color:rgba(226,232,248,.6); background:transparent;
cursor:pointer; font:700 12px/1.2 "PingFang SC","Hiragino Sans GB","Source Han Sans SC",system-ui,sans-serif; letter-spacing:.14em; }
.surface-pill button:hover { color:rgba(238,242,252,.9); }
.surface-pill button.on { color:#f7ebc8; background:rgba(255,255,255,.11); }
.starlake-host > .surface-pill,
.trad-surface ~ .surface-pill { top:12px; }
@media (max-width: 900px) {
.starlake-scene .masthead b { font-size:clamp(24px, 5vw, 36px); }
.starlake-scene .masthead span { font-size:10px; }
.starlake-scene .hud { right:18px; top:25vh; transform:scale(.86); transform-origin:top right; }
.starlake-scene .era-foot { display:none; }
}
.starlake-scene .d-core,.starlake-scene .d-ring,.starlake-scene .d-sat-arm,.starlake-scene .d-dust i,
.starlake-scene .d-mirror,.starlake-scene .d-water,.starlake-scene .source .src-core,
.starlake-scene .stars i,.starlake-scene .lakeStars i,.starlake-scene .rainfall i { animation-play-state:paused; }
.starlake-scene.awake .d-core,.starlake-scene.awake .d-ring,.starlake-scene.awake .d-sat-arm,.starlake-scene.awake .d-dust i,
.starlake-scene.awake .d-mirror,.starlake-scene.awake .d-water,.starlake-scene.awake .source .src-core,
.starlake-scene.awake .stars i,.starlake-scene.awake .lakeStars i,.starlake-scene.awake .rainfall i { animation-play-state:running; }
@media (prefers-reduced-motion: reduce) {
.starlake-scene .d-core,.starlake-scene .d-ring,.starlake-scene .d-sat-arm,.starlake-scene .d-dust i,
.starlake-scene .d-mirror,.starlake-scene .d-water,.starlake-scene .source .src-core,
.starlake-scene .stars i,.starlake-scene .lakeStars i,.starlake-scene .rainfall i { animation:none; }
}

View file

@ -0,0 +1,355 @@
/* 传统工作台 · 玻璃地基八式氛围
地基四层深景光斑 玻璃本体 棱线高光/底部反影 颗粒噪点
风格=换氛围令牌玻璃结构不动作用域 .trad-surface不污染语言世界轨 */
.trad-surface {
position:fixed; inset:0; z-index:60; overflow:hidden;
font-family:"PingFang SC","Hiragino Sans GB","Source Han Sans SC",system-ui,sans-serif;
color:var(--ink); -webkit-font-smoothing:antialiased;
background:var(--bg-base); transition:background .5s ease;
}
.trad-surface.stack { overflow:auto; }
/* ── 八式氛围令牌 ── */
.trad-surface,
.official-world.surface-traditional {
--bg-base:#0a0e1f; --bg-mid:#131a38;
--glow-a:rgba(88,110,255,.5); --glow-b:rgba(190,90,255,.34); --glow-c:rgba(60,220,255,.3);
--accent:#8fb5ff; --accent2:#c9a3ff; --good:#7ce8c0; --warn:#ffc46b; --bad:#ff8fab;
--ink:#eef1ff; --ink-sub:rgba(238,241,255,.62); --ink-faint:rgba(238,241,255,.36);
--glass-hi:rgba(255,255,255,.1); --glass-lo:rgba(255,255,255,.028);
--glass-edge:rgba(255,255,255,.16); --glass-inner:rgba(255,255,255,.22);
}
.trad-surface[data-finish="champagne"],
.official-world.surface-traditional[data-finish="champagne"] {
--bg-base:#141008; --bg-mid:#241a0e;
--glow-a:rgba(255,176,80,.42); --glow-b:rgba(255,120,90,.3); --glow-c:rgba(255,220,150,.26);
--accent:#ffcf8a; --accent2:#ff9f7a; --good:#9fe3a8; --warn:#ffd45e; --bad:#ff7e8e;
--ink:#fdf4e6; --ink-sub:rgba(253,244,230,.62); --ink-faint:rgba(253,244,230,.36);
}
.trad-surface[data-finish="jade"],
.official-world.surface-traditional[data-finish="jade"] {
--bg-base:#071410; --bg-mid:#0d241c;
--glow-a:rgba(40,220,170,.4); --glow-b:rgba(70,160,255,.28); --glow-c:rgba(180,255,200,.22);
--accent:#6fe8c4; --accent2:#7fc4ff; --good:#7ce8c0; --warn:#ffd45e; --bad:#ff8fab;
--ink:#eafaf2; --ink-sub:rgba(234,250,242,.62); --ink-faint:rgba(234,250,242,.36);
}
.trad-surface[data-finish="porcelain"],
.official-world.surface-traditional[data-finish="porcelain"] {
--bg-base:#e8edf6; --bg-mid:#f4f7fd;
--glow-a:rgba(120,150,255,.4); --glow-b:rgba(200,130,255,.3); --glow-c:rgba(120,220,255,.32);
--accent:#4a6cf0; --accent2:#a05cf0; --good:#1fa06a; --warn:#d97a06; --bad:#e04a66;
--ink:#1b2340; --ink-sub:rgba(27,35,64,.6); --ink-faint:rgba(27,35,64,.38);
--glass-hi:rgba(255,255,255,.66); --glass-lo:rgba(255,255,255,.3);
--glass-edge:rgba(255,255,255,.8); --glass-inner:rgba(255,255,255,.9);
}
.trad-surface[data-finish="nebula"],
.official-world.surface-traditional[data-finish="nebula"] {
--bg-base:#170b20; --bg-mid:#2b1340;
--glow-a:rgba(255,110,190,.4); --glow-b:rgba(150,90,255,.4); --glow-c:rgba(90,140,255,.3);
--accent:#ff9ad5; --accent2:#b78aff; --good:#7ce8c0; --warn:#ffc46b; --bad:#ff7e8e;
--ink:#fdeef7; --ink-sub:rgba(253,238,247,.62); --ink-faint:rgba(253,238,247,.36);
--glass-hi:rgba(255,190,225,.11); --glass-lo:rgba(255,190,225,.03);
}
.trad-surface[data-finish="abyss"],
.official-world.surface-traditional[data-finish="abyss"] {
--bg-base:#04101c; --bg-mid:#082033;
--glow-a:rgba(0,150,255,.36); --glow-b:rgba(0,220,200,.26); --glow-c:rgba(60,120,255,.3);
--accent:#4fc3ff; --accent2:#39e6c8; --good:#6fe8b8; --warn:#ffd45e; --bad:#ff8fab;
--ink:#e8f6ff; --ink-sub:rgba(232,246,255,.62); --ink-faint:rgba(232,246,255,.36);
--glass-hi:rgba(160,220,255,.1); --glass-lo:rgba(160,220,255,.028);
}
.trad-surface[data-finish="cinnabar"],
.official-world.surface-traditional[data-finish="cinnabar"] {
--bg-base:#150a0a; --bg-mid:#2a1010;
--glow-a:rgba(255,70,60,.34); --glow-b:rgba(255,150,60,.24); --glow-c:rgba(255,90,120,.22);
--accent:#ff6b5e; --accent2:#ffb35e; --good:#8fe3a8; --warn:#ffd45e; --bad:#ff7e8e;
--ink:#fdf0ec; --ink-sub:rgba(253,240,236,.62); --ink-faint:rgba(253,240,236,.36);
--glass-hi:rgba(255,170,150,.09); --glass-lo:rgba(255,170,150,.026);
}
.trad-surface[data-finish="snow"],
.official-world.surface-traditional[data-finish="snow"] {
--bg-base:#e9f1f7; --bg-mid:#f6fbff;
--glow-a:rgba(90,170,255,.4); --glow-b:rgba(120,220,255,.34); --glow-c:rgba(160,140,255,.26);
--accent:#2f7fd6; --accent2:#6aa8ff; --good:#1fa06a; --warn:#d97a06; --bad:#e04a66;
--ink:#17273f; --ink-sub:rgba(23,39,63,.6); --ink-faint:rgba(23,39,63,.38);
--glass-hi:rgba(255,255,255,.7); --glass-lo:rgba(255,255,255,.32);
--glass-edge:rgba(255,255,255,.85); --glass-inner:rgba(255,255,255,.95);
}
/* ── 地基一层:深景 + 氛围光斑 + 星屑 ── */
.trad-surface .sky { position:absolute; inset:0; z-index:0; pointer-events:none;
background:radial-gradient(120% 90% at 50% -10%, var(--bg-mid), var(--bg-base) 70%); }
.trad-surface .sky::before { content:''; position:absolute; inset:0; filter:blur(30px);
background:
radial-gradient(46vw 34vh at 14% 6%, var(--glow-a), transparent 64%),
radial-gradient(40vw 36vh at 88% 18%, var(--glow-b), transparent 62%),
radial-gradient(50vw 30vh at 50% 108%, var(--glow-c), transparent 64%);
animation:trad-breathe 14s ease-in-out infinite alternate; }
@keyframes trad-breathe { from { opacity:.75; transform:scale(1); } to { opacity:1; transform:scale(1.06); } }
.trad-surface .sky::after { content:''; position:absolute; inset:0;
background-image:radial-gradient(1.4px 1.4px at 12% 22%, rgba(255,255,255,.5), transparent 100%),
radial-gradient(1px 1px at 32% 12%, rgba(255,255,255,.42), transparent 100%),
radial-gradient(1.6px 1.6px at 58% 30%, rgba(255,255,255,.34), transparent 100%),
radial-gradient(1px 1px at 74% 10%, rgba(255,255,255,.5), transparent 100%),
radial-gradient(1.3px 1.3px at 90% 42%, rgba(255,255,255,.3), transparent 100%),
radial-gradient(1px 1px at 44% 52%, rgba(255,255,255,.26), transparent 100%); }
.trad-surface[data-finish="porcelain"] .sky::after, .trad-surface[data-finish="snow"] .sky::after { opacity:.5; }
/* 颗粒噪点 */
.trad-surface .grain { position:absolute; inset:0; z-index:1; pointer-events:none; opacity:.05; mix-blend-mode:overlay;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
/* ── 地基二层:玻璃本体 ── */
.trad-surface .glass { position:relative; border-radius:22px;
background:linear-gradient(165deg, var(--glass-hi), var(--glass-lo) 55%);
-webkit-backdrop-filter:blur(26px) saturate(1.5); backdrop-filter:blur(26px) saturate(1.5);
border:1px solid var(--glass-edge);
box-shadow:inset 0 1px 0 var(--glass-inner), 0 18px 50px rgba(3,6,18,.38); }
.trad-surface .glass::before { content:''; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
background:linear-gradient(115deg, rgba(255,255,255,.14), transparent 34%); }
.trad-surface .glass::after { content:''; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
background:radial-gradient(70% 30% at 50% 112%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 70%); }
.trad-surface[data-finish="porcelain"] .glass, .trad-surface[data-finish="snow"] .glass {
box-shadow:inset 0 1px 0 rgba(255,255,255,.9), 0 14px 40px rgba(70,90,150,.16); }
.trad-surface .lbl { color:var(--ink-faint); font-size:10.5px; font-weight:700; letter-spacing:.24em; }
.trad-surface .big { font-size:25px; font-weight:800; color:var(--ink); font-variant-numeric:tabular-nums; letter-spacing:.01em; }
.trad-surface .big em { font-style:normal; font-size:12px; color:var(--ink-sub); font-weight:650; margin-left:8px; letter-spacing:.04em; }
.trad-surface .tag { font-size:10.5px; font-weight:700; letter-spacing:.06em; padding:3.5px 10px; border-radius:999px; flex:none;
color:var(--tc, var(--accent)); background:color-mix(in srgb, var(--tc, var(--accent)) 13%, transparent);
border:1px solid color-mix(in srgb, var(--tc, var(--accent)) 26%, transparent); }
.trad-surface .bars { display:flex; align-items:flex-end; gap:5px; height:46px; margin-top:14px; }
.trad-surface .bars i { flex:1; border-radius:5px 5px 3px 3px;
background:linear-gradient(180deg, color-mix(in srgb, var(--accent2) 66%, transparent), color-mix(in srgb, var(--accent2) 24%, transparent)); }
.trad-surface .bars i.hot { background:linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 34%, transparent)); }
.trad-surface .row { display:flex; align-items:center; gap:11px; padding:11.5px 12px; border-radius:14px;
transition:background .18s ease; }
.trad-surface .row:hover { background:rgba(255,255,255,.06); }
.trad-surface[data-finish="porcelain"] .row:hover, .trad-surface[data-finish="snow"] .row:hover { background:rgba(255,255,255,.55); }
.trad-surface .row .dot { width:8px; height:8px; border-radius:50%; flex:none; box-shadow:0 0 10px 1px currentColor; }
.trad-surface .row .t { flex:1; min-width:0; }
.trad-surface .row .t b { display:block; font-size:13.5px; font-weight:700; color:var(--ink); letter-spacing:.02em; }
.trad-surface .row .t span { display:block; margin-top:2px; color:var(--ink-faint); font-size:11.5px; font-weight:550; }
.trad-surface .row .num { flex:none; color:var(--ink-sub); font-size:12px; font-weight:650; font-variant-numeric:tabular-nums; }
/* ── 布局:悬浮导航 + 两列 ── */
.trad-surface .stage { position:relative; z-index:2; height:100%; display:flex; flex-direction:column;
padding:var(--pad, 18px) calc(var(--pad, 18px) + 4px); gap:var(--gap, 14px); max-width:1320px; margin:0 auto; }
.trad-surface:not(.stack) .stage { padding-top:max(62px, var(--pad, 18px)); }
.trad-surface.stack .stage { height:auto; min-height:100%; }
.trad-surface .nav { display:flex; align-items:center; gap:20px; padding:11px 20px; border-radius:999px; flex:none; }
.trad-surface .nav .brand { font-size:15.5px; font-weight:800; letter-spacing:.16em; color:var(--ink); }
.trad-surface .nav .brand small { margin-left:9px; color:var(--ink-faint); font-size:9.5px; font-weight:700; letter-spacing:.22em; }
.trad-surface .nav .tabs { display:flex; gap:3px; margin-left:8px; }
.trad-surface .nav .tabs button { border:0; background:transparent; color:var(--ink-sub); font-size:13px; font-weight:650;
letter-spacing:.05em; padding:7px 15px; border-radius:999px; cursor:pointer;
transition:color .18s ease, background .18s ease; }
.trad-surface .nav .tabs button:hover { color:var(--ink); }
.trad-surface .nav .tabs button.on { color:var(--ink); background:rgba(255,255,255,.1); }
.trad-surface[data-finish="porcelain"] .nav .tabs button.on, .trad-surface[data-finish="snow"] .nav .tabs button.on { background:rgba(255,255,255,.7); }
.trad-surface .nav .me { margin-left:auto; display:flex; align-items:center; gap:9px; color:var(--ink-sub); font-size:12px; font-weight:600; }
.trad-surface .nav .me i { width:27px; height:27px; border-radius:50%; display:grid; place-items:center; font-style:normal;
font-size:10px; font-weight:800; color:#0a0e1f;
background:linear-gradient(135deg, var(--accent), var(--accent2));
box-shadow:0 0 14px color-mix(in srgb, var(--accent) 50%, transparent); }
.trad-surface .nav .back { border:1px solid var(--glass-edge); background:transparent; color:var(--ink-sub);
font-size:11.5px; font-weight:700; letter-spacing:.08em; padding:6px 13px; border-radius:999px; cursor:pointer;
transition:color .18s ease, border-color .18s ease; }
.trad-surface .nav .back:hover { color:var(--ink); border-color:color-mix(in srgb, var(--accent) 55%, var(--glass-edge)); }
.trad-surface .grid { flex:1; min-height:0; display:grid; gap:var(--gap, 14px);
grid-template-columns:var(--cols, 1.28fr 1fr); grid-template-rows:1fr 1fr; }
.trad-surface.stack .grid { grid-template-columns:1fr; grid-template-rows:none; }
.trad-surface.stack .hero { grid-row:auto; min-height:500px; }
.trad-surface.stack .card { min-height:250px; }
.trad-surface.stack .nav { flex-wrap:wrap; border-radius:26px; }
.trad-surface .hero { grid-row:span 2; padding:26px 28px; display:flex; flex-direction:column; overflow:hidden; }
.trad-surface .hero .topline { display:flex; justify-content:space-between; align-items:baseline; }
.trad-surface .hero .day { margin-top:14px; font-size:clamp(38px, 5.2vw, 60px); font-weight:850; letter-spacing:.02em; color:var(--ink);
font-variant-numeric:tabular-nums; text-shadow:0 0 40px color-mix(in srgb, var(--accent) 32%, transparent); }
.trad-surface .hero .sub { margin-top:8px; color:var(--ink-sub); font-size:13px; font-weight:600; letter-spacing:.06em; }
.trad-surface .hero .sub b { color:var(--accent); font-weight:750; }
.trad-surface .hero .clock { margin-top:22px; font-size:15px; color:var(--ink-sub); font-weight:650;
letter-spacing:.14em; font-variant-numeric:tabular-nums; }
.trad-surface .hero .stats { margin-top:auto; display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.trad-surface .hero .stat { border-radius:16px; padding:14px 16px;
background:rgba(255,255,255,.055); border:1px solid rgba(255,255,255,.09); }
.trad-surface[data-finish="porcelain"] .hero .stat, .trad-surface[data-finish="snow"] .hero .stat {
background:rgba(255,255,255,.5); border-color:rgba(255,255,255,.75); }
.trad-surface .hero .stat .big { font-size:21px; margin-top:7px; }
/* ── 世界广播 ── */
.trad-surface .broadcast { margin-top:24px; flex:1; min-height:0; display:flex; flex-direction:column; overflow:hidden; }
.trad-surface .broadcast .hd { display:flex; justify-content:space-between; align-items:center; }
.trad-surface .broadcast .live { display:flex; align-items:center; gap:6px; color:var(--good);
font-size:10px; font-weight:700; letter-spacing:.2em; }
.trad-surface .broadcast .live i { width:6px; height:6px; border-radius:50%; background:var(--good);
box-shadow:0 0 9px var(--good); animation:trad-pulse 2.2s ease-in-out infinite; }
@keyframes trad-pulse { 0%,100% { opacity:1; } 50% { opacity:.3; } }
.trad-surface .broadcast .feed { margin-top:11px; flex:1; min-height:0; overflow:hidden; display:flex; flex-direction:column; gap:7px; }
.trad-surface .b-item { display:flex; align-items:baseline; gap:10px; padding:10px 13px; border-radius:13px;
background:rgba(255,255,255,.045); border:1px solid rgba(255,255,255,.07); }
.trad-surface.awake .b-item { animation:trad-bin .55s ease; }
.trad-surface[data-finish="porcelain"] .b-item, .trad-surface[data-finish="snow"] .b-item {
background:rgba(255,255,255,.5); border-color:rgba(255,255,255,.7); }
@keyframes trad-bin { from { opacity:0; transform:translateY(-9px); } to { opacity:1; transform:none; } }
.trad-surface .b-item .d { flex:none; font-size:11px; font-weight:750; letter-spacing:.08em; color:var(--bc); }
.trad-surface .b-item .d::before { content:''; display:inline-block; width:7px; height:7px; border-radius:50%;
background:var(--bc); box-shadow:0 0 8px var(--bc); margin-right:7px; vertical-align:1px; }
.trad-surface .b-item .m { flex:1; min-width:0; color:var(--ink-sub); font-size:12.5px; font-weight:550;
letter-spacing:.02em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.trad-surface .b-item .tm { flex:none; color:var(--ink-faint); font-size:10.5px; font-weight:650;
letter-spacing:.08em; font-variant-numeric:tabular-nums; }
.trad-surface .card { padding:16px; display:flex; flex-direction:column; min-height:0; overflow:hidden; }
.trad-surface .card .hd { display:flex; justify-content:space-between; align-items:center; padding:2px 12px 10px; }
.trad-surface .card .rows { flex:1; min-height:0; overflow:auto; }
/* ── 氛围切换器 ── */
.trad-surface .finisher { position:absolute; right:22px; bottom:18px; z-index:9; display:flex; gap:3px; padding:5px; border-radius:999px; }
.trad-surface .finisher small { align-self:center; color:var(--ink-faint); font-size:9.5px; font-weight:700;
letter-spacing:.2em; padding:0 6px 0 10px; }
.trad-surface .finisher button { border:0; background:transparent; color:var(--ink-sub); font-size:11.5px; font-weight:700;
letter-spacing:.08em; padding:6px 13px; border-radius:999px; cursor:pointer;
transition:color .18s ease, background .18s ease; }
.trad-surface .finisher button:hover { color:var(--ink); }
.trad-surface .finisher button.on { color:var(--ink); background:rgba(255,255,255,.12); }
.trad-surface .nav .back { display:none; }
.trad-surface[data-finish="porcelain"] .finisher button.on, .trad-surface[data-finish="snow"] .finisher button.on { background:rgba(255,255,255,.7); }
/* ── 语言世界侧入口按钮world-footer 内) ── */
.world-footer .surface-switch { margin-left:auto; border:1px solid currentColor; background:transparent;
color:inherit; font-size:11px; font-weight:700; letter-spacing:.14em; padding:5px 14px; border-radius:999px;
cursor:pointer; opacity:.75; transition:opacity .18s ease; }
.world-footer .surface-switch:hover { opacity:1; }
/* 轻量运行:只有用户正在指向或操作软件时,氛围与广播才继续动。 */
.trad-surface .sky::before,
.trad-surface .broadcast .live i,
.trad-surface .b-item { animation-play-state:paused; }
.trad-surface.awake .sky::before,
.trad-surface.awake .broadcast .live i,
.trad-surface.awake .b-item { animation-play-state:running; }
/* 传统轨不是单页皮肤。下游知识库、频道、教育与网文工作台统一继承同一组八式令牌。 */
.official-world.surface-traditional {
--primitive-bg-a:var(--bg-base);
--primitive-bg-b:color-mix(in srgb,var(--bg-base) 72%,var(--bg-mid));
--primitive-bg-c:var(--bg-mid);
--primitive-bg-d:color-mix(in srgb,var(--bg-base) 86%,#02040b);
--surface-sky:var(--bg-base);
--surface-horizon:var(--bg-mid);
--surface-depth:color-mix(in srgb,var(--bg-base) 90%,#02040b);
--surface-panel:color-mix(in srgb,var(--glass-hi) 58%,var(--bg-mid));
--surface-strong:color-mix(in srgb,var(--glass-hi) 72%,var(--bg-mid));
--content-primary:var(--ink);
--content-secondary:var(--ink-sub);
--content-muted:var(--ink-faint);
--content-faint:var(--ink-faint);
--text-primary:var(--ink);
--text-secondary:var(--ink-sub);
--text-muted:var(--ink-faint);
--panel-edge:var(--glass-edge);
--panel-bg:color-mix(in srgb,var(--glass-hi) 54%,var(--bg-base));
--primitive-glass:var(--glass-hi);
--primitive-glass-edge:var(--glass-edge);
--button-primary-bg:var(--accent);
--button-primary-text:var(--bg-base);
--button-secondary-bg:var(--glass-hi);
--button-secondary-text:var(--ink);
--button-secondary-edge:var(--glass-edge);
--accent-light:var(--accent);
--primitive-cool-glow:var(--accent);
--primitive-warm-glow:var(--accent2);
--state-ready:var(--good);
--state-warning:var(--warn);
--state-danger:var(--bad);
position:relative;
z-index:0;
color:var(--ink);
background:
radial-gradient(46vw 34vh at 14% 6%,var(--glow-a),transparent 64%),
radial-gradient(40vw 36vh at 88% 18%,var(--glow-b),transparent 62%),
radial-gradient(50vw 30vh at 50% 108%,var(--glow-c),transparent 64%),
radial-gradient(120% 90% at 50% -10%,var(--bg-mid),var(--bg-base) 70%);
}
.official-world.surface-traditional .world-titlebar,
.official-world.surface-traditional .world-tool,
.official-world.surface-traditional .education-workspace-world,
.official-world.surface-traditional .composition-workspace-world,
.official-world.surface-traditional .wn-workspace,
.official-world.surface-traditional .mobile-sync-world,
.official-world.surface-traditional .persona-body-world {
background:linear-gradient(165deg,var(--glass-hi),var(--glass-lo) 55%);
border-color:var(--glass-edge);
box-shadow:inset 0 1px 0 var(--glass-inner),0 18px 50px rgba(3,6,18,.28);
-webkit-backdrop-filter:blur(26px) saturate(1.5);
backdrop-filter:blur(26px) saturate(1.5);
}
.official-world.surface-traditional .world-scene,
.official-world.surface-traditional .tool-projection,
.official-world.surface-traditional .content-page,
.official-world.surface-traditional .plain-panel,
.official-world.surface-traditional .knowledge-workspace,
.official-world.surface-traditional .channel-workbench,
.official-world.surface-traditional .wn-workspace,
.official-world.surface-traditional .education-workspace {
color:var(--ink);
}
.official-world.surface-traditional .plain-panel,
.official-world.surface-traditional .content-page,
.official-world.surface-traditional .education-panel,
.official-world.surface-traditional .wn-panel,
.official-world.surface-traditional .workbench-panel {
background:linear-gradient(165deg,var(--glass-hi),var(--glass-lo) 55%);
border-color:var(--glass-edge);
}
.official-world.surface-traditional .world-pool {
border:1px solid var(--glass-edge);
border-radius:18px;
background:linear-gradient(165deg,var(--glass-hi),var(--glass-lo) 55%);
box-shadow:inset 0 1px 0 var(--glass-inner),0 14px 38px rgba(3,6,18,.24);
}
.official-world.surface-traditional .world-pool .pool-bay { display:none; }
.official-world.surface-traditional .world-pool .pool-label { top:50%; transform:translate(-50%,-50%); gap:7px; }
.official-world.surface-traditional .world-pool.open {
border-color:color-mix(in srgb,var(--accent) 60%,var(--glass-edge));
box-shadow:inset 0 1px 0 var(--glass-inner),0 0 34px color-mix(in srgb,var(--accent) 18%,transparent);
}
.official-world.qoder-home .world-footer { display:none; }
.official-world.surface-traditional[data-tone="light"] .world-titlebar,
.official-world.surface-traditional[data-tone="light"] .tool-worldbar {
box-shadow:0 1px 0 rgba(27,35,64,.1);
}
.official-world.surface-traditional:not(.motion-awake) .world-titlebar,
.official-world.surface-traditional:not(.motion-awake) .world-tool,
.official-world.surface-traditional:not(.motion-awake) .education-workspace-world,
.official-world.surface-traditional:not(.motion-awake) .composition-workspace-world,
.official-world.surface-traditional:not(.motion-awake) .wn-workspace,
.official-world.surface-traditional:not(.motion-awake) .mobile-sync-world,
.official-world.surface-traditional:not(.motion-awake) .persona-body-world {
-webkit-backdrop-filter:none;
backdrop-filter:none;
}
.global-finish-rail {
position:fixed; z-index:121; right:18px; bottom:13px; display:flex; flex-wrap:wrap; max-width:calc(100vw - 36px);
gap:3px; padding:5px; border:1px solid var(--glass-edge); border-radius:999px;
background:color-mix(in srgb,var(--bg-base) 72%,transparent); box-shadow:inset 0 1px 0 var(--glass-inner),0 10px 30px rgba(0,0,0,.25);
-webkit-backdrop-filter:blur(18px); backdrop-filter:blur(18px);
}
.global-finish-rail button { border:0; border-radius:999px; padding:5px 10px; color:var(--ink-sub); background:transparent;
cursor:pointer; font:700 10.5px/1.2 "PingFang SC","Hiragino Sans GB","Source Han Sans SC",system-ui,sans-serif; letter-spacing:.08em; }
.global-finish-rail button:hover { color:var(--ink); }
.global-finish-rail button.on { color:var(--ink); background:var(--glass-hi); }
.official-world.surface-traditional:not(.motion-awake) .global-finish-rail { -webkit-backdrop-filter:none; backdrop-filter:none; }
@media (max-width:760px) {
.global-finish-rail { right:12px; bottom:9px; border-radius:18px; }
}
@media (prefers-reduced-motion: reduce) {
.trad-surface .sky::before { animation:none; }
.trad-surface .broadcast .live i { animation:none; }
.trad-surface .b-item { animation:none; }
}

View file

@ -0,0 +1,44 @@
import type { FinishId } from './TraditionalSurface'
interface Rgb { r: number; g: number; b: number }
interface FinishProfile { background: string; foreground: string; tone: 'dark' | 'light' }
export const FINISH_PROFILES: Record<FinishId, FinishProfile> = {
aurora: { background: '#0a0e1f', foreground: '#eef1ff', tone: 'dark' },
nebula: { background: '#170b20', foreground: '#fdeef7', tone: 'dark' },
abyss: { background: '#04101c', foreground: '#e8f6ff', tone: 'dark' },
jade: { background: '#071410', foreground: '#eafaf2', tone: 'dark' },
cinnabar: { background: '#150a0a', foreground: '#fdf0ec', tone: 'dark' },
champagne: { background: '#141008', foreground: '#fdf4e6', tone: 'dark' },
porcelain: { background: '#e8edf6', foreground: '#1b2340', tone: 'light' },
snow: { background: '#e9f1f7', foreground: '#17273f', tone: 'light' },
}
function parseHex(value: string): Rgb {
const normalized = value.replace('#', '')
return { r: parseInt(normalized.slice(0, 2), 16), g: parseInt(normalized.slice(2, 4), 16), b: parseInt(normalized.slice(4, 6), 16) }
}
function luminance({ r, g, b }: Rgb) {
const channel = (value: number) => {
const normalized = value / 255
return normalized <= .03928 ? normalized / 12.92 : Math.pow((normalized + .055) / 1.055, 2.4)
}
return .2126 * channel(r) + .7152 * channel(g) + .0722 * channel(b)
}
export function contrastRatio(foreground: string, background: string) {
const a = luminance(parseHex(foreground))
const b = luminance(parseHex(background))
return (Math.max(a, b) + .05) / (Math.min(a, b) + .05)
}
export function resolveVisualBalance(finish: FinishId, phase?: string, weather?: string) {
const profile = FINISH_PROFILES[finish]
return {
tone: profile.tone,
contrast: contrastRatio(profile.foreground, profile.background),
phase: phase?.toLowerCase() || 'time-pending',
weather: weather?.toLowerCase() || 'unavailable',
}
}

View file

@ -571,6 +571,7 @@ export function WebNovelWorkspace({ onBack }: { onBack: () => void }) {
dirtyRef.current = false dirtyRef.current = false
const next = await invoke<WorkDetail>('read_web_novel_work', { input: { workId: detail.work.workId } }) const next = await invoke<WorkDetail>('read_web_novel_work', { input: { workId: detail.work.workId } })
setDetail(next) setDetail(next)
await refreshSnapshot(detail.work.workId)
setMode('writing') setMode('writing')
setChapterCreate(null) setChapterCreate(null)
setMessage('章节已创建,正文会自动保存并生成版本。') setMessage('章节已创建,正文会自动保存并生成版本。')