Skip to content

Nest What's New's release date under its section heading in both Settings shells - #2547

Merged
KyleMit merged 3 commits into
mainfrom
claude/cq5-whats-new-heading-level
Sep 30, 2026
Merged

KyleMit merged 3 commits into
mainfrom
claude/cq5-whats-new-heading-level

Conversation

@KyleMit

@KyleMit KyleMit commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

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 the
same level as the wide shell's pane title (<h3 class="settings-pane-title">, "Updates"), so a
screen reader's heading list shows the date next to "Updates" instead of under it.

Done when:

  • the date sits one level under its section's heading in every Settings shell;
  • New/Improved/Fixed sit one level under the date;
  • no pixel changes (the user's requirement);
  • a test pins the outline and fails on the old markup.

Why the fix passes a level from each shell

WhatsNewSection mounts through SectionBody in two shells, each with a different heading above
it:

Shell Heading over the section body Date before Date after New/Improved/Fixed after
Wide (tablet/desktop) pane title <h3> (WideShell.svelte:349) h3 (bug) h4 h5
Phone drill-in dialog header <h2> (SettingsModal.svelte:189) h3 h3 h4 (unchanged)
Compact landscape phone never renders a section body – – –

No fixed markup inside WhatsNewSection works in both shells. On the phone its first heading must
be h3 or higher, or it skips a level under the <h2>. In the wide pane it must be h4 or lower to
nest 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):

   h3 Updates
-  h3 September 9, 2026
-    h4 New
-    h4 Improved
-    h4 Fixed
+    h4 September 9, 2026
+      h5 New
+      h5 Improved
+      h5 Fixed

Changes

  • SectionBody.svelte:19: new export type SectionHeadingLevel = 2 | 3. SectionBody now takes a
    required sectionHeadingLevel prop (:48) and forwards it (:58), the same way it already
    forwards onSettled.
  • WideShell.svelte:357 passes 3; SettingsModal.svelte:196 (phone drill-in) passes 2. Both
    production callers pass it, so there is no default.
  • WhatsNewSection.svelte:
    • takes the required prop (:25);
    • maps it through a closed 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's
      computed style does not depend on the tag;
    • the body's heading rules (:107, :114) cover every level the shift can reach.
  • ReleaseSectionHeading.svelte: the level union goes from 3 | 4 to 3 | 4 | 5.
  • tools/release/gen-release-notes.mjs:
    • the Settings notes compile against a required headingLevel: 4 | 5 prop
      (CURRENT_RELEASE_SECTION_HEADING_LEVELS, :39) instead of the literal 4;
    • renderCurrentReleaseMarkdown/currentReleaseHeadingLevel (:152-168) apply the same shift
      nestMarkdownHeadings bakes in, at render time and capped at h6. That covers non-vocabulary
      #/###+ headings too.
    • /changelog still uses the static path and is unchanged.
  • CurrentReleaseNotes.svelte: regenerated with npm run gen:releases, not edited by hand.
  • Tests:
    • WhatsNewSection.headingOutline.test.ts runs both levels (it.each([2, 3]));
    • gen-release-notes.test.mjs pins the new emitted markup, including the #, ### and #####
      cases;
    • new web/tests/settings-whats-new-outline.spec.ts covers both shells.

Pixel proof: zero changed pixels

Method.

  • A throwaway Playwright spec (Chromium, reducedMotion: 'reduce', animations: 'disabled',
    caret: 'hide') captured 48 PNGs: all 12 wide-shell sections as element shots, the wide
    modal parked on Updates, and all 11 phone drill-ins except Parent Center. Each set was captured
    in light and night.
  • A small Node script decoded each PNG to raw RGBA with sharp and counted pixels that differ in
    any channel.

Runs.

Comparison Result
base (077c34f) vs this change, run 1 48 compared, 0 differ
base vs this change, run 2 48 compared, 0 differ
change run 1 vs change run 2 (determinism) 48 compared, 0 differ
earlier base 18869d2, run 1 vs run 2 (determinism) 48 compared, 0 differ
18869d2 vs 077c34f (positive control) 2 differ: About, 470/474 px
  • Positive control. The comparator catches small text changes. The About section's
    build-number text (Version 1.6.2317 vs 1.6.2327) differs between the two main commits, and
    it 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.
  • UA defaults. The rival flagged that the date heading's own UA font size and weight vary by
    tag. They are now set explicitly on .whats-new-heading, and the zero diff confirms nothing
    moved.
Wide · light Before After
Updates pane before after
Wide · night Before After
Updates pane before after
Phone drill-in Before After
Light before after
Night before after

Tests and negative controls

  • settings-whats-new-outline.spec.ts asserts, in both shells, that the date is at the section
    heading's level + 1 and every release section at + 2 (getByRole('heading', { level })).
    • On the base markup, the wide test fails ("level: 4 … element(s) not found") and the phone
      test passes, as the bug predicts.
    • On this change both pass.
    • axe's heading-order rule could not catch this. A heading at its parent's level skips
      nothing, and the rule's moderate impact is below a11y.spec.ts's serious/critical gate.
  • WhatsNewSection.headingOutline.test.ts with the old component restored fails both cases.
    • Level 3: "expected 3 to be 4".
    • Level 2: the generated notes now require headingLevel.
  • Generator. After 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;
    • the regenerated CurrentReleaseNotes.svelte equals the committed file, and the tree is clean
      after the two builds, which re-run gen:releases in prebuild;
    • web/src/lib/releaseSections.test.ts, the vocabulary drift guard, passes;
    • CI has no separate check that generated release notes are current (prebuild regenerates
      them).

Startup bundle (/ modulepreload, measured from the release builds)

Build main (077c34f) this branch Δ
npm run build 40 chunks, 371,098 B (140,017 B gz) 40, 371,098 B (140,022 B gz) 0 chunks, 0 B raw, +5 B gz
npm run build:cap 28 chunks, 371,173 B (138,465 B gz) 28, 371,169 B (138,462 B gz) 0 chunks, −4 B raw, −3 B gz

The 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

Command Result
npm run check 0 errors, 0 warnings
npm run lint, lint:css, lint:dead, lint:tokens, format:check all exit 0
vitest gen-release-notes.test.mjs 21 passed
vitest WhatsNewSection.headingOutline.test.ts, releaseSections.test.ts 3 passed
SPLOTCH_E2E_PORT=5311 npm run test:e2e -- settings-whats-new-outline.spec.ts --workers=1 2 passed
SMOKE_PORT=5312 npm run test:browserless all 5 checks passed
npm run build, npm run build:cap (branch and base) exit 0

Decision records (walk-through-decision mode=autonomous)

  1. Mine to make, locked on A: converged; I adopted the rival's pick.
    • Question: how to put the date under its section heading in every shell.
    • Options:
      • (A) pass the heading level from each shell into What's New and its generated notes;
      • (B) make the wide pane title <h2> to match the phone, and move Sound's and Tool Drawer's
        RuleLabel as="h4" to h3.
    • Rival outcome: my first pick was B, on criteria B3/B2 (one uniform rule, three files).
      The rival (Codex) disagreed:
      • B is the user's call under A.1, as a screen-reader hierarchy change across all of wide
        Settings where "Settings" stops being the parent of the section titles;
      • and under A.7, as it expands a What's New repair to six other sections;
      • its pick was A, a level parameter serving two real production callers (so B5 does not
        exclude it).
    • Resolution: I conceded. The rival's two factual corrections were accepted and resolved:
      • B also needed its tag-qualified selectors moved;
      • A's pixel parity had to be proven, and it is proven above.
    • Reverse: revert this PR.
  2. Parked for the user: B, the Settings-wide heading outline. See the drafted follow-ups
    comment for the per-section evidence. If B is chosen later, this PR's sectionHeadingLevel
    becomes a constant and should be deleted.

Review and merge path

  • Rival review, round 1 (Codex, head 07aca54): no
    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 a
    72-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.
  • Per-unit merge gate: reconcile-with-main survey verdict unrelated: no shared file, no
    imported upstream change, no convention change. The upstream diff was docs, a skill note, and
    web/tests/flows-ai.spec.ts. main 3a34443 was merged as
    2fb21fc. npm run check, npm run lint, and this branch's unit
    and generator tests passed on the merge.

Authorization

This is a ship-campaign parallel=3 unit run under ship-issue mode=autonomous. The user's merge
approval, verbatim: "Merge approved throughout the season". The orchestrator performs the merge
after this lane reports ready.

🤖 Generated with Claude Code

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>
@KyleMit

KyleMit commented Sep 30, 2026

Copy link
Copy Markdown
Owner Author

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
across all of wide Settings) and A.7 (it goes beyond this unit). This PR fixes What's New only.

The problem this PR found while grounding F1. Every Settings section body sits under a heading
its shell provides, but the two shells provide different levels:

  • the wide pane title is an <h3> (WideShell.svelte:349);
  • the phone drill-in's dialog header is an <h2> (SettingsModal.svelte:189).

Each section body hard-codes its own sub-heading levels against one or the other, so some sections
are wrong in each shell.

Outline rendered on main 18869d2 (Chromium, wide 1280×900, phone
460×852):

Section Sub-heading (source) Wide shell (title h3) Phone drill-in (title h2)
Sound h4 "What makes sound" (SoundSection.svelte:70) correct skips h3 (h2 → h4)
Tool Drawer h4 "Drawing Tools" (ControlsSection.svelte:105) correct skips h3 (h2 → h4)
AI Art h3 "What turning this on does" (AiValueProp.svelte:36) sibling of its title correct
Parent Center h3 × 5 feature labels (ParentCenterSection.svelte:120) siblings of its title correct (from source)
Install h3 "Desktop" (SetupInstructions.svelte:235) sibling of its title correct
Feedback h3 "Send Feedback" (ReportForm.svelte:96) sibling of its title correct
What's New h3 date → h4 sections (WhatsNewSection.svelte:52) sibling, fixed by this PR correct

axe does not catch either kind of error in CI. heading-order is moderate, below
a11y.spec.ts's serious/critical gate, and it flags only skips, never a sub-heading at its
parent's level.

Options.

  • B (the handler's first pick).
    • Change: make WideShell's pane title <h2>, matching the phone. Move Sound's and Tool
      Drawer's RuleLabel as="h4" to as="h3", along with their tag-qualified margin selectors
      (SoundSection.svelte:110, ControlsSection.svelte:166).
    • Result: every section body is authored once against an h2, both shells are correct, and one
      Playwright spec can walk every section in both shells.
    • No pixels should change: .settings-pane-title and RuleLabel already set every
      property the UA stylesheet varies by level. This still needs a capture to prove it.
    • Cost: in the wide shell, the "Settings" dialog title (drawn larger) becomes a peer of the
      section titles rather than their parent.
    • If picked, this PR's sectionHeadingLevel becomes a constant and should be deleted.
  • Generalize this PR's approach.
    • Change: every section body reads its shell's heading level (for example through a Svelte
      context the shells set) and renders its sub-headings relative to it.
    • Result: "Settings" stays the parent in wide. About seven heading sites become dynamic, and every
      future section heading has to follow the rule.
  • Leave it: keep today's split, with five wide sections wrong and two phone sections skipping a
    level.

The full six-part walkthrough, with the rival's position, is kept with the campaign's unit
records.

2. No drift check that committed generated release notes match the generator

CurrentReleaseNotes.svelte and ReleaseHistory.svelte are committed generated output. They are
regenerated by prebuild, but no CI step fails when the committed copy differs from what
npm run gen:releases produces. Other generated files have that check: gen:tokens:check in
test.yml. Low priority: prebuild regenerates before every build, so a stale committed copy
cannot ship. It can only mislead a reader of the diff.

🤖 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 KyleMit left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@KyleMit
KyleMit merged commit a1968c8 into main Sep 30, 2026
18 checks passed
@KyleMit
KyleMit deleted the claude/cq5-whats-new-heading-level branch September 30, 2026 05:42
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