From 4ed0f0846e009ae88282a0079b4bf1625722b20c Mon Sep 17 00:00:00 2001 From: Rohit Rajendran Date: Tue, 24 Mar 2026 22:35:03 -0400 Subject: [PATCH 1/9] feat: add propertiesPanelOpen flag and silent-update store actions Add propertiesPanelOpen boolean to useToolStore (default false) with setPropertiesPanelOpen action; reset on clearSelection and setActiveTool. Add snapshotForUndo and updateElementSilent to useFloorplanStore to support single-checkpoint undo for gestures and revert-on-cancel for the mobile properties panel. --- .../useFloorplanStore.test.ts | 58 +++++++++++++++++++ .../useFloorplanStore/useFloorplanStore.ts | 30 ++++++++++ src/store/useToolStore/useToolStore.test.ts | 26 +++++++++ src/store/useToolStore/useToolStore.ts | 11 +++- 4 files changed, 124 insertions(+), 1 deletion(-) diff --git a/src/store/useFloorplanStore/useFloorplanStore.test.ts b/src/store/useFloorplanStore/useFloorplanStore.test.ts index 4d6463b..c1edbdb 100644 --- a/src/store/useFloorplanStore/useFloorplanStore.test.ts +++ b/src/store/useFloorplanStore/useFloorplanStore.test.ts @@ -182,6 +182,64 @@ describe('updateElement', () => { }); }); +describe('updateElementSilent', () => { + it('updates the element without pushing to undo history', () => { + useFloorplanStore.getState().addElement(box()); + const pastBefore = useFloorplanStore.getState().past.length; + useFloorplanStore.getState().updateElementSilent('b1', { rotation: 45 }); + const el = getElements()[0] as Extract; + expect(el.rotation).toBe(45); + expect(useFloorplanStore.getState().past.length).toBe(pastBefore); // no new history entry + }); + + it('does not clear future history', () => { + useFloorplanStore.getState().addElement(box()); + useFloorplanStore.getState().updateElement('b1', { rotation: 10 }); + useFloorplanStore.getState().undo(); + const futureLength = useFloorplanStore.getState().future.length; + expect(futureLength).toBeGreaterThan(0); + useFloorplanStore.getState().updateElementSilent('b1', { rotation: 20 }); + expect(useFloorplanStore.getState().future.length).toBe(futureLength); // future unchanged + }); +}); + +describe('snapshotForUndo', () => { + it('pushes current elements to past without changing elements', () => { + useFloorplanStore.getState().addElement(box()); + const elementsBefore = getElements(); + const pastLengthBefore = useFloorplanStore.getState().past.length; + useFloorplanStore.getState().snapshotForUndo(); + expect(useFloorplanStore.getState().past.length).toBe(pastLengthBefore + 1); + expect(getElements()).toEqual(elementsBefore); // elements unchanged + }); + + it('clears future when called', () => { + useFloorplanStore.getState().addElement(box()); + useFloorplanStore.getState().updateElement('b1', { rotation: 10 }); + useFloorplanStore.getState().undo(); + expect(useFloorplanStore.getState().future.length).toBeGreaterThan(0); + useFloorplanStore.getState().snapshotForUndo(); + expect(useFloorplanStore.getState().future.length).toBe(0); + }); + + it('is a no-op on empty plan', () => { + const pastBefore = useFloorplanStore.getState().past.length; + useFloorplanStore.getState().snapshotForUndo(); + expect(useFloorplanStore.getState().past.length).toBe(pastBefore); + }); + + it('snapshot + silent updates = single undo step', () => { + useFloorplanStore.getState().addElement(box()); + useFloorplanStore.getState().snapshotForUndo(); + useFloorplanStore.getState().updateElementSilent('b1', { rotation: 30 }); + useFloorplanStore.getState().updateElementSilent('b1', { rotation: 60 }); + useFloorplanStore.getState().updateElementSilent('b1', { rotation: 90 }); + expect((getElements()[0] as Extract).rotation).toBe(90); + useFloorplanStore.getState().undo(); + expect((getElements()[0] as Extract).rotation).toBe(0); // back to original + }); +}); + describe('deleteElement', () => { it('removes the element', () => { useFloorplanStore.getState().addElement(wall()); diff --git a/src/store/useFloorplanStore/useFloorplanStore.ts b/src/store/useFloorplanStore/useFloorplanStore.ts index 2c617c5..44da994 100644 --- a/src/store/useFloorplanStore/useFloorplanStore.ts +++ b/src/store/useFloorplanStore/useFloorplanStore.ts @@ -32,10 +32,14 @@ type FloorplanStore = { // Element management addElement: (element: Element) => void; updateElement: (id: string, updates: Partial) => void; + updateElementSilent: (id: string, updates: Partial) => void; updateElements: (updates: Record>) => void; deleteElement: (id: string) => void; deleteElements: (ids: Iterable) => void; + // Undo snapshot without changing elements (use before a silent-update gesture) + snapshotForUndo: () => void; + // Undo / redo undo: () => void; redo: () => void; @@ -208,6 +212,23 @@ export const useFloorplanStore = create((set, get) => ({ }); }, + updateElementSilent: (id, updates) => { + set((state) => { + const current = state.plans.find((p) => p.id === state.activeId)?.elements ?? []; + const newElements = current.map((el) => + el.id === id ? ({ ...el, ...updates } as Element) : el, + ); + if (sameElements(current, newElements)) return {}; + const plans = state.plans.map((p) => + p.id === state.activeId + ? { ...p, elements: cloneElements(newElements), updatedAt: new Date().toISOString() } + : p, + ); + persist(plans, state.activeId); + return { plans }; + }); + }, + updateElements: (updates) => { set((state) => { const current = state.plans.find((p) => p.id === state.activeId)?.elements ?? []; @@ -240,6 +261,15 @@ export const useFloorplanStore = create((set, get) => ({ }); }, + snapshotForUndo: () => { + set((state) => { + const current = state.plans.find((p) => p.id === state.activeId)?.elements ?? []; + if (current.length === 0) return {}; + const past = [...state.past.slice(-(MAX_HISTORY - 1)), cloneElements(current)]; + return { past, future: [] }; + }); + }, + undo: () => { set((state) => { if (state.past.length === 0) return {}; diff --git a/src/store/useToolStore/useToolStore.test.ts b/src/store/useToolStore/useToolStore.test.ts index b5f2578..a643ca4 100644 --- a/src/store/useToolStore/useToolStore.test.ts +++ b/src/store/useToolStore/useToolStore.test.ts @@ -13,6 +13,7 @@ beforeEach(() => { measureEnd: null, zoom: 1, pan: { x: 0, y: 0 }, + propertiesPanelOpen: false, }); }); @@ -142,6 +143,31 @@ describe('selection', () => { }); }); +describe('propertiesPanelOpen', () => { + it('defaults to false', () => { + expect(useToolStore.getState().propertiesPanelOpen).toBe(false); + }); + + it('setPropertiesPanelOpen toggles the flag', () => { + useToolStore.getState().setPropertiesPanelOpen(true); + expect(useToolStore.getState().propertiesPanelOpen).toBe(true); + useToolStore.getState().setPropertiesPanelOpen(false); + expect(useToolStore.getState().propertiesPanelOpen).toBe(false); + }); + + it('clearSelection resets propertiesPanelOpen to false', () => { + useToolStore.getState().setPropertiesPanelOpen(true); + useToolStore.getState().clearSelection(); + expect(useToolStore.getState().propertiesPanelOpen).toBe(false); + }); + + it('setActiveTool resets propertiesPanelOpen to false', () => { + useToolStore.getState().setPropertiesPanelOpen(true); + useToolStore.getState().setActiveTool('box'); + expect(useToolStore.getState().propertiesPanelOpen).toBe(false); + }); +}); + describe('zoom and pan', () => { it('setZoom updates the zoom level', () => { useToolStore.getState().setZoom(2.5); diff --git a/src/store/useToolStore/useToolStore.ts b/src/store/useToolStore/useToolStore.ts index 64fd810..59b0036 100644 --- a/src/store/useToolStore/useToolStore.ts +++ b/src/store/useToolStore/useToolStore.ts @@ -28,6 +28,10 @@ type ToolStore = { completeMeasurement: (pt: Point) => void; clearMeasurement: () => void; + // Properties panel visibility — controlled explicitly so mobile can keep canvas clear + propertiesPanelOpen: boolean; + setPropertiesPanelOpen: (v: boolean) => void; + // View — zoom is a multiplier (1 = default, 2 = 2x zoom in) zoom: number; setZoom: (zoom: number) => void; @@ -46,6 +50,7 @@ export const useToolStore = create((set) => ({ isChainArmed: false, measureStart: null, measureEnd: null, + propertiesPanelOpen: false, }), chainPoints: [], @@ -85,7 +90,8 @@ export const useToolStore = create((set) => ({ selectedId: selectedIds.size === 1 ? [...selectedIds][0] : null, }), - clearSelection: () => set({ selectedId: null, selectedIds: new Set() }), + clearSelection: () => + set({ selectedId: null, selectedIds: new Set(), propertiesPanelOpen: false }), measureStart: null, measureEnd: null, @@ -95,6 +101,9 @@ export const useToolStore = create((set) => ({ completeMeasurement: (measureEnd) => set({ measureEnd }), clearMeasurement: () => set({ measureStart: null, measureEnd: null }), + propertiesPanelOpen: false, + setPropertiesPanelOpen: (propertiesPanelOpen) => set({ propertiesPanelOpen }), + zoom: 1, setZoom: (zoom) => set({ zoom }), pan: { x: 0, y: 0 }, From 0584c71d8e377cf006e18044e39402bee514f35b Mon Sep 17 00:00:00 2001 From: Rohit Rajendran Date: Tue, 24 Mar 2026 22:35:13 -0400 Subject: [PATCH 2/9] feat: two-finger twist rotation for selected box on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detect the angle between two touch points and apply the delta to the selected box's rotation on each touchmove frame, snapped to 5° increments. A single snapshotForUndo checkpoint is taken at touchstart so the entire gesture undoes in one step. The properties panel is hidden during the gesture and restored on touchend. Also update mobile onSelect logic so first tap selects without opening the panel; second tap (or desktop) opens it. --- .../Canvas/DrawingCanvas/DrawingCanvas.tsx | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/src/components/Canvas/DrawingCanvas/DrawingCanvas.tsx b/src/components/Canvas/DrawingCanvas/DrawingCanvas.tsx index c5a8c0e..c437162 100644 --- a/src/components/Canvas/DrawingCanvas/DrawingCanvas.tsx +++ b/src/components/Canvas/DrawingCanvas/DrawingCanvas.tsx @@ -76,6 +76,10 @@ export function DrawingCanvas() { initialZoom: number; initialPan: { x: number; y: number }; initialMidpoint: { x: number; y: number }; + initialAngle: number; + selectedBoxId: string | null; + initialBoxRotation: number; + panelWasOpen: boolean; } | null>(null); const isTwoFingerActiveRef = useRef(false); @@ -189,17 +193,50 @@ export function DrawingCanvas() { }; } + function getTouchAngle(touches: TouchList) { + const t1 = touches[0]; + const t2 = touches[1]; + return Math.atan2(t2.clientY - t1.clientY, t2.clientX - t1.clientX) * (180 / Math.PI); + } + function handleTouchStart(e: TouchEvent) { if (e.touches.length === 2) { isTwoFingerActiveRef.current = true; const t1 = e.touches[0]; const t2 = e.touches[1]; const { zoom: currentZoom, pan: currentPan } = useToolStore.getState(); + + // Find selected box for rotation + const toolState = useToolStore.getState(); + const selIds = toolState.selectedIds; + let selectedBoxId: string | null = null; + let initialBoxRotation = 0; + if (selIds.size === 1) { + const [id] = selIds; + const el = useFloorplanStore + .getState() + .activePlan() + ?.elements.find((e) => e.id === id); + if (el?.type === 'box') { + selectedBoxId = id; + initialBoxRotation = el.rotation; + useFloorplanStore.getState().snapshotForUndo(); + } + } + + // Hide the properties panel during the gesture + const panelWasOpen = toolState.propertiesPanelOpen; + useToolStore.getState().setPropertiesPanelOpen(false); + touchGestureRef.current = { initialDist: getTouchDist(t1, t2), initialZoom: currentZoom, initialPan: { ...currentPan }, initialMidpoint: getTouchMidpoint(t1, t2), + initialAngle: getTouchAngle(e.touches), + selectedBoxId, + initialBoxRotation, + panelWasOpen, }; // Cancel any in-progress single-finger action setPointerDown(null); @@ -213,7 +250,15 @@ export function DrawingCanvas() { e.preventDefault(); const t1 = e.touches[0]; const t2 = e.touches[1]; - const { initialDist, initialZoom, initialPan, initialMidpoint } = touchGestureRef.current; + const { + initialDist, + initialZoom, + initialPan, + initialMidpoint, + initialAngle, + selectedBoxId, + initialBoxRotation, + } = touchGestureRef.current; const currentDist = getTouchDist(t1, t2); const currentMidpoint = getTouchMidpoint(t1, t2); @@ -234,11 +279,22 @@ export function DrawingCanvas() { y: currentMidpoint.y - worldUnderMidpoint.y * newZoom, }, }); + + // Apply rotation to selected box + if (selectedBoxId !== null) { + const deltaAngle = getTouchAngle(e.touches) - initialAngle; + const raw = initialBoxRotation + deltaAngle; + const snapped = Math.round((((raw % 360) + 360) % 360) / 5) * 5; + useFloorplanStore.getState().updateElementSilent(selectedBoxId, { rotation: snapped }); + } } } function handleTouchEnd(e: TouchEvent) { if (e.touches.length < 2) { + if (touchGestureRef.current) { + useToolStore.getState().setPropertiesPanelOpen(touchGestureRef.current.panelWasOpen); + } touchGestureRef.current = null; // Brief delay so the finger-lift pointer event doesn't trigger a tap setTimeout(() => { @@ -481,6 +537,9 @@ export function DrawingCanvas() { const rh = Math.abs(marquee.end.y - marquee.start.y); const hit = elements.filter((el) => elementOverlapsRect(el, rx, ry, rw, rh)); setSelectedIds(new Set(hit.map((el) => el.id))); + if (hit.length === 1 && !shouldUseMobileOverlayLayout(window.innerWidth)) { + useToolStore.getState().setPropertiesPanelOpen(true); + } } else if (isCanvasBackgroundTarget(e.target)) { clearSelection(); } @@ -801,10 +860,18 @@ export function DrawingCanvas() { onSelect={(extendSelection) => { if (activeTool !== 'select') return; const toolStore = useToolStore.getState(); + const isMobile = shouldUseMobileOverlayLayout(window.innerWidth); if (extendSelection) { toolStore.toggleSelectedId(el.id); + const single = useToolStore.getState().selectedIds.size === 1; + toolStore.setPropertiesPanelOpen(!isMobile && single); } else { + const alreadySelected = + toolStore.selectedIds.has(el.id) && toolStore.selectedIds.size === 1; toolStore.setSelectedId(el.id); + if (!isMobile || alreadySelected) { + toolStore.setPropertiesPanelOpen(true); + } } }} onGroupDrag={handleGroupDrag} @@ -817,10 +884,18 @@ export function DrawingCanvas() { onSelect={(extendSelection) => { if (activeTool !== 'select') return; const toolStore = useToolStore.getState(); + const isMobile = shouldUseMobileOverlayLayout(window.innerWidth); if (extendSelection) { toolStore.toggleSelectedId(el.id); + const single = useToolStore.getState().selectedIds.size === 1; + toolStore.setPropertiesPanelOpen(!isMobile && single); } else { + const alreadySelected = + toolStore.selectedIds.has(el.id) && toolStore.selectedIds.size === 1; toolStore.setSelectedId(el.id); + if (!isMobile || alreadySelected) { + toolStore.setPropertiesPanelOpen(true); + } } }} onGroupDrag={handleGroupDrag} From d2450a68d297cbc8bfffdb1daf8b4b4ac5c25c77 Mon Sep 17 00:00:00 2001 From: Rohit Rajendran Date: Tue, 24 Mar 2026 22:35:22 -0400 Subject: [PATCH 3/9] feat: add MobileSelectionBar for mobile single-element selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On mobile (≤730px), selecting one element shows a compact action bar at the toolbar slot instead of immediately opening the properties panel. The bar provides Undo, Redo, Edit, and Delete actions. Tapping Edit opens the properties panel and switches the bar to a Cancel/Done mode. Cancel silently restores the element to its pre-edit snapshot; Done commits changes. The toolbar is hidden whenever the bar is visible. --- src/App.tsx | 2 + .../MobileSelectionBar.module.css | 98 +++++++++++++ .../MobileSelectionBar/MobileSelectionBar.tsx | 132 ++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 src/components/Canvas/MobileSelectionBar/MobileSelectionBar.module.css create mode 100644 src/components/Canvas/MobileSelectionBar/MobileSelectionBar.tsx diff --git a/src/App.tsx b/src/App.tsx index f944aef..a7abc80 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import { PropertiesPanel } from './components/PropertiesPanel/PropertiesPanel'; import { HelpOverlay } from './components/HelpOverlay/HelpOverlay'; import { ScaleBar } from './components/Canvas/ScaleBar/ScaleBar'; import { MultiSelectBar } from './components/Canvas/MultiSelectBar/MultiSelectBar'; +import { MobileSelectionBar } from './components/Canvas/MobileSelectionBar/MobileSelectionBar'; import { useFloorplanStore } from './store/useFloorplanStore/useFloorplanStore'; import { decodePlanFromUrl } from './utils/storage/storage'; import styles from './App.module.css'; @@ -65,6 +66,7 @@ export default function App() { + {showHelp && setShowHelp(false)} />} ); diff --git a/src/components/Canvas/MobileSelectionBar/MobileSelectionBar.module.css b/src/components/Canvas/MobileSelectionBar/MobileSelectionBar.module.css new file mode 100644 index 0000000..ac755f2 --- /dev/null +++ b/src/components/Canvas/MobileSelectionBar/MobileSelectionBar.module.css @@ -0,0 +1,98 @@ +.bar { + position: fixed; + bottom: 12px; + left: 50%; + transform: translateX(-50%); + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(8px); + border: 1px solid #c8c4bc; + border-radius: 14px; + padding: 8px; + display: flex; + align-items: center; + justify-content: center; + gap: 2px; + font-family: 'Courier New', monospace; + z-index: 150; + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.12); + user-select: none; +} + +.btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + background: transparent; + border: none; + border-radius: 10px; + color: #262626; + font-family: inherit; + font-size: 12px; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; + padding: 10px 14px; + cursor: pointer; + min-height: 60px; + min-width: 60px; + touch-action: manipulation; + transition: background 0.15s, color 0.15s; +} + +.icon { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; +} + +.icon svg { + width: 100%; + height: 100%; +} + +.btn:hover { + background: #f0ede8; +} + +.btn:disabled { + opacity: 0.35; + cursor: default; +} + +.btn:disabled:hover { + background: transparent; +} + +.btn:focus-visible { + outline: 2px solid #4a6fa5; + outline-offset: 2px; +} + +.editBtn { + composes: btn; +} + +.deleteBtn { + composes: btn; + color: #b83333; +} + +.deleteBtn:hover { + background: #fce8e8; +} + +.cancelBtn { + composes: btn; +} + +.doneBtn { + composes: btn; + color: #2a7a4a; +} + +.doneBtn:hover { + background: #e8f5ee; +} diff --git a/src/components/Canvas/MobileSelectionBar/MobileSelectionBar.tsx b/src/components/Canvas/MobileSelectionBar/MobileSelectionBar.tsx new file mode 100644 index 0000000..9683770 --- /dev/null +++ b/src/components/Canvas/MobileSelectionBar/MobileSelectionBar.tsx @@ -0,0 +1,132 @@ +import { useRef } from 'react'; +import { Undo2, Redo2, SlidersHorizontal, Trash2, Check, X } from 'lucide-react'; +import { useToolStore } from '../../../store/useToolStore/useToolStore'; +import { useFloorplanStore } from '../../../store/useFloorplanStore/useFloorplanStore'; +import { shouldUseMobileOverlayLayout } from '../layout'; +import type { Element } from '../../../types'; +import styles from './MobileSelectionBar.module.css'; + +export function MobileSelectionBar() { + const { selectedIds, selectedId, clearSelection, setPropertiesPanelOpen, propertiesPanelOpen } = + useToolStore(); + const { activePlan, deleteElements, undo, redo, past, future, updateElementSilent } = + useFloorplanStore(); + + const editSnapshotRef = useRef(null); + + const isMobile = shouldUseMobileOverlayLayout(window.innerWidth); + if (!isMobile || selectedIds.size !== 1) return null; + + const plan = activePlan(); + const element = plan?.elements.find((el) => el.id === selectedId) ?? null; + if (!element) return null; + + function handleEdit() { + editSnapshotRef.current = { ...element } as Element; + setPropertiesPanelOpen(true); + } + + function handleCancel() { + const snapshot = editSnapshotRef.current; + if (snapshot) { + updateElementSilent(snapshot.id, snapshot); + editSnapshotRef.current = null; + } + setPropertiesPanelOpen(false); + } + + function handleDelete() { + deleteElements(selectedIds); + clearSelection(); + } + + if (propertiesPanelOpen) { + return ( +
+ + +
+ ); + } + + return ( +
+ + + + +
+ ); +} From d9e0d133a0501d31c654068ccbfcb7eb0688fa48 Mon Sep 17 00:00:00 2001 From: Rohit Rajendran Date: Tue, 24 Mar 2026 22:35:31 -0400 Subject: [PATCH 4/9] feat: adapt toolbar and properties panel for mobile selection UX Toolbar: hide via display:none (preserving keyboard shortcuts) whenever MobileSelectionBar is visible on mobile; add data-testid for targeting. PropertiesPanel: gate render on propertiesPanelOpen flag; hide the Delete button on mobile since it lives in MobileSelectionBar instead. --- .../PropertiesPanel/PropertiesPanel.tsx | 77 +++++++++++++------ src/components/Toolbar/Toolbar.tsx | 14 +++- 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/src/components/PropertiesPanel/PropertiesPanel.tsx b/src/components/PropertiesPanel/PropertiesPanel.tsx index 4ebc25c..b33f163 100644 --- a/src/components/PropertiesPanel/PropertiesPanel.tsx +++ b/src/components/PropertiesPanel/PropertiesPanel.tsx @@ -3,10 +3,19 @@ import { useFloorplanStore } from '../../store/useFloorplanStore/useFloorplanSto import { useToolStore } from '../../store/useToolStore/useToolStore'; import type { Box, Element, Wall } from '../../types'; import { collectConnectedWallIds, formatFeet } from '../../utils/geometry/geometry'; +import { shouldUseMobileOverlayLayout } from '../Canvas/layout'; import { FtInInput } from './FtInInput/FtInInput'; import styles from './PropertiesPanel.module.css'; -function WallProperties({ wall, onDelete }: { wall: Wall; onDelete: () => void }) { +function WallProperties({ + wall, + onDelete, + showDelete, +}: { + wall: Wall; + onDelete: () => void; + showDelete: boolean; +}) { const { updateElements, activePlan } = useFloorplanStore(); const allElements = activePlan()?.elements ?? []; @@ -71,19 +80,29 @@ function WallProperties({ wall, onDelete }: { wall: Wall; onDelete: () => void } testId={isSimple ? 'wall-length-input' : `wall-segment-${i}-input`} /> ))} - + {showDelete && ( + + )} ); } -function BoxProperties({ box, onDelete }: { box: Box; onDelete: () => void }) { +function BoxProperties({ + box, + onDelete, + showDelete, +}: { + box: Box; + onDelete: () => void; + showDelete: boolean; +}) { const { updateElement } = useFloorplanStore(); const [rotationDraft, setRotationDraft] = useState(null); const [labelDraft, setLabelDraft] = useState(null); @@ -150,21 +169,23 @@ function BoxProperties({ box, onDelete }: { box: Box; onDelete: () => void }) { data-testid="box-label-input" /> - + {showDelete && ( + + )} ); } export function PropertiesPanel() { const { activePlan, deleteElement } = useFloorplanStore(); - const { selectedId, selectedIds, setSelectedId } = useToolStore(); + const { selectedId, selectedIds, setSelectedId, propertiesPanelOpen } = useToolStore(); const plan = activePlan(); const element = @@ -176,7 +197,9 @@ export function PropertiesPanel() { setSelectedId(null); } - if (!element) return null; + if (!element || !propertiesPanelOpen) return null; + + const showDelete = !shouldUseMobileOverlayLayout(window.innerWidth); return (
{element.type === 'wall' ? 'Wall' : 'Box'}
{element.type === 'wall' ? ( - + ) : ( - + )} ); diff --git a/src/components/Toolbar/Toolbar.tsx b/src/components/Toolbar/Toolbar.tsx index 6d03d9c..e11bffb 100644 --- a/src/components/Toolbar/Toolbar.tsx +++ b/src/components/Toolbar/Toolbar.tsx @@ -10,6 +10,7 @@ import { } from 'lucide-react'; import { useToolStore } from '../../store/useToolStore/useToolStore'; import { useFloorplanStore } from '../../store/useFloorplanStore/useFloorplanStore'; +import { shouldUseMobileOverlayLayout } from '../Canvas/layout'; import type { ToolType } from '../../types'; import styles from './Toolbar.module.css'; @@ -25,9 +26,12 @@ type Props = { }; export function Toolbar({ onHelpOpen }: Props) { - const { activeTool, setActiveTool } = useToolStore(); + const { activeTool, setActiveTool, selectedIds } = useToolStore(); const { undo, redo, past, future } = useFloorplanStore(); + const selectionBarVisible = + shouldUseMobileOverlayLayout(window.innerWidth) && selectedIds.size === 1; + useEffect(() => { function onKeyDown(e: KeyboardEvent) { if (e.target instanceof HTMLInputElement) return; @@ -41,7 +45,13 @@ export function Toolbar({ onHelpOpen }: Props) { }, [setActiveTool]); return ( -
+
{TOOLS.map((tool) => (