diff --git a/src/Drawd.jsx b/src/Drawd.jsx index e3f926c..8e9945a 100644 --- a/src/Drawd.jsx +++ b/src/Drawd.jsx @@ -284,6 +284,9 @@ export default function Drawd({ initialRoomCode }) { setToast(message); toastTimerRef.current = setTimeout(() => setToast(null), duration); }, []); + useEffect(() => { + return () => { if (toastTimerRef.current) clearTimeout(toastTimerRef.current); }; + }, []); // ── Drag-over state (drop zone overlay) ─────────────────────────────────────────── const [isDraggingOver, setIsDraggingOver] = useState(false); @@ -307,13 +310,13 @@ export default function Drawd({ initialRoomCode }) { const drawdFile = detectDrawdFile(e.dataTransfer.files); if (!drawdFile) { const imageFiles = Array.from(e.dataTransfer.files).filter( - (f) => f.type === "image/png" || f.type === "image/jpeg" + (f) => f.type.startsWith("image/") ); if (imageFiles.length === 0) return; const rect = canvasRef.current.getBoundingClientRect(); const worldX = (e.clientX - rect.left - pan.x) / zoom; const worldY = (e.clientY - rect.top - pan.y) / zoom; - handleCanvasDrop(e, worldX, worldY); + handleCanvasDrop(imageFiles, worldX, worldY); showToast(`Created ${imageFiles.length} screen${imageFiles.length > 1 ? "s" : ""} from dropped images`); return; } diff --git a/src/components/CanvasArea.jsx b/src/components/CanvasArea.jsx index 3a9c8be..42f5e11 100644 --- a/src/components/CanvasArea.jsx +++ b/src/components/CanvasArea.jsx @@ -98,7 +98,7 @@ export function CanvasArea({ style={{ position: "absolute", inset: 0, - background: "rgba(97,175,239,0.08)", + background: COLORS.accent008, border: `2px dashed ${COLORS.accent}`, borderRadius: 12, display: "flex", diff --git a/src/components/Toast.jsx b/src/components/Toast.jsx index da8e944..79a5af3 100644 --- a/src/components/Toast.jsx +++ b/src/components/Toast.jsx @@ -15,7 +15,7 @@ export function Toast({ message }) { color: COLORS.text, fontFamily: FONTS.mono, fontSize: 13, - zIndex: Z_INDEX.contextMenu + 1, + zIndex: Z_INDEX.toast, boxShadow: "0 8px 32px rgba(0,0,0,0.5)", pointerEvents: "none", }}> diff --git a/src/hooks/useScreenManager.js b/src/hooks/useScreenManager.js index 8741bec..a2e16ad 100644 --- a/src/hooks/useScreenManager.js +++ b/src/hooks/useScreenManager.js @@ -19,6 +19,30 @@ import { HEADER_HEIGHT, } from "../constants"; +function makeScreen(overrides = {}) { + return { + id: generateId(), + name: "", + x: 0, + y: 0, + width: DEFAULT_SCREEN_WIDTH, + imageData: null, + description: "", + notes: "", + codeRef: "", + status: "new", + acceptanceCriteria: [], + hotspots: [], + stateGroup: null, + stateName: "", + tbd: false, + tbdNote: "", + roles: [], + figmaSource: null, + ...overrides, + }; +} + export function useScreenManager(pan, zoom, canvasRef) { const [screens, setScreens] = useState([]); const [connections, setConnections] = useState([]); @@ -109,26 +133,12 @@ export function useScreenManager(pan, zoom, canvasRef) { const count = screenCounter.current++; const offsetX = (screens.length % GRID_COLUMNS) * GRID_COL_WIDTH + GRID_MARGIN; const offsetY = Math.floor(screens.length / GRID_COLUMNS) * GRID_ROW_HEIGHT + GRID_MARGIN; - const newScreen = { - id: generateId(), + const newScreen = makeScreen({ name: name || SCREEN_NAME_TEMPLATE(count), x: (-pan.x + offsetX) / zoom, y: (-pan.y + offsetY) / zoom, - width: DEFAULT_SCREEN_WIDTH, imageData, - description: "", - notes: "", - codeRef: "", - status: "new", - acceptanceCriteria: [], - hotspots: [], - stateGroup: null, - stateName: "", - tbd: false, - tbdNote: "", - roles: [], - figmaSource: null, - }; + }); setScreens((prev) => [...prev, newScreen]); setSelectedScreen(newScreen.id); }, [screens, connections, documents, pushHistory, pan, zoom]); @@ -141,27 +151,13 @@ export function useScreenManager(pan, zoom, canvasRef) { const vh = el ? el.clientHeight : VIEWPORT_FALLBACK_HEIGHT; const cx = (-pan.x + vw / 2) / zoom - DEFAULT_SCREEN_WIDTH / 2 + offset * PASTE_STAGGER; const cy = (-pan.y + vh / 2) / zoom - CENTER_HEIGHT_ESTIMATE / 2 + offset * PASTE_STAGGER; - const newScreen = { - id: generateId(), + const newScreen = makeScreen({ name: name || SCREEN_NAME_TEMPLATE(count), x: cx, y: cy, - width: DEFAULT_SCREEN_WIDTH, imageData, - description: "", - notes: "", - codeRef: "", - status: "new", - acceptanceCriteria: [], - hotspots: [], - stateGroup: null, - stateName: "", - tbd: false, - tbdNote: "", - roles: [], - figmaSource: null, ...meta, - }; + }); setScreens((prev) => [...prev, newScreen]); setSelectedScreen(newScreen.id); }, [screens, connections, documents, pushHistory, pan, zoom, canvasRef]); @@ -169,25 +165,11 @@ export function useScreenManager(pan, zoom, canvasRef) { const addScreensBatch = useCallback((screenDefs) => { if (screenDefs.length === 0) return 0; pushHistory(screens, connections, documents); - const newScreens = screenDefs.map((def) => ({ - id: generateId(), + const newScreens = screenDefs.map((def) => makeScreen({ name: def.name, x: def.x, y: def.y, - width: DEFAULT_SCREEN_WIDTH, imageData: def.imageData, - description: "", - notes: "", - codeRef: "", - status: "new", - acceptanceCriteria: [], - hotspots: [], - stateGroup: null, - stateName: "", - tbd: false, - tbdNote: "", - roles: [], - figmaSource: null, })); setScreens((prev) => [...prev, ...newScreens]); setSelectedScreen(newScreens[0].id); @@ -357,11 +339,7 @@ export function useScreenManager(pan, zoom, canvasRef) { }); }, [addScreenAtCenter, selectedScreen, screens, assignScreenImage]); - const handleCanvasDrop = useCallback((e, worldX, worldY) => { - e.preventDefault(); - const files = Array.from(e.dataTransfer.files).filter( - (f) => f.type === "image/png" || f.type === "image/jpeg" - ); + const handleCanvasDrop = useCallback((files, worldX, worldY) => { if (files.length === 0) return; Promise.all( @@ -840,26 +818,14 @@ export function useScreenManager(pan, zoom, canvasRef) { } screenCounter.current++; - const newScreen = { - id: generateId(), + const newScreen = makeScreen({ name: parent.name, x: parent.x + STATE_VARIANT_OFFSET, y: parent.y, width: parent.width || DEFAULT_SCREEN_WIDTH, - imageData: null, - description: "", - notes: "", - codeRef: "", - status: "new", - acceptanceCriteria: [], - hotspots: [], stateGroup: groupId, stateName: `State ${stateNumber - 1}`, - tbd: false, - tbdNote: "", - roles: [], - figmaSource: null, - }; + }); setScreens((prev) => [...prev, newScreen]); setSelectedScreen(newScreen.id); }, [screens, connections, documents, pushHistory]); diff --git a/src/styles/theme.js b/src/styles/theme.js index 459d5b1..021ab9a 100644 --- a/src/styles/theme.js +++ b/src/styles/theme.js @@ -77,6 +77,7 @@ export const Z_INDEX = { batchBar: 900, modal: 1000, contextMenu: 9999, + toast: 10000, }; export const STATUS_CONFIG = { diff --git a/src/utils/dropImport.js b/src/utils/dropImport.js index bdcbd37..7e10794 100644 --- a/src/utils/dropImport.js +++ b/src/utils/dropImport.js @@ -1,4 +1,4 @@ -import { GRID_COLUMNS, GRID_COL_WIDTH, GRID_ROW_HEIGHT, DROP_OVERLAP_MARGIN } from "../constants"; +import { GRID_COLUMNS, GRID_COL_WIDTH, GRID_ROW_HEIGHT, GRID_MARGIN, DROP_OVERLAP_MARGIN } from "../constants"; import { rectsIntersect } from "./canvasMath"; /** @@ -22,10 +22,10 @@ export function filenameToScreenName(filename) { * @param {number[]} heights - height of each item * @param {number} originX * @param {number} originY - * @param {number} rowGap - vertical spacing between rows (default 60) + * @param {number} rowGap - vertical spacing between rows (default GRID_MARGIN) * @returns {Array<{x, y}>} */ -export function gridPositions(heights, originX, originY, rowGap = 60) { +export function gridPositions(heights, originX, originY, rowGap = GRID_MARGIN) { const positions = []; let rowY = originY; let rowMaxHeight = 0;