Skip to content

Commit 287468f

Browse files
authored
🤖 Integrate eslint-plugin-tailwindcss (#394)
## Summary Integrates `eslint-plugin-tailwindcss` (beta v4 support) to enforce consistent Tailwind CSS class ordering and modern syntax. ## Changes - Add `[email protected]` with partial Tailwind v4 support - Configure plugin rules: - `classnames-order`: Enforce consistent class ordering - `enforces-shorthand`: Use Tailwind shorthands (e.g., `truncate` instead of `whitespace-nowrap overflow-hidden text-ellipsis`) - `migration-from-tailwind-2`: Update deprecated v2 classes (e.g., `flex-shrink-0` → `shrink-0`) - `no-contradicting-classname`: Error on conflicting classes ## Results Auto-fixed 408 Tailwind warnings across 57 files: - 368 class ordering fixes for consistency - 20 shorthand replacements for brevity - 20 deprecated class migrations All changes are auto-generated by ESLint --fix and improve code quality without affecting functionality. _Generated with `cmux`_
1 parent f364d8c commit 287468f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+395
-380
lines changed

bun.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"eslint": "^9.36.0",
8080
"eslint-plugin-react": "^7.37.5",
8181
"eslint-plugin-react-hooks": "^5.2.0",
82+
"eslint-plugin-tailwindcss": "4.0.0-beta.0",
8283
"jest": "^30.1.3",
8384
"mermaid": "^11.12.0",
8485
"playwright": "^1.56.0",
@@ -1517,6 +1518,8 @@
15171518

15181519
"eslint-plugin-react-hooks": ["[email protected]", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
15191520

1521+
"eslint-plugin-tailwindcss": ["[email protected]", "", { "dependencies": { "fast-glob": "^3.2.5", "postcss": "^8.4.4", "synckit": "^0.11.4", "tailwind-api-utils": "^1.0.3" }, "peerDependencies": { "tailwindcss": "^3.4.0 || ^4.0.0" } }, "sha512-WWCajZgQu38Sd67ZCl2W6i3MRzqB0d+H8s4qV9iB6lBJbsDOIpIlj6R1Fj2FXkoWErbo05pZnZYbCGIU9o/DsA=="],
1522+
15201523
"eslint-scope": ["[email protected]", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
15211524

15221525
"eslint-visitor-keys": ["[email protected]", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
@@ -2693,6 +2696,8 @@
26932696

26942697
"synckit": ["[email protected]", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="],
26952698

2699+
"tailwind-api-utils": ["[email protected]", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "local-pkg": "^1.1.1" }, "peerDependencies": { "tailwindcss": "^3.3.0 || ^4.0.0 || ^4.0.0-beta" } }, "sha512-KpzUHkH1ug1sq4394SLJX38ZtpeTiqQ1RVyFTTSY2XuHsNSTWUkRo108KmyyrMWdDbQrLYkSHaNKj/a3bmA4sQ=="],
2700+
26962701
"tailwind-merge": ["[email protected]", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
26972702

26982703
"tailwindcss": ["[email protected]", "", {}, "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ=="],

eslint.config.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import js from "@eslint/js";
22
import { defineConfig } from "eslint/config";
33
import react from "eslint-plugin-react";
44
import reactHooks from "eslint-plugin-react-hooks";
5+
import tailwindcss from "eslint-plugin-tailwindcss";
56
import tseslint from "typescript-eslint";
67

78
/**
@@ -154,12 +155,21 @@ export default defineConfig([
154155
plugins: {
155156
react,
156157
"react-hooks": reactHooks,
158+
tailwindcss,
157159
local: localPlugin,
158160
},
159161
settings: {
160162
react: {
161163
version: "detect",
162164
},
165+
tailwindcss: {
166+
// Don't try to load Tailwind config (v4 doesn't export resolveConfig)
167+
config: false,
168+
// CSS files to check
169+
cssFiles: ["**/*.css", "!**/node_modules", "!**/.*", "!**/dist", "!**/build"],
170+
// Disable callees check to avoid resolving config
171+
callees: [],
172+
},
163173
},
164174
rules: {
165175
...react.configs.recommended.rules,
@@ -240,6 +250,15 @@ export default defineConfig([
240250
"react/react-in-jsx-scope": "off",
241251
"react/prop-types": "off",
242252

253+
// Tailwind CSS
254+
"tailwindcss/classnames-order": "warn",
255+
"tailwindcss/enforces-negative-arbitrary-values": "warn",
256+
"tailwindcss/enforces-shorthand": "warn",
257+
"tailwindcss/migration-from-tailwind-2": "warn",
258+
"tailwindcss/no-arbitrary-value": "off",
259+
"tailwindcss/no-contradicting-classname": "error",
260+
"tailwindcss/no-custom-classname": "off",
261+
243262
// Safe Node.js patterns
244263
"local/no-unsafe-child-process": "error",
245264
"local/no-sync-fs-methods": "error",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
"eslint": "^9.36.0",
120120
"eslint-plugin-react": "^7.37.5",
121121
"eslint-plugin-react-hooks": "^5.2.0",
122+
"eslint-plugin-tailwindcss": "4.0.0-beta.0",
122123
"jest": "^30.1.3",
123124
"mermaid": "^11.12.0",
124125
"playwright": "^1.56.0",

src/App.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ function AppInner() {
662662

663663
return (
664664
<>
665-
<div className="flex h-screen overflow-hidden bg-bg-dark [@media(max-width:768px)]:flex-col">
665+
<div className="bg-bg-dark flex h-screen overflow-hidden [@media(max-width:768px)]:flex-col">
666666
<LeftSidebar
667667
projects={projects}
668668
workspaceMetadata={workspaceMetadata}
@@ -682,8 +682,8 @@ function AppInner() {
682682
sortedWorkspacesByProject={sortedWorkspacesByProject}
683683
workspaceRecency={workspaceRecency}
684684
/>
685-
<div className="flex-1 flex flex-col overflow-hidden min-w-0 [@media(max-width:768px)]:w-full">
686-
<div className="flex-1 flex overflow-hidden [@media(max-width:768px)]:flex-col">
685+
<div className="flex min-w-0 flex-1 flex-col overflow-hidden [@media(max-width:768px)]:w-full">
686+
<div className="flex flex-1 overflow-hidden [@media(max-width:768px)]:flex-col">
687687
{selectedWorkspace ? (
688688
<ErrorBoundary
689689
workspaceInfo={`${selectedWorkspace.projectName}/${selectedWorkspace.namedWorkspacePath?.split("/").pop() ?? selectedWorkspace.workspaceId}`}
@@ -701,7 +701,7 @@ function AppInner() {
701701
</ErrorBoundary>
702702
) : (
703703
<div
704-
className="text-center max-w-3xl mx-auto w-full [&_h2]:text-white [&_h2]:mb-4 [&_h2]:font-bold [&_h2]:tracking-tight [&_p]:text-muted [&_p]:leading-[1.6]"
704+
className="[&_p]:text-muted mx-auto w-full max-w-3xl text-center [&_h2]:mb-4 [&_h2]:font-bold [&_h2]:tracking-tight [&_h2]:text-white [&_p]:leading-[1.6]"
705705
style={{
706706
padding: "clamp(40px, 10vh, 100px) 20px",
707707
fontSize: "clamp(14px, 2vw, 16px)",

src/components/AIView.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
250250
)}
251251
style={{ containerType: "inline-size" }}
252252
>
253-
<div className="flex-1 flex flex-col items-center justify-center h-full text-placeholder text-center">
253+
<div className="text-placeholder flex h-full flex-1 flex-col items-center justify-center text-center">
254254
<h3 className="m-0 mb-2.5 text-base font-medium">Loading workspace...</h3>
255255
</div>
256256
</div>
@@ -291,7 +291,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
291291
)}
292292
style={{ containerType: "inline-size" }}
293293
>
294-
<div className="flex-1 flex flex-col items-center justify-center h-full text-placeholder text-center">
294+
<div className="text-placeholder flex h-full flex-1 flex-col items-center justify-center text-center">
295295
<h3 className="m-0 mb-2.5 text-base font-medium">Loading workspace...</h3>
296296
</div>
297297
</div>
@@ -307,7 +307,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
307307
)}
308308
style={{ containerType: "inline-size" }}
309309
>
310-
<div className="flex-1 flex flex-col items-center justify-center h-full text-placeholder text-center">
310+
<div className="text-placeholder flex h-full flex-1 flex-col items-center justify-center text-center">
311311
<h3 className="m-0 mb-2.5 text-base font-medium">No Workspace Selected</h3>
312312
<p className="m-0 text-[13px]">
313313
Select a workspace from the sidebar to view and interact with Claude
@@ -327,10 +327,10 @@ const AIViewInner: React.FC<AIViewProps> = ({
327327
>
328328
<div
329329
ref={chatAreaRef}
330-
className="flex-1 min-w-96 flex flex-col [@media(max-width:768px)]:min-w-0 [@media(max-width:768px)]:w-full [@media(max-width:768px)]:max-h-full"
330+
className="flex min-w-96 flex-1 flex-col [@media(max-width:768px)]:max-h-full [@media(max-width:768px)]:w-full [@media(max-width:768px)]:min-w-0"
331331
>
332-
<div className="py-1 px-[15px] bg-separator border-b border-border-light flex justify-between items-center [@media(max-width:768px)]:py-2 [@media(max-width:768px)]:pl-[60px] [@media(max-width:768px)]:flex-wrap [@media(max-width:768px)]:gap-2">
333-
<div className="font-semibold text-foreground flex items-center gap-2 min-w-0 overflow-hidden">
332+
<div className="bg-separator border-border-light flex items-center justify-between border-b px-[15px] py-1 [@media(max-width:768px)]:flex-wrap [@media(max-width:768px)]:gap-2 [@media(max-width:768px)]:py-2 [@media(max-width:768px)]:pl-[60px]">
333+
<div className="text-foreground flex min-w-0 items-center gap-2 overflow-hidden font-semibold">
334334
<StatusIndicator
335335
streaming={canInterrupt}
336336
title={
@@ -342,16 +342,16 @@ const AIViewInner: React.FC<AIViewProps> = ({
342342
workspaceId={workspaceId}
343343
tooltipPosition="bottom"
344344
/>
345-
<span className="font-mono text-xs whitespace-nowrap overflow-hidden text-ellipsis min-w-0">
345+
<span className="min-w-0 truncate font-mono text-xs">
346346
{projectName} / {branch}
347347
</span>
348-
<span className="font-mono text-muted font-normal text-[11px] whitespace-nowrap overflow-hidden text-ellipsis min-w-0">
348+
<span className="text-muted min-w-0 truncate font-mono text-[11px] font-normal">
349349
{namedWorkspacePath}
350350
</span>
351351
<TooltipWrapper inline>
352352
<button
353353
onClick={handleOpenTerminal}
354-
className="bg-transparent border-none cursor-pointer p-1 flex items-center justify-center text-muted transition-colors hover:text-foreground [&_svg]:w-4 [&_svg]:h-4"
354+
className="text-muted hover:text-foreground flex cursor-pointer items-center justify-center border-none bg-transparent p-1 transition-colors [&_svg]:h-4 [&_svg]:w-4"
355355
>
356356
<svg viewBox="0 0 16 16" fill="currentColor">
357357
<path d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0114.25 15H1.75A1.75 1.75 0 010 13.25V2.75zm1.75-.25a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25H1.75zM7.25 8a.75.75 0 01-.22.53l-2.25 2.25a.75.75 0 01-1.06-1.06L5.44 8 3.72 6.28a.75.75 0 111.06-1.06l2.25 2.25c.141.14.22.331.22.53zm1.5 1.5a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z" />
@@ -364,7 +364,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
364364
</div>
365365
</div>
366366

367-
<div className="flex-1 relative overflow-hidden">
367+
<div className="relative flex-1 overflow-hidden">
368368
<div
369369
ref={contentRef}
370370
onWheel={markUserInteraction}
@@ -375,10 +375,10 @@ const AIViewInner: React.FC<AIViewProps> = ({
375375
aria-busy={canInterrupt}
376376
aria-label="Conversation transcript"
377377
tabIndex={0}
378-
className="h-full overflow-y-auto p-[15px] whitespace-pre-wrap break-words leading-[1.5]"
378+
className="h-full overflow-y-auto p-[15px] leading-[1.5] break-words whitespace-pre-wrap"
379379
>
380380
{mergedMessages.length === 0 ? (
381-
<div className="flex-1 flex flex-col items-center justify-center h-full text-placeholder text-center [&_h3]:m-0 [&_h3]:mb-2.5 [&_h3]:text-base [&_h3]:font-medium [&_p]:m-0 [&_p]:text-[13px]">
381+
<div className="text-placeholder flex h-full flex-1 flex-col items-center justify-center text-center [&_h3]:m-0 [&_h3]:mb-2.5 [&_h3]:text-base [&_h3]:font-medium [&_p]:m-0 [&_p]:text-[13px]">
382382
<h3>No Messages Yet</h3>
383383
<p>Send a message below to begin</p>
384384
</div>
@@ -404,7 +404,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
404404
</div>
405405
{isAtCutoff && (
406406
<div
407-
className="my-5 py-3 px-[15px] text-xs font-medium text-center text-edit-mode bg-edit-mode/10"
407+
className="text-edit-mode bg-edit-mode/10 my-5 px-[15px] py-3 text-center text-xs font-medium"
408408
style={{
409409
borderBottom: "3px solid",
410410
borderImage:
@@ -463,7 +463,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
463463
<button
464464
onClick={jumpToBottom}
465465
type="button"
466-
className="absolute bottom-2 left-1/2 -translate-x-1/2 py-1 px-2 text-white border rounded-[20px] text-xs font-medium shadow-[0_4px_12px_rgba(0,0,0,0.3)] cursor-pointer transition-all duration-200 z-[100] font-primary backdrop-blur-[1px] hover:scale-105 active:scale-95"
466+
className="font-primary absolute bottom-2 left-1/2 z-[100] -translate-x-1/2 cursor-pointer rounded-[20px] border px-2 py-1 text-xs font-medium text-white shadow-[0_4px_12px_rgba(0,0,0,0.3)] backdrop-blur-[1px] transition-all duration-200 hover:scale-105 active:scale-95"
467467
style={{
468468
background: "hsl(from var(--color-assistant-border) h s l / 0.1)",
469469
borderColor: "hsl(from var(--color-assistant-border) h s l / 0.4)",

src/components/ChatInput.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
704704

705705
return (
706706
<div
707-
className="relative pt-[5px] px-[15px] pb-[15px] bg-separator border-t border-border-light flex flex-col gap-1"
707+
className="bg-separator border-border-light relative flex flex-col gap-1 border-t px-[15px] pt-[5px] pb-[15px]"
708708
style={{ containerType: "inline-size" }}
709709
data-component="ChatInputSection"
710710
>
@@ -717,7 +717,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
717717
ariaLabel="Slash command suggestions"
718718
listId={commandListId}
719719
/>
720-
<div className="flex gap-2.5 items-end" data-component="ChatInputControls">
720+
<div className="flex items-end gap-2.5" data-component="ChatInputControls">
721721
<VimTextArea
722722
ref={inputRef}
723723
value={input}
@@ -742,13 +742,13 @@ export const ChatInput: React.FC<ChatInputProps> = ({
742742
<ImageAttachments images={imageAttachments} onRemove={handleRemoveImage} />
743743
<div className="flex flex-col gap-1" data-component="ChatModeToggles">
744744
{editingMessage && (
745-
<div className="text-[11px] text-edit-mode font-medium">
745+
<div className="text-edit-mode text-[11px] font-medium">
746746
Editing message ({formatKeybind(KEYBINDS.CANCEL_EDIT)} to cancel)
747747
</div>
748748
)}
749749
<div className="flex items-center">
750750
<ChatToggles modelString={preferredModel}>
751-
<div className="flex items-center gap-1 mr-3 h-[11px] @[700px]:[&_.help-indicator-wrapper]:hidden">
751+
<div className="mr-3 flex h-[11px] items-center gap-1 @[700px]:[&_.help-indicator-wrapper]:hidden">
752752
<ModelSelector
753753
ref={modelSelectorRef}
754754
value={preferredModel}
@@ -779,7 +779,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
779779
</span>
780780
</div>
781781
</ChatToggles>
782-
<div className="flex items-center gap-1.5 ml-auto max-@[700px]:hidden">
782+
<div className="max-@[700px]:hidden ml-auto flex items-center gap-1.5">
783783
<div
784784
className={cn(
785785
"flex gap-0 bg-toggle-bg rounded",

src/components/ChatInputToast.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ interface ChatInputToastProps {
2222
}
2323

2424
export const SolutionLabel: React.FC<{ children: ReactNode }> = ({ children }) => (
25-
<div className="text-muted-light text-[10px] mb-1 uppercase">{children}</div>
25+
<div className="text-muted-light mb-1 text-[10px] uppercase">{children}</div>
2626
);
2727

2828
export const ChatInputToast: React.FC<ChatInputToastProps> = ({ toast, onDismiss }) => {
@@ -61,27 +61,27 @@ export const ChatInputToast: React.FC<ChatInputToastProps> = ({ toast, onDismiss
6161

6262
if (isRichError) {
6363
return (
64-
<div className="absolute bottom-full left-[15px] right-[15px] mb-2 z-[1000] pointer-events-none [&>*]:pointer-events-auto">
64+
<div className="pointer-events-none absolute right-[15px] bottom-full left-[15px] z-[1000] mb-2 [&>*]:pointer-events-auto">
6565
<div
6666
role="alert"
6767
aria-live="assertive"
68-
className="bg-toast-fatal-bg border border-toast-fatal-border rounded px-3 py-2.5 text-xs text-danger-soft animate-[toastSlideIn_0.2s_ease-out] shadow-[0_4px_12px_rgba(0,0,0,0.3)]"
68+
className="bg-toast-fatal-bg border-toast-fatal-border text-danger-soft animate-[toastSlideIn_0.2s_ease-out] rounded border px-3 py-2.5 text-xs shadow-[0_4px_12px_rgba(0,0,0,0.3)]"
6969
>
7070
<div className="flex items-start gap-1.5">
7171
<span className="text-sm leading-none"></span>
7272
<div className="flex-1">
73-
{toast.title && <div className="font-semibold mb-1.5">{toast.title}</div>}
74-
<div className="text-light leading-[1.4] mt-1.5">{toast.message}</div>
73+
{toast.title && <div className="mb-1.5 font-semibold">{toast.title}</div>}
74+
<div className="text-light mt-1.5 leading-[1.4]">{toast.message}</div>
7575
{toast.solution && (
76-
<div className="bg-dark rounded px-2 py-1.5 mt-2 font-monospace text-[11px] text-code-type">
76+
<div className="bg-dark font-monospace text-code-type mt-2 rounded px-2 py-1.5 text-[11px]">
7777
{toast.solution}
7878
</div>
7979
)}
8080
</div>
8181
<button
8282
onClick={handleDismiss}
8383
aria-label="Dismiss"
84-
className="bg-transparent border-0 text-inherit cursor-pointer p-0 w-4 h-4 flex items-center justify-center text-base leading-none opacity-60 transition-opacity hover:opacity-100"
84+
className="flex h-4 w-4 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-base leading-none text-inherit opacity-60 transition-opacity hover:opacity-100"
8585
>
8686
×
8787
</button>
@@ -93,7 +93,7 @@ export const ChatInputToast: React.FC<ChatInputToastProps> = ({ toast, onDismiss
9393

9494
// Regular toast for simple messages and success
9595
return (
96-
<div className="absolute bottom-full left-[15px] right-[15px] mb-2 z-[1000] pointer-events-none [&>*]:pointer-events-auto">
96+
<div className="pointer-events-none absolute right-[15px] bottom-full left-[15px] z-[1000] mb-2 [&>*]:pointer-events-auto">
9797
<div
9898
role={toast.type === "error" ? "alert" : "status"}
9999
aria-live={toast.type === "error" ? "assertive" : "polite"}
@@ -107,14 +107,14 @@ export const ChatInputToast: React.FC<ChatInputToastProps> = ({ toast, onDismiss
107107
>
108108
<span className="text-sm leading-none">{toast.type === "success" ? "✓" : "⚠"}</span>
109109
<div className="flex-1">
110-
{toast.title && <div className="font-semibold mb-px text-[11px]">{toast.title}</div>}
110+
{toast.title && <div className="mb-px text-[11px] font-semibold">{toast.title}</div>}
111111
<div className="opacity-90">{toast.message}</div>
112112
</div>
113113
{toast.type === "error" && (
114114
<button
115115
onClick={handleDismiss}
116116
aria-label="Dismiss"
117-
className="bg-transparent border-0 text-inherit cursor-pointer p-0 w-4 h-4 flex items-center justify-center text-base leading-none opacity-60 transition-opacity hover:opacity-100"
117+
className="flex h-4 w-4 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-base leading-none text-inherit opacity-60 transition-opacity hover:opacity-100"
118118
>
119119
×
120120
</button>

0 commit comments

Comments
 (0)