diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 19e9b9e39..4e3956c2b 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -20,8 +20,12 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 画布图片/视频节点“放大编辑”窗口改为 `min(1200px, 92vw)`,编辑区最小高度 200px,右下角可对角拖拽,方向键可微调。不改变节点浮动面板的现有视觉。 -- [ ] 点击放大后窗口明显大于节点面板;右下角拖动手柄可同时改宽高,最小约 560×320,不超出视口。 +PR #507 核心能力已按主分支实现保留,并补齐小视口尺寸约束、浏览器缩放窗口后的尺寸收敛、短屏滚动和展开态单一调整手柄。尺寸回归测试与 Vite 生产打包通过;真实 Chrome 组件验证(临时数据,未调用生成接口)覆盖图片/视频、参考图、首尾帧控件、明暗主题、窄屏/短屏、鼠标与键盘调整、切换节点重置。完整 `bun run build` 仍被既有 `canvas-cloud-agent-panel.tsx` 中未定义的 `markdownPlainText` 阻断。 + +- [ ] 点击放大后窗口明显大于节点面板;右下角拖动手柄可同时改宽高,桌面最小约 560×320,小视口优先限制在视口减 24px 内。 - [ ] 方向键可调整,Shift 加速;切换节点后尺寸恢复默认。 +- [ ] 窗口缩小后弹窗尺寸自动收敛;短屏下可滚动访问编辑区、参数和提交按钮,右下角手柄保持可达。 +- [ ] 展开态仅显示右下角窗口手柄;紧凑面板仍可独立调整输入高度。明暗主题继续跟随当前画布。 ## 支付订单与对账 CSV 导出 diff --git a/web/src/components/canvas/canvas-node-prompt-panel.tsx b/web/src/components/canvas/canvas-node-prompt-panel.tsx index f86c452eb..03a01ff0d 100644 --- a/web/src/components/canvas/canvas-node-prompt-panel.tsx +++ b/web/src/components/canvas/canvas-node-prompt-panel.tsx @@ -6,6 +6,7 @@ import { ArrowLeftRight, ArrowUp, AtSign, Boxes, Camera, ChevronDown, FileText, import { ModelPicker } from "@/components/model-picker"; import { defaultConfig, modelOptionName, resolveModelChannel, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store"; import { resolveCanvasGenerationModel } from "@/lib/canvas/canvas-project-generation"; +import { clampPromptEditorModalSize, PROMPT_EDITOR_VIEWPORT_MARGIN } from "@/lib/canvas/canvas-prompt-editor-size"; import { CreditSymbol, requestCreditCost } from "@/constant/credits"; import { canvasThemes } from "@/lib/canvas-theme"; import { modelQuoteRequest } from "@/lib/model-pricing"; @@ -67,9 +68,6 @@ const PROMPT_EDITOR_EXPANDED_MAX_LINES = 14; const PROMPT_EDITOR_MODAL_WIDTH = "min(1200px, 92vw)"; const PROMPT_EDITOR_MODAL_DEFAULT_WIDTH = 1200; const PROMPT_EDITOR_MODAL_DEFAULT_HEIGHT = 420; -const PROMPT_EDITOR_MODAL_MIN_WIDTH = 560; -const PROMPT_EDITOR_MODAL_MIN_HEIGHT = 320; -const PROMPT_EDITOR_MODAL_VIEWPORT_MARGIN = 24; export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChange, onConfigChange, onGenerate, mentionReferences = [], onAddReference, onRemoveReference, onReorderReferences, onReplaceReference, onReplaceReferenceFiles, onClose, onNodeMouseDown, onImageSettingsOpenChange, workspaceMode = "professional" }: CanvasNodePromptPanelProps) { const globalConfig = useEffectiveConfig(); @@ -92,7 +90,6 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan const [promptContentHeight, setPromptContentHeight] = useState(() => estimatePromptContentHeight(savedPrompt, false)); const [expandedPromptContentHeight, setExpandedPromptContentHeight] = useState(() => estimatePromptContentHeight(savedPrompt, true)); const [manualPromptHeight, setManualPromptHeight] = useState(null); - const [manualExpandedPromptHeight, setManualExpandedPromptHeight] = useState(null); const [paramsExpanded, setParamsExpanded] = useState(false); // #98 决策2:B区参数区折叠状态(手风琴) const [promptOptimizerOpen, setPromptOptimizerOpen] = useState(false); const [autoLinkEnabled, setAutoLinkEnabled] = useState(true); @@ -172,7 +169,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan const promptBounds = promptEditorBounds(false, activeReferenceCount > 0); const expandedPromptBounds = promptEditorBounds(true, activeReferenceCount > 0); const composerHeight = clampPromptHeight(manualPromptHeight ?? promptContentHeight + (activeReferenceCount ? PROMPT_REFERENCE_SHELF_HEIGHT : 0), promptBounds); - const expandedComposerHeight = clampPromptHeight(manualExpandedPromptHeight ?? expandedPromptContentHeight + (activeReferenceCount ? PROMPT_REFERENCE_SHELF_HEIGHT : 0), expandedPromptBounds); + const expandedComposerHeight = clampPromptHeight(expandedPromptContentHeight + (activeReferenceCount ? PROMPT_REFERENCE_SHELF_HEIGHT : 0), expandedPromptBounds); const measureExpandedModalSize = () => { const rect = expandedModalRef.current?.getBoundingClientRect(); if (!rect?.width || !rect.height) return { width: PROMPT_EDITOR_MODAL_DEFAULT_WIDTH, height: PROMPT_EDITOR_MODAL_DEFAULT_HEIGHT }; @@ -197,9 +194,20 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan setPromptContentHeight(estimatePromptContentHeight(normalizedSavedPrompt, false)); setExpandedPromptContentHeight(estimatePromptContentHeight(normalizedSavedPrompt, true)); setManualPromptHeight(null); - setManualExpandedPromptHeight(null); }, [node.id]); + useEffect(() => { + if (!expandedPromptOpen) return; + const constrainSize = () => setExpandedModalSize((size) => { + if (!size) return size; + const next = clampExpandedModalSize(size); + return next.width === size.width && next.height === size.height ? size : next; + }); + constrainSize(); + window.addEventListener("resize", constrainSize); + return () => window.removeEventListener("resize", constrainSize); + }, [expandedPromptOpen]); + useEffect(() => { if (!creditsEnabled || !quoteRequest) { setQuotedCredits(null); @@ -455,7 +463,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan const height = expanded ? expandedComposerHeight : composerHeight; return ( <> -
+
- + onResize={setManualPromptHeight} + />} ); }; @@ -551,6 +559,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan footer={null} centered width={expandedModalSize ? expandedModalSize.width : PROMPT_EDITOR_MODAL_WIDTH} + style={{ maxWidth: `calc(100vw - ${PROMPT_EDITOR_VIEWPORT_MARGIN}px)` }} destroyOnHidden onCancel={() => { setExpandedPresetOpen(false); @@ -561,15 +570,17 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan body: { minHeight: 0, padding: 0 }, }} > -
-
{renderComposerHeader(true)}
- {renderPromptEditor(true, Boolean(expandedModalSize))} - {hasVideoPromptTools ? ( -
- onConfigChange(node.id, patch)} /> -
- ) : null} -
{renderComposerControls(true)}
+
+
+
{renderComposerHeader(true)}
+ {renderPromptEditor(true, Boolean(expandedModalSize))} + {hasVideoPromptTools ? ( +
+ onConfigChange(node.id, patch)} /> +
+ ) : null} +
{renderComposerControls(true)}
+
@@ -933,12 +944,7 @@ function PromptResizeHandle({ height, min, max, onResize }: { height: number; mi } function clampExpandedModalSize(size: { width: number; height: number }) { - const maxWidth = Math.max(PROMPT_EDITOR_MODAL_MIN_WIDTH, window.innerWidth - PROMPT_EDITOR_MODAL_VIEWPORT_MARGIN); - const maxHeight = Math.max(PROMPT_EDITOR_MODAL_MIN_HEIGHT, window.innerHeight - PROMPT_EDITOR_MODAL_VIEWPORT_MARGIN); - return { - width: Math.min(maxWidth, Math.max(PROMPT_EDITOR_MODAL_MIN_WIDTH, Math.round(size.width))), - height: Math.min(maxHeight, Math.max(PROMPT_EDITOR_MODAL_MIN_HEIGHT, Math.round(size.height))), - }; + return clampPromptEditorModalSize(size, { width: window.innerWidth, height: window.innerHeight }); } function PromptModalResizeHandle({ size, measure, onResize, accent }: { size: { width: number; height: number } | null; measure: () => { width: number; height: number }; onResize: (size: { width: number; height: number }) => void; accent: string }) { @@ -989,7 +995,8 @@ function PromptModalResizeHandle({ size, measure, onResize, accent }: { size: { return; } event.stopPropagation(); - onResize(clampExpandedModalSize({ width: drag.width + event.clientX - drag.startX, height: drag.height + event.clientY - drag.startY })); + // The modal stays centered, so each edge moves by half the size change. + onResize(clampExpandedModalSize({ width: drag.width + 2 * (event.clientX - drag.startX), height: drag.height + 2 * (event.clientY - drag.startY) })); }} onPointerUp={finishResize} onPointerCancel={finishResize} diff --git a/web/src/lib/canvas/canvas-prompt-editor-size.ts b/web/src/lib/canvas/canvas-prompt-editor-size.ts new file mode 100644 index 000000000..f7b1f7ccc --- /dev/null +++ b/web/src/lib/canvas/canvas-prompt-editor-size.ts @@ -0,0 +1,12 @@ +type Size = { width: number; height: number }; + +export const PROMPT_EDITOR_VIEWPORT_MARGIN = 24; + +export function clampPromptEditorModalSize(size: Size, viewport: Size): Size { + const maxWidth = Math.max(1, viewport.width - PROMPT_EDITOR_VIEWPORT_MARGIN); + const maxHeight = Math.max(1, viewport.height - PROMPT_EDITOR_VIEWPORT_MARGIN); + return { + width: Math.min(maxWidth, Math.max(560, Math.round(size.width))), + height: Math.min(maxHeight, Math.max(320, Math.round(size.height))), + }; +} diff --git a/web/test/canvas-prompt-editor-size.test.ts b/web/test/canvas-prompt-editor-size.test.ts new file mode 100644 index 000000000..8c09946d8 --- /dev/null +++ b/web/test/canvas-prompt-editor-size.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "bun:test"; +import { clampPromptEditorModalSize } from "../src/lib/canvas/canvas-prompt-editor-size"; + +describe("prompt editor window bounds", () => { + test("keeps desktop minimums and leaves a viewport margin", () => { + const viewport = { width: 1440, height: 900 }; + expect(clampPromptEditorModalSize({ width: 100, height: 100 }, viewport)).toEqual({ width: 560, height: 320 }); + expect(clampPromptEditorModalSize({ width: 2000, height: 2000 }, viewport)).toEqual({ width: 1416, height: 876 }); + }); + + test("a narrow or short viewport takes precedence over desktop minimums", () => { + expect(clampPromptEditorModalSize({ width: 1200, height: 800 }, { width: 390, height: 844 })).toEqual({ width: 366, height: 800 }); + expect(clampPromptEditorModalSize({ width: 1200, height: 800 }, { width: 844, height: 300 })).toEqual({ width: 820, height: 276 }); + }); + + test("reconstrains a previously resized window after viewport shrink", () => { + const desktop = clampPromptEditorModalSize({ width: 1200, height: 800 }, { width: 1440, height: 900 }); + const mobile = clampPromptEditorModalSize(desktop, { width: 390, height: 300 }); + expect(mobile).toEqual({ width: 366, height: 276 }); + expect(clampPromptEditorModalSize(mobile, { width: 390, height: 300 })).toEqual(mobile); + }); +});