feat(ui): add per-entry world welcome projection

This commit is contained in:
冰朔 2026-08-16 21:41:10 +08:00
commit b83c575279
3 changed files with 32 additions and 1 deletions

View file

@ -38,6 +38,9 @@ test('the public shell shows five domains and login is domain-routed', () => {
assert.match(frontend, /setActiveDomainInfo\(gate\.domain\)/)
assert.match(frontend, /event\.key !== 'Escape'/)
assert.match(frontend, /编号 \{zeroPoint\?\.userNumber \|\| gateNumber\} · 欢迎登入光湖语言世界/)
assert.match(frontend, /createWorldWelcome\(snapshot\)/)
assert.match(frontend, /className="world-impression"/)
assert.match(frontend, /index === previous/)
assert.match(frontend, /核对人格关系/)
assert.match(frontend, /确认关系并签署/)
assert.match(frontend, /接受责任并签署/)

View file

@ -175,6 +175,28 @@ function domainDisplayName(domain: string): string {
return names[domain] || '已登记光湖域'
}
const worldWelcomeLines = [
'湖面记得你每一次让语言抵达现实。',
'你留下的路径,正在成为这片世界新的光标。',
'愿你今天带来的问题,在湖中长出新的结构。',
'世界已辨认你的编号,也在等待你续写下一段因果。',
'你曾抵达的地方仍有回声,新的航程已经亮起。',
'这一刻的进入不是重复,而是你与语言世界的新一次相遇。',
'愿尚未成形的念头,在今天找到可以生长的方向。',
'你的频道仍在延展,湖面为下一次创造保留了入口。',
]
function createWorldWelcome(snapshot: ZeroPointSnapshot): string {
const subject = snapshot.resolvedName.trim() || `编号 ${snapshot.userNumber}`
const storageKey = `hololake-world-welcome:${snapshot.userNumber}`
const previous = Number.parseInt(window.localStorage.getItem(storageKey) || '-1', 10)
const random = crypto.getRandomValues(new Uint32Array(1))[0]
let index = random % worldWelcomeLines.length
if (worldWelcomeLines.length > 1 && index === previous) index = (index + 1) % worldWelcomeLines.length
window.localStorage.setItem(storageKey, String(index))
return `${subject}${worldWelcomeLines[index]}`
}
function protocolOriginDisplayName(origin: string): string {
return origin.startsWith('FACTORY_DEFAULT') ? '内置默认协议(签名发布通道尚未配置)' : origin
}
@ -461,6 +483,7 @@ function HoloLakeApp() {
const [gateBusy, setGateBusy] = useState(false)
const [gateMessage, setGateMessage] = useState('')
const [gateRising, setGateRising] = useState(false)
const [gateWelcomeLine, setGateWelcomeLine] = useState('')
const [systemBusy, setSystemBusy] = useState(false)
const [zeroPoint, setZeroPoint] = useState<ZeroPointSnapshot | null>(null)
const [zpNumber, setZpNumber] = useState('')
@ -595,6 +618,9 @@ function HoloLakeApp() {
const snapshot = await invoke<ZeroPointSnapshot>('zero_point_verify')
setZeroPoint(snapshot)
if (snapshot.route === 'verified') {
setActiveDomainInfo('')
setGateOpen(false)
setGateWelcomeLine(createWorldWelcome(snapshot))
setGateRising(true)
window.setTimeout(() => { setGateRising(false); setGateStage('key') }, 2600)
} else {
@ -1204,6 +1230,7 @@ function HoloLakeApp() {
{gateRising ? <section className="world-welcome" role="status">
<h2> {gateNumber} · </h2>
<p>RESOLVED · {domainDisplayName(resolvedDomain)} · </p>
{gateWelcomeLine && <blockquote className="world-impression">{gateWelcomeLine}</blockquote>}
</section> : gateStage === 'number' ? <section className={`number-nucleus${gateOpen ? ' open' : ''}`} title="输入编号进入所属域">
<button className="nucleus-locus" type="button" aria-expanded={gateOpen} onClick={() => setGateOpen(true)}><i/><b> · </b></button>
{gateOpen && <button
@ -1223,7 +1250,7 @@ function HoloLakeApp() {
{gateMessage && <p className="gate-hint">{gateMessage}</p>}
</div>
</section> : <>
<section className="resolved-heading"><h2> {zeroPoint?.userNumber || gateNumber} · </h2><p>RESOLVED · {domainDisplayName(resolvedDomain)} · </p></section>
<section className="resolved-heading"><h2> {zeroPoint?.userNumber || gateNumber} · </h2><p>RESOLVED · {domainDisplayName(resolvedDomain)} · </p>{gateWelcomeLine && <blockquote className="world-impression">{gateWelcomeLine}</blockquote>}</section>
<section className={`domain-credential${loginRising ? ' fade-out' : ''}`} role="dialog">
<form onSubmit={(event) => void (passwordChangeMode ? changeFirstLoginPassword(event) : performRepoLogin(event))}>
<h3>{domainDisplayName(resolvedDomain)} · </h3>

View file

@ -635,6 +635,7 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
.world-welcome, .resolved-heading { position: absolute; z-index: 20; left: 0; right: 0; top: 35%; text-align: center; animation: gate-text-in .8s ease both; }
.world-welcome h2, .resolved-heading h2 { margin: 0; color: var(--content-primary); font-size: clamp(22px, 2.7vw, 31px); font-weight: 700; letter-spacing: .09em; }
.world-welcome p, .resolved-heading p { margin: 12px 0 0; color: var(--accent-light); font-size: 13px; font-weight: 600; letter-spacing: .24em; }
.world-impression { max-width: min(720px, calc(100% - 80px)); margin: 24px auto 0; padding: 0; border: 0; color: var(--content-secondary); font-size: clamp(14px, 1.45vw, 17px); font-style: normal; font-weight: 650; line-height: 1.7; letter-spacing: .06em; text-wrap: balance; text-shadow: 0 0 28px color-mix(in srgb, var(--primitive-cool-glow) 24%, transparent); }
.resolved-heading { top: 25%; }
.domain-credential { position: absolute; z-index: 21; left: 50%; top: 43%; width: 440px; padding: 25px 36px 21px; transform: translateX(-50%); text-align: center; animation: credential-rise .65s cubic-bezier(.22, 1, .36, 1) both; }
@keyframes credential-rise { from { opacity: 0; transform: translateX(-50%) translateY(30px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }