Skip to content

Commit 0148b5e

Browse files
committed
test(primitives): pin the twenty dialog set
1 parent 45ce929 commit 0148b5e

3 files changed

Lines changed: 402 additions & 152 deletions

File tree

‎app/src/components/PracticeQuestionsOverlays.tsx‎

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
* 练习=内建习得行动(打点入完成史 practice_tick,宽容缺勤);
66
* 问题=Me 问题化(open/answered/archived + 答沉淀回链——供输出创作
77
* 前翻看与复盘)。
8+
* @ai-context: 批 4 T8 迁移:自建遮罩/居中几何(原 560 px 面板 → `Modal` 的 `l` 档)与自绘
9+
* 头部(标题 + 关闭钮)交给 `Modal`(barrel 导入,B5)——遮罩、ESC、焦点陷阱、
10+
* 层级、body 滚动锁都是它的独占职责(ADR-033 §7)。开合态仍由父层持有
11+
* (`ActionCenterPanel` 的条件挂载)⇒ `open` 恒为 `true`,160ms 退场相位不触发。
812
*/
913
import { useCallback, useEffect, useState } from "react";
10-
import { zIndex } from "../ui/zIndex";
14+
import { Modal } from "../ui/primitives";
1115
import { invoke } from "@tauri-apps/api/core";
1216

1317
/** 响应结构(PracticeItem/QuestionItem 均 serde camelCase——字段须 camel 读取) */
@@ -32,41 +36,20 @@ interface QuestionView {
3236
answerRef: string | null;
3337
}
3438

35-
const overlayStyle: React.CSSProperties = {
36-
position: "fixed",
37-
inset: 0,
38-
background: "rgba(0,0,0,.45)",
39-
zIndex: zIndex("modal"),
40-
display: "flex",
41-
alignItems: "center",
42-
justifyContent: "center",
43-
};
44-
const cardStyle: React.CSSProperties = {
45-
background: "#fff",
46-
borderRadius: 12,
47-
width: 560,
48-
maxWidth: "92vw",
49-
maxHeight: "80vh",
50-
overflow: "auto",
51-
padding: 16,
52-
fontSize: 13,
53-
};
5439
const btn: React.CSSProperties = { padding: "4px 10px", cursor: "pointer", fontSize: 12, borderRadius: 6 };
5540
const okBtn: React.CSSProperties = { ...btn, background: "#0d9488", color: "#fff", border: "none" };
5641
const ghostBtn: React.CSSProperties = { ...btn, background: "#fff", border: "1px solid #e5e7eb", color: "#374151" };
5742

58-
function shell(title: string, subtitle: string, onClose: () => void, children: React.ReactNode) {
43+
/**
44+
* 两个弹层共用的壳。标题(去纯装饰 emoji)与关闭钮归 `Modal` 的 head,`subtitle` 逐字保留为
45+
* 正文首行(原为标题右侧的 `<span>`;字号/色值原样 ⇒ 只是换行位置变了)。
46+
*/
47+
function shell(testId: string, title: string, subtitle: string, onClose: () => void, children: React.ReactNode) {
5948
return (
60-
<div style={overlayStyle} onClick={onClose}>
61-
<div style={cardStyle} onClick={(e) => e.stopPropagation()}>
62-
<div style={{ display: "flex", alignItems: "center", marginBottom: 10, gap: 8 }}>
63-
<h3 style={{ margin: 0, fontSize: 15 }}>{title}</h3>
64-
<span style={{ fontSize: 11, color: "#9ca3af" }}>{subtitle}</span>
65-
<button style={{ ...ghostBtn, marginLeft: "auto" }} onClick={onClose}>关闭</button>
66-
</div>
67-
{children}
68-
</div>
69-
</div>
49+
<Modal open onClose={onClose} title={title} size="l" testId={testId}>
50+
<div style={{ fontSize: 11, color: "#9ca3af", marginBottom: 10 }}>{subtitle}</div>
51+
{children}
52+
</Modal>
7053
);
7154
}
7255

@@ -113,7 +96,7 @@ export function PracticeOverlay({ onClose }: { onClose: () => void }) {
11396

11497
const dueCount = items.filter((i) => i.nextDue != null && i.nextDue <= Math.floor(Date.now() / 1000)).length;
11598

116-
return shell("🎯 练习条目", `该练了 ${dueCount} · 闪卡之外第二条复利曲线(宽容缺勤只记史)`, onClose, (
99+
return shell("practice-overlays", "练习条目", `该练了 ${dueCount} · 闪卡之外第二条复利曲线(宽容缺勤只记史)`, onClose, (
117100
<>
118101
{msg && <div style={{ fontSize: 11, color: "#047857", marginBottom: 6 }}>{msg}</div>}
119102
{err && <div style={{ fontSize: 11, color: "#dc2626", marginBottom: 6 }}>{err}</div>}
@@ -204,7 +187,7 @@ export function QuestionsOverlay({ onClose }: { onClose: () => void }) {
204187
const open = items.filter((q) => q.status === "open");
205188
const answered = items.filter((q) => q.status === "answered");
206189

207-
return shell("❓ 问题清单", "Me 洞见问题化——open/answered/archived(输出创作前翻看/复盘原料)", onClose, (
190+
return shell("questions-overlays", "问题清单", "Me 洞见问题化——open/answered/archived(输出创作前翻看/复盘原料)", onClose, (
208191
<>
209192
{msg && <div style={{ fontSize: 11, color: "#047857", marginBottom: 6 }}>{msg}</div>}
210193
{err && <div style={{ fontSize: 11, color: "#dc2626", marginBottom: 6 }}>{err}</div>}

‎app/src/components/SopRunOverlay.tsx‎

Lines changed: 100 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77
* 轨迹统计 + 保鲜 diff("笔记有出入?")+ 修订建议(纯本地聚合)。
88
* 证据=可选相对路径输入(notes-images/ 前缀白名单——命令层校验;
99
* 图片三入口落盘的完整上传流登记后置)。
10+
* @ai-context: 批 4 T8 迁移:自建遮罩/居中几何(原 640 px 面板 → `Modal` 的 `l` 档)与自绘头部
11+
* (标题 + 关闭钮)交给 `Modal`(barrel 导入,B5)——遮罩、ESC、焦点陷阱、层级、
12+
* body 滚动锁都是它的独占职责(ADR-033 §7)。开合态仍由父层持有
13+
* (`ActionCenterPanel` 的条件挂载)⇒ `open` 恒为 `true`,160ms 退场相位不触发。
1014
*/
1115
import { useEffect, useState } from "react";
12-
import { zIndex } from "../ui/zIndex";
16+
import { Modal } from "../ui/primitives";
1317
import { invoke } from "@tauri-apps/api/core";
1418

1519
/** 响应结构(SopTemplate/SopRunStep/SopRun/SopRunDetail 均 serde camelCase——字段须 camel 读取) */
@@ -56,25 +60,6 @@ interface Props {
5660
onChanged?: () => void;
5761
}
5862

59-
const overlayStyle: React.CSSProperties = {
60-
position: "fixed",
61-
inset: 0,
62-
background: "rgba(0,0,0,.45)",
63-
zIndex: zIndex("modal"),
64-
display: "flex",
65-
alignItems: "center",
66-
justifyContent: "center",
67-
};
68-
const cardStyle: React.CSSProperties = {
69-
background: "#fff",
70-
borderRadius: 12,
71-
width: 640,
72-
maxWidth: "92vw",
73-
maxHeight: "84vh",
74-
overflow: "auto",
75-
padding: 16,
76-
fontSize: 13,
77-
};
7863
const btn: React.CSSProperties = { padding: "4px 10px", cursor: "pointer", fontSize: 12, borderRadius: 6 };
7964
const okBtn: React.CSSProperties = { ...btn, background: "#0d9488", color: "#fff", border: "none" };
8065
const ghostBtn: React.CSSProperties = { ...btn, background: "#fff", border: "1px solid #e5e7eb", color: "#374151" };
@@ -181,111 +166,107 @@ export default function SopRunOverlay({ template, onClose, onChanged }: Props) {
181166
const modeLabel = confirmMode ? "总览核对(DO-CONFIRM)" : "逐步引导(READ-DO)";
182167

183168
return (
184-
<div style={overlayStyle} onClick={onClose}>
185-
<div style={cardStyle} onClick={(e) => e.stopPropagation()}>
186-
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
187-
<h3 style={{ margin: 0, fontSize: 15 }}>🧭 {template.name}</h3>
188-
<span style={{ fontSize: 11, color: "#6b7280" }}>@{template.noteTitle}</span>
189-
<span style={{ fontSize: 11, color: "#0f766e", background: "#f0fdfa", border: "1px solid #99f6e4", borderRadius: 10, padding: "1px 8px" }}>
190-
{modeLabel}
191-
</span>
192-
<button style={{ ...ghostBtn, marginLeft: "auto" }} onClick={onClose}>
193-
关闭
194-
</button>
169+
<Modal open onClose={onClose} title={template.name} size="l" testId="sop-run-overlay">
170+
{/* 自绘头部已删:标题(去纯装饰 emoji 🧭)与关闭钮归 `Modal` 的 head;备注名与模式徽标
171+
逐字保留为正文首行(字号/色值/边框原样) */}
172+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
173+
<span style={{ fontSize: 11, color: "#6b7280" }}>@{template.noteTitle}</span>
174+
<span style={{ fontSize: 11, color: "#0f766e", background: "#f0fdfa", border: "1px solid #99f6e4", borderRadius: 10, padding: "1px 8px" }}>
175+
{modeLabel}
176+
</span>
177+
</div>
178+
179+
{err && <div style={{ fontSize: 12, color: "#dc2626", background: "#fef2f2", border: "1px solid #fca5a5", borderRadius: 6, padding: "6px 10px", marginBottom: 8 }}>{err}</div>}
180+
{detail?.freshnessChanged && stage === "run" && (
181+
<div style={{ fontSize: 12, color: "#b45309", background: "#fffbeb", border: "1px solid #f59e0b", borderRadius: 6, padding: "6px 10px", marginBottom: 8 }}>
182+
⚠ 笔记正文与启动快照已有出入——结算时请对比修订模板(执行即保鲜)
195183
</div>
184+
)}
196185

197-
{err && <div style={{ fontSize: 12, color: "#dc2626", background: "#fef2f2", border: "1px solid #fca5a5", borderRadius: 6, padding: "6px 10px", marginBottom: 8 }}>{err}</div>}
198-
{detail?.freshnessChanged && stage === "run" && (
199-
<div style={{ fontSize: 12, color: "#b45309", background: "#fffbeb", border: "1px solid #f59e0b", borderRadius: 6, padding: "6px 10px", marginBottom: 8 }}>
200-
⚠ 笔记正文与启动快照已有出入——结算时请对比修订模板(执行即保鲜)
186+
{stage === "run" && detail && (
187+
<>
188+
<div style={{ display: "flex", gap: 6, alignItems: "center", marginBottom: 8 }}>
189+
<button style={ghostBtn} onClick={() => setConfirmMode((m) => !m)} title="两种执行模式可随时切换">
190+
{confirmMode ? "切到逐步引导" : "切到总览核对"}
191+
</button>
192+
<span style={{ fontSize: 11, color: "#9ca3af", marginLeft: "auto" }}>
193+
进行中 {detail.stats.done + detail.stats.failed + detail.stats.skipped}/{detail.stats.total} · 待办 {pending}
194+
</span>
201195
</div>
202-
)}
203-
204-
{stage === "run" && detail && (
205-
<>
206-
<div style={{ display: "flex", gap: 6, alignItems: "center", marginBottom: 8 }}>
207-
<button style={ghostBtn} onClick={() => setConfirmMode((m) => !m)} title="两种执行模式可随时切换">
208-
{confirmMode ? "切到逐步引导" : "切到总览核对"}
209-
</button>
210-
<span style={{ fontSize: 11, color: "#9ca3af", marginLeft: "auto" }}>
211-
进行中 {detail.stats.done + detail.stats.failed + detail.stats.skipped}/{detail.stats.total} · 待办 {pending}
212-
</span>
213-
</div>
214-
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
215-
{detail.steps.map((s) => {
216-
const done = s.status === "done";
217-
const failed = s.status === "failed";
218-
const skipped = s.status === "skipped";
219-
return (
220-
<div key={s.id} style={{ border: "1px solid #e5e7eb", borderRadius: 8, padding: "8px 10px", background: done ? "#ecfdf5" : failed ? "#fef2f2" : "#fff" }}>
221-
<div style={{ display: "flex", gap: 6 }}>
222-
<span style={{ fontSize: 11, color: "#9ca3af", flexShrink: 0 }}>步骤 {s.stepNo}</span>
223-
<span style={{ fontSize: 13, color: "#111827", flex: 1 }}>{s.textSnapshot}</span>
224-
<span style={{ fontSize: 11, color: "#9ca3af", flexShrink: 0 }}>
225-
{done ? "✓" : failed ? "✗" : skipped ? "⏭" : s.status}
226-
</span>
227-
</div>
228-
{!done && !failed && !skipped && (
229-
<>
230-
<div style={{ display: "flex", gap: 4, marginTop: 6, flexWrap: "wrap" }}>
231-
<button style={okBtn} disabled={busy} onClick={() => void stepAction(s.stepNo, "done")}>✓ 完成</button>
232-
<button style={ghostBtn} disabled={busy} onClick={() => void stepAction(s.stepNo, "skipped")}>⏭ 跳过</button>
233-
<button style={{ ...ghostBtn, color: "#dc2626" }} disabled={busy} onClick={() => { setFailOpen(failOpen === s.stepNo ? null : s.stepNo); setFailNote(""); }}>✗ 失败</button>
234-
</div>
235-
{failOpen === s.stepNo && (
236-
<div style={{ display: "flex", gap: 4, marginTop: 6 }}>
237-
<input autoFocus placeholder="失败原因(建议记录可观测信号差在哪)" value={failNote} onChange={(e) => setFailNote(e.target.value)} style={{ flex: 1, fontSize: 12, border: "1px solid #e5e7eb", borderRadius: 4, padding: "2px 6px" }} />
238-
<button style={okBtn} disabled={busy} onClick={() => void stepAction(s.stepNo, "failed")}>记录失败</button>
239-
</div>
240-
)}
241-
{s.status === "todo" && (
242-
<div style={{ display: "flex", gap: 4, marginTop: 4, alignItems: "center" }}>
243-
<span style={{ fontSize: 11, color: "#9ca3af" }}>证据路径(notes-images/…,可选):</span>
244-
<input
245-
value={evidenceByStep[s.stepNo] ?? ""}
246-
onChange={(e) => setEvidenceByStep((m) => ({ ...m, [s.stepNo]: e.target.value }))}
247-
placeholder="notes-images/xxx.png"
248-
style={{ flex: 1, fontSize: 12, border: "1px solid #e5e7eb", borderRadius: 4, padding: "2px 6px" }}
249-
/>
250-
</div>
251-
)}
252-
</>
253-
)}
196+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
197+
{detail.steps.map((s) => {
198+
const done = s.status === "done";
199+
const failed = s.status === "failed";
200+
const skipped = s.status === "skipped";
201+
return (
202+
<div key={s.id} style={{ border: "1px solid #e5e7eb", borderRadius: 8, padding: "8px 10px", background: done ? "#ecfdf5" : failed ? "#fef2f2" : "#fff" }}>
203+
<div style={{ display: "flex", gap: 6 }}>
204+
<span style={{ fontSize: 11, color: "#9ca3af", flexShrink: 0 }}>步骤 {s.stepNo}</span>
205+
<span style={{ fontSize: 13, color: "#111827", flex: 1 }}>{s.textSnapshot}</span>
206+
<span style={{ fontSize: 11, color: "#9ca3af", flexShrink: 0 }}>
207+
{done ? "✓" : failed ? "✗" : skipped ? "⏭" : s.status}
208+
</span>
254209
</div>
255-
);
256-
})}
257-
</div>
258-
<div style={{ display: "flex", gap: 6, marginTop: 10 }}>
259-
<button style={okBtn} disabled={busy || pending > 0} onClick={() => void finish("done")}>
260-
✅ 结算(完成)
261-
</button>
262-
<button style={{ ...ghostBtn, color: "#b45309" }} disabled={busy} onClick={() => void finish("aborted")}>
263-
⏹ 中止并归档
264-
</button>
265-
{pending > 0 && <span style={{ fontSize: 11, color: "#b45309" }}>还有 {pending} 步未处理(可先处理或中止)</span>}
266-
</div>
267-
</>
268-
)}
210+
{!done && !failed && !skipped && (
211+
<>
212+
<div style={{ display: "flex", gap: 4, marginTop: 6, flexWrap: "wrap" }}>
213+
<button style={okBtn} disabled={busy} onClick={() => void stepAction(s.stepNo, "done")}>✓ 完成</button>
214+
<button style={ghostBtn} disabled={busy} onClick={() => void stepAction(s.stepNo, "skipped")}>⏭ 跳过</button>
215+
<button style={{ ...ghostBtn, color: "#dc2626" }} disabled={busy} onClick={() => { setFailOpen(failOpen === s.stepNo ? null : s.stepNo); setFailNote(""); }}>✗ 失败</button>
216+
</div>
217+
{failOpen === s.stepNo && (
218+
<div style={{ display: "flex", gap: 4, marginTop: 6 }}>
219+
<input autoFocus placeholder="失败原因(建议记录可观测信号差在哪)" value={failNote} onChange={(e) => setFailNote(e.target.value)} style={{ flex: 1, fontSize: 12, border: "1px solid #e5e7eb", borderRadius: 4, padding: "2px 6px" }} />
220+
<button style={okBtn} disabled={busy} onClick={() => void stepAction(s.stepNo, "failed")}>记录失败</button>
221+
</div>
222+
)}
223+
{s.status === "todo" && (
224+
<div style={{ display: "flex", gap: 4, marginTop: 4, alignItems: "center" }}>
225+
<span style={{ fontSize: 11, color: "#9ca3af" }}>证据路径(notes-images/…,可选):</span>
226+
<input
227+
value={evidenceByStep[s.stepNo] ?? ""}
228+
onChange={(e) => setEvidenceByStep((m) => ({ ...m, [s.stepNo]: e.target.value }))}
229+
placeholder="notes-images/xxx.png"
230+
style={{ flex: 1, fontSize: 12, border: "1px solid #e5e7eb", borderRadius: 4, padding: "2px 6px" }}
231+
/>
232+
</div>
233+
)}
234+
</>
235+
)}
236+
</div>
237+
);
238+
})}
239+
</div>
240+
<div style={{ display: "flex", gap: 6, marginTop: 10 }}>
241+
<button style={okBtn} disabled={busy || pending > 0} onClick={() => void finish("done")}>
242+
✅ 结算(完成)
243+
</button>
244+
<button style={{ ...ghostBtn, color: "#b45309" }} disabled={busy} onClick={() => void finish("aborted")}>
245+
⏹ 中止并归档
246+
</button>
247+
{pending > 0 && <span style={{ fontSize: 11, color: "#b45309" }}>还有 {pending} 步未处理(可先处理或中止)</span>}
248+
</div>
249+
</>
250+
)}
269251

270-
{stage === "settle" && detail && (
271-
<div>
272-
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}>
273-
{detail.run.status === "done" ? "✅ run 已完成并归档入史" : "⏹ run 已中止归档"}
274-
</div>
275-
<div style={{ fontSize: 12, color: "#374151", marginBottom: 6 }}>
276-
轨迹:✓完成 {detail.stats.done} · ⏭跳过 {detail.stats.skipped} · ✗失败 {detail.stats.failed}(共 {detail.stats.total} 步)
277-
</div>
278-
{detail.freshnessChanged && (
279-
<div style={{ fontSize: 12, color: "#b45309", marginBottom: 6 }}>
280-
📝 笔记正文有出入——可对比快照修订模板段落(编辑即模板,无双写)
281-
</div>
282-
)}
283-
<div style={{ display: "flex", gap: 6, marginTop: 8 }}>
284-
<button style={okBtn} onClick={onClose}>返回</button>
252+
{stage === "settle" && detail && (
253+
<div>
254+
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}>
255+
{detail.run.status === "done" ? "✅ run 已完成并归档入史" : "⏹ run 已中止归档"}
256+
</div>
257+
<div style={{ fontSize: 12, color: "#374151", marginBottom: 6 }}>
258+
轨迹:✓完成 {detail.stats.done} · ⏭跳过 {detail.stats.skipped} · ✗失败 {detail.stats.failed}(共 {detail.stats.total} 步)
259+
</div>
260+
{detail.freshnessChanged && (
261+
<div style={{ fontSize: 12, color: "#b45309", marginBottom: 6 }}>
262+
📝 笔记正文有出入——可对比快照修订模板段落(编辑即模板,无双写)
285263
</div>
264+
)}
265+
<div style={{ display: "flex", gap: 6, marginTop: 8 }}>
266+
<button style={okBtn} onClick={onClose}>返回</button>
286267
</div>
287-
)}
288-
</div>
289-
</div>
268+
</div>
269+
)}
270+
</Modal>
290271
);
291272
}

0 commit comments

Comments
 (0)