import { LitElement, html } from 'https://unpkg.com/lit@2.7.5/index.js?module'; /** * * A compact on-screen keyboard for mobile devices. Emits 'key-press' events on window * with detail { type: 'letter'|'navigate'|'backspace', value } */ export class MobileKeyboard extends LitElement { static properties = { // reflect so CSS attribute selectors can react to collapsed state collapsed: { type: Boolean, reflect: true }, _isMobile: { type: Boolean, state: true }, _wideScreen: { type: Boolean, state: true }, }; // styles moved to webui/styles.css; render into light DOM so external CSS applies constructor() { super(); this.collapsed = true; // default collapsed; _onResize will flip for mobile this._isMobile = false; this._letters = 'abcdefghijklmnopqrstuvwxyz'.split(''); this._onResize = this._onResize.bind(this); } createRenderRoot() { return this; } render() { // simple QWERTY-like rows const rows = [ 'qwertyuiop'.split(''), 'asdfghjkl'.split(''), 'zxcvbnm'.split(''), ]; // compute the maximum number of columns across rows (account for backspace in second row now) const counts = rows.map((r, idx) => r.length + (idx === 1 ? 1 : 0)); const arrowCols = 3; // reserve 3 columns on the right for [left][down][right] const baseMax = Math.max(...counts, 10); const maxCols = baseMax; return html`
${html`
${this.collapsed ? '▲' : '▼'}
`}
${rows.map((r, idx) => { // center the letter keys leaving the rightmost `arrowCols` for the arrow block let rowClasses = 'row'; if (idx === 1) rowClasses += ' stagger'; // A row if (idx === 2) rowClasses += ' stagger-deep'; // Z row needs a larger indent return html`
${r.map(l => html``) } ${idx === 1 ? html`` : ''}
${Array.from({ length: arrowCols }).map((_, i) => { if (idx === 2 && i === 1) return html``; return html`
`; })}
`; })}
`; } _emitLetter(l) { this._vibrate(); this._emit({ type: 'letter', value: l }); } _emitNavigate(dir) { this._vibrate(); this._emit({ type: 'navigate', value: dir }); } _emitBackspace() { this._vibrate(); this._emit({ type: 'backspace' }); } _emitSpace() { this._vibrate(); this._emit({ type: 'letter', value: '' }); } _emit(detail) { window.dispatchEvent(new CustomEvent('key-press', { detail })); } _vibrate() { // Use Vibration API for haptic feedback try { console.log('Attempting vibration... navigator.vibrate:', typeof navigator.vibrate); if (navigator.vibrate) { navigator.vibrate(10); // 10ms short buzz console.log('Vibration sent!'); } else { console.log('Vibration API not available on this device'); } } catch (e) { console.warn('Vibration API error:', e); } } connectedCallback() { super.connectedCallback(); window.addEventListener('resize', this._onResize); this._onResize(); } disconnectedCallback() { super.disconnectedCallback(); window.removeEventListener('resize', this._onResize); } _onResize() { const mobile = window.innerWidth <= 900; this._isMobile = mobile; this.classList.toggle('mobile', mobile); this.classList.toggle('desktop', !mobile); // decide wide-screen (landscape/tablet) to change layout behavior const wide = (window.innerWidth / window.innerHeight) > 1.6; this._wideScreen = wide; this.classList.toggle('wide-screen', wide); // collapsed default: expanded on mobile, collapsed on desktop if (mobile) this.collapsed = false; else this.collapsed = true; } _toggleCollapse() { this.collapsed = !this.collapsed; if (this.collapsed) this.setAttribute('collapsed', ''); else this.removeAttribute('collapsed'); } } customElements.define('mobile-keyboard', MobileKeyboard);