From 554dfed38d2b9eb26584113252e41fdc73d451c8 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 9 Jun 2026 15:08:32 +0100 Subject: [PATCH] Add font-weight token validation to stylelint for design-system area Co-authored-by: Copilot --- .../design-tokens.instructions.md | 27 +++++- build/lib/stylelint/validateDesignTokens.ts | 91 +++++++++++++++++++ build/stylelint.ts | 5 +- 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/.github/instructions/design-tokens.instructions.md b/.github/instructions/design-tokens.instructions.md index 5873f8053fb..5a538a45e1c 100644 --- a/.github/instructions/design-tokens.instructions.md +++ b/.github/instructions/design-tokens.instructions.md @@ -95,7 +95,32 @@ Agents window ramp (`src/vs/sessions/**`) — pair size with a weight token, Weights: `--vscode-agents-fontWeight-regular` (400), `--vscode-agents-fontWeight-semiBold` (600). The ramp is **400/600 only** — there -is no medium (500). "Strong" = same size token + `semiBold`. +is no medium (500). "Strong" = same size token + `semiBold`. See +[Font weight](#font-weight--font-weight) below. + +## Font weight — `font-weight` + +The agents window uses a **two-weight ramp** — there are no other weights. +Pair every text style with one of these: + +| weight | Variable | Use | +|--------|----------|-----| +| 400 | `--vscode-agents-fontWeight-regular` | body, labels, metadata | +| 600 | `--vscode-agents-fontWeight-semiBold` | headings, "strong" emphasis | + +- **No medium (500).** `font-weight: 500` is **off the ramp** — snap it to + `semiBold` (600). The same goes for `700`/`bold` and any other numeric weight: + round to the nearer of 400/600. +- **"Strong" is not a separate size.** A "Body 1 Strong" / "Label 2 Strong" + style reuses the matching `--vscode-agents-fontSize-*` token paired with + `semiBold`. Never introduce a separate strong *size* token. +- `normal` ≡ 400 → `regular`. **Leave untouched:** `inherit`, `lighter`, + `bolder`, and any `var()`/`calc()` expression. Preserve `!important`. + +```css +/* avoid */ font-weight: 500; /* not on the 400/600 ramp */ +/* prefer */ font-weight: var(--vscode-agents-fontWeight-semiBold); +``` ## Codicon size — icon `font-size` diff --git a/build/lib/stylelint/validateDesignTokens.ts b/build/lib/stylelint/validateDesignTokens.ts index d104633ad7d..1deada75fb0 100644 --- a/build/lib/stylelint/validateDesignTokens.ts +++ b/build/lib/stylelint/validateDesignTokens.ts @@ -264,3 +264,94 @@ export function validateCornerRadiusTokens(text: string): IDesignTokenViolation[ return violations; } + +// --------------------------------------------------------------------------- +// Font-weight token suggestions (sessions design-system area only) +// --------------------------------------------------------------------------- +// +// The agents font ramp defines exactly two weights: regular (400) and +// semiBold (600). Any other numeric weight (e.g. 500, 700) is off the ramp and +// snaps to the nearer of the two. The CSS keywords `normal` (400) and `bold` +// (700) are normalised before snapping. `inherit`, `lighter`, `bolder` and +// var()/calc() expressions are left untouched. + +const RE_FONT_WEIGHT = /font-weight\s*:\s*([^;{}]+)/i; + +interface IFontWeightToken { + readonly weight: number; + readonly name: string; +} + +/** The only two weights in the agents ramp. */ +const FONT_WEIGHT_TOKENS: readonly IFontWeightToken[] = [ + { weight: 400, name: 'regular' }, + { weight: 600, name: 'semiBold' }, +]; + +/** Resolves a font-weight value to a number, or undefined if not numeric. */ +function parseFontWeight(value: string): number | undefined { + const trimmed = value.trim().toLowerCase(); + if (trimmed === 'normal') { + return 400; + } + if (trimmed === 'bold') { + return 700; + } + const numeric = /^(\d{3})$/.exec(trimmed); + return numeric ? parseInt(numeric[1], 10) : undefined; +} + +/** Maps a numeric weight to its token, snapping off-ramp values to the nearer. */ +function snapFontWeight(weight: number): IFontWeightToken { + let best = FONT_WEIGHT_TOKENS[0]; + let bestDistance = Math.abs(best.weight - weight); + for (const token of FONT_WEIGHT_TOKENS) { + const distance = Math.abs(token.weight - weight); + // `<=` makes the 500 tie prefer the heavier (600) token. + if (distance <= bestDistance) { + best = token; + bestDistance = distance; + } + } + return best; +} + +/** + * Finds hardcoded `font-weight` values and suggests the agents weight-ramp var. + * Exact ramp values (400 / 600, plus `normal` = 400) are flagged as drop-in + * replacements; off-ramp values (e.g. 500, 700, `bold`) snap to the nearer + * token and call out that the value is off the two-weight ramp. `inherit`, + * `lighter`, `bolder` and var()/calc() expressions are ignored. Returns one + * finding per occurrence so the terminal can linkify each `file(line,col)`. + */ +export function validateFontWeightTokens(text: string): IDesignTokenViolation[] { + const violations: IDesignTokenViolation[] = []; + + forEachDeclaration(text, (line, _selector, declaration) => { + const decl = RE_FONT_WEIGHT.exec(declaration); + if (!decl) { + return; + } + const value = decl[1]; + if (/var\(|calc\(/i.test(value)) { + return; + } + const weight = parseFontWeight(value); + if (weight === undefined) { + return; + } + const token = snapFontWeight(weight); + const exact = token.weight === weight; + const shown = value.trim(); + const headline = exact + ? `font-weight ${shown} matches a design ramp token - prefer the var:` + : `font-weight ${shown} is off the agents weight ramp (400/600) - nearest token (${token.weight}):`; + violations.push({ + line, + isNearMiss: !exact, + message: `${headline}\n -> use var(--vscode-agents-fontWeight-${token.name})` + }); + }); + + return violations; +} diff --git a/build/stylelint.ts b/build/stylelint.ts index 61624b8296b..e16713dd0ac 100644 --- a/build/stylelint.ts +++ b/build/stylelint.ts @@ -7,7 +7,7 @@ import es from 'event-stream'; import vfs from 'vinyl-fs'; import { stylelintFilter } from './filters.ts'; import { getVariableNameValidator } from './lib/stylelint/validateVariableNames.ts'; -import { validateCodiconFontSizes, validateFontSizeTokens, validateCornerRadiusTokens } from './lib/stylelint/validateDesignTokens.ts'; +import { validateCodiconFontSizes, validateFontSizeTokens, validateFontWeightTokens, validateCornerRadiusTokens } from './lib/stylelint/validateDesignTokens.ts'; interface FileWithLines { __lines?: string[]; @@ -61,6 +61,9 @@ export default function gulpstylelint(reporter: Reporter): NodeJS.ReadWriteStrea for (const violation of validateFontSizeTokens(contents)) { reporter(file.relative + '(' + violation.line + ',1): ' + violation.message, false); } + for (const violation of validateFontWeightTokens(contents)) { + reporter(file.relative + '(' + violation.line + ',1): ' + violation.message, false); + } for (const violation of validateCornerRadiusTokens(contents)) { reporter(file.relative + '(' + violation.line + ',1): ' + violation.message, false); }