5.7 KiB
HTML Preview Fullscreen Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Workspace 对话框的 HTML 预览面板支持浏览器全屏(Fullscreen API),工具栏在全屏下可用,Esc 退出不误关对话框。
Architecture: 仅改 WorkspaceFileDialog.tsx:header+内容区包入挂 ref 的 wrapper,Fullscreen API 切换 + fullscreenchange 同步状态,DialogContent 加 Esc 拦截。
Tech Stack: React, Radix Dialog, lucide-react, Tailwind。
Global Constraints
- 仅修改
src/app/components/WorkspaceFileDialog.tsx;服务端与 iframesandbox不动。 - 全屏按钮显示条件复用既有
showHtmlToggle。 - 全屏态 wrapper 类:
flex h-full w-full flex-col bg-background p-4;非全屏:flex min-h-0 flex-1 flex-col。 - 设计文档:
docs/superpowers/specs/2026-08-08-html-preview-fullscreen-design.md。 - 回归命令:
npm test与npx tsc --noEmit全绿。
Task 1: 预览面板全屏
Files:
- Modify:
src/app/components/WorkspaceFileDialog.tsx
Interfaces:
-
Consumes: 既有
showHtmlToggle(约 404 行)、header div(约 497-642 行)、内容区 div(约 653 行min-h-0 flex-1 overflow-hidden)。 -
Produces: 无导出变化。
-
Step 1: State、ref 与 fullscreenchange 监听
组件 state 区(const [savedTick, setSavedTick] = useState(0); 之后)追加:
// Browser-level fullscreen for the preview panel (toolbar stays usable).
const previewAreaRef = useRef<HTMLDivElement>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
const onChange = () =>
setIsFullscreen(document.fullscreenElement === previewAreaRef.current);
document.addEventListener("fullscreenchange", onChange);
return () => document.removeEventListener("fullscreenchange", onChange);
}, []);
const toggleFullscreen = () => {
if (document.fullscreenElement) void document.exitFullscreen();
else void previewAreaRef.current?.requestFullscreen();
};
确认 useRef 已在 React import 中(文件已用 useState/useEffect;若无 useRef 则加入)。
- Step 2: lucide 图标 import
现有 lucide-react import 块加入 Maximize2 与 Minimize2(按字母序插入现有列表)。
- Step 3: DialogContent 加 Esc 拦截
现有:
<DialogContent
aria-describedby={undefined}
className="flex h-[80vh] max-h-[80vh] min-w-[60vw] flex-col p-6"
>
改为:
<DialogContent
aria-describedby={undefined}
className="flex h-[80vh] max-h-[80vh] min-w-[60vw] flex-col p-6"
onEscapeKeyDown={(e) => {
// While the preview panel is fullscreen, Esc must only exit
// fullscreen — not close the whole dialog.
if (document.fullscreenElement) e.preventDefault();
}}
>
- Step 4: wrapper 包裹 header + 内容区
现有结构(约 496-875 行):<DialogTitle .../> 之后依次是 header div(<div className="mb-4 flex items-center justify-between gap-3 border-b border-border pb-4">…)、{actionError && …}、内容区 <div className="min-h-0 flex-1 overflow-hidden">…</div>,然后 </DialogContent>。
在 <DialogTitle className="sr-only">{path}</DialogTitle> 之后插入 wrapper 开标签,把 header div、actionError 块、内容区 div 三者包进去,内容区 div 结束后、</DialogContent> 之前闭合:
<DialogTitle className="sr-only">{path}</DialogTitle>
<div
ref={previewAreaRef}
className={
isFullscreen
? "flex h-full w-full flex-col bg-background p-4"
: "flex min-h-0 flex-1 flex-col"
}
>
<div className="mb-4 flex items-center justify-between gap-3 border-b border-border pb-4">
…(header / actionError / 内容区原样保留,仅整体缩进)
</div>
</div>
</DialogContent>
注意:wrapper 是 DialogContent 的唯一 flex 子项(DialogTitle 为 sr-only 不参与布局),非全屏类必须保持 flex min-h-0 flex-1 flex-col 以维持现状布局。
- Step 5: 工具栏全屏按钮
在 Preview/Source 分段组({showHtmlToggle && (<div role="group" …>…</div>)},约 550-581 行)之后、{editable && (…Edit…)} 之前插入:
{showHtmlToggle && (
<Button
variant="ghost"
size="sm"
className="h-8 px-2"
onClick={toggleFullscreen}
aria-label="Toggle fullscreen"
aria-pressed={isFullscreen}
title="Fullscreen"
>
{isFullscreen ? (
<Minimize2 size={16} aria-hidden="true" />
) : (
<Maximize2 size={16} aria-hidden="true" />
)}
</Button>
)}
- Step 6: Typecheck + 全量回归
Run: npx tsc --noEmit && npm test
Expected: no errors; 全部通过
- Step 7: Commit
git add src/app/components/WorkspaceFileDialog.tsx
git commit -m "feat(webui): fullscreen toggle for html preview panel"
Task 2: 手动验证(controller 处理)
- 浏览器验证 spec「测试」节 4 项:全屏进入/按钮退出/Esc 退出不关对话框/非 html 无按钮