feat: W1 frontend — index.html, style.css, keybar, main.ts (esbuild entry)

- 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).
This commit is contained in:
Yaojia Wang
2026-06-16 18:36:32 +02:00
parent de8c366ab2
commit 326d3472dd
5 changed files with 547 additions and 0 deletions

79
public/keybar.ts Normal file
View File

@@ -0,0 +1,79 @@
/**
* 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)
}
}