diff --git a/CLAUDE.md b/CLAUDE.md index d55441a..eb16fcc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -193,7 +193,7 @@ 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. +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. The header reopens the last view from `#hdr-last-view`, inside the workspace strip's frame (`.tab-strip`) but outside its scroller so it never scrolls away with the tabs — the frame, not `.tab-bar`, holds the border, and the overflow chevron sits in it for the same reason. On a phone the same button shares a pill with the workspace switcher (`#mobile-last-view`); the window opens below the header there, so a second press closes it. The view is remembered per reader (`automata-aux-last`). [tests/aux-window.test.js](tests/aux-window.test.js) pins the strip, the arrow keys, maximize and the chip. ### Reference diff --git a/css/layout.css b/css/layout.css index 8bd9b8a..4c68a08 100644 --- a/css/layout.css +++ b/css/layout.css @@ -620,15 +620,30 @@ body:has(.rpanel.is-empty) .hdr-right { overflow: hidden; } -.tab-bar { +/* The strip's frame. It holds the border so that what sits beside the + scroller — the chevron, the last tool view — is inside the strip without + scrolling with it. */ +.tab-strip { display: flex; align-items: center; - gap: 1px; - background: transparent; + gap: 2px; + flex: 1; + min-width: 0; height: 36px; align-self: center; + padding-right: 3px; border: 1px solid var(--border); border-radius: 7px; +} + +.tab-bar { + display: flex; + align-items: center; + gap: 1px; + background: transparent; + height: 100%; + align-self: stretch; + border-radius: 6px 0 0 6px; flex: 1; min-width: 0; overflow-x: auto; @@ -640,6 +655,8 @@ body:has(.rpanel.is-empty) .hdr-right { transition: box-shadow var(--transition-fast); } +/* Drawn flat, like the tabs it lists: it is part of the strip now, and a + bordered box inside a bordered strip read as a second control bolted on. */ .tab-overflow-btn { display: none; align-items: center; @@ -647,12 +664,11 @@ body:has(.rpanel.is-empty) .hdr-right { width: 24px; height: 24px; align-self: center; - margin-right: 2px; flex: 0 0 auto; z-index: 1; - border: 1px solid var(--border); + border: 1px solid transparent; border-radius: 5px; - background: var(--bg); + background: transparent; color: var(--text3); cursor: pointer; transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast); @@ -663,9 +679,9 @@ body:has(.rpanel.is-empty) .hdr-right { } .tab-overflow-btn:hover { - background: var(--surface); + background: var(--tab-hover-bg); color: var(--text2); - border-color: var(--border2); + border-color: var(--border); } .tab-overflow-btn[aria-expanded="true"] { @@ -685,6 +701,64 @@ body:has(.rpanel.is-empty) .hdr-right { transform: rotate(180deg); } +/* ── The last tool view ── + An unselected tab in everything but behaviour: same height, type and hover, + no box at rest. The divider is what says it is not one of the workspaces. */ +.tab-strip-sep { + flex: 0 0 auto; + width: 1px; + align-self: stretch; + margin: 7px 3px; + background: var(--border); +} + +.last-view-btn { + display: inline-flex; + align-items: center; + gap: 7px; + flex: 0 0 auto; + height: 30px; + padding: 0 10px; + border: 1px solid transparent; + border-radius: 5px; + background: transparent; + color: var(--text3); + font-family: var(--sans); + font-size: .74rem; + letter-spacing: .01em; + white-space: nowrap; + cursor: pointer; + transition: color var(--transition-fast), background var(--transition-fast), border-color var(--transition-fast); +} + +.last-view-btn:hover { + background: var(--tab-hover-bg); + color: var(--text2); + border-color: var(--border); +} + +.last-view-btn:focus-visible { + outline: none; + color: var(--text); + border-color: var(--tab-active-border); + box-shadow: 0 0 0 2px var(--focus-ring); +} + +.last-view-ico { + display: inline-flex; + width: 13px; + height: 13px; +} + +.last-view-ico svg { + width: 100%; + height: 100%; +} + +.last-view-btn.is-open { + color: var(--accent); +} + .tab-overflow-menu { max-height: min(60vh, 380px); overflow-y: auto; diff --git a/css/mobile.css b/css/mobile.css index bd06fd3..35bf8c8 100644 --- a/css/mobile.css +++ b/css/mobile.css @@ -18,7 +18,7 @@ .mobile-panel-scrim, .mobile-more-menu, .mobile-sheet-head, -.mobile-ws-btn, +.mobile-ws-pill, .mobile-aux-bar, .mobile-aux-sheet-scrim { display: none; @@ -85,21 +85,72 @@ and a bare class is (0,1,0), so `flex: 1 1 auto` on its own loses to it silently and the button collapses to its text. Capped, or on a tablet it stretches a two-word name across a third of the header. */ - header > .mobile-ws-btn { + header > .mobile-ws-pill { flex: 1 1 auto; - max-width: 200px; + max-width: 260px; } - .mobile-ws-btn { + /* The pill holds the box, so the workspace button and the last tool view + are two halves of one control — the shape the machine picker beside it + already has, a name and then an action. Each half is its own 32px target. */ + .mobile-ws-pill { display: flex; - align-items: center; - gap: 6px; + align-items: stretch; min-width: 0; height: 32px; - padding: 0 8px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface); + overflow: hidden; + } + + .mobile-ws-sep { + flex: 0 0 auto; + width: 1px; + margin: 7px 0; + background: var(--border); + } + + .mobile-last-view { + height: auto; + padding: 0 12px; + border: 0; + border-radius: 0; + color: var(--text2); + font-size: .74rem; + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; + } + + .mobile-last-view .last-view-ico { + width: 15px; + height: 15px; + } + + .mobile-last-view:hover { + background: transparent; + } + + .mobile-last-view:active { + background: var(--surface2); + color: var(--text); + } + + .mobile-last-view.is-open { + color: var(--accent); + } + + .mobile-ws-btn { + display: flex; + flex: 1 1 auto; + align-items: center; + gap: 6px; + min-width: 0; + height: 100%; + padding: 0 8px; + border: 0; + border-radius: 0; + background: transparent; color: var(--text2); font-family: var(--sans); font-size: .74rem; @@ -110,10 +161,17 @@ .mobile-ws-btn:active { background: var(--surface2); - border-color: var(--border2); color: var(--text); } + /* A phone has room for the icon, not the name; the tooltip and the + accessible name still carry it. */ + @media (max-width: 560px) { + .mobile-last-view .last-view-label { + display: none; + } + } + /* The tab strip's accent dot, kept: it is how a workspace is told apart at a glance everywhere else in the app, and the button is now the only place the active one is named. */ diff --git a/index.html b/index.html index b12f4ac..d0c80d8 100644 --- a/index.html +++ b/index.html @@ -89,25 +89,43 @@
- + 900px, where the strip itself is back. It shares a pill with the last + tool view, so the phone's header has the strip's shape: the + workspace, a divider, the way back to the tool window. --> +