mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-03 15:43:31 +08:00
feat: add web frontend rules and design quality hook
This commit is contained in:
@@ -37,7 +37,7 @@
|
||||
{
|
||||
"command": "node .cursor/hooks/after-file-edit.js",
|
||||
"event": "afterFileEdit",
|
||||
"description": "Auto-format, TypeScript check, console.log warning"
|
||||
"description": "Auto-format, TypeScript check, console.log warning, and frontend design-quality reminder"
|
||||
}
|
||||
],
|
||||
"beforeMCPExecution": [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
const { readStdin, runExistingHook, transformToClaude } = require('./adapter');
|
||||
const { hookEnabled, readStdin, runExistingHook, transformToClaude } = require('./adapter');
|
||||
readStdin().then(raw => {
|
||||
try {
|
||||
const input = JSON.parse(raw);
|
||||
@@ -11,6 +11,9 @@ readStdin().then(raw => {
|
||||
// Accumulate edited paths for batch format+typecheck at stop time
|
||||
runExistingHook('post-edit-accumulator.js', claudeStr);
|
||||
runExistingHook('post-edit-console-warn.js', claudeStr);
|
||||
if (hookEnabled('post:edit:design-quality-check', ['standard', 'strict'])) {
|
||||
runExistingHook('design-quality-check.js', claudeStr);
|
||||
}
|
||||
} catch {}
|
||||
process.stdout.write(raw);
|
||||
}).catch(() => process.exit(0));
|
||||
|
||||
@@ -115,3 +115,5 @@ Keep this file detailed for only the current sprint, blockers, and next actions.
|
||||
- 2026-04-02: Closed fresh auto-generated bundle PRs `#1182` and `#1183` under the existing policy. Useful ideas from generator output must be ported manually into canonical repo surfaces instead of merging `.claude`/bundle PRs wholesale.
|
||||
- 2026-04-02: Ported the safe one-file macOS observer fix from `#1164` directly into `main` as a POSIX `mkdir` fallback for `continuous-learning-v2` lazy-start locking, then closed the PR as superseded by direct port.
|
||||
- 2026-04-02: Ported the safe core of `#1153` directly into `main`: markdownlint cleanup for orchestration/docs surfaces plus the Windows `USERPROFILE` and path-normalization fixes in `install-apply` / `repair` tests. Local validation after installing repo deps: `node tests/scripts/install-apply.test.js`, `node tests/scripts/repair.test.js`, and targeted `yarn markdownlint` all passed.
|
||||
- 2026-04-02: Direct-ported the safe web/frontend rules lane from `#1122` into `rules/web/`, but adapted `rules/web/hooks.md` to prefer project-local tooling and avoid remote one-off package execution examples.
|
||||
- 2026-04-02: Adapted the design-quality reminder from `#1127` into the current ECC hook architecture with a local `scripts/hooks/design-quality-check.js`, Claude `hooks/hooks.json` wiring, Cursor `after-file-edit.js` wiring, and dedicated hook coverage in `tests/hooks/design-quality-check.test.js`.
|
||||
|
||||
@@ -35,6 +35,7 @@ User request → Claude picks a tool → PreToolUse hook runs → Tool executes
|
||||
| **PR logger** | `Bash` | Logs PR URL and review command after `gh pr create` |
|
||||
| **Build analysis** | `Bash` | Background analysis after build commands (async, non-blocking) |
|
||||
| **Quality gate** | `Edit\|Write\|MultiEdit` | Runs fast quality checks after edits |
|
||||
| **Design quality check** | `Edit\|Write\|MultiEdit` | Warns when frontend edits drift toward generic template-looking UI |
|
||||
| **Prettier format** | `Edit` | Auto-formats JS/TS files with Prettier after edits |
|
||||
| **TypeScript check** | `Edit` | Runs `tsc --noEmit` after editing `.ts`/`.tsx` files |
|
||||
| **console.log warning** | `Edit` | Warns about `console.log` statements in edited files |
|
||||
|
||||
@@ -226,6 +226,18 @@
|
||||
"description": "Run quality gate checks after file edits",
|
||||
"id": "post:quality-gate"
|
||||
},
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"post:edit:design-quality-check\" \"scripts/hooks/design-quality-check.js\" \"standard,strict\"",
|
||||
"timeout": 10
|
||||
}
|
||||
],
|
||||
"description": "Warn when frontend edits drift toward generic template-looking UI",
|
||||
"id": "post:edit:design-quality-check"
|
||||
},
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"hooks": [
|
||||
|
||||
@@ -17,6 +17,7 @@ rules/
|
||||
├── typescript/ # TypeScript/JavaScript specific
|
||||
├── python/ # Python specific
|
||||
├── golang/ # Go specific
|
||||
├── web/ # Web and frontend specific
|
||||
├── swift/ # Swift specific
|
||||
└── php/ # PHP specific
|
||||
```
|
||||
@@ -33,6 +34,7 @@ rules/
|
||||
./install.sh typescript
|
||||
./install.sh python
|
||||
./install.sh golang
|
||||
./install.sh web
|
||||
./install.sh swift
|
||||
./install.sh php
|
||||
|
||||
@@ -56,6 +58,7 @@ cp -r rules/common ~/.claude/rules/common
|
||||
cp -r rules/typescript ~/.claude/rules/typescript
|
||||
cp -r rules/python ~/.claude/rules/python
|
||||
cp -r rules/golang ~/.claude/rules/golang
|
||||
cp -r rules/web ~/.claude/rules/web
|
||||
cp -r rules/swift ~/.claude/rules/swift
|
||||
cp -r rules/php ~/.claude/rules/php
|
||||
|
||||
@@ -86,6 +89,8 @@ To add support for a new language (e.g., `rust/`):
|
||||
```
|
||||
4. Reference existing skills if available, or create new ones under `skills/`.
|
||||
|
||||
For non-language domains like `web/`, follow the same layered pattern when there is enough reusable domain-specific guidance to justify a standalone ruleset.
|
||||
|
||||
## Rule Priority
|
||||
|
||||
When language-specific rules and common rules conflict, **language-specific rules take precedence** (specific overrides general). This follows the standard layered configuration pattern (similar to CSS specificity or `.gitignore` precedence).
|
||||
|
||||
96
rules/web/coding-style.md
Normal file
96
rules/web/coding-style.md
Normal file
@@ -0,0 +1,96 @@
|
||||
> This file extends [common/coding-style.md](../common/coding-style.md) with web-specific frontend content.
|
||||
|
||||
# Web Coding Style
|
||||
|
||||
## File Organization
|
||||
|
||||
Organize by feature or surface area, not by file type:
|
||||
|
||||
```text
|
||||
src/
|
||||
├── components/
|
||||
│ ├── hero/
|
||||
│ │ ├── Hero.tsx
|
||||
│ │ ├── HeroVisual.tsx
|
||||
│ │ └── hero.css
|
||||
│ ├── scrolly-section/
|
||||
│ │ ├── ScrollySection.tsx
|
||||
│ │ ├── StickyVisual.tsx
|
||||
│ │ └── scrolly.css
|
||||
│ └── ui/
|
||||
│ ├── Button.tsx
|
||||
│ ├── SurfaceCard.tsx
|
||||
│ └── AnimatedText.tsx
|
||||
├── hooks/
|
||||
│ ├── useReducedMotion.ts
|
||||
│ └── useScrollProgress.ts
|
||||
├── lib/
|
||||
│ ├── animation.ts
|
||||
│ └── color.ts
|
||||
└── styles/
|
||||
├── tokens.css
|
||||
├── typography.css
|
||||
└── global.css
|
||||
```
|
||||
|
||||
## CSS Custom Properties
|
||||
|
||||
Define design tokens as variables. Do not hardcode palette, typography, or spacing repeatedly:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-surface: oklch(98% 0 0);
|
||||
--color-text: oklch(18% 0 0);
|
||||
--color-accent: oklch(68% 0.21 250);
|
||||
|
||||
--text-base: clamp(1rem, 0.92rem + 0.4vw, 1.125rem);
|
||||
--text-hero: clamp(3rem, 1rem + 7vw, 8rem);
|
||||
|
||||
--space-section: clamp(4rem, 3rem + 5vw, 10rem);
|
||||
|
||||
--duration-fast: 150ms;
|
||||
--duration-normal: 300ms;
|
||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
```
|
||||
|
||||
## Animation-Only Properties
|
||||
|
||||
Prefer compositor-friendly motion:
|
||||
- `transform`
|
||||
- `opacity`
|
||||
- `clip-path`
|
||||
- `filter` (sparingly)
|
||||
|
||||
Avoid animating layout-bound properties:
|
||||
- `width`
|
||||
- `height`
|
||||
- `top`
|
||||
- `left`
|
||||
- `margin`
|
||||
- `padding`
|
||||
- `border`
|
||||
- `font-size`
|
||||
|
||||
## Semantic HTML First
|
||||
|
||||
```html
|
||||
<header>
|
||||
<nav aria-label="Main navigation">...</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section aria-labelledby="hero-heading">
|
||||
<h1 id="hero-heading">...</h1>
|
||||
</section>
|
||||
</main>
|
||||
<footer>...</footer>
|
||||
```
|
||||
|
||||
Do not reach for generic wrapper `div` stacks when a semantic element exists.
|
||||
|
||||
## Naming
|
||||
|
||||
- Components: PascalCase (`ScrollySection`, `SurfaceCard`)
|
||||
- Hooks: `use` prefix (`useReducedMotion`)
|
||||
- CSS classes: kebab-case or utility classes
|
||||
- Animation timelines: camelCase with intent (`heroRevealTl`)
|
||||
63
rules/web/design-quality.md
Normal file
63
rules/web/design-quality.md
Normal file
@@ -0,0 +1,63 @@
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with web-specific design-quality guidance.
|
||||
|
||||
# Web Design Quality Standards
|
||||
|
||||
## Anti-Template Policy
|
||||
|
||||
Do not ship generic template-looking UI. Frontend output should look intentional, opinionated, and specific to the product.
|
||||
|
||||
### Banned Patterns
|
||||
|
||||
- Default card grids with uniform spacing and no hierarchy
|
||||
- Stock hero section with centered headline, gradient blob, and generic CTA
|
||||
- Unmodified library defaults passed off as finished design
|
||||
- Flat layouts with no layering, depth, or motion
|
||||
- Uniform radius, spacing, and shadows across every component
|
||||
- Safe gray-on-white styling with one decorative accent color
|
||||
- Dashboard-by-numbers layouts with sidebar + cards + charts and no point of view
|
||||
- Default font stacks used without a deliberate reason
|
||||
|
||||
### Required Qualities
|
||||
|
||||
Every meaningful frontend surface should demonstrate at least four of these:
|
||||
|
||||
1. Clear hierarchy through scale contrast
|
||||
2. Intentional rhythm in spacing, not uniform padding everywhere
|
||||
3. Depth or layering through overlap, shadows, surfaces, or motion
|
||||
4. Typography with character and a real pairing strategy
|
||||
5. Color used semantically, not just decoratively
|
||||
6. Hover, focus, and active states that feel designed
|
||||
7. Grid-breaking editorial or bento composition where appropriate
|
||||
8. Texture, grain, or atmosphere when it fits the visual direction
|
||||
9. Motion that clarifies flow instead of distracting from it
|
||||
10. Data visualization treated as part of the design system, not an afterthought
|
||||
|
||||
## Before Writing Frontend Code
|
||||
|
||||
1. Pick a specific style direction. Avoid vague defaults like "clean minimal".
|
||||
2. Define a palette intentionally.
|
||||
3. Choose typography deliberately.
|
||||
4. Gather at least a small set of real references.
|
||||
5. Use ECC design/frontend skills where relevant.
|
||||
|
||||
## Worthwhile Style Directions
|
||||
|
||||
- Editorial / magazine
|
||||
- Neo-brutalism
|
||||
- Glassmorphism with real depth
|
||||
- Dark luxury or light luxury with disciplined contrast
|
||||
- Bento layouts
|
||||
- Scrollytelling
|
||||
- 3D integration
|
||||
- Swiss / International
|
||||
- Retro-futurism
|
||||
|
||||
Do not default to dark mode automatically. Choose the visual direction the product actually wants.
|
||||
|
||||
## Component Checklist
|
||||
|
||||
- [ ] Does it avoid looking like a default Tailwind or shadcn template?
|
||||
- [ ] Does it have intentional hover/focus/active states?
|
||||
- [ ] Does it use hierarchy rather than uniform emphasis?
|
||||
- [ ] Would this look believable in a real product screenshot?
|
||||
- [ ] If it supports both themes, do both light and dark feel intentional?
|
||||
120
rules/web/hooks.md
Normal file
120
rules/web/hooks.md
Normal file
@@ -0,0 +1,120 @@
|
||||
> This file extends [common/hooks.md](../common/hooks.md) with web-specific hook recommendations.
|
||||
|
||||
# Web Hooks
|
||||
|
||||
## Recommended PostToolUse Hooks
|
||||
|
||||
Prefer project-local tooling. Do not wire hooks to remote one-off package execution.
|
||||
|
||||
### Format on Save
|
||||
|
||||
Use the project's existing formatter entrypoint after edits:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"command": "pnpm prettier --write \"$FILE_PATH\"",
|
||||
"description": "Format edited frontend files"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Equivalent local commands via `yarn prettier` or `npm exec prettier --` are fine when they use repo-owned dependencies.
|
||||
|
||||
### Lint Check
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"command": "pnpm eslint --fix \"$FILE_PATH\"",
|
||||
"description": "Run ESLint on edited frontend files"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Type Check
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"command": "pnpm tsc --noEmit --pretty false",
|
||||
"description": "Type-check after frontend edits"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Lint
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"command": "pnpm stylelint --fix \"$FILE_PATH\"",
|
||||
"description": "Lint edited stylesheets"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PreToolUse Hooks
|
||||
|
||||
### Guard File Size
|
||||
|
||||
Block oversized writes from tool input content, not from a file that may not exist yet:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Write",
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const c=i.tool_input?.content||'';const lines=c.split('\\n').length;if(lines>800){console.error('[Hook] BLOCKED: File exceeds 800 lines ('+lines+' lines)');console.error('[Hook] Split into smaller modules');process.exit(2)}console.log(d)})\"",
|
||||
"description": "Block writes that exceed 800 lines"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Stop Hooks
|
||||
|
||||
### Final Build Verification
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"command": "pnpm build",
|
||||
"description": "Verify the production build at session end"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Ordering
|
||||
|
||||
Recommended order:
|
||||
1. format
|
||||
2. lint
|
||||
3. type check
|
||||
4. build verification
|
||||
79
rules/web/patterns.md
Normal file
79
rules/web/patterns.md
Normal file
@@ -0,0 +1,79 @@
|
||||
> This file extends [common/patterns.md](../common/patterns.md) with web-specific patterns.
|
||||
|
||||
# Web Patterns
|
||||
|
||||
## Component Composition
|
||||
|
||||
### Compound Components
|
||||
|
||||
Use compound components when related UI shares state and interaction semantics:
|
||||
|
||||
```tsx
|
||||
<Tabs defaultValue="overview">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
||||
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="overview">...</Tabs.Content>
|
||||
<Tabs.Content value="settings">...</Tabs.Content>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
- Parent owns state
|
||||
- Children consume via context
|
||||
- Prefer this over prop drilling for complex widgets
|
||||
|
||||
### Render Props / Slots
|
||||
|
||||
- Use render props or slot patterns when behavior is shared but markup must vary
|
||||
- Keep keyboard handling, ARIA, and focus logic in the headless layer
|
||||
|
||||
### Container / Presentational Split
|
||||
|
||||
- Container components own data loading and side effects
|
||||
- Presentational components receive props and render UI
|
||||
- Presentational components should stay pure
|
||||
|
||||
## State Management
|
||||
|
||||
Treat these separately:
|
||||
|
||||
| Concern | Tooling |
|
||||
|---------|---------|
|
||||
| Server state | TanStack Query, SWR, tRPC |
|
||||
| Client state | Zustand, Jotai, signals |
|
||||
| URL state | search params, route segments |
|
||||
| Form state | React Hook Form or equivalent |
|
||||
|
||||
- Do not duplicate server state into client stores
|
||||
- Derive values instead of storing redundant computed state
|
||||
|
||||
## URL As State
|
||||
|
||||
Persist shareable state in the URL:
|
||||
- filters
|
||||
- sort order
|
||||
- pagination
|
||||
- active tab
|
||||
- search query
|
||||
|
||||
## Data Fetching
|
||||
|
||||
### Stale-While-Revalidate
|
||||
|
||||
- Return cached data immediately
|
||||
- Revalidate in the background
|
||||
- Prefer existing libraries instead of rolling this by hand
|
||||
|
||||
### Optimistic Updates
|
||||
|
||||
- Snapshot current state
|
||||
- Apply optimistic update
|
||||
- Roll back on failure
|
||||
- Emit visible error feedback when rolling back
|
||||
|
||||
### Parallel Loading
|
||||
|
||||
- Fetch independent data in parallel
|
||||
- Avoid parent-child request waterfalls
|
||||
- Prefetch likely next routes or states when justified
|
||||
64
rules/web/performance.md
Normal file
64
rules/web/performance.md
Normal file
@@ -0,0 +1,64 @@
|
||||
> This file extends [common/performance.md](../common/performance.md) with web-specific performance content.
|
||||
|
||||
# Web Performance Rules
|
||||
|
||||
## Core Web Vitals Targets
|
||||
|
||||
| Metric | Target |
|
||||
|--------|--------|
|
||||
| LCP | < 2.5s |
|
||||
| INP | < 200ms |
|
||||
| CLS | < 0.1 |
|
||||
| FCP | < 1.5s |
|
||||
| TBT | < 200ms |
|
||||
|
||||
## Bundle Budget
|
||||
|
||||
| Page Type | JS Budget (gzipped) | CSS Budget |
|
||||
|-----------|---------------------|------------|
|
||||
| Landing page | < 150kb | < 30kb |
|
||||
| App page | < 300kb | < 50kb |
|
||||
| Microsite | < 80kb | < 15kb |
|
||||
|
||||
## Loading Strategy
|
||||
|
||||
1. Inline critical above-the-fold CSS where justified
|
||||
2. Preload the hero image and primary font only
|
||||
3. Defer non-critical CSS or JS
|
||||
4. Dynamically import heavy libraries
|
||||
|
||||
```js
|
||||
const gsapModule = await import('gsap');
|
||||
const { ScrollTrigger } = await import('gsap/ScrollTrigger');
|
||||
```
|
||||
|
||||
## Image Optimization
|
||||
|
||||
- Explicit `width` and `height`
|
||||
- `loading="eager"` plus `fetchpriority="high"` for hero media only
|
||||
- `loading="lazy"` for below-the-fold assets
|
||||
- Prefer AVIF or WebP with fallbacks
|
||||
- Never ship source images far beyond rendered size
|
||||
|
||||
## Font Loading
|
||||
|
||||
- Max two font families unless there is a clear exception
|
||||
- `font-display: swap`
|
||||
- Subset where possible
|
||||
- Preload only the truly critical weight/style
|
||||
|
||||
## Animation Performance
|
||||
|
||||
- Animate compositor-friendly properties only
|
||||
- Use `will-change` narrowly and remove it when done
|
||||
- Prefer CSS for simple transitions
|
||||
- Use `requestAnimationFrame` or established animation libraries for JS motion
|
||||
- Avoid scroll handler churn; use IntersectionObserver or well-behaved libraries
|
||||
|
||||
## Performance Checklist
|
||||
|
||||
- [ ] All images have explicit dimensions
|
||||
- [ ] No accidental render-blocking resources
|
||||
- [ ] No layout shifts from dynamic content
|
||||
- [ ] Motion stays on compositor-friendly properties
|
||||
- [ ] Third-party scripts load async/defer and only when needed
|
||||
57
rules/web/security.md
Normal file
57
rules/web/security.md
Normal file
@@ -0,0 +1,57 @@
|
||||
> This file extends [common/security.md](../common/security.md) with web-specific security content.
|
||||
|
||||
# Web Security Rules
|
||||
|
||||
## Content Security Policy
|
||||
|
||||
Always configure a production CSP.
|
||||
|
||||
### Nonce-Based CSP
|
||||
|
||||
Use a per-request nonce for scripts instead of `'unsafe-inline'`.
|
||||
|
||||
```text
|
||||
Content-Security-Policy:
|
||||
default-src 'self';
|
||||
script-src 'self' 'nonce-{RANDOM}' https://cdn.jsdelivr.net;
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
||||
img-src 'self' data: https:;
|
||||
font-src 'self' https://fonts.gstatic.com;
|
||||
connect-src 'self' https://*.example.com;
|
||||
frame-src 'none';
|
||||
object-src 'none';
|
||||
base-uri 'self';
|
||||
```
|
||||
|
||||
Adjust origins to the project. Do not cargo-cult this block unchanged.
|
||||
|
||||
## XSS Prevention
|
||||
|
||||
- Never inject unsanitized HTML
|
||||
- Avoid `innerHTML` / `dangerouslySetInnerHTML` unless sanitized first
|
||||
- Escape dynamic template values
|
||||
- Sanitize user HTML with a vetted local sanitizer when absolutely necessary
|
||||
|
||||
## Third-Party Scripts
|
||||
|
||||
- Load asynchronously
|
||||
- Use SRI when serving from a CDN
|
||||
- Audit quarterly
|
||||
- Prefer self-hosting for critical dependencies when practical
|
||||
|
||||
## HTTPS and Headers
|
||||
|
||||
```text
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
Referrer-Policy: strict-origin-when-cross-origin
|
||||
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
||||
```
|
||||
|
||||
## Forms
|
||||
|
||||
- CSRF protection on state-changing forms
|
||||
- Rate limiting on submission endpoints
|
||||
- Validate client and server side
|
||||
- Prefer honeypots or light anti-abuse controls over heavy-handed CAPTCHA defaults
|
||||
55
rules/web/testing.md
Normal file
55
rules/web/testing.md
Normal file
@@ -0,0 +1,55 @@
|
||||
> This file extends [common/testing.md](../common/testing.md) with web-specific testing content.
|
||||
|
||||
# Web Testing Rules
|
||||
|
||||
## Priority Order
|
||||
|
||||
### 1. Visual Regression
|
||||
|
||||
- Screenshot key breakpoints: 320, 768, 1024, 1440
|
||||
- Test hero sections, scrollytelling sections, and meaningful states
|
||||
- Use Playwright screenshots for visual-heavy work
|
||||
- If both themes exist, test both
|
||||
|
||||
### 2. Accessibility
|
||||
|
||||
- Run automated accessibility checks
|
||||
- Test keyboard navigation
|
||||
- Verify reduced-motion behavior
|
||||
- Verify color contrast
|
||||
|
||||
### 3. Performance
|
||||
|
||||
- Run Lighthouse or equivalent against meaningful pages
|
||||
- Keep CWV targets from [performance.md](performance.md)
|
||||
|
||||
### 4. Cross-Browser
|
||||
|
||||
- Minimum: Chrome, Firefox, Safari
|
||||
- Test scrolling, motion, and fallback behavior
|
||||
|
||||
### 5. Responsive
|
||||
|
||||
- Test 320, 375, 768, 1024, 1440, 1920
|
||||
- Verify no overflow
|
||||
- Verify touch interactions
|
||||
|
||||
## E2E Shape
|
||||
|
||||
```ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('landing hero loads', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
- Avoid flaky timeout-based assertions
|
||||
- Prefer deterministic waits
|
||||
|
||||
## Unit Tests
|
||||
|
||||
- Test utilities, data transforms, and custom hooks
|
||||
- For highly visual components, visual regression often carries more signal than brittle markup assertions
|
||||
- Visual regression supplements coverage targets; it does not replace them
|
||||
131
scripts/hooks/design-quality-check.js
Normal file
131
scripts/hooks/design-quality-check.js
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* PostToolUse hook: lightweight frontend design-quality reminder.
|
||||
*
|
||||
* This stays self-contained inside ECC. It does not call remote models or
|
||||
* install packages. The goal is to catch obviously generic UI drift and keep
|
||||
* frontend edits aligned with ECC's stronger design standards.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const FRONTEND_EXTENSIONS = /\.(astro|css|html|jsx|scss|svelte|tsx|vue)$/i;
|
||||
const MAX_STDIN = 1024 * 1024;
|
||||
|
||||
const GENERIC_SIGNALS = [
|
||||
{ pattern: /\bget started\b/i, label: '"Get Started" CTA copy' },
|
||||
{ pattern: /\blearn more\b/i, label: '"Learn more" CTA copy' },
|
||||
{ pattern: /\bgrid-cols-(3|4)\b/, label: 'uniform multi-card grid' },
|
||||
{ pattern: /\bbg-gradient-to-[trbl]/, label: 'stock gradient utility usage' },
|
||||
{ pattern: /\btext-center\b/, label: 'centered default layout cues' },
|
||||
{ pattern: /\bfont-(sans|inter)\b/i, label: 'default font utility' },
|
||||
];
|
||||
|
||||
const CHECKLIST = [
|
||||
'visual hierarchy with real contrast',
|
||||
'intentional spacing rhythm',
|
||||
'depth, layering, or overlap',
|
||||
'purposeful hover and focus states',
|
||||
'color and typography that feel specific',
|
||||
];
|
||||
|
||||
function getFilePaths(input) {
|
||||
const toolInput = input?.tool_input || {};
|
||||
if (toolInput.file_path) {
|
||||
return [String(toolInput.file_path)];
|
||||
}
|
||||
|
||||
if (Array.isArray(toolInput.edits)) {
|
||||
return toolInput.edits
|
||||
.map(edit => String(edit?.file_path || ''))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function readContent(filePath) {
|
||||
try {
|
||||
return fs.readFileSync(path.resolve(filePath), 'utf8');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function detectSignals(content) {
|
||||
return GENERIC_SIGNALS.filter(signal => signal.pattern.test(content)).map(signal => signal.label);
|
||||
}
|
||||
|
||||
function buildWarning(frontendPaths, findings) {
|
||||
const pathLines = frontendPaths.map(fp => ` - ${fp}`).join('\n');
|
||||
const signalLines = findings.length > 0
|
||||
? findings.map(item => ` - ${item}`).join('\n')
|
||||
: ' - no obvious canned-template strings detected';
|
||||
|
||||
return [
|
||||
'[Hook] DESIGN CHECK: frontend file(s) modified:',
|
||||
pathLines,
|
||||
'[Hook] Review for generic/template drift. Frontend should have:',
|
||||
CHECKLIST.map(item => ` - ${item}`).join('\n'),
|
||||
'[Hook] Heuristic signals:',
|
||||
signalLines,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function run(inputOrRaw) {
|
||||
let input;
|
||||
let rawInput = inputOrRaw;
|
||||
|
||||
try {
|
||||
if (typeof inputOrRaw === 'string') {
|
||||
rawInput = inputOrRaw;
|
||||
input = inputOrRaw.trim() ? JSON.parse(inputOrRaw) : {};
|
||||
} else {
|
||||
input = inputOrRaw || {};
|
||||
rawInput = JSON.stringify(inputOrRaw ?? {});
|
||||
}
|
||||
} catch {
|
||||
return { exitCode: 0, stdout: typeof rawInput === 'string' ? rawInput : '' };
|
||||
}
|
||||
|
||||
const filePaths = getFilePaths(input);
|
||||
const frontendPaths = filePaths.filter(filePath => FRONTEND_EXTENSIONS.test(filePath));
|
||||
|
||||
if (frontendPaths.length === 0) {
|
||||
return { exitCode: 0, stdout: typeof rawInput === 'string' ? rawInput : '' };
|
||||
}
|
||||
|
||||
const findings = [];
|
||||
for (const filePath of frontendPaths) {
|
||||
const content = readContent(filePath);
|
||||
findings.push(...detectSignals(content));
|
||||
}
|
||||
|
||||
return {
|
||||
exitCode: 0,
|
||||
stdout: typeof rawInput === 'string' ? rawInput : '',
|
||||
stderr: buildWarning(frontendPaths, findings),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
|
||||
if (require.main === module) {
|
||||
let raw = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
const result = run(raw);
|
||||
if (result.stderr) process.stderr.write(`${result.stderr}\n`);
|
||||
process.stdout.write(typeof result.stdout === 'string' ? result.stdout : raw);
|
||||
process.exit(Number.isInteger(result.exitCode) ? result.exitCode : 0);
|
||||
});
|
||||
}
|
||||
82
tests/hooks/design-quality-check.test.js
Normal file
82
tests/hooks/design-quality-check.test.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Tests for scripts/hooks/design-quality-check.js
|
||||
*
|
||||
* Run with: node tests/hooks/design-quality-check.test.js
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const hook = require('../../scripts/hooks/design-quality-check');
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` Error: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
console.log('\nDesign Quality Hook Tests');
|
||||
console.log('=========================\n');
|
||||
|
||||
if (test('passes through non-frontend files silently', () => {
|
||||
const input = JSON.stringify({ tool_input: { file_path: '/tmp/file.py' } });
|
||||
const result = hook.run(input);
|
||||
assert.strictEqual(result.exitCode, 0);
|
||||
assert.strictEqual(result.stdout, input);
|
||||
assert.ok(!result.stderr);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('warns for frontend file path', () => {
|
||||
const tmpFile = path.join(os.tmpdir(), `design-quality-${Date.now()}.tsx`);
|
||||
fs.writeFileSync(tmpFile, 'export function Hero(){ return <div className="text-center">Get Started</div>; }\n');
|
||||
try {
|
||||
const input = JSON.stringify({ tool_input: { file_path: tmpFile } });
|
||||
const result = hook.run(input);
|
||||
assert.strictEqual(result.exitCode, 0);
|
||||
assert.strictEqual(result.stdout, input);
|
||||
assert.match(result.stderr, /DESIGN CHECK/);
|
||||
assert.match(result.stderr, /Get Started/);
|
||||
} finally {
|
||||
fs.unlinkSync(tmpFile);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('handles MultiEdit edits[] payloads', () => {
|
||||
const tmpFile = path.join(os.tmpdir(), `design-quality-${Date.now()}.css`);
|
||||
fs.writeFileSync(tmpFile, '.hero{background:linear-gradient(to right,#000,#333)}\n');
|
||||
try {
|
||||
const input = JSON.stringify({
|
||||
tool_input: {
|
||||
edits: [{ file_path: tmpFile }, { file_path: '/tmp/notes.md' }]
|
||||
}
|
||||
});
|
||||
const result = hook.run(input);
|
||||
assert.strictEqual(result.exitCode, 0);
|
||||
assert.strictEqual(result.stdout, input);
|
||||
assert.match(result.stderr, /frontend file\(s\) modified/);
|
||||
assert.match(result.stderr, /\.css/);
|
||||
} finally {
|
||||
fs.unlinkSync(tmpFile);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('returns original stdout on invalid JSON', () => {
|
||||
const input = '{not valid json';
|
||||
const result = hook.run(input);
|
||||
assert.strictEqual(result.exitCode, 0);
|
||||
assert.strictEqual(result.stdout, input);
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user