Files
Mortdecai b8a7810bfd chore: archive design handoff bundle for toolbar refresh
Stores SethMux_4-24-26.zip + extracted design_handoff_sethmux_toolbar/
so the spec, mockups, and reference jsx components stay in-repo for
future reference. Not served in production — the only file that ships
is static/toolbar.js, which already matches the design's toolbar.js
byte-for-byte.
2026-04-24 19:51:48 -04:00

10 KiB
Raw Permalink Blame History

Handoff: sethmux mobile toolbar — Google Workspace dark refresh + compose bar

Overview

This handoff covers two related changes to the sethmux web terminal's mobile UI (static/toolbar.js):

  1. Theme refresh — re-skin the existing 2-row mobile button bar to match the Google Workspace dark vocabulary (palette, typography, hairline borders, hover behavior) while keeping sethmux's orange #D35400 as the accent identity.
  2. Mobile autocorrect workaround — add an opt-in compose bar (third row, toggled by a "Type" button) that gives Gboard / iOS keyboards a real <input> to operate on. Typed text is flushed to the terminal stdin on Enter / Send so swipe, autocorrect, and predictions all work despite xterm.js's per-keystroke input model.

About the Design Files

The files in this bundle are design referencestoolbar.js is the single source file to ship; the preview*.html files are visual mockups demonstrating intended look and behavior in a Chrome window and an Android phone frame, and the *.jsx files are throwaway frame components used only by the previews.

The task is to drop the new toolbar.js into the existing sethmux project at static/toolbar.js, replacing the current file. No build step required; no other static files change. The previews are not meant to be served in production.

Fidelity

High-fidelity. All colors, type sizes, spacings, radii, and behaviors are final. Implement to spec.

What changed in toolbar.js

Visual system (Google Workspace dark)

Token Hex Usage
toolbar bg #202124 bar background
button surface #303134 button face at rest
border #3c4043 1px hairlines, group dividers
primary text #e8eaed button labels
secondary text #9aa0a6 mono-class buttons at rest
tertiary text #5f6368 placeholder, disabled
accent (sethmux) #D35400 active/toggled fill, send button, focus ring
accent at rest #f0a36b text color of .hi (orange-tinted) buttons
accent bg-1 #2a1f15 .hi button background at rest
accent bg-2 #3a2a1a hover wash (subtle orange tint)
accent border #5a3a22 .hi border at rest
success at rest #81c995 Save button text
success filled #1e8e3e Save button after confirm

Typography

  • Primary: Roboto, 'Helvetica Neue', Arial, sans-serif, 12px / 500, letter-spacing 0.1px
  • Mono (chord and arrow keys, terminal input): 'Roboto Mono', 'SF Mono', ui-monospace, Menlo, Consolas, monospace, 14px / 400 in the compose input, 12px / 400 on .mono buttons

Geometry

  • Bar padding: 6px 8px 7px
  • Button: height: 32px, min-width: 40px, padding: 0 10px, border-radius: 4px, border: 1px solid #3c4043
  • Narrow-phone breakpoint at max-width: 380px: button height: 30px, min-width: 34px, padding: 0 7px, font-size: 11.5px
  • Compose input: height: 36px, padding: 0 10px, border-radius: 4px, orange caret + orange focus border #D35400
  • Send button: height: 36px, min-width: 54px, filled #D35400 on text #0a0a0a
  • Newline button (): same height as Send, min-width: 38px, neutral surface (#303134 / #3c4043)
  • Group dividers (.sep): 1px wide × 20px tall, #3c4043, 4px horizontal margin (2px on narrow phones)
  • Bar shadow: 0 -1px 0 rgba(0,0,0,.4), 0 -8px 24px rgba(0,0,0,.35)
  • Transitions: background .15s ease, border-color .15s ease, color .15s ease (no bounces)

Button states

  • Default: #303134 bg, #3c4043 border, #e8eaed text
  • Hover: #3a2a1a bg, #fff text
  • Active (mouse-down): full orange fill #D35400 on #0a0a0a
  • .hi (accent at rest): orange-tinted bg #2a1f15, border #5a3a22, text #f0a36b. Hover deepens both
  • .on (toggled, e.g. selection mode active, Type active): full #D35400 fill, hover lightens to #e26416
  • .mono (chord keys): mono font, #9aa0a6 text → #e8eaed on hover
  • .grn (Save): #81c995 text at rest; .grn.on (post-confirm) fills #1e8e3e and shows "✓ Saved" for 1500ms

Layout / structure

The bar is a single position: fixed; bottom: 0 element with flex-direction: column.

Row 1 (always visible):

[+ Tab] [Next] [Prev]  |  [^C] [^D] [Clr]  |  [Esc] [Tab] [▲] [▼]
  • + Tab is .hi, the rest neutral, ^C ^D Esc Tab ▲ ▼ are .mono.

Row 2 (always visible):

[Sel] [Paste] [Zoom] [Save]  |  [V.Spl] [H.Spl] [Pane] [Kill]  |  [Type]
  • Sel, Paste, Type are .hi. Save is .grn. The Type button is the new entry point for the compose bar.

Row 3 — compose (only visible when #mb has class composing):

[          input field           ] [↵] [Send]
  • The input is full-width-flex, monospace, autocorrect on. and Send both flush.

Interactions / behavior

Send to stdin (send(k)): unchanged. Tries term._core.coreService.triggerDataEvent(k) first, falls back to term._core._onData.fire(k), then term.input(k). Always re-focuses the terminal afterward.

Selection mode (Sel): unchanged. Toggles body.selmode which adds pointer-events: none to .xterm-screen and forces text selection. While in selmode the body also gets filter: brightness(.92) for a subtle visual cue. Sending any key auto-exits selmode.

Paste: unchanged. Reads navigator.clipboard.readText() and feeds the result through send(). Alerts if HTTPS clipboard is unavailable.

Save: unchanged behavior — sends \x01S (Ctrl-A S, the tmux capture binding). Adds the .on class and label ✓ Saved for 1500ms, then reverts.

Compose / Type (new):

  • Tapping Type toggles the composing class on #mb.
  • On open, #mb-compose gets focused via setTimeout(...,0) so iOS/Android keyboards open reliably.
  • Input attributes: autocomplete="on" autocorrect="on" autocapitalize="sentences" spellcheck="true" enterkeyhint="send" inputmode="text". These are required — the whole point is to let the OS keyboard's correction layer mutate the input value, which xterm.js's hidden textarea cannot do.
  • Pressing Enter (or tapping Send or ) calls flushCompose(true): reads the input value, clears it, sends the value as a string to stdin, then sends \r. Re-focuses the input so the user can keep typing.
  • Other toolbar buttons remain active while composing. If a chord/arrow/etc. button is tapped with non-empty input text, the typed text is flushed first (preserving order), then the chord is sent. The compose bar stays open; the user is mid-thought.

Layout reflow (relayout()): The terminal pane (.xterm) is sized via height: calc(100vh - {barHeight + 4}px) whenever the bar's height changes. This runs on:

  • DOM mutations (existing MutationObserver)
  • Window resize
  • Toggling the composing class (called from toggleType())

The previous implementation hard-coded calc(100vh - 92px); the new code measures bar.offsetHeight so opening the third row reflows correctly without overlap.

Helper textarea hardening: unchanged. Still sets autocomplete=off, autocorrect=off, autocapitalize=off, spellcheck=false on .xterm-helper-textarea once it appears. This is the standard xterm.js mitigation and is still useful — it prevents Gboard from trying to munge the per-keystroke stream when the user happens to type outside the compose bar.

Mobile breakpoint

The toolbar shows at @media (max-width: 900px). Unchanged. Above that, sethmux assumes a real keyboard and the bar stays hidden.

Why the compose bar is necessary

xterm.js reads from a hidden <textarea> and forwards keydown / keypress events plus IME compositionstart / compositionend to the terminal's data event one keystroke at a time. Mobile keyboards' autocorrect, swipe input, and word predictions don't fire per-key events — they replace the textarea's .value in bulk via input events with inputType: "insertReplacementText" or "insertCompositionText". xterm.js's input handler discards those, so corrections silently fail. The compose bar sidesteps this by giving the OS keyboard a real text field, then sending the assembled string in one shot.

Files in this bundle

File Purpose
toolbar.js Ship this. Drop into static/toolbar.js.
preview.html Minimal mock terminal + the live toolbar.js for in-browser test
preview-desktop.html Chrome window mock showing the full sethmux UI with tmux tabs
preview-mobile.html Two Android frames: collapsed state + compose-open state
browser-window.jsx Used only by preview-desktop.html
android-frame.jsx Used only by preview-mobile.html

Acceptance checklist

  • Bar appears only at viewport widths ≤ 900px.
  • All 20 original buttons present and bound to the same key sequences.
  • Type button toggles a third row with input + + Send.
  • Pressing Enter inside the input sends the typed text plus \r and clears the field.
  • Tapping any other toolbar button while composing flushes the typed text first, then sends the button's key. Compose bar stays open.
  • Terminal pane height reflows when the bar grows/shrinks; nothing overlaps.
  • Autocorrect / swipe / predictions work in the compose input on Gboard and iOS.
  • Save button shows ✓ Saved filled green for 1500ms after tap.
  • Sel button toggles to Done (filled orange) and dims the terminal slightly.
  • No console errors; window._toolbar guards against double-init.