feat(web): give image projects their own canvas - #6865
feat(web): give image projects their own canvas#6865open-design-crew[bot] wants to merge 1 commit into
Conversation
An image project's artifact is the whole set of pictures, but the workspace handed it the generic one-file-at-a-time viewer: a single picture with 下载 / 关闭 and no way to see the set. Prototype and wireframe projects, meanwhile, get a full canvas from the edit-workflow overhaul. This closes that gap. `metadata.kind === 'image'` now swaps the pane for an image canvas — the same shape as a sketch file swapping in SketchEditor, but keyed on the project rather than the active file: - 平铺: justified rows (mixed portrait/landscape, each row filling the width, trailing row unstretched); hover shows model / resolution / ratio - 详情: single picture with a thumbnail rail, wheel to step through - Action dock (评论 / 再次生成 / 引用该图) reusing the HTML canvas's own `.canvas-dock`, so both canvases share one control language - 平铺 / 详情 sits in the shared workspace toolbar row, and 分享 / 导出 / Handoff in its trailing file-actions slot — the row the other file surfaces already use Comments run through the existing `onSavePreviewComment` path; an image pin stores percentage coordinates rather than viewport pixels, since there is no DOM to re-anchor against. 再次生成 is presentation-only for now: it appends a card rather than calling a generation backend. The per-image copy in `campaign-mock.ts` is fixture data for review — nothing in the project record carries a title or model yet.
nettee
left a comment
There was a problem hiding this comment.
@open-design-crew
The new image-canvas surface has blocking regressions in workspace routing, comment persistence/lifecycle, and read-only permissions. I verified the live diff and the existing viewer/comment contracts; the direct web TypeScript check and guard pass, while the full web suite still has the 11 base-branch failures documented in the PR. Please address the inline findings before merge.
🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.| // the active file. The other workspace tabs (Design Files, browser, side | ||
| // chat, terminal, design system) still win, so nothing becomes unreachable. | ||
| const imageCanvasActive = | ||
| projectKind === 'image' |
There was a problem hiding this comment.
[blocking] This condition ignores the active file and live-artifact surfaces. openFile() sets activeTab to a filename, so opening any item from Design Files makes this expression true again; if that item is text/code or a live artifact, the ImageCanvas branch below wins and the real viewer never renders. It also defeats the stated Design Files path for opening a single image. Derive this from an explicit image-canvas landing state and keep the active file/live-artifact render paths authoritative (or gate on the actual active surface) instead of only excluding a few tab-id classes.
|
|
||
| // ---- comments ---------------------------------------------------------- | ||
| const savedPins = useMemo( | ||
| () => (previewComments ?? []).filter((comment) => comment.filePath === current?.id), |
There was a problem hiding this comment.
[blocking] previewComments is the project-wide list and includes attached, applying, needs_review, resolved, and failed rows. The existing canvas filters to status === "open", while this renders every historical/processed row and labels them i + 1; that resurrects resolved pins and renumbers/collides after a pin is resolved or deleted. The contract makes pinSeq permanent across all statuses. Filter the rendered list to open rows and use comment.pinSeq (with the documented creation-order fallback), while computing provisional numbers over all statuses.
| position: { x: pin.x, y: pin.y, width: 0, height: 0 }, | ||
| htmlHint: '', | ||
| }; | ||
| await onSavePreviewComment(target, pin.note.trim(), false); |
There was a problem hiding this comment.
[blocking] savePreviewComment deliberately returns null on missing conversation, upload failure, or upsert failure after showing an error toast. Ignoring that result means a 401/500 on any pin still falls through to setDraftPins([]) and destroys every note in this batch; partial success also makes retry duplicate earlier comments. Track each result, retain failed pins (and only clear confirmed successes), and leave the composer open so the user can retry.
| <span>分享</span> | ||
| </button> | ||
| {current ? ( | ||
| <a |
There was a problem hiding this comment.
[blocking] The viewerOnly permission is not applied to the raw share/download actions here; the same missing guard also leaves the comment button/send path below (lines 616–644) enabled. Existing FileViewer gates raw share/download and annotation/send for read-only collaborators. As written, a viewer-only member can copy/download the asset and can attempt to POST comments despite read-only status. Gate or disable the image canvas share/export and comment entry/send controls with the standard read-only reason, rather than only hiding Handoff.
| title={comment.note} | ||
| onClick={(event) => { | ||
| event.stopPropagation(); | ||
| if (onRemovePreviewComment) void onRemovePreviewComment(comment.id); |
There was a problem hiding this comment.
[blocking] A normal click on an existing pin calls onRemovePreviewComment immediately. There is no open/edit affordance or confirmation, unlike FileViewer, where marker clicks open the composer and deletion is explicit. Users inspecting a comment can therefore permanently delete it with one click. Make marker clicks select/open the note, and expose deletion only behind an explicit control and confirmation.
| // against, so the picture's own box is the only stable frame. | ||
| const pinId = `pin-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; | ||
| const target: PreviewCommentTarget = { | ||
| filePath: current.id, |
There was a problem hiding this comment.
[blocking] Derived 再次生成 cards use synthetic item.id values while item.file.name remains the real project path. For a derived card, current.id here is rerun-…, so comments are saved under a non-existent filePath; they cannot be fetched/reopened, and the savedPins filter at line 251 will not find them. Use the real file path (current.file.name) for comment persistence/filtering, or disable comments on presentation-only cards and keep a separate display id.
|
@open-design-crew The blocking items on this head are already captured in @nettee's review and inline comments, and the current failing checks line up with that state, so the next step is to address those findings and push an update. 💡 To drive this PR to merge hands-free, paste this to your AI coding agent (Claude Code / Codex / opencode / Cursor …): |















Why
Driving a real image project end-to-end kept running into the same wall: the workspace has no idea an image project is a set.
metadata.kind === 'image'fell through to the generic file viewer, so opening the project showed one picture with 下载 / 关闭 and nothing else — no way to see the nine things the run produced, no way to act on one of them, no way to move between them without going back to the Design Files list.Meanwhile prototype and wireframe projects, off the same branch's canvas edit overhaul (#6814), get a full canvas: a dock, a structure rail, comments, a mode segment. The gap isn't that images need more; it's that they were the only output type still being handed a document viewer.
This is a scoped first pass driven against a live image project, aimed at product review of the image workflow. Two behaviours inside it are presentation-only and called out below.
What users will see
Opening an image project now lands on a canvas instead of a single picture.
.canvas-dockshell: 引用该图 (drops the picture into the composer as an attachment), 评论, 再次生成.Nothing else moves: the top bar, the nav rail, the chat pane and the Design Files tab are untouched, and the Design Files tab still reaches the plain file viewer for any single image.
Surface area
apps/web(grid, focus view, thumbnail rail, action dock), plus the view segment and file actions it portals into the shared workspace toolbar rowonSavePreviewCommentpathmetadata.kind === 'image') render differently; no schema, network or setting changesScreenshots
To attach: 平铺 (justified grid), 详情 (thumbnail rail + action dock), and the workspace toolbar row showing 平铺/详情 alongside 分享 / 导出 / Handoff.
Known state
Three things a reviewer should weigh, rather than discover:
campaign-mock.tsis fixture data. Per-image title / model / resolution are hard-coded because nothing in the project record carries them yet. The image files are real project files; only this descriptive layer is fixture.zh-CNusers as-is.Two implementation notes:
PreviewCommentTargetis DOM-anchored (selector/elementId/htmlHint) with a re-anchoring ladder that a picture has no DOM to satisfy. Pins store percentage coordinates against the picture's own box and synthesise the selector fields the daemon's upsert validation requires. The composer, list and save path are all the existing ones; only the anchor semantics differ..chrome-actiongainedtext-decoration: none. 导出 is the class's first<a>(a real download link) and arrived underlined. No other anchor uses the class today.Validation
pnpm guardandpnpm --filter @open-design/web typecheckclean.pnpm --filter @open-design/web test: 6 files / 11 tests fail — the same 6 files and 11 tests fail on this branch's base commit (996f4377c, verified by stashing this change and re-running those suites). These are the reconciliation failures feat(web): overhaul the canvas edit workflow #6814 already documents; this change adds none.Base branch
Targets
agent/web-canvas-workflow-overhaul(#6814), notmain— this builds directly on that branch's canvas work, and basing onmainwould show its 95 files in this diff. Merge #6814 first.