HexKeyboard: refactor to row-based flex layout

Replace 39 per-key absolute positioning CSS rules with 4 row divs
using flexbox. Rows overlap via negative margin-top to preserve
honeycomb staggering. Horizontal offsets via 4 margin-left rules.

- Add RowKeyDef with globalIndex for row-based rendering
- Add rows computed to group layoutKeys into 4 arrays
- Remove all .hex-key-N { top; left } positioning rules
- Keys are now flex children, position: relative, fixed width

Also includes prior changes:
- Row-based greyscale gradients (lighter top, darker home row)
- Per-key re-triggerable press animation (DOM reflow reset)
- Green-to-white exponential fade on keypress
- Gamepad repeat: 90ms horizontal/buttons, 140ms vertical
This commit is contained in:
2026-05-26 16:37:12 +00:00
parent 43ca8ea1d1
commit cf2b90809a
+46 -58
View File
@@ -3,18 +3,24 @@
<Transition name="hex-keyboard-fade"> <Transition name="hex-keyboard-fade">
<div v-if="visible" ref="keyboardRef" class="hex-keyboard" @click.stop @keydown="handleKeyDown"> <div v-if="visible" ref="keyboardRef" class="hex-keyboard" @click.stop @keydown="handleKeyDown">
<div ref="gridRef" class="hex-keyboard-grid"> <div ref="gridRef" class="hex-keyboard-grid">
<template v-for="(key, index) in layoutKeys" :key="key.id"> <div
v-for="(row, rowIndex) in rows"
:key="rowIndex"
class="hex-keyboard-row"
:class="`hex-keyboard-row-${rowIndex}`"
>
<button <button
:ref="(el: unknown) => setKeyRef(el as HTMLElement | null, index)" v-for="key in row"
:key="key.id"
:ref="(el: unknown) => setKeyRef(el as HTMLElement | null, key.globalIndex)"
class="hex-key" class="hex-key"
:class="[ :class="[
`hex-key-${index}`, `hex-key-row-${getCoord(key.globalIndex).row}`,
`hex-key-row-${getCoord(index).row}`,
{ {
'hex-key-blue': key.bg === 'blue', 'hex-key-blue': key.bg === 'blue',
'hex-key-yellow': key.bg === 'yellow', 'hex-key-yellow': key.bg === 'yellow',
'hex-key-red': key.bg === 'red', 'hex-key-red': key.bg === 'red',
'hex-key-pressed': isPressed(index), 'hex-key-pressed': isPressed(key.globalIndex),
}, },
]" ]"
tabindex="-1" tabindex="-1"
@@ -22,7 +28,7 @@
> >
<span class="hex-key-label" :class="{ 'hex-key-label-large': key.id === 'sp' }">{{ key.label }}</span> <span class="hex-key-label" :class="{ 'hex-key-label-large': key.id === 'sp' }">{{ key.label }}</span>
</button> </button>
</template> </div>
<!-- Green focus outline rendered separately on top --> <!-- Green focus outline rendered separately on top -->
<svg <svg
v-if="focusedIndex !== null" v-if="focusedIndex !== null"
@@ -50,6 +56,10 @@ interface KeyDef {
bg?: "blue" | "yellow" | "red" bg?: "blue" | "yellow" | "red"
} }
interface RowKeyDef extends KeyDef {
globalIndex: number
}
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
modelValue: string modelValue: string
@@ -75,6 +85,16 @@ function isPressed(index: number): boolean {
return Date.now() - ts < 1000 return Date.now() - ts < 1000
} }
const rows = computed<RowKeyDef[][]>(() => {
const result: RowKeyDef[][] = [[], [], [], []]
for (let i = 0; i < layoutKeys.length; i++) {
const key = layoutKeys[i]
const coord = getCoord(i)
result[coord.row].push({ ...key, globalIndex: i })
}
return result
})
// Four-row layout with honeycomb staggering // Four-row layout with honeycomb staggering
// Each row shifted 0.5 cell left relative to the one below: // Each row shifted 0.5 cell left relative to the one below:
// Row 0 (numbers): shift 0 // Row 0 (numbers): shift 0
@@ -470,16 +490,32 @@ onUnmounted(() => {
.hex-keyboard-grid { .hex-keyboard-grid {
position: relative; position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
--key-h: 56px; --key-h: 56px;
--key-w: calc(0.866 * var(--key-h)); --key-w: calc(0.866 * var(--key-h));
width: calc(var(--key-w) * 12.5);
height: calc(var(--key-h) * 3.25);
} }
.hex-keyboard-row {
display: flex;
}
/* Overlap: each row overlaps the previous by 0.25 * key-h */
.hex-keyboard-row:not(:first-child) {
margin-top: calc(var(--key-h) * -0.25);
}
/* Row horizontal offsets for honeycomb staggering */
.hex-keyboard-row-0 { margin-left: 0; }
.hex-keyboard-row-1 { margin-left: calc(var(--key-w) * 0.5); }
.hex-keyboard-row-2 { margin-left: calc(var(--key-w) * 1.0); }
.hex-keyboard-row-3 { margin-left: calc(var(--key-w) * 1.5); }
.hex-key { .hex-key {
position: absolute; position: relative;
height: var(--key-h); height: var(--key-h);
aspect-ratio: 0.866 / 1; width: var(--key-w);
border: none; border: none;
color: #ffffff; color: #ffffff;
font-size: 0.95rem; font-size: 0.95rem;
@@ -565,54 +601,6 @@ onUnmounted(() => {
50% { opacity: 0.4; } 50% { opacity: 0.4; }
} }
/* Row positioning */
/* Row 0 (numbers + BS) — shift 0 */
.hex-key-0 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 0.0); }
.hex-key-1 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 1.0); }
.hex-key-2 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 2.0); }
.hex-key-3 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 3.0); }
.hex-key-4 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 4.0); }
.hex-key-5 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 5.0); }
.hex-key-6 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 6.0); }
.hex-key-7 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 7.0); }
.hex-key-8 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 8.0); }
.hex-key-9 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 9.0); }
.hex-key-10 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 10.0); }
/* Row 1 (QWERTY) — shift 0.5 */
.hex-key-11 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 0.5); }
.hex-key-12 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 1.5); }
.hex-key-13 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 2.5); }
.hex-key-14 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 3.5); }
.hex-key-15 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 4.5); }
.hex-key-16 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 5.5); }
.hex-key-17 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 6.5); }
.hex-key-18 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 7.5); }
.hex-key-19 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 8.5); }
.hex-key-20 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 9.5); }
/* Row 2 (ASDF) — shift 1.0 */
.hex-key-21 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 1.0); }
.hex-key-22 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 2.0); }
.hex-key-23 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 3.0); }
.hex-key-24 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 4.0); }
.hex-key-25 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 5.0); }
.hex-key-26 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 6.0); }
.hex-key-27 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 7.0); }
.hex-key-28 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 8.0); }
.hex-key-29 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 9.0); }
/* Row 3 (ZXCV + Space + Close) — shift 1.5 */
.hex-key-30 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 1.5); }
.hex-key-31 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 2.5); }
.hex-key-32 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 3.5); }
.hex-key-33 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 4.5); }
.hex-key-34 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 5.5); }
.hex-key-35 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 6.5); }
.hex-key-36 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 7.5); }
.hex-key-37 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 8.5); }
.hex-key-38 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 9.5); }
/* Transition */ /* Transition */
.hex-keyboard-fade-enter-active, .hex-keyboard-fade-enter-active,
.hex-keyboard-fade-leave-active { .hex-keyboard-fade-leave-active {