docs: update architecture for bracket redesign (Steps 1–3)

Document wallchart center-out geometry engine, radial orbit layout with flag
tokens on rings, rounds pager infrastructure, view toggle persistence, and
art direction (stadium-night, escalating heat). Note retirement of CSS
equal-height connector invariant. Update TODO with Step 4 (champion
celebration + polish + v1.1.0 version bump).
This commit is contained in:
Lucas Kalil 2026-07-03 22:09:54 -03:00
parent c246117545
commit caec00689f
3 changed files with 107 additions and 5 deletions

View file

@ -98,6 +98,25 @@ champion path, debuts champion) once the final lands. The **data-layer stages (G
---
## 8. Bracket redesign (2026-07-03, spec settled via /grill-me — see project-memory)
Two switchable chart layouts (wallchart default + radial) + mobile round pager; stadium-night art
direction; built directly on master, one approval gate per step.
- [x] ~~Step 1 — Foundation + wallchart: center-out layout engine (`computeWallchartLayout()`),
SVG connectors + path highlight, stadium-night backdrop, tiered cards with microlines,
fit-to-chart zoom (fit = "100%"), sim/challenge/share preserved~~ (2026-07-03)
- [x] ~~Step 2 — Mobile round pager (default ≤767px) + view-toggle infrastructure~~ (2026-07-03;
reworked same day per user feedback: **button navigation only** — scroll-snap swiping removed —
and max 2 columns on desktop)
- [x] ~~Step 3 — Radial layout (second chart view on the toggle)~~ (2026-07-03; redesigned to the
user's reference image: circular flag tokens on rings + trophy center = "orbit" view, gold real /
dashed-blue sim route lines, tooltips for names/scores)
- [ ] Step 4 — Champion celebration (gold real / blue sim), polish pass (a11y/i18n/reduced-motion),
`APP_VERSION` → v1.1.0, README note
---
## Quick final checklist
```

View file

@ -41,7 +41,11 @@ worldcup2026/
│ ├── css/
│ │ ├── style.css ★ Palette variables, glassmorphism base, layout,
│ │ │ components — mobile-first
│ │ ├── bracket.css Bracket columns, connectors, highlight states
│ │ ├── bracket.css Knockout views: stadium-night stage, wallchart cards
│ │ │ (heat toward the Final) + SVG connectors, radial orbit
│ │ │ tokens (tk-* states, route lines), rounds-pager cards
│ │ │ + chips, view switch, path highlight, sim/challenge
│ │ │ styles; all motion gated by reduced-motion
│ │ ├── stats.css Stats tab: hero "pulse", overview cards, goals-by-stage chart
│ │ └── animations.css Entry (fade-in, slide-up/left) + interaction
│ │ (hover-scale/glow, pulse, line-draw)
@ -65,7 +69,12 @@ worldcup2026/
│ │ ├── groups.js Standings computation (3/1/0, GD, GF) + group tables
│ │ ├── stadiums.js Stadium cards + "view matches" cross-link
│ │ ├── bracket.js ★ Bracket tree resolution, resolveBracketTeams(),
│ │ │ simulation, challenge score, share prediction
│ │ │ simulation, challenge score, share prediction;
│ │ │ 3 switchable views (wc2026_prefs.bracketView):
│ │ │ computeWallchartLayout() center-out wallchart,
│ │ │ computeRadialLayout()+radialInnerHTML() orbit view
│ │ │ (flag tokens on rings, trophy center), rounds pager
│ │ │ (button-only); fit-to-chart zoom (fit = "100%")
│ │ ├── modal.js Match detail modal (ARIA dialog)
│ │ ├── storage.js localStorage wrapper — wc2026_* keys, auto-JSON
│ │ ├── i18n.js EN/PT-BR dicts + t(key), lang toggle

View file

@ -99,9 +99,12 @@ automated tests / linter (explicit spec constraint).
match (group or knockout); reused by schedule cards, modal, and search/team filters (so knockout
matches become searchable/filterable once resolved). `getBracketTree()` → `{ rounds, third,
nodesByRef, champion }`.
- **CSS connectors depend on an equal-height invariant:** all columns share height with `flex:1`
- ~~**CSS connectors depend on an equal-height invariant:** all columns share height with `flex:1`
slots, so pair children sit at 25%/75% and the next node at 50%; pure-CSS stubs meet exactly.
Column gap = 2 × stub (44px desktop / 36px ≤767). **Breaking equal height breaks the lines.**
Column gap = 2 × stub (44px desktop / 36px ≤767). **Breaking equal height breaks the lines.**~~
**RETIRED 2026-07-03** by the wallchart redesign — see "Bracket redesign (2026-07-03)" below;
connectors are now SVG paths generated from the same JS geometry as the cards, so no CSS
invariant exists anymore.
- **Simulation:** `decide()` applies only real finished results; `applySimulation()` overlays user
picks afterwards and **never overrides a real result** (so `simulated:false` ⇒ real). Stale entries
(winner no longer resolved) are silently ignored. Eligible nodes (both teams resolved, real result
@ -115,6 +118,67 @@ automated tests / linter (explicit spec constraint).
`loadPredictionFromURL()`; stripped from the URL (`history.replaceState`) whether applied or not;
unknown refs rejected wholesale. **Challenge** card scores sim vs real finished knockout results.
### Bracket redesign — center-out wallchart (2026-07-03, Step 1 of 4)
Full redesign spec settled via /grill-me: **two switchable chart layouts** (center-out wallchart =
default, radial = Step 3) + a **mobile round pager** default ≤767px (Step 2), stadium-night art
direction, lean escalating cards, fit-whole-chart initial framing, gold-real/blue-sim champion
celebration (Step 4), built **directly on master** (deploy still gated on user-approved push).
Step 1 (shipped): wallchart replaces the old left-to-right columns.
- **`computeWallchartLayout()` in `bracket.js` is the single geometry source:** absolute px
positions for every card/title/champion box (`GEO` constants; R32 18 left half, 916 right,
later nodes at feeder midpoints) **and** the SVG bezier connectors + champion stem derived from
the same numbers — cards and lines cannot drift apart. Tree/sim/share/challenge logic untouched;
DOM contract preserved (`data-ref`, `data-match-id`, delegation, keyboard activation).
- **Fit-to-chart zoom:** geometry is computed (never DOM-measured); a `ResizeObserver` on
`#bracket-wrap` computes `view.fit` when the panel becomes visible (hidden panel = clientWidth 0)
and re-fits on resize unless the user zoomed. **Zoom label "100%" = fit**, reset returns to fit,
clamp = [fit, 2]. The wrap carries an inline `aspect-ratio: W/H` from the engine so the fit view
is never letterboxed (capped `max-height: min(80vh, 840px)`).
- **Cards:** lean two-row + microline (`.bk-meta`: kickoff via `kickoffShort()` honoring the
Local/Stadium toggle — bracket now listens to `timemodechange`; LIVE pulse; FT = `t('bracket.ft')`,
new i18n key EN/PT). Tier classes `bk-r32…bk-final` escalate size/heat; the Final hero card also
shows venue (`.bk-venue`). Champion box: `has-champion` gold; **`is-sim` = blue + SIM chip**
(a simulated champion must never read as real — same rule as the stats verdict; previously the
old champion box showed sim champions in gold).
- **Path highlight** now also lights SVG connectors: a path turns gold when **both** its
`data-from`/`data-to` endpoints are in `pathRefs()`; the champion stem carries FINAL→FINAL.
- **Motion:** cards rise + connectors draw (`pathLength="1"` + dash animation), staggered by round,
all inside `@media (prefers-reduced-motion: no-preference)`; replays on each tab open (display:none
restarts CSS animations) — intentional "chart assembles" effect.
- ~~**Interim states until later steps:** mobile ≤767 shows the pinch-zoom wallchart (pager = Step 2);
no view toggle yet (registry lands with the second view).~~ Superseded same day — Steps 2+3 below.
### Bracket redesign — Steps 2+3: view toggle, rounds pager, radial "orbit" (2026-07-03)
- **View toggle:** segmented `Fases | Chaveamento | Radial` in the toolbar; explicit choice persists
in `wc2026_prefs.bracketView`; with no stored pref the default follows the breakpoint (≤767px →
rounds pager, else wallchart; a `matchMedia('change')` listener re-renders while unset). Zoom
controls render only for chart views. `render()` dispatches: pager / `chartHTML`
`wallchartInnerHTML` | `radialInnerHTML`. Switching chart layouts resets to a fresh fit
(`view.layoutId` check in `initInteractions`).
- **Rounds pager — button navigation ONLY (user decision):** the first build used a scroll-snap
swipe track; the user rejected horizontal scrolling, so pages are plain sections toggled with
`hidden` by the chips (`initPager` ~15 lines, no ResizeObserver/height-clamp needed). Grid is
**max 2 columns** (≥700px) — 34 columns made cards too narrow (user feedback). Opens on the
first round with an unfinished match (`firstOpenPage`); `pagerIndex` survives re-renders. Cards
(`.bk-pcard`) carry venue·city + status row and reuse `teamRowHTML` + the same
`data-ref`/`data-match-id` delegation (modal + sim editor work unchanged).
- **Radial = "orbit" view, redesigned to a user-supplied reference image** (circular predictions
bracket with the trophy at center). NOT rectangular cards: **circular flag tokens** on concentric
rings — outer ring = 32 entrants, each ring inward = a round's **winner slots** (a match's winner
slot doubles as the next match's participant), trophy centerpiece (= the FINAL's winner slot,
champion flag + name when decided, sim-blue when simulated), third-place = small labeled pair
below the circle. `TGEO` radii chosen so adjacent/consecutive rings never collide (validated with
an automated pairwise-overlap eval in preview — keep doing that after any radius change).
**Semantics:** elbow route lines (radial segment + bend dot) turn **gold = real advancement**,
**dashed blue = simulated pick**; eliminated entrants grey out (`tk-out`), TBD slots are striped
discs (`tk-tbd`); names/scores live in the shared app tooltip (`has-tip`/`data-tip`, delegation
in app.js `initTooltips`) and the modal. **Sim affordance lives on the winner slot** (`opts.slot`,
not `opts.winner` — a TBD slot is exactly the simulatable one; that inversion was a shipped-then-
fixed bug). Path highlight generalized: `showPath` lights ANY `[data-ref]` element + `.bk-le`
endpoints; hover/focus delegation uses `closest('[data-ref]')`.
- **Toolbar gotcha:** `.bracket-tools-left` holds 6 controls — needs `flex-wrap: wrap` or it forces
~500px of page overflow at 375px (found via body scrollWidth sweep on mobile).
### Modal (`modal.js`)
- **Native `<dialog>` + `showModal()`** → focus trap, Esc, `::backdrop` come free. Backdrop click =
`event.target === dialog`. Focus restored to the opener on close. Card→modal is **event delegation
@ -209,6 +273,11 @@ automated tests / linter (explicit spec constraint).
viewport emulation desyncs the capture surface. At emulated widths > native, navigate via
`preview_eval` + `navigateTo()` and verify geometry via eval/inspect; trust screenshots only at
widths ≤ native. `preview_resize preset: desktop` resets it.
9. **`aspect-ratio` + `min-height` can transfer size INTO width** (bracket wallchart, 2026-07-03) —
on a box with `width: auto`, a violated `min-height` transfers back through the ratio and widens
the element past its container (on mobile the 220px min became 585px of page overflow). Fix: give
the box a definite width (`width: 100%`); then the ratio only drives height and min/max-height
clamp it without transfer.
---
@ -491,7 +560,12 @@ supersedes the old "7681439 single-row header" note.
## Current State
**Updated 2026-07-02.** Data: **R32 underway** — group stage COMPLETE (172) + R32 matches **73
**Updated 2026-07-03.** **Bracket redesign Steps 13 shipped on master (not yet pushed):** the
Knockout tab has 3 switchable views — center-out wallchart (desktop default), radial "orbit" (flag
tokens per the user's reference image), rounds pager (mobile default, button-only navigation, ≤2
columns). See Architecture → "Bracket redesign" (both entries). Pending: Step 4 champion
celebration + polish pass + version bump to v1.1.0.
Data: **R32 underway** — group stage COMPLETE (172) + R32 matches **73
(RSA 01 CAN)**, **74 (GER 11 PAR, PAR 43 pens)**, **75 (NED 11 MAR, MAR 32 pens)**, **76
(BRA 21 JPN)**, **77 (FRA 30 SWE)**, **78 (CIV 12 NOR)**, **79 (MEX 20 ECU)**, **80
(ENG 21 COD)**, **81 (USA 20 BIH)** and **82 (BEL 32 SEN, AET)** finished (82/104 total);