Spaces:
Running
Running
| # UI Fix Plan — carepath-omega.vercel.app (scribe/frontend/) | |
| Plan authored by Fable 5 after reviewing the live deploy and source. Executor: Opus 4.8. | |
| Scope: the Vite/React app in `scribe/frontend/` only. Do the fixes in order. Keep diffs minimal — no redesigns, no new dependencies. | |
| ## How findings were verified (and how you re-verify) | |
| The live site was screenshotted and probed with the repo's own Playwright | |
| (`scribe/frontend/node_modules/playwright-core`, headless Chromium) at 1440/1100/900/800/390px widths. | |
| Re-run any probe the same way, e.g.: | |
| ```js | |
| // node, from scribe/frontend/: require('playwright-core'), launch chromium, | |
| // newPage({ viewport }), goto https://carepath-omega.vercel.app/ (or local `npm run preview`) | |
| ``` | |
| Verify locally against `npm run dev`/`preview` in `scribe/frontend/` — do not deploy to verify. | |
| ## Do NOT chase these (verified non-issues) | |
| Full-page/element screenshots of this site produce artifacts because of the sticky nav and | |
| GSAP scroll animations. Already confirmed fine: | |
| - Hero "Audio → SOAP" strip overhanging its card (`.hero__route--scribe { right: -3% }`) is an | |
| intentional accent. `document.scrollWidth - clientWidth === 0` at all tested widths — no | |
| horizontal overflow anywhere. | |
| - Grey band across the bottom of the safety bento in screenshots = GSAP scrub timeline | |
| (`scribe/frontend/src/landing/useLandingMotion.ts:45-68`) caught mid-state. Renders correctly live. | |
| - Scribe chapter body text is complete in source (`scribe/frontend/src/content/strings.ts:340`); the | |
| "truncation" seen in captures was the sticky nav bar overlaying a line during scroll-stitching. | |
| - Marquee is 48px tall and animates correctly; "clipped text" in captures is the same stitching | |
| artifact. | |
| --- | |
| ## P0-1: Lead contact path is dead on production | |
| **Symptom:** Footer "Liên hệ chương trình thí điểm" renders `href="mailto:"` (no recipient). | |
| Submitting the pilot lead form opens a recipient-less mail draft and then shows the success-ish | |
| "mail opened" status. The site's only conversion path silently goes nowhere. | |
| **Root cause:** Neither `VITE_LEAD_ENDPOINT` nor `VITE_LEAD_EMAIL` is set in the Vercel project. | |
| `submitLead()` falls through to `buildLeadMailto(payload, email = "")` | |
| (`scribe/frontend/src/leads.ts:88-130`). The deploy gate `scribe/frontend/scripts/validate-deploy-env.mjs` only | |
| validates `VITE_API_BASE` + `VITE_CONSOLE_URL`, so the build passed anyway | |
| (`scribe/frontend/vercel.json` runs `npm run validate:deploy && npm run build`). | |
| **Fix (all three parts):** | |
| 1. `scribe/frontend/scripts/validate-deploy-env.mjs`: require at least one lead channel — error if both | |
| `VITE_LEAD_ENDPOINT` and `VITE_LEAD_EMAIL` are empty. Extend | |
| `scribe/frontend/scripts/validate-deploy-env.test.mjs` accordingly (`npm run test:deploy-env`). | |
| 2. UI guard, belt-and-suspenders: | |
| - `scribe/frontend/src/leads.ts` / `scribe/frontend/src/LeadForm.tsx`: if no endpoint and no email, `submitLead` | |
| must not open `mailto:` and the form must show the existing error status (`labels.failed` | |
| path) instead of "mail opened". Never report success for a no-op. | |
| - `scribe/frontend/src/LandingPage.tsx:476-478`: don't render the footer mailto link when | |
| `VITE_LEAD_EMAIL` is empty (render the text non-linked, or drop it). | |
| - Existing tests mock `leadEmail`/`endpoint` props (`scribe/frontend/src/LeadForm.test.tsx`, | |
| `scribe/frontend/src/leads.test.ts`) — add the empty-config case. | |
| 3. Tell the user to set `VITE_LEAD_EMAIL` (and optionally `VITE_LEAD_ENDPOINT`) in the Vercel | |
| project settings — the value is a business decision; do not invent one. The code fix must be | |
| correct with or without it. | |
| **Accept when:** with no lead env vars, footer shows no dead link and form submit shows an error, | |
| not success; `npm run test` and `npm run test:deploy-env` pass; with `VITE_LEAD_EMAIL` set the | |
| mailto contains the recipient. | |
| ## P0-2: Mobile nav menu opens off-screen — links unusable on phones | |
| **Symptom:** At 390×844, opening the hamburger shows a white panel whose link labels are | |
| invisible. Measured panel box: `x = -108, width = 304` — a third of it (including all left-padded | |
| label text) is past the left viewport edge. At 900px the panel floats detached mid-nav | |
| (`x = 225`) under the centered button. | |
| **Root cause:** `scribe/frontend/src/styles.css:2267-2277` — `.site-nav__menu > div` is | |
| `position: absolute; right: 0` anchored to `.site-nav__menu` (`position: relative`, line 2248), | |
| i.e. to the 2.6rem hamburger itself, which sits mid-nav between the brand and the VI/EN toggle. | |
| The 19rem-wide panel extends leftward from there. | |
| **Fix:** Anchor the panel to the nav bar, not the button. `.site-nav` is `position: sticky` | |
| (styles.css:129-141) and therefore already a containing block for absolute descendants — the | |
| smallest fix is to drop `position: relative` from `.site-nav__menu` (line 2248) and give the | |
| panel a right offset matching the nav padding (`right: max(1.5rem, calc((100vw - var(--container)) / 2))` | |
| at ≥761px; `1rem`-ish at ≤760px — mirror the nav's own padding values). Keep | |
| `width: min(19rem, calc(100vw - 2rem))`. | |
| **Accept when:** at 320, 390, 760, and 900px widths the open panel's bounding box is fully inside | |
| the viewport, flush near the right edge below the bar, and every link label is visible. | |
| ## P0-3: Menu never closes after navigating | |
| **Symptom:** Tap hamburger → tap "An toàn" → page scrolls to the section but the `<details>` | |
| panel stays open, covering the content just navigated to. Verified: `.site-nav__menu[open]` | |
| still present after link click. | |
| **Root cause:** Native `<details>` doesn't close on child anchor clicks | |
| (`scribe/frontend/src/LandingPage.tsx:60-63`). | |
| **Fix:** In `LandingPage.tsx`, close the details when a menu link is clicked (e.g. `onClick` on | |
| the wrapping div or each link that clears the `open` attribute via a ref). Keep it a few lines — | |
| no state library, no outside-click handler unless it's free. | |
| **Accept when:** tapping any menu link scrolls to the section AND the panel is closed. | |
| ## P1-1: Evidence section headline wraps 2 words × 6 lines on desktop | |
| **Symptom:** At 1440px, "Xem cách mỗi sản phẩm giữ điểm cần duyệt ở đúng chỗ." renders 64px in a | |
| 379px-wide column → 6 cramped lines. Measured: `{w: 379, fs: 64px, lines: 6}`; still 4 lines at | |
| 1100px. Inside the carousel the slide `h3` (up to 2.6rem in a ~270px column, | |
| `.evidence-slide` col `minmax(13rem, 0.65fr)`, styles.css:2062-2094) wraps ~5 lines the same way. | |
| **Root cause:** `.evidence` grid gives the intro `minmax(16rem, 0.55fr)` of a 74rem container | |
| minus up to 8rem gap (styles.css:2049-2056), while `.section-intro h2` uses the global | |
| `clamp(2rem, 4.5vw, 4rem)` scale (styles.css:1615-1618) sized for full-width intros. | |
| **Fix:** Scoped override, not a layout rework — e.g. | |
| `.evidence .section-intro h2 { font-size: clamp(1.8rem, 2.2vw, 2.5rem); }` and similarly cap | |
| `.evidence-slide__copy h3` (~`clamp(1.4rem, 1.8vw, 1.9rem)`). Alternatively rebalance the column | |
| (`0.55fr → 0.75fr`) plus a smaller cap — pick whichever reads best live, but the acceptance bar | |
| is below. Don't touch other sections' headings. | |
| **Accept when:** at 1280–1600px the evidence h2 wraps ≤3 lines with ≥3 words per line, and each | |
| slide h3 wraps ≤3 lines; ≤1023px layouts (already fine) unchanged. | |
| ## P2 (do only after P0/P1; each is a 1–2 line change) | |
| - **Sticky nav dissolves into the page while scrolling.** `--nav-bg` ≈ page beige at 0.92 alpha | |
| with only a hairline border (styles.css:129-141), so the floating VI/EN pill + hamburger appear | |
| to sit directly on content text mid-scroll (most visible on mobile). Add a subtle | |
| `box-shadow` (or slightly stronger bottom border) to `.site-nav` so the bar reads as a surface. | |
| - **Marquee labels are 11.84px** (`.marquee__track span`, `font-size: 0.74rem`, | |
| styles.css:780-791) — uppercase microtext below the 12px floor; bump to `0.78rem`. | |
| - **Demo controls wrap ragged on mobile:** in `.demo__controls` | |
| (`scribe/frontend/src/demo/DemoPlayer.tsx:251`, styles for it in styles.css) "Phát lại" wraps to an orphan | |
| row at 390px. Make the wrap intentional (consistent gap; buttons full-width or evenly split at | |
| ≤760px — match `.product-chapter__actions`' existing pattern at styles.css:2429-2433). | |
| ## Verification (run all from `scribe/frontend/`) | |
| 1. `npm run lint`, `npm run test`, `npm run test:deploy-env` — all green. | |
| 2. `npm run build` — green WITH lead env vars; FAILS with a clear message when both lead vars are | |
| missing (new validator rule). `npm run dev` for visual checks. | |
| 3. Playwright probe (as above) against local dev at 390 / 760 / 900 / 1440px: | |
| menu panel fully in-viewport, closes on navigate; evidence h2 line counts within budget; | |
| `scrollWidth === clientWidth` (no new horizontal overflow at any width). | |
| 4. Both languages (VI default, EN toggle) and `#/scribe` route still render — `App.test.tsx`, | |
| `LandingPage.test.tsx`, `ScribeTool.test.tsx` cover regressions. | |
| 5. The build's `check-diacritics.mjs` runs inside `npm run build` — any copy you touch must keep | |
| Vietnamese diacritics intact. | |
| ## Out of scope | |
| Backend/`scribe/carepath`, the HF-space console (external `VITE_CONSOLE_URL` target), `interpreter/frontend/` | |
| (separate app), content rewrites, redesigns, new sections, dependencies. | |