feat(maestro): mobile special-key bar for web attach (NAP-342)

On touch devices the soft keyboard omits the keys terminals depend on
(Esc, Tab, Ctrl, arrows, Home/End, PgUp/PgDn). Add a Termux-style
scrollable key bar to the attach view that injects the right xterm
sequences into the existing PTY input path.

- Ctrl is a one-shot sticky modifier: arm it and the next character —
  tapped here or typed on the soft keyboard — is sent as its control
  code, so Ctrl+C and friends work on mobile.
- Taps preventDefault on pointerdown so the terminal keeps focus and the
  keyboard stays open.
- Bar is revealed only on coarse-pointer devices; a physical keyboard
  already has these keys.
- Shell height now tracks the visual viewport (installViewportSizing) so
  the bar lands above the soft keyboard instead of behind it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 13:37:51 +00:00
co-authored by Claude Opus 4.8
parent a05966ed2f
commit 4b775ec7cb
4 changed files with 232 additions and 4 deletions
+70 -1
View File
@@ -209,7 +209,10 @@ body {
}
#app {
height: 100%;
/* `--app-vh` tracks the *visual* viewport (set by installViewportSizing) so
the shell stays above the mobile soft keyboard; falls back to the layout
viewport where `visualViewport` is unavailable. */
height: var(--app-vh, 100%);
display: flex;
flex-direction: column;
overflow: hidden;
@@ -1168,6 +1171,72 @@ textarea {
color: var(--danger);
}
/* ---------- mobile special-key bar (Termux-style) ---------- */
/* Hidden on devices with a precise pointer (a physical keyboard already has
these keys). Revealed on touch, where the soft keyboard omits them. The shell
tracks the visual viewport (see `#app`), so this row lands above the keyboard. */
.attach-keybar {
display: none;
}
@media (pointer: coarse) {
.attach-keybar {
display: flex;
flex: 0 0 auto;
align-items: stretch;
gap: var(--sp-1);
padding: var(--sp-1) var(--sp-2);
padding-bottom: calc(var(--sp-1) + env(safe-area-inset-bottom, 0px));
border-top: 1px solid var(--border);
background: var(--surface);
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-x: contain;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.attach-keybar::-webkit-scrollbar {
display: none;
}
}
.attach-keybar .keycap {
flex: 0 0 auto;
min-width: var(--tap);
height: var(--tap);
padding: 0 var(--sp-3);
display: inline-flex;
align-items: center;
justify-content: center;
font-family: var(--font-mono);
font-size: var(--fs-14);
color: var(--ink);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-1);
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
cursor: pointer;
transition:
background var(--dur-fast) var(--ease-out),
border-color var(--dur-fast) var(--ease-out);
}
.attach-keybar .keycap:active {
background: var(--surface-3);
border-color: var(--border-strong);
}
/* Armed sticky Ctrl: lit in signal-green until it folds into the next key. */
.attach-keybar .keycap-mod[aria-pressed="true"] {
color: var(--accent-ink);
background: var(--accent);
border-color: var(--accent-strong);
}
/* ---------- modal ---------- */
.modal-backdrop {