From 5f245789ff0cfbace7a482365716bc8d51ad82d3 Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Tue, 21 Apr 2026 14:36:48 +0000 Subject: [PATCH 1/2] Add user instructions modal (issue #2885 item 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses item 3 of issue #2885: a "how to use the site" overview for first-time users and an exercise-screen contextual help that explains the three playback steps. ## What this adds - `` — a four-step overview ("pick a topic → open a task → run Listen/Interact/Solve → see the results") triggered from the global header on every authenticated route. Icon + label ("Как заниматься" / "How to practise") visible at every breakpoint (no icon-only mobile variant — users include seniors and 7-year-olds). - `` — a contextual mini-help in the fullscreen exercise overlay that only covers the three playback buttons, since the user is already past the "pick a topic" stage. Placed next to the existing close-X, mirror-positioned. - `` — shared shell built on the native element (modeled on `ui/confirm-dialog`), which gives focus trap, ESC-to-close, and backdrop-click-to-close for free. Both modals above are thin content wrappers around this shell. - `UiExerciseButton` tooltip: replaced the old "Exercise 3" level-only title with the two new strings (`exercise_button.tooltip_available` for unlocked tasks, `exercise_button.tooltip_locked` for locked tasks) — also requested in the same issue. ## Copy decisions (domain review) - Dropped the author's "75%" threshold claim. `app/schemas/exercise.ts` `isCompleted` is binary (all tasks or nothing). Wording softened to "когда правильных ответов наберётся достаточно" so the copy survives any future backend threshold changes. - Dropped the "green check with exclamation mark" state — it does not exist in the current UI. - Confirmed Listen→Interact auto-advances and Interact→Solve is manual (matches PR #2889 behavior). ## Accessibility - `` native element → inherent `role="dialog"`, ESC closes, backdrop visible. - `aria-modal="true"`, `aria-labelledby` pointing at an

inside. - Trigger carries `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls`, `aria-label`. - Focus is restored to the trigger when the dialog closes. - All interactive elements meet 44×44 touch-target minimum. ## Out of scope - Auto-open on first visit — needs a server-persisted `hasSeenInstructions` flag. Devices are shared at rehab centres, so localStorage would be wrong. - Drawer/bottom-sheet mobile variant — centered modal for v1, visual follow-up possible. - Per-step icons and screenshots (asset work). - Red-exclamation icon for <75% completion (backend flag doesn't exist). - Analytics for "instructions opened". ## Tests - `instructions-modal/component-test.gjs` — trigger renders, dialog opens with the four steps, ESC/X/OK close, aria metadata correct, Russian locale renders Russian title. - `exercise-playback-help/component-test.gjs` — trigger renders, dialog opens with the playback-specific title, close dismisses it. - `ui/exercise-button-test.gjs` — tooltip switches between `exercise_button.tooltip_available` and `tooltip_locked` based on `@isAvailable`. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/.gitignore | 3 + .../exercise-playback-help/index.gts | 54 +++++++++++ frontend/app/components/header/index.gts | 2 + .../components/instructions-modal/index.gts | 83 +++++++++++++++++ .../components/ui/exercise-button/index.gts | 9 +- .../ui/instructions-dialog/index.gts | 91 +++++++++++++++++++ .../group/series/subgroup/exercise.gts | 3 + .../exercise-playback-help/component-test.gjs | 38 ++++++++ .../instructions-modal/component-test.gjs | 81 +++++++++++++++++ .../components/ui/exercise-button-test.gjs | 38 ++++++++ frontend/translations/en-us.yaml | 25 +++++ frontend/translations/ru-ru.yaml | 25 +++++ 12 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 frontend/app/components/exercise-playback-help/index.gts create mode 100644 frontend/app/components/instructions-modal/index.gts create mode 100644 frontend/app/components/ui/instructions-dialog/index.gts create mode 100644 frontend/tests/integration/components/exercise-playback-help/component-test.gjs create mode 100644 frontend/tests/integration/components/instructions-modal/component-test.gjs diff --git a/frontend/.gitignore b/frontend/.gitignore index e1ac95e1e..82e94e657 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -31,3 +31,6 @@ jsconfig.json # Playwright /e2e/test-results/ /e2e/playwright-report/ + +# Claude Code harness state +/.claude/ diff --git a/frontend/app/components/exercise-playback-help/index.gts b/frontend/app/components/exercise-playback-help/index.gts new file mode 100644 index 000000000..841199491 --- /dev/null +++ b/frontend/app/components/exercise-playback-help/index.gts @@ -0,0 +1,54 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { on } from '@ember/modifier'; +import { t } from 'ember-intl'; +import UiHelp from 'brn/components/ui/help'; +import UiInstructionsDialog from 'brn/components/ui/instructions-dialog'; + +interface ExercisePlaybackHelpSignature { + Args: { + triggerClass?: string; + }; + Element: HTMLButtonElement; +} + +export default class ExercisePlaybackHelpComponent extends Component { + @tracked isOpen = false; + triggerElement: HTMLButtonElement | null = null; + + @action + open(event: Event) { + this.triggerElement = event.currentTarget as HTMLButtonElement; + this.isOpen = true; + } + + @action + close() { + this.isOpen = false; + this.triggerElement?.focus(); + } + + +} diff --git a/frontend/app/components/header/index.gts b/frontend/app/components/header/index.gts index d59e2d1a4..eee773ec3 100644 --- a/frontend/app/components/header/index.gts +++ b/frontend/app/components/header/index.gts @@ -17,6 +17,7 @@ import GlobalTimer from 'brn/components/global-timer'; import LoadingSpinner from 'brn/components/loading-spinner'; import XpBadge from 'brn/components/xp-badge'; import StreakCounter from 'brn/components/streak-counter'; +import InstructionsModal from 'brn/components/instructions-modal'; import GamificationService from 'brn/services/gamification'; const ExternalLinkIcon = , ); - assert.dom('.c-exercise-button').hasAttribute( - 'title', - 'Click to start this task', - ); + assert + .dom('.c-exercise-button') + .hasAttribute('title', 't:exercise_button.tooltip_available'); }); test('tooltip points at the locked key when the exercise is not available', async function (assert) { @@ -58,9 +57,8 @@ module('Integration | Component | ui/exercise-button', function (hooks) { , ); - assert.dom('.c-exercise-button').hasAttribute( - 'title', - 'This task is not available yet. It will open once you finish the previous one.', - ); + assert + .dom('.c-exercise-button') + .hasAttribute('title', 't:exercise_button.tooltip_locked'); }); });