2026-02-19 15:08:05 +00:00
// eslint-plugin-storybook optional: install if needed for Storybook-specific lint rules
// import storybook from "eslint-plugin-storybook";
2026-02-02 18:34:14 +00:00
2025-12-03 21:56:50 +00:00
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' ;
2026-02-02 18:34:14 +00:00
export default [ js . configs . recommended , {
files : [ '**/*.{ts,tsx,js,jsx}' ] ,
languageOptions : {
parser : typescriptParser ,
parserOptions : {
ecmaFeatures : {
jsx : true ,
2025-12-03 21:56:50 +00:00
} ,
2026-02-02 18:34:14 +00:00
ecmaVersion : 2022 ,
sourceType : 'module' ,
2025-12-03 21:56:50 +00:00
} ,
2026-02-02 18:34:14 +00:00
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' ,
2026-02-19 15:27:10 +00:00
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' ,
2026-02-02 18:34:14 +00:00
// 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' ,
2025-12-03 21:56:50 +00:00
} ,
2026-02-02 18:34:14 +00:00
} ,
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' ,
2026-03-09 18:36:33 +00:00
'@typescript-eslint/no-explicit-any' : 'warn' ,
2026-02-02 18:34:14 +00:00
'@typescript-eslint/no-non-null-assertion' : 'warn' ,
2026-01-07 18:39:21 +00:00
2026-02-02 18:34:14 +00:00
// 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 }
] ,
2026-01-07 18:39:21 +00:00
2026-02-02 18:34:14 +00:00
// 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-useless-escape' : 'error' ,
'no-prototype-builtins' : 'warn' ,
2026-01-15 20:23:31 +00:00
2026-02-02 18:34:14 +00:00
// 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.' ,
} ,
2026-02-12 01:15:11 +00:00
// Colors: Prevent Tailwind default colors (use SUMI design system semantic tokens instead)
2026-02-02 18:34:14 +00:00
// 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
2026-02-12 01:15:11 +00:00
// Allow: semantic tokens (primary, secondary, destructive, success, warning, muted, etc.) and sumi-* tokens
2026-02-02 18:34:14 +00:00
{
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 :
2026-02-12 01:15:11 +00:00
'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.' ,
2026-02-02 18:34:14 +00:00
} ,
// 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.' ,
2025-12-03 21:56:50 +00:00
} ,
feat(web): UI premium Discord/Spotify-like — tokens, shadows, focus, layout
Plan UI premium 6–8 semaines (design system, shell, Storybook, a11y):
- Design system: DESIGN_TOKENS.md, APP_SHELL.md, FULL_LAYOUT_PAGE.md. Single source
for layout/shell (index.css), shadows (design-system.css), durations/easing.
- Tokens: shadow-cover-depth, shadow-gold-glow, shadow-fab-glow; layout max-height
(max-h-layout-drawer, max-h-layout-panel, max-h-layout-list). All duration-200/300/500
replaced by --duration-fast/normal/slow. Arbitrary shadows replaced by token classes.
- Shell & player: Sidebar, Header, GlobalPlayer, MiniPlayer, PlayerQueue, PlayerControls,
AudioPlayer use tokens; focus-visible on Sidebar, PlayerQueue, DropdownMenuTrigger/Item,
TabsTrigger. Typography: text-[10px]/[9px] → text-xs where applicable.
- ESLint: no-restricted-syntax (warn) for w-/h-/rounded-/shadow-/text-/spacing arbitrary.
- Scripts: report-arbitrary-values.mjs, capture/compare/generate visual; visual-complete.spec.ts.
- Stories full layout: Dashboard, Playlists, Library, Settings, Profile in DashboardLayout.stories.
- .cursorrules + README: DESIGN_TOKENS, APP_SHELL, visual commands, no arbitrary without justification.
- apps/web/.gitignore: e2e test artifacts (test-results-visual, playwright-report-visual).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 16:15:58 +00:00
// 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.' ,
} ,
2025-12-03 21:56:50 +00:00
] ,
} ,
2026-02-02 18:34:14 +00:00
settings : {
react : {
version : 'detect' ,
} ,
} ,
2026-02-19 15:27:10 +00:00
} , {
files : [ '**/*.stories.tsx' , '**/*.stories.ts' ] ,
rules : {
'react-hooks/rules-of-hooks' : 'off' ,
} ,
2026-03-09 18:36:33 +00:00
} , {
files : [ '**/*.test.ts' , '**/*.test.tsx' , '**/__tests__/**' ] ,
rules : {
'@typescript-eslint/no-explicit-any' : 'off' ,
} ,
2026-02-02 18:34:14 +00:00
} , {
ignores : [
'node_modules/' ,
'dist/' ,
2026-02-19 15:08:05 +00:00
'dist_verification/' ,
2026-02-02 18:34:14 +00:00
'build/' ,
'target/' ,
2026-02-19 15:08:05 +00:00
'storybook-static/' ,
2026-02-19 15:27:10 +00:00
'e2e/' ,
'playwright-report/' ,
'public/sw.js' ,
'scripts/' ,
'src/types/generated/' ,
2026-02-02 18:34:14 +00:00
'_archive/' ,
'archive/' ,
'*.config.js' ,
'*.config.ts' ,
'*.config.cjs' ,
'**/ui.backup/**' ,
] ,
2026-02-19 15:08:05 +00:00
} ] ;