Add font-weight token validation to stylelint for design-system area

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
mrleemurray
2026-06-09 15:08:32 +01:00
parent 1c9193582f
commit 554dfed38d
3 changed files with 121 additions and 2 deletions

View File

@@ -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`

View File

@@ -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;
}

View File

@@ -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);
}