feat(ui): collapsible sidebar + rail, off-canvas mobile drawers, responsive layout
- Left sidebar collapses (topbar menu button); rail collapses (topbar button + strip). - <=860px / portrait: sidebar + rail become off-canvas drawers over main with a scrim, closed by default; main goes full-width single-column. - Smooth transitions; crossing the breakpoint resets to a sane default. - New components/chrome.js owns toggle + drawer state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
76
public/components/chrome.js
Normal file
76
public/components/chrome.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// Collapsible chrome controller: left sidebar + right companion rail.
|
||||
// Desktop → panels shrink the grid (state persisted). Narrow/portrait → panels
|
||||
// become off-canvas drawers over the main area with a scrim; closed by default.
|
||||
//
|
||||
// State lives as classes on #shell: `sidebar-collapsed` / `rail-collapsed`.
|
||||
// rightrail.js also toggles `rail-collapsed` (its own strip) + the same key, so
|
||||
// a MutationObserver keeps the scrim in sync no matter who toggles.
|
||||
|
||||
const SB_KEY = 'void_sidebar_collapsed';
|
||||
const RAIL_KEY = 'void_rail_collapsed';
|
||||
|
||||
const shell = () => document.getElementById('shell');
|
||||
const isNarrow = () => window.matchMedia('(max-width: 860px)').matches;
|
||||
|
||||
function syncScrim() {
|
||||
const s = shell();
|
||||
if (!s) return;
|
||||
const anyOpen = isNarrow() &&
|
||||
(!s.classList.contains('sidebar-collapsed') || !s.classList.contains('rail-collapsed'));
|
||||
document.body.classList.toggle('drawer-open', anyOpen);
|
||||
}
|
||||
|
||||
export function toggleSidebar() {
|
||||
const s = shell();
|
||||
const opening = s.classList.contains('sidebar-collapsed');
|
||||
s.classList.toggle('sidebar-collapsed');
|
||||
if (opening && isNarrow()) s.classList.add('rail-collapsed'); // one drawer at a time
|
||||
if (!isNarrow()) localStorage.setItem(SB_KEY, String(!opening));
|
||||
}
|
||||
|
||||
export function toggleRail() {
|
||||
const s = shell();
|
||||
const opening = s.classList.contains('rail-collapsed');
|
||||
s.classList.toggle('rail-collapsed');
|
||||
if (opening && isNarrow()) s.classList.add('sidebar-collapsed');
|
||||
localStorage.setItem(RAIL_KEY, String(!opening));
|
||||
}
|
||||
|
||||
function closeDrawers() {
|
||||
shell().classList.add('sidebar-collapsed', 'rail-collapsed');
|
||||
}
|
||||
|
||||
export function initChrome() {
|
||||
const s = shell();
|
||||
if (!s) return;
|
||||
|
||||
if (!document.getElementById('scrim')) {
|
||||
const scrim = document.createElement('div');
|
||||
scrim.id = 'scrim';
|
||||
scrim.addEventListener('click', closeDrawers);
|
||||
document.body.appendChild(scrim);
|
||||
}
|
||||
|
||||
// Initial state.
|
||||
if (isNarrow()) {
|
||||
s.classList.add('sidebar-collapsed', 'rail-collapsed'); // drawers closed on phones/portrait
|
||||
} else if (localStorage.getItem(SB_KEY) === 'true') {
|
||||
s.classList.add('sidebar-collapsed'); // restore desktop preference (rail handled by rightrail.js)
|
||||
}
|
||||
|
||||
// Keep the scrim correct whenever shell classes change (covers rightrail's own toggle).
|
||||
new MutationObserver(syncScrim).observe(s, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
// Crossing the breakpoint: collapse to a sane default for the new mode.
|
||||
let wasNarrow = isNarrow();
|
||||
window.addEventListener('resize', () => {
|
||||
const narrow = isNarrow();
|
||||
if (narrow === wasNarrow) return;
|
||||
wasNarrow = narrow;
|
||||
if (narrow) closeDrawers();
|
||||
else s.classList.remove('sidebar-collapsed'); // show the sidebar again on desktop
|
||||
syncScrim();
|
||||
});
|
||||
|
||||
syncScrim();
|
||||
}
|
||||
Reference in New Issue
Block a user