77 lines
3.6 KiB
JavaScript
77 lines
3.6 KiB
JavaScript
// The "one place for colour and size" gate, the CSS half (STACK_DECISIONS §3).
|
|
// The TSX half is in eslint.config.js.
|
|
//
|
|
// Two layers: the ban on literal colours catches them in ANY property, including shorthands such
|
|
// as `border: 1px solid #fff`, while the allowed-list adds the properties where a literal is not a
|
|
// colour (`font-size`, `z-index`). `reportDisables` also makes turning a rule off by comment an
|
|
// error.
|
|
const gate = { reportDisables: true };
|
|
const token = '/^var\\(--/';
|
|
const colorValues = [token, 'inherit', 'currentColor', 'currentcolor', 'transparent'];
|
|
|
|
// Ф-4: the spacing scale was proven by the S2 shell, so the gate is on. A value is a sequence of
|
|
// tokens; `0` and `auto` are legal because they are not sizes but the absence of one and "work it
|
|
// out". `calc()` is deliberately not allowed: a compound expression of literals cannot be checked,
|
|
// and a size that is genuinely new belongs in tokens.css rather than inside the brackets.
|
|
const spacingValues = ['/^(?:(?:var\\(--[a-z0-9-]+\\)|0|auto)\\s*)+$/', 'inherit'];
|
|
|
|
// System colour keywords are not "named colours": `color-named` does not see them, and inside a
|
|
// shorthand (outline, box-shadow, border) nothing catches them at all — the hole from Ф-9.
|
|
const systemColors =
|
|
'/\\b(AccentColor|AccentColorText|ActiveText|ButtonBorder|ButtonFace|ButtonText|Canvas|CanvasText|Field|FieldText|GrayText|Highlight|HighlightText|LinkText|Mark|MarkText|SelectedItem|SelectedItemText|VisitedText)\\b/i';
|
|
|
|
export default {
|
|
extends: ['stylelint-config-standard'],
|
|
// `reportDisables` on a rule only catches a NAMED disable (`/* stylelint-disable color-no-hex */`).
|
|
// A bare `/* stylelint-disable */` at the top of a file lifted the gate entirely and silently —
|
|
// measured. This option demands that a disable always names its rule, which closes the bypass.
|
|
reportUnscopedDisables: true,
|
|
rules: {
|
|
'color-no-hex': [true, gate],
|
|
'color-named': ['never', gate],
|
|
// `color()` and `light-dark()` also state a colour literally, and inside a shorthand
|
|
// (`border: 1px solid color(...)`) nothing else catches them. `color-mix()` is left alone: it
|
|
// has nothing to mix but tokens.
|
|
'function-disallowed-list': [
|
|
['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklab', 'oklch', 'color', 'light-dark'],
|
|
gate,
|
|
],
|
|
'declaration-property-value-allowed-list': [
|
|
{
|
|
color: colorValues,
|
|
'background-color': colorValues,
|
|
'border-color': colorValues,
|
|
background: [...colorValues, 'none'],
|
|
fill: [...colorValues, 'none'],
|
|
stroke: [...colorValues, 'none'],
|
|
'font-size': [token, 'inherit'],
|
|
'z-index': [token],
|
|
'/^(padding|margin)(-|$)/': spacingValues,
|
|
'/^(gap|row-gap|column-gap)$/': spacingValues,
|
|
'/^border-.*radius$/': spacingValues,
|
|
},
|
|
{ ...gate, message: 'A value comes from a token: var(--...) out of src/tokens/tokens.css' },
|
|
],
|
|
'declaration-property-value-disallowed-list': [
|
|
{ '/.*/': [systemColors] },
|
|
{
|
|
...gate,
|
|
message: 'A system colour is not our colour: tones come from src/tokens/tokens.css',
|
|
},
|
|
],
|
|
// A CSS Module is read from TS as styles.panelHeader, so class names are camelCase.
|
|
'selector-class-pattern': ['^[a-z][a-zA-Z0-9]*$', { message: 'A class name is camelCase' }],
|
|
},
|
|
overrides: [
|
|
{
|
|
// The one place a literal colour is legal: the tokens themselves.
|
|
files: ['src/tokens/tokens.css'],
|
|
rules: {
|
|
'color-no-hex': null,
|
|
'color-named': null,
|
|
'function-disallowed-list': null,
|
|
'declaration-property-value-allowed-list': null,
|
|
},
|
|
},
|
|
],
|
|
};
|