From 7271de4c72551e85e77473c57b5a0acc0d6ffe2e Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:55:35 -0700 Subject: [PATCH] fix(v2): give the zh hero suffix its own line on a phone (TASK-213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lily-shen ruled option 1 on TASK-212 (2026-09-30 00:23Z). At 320-414 the zh h1 measures 94.6px under three of the four rotator terms and 140.6px under the fourth, so the lede, the CTAs and every section below move 46px once per rotation cycle. A block suffix at <=680 makes the height the same under every term. The rule is one line in the phone block. `margin-left: 0` is part of it, not tidying: the base rule's .18em is the inline separator between the term and the suffix, and on its own line it would indent the suffix 7.9px. Two TASK-211 obligations ride along, both already recorded on that row and both in these same two files: the CSS comment compared a viewport x against the column width (对 fitted at 348.5; 话 is 44.3px wide and would have ended 26.8px past the 366 edge, which is the real mechanism of the split), and the hero test's language restore is now in a finally so a red assertion there cannot leave the suite in zh. TASK-211's nowrap stays: the media rule sets display, so the two compose. --- .../v2/__tests__/v2-layout-invariants.test.ts | 30 +++++++++++++-- .../__tests__/landingHeroContent.test.tsx | 37 ++++++++++++------- frontend/src/v2/landing/v2-landing.css | 18 ++++++++- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 9d58a2612..7ed4da432 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -1467,10 +1467,11 @@ describe('v2 layout invariants (CSS rule presence)', () => { test('the zh hero title suffix cannot be split across lines', () => { // TASK-211. The zh hero reads 「与你的___对话」: the rotator swaps the object // and 对话 trails it. At 390px with the "Claude Code" term the suffix - // straddled the wrap — 对 ended line 2 at x=348.5 in a 342px column and 话 - // sat alone on line 3 (measured live; at 320 the 整个团队 term did the same). - // `white-space: nowrap` is what produces the measured fix, so that is what - // is pinned here — jsdom has no line boxes, and the browser is the gate. + // straddled the wrap — the h1's content box ran 24 → 366, 对 fitted + // (348.5), and 话 is 44.3px wide and would have ended 26.8px past the edge, + // so 话 sat alone on line 3 (measured live; at 320 the 整个团队 term did the + // same). `white-space: nowrap` is what produces the measured fix, so that is + // what is pinned here — jsdom has no line boxes, and the browser is the gate. // The companion assertion that the suffix is rendered as its own element // (so this rule has something to bind to) lives in // landingHeroContent.test.tsx; a declaration pin alone would survive the @@ -1478,6 +1479,27 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(ruleBody(landing, '.v2-landing__title-suffix')).toContain('white-space: nowrap'); }); + test('the zh hero suffix takes its own line on a phone', () => { + // TASK-213. Even with the suffix unbreakable (the test above), the zh h1 + // gains a line whenever a term is long enough to push 对话 onto a third + // line: at 320–414 it measures 94.6px under three terms and 140.6px under + // the fourth, so the lede, the CTAs and everything below move 46px once per + // rotation cycle. lily-shen ruled option 1 (2026-09-30 00:23Z): the suffix + // is a block at ≤680, which makes the height the same under every term. + // Scoped with `mediaAt` rather than a whole-sheet read or an `indexOf`: the + // sheet has one (max-width: 680px) block today, and the helper is what makes + // a later one unable to satisfy this assertion by accident — `mediaAt` + // returns '' when the at-rule is absent, so a dropped block reds rather than + // silently matching some other rule. The declaration pin binds to the + // browser gate in ux-lead's sweep; `margin-left: 0` is asserted separately + // from `display: block` because it is what puts 对 on the column's left + // edge (the base rule's .18em indent would otherwise move with it). + const phone = mediaAt(landing, '@media (max-width: 680px) {'); + expect(phone).not.toBe(''); + expect(selectorRuleBody(phone, '.v2-landing__title-suffix')).toContain('display: block'); + expect(selectorRuleBody(phone, '.v2-landing__title-suffix')).toContain('margin-left: 0'); + }); + test('reaction chips baseline-align emoji ink with the count (not box-centering)', () => { // align-items: center centers the spans' layout boxes, but Apple Color // Emoji ink extends below the baseline while digit ink does not, so the diff --git a/frontend/src/v2/landing/__tests__/landingHeroContent.test.tsx b/frontend/src/v2/landing/__tests__/landingHeroContent.test.tsx index c376b4eac..830bcf5c7 100644 --- a/frontend/src/v2/landing/__tests__/landingHeroContent.test.tsx +++ b/frontend/src/v2/landing/__tests__/landingHeroContent.test.tsx @@ -134,19 +134,28 @@ describe('V2LandingPage hero content (TASK-152)', () => { expect(zhSuffix.length).toBeGreaterThan(0); await act(async () => { await i18n.changeLanguage('zh-CN'); }); - const zh = renderLanding(); - const node = zh.container.querySelector('.v2-landing__title-suffix'); - expect(node).not.toBeNull(); - expect(node?.textContent).toBe(zhSuffix); - expect(node?.closest('h1.v2-landing__title')).not.toBeNull(); - zh.unmount(); - - // en ships an empty suffix, so no span is rendered there and the rule is - // zh-only by construction — this is the control that keeps the pin from - // quietly becoming an English-hero assertion. - await act(async () => { await i18n.changeLanguage('en'); }); - const en = renderLanding(); - expect(en.container.querySelector('.v2-landing__title-suffix')).toBeNull(); - en.unmount(); + try { + const zh = renderLanding(); + const node = zh.container.querySelector('.v2-landing__title-suffix'); + expect(node).not.toBeNull(); + expect(node?.textContent).toBe(zhSuffix); + expect(node?.closest('h1.v2-landing__title')).not.toBeNull(); + zh.unmount(); + + // en ships an empty suffix, so no span is rendered there and the rule is + // zh-only by construction — this is the control that keeps the pin from + // quietly becoming an English-hero assertion. + await act(async () => { await i18n.changeLanguage('en'); }); + const en = renderLanding(); + expect(en.container.querySelector('.v2-landing__title-suffix')).toBeNull(); + en.unmount(); + } finally { + // TASK-213: the language is the suite's ambient state, so the restore has + // to survive a failure above it. Unguarded, a red assertion in here left + // every later test rendering zh, and their English misses read as defects + // in the page rather than in this test (found by sprint-review on #2033, + // reproduced with a forced-failure probe). + await act(async () => { await i18n.changeLanguage('en'); }); + } }); }); diff --git a/frontend/src/v2/landing/v2-landing.css b/frontend/src/v2/landing/v2-landing.css index bc5f4d95a..3b2814613 100644 --- a/frontend/src/v2/landing/v2-landing.css +++ b/frontend/src/v2/landing/v2-landing.css @@ -63,8 +63,10 @@ margin-left: 0.18em; /* TASK-211: the zh hero is 「与你的___对话」, so this suffix is a word the reader has to see whole. At 390 with the "Claude Code" term the two - characters straddled the wrap — 对 ended line 2 at x=348.5 in a 342px - column and 话 sat alone on line 3. nowrap makes the suffix one unbreakable + characters straddled the wrap: the h1's content box ran 24 → 366, so 对 + fitted (348.5, i.e. 17.5px inside the right edge) and it was 话 — 44.3px + wide, which would have ended at 392.8, 26.8px past it — that sat alone on + line 3. nowrap makes the suffix one unbreakable unit: it moves to the next line as a pair instead of splitting. Measured live at 390 (44px/46px) and again at 320, where the 整个团队 term split it the same way. en ships an empty suffix and renders no span, so this is @@ -1250,6 +1252,18 @@ .v2-landing__footer { padding-top: 32px; } .v2-landing__footer-legal { padding: 16px 0 20px; } .v2-landing__title { font-size: 44px; line-height: 46px; } + /* TASK-213 (lily-shen's ruling, option 1): on a phone the suffix takes its own + line, so the title is 「与你的 / / 对话」 at every width and only the + term rotates. Without this the h1 is 94.6px under three of the four terms + and 140.6px under the fourth at 320–414, so the lede, the CTAs and every + section below jumped 46px once per cycle (measured on main abe19fe0 and + again on #2033). `margin-left: 0` is part of the rule rather than tidying: + the base rule's .18em is the inline separator, and on its own line it would + indent 对 by 7.9px. Specificity is the base rule's (0,1,0) and this block is + later in the sheet, so it wins by order — the same mechanism the TASK-205 + scroll-padding-top above documents. TASK-211's nowrap still applies here; + this rule changes `display`, so the two compose. */ + .v2-landing__title-suffix { display: block; margin-left: 0; } .v2-landing__lede { font-size: 16px; line-height: 26px; } .v2-landing__nav { gap: 14px; } .v2-landing__nav .v2-landing__navlink { display: none; }