From 78d31e2c2318e2f4aaa0663ac78517bf5c5cf30d Mon Sep 17 00:00:00 2001 From: Egor Poderiagin Date: Fri, 11 Sep 2026 23:38:23 +0700 Subject: [PATCH] feat: added settings and mobile version --- .../.openspec.yaml | 2 + .../design.md | 150 ++++++++++++++++++ .../proposal.md | 60 +++++++ .../specs/typing-practice/spec.md | 113 +++++++++++++ .../tasks.md | 103 ++++++++++++ .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/local-dev-environment/spec.md | 0 .../specs/production-deployment/spec.md | 0 .../tasks.md | 2 +- openspec/specs/local-dev-environment/spec.md | 73 ++++++++- openspec/specs/production-deployment/spec.md | 47 ++++++ openspec/specs/typing-practice/spec.md | 112 +++++++++++++ src/frontend/e2e/audio.spec.js | 40 +++++ src/frontend/e2e/practice.spec.js | 6 +- src/frontend/e2e/responsive-keyboard.spec.js | 34 ++++ src/frontend/e2e/virtual-keyboard.spec.js | 118 ++++++++++++++ src/frontend/src/App.vue | 8 +- src/frontend/src/components/AnswerInput.vue | 7 + src/frontend/src/components/AudioPlayer.vue | 18 ++- .../src/components/OnScreenKeyboard.vue | 116 ++++++++------ src/frontend/src/components/SentenceView.vue | 11 +- src/frontend/src/components/SettingsMenu.vue | 150 ++++++++++++++++++ src/frontend/src/lib/__tests__/device.test.js | 102 ++++++++++++ src/frontend/src/lib/device.js | 54 +++++++ .../src/stores/__tests__/settings.test.js | 142 +++++++++++++++++ src/frontend/src/stores/settings.js | 80 ++++++++++ 28 files changed, 1479 insertions(+), 69 deletions(-) create mode 100644 openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/.openspec.yaml create mode 100644 openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/design.md create mode 100644 openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/proposal.md create mode 100644 openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/specs/typing-practice/spec.md create mode 100644 openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/tasks.md rename openspec/changes/{serve-static-and-media => archive/2026-09-11-serve-static-and-media}/.openspec.yaml (100%) rename openspec/changes/{serve-static-and-media => archive/2026-09-11-serve-static-and-media}/design.md (100%) rename openspec/changes/{serve-static-and-media => archive/2026-09-11-serve-static-and-media}/proposal.md (100%) rename openspec/changes/{serve-static-and-media => archive/2026-09-11-serve-static-and-media}/specs/local-dev-environment/spec.md (100%) rename openspec/changes/{serve-static-and-media => archive/2026-09-11-serve-static-and-media}/specs/production-deployment/spec.md (100%) rename openspec/changes/{serve-static-and-media => archive/2026-09-11-serve-static-and-media}/tasks.md (98%) create mode 100644 src/frontend/e2e/responsive-keyboard.spec.js create mode 100644 src/frontend/e2e/virtual-keyboard.spec.js create mode 100644 src/frontend/src/components/SettingsMenu.vue create mode 100644 src/frontend/src/lib/__tests__/device.test.js create mode 100644 src/frontend/src/lib/device.js create mode 100644 src/frontend/src/stores/__tests__/settings.test.js create mode 100644 src/frontend/src/stores/settings.js diff --git a/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/.openspec.yaml b/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/.openspec.yaml new file mode 100644 index 0000000..515eaae --- /dev/null +++ b/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-11 diff --git a/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/design.md b/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/design.md new file mode 100644 index 0000000..6bee2fe --- /dev/null +++ b/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/design.md @@ -0,0 +1,150 @@ +## Context + +`AnswerInput.vue` holds a plain `` that the app auto-focuses on mount +and that the on-screen `OnScreenKeyboard.vue` writes into (see +`openspec/specs/typing-practice/spec.md` — "On-screen keyboard for the +exercise's language"). On a touchscreen, focusing any `` is the +browser's own trigger for the OS virtual keyboard; the app has never +suppressed it because the desktop/laptop case this was built for (see "The +exercise fits on one screen") has no virtual keyboard to suppress. There is no +device-classification, settings, or persistence code anywhere in the frontend +yet — this change introduces all three. + +## Goals / Non-Goals + +**Goals:** +- Keep the OS virtual keyboard off the answer field by default on phones. +- Leave it available by default everywhere else, since a connected physical + keyboard cannot be detected. +- Give the learner a persistent, explicit override in both directions. +- Keep the on-screen keyboard's own control reachable on a phone-sized + screen, and let the learner hide that board entirely. + +**Non-Goals:** +- General responsive layout for small screens beyond the keyboard board + itself (the "fits on one screen" requirement is still laptop-scoped; + reflowing the exercise column or the sentence's type scale for narrow + viewports is separate work). The keyboard board's own overflow is + in scope below — it turned out to block the override control's own + reachability during e2e verification, not just a cosmetic gap. +- Detecting a physical keyboard directly — no browser API exposes this, which + is precisely why an explicit override exists. +- A general settings panel/infrastructure beyond what these two settings + need — no accounts, no server-side sync, no third setting speculatively + added. + +## Decisions + +### Suppress via `inputmode="none"`, not `readonly` or a hidden proxy input +Setting the field's `inputmode` attribute to `"none"` tells the browser not to +draw its virtual keyboard on focus, while the field stays focused, editable, +and still receives physical key events — because `inputmode` only hints at +which virtual keyboard to show, it does not disable input. `readonly` was +considered and rejected: it also blocks physical keyboard input and caret +placement, which would break the "physical keyboard also works" scenario +already in the spec. A hidden/offscreen proxy input was considered and +rejected as needless complexity — `v-model`, `@keyup.enter`, and the visible +caret all keep working unchanged with `inputmode` alone. + +`inputmode` becomes a computed property on the input, driven by whichever of +the two settings states below is active. + +### Two independent pieces of state: device classification and override +1. **Device classification** (`phone` vs. not) is computed, not stored — a + small composable reads it live from `matchMedia`, so it reacts if the + viewport or input mode changes (e.g. rotating a phone, or a tablet + docking a keyboard, changes `hover`/`pointer` on some platforms). + Classification rule: `(max-width: 640px)` — Tailwind's own `sm` breakpoint, + already the project's small-screen line — **and** `(hover: none) and + (pointer: coarse)`, i.e. touch-primary with no mouse/trackpad. Both must + hold; a narrow desktop window with a mouse is not a phone. +2. **Override** is a tri-state value — `auto` (default), `on`, `off` — held in + a small Pinia store and mirrored to `localStorage` under one key, read + once at store creation and written on every change. `auto` means "follow + classification"; `on`/`off` force the `inputmode` regardless of it. + +Keeping these separate (rather than collapsing to one persisted boolean) +means a learner who never touches the setting keeps getting the +device-appropriate default even if they later switch devices with the same +browser profile — `localStorage` is per-origin-per-browser, not per-device, +so a forced boolean would follow them somewhere it no longer applies. + +### Settings control placement: a header menu, not the keyboard's bottom bar +Tried first as a pill in `OnScreenKeyboard.vue`'s existing bottom bar +(alongside the layout switch and finger legend). Phone-width e2e +verification (Playwright, `hasTouch`/`isMobile` emulation) caught what a +static mockup didn't: at 390px the bar is already tight, and the pill was +genuinely unclickable — other elements physically overlapped it. Rather than +fight that bar for space, both settings moved to a small popover menu off a +gear icon in the app header (`SettingsMenu.vue`), positioned +absolutely so it costs the header row no height on any viewport (`App.vue`'s +title row must stay short — see "The exercise fits on one screen"). This +also gave the second setting (below) a natural home instead of a second +crowded pill. + +### The on-screen keyboard scrolls sideways rather than shrinking +`OnScreenKeyboard.vue`'s panel was `w-[var(--board)]`, a fixed width; below +that width the whole page grew wider than the viewport instead of the panel +adapting. Shrinking every key to fit was rejected — a key below a tappable +size defeats the board's whole point, and there is no width where a +13-column row shrinks to fit a phone and stays tappable. Instead: the panel +is now `w-full max-w-[var(--board)]` (shrinks with the viewport, matching +`AnswerInput.vue`'s column), and inside it the rows + space bar sit in an +`overflow-x-auto` wrapper at their full, unshrunk width — so on any viewport +narrower than `--board` the keys stay true size and the learner scrolls +sideways to reach the rest, the way they would scroll any wide content on a +touch device. The bottom bar (legend + layout switch) sits outside that +scroll wrapper, so it never scrolls out of reach itself; the legend also +gained `flex-wrap` for narrow panels. + +### A second, independent setting: hide the on-screen keyboard +Once a learner is typing on a physical keyboard they already know work, the +board is a lot of screen for a hint they are not using — a plain "hide it" +option, not tied to the virtual-keyboard override (that one is about the +*OS* keyboard; this one is about *this app's own* board). Held as a second +field, `onScreenKeyboardVisible`, on the same settings store, defaulting to +shown and persisted the same way. `SentenceView.vue` wraps +`` in `v-if`, not `v-show`: the board holds no state worth +preserving while hidden (its layer and scroll position reset the same way a +new exercise already resets them), so mounting fresh on re-show costs +nothing and is simpler than tracking what to reset by hand. + +### Where the logic lives +- `src/frontend/src/lib/device.js`: the `matchMedia`-backed + phone-classification composable/helper, unit-testable by mocking + `matchMedia` the way the existing `lib/keyboard` tests mock their inputs. +- `src/frontend/src/stores/settings.js`: the Pinia store holding both + settings, their `localStorage` keys, and a computed `virtualKeyboardEnabled` + that combines the override + classification into the boolean + `AnswerInput.vue` needs. +- `AnswerInput.vue`: binds `:inputmode` to `store.virtualKeyboardEnabled ? + 'text' : 'none'`. +- `SettingsMenu.vue` (new): the header's gear button and its popover, reading + and writing both settings. +- `OnScreenKeyboard.vue`: the `overflow-x-auto` scroll wrapper; no longer + holds any settings-control markup itself. +- `SentenceView.vue`: `v-if`s the board on `onScreenKeyboardVisible`. + +## Risks / Trade-offs + +- **Classification is a heuristic and will misjudge some devices** (e.g. a + large phone above 640px, or a small tablet at 640px in portrait) → this is + exactly what the override exists for; the proposal treats the override as a + first-class requirement rather than an escape hatch, and the design keeps + the default in the "safer to leave the keyboard available" direction for + anything not confidently phone-sized. +- **`inputmode="none"` support varies slightly across mobile browsers** + (notably some older WebKit versions honored it inconsistently) → the + override lets an affected learner switch it back on if suppression fails to + behave, and lets one switch it off if a browser shows the virtual keyboard + despite `inputmode="none"`; no feature detection is added since there is + nothing reliable to detect. +- **`localStorage` can be unavailable** (private browsing in some browsers, + storage disabled) → reads/writes are wrapped so a failure falls back to the + in-memory default (`auto`) for that session rather than throwing. + +## Migration Plan + +Additive, frontend-only change with no persisted server data and no schema. +Ships as a normal frontend deploy; nothing to migrate or roll back beyond +reverting the commit if the heuristic proves wrong in practice. diff --git a/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/proposal.md b/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/proposal.md new file mode 100644 index 0000000..18469c8 --- /dev/null +++ b/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/proposal.md @@ -0,0 +1,60 @@ +## Why + +On a phone, focusing the answer field pops up the OS's virtual keyboard on top +of the app's own on-screen Kedmanee keyboard — two keyboards fighting for the +same screen, with the OS one usually covering the on-screen one entirely. A +phone has no physical keyboard, so the virtual keyboard adds nothing the +on-screen board doesn't already provide, and should stay down. A tablet is a +harder case: some are typed on with a connected physical keyboard, but many +are not, and the app cannot reliably detect a connected one — so it must not +assume a tablet is keyboard-less the way a phone can be assumed to be. Because +that assumption can be wrong in both directions, the learner needs a manual +override. + +## What Changes + +- Suppress the OS virtual keyboard on the answer field when the device is + classified as a phone (small viewport, touch-primary), since it has no + physical keyboard and the on-screen board already covers typing. +- Leave the virtual keyboard available by default on tablets and larger + touch devices, since a physical keyboard cannot be reliably detected and + many tablets are typed on without one. +- Add a settings menu (a gear icon in the header) where the learner can force + the virtual keyboard on or off, overriding the device-based default — for a + tablet with a physical keyboard attached (force off) or a phone-sized + device misclassified as a tablet, or vice versa (force either way). The + choice persists across visits. +- Fix the on-screen keyboard so it no longer forces the page wider than a + phone screen: it scrolls sideways below its full width instead of shrinking + keys past a tappable size. This surfaced during verification — the settings + control itself was unreachable on a real phone width until this was fixed. +- Add a second, independent setting in the same menu letting the learner hide + the app's own on-screen keyboard entirely, for anyone typing on a physical + keyboard they already know. + +## Capabilities + +### New Capabilities + +(none) + +### Modified Capabilities + +- `typing-practice`: adds device-aware control over whether the OS virtual + keyboard is allowed to appear when the answer field is focused, a + learner-facing setting that overrides the device default, a requirement + that the on-screen keyboard stays usable (not shrunk below a tappable size) + on any viewport, and a setting to hide the on-screen keyboard entirely. + +## Impact + +- Affected code: `src/frontend/src/components/AnswerInput.vue` (the answer + field's `inputmode`), a new `SettingsMenu.vue` component (header gear + button + popover) replacing the answer field's earlier in-keyboard control, + `OnScreenKeyboard.vue` (the board's panel now scrolls instead of a fixed + width) and `SentenceView.vue` (conditionally renders the board), a settings + Pinia store backed by `localStorage`, and a small device-classification + helper under `src/frontend/src/lib/`. +- No backend, API, or data changes. +- No new dependencies expected — classification uses standard `matchMedia` + queries already available in the target browsers. diff --git a/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/specs/typing-practice/spec.md b/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/specs/typing-practice/spec.md new file mode 100644 index 0000000..9775205 --- /dev/null +++ b/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/specs/typing-practice/spec.md @@ -0,0 +1,113 @@ +## ADDED Requirements + +### Requirement: Virtual keyboard suppressed on phone-classified devices +The frontend SHALL classify the device as a phone when it has a small screen +and is primarily touch-operated (no hover-capable pointer such as a mouse or +trackpad), and SHALL keep the OS virtual keyboard from appearing when the +answer field is focused on such a device, unless the learner has overridden +this. A phone has no physical keyboard, and the app's own on-screen Kedmanee +keyboard already covers typing, so a second, OS-drawn keyboard only competes +for the same screen space. + +#### Scenario: Focusing the field on a phone +- **WHEN** the learner taps or otherwise focuses the answer field on a + phone-classified device, and no override is set +- **THEN** the OS virtual keyboard does not appear, and the on-screen keyboard + remains the way to type + +#### Scenario: A physical keyboard still works on a phone-classified device +- **WHEN** the learner types on a physical keyboard while the answer field is + focused on a phone-classified device +- **THEN** the typed characters reach the field exactly as they do on any + other device, because suppressing the virtual keyboard does not block + physical key input + +### Requirement: Virtual keyboard available by default on tablets and larger devices +The frontend SHALL leave the OS virtual keyboard free to appear when the +answer field is focused on any device not classified as a phone, unless the +learner has overridden this. Whether such a device has a physical keyboard +attached cannot be reliably determined from the browser, and many of these +devices are typed on without one, so the default SHALL favor not removing a +learner's only way to type. + +#### Scenario: Focusing the field on a tablet +- **WHEN** the learner taps or otherwise focuses the answer field on a device + not classified as a phone, and no override is set +- **THEN** the OS virtual keyboard is free to appear as it would on any other + input field + +### Requirement: Learner can override virtual keyboard visibility +The frontend SHALL provide a setting the learner can use to force the OS +virtual keyboard on or off for the answer field, overriding the device-based +default in either direction, and SHALL persist that choice across visits. +Device classification is a heuristic and can be wrong in both directions — a +tablet with a physical keyboard attached, or a phone-sized device the +heuristic misreads — so the learner needs a way to correct it. + +#### Scenario: Forcing the virtual keyboard off +- **WHEN** the learner turns the setting to "off" and then focuses the answer + field, regardless of device classification +- **THEN** the OS virtual keyboard does not appear + +#### Scenario: Forcing the virtual keyboard on +- **WHEN** the learner turns the setting to "on" and then focuses the answer + field, regardless of device classification +- **THEN** the OS virtual keyboard is free to appear + +#### Scenario: The override persists +- **WHEN** the learner sets the override and later reloads the application or + returns in a new visit, on the same device and browser +- **THEN** the same override is still in effect, without the learner setting + it again + +#### Scenario: No override set +- **WHEN** the learner has never changed the setting +- **THEN** the device-classified default governs whether the virtual keyboard + appears, and the setting shows that this is the current, unforced state + +### Requirement: The on-screen keyboard fits any viewport without shrinking below a tappable size +The on-screen keyboard's keys SHALL NOT be sized smaller than they are today +to fit a narrow viewport; where the layout's rows are wider than the +viewport, the frontend SHALL let the learner scroll the keyboard sideways to +reach the rest of it instead. A key shrunk to fit a phone stops being a +target a thumb can reliably hit, and stops being the size the finger-position +promise this keyboard makes is about. + +#### Scenario: The page does not grow wider than the viewport +- **WHEN** the exercise is displayed on a viewport narrower than the + keyboard's full width +- **THEN** the page itself does not scroll or grow wider than the viewport — + only the keyboard's own rows do + +#### Scenario: Every key stays reachable +- **WHEN** the keyboard is wider than the viewport showing it +- **THEN** every key the current layout can type, including ones initially + off-screen, can still be reached and pressed by scrolling the keyboard + +### Requirement: Learner can hide the on-screen keyboard +The frontend SHALL provide a setting, independent of the virtual keyboard +setting, that lets the learner hide the on-screen keyboard entirely, and +SHALL persist that choice across visits. A learner typing on a physical +keyboard they already know needs no help finding the keys, and the board is +a lot of screen to keep spending on a hint they are not using. + +#### Scenario: Hiding the on-screen keyboard +- **WHEN** the learner turns the on-screen keyboard setting to "hide" +- **THEN** the on-screen keyboard is no longer displayed + +#### Scenario: Typing still works while it is hidden +- **WHEN** the on-screen keyboard is hidden +- **THEN** the learner can still type into the answer field, whether from a + physical keyboard or, where enabled, the OS virtual keyboard, and checking + the answer works exactly as it does with the board shown + +#### Scenario: Showing it again +- **WHEN** the learner turns the on-screen keyboard setting back to "show" +- **THEN** the on-screen keyboard is displayed again, following the current + exercise's layout and next-key highlight as usual + +#### Scenario: The choice persists +- **WHEN** the learner sets this independently of the virtual keyboard + setting and later reloads the application or returns in a new visit +- **THEN** the on-screen keyboard's shown/hidden state is unchanged, and the + virtual keyboard setting is unaffected by it diff --git a/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/tasks.md b/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/tasks.md new file mode 100644 index 0000000..333ba2e --- /dev/null +++ b/openspec/changes/archive/2026-09-11-mobile-virtual-keyboard-control/tasks.md @@ -0,0 +1,103 @@ +## 1. Device classification + +- [x] 1.1 Add `src/frontend/src/lib/device.js` exporting a reactive + `isPhone` (or equivalent composable) backed by `matchMedia` queries for + `(max-width: 640px)` and `(hover: none) and (pointer: coarse)`, both + required for a phone classification +- [x] 1.2 Add `src/frontend/src/lib/__tests__/device.test.js` mocking + `window.matchMedia` and verify: both conditions true → phone; either + false → not phone; a change event on the media query list updates the + reactive value + +## 2. Settings store + +- [x] 2.1 Add `src/frontend/src/stores/settings.js`, a Pinia store holding + `virtualKeyboardOverride` (`'auto' | 'on' | 'off'`, default `'auto'`) + and a computed `virtualKeyboardEnabled` that resolves to the override + when set, otherwise `!isPhone` from `lib/device.js` +- [x] 2.2 Persist `virtualKeyboardOverride` to `localStorage` on change and + hydrate it from `localStorage` on store creation, with reads/writes + wrapped so a failure (e.g. storage disabled) falls back to `'auto'` + in memory instead of throwing +- [x] 2.3 Add `src/frontend/src/stores/__tests__/settings.test.js` and + verify: default is `'auto'`; setting `'on'`/`'off'` updates + `virtualKeyboardEnabled` regardless of a mocked `isPhone`; a persisted + value is read back on a fresh store instance; a `localStorage` that + throws on get/set does not crash store creation or a setting change + +## 3. Suppress the virtual keyboard on the answer field + +- [x] 3.1 In `AnswerInput.vue`, bind `:inputmode` on the field to + `store.virtualKeyboardEnabled ? 'text' : 'none'` (reading the new + settings store), and verify existing `v-model`, `@keyup.enter`, and + focus-on-mount behavior in `AnswerInput.vue` are unchanged (existing + component tests, if any, continue to pass; otherwise a quick manual + check that typing and Enter-to-check still work) +- [x] 3.2 Verify manually in a touch-emulated mobile viewport (browser + devtools device toolbar, or a real phone) that focusing the field no + longer raises the OS virtual keyboard when classified as a phone with + no override set + +## 4. Override control + +- [x] 4.1 ~~Add a control to `OnScreenKeyboard.vue`'s bottom bar~~ — superseded + by task 6.2: e2e verification at phone width found the bottom-bar pill + unreachable (overlapped by other elements), so the control lives in a + new header settings menu instead (see design.md, "Settings control + placement"). `settingsStore.virtualKeyboardOverride` and its three + states (`auto`/`on`/`off`) are unchanged. +- [x] 4.2 Verify by keyboard/mouse interaction (Playwright, phone-width and + desktop-width viewports) that choosing an option changes + `virtualKeyboardEnabled` and that the change is reflected in + `AnswerInput.vue`'s `inputmode` without a reload + (`e2e/virtual-keyboard.spec.js`) + +## 5. Spec and regression check + +- [x] 5.1 Run the frontend test suite (`npm test` in `src/frontend/`) and + verify all tests pass, including the new device/settings tests +- [x] 5.2 Walk each scenario in + `openspec/changes/mobile-virtual-keyboard-control/specs/typing-practice/spec.md` + against the running app (phone-emulated viewport, tablet-emulated + viewport, and override on/off/auto) and confirm each holds — covered by + the full e2e suite (44/44 passing) run in the Tilt dev pod, including + `e2e/virtual-keyboard.spec.js` and `e2e/responsive-keyboard.spec.js` + +## 6. The on-screen keyboard board fits a phone width + +- [x] 6.1 In `OnScreenKeyboard.vue`, make the panel `w-full + max-w-[var(--board)]` (was a fixed `w-[var(--board)]`) and wrap the + rows + space bar in an `overflow-x-auto` container at their full, + unshrunk width, so keys never shrink below their current tappable size + — the panel adapts to the viewport, its contents scroll sideways below + `--board` +- [x] 6.2 Move the virtual-keyboard override control out of + `OnScreenKeyboard.vue`'s bottom bar into a new `SettingsMenu.vue` + (a gear button in `App.vue`'s header, opening a small popover), + positioned so it adds no height to the header row on any viewport +- [x] 6.3 Add `e2e/responsive-keyboard.spec.js` and verify at a phone-sized, + touch-primary viewport: the page itself never scrolls sideways, and + the longest catalog sentence can still be typed key-by-key through the + on-screen keyboard, off-screen keys included +- [x] 6.4 Update `e2e/practice.spec.js`'s DOM-depth locator for the keyboard + panel (one extra wrapper level from the new scroll container) and + re-verify the existing "fits on one screen" / "field lines up with the + keys" desktop-viewport assertions still pass + +## 7. Learner can hide the on-screen keyboard + +- [x] 7.1 Add `onScreenKeyboardVisible` (boolean, default `true`) to + `stores/settings.js`, persisted to `localStorage` the same + fail-soft way as `virtualKeyboardOverride` +- [x] 7.2 Add its test coverage to `stores/__tests__/settings.test.js`: + defaults to shown, is read back by a fresh store instance, a broken + `localStorage` doesn't crash a read or a write, and it is independent + of `virtualKeyboardOverride` +- [x] 7.3 In `SentenceView.vue`, `v-if` `` on + `settingsStore.onScreenKeyboardVisible` +- [x] 7.4 Add a second option group to `SettingsMenu.vue` ("On-screen + keyboard": Show / Hide) reading and writing the new setting +- [x] 7.5 Add e2e coverage (`e2e/virtual-keyboard.spec.js`) that hiding + removes the board while typing and checking still work (a physical + keyboard into the plain ``), and that showing it again brings + it back diff --git a/openspec/changes/serve-static-and-media/.openspec.yaml b/openspec/changes/archive/2026-09-11-serve-static-and-media/.openspec.yaml similarity index 100% rename from openspec/changes/serve-static-and-media/.openspec.yaml rename to openspec/changes/archive/2026-09-11-serve-static-and-media/.openspec.yaml diff --git a/openspec/changes/serve-static-and-media/design.md b/openspec/changes/archive/2026-09-11-serve-static-and-media/design.md similarity index 100% rename from openspec/changes/serve-static-and-media/design.md rename to openspec/changes/archive/2026-09-11-serve-static-and-media/design.md diff --git a/openspec/changes/serve-static-and-media/proposal.md b/openspec/changes/archive/2026-09-11-serve-static-and-media/proposal.md similarity index 100% rename from openspec/changes/serve-static-and-media/proposal.md rename to openspec/changes/archive/2026-09-11-serve-static-and-media/proposal.md diff --git a/openspec/changes/serve-static-and-media/specs/local-dev-environment/spec.md b/openspec/changes/archive/2026-09-11-serve-static-and-media/specs/local-dev-environment/spec.md similarity index 100% rename from openspec/changes/serve-static-and-media/specs/local-dev-environment/spec.md rename to openspec/changes/archive/2026-09-11-serve-static-and-media/specs/local-dev-environment/spec.md diff --git a/openspec/changes/serve-static-and-media/specs/production-deployment/spec.md b/openspec/changes/archive/2026-09-11-serve-static-and-media/specs/production-deployment/spec.md similarity index 100% rename from openspec/changes/serve-static-and-media/specs/production-deployment/spec.md rename to openspec/changes/archive/2026-09-11-serve-static-and-media/specs/production-deployment/spec.md diff --git a/openspec/changes/serve-static-and-media/tasks.md b/openspec/changes/archive/2026-09-11-serve-static-and-media/tasks.md similarity index 98% rename from openspec/changes/serve-static-and-media/tasks.md rename to openspec/changes/archive/2026-09-11-serve-static-and-media/tasks.md index 3f08bce..8a0070c 100644 --- a/openspec/changes/serve-static-and-media/tasks.md +++ b/openspec/changes/archive/2026-09-11-serve-static-and-media/tasks.md @@ -32,7 +32,7 @@ - [x] 5.1 Bring the stack up in the default mode and fetch a clip through the Gateway: a `200` with `audio/mpeg`, served by the media server rather than Django - [x] 5.2 Confirm a `Range` request returns `206` with a correct `Content-Range`, which is what seeking in `