mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-04 05:28:30 +00:00
* 📱 feat: Give the Mobile Nav the Whole Screen The drawer was `min(85vw, 380px)` with the 52px icon rail inside it, so the conversation list got well under half the screen while ten unlabelled glyphs held a permanent column. The drawer now takes the viewport. Neither side needs a width literal any more: the panel is `fixed`, so `w-full` is the initial containing block, and the chat pane's `translateX(100%)` is self-referential and survives rotation. The shared transition moves to a constant — the two elements must stay frame-locked or the seam shows mid-animation. Drop the rail on mobile by not rendering `ExpandedPanel` rather than branching inside it, so desktop keeps an untouched file. Its four jobs move to a drawer header (panel switcher, account, close) and a bottom bar. The switcher doubles as the drawer title, answering "where am I" and "take me elsewhere" with one control, and lists panels as labelled rows. Search and new chat were both in the top corner — the two most frequent actions in the hardest place to reach one-handed. They move to a bottom bar built as a flex footer, not an overlay, so the virtualized list shrinks around it and can never be occluded. The backdrop is gone: at full width it can never be tapped, and `Root` already marks the covered pane `inert`. That makes the header's close button the primary dismissal, so it keeps `CLOSE_SIDEBAR_ID`, which `OpenSidebar` focuses after opening. Reset the drawer closed once per mobile mount. `sidebarExpanded` persists, and at full width a stale open state would launch into the nav rather than the conversation. Conversation rows revealed their overflow menu on hover, which touch does not have, leaving it reachable only on the active row. Touch now gets a cheap always-visible trigger that mounts the real menu already open, rather than mounting six mutations per overscanned row. * 🩹 fix: Address Codex Findings on the Full-Width Mobile Nav The panel switcher was unreachable. `DropdownPopup` portals to `document.body`, where `usePopoverZIndex()` hands it 50 outside a dialog — behind the opaque full-screen drawer at 110 — so none of its destinations could be selected. Render it inside the drawer instead; nothing between the trigger and the drawer root clips overflow. The drawer's z-index moves to a named constant carrying that reasoning. Panel keyboard shortcuts stopped working on mobile. They locate a panel by its rail button, read `aria-pressed`, then click it, and the rail no longer exists — so Agents, Prompts, Memories and the rest silently no-opped on a narrow window or a tablet with a keyboard. Hidden persistent targets keep that contract without reviving the rail. Routing the shortcuts through `useActivePanel` instead would mean hoisting `ActivePanelProvider` above `SidebarChatProvider`, which exists to keep panel changes from re-running `useChatHelpers`. Only available links render, so a shortcut for a panel this endpoint lacks still correctly does nothing. The persisted-drawer reset ran after the first paint, so a reload with the drawer open showed the nav covering the app and then animated it shut — the exact state it was meant to prevent. `atomWithLocalStorage` already accepts a normalizer, so the value is corrected during atom initialization and the closed state reaches the first paint. Drops the effect entirely. Note the normalizer also rewrites the stored value, so opening the drawer on a phone leaves that browser's desktop sidebar collapsed until toggled. * 🩹 fix: Address the Second Codex Round on the Full-Width Nav The conversation row's overflow menu was unreachable on mobile for the same reason the panel switcher was: `ConvoOptions` portals to `document.body`, where `usePopoverZIndex()` gives it 50, behind the drawer at 110. It now portals only off mobile — on desktop the sidebar is in normal flow, so portaling still buys escape from the list's clipping. The touch trigger also lost its own first tap. Touch browsers focus a button mid-tap, and the row's `onFocus` sets `hasInteracted`, which swaps the trigger for `ConvoOptions` before the click can land. Moving to `pointerdown` runs the handler before the swap. Crossing into the mobile breakpoint left the drawer open. The persisted value is normalized when the atom initializes, which covers loading on a phone, but narrowing a window or rotating a tablet has no such moment and an expanded desktop sidebar became a drawer covering the app. Collapse on the transition specifically, so the initial mobile paint still comes from the normalizer rather than an effect. The new spec pins the tap contract: it fires only `pointerdown`, so a click-based handler fails it. * 🩹 fix: Address the Third Codex Round on the Full-Width Nav The touch options trigger handled only `pointerdown`, so assistive tech, voice control and keyboard activation — which dispatch `click` with no preceding pointer event — did not reach it, and the click bubbled to the row and navigated away instead. It now handles pointer, click and Enter or Space through one handler. The two paths cannot double-fire, since `pointerdown` removes the button before a click could follow. The breakpoint reset still animated. Correcting it in an effect meant the first render after crossing into mobile painted the drawer open with the conversation translated fully offscreen, then moved both back over 300ms. The closed state is now derived during the transition render itself, and the effect only commits it. That derivation has to be shared: `UnifiedSidebar` draws the drawer while `Root` translates the pane, and both read the atom independently, so either one deciding alone would disagree with the other for that frame. Both now read through `useSidebarState`. * 🩹 fix: Restore Portaling and Scope the Drawer's Close Identity Revert the mobile menus to `portal={true}`. The premise behind rendering them in place was wrong: the drawer's z-index only ranks it inside `Root`'s `relative z-0` stacking context, so it cannot occlude a popup portaled to `document.body` regardless of the values involved. `ConvoOptions` has always portaled from inside this drawer and has always worked. Rendering in place cost real breakage: the row sits under the nav's `overflow-hidden` and a virtualized list, and the drawer's transform makes it the containing block for fixed descendants, so menus near a list edge were clipped and their rename, archive and delete actions unreachable. Scope the drawer's close button to the open state. It stays mounted while closed so the drawer can slide, and a translated element still counts as visible, so anything probing for `close-sidebar-button` found a control sitting off-viewport — which is what stalled the mobile visual specs. The rail this replaced only published that id while expanded; match it, and keep the closed drawer out of the tab order. * 🩹 fix: Let an Ordinary Click Open the Conversation Menu The trigger committed on `pointerdown`, so beginning a vertical scroll on an ellipsis opened that conversation's menu before the browser could tell a tap from a swipe. That handler only existed to beat a race of our own making: `hasInteracted` is hover- and focus-driven, which is meaningful on a pointer device but not on touch, where focus lands mid-tap — swapping the trigger for `ConvoOptions` while the finger was still down. Key the swap to the menu's own state on touch and the race disappears, so a plain click suffices. The browser already withholds a click until a press resolves as a tap, and synthesises one for keyboard and assistive-technology activation, which the `pointerdown` path had to special-case separately. Also correct the drawer z-index comment, which described the opposite of the layering the code settled on and would have led the next caller back into the clipping bug, and restore `aria-keyshortcuts` on the new-chat button so its binding stays discoverable. * 🩹 fix: Let Escape Leave the Menu Before the Drawer Menus opened from the drawer portal out of it, so their Escape still reached the drawer's document listener and collapsed the whole thing rather than the level the user meant to leave. Those menus unmount when closed, so their presence in the document is the signal to stand down. Also restore the toggle binding on the close control. It is the only close affordance while the drawer is open — the header's `OpenSidebar` is inside the inert, translated chat pane — so assistive technology had no way to discover the shortcut from there. * 🩹 fix: Only Treat an Open Menu as Reason to Keep the Drawer The Escape guard matched any `[role="menu"]` in the document, but not every menu unmounts when closed — the account menu stays mounted and merely `hidden`. Once it had lazily loaded, a closed menu would have suppressed Escape for the drawer permanently. Match only menus that are actually open. * ✅ test: Pin the Ariakit Closed-Menu Contract The drawer's Escape guard stands down only for menus that are actually open, which depends on Ariakit keeping a closed menu mounted and marking it `hidden` rather than unmounting it — the account menu behaves this way and would otherwise suppress Escape for the drawer permanently. Exercised against the real library rather than a mock, so a change in that behaviour fails here and points at the guard. * 🩹 fix: Keep the Row's Menu Mounted Once It Has Been Opened Keying the swap to `isPopoverActive` meant dismissing the menu unmounted `ConvoOptions` immediately, destroying Ariakit's own button — its final-focus target — mid-close. The lightweight trigger that took its place is a different node and never received focus, so a keyboard or assistive-technology user was dropped to the document instead of returning to the control they opened. Once a row's menu has been opened, keep the real one. Rows the user never touched still mount nothing, which was the reason for the trigger. * 🩹 fix: Complete the Retained-Menu Path for Touch Rows Three gaps in the retained-menu approach, all reachable. `hasOpenedMenu` was only set by the touch trigger, but the active row already renders the real menu and never passes through it. A row opened while active and later demoted would swap its focused button for a new node and drop focus — the same defect the retention was added to prevent. Recorded on every opening instead. The retained button then stayed invisible: `ConvoOptions` reveals its trigger on hover or focus when the row is neither active nor open, and touch has neither, so an interacted row was left with an invisible hit target. Kept visible on small screens. The touch trigger also restated the shared control's sizing, rounding and text treatment by hand, losing the focus ring, transitions and disabled handling that come with it. Composed from `Button` with only the local sizing retained. * ♻️ refactor: Give the Row's Overflow Control One Owner Five review rounds in this file each fixed something the previous fix introduced — trigger swap, activation path, scroll-versus-tap, focus return, retention completeness. The cause was structural rather than any one mistake: two controls can represent a row's menu, `ConvoOptions` and the cheap placeholder that stands in for it, and the rules they must agree on were spread across four separate expressions and a button, so each repair taught one of them something the other never learned. `ConvoActions` now settles them together — which control renders, when the real one becomes permanent, how it stays visible without hover, and how activation is claimed — with the reasoning for each recorded where the decision is made, including why a plain click is the right event and what breaks if a press is claimed earlier. Behaviour is unchanged; this is the same set of rules in one place. `Convo` keeps the open state, which it needs to suppress row navigation, and now passes a single `onOpenChange` rather than driving the swap itself. * 🩹 fix: Reveal the Real Menu Trigger on Touch and Recheck the Drawer Default The conversation menu has two triggers — the shift-held variant and the Ariakit button used the rest of the time — and only the first was taught to stay visible without hover. The second restated the same class string by hand instead of sharing it, so the earlier fix silently missed the trigger that actually matters. It now composes the shared string, which is why the two could disagree at all. Separately, the sidebar default is captured when the store module is evaluated, and `atomWithLocalStorage` only ran its normalizer when a saved value existed. A first visit that loaded wide and narrowed before the app mounted — a login screen being resized — therefore kept `true` with nothing to correct it, and `useMediaQuery` now resolving on the first render means the breakpoint guard sees no transition either. Normalize the default at initialization as well; callers without a normalizer get the identity function, so nothing else changes. * ✅ test: Cover the Normalized Default in `atomWithLocalStorage` Normalizing the default reaches every atom built with the helper, so the cases worth pinning are the ones where a normalizer exists and could move an untouched default: no normalizer, one that accepts the default — the shape the speech-engine atoms have — one that rejects it, and a persisted value, which must still be normalized as before. * 🩹 fix: Carry the Search Text Across a Breakpoint Change Moving search into the drawer's bottom bar left it mounted in two places — the list on a pointer device, the bottom bar on touch — so crossing the breakpoint mid-search destroys one instance and builds another. The field seeded its text to an empty string and never read the stored query, so the results stayed filtered by a term the box no longer showed, with no clear affordance to undo it. Seeded from the query instead, along with the clear button's state. * 💄 style: Settle the Drawer's Panel Switcher and Bookmark Filter The switcher's chevron trailed the panel name instead of sitting on the edge, so the control read as text with an arrow stuck to it rather than a menu spanning the header. The label now takes the slack. Moving search to the bottom bar also left the bookmark filter alone on a row of its own above the list, with nothing to sit beside. It moves next to the Chats heading, matching the Projects heading that already keeps its actions there, and the row disappears on mobile rather than lingering with one icon in it. `ChatsHeader` gains a trailing slot for that, so section actions have a home instead of a floating row. * 💄 style: Match the Bookmark Filter to the Section Actions The bookmark control was built for the row it used to share with the search field — 36px, `rounded-lg`, a larger icon — so beside a section heading it read as a different kind of control to the Projects actions sitting one row above it. Both now draw from one recipe, at every width rather than only where the move exposed it, so the two headings cannot drift apart in size, radius or hover treatment. * 🩹 fix: Cancel the Search Debounce the Field Leaves Behind The debounced commit writes to shared search state, so a pending timer outlives the instance that scheduled it. Mounting the field in two places made that reachable: crossing the breakpoint mid-keystroke destroys the list's field and builds the bottom bar's, and the departing timer would then reinstate a query the replacement had already edited or cleared. Clearing the field had the same hole within a single instance. Cancelling needs a debounce that is stable for the field's lifetime. A memo rebuilt on dependency changes leaves the previous instance's timer running past the cancel meant to stop it, and cancelling on that rebuild discards live keystrokes instead — so the handlers are read through a ref and the debounce is built once. Renames the spec, since hydrating the arriving instance and silencing the departing one are two halves of the same remount. * 🩹 fix: Hand the Uncommitted Query to the Arriving Search Field Cancelling the departing field's debounce stopped it overwriting a query the replacement had edited, but it also stranded the simpler case: a user who crosses the breakpoint and then just stops typing. The commit that would have published their query died with the instance that scheduled it, so the arriving field showed text the list was not filtered by and `isTyping` was never cleared — the loading state has no other way out while `debouncedQuery` and `query` disagree. The arriving field now takes the handoff, scheduling the commit itself when it mounts with an uncommitted query. Reading that at first render keeps it to the moment of the swap, so a real edit still wins. * 💄 style: Give Section Actions a Home in the Button Recipe The two sidebar headings shared their icon-button appearance through a feature-local class string, which is the shallow wrapper the styling rules warn about: sizing, radius, hover and focus ring are reusable appearance decisions, so they belong to the shared primitive where future theme and accessibility work will reach them. `sectionAction` and an `iconSm` size carry that recipe now, and the call sites keep only their layout. The drawer's panel switcher gets the same treatment for a sharper reason than consistency: its hand-written class string had no focus-visible state at all, so keyboard focus on the drawer's primary navigation control was invisible. Composing the shared ghost recipe restores the ring and transition, leaving only the row-filling layout local. `buttonVariants` returns unmerged recipe output, so every call site wraps it in `cn` — a spec pins that, since forgetting it silently reinstates whichever base utility the variant meant to override. * 🩹 fix: Publish the Search Field's Pending Query When It Leaves Cancelling on unmount assumed a replacement field would always arrive to inherit the query, so the fix grew a second mechanism to hand it over. The bottom bar disproves the assumption: switching panels drops the search entirely, leaving `query` set, `debouncedQuery` stale and `isTyping` on with nothing left to clear it. Flushing replaces both mechanisms. It publishes the pending commit rather than discarding it, so a field that leaves without a successor still settles the state it changed. And because a flush is synchronous with the unmount, it lands before any edit the replacement makes — which is what the cancel was for, so nothing is given up. Also normalizes the default on the parse-error path in `atomWithLocalStorage`: unparseable storage falls back to the same module-time default as a missing key, and only the missing-key path was re-checking it against the current viewport. |
||
|---|---|---|
| .. | ||
| public | ||
| scripts | ||
| src | ||
| sw | ||
| test | ||
| babel.config.cjs | ||
| check_updates.sh | ||
| index.html | ||
| jest.config.cjs | ||
| jest.resolver.cjs | ||
| nginx.conf | ||
| package.json | ||
| postcss.config.cjs | ||
| tailwind.config.cjs | ||
| tsconfig.json | ||
| vite.config.ts | ||