build(frontend): fail css-check on a nested rule the phone drops #180

Merged
logan merged 2 commits from fix/154-nested-css-check into main 2026-08-21 15:59:25 +00:00
Collaborator

The issue. The device renders in Chrome 113, which predates relaxed
CSS nesting (Chrome 120), so a nested rule whose selector starts with an
element name is not a parse error anyone would notice — it is silently
dropped, on the phone and only on the phone. Three were live in
index.css, and the one that mattered was the text-overflow: ellipsis
on the bottom bar's title and artist, which had therefore never
truncated on the device. Those three went with #23/#42; #154 is about
the class.

What changed. make css-check runs a second script,
frontend/scripts/check-css-nesting.mjs, over index.css and the css
literals in src/**/*.ts alike — a shadow-root stylesheet is parsed by
the same engine. It names the file, the line and the fix, which is a
leading &: valid in both syntaxes, so no nested rule here has a reason
to omit it. It is a lefthook pre-commit entry as well, beside the
existing css-literals one, and CI already runs make css-check.

The detection (css-nesting.mjs) walks blocks rather than matching
lines, and both things the issue asks it to get right fall out of one
rule — a rule is nested when a style rule is somewhere above it, not
when its immediate parent is a block:

  • @media (…) { bottom-nav { … } } at the top level is an ordinary rule
    and is not flagged. That is the majority of what a regex over the file
    would report.
  • The same rule inside .bar { @media (…) { … } } is nested and is
    flagged, at-rule in between or not.

Strings and comments are read through, so a brace in a url() is not a
block. The literal scanner the two checks now share is lifted into
css-literals.mjs unchanged, except that a ${} substitution is
blanked keeping its newlines so a line number survives it.

The tree has no violation left, so the check would pass just as happily
over an empty glob — it refuses one, in the spirit of
icon-language.test.ts's first assertion.

Verification tiers run.

  • make css-check — clean, and watched failing. Removing the three
    &s from index.css's bottom-bar block (the historical cases, lines
    282/289/293) reports exactly those three lines with the right
    selectors; a span { … } injected into page-header's css literal
    is reported at its own line, which exercises the literal path and the
    line mapping through it. Both reverts left the tree clean.
  • make ui-test991 passed, 7 of them the new
    frontend/test/utils/css-nesting.test.ts, which pins the at-rule
    distinction in both directions plus the string/comment handling.
  • npx tsc --noEmit — clean. make skill-check — clean.
  • The lefthook pre-commit and pre-push hooks ran on the commit and the
    push, including the new css-nesting entry.

Deliberately not done.

  • The alternative the issue asks to weigh first — a build-time downlevel
    (Lightning CSS or PostCSS targeting Chrome 113), which would fix the
    class permanently rather than reject it — is not taken. It is a
    dependency and a build step against twenty lines, and the issue's own
    Direction says the check is the version that matches how this repo has
    handled the other silent-on-one-platform traps. It is written down in
    CLAUDE.md as the expensive answer, so the trade survives this PR.
  • No device tier: none is attached, and none is needed to check a rule
    the check enforces about source text.
  • The sweep covers frontend/index.css and css literals. A .css
    file added elsewhere under frontend/ would not be swept.

CLAUDE.md gains a paragraph under the Chrome 113 section and the
yellowjacket-dev skill's make css-check bullet gains the second
check, since that file is the imperative half.

Closes #154

**The issue.** The device renders in Chrome 113, which predates relaxed CSS nesting (Chrome 120), so a nested rule whose selector starts with an element name is not a parse error anyone would notice — it is silently dropped, on the phone and only on the phone. Three were live in `index.css`, and the one that mattered was the `text-overflow: ellipsis` on the bottom bar's title and artist, which had therefore never truncated on the device. Those three went with #23/#42; #154 is about the **class**. **What changed.** `make css-check` runs a second script, `frontend/scripts/check-css-nesting.mjs`, over `index.css` and the `css` literals in `src/**/*.ts` alike — a shadow-root stylesheet is parsed by the same engine. It names the file, the line and the fix, which is a leading `&`: valid in both syntaxes, so no nested rule here has a reason to omit it. It is a lefthook pre-commit entry as well, beside the existing `css-literals` one, and CI already runs `make css-check`. The detection (`css-nesting.mjs`) walks blocks rather than matching lines, and both things the issue asks it to get right fall out of one rule — a rule is nested when a **style** rule is somewhere above it, not when its immediate parent is a block: - `@media (…) { bottom-nav { … } }` at the top level is an ordinary rule and is not flagged. That is the majority of what a regex over the file would report. - The same rule inside `.bar { @media (…) { … } }` **is** nested and is flagged, at-rule in between or not. Strings and comments are read through, so a brace in a `url()` is not a block. The literal scanner the two checks now share is lifted into `css-literals.mjs` unchanged, except that a `${}` substitution is blanked keeping its newlines so a line number survives it. The tree has no violation left, so the check would pass just as happily over an empty glob — it refuses one, in the spirit of `icon-language.test.ts`'s first assertion. **Verification tiers run.** - `make css-check` — clean, and **watched failing**. Removing the three `&`s from `index.css`'s bottom-bar block (the historical cases, lines 282/289/293) reports exactly those three lines with the right selectors; a `span { … }` injected into `page-header`'s `css` literal is reported at its own line, which exercises the literal path and the line mapping through it. Both reverts left the tree clean. - `make ui-test` — **991 passed**, 7 of them the new `frontend/test/utils/css-nesting.test.ts`, which pins the at-rule distinction in both directions plus the string/comment handling. - `npx tsc --noEmit` — clean. `make skill-check` — clean. - The lefthook pre-commit and pre-push hooks ran on the commit and the push, including the new `css-nesting` entry. **Deliberately not done.** - The alternative the issue asks to weigh first — a build-time downlevel (Lightning CSS or PostCSS targeting Chrome 113), which would fix the class permanently rather than reject it — is not taken. It is a dependency and a build step against twenty lines, and the issue's own Direction says the check is the version that matches how this repo has handled the other silent-on-one-platform traps. It is written down in `CLAUDE.md` as the expensive answer, so the trade survives this PR. - No device tier: none is attached, and none is needed to check a rule the check enforces about source text. - The sweep covers `frontend/index.css` and `css` literals. A `.css` file added elsewhere under `frontend/` would not be swept. `CLAUDE.md` gains a paragraph under the Chrome 113 section and the `yellowjacket-dev` skill's `make css-check` bullet gains the second check, since that file is the imperative half. Closes #154
Author
Collaborator

CI is green on 4bf2414: check success (run 478) and e2e success, the latter over both Chromium and WebKit. make css-check runs the new script in that first job, so the check is gating from this commit on.

CI is green on `4bf2414`: **`check` success** (run 478) and **`e2e` success**, the latter over both Chromium and WebKit. `make css-check` runs the new script in that first job, so the check is gating from this commit on.
logan force-pushed fix/154-nested-css-check from 4bf2414653 to cee322dc31 2026-08-21 14:36:46 +00:00 Compare
logan added 2 commits 2026-08-21 15:16:49 +00:00
The device renders in Chrome 113, which predates relaxed CSS nesting, so
a nested rule whose selector starts with an element name is not a parse
error anyone would notice -- the rule simply does not exist, there and
nowhere else. Three were live in `index.css`, and the one that mattered
was the `text-overflow: ellipsis` on the bottom bar's title and artist,
which had therefore never truncated on the device. No tier here can see
the class at all: the component tier, the e2e tier and `make ui-visual`
all run a current engine, where the rule applies normally.

So `make css-check` carries a second script. It reads `index.css` and
the `css` literals in `src/**/*.ts` alike, since a shadow-root
stylesheet is parsed by the same engine, and it names the file, the line
and the fix -- a leading `&`, which is valid in both syntaxes.

The detection walks blocks rather than matching lines, and both things
it has to get right fall out of one rule: a rule is nested when a
*style* rule is somewhere above it, not when its immediate parent is a
block. That leaves `@media (...) { bottom-nav { ... } }` at the top
level alone, which is the majority of what a regex over the file would
report, and still flags the same rule inside an at-rule that is itself
inside a style rule. Strings and comments are read through, so a brace
in a `url()` is not a block.

The tree has no violation left, so the check would pass just as happily
over an empty glob: it refuses one, and `test/utils/css-nesting.test.ts`
pins the semantics that make the sweep mean something. The literal
scanner the two checks share is lifted into `css-literals.mjs`
unchanged, except that a `${}` substitution is now blanked keeping its
newlines so a line number survives it.

Closes #154
build(frontend): sweep every stylesheet, not index.css by name
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m32s
CI / e2e (pull_request) Successful in 9m16s
11ba7b3180
The hook fires on frontend/**/*.{ts,css} while the script read one hardcoded path, so a second stylesheet would have been silently unswept while the hook still went green over it. There is only index.css today, which is exactly when this is cheap to fix. Watched catching a planted nested rule in a second file.
logan force-pushed fix/154-nested-css-check from 19d91639a4 to 11ba7b3180 2026-08-21 15:16:49 +00:00 Compare
logan merged commit 60f1c5a6b2 into main 2026-08-21 15:59:25 +00:00
Sign in to join this conversation.