Skip to content

Commit edef789

Browse files
waleedlatif1Waleed Latif
andauthored
feat(desktop): floating hover-peek sidebar (#6086)
* feat(desktop): floating hover-peek sidebar On the macOS desktop shell the collapsed sidebar has no width at all (`--sidebar-collapsed-width: 0`), so the only ways back were ⌘B and the title-bar toggle. Hovering that toggle now floats the sidebar in as a card over the content, inset from the window edge, and it retracts when the pointer leaves. Clicking the same control still docks it for good. The card is the existing `.sidebar-shell-outer` re-styled, not a second surface: `<Sidebar>` is never re-mounted, so its scroll position and expansion state survive a peek. One CSS rule re-points `--sidebar-width` at a new `--sidebar-expanded-width`, and the inner shell and aside follow with no per-element overrides. Chrome is all existing tokens — `--radius`, `--border`, `shadow-overlay`, `--z-modal` — and the fill is the sidebar's own `--surface-1`, so docked and floating are the same surface. Enter/exit use the popper idiom (`fade-in-0 zoom-in-95`) that emcn's Radix surfaces already use. Also migrates the search palette from raw `z-40`/`z-50` to `--z-modal`. It was the one overlay outranked by the new card, and the raw values also put it below `--z-toast`, inverting the order globals.css documents. Settings drive-bys, all pre-existing violations in files this touches: tokenize two literal pixel text sizes in passwords-view, route its cards through `SettingsResourceRow` instead of four re-derived chrome constants, and collapse three `{null}`-bodied `SettingsSection`s in browser settings into one section of real rows. * fix(desktop): close the peek's three timer races Review round 1 (Cursor Bugbot) found three ways the card could flicker: - A modal opening mid-dwell left the open timer pending, so the card mounted over the modal before the dismiss path could run. - A modal opening while the card was already animating out let it keep animating on top of the modal. - Re-hovering the toggle late in the exit window scheduled a fresh dwell without clearing the exit timer, so the card unmounted and then re-mounted — a visible gap with the pointer never leaving the toggle. The first two collapse into one fix: merge the two reset effects so an unavailable peek and a screen-owning modal both drop the card immediately, unconditionally clearing all three timers. Instant is also the right look, since the modal's scrim covers the card's position on the same frame. For the third, a re-hover during the exit now re-opens synchronously rather than scheduling another dwell — the card is already on screen, so the dwell that guards against accidental opens from a cold state does not apply. One regression test each. * fix(desktop): arm the peek before first paint Review round 2: the title-bar seed ran in a passive effect, which lands after paint — long enough for a `mouseenter` on the toggle to be dropped while the peek still read disabled, with nothing to retry it until the pointer left and returned. A regression from folding the seed into the window-state effect during cleanup. Promoted to a layout effect, which runs before paint and before any mouse event can be dispatched. The body is cheap synchronous reads plus a subscription; the bridge's getState stays async and blocks nothing. --------- Co-authored-by: Waleed Latif <waleed@simstudio.ai>
1 parent 11c0d3b commit edef789

13 files changed

Lines changed: 1064 additions & 127 deletions

File tree

apps/sim/app/_styles/globals.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
:root {
1313
--sidebar-width: 0px; /* 0 outside workspace; blocking script always sets actual value on workspace pages */
1414
--sidebar-collapsed-width: 51px; /* icon rail on web; desktop overrides to 0 before first paint */
15+
--sidebar-expanded-width: 248px; /* SIDEBAR_WIDTH.DEFAULT; the width to restore to, held even while collapsed */
1516
--desktop-title-bar-height: 0px; /* macOS traffic-light lane; desktop overrides before first paint */
1617
--desktop-title-bar-inset-x: 0px; /* clearance past the traffic lights; desktop overrides */
1718
--desktop-title-bar-control-offset: 0px; /* centres a lane control; desktop overrides */
@@ -154,6 +155,24 @@ html[data-sim-desktop-title-bar="inset"]
154155
--sidebar-width: var(--sidebar-collapsed-width);
155156
}
156157

158+
/**
159+
* Hover-peek: the shell floats out of flow as a card, so its subtree reads the restore
160+
* width instead of the collapsed one. Re-declaring the same variable the rule above
161+
* sets carries the inner shell and the aside along with no per-element overrides.
162+
*
163+
* Lives here rather than on the component because a Tailwind arbitrary property is one
164+
* class (0,1,0) and would lose to that rule's (0,2,0) selector.
165+
*/
166+
.sidebar-shell-outer[data-collapsed][data-peek] {
167+
--sidebar-width: var(--sidebar-expanded-width);
168+
}
169+
170+
/* The card appears at full width, so the aside's own width transition would animate
171+
0 -> expanded inside it. */
172+
.sidebar-shell-outer[data-peek] .sidebar-container {
173+
transition: none;
174+
}
175+
157176
.sidebar-container span,
158177
.sidebar-container .text-small {
159178
transition: opacity 120ms ease;

apps/sim/app/layout.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -112,22 +112,26 @@ export default function RootLayout({ children }: { children: React.ReactNode })
112112
document.cookie = 'sidebar_collapsed=' + (collapsed ? '1' : '0') + '; path=/; max-age=31536000; samesite=lax';
113113
}
114114
115-
if (collapsed) {
116-
document.documentElement.style.setProperty(
117-
'--sidebar-width',
118-
collapsedSidebarWidth + 'px'
119-
);
120-
} else {
121-
var width = state && state.sidebarWidth;
122-
var maxSidebarWidth = Math.max(248, window.innerWidth * 0.3);
123-
var finalWidth =
124-
typeof width === 'number' && isFinite(width)
125-
? Math.min(Math.max(width, 248), maxSidebarWidth)
126-
: defaultSidebarWidth;
127-
document.documentElement.style.setProperty('--sidebar-width', finalWidth + 'px');
128-
}
115+
// The expanded width is published unconditionally, even while
116+
// collapsed, because the desktop hover-peek renders the sidebar at
117+
// its restore width while --sidebar-width still reads collapsed.
118+
var width = state && state.sidebarWidth;
119+
var maxSidebarWidth = Math.max(248, window.innerWidth * 0.3);
120+
var expandedWidth =
121+
typeof width === 'number' && isFinite(width)
122+
? Math.min(Math.max(width, 248), maxSidebarWidth)
123+
: defaultSidebarWidth;
124+
document.documentElement.style.setProperty(
125+
'--sidebar-expanded-width',
126+
expandedWidth + 'px'
127+
);
128+
document.documentElement.style.setProperty(
129+
'--sidebar-width',
130+
(collapsed ? collapsedSidebarWidth : expandedWidth) + 'px'
131+
);
129132
} catch (e) {
130133
document.documentElement.style.setProperty('--sidebar-width', defaultSidebarWidth + 'px');
134+
document.documentElement.style.setProperty('--sidebar-expanded-width', defaultSidebarWidth + 'px');
131135
}
132136
133137
// Panel width and active tab

0 commit comments

Comments
 (0)