Skip to content

🎯 fix: Center Collapsed Sidebar Icons and Unclip Sidebar Focus Rings - #193

Merged
danny-avila merged 3 commits into
mainfrom
pr/redo-sidebar-rail-focus
Oct 28, 2026
Merged

danny-avila merged 3 commits into
mainfrom
pr/redo-sidebar-rail-focus

Conversation

@danny-avila

@danny-avila danny-avila commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request Template

⚠️ Before Submitting a PR, Please Review:

  • Please ensure that you have thoroughly read and understood the Contributing Docs before submitting your Pull Request.

⚠️ Documentation Updates Notice:

  • Kindly note that documentation updates are managed in this repository: librechat.ai

Summary

This PR supersedes #122 by @dustinhealy and redoes it on current main. The approach is from #122: collapsed rows with gap-0 and justify-center, zero-width labels, a fixed-width rail row, an unclipped avatar row, and an inset ring on the toggle. #122 doesn't apply as it stands: it conflicts with main in Sidebar.tsx and src/test/setup.ts, and main already has the click-ui 0.9.1 bump it carried.

The sidebar had three problems:

  • Off-center rail: in the collapsed rail (56px wide, inner center at x=27.5), the logo, nav icons and avatar sat at x=26. The rows lay out from the left in both states, so the icons stayed where the expanded panel puts them.
  • Clipped avatar ring: the avatar's focus ring (2px at a 2px offset) was cut by its row wrapper's overflow-hidden: 2px on the left and 3.75px at the top and bottom, collapsed and expanded.
  • Clipped toggle ring: the collapse toggle sits flush with the panel's clipped left and right edges and the bottom of the viewport. Of its ring, only the top edge showed.

Changes

  • Rail rows: when collapsed, the logo row, the nav links and the user row use gap-0 with justify-center. The labels beside the icons (app title, nav labels, user name and email) become w-0 flex-none overflow-hidden, so they take no space. The logo, nav icons and avatar center at x=27.5.
  • No sweep while animating: collapsed rows are the rail's inner width, w-[calc(--spacing(10)-1px)] (39px: w-14 minus the 1px border and px-2 on each side), from the first frame of the collapse. The icons sit at 27.5 while the panel width animates from 252px to 56px. With justify-center alone, the nav icons start at x=125.5 (the middle of the shrinking panel) and sweep in, and the logo and avatar stay at 26. The expand/collapse animation is unchanged.
  • Short viewports: the rows also get max-w-full. When the window is too short for the five nav rows, the collapsed nav scrolls and a classic scrollbar takes 6px of its width. The rows then narrow to the space left, so the nav doesn't scroll sideways and the icons stay centered in it.
  • Avatar row: the wrapper (flex items-center gap-2.5 overflow-hidden px-0.5) no longer clips, so the ring stays whole in both states. min-w-0 replaces overflow-hidden so a long name and email still truncate inside the panel. Without it, a long email runs past the panel's edge.
  • Toggle ring: the toggle gets a sidebar-toggle class. A scoped .sidebar-toggle:focus-visible { outline-offset: calc(-1 * var(--focus-ring-width)) !important; } rule, next to the other scoped focus overrides in styles.css, draws the ring inside the button.

Not carried over from #122

  • The click-ui 0.9.1 bump. main already has it.
  • renderWithLayoutProviders (src/test/layout-test-utils.tsx), the LayoutRenderOptions type, and the global matchMedia/scrollTo/ResizeObserver/scrollIntoView stubs in src/test/setup.ts. The new test renders the sidebar under its own memory router and stubs what it needs, as src/routes/-tests/app.test.tsx does.
  • 🎯 fix: Center Collapsed Sidebar Icons and Unclip Sidebar Focus Rings #122's tests for the user menu, the dropdown's aria-expanded and the capability-gated nav items, which are outside this fix.
  • The hard-coded -2px offset and the border-radius on the toggle ring. The offset follows --focus-ring-width, and the toggle stays a square row.
  • w-10 (40px) rows. They are 1px wider than the rail's inner width, so the nav icons land at x=28 while the logo and avatar rows shrink back to 39px and land at x=27.5.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

Unit: src/components/__tests__/Sidebar.test.tsx renders the real Sidebar under a memory router with an /_app route:

  • Collapsed: the logo, each nav icon and the avatar sit alone in a rail-width, centered row with a zero-width label.
  • Expanded: rows lay out from the left with gap-2.5 and their labels, and the email is visible.
  • Avatar ring (collapsed and expanded): nothing between the avatar and the panel clips its overflow, and the row can shrink (min-w-0).
  • Toggle ring (collapsed and expanded): the toggle carries sidebar-toggle, and styles.css insets its focus ring.

On main, the collapsed, both avatar ring and both toggle ring tests fail. The expanded test passes on both.

E2E: e2e/sidebar.spec.ts measures the geometry jsdom can't:

  • Centering: after a click on Collapse sidebar, the logo, nav icons and avatar are at the rail's inner center on the first animation frame and when the transition ends.
  • Focus rings: reached with Tab, the avatar's and the toggle's rings reach past no clipping ancestor and not past the viewport, collapsed and expanded.
  • Truncation: a long name and email truncate inside the expanded panel.
  • Scrolling nav: at 1280×300 the collapsed nav scrolls vertically, has no sideways overflow, and its icons sit at its center. The spec runs with real scrollbars (Playwright's headless Chromium hides them by default with --hide-scrollbars), and this test fails without max-w-full.

On main, the centering test and all four ring tests fail. The truncation test passes on both and fails if min-w-0 is dropped (the email then ends at x=381, past the panel's inner edge at x=251). The centering test also fails with justify-center alone, without the fixed-width rows.

Since #185, the /_app loader fails closed when the e2e mock backend doesn't serve /api/admin/grants/effective. Against the mock, this spec therefore needs #190. It passes against a live backend with E2E_BASE_URL, with no other changes.

Live check: against a LibreChat backend and the production build, in Chromium at 1280×800, light and dark, with keyboard Tab focus. Positions are x offsets from the panel's left edge.

main This PR
Collapsed: logo, 5 nav icons, avatar 26 each (rail center 27.5) 27.5 each
Expanded: logo, nav icons, avatar 26 each 26 each
Every frame of the collapse (252px → 56px) 26 27.5
Avatar ring, collapsed and expanded cut 2px left and 3.75px top and bottom by the row wrapper whole
Toggle ring, collapsed and expanded cut 4px left and right by the panel and 4px at the bottom by the viewport; only the top edge shows whole, drawn inside the button
Nav link rings whole whole
Long name and email, expanded truncated inside the panel truncated inside the panel

The collapsed rail is the default, so it also appears in the visual baselines in e2e/a11y/config.spec.ts. The 1440px baselines pass with this PR. On the same stack, the three 390px baselines fail on main too (their page content differs from the stack's config). The 1.5px icon shift adds about 430 differing pixels per image, about 0.1% of the image, well inside the 2% tolerance.

Test Configuration:

  • LibreChat dev backend, MongoDB 8.2 replica set.
  • The panel's production build (bun run build + bun server.ts), Chromium via Playwright.
  • npx playwright test e2e/sidebar.spec.ts against the mock backend with 🧪 fix: Serve the Admin's Effective Capabilities from the E2E Mock Backend #190's mock route, and against the live stack with E2E_BASE_URL.
  • npx tsc --noEmit, npx eslint src/ e2e/ --max-warnings 0, npx prettier --check on the changed files, bun run test:lint-rules (19 pass), npx vitest run (155 files, 3344 tests), bun run build.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

The collapsed rail drew the logo, nav icons and avatar at x=26, 1.5px left of
the rail's inner center (27.5): the rows lay out from the left in both states,
so the icons stayed where the expanded panel puts them. Collapsed rows now use
gap-0 with justify-center, and the labels beside the icons (app title, nav
labels, user name and email) become w-0 flex-none overflow-hidden so they take
no space. The rows are also the rail's inner width (39px) from the first frame
of the collapse, so the icons sit at 27.5 while the panel width animates
instead of sweeping in from the middle of the shrinking panel.

The avatar's focus ring (2px at a 2px offset) was cut by its row wrapper's
overflow-hidden. The wrapper no longer clips; min-w-0 keeps a long name and
email truncating inside the panel.

The collapse toggle is flush with the panel's clipped edges and the bottom of
the viewport, so only the top of its ring showed. A scoped
.sidebar-toggle:focus-visible rule draws the ring inset.

Supersedes #122 by @dustinhealy, whose approach this follows.
@danny-avila

Copy link
Copy Markdown
Contributor Author

@codex review

Pass 1. Head: ba025ac

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T07:45:46.480436Z 0fda88b Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba025ac18d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread e2e/sidebar.spec.ts Resolved
Comment thread src/components/Sidebar.tsx Resolved
@danny-avila

Copy link
Copy Markdown
Contributor Author

@codex review

Pass 2. Head: 0fda88b

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: 0fda88b2c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Conflicts:
#	src/components/Sidebar.tsx
@danny-avila
danny-avila merged commit 87240e3 into main Oct 28, 2026
3 checks passed
danny-avila added a commit that referenced this pull request Oct 29, 2026
…ides Out of the Visual Baselines and Refresh the Two Stale 390 px Ones (#210)

e2e/specs.spec.ts reverted the Base spec list only as the last statement of
its two saving journeys. When an assertion after Save failed, "E2E Spec" or
"E2E Last" stayed in Base, the next run's new spec was named e2e-spec-2, and
the serial describe skipped every journey after it. revertList now runs in
a beforeAll, for a list an earlier run left behind, and after every journey,
passed or failed. It opens a page in a context of its own, so whatever a
journey left on its page (a review dialog, unsaved edits) cannot get in the
way, returns early when Base stores no list, and checks that the revert
took.

e2e/import.spec.ts left the Starting credits it imports into Base behind
after every live run. They are now reverted after the journey, passed or
failed.

Every Base override shows in the configuration visual baselines: its tab's
override count, the header's revert action at 1440 px, and its chip and
section count on its own tab. Each baseline now fails before its screenshot
when Base stores an override, naming its tabs. A check alone only covers
the moment the page loads: six spec files save to Base in a parallel live
run, and a save between a baseline's check and its screenshot reaches the
image once the page fetches Base again, as React Query does on window
focus when the data is 30 s old. So the cross-worker mkdir lock behind
lockUserProfile now also backs a Base lock. Each baseline holds it from
loading its tab until its screenshot is taken. Every describe whose
journeys save to Base (import, model specs, features, providers, custom
endpoint saves, MCP servers) holds it from before the first save until the
save is reverted, and releases it after a failure. The import and spec list
reverts run as the lock's cleanup. Model specs takes the Base lock before
the USER role profile lock, so every worker takes the two in the same
order, and the baselines take both, since the System tab also draws a
USER profile's override. A leftover therefore never reaches a comparison
or an --update-snapshots run.

The 390 px system and features baselines failed on main on a clean
database. #170 wrote them before its own last changes: at phone width a
row's source chip moved under its label, the startup note gained a title
and new wording, and "OAuth state TTL (ms)" became "OAuth state TTL". #193
then moved the collapsed rail's icons 1.5 px to its center. Both baselines
are regenerated from main on a clean database. No threshold changed.

The Base import's cleanup finds its revert by the names revert actions
have had since they start with their visible words ("Revert to
librechat.yaml (…): Starting credits"); it looked for the old name,
found nothing and left the override in Base.
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.

1 participant