Repository navigation
Nest What's New's release date under its section heading in both Settings shells - #2547
Conversation
Settings' What's New rendered its release date as an <h3>. On the phone drill-in that sits under the section's <h2> dialog header, but the wide shell heads each section with an <h3> pane title, so there the date was a sibling of "Updates" rather than inside it, and the generated release sections hung off the date instead of the pane. No fixed markup fits both shells, so each shell now passes SectionBody the level of the heading it puts over a section (required, 2 | 3), and What's New sets its date one level under it and its release sections one under the date: h3/h4 on the phone (unchanged), h4/h5 in the wide pane. gen-release-notes.mjs compiles the Settings notes against a required headingLevel prop (4 | 5) instead of a literal, applying the same heading shift it used to bake in; the /changelog output is byte-identical. The date heading now sets its own font size, weight, and colour, and the notes' heading rules cover every level the shift can reach, so the tag change moves no pixels: 48 Chromium captures of every Settings section in both shells, light and night, decode to identical RGBA before and after. settings-whats-new-outline.spec.ts pins the date and release sections one and two levels under each shell's own section heading; the wide case fails on the previous markup. Refs #2529 Refs #2500 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drafted follow-ups (not filed)Drafts for the owner to review. Nothing below is filed or implemented in this PR. 1. Parked decision: one heading level for a Settings section title in both shells (option B)Yours to make. The rival reclassified it under A.1 (it changes the screen-reader hierarchy The problem this PR found while grounding F1. Every Settings section body sits under a heading
Each section body hard-codes its own sub-heading levels against one or the other, so some sections Outline rendered on main 18869d2 (Chromium, wide 1280×900, phone
axe does not catch either kind of error in CI. Options.
The full six-part walkthrough, with the rival's position, is kept with the campaign's unit 2. No drift check that committed generated release notes match the generator
🤖 Generated with Claude Code |
The hub rows idle-mount and fly in, so a single click can land before the row is wired; openHubSection retries until the section's own field shows. Refs #2529 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KyleMit
left a comment
There was a problem hiding this comment.
Independent review by the codex rival agent, round 1, of pull request 2547.
No actionable defects found. Reviewed the full diff, CSS changes, required literal-union props, type-only imports, shell heading agreement, and staged-reveal assertions. All 21 generator tests, both settled component tests, npm run check, and knip passed. Both Chromium outline tests passed through the handler on port 5311. An additional 72-case matrix confirmed heading offsets, h6 capping, inline markup, and escaped entities match the prior nesting algorithm; all fixtures compiled. Regenerated CurrentReleaseNotes and ReleaseHistory matched existing bytes, and ReleaseHistory is untouched by the diff. The supplied pixel-parity evidence was considered; screenshots were not independently recaptured. The agreed shell-driven approach appears sound.
Refs #2529 (campaign tracking issue) · Refs #2500 (source: leftovers comment, follow-up 1)
Free-form campaign unit F1 of
ship-campaign parallel=3. There is no issue to close.Spec
In Settings' What's New, the release date renders as
<h3 class="whats-new-heading">. That is thesame level as the wide shell's pane title (
<h3 class="settings-pane-title">, "Updates"), so ascreen reader's heading list shows the date next to "Updates" instead of under it.
Done when:
Why the fix passes a level from each shell
WhatsNewSectionmounts throughSectionBodyin two shells, each with a different heading aboveit:
<h3>(WideShell.svelte:349)<h2>(SettingsModal.svelte:189)No fixed markup inside
WhatsNewSectionworks in both shells. On the phone its first heading mustbe h3 or higher, or it skips a level under the
<h2>. In the wide pane it must be h4 or lower tonest under the
<h3>. The generated release sections are pinned exactly one level below the date,so they have to follow it. Each shell therefore passes the level it provides.
Rendered outline in the wide shell (dumped from the running app; the phone outline is unchanged):
Changes
SectionBody.svelte:19: newexport type SectionHeadingLevel = 2 | 3. SectionBody now takes arequired
sectionHeadingLevelprop (:48) and forwards it (:58), the same way it alreadyforwards
onSettled.WideShell.svelte:357passes3;SettingsModal.svelte:196(phone drill-in) passes2. Bothproduction callers pass it, so there is no default.
WhatsNewSection.svelte::25);Record<SectionHeadingLevel, …>(:30-34) to the date tag(
<svelte:element>) and the release-notes level (4 | 5);.whats-new-heading(:95) now sets font size, weight and colour itself, so the element'scomputed style does not depend on the tag;
:107,:114) cover every level the shift can reach.ReleaseSectionHeading.svelte: thelevelunion goes from3 | 4to3 | 4 | 5.tools/release/gen-release-notes.mjs:headingLevel: 4 | 5prop(
CURRENT_RELEASE_SECTION_HEADING_LEVELS,:39) instead of the literal4;renderCurrentReleaseMarkdown/currentReleaseHeadingLevel(:152-168) apply the same shiftnestMarkdownHeadingsbakes in, at render time and capped at h6. That covers non-vocabulary#/###+ headings too./changelogstill uses the static path and is unchanged.CurrentReleaseNotes.svelte: regenerated withnpm run gen:releases, not edited by hand.WhatsNewSection.headingOutline.test.tsruns both levels (it.each([2, 3]));gen-release-notes.test.mjspins the new emitted markup, including the#,###and#####cases;
web/tests/settings-whats-new-outline.spec.tscovers both shells.Pixel proof: zero changed pixels
Method.
reducedMotion: 'reduce',animations: 'disabled',caret: 'hide') captured 48 PNGs: all 12 wide-shell sections as element shots, the widemodal parked on Updates, and all 11 phone drill-ins except Parent Center. Each set was captured
in light and night.
sharpand counted pixels that differ inany channel.
Runs.
build-number text (
Version 1.6.2317vs1.6.2327) differs between the two main commits, andit flagged exactly those two shots. For that reason, the "after" shots were captured with the
change applied, uncommitted, on the same HEAD as the base shots. That keeps the version string
equal.
tag. They are now set explicitly on
.whats-new-heading, and the zero diff confirms nothingmoved.
Tests and negative controls
settings-whats-new-outline.spec.tsasserts, in both shells, that the date is at the sectionheading's level + 1 and every release section at + 2 (
getByRole('heading', { level })).level: 4… element(s) not found") and the phonetest passes, as the bug predicts.
heading-orderrule could not catch this. A heading at its parent's level skipsnothing, and the rule's
moderateimpact is belowa11y.spec.ts's serious/critical gate.WhatsNewSection.headingOutline.test.tswith the old component restored fails both cases.headingLevel.npm run gen:releases:git diff --exit-code -- web/src/lib/components/page/ReleaseHistory.svelte web/src/lib/releases.json fastlane/exits 0, so
/changelog's generated output and the store changelogs are byte-identical;CurrentReleaseNotes.svelteequals the committed file, and the tree is cleanafter the two builds, which re-run
gen:releasesinprebuild;web/src/lib/releaseSections.test.ts, the vocabulary drift guard, passes;prebuildregeneratesthem).
Startup bundle (
/modulepreload, measured from the release builds)npm run buildnpm run build:capThe same chunks are preloaded, and each keeps its size except one native chunk (−4 B). Only
content hashes changed, because the lazy Settings chunks that startup chunks name by hash changed.
Commands run
npm run checknpm run lint,lint:css,lint:dead,lint:tokens,format:checkvitestgen-release-notes.test.mjsvitestWhatsNewSection.headingOutline.test.ts,releaseSections.test.tsSPLOTCH_E2E_PORT=5311 npm run test:e2e -- settings-whats-new-outline.spec.ts --workers=1SMOKE_PORT=5312 npm run test:browserlessnpm run build,npm run build:cap(branch and base)Decision records (
walk-through-decision mode=autonomous)<h2>to match the phone, and move Sound's and Tool Drawer'sRuleLabel as="h4"toh3.The rival (Codex) disagreed:
Settings where "Settings" stops being the parent of the section titles;
exclude it).
comment for the per-section evidence. If B is chosen later, this PR's
sectionHeadingLevelbecomes a constant and should be deleted.
Review and merge path
findings, none unverified. The rival reran the generator tests, the component tests,
npm run check, knip, and both outline tests (on port 5311, through the broker). It also ran a72-case matrix confirming the render-time heading shift matches the old static nesting, and
regenerated both release-notes components byte-identically. Round 2 was skipped because round 1
changed no code.
reconcile-with-mainsurvey verdictunrelated: no shared file, noimported upstream change, no convention change. The upstream diff was docs, a skill note, and
web/tests/flows-ai.spec.ts. main 3a34443 was merged as2fb21fc.
npm run check,npm run lint, and this branch's unitand generator tests passed on the merge.
Authorization
This is a
ship-campaign parallel=3unit run undership-issue mode=autonomous. The user's mergeapproval, verbatim: "Merge approved throughout the season". The orchestrator performs the merge
after this lane reports ready.
🤖 Generated with Claude Code