From 30d129c90edde8ba02abdadea86b1d1f19157213 Mon Sep 17 00:00:00 2001 From: Valentin Millet Date: Fri, 18 Sep 2026 14:19:26 +0200 Subject: [PATCH] Hand the format ring down to the badge that draws it A rule written in the rail's stylesheet cannot reach '.lang-tag': that element belongs to 'app-language-badge', and emulated encapsulation rewrites the selector with the rail's own '_ngcontent' attribute, so it matches nothing. The selected format lost its mark entirely. 'hue-badge' now reads '--badge-ring' and '--badge-surface' with its hue as the fallback, and the rail sets them on the chip: a custom property inherits across the boundary a rule cannot cross. The assertion that missed this counted how many boxes were drawn around the badge, and the badge's own hairline kept that count right. It now compares the same badge's border colour selected against at rest. Closes #207 --- e2e/pageobjects/canvas.page.ts | 16 ++++++++++++++++ e2e/specs/06-search-and-filters.e2e.ts | 15 +++++++++++++++ .../language-rail/language-rail.component.scss | 15 ++++++++++----- src/styles/_mixins.scss | 6 ++++-- 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/e2e/pageobjects/canvas.page.ts b/e2e/pageobjects/canvas.page.ts index ca5d60e..d235f94 100644 --- a/e2e/pageobjects/canvas.page.ts +++ b/e2e/pageobjects/canvas.page.ts @@ -207,6 +207,22 @@ export const canvas = { ); }, + /** + * The colour of the hairline around one format's badge in the rail. ⚠️ Read on the badge + * and not on the chip: the chip is the click surface, the badge is what carries the ring, + * and they live in two different components — which is the whole trap (#207). + */ + badgeRing(language: string): Promise { + return browser.execute( + (chipSelector: string, wanted: string) => { + const chip = document.querySelector(`${chipSelector}[data-language="${wanted}"]`); + const badge = chip?.querySelector('.lang-tag'); + return badge ? getComputedStyle(badge).borderTopColor : null; + }, + testid('language-chip'), + language, + ); + }, async waitForCard(title: string): Promise { await browser.waitUntil(async () => (await canvas.titles()).includes(title), { timeout: 15_000, diff --git a/e2e/specs/06-search-and-filters.e2e.ts b/e2e/specs/06-search-and-filters.e2e.ts index 47fbdf6..c0d3a8f 100644 --- a/e2e/specs/06-search-and-filters.e2e.ts +++ b/e2e/specs/06-search-and-filters.e2e.ts @@ -145,6 +145,21 @@ describe('Search, filters and facets', () => { await canvas.toggleLanguage('sh'); }); + /** + * ⚠️ #207: the rail cannot reach the badge. `.lang-tag` belongs to `app-language-badge`, + * so a rule written in the rail's own stylesheet is rewritten with the rail's + * `_ngcontent` attribute and never matches — the selection had no mark at all while + * `outlines` above still counted one. The badge's own hairline was that one. + */ + it('marks the selected format on the badge that carries the ring', async () => { + const resting = await canvas.badgeRing('sh'); + await canvas.toggleLanguage('sh'); + const selected = await canvas.badgeRing('sh'); + await canvas.toggleLanguage('sh'); + + expect(resting).not.toBeNull(); + expect(selected).not.toBe(resting); + }); it('drops the search, the tag and the language in one click', async () => { await canvas.search('Docker'); await canvas.toggleTag('ops'); diff --git a/src/app/notes/header/language-rail/language-rail.component.scss b/src/app/notes/header/language-rail/language-rail.component.scss index dd1be54..4dc8e1d 100644 --- a/src/app/notes/header/language-rail/language-rail.component.scss +++ b/src/app/notes/header/language-rail/language-rail.component.scss @@ -35,13 +35,18 @@ // #196, and a second outline two pixels further out — at a different radius — read as two // rings around one badge rather than as a selection. The padding stays, because it is what // the chip is clickable by. -.language-chip:hover .lang-tag { - border-color: var(--line); +// +// ⚠️ Which is why the state is handed down as a custom property and not written as a rule +// on `.lang-tag`: that element belongs to `app-language-badge`, and emulated encapsulation +// rewrites a selector spelled here with *this* component's `_ngcontent` attribute. It then +// matches nothing, silently — and the selected format had no mark at all (#207). +.language-chip:hover { + --badge-ring: var(--line); } -.language-chip.on .lang-tag { - border-color: var(--amber); - background: var(--bg-2); +.language-chip.on { + --badge-ring: var(--amber); + --badge-surface: var(--bg-2); } .language-chip:focus-visible { diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index bfd615a..ad090e8 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -188,9 +188,11 @@ // ⚠️ The hairline is what gives the chip a shape, and it is in the hue rather than in a // grey: `--bg-3` sits at 1.1:1 against the light theme's surfaces, so a neutral fill alone // left the badge as bare text on every light screen. +// ⚠️ `--badge-ring` and `--badge-surface` are the only way in from outside: the badge is a +// component of its own, so a consumer's stylesheet cannot reach `.lang-tag` with a rule. @mixin hue-badge($colour) { - background: var(--bg-3); - border: 1px solid color-mix(in srgb, #{$colour} 40%, transparent); + background: var(--badge-surface, var(--bg-3)); + border: 1px solid var(--badge-ring, color-mix(in srgb, #{$colour} 40%, transparent)); color: $colour; }