🐛 Drawer/Dock Layout Snap on Close
Date: 2026-04-12
Severity: High (visual regression)
Status: ✅ Fixed
Components: GameDrawer, GameDock, gameDrawer.svelte.ts, play/[id]/+page.svelte
Symptoms
- Empty space / ghost gap flashes briefly after closing the game drawer
- Dock bar snaps rightward momentarily before settling at full width
- Layout “jumps” visible on desktop when repeatedly opening/closing drawer
Root Cause
The --drawer-w CSS custom property was being removed instantly when the drawer closed, but multiple CSS consumers (.play-root, .game-dock, .pool-iframe) were still mid-transition using transition: right 0.22s:
GameDrawer.svelte$effectcleanup calledremoveProperty('--drawer-w')synchronously whengameDrawer.open→false.play-root.drawer-openusedright: var(--drawer-w, 24rem)with a 220ms transition- When
--drawer-wvanished mid-transition, CSS fell back to24rem(the hardcoded fallback) → visible snap - Same issue affected
GameDock(.game-dock--drawer-open) andViewportPooliframe sizing
Secondary issue: dockEl in GameDock.svelte was declared as plain let instead of $state, risking null when bind:this assignment raced against onMount + transition:fly.
Fix
1. Centralized --drawer-w lifecycle in gameDrawer.svelte.ts
openDrawer(): Cancels any pending cleanup timer, pre-seeds--drawer-wif missingcloseDrawer(): DefersremoveProperty('--drawer-w')by250ms(> transition duration220ms) so CSS transitions animate using the live pixel value, never hitting the 24rem fallbackreset(): Immediately removes the var (no transition context)- Timer is tracked via
drawerWidthCleanupTimer— cancelled if drawer re-opens before cleanup fires
2. Removed premature cleanup from GameDrawer.svelte $effect
The ResizeObserver effect no longer calls removeProperty('--drawer-w') in its cleanup. The store is the single owner of the CSS var lifecycle.
3. Fixed dockEl reactivity in GameDock.svelte
Changed let dockEl: HTMLElement | null = null → $state(null) for proper Svelte 5 bind:this reactivity.
Files Changed
| File | Change |
|---|---|
src/lib/stores/gameDrawer.svelte.ts | Added TRANSITION_DURATION_MS, DEFAULT_DRAWER_WIDTH, deferred cleanup in closeDrawer(), cancel-on-reopen in openDrawer(), immediate cleanup in reset() |
src/lib/components/games/GameDrawer.svelte | Removed removeProperty('--drawer-w') from $effect cleanup |
src/lib/components/games/GameDock.svelte | dockEl → $state(null) |
Consumers of --drawer-w
| Consumer | Fallback | Guarded by .drawer-open? | Status |
|---|---|---|---|
.play-root.drawer-open | 24rem | ✅ Yes | Fixed (deferred removal) |
.game-dock--drawer-open | 24rem | ✅ Yes | Fixed (deferred removal) |
ViewportPool .pool-active | 0px | ✅ Yes (:global(.drawer-open)) | Was safe |
NativeGameHost .pool-native | 0px | ✅ Yes (:global(.drawer-open)) | Was safe |
GameDrawer itself | 24rem | N/A (removed with {#if}) | Converges on mount |
Verification
svelte-check: 0 errors ✅vite build: Success ✅