feat: bind public vestibules to domain login gates
This commit is contained in:
parent
2c50b28557
commit
b24d5b5452
6 changed files with 147 additions and 22 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useMemo, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';
|
||||||
import { api, DocTreeNode, DocContent, type ChannelState, type ModuleManifest } from './api';
|
import { api, DocTreeNode, DocContent, type ChannelState, type ModuleManifest } from './api';
|
||||||
import { DocTree } from './components/DocTree';
|
import { DocTree } from './components/DocTree';
|
||||||
import { Editor } from './components/Editor';
|
import { Editor } from './components/Editor';
|
||||||
|
|
@ -13,6 +13,7 @@ import { ModuleLibrarySheet } from './components/ModuleLibrarySheet';
|
||||||
import { cleanDisplayText } from './presentation';
|
import { cleanDisplayText } from './presentation';
|
||||||
import { WorldEntry } from './components/WorldEntry';
|
import { WorldEntry } from './components/WorldEntry';
|
||||||
import { DomainConnectionSheet } from './components/DomainConnectionSheet';
|
import { DomainConnectionSheet } from './components/DomainConnectionSheet';
|
||||||
|
import { createDomainEntryTarget, type DomainEntryTarget, type DomainNodeType } from './domain-entry-state';
|
||||||
import type { DomainRouteId } from './public-domain-directory';
|
import type { DomainRouteId } from './public-domain-directory';
|
||||||
|
|
||||||
type View = 'editor' | 'history';
|
type View = 'editor' | 'history';
|
||||||
|
|
@ -84,6 +85,7 @@ export default function App() {
|
||||||
const [activeModule, setActiveModule] = useState<ModuleId>('knowledge');
|
const [activeModule, setActiveModule] = useState<ModuleId>('knowledge');
|
||||||
const [storageSheetOpen, setStorageSheetOpen] = useState(false);
|
const [storageSheetOpen, setStorageSheetOpen] = useState(false);
|
||||||
const [domainConnectionOpen, setDomainConnectionOpen] = useState(false);
|
const [domainConnectionOpen, setDomainConnectionOpen] = useState(false);
|
||||||
|
const [domainEntryTarget, setDomainEntryTarget] = useState<DomainEntryTarget | null>(null);
|
||||||
const [storageSheetInitialMode, setStorageSheetInitialMode] = useState<'local' | 'server' | undefined>();
|
const [storageSheetInitialMode, setStorageSheetInitialMode] = useState<'local' | 'server' | undefined>();
|
||||||
const [serverSession, setServerSession] = useState<ServerSession>({ authenticated: false, nodeId: '' });
|
const [serverSession, setServerSession] = useState<ServerSession>({ authenticated: false, nodeId: '' });
|
||||||
const [serverProfiles, setServerProfiles] = useState<ServerProfile[]>([]);
|
const [serverProfiles, setServerProfiles] = useState<ServerProfile[]>([]);
|
||||||
|
|
@ -101,6 +103,7 @@ export default function App() {
|
||||||
const [lastChannelReceipt, setLastChannelReceipt] = useState('');
|
const [lastChannelReceipt, setLastChannelReceipt] = useState('');
|
||||||
const [worldEntered, setWorldEntered] = useState(false);
|
const [worldEntered, setWorldEntered] = useState(false);
|
||||||
const [domainAccess, setDomainAccess] = useState<DomainAccessStatus>({ blockers: [], runtimeReady: false, stage: 'checking' });
|
const [domainAccess, setDomainAccess] = useState<DomainAccessStatus>({ blockers: [], runtimeReady: false, stage: 'checking' });
|
||||||
|
const domainAccessRequest = useRef(0);
|
||||||
|
|
||||||
const storageMode = repositoryStatus?.remote ? 'server' : 'local';
|
const storageMode = repositoryStatus?.remote ? 'server' : 'local';
|
||||||
const storageLabel = storageMode === 'server' ? '服务器已托管' : '仅本机';
|
const storageLabel = storageMode === 'server' ? '服务器已托管' : '仅本机';
|
||||||
|
|
@ -176,19 +179,35 @@ export default function App() {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const refreshDomainAccess = useCallback(async () => {
|
const refreshDomainAccess = useCallback(async (domainId = 'DOM-FIFTH-0001') => {
|
||||||
|
const request = ++domainAccessRequest.current;
|
||||||
const server = (window as any).hololake?.server;
|
const server = (window as any).hololake?.server;
|
||||||
if (!server?.domainAccess) {
|
if (!server?.domainAccess) {
|
||||||
setDomainAccess({ blockers: ['desktop_runtime_required'], runtimeReady: false, stage: 'login-required' });
|
if (request === domainAccessRequest.current) setDomainAccess({ blockers: ['desktop_runtime_required'], domainId, runtimeReady: false, stage: 'login-required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setDomainAccess(await server.domainAccess('DOM-FIFTH-0001'));
|
const result = await server.domainAccess(domainId);
|
||||||
|
if (request === domainAccessRequest.current) setDomainAccess(result);
|
||||||
} catch {
|
} catch {
|
||||||
setDomainAccess({ blockers: ['domain_access_probe_failed'], runtimeReady: false, stage: 'login-required' });
|
if (request === domainAccessRequest.current) setDomainAccess({ blockers: ['domain_access_probe_failed'], domainId, runtimeReady: false, stage: 'login-required' });
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const openDomainConnection = useCallback((routeId: DomainRouteId | null) => {
|
||||||
|
const target = routeId ? createDomainEntryTarget(routeId) : null;
|
||||||
|
setDomainEntryTarget(target);
|
||||||
|
setDomainConnectionOpen(true);
|
||||||
|
if (target) {
|
||||||
|
setDomainAccess({ blockers: [], domainId: target.domain.stableDomainId, runtimeReady: false, stage: 'checking' });
|
||||||
|
void refreshDomainAccess(target.domain.stableDomainId);
|
||||||
|
}
|
||||||
|
}, [refreshDomainAccess]);
|
||||||
|
|
||||||
|
const selectDomainNodeType = useCallback((nodeType: DomainNodeType) => {
|
||||||
|
setDomainEntryTarget(current => current ? createDomainEntryTarget(current.domain.routeId, nodeType) : null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const refreshChannel = useCallback(async () => {
|
const refreshChannel = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const [channel, registry] = await Promise.all([api.getChannel(), api.getModules()]);
|
const [channel, registry] = await Promise.all([api.getChannel(), api.getModules()]);
|
||||||
|
|
@ -393,9 +412,7 @@ export default function App() {
|
||||||
setActiveModule('knowledge');
|
setActiveModule('knowledge');
|
||||||
setWorldEntered(true);
|
setWorldEntered(true);
|
||||||
}}
|
}}
|
||||||
onOpenConnection={() => {
|
onOpenConnection={openDomainConnection}
|
||||||
setDomainConnectionOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
|
@ -581,10 +598,13 @@ export default function App() {
|
||||||
/>
|
/>
|
||||||
<DomainConnectionSheet
|
<DomainConnectionSheet
|
||||||
access={domainAccess}
|
access={domainAccess}
|
||||||
|
target={domainEntryTarget}
|
||||||
open={domainConnectionOpen}
|
open={domainConnectionOpen}
|
||||||
onClose={() => setDomainConnectionOpen(false)}
|
onClose={() => setDomainConnectionOpen(false)}
|
||||||
|
onSelectTarget={routeId => openDomainConnection(routeId)}
|
||||||
|
onSelectNodeType={selectDomainNodeType}
|
||||||
onEnterRuntime={() => {
|
onEnterRuntime={() => {
|
||||||
if (!domainAccess.runtimeReady) return;
|
if (!domainEntryTarget || domainEntryTarget.domain.routeId !== 'fifth' || !domainAccess.runtimeReady || domainAccess.domainId !== domainEntryTarget.domain.stableDomainId) return;
|
||||||
setActiveRoute('fifth');
|
setActiveRoute('fifth');
|
||||||
setActiveModule('knowledge');
|
setActiveModule('knowledge');
|
||||||
setWorldEntered(true);
|
setWorldEntered(true);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,23 @@
|
||||||
import { Cloud, Laptop, ShieldCheck, X } from 'lucide-react';
|
import { Cloud, Laptop, ShieldCheck, X } from 'lucide-react';
|
||||||
import { projectDomainConnectionSteps, projectDomainTrustSource, type DomainAccessProjection } from '../domain-connection';
|
import { projectDomainConnectionSteps, projectDomainTrustSource, type DomainAccessProjection } from '../domain-connection';
|
||||||
|
import { canEnterSelectedDomainRuntime, projectDomainRuntimeBoundary, type DomainEntryTarget, type DomainNodeType } from '../domain-entry-state';
|
||||||
|
import { publicDomainDirectory, type DomainRouteId } from '../public-domain-directory';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
access: DomainAccessProjection;
|
access: DomainAccessProjection;
|
||||||
|
target: DomainEntryTarget | null;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onSelectTarget: (routeId: DomainRouteId) => void;
|
||||||
|
onSelectNodeType: (nodeType: DomainNodeType) => void;
|
||||||
onEnterRuntime: () => void;
|
onEnterRuntime: () => void;
|
||||||
onOpenCodeChannel: () => void;
|
onOpenCodeChannel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DomainConnectionSheet({ access, open, onClose, onEnterRuntime, onOpenCodeChannel }: Props) {
|
export function DomainConnectionSheet({ access, target, open, onClose, onSelectTarget, onSelectNodeType, onEnterRuntime, onOpenCodeChannel }: Props) {
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
const steps = projectDomainConnectionSteps(access);
|
const steps = projectDomainConnectionSteps(access);
|
||||||
|
const canEnter = canEnterSelectedDomainRuntime(target, access);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="storage-sheet-backdrop" role="presentation" onMouseDown={event => {
|
<div className="storage-sheet-backdrop" role="presentation" onMouseDown={event => {
|
||||||
|
|
@ -20,18 +26,22 @@ export function DomainConnectionSheet({ access, open, onClose, onEnterRuntime, o
|
||||||
<section className="storage-sheet domain-connection-sheet" role="dialog" aria-modal="true" aria-labelledby="domain-connection-title">
|
<section className="storage-sheet domain-connection-sheet" role="dialog" aria-modal="true" aria-labelledby="domain-connection-title">
|
||||||
<header className="storage-sheet-header">
|
<header className="storage-sheet-header">
|
||||||
<div>
|
<div>
|
||||||
<small>DOM-FIFTH-0001</small>
|
<small>{target?.domain.stableDomainId ?? 'SELECT-DOMAIN'}</small>
|
||||||
<h2 id="domain-connection-title">接入第五域 · 光湖本源域</h2>
|
<h2 id="domain-connection-title">{target ? `接入${target.domain.displayName}` : '选择要接入的域'}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button className="icon-button" onClick={onClose} aria-label="关闭域接入状态"><X aria-hidden="true" /></button>
|
<button className="icon-button" onClick={onClose} aria-label="关闭域接入状态"><X aria-hidden="true" /></button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p className="domain-connection-lead">进入域不是切换页面。系统必须验证账户、登记节点、目标域授权、会话能力和在线回执。</p>
|
<p className="domain-connection-lead">进入域不是切换页面。系统必须验证账户、登记节点、目标域授权、会话能力和在线回执。</p>
|
||||||
|
<div className="domain-target-picker" aria-label="选择目标域">
|
||||||
|
{publicDomainDirectory.map(domain => <button key={domain.routeId} type="button" data-selected={target?.domain.routeId === domain.routeId} onClick={() => onSelectTarget(domain.routeId)}><span>{domain.directoryNumber}</span>{domain.displayName}</button>)}
|
||||||
|
</div>
|
||||||
|
{!target ? <p className="domain-connection-boundary">{projectDomainRuntimeBoundary(target, access)}</p> : <>
|
||||||
<p className="domain-connection-boundary">{projectDomainTrustSource(access)}</p>
|
<p className="domain-connection-boundary">{projectDomainTrustSource(access)}</p>
|
||||||
|
|
||||||
<div className="domain-node-modes" aria-label="支持的节点类型">
|
<div className="domain-node-modes" aria-label="支持的节点类型">
|
||||||
<article><Laptop aria-hidden="true" /><span><strong>本地终端节点</strong><small>使用当前联网电脑承载本地工作;正式节点登记尚未开放。</small></span></article>
|
<button type="button" data-selected={target.nodeType === 'local-terminal'} onClick={() => onSelectNodeType('local-terminal')}><Laptop aria-hidden="true" /><span><strong>本地终端节点</strong><small>使用当前联网电脑承载本地工作;仍须完成节点登记。</small></span></button>
|
||||||
<article><Cloud aria-hidden="true" /><span><strong>云常驻节点</strong><small>服务器可持续在线;代码频道登录不等于取得域运行权限。</small></span></article>
|
<button type="button" data-selected={target.nodeType === 'cloud-resident'} onClick={() => onSelectNodeType('cloud-resident')}><Cloud aria-hidden="true" /><span><strong>云常驻节点</strong><small>服务器可持续在线;代码频道登录不等于取得域权限。</small></span></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ol className="domain-connection-steps">
|
<ol className="domain-connection-steps">
|
||||||
|
|
@ -44,14 +54,15 @@ export function DomainConnectionSheet({ access, open, onClose, onEnterRuntime, o
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<p className="domain-connection-boundary">代码频道账号只用于仓库与同步,是节点身份的辅助证明之一,不能单独签发第五域权限。</p>
|
<p className="domain-connection-boundary">{projectDomainRuntimeBoundary(target, access)} 代码频道账号只用于仓库与同步,不能单独签发域权限。</p>
|
||||||
|
</>}
|
||||||
|
|
||||||
<footer className="storage-sheet-footer">
|
<footer className="storage-sheet-footer">
|
||||||
<button className="secondary-button" onClick={onOpenCodeChannel}>代码频道与同步设置</button>
|
<button className="secondary-button" onClick={onOpenCodeChannel} disabled={!target}>代码频道与同步设置</button>
|
||||||
<span />
|
<span />
|
||||||
<button className="secondary-button" onClick={onClose}>返回门厅</button>
|
<button className="secondary-button" onClick={onClose}>返回门厅</button>
|
||||||
<button className="primary-button" onClick={onEnterRuntime} disabled={!access.runtimeReady}>
|
<button className="primary-button" onClick={onEnterRuntime} disabled={!canEnter}>
|
||||||
{access.runtimeReady ? '进入第五域运行体' : '等待可信域接入'}
|
{canEnter ? '进入第五域运行体' : target ? '等待可信域接入' : '请先选择域'}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ interface Props {
|
||||||
access: { blockers: string[]; runtimeReady: boolean; stage: 'checking' | 'login-required' | 'identity-verified' | 'runtime-ready' };
|
access: { blockers: string[]; runtimeReady: boolean; stage: 'checking' | 'login-required' | 'identity-verified' | 'runtime-ready' };
|
||||||
onEnterFifthRuntime: () => void;
|
onEnterFifthRuntime: () => void;
|
||||||
onEnterLocalWorkspace: () => void;
|
onEnterLocalWorkspace: () => void;
|
||||||
onOpenConnection: () => void;
|
onOpenConnection: (routeId: DomainRouteId | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorldEntry({ access, onEnterFifthRuntime, onEnterLocalWorkspace, onOpenConnection }: Props) {
|
export function WorldEntry({ access, onEnterFifthRuntime, onEnterLocalWorkspace, onOpenConnection }: Props) {
|
||||||
|
|
@ -55,7 +55,7 @@ export function WorldEntry({ access, onEnterFifthRuntime, onEnterLocalWorkspace,
|
||||||
<div><dt><DoorOpen aria-hidden="true" /><span>状态</span></dt><dd>{access.stage === 'runtime-ready' ? '节点、域清单、会话能力与连接回执已验证' : access.stage === 'identity-verified' ? '账户与节点已验证 · 尚缺域清单、会话能力和连接回执' : access.stage === 'checking' ? '正在核对真实接入证据' : '公开门厅可查看 · 尚未登录或接入节点'}</dd></div>
|
<div><dt><DoorOpen aria-hidden="true" /><span>状态</span></dt><dd>{access.stage === 'runtime-ready' ? '节点、域清单、会话能力与连接回执已验证' : access.stage === 'identity-verified' ? '账户与节点已验证 · 尚缺域清单、会话能力和连接回执' : access.stage === 'checking' ? '正在核对真实接入证据' : '公开门厅可查看 · 尚未登录或接入节点'}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
<div className="fifth-actions">
|
<div className="fifth-actions">
|
||||||
<button className="fifth-primary" type="button" onClick={access.runtimeReady ? onEnterFifthRuntime : onOpenConnection}><LogIn aria-hidden="true" />{access.runtimeReady ? '进入第五域运行体' : '登录或接入节点'}</button>
|
<button className="fifth-primary" type="button" onClick={access.runtimeReady ? onEnterFifthRuntime : () => onOpenConnection('fifth')}><LogIn aria-hidden="true" />{access.runtimeReady ? '进入第五域运行体' : '登录或接入节点'}</button>
|
||||||
<button type="button" onClick={onEnterLocalWorkspace}><DoorOpen aria-hidden="true" />进入仅本机工作空间</button>
|
<button type="button" onClick={onEnterLocalWorkspace}><DoorOpen aria-hidden="true" />进入仅本机工作空间</button>
|
||||||
<button type="button" onClick={() => setStage('lighthouse')}><Landmark aria-hidden="true" />返回灯塔</button>
|
<button type="button" onClick={() => setStage('lighthouse')}><Landmark aria-hidden="true" />返回灯塔</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -82,7 +82,7 @@ export function WorldEntry({ access, onEnterFifthRuntime, onEnterLocalWorkspace,
|
||||||
<button className="public-primary" type="button" onClick={() => setStage(stage === 'directory' ? 'lighthouse' : 'directory')}>
|
<button className="public-primary" type="button" onClick={() => setStage(stage === 'directory' ? 'lighthouse' : 'directory')}>
|
||||||
{stage === 'directory' ? '返回公共灯塔' : '查看五域'}
|
{stage === 'directory' ? '返回公共灯塔' : '查看五域'}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={onOpenConnection}>登录或接入节点</button>
|
<button type="button" onClick={() => onOpenConnection(selectedDomain)}>登录或接入节点</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -106,6 +106,7 @@ export function WorldEntry({ access, onEnterFifthRuntime, onEnterLocalWorkspace,
|
||||||
<strong>{selectedVestibule.stableDomainId}</strong>
|
<strong>{selectedVestibule.stableDomainId}</strong>
|
||||||
<span>{selectedVestibule.themeLabel}</span>
|
<span>{selectedVestibule.themeLabel}</span>
|
||||||
<small>仅公开门厅资料 · 私有资源未装载</small>
|
<small>仅公开门厅资料 · 私有资源未装载</small>
|
||||||
|
<button type="button" onClick={() => onOpenConnection(selectedVestibule.routeId)}>登录进入此域</button>
|
||||||
</aside>
|
</aside>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { canEnterSelectedDomainRuntime, createDomainEntryTarget, projectDomainRuntimeBoundary } from './domain-entry-state.js';
|
||||||
|
|
||||||
|
const ready = (domainId: string) => ({ blockers: [], domainId, runtimeReady: true, stage: 'runtime-ready' as const });
|
||||||
|
|
||||||
|
test('a generic login request does not silently target the Fifth Domain', () => {
|
||||||
|
assert.equal(canEnterSelectedDomainRuntime(null, ready('DOM-FIFTH-0001')), false);
|
||||||
|
assert.match(projectDomainRuntimeBoundary(null, ready('DOM-FIFTH-0001')), /先选择目标域/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the selected stable domain id must match the returned access evidence', () => {
|
||||||
|
const fifth = createDomainEntryTarget('fifth', 'cloud-resident');
|
||||||
|
assert.equal(canEnterSelectedDomainRuntime(fifth, ready('DOMAIN-MAIN')), false);
|
||||||
|
assert.match(projectDomainRuntimeBoundary(fifth, ready('DOMAIN-MAIN')), /另一个域/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enterprise vestibules cannot reuse the Fifth Domain renderer', () => {
|
||||||
|
const main = createDomainEntryTarget('main');
|
||||||
|
assert.equal(main.domain.stableDomainId, 'DOMAIN-MAIN');
|
||||||
|
assert.equal(main.nodeType, 'local-terminal');
|
||||||
|
assert.equal(canEnterSelectedDomainRuntime(main, ready('DOMAIN-MAIN')), false);
|
||||||
|
assert.match(projectDomainRuntimeBoundary(main, ready('DOMAIN-MAIN')), /独立运行端点与主题包尚未登记/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the Fifth Domain opens only with matching runtime-ready evidence', () => {
|
||||||
|
const fifth = createDomainEntryTarget('fifth', 'cloud-resident');
|
||||||
|
assert.equal(canEnterSelectedDomainRuntime(fifth, ready('DOM-FIFTH-0001')), true);
|
||||||
|
assert.match(projectDomainRuntimeBoundary(fifth, ready('DOM-FIFTH-0001')), /已经匹配/);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { getPublicDomainVestibule, type DomainRouteId, type PublicDomainVestibule } from './public-domain-directory.js';
|
||||||
|
import type { DomainAccessProjection } from './domain-connection.js';
|
||||||
|
|
||||||
|
export type DomainNodeType = 'local-terminal' | 'cloud-resident';
|
||||||
|
|
||||||
|
export interface DomainEntryTarget {
|
||||||
|
domain: PublicDomainVestibule;
|
||||||
|
nodeType: DomainNodeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDomainEntryTarget(routeId: DomainRouteId, nodeType: DomainNodeType = 'local-terminal'): DomainEntryTarget {
|
||||||
|
const domain = getPublicDomainVestibule(routeId);
|
||||||
|
if (!domain.supportedNodeTypes.includes(nodeType)) throw new Error('DOMAIN_NODE_TYPE_NOT_SUPPORTED');
|
||||||
|
return Object.freeze({ domain, nodeType });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canEnterSelectedDomainRuntime(target: DomainEntryTarget | null, access: DomainAccessProjection): boolean {
|
||||||
|
if (!target || !access.runtimeReady) return false;
|
||||||
|
if (access.domainId !== target.domain.stableDomainId) return false;
|
||||||
|
// The present desktop bundle contains only the Fifth Domain renderer. The four
|
||||||
|
// enterprise domains must provide their own signed runtime package and endpoint.
|
||||||
|
return target.domain.routeId === 'fifth';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function projectDomainRuntimeBoundary(target: DomainEntryTarget | null, access: DomainAccessProjection): string {
|
||||||
|
if (!target) return '请先选择目标域;系统不会把通用登录请求默认路由到第五域。';
|
||||||
|
if (access.domainId && access.domainId !== target.domain.stableDomainId) {
|
||||||
|
return '回读证据属于另一个域;当前入口保持关闭。';
|
||||||
|
}
|
||||||
|
if (target.domain.routeId !== 'fifth') {
|
||||||
|
return `${target.domain.displayName}的独立运行端点与主题包尚未登记;当前只能查看公开门厅。`;
|
||||||
|
}
|
||||||
|
return access.runtimeReady
|
||||||
|
? '第五域运行端点、签名清单、会话能力与在线回执已经匹配。'
|
||||||
|
: '第五域入口保持关闭,直到四项真实接入证据全部匹配。';
|
||||||
|
}
|
||||||
|
|
@ -3142,6 +3142,28 @@ select:focus-visible {
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.domain-target-picker {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
padding: 14px 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-target-picker button {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: 52px;
|
||||||
|
padding: 8px 6px;
|
||||||
|
border: 1px solid var(--lake-border-soft);
|
||||||
|
border-radius: 9px;
|
||||||
|
background: rgba(7, 17, 29, .62);
|
||||||
|
color: var(--lake-text-soft);
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-target-picker button span { color: var(--lake-muted); }
|
||||||
|
.domain-target-picker button[data-selected="true"] { border-color: var(--lake-accent); background: var(--lake-accent-soft); color: var(--lake-text); }
|
||||||
|
|
||||||
.domain-node-modes {
|
.domain-node-modes {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
|
@ -3149,15 +3171,19 @@ select:focus-visible {
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.domain-node-modes article {
|
.domain-node-modes button {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 11px;
|
gap: 11px;
|
||||||
padding: 13px;
|
padding: 13px;
|
||||||
border: 1px solid var(--lake-border-soft);
|
border: 1px solid var(--lake-border-soft);
|
||||||
border-radius: 11px;
|
border-radius: 11px;
|
||||||
background: rgba(7, 17, 29, .62);
|
background: rgba(7, 17, 29, .62);
|
||||||
|
color: var(--lake-text);
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.domain-node-modes button[data-selected="true"] { border-color: var(--lake-accent); background: var(--lake-accent-soft); }
|
||||||
|
|
||||||
.domain-node-modes svg {
|
.domain-node-modes svg {
|
||||||
width: 19px;
|
width: 19px;
|
||||||
color: var(--lake-accent);
|
color: var(--lake-accent);
|
||||||
|
|
@ -3224,6 +3250,7 @@ select:focus-visible {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 660px) {
|
@media (max-width: 660px) {
|
||||||
|
.domain-target-picker { grid-template-columns: 1fr 1fr; }
|
||||||
.domain-node-modes { grid-template-columns: 1fr; }
|
.domain-node-modes { grid-template-columns: 1fr; }
|
||||||
.domain-connection-sheet .storage-sheet-footer { flex-wrap: wrap; }
|
.domain-connection-sheet .storage-sheet-footer { flex-wrap: wrap; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue