From 2fcfc06c3ce232aebd70014663475fb9a3b87d0c Mon Sep 17 00:00:00 2001
From: Miguel Palhas maestro
-
-
-
- `;
- // Mobile-only ☰ (CSS hides it on desktop) — opens the off-canvas session drawer.
- (header.querySelector(".app-menu") as HTMLElement).addEventListener("click", () =>
- onToggleSidebar?.(),
- );
-
- // Session tally lives centered in the header (not the sidebar).
- const statusHost = header.querySelector(".app-status") as HTMLElement;
- const renderStatus = () => {
- const state = store.snapshot();
- statusHost.replaceChildren(renderStatusbar([...state.sessions.values()]));
- };
- store.subscribe(renderStatus);
- renderStatus();
-
- const actions = header.querySelector(".app-actions") as HTMLElement;
- // DEV-only shortcut to the standalone design kit (?kit). Lets you reach it by
- // clicking inside a proxied preview (e.g. crit live) that has no address bar.
- if (import.meta.env.DEV) {
- const kit = document.createElement("a");
- kit.className = "btn btn-ghost";
- kit.href = "?kit";
- kit.textContent = "kit";
- kit.title = "Open the design kit (DEV)";
- actions.appendChild(kit);
- }
- return header;
-}
-
/** Two-tier sidebar highlight state (issue #33): which sessions are open in some
* pane (quiet), and which one is focused (loud). */
export interface SidebarHighlights {
@@ -426,6 +369,16 @@ export function renderSidebar(store: CatalogStore, opts: SidebarOptions = {}): S
helpBtn.addEventListener("click", () => toggleHelp());
footer.appendChild(helpBtn);
footer.appendChild(createThemeToggle());
+ // DEV-only shortcut to the standalone design kit (?kit). Lets you reach it by
+ // clicking inside a proxied preview (e.g. crit live) that has no address bar.
+ if (import.meta.env.DEV) {
+ const kit = document.createElement("a");
+ kit.className = "btn btn-ghost";
+ kit.href = "?kit";
+ kit.textContent = "kit";
+ kit.title = "Open the design kit (DEV)";
+ footer.appendChild(kit);
+ }
const spawnBtn = document.createElement("button");
spawnBtn.type = "button";
spawnBtn.className = "btn btn-primary spawn-btn";
diff --git a/web/src/drawer.ts b/web/src/drawer.ts
index dc65976..ab8155c 100644
--- a/web/src/drawer.ts
+++ b/web/src/drawer.ts
@@ -8,6 +8,14 @@
import { OPEN_SPAWN_EVENT, SPAWN_SIMILAR_EVENT } from "./catalog.js";
+/** Toggling the drawer from outside the shell (the pane header's ☰ on mobile),
+ * without threading the controller through every view. */
+export const TOGGLE_DRAWER_EVENT = "maestro-toggle-drawer";
+
+export function requestToggleDrawer(): void {
+ document.dispatchEvent(new CustomEvent(TOGGLE_DRAWER_EVENT));
+}
+
export interface Drawer {
open(): void;
close(): void;
@@ -31,6 +39,7 @@ export function installDrawer(
sidebarEl.addEventListener("click", (e) => {
if ((e.target as HTMLElement).closest(".tree-leaf")) drawer.close();
});
+ document.addEventListener(TOGGLE_DRAWER_EVENT, () => drawer.toggle());
document.addEventListener(OPEN_SPAWN_EVENT, () => drawer.close());
document.addEventListener(SPAWN_SIMILAR_EVENT, () => drawer.close());
diff --git a/web/src/kit.ts b/web/src/kit.ts
index 1d4386f..352afe2 100644
--- a/web/src/kit.ts
+++ b/web/src/kit.ts
@@ -205,7 +205,6 @@ export function renderKit(): HTMLElement {
);
activeLine.setAttribute("data-activity", "active");
body.append(
- specimen("session-tally", h("span", "session-tally", "6 sessions · 5 live")),
specimen("folder-count", h("span", "folder-count", "4")),
specimen("card-line2 · active", activeLine),
specimen("attach-state · open", h("span", "attach-state", "live")),
diff --git a/web/src/main.ts b/web/src/main.ts
index 6ff41be..e9d40c7 100644
--- a/web/src/main.ts
+++ b/web/src/main.ts
@@ -2,9 +2,9 @@ import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/jetbrains-mono/500.css";
import "@fontsource/jetbrains-mono/600.css";
import "@fontsource/jetbrains-mono/700.css";
-import { renderSidebar, renderTopbar } from "./catalog.js";
+import { renderSidebar } from "./catalog.js";
import { ConnectionPool } from "./connections.js";
-import { installDrawer, installDrawerSwipe } from "./drawer.js";
+import { installDrawer, installDrawerSwipe, requestToggleDrawer } from "./drawer.js";
import { installHudKeybind } from "./debug.js";
import { installHelpKeybind, isHelpOpen, isTypingTarget } from "./help.js";
import { CatalogStore } from "./events.js";
@@ -93,16 +93,15 @@ const content = document.createElement("section");
content.className = "content";
// Mobile (issue #35): the sidebar is an off-canvas drawer toggled by the ☰ in
-// the header. `installDrawer` owns the close rules (session tap, Spawn, Spawn
+// the pane header (and the welcome state). `installDrawer` owns the close rules (session tap, Spawn, Spawn
// similar, backdrop); on desktop the class it toggles is inert.
const backdrop = document.createElement("div");
backdrop.className = "mv-backdrop";
const drawer = installDrawer(shellEl, sidebar.el, backdrop);
installDrawerSwipe(drawer);
-const toggleDrawer = () => drawer.toggle();
shellEl.append(sidebar.el, content, backdrop);
-root.replaceChildren(renderTopbar(store, toggleDrawer), shellEl);
+root.replaceChildren(shellEl);
// The pool owns attach connections independently of the route: navigating away
// parks a session (keeps it streaming); navigating back re-mounts it instantly.
@@ -182,13 +181,22 @@ function renderWelcome(): HTMLElement {
Press ? for keyboard shortcuts.
`; + const actions = el.querySelector(".empty-actions") as HTMLElement; + // Mobile-only: with no pane open there is no pane header to carry the ☰, so + // the drawer would be swipe-only. CSS hides this on desktop. + const sessions = document.createElement("button"); + sessions.type = "button"; + sessions.className = "btn btn-ghost welcome-sessions"; + sessions.textContent = "☰ Sessions"; + sessions.addEventListener("click", () => requestToggleDrawer()); + actions.appendChild(sessions); const spawn = document.createElement("button"); spawn.type = "button"; spawn.className = "btn btn-primary"; spawn.textContent = "+ Spawn session"; spawn.title = "Draft pane: n — fast-forward spawn: N"; spawn.addEventListener("click", () => requestOpenSpawn()); - (el.querySelector(".empty-actions") as HTMLElement).appendChild(spawn); + actions.appendChild(spawn); return el; } @@ -226,7 +234,7 @@ function showMultiView(): void { updateShellRoute(); } -// Spawn affordances (#62): the header button, the sidebar's empty state, and +// Spawn affordances (#62): the sidebar's footer button, its empty state, and // `?mock&spawn` all open a draft pane; "Spawn similar" seeds one from the // cloned session (see spawnSimilar.ts). function requestOpenSpawn(): void { diff --git a/web/src/multiview.ts b/web/src/multiview.ts index 1408a05..9f129c6 100644 --- a/web/src/multiview.ts +++ b/web/src/multiview.ts @@ -470,7 +470,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView { canvas.replaceChildren(); const tab = activeTab(layout); // Mobile: the tab bar is a second navigation surface we don't want, so CSS - // collapses it; the header ☰ keeps the sidebar drawer reachable. + // collapses it; the pane header's ☰ keeps the sidebar drawer reachable. el.classList.toggle("mv-mobile-solo", isMobile); if (!tab.root) { diff --git a/web/src/styles.css b/web/src/styles.css index d73701f..f2de946 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -365,73 +365,6 @@ textarea { /* ---------- app shell: sidebar + content ---------- */ -/* Global app header: brand + app-level actions (spawn, theme toggle), spanning - the full width above the split. */ -.app-header { - display: flex; - align-items: center; - gap: var(--sp-3); - flex: 0 0 auto; - padding: var(--sp-1) var(--sp-3); - border-bottom: 1px solid var(--border); - background: color-mix(in oklab, var(--surface) 82%, transparent); - backdrop-filter: blur(8px); -} - -/* Hamburger to the left of the brand — toggles the off-canvas session drawer. - Mobile only (desktop keeps the sidebar permanently in view). */ -.app-menu { - display: none; - place-items: center; - flex: none; - width: 32px; - height: 32px; - border: none; - border-radius: var(--r-1); - background: transparent; - color: var(--ink); - font-size: var(--fs-17); - cursor: pointer; -} -.app-menu:hover { - background: var(--surface-2); -} - -/* Three-part header: brand (left), status (centered), actions (right). Equal - flex on the outer two keeps the status group truly centered. */ -.app-header .brand { - flex: 1 1 0; - min-width: 0; - margin-right: 0; -} - -.app-status { - flex: 0 1 auto; - display: flex; - justify-content: center; - min-width: 0; - overflow: hidden; -} - -.app-status .statusbar { - border-bottom: 0; - background: transparent; - padding: 0; - flex-wrap: nowrap; -} - -.app-status .statusbar .spacer { - display: none; -} - -.app-actions { - display: flex; - align-items: center; - gap: var(--sp-2); - flex: 1 1 0; - justify-content: flex-end; -} - .app-shell { flex: 1 1 auto; min-height: 0; @@ -479,9 +412,6 @@ textarea { .sidebar-resize { display: none; } - .app-status { - display: none; - } } /* Sidebar's own slim header: just the session search. */ @@ -638,42 +568,6 @@ textarea { box-shadow: 0 0 0 3px var(--accent-tint); } -/* ---------- status bar ---------- */ - -.statusbar { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: var(--sp-2) var(--sp-3); - padding: var(--sp-2) var(--sp-3); - border-bottom: 1px solid var(--border); - background: var(--surface); - font-size: var(--fs-12); - color: var(--muted); -} - -.statusbar .spacer { - /* zero basis: claim leftover space without inflating the bar's min width */ - flex: 1 1 0; - min-width: var(--sp-2); -} - -.session-tally { - display: inline-flex; - align-items: center; - gap: 7px; - font-variant-numeric: tabular-nums; -} - -.session-tally::before { - content: ""; - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--live); - box-shadow: 0 0 5px var(--live-glow); -} - /* ---------- session tree ---------- */ .tree { @@ -1247,16 +1141,17 @@ input.leaf-rename:focus { width: 26px; } -.attach-header .back { +/* Opens the off-canvas session drawer. Desktop keeps the sidebar permanently in + view, so it is mobile-only — same rule as the welcome state's ☰. */ +.attach-header .sessions-btn { font-size: var(--fs-15); width: 28px; height: 26px; } -/* Desktop keeps the session list permanently in view, so the in-header "back" - arrow is redundant — the sidebar is the nav. Mobile (single-pane) keeps it. */ @media (min-width: 860px) { - .attach-header .back { + .attach-header .sessions-btn, + .welcome-sessions { display: none; } } @@ -2087,9 +1982,6 @@ input.leaf-rename:focus { max-width: none; order: -1; } - .statusbar .spacer { - display: none; - } .tree-leaf .leaf-actions { opacity: 1; } @@ -2383,11 +2275,6 @@ input.leaf-rename:focus { .mv-pane:not(.is-focused) .attach-header { opacity: 0.7; } -/* per-pane back button is meaningless in multi-view (use Close pane instead) */ -.mv-pane .attach-header .back { - display: none; -} - /* Draft pane (#57): an empty pane renders the shared spawn form inline. The host is already a flex column; the form fills and scrolls within it. */ .mv-pane-host > .mv-draft { @@ -2509,10 +2396,6 @@ input.leaf-rename:focus { } @media (max-width: 859px) { - .app-menu { - display: inline-grid; - } - /* sidebar → off-canvas drawer (override the route-based show/hide) */ .app-shell > .sidebar { display: flex !important;