From 1852618a2048028c9713dc69a3a34e666666cd74 Mon Sep 17 00:00:00 2001 From: flamboh Date: Thu, 5 Nov 2026 03:10:00 +0000 Subject: [PATCH 1/2] fix(web): holding the workspace key glides through workspaces without black frames Co-Authored-By: Claude Opus 5.5 --- apps/web/src/layout/SurfaceHost.tsx | 2 +- apps/web/src/motion/workspaceSwitch.test.ts | 19 +++++++++++++++ apps/web/src/motion/workspaceSwitch.ts | 26 +++++++++++++++------ 3 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/motion/workspaceSwitch.test.ts diff --git a/apps/web/src/layout/SurfaceHost.tsx b/apps/web/src/layout/SurfaceHost.tsx index 8f7cb6b6baae4..a1aa09291fd71 100644 --- a/apps/web/src/layout/SurfaceHost.tsx +++ b/apps/web/src/layout/SurfaceHost.tsx @@ -725,8 +725,8 @@ export function LayoutSurfaceHost({ if (!scroller || !element) return; if (stacked) { for (const row of stackRows) rememberWorkspaceCamera(row.id, row.camera); + if (!wasStacked.current) scroller.scrollLeft = 0; wasStacked.current = true; - scroller.scrollLeft = 0; applyWorkspaceStackTransform(); return; } diff --git a/apps/web/src/motion/workspaceSwitch.test.ts b/apps/web/src/motion/workspaceSwitch.test.ts new file mode 100644 index 0000000000000..e306320fbf9ce --- /dev/null +++ b/apps/web/src/motion/workspaceSwitch.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it, vi } from "vite-plus/test"; + +vi.mock(import("./stripCamera"), () => ({ + moveStripCamera: () => {}, + settleStripCamera: () => {}, +})); + +import { placeStackRow } from "./workspaceSwitch"; + +describe("placeStackRow", () => { + it("keeps every row the stage still crosses on its way to the new one", () => { + const rows = ["a", "b", "c", "d"].map((id, offset) => ({ id, offset })); + const placed = (position: number) => + placeStackRow(rows, { id: "e", offset: 4 }, rows[3]!, position).map((row) => row.id); + + expect(placed(0.2)).toEqual(["a", "b", "c", "d", "e"]); + expect(placed(2.9)).toEqual(["c", "d", "e"]); + }); +}); diff --git a/apps/web/src/motion/workspaceSwitch.ts b/apps/web/src/motion/workspaceSwitch.ts index 495cc0d7d736a..c2ae4a1ef1e00 100644 --- a/apps/web/src/motion/workspaceSwitch.ts +++ b/apps/web/src/motion/workspaceSwitch.ts @@ -160,16 +160,26 @@ function placeRow(id: string, from: WorkspaceStackRow, direction: 1 | -1): Works const offset = from.offset + direction; const existing = current.rows.find((row) => row.id === id); if (existing?.offset === offset) return existing; - const p = currentPosition(); const row = { id, offset }; - const rows = current.rows.filter( + setStack({ ...current, rows: placeStackRow(current.rows, row, from, currentPosition()) }); + return row; +} + +export function placeStackRow( + rows: readonly WorkspaceStackRow[], + row: WorkspaceStackRow, + from: WorkspaceStackRow, + position: number, +): WorkspaceStackRow[] { + const low = Math.min(position, row.offset) - 1; + const high = Math.max(position, row.offset) + 1; + const kept = rows.filter( (candidate) => - candidate.id !== id && - candidate.offset !== offset && - (candidate === from || Math.abs(candidate.offset - p) < 1), + candidate.id !== row.id && + candidate.offset !== row.offset && + (candidate === from || (candidate.offset > low && candidate.offset < high)), ); - setStack({ ...current, rows: [...rows, row].toSorted((a, b) => a.offset - b.offset) }); - return row; + return [...kept, row].toSorted((a, b) => a.offset - b.offset); } function rowOf(id: string): WorkspaceStackRow | null { @@ -196,6 +206,7 @@ function springTo(offset: number, velocity?: number): void { if (!host || !stack) return; const from = currentPosition(); const v = velocity ?? run?.velocity() ?? 0; + const retargeted = run !== null; run?.cancel(); run = null; const motion = MotionAnimation.start("workspace-switch", from, offset, v); @@ -212,6 +223,7 @@ function springTo(offset: number, velocity?: number): void { if (run === started) run = null; settle(); }, + retargeted ? document.timeline.currentTime : undefined, ); run = motion.durationMs > 0 ? started : null; settle(); From 7b7d1bea60c50ed7cd73dadc062b5fbdac344ce9 Mon Sep 17 00:00:00 2001 From: flamboh Date: Sat, 7 Nov 2026 18:10:42 +0000 Subject: [PATCH 2/2] test(web): drop the stack row test for a visual workspace slide fix --- apps/web/src/motion/workspaceSwitch.test.ts | 19 ------------------- apps/web/src/motion/workspaceSwitch.ts | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 apps/web/src/motion/workspaceSwitch.test.ts diff --git a/apps/web/src/motion/workspaceSwitch.test.ts b/apps/web/src/motion/workspaceSwitch.test.ts deleted file mode 100644 index e306320fbf9ce..0000000000000 --- a/apps/web/src/motion/workspaceSwitch.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { describe, expect, it, vi } from "vite-plus/test"; - -vi.mock(import("./stripCamera"), () => ({ - moveStripCamera: () => {}, - settleStripCamera: () => {}, -})); - -import { placeStackRow } from "./workspaceSwitch"; - -describe("placeStackRow", () => { - it("keeps every row the stage still crosses on its way to the new one", () => { - const rows = ["a", "b", "c", "d"].map((id, offset) => ({ id, offset })); - const placed = (position: number) => - placeStackRow(rows, { id: "e", offset: 4 }, rows[3]!, position).map((row) => row.id); - - expect(placed(0.2)).toEqual(["a", "b", "c", "d", "e"]); - expect(placed(2.9)).toEqual(["c", "d", "e"]); - }); -}); diff --git a/apps/web/src/motion/workspaceSwitch.ts b/apps/web/src/motion/workspaceSwitch.ts index c2ae4a1ef1e00..76f12d497c010 100644 --- a/apps/web/src/motion/workspaceSwitch.ts +++ b/apps/web/src/motion/workspaceSwitch.ts @@ -165,7 +165,7 @@ function placeRow(id: string, from: WorkspaceStackRow, direction: 1 | -1): Works return row; } -export function placeStackRow( +function placeStackRow( rows: readonly WorkspaceStackRow[], row: WorkspaceStackRow, from: WorkspaceStackRow,