feat: admit signed mobile sync bridge module
This commit is contained in:
parent
eb7223ea4c
commit
292889f934
21 changed files with 1891 additions and 15 deletions
|
|
@ -9,6 +9,7 @@ const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').
|
|||
const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody })))
|
||||
const EducationWorkspace = lazy(() => import('./modules/education-workspace').then((module) => ({ default: module.EducationWorkspace })))
|
||||
const WebNovelWorkspace = lazy(() => import('./modules/web-novel/WebNovelWorkspace').then((module) => ({ default: module.WebNovelWorkspace })))
|
||||
const MobileSyncPanel = lazy(() => import('./modules/mobile-sync').then((module) => ({ default: module.MobileSyncPanel })))
|
||||
|
||||
const TAG_TINTS = ['tag-lavender', 'tag-sky', 'tag-mint', 'tag-amber', 'tag-rose', 'tag-slate']
|
||||
|
||||
|
|
@ -55,7 +56,7 @@ import './design-tokens.css'
|
|||
import './styles.css'
|
||||
|
||||
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
|
||||
type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'webNovel' | 'persona' | 'code' | 'receipts' | 'system'
|
||||
type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'webNovel' | 'mobileSync' | 'persona' | 'code' | 'receipts' | 'system'
|
||||
type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool'
|
||||
type KnowledgeSource = 'native' | 'legacy'
|
||||
|
||||
|
|
@ -369,7 +370,7 @@ const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], a
|
|||
const themes: Array<{ id: ThemeId; name: string }> = [
|
||||
{ id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' },
|
||||
]
|
||||
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', education: '教育工作台', webNovel: '网文作者工作台', persona: '人格频道本体', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
|
||||
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', education: '教育工作台', webNovel: '网文作者工作台', mobileSync: '移动同步桥', persona: '人格频道本体', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
|
||||
const domainGates = [
|
||||
{ domain: 'BRANCH_DOMAIN', className: 'd-sub', title: '光湖分域', gate: 'GATE 02 · ONLINE', facts: [
|
||||
['域标识', 'BRANCH_DOMAIN'], ['责任主体', '花尔 · TCS-GL-0005∞'], ['人格体主体', '爆米花 · PER-BMH001 · AGE'], ['关系支持', '糖星云 · PER-TXY001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'],
|
||||
|
|
@ -615,6 +616,9 @@ function HoloLakeApp() {
|
|||
const [webNovelModule, setWebNovelModule] = useState<BundledModuleDescriptor | null>(null)
|
||||
const [webNovelBusy, setWebNovelBusy] = useState(false)
|
||||
const [webNovelMessage, setWebNovelMessage] = useState('')
|
||||
const [mobileSyncModule, setMobileSyncModule] = useState<BundledModuleDescriptor | null>(null)
|
||||
const [mobileSyncBusy, setMobileSyncBusy] = useState(false)
|
||||
const [mobileSyncMessage, setMobileSyncMessage] = useState('')
|
||||
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['导入']))
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [draft, setDraft] = useState('')
|
||||
|
|
@ -1207,6 +1211,27 @@ function HoloLakeApp() {
|
|||
finally { setWebNovelBusy(false) }
|
||||
}
|
||||
|
||||
const refreshMobileSyncModule = async () => {
|
||||
try {
|
||||
const catalog = await invoke<BundledModuleDescriptor[]>('get_bundled_module_catalog')
|
||||
const module = catalog.find((item) => item.moduleNumber === 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001') || null
|
||||
setMobileSyncModule(module)
|
||||
return module
|
||||
} catch (error) { setMobileSyncMessage(humanError(error, 'system')); return null }
|
||||
}
|
||||
const openMobileSync = () => { openWorldTool('mobileSync'); void refreshMobileSyncModule() }
|
||||
const activateMobileSyncModule = async () => {
|
||||
setMobileSyncBusy(true)
|
||||
setMobileSyncMessage('正在验证官方签名、登记通信编号并确认五项本机网络边界……')
|
||||
try {
|
||||
await invoke('activate_bundled_module', { input: { moduleNumber: 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001', humanConfirmedPermissionExpansion: true } })
|
||||
const module = await refreshMobileSyncModule()
|
||||
if (!module || module.installedState !== 'ACTIVE') throw new Error('HOLOLAKE_MODULE_NOT_ACTIVE')
|
||||
setMobileSyncMessage('移动同步桥已通过签名、编号、权限和自检验收;监听器仍保持关闭,等待本人显式开启。')
|
||||
} catch (error) { setMobileSyncMessage(humanError(error, 'system')) }
|
||||
finally { setMobileSyncBusy(false) }
|
||||
}
|
||||
|
||||
const cloneCodeChannel = async (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (!cloneUrl.trim()) return
|
||||
|
|
@ -1635,6 +1660,20 @@ function HoloLakeApp() {
|
|||
</section>
|
||||
)
|
||||
|
||||
const renderMobileSync = () => (
|
||||
<section className="full-workbench mobile-sync-workbench-world">
|
||||
{mobileSyncModule?.installedState === 'ACTIVE'
|
||||
? <Suspense fallback={<div className="workbench-empty"><p>正在装载移动同步桥……</p></div>}><MobileSyncPanel onBack={() => setWorldStage(toolReturnStage)}/></Suspense>
|
||||
: <div className="workbench-empty">
|
||||
<span>桥</span><h2>启用移动同步桥</h2>
|
||||
<p>模块只在本人确认后开放同一局域网入口;电脑仍是唯一根节点,手机只读取最小投影并把快速记录写入隔离收件箱。</p>
|
||||
<code>HLP-MOD-OFFICIAL-MOBILE-SYNC-0001 · 5 项本机网络权限</code>
|
||||
<button className="primary-button" type="button" disabled={mobileSyncBusy} onClick={() => void activateMobileSyncModule()}>{mobileSyncBusy ? '正在验收官方模块…' : '确认五项边界并启用'}</button>
|
||||
{mobileSyncMessage && <p>{mobileSyncMessage}</p>}
|
||||
</div>}
|
||||
</section>
|
||||
)
|
||||
|
||||
const renderCode = () => (
|
||||
<section className="full-workbench code-workbench">
|
||||
<aside className="code-channels">
|
||||
|
|
@ -1906,6 +1945,7 @@ function HoloLakeApp() {
|
|||
<LakePool className="channel-code" title="资料工作台" meta="签名模块 · 文档与智能表格" onClick={openWorkbench}/>
|
||||
<LakePool className="channel-education" title="教育工作台" meta="官方编号模块 · 教育文档、表格与自动化" onClick={openEducation}/>
|
||||
<LakePool className="channel-main" title="网文作者工作台" meta="官方编号模块 · 码字、设定、编辑与交付" onClick={openWebNovel}/>
|
||||
<LakePool className="channel-mobile" title="移动同步桥" meta="同一人格系统 · 局域网加密入口" onClick={openMobileSync}/>
|
||||
{timeAuthorityModule && <LakePool className="channel-time" title="时间主控" meta={beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay} 天` : '北京时间正在流动'} onClick={openEraTimeline}/>}
|
||||
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
||||
</section>}
|
||||
|
|
@ -1919,7 +1959,7 @@ function HoloLakeApp() {
|
|||
</section>}
|
||||
{worldStage === 'tool' && <section className={`world-tool world-tool-${view}`}>
|
||||
<header className="tool-worldbar"><button type="button" onClick={() => setWorldStage(toolReturnStage)}>← 退回频道全景</button><b>{viewLabels[view]}</b><span>{domainDisplayName(repoLogin.domain)}</span></header>
|
||||
<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 === '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>}
|
||||
</main>
|
||||
<footer className="world-footer"><b>光湖语言系统 · 通用人工智能操作平台</b><span>GH-AIOS</span></footer>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { numberedInvoke as invoke } from '../numbered-ipc'
|
||||
import './styles.css'
|
||||
|
||||
interface PairedDevice {
|
||||
deviceId: string
|
||||
displayName: string
|
||||
platform: string
|
||||
createdAtUnixMs: number
|
||||
lastSeenAtUnixMs?: number
|
||||
state: 'ACTIVE' | 'REVOKED'
|
||||
}
|
||||
|
||||
interface MobileSyncStatus {
|
||||
schema: string
|
||||
state: 'OFFLINE' | 'PAIRING_READY' | 'RUNNING'
|
||||
running: boolean
|
||||
lanAddress?: string
|
||||
port?: number
|
||||
pairingUri?: string
|
||||
pairingQrSvg?: string
|
||||
pairingExpiresAtUnixMs?: number
|
||||
pairedDevices: PairedDevice[]
|
||||
transport: string
|
||||
encryption: string
|
||||
desktopIsRootNode: boolean
|
||||
platformPrivateDataCustody: boolean
|
||||
}
|
||||
|
||||
interface MobileCapture {
|
||||
cursor: number
|
||||
captureId: string
|
||||
title: string
|
||||
body: string
|
||||
sourceDeviceId: string
|
||||
createdAtUnixMs: number
|
||||
}
|
||||
|
||||
interface MobileSyncSnapshot {
|
||||
state: string
|
||||
cursor: number
|
||||
generatedAtUnixMs: number
|
||||
desktopName: string
|
||||
rootNodeOnline: boolean
|
||||
personalChannel: { home: string; growthEventCount: number; integrity: string }
|
||||
webNovel: {
|
||||
workCount: number
|
||||
volumeCount: number
|
||||
chapterCount: number
|
||||
works: Array<{ workId: string; title: string; status: string; updatedAtUnixMs: number }>
|
||||
}
|
||||
education: {
|
||||
activeTableCount: number
|
||||
archivedTableCount: number
|
||||
unassignedTableCount: number
|
||||
sensitiveValuesIncluded: boolean
|
||||
}
|
||||
recentCaptures: MobileCapture[]
|
||||
boundary: {
|
||||
mobileRole: string
|
||||
remoteDesktopClone: boolean
|
||||
desktopOfflineExecution: boolean
|
||||
sensitiveEducationValues: string
|
||||
modelApi: string
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(value?: number) {
|
||||
return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '尚未连接'
|
||||
}
|
||||
|
||||
export function MobileSyncPanel({ onBack }: { onBack: () => void }) {
|
||||
const [status, setStatus] = useState<MobileSyncStatus | null>(null)
|
||||
const [snapshot, setSnapshot] = useState<MobileSyncSnapshot | null>(null)
|
||||
const [message, setMessage] = useState('手机是当前个人节点的远程入口,不是第二套人格系统。')
|
||||
const [pending, setPending] = useState(false)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const next = await invoke<MobileSyncStatus>('get_mobile_sync_status')
|
||||
setStatus(next)
|
||||
if (next.running) setSnapshot(await invoke<MobileSyncSnapshot>('get_mobile_sync_snapshot'))
|
||||
else setSnapshot(null)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
refresh().catch((error) => !cancelled && setMessage(String(error)))
|
||||
return () => { cancelled = true }
|
||||
}, [refresh])
|
||||
|
||||
const run = async (action: () => Promise<unknown>, success: string) => {
|
||||
if (pending) return
|
||||
setPending(true)
|
||||
try {
|
||||
await action()
|
||||
setMessage(success)
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
setMessage(String(error))
|
||||
} finally {
|
||||
setPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
const qrSource = useMemo(
|
||||
() => status?.pairingQrSvg ? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(status.pairingQrSvg)}` : '',
|
||||
[status?.pairingQrSvg],
|
||||
)
|
||||
|
||||
return <section className="mobile-sync-world" aria-label="HoloLake 多端同步">
|
||||
<header className="mobile-sync-worldbar">
|
||||
<button type="button" onClick={onBack}>← 返回我的频道</button>
|
||||
<div><span>HOLOLAKE / SAME PERSONA SYSTEM</span><b>iPhone 多端同步</b></div>
|
||||
<em>{status?.running ? '个人主节点在线' : '同步未开启'}</em>
|
||||
</header>
|
||||
|
||||
<div className="mobile-sync-focus">
|
||||
<div className={`mobile-sync-orbit ${status?.running ? 'is-online' : ''}`} aria-hidden="true"><i/><i/><i/></div>
|
||||
<div><span>手机远程身体入口</span><h1>{status?.running ? '同一频道,正在等待 iPhone' : '由这台电脑守住唯一真相'}</h1><p>桌面保留身份、人格、数据与执行主权;手机只同步经过授权的投影与快速记录。</p></div>
|
||||
</div>
|
||||
|
||||
{!status?.running ? <section className="mobile-sync-start">
|
||||
<b>同一局域网 · 应用层端到端加密</b>
|
||||
<p>开启后只在当前登录账号的数据根建立同步库。平台服务器不托管你的作品、教育数据、人格记忆或设备密钥。</p>
|
||||
<button type="button" disabled={pending} onClick={() => void run(() => invoke('start_mobile_sync'), 'iPhone 同步入口已开启。')}>{pending ? '正在开启…' : '开启 iPhone 同步'}</button>
|
||||
</section> : <div className="mobile-sync-grid">
|
||||
<section className="mobile-sync-pairing">
|
||||
<header><div><span>PAIRING</span><h2>配对这台 iPhone</h2></div><button type="button" disabled={pending} onClick={() => void run(() => invoke('rotate_mobile_pairing'), '已换成新的十分钟配对凭据。')}>换一枚配对凭据</button></header>
|
||||
{status.pairingUri ? <>
|
||||
{qrSource && <img src={qrSource} alt="HoloLake iPhone 配对二维码"/>}
|
||||
<p>用 iPhone 版 HoloLake 扫描。二维码内含一次性高强度配对密钥,十分钟后或使用一次后失效。</p>
|
||||
<button type="button" onClick={() => void navigator.clipboard.writeText(status.pairingUri || '').then(() => setMessage('配对链接已复制。'))}>复制配对链接</button>
|
||||
<small>到期:{formatTime(status.pairingExpiresAtUnixMs)}</small>
|
||||
</> : <div className="mobile-sync-pairing-used"><b>当前配对凭据已使用或到期</b><p>需要添加另一台手机时,再生成一枚新的凭据。</p></div>}
|
||||
</section>
|
||||
|
||||
<section className="mobile-sync-projection">
|
||||
<header><span>ROOT NODE PROJECTION</span><h2>当前同步投影</h2></header>
|
||||
<div className="mobile-sync-metrics">
|
||||
<article><b>{snapshot?.webNovel.workCount ?? 0}</b><span>网文作品</span></article>
|
||||
<article><b>{snapshot?.webNovel.chapterCount ?? 0}</b><span>章节</span></article>
|
||||
<article><b>{snapshot?.education.activeTableCount ?? 0}</b><span>教育表格</span></article>
|
||||
<article><b>{snapshot?.personalChannel.growthEventCount ?? 0}</b><span>成长事件</span></article>
|
||||
</div>
|
||||
<p>教育敏感值:{snapshot?.education.sensitiveValuesIncluded ? '已包含' : '不会进入手机摘要'} · 电脑离线时手机不会冒充执行。</p>
|
||||
</section>
|
||||
|
||||
<section className="mobile-sync-devices">
|
||||
<header><span>DEVICES</span><h2>已配对设备</h2></header>
|
||||
{status.pairedDevices.length ? status.pairedDevices.map((device) => <article key={device.deviceId}>
|
||||
<div><b>{device.displayName}</b><span>{device.platform} · {device.state === 'ACTIVE' ? `最近连接 ${formatTime(device.lastSeenAtUnixMs)}` : '已撤销'}</span></div>
|
||||
{device.state === 'ACTIVE' && <button type="button" disabled={pending} onClick={() => void run(() => invoke('revoke_mobile_sync_device', { input: { deviceId: device.deviceId } }), `已撤销 ${device.displayName}。`)}>撤销</button>}
|
||||
</article>) : <p>还没有完成配对的 iPhone。</p>}
|
||||
</section>
|
||||
|
||||
<section className="mobile-sync-captures">
|
||||
<header><span>MOBILE CAPTURES</span><h2>手机快速记录</h2></header>
|
||||
{snapshot?.recentCaptures.length ? snapshot.recentCaptures.map((capture) => <article key={capture.captureId}><div><b>{capture.title || '未命名记录'}</b><time>{formatTime(capture.createdAtUnixMs)}</time></div><p>{capture.body}</p></article>) : <p>手机写下的想法会加密回到这台电脑,并出现在这里。</p>}
|
||||
</section>
|
||||
</div>}
|
||||
|
||||
<footer className="mobile-sync-footer"><span>{message}</span><div><button type="button" disabled={pending} onClick={() => void run(refresh, '状态已从本机根节点重新读取。')}>刷新状态</button>{status?.running && <button type="button" disabled={pending} onClick={() => void run(() => invoke('stop_mobile_sync'), '同步入口已关闭;已配对设备登记仍保留,可单独撤销。')}>关闭本机同步入口</button>}</div></footer>
|
||||
</section>
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1120,6 +1120,54 @@ const ROUTES = {
|
|||
"moduleNumber": "HLP-NIPC-MOD-0029",
|
||||
"operationNumber": "HLP-NIPC-OP-0140",
|
||||
"targetNumber": "HLP-NIPC-TGT-0029"
|
||||
},
|
||||
"start_mobile_sync": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0030",
|
||||
"operationNumber": "HLP-NIPC-OP-0141",
|
||||
"targetNumber": "HLP-NIPC-TGT-0030"
|
||||
},
|
||||
"get_mobile_sync_status": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0030",
|
||||
"operationNumber": "HLP-NIPC-OP-0142",
|
||||
"targetNumber": "HLP-NIPC-TGT-0030"
|
||||
},
|
||||
"rotate_mobile_pairing": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0030",
|
||||
"operationNumber": "HLP-NIPC-OP-0143",
|
||||
"targetNumber": "HLP-NIPC-TGT-0030"
|
||||
},
|
||||
"stop_mobile_sync": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0030",
|
||||
"operationNumber": "HLP-NIPC-OP-0144",
|
||||
"targetNumber": "HLP-NIPC-TGT-0030"
|
||||
},
|
||||
"revoke_mobile_sync_device": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0030",
|
||||
"operationNumber": "HLP-NIPC-OP-0145",
|
||||
"targetNumber": "HLP-NIPC-TGT-0030"
|
||||
},
|
||||
"get_mobile_sync_snapshot": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0030",
|
||||
"operationNumber": "HLP-NIPC-OP-0146",
|
||||
"targetNumber": "HLP-NIPC-TGT-0030"
|
||||
}
|
||||
} as const
|
||||
|
||||
|
|
|
|||
|
|
@ -681,6 +681,7 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.channel-main .pool-bay { width: 260px; height: 84px; } .channel-main .pool-label { top: 92px; }
|
||||
.channel-knowledge { left: 14%; top: 48%; } .channel-code { left: 31%; top: 61%; } .channel-light { right: 31%; top: 61%; } .channel-status { right: 14%; top: 48%; }
|
||||
.channel-time { left: 50%; bottom: 4%; transform: translateX(-50%); }
|
||||
.channel-mobile { right: 7%; top: 70%; }
|
||||
.private-route-note { position: absolute; z-index: 10; left: 50%; bottom: 11%; width: min(720px, calc(100% - 64px)); margin: 0; transform: translateX(-50%); color: var(--content-muted); text-align: center; font-size: 12.5px; font-weight: 650; letter-spacing: .08em; }
|
||||
.personal-node-guide { position: absolute; z-index: 12; left: 50%; top: 55%; width: min(760px, calc(100% - 72px)); max-height: calc(100% - 190px); overflow: auto; padding: 26px 30px; transform: translate(-50%, -50%); border: 1px solid var(--panel-edge); border-radius: 20px; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 88%, transparent); box-shadow: 0 30px 90px rgba(0, 0, 0, .42); backdrop-filter: blur(22px); }
|
||||
.personal-node-guide header span { color: var(--accent-light); font-size: 11px; font-weight: 750; letter-spacing: .2em; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue