Skip to content

Add collapsible sidebar and redesign app layout#1748

Draft
Flo0807 wants to merge 31 commits intodevelopfrom
feature/collapsible-sidebar
Draft

Add collapsible sidebar and redesign app layout#1748
Flo0807 wants to merge 31 commits intodevelopfrom
feature/collapsible-sidebar

Conversation

@Flo0807
Copy link
Copy Markdown
Collaborator

@Flo0807 Flo0807 commented Jan 8, 2026

collapsible_sidebar.mov

@Flo0807 Flo0807 self-assigned this Jan 8, 2026
@Flo0807 Flo0807 marked this pull request as draft January 8, 2026 11:14
@Flo0807
Copy link
Copy Markdown
Collaborator Author

Flo0807 commented Jan 8, 2026

As this MR handles persistent state via local storage, it will result in showing the sidebar for a short amount of time on page load.

I suggest that we work on a more generic "UI state" solution in a dedicated PR.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds a collapsible sidebar and redesigns the application layout. The changes replace the previous drawer-based sidebar with a new implementation that supports both mobile overlay and desktop push-aside modes, with state persistence for the desktop view.

Changes:

  • Replaced BackpexSidebarSections hook with a new BackpexSidebar hook that manages both sidebar visibility and section expansion
  • Redesigned app shell layout to use a fixed sidebar with overlay on mobile and content-shifting on desktop
  • Moved branding component from topbar to sidebar (breaking change from topbar_branding to sidebar_branding)

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
assets/js/hooks/_sidebar.js New hook implementation combining sidebar visibility management with section expansion logic
assets/js/hooks/_sidebar_sections.js Removed old sections-only hook
assets/js/hooks/index.js Updated export to reference new BackpexSidebar hook
priv/static/js/backpex.esm.js Compiled ESM bundle with new sidebar implementation
priv/static/js/backpex.cjs.js Compiled CJS bundle with new sidebar implementation
lib/backpex/html/layout.ex Redesigned app shell layout with fixed sidebar and renamed branding component
demo/lib/demo_web/components/layouts/admin.html.heex Updated demo to use new sidebar structure with sidebar_branding
demo/assets/css/app.css Added CSS custom property for sidebar width
priv/gettext/backpex.pot Updated translation strings for simplified navigation labels

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread priv/static/js/backpex.cjs.js
Comment thread priv/static/js/backpex.cjs.js
Comment thread lib/backpex/html/layout.ex Outdated
Comment thread assets/js/hooks/_sidebar.js Outdated
Comment thread assets/js/hooks/_sidebar.js
Comment thread priv/static/js/backpex.esm.js Outdated
Comment thread lib/backpex/html/layout.ex
Comment thread priv/static/js/backpex.cjs.js Outdated
Comment thread priv/static/js/backpex.cjs.js
Comment thread priv/static/js/backpex.cjs.js Outdated
Flo0807 added 13 commits April 17, 2026 09:15
The priv generator template and installation guide still referenced the
removed topbar_branding component and the old sidebar slot shape. Move
the branding into the sidebar slot as sidebar_branding and wrap nav
content in <nav><ul> so copy-pasted layouts compile and render correctly
under the new app_shell API.
Document the breaking sidebar API changes (topbar_branding rename and
slot move, required <nav>/<ul> wrapper, --sidebar-width CSS variable,
and BackpexSidebarSections hook rename) so existing users can migrate
on the next release.
Use Tailwind's arbitrary-value syntax with a var() fallback so the
sidebar renders at a sane width even when the consumer hasn't defined
--sidebar-width. Consumers can still override by setting the variable
in their own stylesheet.
Replace the non-interactive <span> with a <button>, add aria-expanded
and aria-controls pointing at the content <ul>, and keep the ARIA
state in sync in the hook on both initial mount and click. Users can
now focus the toggle with Tab and expand/collapse it with Enter or
Space, fixing WCAG 2.1.1 and 4.1.2 violations.
The mobile sidebar is a modal overlay but was leaking Tab focus into
the hidden content underneath and never returned focus to the toggle
on close. Store the previously focused element when opening, move
focus into the sidebar, trap Tab within it while open, and restore
focus on close. Apply role="dialog"/aria-modal dynamically so the
desktop collapsible panel is not mislabeled as a modal.
When the sidebar is translated off-canvas, its links and buttons stay
in the DOM and keyboard focus can land on invisible elements. Toggle
the inert attribute alongside the transform so the off-canvas subtree
is removed from the tab order and the accessibility tree.
Server-side renders the sidebar hidden on mobile and visible on
desktop via -translate-x-full md:translate-x-0 so the CSS output
matches the default state before JS loads. The hook now writes inline
transform and margin styles (which win over the Tailwind responsive
classes) and clears a data-suppress-transition attribute on the first
animation frame so the snap to a stored non-default preference is
instant rather than animated on page load.

A remaining animation can occur on desktop hard-reload when the user
previously collapsed the sidebar, because the server does not yet
know the stored preference. A future cookie-backed UI state system
will close that gap.
Store bound handlers as instance references during mount so the hook
can remove its document keydown, matchMedia change, toggle click,
overlay click, and per-section click listeners when the element is
destroyed. Matches the cleanup pattern used by every other hook in
this directory and prevents handlers from leaking across LiveView
navigations.
Attach the BackpexSidebar hook conditionally on the presence of the
sidebar slot, and early-return from mounted()/updated() when the
sidebar DOM is missing. This prevents a TypeError on layouts that
use app_shell without a sidebar.
Change the outer sidebar element from <aside aria-label="Main navigation">
to <nav> and drop the nested <nav> inside the slot. This avoids a double
landmark and keeps screen reader navigation unambiguous.
Tailwind v4's -translate-x-full and md:translate-x-0 utilities emit the
CSS `translate` property rather than `transform`. Setting the element's
inline `transform` therefore did not override the SSR state, leaving the
mobile drawer off-canvas. Writing to `style.translate` puts the hook on
the same property it needs to win against.
Apply sidebar, overlay, and main-margin transition utilities only when
the user has not requested reduced motion. Users who set
prefers-reduced-motion: reduce no longer see the 300 ms slide and fade
animations, satisfying WCAG 2.3.3.
Flo0807 added 3 commits April 17, 2026 10:30
Switch the mobile/desktop boundary from 768 px to 1024 px so iPad
portrait and similar narrow-tablet viewports render the sidebar as a
drawer instead of eating roughly half of the content area. Updates the
Tailwind responsive classes in app_shell and the matching JS breakpoint
in the sidebar hook.
Switch the mobile drawer backdrop from the hard-coded bg-black/50 to
bg-neutral/50 so the scrim adapts to the active daisyUI theme instead
of always rendering as cold black.
Switch the main container from transition-[margin] to
transition-[margin-left] so only the single property that actually
changes is animated. Same visual result, smaller animation scope and
fewer styles forced onto the transition list.
@Flo0807 Flo0807 force-pushed the feature/collapsible-sidebar branch from b764bb6 to 5ecfabe Compare April 17, 2026 08:45
Flo0807 added 2 commits April 17, 2026 10:47
Drop the "section" default id. The id is used as the localStorage key
for the collapsed state and as the basis for aria-controls, so two
sections sharing the default would toggle together and announce
conflicting relationships to assistive tech. Making the attribute
required forces every section on a page to have its own identity, and
the v0.19 upgrade guide documents the breaking change.
standard 17.1.2 does not recognize `requestAnimationFrame` as a browser
global, matching prior gaps for `localStorage` and `IntersectionObserver`.
Add it to the explicit globals list so CI lint passes.
@Flo0807 Flo0807 requested a review from Copilot April 17, 2026 09:06
@Flo0807 Flo0807 added the breaking-change A breaking change label Apr 17, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 16 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread assets/js/hooks/_sidebar.js Outdated
Comment thread priv/static/js/backpex.esm.js Outdated
Comment thread priv/static/js/backpex.cjs.js Outdated
Flo0807 added 2 commits April 17, 2026 11:13
Replaces the `toggle._handler` expando property with a WeakMap keyed by
the toggle element. This keeps handler references off DOM nodes and
removes the awkward first-mount `removeEventListener('click', undefined)`
call by guarding the removal on a previously stored handler.
Tailwind v4 exposes breakpoints as CSS custom properties. Reading
--breakpoint-lg in the sidebar hook keeps the `lg:` utilities in the
layout and the JS media query in sync when consumers customize it,
instead of hardcoding 1024px in two places.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change A breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants