diff --git a/docs/superpowers/plans/2026-09-12-drawer-favicon.md b/docs/superpowers/plans/2026-09-12-drawer-favicon.md new file mode 100644 index 0000000..ca85e87 --- /dev/null +++ b/docs/superpowers/plans/2026-09-12-drawer-favicon.md @@ -0,0 +1,67 @@ +# favicon 修复 + 抽屉(删左栏/宽度变量/蒙版)— 实施计划 + +目标文件:`src/genesis/server/{app.py}`、`src/genesis/server/static/{chat.html,chat.css,chat.js}`、新增 `static/favicon.svg` +后端改动仅 favicon 相关;其余为前端。 + +## 已确认决策 +- D1 抽屉宽度 `--drawer-w: 520px`。 +- D2 蒙版 `rgba(0,0,0,.5)`。 +- D3 点蒙版关闭抽屉(带未保存确认)。 +- D4 删除抽屉左侧项目列表,抽屉只留表单。 + +--- + +## Task 1:favicon 404 +- 新增 `src/genesis/server/static/favicon.svg`:品牌「G」(深底 `#0B0F17` + accent `#5EEAD4`)。 +- `chat.html ` 增:``。 +- `app.py`: + - `from fastapi.responses import FileResponse, HTMLResponse, Response`。 + - 在 `index` 附近新增 `@app.get("/favicon.ico")` → `Response(status_code=204)`。 + - `_asset_names` 加入 `"favicon.svg"`。 + +## Task 2:抽屉删左栏 + 宽度变量 + 蒙版 +- `chat.html`: + - 删除 `
`。 + - 新增 `
`(与 `#proj-drawer` 同级)。 +- `chat.js`: + - 删除 3 处 `renderDrawerList();`(`loadProjects`/`openProjectDrawer`/`loadDrawerForm`)与 `renderDrawerList()` 函数定义。 + - `openProjectDrawer` → mask `show`;`closeProjectDrawer` → 过 `guardDrawerDirty()` 后 mask 隐藏。 + - 点蒙版 → `closeProjectDrawer()`。 +- `chat.css`: + - `:root` 增 `--drawer-w: 520px; --mask-bg: rgba(0,0,0,.5);`;`#proj-drawer { width: var(--drawer-w); }`。 + - 删除 `.drawer-list` 规则(`#drawer-list` 不再存在)。 + - 新增 `.drawer-mask`(fixed/inset/z-index 99/opacity+visibility 过渡/`.show`)与 `@media (max-width:860px) .drawer-mask{display:none}`。 + +## Task 3:验证与提交 +- `node --check`、`node --test`、`python -m pytest`(≥99%)。 +- 手动:favicon 无 404;抽屉只留表单、宽 520px;蒙版 0.5;点蒙版/×/Esc 关闭(带未保存确认)。 +- 更新 `_AI_USAGE_LOG.md` 并提交。 + +## 风险 +- 蒙版使抽屉从"非模态"变"模态"(打开时点侧边栏会被蒙版拦截并关闭抽屉)。 +- 窄屏(≤860px)抽屉全屏,蒙版隐藏,仅 ×/Esc 关闭。 + +--- + +## 评审结论与计划调整(plan-eng-review) + +### 采纳的修正 +- **F5(真实缺陷,修正计划)**:**不要**把 `.drawer-body` 改成 `display:block`。保持 `flex:1; display:flex; min-height:0`,仅删除 `.drawer-list` 规则——否则 `.drawer-form { flex:1; overflow-y:auto }` 失去高度约束,长表单溢出且不滚动。 +- **F6(测试缺口,采纳)**:新增 favicon 回归测试(`tests/test_server_api.py`):`GET /favicon.ico` == 204 且 `GET /` 文本含 `rel="icon"`。 +- **窄屏蒙版**:保留 `@media (max-width:860px) .drawer-mask { display:none }`(全屏抽屉时仅 ×/Esc 关闭)。 + +### 记录(接受,不改) +- F3 行为变更:加蒙版后抽屉转为**模态**——打开时点侧边栏/空白会被蒙版拦截并触发关闭(未保存先确认)。 +- F4:蒙版不加 `aria-hidden`,作为纯装饰层。 +- F7/F8/F9:层叠、幂等、资源版本一致性均按计划成立。 + +### 受影响任务更新 +- Task 2:删除 `.drawer-list` 规则即可,`.drawer-body` **保持不变**。 +- Task 3:新增 favicon 回归测试。 + +## GSTACK REVIEW REPORT +- STATUS: DONE +- REVIEWED: docs/superpowers/plans/2026-09-12-drawer-favicon.md +- FINDINGS: F5 缺陷(已修正) / F6 测试缺口(已采纳) / F3 行为变更(记录) / F4 a11y(记录);F7-F9 接受 +- KEY DECISION: `.drawer-body` 保持 flex;新增 favicon 回归测试;窄屏隐藏蒙版 +- NEXT: 待用户批准后实施 diff --git a/src/genesis/server/app.py b/src/genesis/server/app.py index fe46cf8..89d0d56 100644 --- a/src/genesis/server/app.py +++ b/src/genesis/server/app.py @@ -16,7 +16,7 @@ from pathlib import Path from typing import Any from fastapi import FastAPI, File, Form, HTTPException, UploadFile, WebSocket, WebSocketDisconnect -from fastapi.responses import FileResponse, HTMLResponse +from fastapi.responses import FileResponse, HTMLResponse, Response from fastapi.staticfiles import StaticFiles from pydantic import BaseModel @@ -117,7 +117,7 @@ def create_app( static_dir = Path(__file__).parent / "static" # 静态资源版本号:按 mtime 计算,随任何前端文件变更而变化(重启后生效) - _asset_names = ("chat.html", "chat.css", "chat.js", "chat_state.js", "chat_ws.js") + _asset_names = ("chat.html", "chat.css", "chat.js", "chat_state.js", "chat_ws.js", "favicon.svg") try: _asset_ver = str(max(int((static_dir / n).stat().st_mtime_ns) for n in _asset_names if (static_dir / n).exists())) @@ -138,6 +138,11 @@ def create_app( def index(): return HTMLResponse(chat_html, headers={"Cache-Control": "no-store", "Pragma": "no-cache"}) + @app.get("/favicon.ico") + def favicon(): + # 浏览器默认请求;返回 204 避免 404(真实图标由 指向 favicon.svg) + return Response(status_code=204) + # 静态资源(chat.css / chat_state.js / chat_ws.js / 未来的 chat.js) # 统一通过 /static/ 访问,避免逐个文件注册 app.mount("/static", StaticFiles(directory=str(static_dir), html=False), name="static") diff --git a/src/genesis/server/static/chat.css b/src/genesis/server/static/chat.css index 6121e46..7f0b5a7 100644 --- a/src/genesis/server/static/chat.css +++ b/src/genesis/server/static/chat.css @@ -42,6 +42,10 @@ --send-size: 40px; --composer-gap: 10px; --composer-gutter: calc(var(--send-size) + var(--composer-gap)); + + /* 抽屉 */ + --drawer-w: 520px; + --mask-bg: rgba(0, 0, 0, 0.5); } /* === 全局 === */ @@ -351,7 +355,7 @@ label.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px /* === 项目抽屉 (spec §2.6) === */ #proj-drawer { - position: fixed; top: 0; right: 0; height: 100vh; width: 460px; + position: fixed; top: 0; right: 0; height: 100vh; width: var(--drawer-w); background: var(--bg-surface); border-left: 1px solid var(--border-subtle); box-shadow: -8px 0 32px rgba(0,0,0,.5); transform: translateX(100%); transition: transform .3s var(--ease); @@ -372,32 +376,13 @@ label.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px } .drawer-close:hover { background: var(--danger); border-color: var(--danger); color: var(--bg-base); } .drawer-body { flex: 1; display: flex; min-height: 0; } -.drawer-list { - width: 38%; border-right: 1px solid var(--border-subtle); overflow-y: auto; - padding: 10px 8px; background: var(--bg-base); - display: flex; flex-direction: column; gap: 2px; +/* 抽屉蒙版 */ +.drawer-mask { + position: fixed; inset: 0; z-index: 99; background: var(--mask-bg); + opacity: 0; visibility: hidden; + transition: opacity .3s var(--ease), visibility .3s var(--ease); } -.drawer-item { - padding: 10px 12px; border-radius: var(--r-sm); cursor: pointer; font-size: 13px; - color: var(--text-secondary); border: 1px solid transparent; - transition: all var(--dur) var(--ease); - display: flex; align-items: center; gap: 8px; position: relative; -} -.drawer-item:hover { background: var(--bg-elevated); color: var(--text-primary); } -.drawer-item.active { - background: var(--bg-elevated); color: var(--text-primary); font-weight: 700; -} -.drawer-item.active::before { - content: ""; position: absolute; left: 0; top: 6px; bottom: 6px; width: 2px; - background: var(--accent); border-radius: 0 2px 2px 0; -} -.drawer-item .di-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.drawer-item .di-new { color: var(--accent); } -.drawer-item.di-create { - border: 1px dashed var(--border-strong); background: transparent; - color: var(--accent); font-weight: 700; justify-content: center; gap: 4px; -} -.drawer-item.di-create:hover { background: var(--accent-soft); border-color: var(--accent); } +.drawer-mask.show { opacity: 1; visibility: visible; } .drawer-form { flex: 1; overflow-y: auto; padding: var(--sp-4) var(--sp-5); display: flex; flex-direction: column; gap: 10px; } .drawer-form label { font-family: var(--font-mono); @@ -435,6 +420,7 @@ label.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px #sidebar.collapsed { width: 48px; } #sid-badge { max-width: 24%; } #proj-drawer { width: 100vw; } + .drawer-mask { display: none; } } /* === 顶栏会话名 === */ diff --git a/src/genesis/server/static/chat.html b/src/genesis/server/static/chat.html index dbc6359..56a9cb7 100644 --- a/src/genesis/server/static/chat.html +++ b/src/genesis/server/static/chat.html @@ -5,6 +5,7 @@ Genesis — 概要设计书自动生成(对话) + +