From d6033262feef2637990c7850855b885d29ecce1e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 02:43:44 +0000 Subject: [PATCH] Tool window: tabs across the four views, maximize, one rail The window Library, Algorithms, Grammar and Reference share had a title bar that named the open view and offered only a close button, so moving between views meant closing the window and reopening it from the More menu. Its bar is now a tab strip across the four, Library first, built from AUX_META so a view is named and iconed in one place. - Tabs: roving tabindex, Arrow/Home/End, the old subtitle as each tab's tooltip with its shortcut. AUX_VIEWS is the order of the strip, the More menu and the digits; auxViewKey() derives the digit from the position, so Library is now 2 and the tools 3-5. - Maximize: a toggle (aria-pressed), remembered per reader, desktop only; double-clicking the bar does the same. No minimize - every view already keeps its place when closed. - The canvas machine is named in the bar while Algorithms is open. - The four rails share one width and one row face (they were 200, 208, 190 and 190px, two of them inline, in two different fonts). - Close hovers neutral rather than red. - Empty-canvas messages pointed at a Build tab that no longer exists; they now offer a Back to canvas button. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HPVKDwzQWjyqE1nApBQt6s --- CLAUDE.md | 2 + css/views.css | 259 ++++++++++++++++++++++++++++++--------- index.html | 52 ++++---- js/algorithms-fa.js | 22 ++-- js/ui.js | 13 +- js/view.js | 142 ++++++++++++++++++++- tests/aux-window.test.js | 134 ++++++++++++++++++++ 7 files changed, 529 insertions(+), 95 deletions(-) create mode 100644 tests/aux-window.test.js diff --git a/CLAUDE.md b/CLAUDE.md index 080518a..d55441a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -193,6 +193,8 @@ Node internals are reached through `node.__parts` (`circle`, `label`, `ring`, `s `setView()` in [js/view.js](js/view.js) is the single entry point. The build view (canvas) is always mounted; `algo`, `grammar`, `reference` and `library` render as overlays on top of it, so canvas geometry stays measurable. Algorithms call `setView('build')` to reveal a result. +The four share one window (`#aux-overlay`), and its bar is a **tab strip across them**, built by `ensureAuxChrome()` from `AUX_META` on first open — so a view is renamed or re-iconed in one place, and none of the bar's controls are in `bridge.js`. Library comes first. `AUX_VIEWS` is the order of the strip, the More menu and the shortcut digits alike — `auxViewKey()` derives the digit from the position (Build is 1), so reordering is one edit. Each view keeps its place when closed, which is why the bar has a close button and no minimize. Maximize is a per-reader preference (`automata-aux-max`), desktop only. The rails share one width, `--aux-rail-w`, set by the window rather than per view: once the views sit a click apart, a rail edge that moves between them is visible. `#aux-ctx` names the canvas machine while Algorithms is open, because that is the view that reads it. [tests/aux-window.test.js](tests/aux-window.test.js) pins the strip, the arrow keys, maximize and the chip. + ### Reference The third aux view is the reference. Rendering is [js/reference.js](js/reference.js); content is data, split across two registries that share one page shape and one renderer: diff --git a/css/views.css b/css/views.css index 45760ab..742b7ea 100644 --- a/css/views.css +++ b/css/views.css @@ -118,65 +118,147 @@ } } -/* ── Shared modal chrome ── */ +/* ── Shared window chrome ── + One row: the four views as tabs, then what belongs to the window rather + than to a view — the machine an algorithm will read, maximize, close. The + title and subtitle that used to sit here repeated the tab a reader had just + picked; the title survives as the dialog's accessible name and the subtitle + as each tab's tooltip. + + A tab is cut out of the bar the way the sidebar tabs are cut out of the + panel header (see css/panels.css): the selected one takes the body's own + --bg2 and covers the bar's rule beneath it, so the tab and the view it + names meet with nothing between them. */ .aux-overlay-bar { display: flex; - align-items: center; - gap: 13px; - padding: 15px 14px 15px 20px; + align-items: flex-end; + gap: 12px; + min-height: 46px; + padding: 0 8px 0 10px; background: var(--surface); border-bottom: 1px solid var(--border); flex-shrink: 0; + user-select: none; } -.aux-overlay-icon { +.aux-tabs { display: flex; + align-items: flex-end; + gap: 2px; + min-width: 0; + flex: 1 1 auto; + overflow-x: auto; + scrollbar-width: none; +} + +.aux-tabs::-webkit-scrollbar { display: none; } + +.aux-tab { + position: relative; + display: inline-flex; align-items: center; - justify-content: center; - width: 34px; - height: 34px; - flex-shrink: 0; - border-radius: 9px; - background: var(--accent-soft); - border: 1px solid var(--accent-border); - color: var(--accent); + gap: 8px; + height: 36px; + padding: 0 14px; + margin-bottom: -1px; + border: 1px solid transparent; + border-bottom: 0; + border-radius: 9px 9px 0 0; + background: transparent; + color: var(--text3); + font-family: var(--sans); + font-size: .8rem; + font-weight: 500; + letter-spacing: -.005em; + white-space: nowrap; + cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast), + border-color var(--transition-fast); } -.aux-overlay-icon svg { - width: 17px; - height: 17px; +.aux-tab-ico { + display: inline-flex; + width: 15px; + height: 15px; + opacity: .75; + transition: opacity var(--transition-fast); } -.aux-overlay-heading { - flex: 1; - min-width: 0; +.aux-tab-ico svg { width: 100%; height: 100%; } + +.aux-tab:hover { + color: var(--text); + background: color-mix(in srgb, var(--bg2) 55%, transparent); } -.aux-overlay-title { - font-family: var(--sans); - font-size: 1.02rem; - font-weight: 600; - letter-spacing: -.01em; +.aux-tab:hover .aux-tab-ico { opacity: 1; } + +/* The accent rides the top edge, because the bottom edge is the seam. */ +.aux-tab.active { color: var(--text); - line-height: 1.25; + background: var(--bg2); + border-color: var(--border); + height: 37px; +} + +.aux-tab.active::before { + content: ''; + position: absolute; + top: -1px; + left: 10px; + right: 10px; + height: 2px; + border-radius: 0 0 2px 2px; + background: var(--accent); +} + +.aux-tab.active .aux-tab-ico { + opacity: 1; + color: var(--accent); +} + +.aux-tab:focus-visible { + outline: 2px solid var(--accent); + outline-offset: -3px; +} + +/* Sits on the tabs' centre line, not the bar's: the tabs hang from the + bottom edge, so centring on the bar would float these above the labels. */ +.aux-bar-end { + display: flex; + align-items: center; + gap: 4px; + margin-left: auto; + margin-bottom: 4px; + flex-shrink: 0; } -.aux-overlay-sub { +/* The machine the Algorithms view reads off the canvas. */ +.aux-ctx { + display: inline-flex; + align-items: center; + gap: 8px; + margin-right: 8px; + padding: 3px 10px 3px 4px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--bg2); font-family: var(--mono); - font-size: .67rem; - color: var(--text3); - margin-top: 2px; + font-size: .66rem; + color: var(--text2); white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } -.aux-overlay-close { +.aux-ctx[hidden] { display: none; } + +.aux-ctx .badge { margin: 0; } + +.aux-bar-btn { display: flex; align-items: center; justify-content: center; - width: 32px; - height: 32px; + width: 30px; + height: 30px; flex-shrink: 0; padding: 0; border: 1px solid transparent; @@ -188,22 +270,45 @@ border-color var(--transition-fast); } -.aux-overlay-close svg { +.aux-bar-btn svg { width: 15px; height: 15px; } -.aux-overlay-close:hover { - background: var(--red-soft); - border-color: var(--red-border); - color: var(--red); +/* Neutral, not red: closing is "back to the canvas", and every view keeps its + place. Red is what the app says before it throws something away. */ +.aux-bar-btn:hover { + background: var(--bg2); + border-color: var(--border); + color: var(--text); } -.aux-overlay-close:focus-visible { +.aux-bar-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } +.aux-max-ico-shrink, +.aux-overlay.is-max .aux-max-ico-grow { display: none; } +.aux-overlay.is-max .aux-max-ico-shrink { display: block; } + +/* Maximized: the window fills the app with a narrow margin, so the canvas + still frames it and it still reads as a window on top of something. */ +@media (min-width: 901px) { + .aux-overlay.is-max { + width: calc(100vw - 24px); + height: calc(100vh - 24px); + height: calc(100dvh - 24px); + } +} + +@media (min-width: 901px) and (prefers-reduced-motion: no-preference) { + .aux-overlay { + transition: width .24s cubic-bezier(0.22, 1, 0.36, 1), + height .24s cubic-bezier(0.22, 1, 0.36, 1); + } +} + .aux-overlay-body { flex: 1; display: flex; @@ -239,10 +344,22 @@ } /* The nav rails are the modal's inner edge, not the window's — a full-weight - border reads as a seam here, so lighten it. */ + border reads as a seam here, so lighten it. + + One width for all four. They were 200, 208, 190 and 190 — two of them set + inline — which was invisible while the only way between views was to close + the window, and is a rail edge jumping under the pointer now that the tabs + sit one click apart. Below 900px the rails become sheets and size + themselves (css/mobile.css). */ +.aux-overlay { + --aux-rail-w: 212px; +} + .aux-overlay .algo-nav, .aux-overlay .gram-nav, .aux-overlay .ref-nav { + width: var(--aux-rail-w); + min-width: var(--aux-rail-w); border-right: 1px solid var(--border); } @@ -286,17 +403,27 @@ } .aux-overlay-bar { - padding: 11px 10px 11px 14px; - gap: 10px; + padding: 0 6px 0 6px; + gap: 6px; + } + + /* A phone has room for four icons, not four names. The tooltip and the + accessible name still carry the name. */ + .aux-tab { + padding: 0 12px; + } + + .aux-tab-label { + display: none; } - .aux-overlay-icon { - width: 30px; - height: 30px; + .aux-tab.active .aux-tab-label { + display: inline; } - .aux-overlay-title { - font-size: .92rem; + .aux-ctx, + .aux-max-btn { + display: none !important; } /* The modal is a fixed-height box, so its contents scroll inside it rather @@ -3548,13 +3675,15 @@ body.aux-open .canvas-aurora > i { color: var(--accent); } +/* One group heading for all four rails — this, .gram-nav-group and + .ref-nav-group draw the same thing and must keep agreeing. */ .algo-grp { font-family: var(--mono); font-size: .58rem; - letter-spacing: .12em; + letter-spacing: .1em; text-transform: uppercase; color: var(--text3); - padding: 10px 16px 4px; + padding: 14px 16px 4px; } .algo-content { @@ -5018,7 +5147,7 @@ body.aux-open .canvas-aurora > i { letter-spacing: .1em; text-transform: uppercase; color: var(--text3); - padding: 14px 14px 4px; + padding: 14px 16px 4px; } .gram-nav-link { @@ -5295,19 +5424,24 @@ body.aux-open .canvas-aurora > i { letter-spacing: .1em; text-transform: uppercase; color: var(--text3); - padding: 14px 14px 4px; + padding: 14px 16px 4px; } +/* The same row as .algo-item and .gram-nav-link. It was the last rail set in + mono, so the Reference and Library rails read as a different component from + the two beside them in the tab strip. "Queue Automaton" and "Browse & + search" are words, which is what --sans is for. */ .ref-nav-link { display: block; - font-family: var(--mono); - font-size: .72rem; + font-family: var(--sans); + font-size: .76rem; color: var(--text2); padding: 6px 14px 6px 16px; cursor: pointer; text-decoration: none; border-left: 2px solid transparent; - transition: all var(--transition-fast); + transition: background var(--transition-fast), color var(--transition-fast), + border-color var(--transition-fast); } .ref-nav-link:hover { @@ -5749,3 +5883,18 @@ body.aux-open .canvas-aurora > i { .tracker-st-btn:not([hidden]) + .tracker-copy-btn { margin-left: 2px; } + +/* An algorithm with nothing on the canvas to read. */ +.algo-empty { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + font-family: var(--sans); + font-size: .8rem; + color: var(--text2); +} + +.algo-empty-btn { + flex-shrink: 0; +} diff --git a/index.html b/index.html index c26ca51..b12f4ac 100644 --- a/index.html +++ b/index.html @@ -268,21 +268,21 @@ its text. -->
+ -
@@ -1205,22 +1205,32 @@ + Algorithms, Grammar, Reference and Library are not peers of Build: + they share one window over the canvas, which stays mounted + underneath, and are dismissed with Esc, the close button, or a click + on the backdrop. The bar is a tab strip across the four, built by + js/view.js from AUX_META, so moving between them never means + closing the window first. -->