Skip to content

fix(ui): resolve 9 unresolved CSS class orphans — triage per-orphan (#2511)#2515

Merged
alexey-pelykh merged 1 commit intomainfrom
fix/issue-2511-css-orphans-triage
Apr 23, 2026
Merged

fix(ui): resolve 9 unresolved CSS class orphans — triage per-orphan (#2511)#2515
alexey-pelykh merged 1 commit intomainfrom
fix/issue-2511-css-orphans-triage

Conversation

@alexey-pelykh
Copy link
Copy Markdown

Closes #2511. Part of the unresolved bucket from #2502's CSS class drift audit (complement to Wave 1: #2512 / #2513 / #2514).

Summary

Per-orphan triage of the 9 orphans with no CSS-history removing-commit. Each required individual investigation because the dispositions span different bug classes — BEM hooks added without CSS, fork-owned hooks superseded by inline styles, near-miss typos, and one audit false positive.

Per-orphan dispositions

# Class Callsite(s) Disposition Rationale
1 .cron-run-entry__main views/cron.ts:1726 REMOVE .list-main (components.css:1440) handles all styling; siblings __summary / __meta keep their CSS-backed hooks
2 .cron-run-entry__title views/cron.ts:1727 REMOVE .list-title (components.css:1446) handles styling; same pattern as #1
3 .debug-event-log views/debug.ts:128 REMOVE .list (components.css:1409) handles container CSS; children __item / __meta / __payload keep their CSS-backed hooks
4 .form-field views/channels.nostr-profile-form.ts:94, :118 REMOVE Element uses inline style="margin-bottom: 12px"; class is a pure hook with no CSS rule and no JS/test references
5 .language- markdown.ts:187 SCOPE-OUT Audit false positive from `class="language-${escapeHtml(lang)}"`; fixed at the tokenizer level (see audit script change below)
6 .monospace views/channels.nostr.ts:78, :210 RENAME to .mono .mono utility exists at components.css:1187 (font-family: var(--mono)); fork markup picked a near-miss name
7 .nav-item--external app-render.ts:284 REMOVE Fork-added modifier never CSS-defined; external-link semantics fully via target=${EXTERNAL_LINK_TARGET} + rel=${buildExternalLinkRel()}
8 .nav-section--links app-render.ts:278 REMOVE Fork-added modifier never CSS-defined; .nav-section parent handles all styling
9 .nostr-profile-form views/channels.nostr-profile-form.ts:167 REMOVE Container hook on a form using inline styles; same pattern as #4

Result: 7 REMOVE, 1 RENAME, 1 SCOPE-OUT (via script fix).

Audit script adjacency fix

.language- isn't a real orphan — it's a static prefix to a dynamic interpolation (class="language-${escapeHtml(lang)}") that the original tokenizer captured as a standalone class name because it replaced ${...} with a space before splitting on whitespace.

Fixed by introducing an adjacency rule in the tokenizer: a static fragment that touches an interpolation without separating whitespace contributes a PARTIAL class name at the boundary, which is dropped.

  • New helper splitStaticFragments preserves each static fragment verbatim so adjacency can be detected.
  • New helper emitFragmentTokens applies the adjacency rule uniformly, called from both addTokensFromLitClass (for class="…") and the template-literal path (for class=${`…`}) — previously the two paths had divergent tokenization.
  • Scope verified: grep confirmed markdown.ts:187 is the ONLY callsite in ui/src/ using the class-prefix + adjacency pattern (class="[a-zA-Z0-9_-]+${). Template-literal forms (chip ${x} chip-ok, cron-job-status-pill ${x}) all use whitespace-separated static tokens, unaffected by the change.

Caller-context note

The caller suggested .nav-item--external might want to rename to .sidebar-item--external for consistency with #2509's .nav.sidebar rename. Investigation found that Wave 1 renamed only the container (.nav.sidebar); the .nav-section / .nav-item vocabulary is stable upstream, and .sidebar-section / .sidebar-item do not exist. So the REMOVE disposition stands — the modifier was never CSS-defined regardless of container vocabulary.

Closes #2502 unresolved bucket

With this PR merged, the audit runs clean (0 orphans) on main. The remaining drift sources (inverse direction: CSS rules defined but unused — e.g. upstream-inherited .nav-item__external-icon) are a separate category and out of scope here.

Test plan

  • pnpm format:check — PASS (5110 files)
  • pnpm tsgo — PASS (exit 0, no errors)
  • pnpm lint — PASS (0 warnings, 0 errors, 3926 files)
  • pnpm check (format + tsgo + lint + lint:tmp:no-random-messaging + lint:no-remoteclaw-ai) — PASS
  • pnpm canvas:a2ui:bundle — PASS (457.15 kB, 53 ms)
  • pnpm vitest run --config vitest.unit.config.ts ui/src/ui/ — PASS (4 files, 43 tests)
  • Fork-integrity gates: check-no-zombie-imports.mjs, check-stub-debt.mjs (126 baseline), check-throwing-stub-callers.mjs — all pass
  • node scripts/audit-css-class-drift.mjs — orphans 9 → 0, references 412 → 403
  • Adversarial: with script change stashed and markup changes applied, orphan count is 1 (just .language-), confirming the script fix is load-bearing for AC2
  • Visual smoke (requires human browser pass): cron run log entries, debug event log, nostr profile form, nostr account card public-key rows, sidebar "Resources → Docs" link — all should render unchanged

References

🤖 Generated with Claude Code

…ispositions (#2511)

Addresses the "unresolved" bucket from #2502's CSS class drift audit: 9
orphans with no CSS-history removing-commit, each requiring per-orphan
investigation rather than a blanket rename.

Dispositions:
- REMOVE unstyled BEM markup hooks — container/element class tokens that
  had no CSS rule and were redundant with a sibling class handling all
  styling. Applies to `.cron-run-entry__main` / `__title` (paired with
  `.list-main` / `.list-title`), `.debug-event-log` (paired with `.list`),
  `.form-field` + `.nostr-profile-form` (inline `style="…"` carries all
  styling), and `.nav-section--links` + `.nav-item--external` (modifier
  hooks never CSS-defined; external-link semantics via `target`/`rel`).
- RENAME `.monospace` → `.mono` at `views/channels.nostr.ts:78,210` —
  `.mono` utility exists at `components.css:1187`; fork-side markup
  picked a near-miss name.
- SCOPE-OUT `.language-` at `markdown.ts:187` — audit false positive.
  Source: `class="language-${escapeHtml(lang)}"` (dynamic prefix for code
  highlighting). Root-cause fix in `scripts/audit-css-class-drift.mjs`:
  adjacency-aware tokenization via new `splitStaticFragments` +
  `emitFragmentTokens` helpers drops boundary tokens of fragments
  touching `${...}` without separating whitespace. Applied to both
  `class="…"` and `class=${\`…\`}` forms for consistency.

Verified: `node scripts/audit-css-class-drift.mjs` reports 0 orphans
(down from 9). `pnpm check` + `pnpm vitest ui/src/ui/` pass. No other
adjacency-pattern callsites exist in `ui/src/` (grep verified), so the
tokenizer change is surgical.

Closes #2511.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alexey-pelykh alexey-pelykh merged commit 20984fb into main Apr 23, 2026
15 checks passed
@alexey-pelykh alexey-pelykh deleted the fix/issue-2511-css-orphans-triage branch April 23, 2026 23:10
alexey-pelykh added a commit that referenced this pull request Apr 24, 2026
…am-sync class renames from silently desyncing (#2503)

Adds lint:ui:no-css-class-drift pnpm script invoking the existing
scripts/audit-css-class-drift.mjs (#2502), and appends it to the
pnpm check chain so the lint CI job now fails on any template-string
class reference in ui/src/**/*.{ts,tsx,html} that has no matching
rule in the CSS files reachable from ui/src/styles.css.

Baseline on main: 0 orphans / 403 references / 733 defined classes
(cluster fixes from #2506, #2512, #2513, #2514, #2515 already
resolved all existing drift before this gate is wired, per the
#2503 AC "All findings from D-1 resolved BEFORE this check is wired
into CI").

Script name adapted from the issue's suggested lint:css-classes to
lint:ui:no-css-class-drift — matches the lint:<area>:no-<thing>
pattern used by lint:ui:no-raw-window-open,
lint:tmp:no-random-messaging, and
lint:plugins:no-monolithic-plugin-sdk-entry-imports.

Documented in CLAUDE.md § Formatting & Linting (local-dev
orientation) and § Fork-integrity gates (CI/fork-lifecycle
orientation, cross-referenced with the existing rebrand /
zombie-import / stub-debt / throwing-stub-callers /
obsolescence-audit gates).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
alexey-pelykh added a commit that referenced this pull request Apr 24, 2026
…am-sync class renames from silently desyncing (#2503) (#2516)

Adds lint:ui:no-css-class-drift pnpm script invoking the existing
scripts/audit-css-class-drift.mjs (#2502), and appends it to the
pnpm check chain so the lint CI job now fails on any template-string
class reference in ui/src/**/*.{ts,tsx,html} that has no matching
rule in the CSS files reachable from ui/src/styles.css.

Baseline on main: 0 orphans / 403 references / 733 defined classes
(cluster fixes from #2506, #2512, #2513, #2514, #2515 already
resolved all existing drift before this gate is wired, per the
#2503 AC "All findings from D-1 resolved BEFORE this check is wired
into CI").

Script name adapted from the issue's suggested lint:css-classes to
lint:ui:no-css-class-drift — matches the lint:<area>:no-<thing>
pattern used by lint:ui:no-raw-window-open,
lint:tmp:no-random-messaging, and
lint:plugins:no-monolithic-plugin-sdk-entry-imports.

Documented in CLAUDE.md § Formatting & Linting (local-dev
orientation) and § Fork-integrity gates (CI/fork-lifecycle
orientation, cross-referenced with the existing rebrand /
zombie-import / stub-debt / throwing-stub-callers /
obsolescence-audit gates).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant