mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-10 17:59:43 +08:00
feat: add web frontend rules and design quality hook
This commit is contained in:
parent
a60d5fbc00
commit
31c9f7c33e
@ -37,7 +37,7 @@
|
|||||||
{
|
{
|
||||||
"command": "node .cursor/hooks/after-file-edit.js",
|
"command": "node .cursor/hooks/after-file-edit.js",
|
||||||
"event": "afterFileEdit",
|
"event": "afterFileEdit",
|
||||||
"description": "Auto-format, TypeScript check, console.log warning"
|
"description": "Auto-format, TypeScript check, console.log warning, and frontend design-quality reminder"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"beforeMCPExecution": [
|
"beforeMCPExecution": [
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
const { readStdin, runExistingHook, transformToClaude } = require('./adapter');
|
const { hookEnabled, readStdin, runExistingHook, transformToClaude } = require('./adapter');
|
||||||
readStdin().then(raw => {
|
readStdin().then(raw => {
|
||||||
try {
|
try {
|
||||||
const input = JSON.parse(raw);
|
const input = JSON.parse(raw);
|
||||||
@ -11,6 +11,9 @@ readStdin().then(raw => {
|
|||||||
// Accumulate edited paths for batch format+typecheck at stop time
|
// Accumulate edited paths for batch format+typecheck at stop time
|
||||||
runExistingHook('post-edit-accumulator.js', claudeStr);
|
runExistingHook('post-edit-accumulator.js', claudeStr);
|
||||||
runExistingHook('post-edit-console-warn.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 {}
|
} catch {}
|
||||||
process.stdout.write(raw);
|
process.stdout.write(raw);
|
||||||
}).catch(() => process.exit(0));
|
}).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: 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 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: 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` |
|
| **PR logger** | `Bash` | Logs PR URL and review command after `gh pr create` |
|
||||||
| **Build analysis** | `Bash` | Background analysis after build commands (async, non-blocking) |
|
| **Build analysis** | `Bash` | Background analysis after build commands (async, non-blocking) |
|
||||||
| **Quality gate** | `Edit\|Write\|MultiEdit` | Runs fast quality checks after edits |
|
| **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 |
|
| **Prettier format** | `Edit` | Auto-formats JS/TS files with Prettier after edits |
|
||||||
| **TypeScript check** | `Edit` | Runs `tsc --noEmit` after editing `.ts`/`.tsx` files |
|
| **TypeScript check** | `Edit` | Runs `tsc --noEmit` after editing `.ts`/`.tsx` files |
|
||||||
| **console.log warning** | `Edit` | Warns about `console.log` statements in edited 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",
|
"description": "Run quality gate checks after file edits",
|
||||||
"id": "post:quality-gate"
|
"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",
|
"matcher": "Edit|Write|MultiEdit",
|
||||||
"hooks": [
|
"hooks": [
|
||||||
|
|||||||
@ -17,6 +17,7 @@ rules/
|
|||||||
├── typescript/ # TypeScript/JavaScript specific
|
├── typescript/ # TypeScript/JavaScript specific
|
||||||
├── python/ # Python specific
|
├── python/ # Python specific
|
||||||
├── golang/ # Go specific
|
├── golang/ # Go specific
|
||||||
|
├── web/ # Web and frontend specific
|
||||||
├── swift/ # Swift specific
|
├── swift/ # Swift specific
|
||||||
└── php/ # PHP specific
|
└── php/ # PHP specific
|
||||||
```
|
```
|
||||||
@ -33,6 +34,7 @@ rules/
|
|||||||
./install.sh typescript
|
./install.sh typescript
|
||||||
./install.sh python
|
./install.sh python
|
||||||
./install.sh golang
|
./install.sh golang
|
||||||
|
./install.sh web
|
||||||
./install.sh swift
|
./install.sh swift
|
||||||
./install.sh php
|
./install.sh php
|
||||||
|
|
||||||
@ -56,6 +58,7 @@ cp -r rules/common ~/.claude/rules/common
|
|||||||
cp -r rules/typescript ~/.claude/rules/typescript
|
cp -r rules/typescript ~/.claude/rules/typescript
|
||||||
cp -r rules/python ~/.claude/rules/python
|
cp -r rules/python ~/.claude/rules/python
|
||||||
cp -r rules/golang ~/.claude/rules/golang
|
cp -r rules/golang ~/.claude/rules/golang
|
||||||
|
cp -r rules/web ~/.claude/rules/web
|
||||||
cp -r rules/swift ~/.claude/rules/swift
|
cp -r rules/swift ~/.claude/rules/swift
|
||||||
cp -r rules/php ~/.claude/rules/php
|
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/`.
|
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
|
## 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).
|
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);
|
||||||
Loading…
x
Reference in New Issue
Block a user