tasty.gallery
Plugins · Plugin surfaces
← Design
Specs
Theme

Plugin surfaces

Explore the Explorer and the Markdown, HTML, and image viewers. These tools sit alongside terminals in the work area, so you can read files and check results without leaving your workspace.

This page uses a saved copy of the design system, last changed on . It may differ from the current design source.

Explorer — the file-manager surface

Editable path field — shared address component (Explorer + Markdown)

Both the Explorer toolbar and the Markdown chrome now share one editable path field — a single design SoT for the “path address” role. Display: one mono path line (ellipsis, --tasty-text-secondary) behind a leading icon, with a trailing Go (arrow-right) button. Click to edit: focus ring + blink caret, text goes --tasty-text-primary; ↵ or Go navigate to the typed path, Esc reverts. While editing, a AutoComplete candidate dropdown hangs below the field — a typeahead of recent paths (Explorer = recent directories, Markdown = recent files) that narrows as you type; ↑↓ move the keyboard-active row, ↵/Go confirm. The only per-surface parameters are the leading icon slot (Explorer folderOpen / Markdown file) and the candidate list. Field tokens align to forms/Input: --tasty-input-bg / --tasty-input-border (idle) → --tasty-input-border-focus (edit) + focus ring.

Layout spec
display
mono path · ellipsis · text-secondary
editing
focus ring (2px) + caret · text-primary
suggestions
editing → AutoComplete dropdown (recent paths)
leading icon
per-surface slot — folderOpen / file
candidates
per-surface — recent dirs / recent files
Go
arrow-right · IconButton sm · ↵ also confirms
navigate
↵ / Go → typed path · Esc reverts
height / radius
28 · --tasty-radius
Tokens used
input-bg— field fillinput-border— idle borderinput-border-focus— editing borderborder-focus— focus ring (35%)autocomplete-menu-bg— dropdown fillautocomplete-row-bg-active— keyboard-active rowinput-icon-fg— leading iconaccent-primary— caret + Go glyph

Open questions resolved (design SoT): (1) leading icon is a per-surface slot — Explorer folderOpen, Markdown file — mirroring forms/Input's leading-icon slot. (2) Explorer's breadcrumb (Crumb + chevron) is dropped for the plain editable field; a hybrid crumb↔edit form was rejected. (3) Go button on both surfaces. (4) only the field + dropdown are shared; the outer bar stays per-surface. (5 — new) the edit-time candidate dropdown is the formal AutoComplete component (full state matrix — filtered / overflow-scroll / empty / hover vs keyboard-active — lives on the Components page), replacing the source-only Combobox. Source home: extract PathField into crates/tasty-ui-widgets composing the renamed AutoComplete (was Combobox); recent-path candidate source + wiring/i18n tracked separately from this design pass.

Explorer surface — full layout (Detail view)

A full file manager living in a work-area tile, like the terminal/markdown surfaces. Top→bottom: an internal directory tab bar (lighter than the pane tab strip — each tab is its own cwd), a toolbar (Back / Forward / Up / Refresh · editable path field · grid/list/detail view toggle), then a body of a 196px sidebar (Files tree over a Favorites list) feeding the content area. Default view is Detail; the address is an editable path field shared with the Markdown surface (click to type a path, ↵/Go to jump to that directory).

Layout spec
surface
fills a work-area tile
internal tabs
28px · per-cwd, with × + +
toolbar
44px · nav · path field · view toggle
sidebar
196px — Files tree + Favorites
splitter
1px --tasty-separator, drag to resize
row height
28 (Detail) · --tasty-table-cell-height — the shared Table
detail header
shared Table header — UI font · table-header-font-size 11 · weight medium · caps
min height
180 · --tasty-explorer-min-height — the split drag stops here (tabs 28 + toolbar 44 + header 28 + 2 rows + status 24)
selected row
--tasty-surface-active
Tokens used
bg-panel— surface + contentbg-sidebar— tabs + sidebar + headersurface-raised— view toggle containersegtoggle-on-bg— view toggle — selected segmentinput-bg— path fieldsurface-active— selected row (rows only)accent-warning— favorite staraccent-primary— active tab bar

Defaults chosen (briefs left these open): Detail is the default view; the right-hand preview panel is dropped in favour of a wider content area (re-add later as a toggle if needed); no in-toolbar filter search in this pass; the tree is rooted at Home. Favorites are global across surfaces.

View toggle (SegToggle) — a segment, so it fills (settled)

The 2026-09-17 rule reads “underline switches a view, fill picks a value”. The Explorer view toggle looked like it sat on both sides: it is drawn as a fill, but what it changes is called ExplorerViewMode. It is a segment. “View” in the rule means different content — a tab strip swaps what is shown. grid / list / detail show the same listing drawn three ways; that is a setting, a value. So the selected segment takes the segment recipe already settled for the remote Connection toggle: --tasty-accent-primary fill + --tasty-text-on-accent ink — icon-only segments included, the glyph inherits the ink via currentColor. --tasty-surface-active leaves this control for good: it is row selection, never a segment fill. Off and hover are unchanged.

Layout spec
verdict
a VALUE — segment recipe (fill), same as remote Connection
on
--tasty-segtoggle-on-bg → accent-primary · --tasty-segtoggle-on-fg → text-on-accent
off
--tasty-text-muted glyph, no fill
hover (off)
--tasty-overlay-hover
container
surface-raised + border-default 1px · radius · pad 4 · gap 4 · segment 24 × 20 (2026-10-07: the product dims) — width = pad*2 + seg*3 + gap*2 + border*2
ink contrast
Mocha crust on blue ≈ 9.4:1 · Latte white on blue ≈ 4.8:1 — AA in both
motion
0ms — click swaps immediately
surface-active
row selection only — not a segment fill, not a tab fill
Tokens used
segtoggle-on-bg— selected segment fillsegtoggle-on-fg— selected segment glyphtext-muted— off glyphsurface-raised— container

Rule, extended: underline switches content (tab strip); fill picks a value (segment) — including a value that only changes how the same content is drawn. --tasty-surface-active is row selection and belongs to neither. No third branch is needed; the word “view” in a variable name does not make a control a tab strip.

Don't draw a 2px underline inside the toggle container — the segments sit on the container's own border and an underline would collide with it; that vocabulary belongs to tab strips at the top of a surface.

Sidebar Favorites — populated vs. empty state

The sidebar Favorites section caption is always shown, even at zero favorites — so the feature never disappears and reads as unimplemented. With favorites it lists STAR rows (left); empty, the caption stays and an empty state fills the slot: a faint outline star + "No favorites yet" in --tasty-text-muted, then a dimmer one-line hint pointing at the add path. Same tone as the content-area "This folder is empty" state — left-aligned to fit the 196px column.

Layout spec
caption
always shown — micro caps, text-muted
empty line
outline star + "No favorites yet" · --tasty-text-muted
hint
1 line · --tasty-text-placeholder
align
left, inside 196px column
condition
was: hide section at 0 → now: always show
Tokens used
text-muted— caption + empty linetext-placeholder— hint lineaccent-warning— filled star (populated)

New string explorer.sidebar.favorites_empty — en "No favorites yet" · ko "아직 즐겨찾기가 없습니다" · add to lang/{en,ko,ja}.toml. The hint reuses existing "Add to favorites" copy. Implementer: replace if !favorites.is_empty() in explorer.rs sidebar() with an always-rendered caption + empty branch.

Sidebar layout — Favorites PINNED to the bottom (2-region split)

The sidebar is not one scroll column any more. It splits into two independent regions: Files takes the remainder at the top and scrolls on its own, and Favorites is pinned to the bottom at a computed fixed height — so however deep the tree gets, the favorites list is always on screen. The boundary is a fixed 1px --tasty-explorer-split-border at a screen coordinate: it does not flow with content, so a short tree leaves empty --tasty-bg-sidebar above the line rather than pulling the line up. Pin height = 240 (--tasty-explorer-favorites-pin-height) while the sidebar body is ≥ 600 (--tasty-explorer-favorites-pin-threshold); below that it becomes 40% of the body height (--tasty-explorer-favorites-pin-ratio), snapped to the 4px grid and floored at 120 (--tasty-explorer-favorites-pin-min-height). Row, caption and empty-state visuals are unchanged — this is a layout change only.

Layout spec
structure
2 regions · independent scroll state
Files region
flex 1 · own scroll · caption pinned at its top
Favorites region
fixed height, bottom-pinned · --tasty-explorer-favorites-pin-height
pin height
body ≥ 600 → 240 · else 40% (4px-snapped) · min 120
boundary
1px --tasty-explorer-split-border at a fixed coordinate
sidebar width
196 unchanged · --tasty-explorer-sidebar-width
resize
recomputed from the live body height — no drag handle
row visuals
unchanged (tree row · star row · empty state)
Tokens used
explorer-sidebar-width— 196 columnexplorer-favorites-pin-height— pinned region heightexplorer-favorites-pin-threshold— small-surface switchexplorer-favorites-pin-min-height— lower clampexplorer-split-border— fixed boundary linebg-sidebar— both regions' fillaccent-warning— filled star

Open decisions resolved (design SoT): (1) threshold basis = the sidebar body height (the split container itself), not the whole explorer tab — the pin only competes with the tree, and this keeps the rule local to sidebar(). (2) scrollbar = the shared .tasty-scroll behaviour, hover-revealed like every other scroll area — the pinned region gets no special always-on bar. (3) slack space at 0–1 favorites stays empty --tasty-bg-sidebar: no filler padding, no vertical centering, so rows always start directly under the caption. (4) boundary weight = the same 1px separator as before — no shadow, no tint; "pinned" is communicated by behaviour (the line never moves, each side scrolls alone), not by extra decoration. (5) 240 ↔ 40% jump is left as a hard switch (no interpolation) as the requester allows, with a 120 floor added so a very short surface still shows caption + ~3 rows. Implementer: split the single ScrollArea in explorer.rs sidebar() into two — ScrollArea::vertical().id_salt("exp_tree") in the top allocation and id_salt("exp_favs") inside a bottom-up allocated strip of the computed height — and draw the separator as the strip's top edge instead of an inline ui.separator().

Short cell — Favorites drops below 240, the cell stops at 180 (2026-10-07 · floor 2026-10-08)

When the explorer is split low, the sidebar body can be shorter than the Favorites floor. Below 240 (--tasty-explorer-favorites-hide-below = the 120 Favorites floor + 120 for Files) the Favorites region is not drawn and Files takes the whole body; it comes back as soon as the body reaches 240 again. The explorer cell itself has a floor: the split drag stops at 180 (--tasty-explorer-min-height). 2026-10-08: the old 160 counted toolbar + status line + two rows but left out the internal tab bar and the Detail header, so the app showed only .. at the floor. 180 is the full stack, top to bottom: internal tabs 28 · toolbar 44 · Detail header 28 · two rows 56 · status line 24. The explorer's internal layout does not change at the floor (no smaller header or status line); the floor follows the stack. The status line always has its place and Detail shows two rows. Nothing is cut mid-row. Outside a drag (2026-10-09 wording): a split (tasty split or the split menu) that would leave an explorer cell under the floor places the new divider so the explorer keeps 180 and the sibling takes the rest. Only explorer cells have a floor; a sibling of another kind has none of its own, but a new split never leaves it under 56 (--tasty-split-sibling-min-height = pane tab strip 28 + one 28 row), so no sliver cell is made. The split is refused (no change) when that 56 can't be kept, when two explorer cells can't both keep 180, or when an explorer cell already under 180 would be split further. User split → info Toast “Not enough room to split this pane”; agent split → error split refused: not enough room. The check uses the largest view showing the workspace; with no window showing it, the floor is skipped. A window resize does not hold the floor — the cell may shrink below 180. Short body: when the content body is under 120 (--tasty-explorer-state-compact-below) every status screen switches to a one-line compact row — 16px glyph, title (ellipsis), and the state's buttons; the OS reason and the sub line move to the title tooltip. Retry and Go up stay on the row. Nothing is clipped.

Layout spec
hide Favorites
body < 240 · --tasty-explorer-favorites-hide-below
Files only
caption + tree, own scroll, full body
return
body ≥ 240 → pin ladder as before (120 floor)
cell floor
180 · --tasty-explorer-min-height · split drag stops here · tabs 28 + toolbar 44 + header 28 + 2×28 + status 24
split
explorer keeps 180, sibling takes the rest (≥ 56 · split-sibling-min-height); refused if 56 can't be kept, if two explorer cells can't both keep 180, or if an explorer under 180 would shrink further
refusal
user → info Toast “Not enough room to split this pane” · agent → split refused: not enough room
window resize
floor not held
compact state
content body < 120 · --tasty-explorer-state-compact-below · glyph · title · buttons on one row, reason in tooltip
scope
floor = explorer cells only · other kinds have no floor (0) during drags and resizes; 56 guards only a new split
sidebar
never hidden as a whole
Tokens used
explorer-favorites-hide-below— → size-240explorer-min-height— → size-180 (was 160)explorer-state-compact-below— → size-120split-sibling-min-height— → size-56 (2026-10-09)explorer-favorites-pin-min-height— 120 floor (unchanged)

The Favorites height rule already reads tokens (--tasty-explorer-favorites-pin-height 240, -pin-threshold 600, -pin-ratio 0.4, -pin-min-height 120). The three dimensions scale with the UI zoom like the other explorer tokens; the ratio is unitless and does not.

View modes — Grid · List · Detail

Layout spec
grid cell
80px · icon 16 + 3-line label wrap (…)
list row
22 · --tasty-tree-row-height (same row as the file tree) · icon + name
detail row / header
28 · --tasty-table-cell-height (same as the shared Table) · 4 columns
cell states
default · hover (8%) · selected (12%) · cut (50%) · renaming
sort
click a column header → ▲ / ▼
Tokens used
tree-row-height— List row (22)table-cell-height— Detail row + header (28)overlay-hover— row hover (8%)surface-active— selected (12%)accent-info— image-file glyph

Grid cell (updated): icon shrinks to icon_glyph_size_md (16, was 28) and the label wraps up to 3 lines at font_size_caption (11) with … overflow on the last line — replacing the old 12-char hard cut. Height is fixed to 3 label lines so grid rows stay uniform (short names reserve the space, label top-aligned). CELL_W stays 80. Real side: LayoutJob with wrap.max_rows = 3 + overflow_character = '…', drop truncate(&e.name, 12), recompute cell_h. Cut items dim to ~50% until pasted. A renaming cell swaps its label for an inline Input (extension preserved). Image cells show a thumbnail in the icon slot at implementation time — here a tinted glyph stands in. Row heights (2026-09-29): no Explorer-specific height. List reuses the tree row (--tasty-tree-row-height 22), Detail reuses the shared Table (--tasty-table-cell-height 28 for both header and body rows). The old 24 / 26 literals are retired. Implementer: the Table widget must read table_cell_height for row_height and header_height instead of its font-derived default (~35).

Right-click context menu — target resolves to 4 shapes

Finder/VS Code rule: right-clicking an item inside the selection targets the whole selection; outside it, selection collapses to that one item; on the empty background, the target is the current directory (cwd). Menu items appear/disappear accordingly, grouped by divider into clipboard · file ops · system.

Layout spec
target
in-selection → all · out → that one · empty → cwd
favorites
folder + cwd only
paste
shown when the clipboard holds files
open in system
single folder only
groups
clipboard · file ops · system (dividers)
Tokens used
menu-bg— menu fillmenu-border— 1px edgeshadow-popover— floataccent-danger— Trash (danger item)

Reuses the shared MenuItem / Popup visual language — no bespoke menu chrome. Shortcut hints are representative; real keys come from Keybindings settings.

Empty / permission / loading / read error · favorite + rename popups

Status screens fill the content area; the two small editors reuse the Popup/rename visual language. Read error (2026-10-07): any failure that is not a permission denial (path gone, not a directory, I/O error, remote read failure) gets its own state instead of falling through to “This folder is empty”: alertTriangle in --tasty-explorer-error-fg (→ accent-danger, as in the file picker error), title “Can't read this folder”, the OS reason as one mono caption line (text-muted, max 200, untranslated), and two buttons: Retry (secondary sm — re-reads the same path) and Go up (ghost sm — opens the parent; hidden at a root). Toolbar, tree and status line stay usable.

Layout spec
empty
centered glyph + line
permission
warning (peach) tone
loading
Spinner — busy-indicator policy
popups
Popup language · ghost + primary footer
Tokens used
accent-warning— permission tonespinner-indicator— loadingshadow-modal— popup lift

Explorer font (family + size, 14px cap) gets an Appearance › Explorer sub-tab mirroring the Terminal/Markdown font sections — same control layout, deferred to the Settings specimen.

Markdown viewer

Markdown — read-only viewer (address bar + body)

The markdown surface is an address-bar chrome over a single vertical scroll body that fills the tile (body on --tasty-md-doc-bg = crust, reloads on file change). The chrome is native UI. The body is an HTML/CSS document in a WebView: colour, heading sizes, leading and block spacing all come from Tasty tokens through theme_css() (maps in content colour and heading hierarchy below). The drawing uses the same values.

Layout spec
chrome
40px address bar + Go, on --tasty-bg-sidebar
body
13px · CSS line-height 1.6
headings
h1 20 → h6 13, 5-step CSS interpolation (20 · 18.6 · 17.2 · 15.8 · 14.4 · 13)
heading style
600 · text-primary at every level
h1
prose-h1 — the only cap-exempt content size
code
mono on --tasty-surface-raised
link
--tasty-accent-primary
table
grid + zebra, read-only — --tasty-md-table-*
Tokens used
bg-sidebar— chrome barmd-doc-bg— document bed (crust)input-bg— address fieldtext-secondary— bodytext-primary— headings · strongsurface-raised— code bg + inline chipaccent-primary— links + Gomd-table-border— table grid (surface1)md-table-header-bg— table header (surface0)md-table-row-bg-zebra— zebra row (mantle)font-size-prose-h1— h1 (cap-exempt)

The surface leads with a browser-style address bar (path display/edit) + Go button. The body follows the WebView/CSS path (2026-10-07); the earlier egui_commonmark renderer and its library-owned sizes are retired. Removed/redefined content tokens (--tasty-font-size-prose-h2, --tasty-line-height-prose removed; --tasty-font-size-prose-h1 → Heading anchor) are covered in the heading-hierarchy specimen.

Markdown — document background (single bed, webview render path)

Under the webview render channel the markdown body is an HTML/CSS document drawn by the host WebView — there is no focused/unfocused signal on that path, so markdown has exactly one background (unlike the terminal, which swaps two). That bed is --tasty-md-doc-bg → --tasty-surface-markdown-focused-bg = crust. Pure black is a terminal convention (raw TTY) and sits outside the Catppuccin ramp; prose at the 14px cap on #000 halates. crust is the palette's deepest tone, so every content fill still steps up and stays discriminable: crust (page) < mantle (table zebra) < base < surface0 (code + table header) < surface1 (table grid). mantle would have swallowed the zebra stripe; base would have flattened the table's own fill against the page.

Layout spec
backgrounds
one (no focus swap on the webview path)
mocha
crust #11111b
latte
crust #dce0e8 (tracks bg-app, no branch)
terminal
unchanged — pure black stays terminal-only
unfocused_bg
unused by the webview renderer
Tokens used
md-doc-bg— document bed → --md-bgmd-code-bg— code fill → --md-code-bgmd-table-row-bg-zebra— zebra → --md-zebramd-table-border— grid → --md-bordermd-quote-bar— blockquote barmd-rule— horizontal rule

Resolves the copy-paste value in [surfaces.markdown].focused_bg (was #000000, identical to terminal). Implementing side: set mocha focused_bg = crust, drop the Latte white override, and keep unfocused_bg as-is — it is dead on the webview path.

Markdown — content colour (WebView + CSS, 2026-10-07)

Markdown renders in a WebView. render.rs theme_css() writes Tasty tokens into CSS variables, and the stylesheet reads only those variables, so the page follows Mocha/Latte 1:1. Code highlighting is highlight.js (offline bundle): hljs_css() maps each hljs-* class to a palette role. Sizes, leading and block spacing are CSS too and belong to the next spec. The tables below are the source of truth (the old egui_commonmark / syntect path is retired).

Layout spec
path
WebView · theme_css() → CSS variables
--md-fg
--tasty-text-secondary
--md-strong
--tasty-text-primary
--md-link
--tasty-accent-primary
--md-code-bg / -border
--tasty-surface-raised / --tasty-separator
--md-quote-bar · --md-rule
--tasty-border-strong · --tasty-separator
hljs
keyword mauve · string green · title/function blue · number peach · type yellow · comment text-muted italic · tag teal · variable lavender · built_in red
diff
deletion / addition bg = accent-danger / accent-success at tint-fill-alpha
callouts
type colour bar (md-quote-bar-width) + type colour fill at tint-fill-alpha + radius + 16px type icon before the label · gaps 4 (md-callout-icon-gap) · collapsible: chevronRight at md-callout-marker-size (→ icon-size-sm) in the type colour, chevronDown when open, default <details> marker hidden · note accent-primary · tip accent-success · important accent-agent · warning accent-warning · caution accent-danger
Tokens used
text-secondary— bodytext-primary— headings / strongaccent-primary— linkssurface-raised— code fillsborder-strong— blockquote barseparator— code-block border

Both tables follow theme_css() / hljs_css() one row per variable or class; the two diff backgrounds are in the Meta. Both themes follow automatically because the palette roles re-point per theme.

Address bar states · large-file confirm

The address bar is a browser-style path field. Display shows the current file path (mono, secondary); click to edit (focus ring + caret) and type a new path — ↵ or the Go button opens it in place, Esc reverts to the original. A non-.md extension still opens as markdown. Opening a file over 1 MB first raises a surface-scoped confirm — clamped to the tile (not a window scrim), same 360px shell as markdown_open — with Open / Cancel; Cancel opens nothing.

Layout spec
field
mono path · click to edit
idle
secondary text, --tasty-input-border
editing
focus ring + caret, --tasty-input-border-focus
Go
IconButton · ↵ also confirms
popup scope
surface (clamped to tile)
popup
360px · Open / Cancel
Tokens used
input-bg— field fillinput-border-focus— edit borderborder-focus— focus ringaccent-warning— size chipshadow-modal— popup liftaccent-primary— Open (primary)

The address bar is the shared editable path field (see Explorer section) with a file leading icon — same field, same edit/navigate/revert contract as the Explorer toolbar, and the same edit-time AutoComplete dropdown (candidates = recent files). Copy: title “Open large file?” · body “Over 1 MB — rendering may be slow.” · size chip formatted (e.g. “3.2 MB”). Surface-scoped (PopupScope::Surface) so it dims only this tile; Cancel replaces nothing. New i18n keys when wiring: markdown.large_file.title / body.

Large file — read deferred after Cancel (2026-10-09)

Cancelling the 1 MB confirm leaves the body on the neutral state block (.tasty-state, same place and colours as the empty-file state). The block now carries a way back: a secondary sm Button “Open file…” under the line, --tasty-space-sm below it. It raises the same confirm again (it does not read the file directly), so the 1 MB rule has one entry point. The ellipsis says a question follows. Hover / active are the Button's own derived states. Local and mirror documents both get it; the CLI tasty markdown reload stays as it is.

Layout spec
block
.tasty-state — unchanged (centered, text-muted, body 13)
action
Button secondary sm · “Open file…” · under the line
gap
--tasty-space-sm line → button
click
raises large-file-confirm again (Open / Cancel)
Cancel again
same block, button stays
Tokens used
button-secondary-bg— button filltext-muted— state linespace-sm— line → button

WebView: render the button as a <button class="tasty-state-action"> styled from the injected Button variables; it posts the same IPC as the reload path. i18n: markdown.state.large_deferred (line) · markdown.state.large_deferred_open “Open file…”.

Address bar — folder button · local · remote (2026-10-21)

A folder button (folderOpen) now leads the band; it opens the Tasty file picker at the current document's folder with the Markdown filter, and the picked file replaces this surface's document. It sits outside the field and the field keeps its leading kind glyph (file = local, remote = mirror): the glyph says what the field holds, the button is the action, and the remote glyph is the same mirror marker the HTML band uses. The Markdown mirror field is now editable: type a remote path, Enter or Go replaces the document, Esc reverts. Reload joins the folder button in the left cluster, where the HTML band also keeps it. Raw → and ↻ are retired: every button is the shared IconButton ghost sm with an icons/ glyph.

Layout spec
band
md-address-bar-height 40 · md-address-bar-bg (bg-sidebar) · 1px md-address-bar-border below · padding-x 8 · group gap 8
left cluster
Folder (local) · Folder + Reload (remote), gap md-address-bar-nav-gap 4
buttons
IconButton ghost sm — 24 box, icon-size-md 16 glyph, radius; hover / active = Theme overlay 8% / 12%; disabled = state-disabled-fg, no overlay
field
PathField box — input-height 28 · pad-x space-sm · input-gap · leading kind glyph icon-size-sm (file | remote) · mono input-font-size · front ellipsis (tail = file name) · flex 1, floor md-address-field-min-width 96
Go
arrowRight (new glyph), trailing, both variants; Enter is the same action
remote editing
Enter / Go → same surface opens that remote path · Esc reverts · no recent list (local only)
stale
Reload = filled md-address-stale-bg (accent-primary) + md-address-stale-fg (text-on-accent); folder button stays ghost — the fill is the only loud thing in the band
disconnected
field = input-readonly box, path in state-disabled-fg, not editable · Folder · Reload · Go disabled · tooltip on each “Disconnected from the remote”
navigate failed
field keeps the typed path, input-border-invalid edge + ring, still editing · error row under the band (md-address-error-height 24, alertCircle in md-address-error-glyph, caption text-primary) — the HTML band's error row · clears on next keystroke, Esc, or a successful open
tooltips
open TOP, never down into the WebView
Tokens used
md-address-bar-bg— new · → html-address-bar-bgmd-address-stale-bg— new · → accent-primarymd-address-stale-fg— new · → text-on-accentmd-address-error-glyph— new · → html-address-error-glyphinput-readonly-bg— disconnected fieldinput-border-invalid— failed navigate edge

Markdown mirror vs HTML mirror. Same band box, same left-cluster Reload, same remote glyph as the mirror marker, same error row. The one difference is the field: HTML mirror = read-only box (input-readonly tokens) with no Go; Markdown mirror = the normal editable box with Go. The field box is how a user tells “I can type here” apart.

Folder button — states · tooltip strings

The shared IconButton; the only per-surface parts are the glyph and the tooltip. Hover / active use the Theme overlays; focus is the 2px focus ring; disabled (remote disconnected) has no overlay.

Strings (tooltip = aria-label without the ellipsis) — markdown.address.open: Open a file… · 파일 열기… · ファイルを開く… — .open_remote: Open a remote file… · 원격 파일 열기… · リモートのファイルを開く… — .go: Go · 이동 · 移動 — .reload: Reload · 새로 고침 · 再読み込み — .stale: The remote file changed — reload · 원격 파일이 바뀌었습니다 — 새로 고침 · リモートのファイルが変更されました — 再読み込み — .disconnected: Disconnected from the remote · 원격 연결이 끊겼습니다 · リモートから切断されました — .error.not_found: Can't open {path} on the remote: file not found. · 원격에서 {path} 파일을 열 수 없습니다: 파일이 없습니다. · リモートで {path} を開けません: ファイルが見つかりません。 Tooltips wrap at the tooltip max-width (size-240); the ja stale string takes two lines, the rest one.

Remote navigate failed — by kind · still waiting (2026-10-21)

The error row is one shape for every failure (alertCircle, caption text-primary, 24 row, field keeps the typed path with the invalid edge); only the sentence changes. Each kind the host can tell apart gets its own sentence, because the right next step differs: file not found (unchanged) · remote refused — the remote's own one-line reason is shown after the colon as sent (English, not translated; it is the only detail the user can act on) · connection lost before the answer — whether it applied is unknown, so the row says reconnect and try again and claims nothing about the document · cancelled because the connection / mapping changed before sending · internal (the host never got the request). No kind reuses “file not found”. After 30 s without a result the band shows a status row (Spinner + “Still waiting for the remote…”, md-address-pending-fg) in the error row's place — it is not an error, the remote may still answer; the field keeps the typed path in the editing look. The row goes away when the result arrives (success replaces the document; failure swaps in the error row) and on the same triggers that clear an error (next keystroke, Esc). A late result still applies as today.

Layout spec
row
unchanged error row — md-address-error-height 24 · alertCircle icon-size-sm md-address-error-glyph · caption text-primary · ellipsis at the end, full sentence as tooltip
path
mono inside the sentence, as the not-found row
reason
rejected only · the remote's line verbatim after “: ” · not translated (lang=en) · empty reason → internal sentence
waiting row
same 24 row, role=status · Spinner icon-size-sm · caption md-address-pending-fg (text-secondary on bg-sidebar) · starts at 30 s, no countdown
clears
error and waiting rows: next keystroke · Esc · successful open (unchanged rule)
Tokens used
md-address-pending-fg— new · → text-secondarymd-address-error-glyph— unchanged

i18n (new, markdown.address.error.*) — .rejected: Can't open {path} on the remote: {reason} · 원격에서 {path} 파일을 열 수 없습니다: {reason} · リモートで {path} を開けません: {reason} — .connection_lost: Connection lost before the remote answered. Reconnect and try again. · 원격이 응답하기 전에 연결이 끊겼습니다. 다시 연결한 뒤 시도하세요. · リモートの応答前に接続が切れました。再接続してからもう一度お試しください。 — .cancelled: Didn't open {path}: the remote connection changed. Try again. · 원격 연결이 바뀌어 {path} 파일을 열지 않았습니다. 다시 시도하세요. · リモート接続が変わったため {path} を開きませんでした。もう一度お試しください。 — .internal: Couldn't send the request to open {path}. Try again. · {path} 파일을 여는 요청을 보내지 못했습니다. 다시 시도하세요. · {path} を開く要求を送信できませんでした。もう一度お試しください。 — markdown.address.waiting: Still waiting for the remote… · 원격의 응답을 기다리는 중… · リモートの応答を待っています… — existing .error.not_found unchanged. Kind → key: NotFound → not_found · Rejected(reason) → rejected · Disconnected → connection_lost · Cancelled → cancelled · Internal → internal.

Narrow surface · long paths

Buttons never shrink. When the field would fall below 160 (md-address-go-hide-below), Go hides first; Enter still navigates. The field then shrinks to its 96 floor and ellipsises from the left, so the file name stays visible. Folder and Reload are never hidden: Folder is the only pointer route to the picker, and Reload carries the stale signal. The error row ellipsises at its end; the full message is its tooltip.

Layout spec
hide order
Go (field < 160) → nothing else hides
floor
field 96 = md-address-field-min-width; below that the surface clips the band at its end (no wrap)
local min band
8 + 24 + 8 + 96 + 8 = 144 without Go
remote min band
8 + 24 + 4 + 24 + 8 + 96 + 8 = 172 without Go
Tokens used
md-address-go-hide-below— new · → field-width-md 160md-address-field-min-width— new · → html-address-field-min-width 96

Keep the tail of the path (the file name) visible: ellipsise the field from the left.

Drop the folder button to make room; hide Go, which Enter replaces.

HTML viewer

Markdown — heading hierarchy (CSS-interpolated, 2026-10-07)

Heading sizes are set by Tasty in CSS: heading_sizes_px() interpolates five steps from h1 = --tasty-font-size-prose-h1 (20, the only cap-exempt content size) down to h6 = body (13) and writes them as --md-h1…--md-h6. Body line-height is 1.6 and block spacing is CSS as well. Weight and colour come from the same token map as the previous spec.

Layout spec
ladder
CSS — 5-step interpolation 20 → 13 (--md-h1 … --md-h6)
h1
prose-h1 (20) · cap-exempt
h2 … h5
18.6 · 17.2 · 15.8 · 14.4
h6
body 13
weight · colour
600 · text-primary, every level
leading
CSS line-height 1.6
block spacing
CSS
small
11 · muted
Tokens used
font-size-prose-h1— Heading anchor (cap-exempt)text-primary— headings / strongtext-secondary— bodytext-muted— de-emphasis

Load-fail / empty / loading states (proposed): a peach-toned "Failed to load" instead of raw Error: text, a centered "This file is empty", and the Spinner for slow loads — all in the Explorer states pattern above. Markdown font gets its own Appearance › Markdown sub-tab. Size, leading and spacing are all Tasty's (CSS).

HTML viewer

HTML — webview chrome (4 states)

The html surface is thin chrome + a native OS WebView overlay — tasty does not theme the page pixels. What tasty paints is the tile skeleton before/instead of the overlay: boundary (region marker), placeholder (no URL), loading (Spinner), error (danger). Same tile (--tasty-bg-panel + 1px border), centered content. The address bar band above these states is specified below (2026-10-12).

Layout spec
content
native OS WebView (token-agnostic)
tile
--tasty-bg-panel + 1px --tasty-border-default
boundary (2026-10-06)
plain tile only (bg-panel + 1px border) — shown only while an egui overlay hides a loaded page; no glyph, label or URL / path
placeholder
GLOBE (disabled) + 'No page loaded'
loading
Spinner + 'Loading…'
error
ALERT (danger) + URL
Tokens used
text-muted— placeholder / loading labeltext-disabled— placeholder / URLspinner-indicator— loadingaccent-danger— error

HTML — address bar band (2026-10-12)

The page is a native WebView that tasty cannot paint over, so the address bar is a host band above it and the WebView rect starts below the band (same inset idea as the script banner). Composition = the Explorer toolbar recipe: Back · Forward · Reload (IconButton sm) then the PathField box — without the Go button (Enter opens; the band already carries three buttons). Height 40 and --tasty-bg-sidebar + 1px separator match the markdown address bar. Local files show as a path (file:// input is accepted and shown as the path after open); web pages show the full URL. Loading swaps Reload for a Spinner in the same 24 cell. Input errors stand in a 24px row inside the band (danger edge on the field, glyph + message below); they clear on the next keystroke, on Esc, or on a successful open — never on a timer. Stack order under the band: size banner → script banner → page, banner-margin 8 above the first, banner-inset-gap 8 between and below.

Layout spec
band
40 · bg-sidebar · 1px separator below · padding-x 8
nav
Back · Forward · Reload — IconButton sm 24, gap 4 · disabled at history ends
field
PathField box (input tokens, 28, mono) · no Go · leading glyph file / link / html / remote
address
local = path (~ for home) · web = full URL · typed and shown the same
loading
Spinner in the Reload cell, not clickable
error
danger edge + 24 row in the band · clears on keystroke / Esc / successful open
stack
band → size banner → script banner → page · 8 / 8 / 8
narrow
< banner-narrow-below 440 → Forward hidden · field floor 96
mirror
read-only field + Reload only · band hidden while mirror HTML is a placeholder
tooltips
top — never down into the WebView
empty
keep glyph + 'No page loaded' · add hint + Alt+L / ⌘L
Tokens used
html-address-bar-height— → size-40html-address-bar-bg— → bg-sidebarhtml-address-bar-border— → separatorhtml-address-bar-padding-x— → space-smhtml-address-bar-gap— → space-smhtml-address-bar-nav-gap— → space-xshtml-address-field-min-width— → size-96html-address-error-height— → size-24html-address-error-glyph— → accent-dangerinput-border-invalid— field edge on error

An agent changing the address while the user edits does not touch the typed text or focus, and gets no extra marker — Esc reverts to whatever the current address is by then.

Address field ink — idle and read-only real values (2026-10-21)

The idle address and the mirror's read-only address are the real value the user reads, selects and copies, so they need body-text contrast (4.5:1). Both were text-secondary on the field ground (input-bg / input-readonly-bg → surface-raised): 7.10 in Mocha but 4.05 in Latte. They now use text-secondary-raised, the existing role for secondary ink on surface-raised (Mocha = text-secondary, Latte = neutral-1100). Mocha does not change. In Latte the idle and editing values share one ink; editing is still told apart by the focus edge, the ring and the caret. Placeholder, disabled, invalid edge and error row keep their own roles.

Layout spec
idle value
html-address-value-fg → text-secondary-raised · Mocha 7.10 · Latte 5.17 (was 4.05)
read-only value (mirror)
input-readonly-fg → text-secondary-raised · same numbers · no opacity, stays selectable / copyable
editing value
input-fg → text-primary · Mocha 8.69 · Latte 5.17 · unchanged
placeholder
input-placeholder → text-placeholder · unchanged
disabled
state-disabled-fg · unchanged (disabled is a different state from read-only)
invalid / error row
input-border-invalid edge · alertCircle html-address-error-glyph · caption text-primary · unchanged
box / geometry
band 40 · field 28 · min 96 · padding 8 · nav gap 4 · error row 24 · Forward hidden < 440 — all unchanged
Tokens used
html-address-value-fg— new · → text-secondary-raisedinput-readonly-fg— changed · text-secondary → text-secondary-raisedmd-address-value-fg— new · → html-address-value-fg (same box in the Markdown band)

Scope. input-readonly-fg is changed for every read-only Input, because the read-only box is surface-raised everywhere and the same 4.05 applies to all of them. Mocha is identical, so only Latte moves (n1000 → n1100). Consumers: Input readOnly (Appearance colour rows at Default; Explorer remote rename while waiting), fp-folder-path-fg (file picker folder mode path readout), the HTML mirror address field. Input ground="raised" read-only sits on state-disabled-fill-raised (one step lower than surface-raised), where n1100 reads higher still. Disabled ink, placeholder and the editing ink are not touched. The Markdown band's idle path had the same text-secondary on the same box; it now uses md-address-value-fg. No new strings.

HTML mirror — remote size banners and the receiving state (2026-10-12)

A mirror receives the entry document first, then each resource the page asks for. One file over 100 KB raises the large-file banner (name, size Tag, Decline / Allow); several waiting files are asked one at a time, with a “+N more waiting” count. Over 2 MB per entry document raises the limit banner (Stop / Continue) and auto receiving pauses. Both use the script banner's inset shell and button boxes, and sit above the script banner (they block rendering; the script banner is about the rendered document). Neither has × — each is a decision; neither takes keyboard focus. While a banner waits the page rect shows Receiving remote files… with a static glyph and “Paused”; while receiving runs it shows a Spinner with bytes · files received, so it reads differently from the local Loading state. Declined files leave a “N skipped” Tag in the read-only field (tooltip lists them) until Reload.

Layout spec
placement
inset, same as the script banner · above it when both show
large file
download glyph (info) · title · path (left-ellipsis) + size Tag warning · Decline ghost / Allow secondary
several files
one at a time · '+N more waiting'
limit
alertTriangle (warning) · received total · Stop ghost / Continue secondary
dismiss
no × · no keyboard focus · click only
receiving
Spinner + bytes · files (mono)
paused
static download glyph + 'Paused — answer the notice above'
declined
'N skipped' Tag in the read-only field, tooltip lists names · cleared by Reload
macOS 11–13
info glyph + copy in the page rect · band stays
disconnected
Reload disabled (tooltip says why) · page rect notice if nothing arrived; a received page stays
Tokens used
html-size-banner-glyph— → accent-infohtml-limit-banner-glyph— → accent-warninghtml-receiving-fg— → text-mutedhtml-receiving-glyph— → glyph-dimbanner-inset-gap— between banners + below the last

HTML — settings (Appearance › HTML viewer)

Layout spec
pattern
plugin settings page (label + control rows)
default zoom
number + % suffix
color scheme
select · follow / light / dark
allow remote
switch (off default)
sandbox scripts
switch (on default)
Tokens used
select-bg— scheme selectswitch-track-bg-on— enabled toggleinput-bg— zoom field

Reuses the existing plugin-settings row vocabulary (switch 28×16 · select · number) — no bespoke controls. Toolbar / address bar / find-in-page / retry stay open decisions (not in this pass).

Image viewer

Image — viewer & edit (paint) modes

The image surface is a single horizontal control bar over a canvas. Two modes toggle the bar: viewer (◀ ▶ ↻ ✎ + · filename (i/n) · right-aligned zoom group) and edit / paint (Save · Cancel · undo/redo · brush size · color · zoom). Canvas fills with the --tasty-bg-sidebar tone so the image floats; ≤100% fits to window, >100% pans on drag. ◀ ▶ show only when sibling images exist.

Layout spec
layout
control bar (40px) + canvas
viewer bar
◀ ▶ ↻ ✎ + · name (i/n) · zoom
canvas
--tasty-bg-sidebar · fit ≤100% · pan >100%
zoom group
Fit · + · % · − (right)
buttons
surface-raised + --tasty-border-default
Tokens used
bg-sidebar— canvas (mantle)surface-raised— control buttonstext-muted— filename · zoom %

Image — paint bar · floating selection · popups

Edit mode swaps in the paint bar (undo/redo with disabled state, brush-size slider, color swatch defaulting to --tasty-accent-danger). A pasted image becomes a floating selection — accent border + 8 resize handles — that you move/resize before committing. New Image and Save As are small popups.

Layout spec
paint bar
Save · Cancel · ↶ ↷ · brush · color · zoom
undo/redo
enabled / disabled (text-disabled)
floating sel
accent border + 8 handles · 6 · --tasty-image-handle-size (on-scale), centred on the edge
popup card
300 · pad 12 / 14 / 10 · title 14 semibold, 10 below · buttons gap 8 · --tasty-image-popup-*
size field
64 · --tasty-image-size-input-width
path row gap
6 · --tasty-image-path-row-gap
zoom %
mono 11 · min 40 · --tasty-image-zoom-font-size
commit
click outside = composite · Esc = cancel
New Image
Width × Height (1–8192)
Save As
path input + browse · PNG
Tokens used
accent-primary— floating selection + handlesaccent-danger— default brush colorsurface-active— brush slider trackshadow-modal— popups

Defaults (brief §6): metadata status-bar, filmstrip, async loading indicator, rotate/flip/crop tools, and transparency checkerboard are out of scope this pass — viewer + brush/paste paint only. The two canvas states (empty · load failed) are the next spec.

Image — empty canvas vs load failed · Save As name clash (2026-10-09)

The canvas reuses the explorer state screen (glyph · title · sub / OS reason · buttons, and its one-line compact form when the canvas is under 120) on the canvas tone --tasty-bg-sidebar. Empty (surface opened without a file) keeps the single muted line “No image loaded” and no buttons. Load failed is split by cause, each with its own title: missing (path gone or moved), permission (lock glyph, warning tone — the explorer's permission state), decode (damaged or unsupported; the decoder message goes on the mono reason line instead of only the log). Every failure has one button, Retry (secondary sm = the toolbar refresh). The file watcher still reloads by itself within 1 s, and ◀ ▶ in the bar keep working when the folder has other images. Save As opened because a .png of the same name exists gets a caption above the field in accent-warning and the field pre-filled with the next free name, stem selected.

Layout spec
states
image-empty · image-load-failed-missing · -permission · -decode · -too-large (b10)
gallery grid
3 per row (b10) — never squeeze five into one row
too large
image glyph · image-too-large-fg (→ accent-warning; refused by limit, the file is fine) · title text-primary · sub with the limits · reason = real size “{w} × {h} px” (or “needs {mem}”) · NO Retry (same file, same result)
units
binary — MiB (also the explorer preview: 256 MiB, and its byte limit “Over 1 MiB.” b11)
line breaks (b11)
number + unit joined by NBSP in every language (“512 MiB”, “16384 px”) · state title + sub use balanced wrap (lines of near-equal length — no 1–2 character tail; CSS text-wrap: balance) · same rule for the explorer preview state
layout
explorer state screen on bg-sidebar · compact under explorer-state-compact-below 120
empty
image glyph · “No image loaded” · muted · no button
missing / decode
alertTriangle · --tasty-image-error-fg (→ accent-danger) title
permission
lock · accent-warning (same as explorer)
reason
mono caption · muted · path (missing) or decoder message (decode)
button
Retry · secondary sm · = refresh
recovery
watcher reload ≤ 1 s → viewer; ◀ ▶ still step the folder
Save As clash
caption 11 · accent-warning · above the field · field = next free name (name-1.png), stem selected
Tokens used
image-error-fg— → accent-dangerimage-too-large-fg— → accent-warning (b10)accent-warning— permission · name clashbg-sidebar— canvasexplorer-state-compact-below— compact switch

Strings (en / ko / ja): image.state.missing “Image not found” / “이미지를 찾을 수 없습니다” / “画像が見つかりません” · sub “It may have been moved or deleted.” · image.state.permission “Permission denied” / “권한이 없습니다” / “アクセス権がありません” · image.state.decode “Can't open this image” / “이미지를 열 수 없습니다” / “この画像を開けません” · sub “The file is damaged or in an unsupported format.” · image.save_as.exists “{name} already exists. Save the PNG under another name.”

ko / ja confirmed (b10): missing_sub “이동했거나 삭제되었을 수 있습니다.” / “移動または削除された可能性があります。” · permission_sub “이 파일을 읽을 권한이 없습니다.” / “このファイルを読み取る権限がありません。” · decode_sub “파일이 손상되었거나 지원하지 않는 형식입니다.” / “ファイルが破損しているか、対応していない形式です。” · Retry “다시 시도” / “再試行” · save_as.exists “{name} 파일이 이미 있습니다. 다른 이름으로 PNG를 저장하세요.” / “{name} は既に存在します。別の名前で PNG を保存してください。” · too_large “이미지가 너무 커서 열 수 없습니다” / “画像が大きすぎて開けません” · too_large_sub “한 변이 {px} px를 넘거나 디코딩에 {mem}보다 많은 메모리가 필요합니다.” / “一辺が {px} px を超えるか、デコードに {mem} を超えるメモリが必要です。” · markdown large_deferred_open “파일 열기…” / “ファイルを開く…” (kept). Gallery stays en-only; ko/ja live in these notes and the app's lang files.