// Гейт «одно место для цвета и размера», половина в CSS (STACK_DECISIONS §3). // TSX-половина — в eslint.config.js. // // Двухслойно: запрет литеральных цветов ловит их в ЛЮБОМ свойстве, включая сокращённые // записи вроде `border: 1px solid #fff`, а allowed-list добавляет свойства, где литерал // не цвет (`font-size`, `z-index`). reportDisables делает ошибкой и попытку отключить // правило комментарием. const gate = { reportDisables: true }; const token = '/^var\\(--/'; const colorValues = [token, 'inherit', 'currentColor', 'currentcolor', 'transparent']; // Ф-4: шкала отступов обкатана оболочкой S2, гейт включён. Значение — последовательность токенов; // 0 и auto легальны, потому что это не размеры, а отсутствие размера и «посчитай сам». // calc() намеренно не разрешён: составное выражение из литералов проверить нечем, и если размер // понадобился новый, его место — в tokens.css, а не в скобках calc. const spacingValues = ['/^(?:(?:var\\(--[a-z0-9-]+\\)|0|auto)\\s*)+$/', 'inherit']; // Системные цветовые ключевые слова — не «именованные цвета», их не видит color-named, и в // сокращённых записях (outline, box-shadow, border) их не ловит вообще ничто (дыра из Ф-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 на правиле ловит только ИМЕНОВАННОЕ отключение // (`/* stylelint-disable color-no-hex */`). Голое `/* stylelint-disable */` в шапке файла // снимало гейт целиком и молча — проверено. Эта опция требует, чтобы отключение всегда // называло правило, и тем самым закрывает обход. reportUnscopedDisables: true, rules: { 'color-no-hex': [true, gate], 'color-named': ['never', gate], // color() и light-dark() тоже задают цвет литералом, а в сокращённой записи // (`border: 1px solid color(...)`) их не ловит ничто другое. color-mix() оставлен: // смешивать ему нечего, кроме токенов. '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: 'Значение только из токена: var(--...) из src/tokens/tokens.css' }, ], 'declaration-property-value-disallowed-list': [ { '/.*/': [systemColors] }, { ...gate, message: 'Системный цвет ОС — не наш цвет: тон берётся из src/tokens/tokens.css' }, ], // CSS Modules читаются из TS как styles.panelHeader, поэтому классы camelCase. 'selector-class-pattern': ['^[a-z][a-zA-Z0-9]*$', { message: 'Имя класса — camelCase' }], }, overrides: [ { // Единственное место, где литеральный цвет легален: сами токены. files: ['src/tokens/tokens.css'], rules: { 'color-no-hex': null, 'color-named': null, 'function-disallowed-list': null, 'declaration-property-value-allowed-list': null, }, }, ], };