From 733a28bcc07456cb3b5f21b5f2b536cbd06978c5 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Thu, 5 Nov 2026 11:13:01 +0000 Subject: [PATCH] feat(web): hide or show the sidebar The menu button is now shown on wide screens too and hides the sidebar so the page gets the full width; b does the same from the keyboard. The choice is kept with the other preferences. On a phone the button still opens the sidebar as a drawer. In the standalone app the keys work wherever the focus is, and / shows a hidden sidebar before focusing the search box. --- web/src/app.ts | 44 +++++++++++++++++++++++++++++++++------- web/src/ctx.ts | 2 ++ web/src/styles.css | 12 ++++++----- web/src/views/dialogs.ts | 3 ++- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/web/src/app.ts b/web/src/app.ts index d3afff5..45296fd 100644 --- a/web/src/app.ts +++ b/web/src/app.ts @@ -38,6 +38,7 @@ const DEFAULT_PREFS: Prefs = { direction: "DOWN", enums: true, incoming: true, + sidebar: true, }; interface Example { @@ -74,6 +75,21 @@ export class App implements AppCtx { private sourceCache = new Map>(); private _cmp: Compare | null = null; private onHash = () => this.readHash(); + private onKey = (ev: KeyboardEvent) => { + const target = ev.composedPath()[0] as HTMLElement | undefined; + if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.defaultPrevented) return; + if (target && (target.closest?.("input, select, textarea, [contenteditable]") || target.isContentEditable)) return; + // The Changes view has its own "/" (its filter box). + if (ev.key === "/" && this.side && this._route.view !== "diff" && this._route.view !== "difff") { + ev.preventDefault(); + if (this.isNarrow()) this.side.el.classList.add("open"); + else if (!this._prefs.sidebar) this.toggleSidebar(); + this.side.focusSearch(); + } else if (ev.key === "b" && this.side) { + ev.preventDefault(); + this.toggleSidebar(); + } + }; constructor(root: HTMLElement, opts: AppOptions) { this.root = root; @@ -87,17 +103,14 @@ export class App implements AppCtx { root.appendChild(this.shellEl); this.applyTheme(); if (opts.routing) window.addEventListener("hashchange", this.onHash); - root.addEventListener("keydown", (ev) => { - if (ev.key === "/" && !(ev.target instanceof HTMLInputElement) && !(ev.target instanceof HTMLTextAreaElement)) { - ev.preventDefault(); - this.side?.focusSearch(); - } - }); + // The standalone app owns the page, so its keys work wherever the focus is. + (opts.routing ? window : root).addEventListener("keydown", this.onKey as EventListener); this.wireDrop(); } destroy(): void { window.removeEventListener("hashchange", this.onHash); + window.removeEventListener("keydown", this.onKey); } // ---- AppCtx ------------------------------------------------------------- @@ -540,6 +553,23 @@ export class App implements AppCtx { this.side = sidebar(this); this.bodyEl.insertBefore(this.side.el, this.mainEl); this.bodyEl.classList.remove("no-sidebar"); + this.bodyEl.classList.toggle("sidebar-hidden", !this._prefs.sidebar); + } + + private isNarrow(): boolean { + return !!window.matchMedia?.("(max-width: 860px)").matches; + } + + /** Show or hide the sidebar: a drawer on a phone, a remembered choice on a wide screen. */ + private toggleSidebar(): void { + if (!this.side) return; + if (this.isNarrow()) { + this.side.el.classList.toggle("open"); + return; + } + this.setPrefs({ sidebar: !this._prefs.sidebar }); + this.bodyEl.classList.toggle("sidebar-hidden", !this._prefs.sidebar); + this.renderTopbar(); } // ---- rendering ---------------------------------------------------------- @@ -604,7 +634,7 @@ export class App implements AppCtx { const t = this.topbar; t.replaceChildren(); const m = this.model; - if (m) t.appendChild(button("", () => this.side?.el.classList.toggle("open"), { icon: "menu", cls: "ghost icon-only menu-btn", title: "Types, elements and search" })); + if (m && !this.opts.minimal) t.appendChild(button("", () => this.toggleSidebar(), { icon: "menu", cls: "ghost icon-only menu-btn", title: `${this._prefs.sidebar ? "Hide" : "Show"} the sidebar: types, elements and search (b)` })); t.appendChild(h("a", { class: "title", href: this.hrefRoute({ view: "home" }), onClick: (ev: Event) => { ev.preventDefault(); if (this.model) this.goRoute({ view: "home" }); diff --git a/web/src/ctx.ts b/web/src/ctx.ts index e2eb683..86e11cc 100644 --- a/web/src/ctx.ts +++ b/web/src/ctx.ts @@ -20,6 +20,8 @@ export interface Prefs { direction: "DOWN" | "RIGHT"; enums: boolean; incoming: boolean; + /** Show the sidebar (types, elements and search) on wide screens. */ + sidebar: boolean; } export interface AppCtx { diff --git a/web/src/styles.css b/web/src/styles.css index f748923..9b5caf0 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -234,13 +234,15 @@ code, .topbar > select { flex-shrink: 0; } -.btn.menu-btn { - display: none; +@media (min-width: 861px) { + .body.sidebar-hidden { + grid-template-columns: 1fr; + } + .body.sidebar-hidden > .sidebar { + display: none; + } } @media (max-width: 860px) { - .btn.menu-btn { - display: inline-flex; - } /* Room for the title on a phone: the share button keeps its icon, About moves to the footer. */ .topbar .share-btn span, .topbar .btn.about-btn { diff --git a/web/src/views/dialogs.ts b/web/src/views/dialogs.ts index cfdf8dc..b56d0d0 100644 --- a/web/src/views/dialogs.ts +++ b/web/src/views/dialogs.ts @@ -71,7 +71,8 @@ export function openHelp(host: HTMLElement): void { "Finding things", false, ["Type in the ", b("search"), " box (", k("/"), " jumps to it) to match names of types, elements, attributes and groups, and words of their documentation and enumeration values. Use ", k("↑"), " ", k("↓"), " and ", k("Enter"), " to open a result."], - ["The tabs under the search box list ", b("Types"), " (complex types), ", b("Simple"), " types, global ", b("Elements"), ", ", b("Groups"), " (model groups, attribute groups, global attributes) and ", b("Files"), ". The namespace list narrows them to one namespace."] + ["The tabs under the search box list ", b("Types"), " (complex types), ", b("Simple"), " types, global ", b("Elements"), ", ", b("Groups"), " (model groups, attribute groups, global attributes) and ", b("Files"), ". The namespace list narrows them to one namespace."], + ["The menu button at the top left (or ", k("b"), ") hides the sidebar to give the page the full width, and shows it again. The choice is remembered."] ), section( "Type pages",