Skip to content

LW-W5-R1: Web product UX completion + visual system - #7

Merged
PNHD merged 15 commits into
mainfrom
feature/lw-w5-product-ux
Aug 12, 2026
Merged

LW-W5-R1: Web product UX completion + visual system#7
PNHD merged 15 commits into
mainfrom
feature/lw-w5-product-ux

Conversation

@PNHD

@PNHD PNHD commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Final closeout — LW-W5-R1

W5 web UX completion is fully live-verified and ready to merge.

Key outcomes:

  • Visual foundation, shared interactive states, reduced-motion hook, responsive/desktop chrome alignment
  • Home CTA hierarchy fixed; /preview demoted to internal QA
  • Vietnamese four-direction address-term setup clarified
  • Story/choice/composer/Characters/Character Chat polish
  • Remember-in-story now reflects server truth after reload
  • Replay affordance density reduced
  • Quota/error states differentiated
  • Cross-guest Character Chat ownership errors now return clean 403 forbidden while preserving isolation
  • Hydration warning investigated across controlled local/production flows; not reproducible; permanent console-clean navigation regression coverage added

Validation:

  • IMPLEMENTATION_CODE_HEAD: 5bfcf750ddb5fce3c4cdb804482299da154016cd
  • FINAL_PR_HEAD: 32b328005baafd51cd113e8753e6c22397132244
  • Exact-head CI: 31571063852 — SUCCESS
  • Unit: 168/168
  • Playwright E2E: 12/12 across desktop + mobile projects
  • Supabase character-chat: v8 ACTIVE, verify_jwt: true
  • Production deployment: 53c0b39a-c06f-444a-9ccd-25b819490975
  • Stable production: https://lorewish.pages.dev
  • Native work untouched/deferred

Known non-blocking follow-ups: motion polish and manual assistive-technology pass.

Independent review completed. W5_R1_PASS.

PNHD and others added 15 commits August 11, 2026 20:27
Evidence-based walk of every current screen (desktop/375/360, EN+VI)
against docs/UX_CONTRACT.md and the new W5 spec. Classifies findings
P0-P3: unlabeled Home /preview CTA, unlabeled VI address-term summary,
Remember-in-story reload state (client-only, server already idempotent),
replay affordance density, missing hover/focus states, incomplete color
roles, unimplemented motion contract.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Visual foundation (Part 2): add warning/success/focus color roles and
motion duration tokens (src/theme/tokens.ts); add a shared
hover/focus-visible/press style helper (src/theme/interactive.ts) used
across the reading, setup, and composer Pressables so desktop finally
shows interactive affordance and keyboard focus is visible (P1-2/P1-3);
add useReducedMotion (react-native-web's AccessibilityInfo is already
matchMedia-backed, so one hook covers native + web).

Fixes three P0 findings from handoff/LW-W5-R1/ux-audit.md:
- Home no longer gives /preview equal weight to "Start a Story" — it's
  now a small internal QA link, not a second primary CTA.
- Vietnamese address-term summary in Advanced Setup now renders as four
  labeled rows (character calls you / calls themselves, you call
  character / call yourself) instead of an unlabeled
  "tôi · cậu · tôi · cậu" join of the same preset data.
- "Remember in story" now reflects server truth: the character-chat
  loadThread read now joins canon_facts on
  (source_chat_message_id, source_chat_candidate_index) — the same key
  lw_promote_chat_memory already uses for its existing idempotency
  check — and reports `promoted` per candidate, so a reload shows a
  stable "Remembered" state instead of re-offering promotion. No schema
  change, no client-only fake persistence.

Also thins the per-scene "Replay from here" control (P1-1): historical
scenes get a small secondary text-weight ReplayLink instead of a
full-size bordered button repeated after every scene in a long story;
the current scene's own forward action keeps full ActionButton weight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Story reader and Character Chat error/quota states all rendered in the
same default text color before this (P1-3) — now warning (a boundary
the player can act on or wait out: allowance exhausted, beta capacity,
offline, safety rejection) is visually distinct from danger (a genuine
provider/transport failure), using the tokens added in the previous
commit. Copy is unchanged; only color role assignment changes.

Character directory: drop the redundant top border on the first
character card (a section heading already separates it) and add
hover/focus to "Talk to character". Character Chat: the promoted
"Remembered in story" pill now renders in the success color with a
matching border instead of looking identical to the pre-promotion
state once disabled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every screen's header bar (back link + language switcher) spanned full
width edge-to-edge while its body content sat in a centered
readingWidth.maxContentWidth column below it — on desktop this is what
made the app read as "a mobile page stretched wide" rather than a
considered layout (P1-6). New shared ScreenHeaderBar/ScreenHeaderRow/
BackLink (src/components/screen-header-bar.tsx) keep the bar's
border/background full-bleed but center its content to the same
column width, and consolidate six near-identical header blocks
(run, new-story, characters, character-chat, account, preview) into
one component with hover/focus already wired in. Layout-only change,
verified at 1280px (header now left-aligns at x=320, matching the
content column) with no new horizontal overflow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
new-story.tsx and advanced-setup-form.tsx each defined an identical
selectable-pill Pressable (path tabs, language, genre, tone, POV) with
no hover/focus treatment. Extracted to src/components/choice-pill.tsx
(built on the theme/interactive helpers from the visual-foundation
commit) and reused from both call sites — same visual result, no
duplicated implementation, and pills now get the same hover/focus
affordance as every other interactive control in this pass. Also wires
hover/focus into the Quick/Advanced Setup "Start" button, the one
primary Pressable on that screen that still only branched on `pressed`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…itcher

ChoicePill and LanguageSwitcher's radios rendered around 28px tall on
mobile, short of the ~44px tap-target guidance (Part 19). Added hitSlop
instead of enlarging the visible pill, so restrained chrome (Part 8) is
unaffected but the touch target grows. LanguageSwitcher previously had
no hover/focus treatment at all, unlike every other Pressable touched
in this pass — brought it onto the same theme/interactive helpers.

Verified live via keyboard Tab: focused radios now show the custom
colors.focus outline (rgb(61,110,133) / #3D6E85, solid, 2px) instead of
the browser's default focus ring.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Home, Account, and Quick/Advanced Setup's top-level titles rendered as
plain text with no accessibilityRole="header", unlike Story/Characters/
Character Chat which already had it — meant screen-reader heading
navigation skipped three of the six main screens. Also verified the new
warning/success/focus color tokens against their surfaces: all pairs
are >=4.5:1 contrast (WCAG AA), light warning/background 4.74 being the
tightest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the end-to-end proof the spec asked for (Part 26): the mock
character-chat route now tracks a promotedCandidates Set keyed by
(message_id, candidate_index) — the same idempotency key
lw_promote_chat_memory uses — and the lw_promote_chat_memory mock
populates it, so "open" responses report `promoted` from that
server-truth mock instead of a hardcoded value. New test promotes a
candidate, asserts the button flips to "Remembered in story", reloads
the page, and asserts it's still "Remembered in story" rather than
reverting to the pre-promotion label. Runs on both desktop-chromium
and mobile-chromium (Pixel 7). Complements the existing
chat-memory-promotion.test.ts unit coverage of the pure mapping logic.

Full local CI verified green: typecheck, lint, 164 vitest unit tests,
web export, 10/10 Playwright e2e (both projects).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real bug found only by actually rendering the page: Home's "START A
STORY" button was wrapped in <Link href="/play" asChild><Pressable
style={fn}>. Link's child-cloning does not compose correctly with an
array-valued style function — the button painted at near-zero opacity
(confirmed via screenshot, both desktop and mobile). Every other
primary button in the app already uses a plain Pressable +
router.push(), which composes fine (verified: "Talk to character" and
the Advanced Setup "Start" button both render correctly with the same
pattern) — switched Home to match. Also stopped washing out the
"Remembered in story" pill with disabledOpacity: it's a confirmed
state, not a low-emphasis disabled one, so it now stays at full
opacity and the success color reads clearly.

Adds scripts/capture-w5-screenshots.mjs: a standalone Playwright driver
(reusing the same deterministic route-mocking as
tests/e2e/roleplay-chat.spec.ts) that captures the Part 24 screenshot
set — desktop Home/Advanced-Setup/Story/Characters/Character-Chat,
mobile (Pixel 7) Home/Advanced-Setup-VI/Story/Character-Chat, plus an
allowance-exhausted state showing the new warning color — into
handoff/LW-W5-R1/screenshots/. Used because the interactive Browser
pane could not composite frames in this session (screenshot/click both
timed out); Playwright's own headless capture is unaffected and is
what actually caught the Home CTA bug above.

Full local CI re-verified green after both fixes: typecheck, lint, 164
unit tests, web export, 10/10 Playwright e2e (both projects).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Documents the evidence gathered during implementation: before/after
usability rationale per surface (visual-review.md, including the Home
CTA bug found only by actual screenshot capture), keyboard-focus and
contrast verification (accessibility-review.md), desktop chrome
alignment and no-overflow checks at 1280/412/375/360px
(responsive-review.md), and the automated + manual browser QA record
including the interactive-pane compositing limitation encountered
mid-session (browser-e2e.md). Raw typecheck/lint/vitest and web-export/
Playwright output captured in test-results.txt and ci-results.txt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Master handoff summary (A-Z per the task template), production-smoke.md
recording that deploy is deliberately deferred pending the user's
separate go-ahead (asked directly; declined for now), and the git
status/log/diff snapshots as of this point in the branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Live closeout per independent review: character-chat was still v6 on
sfarcofvqfeobtcizxyv (pre-W5), so the P0-3 fix was not actually live
despite being merged and tested. This commit records the resolution,
no source code changed:

- character-chat redeployed v6 -> v7 (only function depending on the
  changed shared files, per dependency-graph check; submit-turn and
  replay-branch untouched). verify_jwt unchanged. Downloaded deployed
  source verified byte-identical to committed source.
- Actual Cloudflare Pages production deployed from HEAD 9108b02,
  confirmed via `wrangler pages deployment list` (Environment:
  Production, Branch: main, not inferred from URL).
- Remember-in-story fix proven end-to-end against real backend + real
  DeepSeek generation: real guest, real VI story respecting the
  configured address register, real Character Chat, real promotion,
  server truth confirmed via direct API call, and confirmed to survive
  an actual browser reload. Also verified: no duplicate canon_facts
  row on repeat promotion, cross-guest access denied, unauthenticated
  access denied.
- Full 14-item production smoke checklist recorded, 5 real provider
  calls used (within the 8-call budget).

Updated: production-smoke.md (full live-verification record),
HANDOFF.md (corrected verdict, IMPLEMENTATION_CODE_HEAD vs
FINAL_PR_HEAD distinction), ci-results.txt (exact-head CI run
appended), git-status.txt, git-log.txt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records both exact-head CI runs: 31553617939 on 9108b02 (the last
source commit, deployed and live-verified) and 31555941274 on this
docs commit's parent, ebfc2aa (evidence-only). No source changed
between the two — confirmed by diffstat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LW-W5-R1-R1 blocker 2. Character Chat's open()/send() threw the raw
Postgres exception text ("chat thread: run not owned" etc.) on a
cross-owner access attempt, which matched neither the "unauthenticated"
nor "forbidden" literal strings the edge function's catch block checks
for, so it fell through to a generic 500 internal_error. Access was
already correctly denied either way (confirmed live in the prior
closeout: no data crossed the guest boundary) — only the error shape
was wrong.

Every cross-owner RPC guard in the character-chat migrations already
raises SQLSTATE 42501 (insufficient_privilege) consistently. Added
ownership-error.ts (Deno-import-free, unit tested) mapping that code to
the literal "forbidden" message loadThread()'s own inline check already
used — reused at both open()'s and send()'s RPC call sites. No schema
change, no ownership check weakened, just a more precise error
classification using the SQLSTATE Postgres already provides instead of
matching against exception message text.

Also adds a regression test for LW-W5-R1-R1 blocker 1 (the reported
React hydration console error): after exhaustive independent
reproduction across 5 methods (local dev server, exact new-story submit
flow, and three passes against real production — including the one
sequence not previously tried, switching UI language mid-session before
a real submission) the error could not be reproduced. Current
client-side navigation (Home<->Setup, EN/VI switch+persistence, real
browser back/forward) is verified console-clean; a permanent Playwright
assertion now locks this in as a regression guard.

168/168 unit tests, 12/12 e2e (both projects), typecheck/lint clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records the second live-closeout pass: character-chat redeployed
v7->v8 with the cross-guest 42501->403 mapping fix, Cloudflare
production redeployed from 5bfcf75 and reconfirmed via deployment
metadata, hydration investigation findings (not reproducible under 5
independent methods), and the corrected verdict distinguishing
IMPLEMENTATION_CODE_HEAD (5bfcf75, last source change) from
FINAL_PR_HEAD (this evidence commit — its exact SHA is reported in the
closing chat message and git-log.txt/git-status.txt rather than
predicted in HANDOFF.md, avoiding the exact staleness this instruction
was issued to fix).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@PNHD
PNHD force-pushed the feature/lw-w5-product-ux branch from 8db0572 to 32b3280 Compare August 12, 2026 06:53
@PNHD
PNHD marked this pull request as ready for review August 12, 2026 06:53
@PNHD
PNHD merged commit 8113e92 into main Aug 12, 2026
6 checks passed
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.

1 participant