|
| 1 | +import { useState } from '@lynx-js/react'; |
| 2 | +import type { BaseKeyEvent } from '@lynx-js/types'; |
| 3 | + |
| 4 | +import './keyboard.css'; |
| 5 | + |
| 6 | +type LynxKeyEvent = BaseKeyEvent<unknown> & { code?: string }; |
| 7 | + |
| 8 | +interface KeyDef { |
| 9 | + code: string; |
| 10 | + label: string; |
| 11 | + flex: number; |
| 12 | +} |
| 13 | + |
| 14 | +const ROW1: KeyDef[] = [ |
| 15 | + { code: 'Backquote', label: '`', flex: 1 }, |
| 16 | + { code: 'Digit1', label: '1', flex: 1 }, |
| 17 | + { code: 'Digit2', label: '2', flex: 1 }, |
| 18 | + { code: 'Digit3', label: '3', flex: 1 }, |
| 19 | + { code: 'Digit4', label: '4', flex: 1 }, |
| 20 | + { code: 'Digit5', label: '5', flex: 1 }, |
| 21 | + { code: 'Digit6', label: '6', flex: 1 }, |
| 22 | + { code: 'Digit7', label: '7', flex: 1 }, |
| 23 | + { code: 'Digit8', label: '8', flex: 1 }, |
| 24 | + { code: 'Digit9', label: '9', flex: 1 }, |
| 25 | + { code: 'Digit0', label: '0', flex: 1 }, |
| 26 | + { code: 'Minus', label: '-', flex: 1 }, |
| 27 | + { code: 'Equal', label: '=', flex: 1 }, |
| 28 | + { code: 'Backspace', label: '⌫', flex: 2 }, |
| 29 | +]; |
| 30 | + |
| 31 | +const ROW2: KeyDef[] = [ |
| 32 | + { code: 'Tab', label: 'Tab', flex: 1.5 }, |
| 33 | + { code: 'KeyQ', label: 'Q', flex: 1 }, |
| 34 | + { code: 'KeyW', label: 'W', flex: 1 }, |
| 35 | + { code: 'KeyE', label: 'E', flex: 1 }, |
| 36 | + { code: 'KeyR', label: 'R', flex: 1 }, |
| 37 | + { code: 'KeyT', label: 'T', flex: 1 }, |
| 38 | + { code: 'KeyY', label: 'Y', flex: 1 }, |
| 39 | + { code: 'KeyU', label: 'U', flex: 1 }, |
| 40 | + { code: 'KeyI', label: 'I', flex: 1 }, |
| 41 | + { code: 'KeyO', label: 'O', flex: 1 }, |
| 42 | + { code: 'KeyP', label: 'P', flex: 1 }, |
| 43 | + { code: 'BracketLeft', label: '[', flex: 1 }, |
| 44 | + { code: 'BracketRight', label: ']', flex: 1 }, |
| 45 | + { code: 'Backslash', label: '\\', flex: 1.5 }, |
| 46 | +]; |
| 47 | + |
| 48 | +const ROW3: KeyDef[] = [ |
| 49 | + { code: 'CapsLock', label: 'Caps', flex: 1.75 }, |
| 50 | + { code: 'KeyA', label: 'A', flex: 1 }, |
| 51 | + { code: 'KeyS', label: 'S', flex: 1 }, |
| 52 | + { code: 'KeyD', label: 'D', flex: 1 }, |
| 53 | + { code: 'KeyF', label: 'F', flex: 1 }, |
| 54 | + { code: 'KeyG', label: 'G', flex: 1 }, |
| 55 | + { code: 'KeyH', label: 'H', flex: 1 }, |
| 56 | + { code: 'KeyJ', label: 'J', flex: 1 }, |
| 57 | + { code: 'KeyK', label: 'K', flex: 1 }, |
| 58 | + { code: 'KeyL', label: 'L', flex: 1 }, |
| 59 | + { code: 'Semicolon', label: ';', flex: 1 }, |
| 60 | + { code: 'Quote', label: '\'', flex: 1 }, |
| 61 | + { code: 'Enter', label: '↵ Enter', flex: 2.25 }, |
| 62 | +]; |
| 63 | + |
| 64 | +const ROW4: KeyDef[] = [ |
| 65 | + { code: 'ShiftLeft', label: '⇧ Shift', flex: 2.25 }, |
| 66 | + { code: 'KeyZ', label: 'Z', flex: 1 }, |
| 67 | + { code: 'KeyX', label: 'X', flex: 1 }, |
| 68 | + { code: 'KeyC', label: 'C', flex: 1 }, |
| 69 | + { code: 'KeyV', label: 'V', flex: 1 }, |
| 70 | + { code: 'KeyB', label: 'B', flex: 1 }, |
| 71 | + { code: 'KeyN', label: 'N', flex: 1 }, |
| 72 | + { code: 'KeyM', label: 'M', flex: 1 }, |
| 73 | + { code: 'Comma', label: ',', flex: 1 }, |
| 74 | + { code: 'Period', label: '.', flex: 1 }, |
| 75 | + { code: 'Slash', label: '/', flex: 1 }, |
| 76 | + { code: 'ShiftRight', label: 'Shift ⇧', flex: 2.75 }, |
| 77 | +]; |
| 78 | + |
| 79 | +const ROW5: KeyDef[] = [ |
| 80 | + { code: 'ControlLeft', label: 'Ctrl', flex: 1.5 }, |
| 81 | + { code: 'MetaLeft', label: '⌘', flex: 1.25 }, |
| 82 | + { code: 'AltLeft', label: 'Alt', flex: 1.25 }, |
| 83 | + { code: 'Space', label: '', flex: 6 }, |
| 84 | + { code: 'AltRight', label: 'Alt', flex: 1.25 }, |
| 85 | + { code: 'MetaRight', label: '⌘', flex: 1.25 }, |
| 86 | + { code: 'ControlRight', label: 'Ctrl', flex: 1.5 }, |
| 87 | +]; |
| 88 | + |
| 89 | +const ROWS = [ROW1, ROW2, ROW3, ROW4, ROW5]; |
| 90 | + |
| 91 | +const MODIFIER_CODES = new Set([ |
| 92 | + 'ShiftLeft', |
| 93 | + 'ShiftRight', |
| 94 | + 'ControlLeft', |
| 95 | + 'ControlRight', |
| 96 | + 'AltLeft', |
| 97 | + 'AltRight', |
| 98 | + 'MetaLeft', |
| 99 | + 'MetaRight', |
| 100 | + 'CapsLock', |
| 101 | +]); |
| 102 | + |
| 103 | +const REGULAR_COLOR = '#2a2a2a'; |
| 104 | +const LIT_COLOR = '#39ff14'; |
| 105 | +const MOD_COLOR = '#00d4ff'; |
| 106 | + |
| 107 | +export function KeyboardDemo() { |
| 108 | + const [pressed, setPressed] = useState<ReadonlySet<string>>(new Set()); |
| 109 | + const [lastKey, setLastKey] = useState('—'); |
| 110 | + |
| 111 | + const handleKeyDown = (e: LynxKeyEvent) => { |
| 112 | + 'background-only'; |
| 113 | + const code = e.code ?? ''; |
| 114 | + setLastKey(e.key); |
| 115 | + setPressed(prev => new Set([...prev, code])); |
| 116 | + }; |
| 117 | + |
| 118 | + const handleKeyUp = (e: LynxKeyEvent) => { |
| 119 | + 'background-only'; |
| 120 | + const code = e.code ?? ''; |
| 121 | + setPressed(prev => { |
| 122 | + const next = new Set(prev); |
| 123 | + next.delete(code); |
| 124 | + return next; |
| 125 | + }); |
| 126 | + }; |
| 127 | + |
| 128 | + return ( |
| 129 | + <view |
| 130 | + className='kb-root' |
| 131 | + {...{ |
| 132 | + 'global-bindkeydown': handleKeyDown, |
| 133 | + 'global-bindkeyup': handleKeyUp, |
| 134 | + }} |
| 135 | + > |
| 136 | + {/* Header */} |
| 137 | + <view className='kb-header'> |
| 138 | + <text style={{ color: '#fff', fontSize: '22px', fontWeight: 'bold' }}> |
| 139 | + KEYBOARD EVENTS |
| 140 | + </text> |
| 141 | + <text style={{ color: '#555', fontSize: '13px' }}> |
| 142 | + Click here, then press keys |
| 143 | + </text> |
| 144 | + </view> |
| 145 | + |
| 146 | + {/* Last key */} |
| 147 | + <view className='kb-last-key-row'> |
| 148 | + <text style={{ color: '#555', fontSize: '13px', flex: 1 }}> |
| 149 | + Last key |
| 150 | + </text> |
| 151 | + <text |
| 152 | + style={{ |
| 153 | + color: pressed.size > 0 ? LIT_COLOR : '#444', |
| 154 | + fontSize: '26px', |
| 155 | + fontWeight: 'bold', |
| 156 | + }} |
| 157 | + > |
| 158 | + {lastKey} |
| 159 | + </text> |
| 160 | + </view> |
| 161 | + |
| 162 | + {/* Keyboard */} |
| 163 | + <view className='kb-board'> |
| 164 | + {ROWS.map((row, rowIdx) => ( |
| 165 | + <view key={rowIdx} className='kb-row'> |
| 166 | + {row.map((key) => { |
| 167 | + const isPressed = pressed.has(key.code); |
| 168 | + const isMod = MODIFIER_CODES.has(key.code); |
| 169 | + const bg = isPressed |
| 170 | + ? (isMod ? MOD_COLOR : LIT_COLOR) |
| 171 | + : REGULAR_COLOR; |
| 172 | + const labelColor = isPressed ? '#000' : '#bbb'; |
| 173 | + const isSpace = key.code === 'Space'; |
| 174 | + |
| 175 | + return ( |
| 176 | + <view |
| 177 | + key={key.code} |
| 178 | + className={isSpace ? 'kb-key kb-space' : 'kb-key'} |
| 179 | + style={{ flex: key.flex, backgroundColor: bg }} |
| 180 | + > |
| 181 | + {!isSpace && ( |
| 182 | + <text |
| 183 | + style={{ |
| 184 | + color: labelColor, |
| 185 | + fontSize: key.label.length > 3 ? '9px' : '12px', |
| 186 | + fontWeight: isPressed ? 'bold' : 'normal', |
| 187 | + textAlign: 'center', |
| 188 | + }} |
| 189 | + > |
| 190 | + {key.label} |
| 191 | + </text> |
| 192 | + )} |
| 193 | + </view> |
| 194 | + ); |
| 195 | + })} |
| 196 | + </view> |
| 197 | + ))} |
| 198 | + </view> |
| 199 | + |
| 200 | + {/* Legend */} |
| 201 | + <view className='kb-legend'> |
| 202 | + <view className='kb-legend-item'> |
| 203 | + <view |
| 204 | + style={{ |
| 205 | + width: '12px', |
| 206 | + height: '12px', |
| 207 | + backgroundColor: LIT_COLOR, |
| 208 | + borderRadius: '3px', |
| 209 | + marginRight: '6px', |
| 210 | + }} |
| 211 | + /> |
| 212 | + <text style={{ color: '#555', fontSize: '11px' }}>Key</text> |
| 213 | + </view> |
| 214 | + <view className='kb-legend-item'> |
| 215 | + <view |
| 216 | + style={{ |
| 217 | + width: '12px', |
| 218 | + height: '12px', |
| 219 | + backgroundColor: MOD_COLOR, |
| 220 | + borderRadius: '3px', |
| 221 | + marginRight: '6px', |
| 222 | + }} |
| 223 | + /> |
| 224 | + <text style={{ color: '#555', fontSize: '11px' }}>Modifier</text> |
| 225 | + </view> |
| 226 | + </view> |
| 227 | + </view> |
| 228 | + ); |
| 229 | +} |
0 commit comments