- T8 public/index.html — #term/#keybar, loads ./build/main.js (type=module)
- T9 public/style.css — fullscreen term, keybar fixed bottom, hidden >768px
- T10 public/keybar.ts — key→byte map + mountKeybar (13 tests)
- T11 public/main.ts — xterm+fit, WS client (wss on https/M6), reconnect
backoff, resize debounce, mountKeybar wiring
Verified by orchestrator: backend+frontend tsc clean; full suite 141 tests;
npm run build:web bundles main.ts -> public/build/main.js (430kb).
Note: main.ts uses one @ts-ignore for the CSS import (minor; future: *.css d.ts).
80 lines
2.2 KiB
TypeScript
80 lines
2.2 KiB
TypeScript
/**
|
|
* public/keybar.ts — Mobile touch key bar for terminal input (T10).
|
|
*
|
|
* Exports pure key-to-byte mappings and a mount function that binds DOM buttons
|
|
* to onSend callback on touchstart (bypassing xterm to avoid soft keyboard popup).
|
|
*
|
|
* Key bytes (ARCHITECTURE §5):
|
|
* Esc → \x1b (0x1B)
|
|
* Shift+Tab → \x1b[Z
|
|
* ↑ → \x1b[A
|
|
* ↓ → \x1b[B
|
|
* ← → \x1b[D
|
|
* → → \x1b[C
|
|
* Enter → \r (0x0D, not \n)
|
|
* Ctrl+C → \x03 (0x03)
|
|
* Tab → \t (0x09)
|
|
*/
|
|
|
|
import type { MountKeybar } from '../src/types.js'
|
|
|
|
/** Key name → byte string mapping (pure data, for testing). */
|
|
export const KEY_MAP = {
|
|
esc: '\x1b',
|
|
shiftTab: '\x1b[Z',
|
|
arrowUp: '\x1b[A',
|
|
arrowDown: '\x1b[B',
|
|
arrowLeft: '\x1b[D',
|
|
arrowRight: '\x1b[C',
|
|
enter: '\r',
|
|
ctrlC: '\x03',
|
|
tab: '\t',
|
|
} as const
|
|
|
|
export type KeyName = keyof typeof KEY_MAP
|
|
|
|
/** Mount the key bar: create buttons in #keybar, bind touchstart → onSend(bytes) + preventDefault. */
|
|
export const mountKeybar: MountKeybar = (onSend) => {
|
|
const keybarEl = document.getElementById('keybar')
|
|
if (!keybarEl) return
|
|
|
|
// Button labels and their key map entries
|
|
const buttons: Array<{
|
|
label: string
|
|
keyName: KeyName
|
|
}> = [
|
|
{ label: 'Esc', keyName: 'esc' },
|
|
{ label: 'Shift+Tab', keyName: 'shiftTab' },
|
|
{ label: '↑', keyName: 'arrowUp' },
|
|
{ label: '↓', keyName: 'arrowDown' },
|
|
{ label: '←', keyName: 'arrowLeft' },
|
|
{ label: '→', keyName: 'arrowRight' },
|
|
{ label: 'Enter', keyName: 'enter' },
|
|
{ label: 'Ctrl+C', keyName: 'ctrlC' },
|
|
{ label: 'Tab', keyName: 'tab' },
|
|
]
|
|
|
|
for (const { label, keyName } of buttons) {
|
|
const btn = document.createElement('button')
|
|
btn.textContent = label
|
|
btn.classList.add('keybar-btn')
|
|
btn.dataset.key = keyName
|
|
|
|
const bytes = KEY_MAP[keyName]
|
|
|
|
// touchstart: send immediately, prevent default to avoid soft keyboard
|
|
btn.addEventListener('touchstart', (e) => {
|
|
e.preventDefault()
|
|
onSend(bytes)
|
|
})
|
|
|
|
// For desktop testing/fallback: click also sends
|
|
btn.addEventListener('click', (e) => {
|
|
e.preventDefault()
|
|
onSend(bytes)
|
|
})
|
|
|
|
keybarEl.appendChild(btn)
|
|
}
|
|
}
|