Add no-restricted-syntax rule matching string literals of form #RGB / #RRGGBB /
#RRGGBBAA. Catches hex colors anywhere in JS/TS — JSX inline styles, template
literals, prop defaults, config arrays, etc.
Message points users to the right escape hatch:
- var(--sumi-*) for CSS contexts (JSX style/className, template literals)
- import {ColorVizIndigo, ...} from '@veza/design-system/tokens-generated' for
canvas/runtime contexts where var() can't resolve.
Single source of truth: packages/design-system/tokens/primitive/color.json.
Severity: warn (not error) — gives a smooth migration ramp; can be flipped to
error in a future sprint once the 3 PieChart pigment TODOs (sakura, terminal,
magenta) are canonized in tokens.
The rule will catch any new hex regression at lint time, completing the
"single source of truth" guarantee started by Style Dictionary in Sprint 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
312 lines
13 KiB
JavaScript
312 lines
13 KiB
JavaScript
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
|
import storybook from "eslint-plugin-storybook";
|
|
|
|
// eslint-plugin-storybook optional: install if needed for Storybook-specific lint rules
|
|
// import storybook from "eslint-plugin-storybook";
|
|
|
|
import js from '@eslint/js';
|
|
import typescript from '@typescript-eslint/eslint-plugin';
|
|
import typescriptParser from '@typescript-eslint/parser';
|
|
import react from 'eslint-plugin-react';
|
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
|
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
|
|
|
export default [js.configs.recommended, {
|
|
files: ['**/*.{ts,tsx,js,jsx}'],
|
|
languageOptions: {
|
|
parser: typescriptParser,
|
|
parserOptions: {
|
|
ecmaFeatures: {
|
|
jsx: true,
|
|
},
|
|
ecmaVersion: 2022,
|
|
sourceType: 'module',
|
|
},
|
|
globals: {
|
|
// Browser globals
|
|
window: 'readonly',
|
|
document: 'readonly',
|
|
localStorage: 'readonly',
|
|
sessionStorage: 'readonly',
|
|
console: 'readonly',
|
|
setTimeout: 'readonly',
|
|
clearTimeout: 'readonly',
|
|
setInterval: 'readonly',
|
|
clearInterval: 'readonly',
|
|
fetch: 'readonly',
|
|
WebSocket: 'readonly',
|
|
File: 'readonly',
|
|
FormData: 'readonly',
|
|
CustomEvent: 'readonly',
|
|
Event: 'readonly',
|
|
CloseEvent: 'readonly',
|
|
MessageEvent: 'readonly',
|
|
KeyboardEvent: 'readonly',
|
|
HTMLElement: 'readonly',
|
|
HTMLDivElement: 'readonly',
|
|
HTMLInputElement: 'readonly',
|
|
HTMLButtonElement: 'readonly',
|
|
HTMLAnchorElement: 'readonly',
|
|
HTMLParagraphElement: 'readonly',
|
|
HTMLHeadingElement: 'readonly',
|
|
HTMLTextAreaElement: 'readonly',
|
|
HTMLSelectElement: 'readonly',
|
|
HTMLImageElement: 'readonly',
|
|
HTMLAudioElement: 'readonly',
|
|
Element: 'readonly',
|
|
Node: 'readonly',
|
|
MouseEvent: 'readonly',
|
|
Blob: 'readonly',
|
|
FileReader: 'readonly',
|
|
Image: 'readonly',
|
|
global: 'readonly',
|
|
NodeJS: 'readonly',
|
|
Buffer: 'readonly',
|
|
crypto: 'readonly',
|
|
performance: 'readonly',
|
|
require: 'readonly',
|
|
process: 'readonly',
|
|
// URL API globals
|
|
URL: 'readonly',
|
|
URLSearchParams: 'readonly',
|
|
// DOM API globals
|
|
DOMRect: 'readonly',
|
|
DOMRectReadOnly: 'readonly',
|
|
Headers: 'readonly',
|
|
navigator: 'readonly',
|
|
WindowEventMap: 'readonly',
|
|
requestAnimationFrame: 'readonly',
|
|
cancelAnimationFrame: 'readonly',
|
|
Notification: 'readonly',
|
|
NotificationOptions: 'readonly',
|
|
NotificationPermission: 'readonly',
|
|
IntersectionObserver: 'readonly',
|
|
IntersectionObserverInit: 'readonly',
|
|
MessageChannel: 'readonly',
|
|
confirm: 'readonly',
|
|
alert: 'readonly',
|
|
// Web API globals
|
|
AudioContext: 'readonly',
|
|
AnalyserNode: 'readonly',
|
|
MediaElementAudioSourceNode: 'readonly',
|
|
HTMLIFrameElement: 'readonly',
|
|
HTMLMediaElement: 'readonly',
|
|
XMLHttpRequest: 'readonly',
|
|
BinaryType: 'readonly',
|
|
EventListenerOrEventListenerObject: 'readonly',
|
|
ReadableStream: 'readonly',
|
|
BeforeUnloadEvent: 'readonly',
|
|
CryptoKey: 'readonly',
|
|
btoa: 'readonly',
|
|
atob: 'readonly',
|
|
TextEncoder: 'readonly',
|
|
TextDecoder: 'readonly',
|
|
CanvasRenderingContext2D: 'readonly',
|
|
MutationObserver: 'readonly',
|
|
Window: 'readonly',
|
|
Storage: 'readonly',
|
|
TransformStream: 'readonly',
|
|
// Service Worker globals
|
|
self: 'readonly',
|
|
caches: 'readonly',
|
|
ServiceWorkerRegistration: 'readonly',
|
|
Cache: 'readonly',
|
|
CacheStorage: 'readonly',
|
|
Response: 'readonly',
|
|
Request: 'readonly',
|
|
clients: 'readonly',
|
|
// React globals
|
|
React: 'readonly',
|
|
// Test globals
|
|
beforeAll: 'readonly',
|
|
afterAll: 'readonly',
|
|
afterEach: 'readonly',
|
|
beforeEach: 'readonly',
|
|
describe: 'readonly',
|
|
it: 'readonly',
|
|
test: 'readonly',
|
|
expect: 'readonly',
|
|
vi: 'readonly',
|
|
vitest: 'readonly',
|
|
waitFor: 'readonly',
|
|
jest: 'readonly',
|
|
AbortController: 'readonly',
|
|
AbortSignal: 'readonly',
|
|
BroadcastChannel: 'readonly',
|
|
DOMException: 'readonly',
|
|
atob: 'readonly',
|
|
PerformanceNavigationTiming: 'readonly',
|
|
PerformanceObserver: 'readonly',
|
|
HTMLFormElement: 'readonly',
|
|
HTMLTableElement: 'readonly',
|
|
HTMLTableSectionElement: 'readonly',
|
|
HTMLTableRowElement: 'readonly',
|
|
HTMLTableCellElement: 'readonly',
|
|
HTMLTableCaptionElement: 'readonly',
|
|
HTMLSpanElement: 'readonly',
|
|
HTMLCanvasElement: 'readonly',
|
|
HTMLLabelElement: 'readonly',
|
|
FileList: 'readonly',
|
|
MediaQueryListEvent: 'readonly',
|
|
IntersectionObserver: 'readonly',
|
|
IntersectionObserverEntry: 'readonly',
|
|
IntersectionObserverCallback: 'readonly',
|
|
ResizeObserver: 'readonly',
|
|
ResizeObserverEntry: 'readonly',
|
|
HeadersInit: 'readonly',
|
|
EventListener: 'readonly',
|
|
},
|
|
},
|
|
plugins: {
|
|
'@typescript-eslint': typescript,
|
|
'react': react,
|
|
'react-hooks': reactHooks,
|
|
'react-refresh': reactRefresh,
|
|
'jsx-a11y': jsxA11y,
|
|
},
|
|
rules: {
|
|
// TypeScript
|
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
'@typescript-eslint/no-explicit-any': 'warn',
|
|
'@typescript-eslint/no-non-null-assertion': 'warn',
|
|
|
|
// React
|
|
'react/react-in-jsx-scope': 'off',
|
|
'react/prop-types': 'off',
|
|
'react-hooks/rules-of-hooks': 'error',
|
|
'react-hooks/exhaustive-deps': 'warn',
|
|
'react-refresh/only-export-components': [
|
|
'warn',
|
|
{ allowConstantExport: true }
|
|
],
|
|
|
|
// General
|
|
'no-console': 'off',
|
|
'no-debugger': 'error',
|
|
'prefer-const': 'error',
|
|
'no-var': 'error',
|
|
'object-shorthand': 'error',
|
|
'prefer-template': 'error',
|
|
'no-unused-vars': 'off', // Handled by @typescript-eslint/no-unused-vars
|
|
'no-undef': 'off', // TypeScript handles this; no-undef doesn't understand TS types (JSX, etc.)
|
|
'no-useless-escape': 'error',
|
|
'no-prototype-builtins': 'warn',
|
|
|
|
// Typography: Enforce type scale usage
|
|
// Warn on arbitrary text sizes in className strings (e.g., text-[10px], text-[9px])
|
|
// Note: SVG chart text (text-[2px], text-[1.5px]) may need exceptions - review case by case
|
|
'no-restricted-syntax': [
|
|
'warn',
|
|
{
|
|
selector:
|
|
"Literal[value=/text-\\[\\d+(\\.\\d+)?(px|rem)\\]/], TemplateElement[value.raw=/text-\\[\\d+(\\.\\d+)?(px|rem)\\]/]",
|
|
message:
|
|
'Use type scale classes (text-xs, text-sm, text-base, text-lg, text-xl, text-2xl, text-3xl, text-4xl) instead of arbitrary sizes. SVG chart text (text-[2px], text-[1.5px]) may be an exception - add eslint-disable comment if needed.',
|
|
},
|
|
// Spacing: Enforce spacing scale usage
|
|
// Warn on arbitrary spacing values that don't follow 4px base scale
|
|
// Valid scale values: 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24
|
|
// Arbitrary values like gap-[7px], p-[9px], etc. should use scale values instead
|
|
{
|
|
selector:
|
|
"Literal[value=/(gap-|p-|m-|px-|py-|mx-|my-|space-[xy]-)\\[\\d+(\\.\\d+)?(px|rem)\\]/], TemplateElement[value.raw=/(gap-|p-|m-|px-|py-|mx-|my-|space-[xy]-)\\[\\d+(\\.\\d+)?(px|rem)\\]/]",
|
|
message:
|
|
'Use spacing scale classes (gap-0 through gap-24, p-0 through p-24, etc.) instead of arbitrary sizes. Valid scale values follow 4px base: 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24. For exceptions, add eslint-disable comment.',
|
|
},
|
|
// Colors: Prevent Tailwind default colors (use SUMI design system semantic tokens instead)
|
|
// Warn on default Tailwind color classes: slate, gray, zinc, neutral, stone, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose
|
|
// Allow: semantic tokens (primary, secondary, destructive, success, warning, muted, etc.) and sumi-* tokens
|
|
{
|
|
selector:
|
|
"Literal[value=/(text-|bg-|border-|ring-|outline-|divide-|placeholder-|from-|via-|to-|accent-|caret-|fill-|stroke-)(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900|950)/], TemplateElement[value.raw=/(text-|bg-|border-|ring-|outline-|divide-|placeholder-|from-|via-|to-|accent-|caret-|fill-|stroke-)(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900|950)/]",
|
|
message:
|
|
'Use SUMI design system semantic tokens (primary, secondary, destructive, success, warning, muted, foreground, etc.) instead of Tailwind default colors. See apps/web/docs/DESIGN_TOKENS.md for token mapping. For exceptions (e.g., test files), add eslint-disable comment.',
|
|
},
|
|
// Hex colors: Prevent literal hex colors in JS/TS strings (use tokens instead).
|
|
// Matches strings like '#7c9dd6', '#fff', '#0d0d0fAA' — anywhere in code.
|
|
// Use var(--sumi-*) in CSS contexts (JSX style props, template literals)
|
|
// OR import { ColorXxx } from '@veza/design-system/tokens-generated' for canvas/runtime.
|
|
// Exceptions: rgba()/hsla() (not # prefix), the design-system package itself (different rule scope).
|
|
{
|
|
selector:
|
|
"Literal[value=/^#[0-9a-fA-F]{3,8}$/]",
|
|
message:
|
|
'Hardcoded hex color literal. Use SUMI tokens: var(--sumi-*) for CSS strings (JSX style/className), or import {ColorVizIndigo, ColorMizuBase, ...} from \'@veza/design-system/tokens-generated\' for canvas/runtime contexts. Source of truth: packages/design-system/tokens/primitive/color.json. See CHARTE_GRAPHIQUE_TALAS.md §4.',
|
|
},
|
|
// Components: Enforce Button component usage (prevent native button elements)
|
|
// Warn on native <button> elements - use <Button> component from @/components/ui/button instead
|
|
{
|
|
selector: 'JSXOpeningElement[name.name="button"]',
|
|
message:
|
|
'Use the Button component from @/components/ui/button instead of native <button> elements. This ensures consistent styling, accessibility, and design system compliance. For exceptions (e.g., test files, third-party components), add eslint-disable comment.',
|
|
},
|
|
// Width: Avoid arbitrary width classes — use layout tokens or scale (w-4, max-w-layout-content, etc.)
|
|
{
|
|
selector:
|
|
"Literal[value=/.*(w-|min-w-|max-w-)\\[[^]]+\\].*/], TemplateElement[value.raw=/.*(w-|min-w-|max-w-)\\[[^]]+\\].*/]",
|
|
message:
|
|
'Avoid arbitrary width classes (w-[...], min-w-[...], max-w-[...]). Use scale (w-4, min-w-80) or layout tokens (max-w-layout-content). See docs/DESIGN_TOKENS.md. For exceptions (e.g. SVG), add eslint-disable.',
|
|
},
|
|
// Height: Avoid arbitrary height classes
|
|
{
|
|
selector:
|
|
"Literal[value=/.*(h-|min-h-|max-h-)\\[[^]]+\\].*/], TemplateElement[value.raw=/.*(h-|min-h-|max-h-)\\[[^]]+\\].*/]",
|
|
message:
|
|
'Avoid arbitrary height classes (h-[...], min-h-[...], max-h-[...]). Use scale (h-4, min-h-8) or layout tokens. See docs/DESIGN_TOKENS.md. For exceptions, add eslint-disable.',
|
|
},
|
|
// Rounded: Avoid arbitrary rounded — use rounded, rounded-lg, rounded-xl, rounded-full or var(--radius-xl)
|
|
{
|
|
selector:
|
|
"Literal[value=/.*rounded-\\[[^]]+\\].*/], TemplateElement[value.raw=/.*rounded-\\[[^]]+\\].*/]",
|
|
message:
|
|
'Avoid arbitrary rounded classes (rounded-[...]). Use rounded, rounded-lg, rounded-xl, rounded-full or rounded-[var(--radius-xl)]. See docs/DESIGN_TOKENS.md.',
|
|
},
|
|
// Shadow: Avoid arbitrary shadow — use tokens (shadow-card, shadow-modal, shadow-lg, etc.)
|
|
{
|
|
selector:
|
|
"Literal[value=/.*shadow-\\[[^]]+\\].*/], TemplateElement[value.raw=/.*shadow-\\[[^]]+\\].*/]",
|
|
message:
|
|
'Avoid arbitrary shadow classes (shadow-[...]). Use design tokens (shadow-card, shadow-modal, shadow-button-primary-glow) or Tailwind shadow-lg, shadow-xl. See docs/DESIGN_TOKENS.md. For exceptions, add eslint-disable.',
|
|
},
|
|
],
|
|
},
|
|
settings: {
|
|
react: {
|
|
version: 'detect',
|
|
},
|
|
},
|
|
}, {
|
|
files: ['**/*.stories.tsx', '**/*.stories.ts'],
|
|
rules: {
|
|
'react-hooks/rules-of-hooks': 'off',
|
|
},
|
|
}, {
|
|
files: ['**/*.test.ts', '**/*.test.tsx', '**/__tests__/**'],
|
|
rules: {
|
|
'@typescript-eslint/no-explicit-any': 'off',
|
|
},
|
|
}, {
|
|
ignores: [
|
|
'node_modules/',
|
|
'dist/',
|
|
'dist_verification/',
|
|
'build/',
|
|
'target/',
|
|
'storybook-static/',
|
|
'e2e/',
|
|
'playwright-report/',
|
|
'public/sw.js',
|
|
'scripts/',
|
|
'src/types/generated/',
|
|
'src/services/generated/',
|
|
'_archive/',
|
|
'archive/',
|
|
'*.config.js',
|
|
'*.config.ts',
|
|
'*.config.cjs',
|
|
'**/ui.backup/**',
|
|
],
|
|
}, ...storybook.configs["flat/recommended"]];
|