From 50d1b373d3cbc43899b462c09a874a50f5c11906 Mon Sep 17 00:00:00 2001 From: highperfocused Date: Mon, 7 Sep 2026 21:59:29 +0200 Subject: [PATCH] Address remaining review feedback on app folders - Cap reconcileFolderState() at MAX_FOLDERS and 500 membership entries so it always produces schema-valid state, matching loadFolderState's parse limits (previously a state that saved fine could reload as empty). - Recognize folder tiles during desktop drag-over/drop hit-testing by also tagging them with data-home-icon-id and matching it in elementFromPoint lookups, so dragging an app onto a folder now highlights and files it. - Make moveToFolder stable via a ref in the mobile pointer-listeners effect, and drop the now-unnecessary exhaustive-deps suppression. - Let FolderVisual take a statusId prop instead of hardcoding 'icon-layout-status', so mobile folder tiles point aria-describedby at the mobile live region instead of a nonexistent desktop one. - Mint folder ids before calling setState instead of inside the updater, so createFolder's updater is pure (safe under strict-mode retries) while still returning the right id synchronously. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto --- src/components/os/Desktop.tsx | 9 +++++---- src/components/os/FolderIcon.tsx | 5 ++++- src/components/os/MobileAppShell.tsx | 10 ++++++++-- src/os/folders.ts | 11 ++++++++++- src/os/useFolders.ts | 16 ++++++---------- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/components/os/Desktop.tsx b/src/components/os/Desktop.tsx index 2a8f7c0..bb48181 100644 --- a/src/components/os/Desktop.tsx +++ b/src/components/os/Desktop.tsx @@ -177,16 +177,16 @@ export function Desktop() { setCandidate(cellAt(event.clientX, event.clientY)); // Dragging an app over a folder icon highlights it as a drop target. if (!active.id.startsWith(FOLDER_ID_PREFIX)) { - const hit = document.elementFromPoint(event.clientX, event.clientY)?.closest('[data-icon-id]'); - const over = hit?.dataset.iconId; + const hit = document.elementFromPoint(event.clientX, event.clientY)?.closest('[data-icon-id], [data-home-icon-id]'); + const over = hit?.dataset.iconId ?? hit?.dataset.homeIconId; setDropFolder(over?.startsWith(FOLDER_ID_PREFIX) && over !== active.id ? over : null); } }, [cellAt]); const finishPointer = useCallback((event: PointerEvent) => { const active = pointerStart.current; if (active?.moved) { - const hit = document.elementFromPoint(event.clientX, event.clientY)?.closest('[data-icon-id]'); - const over = hit?.dataset.iconId; + const hit = document.elementFromPoint(event.clientX, event.clientY)?.closest('[data-icon-id], [data-home-icon-id]'); + const over = hit?.dataset.iconId ?? hit?.dataset.homeIconId; if (!active.id.startsWith(FOLDER_ID_PREFIX) && over?.startsWith(FOLDER_ID_PREFIX) && over !== active.id) { moveToFolder(active.id, over.slice(FOLDER_ID_PREFIX.length)); } else { @@ -354,6 +354,7 @@ export function Desktop() { dragging={dragging === iconId} pickedUp={picked === iconId} dropTarget={dropFolder === iconId} + data-home-icon-id={iconId} tabIndex={0} style={{ position: 'absolute', left: SURFACE_PADDING + displaySlot.col * CELL_WIDTH, top: SURFACE_PADDING + displaySlot.row * CELL_HEIGHT, zIndex: dragging === iconId ? 2 : 1 }} onPointerDown={(event) => { diff --git a/src/components/os/FolderIcon.tsx b/src/components/os/FolderIcon.tsx index 9595b72..3700038 100644 --- a/src/components/os/FolderIcon.tsx +++ b/src/components/os/FolderIcon.tsx @@ -25,6 +25,8 @@ export interface FolderVisualProps { className?: string; /** Hit-target id used by drag-and-drop on the home screen. */ 'data-home-icon-id'?: string; + /** id of the live region announcing pick-up/move status; differs between desktop and mobile. */ + statusId?: string; } /** @@ -48,6 +50,7 @@ export function FolderVisual({ style, className, 'data-home-icon-id': homeIconId, + statusId = 'icon-layout-status', }: FolderVisualProps) { return (