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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudia-mentor",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Proactive technology mentor, security advisor, and prompt coach for Claude Code",
5
5
  "author": "Regan O'Malley <regan@reganomalley.com>",
6
6
  "license": "MIT",