From 6819db40a80030fd3b73b980355c8294d3ec04b0 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 23:10:36 +0800 Subject: [PATCH 1/6] fix(auth): rebalance account introduction typography and spacing --- apps/waterfall-wiki/src/pages/account.tsx | 5 -- .../src/styles/guidance.module.css | 84 ++++++++++--------- 2 files changed, 43 insertions(+), 46 deletions(-) diff --git a/apps/waterfall-wiki/src/pages/account.tsx b/apps/waterfall-wiki/src/pages/account.tsx index 9113181..bc47aee 100644 --- a/apps/waterfall-wiki/src/pages/account.tsx +++ b/apps/waterfall-wiki/src/pages/account.tsx @@ -1,4 +1,3 @@ -import { Brand } from "../components/Brand"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState, type FormEvent } from "react"; @@ -12,7 +11,6 @@ import { Link2, LogOut, MailCheck, - Waypoints, } from "lucide-react"; import GuidanceShell, { useGuidanceAccount } from "@/components/GuidanceShell"; import { Notice } from "@/components/GuidanceUI"; @@ -115,9 +113,6 @@ function AccountContent() { return (
- - - YOUR KNOWLEDGE, CONNECTED

让有用的知识, diff --git a/apps/waterfall-wiki/src/styles/guidance.module.css b/apps/waterfall-wiki/src/styles/guidance.module.css index 4c1df1a..b31fe65 100644 --- a/apps/waterfall-wiki/src/styles/guidance.module.css +++ b/apps/waterfall-wiki/src/styles/guidance.module.css @@ -1680,63 +1680,67 @@ } .accountLayout { display: grid; - grid-template-columns: minmax(0, 1fr) 410px; - max-width: 990px; - gap: 70px; + grid-template-columns: minmax(0, 1fr) 400px; + max-width: 1000px; + gap: 80px; align-items: center; - margin: 50px auto 70px; + margin: 40px auto 64px; } .accountIntro { - padding: 20px 0; + min-width: 0; + max-width: 460px; + padding: 16px 20px 16px 8px; } -.accountSymbol { - width: 54px; - height: 54px; - display: grid; - place-items: center; - background: var(--selection); - border: 1px solid var(--selection-border); - border-radius: 14px; - color: var(--primary); - margin-bottom: 28px; +.accountIntro > .eyebrow { + display: block; + margin-bottom: 20px; + font-size: 11px; + letter-spacing: .14em; + line-height: 1.6; } .accountIntro h1 { - font-size: 32px; - line-height: 1.5; - letter-spacing: -1px; + margin: 0 0 22px; + font-size: 36px; + line-height: 1.45; + letter-spacing: -.035em; font-weight: 650; - margin-bottom: 17px; + text-wrap: balance; } .accountIntro > p { - font-size: 13px; + margin: 0; + font-size: 15px; line-height: 1.9; color: var(--muted-foreground); - max-width: 360px; + max-width: 380px; } .accountFeatures { display: grid; - gap: 21px; - margin-top: 35px; + gap: 22px; + margin-top: 32px; + padding-top: 26px; + border-top: 1px solid var(--border); } .accountFeatures > div { - display: flex; - align-items: flex-start; - gap: 12px; + display: grid; + grid-template-columns: 20px minmax(0, 1fr); + align-items: start; + gap: 14px; } .accountFeatures svg { color: var(--muted-foreground); - margin-top: 2px; + margin-top: 3px; } .accountFeatures strong { display: block; - font-size: 12px; + font-size: 14px; + line-height: 1.6; font-weight: 600; } .accountFeatures p { - font-size: 11px; + margin: 4px 0 0; + font-size: 13px; color: var(--muted-foreground); line-height: 1.8; - margin-top: 5px; } .authCard { padding: 28px; @@ -1923,12 +1927,9 @@ gap: 20px; } .accountLayout { - gap: 35px; + gap: 48px; grid-template-columns: minmax(0, 1fr) 370px; } - .accountIntro h1 { - font-size: 29px; - } .protocolLayout { grid-template-columns: minmax(0, 1fr) 190px; } @@ -1975,19 +1976,23 @@ gap: 25px; } .accountIntro { - padding: 0; + padding: 0 4px; + max-width: none; + } + .accountIntro > .eyebrow { + margin-bottom: 12px; + font-size: 10px; } .accountIntro h1 { font-size: 26px; margin-bottom: 8px; } - .accountSymbol, .accountFeatures { display: none; } .accountIntro > p { max-width: none; - font-size: 12px; + font-size: 14px; } .accountIntro h1 br { display: none; @@ -2310,10 +2315,7 @@ font-size: 24px; } .accountIntro > p { - font-size: 11px; - } - .accountSymbol { - display: none; + font-size: 14px; } .modalBody { padding: 23px; -- 2.52.0 From 88d68f3d3c2d9890d4abb1f5616f34111b4e3977 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 23:19:23 +0800 Subject: [PATCH 2/6] feat(auth): introduce immersive split-screen login design --- .../src/components/GuidanceShell.tsx | 6 +- apps/waterfall-wiki/src/pages/account.tsx | 53 +++----- .../src/styles/account.module.css | 52 +++++++ .../src/styles/guidance.module.css | 128 ++---------------- 4 files changed, 82 insertions(+), 157 deletions(-) create mode 100644 apps/waterfall-wiki/src/styles/account.module.css diff --git a/apps/waterfall-wiki/src/components/GuidanceShell.tsx b/apps/waterfall-wiki/src/components/GuidanceShell.tsx index 7a6fe50..b592f49 100644 --- a/apps/waterfall-wiki/src/components/GuidanceShell.tsx +++ b/apps/waterfall-wiki/src/components/GuidanceShell.tsx @@ -1,3 +1,4 @@ +import a from "@/styles/account.module.css"; import { ThemeToggle } from "./ThemeToggle"; import { Brand } from "./Brand"; import Head from "next/head"; @@ -191,11 +192,10 @@ export default function GuidanceShell({ } if (standalone) return ( -
+
{`${title} · 啡跃`} 跳到正文 -
- +
返回首页
{children}
diff --git a/apps/waterfall-wiki/src/pages/account.tsx b/apps/waterfall-wiki/src/pages/account.tsx index bc47aee..4a405ba 100644 --- a/apps/waterfall-wiki/src/pages/account.tsx +++ b/apps/waterfall-wiki/src/pages/account.tsx @@ -1,14 +1,13 @@ +import { Brand } from "@/components/Brand"; +import a from "@/styles/account.module.css"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState, type FormEvent } from "react"; import { ArrowRight, - BookOpen, CheckCircle2, Eye, EyeOff, - GitBranch, - Link2, LogOut, MailCheck, } from "lucide-react"; @@ -111,43 +110,23 @@ function AccountContent() { setFeedback(null); } return ( -
-
- YOUR KNOWLEDGE, CONNECTED -

- 让有用的知识, -
- 成为下一次行动的起点。 -

-

- 用邮箱建立你的知识空间。把工作方法写成独立文档,让 Agent - 在需要的时候找到它。 -

-
-
- -
- 每篇文档,都能独立使用 -

从当前任务出发,无需加载整个集合。

-
-
-
- -
- 每次更新,都有固定版本 -

历史引用保持可读,修改始终有迹可循。

-
-
-
- -
- 知识之间,按条件连接 -

需要更多背景时,再沿有用的链接继续阅读。

-
+
+
+ +
+

A SPACE FOR YOUR NEXT IDEA

+

给想法留白,
让灵感有形。

+

收集一闪而过的灵感,把有用的知识连接起来。下一件作品,从这里开始。

+
-
+

{account.user ? "你的账户" diff --git a/apps/waterfall-wiki/src/styles/account.module.css b/apps/waterfall-wiki/src/styles/account.module.css new file mode 100644 index 0000000..4dc6d79 --- /dev/null +++ b/apps/waterfall-wiki/src/styles/account.module.css @@ -0,0 +1,52 @@ +.page { min-height: 100svh; background: var(--surface); color: var(--foreground); } +.header { position: absolute; z-index: 2; top: 28px; right: 36px; } +.header > div { display: flex; align-items: center; gap: 20px; font-size: 13px; color: var(--muted-foreground); } +.header a:hover { color: var(--foreground-strong); } +.layout { display: grid; grid-template-columns: 46% minmax(0, 1fr); min-height: 100svh; } +.intro { position: relative; isolation: isolate; display: flex; flex-direction: column; min-width: 0; padding: 38px clamp(32px, 5vw, 80px) 30px; overflow: hidden; background: var(--background); color: var(--foreground); } +.intro::before { content: ''; position: absolute; z-index: -1; inset: 0; background: radial-gradient(ellipse at 0% 35%, color-mix(in srgb, var(--el-deep-mocha) 85%, transparent), transparent 60%), radial-gradient(ellipse at 100% 92%, color-mix(in srgb, var(--primary) 45%, transparent), transparent 65%); } +.brand { align-self: flex-start; } +.story { margin: auto 0; padding-top: 64px; } +.eyebrow { margin: 0 0 20px; font-size: 10px; letter-spacing: .18em; color: var(--muted-foreground); line-height: 1.7; } +.story h1 { margin: 0 0 22px; font-size: clamp(32px, 3.3vw, 50px); font-weight: 600; line-height: 1.3; letter-spacing: -.035em; color: var(--foreground-strong); } +.description { margin: 0; max-width: 340px; font-size: 15px; line-height: 1.9; color: var(--foreground); } +.signature { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 32px 0 0; font-size: 11px; line-height: 1.7; color: var(--muted-foreground); } +.signature span { opacity: .8; } +.art { position: relative; height: 300px; margin: 28px -12px 0; pointer-events: none; } +.orbit { position: absolute; width: 245px; height: 245px; left: 15%; top: 5%; border: 1px solid color-mix(in srgb, var(--foreground) 18%, transparent); border-radius: 50%; } +.orbit::after { content: ''; position: absolute; inset: 25px -35px; border: inherit; border-radius: 50%; transform: rotate(-38deg); } +.idea, .work { position: absolute; display: flex; flex-direction: column; border: 1px solid color-mix(in srgb, var(--foreground) 24%, transparent); border-radius: 14px; box-shadow: 0 20px 50px var(--shadow); } +.idea { left: 0; top: 42px; width: 48%; min-height: 196px; padding: 22px 18px; transform: rotate(-8deg); background: color-mix(in srgb, var(--surface) 78%, transparent); backdrop-filter: blur(16px); } +.idea span, .work > span { font-size: 8px; letter-spacing: .12em; line-height: 1.6; } +.idea strong { margin: 24px 0 18px; font-size: clamp(18px, 1.8vw, 26px); font-weight: 500; line-height: 1.5; } +.idea i, .work i { font-size: 9px; font-style: normal; line-height: 1.7; opacity: .72; } +.work { right: 0; top: 15px; width: 51%; padding: 18px; transform: rotate(8deg); background: var(--el-cream); color: var(--el-espresso); } +.artwork { height: 110px; margin: 16px 0 14px; overflow: hidden; border-radius: 100px 100px 8px 8px; background: radial-gradient(ellipse at 75% 20%, var(--el-cream), transparent 60%), linear-gradient(135deg, var(--el-deep-mocha), var(--el-espresso)); } +.work strong { margin-bottom: 6px; font-size: 19px; font-weight: 500; } +.artCaption { position: absolute; bottom: 0; left: 0; right: 0; text-align: center; font-size: 8px; letter-spacing: .2em; color: var(--muted-foreground); } +.page .form { align-self: center; width: min(400px, calc(100% - 96px)); min-width: 0; margin: 116px auto 80px; padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; } +.page .form h2 { font-size: 28px; font-weight: 600; text-align: center; letter-spacing: -.04em; } +.page .form > p:first-of-type { font-size: 14px; text-align: center; margin: 10px 0 32px; } +.page .form label { font-size: 13px; font-weight: 500; } +.page .form input { min-height: 46px; font-size: 14px; background: var(--surface); border: 1px solid var(--input); border-radius: 8px; box-shadow: 0 1px 2px var(--shadow); } +.page .form nav { background: var(--surface-muted); padding: 4px; margin-bottom: 28px; border: 1px solid var(--border); border-radius: 9px; } +.page .form nav button { min-height: 36px; font-size: 13px; } +.page .form button[type='submit'] { min-height: 46px; font-size: 14px; border-radius: 8px; margin-top: 8px; } +.page .form form + div { margin-top: 22px; } +.page .form form + div button { font-size: 12px; } +.page .form > p:last-child { margin-top: 32px !important; font-size: 12px; line-height: 1.8; } +@media (min-width: 1500px) { .story { max-width: 480px; width: 100%; margin-left: auto; margin-right: auto; } .art { height: 330px; } .brand, .signature { margin-left: 0; } } +@media (max-width: 1000px) { .intro { padding-left: 28px; padding-right: 28px; } .story h1 { font-size: 34px; } .description { font-size: 14px; } .page .form { width: min(380px, calc(100% - 64px)); } .header { right: 28px; } } +@media (max-width: 760px) { + .header { position: static; display: flex; justify-content: flex-end; padding: 16px 20px; } + .layout { display: flex; flex-direction: column; min-height: auto; } + .intro { margin: 0 16px; padding: 24px; border-radius: 16px; } + .brand { margin-bottom: 28px; } + .story { margin: 0; padding: 0; } + .eyebrow { margin-bottom: 12px; font-size: 9px; } + .story h1 { font-size: 30px; margin-bottom: 14px; } + .description { max-width: 420px; font-size: 13px; line-height: 1.8; } + .art, .signature { display: none; } + .page .form { width: min(400px, calc(100% - 48px)); margin: 40px auto 48px; } + .page .form h2 { font-size: 25px; } +} diff --git a/apps/waterfall-wiki/src/styles/guidance.module.css b/apps/waterfall-wiki/src/styles/guidance.module.css index b31fe65..56352c8 100644 --- a/apps/waterfall-wiki/src/styles/guidance.module.css +++ b/apps/waterfall-wiki/src/styles/guidance.module.css @@ -1678,70 +1678,11 @@ height: 31px; color: var(--muted-foreground); } -.accountLayout { - display: grid; - grid-template-columns: minmax(0, 1fr) 400px; - max-width: 1000px; - gap: 80px; - align-items: center; - margin: 40px auto 64px; -} -.accountIntro { - min-width: 0; - max-width: 460px; - padding: 16px 20px 16px 8px; -} -.accountIntro > .eyebrow { - display: block; - margin-bottom: 20px; - font-size: 11px; - letter-spacing: .14em; - line-height: 1.6; -} -.accountIntro h1 { - margin: 0 0 22px; - font-size: 36px; - line-height: 1.45; - letter-spacing: -.035em; - font-weight: 650; - text-wrap: balance; -} -.accountIntro > p { - margin: 0; - font-size: 15px; - line-height: 1.9; - color: var(--muted-foreground); - max-width: 380px; -} -.accountFeatures { - display: grid; - gap: 22px; - margin-top: 32px; - padding-top: 26px; - border-top: 1px solid var(--border); -} -.accountFeatures > div { - display: grid; - grid-template-columns: 20px minmax(0, 1fr); - align-items: start; - gap: 14px; -} -.accountFeatures svg { - color: var(--muted-foreground); - margin-top: 3px; -} -.accountFeatures strong { - display: block; - font-size: 14px; - line-height: 1.6; - font-weight: 600; -} -.accountFeatures p { - margin: 4px 0 0; - font-size: 13px; - color: var(--muted-foreground); - line-height: 1.8; -} + + + + + .authCard { padding: 28px; border: 1px solid var(--line); @@ -1926,10 +1867,7 @@ grid-template-columns: minmax(0, 1fr) 230px; gap: 20px; } - .accountLayout { - gap: 48px; - grid-template-columns: minmax(0, 1fr) 370px; - } + .protocolLayout { grid-template-columns: minmax(0, 1fr) 190px; } @@ -1969,34 +1907,9 @@ .protocolLayout > aside { display: none; } - .accountLayout { - grid-template-columns: 1fr; - max-width: 430px; - margin: 15px auto 40px; - gap: 25px; - } - .accountIntro { - padding: 0 4px; - max-width: none; - } - .accountIntro > .eyebrow { - margin-bottom: 12px; - font-size: 10px; - } - .accountIntro h1 { - font-size: 26px; - margin-bottom: 8px; - } - .accountFeatures { - display: none; - } - .accountIntro > p { - max-width: none; - font-size: 14px; - } - .accountIntro h1 br { - display: none; - } + + + .topbarProtocol { display: none; } @@ -2307,16 +2220,8 @@ .authCard { padding: 24px; } - .accountLayout { - margin-top: 8px; - gap: 24px; - } - .accountIntro h1 { - font-size: 24px; - } - .accountIntro > p { - font-size: 14px; - } + + .modalBody { padding: 23px; } @@ -2382,14 +2287,3 @@ .primaryButton,.primaryButton:hover{color:var(--primary-foreground);background:var(--primary)} .primaryButton:hover{background:var(--primary-hover)} .primaryButton:disabled{background:var(--disabled-background);color:var(--disabled-foreground)} - -/* Authentication has its own public page, without workspace navigation. */ -.accountPage { min-height: 100vh; background: var(--background); color: var(--foreground); } -.accountHeader { max-width: 1200px; margin: 0 auto; padding: 24px 32px; display: flex; align-items: center; justify-content: space-between; gap: 20px; } -.accountHeader a { color: var(--muted-foreground); text-decoration: none; } -.accountHeader > div { display: flex; align-items: center; gap: 20px; font-size: 13px; } -.accountPage > main { max-width: 1120px; margin: 0 auto; padding: 48px 32px 72px; } -@media (max-width: 650px) { - .accountHeader { padding: 20px; } - .accountPage > main { padding: 20px 20px 48px; } -} -- 2.52.0 From 8c48812a11a007f9a5b954cc7bdb06429f87eb17 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 23:24:31 +0800 Subject: [PATCH 3/6] test(auth): capture deployed split login layouts in release acceptance --- scripts/verify-studio-visual.cjs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/scripts/verify-studio-visual.cjs b/scripts/verify-studio-visual.cjs index 31dbb6e..dee8da8 100644 --- a/scripts/verify-studio-visual.cjs +++ b/scripts/verify-studio-visual.cjs @@ -27,6 +27,26 @@ const assert=require('node:assert/strict'); } if(checkLegacy&&!process.argv.includes('--diagnose'))assert.deepEqual(metrics.filter(m=>m.overflow.length),[],'Default copy must fit every native slot'); await page.goto(base+'/',{waitUntil:'networkidle'});await page.getByRole('heading',{name:/想法不该停在对话里/}).waitFor();await page.screenshot({path:dir+'/landing-desktop.png',fullPage:true}); + // Verify the public account flow in this fresh context, without submitting credentials. + await page.setViewportSize({width:1128,height:959}); + await page.goto(base+'/account?next=/workspace',{waitUntil:'domcontentloaded'}); + await page.locator('button[data-theme-ready="true"]').waitFor(); + await page.getByLabel('邮箱地址',{exact:true}).waitFor(); + assert.equal(await page.locator('aside').count(),0,'Authentication must not include a workspace sidebar'); + assert.equal(await page.getByRole('link',{name:'创建文档',exact:true}).count(),0); + assert.equal(await page.locator('.endoleap-brand').count(),1,'Only one brand belongs on the account page'); + const accountSections=await page.locator('#account-content > div > section').evaluateAll(elements=>elements.map(el=>{const r=el.getBoundingClientRect();return {x:r.x,right:r.right,width:r.width}})); + assert.equal(accountSections.length,2); + assert.ok(accountSections[0].right<=accountSections[1].x,'Brand panel and credentials occupy separate columns'); + await page.screenshot({path:dir+'/account-desktop.png',fullPage:true}); + await page.getByRole('button',{name:'创建账户',exact:true}).click(); + await page.getByRole('heading',{name:'创建你的账户',exact:true}).waitFor(); + await page.screenshot({path:dir+'/account-register.png',fullPage:true}); + await page.getByRole('button',{name:'登录',exact:true}).click(); + await page.setViewportSize({width:390,height:844}); + assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'Mobile account page must not overflow horizontally'); + await page.screenshot({path:dir+'/account-mobile.png',fullPage:true}); + await page.setViewportSize({width:1100,height:1000}); await page.goto(base+'/graphic',{waitUntil:'networkidle'}); assert.equal(new URL(page.url()).pathname,'/graphic'); await page.getByRole('button',{name:'添加文字',exact:true}).waitFor({state:'visible'}); -- 2.52.0 From 1da1114d52a0bd5f7bf3fd7df2cf8ffc0adb7255 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 23:33:25 +0800 Subject: [PATCH 4/6] fix(auth): clarify verification resend eligibility --- apps/waterfall-wiki/src/pages/account.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/waterfall-wiki/src/pages/account.tsx b/apps/waterfall-wiki/src/pages/account.tsx index 4a405ba..4d5d73f 100644 --- a/apps/waterfall-wiki/src/pages/account.tsx +++ b/apps/waterfall-wiki/src/pages/account.tsx @@ -142,7 +142,7 @@ function AccountContent() { : mode === "register" ? "验证邮箱后,就可以发布第一篇文档。" : mode === "resend" - ? "输入注册时的邮箱和密码,重新获取验证链接。" + ? "仅适用于尚未验证的账户。已验证的邮箱可直接登录。" : "使用邮箱与密码登录,继续你的知识工作。"}

{token && ( @@ -326,7 +326,7 @@ function AccountContent() { )}

- 注册后需要先验证邮箱。公开文档始终可以直接阅读。 + 首次注册需要验证邮箱;已验证的账户可直接登录。公开文档始终可以直接阅读。

)} -- 2.52.0 From ee72835ea38cd0a853d6f263dae3e6010df0e9db Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 23:50:32 +0800 Subject: [PATCH 5/6] feat(auth): add recovery UI and stable viewport login interactions --- apps/waterfall-wiki/src/pages/account.tsx | 359 ++++-------------- .../src/styles/account.module.css | 99 +++-- .../tests/guidance-workspace-ui.ts | 40 ++ scripts/verify-studio-visual.cjs | 7 +- 4 files changed, 196 insertions(+), 309 deletions(-) diff --git a/apps/waterfall-wiki/src/pages/account.tsx b/apps/waterfall-wiki/src/pages/account.tsx index 4d5d73f..d427b8c 100644 --- a/apps/waterfall-wiki/src/pages/account.tsx +++ b/apps/waterfall-wiki/src/pages/account.tsx @@ -12,9 +12,8 @@ import { MailCheck, } from "lucide-react"; import GuidanceShell, { useGuidanceAccount } from "@/components/GuidanceShell"; -import { Notice } from "@/components/GuidanceUI"; import s from "@/styles/guidance.module.css"; -type Mode = "login" | "register" | "resend"; +type Mode = "login" | "register" | "resend" | "forgot" | "reset" | "verify"; function localNext(value: string | string[] | undefined) { if ( typeof value !== "string" || @@ -33,304 +32,104 @@ function localNext(value: string | string[] | undefined) { } } export default function AccountPage() { - return ( - - - - ); + return ; } function AccountContent() { - const account = useGuidanceAccount(), - router = useRouter(); - const [mode, setMode] = useState("login"), - [email, setEmail] = useState(""), - [password, setPassword] = useState(""), - [showPassword, setShowPassword] = useState(false); - const [busy, setBusy] = useState(false), - [token, setToken] = useState(""), - [feedback, setFeedback] = useState<{ - tone: "success" | "error"; - text: string; - } | null>(null); + const account = useGuidanceAccount(), router = useRouter(); + const [mode, setMode] = useState("login"); + const [email, setEmail] = useState(""), [password, setPassword] = useState(""), [confirm, setConfirm] = useState(""); + const [showPassword, setShowPassword] = useState(false), [busy, setBusy] = useState(false); + const [token, setToken] = useState(""); + const [feedback, setFeedback] = useState<{tone: "success" | "error"; text: string} | null>(null); useEffect(() => { const url = new URL(window.location.href); - const value = url.searchParams.get("verify"); - if (value) { - setToken(value); - url.searchParams.delete("verify"); + const reset = url.searchParams.get("reset"), verify = url.searchParams.get("verify"); + if (reset || verify) { + setToken(reset || verify || ""); setMode(reset ? "reset" : "verify"); + url.searchParams.delete("reset"); url.searchParams.delete("verify"); window.history.replaceState({}, "", url.pathname + url.search + url.hash); } }, []); - async function action(name: Mode | "verify" | "logout", body: object = {}) { + function switchMode(value: Mode) { setMode(value); setFeedback(null); setPassword(""); setConfirm(""); setShowPassword(false); } + async function action(name: Mode | "logout", body: object = {}) { if (busy) return; - setBusy(true); - setFeedback(null); + setBusy(true); setFeedback(null); try { - const response = await fetch("/api/account/" + name, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); + const response = await fetch("/api/account/" + name, {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(body)}); const data = await response.json(); if (!response.ok) throw Error(data.error || "暂时无法完成操作,请重试"); - if (name === "verify") { - setToken(""); - setMode("login"); - setFeedback({ tone: "success", text: "邮箱已验证,现在可以登录。" }); - } else if (name === "login") { - setPassword(""); - setFeedback({ tone: "success", text: "登录成功,欢迎回到啡跃。" }); - } else if (name === "logout") { - setPassword(""); - setFeedback({ tone: "success", text: "已退出登录。" }); - } else - setFeedback({ - tone: "success", - text: - data.message || - "如果信息符合条件,验证邮件已发送。请检查收件箱和垃圾邮件。", - }); + if (name === "verify" || name === "reset") { setToken(""); switchMode("login"); } + if (name === "login" || name === "logout" || name === "reset") { setPassword(""); setConfirm(""); } + setFeedback({tone: "success", text: name === "verify" ? "邮箱已验证,现在可以登录。" : name === "reset" ? "密码已更新,请使用新密码登录。" : name === "login" ? "登录成功,欢迎回到啡跃。" : name === "logout" ? "已退出登录。" : data.message || "若账户符合条件,邮件将发送到你的邮箱。"}); await account.refresh(); if (name === "login") await router.push(localNext(router.query.next)); - } catch (e) { - setFeedback({ - tone: "error", - text: e instanceof Error ? e.message : "网络异常,请稍后重试", - }); - } finally { - setBusy(false); - } + } catch (error) { setFeedback({tone: "error", text: error instanceof Error ? error.message : "网络异常,请稍后重试"}); } + finally { setBusy(false); } } function submit(event: FormEvent) { event.preventDefault(); - void action(mode, { email, password }); + if (mode === "reset" && password !== confirm) { setFeedback({tone: "error", text: "两次输入的密码不一致。"}); return; } + void action(mode, mode === "reset" ? {token, password} : mode === "verify" ? {token} : {email, password}); } - function switchMode(value: Mode) { - setMode(value); - setFeedback(null); - } - return ( -
-
- -
-

A SPACE FOR YOUR NEXT IDEA

-

给想法留白,
让灵感有形。

-

收集一闪而过的灵感,把有用的知识连接起来。下一件作品,从这里开始。

- + + + +
{mode === "login" || mode === "register" ? : }
+

{mode === "forgot" || mode === "reset" ? "链接仅供本人使用。重设成功后,所有旧会话将退出。" : "首次注册需要验证邮箱;已验证的账户可直接登录。"}

+ } +
+
+ {toast &&

{toast.text}

} +
; } diff --git a/apps/waterfall-wiki/src/styles/account.module.css b/apps/waterfall-wiki/src/styles/account.module.css index 4dc6d79..e1f7581 100644 --- a/apps/waterfall-wiki/src/styles/account.module.css +++ b/apps/waterfall-wiki/src/styles/account.module.css @@ -1,18 +1,19 @@ -.page { min-height: 100svh; background: var(--surface); color: var(--foreground); } +.page { height: 100dvh; overflow: hidden; background: var(--surface); color: var(--foreground); } +.page > main { height: 100%; min-height: 0; } .header { position: absolute; z-index: 2; top: 28px; right: 36px; } .header > div { display: flex; align-items: center; gap: 20px; font-size: 13px; color: var(--muted-foreground); } .header a:hover { color: var(--foreground-strong); } -.layout { display: grid; grid-template-columns: 46% minmax(0, 1fr); min-height: 100svh; } +.layout { display: grid; grid-template-columns: 46% minmax(0, 1fr); height: 100%; min-height: 0; } .intro { position: relative; isolation: isolate; display: flex; flex-direction: column; min-width: 0; padding: 38px clamp(32px, 5vw, 80px) 30px; overflow: hidden; background: var(--background); color: var(--foreground); } .intro::before { content: ''; position: absolute; z-index: -1; inset: 0; background: radial-gradient(ellipse at 0% 35%, color-mix(in srgb, var(--el-deep-mocha) 85%, transparent), transparent 60%), radial-gradient(ellipse at 100% 92%, color-mix(in srgb, var(--primary) 45%, transparent), transparent 65%); } .brand { align-self: flex-start; } -.story { margin: auto 0; padding-top: 64px; } +.story { margin: auto 0; padding-top: clamp(16px, 4dvh, 40px); } .eyebrow { margin: 0 0 20px; font-size: 10px; letter-spacing: .18em; color: var(--muted-foreground); line-height: 1.7; } .story h1 { margin: 0 0 22px; font-size: clamp(32px, 3.3vw, 50px); font-weight: 600; line-height: 1.3; letter-spacing: -.035em; color: var(--foreground-strong); } .description { margin: 0; max-width: 340px; font-size: 15px; line-height: 1.9; color: var(--foreground); } .signature { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 32px 0 0; font-size: 11px; line-height: 1.7; color: var(--muted-foreground); } .signature span { opacity: .8; } -.art { position: relative; height: 300px; margin: 28px -12px 0; pointer-events: none; } +.art { position: relative; height: clamp(180px, 28dvh, 280px); margin: 28px -12px 0; pointer-events: none; } .orbit { position: absolute; width: 245px; height: 245px; left: 15%; top: 5%; border: 1px solid color-mix(in srgb, var(--foreground) 18%, transparent); border-radius: 50%; } .orbit::after { content: ''; position: absolute; inset: 25px -35px; border: inherit; border-radius: 50%; transform: rotate(-38deg); } .idea, .work { position: absolute; display: flex; flex-direction: column; border: 1px solid color-mix(in srgb, var(--foreground) 24%, transparent); border-radius: 14px; box-shadow: 0 20px 50px var(--shadow); } @@ -24,29 +25,71 @@ .artwork { height: 110px; margin: 16px 0 14px; overflow: hidden; border-radius: 100px 100px 8px 8px; background: radial-gradient(ellipse at 75% 20%, var(--el-cream), transparent 60%), linear-gradient(135deg, var(--el-deep-mocha), var(--el-espresso)); } .work strong { margin-bottom: 6px; font-size: 19px; font-weight: 500; } .artCaption { position: absolute; bottom: 0; left: 0; right: 0; text-align: center; font-size: 8px; letter-spacing: .2em; color: var(--muted-foreground); } -.page .form { align-self: center; width: min(400px, calc(100% - 96px)); min-width: 0; margin: 116px auto 80px; padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; } -.page .form h2 { font-size: 28px; font-weight: 600; text-align: center; letter-spacing: -.04em; } -.page .form > p:first-of-type { font-size: 14px; text-align: center; margin: 10px 0 32px; } -.page .form label { font-size: 13px; font-weight: 500; } -.page .form input { min-height: 46px; font-size: 14px; background: var(--surface); border: 1px solid var(--input); border-radius: 8px; box-shadow: 0 1px 2px var(--shadow); } -.page .form nav { background: var(--surface-muted); padding: 4px; margin-bottom: 28px; border: 1px solid var(--border); border-radius: 9px; } -.page .form nav button { min-height: 36px; font-size: 13px; } -.page .form button[type='submit'] { min-height: 46px; font-size: 14px; border-radius: 8px; margin-top: 8px; } -.page .form form + div { margin-top: 22px; } -.page .form form + div button { font-size: 12px; } -.page .form > p:last-child { margin-top: 32px !important; font-size: 12px; line-height: 1.8; } -@media (min-width: 1500px) { .story { max-width: 480px; width: 100%; margin-left: auto; margin-right: auto; } .art { height: 330px; } .brand, .signature { margin-left: 0; } } -@media (max-width: 1000px) { .intro { padding-left: 28px; padding-right: 28px; } .story h1 { font-size: 34px; } .description { font-size: 14px; } .page .form { width: min(380px, calc(100% - 64px)); } .header { right: 28px; } } + +.form { align-self: center; justify-self: center; width: min(400px, calc(100% - 80px)); min-width: 0; padding: 0; } +.frame { height: 560px; display: flex; flex-direction: column; justify-content: center; } +.frame h2 { margin: 0; font-size: 28px; font-weight: 600; line-height: 1.3; text-align: center; letter-spacing: -.04em; animation: mode-in 180ms ease-out; } +.formDescription { height: 42px; flex-shrink: 0; margin: 10px 0 20px; font-size: 14px; line-height: 1.5; text-align: center; color: var(--muted-foreground); } +.tabSlot { height: 44px; flex-shrink: 0; margin-bottom: 24px; } +.tabs { position: relative; display: grid; grid-template-columns: 1fr 1fr; height: 44px; padding: 4px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface-muted); isolation: isolate; } +.tabs::before { content: ''; position: absolute; z-index: -1; top: 4px; bottom: 4px; left: 4px; width: calc(50% - 4px); border-radius: 5px; background: var(--surface); box-shadow: 0 1px 3px var(--shadow); transition: transform 180ms ease; } +.tabs[data-active='register']::before { transform: translateX(100%); } +.tabs button { border: 0; background: transparent; font-size: 13px; color: var(--muted-foreground); } +.tabs button[aria-pressed='true'] { color: var(--foreground-strong); font-weight: 600; } +.fieldset { margin: 0; padding: 0; min-width: 0; border: 0; } +.fields { height: 168px; display: flex; flex-direction: column; gap: 16px; animation: mode-in 180ms ease-out; } +.field { min-width: 0; } +.field label { display: block; margin-bottom: 7px; font-size: 13px; line-height: 20px; font-weight: 500; } +.labelRow { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; } +.labelRow button { font-size: 12px; color: var(--muted-foreground); } +.labelRow button:hover { color: var(--foreground-strong); } +.page .form input { width: 100%; height: 44px; min-height: 44px; padding: 0 12px; font-size: 14px; background: var(--surface); border: 1px solid var(--input); border-radius: 8px; box-shadow: 0 1px 2px var(--shadow); } +.page .form input[name='password'] { padding-right: 44px; } +.page .submit { width: 100%; min-height: 44px; margin-top: 8px; font-size: 14px; border-radius: 8px; } +.secondarySlot { min-height: 38px; display: grid; place-items: center; margin-top: 14px; } +.secondary { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 32px; padding: 0; font-size: 12px; color: var(--muted-foreground); text-decoration: underline; text-underline-offset: 4px; } +.footnote { height: 60px; flex-shrink: 0; margin: 18px 0 0; padding-top: 16px; border-top: 1px solid var(--border); font-size: 12px; line-height: 1.7; text-align: center; color: var(--muted-foreground); } +.verifyInfo, .accountDetails { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; height: 168px; text-align: center; color: var(--muted-foreground); } +.accountDetails strong { color: var(--foreground-strong); overflow-wrap: anywhere; } +.accountDetails span, .verifyInfo p { font-size: 13px; line-height: 1.8; } +.loading { height: 320px; display: grid; place-items: center; font-size: 13px; color: var(--muted-foreground); } +.toast { position: fixed; z-index: 50; right: 20px; bottom: 20px; width: min(420px, calc(100% - 40px)); display: flex; align-items: flex-start; gap: 10px; padding: 14px 16px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); color: var(--foreground); box-shadow: 0 8px 32px var(--shadow); animation: mode-in 180ms ease-out; } +.toast > span { flex: 0 0 20px; height: 20px; text-align: center; border-radius: 50%; background: var(--success-background); color: var(--success); font-size: 13px; font-weight: 600; } +.toast[data-tone='error'] > span { color: var(--danger); background: var(--danger-background); } +.toast p { flex: 1; min-width: 0; margin: 0; font-size: 13px; line-height: 1.6; overflow-wrap: anywhere; } +.toast button { flex: 0 0 24px; min-height: 24px; color: var(--muted-foreground); } +@keyframes mode-in { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } +@media (min-width: 1500px) { .story { max-width: 480px; width: 100%; margin-left: auto; margin-right: auto; } } +@media (max-width: 1000px) { .intro { padding-left: 28px; padding-right: 28px; } .story h1 { font-size: 34px; } .description { font-size: 14px; } .form { width: min(380px, calc(100% - 64px)); } .header { right: 28px; } } @media (max-width: 760px) { - .header { position: static; display: flex; justify-content: flex-end; padding: 16px 20px; } - .layout { display: flex; flex-direction: column; min-height: auto; } - .intro { margin: 0 16px; padding: 24px; border-radius: 16px; } - .brand { margin-bottom: 28px; } - .story { margin: 0; padding: 0; } - .eyebrow { margin-bottom: 12px; font-size: 9px; } - .story h1 { font-size: 30px; margin-bottom: 14px; } - .description { max-width: 420px; font-size: 13px; line-height: 1.8; } - .art, .signature { display: none; } - .page .form { width: min(400px, calc(100% - 48px)); margin: 40px auto 48px; } - .page .form h2 { font-size: 25px; } + .header { top: 20px; right: 20px; } + .layout { grid-template-columns: minmax(0, 1fr); } + .intro { position: absolute; top: 20px; left: 24px; padding: 0; background: transparent; overflow: visible; } + .intro::before, .story, .signature { display: none; } + .brand { padding: 5px; border-radius: 8px; background: var(--background); } + .form { width: min(400px, calc(100% - 48px)); padding-top: 40px; } + .frame h2 { font-size: 25px; } } +@media (max-height: 760px) { + .art, .signature { display: none; } + .intro { padding-top: 24px; padding-bottom: 24px; } + .formDescription { height: 34px; font-size: 13px; margin: 8px 0 14px; } + .frame { height: 460px; } + .frame h2 { font-size: 25px; } + .tabSlot { margin-bottom: 18px; } + .fields { height: 158px; gap: 12px; } + .footnote { display: none; } + .secondarySlot { margin-top: 10px; } +} +@media (max-height: 560px) { + .header a { display: none; } + .formDescription { display: none; } + .frame { height: 340px; } + .frame h2 { font-size: 22px; margin-bottom: 14px; } + .tabSlot { height: 36px; margin-bottom: 14px; } + .tabs { height: 36px; } + .story h1 { font-size: 28px; } + .eyebrow { display: none; } +} +@media (max-width: 760px) and (max-height: 760px) { .intro { padding: 0; } } +@media (prefers-reduced-motion: reduce) { .frame h2, .fields, .toast { animation: none; } .tabs::before { transition: none; } } diff --git a/apps/waterfall-wiki/tests/guidance-workspace-ui.ts b/apps/waterfall-wiki/tests/guidance-workspace-ui.ts index dbd7264..b8b3956 100644 --- a/apps/waterfall-wiki/tests/guidance-workspace-ui.ts +++ b/apps/waterfall-wiki/tests/guidance-workspace-ui.ts @@ -164,6 +164,26 @@ export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser) assert.equal(await page.getByRole("complementary").count(), 0, "Account page has no workspace sidebar"); assert.equal(await page.getByRole("link", { name: "创建文档", exact: true }).count(), 0); await page.getByRole("link", { name: "返回首页", exact: true }).waitFor(); + const accountFits = async () => assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1 && document.documentElement.scrollHeight <= innerHeight + 1), "Account page fits the viewport on both axes"); + await accountFits(); + await context.route("**/api/account/login", route => route.fulfill({status: 401, json: {error: "测试登录失败,请重试"}})); + await page.getByLabel("邮箱地址", {exact: true}).fill("fixture@example.com"); + await page.getByLabel("密码", {exact: true}).fill("fixture-password-long"); + await page.waitForFunction(() => document.getAnimations().every(animation => animation.playState !== "running")); + const beforeMessage = await page.getByLabel("邮箱地址", {exact: true}).boundingBox(); + await page.locator("form").getByRole("button", {name: "登录", exact: true}).click(); + await page.getByRole("alert").getByText("测试登录失败,请重试").waitFor(); + const afterMessage = await page.getByLabel("邮箱地址", {exact: true}).boundingBox(); + assert.equal(afterMessage?.y, beforeMessage?.y, "Status messages must not move the form"); + await accountFits(); + await page.getByRole("button", {name: "关闭提示", exact: true}).click(); + await context.route("**/api/account/forgot", route => route.fulfill({json: {ok: true, message: "若账户符合条件,重设链接将发送到邮箱。"}})); + await page.getByRole("button", {name: "忘记密码?", exact: true}).click(); + await page.getByRole("heading", {name: "找回你的账户", exact: true}).waitFor(); + await page.getByLabel("邮箱地址", {exact: true}).fill("fixture@example.com"); + await page.getByRole("button", {name: "发送重设链接", exact: true}).click(); + await page.getByRole("status").getByText("若账户符合条件,重设链接将发送到邮箱。").waitFor(); + await page.getByRole("button", {name: "返回登录", exact: true}).click(); await page.getByRole("button", { name: "创建账户", exact: true }).click(); await page.getByRole("heading", { name: "创建你的账户" }).waitFor(); await page @@ -181,6 +201,26 @@ export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser) path: process.env.WORKSPACE_SCREENSHOT_DIR + "/account.png", fullPage: true, }); + for (const viewport of [{width: 390, height: 844}, {width: 320, height: 568}]) { + await page.setViewportSize(viewport); + await accountFits(); + await page.getByLabel("邮箱地址", {exact: true}).waitFor({state: "visible"}); + } + await page.setViewportSize({width: 1440, height: 1000}); + await context.route("**/api/account/reset", async route => { + const body = route.request().postDataJSON(); + assert.equal(body.token, "f".repeat(64)); + assert.equal(body.password, "replacement-fixture-password"); + await route.fulfill({json: {ok: true}}); + }); + await visitBrowserDocument(page, base + "/account?reset=" + "f".repeat(64)); + await page.getByRole("heading", {name: "设置新密码", exact: true}).waitFor(); + assert.equal(new URL(page.url()).searchParams.has("reset"), false, "Recovery token is removed from the visible URL"); + await page.getByLabel("新密码", {exact: true}).fill("replacement-fixture-password"); + await page.getByLabel("确认新密码", {exact: true}).fill("replacement-fixture-password"); + await page.getByRole("button", {name: "保存新密码", exact: true}).click(); + await page.getByRole("heading", {name: "欢迎回来", exact: true}).waitFor(); + await accountFits(); await navigate(base + "/write"); await page.getByRole("heading", { name: "登录后开始编写" }).waitFor(); signedIn = true; diff --git a/scripts/verify-studio-visual.cjs b/scripts/verify-studio-visual.cjs index dee8da8..99d5a7d 100644 --- a/scripts/verify-studio-visual.cjs +++ b/scripts/verify-studio-visual.cjs @@ -38,14 +38,19 @@ const assert=require('node:assert/strict'); const accountSections=await page.locator('#account-content > div > section').evaluateAll(elements=>elements.map(el=>{const r=el.getBoundingClientRect();return {x:r.x,right:r.right,width:r.width}})); assert.equal(accountSections.length,2); assert.ok(accountSections[0].right<=accountSections[1].x,'Brand panel and credentials occupy separate columns'); + const accountFits=async()=>assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1&&document.documentElement.scrollHeight<=innerHeight+1),'Account document fits both viewport axes'); + const accountControlsFit=async()=>assert.ok(await page.locator('#account-content input, #account-content button[type="submit"]').evaluateAll(elements=>elements.every(el=>{const r=el.getBoundingClientRect();return r.left>=0&&r.right<=innerWidth&&r.top>=0&&r.bottom<=innerHeight})),'Account inputs and submit button remain visible, not merely clipped'); + await accountFits();await accountControlsFit(); await page.screenshot({path:dir+'/account-desktop.png',fullPage:true}); await page.getByRole('button',{name:'创建账户',exact:true}).click(); await page.getByRole('heading',{name:'创建你的账户',exact:true}).waitFor(); await page.screenshot({path:dir+'/account-register.png',fullPage:true}); await page.getByRole('button',{name:'登录',exact:true}).click(); await page.setViewportSize({width:390,height:844}); - assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'Mobile account page must not overflow horizontally'); + await accountFits();await accountControlsFit(); await page.screenshot({path:dir+'/account-mobile.png',fullPage:true}); + await page.setViewportSize({width:320,height:568});await accountFits();await accountControlsFit(); + await page.screenshot({path:dir+'/account-mobile-compact.png',fullPage:true}); await page.setViewportSize({width:1100,height:1000}); await page.goto(base+'/graphic',{waitUntil:'networkidle'}); assert.equal(new URL(page.url()).pathname,'/graphic'); -- 2.52.0 From fd96b6d9e9c856e9fb78c7a6711f469dba48a2a7 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 23:54:21 +0800 Subject: [PATCH 6/6] fix(auth): preserve form state after email-only actions --- apps/waterfall-wiki/src/pages/account.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/waterfall-wiki/src/pages/account.tsx b/apps/waterfall-wiki/src/pages/account.tsx index d427b8c..b1360da 100644 --- a/apps/waterfall-wiki/src/pages/account.tsx +++ b/apps/waterfall-wiki/src/pages/account.tsx @@ -61,7 +61,7 @@ function AccountContent() { if (name === "verify" || name === "reset") { setToken(""); switchMode("login"); } if (name === "login" || name === "logout" || name === "reset") { setPassword(""); setConfirm(""); } setFeedback({tone: "success", text: name === "verify" ? "邮箱已验证,现在可以登录。" : name === "reset" ? "密码已更新,请使用新密码登录。" : name === "login" ? "登录成功,欢迎回到啡跃。" : name === "logout" ? "已退出登录。" : data.message || "若账户符合条件,邮件将发送到你的邮箱。"}); - await account.refresh(); + if (["verify", "login", "logout", "reset"].includes(name)) await account.refresh(); if (name === "login") await router.push(localNext(router.query.next)); } catch (error) { setFeedback({tone: "error", text: error instanceof Error ? error.message : "网络异常,请稍后重试"}); } finally { setBusy(false); } -- 2.52.0