From 7755632e0584ca1554faad4fe71da1ff1f4356ab Mon Sep 17 00:00:00 2001 From: Quang Tran <16215255+trmquang93@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:15:17 +0700 Subject: [PATCH 1/2] fix: include screen status changes in undo history updateScreenStatus and markAllExisting were missing pushHistory calls, so single-screen status edits and the bulk "All existing" action were invisible to Cmd/Ctrl+Z. Mirrors the existing pattern used by updateScreenNotes / renameScreen. Refs backlog 8.2. --- src/hooks/useScreenManager.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/hooks/useScreenManager.js b/src/hooks/useScreenManager.js index abfdf37..929a643 100644 --- a/src/hooks/useScreenManager.js +++ b/src/hooks/useScreenManager.js @@ -258,8 +258,9 @@ export function useScreenManager(pan, zoom, canvasRef, commentCallbacks = {}) { }, []); const updateScreenStatus = useCallback((id, status) => { + pushHistory(screens, connections, documents); setScreens((prev) => prev.map((s) => (s.id === id ? { ...s, status } : s))); - }, []); + }, [screens, connections, documents, pushHistory]); // Component (shared/reusable screen) actions. // mode: "canonical" | "instance" | "unlink" @@ -328,8 +329,9 @@ export function useScreenManager(pan, zoom, canvasRef, commentCallbacks = {}) { }, [screens, connections, documents, pushHistory]); const markAllExisting = useCallback(() => { + pushHistory(screens, connections, documents); setScreens((prev) => prev.map((s) => ({ ...s, status: "existing" }))); - }, []); + }, [screens, connections, documents, pushHistory]); // Lightweight image patch for collab sync (no undo, no dimension clear) const patchScreenImage = useCallback((id, imageData) => { From 439deb98b48fb63a92fce5d6dc440f59207cb1a4 Mon Sep 17 00:00:00 2001 From: Quang Tran <16215255+trmquang93@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:15:25 +0700 Subject: [PATCH 2/2] feat: bidirectional screen status with right-click menus The canvas status chip is now always visible (previously hidden for status="new") and is clickable: left-click cycles, right-click opens a 3-option menu to jump directly to New/Modify/Existing. The right Sidebar's Build status pill gains the same right-click menu and now honors isReadOnly. Both popovers render via React portals so they escape the transformed canvas ancestor and use Z_INDEX.contextMenu. Read-only viewers see a plain status chip with no interactions. User guide updated with a Build status subsection covering the three statuses, the click vs right-click affordances, and the undoable bulk "All existing" action. Refs backlog 8.2. --- src/Drawd.jsx | 1 + src/components/CanvasArea.jsx | 3 +- src/components/ScreenNode.jsx | 94 +++++++++++++++++++++++++++++++++-- src/components/Sidebar.jsx | 73 +++++++++++++++++++++++++-- src/pages/docs/userGuide.md | 20 ++++++++ 5 files changed, 183 insertions(+), 8 deletions(-) diff --git a/src/Drawd.jsx b/src/Drawd.jsx index f363bd2..413ba3f 100644 --- a/src/Drawd.jsx +++ b/src/Drawd.jsx @@ -625,6 +625,7 @@ export default function Drawd({ initialRoomCode }) { toggleSelection={toggleSelection} onMultiDragStart={onMultiDragStart} isReadOnly={isReadOnly} + onUpdateStatus={updateScreenStatus} onFormSummary={(screenId) => { const s = screens.find((sc) => sc.id === screenId); if (s) setFormSummaryScreen(s); diff --git a/src/components/CanvasArea.jsx b/src/components/CanvasArea.jsx index b88d933..9fca43e 100644 --- a/src/components/CanvasArea.jsx +++ b/src/components/CanvasArea.jsx @@ -33,7 +33,7 @@ export function CanvasArea({ onHotspotDragHandleMouseDown, onResizeHandleMouseDown, onScreenDimensions, drawRect, updateScreenDescription, addState, handleDropImage, activeTool, scopeRoot, scopeScreenIds, canvasSelection, toggleSelection, onMultiDragStart, - isReadOnly, onFormSummary, + isReadOnly, onFormSummary, onUpdateStatus, // Sticky notes stickyNotes, selectedStickyNote, updateStickyNote, deleteStickyNote, addStickyNote, // Selection overlay @@ -208,6 +208,7 @@ export function CanvasArea({ onToggleSelect={toggleSelection} onMultiDragStart={onMultiDragStart} isReadOnly={isReadOnly} + onUpdateStatus={onUpdateStatus} onFormSummary={onFormSummary} mcpFlash={mcpFlashIds?.has(screen.id)} commentPins={(comments || []).filter( diff --git a/src/components/ScreenNode.jsx b/src/components/ScreenNode.jsx index 941a9ee..0d34f01 100644 --- a/src/components/ScreenNode.jsx +++ b/src/components/ScreenNode.jsx @@ -1,5 +1,6 @@ import { useState, useRef, useCallback, useEffect } from "react"; -import { COLORS, FONTS } from "../styles/theme"; +import { createPortal } from "react-dom"; +import { COLORS, FONTS, STATUS_CONFIG, STATUS_CYCLE, Z_INDEX } from "../styles/theme"; import { DEFAULT_SCREEN_WIDTH, DEFAULT_IMAGE_HEIGHT, HEADER_HEIGHT, DESCRIPTION_MAX_LENGTH } from "../constants"; import { CommentPin } from "./CommentPin"; @@ -13,6 +14,7 @@ export function ScreenNode({ scopeRoot, isInScope, onContextMenu, isMultiSelected, onToggleSelect, onMultiDragStart, isReadOnly, + onUpdateStatus, onFormSummary, mcpFlash, // Comment mode @@ -31,6 +33,7 @@ export function ScreenNode({ const [draftDesc, setDraftDesc] = useState(""); const [isDragOver, setIsDragOver] = useState(false); const [altHeld, setAltHeld] = useState(false); + const [statusMenu, setStatusMenu] = useState(null); // { x, y } | null const imgRef = useRef(null); const [prevImageData, setPrevImageData] = useState(screen.imageData); @@ -313,7 +316,7 @@ export function ScreenNode({ ↗ Instance )} - {(status !== "new" || isScopeRoot) && ( + {isReadOnly ? ( {STATUS_CHIP[status].label} + ) : ( + )} @@ -808,6 +840,62 @@ export function ScreenNode({ border: `2px solid ${COLORS.surface}`, }} /> + {statusMenu && createPortal( +
setStatusMenu(null)} + onContextMenu={(e) => { e.preventDefault(); setStatusMenu(null); }} + style={{ position: "fixed", inset: 0, zIndex: Z_INDEX.contextMenu - 1 }} + > +
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + style={{ + position: "fixed", + top: statusMenu.y, + left: statusMenu.x, + background: COLORS.surface, + border: `1px solid ${COLORS.border}`, + borderRadius: 6, + boxShadow: "0 4px 16px rgba(0,0,0,0.4)", + zIndex: Z_INDEX.contextMenu, + minWidth: 160, + overflow: "hidden", + padding: "4px 0", + }} + > + {(["new", "modify", "existing"]).map((s) => { + const cfg = STATUS_CONFIG[s]; + const isCurrent = status === s; + return ( + + ); + })} +
+
, + document.body + )} ); } diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 5aeacac..d8b4785 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -1,5 +1,6 @@ -import { COLORS, FONTS, STATUS_CONFIG, STATUS_CYCLE, COMPONENT_CONFIG } from "../styles/theme"; +import { COLORS, FONTS, STATUS_CONFIG, STATUS_CYCLE, COMPONENT_CONFIG, Z_INDEX } from "../styles/theme"; import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; import { COPY_FEEDBACK_MS, SIDEBAR_WIDTH } from "../constants"; export function Sidebar({ screen, screens, connections, onClose, onRename, onAddHotspot, onEditHotspot, onAddState, onSelectScreen, onUpdateStateName, onUpdateNotes, onUpdateCodeRef, onUpdateCriteria, onUpdateStatus, onUpdateTbd, onUpdateRoles, onSetComponent, isReadOnly }) { @@ -13,6 +14,7 @@ export function Sidebar({ screen, screens, connections, onClose, onRename, onAdd const [newRole, setNewRole] = useState(""); const [rolesScreenId, setRolesScreenId] = useState(screen.id); const [idCopied, setIdCopied] = useState(false); + const [statusMenu, setStatusMenu] = useState(null); // { x, y } | null // Reset the "Copied!" flag when switching to a different screen so it // never lingers from a previous screen's click. @@ -212,8 +214,15 @@ export function Sidebar({ screen, screens, connections, onClose, onRename, onAdd Build status + {statusMenu && createPortal( +
setStatusMenu(null)} + onContextMenu={(e) => { e.preventDefault(); setStatusMenu(null); }} + style={{ position: "fixed", inset: 0, zIndex: Z_INDEX.contextMenu - 1 }} + > +
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + style={{ + position: "fixed", + top: statusMenu.y, + left: statusMenu.x, + background: COLORS.surface, + border: `1px solid ${COLORS.border}`, + borderRadius: 6, + boxShadow: "0 4px 16px rgba(0,0,0,0.4)", + zIndex: Z_INDEX.contextMenu, + minWidth: 160, + overflow: "hidden", + padding: "4px 0", + }} + > + {(["new", "modify", "existing"]).map((s) => { + const cfg = STATUS_CONFIG[s]; + const isCurrent = status === s; + return ( + + ); + })} +
+
, + document.body + )} {/* Reusable Component */}
[!TIP] > Multiple images can be uploaded or dropped at once. Drawd auto-arranges them in a grid layout on the canvas. +### Build status + +Every screen carries a build status that drives instruction generation: + +- `New` — screen needs to be built. Becomes a "to build" task in the generated AI instructions. +- `Modify` — screen already exists in the codebase but needs changes. Treated as a build task with edit context. +- `Existing` — screen is already built and only referenced for navigation context. Excluded from build tasks. + +The status chip is always visible in the canvas card header and in the right Sidebar's "Build status" pill. To change it: + +- **Left-click** the chip or pill to cycle: `New → Modify → Existing → New`. +- **Right-click** the chip or pill to open a menu and jump directly to any status. +- In the left Screens Panel, the per-row chip cycles on click and right-clicking the row opens the same menu. +- The "All existing" header button in the Screens Panel marks every screen as `Existing` in one click — useful when adding a single new screen to a fully built app. + +All status changes (single-screen and bulk) participate in undo history — press `Cmd/Ctrl+Z` to revert. + +> [!NOTE] +> Read-only viewers (collab guests on a shared flow) see the status chip but cannot click or right-click to change it. + ## Canvas Navigation The canvas is an infinite workspace where you arrange and connect screens. You can pan freely in any direction and zoom in or out.