feat(ui): add per-entry world welcome projection
This commit is contained in:
parent
3bdb690100
commit
b83c575279
3 changed files with 32 additions and 1 deletions
|
|
@ -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, /setActiveDomainInfo\(gate\.domain\)/)
|
||||||
assert.match(frontend, /event\.key !== 'Escape'/)
|
assert.match(frontend, /event\.key !== 'Escape'/)
|
||||||
assert.match(frontend, /编号 \{zeroPoint\?\.userNumber \|\| gateNumber\} · 欢迎登入光湖语言世界/)
|
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, /确认关系并签署/)
|
assert.match(frontend, /确认关系并签署/)
|
||||||
assert.match(frontend, /接受责任并签署/)
|
assert.match(frontend, /接受责任并签署/)
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,28 @@ function domainDisplayName(domain: string): string {
|
||||||
return names[domain] || '已登记光湖域'
|
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 {
|
function protocolOriginDisplayName(origin: string): string {
|
||||||
return origin.startsWith('FACTORY_DEFAULT') ? '内置默认协议(签名发布通道尚未配置)' : origin
|
return origin.startsWith('FACTORY_DEFAULT') ? '内置默认协议(签名发布通道尚未配置)' : origin
|
||||||
}
|
}
|
||||||
|
|
@ -461,6 +483,7 @@ function HoloLakeApp() {
|
||||||
const [gateBusy, setGateBusy] = useState(false)
|
const [gateBusy, setGateBusy] = useState(false)
|
||||||
const [gateMessage, setGateMessage] = useState('')
|
const [gateMessage, setGateMessage] = useState('')
|
||||||
const [gateRising, setGateRising] = useState(false)
|
const [gateRising, setGateRising] = useState(false)
|
||||||
|
const [gateWelcomeLine, setGateWelcomeLine] = useState('')
|
||||||
const [systemBusy, setSystemBusy] = useState(false)
|
const [systemBusy, setSystemBusy] = useState(false)
|
||||||
const [zeroPoint, setZeroPoint] = useState<ZeroPointSnapshot | null>(null)
|
const [zeroPoint, setZeroPoint] = useState<ZeroPointSnapshot | null>(null)
|
||||||
const [zpNumber, setZpNumber] = useState('')
|
const [zpNumber, setZpNumber] = useState('')
|
||||||
|
|
@ -595,6 +618,9 @@ function HoloLakeApp() {
|
||||||
const snapshot = await invoke<ZeroPointSnapshot>('zero_point_verify')
|
const snapshot = await invoke<ZeroPointSnapshot>('zero_point_verify')
|
||||||
setZeroPoint(snapshot)
|
setZeroPoint(snapshot)
|
||||||
if (snapshot.route === 'verified') {
|
if (snapshot.route === 'verified') {
|
||||||
|
setActiveDomainInfo('')
|
||||||
|
setGateOpen(false)
|
||||||
|
setGateWelcomeLine(createWorldWelcome(snapshot))
|
||||||
setGateRising(true)
|
setGateRising(true)
|
||||||
window.setTimeout(() => { setGateRising(false); setGateStage('key') }, 2600)
|
window.setTimeout(() => { setGateRising(false); setGateStage('key') }, 2600)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1204,6 +1230,7 @@ function HoloLakeApp() {
|
||||||
{gateRising ? <section className="world-welcome" role="status">
|
{gateRising ? <section className="world-welcome" role="status">
|
||||||
<h2>编号 {gateNumber} · 欢迎登入光湖语言世界</h2>
|
<h2>编号 {gateNumber} · 欢迎登入光湖语言世界</h2>
|
||||||
<p>RESOLVED · {domainDisplayName(resolvedDomain)} · 已接入</p>
|
<p>RESOLVED · {domainDisplayName(resolvedDomain)} · 已接入</p>
|
||||||
|
{gateWelcomeLine && <blockquote className="world-impression">{gateWelcomeLine}</blockquote>}
|
||||||
</section> : gateStage === 'number' ? <section className={`number-nucleus${gateOpen ? ' open' : ''}`} title="输入编号进入所属域">
|
</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>
|
<button className="nucleus-locus" type="button" aria-expanded={gateOpen} onClick={() => setGateOpen(true)}><i/><b>编号验证 · 进入世界的第一个入口</b></button>
|
||||||
{gateOpen && <button
|
{gateOpen && <button
|
||||||
|
|
@ -1223,7 +1250,7 @@ function HoloLakeApp() {
|
||||||
{gateMessage && <p className="gate-hint">{gateMessage}</p>}
|
{gateMessage && <p className="gate-hint">{gateMessage}</p>}
|
||||||
</div>
|
</div>
|
||||||
</section> : <>
|
</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">
|
<section className={`domain-credential${loginRising ? ' fade-out' : ''}`} role="dialog">
|
||||||
<form onSubmit={(event) => void (passwordChangeMode ? changeFirstLoginPassword(event) : performRepoLogin(event))}>
|
<form onSubmit={(event) => void (passwordChangeMode ? changeFirstLoginPassword(event) : performRepoLogin(event))}>
|
||||||
<h3>{domainDisplayName(resolvedDomain)} · 域凭证</h3>
|
<h3>{domainDisplayName(resolvedDomain)} · 域凭证</h3>
|
||||||
|
|
|
||||||
|
|
@ -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, .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 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-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%; }
|
.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; }
|
.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); } }
|
@keyframes credential-rise { from { opacity: 0; transform: translateX(-50%) translateY(30px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue