diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e12b9e..3449c19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: chart: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 # prerequisiteChecks off for lint alone: lint renders without a cluster and # cannot be told what one has, so the checks would report a missing # operator that the render steps below supply properly. @@ -102,15 +102,15 @@ jobs: IMAGE: ${{ needs.setup.outputs.backend_image }} TAG: ${{ needs.setup.outputs.candidate_tag }} steps: - - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: actions/checkout@v7 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push the backend candidate (runtime — what would ship) - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: ./src/backend file: ./src/backend/Containerfile @@ -124,7 +124,7 @@ jobs: # that group — same Python, same code — so the suite still runs inside the # artifact this run built rather than an environment assembled beside it. - name: Build and push the backend test stage - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: ./src/backend file: ./src/backend/Containerfile @@ -144,15 +144,15 @@ jobs: IMAGE: ${{ needs.setup.outputs.frontend_image }} TAG: ${{ needs.setup.outputs.candidate_tag }} steps: - - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: actions/checkout@v7 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push the frontend candidate (nginx, built bundle) - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: ./src/frontend file: ./src/frontend/Containerfile @@ -166,7 +166,7 @@ jobs: # pushed too and the two test jobs below run inside it — what is tested is # then what was actually installed, not a separately assembled environment. - name: Build and push the frontend test stage - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: ./src/frontend file: ./src/frontend/Containerfile @@ -200,7 +200,7 @@ jobs: --health-timeout 5s --health-retries 5 steps: - - uses: docker/login-action@v3 + - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -226,7 +226,7 @@ jobs: IMAGE: ${{ needs.setup.outputs.frontend_image }} TAG: ${{ needs.setup.outputs.candidate_tag }} steps: - - uses: docker/login-action@v3 + - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -244,7 +244,7 @@ jobs: IMAGE: ${{ needs.setup.outputs.frontend_image }} TAG: ${{ needs.setup.outputs.candidate_tag }} steps: - - uses: docker/login-action@v3 + - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -269,8 +269,8 @@ jobs: FRONTEND_IMAGE: ${{ needs.setup.outputs.frontend_image }} TAG: ${{ needs.setup.outputs.candidate_tag }} steps: - - uses: actions/checkout@v4 - - uses: docker/login-action@v3 + - uses: actions/checkout@v7 + - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -328,8 +328,8 @@ jobs: FRONTEND_IMAGE: ${{ needs.setup.outputs.frontend_image }} TAG: ${{ needs.setup.outputs.candidate_tag }} steps: - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} diff --git a/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/.openspec.yaml b/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/.openspec.yaml new file mode 100644 index 0000000..515eaae --- /dev/null +++ b/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-11 diff --git a/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/design.md b/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/design.md new file mode 100644 index 0000000..2a6e557 --- /dev/null +++ b/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/design.md @@ -0,0 +1,132 @@ +## Context + +See proposal.md - Why. The board and finger data live in two files: + +- `src/frontend/src/lib/keyboard/board.js` describes the physical ANSI board: + a `FINGERS` grid (one finger id per row position) and `FURNITURE` (Backspace, + Shift, AltGr, each with its own `finger`). Positions carry a finger id but + nothing marking any of them as a "home" position. +- `src/frontend/src/components/OnScreenKeyboard.vue` owns all rendering: the + `FINGER_TINT` map from finger id to Tailwind classes, the `LEGEND` array + shown under the board, and the `highlighted()`/`ACTIVE_CLASSES` logic for + the next-key ring. + +The home row is `FINGERS` row index 2 (`ASDF GHJKL;` on a Latin board, 11 +positions). Within it, column 3 is the left index finger's home key (F) and +column 6 is the right index finger's home key (J); columns 4 and 5 (G and H) +are also `l-index`/`r-index` but are the lateral reach, not the resting +position — a real keyboard puts no bump there either. This design marks +exactly columns 3 and 6 of row index 2. + +## Goals / Non-Goals + +**Goals:** +- Split `l-index`/`r-index` into two distinct colours in `FINGER_TINT` and + `LEGEND`, with no change to which finger is assigned to which position. +- Add a per-position "home" flag to the board data, and render it as a small, + static marker (a bar) on the two keys it applies to, visible independently + of hover/highlight state. +- Keep the next-key highlight (`ACTIVE_CLASSES`) legible when it lands on a + home-marked key, per the existing "highlight stays legible over the + colours" requirement extended to the home marker. + +**Non-Goals:** +- Not touching `l-pinky`/`r-pinky`, `l-ring`/`r-ring`, or `l-middle`/`r-middle` + — they keep mirroring, per the user's chosen scope. +- Not marking a home position for every finger (pinky/ring/middle) — only the + two index fingers get the marker, matching the physical F/J bump. +- Not changing `kedmanee.js` or `layout.js` — no layout, layer, or key-to- + character mapping changes; this is purely a board-data + rendering change. +- Not changing keyboard behaviour (pressing, highlighting logic, layout + switching) beyond the visual additions. + +## Decisions + +**Where the home flag lives: `board.js`, not the component.** +The finger-guidance requirement already establishes "the finger is a property +of the physical board." A home position is the same kind of fact — it depends +on which key a finger rests on, not on the layout or layer displayed — so it +belongs next to `FINGERS` in `board.js` rather than being hardcoded as +row/column literals inside the Vue component. Concretely, `FINGERS` entries +become `{ finger, home: true }` for the two marked positions (F and J's board +positions) and a bare finger id — or `{ finger }` — elsewhere; `keyboardModel` +(in `keyboard/index.js`, which turns board+layout into rendered cells) carries +the `home` flag through to each cell alongside `finger`, the way it already +carries `finger` through untouched. + +Alternative considered: keep `FINGERS` as a flat array of finger ids and add a +separate `HOME_POSITIONS` list of `[row, col]` pairs. Rejected because it +duplicates the row/column indexing `FINGERS` already does and separates two +facts about the same position, which is exactly the split the file's own +header comment (interleaving discouraged) warns against for finger data. + +**Index-finger colours: one shade of blue each, not two different hues.** +`sky` (currently shared by both index fingers) stays with the left hand; the +right gets a different shade of the same blue family (`blue-600`, deeper than +`sky-500`) rather than an unrelated hue such as lime or indigo. A shared hue +reads as "the two index fingers, told apart by shade" — which is what they +are, a mirrored pair like the other three — where two unrelated hues would +have read as two unrelated fingers, undoing the mirroring the other three +fingers still rely on. Pinky/ring/middle/thumb keep their own hues +unchanged, so no colour collides with them. + +Alternative considered: a fifth, unrelated hue (lime, indigo, cyan) for the +right index finger, maximizing hue distance from every other finger. Rejected +per explicit user preference — a hue that reads as "an unrelated finger" +undersells that the two index fingers are still one pair, just distinguished +by side rather than merged. + +**Home marker rendering: a short bar, not a dot, a border, or a background +change.** +The finger colour already occupies the key's background, and the next-key +highlight already occupies the ring/border. A third indicator needs a channel +that doesn't collide with either — but it also can't be a round dot: Kedmanee +already draws a dotted circle (`◌`) under a combining mark's label so the mark +has somewhere to sit (see `decorate` in `kedmanee.js`), and `่`, the *right* +index finger's own home key, is one of those marks. A round marker at the +bottom of that exact key reads as a second occurrence of a glyph already on +it, not as an unrelated indicator. A short horizontal bar avoids that +collision by shape alone — it also happens to be closer to what a physical +keyboard's F/J bump actually looks like, a ridge rather than a dot — sized as +a fraction of the key's own width so it scales between a normal keycap and +the narrower legend swatch. It is positioned at the bottom-center of the +keycap, rendered whenever the cell's `home` flag is true, in a neutral colour +(current text colour at reduced opacity) so it reads on top of any finger +tint and +under the highlight ring. + +**Legend additions are appended, not restructured.** +The existing `LEGEND` array (one entry per left-hand finger, relying on the +mirror) gains a second index-finger entry (so index appears twice, once per +colour) and one line explaining the home marker. The legend's existing +five-item layout grows to six items plus one marker note; no redesign of the +legend's layout beyond accommodating one more item. + +## Risks / Trade-offs + +- [Two shades of the same hue are closer together than two unrelated hues, + so the left/right index distinction could be harder to read at a glance, + especially for a colour-vision-deficient learner, than the lime/indigo + alternative would have been] → Mitigation: the shades are spaced further + apart than the tint/hover step already used elsewhere on the board + (`sky-500` vs `blue-600`, not two points on the same 500-scale), and the + aria-label and home marker carry the left/right distinction independently + of colour, so the colour is a reinforcement rather than the only signal. +- [A bar marker could be mistaken for a stray artifact or a third kind of + highlight] → Mitigation: the legend explicitly names it, per the new + "home marker is explained" scenario, and it renders identically regardless + of highlight/hover state so its meaning stays constant. +- [Marking only F/J and not the full home row could read as inconsistent once + a learner asks "why not my other fingers too?"] → Accepted trade-off per + the user's explicit choice; the proposal and spec scope this to the index + fingers only, matching the physical keyboard's own bumps. + +## Migration Plan + +Frontend-only, additive change to existing files; no data migration, no API +change, no backend involvement. Ship behind no flag — the change is purely +visual/additive and the existing Playwright/unit tests for the keyboard +(`__tests__/kedmanee.test.js`, `__tests__/layout.test.js`, +`e2e/practice.spec.js`) continue to exercise typing behaviour unchanged; new +tests cover the two colour and marker scenarios. Rollback is a plain revert +since no persisted state or schema is touched. diff --git a/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/proposal.md b/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/proposal.md new file mode 100644 index 0000000..f311070 --- /dev/null +++ b/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/proposal.md @@ -0,0 +1,47 @@ +## Why + +The on-screen keyboard's finger guidance mirrors colours across hands: both index +fingers show the same blue, both pinkies the same rose, and so on. A learner +glancing at a highlighted key can tell which finger presses it but not which +hand — for the index fingers this matters most, since they anchor the whole home +row on a physical keyboard. The board also has no equivalent of the tactile +bumps a physical keyboard puts on F and J, so nothing on screen tells a learner +where their fingers should rest between keystrokes. Both gaps make the guidance +read as "here is a key" rather than "here is where your hand lives." + +## What Changes + +- Give the left and right index fingers their own distinct colours instead of + sharing one hue, so a highlighted index-finger key also says which hand + reaches it. Pinky, ring, and middle fingers keep mirroring across hands as + today. +- Mark the home-row key of each index finger — F and J on a Latin board, their + Kedmanee equivalents on the Thai board — with a visible resting-position + indicator, the on-screen equivalent of the tactile bump on a physical + keyboard. +- Extend the legend so the two index-finger colours and the home-position + marker are each named, not just the mirrored colours already explained. + +## Capabilities + +### New Capabilities + +(none) + +### Modified Capabilities + +- `typing-practice`: the "Finger guidance on the keyboard" requirement changes + from a fully mirrored, unmarked colour scheme to one where the index fingers + carry distinct left/right colours and their home-row keys carry a resting- + position marker. + +## Impact + +- `src/frontend/src/lib/keyboard/board.js` — the physical board description + gains a way to mark a position as a finger's home key (currently only + position and finger are recorded). +- `src/frontend/src/components/OnScreenKeyboard.vue` — the finger-colour map + splits `l-index`/`r-index` into two colours, renders the home-position + marker, and extends the legend. +- No backend, API, or layout (`kedmanee.js`, `layout.js`) changes: the finger + assignments and key positions are unchanged, only how two of them are drawn. diff --git a/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/specs/typing-practice/spec.md b/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/specs/typing-practice/spec.md new file mode 100644 index 0000000..a4411c8 --- /dev/null +++ b/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/specs/typing-practice/spec.md @@ -0,0 +1,52 @@ +## MODIFIED Requirements + +### Requirement: Finger guidance on the keyboard +Key position alone does not tell a learner which finger to press a key with, so the on-screen keyboard SHALL show the touch-typing finger for every key, and SHALL explain what the indication means rather than relying on an unexplained visual code. The finger SHALL be a property of the physical board rather than of the layout printed on it, so every layout on that board guides the same finger to the same position. The two index fingers SHALL each carry their own colour, distinct from each other and from every other finger, so a highlighted index-finger key also says which hand reaches it; every other finger SHALL keep sharing one colour between its left and right instance. Each index finger's home-row key SHALL carry a visible resting-position marker, the on-screen equivalent of the tactile bump a physical keyboard puts on F and J, so a learner has an anchor to return to between keystrokes. + +#### Scenario: Keys are grouped by finger +- **WHEN** the keyboard is displayed +- **THEN** each key carries the colour of the finger that presses it, so the columns belonging to one finger are distinguishable from its neighbours + +#### Scenario: The index fingers are distinguishable by hand +- **WHEN** the keyboard is displayed +- **THEN** the left index finger's keys carry a different colour from the right index finger's keys, so the two are told apart without reading a label + +#### Scenario: Other fingers still mirror across hands +- **WHEN** the keyboard is displayed +- **THEN** the pinky, ring, and middle fingers each keep one colour shared between their left and right instance, so the palette does not grow beyond what is needed to tell the index fingers apart + +#### Scenario: The colours are explained +- **WHEN** the keyboard is displayed +- **THEN** a legend naming each finger accompanies it, including the two index-finger colours as distinct entries, so the colour code can be read without being guessed + +#### Scenario: A key's finger does not depend on the layer +- **WHEN** the displayed layer changes to any other layer of the layout +- **THEN** the key in a given position keeps the same finger, because the finger follows the position rather than the character printed on it + +#### Scenario: A key's finger does not depend on the layout +- **WHEN** two layouts on the same physical board are compared +- **THEN** a given position carries the same finger in both, because the finger belongs to the board + +#### Scenario: The modifiers are covered too +- **WHEN** the keyboard is displayed +- **THEN** every modifier the layout declares, along with Backspace and the space bar, carries a finger indication on the same scheme as the character keys + +#### Scenario: The next-key highlight stays legible over the colours +- **WHEN** a key is highlighted as the next expected character +- **THEN** the highlight remains distinguishable regardless of which finger colour that key carries + +#### Scenario: An index finger's home key is marked +- **WHEN** the keyboard is displayed +- **THEN** the key each index finger rests on when no other key is being reached — F and J on a Latin board, their Kedmanee equivalents on the Thai board — carries a visible marker distinct from the highlight used for the next expected character + +#### Scenario: The home marker does not depend on the layout or layer +- **WHEN** the displayed layout or layer changes +- **THEN** the home marker stays on the same position — the index fingers' home key on the physical board — because it marks a position, not a character + +#### Scenario: The home marker survives the next-key highlight +- **WHEN** the key carrying the home marker is also the next expected character +- **THEN** both the home marker and the highlight are visible on that key, so neither indication is lost to the other + +#### Scenario: The home marker is explained +- **WHEN** the keyboard is displayed +- **THEN** the legend states what the resting-position marker means, alongside the finger colours diff --git a/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/tasks.md b/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/tasks.md new file mode 100644 index 0000000..4a82cf5 --- /dev/null +++ b/openspec/changes/archive/2026-09-11-improve-keyboard-finger-guidance/tasks.md @@ -0,0 +1,62 @@ +## 1. Board data: mark the index fingers' home keys + +- [x] 1.1 In `board.js`, change the two `FINGERS` entries for the home row's F + and J positions (row index 2, columns 3 and 6) from bare finger ids to + `{ finger: 'l-index', home: true }` / `{ finger: 'r-index', home: true }`, + and normalize every other position in `FINGERS` to the same shape (a bare + finger id, or `{ finger }`) so the row-building code has one shape to read + — verify by inspecting the updated `FINGERS` grid and running the existing + `layout.test.js` suite, which reads `fingers` per row and must still pass + unmodified +- [x] 1.2 Update `layout.js`'s row-building code (around the `fingers[col]` + assignment) to read the normalized shape and attach both `finger` and + `home` to each character cell — verify with a unit test asserting the F + and J cells of the built home row carry `home: true` and every other + cell carries `home: false`/`undefined` + +## 2. Rendering: distinct index-finger colours + +- [x] 2.1 In `OnScreenKeyboard.vue`, split the `FINGER_TINT` map so `l-index` + and `r-index` resolve to two distinct Tailwind colour classes (keeping + `l-pinky`/`r-pinky`, `l-ring`/`r-ring`, `l-middle`/`r-middle` mirrored as + today), choosing a hue not already used by another finger and checked for + readable contrast against key text in both light and dark mode — verify + visually by loading the keyboard and confirming F-column and J-column + keys render in different colours +- [x] 2.2 Add a second index-finger entry to the `LEGEND` array (one swatch per + colour rather than one per mirrored pair) so both index-finger colours + are named — verify the legend renders six finger entries where it + previously rendered five + +## 3. Rendering: home-position marker + +- [x] 3.1 In `OnScreenKeyboard.vue`, render a small resting-position marker + (a centered bar, not a dot — a round dot reads as the dotted-circle + Kedmanee already draws under a combining mark's label, and ่ is one of + the two home keys) on any cell whose `home` flag is true, styled so it + stays visible over any finger tint and does not visually compete with the + `ACTIVE_CLASSES` next-key ring when both are present on the same key — + verify by triggering an exercise whose first character is on the F or J + home key and confirming both the marker and the highlight ring are + visible together +- [x] 3.2 Add a line to the legend area explaining what the marker means (e.g. + "rest position") — verify by reading the rendered legend + +## 4. Tests + +- [x] 4.1 Add/extend unit tests in `lib/keyboard/__tests__/layout.test.js` (or a + new test file) covering: the home flag lands on exactly the F and J + positions of every layer of every registered layout, and that flag is + unchanged when the modifier/layer changes — verify by running the + frontend test suite +- [x] 4.2 Extend `e2e/practice.spec.js` (or add a focused component test) to + assert the on-screen keyboard shows two visually distinct index-finger + colours and a home marker on the F/J-equivalent keys — verify the new + assertions pass in the existing test run + +## 5. Spec sync + +- [x] 5.1 Confirm the delta in + `openspec/changes/improve-keyboard-finger-guidance/specs/typing-practice/spec.md` + matches what was built (colours, marker, legend) before archiving — verify + with `openspec validate --change improve-keyboard-finger-guidance --strict` diff --git a/openspec/specs/typing-practice/spec.md b/openspec/specs/typing-practice/spec.md index 34e5dd7..3c901c6 100644 --- a/openspec/specs/typing-practice/spec.md +++ b/openspec/specs/typing-practice/spec.md @@ -181,15 +181,23 @@ Layouts SHALL be registered per language and selected by the exercise's language - **THEN** the typed answer shortens by one character and the next-key highlight follows it back, and the backspace key stops being highlighted as soon as the answer is a correct prefix again ### Requirement: Finger guidance on the keyboard -Key position alone does not tell a learner which finger to press a key with, so the on-screen keyboard SHALL show the touch-typing finger for every key, and SHALL explain what the indication means rather than relying on an unexplained visual code. The finger SHALL be a property of the physical board rather than of the layout printed on it, so every layout on that board guides the same finger to the same position. +Key position alone does not tell a learner which finger to press a key with, so the on-screen keyboard SHALL show the touch-typing finger for every key, and SHALL explain what the indication means rather than relying on an unexplained visual code. The finger SHALL be a property of the physical board rather than of the layout printed on it, so every layout on that board guides the same finger to the same position. The two index fingers SHALL each carry their own colour, distinct from each other and from every other finger, so a highlighted index-finger key also says which hand reaches it; every other finger SHALL keep sharing one colour between its left and right instance. Each index finger's home-row key SHALL carry a visible resting-position marker, the on-screen equivalent of the tactile bump a physical keyboard puts on F and J, so a learner has an anchor to return to between keystrokes. #### Scenario: Keys are grouped by finger - **WHEN** the keyboard is displayed - **THEN** each key carries the colour of the finger that presses it, so the columns belonging to one finger are distinguishable from its neighbours +#### Scenario: The index fingers are distinguishable by hand +- **WHEN** the keyboard is displayed +- **THEN** the left index finger's keys carry a different colour from the right index finger's keys, so the two are told apart without reading a label + +#### Scenario: Other fingers still mirror across hands +- **WHEN** the keyboard is displayed +- **THEN** the pinky, ring, and middle fingers each keep one colour shared between their left and right instance, so the palette does not grow beyond what is needed to tell the index fingers apart + #### Scenario: The colours are explained - **WHEN** the keyboard is displayed -- **THEN** a legend naming each finger accompanies it, so the colour code can be read without being guessed +- **THEN** a legend naming each finger accompanies it, including the two index-finger colours as distinct entries, so the colour code can be read without being guessed #### Scenario: A key's finger does not depend on the layer - **WHEN** the displayed layer changes to any other layer of the layout @@ -207,6 +215,22 @@ Key position alone does not tell a learner which finger to press a key with, so - **WHEN** a key is highlighted as the next expected character - **THEN** the highlight remains distinguishable regardless of which finger colour that key carries +#### Scenario: An index finger's home key is marked +- **WHEN** the keyboard is displayed +- **THEN** the key each index finger rests on when no other key is being reached — F and J on a Latin board, their Kedmanee equivalents on the Thai board — carries a visible marker distinct from the highlight used for the next expected character + +#### Scenario: The home marker does not depend on the layout or layer +- **WHEN** the displayed layout or layer changes +- **THEN** the home marker stays on the same position — the index fingers' home key on the physical board — because it marks a position, not a character + +#### Scenario: The home marker survives the next-key highlight +- **WHEN** the key carrying the home marker is also the next expected character +- **THEN** both the home marker and the highlight are visible on that key, so neither indication is lost to the other + +#### Scenario: The home marker is explained +- **WHEN** the keyboard is displayed +- **THEN** the legend states what the resting-position marker means, alongside the finger colours + ### Requirement: A layout describes one physical board A layout SHALL declare the physical board it sits on and one layer for each modifier state it offers. Every layer SHALL cover exactly that board's key positions, so a layer decides what a key types and never how many keys there are, where they sit, or which finger presses them. The number of layers SHALL be the layout's own — two where the language needs Shift alone, three where it also needs a layer reached with AltGr — and the keyboard SHALL render whatever the layout declares rather than assuming a count. diff --git a/src/frontend/e2e/practice.spec.js b/src/frontend/e2e/practice.spec.js index 5a077d9..356c4af 100644 --- a/src/frontend/e2e/practice.spec.js +++ b/src/frontend/e2e/practice.spec.js @@ -331,6 +331,50 @@ test.describe('the keyboard is one board', () => { }) }) +test.describe('finger guidance', () => { + // ด and เ are both left-index keys on the home row; ่ is the right-index + // home key. Their aria-labels start with the character, a comma, then the + // finger name and — for a home key — ", rest position". + const key = (page, char) => page.locator(`button[aria-label^="${char},"]`) + + test('the two index fingers are shown in different colours', async ({ page }) => { + await mockBackend(page) + await page.goto('/') + + const leftIndexTint = await key(page, 'ด').getAttribute('class') + const rightIndexTint = await key(page, '่').getAttribute('class') + + const backgroundClasses = (classAttr) => + classAttr.split(' ').filter((name) => name.startsWith('bg-')) + + expect(backgroundClasses(leftIndexTint)).not.toEqual(backgroundClasses(rightIndexTint)) + }) + + test('the two index fingers home keys are marked, and their other keys are not', async ({ + page, + }) => { + await mockBackend(page) + await page.goto('/') + + await expect(key(page, 'ด')).toHaveAttribute('aria-label', /rest position/) + await expect(key(page, '่')).toHaveAttribute('aria-label', /rest position/) + + // เ and ้ are the same two fingers' *other* home-row column — reached, not + // rested on — so they carry no marker. + await expect(key(page, 'เ')).not.toHaveAttribute('aria-label', /rest position/) + await expect(key(page, '้')).not.toHaveAttribute('aria-label', /rest position/) + }) + + test('the legend explains both the index colours and the marker', async ({ page }) => { + await mockBackend(page) + await page.goto('/') + + await expect(page.getByText('left index')).toBeVisible() + await expect(page.getByText('right index')).toBeVisible() + await expect(page.getByText('rest position')).toBeVisible() + }) +}) + /** The sentence with its last character swapped — same length, wrong answer. */ function wrongAnswerFor(sentence) { const chars = [...sentence] diff --git a/src/frontend/src/components/OnScreenKeyboard.vue b/src/frontend/src/components/OnScreenKeyboard.vue index fa54e8d..d6d93bd 100644 --- a/src/frontend/src/components/OnScreenKeyboard.vue +++ b/src/frontend/src/components/OnScreenKeyboard.vue @@ -75,37 +75,63 @@ watch( { immediate: true }, ) -const KEY_CLASSES = 'h-12 rounded-lg text-lg transition-colors' +const KEY_CLASSES = 'relative h-12 rounded-lg text-lg transition-colors' + +// The on-screen equivalent of the tactile bump a physical keyboard puts on F +// and J: a short bar on the key an index finger rests on when it isn't +// reaching anywhere, so a learner has an anchor to return to. A bar, not a +// dot: a round dot reads as the dotted circle Kedmanee already draws under a +// combining mark's label (◌ + the mark itself, in kedmanee.js) — and ่, the +// right index finger's own home key, is one of those marks, so a dot there +// would be mistaken for the character rather than an indicator about it. The +// bar sits on its own visual channel regardless — a shape at the bottom of +// the key rather than a colour or a border — so it survives being drawn +// under the next-key highlight ring and over any finger tint. Sized as a +// fraction of the key's own width rather than a fixed number of pixels, so it +// scales sensibly on both a normal key and the narrower legend swatch. +const HOME_MARKER_CLASSES = + 'pointer-events-none absolute inset-x-0 bottom-1.5 mx-auto h-1 w-[45%] rounded-full bg-black/50 dark:bg-white/60' // A colour per finger, so the column a key belongs to is readable at a glance: -// position alone does not say which finger reaches it. The two hands mirror -// each other — one hue per finger rather than eight separate hues — because -// four colours are told apart reliably and eight are not, and which hand is -// never in doubt once the board is split down the middle. +// position alone does not say which finger reaches it. Pinky, ring, and +// middle still mirror across hands — one hue for both — because four colours +// are told apart reliably and eight are not. The index fingers are the +// exception: they anchor the home row, so knowing *which* index finger a key +// belongs to matters more than it does for the others. Rather than a hue of +// its own, the right index finger gets a different shade of the same blue — +// close enough to read as "the other index finger" rather than an unrelated +// finger, distinct enough to tell the two apart. const FINGER_TINT = { 'l-pinky': 'bg-rose-500/15 hover:bg-rose-500/30 dark:bg-rose-400/20 dark:hover:bg-rose-400/35', 'l-ring': 'bg-amber-500/15 hover:bg-amber-500/30 dark:bg-amber-400/20 dark:hover:bg-amber-400/35', 'l-middle': 'bg-emerald-500/15 hover:bg-emerald-500/30 dark:bg-emerald-400/20 dark:hover:bg-emerald-400/35', 'l-index': 'bg-sky-500/15 hover:bg-sky-500/30 dark:bg-sky-400/20 dark:hover:bg-sky-400/35', - 'r-index': 'bg-sky-500/15 hover:bg-sky-500/30 dark:bg-sky-400/20 dark:hover:bg-sky-400/35', + 'r-index': 'bg-blue-600/15 hover:bg-blue-600/30 dark:bg-blue-400/25 dark:hover:bg-blue-400/40', 'r-middle': 'bg-emerald-500/15 hover:bg-emerald-500/30 dark:bg-emerald-400/20 dark:hover:bg-emerald-400/35', 'r-ring': 'bg-amber-500/15 hover:bg-amber-500/30 dark:bg-amber-400/20 dark:hover:bg-amber-400/35', 'r-pinky': 'bg-rose-500/15 hover:bg-rose-500/30 dark:bg-rose-400/20 dark:hover:bg-rose-400/35', thumb: 'bg-violet-500/15 hover:bg-violet-500/30 dark:bg-violet-400/20 dark:hover:bg-violet-400/35', } -// One entry per finger of one hand: the mirror makes a second set redundant. +// One entry per finger, except the mirrored trio (pinky/ring/middle) which +// still cover both hands with one swatch each. The index fingers get one +// entry per hand, since they are the pair this keyboard tells apart by colour. const LEGEND = [ { finger: 'l-pinky', name: 'little', swatch: 'bg-rose-500/40' }, { finger: 'l-ring', name: 'ring', swatch: 'bg-amber-500/40' }, { finger: 'l-middle', name: 'middle', swatch: 'bg-emerald-500/40' }, - { finger: 'l-index', name: 'index', swatch: 'bg-sky-500/40' }, + { finger: 'l-index', name: 'left index', swatch: 'bg-sky-500/40' }, + { finger: 'r-index', name: 'right index', swatch: 'bg-blue-600/40' }, { finger: 'thumb', name: 'thumb', swatch: 'bg-violet-500/40' }, ] const tint = (finger) => FINGER_TINT[finger] ?? '' const fingerName = (finger) => FINGER_NAMES[finger] ?? '' +// Read out alongside the finger name, so the marker means the same thing to a +// screen reader that it does visually. +const homeSuffix = (cell) => (cell.home ? ', rest position' : '') + // Now that the key backgrounds carry finger colour, an indigo highlight would // be one more hue competing with them. Maximum contrast against every tint, // in the page's own black and white, keeps the next key unmistakable. @@ -172,6 +198,12 @@ const SWITCH_WIDTH = 'w-14' :key="index" :class="['flex w-full gap-1', row.indent]" > + + >{{ cell.label }}