diff --git a/src/app/dashboard/[workspace]/[board]/page.tsx b/src/app/dashboard/[workspace]/[board]/page.tsx index 0411059..39fb66f 100644 --- a/src/app/dashboard/[workspace]/[board]/page.tsx +++ b/src/app/dashboard/[workspace]/[board]/page.tsx @@ -5,7 +5,8 @@ import { Suspense } from "react"; import GraphPage from "@/components/workspace/graph-page"; -/** params按当前Next版本await;Suspense给客户端画布准备过程提供状态占位,不在此发模型请求。 */ +/** params按当前Next版本await;Suspense为子树渲染时的挂起提供占位,不在此发模型请求。 + * params 的 await 在此边界外;GraphPage 在 effect 中加载模型时的进度/错误由自身状态呈现。 */ export default async function Page({ params }: { params: Promise<{ workspace: string; board: string }> }) { const { workspace, board } = await params; return ( diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 099b018..9311a54 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -8,7 +8,8 @@ import { ActiveBoardProvider } from "@/components/workspace/active-board"; import DashboardSidebar from "./dashboard"; import styles from "@/components/workspace/workspace.module.css"; -/** 当前board信息由ActiveBoardProvider跨侧栏/画布共享;Suspense只包侧栏,fallback保留其布局宽度。 */ +/** 当前board信息由ActiveBoardProvider跨侧栏/画布共享;Suspense只包侧栏,fallback保留其布局宽度。 + * 该边界响应侧栏渲染期间的 suspend,不自动等待侧栏在 effect 中发起的普通请求。 */ export default function DashboardLayout({ children }: { children: ReactNode }) { return ( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 020a238..508607f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -22,7 +22,8 @@ export const metadata: Metadata = { }, }; -/** 服务端布局输出中文html/body壳;客户端反馈Provider包在runtime宿主外侧,供宿主状态提示使用。 */ +/** 服务端布局输出中文html/body壳;客户端反馈Provider包在runtime宿主外侧,供宿主状态提示使用。 + * children 作为插槽传入客户端宿主,不会因此把所有子页面模块变成客户端代码;宿主的释放逻辑在自身模块。 */ export default function RootLayout({ children }: { children: ReactNode }) { return ( diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 28393f8..038ef3d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -35,6 +35,7 @@ export default function Login() { verify = hash.get("verify_email"), failure = hash.get("error"); if (!code && !verify && !failure) return; + // 不只删除认证片段,也删除原 query;当前页面没有保留/恢复任意 returnTo 的流程。 history.replaceState(null, "", location.pathname); if (failure) { setError("第三方登录未完成,请重新尝试。"); @@ -68,6 +69,7 @@ export default function Login() { * HTML输入限制给即时反馈,服务端仍负责真实凭证/注册校验。 */ async function submit(e: FormEvent) { e.preventDefault(); + // busy 是显示状态,不是函数入口互斥锁;按钮禁用限制常规点击,submit 本身不检查已在执行。 setBusy(true); setError(""); setMessage(""); @@ -76,6 +78,7 @@ export default function Login() { method: "POST", body: JSON.stringify({ email, password }), }); + // 使用触发本次提交时 render 闭包里的 register;等待期间切换模式不会改变这次请求的后续分支。 if (register) setMessage("验证邮件已请求发送,请检查邮箱并点击验证链接。"); else router.push("/dashboard"); @@ -98,6 +101,7 @@ export default function Login() { setError("无法启动登录,请重新尝试。"); } } + // submit/oauth 同样没有卸载取消或结果代次校验;回调链结束前可继续调用 router/setState。 // 模式切换只清提示,已有邮箱/密码和可见性独立保留。 // 渲染分为品牌故事、模式切换、受控凭证表单、OAuth按钮与demo入口;错误用alert,非错误提示用status。 return ( diff --git a/src/app/page.tsx b/src/app/page.tsx index 15b8cfe..69d06c5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -21,12 +21,14 @@ export default async function Home() { signal: AbortSignal.timeout(15000), }); // 按响应状态确定是否已认证,再丢弃不需要的body;此处不解析profile字段。 + // 先保存 HTTP 成功判定;随后 cancel 若抛错,catch 不会把 authenticated 重置为 false。 authenticated = response.ok; await response.body?.cancel(); } catch { // An unverified session must not hide the public landing page. } } + // redirect 放在上方 try/catch 外,避免把框架用于导航的控制流异常当作 profile 请求失败吞掉。 // 仅在确认成功后跳工作台,未认证时沿用同一静态展示组件。 if (authenticated) redirect("/dashboard"); return ; diff --git a/src/components/landing-page.tsx b/src/components/landing-page.tsx index d3da5f9..6be2c63 100644 --- a/src/components/landing-page.tsx +++ b/src/components/landing-page.tsx @@ -28,6 +28,7 @@ export default function LandingPage() { const [active, setActive] = useState(0); // null表示全部收起;点击当前问题置null,点击其它问题改索引。hidden与aria-expanded/controls对应同一状态。 const [faq, setFaq] = useState(null); + // FAQ 的 hidden 只切换可见性,问答节点仍保留在 DOM;页面没有请求型加载或异步清理任务。 // 页面顺序:导航→首屏交互预览→理念横条→三个功能说明→工作步骤→FAQ→行动入口→页脚。 // 功能卡与步骤通过静态内容展示;FAQ以问题文本作key,步骤以序号作key,预览以说明作key。 // 状态仅在本次组件挂载期间保留;切换预览不自动轮播,FAQ展开不发网络请求。 diff --git a/src/components/ui/app-feedback.tsx b/src/components/ui/app-feedback.tsx index 5bc937d..7cf0416 100644 --- a/src/components/ui/app-feedback.tsx +++ b/src/components/ui/app-feedback.tsx @@ -13,7 +13,8 @@ export const FeedbackContext = createContext(false); /** 包住页面一次即可共享提示延迟;400ms首次出现,200ms内切换提示可跳过等待。 * Toaster放在右下角,最多展示三条通知,默认4500ms消失并允许关闭;颜色读取主题变量。 - * children正常透传,本组件不订阅文档、队列或服务端状态。 */ + * children正常透传,本组件不订阅文档、队列或服务端状态。 + * visibleToasts 限制同时可见数量,不是业务事件队列容量;通知创建/撤销仍通过通知适配器调用。 */ export function AppFeedback({ children }: { children: ReactNode }) { return diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index b287e0c..f9eb122 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -53,6 +53,7 @@ export interface ButtonProps const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { // 只选择宿主,不另包一层DOM;cn合并变体和调用方样式,业务逻辑不进入这个封装。 + // asChild 不把链接转换成原生按钮:disabled 属性本身不保证链接无法导航,调用方须按宿主处理行为。 const Comp = asChild ? Slot : "button"; return ( , React.ComponentPropsWithoutRef diff --git a/src/components/ui/icon-button.tsx b/src/components/ui/icon-button.tsx index 1490afe..4257a89 100644 --- a/src/components/ui/icon-button.tsx +++ b/src/components/ui/icon-button.tsx @@ -21,6 +21,7 @@ export type IconButtonProps = Omit, "chi export const IconButton = forwardRef( function IconButton({ icon: Icon, label, title = label, className = "", type = "button", ...props }, ref) { // Provider存在时由Radix显示提示,移除原生title以免出现两层提示;图标自身对读屏隐藏。 + // 后置 ...props 可以覆盖 aria-label 等同名属性;label 是默认名称,最终 DOM 语义由调用者共同负责。 const feedback = useContext(FeedbackContext); const button = (