textmachine/frontend/stylelint.config.js

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