Skip to content

Rework the global protected terms UI, and fix the header controls - #94

Merged
simoncodes-ca merged 13 commits into
developfrom
feature/improve-protected-terms
Sep 5, 2026
Merged

simoncodes-ca merged 13 commits into
developfrom
feature/improve-protected-terms

Conversation

@simoncodes-ca

@simoncodes-ca simoncodes-ca commented Sep 5, 2026 •

Copy link
Copy Markdown
Owner

What

The protected-terms chip multi-select on /settings becomes a real list, and the app header's three controls get sorted out.

Protected terms

  • A list you can add to, delete from, and rename in place, saved explicitly.
  • Deleting keeps the row visible, struck through and marked REMOVING, with Undo. It reaches the file only on Save, so a pending removal is reviewable and reversible without a confirm dialog.
  • Renaming happens inline on the row; Enter commits, Escape cancels, collisions are reported in place.
  • Terms are listed and written alphabetically. The list is a set, and a sorted file keeps its diffs to one line per change — which is what docs/features/protected-terms.md already promises.
  • A filter appears past eight terms. Adding or renaming a term the filter would hide drops the filter, so what you just typed is never invisible.
  • The save controls sit in the page header and the list scrolls inside itself, so Save, the add field and the filter can't scroll out of view. Below 500px of viewport height the explanation and file path give up their rows.

Header

  • The gear opened the theme menu and a sliders icon went to Settings. The gear now goes to Settings.
  • The theme switcher wears the theme you're looking at — sun or moon — instead of a fixed glyph. On system it shows the resolved theme; which mode is selected stays legible through the label and the menu check.
  • The locale picker was a mat-icon-button holding an icon and a text label, so the globe was clipped to a sliver by the button's fixed square.
  • Gaps between the three controls were 16px / 8px / 16px, which is why they read as three floating items. Now one 4px gap set once on a container.
  • Settings goes inert on /settings, where clicking it did nothing.

Worth a reviewer's attention

The explanation text was wrong, and not in the way it looked. It credited auto-translation with keeping terms verbatim. Auto-translation doesn't read the list at all — the "protection" in that pipeline is placeholder protection, swapping ICU placeholders for markers. The list is enforced in export (marks terms found in each source string for translators) and import (rejects a translation that altered one). The copy now names both.

Two Angular traps that are worth knowing because the same shapes exist elsewhere in this app:

  • A computed() calling transloco.translate() caches whatever it reads first, and first read happens before the translation file loads — so it serves the raw key forever after. The theme label now resolves through the pipe. keysText in the same component has the same shape and only escapes it because another dependency changes after load.
  • .settings-page needed an explicit box-sizing: border-box. There's no global reset, so height: 100% plus padding overflowed the viewport by exactly the padding.

Settings goes inert via disabledInteractive, not a plain disabled. A plain disabled drops the control out of the tab order with no explanation; this keeps it focusable, announces aria-disabled, and adds aria-current="page" to say why. Easy to change if you'd rather it be fully removed.

Found but deliberately not fixed

The app shell renders wider than the viewport on /browser. .app-header measures 983px inside a 900px .app-container below about 1050px, because that route's content forces the grid column open and drags the header with it — pushing Settings off the right edge. /collections at the same width is clean, and the control group is byte-identical in width to before. Same mechanism puts a hard ~397px floor under the whole app. The candidate fix is grid-template-columns: minmax(0, 1fr) on .app-container, but .app-content is overflow-x: hidden, so that would clip the browser route rather than scroll it. Needs a decision about that surface.

Minor, untouched: the theme menu renders its check between the icon and the label ("☀ ✓ Light") rather than trailing it, and the .active row styling doesn't appear to take.

Accessibility

Six contrast failures on the settings page were measured and fixed, including the page title rendering black-on-black in dark mode — Angular Material's typography was overriding the theme token, and that predates this branch. Badge, error and icon colours are now mixed toward the foreground so they invert with the theme.

Also fixed: Material's 48px icon-button touch target overflowed the 45px rows and made the scroller report phantom overflow, so a scrollbar appeared on lists that fit. Dropped on fine pointers, restored at 44px under pointer: coarse.

i18n

21 new resources across all six shipped locales, added through the CLI. The two ICU plurals were written by hand with correct per-locale plural categories (Russian gets one/few/many). One Russian string was hand-corrected where the machine rendered "exports"/"imports" as "export systems"/"import systems".

settings.protectedTermsHint changed twice: once because the "press Enter or comma" mechanic no longer exists, once for the accuracy fix above.

Testing

523 tests pass, lint clean, production build succeeds.

  • settings.spec.ts goes from 6 tests to 28.
  • app-header.spec.ts is new — the component had no spec at all.
  • Verified in Chromium across en, de, es, fr-ca, ja, ru at 320–1440px, light and dark, including the save round-trip actually writing the file sorted.

Note

Two commits on this branch predate this work and are included here: 0f8542c (nx/pnpm settings) and 2f759ed (PRODUCT.md). Split them out if you'd rather they land separately.

🤖 Generated with Claude Code

SimonNodel-AI and others added 13 commits September 4, 2026 23:32
Record durable product truth for the Tracker web UI: primary and secondary
users, positioning, operating context, capabilities and constraints, the
binding watercolor brand identity, and the evidence on hand.

Also records the Impeccable build-path workflow default and gitignores the
skill's machine-local state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RifJr7xyhZkaTWn4ssV1b4
Nx 22 wrote `analytics: false` and reformatted the `translationFixtures`
named input; pnpm recorded `@swc/core` and `nx` as ignored built
dependencies. Committing so the settings stop reappearing as local noise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RifJr7xyhZkaTWn4ssV1b4
Replace the chip multi-select on /settings with a list that supports adding,
deleting and renaming terms, with an explicit save.

Interaction:
- Deleting keeps the row visible, struck through and marked, with Undo; it is
  written only on Save, so a pending removal is always reviewable and reversible.
- Renaming happens inline on the row; Enter commits, Escape cancels, and a
  collision is reported in place.
- Re-adding a term that is marked for removal takes the removal back instead of
  reporting a duplicate.
- A filter appears past eight terms, mounted inside the panel so it survives a
  no-match state. Adding or renaming a term the filter would hide drops the
  filter, and the affected row is scrolled into view.

Layout:
- The save controls live in the page header, so they cannot scroll out of view.
- The page is a fixed-height column: the list grows with its rows until the
  viewport runs out, then scrolls internally, keeping the add field and the
  filter on screen.
- Below 500px of viewport height the hint and the file path give up their rows.

Terms are listed and saved alphabetically: the list is a set, and a sorted file
keeps its Git diffs minimal.

Fixes found while building this:
- Angular Material's typography painted the page title black in both themes,
  making it invisible in dark mode.
- Six text and control colours fell below their contrast floors; badge, error
  and icon colours are now mixed toward the foreground so they invert with the
  theme.
- Material's 48px icon-button touch target overflowed the 45px rows and made the
  scroller report phantom overflow. It is dropped on fine pointers and restored
  at 44px under `pointer: coarse`.
- `.settings-page` needs an explicit `box-sizing: border-box`; the app has no
  global reset, so `height: 100%` plus padding overflowed the viewport.

Adds 20 translated resources across all six locales; the two ICU plurals carry
per-locale plural categories. The protected-terms hint changed because the
"press Enter or comma" mechanic no longer exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RifJr7xyhZkaTWn4ssV1b4
The settings hint credited auto-translation with keeping protected terms
verbatim. Auto-translation does not read the list at all — the protection it
applies is placeholder protection, which swaps ICU placeholders for markers and
is unrelated. The list is enforced in export and import instead: export marks
each string with the protected terms found in its source so translators and MT
services know what to leave alone, and import rejects an incoming translation
that altered one.

The hint now names both, matching docs/features/protected-terms.md.

The Russian translation is hand-corrected: the machine rendered "exports" and
"imports" as "export systems" and "import systems", which are not things.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RifJr7xyhZkaTWn4ssV1b4
…reens

The save controls moved into the page header, where a single non-wrapping row
put German's "Alle zurücksetzen" and "Speichern" 84px past a 390px viewport.
`.settings-page` clips its overflow, so the Save button was invisible and
unreachable rather than merely off-grid — and a horizontal-overflow check on the
document reported clean.

The row now wraps and the buttons never shrink; the status pill gives up width
instead and truncates its label. A flex child defaults to `min-width: auto`, so
the pill needed that cleared on both itself and its span before it would yield.

Verified with both controls enabled across en, de, es, fr-ca, ja and ru at 390,
768 and 1440px. Below roughly 397px the app shell itself overflows horizontally,
on /collections equally, which is a pre-existing limit and untouched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RifJr7xyhZkaTWn4ssV1b4
…lout

The hint was a loose paragraph between the heading and the file path, reading as
overflow from the heading rather than as its own thing. It becomes a bordered,
tinted block with an info icon.

Built from the existing `--color-info` token, so it inverts with the theme. The
text takes its cast from that hue rather than staying grey, which a coloured
surface calls for. No accent rule down the side: the tint and the icon already
say what kind of block this is.

Measured at 8.8:1 light and 11.4:1 dark for the text, 4.0:1 and 7.7:1 for the
icon. The short-viewport rule that reclaims these rows below 500px now targets
the callout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RifJr7xyhZkaTWn4ssV1b4
The three app controls read as three floating items rather than one group, and
two of them wore each other's icon.

- Gaps were 16px / 8px / 16px between peers. They are now one 4px gap set once
  on a `.toolbar-actions` container, with the per-button margins removed.
- The gear opened the theme menu and a sliders icon went to Settings. The gear
  now goes to Settings, which is what a gear means everywhere else.
- The theme switcher wears the theme you are looking at — light_mode or
  dark_mode — instead of a fixed glyph. On `system` it shows the resolved theme;
  which mode is selected stays legible through the label and the menu check.
- The locale picker was a mat-icon-button holding an icon *and* a text label, so
  the globe was clipped to a sliver by the button's fixed square. It is a text
  button sized to its content, matched to the icon buttons' 40px height.
- Order is language, appearance, settings, with Settings as the right anchor and
  the only route among the three. DOM order follows, so focus order does too.

The label resolves through the transloco pipe rather than a computed calling
`translate`: a computed caches whatever it reads first, and first read happens
before the translation file loads, so it served the raw key forever after.

Not fixed here, and pre-existing: on /browser the app shell renders wider than
the viewport below about 1050px — `.app-header` measures 983px inside a 900px
`.app-container` — because that route's content forces the grid column open and
drags the header with it. It pushes Settings off the right edge. The control
group itself is byte-identical in width to before (172px from the divider), so
this is a shell issue, not a header one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RifJr7xyhZkaTWn4ssV1b4
Clicking it there did nothing, with no sign that it would.

It uses `disabledInteractive` rather than a plain `disabled`, so the control
keeps its place in the tab order and announces `aria-disabled` instead of
disappearing for keyboard users. `aria-current="page"` says why it is inert,
which a bare disabled state does not. Active state comes from routerLinkActive
with exact matching, so it recovers on navigating away.

Adds the first spec for this component, covering the inert/live transition, its
focusability while inert, and the theme button's icon across light, dark and
system. The shared transloco testing fixture gains the header, locale, settings
and theme keys it was missing.

Material's disabled colour reaches 2.3:1 in light and 3.6:1 in dark. Left as it
is: WCAG exempts inactive controls, and matching every other disabled control in
the app is worth more here than singling this one out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RifJr7xyhZkaTWn4ssV1b4
…spec

The CDK BreakpointObserver subscribes through mql.addListener, which the
spec's matchMedia stub did not provide, so every AppHeader test emitted an
unhandled TypeError and failed the suite in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RifJr7xyhZkaTWn4ssV1b4
The collections page was a launcher with a lot of dead space: cards were a
fixed 160px tall with an empty band between the folder path and the locale
pills, the grid used hard 1/2/3-column breakpoints that left an empty column,
collections came out in arbitrary config order, and the base locale — the one
distinction that matters when reading a locale list — was rendered the same as
every other pill.

- Rebuild the card as a single element (no nested accent/inner elements, no
  decorative backdrop-filter), sized by its content
- Sort collections by name, case-insensitively
- Put the base locale first and mark it; cap the chip row at four chips so
  cards in a row stay flush, with the rest behind a "+n" chip and a tooltip
- Switch the grid to auto-fill minmax(300px, 1fr)
- Shrink the page title to an operate-appropriate size and add a collection count
- Make read-only collections unmistakable: an amber lock icon plus an
  explanatory note on the card
- Move the folder path and count text off --color-text-tertiary, which failed
  AA at 2.2:1, and clamp the path to two lines with a tooltip
- Make the open-affordance arrow persistent rather than hover-only
- Add a name/folder filter above six collections, mirroring the settings page,
  with a no-matches state

Also theme ::selection, caret-color, accent-color and scrollbar-color app-wide;
they were browser defaults.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
readOnlyBadge shipped the English "Read-only" in all five non-base locales.
The four fr-ca entries already held correct French but carried no metadata, so
they still counted as untranslated.

- Translate collections.card.readOnlyBadge into es, fr-ca, ru, de and ja
- Re-apply the existing fr-ca values for collections.title, addButton,
  card.edit and card.delete so the CLI records their checksums and status

No collection resource under collections/ is left new or stale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The collections card redesign replaced the read-only badge with a note row
that uses readOnlyTooltip, leaving readOnlyBadge referenced nowhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The theme button previously wore the *resolved* theme, so selecting System
looked identical to pinning light or dark. It now wears the selected mode,
giving System its own monitor icon — matching the glyph in the menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@simoncodes-ca
simoncodes-ca merged commit 86576d3 into develop Sep 5, 2026
1 check passed
@simoncodes-ca
simoncodes-ca deleted the feature/improve-protected-terms branch September 5, 2026 23:27
@lingo-tracker-release

Copy link
Copy Markdown

🎉 This PR is included in version 0.18.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants