mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 10:03:29 +00:00
8346f9348e
* feat(web): replace desktop navbar scroll with overflow menu (TASK-759) The desktop top bar's workspace list previously used `overflow-x: auto` with a hidden scrollbar — workspaces past the visible edge were reachable only by horizontal scroll, with no visual cue that anything was hidden. Mobile already solved this via BottomSheet (TASK-637); desktop never got the equivalent. This change implements a "priority+" overflow pattern in TopBar.svelte: - Pills are measured in a hidden ghost row keyed by slug. - A ResizeObserver tracks the visible container's width. - Pills that don't fit move into a `…` overflow menu anchored under the trigger. The active workspace is pinned to the visible row regardless of fit position so the "you are here" cue is never hidden. - The trigger is always rendered (with `visibility: hidden` when empty) to prevent layout oscillation as workspaces are added or removed. Drag-and-drop works to and from the overflow menu on day one. Three dndzones share `type: 'topbar-workspace'`: the visible row, the menu, and the trigger as a single-slot drop target. A 400 ms spring-loaded auto-open lets the user drag onto the trigger and place the dropped item at a precise position inside the menu. Dropping on the trigger without waiting appends to overflow. Active is rejected from overflow finalize and snapped back to visible. Persistence reuses the existing `api.workspaces.reorder()` path. Both zones' finalize events are coalesced into a single persist via queueMicrotask. A 1s `dropCooldown` prevents store→local sync from fighting the just-written order, mirroring BoardView's pattern. Mobile (≤640px) is unchanged — still uses WorkspaceSwitcher BottomSheet. Spec: IDEA-758. * fix(web): address Codex review round 1 (TASK-759) Per Codex review on PR #244, round 1: HIGH — Drop active onto `…` trigger silently dropped active from the persisted order. handleTriggerFinalize stripped active from droppedSafe without restoring it to visibleZone, so persistGlobalOrder rebuilt fullOrder = visibleZone + overflowZone with active missing from both. Now both rejection paths (overflow zone and trigger zone) reset all zones from the un-mutated propVisible/propOverflow derived split and cancel the queued persist via cancelPersist(). MEDIUM — Active-pin rejection in the overflow zone snapped active to the END of visible instead of restoring its original position. Same fix as above — reset from the derived split, which preserves sort order. MEDIUM — Failure rollback was hidden by dropCooldown for ~1s. The catch block now also clears the cooldown timer, immediately resyncs zones from the restored derived split, and unblocks the sync effect. MEDIUM — dropCooldown setTimeouts stacked. Track a single cooldownTimer, clearTimeout it on each new write, and cancel on rollback. MEDIUM — A single long active-workspace name could blow past the bar because active is pinned visible. Cap `.workspace-name` at max-width 200px with ellipsis inside `.workspace-list` and `.workspace-ghost` (not in the overflow menu — full names read better there). LOW — Lost the "click current workspace → workspace dashboard" override during the click-handler refactor. The pre-PR onclick branched on `ws.slug === currentSlug`. Restored. LOW — Pending springLoadTimer / cooldownTimer would survive component destroy. Added an $effect cleanup that cancels both on unmount. * fix(web): address Codex review round 2 (TASK-759) HIGH — Active-pin rejection only worked when the target zone's finalize fired AFTER the source's. svelte-dnd-action does not guarantee the order, so when handleVisibleFinalize ran AFTER handleOverflow/Trigger finalize, it overwrote the freshly-restored visibleZone with its own post-drag items (which excluded active). Added a `dragRejected` flag: target-zone rejection sets it, handleVisibleFinalize early-returns if set so the reset isn't clobbered. Cleared at the start of every consider event so it doesn't bleed across drags. MEDIUM — Cooldown timer race: a prior persist's pending timer was only cleared AFTER awaiting the new persist's reorder/load, so it could fire mid-request and flip dropCooldown false while a newer write was still in flight. Cleared the prior timer at the start of persistGlobalOrder (before the await) instead. * fix(web): address Codex review round 3 (TASK-759) MEDIUM — persistCancelled could leak past a rejected active-pin drag. On pointer DnD svelte-dnd-action finalizes the target zone BEFORE the source. In that order, cancelPersist() runs in the rejection handler when no microtask was queued (the source's schedulePersist hadn't fired yet), then handleVisibleFinalize early-returns on dragRejected without scheduling. The flag was left set, so the next legitimate reorder was silently dropped. Fixed by clearing persistCancelled at the start of schedulePersist — each new schedule begins from a clean slate, regardless of what stale state a prior rejection may have left.