@booklib/skills 1.2.0 → 1.3.1
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/CONTRIBUTING.md +122 -0
- package/README.md +20 -2
- package/ROADMAP.md +36 -0
- package/animation-at-work/evals/evals.json +44 -0
- package/animation-at-work/examples/after.md +64 -0
- package/animation-at-work/examples/before.md +35 -0
- package/animation-at-work/scripts/audit_animations.py +295 -0
- package/bin/skills.js +552 -42
- package/clean-code-reviewer/SKILL.md +109 -1
- package/clean-code-reviewer/evals/evals.json +121 -3
- package/clean-code-reviewer/examples/after.md +48 -0
- package/clean-code-reviewer/examples/before.md +33 -0
- package/clean-code-reviewer/references/api_reference.md +158 -0
- package/clean-code-reviewer/references/practices-catalog.md +282 -0
- package/clean-code-reviewer/references/review-checklist.md +254 -0
- package/clean-code-reviewer/scripts/pre-review.py +206 -0
- package/data-intensive-patterns/evals/evals.json +43 -0
- package/data-intensive-patterns/examples/after.md +61 -0
- package/data-intensive-patterns/examples/before.md +38 -0
- package/data-intensive-patterns/scripts/adr.py +213 -0
- package/data-pipelines/evals/evals.json +45 -0
- package/data-pipelines/examples/after.md +97 -0
- package/data-pipelines/examples/before.md +37 -0
- package/data-pipelines/scripts/new_pipeline.py +444 -0
- package/design-patterns/evals/evals.json +46 -0
- package/design-patterns/examples/after.md +52 -0
- package/design-patterns/examples/before.md +29 -0
- package/design-patterns/scripts/scaffold.py +807 -0
- package/domain-driven-design/SKILL.md +120 -0
- package/domain-driven-design/evals/evals.json +48 -0
- package/domain-driven-design/examples/after.md +80 -0
- package/domain-driven-design/examples/before.md +43 -0
- package/domain-driven-design/scripts/scaffold.py +421 -0
- package/effective-java/evals/evals.json +46 -0
- package/effective-java/examples/after.md +83 -0
- package/effective-java/examples/before.md +37 -0
- package/effective-java/scripts/checkstyle_setup.py +211 -0
- package/effective-kotlin/evals/evals.json +45 -0
- package/effective-kotlin/examples/after.md +36 -0
- package/effective-kotlin/examples/before.md +38 -0
- package/effective-python/evals/evals.json +44 -0
- package/effective-python/examples/after.md +56 -0
- package/effective-python/examples/before.md +40 -0
- package/effective-python/references/api_reference.md +218 -0
- package/effective-python/references/practices-catalog.md +483 -0
- package/effective-python/references/review-checklist.md +190 -0
- package/effective-python/scripts/lint.py +173 -0
- package/kotlin-in-action/evals/evals.json +43 -0
- package/kotlin-in-action/examples/after.md +53 -0
- package/kotlin-in-action/examples/before.md +39 -0
- package/kotlin-in-action/scripts/setup_detekt.py +224 -0
- package/lean-startup/evals/evals.json +43 -0
- package/lean-startup/examples/after.md +80 -0
- package/lean-startup/examples/before.md +34 -0
- package/lean-startup/scripts/new_experiment.py +286 -0
- package/microservices-patterns/SKILL.md +140 -0
- package/microservices-patterns/evals/evals.json +45 -0
- package/microservices-patterns/examples/after.md +69 -0
- package/microservices-patterns/examples/before.md +40 -0
- package/microservices-patterns/scripts/new_service.py +583 -0
- package/package.json +2 -8
- package/refactoring-ui/evals/evals.json +45 -0
- package/refactoring-ui/examples/after.md +85 -0
- package/refactoring-ui/examples/before.md +58 -0
- package/refactoring-ui/scripts/audit_css.py +250 -0
- package/skill-router/SKILL.md +142 -0
- package/skill-router/evals/evals.json +38 -0
- package/skill-router/examples/after.md +63 -0
- package/skill-router/examples/before.md +39 -0
- package/skill-router/references/api_reference.md +24 -0
- package/skill-router/references/routing-heuristics.md +89 -0
- package/skill-router/references/skill-catalog.md +156 -0
- package/skill-router/scripts/route.py +266 -0
- package/storytelling-with-data/evals/evals.json +47 -0
- package/storytelling-with-data/examples/after.md +50 -0
- package/storytelling-with-data/examples/before.md +33 -0
- package/storytelling-with-data/scripts/chart_review.py +301 -0
- package/system-design-interview/evals/evals.json +45 -0
- package/system-design-interview/examples/after.md +94 -0
- package/system-design-interview/examples/before.md +27 -0
- package/system-design-interview/scripts/new_design.py +421 -0
- package/using-asyncio-python/evals/evals.json +43 -0
- package/using-asyncio-python/examples/after.md +68 -0
- package/using-asyncio-python/examples/before.md +39 -0
- package/using-asyncio-python/scripts/check_blocking.py +270 -0
- package/web-scraping-python/evals/evals.json +46 -0
- package/web-scraping-python/examples/after.md +109 -0
- package/web-scraping-python/examples/before.md +40 -0
- package/web-scraping-python/scripts/new_scraper.py +231 -0
- /package/{effective-python-skill → effective-python}/SKILL.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-01-pythonic-thinking.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-02-lists-and-dicts.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-03-functions.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-04-comprehensions-generators.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-05-classes-interfaces.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-06-metaclasses-attributes.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-07-concurrency.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-08-robustness-performance.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-09-testing-debugging.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-10-collaboration.md +0 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for wanting to add a skill. A skill packages expert knowledge from a book into reusable instructions that AI agents can apply to real tasks.
|
|
4
|
+
|
|
5
|
+
## What makes a good skill?
|
|
6
|
+
|
|
7
|
+
A skill is worth adding when the source book:
|
|
8
|
+
- Contains specific, actionable advice (not just general philosophy)
|
|
9
|
+
- Covers a topic useful to software engineers or designers
|
|
10
|
+
- Has enough depth to fill a meaningful SKILL.md (300+ lines)
|
|
11
|
+
|
|
12
|
+
## Adding a new skill
|
|
13
|
+
|
|
14
|
+
### 1. Create the folder
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
skill-name/
|
|
18
|
+
├── SKILL.md # Required
|
|
19
|
+
├── examples/
|
|
20
|
+
│ ├── before.md # Code or artifact before applying the skill
|
|
21
|
+
│ └── after.md # The improved version
|
|
22
|
+
└── evals/
|
|
23
|
+
└── evals.json # Test cases
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The folder name must be lowercase, hyphen-separated, and match the `name` field in `SKILL.md` exactly.
|
|
27
|
+
|
|
28
|
+
### 2. Write SKILL.md
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
---
|
|
32
|
+
name: skill-name
|
|
33
|
+
description: >
|
|
34
|
+
What this skill does and when to trigger it. Include specific
|
|
35
|
+
keywords agents should look for. Max 1024 characters.
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
# Skill Title
|
|
39
|
+
|
|
40
|
+
You are an expert in [domain] grounded in [Book Title] by [Author].
|
|
41
|
+
|
|
42
|
+
## When to use this skill
|
|
43
|
+
|
|
44
|
+
[Describe trigger conditions — what user requests or code patterns activate this skill]
|
|
45
|
+
|
|
46
|
+
## Core principles
|
|
47
|
+
|
|
48
|
+
[The key ideas from the book, organized for an AI agent to apply]
|
|
49
|
+
|
|
50
|
+
## How to apply
|
|
51
|
+
|
|
52
|
+
[Step-by-step process the agent follows]
|
|
53
|
+
|
|
54
|
+
## Examples
|
|
55
|
+
|
|
56
|
+
[At least one concrete before/after showing the skill in action]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Requirements:**
|
|
60
|
+
- `name`: lowercase letters and hyphens only, matches folder name
|
|
61
|
+
- `description`: 1–1024 characters, describes what it does AND when to use it
|
|
62
|
+
- Body: clear instructions an AI agent can follow immediately
|
|
63
|
+
|
|
64
|
+
**Keep SKILL.md under 500 lines.** Move deep reference material to `references/` and link to it.
|
|
65
|
+
|
|
66
|
+
### 3. Add before/after examples
|
|
67
|
+
|
|
68
|
+
`examples/before.md` — code or artifact that violates the book's principles.
|
|
69
|
+
`examples/after.md` — the same thing improved by applying the skill.
|
|
70
|
+
|
|
71
|
+
These power the `npx @booklib/skills demo <name>` command.
|
|
72
|
+
|
|
73
|
+
### 4. Add evals
|
|
74
|
+
|
|
75
|
+
`evals/evals.json` — array of test cases verifying the skill works:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"evals": [
|
|
80
|
+
{
|
|
81
|
+
"id": "eval-01-short-description",
|
|
82
|
+
"prompt": "The prompt to send to the agent (include code or a scenario)",
|
|
83
|
+
"expectations": [
|
|
84
|
+
"The agent should do X",
|
|
85
|
+
"The agent should flag Y",
|
|
86
|
+
"The agent should NOT do Z"
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Aim for 3–5 evals per skill covering:
|
|
94
|
+
1. A clear violation of the book's principles
|
|
95
|
+
2. A subtle or intermediate case
|
|
96
|
+
3. Already-good code (the agent should recognize it and not manufacture issues)
|
|
97
|
+
|
|
98
|
+
### 5. Submit a PR
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git checkout -b skill/book-name
|
|
102
|
+
# add your skill folder
|
|
103
|
+
git add skill-name/
|
|
104
|
+
git commit -m "feat: add skill-name skill"
|
|
105
|
+
gh pr create --title "feat: add skill-name" --body "..."
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
PR checklist:
|
|
109
|
+
- [ ] Folder name matches `name` in SKILL.md
|
|
110
|
+
- [ ] `description` is under 1024 characters
|
|
111
|
+
- [ ] SKILL.md is under 500 lines
|
|
112
|
+
- [ ] `examples/before.md` and `examples/after.md` exist
|
|
113
|
+
- [ ] `evals/evals.json` has at least 3 test cases
|
|
114
|
+
- [ ] README.md skills table updated
|
|
115
|
+
|
|
116
|
+
## Requesting a skill
|
|
117
|
+
|
|
118
|
+
Open an issue titled **"Skill Request: [Book Name]"** and describe why the book would make a good skill. Community members can then pick it up.
|
|
119
|
+
|
|
120
|
+
## Questions
|
|
121
|
+
|
|
122
|
+
Use [GitHub Discussions](../../discussions) for questions, ideas, and feedback.
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ npx @booklib/skills add effective-kotlin
|
|
|
63
63
|
npx @booklib/skills add --all
|
|
64
64
|
|
|
65
65
|
# Add globally (available in all projects)
|
|
66
|
-
npx @booklib/skills add --all --
|
|
66
|
+
npx @booklib/skills add --all --globalcl
|
|
67
67
|
|
|
68
68
|
# List available skills
|
|
69
69
|
npx @booklib/skills list
|
|
@@ -79,6 +79,23 @@ git clone https://github.com/ZLStas/skills.git
|
|
|
79
79
|
cp -r skills/effective-kotlin /path/to/project/.claude/skills/
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
## Automatic Skill Routing
|
|
83
|
+
|
|
84
|
+
You don't need to know which skill to apply — the **[skill-router](./skill-router/)** meta-skill does it for you.
|
|
85
|
+
|
|
86
|
+
When an AI agent receives a task, it can invoke `skill-router` first to identify the 1–2 most relevant skills based on the file, language, domain, and work type. The router then returns a ranked recommendation with rationale, so the right expertise is applied automatically.
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
User: "Review my order processing service"
|
|
90
|
+
|
|
91
|
+
→ skill-router selects:
|
|
92
|
+
Primary: domain-driven-design — domain model design (Aggregates, Value Objects)
|
|
93
|
+
Secondary: microservices-patterns — service boundaries and inter-service communication
|
|
94
|
+
Skip: clean-code-reviewer — premature at design stage; apply later on implementation code
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This means skills compose: `skill-router` acts as an orchestrator that picks the right specialist skills for the context, without requiring the user to know the library upfront.
|
|
98
|
+
|
|
82
99
|
## Skills
|
|
83
100
|
|
|
84
101
|
| Skill | Description |
|
|
@@ -89,13 +106,14 @@ cp -r skills/effective-kotlin /path/to/project/.claude/skills/
|
|
|
89
106
|
| [data-pipelines](./data-pipelines/) | Data pipeline practices from James Densmore's *Data Pipelines Pocket Reference* — ingestion, streaming, transformation, and orchestration |
|
|
90
107
|
| [design-patterns](./design-patterns/) | Apply and review GoF design patterns from *Head First Design Patterns* — creational, structural, and behavioral patterns |
|
|
91
108
|
| [domain-driven-design](./domain-driven-design/) | Design and review software using patterns from Eric Evans' *Domain-Driven Design* — tactical and strategic patterns, and Ubiquitous Language |
|
|
92
|
-
| [effective-python](./effective-python
|
|
109
|
+
| [effective-python](./effective-python/) | Python best practices from Brett Slatkin's *Effective Python* (2nd Edition) — Pythonic thinking, functions, classes, concurrency, and testing |
|
|
93
110
|
| [effective-java](./effective-java/) | Java best practices from Joshua Bloch's *Effective Java* (3rd Edition) — object creation, generics, enums, lambdas, and concurrency |
|
|
94
111
|
| [effective-kotlin](./effective-kotlin/) | Best practices from Marcin Moskała's *Effective Kotlin* (2nd Ed) — safety, readability, reusability, and abstraction |
|
|
95
112
|
| [kotlin-in-action](./kotlin-in-action/) | Practices from *Kotlin in Action* (2nd Ed) — functions, classes, lambdas, nullability, and coroutines |
|
|
96
113
|
| [lean-startup](./lean-startup/) | Practices from Eric Ries' *The Lean Startup* — MVP testing, validated learning, Build-Measure-Learn loop, and pivots |
|
|
97
114
|
| [microservices-patterns](./microservices-patterns/) | Expert guidance on microservices patterns from Chris Richardson's *Microservices Patterns* — decomposition, sagas, API gateways, event sourcing, CQRS, and service mesh |
|
|
98
115
|
| [refactoring-ui](./refactoring-ui/) | UI design principles from *Refactoring UI* by Adam Wathan & Steve Schoger — visual hierarchy, layout, typography, and color |
|
|
116
|
+
| [skill-router](./skill-router/) | **Meta-skill.** Automatically selects the 1–2 most relevant skills for a given file, PR, or task — routes by language, domain, and work type with conflict resolution. Use this when the right skill isn't obvious, or let the AI invoke it automatically before applying any skill |
|
|
99
117
|
| [storytelling-with-data](./storytelling-with-data/) | Data visualization and storytelling from Cole Nussbaumer Knaflic's *Storytelling with Data* — effective visuals, decluttering, and narrative structure |
|
|
100
118
|
| [system-design-interview](./system-design-interview/) | System design principles from Alex Xu's *System Design Interview* — scaling, estimation, and real-world system designs |
|
|
101
119
|
| [using-asyncio-python](./using-asyncio-python/) | Asyncio practices from Caleb Hattingh's *Using Asyncio in Python* — coroutines, event loop, tasks, and signal handling |
|
package/ROADMAP.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
- [x] Add repo description: "Book knowledge distilled into AI agent skills — Clean Code, DDD, Effective Kotlin, and more"
|
|
3
|
+
- [x] Add topics: `claude-code`, `agent-skills`, `anthropic-skills`, `ai-skills`, `claude-skills`, `effective-kotlin`, `clean-code`, `design-patterns`, `code-review`, `book-skills`
|
|
4
|
+
- [x] Add website link (npm package page: https://www.npmjs.com/package/@booklib/skills)
|
|
5
|
+
- [ ] Pin the repo on GitHub profile
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
- [x] Keep generic name `@booklib/skills` on npm (already the case)
|
|
10
|
+
- [x] Verify compatibility with agentskills.io spec for all `SKILL.md` files — fixed `effective-python-skill/` → `effective-python/` folder rename
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
- [x] Add before/after examples to each skill's folder (`examples/before.md`, `examples/after.md`)
|
|
15
|
+
- [x] Add `evals/` with real test cases for every skill
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
- [x] Create `CONTRIBUTING.md` with guide on how to add a new book-skill
|
|
20
|
+
- [x] Open 10 "Skill Request: [Book Name]" issues to invite contributions (#2–#11)
|
|
21
|
+
- [x] Add GitHub Discussions to the repo
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
- [x] Make `npx @booklib/skills list` output visually appealing with descriptions
|
|
26
|
+
- [x] Add `--demo` flag showing before/after for each skill
|
|
27
|
+
- [x] Add `--info <skill-name>` to preview what a skill does before installing
|
|
28
|
+
- [ ] Write npm package README with GIFs showing the CLI in action
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
- [x] Expand each skill to include `references/` docs with deeper content
|
|
33
|
+
- [ ] Add `scripts/` with reusable automation where applicable
|
|
34
|
+
- [x] Create a "skill quality checklist" and badge system for completeness (`QUALITY.md`)
|
|
35
|
+
- [x] `skills check <name>` — automated quality validator (Bronze/Silver/Gold/Platinum)
|
|
36
|
+
- [x] `skills eval <name>` — eval runner against Claude API (needs `ANTHROPIC_API_KEY`)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"evals": [
|
|
3
|
+
{
|
|
4
|
+
"id": "eval-01-no-easing-layout-properties-inconsistent-duration",
|
|
5
|
+
"prompt": "Review these CSS animations:\n\n```css\n/* Modal open animation */\n.modal {\n display: none;\n width: 600px;\n padding: 40px;\n background: white;\n border-radius: 8px;\n position: fixed;\n top: 50%;\n left: 50%;\n margin-top: -200px;\n margin-left: -300px;\n}\n\n.modal.visible {\n display: block;\n animation: modal-open 0.4s linear;\n}\n\n@keyframes modal-open {\n from {\n margin-top: -400px;\n opacity: 0;\n width: 400px;\n }\n to {\n margin-top: -200px;\n opacity: 1;\n width: 600px;\n }\n}\n\n/* Button hover */\n.btn:hover {\n background-color: #0056b3;\n padding: 14px 28px;\n border-radius: 12px;\n transition: all 2s linear;\n}\n\n/* Notification badge */\n.badge-new {\n animation: pulse 0.8s linear infinite;\n}\n\n@keyframes pulse {\n 0% { transform: scale(1); }\n 50% { transform: scale(1.2); }\n 100% { transform: scale(1); }\n}\n```",
|
|
6
|
+
"expectations": [
|
|
7
|
+
"Flags `linear` easing on the modal open animation: linear motion feels robotic for UI elements entering the screen; recommends `ease-out` for elements entering (ease-out: fast start, gentle stop conveys natural deceleration)",
|
|
8
|
+
"Flags animating `margin-top` and `width` in the modal keyframes: these are layout properties that trigger full reflow on every frame; recommends replacing with `transform: translateY()` and `transform: scale()` or opacity (Ch 3 / Performance: only animate composite properties — transform and opacity)",
|
|
9
|
+
"Flags `transition: all 2s linear` on the button hover: 2 seconds is far too slow for a hover feedback interaction; functional UI feedback should be 100-200ms; recommends `transition: background-color 150ms ease-out` (Ch 1: duration guidance — micro-interactions 100-200ms)",
|
|
10
|
+
"Flags `transition: all` as an anti-pattern: it animates every changing property including layout properties like padding and border-radius that trigger reflow; recommends specifying only the intended properties",
|
|
11
|
+
"Flags animating `padding` and `border-radius` on the button hover for the same layout-thrashing reason",
|
|
12
|
+
"Flags the `linear` easing on the pulse badge animation: while linear is acceptable for continuous looping motion, there is no `prefers-reduced-motion` media query to disable or reduce this infinite animation for users with vestibular disorders",
|
|
13
|
+
"Notes the modal uses `display: none` switching to `display: block` which cannot be animated directly; the animation will jump; recommends using opacity/transform with visibility or pointer-events instead"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "eval-02-hover-animation-too-slow",
|
|
18
|
+
"prompt": "Review these CSS hover and focus animations:\n\n```css\n/* Navigation link hover */\n.nav-link {\n color: #333;\n text-decoration: none;\n border-bottom: 2px solid transparent;\n transition: border-bottom-color 1.5s ease-in-out,\n color 1.5s ease-in-out;\n}\n\n.nav-link:hover,\n.nav-link:focus {\n color: #0066cc;\n border-bottom-color: #0066cc;\n}\n\n/* Card hover lift effect */\n.card {\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n transition: box-shadow 1.2s ease,\n top 1.2s ease;\n position: relative;\n top: 0;\n}\n\n.card:hover {\n box-shadow: 0 8px 25px rgba(0,0,0,0.15);\n top: -4px;\n}\n\n/* Icon button feedback */\n.icon-btn:active {\n transform: scale(0.9);\n transition: transform 800ms ease;\n}\n```",
|
|
19
|
+
"expectations": [
|
|
20
|
+
"Flags the 1.5s transition on `.nav-link` hover as far too slow for a hover feedback: users expect near-instant response (100-200ms) for hover states; a 1.5s delay makes the interface feel broken or laggy (Ch 1: duration guidance — micro-interactions 100-200ms; functional animations under 1s)",
|
|
21
|
+
"Flags the 1.2s transition on `.card` hover for the same reason: hover lift effects should be 150-250ms to feel responsive",
|
|
22
|
+
"Flags animating `top` on the card hover: `top` is a layout property that triggers reflow; recommends replacing with `transform: translateY(-4px)` which is GPU-accelerated (performance: animate composite properties only)",
|
|
23
|
+
"Flags animating `box-shadow` on the card: box-shadow is not a composite-only property (Ch. 3: only transform and opacity are GPU-composited and avoid layout/paint triggers); recommends replacing with a transform-based approach",
|
|
24
|
+
"Flags the 800ms active/press feedback on `.icon-btn`: press feedback should be the fastest animation in the UI (100-150ms); 800ms means the animation outlasts the press itself and confuses the user",
|
|
25
|
+
"Flags `ease-in-out` on all hover effects: `ease-in-out` is appropriate for elements that move and stay on screen, but for hover state changes `ease-out` is more natural (fast onset, settles gently)",
|
|
26
|
+
"Notes the absence of a `prefers-reduced-motion` media query anywhere in the stylesheet"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "eval-03-clean-animation-transform-opacity-consistent-easing",
|
|
31
|
+
"prompt": "Review these CSS and JavaScript animations:\n\n```css\n/* Dropdown menu enter/exit */\n.dropdown {\n opacity: 0;\n transform: translateY(-8px);\n pointer-events: none;\n transition: opacity 200ms ease-out,\n transform 200ms ease-out;\n}\n\n.dropdown.open {\n opacity: 1;\n transform: translateY(0);\n pointer-events: auto;\n}\n\n/* Toast notification slide-in */\n.toast {\n transform: translateX(110%);\n transition: transform 300ms ease-out;\n}\n\n.toast.show {\n transform: translateX(0);\n}\n\n.toast.hide {\n transform: translateX(110%);\n transition: transform 200ms ease-in;\n}\n\n/* Button press feedback */\n.btn:active {\n transform: scale(0.96);\n transition: transform 100ms ease-in;\n}\n\n/* Reduced motion support */\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n}\n```\n\n```js\n// Page section fade-in on scroll (Web Animations API)\ndocument.querySelectorAll('.section').forEach(section => {\n const observer = new IntersectionObserver(entries => {\n entries.forEach(entry => {\n if (entry.isIntersecting) {\n entry.target.animate(\n [{ opacity: 0, transform: 'translateY(20px)' },\n { opacity: 1, transform: 'translateY(0)' }],\n { duration: 400, easing: 'ease-out', fill: 'forwards' }\n );\n observer.unobserve(entry.target);\n }\n });\n }, { threshold: 0.15 });\n observer.observe(section);\n});\n```",
|
|
32
|
+
"expectations": [
|
|
33
|
+
"Recognizes this is a well-designed set of animations and says so explicitly",
|
|
34
|
+
"Praises animating only `transform` and `opacity` throughout: these are GPU-composited properties that avoid layout reflow and paint (performance: composite-only properties)",
|
|
35
|
+
"Praises using `ease-out` for entering elements (dropdown open, toast show, scroll fade-in) and `ease-in` for exiting elements (toast hide) — correct easing directionality (Ch 1: ease-out for entering, ease-in for leaving)",
|
|
36
|
+
"Praises consistent duration scale: 100ms for press feedback, 200ms for dropdown, 300ms for toast entry, 400ms for scroll reveal — all within appropriate ranges and ordered by interaction importance (Ch 1: duration guidance)",
|
|
37
|
+
"Praises the `prefers-reduced-motion` media query that effectively disables all transitions and animations for users who need it (Ch 5: accessibility, vestibular disorders)",
|
|
38
|
+
"Praises `pointer-events: none` on the hidden dropdown to prevent interaction with invisible elements without removing from DOM (correct implementation detail)",
|
|
39
|
+
"Praises using Web Animations API with IntersectionObserver for scroll-triggered animations: avoids scroll-event jank and unobserves after trigger preventing repeat animations (Ch 3: Web Animations API for playback control)",
|
|
40
|
+
"Does NOT manufacture issues to appear thorough; any suggestions are explicitly framed as minor optional improvements"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# After
|
|
2
|
+
|
|
3
|
+
The navigation drawer animation switches to composite-only properties (`transform` + `opacity`), uses `ease-out` easing, a 250ms duration, and respects `prefers-reduced-motion`.
|
|
4
|
+
|
|
5
|
+
```css
|
|
6
|
+
/* Nav drawer — animate only composite properties (transform, opacity) */
|
|
7
|
+
.nav-drawer {
|
|
8
|
+
/* Position off-screen using transform — not width/height */
|
|
9
|
+
transform: translateX(-280px);
|
|
10
|
+
opacity: 0;
|
|
11
|
+
width: 280px; /* fixed dimensions, never animated */
|
|
12
|
+
height: 100vh;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
background-color: #1a1a2e;
|
|
15
|
+
/* ease-out: fast start → gentle stop — natural for entering elements (Ch 1) */
|
|
16
|
+
transition:
|
|
17
|
+
transform 250ms ease-out,
|
|
18
|
+
opacity 200ms ease-out;
|
|
19
|
+
/* Hint the browser to promote this element to its own compositor layer */
|
|
20
|
+
will-change: transform;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.nav-drawer.open {
|
|
24
|
+
transform: translateX(0);
|
|
25
|
+
opacity: 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Menu items stagger in using animation-delay for follow-through (Ch 1) */
|
|
29
|
+
.nav-drawer .menu-item {
|
|
30
|
+
opacity: 0;
|
|
31
|
+
transform: translateX(-12px);
|
|
32
|
+
transition:
|
|
33
|
+
opacity 180ms ease-out,
|
|
34
|
+
transform 180ms ease-out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.nav-drawer.open .menu-item {
|
|
38
|
+
opacity: 1;
|
|
39
|
+
transform: translateX(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Stagger each item for natural overlapping action */
|
|
43
|
+
.nav-drawer.open .menu-item:nth-child(1) { transition-delay: 60ms; }
|
|
44
|
+
.nav-drawer.open .menu-item:nth-child(2) { transition-delay: 90ms; }
|
|
45
|
+
.nav-drawer.open .menu-item:nth-child(3) { transition-delay: 120ms; }
|
|
46
|
+
.nav-drawer.open .menu-item:nth-child(4) { transition-delay: 150ms; }
|
|
47
|
+
|
|
48
|
+
/* Accessibility: remove all motion for users who prefer it (Ch 5) */
|
|
49
|
+
@media (prefers-reduced-motion: reduce) {
|
|
50
|
+
.nav-drawer,
|
|
51
|
+
.nav-drawer .menu-item {
|
|
52
|
+
transition: opacity 150ms linear !important;
|
|
53
|
+
transform: none !important;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Key improvements:
|
|
59
|
+
- `transform: translateX()` replaces `width`/`height` animation — `transform` and `opacity` are the only composite-only properties that animate on the GPU without triggering layout recalculation (Ch 3: Performance — composite-only properties)
|
|
60
|
+
- Duration reduced from 1.5s to 250ms — functional UI animations should be 200–500ms; 1.5s feels sluggish and blocks the user (Ch 1: Timing and duration)
|
|
61
|
+
- `ease-out` replaces `linear` — entering elements should start fast and slow to a stop; linear easing feels robotic for UI elements (Ch 1: 12 Principles — ease in / ease out)
|
|
62
|
+
- `will-change: transform` promotes the drawer to its own compositor layer, enabling smooth 60fps animation on mobile devices (Ch 3: will-change)
|
|
63
|
+
- Staggered `transition-delay` on menu items creates overlapping action — the drawer and its children don't all stop simultaneously, producing a more natural feel (Ch 1: 12 Principles — follow-through and overlapping action)
|
|
64
|
+
- `@media (prefers-reduced-motion: reduce)` is implemented — users with vestibular disorders receive a simple fade instead of a lateral sweep (Ch 5: Accessibility — prefers-reduced-motion)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Before
|
|
2
|
+
|
|
3
|
+
A CSS animation on a navigation drawer that animates `width` and `height` (layout-triggering properties) with `linear` easing and a 1.5-second duration, with no `prefers-reduced-motion` support.
|
|
4
|
+
|
|
5
|
+
```css
|
|
6
|
+
.nav-drawer {
|
|
7
|
+
width: 0;
|
|
8
|
+
height: 0;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
background-color: #1a1a2e;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.nav-drawer.open {
|
|
14
|
+
width: 280px;
|
|
15
|
+
height: 100vh;
|
|
16
|
+
/* Animates layout properties — forces browser to recalculate layout
|
|
17
|
+
on every frame, causing jank on low-powered devices */
|
|
18
|
+
transition: width 1.5s linear, height 1.5s linear;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.nav-drawer .menu-item {
|
|
22
|
+
opacity: 0;
|
|
23
|
+
/* Also animates layout property margin */
|
|
24
|
+
margin-left: -280px;
|
|
25
|
+
transition: opacity 1.5s linear, margin-left 1.5s linear;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.nav-drawer.open .menu-item {
|
|
29
|
+
opacity: 1;
|
|
30
|
+
margin-left: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* No prefers-reduced-motion support — users with vestibular
|
|
34
|
+
disorders experience the full 1.5s sweep animation */
|
|
35
|
+
```
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
audit_animations.py - Audit CSS/SCSS for animation anti-patterns.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python audit_animations.py <file_or_directory>
|
|
7
|
+
|
|
8
|
+
Scans .css and .scss files for animation anti-patterns documented in
|
|
9
|
+
"Animation at Work" by Rachel Nabors.
|
|
10
|
+
|
|
11
|
+
Checks performed:
|
|
12
|
+
1. Animating layout-triggering properties (use transform instead)
|
|
13
|
+
2. transition: all (too broad)
|
|
14
|
+
3. Transitions > 500ms or animations > 1000ms (too slow)
|
|
15
|
+
4. Durations < 100ms (too fast to perceive)
|
|
16
|
+
5. linear easing on UI transitions (use ease-out or cubic-bezier)
|
|
17
|
+
6. Missing prefers-reduced-motion in files that animate
|
|
18
|
+
7. infinite animations without a pause mechanism
|
|
19
|
+
|
|
20
|
+
Outputs: file, line, offending CSS, and recommended fix.
|
|
21
|
+
Summary at end: total issues by category.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import pathlib
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
from collections import defaultdict
|
|
29
|
+
from typing import NamedTuple
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Issue(NamedTuple):
|
|
33
|
+
file: str
|
|
34
|
+
line: int
|
|
35
|
+
category: str
|
|
36
|
+
snippet: str
|
|
37
|
+
advice: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Properties that trigger layout recalculation — animating them is expensive.
|
|
41
|
+
LAYOUT_TRIGGERING = [
|
|
42
|
+
"width", "height", "top", "left", "right", "bottom",
|
|
43
|
+
"margin", "margin-top", "margin-right", "margin-bottom", "margin-left",
|
|
44
|
+
"padding", "padding-top", "padding-right", "padding-bottom", "padding-left",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
CATEGORIES = {
|
|
48
|
+
"layout_property": "Layout-triggering property animated",
|
|
49
|
+
"transition_all": "transition: all used",
|
|
50
|
+
"too_slow": "Duration too long (sluggish UI)",
|
|
51
|
+
"too_fast": "Duration too short (imperceptible)",
|
|
52
|
+
"linear_easing": "Linear easing on UI transition",
|
|
53
|
+
"no_reduced_motion": "Missing prefers-reduced-motion",
|
|
54
|
+
"infinite_no_pause": "Infinite animation without pause mechanism",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_duration_ms(value: str) -> float | None:
|
|
59
|
+
"""Convert a CSS duration string (e.g. '0.3s', '300ms') to milliseconds."""
|
|
60
|
+
value = value.strip()
|
|
61
|
+
if value.endswith("ms"):
|
|
62
|
+
try:
|
|
63
|
+
return float(value[:-2])
|
|
64
|
+
except ValueError:
|
|
65
|
+
return None
|
|
66
|
+
if value.endswith("s"):
|
|
67
|
+
try:
|
|
68
|
+
return float(value[:-1]) * 1000
|
|
69
|
+
except ValueError:
|
|
70
|
+
return None
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def find_css_files(path: pathlib.Path) -> list[pathlib.Path]:
|
|
75
|
+
if path.is_file():
|
|
76
|
+
if path.suffix in {".css", ".scss"}:
|
|
77
|
+
return [path]
|
|
78
|
+
print(f"WARNING: {path} is not a .css or .scss file — skipping.")
|
|
79
|
+
return []
|
|
80
|
+
return sorted(path.rglob("*.css")) + sorted(path.rglob("*.scss"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def audit_file(filepath: pathlib.Path) -> list[Issue]:
|
|
84
|
+
issues: list[Issue] = []
|
|
85
|
+
try:
|
|
86
|
+
lines = filepath.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
87
|
+
except OSError as exc:
|
|
88
|
+
print(f"ERROR reading {filepath}: {exc}")
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
file_str = filepath.as_posix()
|
|
92
|
+
full_text = "\n".join(lines)
|
|
93
|
+
|
|
94
|
+
has_animation = bool(
|
|
95
|
+
re.search(r"\b(transition|animation)\s*:", full_text)
|
|
96
|
+
)
|
|
97
|
+
has_reduced_motion = "prefers-reduced-motion" in full_text
|
|
98
|
+
|
|
99
|
+
for lineno, raw_line in enumerate(lines, start=1):
|
|
100
|
+
line = raw_line.strip()
|
|
101
|
+
if not line or line.startswith("//") or line.startswith("/*"):
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# 1. Animating layout-triggering properties
|
|
105
|
+
transition_match = re.match(r"transition\s*:\s*(.+)", line, re.IGNORECASE)
|
|
106
|
+
if transition_match:
|
|
107
|
+
props_part = transition_match.group(1)
|
|
108
|
+
for prop in LAYOUT_TRIGGERING:
|
|
109
|
+
if re.search(r"\b" + re.escape(prop) + r"\b", props_part, re.IGNORECASE):
|
|
110
|
+
issues.append(Issue(
|
|
111
|
+
file=file_str,
|
|
112
|
+
line=lineno,
|
|
113
|
+
category="layout_property",
|
|
114
|
+
snippet=line[:120],
|
|
115
|
+
advice=(
|
|
116
|
+
f"Animating '{prop}' triggers layout recalculation on every frame. "
|
|
117
|
+
"Use 'transform: translate/scale' or 'opacity' instead — "
|
|
118
|
+
"these are compositor-only and do not cause reflow."
|
|
119
|
+
),
|
|
120
|
+
))
|
|
121
|
+
|
|
122
|
+
# 2. transition: all
|
|
123
|
+
if re.search(r"transition\s*:\s*all\b", line, re.IGNORECASE):
|
|
124
|
+
issues.append(Issue(
|
|
125
|
+
file=file_str,
|
|
126
|
+
line=lineno,
|
|
127
|
+
category="transition_all",
|
|
128
|
+
snippet=line[:120],
|
|
129
|
+
advice=(
|
|
130
|
+
"'transition: all' animates every animatable property, including "
|
|
131
|
+
"layout-triggering ones you may not intend. List specific properties: "
|
|
132
|
+
"e.g., 'transition: opacity 0.2s ease-out, transform 0.2s ease-out'."
|
|
133
|
+
),
|
|
134
|
+
))
|
|
135
|
+
|
|
136
|
+
# 3 & 4. Duration checks — transition and animation shorthand
|
|
137
|
+
duration_patterns = [
|
|
138
|
+
re.compile(r"transition\s*:[^;]+", re.IGNORECASE),
|
|
139
|
+
re.compile(r"animation\s*:[^;]+", re.IGNORECASE),
|
|
140
|
+
re.compile(r"transition-duration\s*:\s*([^;]+)", re.IGNORECASE),
|
|
141
|
+
re.compile(r"animation-duration\s*:\s*([^;]+)", re.IGNORECASE),
|
|
142
|
+
]
|
|
143
|
+
for pat in duration_patterns:
|
|
144
|
+
m = pat.search(line)
|
|
145
|
+
if not m:
|
|
146
|
+
continue
|
|
147
|
+
value_str = m.group(0)
|
|
148
|
+
is_animation = "animation" in value_str.lower() and "transition" not in value_str.lower()
|
|
149
|
+
for dur_match in re.finditer(r"\d+(?:\.\d+)?(?:ms|s)\b", value_str):
|
|
150
|
+
dur_ms = parse_duration_ms(dur_match.group(0))
|
|
151
|
+
if dur_ms is None:
|
|
152
|
+
continue
|
|
153
|
+
slow_limit = 1000 if is_animation else 500
|
|
154
|
+
if dur_ms > slow_limit:
|
|
155
|
+
issues.append(Issue(
|
|
156
|
+
file=file_str,
|
|
157
|
+
line=lineno,
|
|
158
|
+
category="too_slow",
|
|
159
|
+
snippet=line[:120],
|
|
160
|
+
advice=(
|
|
161
|
+
f"Duration {dur_ms:.0f}ms feels sluggish for UI feedback. "
|
|
162
|
+
f"Keep UI transitions under {slow_limit}ms. "
|
|
163
|
+
"Aim for 200-300ms for most interactions."
|
|
164
|
+
),
|
|
165
|
+
))
|
|
166
|
+
elif dur_ms < 100 and dur_ms > 0:
|
|
167
|
+
issues.append(Issue(
|
|
168
|
+
file=file_str,
|
|
169
|
+
line=lineno,
|
|
170
|
+
category="too_fast",
|
|
171
|
+
snippet=line[:120],
|
|
172
|
+
advice=(
|
|
173
|
+
f"Duration {dur_ms:.0f}ms is below the human perception threshold (~100ms). "
|
|
174
|
+
"The animation will not be noticed. Use 100-200ms for snappy transitions."
|
|
175
|
+
),
|
|
176
|
+
))
|
|
177
|
+
|
|
178
|
+
# 5. Linear easing on transitions
|
|
179
|
+
if re.search(r"transition\s*:", line, re.IGNORECASE):
|
|
180
|
+
if re.search(r"\blinear\b", line, re.IGNORECASE):
|
|
181
|
+
issues.append(Issue(
|
|
182
|
+
file=file_str,
|
|
183
|
+
line=lineno,
|
|
184
|
+
category="linear_easing",
|
|
185
|
+
snippet=line[:120],
|
|
186
|
+
advice=(
|
|
187
|
+
"Linear easing feels mechanical and unnatural for UI elements. "
|
|
188
|
+
"Use 'ease-out' for elements entering the screen, 'ease-in' for "
|
|
189
|
+
"elements leaving, or a custom cubic-bezier for branded motion."
|
|
190
|
+
),
|
|
191
|
+
))
|
|
192
|
+
|
|
193
|
+
# 7. Infinite animation without pause mechanism
|
|
194
|
+
if re.search(r"animation-iteration-count\s*:\s*infinite\b", line, re.IGNORECASE):
|
|
195
|
+
# Check nearby lines (±10) for a paused state or play-state control
|
|
196
|
+
start = max(0, lineno - 10)
|
|
197
|
+
end = min(len(lines), lineno + 10)
|
|
198
|
+
context_block = "\n".join(lines[start:end])
|
|
199
|
+
if "animation-play-state" not in context_block and "paused" not in context_block:
|
|
200
|
+
issues.append(Issue(
|
|
201
|
+
file=file_str,
|
|
202
|
+
line=lineno,
|
|
203
|
+
category="infinite_no_pause",
|
|
204
|
+
snippet=line[:120],
|
|
205
|
+
advice=(
|
|
206
|
+
"Infinite animations can be distracting and drain battery on mobile. "
|
|
207
|
+
"Add 'animation-play-state: paused' controlled via :hover, :focus, "
|
|
208
|
+
"or a JS toggle so users can pause it."
|
|
209
|
+
),
|
|
210
|
+
))
|
|
211
|
+
|
|
212
|
+
# 6. Missing prefers-reduced-motion
|
|
213
|
+
if has_animation and not has_reduced_motion:
|
|
214
|
+
issues.append(Issue(
|
|
215
|
+
file=file_str,
|
|
216
|
+
line=0,
|
|
217
|
+
category="no_reduced_motion",
|
|
218
|
+
snippet="(entire file)",
|
|
219
|
+
advice=(
|
|
220
|
+
"This file contains animations but no '@media (prefers-reduced-motion: reduce)' "
|
|
221
|
+
"block. Add one to disable or reduce motion for users who request it — "
|
|
222
|
+
"required for WCAG 2.1 AA compliance."
|
|
223
|
+
),
|
|
224
|
+
))
|
|
225
|
+
|
|
226
|
+
return issues
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def print_issues(issues: list[Issue]) -> None:
|
|
230
|
+
for issue in issues:
|
|
231
|
+
loc = f"{issue.file}:{issue.line}" if issue.line else issue.file
|
|
232
|
+
category_label = CATEGORIES.get(issue.category, issue.category)
|
|
233
|
+
print(f"\n[{category_label}]")
|
|
234
|
+
print(f" Location : {loc}")
|
|
235
|
+
print(f" CSS : {issue.snippet}")
|
|
236
|
+
print(f" Fix : {issue.advice}")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def print_summary(issues: list[Issue]) -> None:
|
|
240
|
+
counts: dict[str, int] = defaultdict(int)
|
|
241
|
+
for issue in issues:
|
|
242
|
+
counts[issue.category] += 1
|
|
243
|
+
print("\n" + "=" * 60)
|
|
244
|
+
print("SUMMARY")
|
|
245
|
+
print("=" * 60)
|
|
246
|
+
if not counts:
|
|
247
|
+
print("No issues found.")
|
|
248
|
+
return
|
|
249
|
+
for cat, label in CATEGORIES.items():
|
|
250
|
+
count = counts.get(cat, 0)
|
|
251
|
+
if count:
|
|
252
|
+
print(f" {count:3d} {label}")
|
|
253
|
+
print(f" ---")
|
|
254
|
+
print(f" {sum(counts.values()):3d} Total issues")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def main() -> None:
|
|
258
|
+
parser = argparse.ArgumentParser(
|
|
259
|
+
description="Audit CSS/SCSS files for animation anti-patterns."
|
|
260
|
+
)
|
|
261
|
+
parser.add_argument(
|
|
262
|
+
"path",
|
|
263
|
+
help="A .css/.scss file or directory to scan recursively.",
|
|
264
|
+
)
|
|
265
|
+
args = parser.parse_args()
|
|
266
|
+
|
|
267
|
+
target = pathlib.Path(args.path)
|
|
268
|
+
if not target.exists():
|
|
269
|
+
print(f"ERROR: Path not found: {target}")
|
|
270
|
+
sys.exit(1)
|
|
271
|
+
|
|
272
|
+
files = find_css_files(target)
|
|
273
|
+
if not files:
|
|
274
|
+
print("No .css or .scss files found.")
|
|
275
|
+
sys.exit(0)
|
|
276
|
+
|
|
277
|
+
print(f"Scanning {len(files)} file(s) ...\n")
|
|
278
|
+
|
|
279
|
+
all_issues: list[Issue] = []
|
|
280
|
+
for f in files:
|
|
281
|
+
file_issues = audit_file(f)
|
|
282
|
+
all_issues.extend(file_issues)
|
|
283
|
+
|
|
284
|
+
if all_issues:
|
|
285
|
+
print_issues(all_issues)
|
|
286
|
+
else:
|
|
287
|
+
print("No animation anti-patterns detected.")
|
|
288
|
+
|
|
289
|
+
print_summary(all_issues)
|
|
290
|
+
|
|
291
|
+
sys.exit(1 if all_issues else 0)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
if __name__ == "__main__":
|
|
295
|
+
main()
|