Skip to content

fix(frontend): sidebar refresh/rotate flash + chart crosshair re-park on sidebar toggle; 2 flaky-test guards#326

Merged
dackclup merged 3 commits into
mainfrom
claude/optimistic-brown-UUcXA
May 30, 2026
Merged

fix(frontend): sidebar refresh/rotate flash + chart crosshair re-park on sidebar toggle; 2 flaky-test guards#326
dackclup merged 3 commits into
mainfrom
claude/optimistic-brown-UUcXA

Conversation

@dackclup
Copy link
Copy Markdown
Owner

Three stability fixes (the two user-reported sidebar/chart bugs + finishing the flaky-test hardening). Frontend + test-only — zero schema / compute / scoring / valuation / output-JSON change.

Bug A — sidebar "opens then shrinks back by itself" on refresh OR portrait↔landscape rotation

Two root causes, two-part fix:

  • Refresh flash: the static export bakes the expanded rail into every page's HTML, so on refresh it painted wide, then AppShell's mount effect read localStorage and collapsed it. Fix — a pre-paint inline <script> in layout.tsx adds .sidebar-collapsed to <html> before the body paints (mirrors how next-themes pre-paints dark mode), and a globals.css rule renders the rail at its collapsed 4rem width immediately → React's post-hydration collapse is a no-op. AppShell keeps the class synced to live state so it never fights React once interactive.
  • Animated shrink: the <aside> carried a permanent width 200ms transition, so both the hydration-collapse and the breakpoint cross on rotation animated the shrink. Fix — a new animate flag (owned by AppShell) is true only for ~250 ms around an explicit user toggle; at rest the aside is transition-none, so refresh + rotation + resize switch instantly. Explicit toggles still animate smoothly.

Bug B — chart crosshair jumps to the far LEFT when the sidebar expands/collapses

Toggling the rail reflows the main-content width → ResponsiveContainer re-measures, but Recharts applies defaultIndex (latest-point park) only on mount, so the crosshair drifted left. Fix (PriceHistoryChart.tsx) — replaced the orientation-only matchMedia re-park with a width-delta ResizeObserver that debounce-bumps the <AreaChart> remount key ~300 ms after the container re-measures, re-parking at the latest point. Subsumes rotation (it changes width too) and now also catches the sidebar toggle + window resize. A <1px width-delta gate ignores height-only / crosshair-render churn.

Flaky tests (เก็บ flaky ให้ครบ)

  • test_ranking_history.py::test_load_ranking_history_smoke_recent_window — explicit pytest.skip on an empty frame (shallow CI clone, actions/checkout fetch-depth=1). Mirrors the list_ranking_commits guard from feat(frontend): fluid root font-size for app-wide responsive scaling #325 + PR fix(test): make manipulation_distribution smoke resilient to shallow clones #284; the test previously passed-on-empty only by coincidence of the empty-return path's .set_index(...). Proven on a real depth-1 clone (46/46 git-dependent tests pass).
  • test_osap.py::test_package_imports_and_exposes_openap_class — was a default-lane (non-@network) test that instantiated openassetpricing.OpenAP(), whose 0.0.2 constructor does a live Google-Drive fetch → flaky on Drive quota (hit live in this session's pre-push gate: ColumnNotFoundError on a "Quota exceeded" HTML page). Fix — check the API surface on the class (hasattr(OpenAP, method)) instead of an instance; the scout signal stays offline, the live path stays in the @network test in the same file.

Verification (empirical)

  • ruff check . clean · pytest -m "not network"1407 passed (was 1406 + 1 OSAP flake) · tsc --noEmit clean on edited files · next build → 506 routes
  • Playwright (real chromium, served static export):
    • A.1 refresh no-flash — rail measured 72px across all 62 frames of the first 700 ms (never wide)
    • A.2 transition gating — at rest transitionProperty=none; during toggle transform, width; settles back to none
    • A.3 rotation — portrait→landscape keeps transitionProperty=none (instant, no shrink), rail lands at 72px
    • B crosshair re-park — cursor x-ratio before=1.0, transient@120ms=0.0 (the bug), after-repark=1.0 (fixed)

Lockstep via PHASE_STATUS_INFLIGHT.md (per #237). No new invariant beyond what the existing §Gotchas already frame.

https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7


Generated by Claude Code

…shair on sidebar toggle; guard 2 flaky tests

Bug A — sidebar "opens then shrinks back by itself" on refresh OR
portrait↔landscape rotation:
- Pre-paint a `.sidebar-collapsed` class (inline script in layout.tsx) +
  a globals.css rule renders the collapsed rail at its narrow width before
  React hydrates (the static export bakes the EXPANDED markup) — no
  wide→narrow flash on refresh. AppShell keeps the class synced to live state.
- Gate the aside width/transform transition behind an `animate` flag that's
  on only for ~250ms around an EXPLICIT user toggle; refresh + rotation +
  resize switch instantly (no shrink animation). Explicit toggles still
  animate smoothly.

Bug B — chart crosshair jumps to the far left when the sidebar expands/
collapses:
- Replace the orientation-only matchMedia re-park with a width-delta
  ResizeObserver on the chart wrapper that debounce-bumps the AreaChart
  remount key after the container re-measures, re-parking defaultIndex at the
  latest point. Subsumes rotation + now also catches sidebar toggle + resize.

Flaky tests (finish the shallow-clone / network hardening):
- test_ranking_history smoke: explicit pytest.skip on an empty frame (shallow
  CI clone, fetch-depth=1) — mirrors the #325 + PR #284 guard; proven on a
  real depth-1 clone (46/46 git-dependent tests pass).
- test_osap import scout: check the OpenAP API surface on the class instead of
  instantiating (the openassetpricing 0.0.2 ctor does a live Google-Drive
  fetch → flaky on Drive quota); the live path stays in the @network test.

Verified: ruff clean; pytest -m "not network" 1407 passed; tsc clean on edited
files; next build 506 routes; Playwright empirical Bug A x3 + Bug B all PASS.

https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
quantrank Ready Ready Preview, Comment May 30, 2026 9:29am

- FAIL (reduced-motion): the gated sidebar `[transition:…]` arbitrary class
  wasn't covered by the globals.css prefers-reduced-motion guard, so an
  explicit toggle still animated for reduced-motion users. Add
  `motion-reduce:transition-none` (always-on) so the toggle is instant under
  reduced motion. Verified: reduced-motion ctx → transitionProperty=none.
- WARN (ResizeObserver deps): drop `chartData.length` from the dep array — the
  chart wrapper div persists across period switches (only the inner <AreaChart>
  remounts via key), so the observer needn't disconnect/re-observe on every
  period change; [loading, error, data] already covers wrapper-mount.

Verified empirically (Playwright): regression — original 4 still PASS (refresh
no-flash / transition gating / rotation instant / crosshair re-park); new —
reduced-motion transition none on toggle + collapsed-rail focus ring fits
within the aside (aside[0,72] link[9,62] ring±4, not clipped). tsc clean on
edited files; next build 506 routes.

https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
… text flash on refresh)

Follow-up to the Bug A refresh fix: the pre-paint CSS only forced the rail
WIDTH, so on a collapsed refresh the expanded inner content (wordmark
"QuantRank" + nav labels + section headers + footer chip) still painted for a
frame before React hydrated and hid it ("text pops up then disappears"). Extend
the pre-paint `html.sidebar-collapsed` CSS to render the ENTIRE collapsed layout
from the first frame — hide text (data-rail="hide"), center nav items
(navlink), stack the header (header), full-width chevron box (chevron), show the
icon theme-toggle (show) — mirroring the `collapsed ? 'md:…'` Tailwind classes
the React tree applies, via additive data-rail hooks that don't touch the
working React ternaries. The CSS agrees with React's collapsed classes
post-hydration (both keyed/synced to the same collapsed state) so they never
fight.

Verified (Playwright): collapsed refresh light + dark — max rendered width of
any hidden-text element = 0 across all frames (text never painted); regression —
original Bug A/B checks still PASS. tsc clean; next build 506 routes.

https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
@dackclup dackclup marked this pull request as ready for review May 30, 2026 09:42
@dackclup dackclup merged commit b82b845 into main May 30, 2026
4 checks passed
@dackclup dackclup deleted the claude/optimistic-brown-UUcXA branch May 30, 2026 09:42
dackclup added a commit that referenced this pull request May 30, 2026
…#326 §Gotchas (#327)

Full docs-reviewer substance pass on CLAUDE.md → 5 MUST-FIX + 4 SHOULD-FIX, all fixed. CLAUDE.md only (+ PHASE_STATUS_INFLIGHT.md lockstep entry); no code/schema/compute/frontend change.

MUST-FIX: §Phase status "In flight" marked PR #312 (tasteful-motion) as "THIS PR" → merged (e602485); "Recently merged" frozen at #310 → drained #311#326 (16 entries, SHAs verified); "Next deliverables" Issue #67 flip listed pending → DONE (PR #294, USE_SECTOR_COE=True); "Section A-H/A-J" → "A-L" (helper is A-L since PR #221).

SHOULD-FIX: §Stack "Phase 3b on this PR" → "(merged)" + edgartools 5.31→5.32; §Gotchas compute/main.py line refs re-anchored (840→879 · 1965-66→2084-85 · 717→728 · 785→805 · 972→1025); +2 §Gotchas for PR #326 invariants (sidebar data-rail↔globals.css pre-paint lockstep; AreaChart re-park debounce ≥300ms).

Confirmed no drift: schema 0.10.11-phase4.6 · skills 46 · agents 19 · hooks 3. docs-reviewer re-check: DOCS-CLEAN (all 16 SHAs match, 5 line refs accurate, 2 gotchas code-backed). Lockstep via PHASE_STATUS_INFLIGHT.md side-file (PR #237).
dackclup added a commit that referenced this pull request May 30, 2026
…on-only rail (#328)

On desktop, when the sidebar is COLLAPSED the 64px rail now keeps ONLY the collapse chevron (back to its original 32px square — no longer a full-width filled box), and the brand (green Q + "QuantRank") moves OUT to the top header. Expanding moves the brand back INTO the rail. Mobile drawer unchanged.

Mechanism (reuses the PR #326 data-rail pre-paint pattern → no refresh flash):
- Sidebar.tsx: rail brand Link carries data-rail="hide" (Q + wordmark hide together at md+ when collapsed); chevron drops the full-width/fill classes and re-centers (32px square).
- AppShell.tsx: new brand Link in the top header carries data-rail="show" (hidden base, md:flex only when collapsed).
- globals.css: pre-paint rules updated to match (header→center-the-chevron; chevron→margin-left:0).

Verified (Playwright, real chromium): EXPANDED brand-in-rail/not-header · COLLAPSED brand-in-header/not-rail + chevron 36×36 square · COLLAPSED REFRESH rail brand never flashes (0px across 45 frames) · TOGGLE round-trip · mobile+collapsed=1 shows exactly ONE header wordmark (no double). frontend-design-reviewer: READY-FOR-SPOT-CHECK, 0 FAIL. tsc clean; next build 506 routes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants