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.
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):
- 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
#D35400as the accent identity. - 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 references — toolbar.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.monobuttons
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: buttonheight: 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#D35400on 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:
#303134bg,#3c4043border,#e8eaedtext - Hover:
#3a2a1abg,#ffftext - Active (mouse-down): full orange fill
#D35400on#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#D35400fill, hover lightens to#e26416.mono(chord keys): mono font,#9aa0a6text →#e8eaedon hover.grn(Save):#81c995text at rest;.grn.on(post-confirm) fills#1e8e3eand 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] [▲] [▼]
+ Tabis.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,Typeare.hi.Saveis.grn. TheTypebutton 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.
↵andSendboth 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
Typetoggles thecomposingclass on#mb. - On open,
#mb-composegets focused viasetTimeout(...,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 tappingSendor↵) callsflushCompose(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
composingclass (called fromtoggleType())
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.
Typebutton toggles a third row with input +↵+Send.- Pressing Enter inside the input sends the typed text plus
\rand 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
heightreflows when the bar grows/shrinks; nothing overlaps. - Autocorrect / swipe / predictions work in the compose input on Gboard and iOS.
- Save button shows
✓ Savedfilled green for 1500ms after tap. - Sel button toggles to
Done(filled orange) and dims the terminal slightly. - No console errors;
window._toolbarguards against double-init.