docs(workspace): explain host page and sidebar lifecycles

This commit is contained in:
2026-11-12 01:46:09 +08:00
parent 86dac408c3
commit 1306df2b35
8 changed files with 115 additions and 0 deletions
@@ -1,8 +1,13 @@
/**
* 工作台标题的局部交互状态机:idle→selected→editing→saving,支持二次点击/F2编辑、Enter/失焦保存、Escape取消。
* 只管理草稿、焦点和提示,真正改名由异步onSave负责;组件本身不修改目录或画布文档。
*/
"use client";
import { useEffect, useRef, useState } from "react";
import styles from "./editable-title.module.css";
/** editable控制能否进入编辑;title是宿主已采用值,draft只在一次编辑会话内使用,保存失败回editing重试。 */
export function EditableTitle({ title, editable, onSave, className = "" }: {
title: string;
editable: boolean;
@@ -15,11 +20,14 @@ export function EditableTitle({ title, editable, onSave, className = "" }: {
const shell = useRef<HTMLDivElement>(null);
const input = useRef<HTMLInputElement>(null);
const button = useRef<HTMLButtonElement>(null);
// ref防Enter和随后的blur重复调用;cancelled防Escape/已完成后的blur重新提交。
const pending = useRef(false);
const cancelled = useRef(false);
/** selected阶段才监听外侧pointerdown;离开此模式即退订,编辑/保存不由这条外侧点击逻辑擅自退出。 */
useEffect(() => {
if (mode !== "selected") return;
/** 只在实际DOM目标落在shell外时取消选择,标题内部点击保留进入编辑的机会。 */
const deselect = (event: PointerEvent) => {
if (event.target instanceof Node && !shell.current?.contains(event.target)) setMode("idle");
};
@@ -27,10 +35,12 @@ export function EditableTitle({ title, editable, onSave, className = "" }: {
return () => document.removeEventListener("pointerdown", deselect);
}, [mode]);
/** 进入editing后挂载的input才可聚焦/全选,不在渲染阶段直接访问未就绪DOM。 */
useEffect(() => {
if (mode === "editing") { input.current?.focus(); input.current?.select(); }
}, [mode]);
/** 每次从宿主最新title初始化草稿并清旧错误,不能沿用上次取消的输入。 */
function edit() {
if (!editable) return;
cancelled.current = false;
@@ -38,12 +48,15 @@ export function EditableTitle({ title, editable, onSave, className = "" }: {
setError("");
setMode("editing");
}
/** 标记取消以抑制blur保存,回selected并下一帧把焦点还给标题按钮。 */
function cancel() {
cancelled.current = true;
setError("");
setMode("selected");
requestAnimationFrame(() => button.current?.focus());
}
/** 同步ref挡重复提交,trim后校验非空及UTF-8≤300字节;同名不发请求。
* 成功回idle,失败留草稿/错误重编辑,finally释放pending允许下一次明确尝试。 */
async function save() {
if (pending.current || cancelled.current) return;
const name = draft.trim();
@@ -65,6 +78,7 @@ export function EditableTitle({ title, editable, onSave, className = "" }: {
setMode("editing");
} finally { pending.current = false; }
}
// saving保留input但只读;输入法组合不把Enter当提交,键盘事件停止冒泡避免触发画布快捷键。
return (
<div ref={shell} className={`${styles.shell} ${className}`}>
{mode === "editing" || mode === "saving" ? (