claudia-mentor 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/hooks.json +11 -1
- package/hooks/scripts/check-css.py +179 -0
- package/package.json +1 -1
package/hooks/hooks.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Claudia mentor hooks: security, anti-patterns, dependencies, Docker, git hygiene, accessibility, license compliance, teaching moments, compaction tips, session tips, prompt coaching, run suggestions, next steps, and milestones",
|
|
2
|
+
"description": "Claudia mentor hooks: security, anti-patterns, CSS, dependencies, Docker, git hygiene, accessibility, license compliance, teaching moments, compaction tips, session tips, prompt coaching, run suggestions, next steps, and milestones",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
@@ -71,6 +71,16 @@
|
|
|
71
71
|
}
|
|
72
72
|
],
|
|
73
73
|
"matcher": "Edit|Write|MultiEdit"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"hooks": [
|
|
77
|
+
{
|
|
78
|
+
"type": "command",
|
|
79
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/check-css.py",
|
|
80
|
+
"timeout": 10
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"matcher": "Edit|Write|MultiEdit"
|
|
74
84
|
}
|
|
75
85
|
],
|
|
76
86
|
"Stop": [
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Claudia: check-css.py
|
|
4
|
+
PreToolUse hook that warns on common CSS anti-patterns.
|
|
5
|
+
Advisory only (exit 0 with systemMessage), never blocks.
|
|
6
|
+
Session-aware dedup to avoid repeating warnings.
|
|
7
|
+
Only fires on CSS/SCSS/style files and inline styles.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
# CSS anti-patterns to detect
|
|
16
|
+
# Each: (pattern_regex, description, advice, pattern_id)
|
|
17
|
+
CSS_ANTI_PATTERNS = [
|
|
18
|
+
(
|
|
19
|
+
r'!important',
|
|
20
|
+
"!important usage detected",
|
|
21
|
+
"!important overrides all specificity and makes styles hard to maintain. Fix the specificity conflict instead.",
|
|
22
|
+
"important",
|
|
23
|
+
),
|
|
24
|
+
(
|
|
25
|
+
r'z-index:\s*(?:9{3,}|[1-9]\d{3,})',
|
|
26
|
+
"Extremely high z-index",
|
|
27
|
+
"z-index values like 9999 create an arms race. Use a z-index scale (10, 20, 30...) or CSS variables.",
|
|
28
|
+
"z_index_high",
|
|
29
|
+
),
|
|
30
|
+
(
|
|
31
|
+
r'(?:color|background(?:-color)?|border(?:-color)?)\s*:\s*#[0-9a-fA-F]{3,8}\s*[;\n]',
|
|
32
|
+
"Hardcoded color value",
|
|
33
|
+
"Use CSS custom properties (var(--color-name)) or design tokens instead of hardcoded hex values for maintainability.",
|
|
34
|
+
"hardcoded_color",
|
|
35
|
+
),
|
|
36
|
+
(
|
|
37
|
+
r'\*\s*\{[^}]*(?:margin|padding)\s*:\s*0',
|
|
38
|
+
"Universal selector reset",
|
|
39
|
+
"Resetting all margins/padding with * {} is expensive and can break components. Use a targeted reset or normalize.css.",
|
|
40
|
+
"universal_reset",
|
|
41
|
+
),
|
|
42
|
+
(
|
|
43
|
+
r'(?:width|height)\s*:\s*\d+px\s*[;\n].*(?:width|height)\s*:\s*\d+px',
|
|
44
|
+
"Fixed pixel dimensions on layout",
|
|
45
|
+
"Fixed px widths can break responsiveness. Consider max-width, min-width, or relative units (%, rem, vw).",
|
|
46
|
+
"fixed_dimensions",
|
|
47
|
+
),
|
|
48
|
+
(
|
|
49
|
+
r'@import\s+(?:url\()?["\'](?!.*\.css)',
|
|
50
|
+
"@import in CSS",
|
|
51
|
+
"@import creates extra HTTP requests and blocks rendering. Use your bundler's import or <link> tags instead.",
|
|
52
|
+
"css_import",
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
r'float\s*:\s*(?:left|right)',
|
|
56
|
+
"Float-based layout",
|
|
57
|
+
"Floats for layout are legacy. Use flexbox or grid instead — they're easier and more predictable.",
|
|
58
|
+
"float_layout",
|
|
59
|
+
),
|
|
60
|
+
(
|
|
61
|
+
r'(?:top|left|right|bottom)\s*:\s*50%.*transform\s*:\s*translate',
|
|
62
|
+
"Manual centering with position + transform",
|
|
63
|
+
"Consider using flexbox (display: flex; align-items: center; justify-content: center) or grid (place-items: center) for cleaner centering.",
|
|
64
|
+
"manual_centering",
|
|
65
|
+
),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# File extensions where CSS anti-patterns are relevant
|
|
69
|
+
CSS_EXTENSIONS = {'.css', '.scss', '.sass', '.less', '.styl', '.pcss'}
|
|
70
|
+
STYLE_FILE_PATTERNS = ['style', 'global', 'theme', 'tailwind']
|
|
71
|
+
|
|
72
|
+
# Files likely to have legitimate hardcoded colors
|
|
73
|
+
COLOR_EXCEPTION_PATTERNS = ['theme', 'tokens', 'variables', 'colors', 'palette', 'global.css', 'tailwind']
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_css_file(file_path):
|
|
77
|
+
"""Check if this is a CSS-like file or a file likely containing styles."""
|
|
78
|
+
_, ext = os.path.splitext(file_path)
|
|
79
|
+
if ext.lower() in CSS_EXTENSIONS:
|
|
80
|
+
return True
|
|
81
|
+
# Also check styled-components / CSS-in-JS in .tsx/.jsx/.vue/.svelte
|
|
82
|
+
if ext.lower() in {'.tsx', '.jsx', '.vue', '.svelte', '.astro'}:
|
|
83
|
+
return True
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_state_file(session_id):
|
|
88
|
+
return os.path.expanduser(f"~/.claude/claudia_css_state_{session_id}.json")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_state(session_id):
|
|
92
|
+
state_file = get_state_file(session_id)
|
|
93
|
+
if os.path.exists(state_file):
|
|
94
|
+
try:
|
|
95
|
+
with open(state_file) as f:
|
|
96
|
+
return set(json.load(f))
|
|
97
|
+
except (json.JSONDecodeError, IOError):
|
|
98
|
+
return set()
|
|
99
|
+
return set()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def save_state(session_id, shown):
|
|
103
|
+
state_file = get_state_file(session_id)
|
|
104
|
+
try:
|
|
105
|
+
os.makedirs(os.path.dirname(state_file), exist_ok=True)
|
|
106
|
+
with open(state_file, "w") as f:
|
|
107
|
+
json.dump(list(shown), f)
|
|
108
|
+
except IOError:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def extract_content(tool_name, tool_input):
|
|
113
|
+
if tool_name == "Write":
|
|
114
|
+
return tool_input.get("content", "")
|
|
115
|
+
elif tool_name == "Edit":
|
|
116
|
+
return tool_input.get("new_string", "")
|
|
117
|
+
elif tool_name == "MultiEdit":
|
|
118
|
+
edits = tool_input.get("edits", [])
|
|
119
|
+
return " ".join(e.get("new_string", "") for e in edits)
|
|
120
|
+
return ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main():
|
|
124
|
+
try:
|
|
125
|
+
input_data = json.loads(sys.stdin.read())
|
|
126
|
+
except json.JSONDecodeError:
|
|
127
|
+
sys.exit(0)
|
|
128
|
+
|
|
129
|
+
session_id = input_data.get("session_id", "default")
|
|
130
|
+
tool_name = input_data.get("tool_name", "")
|
|
131
|
+
tool_input = input_data.get("tool_input", {})
|
|
132
|
+
file_path = tool_input.get("file_path", "")
|
|
133
|
+
|
|
134
|
+
if tool_name not in ("Edit", "Write", "MultiEdit"):
|
|
135
|
+
sys.exit(0)
|
|
136
|
+
|
|
137
|
+
if not is_css_file(file_path):
|
|
138
|
+
sys.exit(0)
|
|
139
|
+
|
|
140
|
+
content = extract_content(tool_name, tool_input)
|
|
141
|
+
if not content:
|
|
142
|
+
sys.exit(0)
|
|
143
|
+
|
|
144
|
+
# For non-CSS files, only check if content contains style-related code
|
|
145
|
+
_, ext = os.path.splitext(file_path)
|
|
146
|
+
if ext.lower() not in CSS_EXTENSIONS:
|
|
147
|
+
has_styles = bool(re.search(r'(?:styled|css`|className=|class=|<style)', content))
|
|
148
|
+
if not has_styles:
|
|
149
|
+
sys.exit(0)
|
|
150
|
+
|
|
151
|
+
# Is this a theme/token file where hardcoded colors are expected?
|
|
152
|
+
basename = os.path.basename(file_path).lower()
|
|
153
|
+
is_theme_file = any(p in basename for p in COLOR_EXCEPTION_PATTERNS)
|
|
154
|
+
|
|
155
|
+
shown = load_state(session_id)
|
|
156
|
+
warnings = []
|
|
157
|
+
|
|
158
|
+
for pattern, description, advice, pattern_id in CSS_ANTI_PATTERNS:
|
|
159
|
+
# Skip hardcoded color warning in theme/token files
|
|
160
|
+
if pattern_id == "hardcoded_color" and is_theme_file:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
if re.search(pattern, content, re.DOTALL):
|
|
164
|
+
warning_key = f"{file_path}-{pattern_id}"
|
|
165
|
+
if warning_key not in shown:
|
|
166
|
+
shown.add(warning_key)
|
|
167
|
+
warnings.append(f"- {description}: {advice}")
|
|
168
|
+
|
|
169
|
+
if warnings:
|
|
170
|
+
save_state(session_id, shown)
|
|
171
|
+
message = "Claudia noticed some CSS patterns worth reviewing:\n" + "\n".join(warnings)
|
|
172
|
+
output = json.dumps({"systemMessage": f"\033[38;5;160m{message}\033[0m"})
|
|
173
|
+
print(output)
|
|
174
|
+
|
|
175
|
+
sys.exit(0)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|
package/package.json
CHANGED