feat: line badges, rarity tags, dynamic masthead#65
Merged
Conversation
LineBadge.svelte: a small inline SVG pastille drawn from the official RATP palette. M1..M14 render as colored discs, RER A..E as rounded squares (matches the RATP signage convention). Customisable size and optional aria-label override. Rather than ship binary PNG/SVG assets, the badge is inlined so it can re-color through CSS variables (works in dark mode), scales next to the surrounding type, and adds zero HTTP requests. PredicateChip.svelte: tokenises the localised predicate label with a regex matching 'ligne X', 'line X', or 'RER X' (case-insensitive, captures digits 1-14 with optional 'bis' suffix and letters A-E). Tokens are split into text/badge segments and rendered with LineBadge inline. Other predicate families pass through untouched. The chip now receives a prop so it picks the right palette (metro vs rer); Grid.svelte forwards its existing domain prop down. This is editorial/non-commercial use of the line colour scheme — just the disc + line code, no RATP wordmark or logo.
Two improvements bundled because they share the same /play response shape change. contracts + server: - PlayResponse now carries fameScore (nullable, 0..=100) when the answer was correct. Read from grid.payload.entities[id].fame_score on the server, snapshotted at generation time so the value is stable even after a domain re-ingest. Null on wrong answers and on entities without fame data. web — rarity: - src/lib/rarity.ts: rarityFor(fame) → 'common' | 'rare' | 'epic' | 'legendary' using the 80/50/20 thresholds we agreed on. Null fame → 'common' on purpose so un-scored stations don't silently get mis-labelled. - CellAnswer.fameScore persisted in localStorage so the rarity badge survives a refresh. - CellButton: persistent corner badge on solved cells (rare / epic / legendary). Common cells keep the understated check so the badge wall stays readable. - Grid: a transient toast pops above the grid on each correct answer whose rarity is >= rare, fades after 1.7 s, gold for legendary with a soft glow. prefers-reduced-motion respected. web — masthead: - Grid header now reads (mode, domain) instead of hardcoding 'Grille du jour / Métro de Paris'. The eyebrow switches between daily / solo / duel; the title between 'Grille du jour' / 'Solo' / 'Duel'; the date renders only in daily mode (solo and duel grids aren't 'of the day'). domainLabel picks paris-metro vs rer i18n keys. i18n: 9 new fr+en keys covering rarity tiers + grid_mode_* and grid_title_* variants.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Three visual improvements to make the game more readable and a bit more rewarding to play.
fame_score. The client derives a Common / Rare / Épique / Légendaire tier (80/50/20 thresholds), pops a transient toast above the grid, and leaves a persistent corner badge on the cell.Why
The masthead bug was an explicit ask after the multi-domain refactor —
/rer/playwas still claiming "Métro de Paris" in its header, which is obviously wrong.Line badges and rarity tags came together: both lift the UI from "list of grey predicates" to something that feels like the metro/rer it represents. With #57 baking real fame scores into the dataset, the rarity dimension can finally surface during play instead of waiting for the end-game modal.
How
40771a5 feat(web): inline SVG line badges in predicate chipsLineBadge.sveltecomponent. Inline SVG (no binary assets), colors from the official RATP palette via CSS variables. Discs for métro, rounded squares for RER. Scales withsizeprop (default 18 px).PredicateChip.sveltetokenises the localised label with a regex (/\b(?:ligne|line|RER)\s+([A-E]|\d{1,2}(?:\s?bis)?)\b/). Tokens render text or badge. Other predicate families untouched.domainprop down so the chip picks the right palette.1efad9c feat: live rarity tags + dynamic mastheadContracts + server:
PlayResponsecarries an optionalfameScorefield. Server readsgrid.payload.entities[id].fame_score(snapshotted at generation time, stable through re-ingest), null on wrong answers and un-scored entities.Web — rarity:
src/lib/rarity.ts:rarityFor(fame)with 80/50/20 thresholds, null → 'common'.CellAnswer.fameScorepersisted in localStorage so badges survive a refresh.CellButton.svelte: persistent corner badge on cells where rarity ≥ rare (common cells keep the understated check).Grid.svelte: transient toast above the grid on each correct answer ≥ rare, 1.7 s fade, gold w/ glow for legendary.prefers-reduced-motionrespected.Web — masthead:
mode+domainprops. Eyebrow flips between daily / solo / duel labels. Title flips too. Date renders only in daily mode (solo/duel grids aren't "of the day").domainLabelswitches between paris-metro and rer i18n keys.i18n: 13 new fr+en keys (4 rarity tiers, grid_mode_, grid_title_).
Checklist
cargo fmt --check && clippy --all-targets -- -D warnings && test --workspacepassespnpm --dir web lint && typecheck && test && buildpassesunsafe, nounwrap()in non-test codecontracts/openapi.yaml(additive —fameScoreis optional)Test plan
/paris-metro/play, answer with Châtelet → toast doesn't show (common), no badge/rer/play→ predicate "Sur la ligne A" → 'A' renders as a red rounded square/paris-metro/playshows "Grille solo / Métro de Paris" + title "Solo", no date/rer/(daily) shows "Grille du jour / RER d'Île-de-France" + title "Grille du jour" + today's date