Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 28 additions & 23 deletions README.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions docs/ACCESSIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ The interface supports light and dark themes with separate color tokens. Interac

The color themes have not yet been comprehensively measured against WCAG contrast criteria. Focus, contrast, zoom, and screen-reader behavior need verification in the complete application.

### The Gymnast page and the element page

These pages were checked for a narrow phone and for both themes, in a real browser (`e2e/gymnast-page.e2e.ts`, `e2e/element-page.e2e.ts`):

- **Reflow (WCAG 1.4.10) and text spacing (1.4.12):** no sideways scrolling at 280, 320, 360 and 390 px, also with a name of 56 letters without a space and with the text spacing of 1.4.12.
- **Contrast (1.4.3, 1.4.11):** axe finds no violation in light and dark mode, and the dark mode of the element page and the badges of the vaults. The color under the pointer on a row is measured in the browser, and the text on it can be read.
- **Not only color (1.4.1):** the trash icon turns red on hover and while it is pressed, but its meaning is in its name ("Delete ..."), and a touch screen, which cannot hover, gets the red from the pressed state.
- **Keyboard (2.1.1, 2.4.3, 2.4.7):** the arrow keys, Home and End move between the apparatus tabs; the question "are you sure?" closes with Escape and gives the focus back to the button; after a removal the focus goes to the heading of the list; the menu of "Add to gymnast" opens with the keyboard.
- **Size of what is pressed (2.5.8):** every button, tab and link in the main part is at least 40 px tall and wide on a phone (the criterion asks for 24).
- **Names and roles (4.1.2):** a link that looks like a button is still a link (`LinkButton`); "Add to gymnast" for a visitor who is not logged in is a link with a lock and the text "(log in first)", not a disabled button, which the keyboard cannot reach and which does not say why.

**Where:** [GymnastPage.tsx](../src/pages/GymnastPage.tsx), [ApparatusTabs.tsx](../src/components/gymnasts/ApparatusTabs.tsx), [TrashButton.tsx](../src/components/gymnasts/TrashButton.tsx), [LinkButton.tsx](../src/components/ui/LinkButton.tsx), [AddToGymnast.tsx](../src/components/elements/AddToGymnast.tsx).

### Base UI

The project uses Base UI primitives through the shared button component. This gives buttons a reusable, accessible foundation rather than implementing their basic behavior from scratch. It does not automatically make every composed page or interaction accessible; labels, descriptions, keyboard flow, and visual contrast still need to be designed and checked by the project.
Expand All @@ -61,4 +74,5 @@ Accessibility requirements should be considered while planning each feature and
## Related documentation

- [Sustainable choices](./SUSTAINABILITY.md) – data transfer, image delivery, performance, and maintainability.
- [State management](./STATE_MANAGEMENT.md) – where each kind of state lives, and the shared parts.
- [UI plans](./UI_PLANS.md) – target users and user needs.
42 changes: 42 additions & 0 deletions docs/AI_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,3 +532,45 @@
- **Tool:** Claude Code
- **What the team did:** Wrote issue #97: move the pages out of `src/components/`, so it is clear which files are routes and which are parts of pages, and describe the folders in the README.
- **What Claude did:** Moved the four pages and their tests with `git mv` (so their history follows them), and moved the three placeholder pages out of `App.tsx` into their own files, so every route has a file in `src/pages/`. Updated the imports, the links in the README and the docs, and the "Project structure" table. Found that `src/styles/gymnastTheme.test.ts` reads the files of the gymnasts and login pages by folder, so after the move it would have stopped checking the two pages without failing; added them to the list. The shared components in `components/ui/` were left where they are (see the issue). After that, asked to move the frontend tests to `src/tests/` as well: moved all 29 with `git mv` into the same folders as the code they test, changed their relative imports to the `@/` alias (so a test does not depend on where it is), and updated the paths that `gymnastTheme.test.ts` reads files from.

## The Gymnast page (`src/pages/GymnastPage.tsx`, `src/components/gymnasts/`, `src/components/ConfirmDialog.tsx`, `src/components/Error*.tsx`, `src/lib/gymnastDetail*.ts`, and the parts of the element page that grew out of it)

- **Tool:** Claude Code
- **What the team did:** Wrote issue #149 and the mockup (`docs/design/gymnastDetail.png`), and decided the first scope: the apparatus tabs, the routines and the list of elements of one apparatus, and removing an element from the list. Editing the name and deleting a routine belong to other issues. After the first result the team kept directing the work in several rounds (next bullet), and the scope grew: "New routine" and "Add from search" became links, and the element page got a working "Add to gymnast", the shared button for "Try in a routine", a dark mode and the difficulty badge for vaults. The first plan said that no other page would be changed; that is no longer true, and the pages that were changed are listed under "Simplifications and refactoring".
- **How the design was reached:** The first result was not the final one. The design went through several iterations, and the team asked for changes after each result. Each result was checked by the team on localhost (the dev server, in light and dark mode, and at narrow widths in the browser's inspector) and by reading the code, and what was seen there decided the next request. The requests, in the order they came:
- The tabs: only the name of the apparatus (no highest score), only the line like the shadcn/ui "line" example (no white tile), the line under the chosen tab on the same line as the line below it, and a lower tab.
- No count under the name of the gymnast ("5 routines · 11 elements in the list").
- The remove button: a trash icon instead of a cross, then one shared component for the gymnasts page and the Gymnast page, in the text color until the pointer is over it or it is pressed.
- The D-score of a routine in a smaller size, and no underline when the pointer is over the name of an element.
- A "New routine" button as in the mockup, the same button component and color as the other buttons, linking to the calculator with the gymnast in the address; then a plus in the button, and an "Add from search" button of the same kind.
- The text of the remove dialog: "1.104 will be removed from [name of the gymnast]".
- The hover color of the lists in dark mode, the color of the "Highest" label (the color of the buttons), and a layout that works on a phone.
- On the element page: "Try in a routine" in the shared button, an "Add to gymnast" that works, dark mode, and the difficulty badge for vaults.

In some of the rounds the team asked how to decide, and Claude answered with options and a recommendation first (the red of the trash icon on a phone, whether the lists and rows should be one component, one shared error component). The team chose, and Claude changed the code.

- **What Claude did:** Wrote the plan and the tests first (the grouping logic, the page on the real Apollo Client with a stub for `fetch`, and the confirmation dialog), watched them fail, and then wrote the page. One query asks for the gymnast with all routines and elements (they are limited by the API), and the page groups them by apparatus itself, so changing apparatus costs no request. Reused `PageTitle`, `Button` with `PILL`, `DifficultyBadge`, the structure query of the filters, the Base UI tabs and alert dialog, and the existing color tokens. The tabs are the shadcn/ui tabs (added with the shadcn CLI). In the later rounds Claude made the changes above, one at a time, ran the checks after each round (the lint, the type check, the tests and mostly the build), and wrote tests for the new behavior. "Add to gymnast" is a menu of the gymnasts of the coach when someone is logged in, and for a visitor who is not logged in it is a link to the login page with a lock icon, which sends the visitor back to the element after the login. Claude recommended that over a disabled button (a disabled button cannot be reached by the keyboard and does not say why), and the team has not asked for the other way.
- **Accessibility and phone check (WCAG 2.2 AA):** Done with axe-core and Playwright against the running app, logged in with the test login, on a test gymnast with long names and routines. The widths were 280, 320, 360, 390 and 768 px, in light and dark mode, also with the text spacing of WCAG 1.4.12 added, and with the keyboard on the tabs and the remove dialog. The contrast of the hover colors was calculated from the tokens and not judged by eye. It found:
- At 320 px the page was 3 px wider than the window, because a grid column took the width of its content. Fixed with `minmax(0, 1fr)`.
- At 320 px the name of an element got about 60 px, one word on a line, because the code was beside it. The code is now above the name on a narrow window.
- The four tabs broke into three rows. They are now two by two on a narrow window.
- In dark mode the hover color of a row was 1.05:1 to the page behind the card, so a row under the pointer looked like a hole in the card. It is now a deeper navy of the theme (`--card-hover`): 1.67:1 to the page, 1.29:1 to the card at rest, and 12.4:1 for the text. A test follows the token.
- "Your gymnasts" ran out of the window at every width: the title and the button did not wrap, the list had the same grid problem, and a long name without spaces was not broken.
- The value under the difficulty badge could not be read in dark mode, the focus ring of the icon buttons was missing (the shared button sets `outline-none`), and the focus was lost after removing an element (a disabled button loses the focus, and one `ref` shared by the panels of all apparatus ended up empty). These three were found in the first round and fixed then.

After the fixes the Gymnast page has no sideways scroll at any of the widths, in both modes, also with a name of about 40 letters without spaces and with the text spacing of 1.4.12, and axe reports no violations. The arrow keys change the tab, the remove dialog fits in a 320 px window, the focus is inside it, and Escape gives the focus back to the button that was pressed. The element page (an element and a vault, in both modes, at 320 and 1100 px) also has no violations, with the color contrast checked, and no sideways scroll.

One mistake in the check itself is worth noting: the first runs used the same refresh token in several browser contexts, but a refresh token can be used once, so the login ended and some of the runs measured the login page and not the Gymnast page. It was found when a later step could not find the tabs on the page. Every context now logs in for itself, and the run stops if the heading of the page it got is "Log in".

- **Tests, and what the tests are worth:** The checks above were first run by hand with scripts that were not kept. At the team's request the part that a component test cannot see (jsdom has no layout, no focus rules and no real colors) was written as end-to-end tests in `e2e/gymnast-page.e2e.ts` and `e2e/element-page.e2e.ts` (see "End-to-end tests" in the README): axe in both modes and on a phone, no sideways scrolling at four narrow widths, the size of what is pressed on a phone, the arrow keys on the tabs, where the focus goes around the dialog, the real hover color of a row and of the trash icon, and "Add to gymnast" in a real browser. The team also asked for a list of every test to judge them one by one, and removed the ones that only compared class names or read source files, because they break on a harmless change and stay green when the real thing is broken: the text and icon colors of `ErrorMessage`, the caller's classes on `ErrorPage` and `TrashButton`, the look of `AddButton`, the plus and the placeholder message of "Try in a routine", the classes of the trash button and of the cards, the trash icon and the vault badge by class name, the eight tests that read the source of the element page for light-only colors, the test that read `RoutineList` for the button colors, the tab order test, and a touch test that stayed green when the bug it should catch was added. In a second round the team removed more tests that checked how something is written and not what it does, or that tested the library and not our code: the classes and the icon class names (`lucide-plus`, `lucide-lock-keyhole`, `lucide-trash-2`), a regex over the HTML of `Button`, the tests that parsed `--card-hover` in the CSS and checked a ratio of 1.5:1 that was our own choice (the real hover color is measured in the browser instead), the fifth Tab press in the dialog (that is the behaviour of Base UI, and what is ours, the focus back to the button, is kept), and the repeated checks of the exact dialog text in the browser (a component test has it). The four axe runs of the vault page became one, in dark mode on a phone, which is the hardest case for the badges. The team said that test coverage is measured by someone else, so no coverage tool was added. A better touch test was found (a device that cannot hover must not get the hover color; it passes now and fails when the hover is not gated), but it has not been added to the repository. A green test can be worthless, so each important end-to-end test was checked by breaking the code on purpose and seeing it fail: the grid column that makes a page wider than a phone, the dark hover color, the size of the trash button, the dark background of the element page, the arrow keys of the tabs, and the focus after the dialog. That check found one weak test: removing the grid fix did not fail the sideways-scrolling test, because the routine names of the test data were short enough to break. The test data now has a routine name of 56 letters without a space, and the test fails without the fix. Two other tests first failed because they read the focus and a color that changes over 150 ms too early; they now wait, and the app was not wrong.
- **Simplifications and refactoring:** Claude first listed what was repeated, and then which of it should be shared and which should not.
- Found: the same skeleton of a list (section, heading, empty text, list in a card) in three places; the long hover string of a card in two places; a link that covers a whole card copied three times; two almost equal error states; and eleven places with `role="alert"` that differ in color, markup and button.
- Shared: `ListSection` (the routines and the elements), `StretchedLink`, `AddButton` and `LinkButton` (a link with the look of the pill button; `Button` also takes an `icon` now, which the menu buttons use), `TrashButton`, `ConfirmDialog`, and the constants of `listCard.ts` (also used by the cards of the gymnasts page). Errors are in three layers, `ErrorMessage`, `ErrorPanel` and `ErrorPage`, which the Gymnast page uses.
- `LinkButton` exists because of a finding of the end-to-end run: given a link, the Base UI `Button` warns in the console that it is not a `<button>`, and the fix it suggests (`nativeButton={false}`) gives the link `role="button"`, so a screen reader would say "button" for something that goes to another page. A real `<a>` with the same `buttonVariants` and `PILL` has the same look and the right role. Other pages still use `Button` with a link in `render`; they were not changed.
- Logic out of the menu: the gymnasts query, the mutation and `add()` of "Add to gymnast" were first inside the menu component. They are now in `useAddElementToGymnast`, next to `useElementRemoval` for the Gymnast page, so the component only shows the choices. The behaviour is the same and the nine component tests passed unchanged. This was done on the team's request after a check that the pull request adds no debt for the issue about moving page logic into hooks.
- Removed: `ElementValueBadge`, because `DifficultyBadge` now follows the theme (the pill colors) and is used for vaults too, on the element page and in its list of variants; and two special rules in `global.css` that only changed this badge on the Gymnast page in dark mode.
- Kept apart on purpose: the row of a routine and the row of an element. They have only the `<li>` in common, so one row would need a switch for every difference, which is the wrong abstraction. Two sources were read for this, "AHA programming" by Kent C. Dodds and "The Wrong Abstraction" by Sandi Metz, as summaries only.
- The result did not make the code shorter: the files under `src/components/gymnasts/` have more lines than before, because of the new files and their comments. What changed is that each decision now has one place.
- Changed outside the Gymnast page: the element page (colors, the badge, "Try in a routine", "Add to gymnast"), "Your gymnasts" (the wrapping of the title and the list), `variables.css` (`--card-hover`), and two shared parts that other pages use too: `Button` (the `icon` prop, which changes nothing when it is not given) and `DifficultyBadge` (its colors now follow the theme, also in the results of the Elements page).
- Not done on purpose: moving the error messages of the other pages (the elements results, the element page, the login, "Your gymnasts") to the three layers, because those files belong to other pull requests and the change would conflict with them. The menus of the Elements page still have their own copies of the menu classes that `menuStyles.ts` now holds.
- **Limits:** The end-to-end tests need the database and the backend, and are run by hand: the course git server has no runners. A screen reader was not used: that has to be done by a person. Lighthouse was started on a production build in the last round, but the run did not finish (the computer was low on memory), so there are no Lighthouse scores from that round. "Your gymnasts" was fixed after the last check in a browser and has not been measured again, and the results table of the Elements page, which uses the changed badge, has not been looked at in dark mode. The test gymnast was made with the test login in the local database and deleted again.
Loading