fix(ui): resolve 9 unresolved CSS class orphans — triage per-orphan (#2511)#2515
Merged
alexey-pelykh merged 1 commit intomainfrom Apr 23, 2026
Merged
Conversation
…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
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>
7 tasks
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>
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
.cron-run-entry__mainviews/cron.ts:1726.list-main(components.css:1440) handles all styling; siblings__summary/__metakeep their CSS-backed hooks.cron-run-entry__titleviews/cron.ts:1727.list-title(components.css:1446) handles styling; same pattern as #1.debug-event-logviews/debug.ts:128.list(components.css:1409) handles container CSS; children__item/__meta/__payloadkeep their CSS-backed hooks.form-fieldviews/channels.nostr-profile-form.ts:94, :118style="margin-bottom: 12px"; class is a pure hook with no CSS rule and no JS/test references.language-markdown.ts:187`class="language-${escapeHtml(lang)}"`; fixed at the tokenizer level (see audit script change below).monospaceviews/channels.nostr.ts:78, :210.mono.monoutility exists atcomponents.css:1187(font-family: var(--mono)); fork markup picked a near-miss name.nav-item--externalapp-render.ts:284target=${EXTERNAL_LINK_TARGET}+rel=${buildExternalLinkRel()}.nav-section--linksapp-render.ts:278.nav-sectionparent handles all styling.nostr-profile-formviews/channels.nostr-profile-form.ts:167Result: 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.
splitStaticFragmentspreserves each static fragment verbatim so adjacency can be detected.emitFragmentTokensapplies the adjacency rule uniformly, called from bothaddTokensFromLitClass(forclass="…") and the template-literal path (forclass=${`…`}) — previously the two paths had divergent tokenization.markdown.ts:187is the ONLY callsite inui/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--externalmight want to rename to.sidebar-item--externalfor consistency with #2509's.nav→.sidebarrename. Investigation found that Wave 1 renamed only the container (.nav→.sidebar); the.nav-section/.nav-itemvocabulary is stable upstream, and.sidebar-section/.sidebar-itemdo 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) — PASSpnpm 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)check-no-zombie-imports.mjs,check-stub-debt.mjs(126 baseline),check-throwing-stub-callers.mjs— all passnode scripts/audit-css-class-drift.mjs— orphans 9 → 0, references 412 → 403.language-), confirming the script fix is load-bearing for AC2References
.btn-sm), fix(ui): restore config sidebar/nav/subnav/tag-picker CSS (#2508) #2513 (cluster 1 config sidebar), fix(ui): migrate app-render nav + theme-toggle to upstream sidebar + theme-orb — resolve v2026.3.13-1 sync drift (#2509) #2514 (cluster 2 app-render nav+theme-toggle)remoteclaw/hq#57🤖 Generated with Claude Code