// 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, }, }, ], };