Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Drawd.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/components/CanvasArea.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
94 changes: 91 additions & 3 deletions src/components/ScreenNode.jsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -13,6 +14,7 @@ export function ScreenNode({
scopeRoot, isInScope, onContextMenu,
isMultiSelected, onToggleSelect, onMultiDragStart,
isReadOnly,
onUpdateStatus,
onFormSummary,
mcpFlash,
// Comment mode
Expand All @@ -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);

Expand Down Expand Up @@ -313,7 +316,7 @@ export function ScreenNode({
↗ Instance
</span>
)}
{(status !== "new" || isScopeRoot) && (
{isReadOnly ? (
<span
style={{
fontSize: 9,
Expand All @@ -324,11 +327,40 @@ export function ScreenNode({
padding: "1px 5px",
fontFamily: FONTS.mono,
whiteSpace: "nowrap",
display: status === "new" && !isScopeRoot ? "none" : "inline",
}}
>
{STATUS_CHIP[status].label}
</span>
) : (
<button
className="screen-btn"
title="Click to cycle • Right-click for options"
onMouseDown={(e) => { e.stopPropagation(); }}
onClick={(e) => {
e.stopPropagation();
onUpdateStatus?.(screen.id, STATUS_CYCLE[status]);
}}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
setStatusMenu({ x: e.clientX, y: e.clientY });
}}
style={{
fontSize: 9,
fontWeight: 600,
color: STATUS_CHIP[status].color,
background: STATUS_CHIP[status].bg,
border: "none",
borderRadius: 4,
padding: "1px 5px",
fontFamily: FONTS.mono,
whiteSpace: "nowrap",
cursor: "pointer",
lineHeight: 1.4,
}}
>
{STATUS_CHIP[status].label}
</button>
)}
</div>

Expand Down Expand Up @@ -808,6 +840,62 @@ export function ScreenNode({
border: `2px solid ${COLORS.surface}`,
}}
/>
{statusMenu && createPortal(
<div
onClick={() => setStatusMenu(null)}
onContextMenu={(e) => { e.preventDefault(); setStatusMenu(null); }}
style={{ position: "fixed", inset: 0, zIndex: Z_INDEX.contextMenu - 1 }}
>
<div
onClick={(e) => 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 (
<button
key={s}
onClick={() => { onUpdateStatus?.(screen.id, s); setStatusMenu(null); }}
style={{
display: "flex",
alignItems: "center",
gap: 8,
width: "100%",
padding: "7px 14px",
background: isCurrent ? COLORS.accent01 : "none",
border: "none",
color: COLORS.text,
cursor: "pointer",
textAlign: "left",
fontFamily: FONTS.ui,
fontSize: 12,
}}
onMouseEnter={(e) => { if (!isCurrent) e.currentTarget.style.background = COLORS.surfaceHover; }}
onMouseLeave={(e) => { if (!isCurrent) e.currentTarget.style.background = "none"; }}
>
<span style={{ width: 8, height: 8, borderRadius: "50%", background: cfg.color, flexShrink: 0 }} />
Mark as {cfg.label}
</button>
);
})}
</div>
</div>,
document.body
)}
</div>
);
}
73 changes: 69 additions & 4 deletions src/components/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -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 }) {
Expand All @@ -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.
Expand Down Expand Up @@ -212,8 +214,15 @@ export function Sidebar({ screen, screens, connections, onClose, onRename, onAdd
Build status
</span>
<button
onClick={() => onUpdateStatus?.(screen.id, STATUS_CYCLE[status])}
title="Click to cycle: New → Modify → Existing"
onClick={() => { if (isReadOnly) return; onUpdateStatus?.(screen.id, STATUS_CYCLE[status]); }}
onContextMenu={(e) => {
if (isReadOnly) return;
e.preventDefault();
e.stopPropagation();
setStatusMenu({ x: e.clientX, y: e.clientY });
}}
disabled={isReadOnly}
title={isReadOnly ? statusCfg.label : "Click to cycle • Right-click for options"}
style={{
padding: "3px 9px",
borderRadius: 4,
Expand All @@ -223,13 +232,69 @@ export function Sidebar({ screen, screens, connections, onClose, onRename, onAdd
fontFamily: FONTS.mono,
fontSize: 11,
fontWeight: 600,
cursor: "pointer",
cursor: isReadOnly ? "default" : "pointer",
letterSpacing: "0.03em",
}}
>
{statusCfg.label}
</button>
</div>
{statusMenu && createPortal(
<div
onClick={() => setStatusMenu(null)}
onContextMenu={(e) => { e.preventDefault(); setStatusMenu(null); }}
style={{ position: "fixed", inset: 0, zIndex: Z_INDEX.contextMenu - 1 }}
>
<div
onClick={(e) => 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 (
<button
key={s}
onClick={() => { onUpdateStatus?.(screen.id, s); setStatusMenu(null); }}
style={{
display: "flex",
alignItems: "center",
gap: 8,
width: "100%",
padding: "7px 14px",
background: isCurrent ? COLORS.accent01 : "none",
border: "none",
color: COLORS.text,
cursor: "pointer",
textAlign: "left",
fontFamily: FONTS.ui,
fontSize: 12,
}}
onMouseEnter={(e) => { if (!isCurrent) e.currentTarget.style.background = COLORS.surfaceHover; }}
onMouseLeave={(e) => { if (!isCurrent) e.currentTarget.style.background = "none"; }}
>
<span style={{ width: 8, height: 8, borderRadius: "50%", background: cfg.color, flexShrink: 0 }} />
Mark as {cfg.label}
</button>
);
})}
</div>
</div>,
document.body
)}

{/* Reusable Component */}
<div
Expand Down
6 changes: 4 additions & 2 deletions src/hooks/useScreenManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) => {
Expand Down
20 changes: 20 additions & 0 deletions src/pages/docs/userGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ Below the description is an "Implementation Notes" field. Use this for developer
> [!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.
Expand Down
Loading