HERMES-GIELIE/static
nesquena-hermes 2dbee503c2 feat(ux): collapse sidebar by clicking the active rail icon (fuses #1884 + #1924)
Lets desktop users collapse the session-list sidebar to maximise the chat
area, without adding any visible UI affordance. Default appearance is
identical to master — only users who actively try to toggle (or know the
keyboard shortcut) ever see a difference.

## Behaviour (desktop only, ≥641px)

| State                              | Action                | Result                                  |
|------------------------------------|-----------------------|-----------------------------------------|
| Sidebar open, click active rail    | Toggle                | Sidebar collapses to width:0            |
| Sidebar open, click different rail | Normal switch         | **Sidebar stays open** (no surprise)    |
| Sidebar collapsed, click any rail  | Expand + switch       | Sidebar expands, then panel switches    |
| Anywhere, Cmd/Ctrl+B               | Toggle                | Same as same-active-rail click          |
| Mobile (<641px), any of the above  | No-op                 | Mobile overlay behaviour unchanged       |

Two discoverability paths, both opt-in. **No new visible buttons.** Users
who never click the active rail icon see zero UI change vs. master.

## Surface-minimal design

The behaviour is contained behind one extra arg on the rail/sidebar-nav
onclick: `switchPanel('chat',{fromRailClick:true})`. Without that flag the
function preserves master's behaviour exactly — every programmatic
`switchPanel(name)` callsite (commands, deeplinks, internal state changes)
is unaffected. The guard chain inside `switchPanel`:

  opts.fromRailClick && _isDesktopWidth() && (
      _isSidebarCollapsed() ? expandSidebar() :
      prevPanel === nextPanel ? (toggleSidebar(true); return false))

is the ONLY new code path that can cause a collapse. Cross-panel clicks
fall through to the existing switch logic untouched.

## Polish from both source PRs

- **Click-active gesture** as the primary toggle (#1884 @jasonjcwu — the
  genuine UX innovation; no extra button needed)
- **Cmd/Ctrl+B keyboard shortcut** (#1924 @spektro33; VS Code convention).
  Guarded against firing when typing in INPUT / TEXTAREA / contenteditable
  so the shortcut never steals from in-progress text editing.
- **Inline flash-prevention `<script>`** in `<head>` (#1924) sets
  `data-sidebar-collapsed='1'` on `<html>` BEFORE the stylesheet loads,
  so cold loads with a persisted-collapsed state paint correctly from
  frame 0 with no flicker. Cleared by JS once the class system takes over.
- **Smooth slide animation** via `.24s cubic-bezier(.22,1,.36,1)`
  (#1924, mirrors the existing workspace-panel collapse on the right)
- **`aria-expanded` mirrored** on the active rail button (#1884) so
  screen readers announce open/collapsed transitions.
- **`body.resizing` transition-suppression** (#1884) keeps the drag-resize
  cursor instant — no animation during a width-resize gesture.
- **bfcache `pageshow` re-sync** (#1884) — if another tab toggled the
  sidebar while this page was frozen, bring it in line on restore.

## Drops vs. #1924

- No persistent rail "toggle sidebar" button (Nathan: keep the UI stealth)
- No close-X button in chat panel head (same reason)
- No i18n keys for the dropped buttons

## What did NOT change

- 22 rail/sidebar-nav `onclick` handlers gained the `{fromRailClick:true}`
  arg — function-call shape, invisible to users
- 1 inline `<script>` in `<head>` (flash prevention) — invisible
- 5 lines of CSS — invisible unless someone collapses

That's the entire visible-UI delta. **23 ins / 22 del on `index.html`,
all string-replace.**

## Verification

- 5,151 pytest passing including a new 34-test structural suite covering
  every contract (CSS rules, JS functions, fromRailClick guard, legacy
  proxy forwarding, flash-prevention `<script>` ordering, mobile
  exclusion via :not(.mobile-open) selector, aria-expanded sync).

- Live browser walkthrough at 1280px verified:
  - Default boot state identical to master (sidebar open, width 300px)
  - Click active rail → collapse (width 1, opacity 0, translateX -14px,
    localStorage='1', aria-expanded=false). Panel unchanged.
  - Click active rail again → expand back to width 300, aria=true
  - Click DIFFERENT rail → normal switch, sidebar stays open (legacy-
    preserving case, verified explicitly)
  - Click rail while collapsed → expand + switch in one gesture
  - Cmd+B toggles correctly
  - Cmd+B inside `<textarea>` → suppressed (defaultPrevented=false)
  - Reload with collapsed state persisted → restores without flash
  - Mobile simulation (matchMedia returns false for min-width:641px):
    same-active-rail click is no-op, Cmd+B is no-op, sidebar stays at 300px

Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
Co-authored-by: spektro33 <spektro33@users.noreply.github.com>
Closes #1884
Closes #1924
2026-05-11 04:49:18 +00:00
..
vendor chore: vendor streaming-markdown@0.2.15, remove CDN dependency 2026-04-24 01:05:20 +00:00
apple-touch-icon.png feature: add manifest and icons to enable app install on android 2026-05-02 19:06:39 +02:00
boot.js feat(ux): collapse sidebar by clicking the active rail icon (fuses #1884 + #1924) 2026-05-11 04:49:18 +00:00
commands.js fix(i18n): localize /goal runtime status strings 2026-05-10 15:21:24 +08:00
favicon-32.png fix: add favicon (SVG + PNG + ICO), fix static MIME types (#613) 2026-04-16 20:11:02 -07:00
favicon-192.png feature: add manifest and icons to enable app install on android 2026-05-02 19:06:39 +02:00
favicon-512.png feature: add manifest and icons to enable app install on android 2026-05-02 19:06:39 +02:00
favicon-512.svg feature: add manifest and icons to enable app install on android 2026-05-02 19:06:39 +02:00
favicon.ico fix: add favicon (SVG + PNG + ICO), fix static MIME types (#613) 2026-04-16 20:11:02 -07:00
favicon.svg fix: add favicon (SVG + PNG + ICO), fix static MIME types (#613) 2026-04-16 20:11:02 -07:00
i18n.js Clarify one-shot cron schedules 2026-05-11 07:03:17 +08:00
icons.js fix(composer): distinct voice-mode icon, descriptive labels, opt-in pref (#1488) 2026-05-02 22:16:23 +00:00
index.html feat(ux): collapse sidebar by clicking the active rail icon (fuses #1884 + #1924) 2026-05-11 04:49:18 +00:00
login.js fix(login): retry connectivity probe every 3s, auto-reload when server recovers 2026-05-01 19:54:47 +00:00
manifest.json feat: add browser offline recovery and PWA cache hardening 2026-05-08 21:16:17 +00:00
messages.js Stage 333: PR #2009 — feat(context): live status tracking during streaming by @dobby-d-elf 2026-05-10 18:16:59 +00:00
onboarding.js docs: clarify Anthropic auth choices in onboarding 2026-05-06 06:26:43 +00:00
panels.js feat(ux): collapse sidebar by clicking the active rail icon (fuses #1884 + #1924) 2026-05-11 04:49:18 +00:00
sessions.js Stage 333: PR #2009 — feat(context): live status tracking during streaming by @dobby-d-elf 2026-05-10 18:16:59 +00:00
style.css feat(ux): collapse sidebar by clicking the active rail icon (fuses #1884 + #1924) 2026-05-11 04:49:18 +00:00
sw.js feat: add browser offline recovery and PWA cache hardening 2026-05-08 21:16:17 +00:00
terminal.js fix: anchor active sessions per browser tab 2026-05-01 19:52:05 +08:00
ui.js Merge remote-tracking branch 'origin/master' into stage-332 2026-05-10 18:07:50 +00:00
workspace.js fix: keep workspace open from preview breadcrumb 2026-05-07 06:25:17 +00:00