* Studio: make the sidebar width draggable
The sidebar was locked at 17.5rem. Long chat titles truncated early with
no way to trade content width for sidebar width.
Adds a drag handle on the sidebar edge. Drag to resize between 264px and
480px (also capped at 40% of the window), click to collapse or expand,
arrow keys to nudge, Home to restore the default. The width persists in
localStorage next to the existing pin flag and syncs across tabs.
Dragging in stops at the minimum rather than collapsing, so an overshoot
while resizing cannot snap the sidebar shut.
The minimum is set by the header: the logo lockup and the search and
collapse buttons need ~258px. The wordmark now truncates instead of
letting the search icon ride over the logo when the UI font scale pushes
the lockup wider.
Resizing relayouts the whole shell, so the live width is painted straight
to the wrapper's custom property once per animation frame instead of on
every pointermove, and only committed to the store on release.
* Studio: address review on the draggable sidebar
Four fixes from the review:
Re-clamp on viewport change. The 40% window cap was only evaluated on
load and on an explicit set, so a stored 480px stayed 480px after the
window narrowed. The store now keeps the preference whole and derives an
effective width from it, recomputed on resize, so narrowing shrinks the
sidebar and widening restores the preference instead of discarding it.
Keep the DOM and the store in step when a drag does not commit. On
pointercancel, and on a collapsed-rail drag that never reached the
minimum, the live width was left painted on the wrapper without being
stored. Since the provider does not re-render, React never rewrote the
property and the next expand could render at the rail's 48px. Drag end
now hands the property back to the committed value; a commit re-renders
with the new width.
Feed the resized width to the custom titlebar. WindowTitlebar sits
outside the sidebar wrapper so it cannot inherit --sidebar-width, and it
was positioning its seam and drag region from a fixed 17.5rem. It reads
the same store now.
Mirror the handle for side="right". Placement, cursor, tooltip side and
the pointer delta all follow the configured side. Measuring the rail from
the sidebar container makes the start width side-agnostic too.
Adds unit tests for the clamp, including the viewport cap and the floor
winning when 40% falls below it.
* Studio: keyboard, aria and titlebar fixes for the sidebar edge
Three more from review, and the handle is extracted so the run settings
panel can reuse it.
Keyboard activation. The handle advertises collapse and expand in its
label, but that only ran from pointer-up, and a button's synthesized
click is swallowed by the tooltip trigger. Enter and Space now toggle,
and the outward arrow reopens a collapsed rail rather than returning
early, so a focused handle is not a dead end.
Announced maximum. aria-valuemax was the absolute 480 even when the 40%
viewport cap put the real limit lower, so a screen reader offered
adjustment that could not happen. The store now exposes the effective
maximum and recomputes it on resize.
Titlebar during a drag. The custom titlebar reads the committed store
value, so its seam sat still while the sidebar moved. The drag now
mirrors the live width onto the root for it to read, and clears it on
release.
The drag mechanics move to PanelResizeHandle and the store to a
createPanelWidthStore factory. Behaviour is unchanged; both exist so the
run settings panel gets the same edge without a second copy.
* Studio: keep the stored width when a drag is viewport-capped
Dragging outward while the 40% cap is active committed the capped value,
so a 480px preference became 320px on a narrow window and never came
back when the window widened again.
The drag now commits what the pointer asked for rather than what was
painted. setWidth still clamps to the absolute range, so a deliberate
inward drag is honoured as before; only the capped case stops writing a
smaller preference than the user chose.
* Studio: review fixes for the panel resize handle
Five more from review.
Stale width after a resize with nothing mounted. With no subscribers
there is no resize listener, so a resize on /login or /onboarding left
the cached width and cap stale, and returning to the app restored the
old width past the viewport cap. The store now recomputes when a
subscriber attaches.
Capped outward drags no longer lower the stored preference. The drag
starts from the effective width, so with 480 stored in a capped window a
small outward pull committed a smaller number and discarded the
preference for good. The commit and the outward arrow now leave it alone
when the panel is already pinned at the cap. A deliberate inward drag
still commits.
Keyboard focus was invisible. The app zeroes the native outline on
buttons, so a tabbed handle showed nothing at all. It now paints its line
on focus-visible and opens the hint.
Collapsed aria. The separator reported 264 as its current value while the
rail renders at 48 and may restore to something else entirely. The range
attributes are dropped when collapsed, leaving the label to describe it.
Localised copy. The tooltip is visible text and was hardcoded English in
all eleven locales. The strings are props now, supplied through the
translation layer, with keys added across every locale.
* Studio: support click activation and fix collapsed role
Two more from review.
Switch and voice control activate a control by dispatching a bare click
with no pointer or key events. Everything here hung off pointer-up or
keydown, so those users could not toggle the panel at all. There is now a
click path, guarded so the click the browser sends after a real pointer
release does not toggle a second time.
The guard is set when any sequence ends, not only on release: a cancelled
drag also ends without a toggle, and its click would otherwise collapse
the panel. The suite caught that on the first attempt.
A focusable separator is an adjustable widget and needs a current value.
Dropping the range attributes while collapsed left an invalid range
control, so it reports as a button when closed and a separator with a
value when open.
* Studio: only suppress the click after a real pointer sequence
endDrag doubles as the effect cleanup, so setting the guard there
unconditionally swallowed the first click from switch or voice control
when no drag had happened. It now only arms after a sequence that
actually started, whether it ended in a release or a cancel.
* Studio: do not arm the click guard on keyboard toggles
preventDefault cancels the native synthesized click, so nothing followed
to guard against and the flag stayed set. The next switch or voice
activation was then read as a duplicate and ignored.
* Make the resize handle click guard self-healing
A canceled drag emits no compatibility click, so the boolean guard stayed
armed and swallowed the next click from a switch or voice control. Record
when the pointer sequence ended instead and ignore only a click that lands
inside the browser's compatibility window.
* Clear the stored sidebar width on preference reset
Reset all local preferences dropped every other UI key but left
sidebar_width, so the reload restored the old width instead of the
default.
* Guard that persisted panel widths stay in the reset list
* Tighten the sidebar header actions and lower the width floor
The search and collapse buttons carried 8px of padding each side of a 16px
icon, so the pair read as one wide block. Narrow them to 28px and close the
gap to 1px, which brings the glyphs from 18px apart to 13px.
That frees room in the header lockup, so the drag floor drops from 264px to
260px. 260 is the narrowest width that leaves the wordmark unclipped in
Firefox, which renders it ~3px wider than Chromium and WebKit.