Popups & menus
Try menus and popups that open beside a control. Each example shows where it appears and whether it closes on an outside click, Escape, or key release.
This page uses a saved copy of the design system, last changed on . It may differ from the current design source.
Tools menu
Tools menu — anchored, no scrim
A popup anchored above the sidebar Tools button (left-aligned to it). Unlike the modal layer this is a lightweight menu — no scrim, dismisses on outside click or Esc. Built-in entries first, in this fixed order — Command palette… · Listening ports… · Remote connections… · Presets · Tutorial… · Task DAGs · Open File… — then a separator, then plugin-contributed tools in manifest order (Clipboard Viewer 100 · Git 130). Every label that opens a further window ends in the single ellipsis character …, never three periods. Width (2026-10-07): the menu is as wide as its longest row (label + --tasty-menu-item-padding-x both sides + the 4 ring + border), never narrower than 160 and never wider than 240; a label past 240 ellipsizes at its end. This covers translated labels without a per-locale width.
Plugins extend the list below the separator only — built-in order is fixed. This is a popover, not a dialog: it never dims the app.
Search bar
Search bar — Ctrl/⌘F, headless
A 360px headless popup floating at the top-right of the focused surface — not on a scrim, and it stays open on outside click. One row: a flex Input, a cur/total counter (red on zero matches), ▲▼ steppers, Aa / .* / ab option toggles, a divider, and close. ↵ next, Esc closes.
Sticky-focus popover: the input keeps focus and the bar persists through clicks in the terminal — it closes only on Esc or its × button.
Query field ground — normal · focused · invalid (2026-10-27)
The bar is --tasty-surface-raised, so the query field is an Input ground="raised" (the 10-14 rule, no exception): bg-panel fill, border-strong edge. normal is the box after a shortcut hands focus back to the terminal; the bar stays open. invalid is a regex error with .* on: the edge is accent-danger in both focus states, and focused adds the danger ring. Behaviour (Enter, Shift+Enter, Esc, filtering) and copy are unchanged.
Switch-number overlay
Tab switch overlay — modifier held
Holding the tab switch modifier (Ctrl by default — rebindable) paints a number keycap over every tab, previewing the Ctrl+1…Ctrl+9 / Ctrl+0 switch shortcuts. The keycap replaces the tab's leading icon in place — no width change, no scrim; the icon returns on release. The active tab's keycap is accent-filled so "where am I" stays legible. Workspaces show nothing — only the modifier bound to tabs triggers this.
The digit equals the key you press, so the 10th tab reads 0 (not "10") to match Ctrl+0. Tabs past the 10th carry no shortcut, so they get no keycap — never paint a number a key won't trigger.
Workspace switch overlay — modifier held
Holding the workspace switch modifier (Alt by default — rebindable) paints a keycap over each sidebar workspace, previewing Alt+1…Alt+9. In the full sidebar the keycap replaces the leading status dot; in the collapsed rail it replaces the letter avatar. Same in-place, no-scrim rule as tabs; tabs show nothing here. Releasing dismisses instantly.
Workspace shortcuts only go 1–9 (no 0), so the 10th workspace and beyond get no keycap — in the collapsed rail they keep their letter avatar (last item above). The bound modifier is read from the same keybindings the shortcut uses: rebind it and the overlay follows.
Do replace the leading indicator rather than overlapping the label — the keycap's own surface-raised fill + border clears 4.5:1 on both tab and sidebar backgrounds without a scrim.
Category switch overlay — Ctrl+Shift held
A third switch axis, present only when Workspace categories (folders) is on. Holding the category switch modifier (Ctrl+Shift by default — rebindable) paints a keycap over each category, previewing Ctrl+Shift+1…Ctrl+Shift+9 / Ctrl+Shift+0. In the full sidebar the keycap is right-aligned on the category header — it does not replace the chevron, which stays to carry collapse state (and its auto-expand rotation). In the rail it sits centered on the --- boundary. The reserved normal category (“Workspaces”) is 1. This overlay and the workspace overlay are modifier-exclusive — Alt paints workspaces, Ctrl+Shift paints categories, never both at once, so workspace rows keep their status dots here. The chord is the product default category_switch_modifier = ctrl+shift (every preset) — the gallery and the app read it from there.
Auto-expand: switching to a collapsed category rotates its chevron open and reveals its rows (the chevron is why it isn't replaced by the keycap) — the new collapse state persists in layout.json. Last-active: the switch lands on that category's most-recently-focused workspace (its first if never visited), shown with the ordinary surface-active + 2px accent bar — no separate affordance. Reuses the NumCap keycap and every --tasty-switch-overlay-* token; no new tokens.
Modifier hints
Modifier hint panel — hold to reveal
A floating, focus-less discoverability panel painted while a modifier is held — like macOS's hold-⌘ shortcut sheet. Press & hold the Ctrl control: after a 500ms dwell the panel fades in (opacity 0.2→1 over 200ms); release dismisses it instantly. It defaults to the bottom-left, just above the sidebar footer, and is draggable + edge-resizable. It never dims the app and never takes keyboard focus.
Reduced motion skips the fade — the panel appears at full opacity the instant the 500ms hold completes. The bound modifier set is read from the same keybindings source the shortcuts use (Win/Linux: Ctrl / Alt / Shift; macOS adds Cmd / Option).
Anatomy & chord ordering
Sections are the shortcut chords that contain the held modifier, ordered by chord size (1→2→3→4) then by modifier priority Ctrl → Cmd/Alt → Option → Shift. Each section header is the chord as Kbd keycaps; under it sit the bound keycap rows (action + Kbd), an optional special-role row (what the chord itself does — e.g. Ctrl = tab-switch numbers, Shift = TUI mouse-capture bypass), and plugin rows carrying the agent dot. A chord with no bindings and no role is omitted entirely.
Do keep it opaque and low-chrome — it floats over live terminal output, and the muted drag strip (not a titlebar) is the promise that it will never steal focus.
Don't hide a chord that a key combo could reach — a bound-empty chord shows its ChordHead with a muted “No shortcuts bound” placeholder (see below), never nothing, so holding an all-empty combo still surfaces the panel.
When Workspace categories (folders) is on, the Ctrl+Shift chord carries a “Switch category” role row (folder glyph) — the discoverability entry for the category quick-switch (see Switch-number overlay). A chord's role row is how a numeric switch announces itself, exactly like Ctrl = tab-switch numbers.
Empty chord — “No shortcuts bound” placeholder
A chord that exists but carries no binding and no role is not omitted — its ChordHead shows with a single muted placeholder row beneath it. This reverses the original decision (“empty chord → omitted entirely, never ‘none’”): holding an all-empty combo like Ctrl+Alt+Shift now surfaces the panel with a “No shortcuts bound” line instead of showing nothing. The placeholder is the quietest row in the panel — text-muted, no keycap, no wash, no leading glyph — an absence signal, deliberately not a role-row. It is fully static and non-interactive.
Do keep the placeholder muted and bare — it announces an absence, so it must read quieter than a bound keycap row and never borrow the role-row's wash or glyph.
Don't give it a keycap, a hover highlight, or a washed background — a keycap implies a binding that isn't there, and a wash makes it look like a role-row that does something.
i18n key modifier_hint.empty — en “No shortcuts bound” · ko “지정된 단축키 없음” · ja “割り当てなし”. Fits one line in the 180px panel across all three. New token --tasty-modhint-empty-fg → text-muted (a pointer, no new primitive), so the implementing side flips build_hint_sections()' empty-retain and paints one draw_row variant.
Workspace categories
Sidebar context menu — target resolves under the cursor
Right-clicking anywhere in the sidebar workspace area opens this menu (no dedicated buttons) — only when Settings › General › Workspace categories (folders) is on. Items resolve to the target under the cursor: empty background, a category header, or a workspace row. The reserved normal category (“Workspaces”) is additive-only — no rename/delete. Same MenuItem + anchored-popup language as the Tools menu.
“Move to category” is an inert header with the target categories listed flat beneath it (current category omitted) — no submenu, because the product draws this menu with the OS-native context menu (2026-10-07). Drag-and-drop reorder stays the primary path; this is the menu fallback. New strings: workspace_category.add_workspace · collapse · expand. Existing: new_category · rename_category · delete_category · move_to_category.
Collapsed rail — `---` category button + anchored popup
In the 52px rail, categories aren't labelled — each boundary is a clickable horizontal --- button (the old thin separator, now a button). Clicking it opens a popup to its right (like the rail Tools button). The popup's top line is the category name, a non-clickable header; below are the actions. A category's avatars sit below its ---; a collapsed or empty category shows only the --- button.
Collapse state is per-category and shared with the full sidebar (both seed from the persisted collapsed flag in layout.json). Toggling here or in the full header updates the same state.
Notice column — restore failure · save halt · response wait (2026-10-19)
Restore failure — non-focus notice, top right (2026-10-19)
When the saved layout cannot be read at all at boot, Tasty does not stop: it opens one default workspace in its initial state and shows a small notice in the window's top-right notice column. No scrim, no centred modal, no focus move: the terminal keeps the keyboard, and keys, selection and scroll outside the notice behave as if it were not there. The notice is the shared popup frame (PopupTitleBar with ×, popup shadow): cause line with an error glyph, the saved-layout path (selectable, wraps anywhere, copy button), what happened and that the original is untouched, then Continue. × and Continue are the same action. The same error is recorded in Notifications, so dismissing loses nothing. Not for: a home locked by another instance, the first shell failing to start, an explicit webhook port failure.
Contrast (on bg-panel): cause text-primary Mocha ≈ 11:1 · Latte ≈ 7.1:1 · explanation text-secondary Mocha ≈ 8:1 · Latte n1000 ≈ 5.9:1 · label text-muted Mocha ≈ 5.4:1 · Latte n900 ≈ 4.6:1 · path text-primary on bg-sidebar Mocha ≈ 11.6:1 · Latte ≈ 6.5:1 · glyph accent-danger Mocha ≈ 6.5:1 · Latte ≈ 4.6:1 (≥ 3).
Narrow · long path · split + search bar · save halt · ko / ja · the notification record
Narrow window: the notice takes the work area minus 8 on each side; the path wraps, the button never leaves the frame. With a split, the column still belongs to the window's top-right; if the top-right pane has its search bar open, the column starts under it (the bar is where the user is typing and keeps its place). Save halt (running, saving stopped) uses the same notice: current output and input go on, and it says layout changes from now on may not be restored. The Notifications record is an ordinary error item with the same title, cause and path.
Notice column — what sits where, top right (2026-10-19)
One column, the window's work-area top-right, inset 8. Order top → bottom: restore / save-halt notice, then the response-wait chip (PendingIndicator), 8 apart, right-aligned. The column starts below a window-scope banner (resize failure, reconnect) and below an open search bar in the top-right pane; neither is moved by it. Pane- and surface-scope banners stay inside their own scope and do not move the column. While a reconnect banner is up the chip is gone (the request ended in the disconnect path). The chip never becomes a scrim or a full-width bar.
Over a web page — opaque mode (2026-10-21)
Contract — popup over a page in opaque mode
A markdown or html page is a native WebView. Where the host cannot composite over it with transparency (e.g. Linux X11 with no compositing manager), the host paints only inside the popup box rect and each dropdown box rect; those pixels are fully opaque and the page outside them cannot change. Transparent mode is unchanged. The mode is decided by the environment; there is no user setting and no on-screen indicator. Toasts and passive popups keep the current rule (overlapped page hidden).
Input routing (unchanged contract, annotated here): while a target popup is visible, a click on the page body is that popup's outside click and never reaches the page — popups that close on outside click close (convert_surface; command palette, which is sticky_focus=true, closes too), others stay open and only lose focus (notification panel, search bar). Keys and IME go to the focused popup, never to the page. Closing the popup leaves the page exactly as before it opened.
Why no scrim: the scrim cannot darken the page, so drawing it only on terminal / tab bar / sidebar makes the page the brightest thing on screen while a modal is up (measured). Dropping it keeps the screen uniform; the modal is still marked by its frame border, its focus, and outside-click routing. When no opaque page is on screen (page in a background tab, or transparent mode), the scrim draws as today.
A — convert_surface over a full-pane markdown page
Surface-scoped scrim, and the surface is the page. Transparent: scrim over the page, rounded shell with --tasty-shadow-modal. Opaque: no scrim, square shell, no shadow, --tasty-border-frame edge kept. Current: page hidden behind the boundary tile.
The 240 × ui_scale width is unchanged; longer de / ja / ko titles take the same band and tooltip rule as today, the edge treatment does not depend on width.
B — command palette across markdown page | terminal
Window scrim. Opaque mode drops the scrim on both panes, because the page pane cannot be dimmed. The fourth column is the rejected option: scrim kept outside the page.
Don't keep the scrim on the non-page areas only: the page stays at full brightness while the terminal, tab bar and sidebar go dark (the blotch seen in measurement).
C — Tools menu straddling sidebar | page
Scrim-less, --tasty-shadow-popover in transparent mode. Part of the menu sits over the sidebar, part over the page. Because the box rect intersects the page, the whole menu is square with no shadow, including the part over the sidebar where a shadow could be drawn.
Do decide the edge once per box from its rect. A half-rounded, half-shadowed menu reads as a rendering fault.
D — file picker with the path-bar dropdown hanging over the page
Two host rects: the picker box and the crumb dropdown box, which extends past the picker's bottom edge onto the page. Both are square with no shadow; the dropdown keeps --tasty-menu-border. Select / SearchableSelect menus opened from a popup over a page follow the same rule.
A dropdown opened from an opaque-mode shell is opaque-mode even if its own rect ends over a terminal, so the parent and child never disagree.