Skip to content

feat: drag to resize the input sidebar (#80)#180

Open
dkedar7 wants to merge 2 commits into
releasefrom
feat/draggable-sidebar
Open

feat: drag to resize the input sidebar (#80)#180
dkedar7 wants to merge 2 commits into
releasefrom
feat/draggable-sidebar

Conversation

@dkedar7

@dkedar7 dkedar7 commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Closes #80"Users should be able to drag along the width of the input component sidebar in the sidebar layout up to 50% of the width of the screen."

Adds a grab strip on the sidebar's trailing edge. Drag it to resize the input sidebar, capped at half the viewport as the issue asks.

How it works

Mantine's AppShell drives both the navbar box and the main pane's offset from two CSS variables it writes inline on the shell (--app-shell-navbar-width / --app-shell-navbar-offset), and it builds the collapse transform out of the width var. The drag writes those two vars, so:

  • the sidebar and the output pane move together (writing only the width would grow the sidebar over the content),
  • the collapse animation stays correct for free,
  • and there's no server round-trip while dragging — it tracks the pointer.

The persistence problem this had to solve

dmc recomputes those vars from the navbar prop on every shell re-render, and toggle_sidebar returns a fresh dict on each collapse/expand — which would snap a resized sidebar back to the default. So on release the width is persisted into a dcc.Store that toggle_sidebar now reads back. This is the same "width lives in two places" constraint as v0.5.5 / #143. A missing or nonsensical stored value falls back to the default.

Details

  • Keyboard operable: the handle is focusable with role="separator"; arrow keys nudge it (shift = coarser step), so resizing isn't pointer-only.
  • Hidden below the breakpoint, where the navbar becomes a full-width overlay with no edge to drag.
  • Min width 200px so controls don't clip; max 50vw. Re-clamps if the viewport shrinks.
  • Handle is invisible until hover/focus, so it adds no permanent line to the chrome.

A bug this surfaced

generate_input_component() returns a list, and Dash does not flatten a nested list in children — so naively appending the handle as [navbar_content, handle] silently dropped the chat sidecar's components. Caught by test_chat (4 failures) and fixed by flattening the navbar children in both layouts.

Tests

New tests/test_sidebar_resize.py (8): handle + store present in both the sidebar and chat layouts, the default width when never dragged, a dragged width preserved, survival across a collapse/expand cycle, bogus-value fallback, keyboard reachability, and the 50%-cap contract in the drag script. Driven through the real toggle_sidebar callback over Dash's update wire, not by calling the function directly.

426 passed across the non-browser suites; Selenium runs in CI.

What I could not verify here

The drag interaction itself is clientside and needs a real browser — Playwright isn't available in this environment, so I verified the wiring, the persistence over Dash's update wire, and the CSS/JS contracts, but not the visual feel of dragging. Worth a quick manual check before release.

🤖 Generated with Claude Code

dkedar7 and others added 2 commits July 25, 2026 11:14
Adds a grab strip on the sidebar's trailing edge. Dragging it resizes the input
sidebar, capped at half the screen width as the issue asks.

How it works: Mantine's AppShell drives both the navbar box and the main pane's
offset from two CSS variables it writes inline on the shell
(--app-shell-navbar-width / --app-shell-navbar-offset), and builds the collapse
transform out of the width var. The drag writes those two vars, so the sidebar
and the output pane move together, the collapse animation stays correct, and
there is no server round-trip while dragging.

dmc recomputes those vars from the navbar prop whenever the shell re-renders,
and toggle_sidebar returns a fresh dict on every collapse/expand -- which would
snap a resized sidebar back to the default. On release the width is persisted
into a dcc.Store that toggle_sidebar now reads, so a dragged width survives a
collapse/expand cycle (same "width lives in two places" constraint as v0.5.5 /
 #143). A missing or nonsensical stored width falls back to the default.

The handle is keyboard operable (focusable, role=separator, arrow keys nudge,
shift for a coarser step) and hidden below the AppShell breakpoint, where the
navbar is a full-width overlay with no edge to drag.

Also flattens the navbar children in both layouts: generate_input_component
returns a list, and Dash does not flatten a nested one -- appending the handle
naively dropped the chat sidecar's components (caught by test_chat).

Closes #80

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h var

Real-browser verification (Playwright) caught the keyboard path being broken:
the first ArrowRight jumped the sidebar 300 -> 210 instead of stepping to 310.

The handler read the current width out of --app-shell-navbar-width, but dmc
supplies that var from a stylesheet as a rem expression, `calc(18.75rem * 1)`,
not as inline pixels. parseInt returned NaN, so the nudge fell back to
MIN_WIDTH. Reading the computed value instead did not help either -- it is the
same rem expression.

Measure the rendered navbar box (getBoundingClientRect) instead, which is always
real pixels and true no matter how the width was set. The drag path was
unaffected because it derives the width from pointer position, which is why this
only showed up once a browser actually drove the keyboard.

Verified in Chromium: ArrowRight +10, Shift+ArrowRight +50, ArrowLeft -10, and
the drag/cap/collapse-survival behaviours all still hold.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dkedar7

dkedar7 commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Verified in a real browser now (Playwright + Chromium), which changed the outcome — it caught a bug the headless tests could not.

The bug it caught

The keyboard path was broken: the first ArrowRight jumped the sidebar 300 → 210 instead of stepping to 310.

The handler read the current width out of --app-shell-navbar-width, but dmc supplies that var from a stylesheet as a rem expressioncalc(18.75rem * 1) — not inline pixels, so parseInt returned NaN and the nudge fell back to MIN_WIDTH. Reading the computed value didn't help either; it's the same rem expression. Fixed by measuring the rendered navbar box (getBoundingClientRect), which is always real pixels regardless of how the width was set.

The drag path was unaffected because it derives width from pointer position — which is exactly why only a browser driving the keyboard surfaced it.

Verified behaviour (viewport 1400px)

1. initial            : sidebar=300px  main starts at x=300
2. drag right to 600  : sidebar=600px  main starts at x=600
   sidebar grew                        : True
   main pane moved WITH it (no overlap): True
3. drag left to 260   : sidebar=260px  main starts at x=260
4. drag far right     : sidebar=700px  (cap is 50% = 700px)
   capped at half the screen           : True
5. collapse/expand    : 520px -> 520px
   dragged width survived              : True

ArrowRight  : 300 -> 310  (+10)  ✓
Shift+Right : 310 -> 360  (+50)  ✓
ArrowLeft   : 360 -> 350  (-10)  ✓
hairline invisible at rest, appears on hover  ✓

So the earlier "could not verify the drag feel" caveat is now closed: the drag, the 50% cap, the main-pane tracking, collapse/expand survival, keyboard stepping, and the hover affordance are all confirmed against a live app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make the input component sidebar draggable

1 participant