web-app-ux-auditor-skill 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
1
+ # Web UX Audit Reference
2
+
3
+ Use this reference when auditing or improving web apps across React, Next.js, Vue, Nuxt, Svelte, SvelteKit, Angular, Solid, Remix, Astro, or plain HTML/CSS/JavaScript.
4
+
5
+ ## Current Research Anchors
6
+
7
+ Prefer current official docs when the user's request depends on latest browser, accessibility, or framework behavior.
8
+
9
+ - W3C WCAG 2.2: use the most current stable WCAG recommendation for accessibility review. Watch especially focus not obscured, target size, dragging alternatives, consistent help, redundant entry, accessible authentication, text alternatives, contrast, keyboard, labels, and status messages.
10
+ - WAI-ARIA Authoring Practices Guide: use for custom widgets such as dialogs, menus, tabs, comboboxes, listboxes, trees, grids, accordions, and toolbars. Native HTML remains the first choice.
11
+ - W3C WAI accessibility principles: all mouse functionality must be available by keyboard, focus must not trap unexpectedly, and navigation must support different user strategies.
12
+ - web.dev/Core Web Vitals: evaluate LCP, INP, and CLS using field data when available and lab data when field data is unavailable.
13
+ - MDN accessibility and forms: use visible labels, semantic elements, correct control names, frame titles, and predictable focus behavior.
14
+ - web.dev forms: use `autocomplete`, standard controls, password-manager-friendly auth, one-time-code support, and autofill-friendly field names.
15
+ - GOV.UK Design System: use field-specific errors, error summaries, clear labels, and service-design-grade form patterns when applicable.
16
+ - Baymard: for ecommerce/checkout, reduce unnecessary fields, avoid forced account creation, preserve data, support recovery, and avoid mid-flow distractions.
17
+ - Public agent-skill patterns such as UI/UX Pro Max and Taste Skill show a useful structure: concise `SKILL.md`, deeper reference files, search/scanner scripts, anti-generic design constraints, and explicit quality gates. Use the structure, not copied prose or persona imitation.
18
+
19
+ ## Cross-Agent Compatibility
20
+
21
+ This skill is portable when it stays inside the open skill shape:
22
+
23
+ - `SKILL.md` with YAML frontmatter and Markdown instructions.
24
+ - Optional `references/`, `scripts/`, and `assets/`.
25
+ - Optional `agents/openai.yaml` for Codex UI metadata; Claude Code ignores it.
26
+ - Install locations:
27
+ - Codex: `~/.codex/skills/web-app-ux-auditor/`
28
+ - Claude Code personal: `~/.claude/skills/web-app-ux-auditor/`
29
+ - Project-local Claude Code: `<repo>/.claude/skills/web-app-ux-auditor/`
30
+
31
+ Avoid relying on platform-specific frontmatter unless the skill is intentionally platform-specific. Put platform-specific execution notes in the body.
32
+
33
+ ## UX Engineer Operating Model
34
+
35
+ Use this order before proposing changes:
36
+
37
+ 1. **Design read**: category, audience, job, usage environment, trust level, density, maturity, visual register, constraints.
38
+ 2. **User journey**: first value, primary task, recovery, re-entry, expansion, cancellation/delete.
39
+ 3. **Evidence capture**: code scan, route map, screenshots, browser walkthrough, keyboard walkthrough, performance data, accessibility signals.
40
+ 4. **Friction diagnosis**: confusion, too many choices, too many fields, hidden state, slow feedback, inaccessible control, broken recovery, trust gap.
41
+ 5. **Design response**: remove, reorder, relabel, disclose progressively, add feedback, make state visible, improve defaults, strengthen affordance.
42
+ 6. **Implementation response**: smallest code changes that improve completion and preserve architecture.
43
+ 7. **Verification**: prove the new flow works with at least one realistic path and one failure path.
44
+
45
+ Do not invent a celebrity-designer persona. Adopt the discipline: evidence, taste, consistency, accessibility, and verification.
46
+
47
+ ## Scorecard
48
+
49
+ Score each area from 0 to 5. A production app should have no area below 3 and critical flows should average 4+.
50
+
51
+ | Area | 0-1 | 3 | 5 |
52
+ | --- | --- | --- | --- |
53
+ | Task success | User cannot finish or loses data | Main path works with friction | Main, edge, and recovery paths are clear |
54
+ | Orientation | User cannot tell where they are | Headings/nav mostly work | Location, state, and next action are always obvious |
55
+ | Information architecture | Routes mirror org chart or implementation | Usable but uneven depth | Routes match user jobs and mental models |
56
+ | Forms | Placeholder labels, late errors, repeated entry | Basic labels/errors | Autofill, inline recovery, persistence, clear summaries |
57
+ | Accessibility | Mouse-only or unlabeled controls | Basic semantics | Keyboard, screen reader, contrast, focus, motion all verified |
58
+ | Responsive behavior | Breaks on mobile/zoom | Common breakpoints pass | Mobile, tablet, desktop, zoom, RTL/long text considered |
59
+ | Performance | Slow, jumpy, blocked interactions | Acceptable lab scores | Field-informed LCP/INP/CLS and perceived speed optimized |
60
+ | System states | Only happy path designed | Loading/error exist | Empty, loading, error, offline, partial, disabled, success are complete |
61
+ | Design consistency | Random styles and components | Repeated components | Tokens, spacing, radius, type, color, and motion are coherent |
62
+ | Trust and retention | Dark patterns or unclear consequences | Honest but minimal | Clear value, privacy, pricing, cancellation, and useful return loops |
63
+
64
+ ## Design Consistency Gates
65
+
66
+ Before calling a redesign good, check:
67
+
68
+ - **Typography**: one scale, readable line lengths, clear hierarchy, no clipped text, no tiny disabled-looking body copy.
69
+ - **Spacing**: a repeated rhythm with intentional density; no random gaps or card padding.
70
+ - **Color**: one semantic palette with accessible contrast; color is not the only signal.
71
+ - **Shape**: one radius system; buttons, inputs, cards, dialogs, and tags follow a rule.
72
+ - **Elevation**: shadows, borders, surfaces, and overlays communicate hierarchy rather than decoration.
73
+ - **Iconography**: one icon family and consistent stroke/fill logic; ambiguous icons have labels.
74
+ - **Motion**: purposeful transitions with reduced-motion fallback; no animation that delays task completion.
75
+ - **Copy**: labels are specific, buttons say what happens, errors tell users how to fix the problem.
76
+ - **Data display**: units, dates, freshness, comparison baseline, empty values, and filters are visible.
77
+ - **Aesthetic fit**: style follows audience and product job, not generic AI gradients, repeated cards, or trend imitation.
78
+
79
+ ## Discovery Pass
80
+
81
+ Inspect before judging. Search for:
82
+
83
+ ```bash
84
+ rg -n "BrowserRouter|Routes|Route|createBrowserRouter|Link|NavLink|useNavigate|next/link|app/|pages/|router|redirect|loader|action|layout" .
85
+ rg -n "form|Formik|React Hook Form|useForm|zod|yup|valibot|schema|input|select|textarea|label|autocomplete|aria-|role=|tabIndex|onKeyDown|dialog|modal|popover|toast" .
86
+ rg -n "button|a href|div.*onClick|span.*onClick|preventDefault|stopPropagation|focus\\(|autoFocus|inert|aria-hidden|VisuallyHidden|sr-only|Skip" .
87
+ rg -n "loading|skeleton|spinner|empty|error|offline|retry|toast|alert|notification|permission|onboarding|signup|sign-up|login|auth|checkout|subscribe|paywall" .
88
+ rg -n "analytics|track|posthog|segment|amplitude|gtag|dataLayer|plausible|sentry|datadog|web-vitals|reportWebVitals" .
89
+ rg -n "theme|tokens|tailwind|styles|css|scss|container|breakpoint|media query|prefers-reduced-motion|prefers-color-scheme" .
90
+ ```
91
+
92
+ Run the bundled scanner when available:
93
+
94
+ ```bash
95
+ python scripts/web_ux_static_scan.py .
96
+ ```
97
+
98
+ Then produce:
99
+
100
+ - App type, audience, and primary user jobs.
101
+ - Route inventory, top-level navigation, protected routes, and nested layouts.
102
+ - First-visit flow from landing/open to first value.
103
+ - Primary task flow with click count, waits, account gates, permissions, payment gates, and error paths.
104
+ - Return flow: bookmark, reload, browser back/forward, deep link, email link, notification, expired session.
105
+
106
+ ## Audit Checklist
107
+
108
+ ### Information Architecture and Navigation
109
+
110
+ - Make top-level destinations match user jobs, not internal org structure.
111
+ - Use persistent global navigation for broad apps; use local navigation/tabs for sibling content within a route.
112
+ - Keep current location visible with route-aware active states, headings, breadcrumbs where depth requires them, and page titles.
113
+ - Browser back/forward, reload, deep links, and copied URLs must preserve meaningful state or recover gracefully.
114
+ - Search should be available when navigation cannot reasonably expose all content; filters and sorting should be understandable and persistent when useful.
115
+ - Avoid nesting dashboards, settings, and admin areas so deeply that users cannot predict where to go next.
116
+ - Provide obvious exits from modals, setup, full-screen flows, checkout, and auth.
117
+
118
+ ### First Visit, Onboarding, and Activation
119
+
120
+ - Let users understand value before account creation, payment, permissions, or integrations when possible.
121
+ - Keep onboarding tied to the first meaningful task; replace generic tours with progressive guidance in context.
122
+ - Let users skip nonessential setup and return later.
123
+ - Preserve progress across auth, SSO, email verification, payment, failed validation, reload, and expired sessions.
124
+ - Use sample data or templates when an empty app would otherwise feel broken.
125
+ - Ask for browser permissions only when the user initiates a feature that needs them.
126
+
127
+ ### Dashboards and Data-Dense Apps
128
+
129
+ - Start with the user decision or action the dashboard supports, not just available metrics.
130
+ - Put high-priority status, exceptions, and next actions above secondary charts.
131
+ - Make filters, date ranges, comparisons, units, and data freshness visible.
132
+ - Empty, loading, stale, partial, and error states must be explicit.
133
+ - Avoid decorative chart density. Prefer tables for exact comparison and charts for trends, distribution, or anomaly detection.
134
+ - Support saved views, column controls, bulk actions, undo, and export only when the workflow needs them.
135
+
136
+ ### Forms, Auth, Checkout, and Complex Tasks
137
+
138
+ - Use native form controls and visible labels. Do not rely on placeholder-only labels.
139
+ - Use correct `type`, `inputmode`, `autocomplete`, `name`, `id`, `for`, validation attributes, and password-manager-friendly markup.
140
+ - Reduce fields before reducing steps. Hide optional or rare fields behind disclosure, and infer values where reliable.
141
+ - Use inline validation for fixable field errors and an error summary for long forms or submit failures.
142
+ - Error messages should name the field/question and tell the user how to fix it.
143
+ - Preserve entered data after errors, reloads, auth redirects, and network failures.
144
+ - Avoid forced account creation during checkout or high-intent conversion unless legally/product-critical.
145
+ - Make cancellation, refund, subscription, plan limits, and billing consequences clear before commitment.
146
+
147
+ ### Accessibility and Keyboard UX
148
+
149
+ - Validate semantic structure: one useful page title, logical headings, landmarks, lists, tables, buttons, links, and form labels.
150
+ - All interactive functionality must work by keyboard. Tab order must preserve meaning and operation.
151
+ - Focus indicators must be visible and not obscured by sticky headers, cookie banners, drawers, or overlays.
152
+ - Modals/dialogs must move focus inside on open, trap focus while modal, make background inert, close predictably, and return focus to the trigger.
153
+ - Dynamic updates such as validation errors, search result counts, save status, loading completion, and destructive confirmations need appropriate announcements.
154
+ - Do not put `aria-hidden` on focusable content or create nested interactive controls.
155
+ - Use ARIA only when native HTML cannot express the control; match APG keyboard patterns for custom widgets.
156
+ - Test with keyboard, browser zoom, screen reader spot-checks, reduced motion, high contrast/forced colors, and at least one automated accessibility checker.
157
+
158
+ ### Responsive Layout and Cross-Browser Behavior
159
+
160
+ - Audit mobile, tablet, desktop, narrow sidebars, zoomed text, and browser font-size changes.
161
+ - Use responsive constraints, container queries/media queries, and stable dimensions so content does not overlap or cause layout jumps.
162
+ - Make hit targets large enough and spaced enough for touch, pointer, and coarse input.
163
+ - Avoid hover-only affordances. Anything revealed on hover must also work by keyboard and touch.
164
+ - Keep sticky headers, sidebars, toasts, and banners from covering focused controls, errors, or primary actions.
165
+ - Support long words, translated strings, RTL when localized, and user zoom up to common accessibility levels.
166
+
167
+ ### Feedback, Loading, Empty, Error, and Offline States
168
+
169
+ - Show immediate feedback for clicks, saves, uploads, deletes, payments, and async actions.
170
+ - Use optimistic updates only when rollback and error recovery are clear.
171
+ - Skeletons should reserve final layout space; spinners need context if the wait matters.
172
+ - Empty states should explain why content is missing and offer the next useful action.
173
+ - Error states should say what happened, what the user can do, and whether data is safe.
174
+ - Offline or degraded network states should preserve user input and support retry/resume where feasible.
175
+ - Toasts should not be the only place important information appears.
176
+
177
+ ### Performance and Core Web Vitals
178
+
179
+ - Check LCP, INP, and CLS. Prefer field data from Chrome UX Report, RUM, Search Console, or analytics; use Lighthouse/PageSpeed/DevTools for lab diagnosis.
180
+ - LCP risks: unoptimized hero/content images, blocked fonts, slow server/rendering, unnecessary client-side waterfalls, late CSS, and no preloading for critical assets.
181
+ - INP risks: long JavaScript tasks, expensive hydration, heavy event handlers, large client bundles, synchronous validation, and blocking third-party scripts.
182
+ - CLS risks: images/media without dimensions, ads/embeds without reserved space, late banners, web font swaps, skeleton mismatch, and inserted content above current viewport.
183
+ - Audit route transitions, data fetching, caching, suspense/loading states, and bundle splitting.
184
+ - Performance recommendations must preserve UX and accessibility; do not remove useful labels, focus states, or content just to improve a score.
185
+
186
+ ### Trust, Privacy, and Ethical Retention
187
+
188
+ - State clearly what data is needed, why, and what happens next.
189
+ - Make destructive actions reversible where possible, or require explicit confirmation where not.
190
+ - Keep pricing, plan limits, trial end, renewal, cancellation, and data deletion clear.
191
+ - Use notifications, email, and nudges only for user-valued return reasons.
192
+ - Treat retention as repeated successful value: faster re-entry, saved work, useful reminders, collaboration signals, new relevant content, and clear progress.
193
+ - Avoid dark patterns: hidden cancellation, forced continuity, fake scarcity, guilt copy, preselected consent, disguised ads, confirmshaming, and broken back paths.
194
+
195
+ ## Framework-Specific Fix Map
196
+
197
+ ### React, Next.js, Remix, and Astro
198
+
199
+ - Prefer semantic JSX and framework routing primitives (`Link`, loaders/actions/server actions where applicable) over click handlers on non-interactive elements.
200
+ - Check route titles, metadata, active navigation, focus restoration, route announcements, suspense/loading boundaries, and error boundaries.
201
+ - Use `eslint-plugin-jsx-a11y`, TypeScript, component tests, Playwright, axe, and framework linting where available.
202
+ - Watch for hydration-heavy widgets, client components where server rendering would work, unstable layout from late data, and forms that bypass native browser behavior.
203
+
204
+ ### Vue, Nuxt, Svelte, SvelteKit, Solid, and Angular
205
+
206
+ - Preserve native semantics in templates. Use router links, route guards, layouts, and form actions consistently with the framework.
207
+ - Check focus behavior on route change, dialogs, drawers, and client-side transitions.
208
+ - Use built-in or ecosystem a11y linting and component testing where the project already has it.
209
+ - Avoid custom stores/effects that reset user input, scroll, focus, filters, or form state unexpectedly.
210
+
211
+ ### Plain HTML/CSS/JavaScript
212
+
213
+ - Use real anchors for navigation and real buttons for actions.
214
+ - Use labels, fieldsets, legends, headings, landmarks, lists, and tables according to meaning.
215
+ - Keep custom widgets small and follow APG patterns if they cannot be native.
216
+ - Use progressive enhancement: critical flows should remain understandable when JavaScript is slow, delayed, or partially failed.
217
+
218
+ ## Report Template
219
+
220
+ ```markdown
221
+ ## Findings
222
+
223
+ | Severity | Route/Flow | Evidence | User Impact | Fix |
224
+ | --- | --- | --- | --- | --- |
225
+ | P1 | `/checkout/shipping` | `src/routes/checkout.tsx:88`, keyboard walkthrough | Users re-enter address after validation failure | Persist form state, add visible labels, add error summary and field-level errors |
226
+
227
+ ## Flow Map
228
+
229
+ Current: Home -> sign up wall -> empty dashboard -> settings -> connect integration -> first value
230
+ Recommended: Home -> sample value preview -> connect integration -> first useful result -> account save prompt
231
+
232
+ ## Implementation Notes
233
+
234
+ - Framework-specific files/components to change.
235
+ - Native HTML, ARIA/APG, routing, or performance APIs to use.
236
+ - Analytics/events to inspect or add.
237
+
238
+ ## Verification
239
+
240
+ - What was run.
241
+ - What could not be run.
242
+ - Remaining UX risks.
243
+ ```
244
+
245
+ ## Common Failure Modes
246
+
247
+ - Auditing only the homepage and missing app states after login.
248
+ - Treating a web app like a landing page instead of a task surface.
249
+ - Replacing semantic controls with styled `div`s.
250
+ - Ignoring keyboard users, focus order, route changes, and modal focus management.
251
+ - Making desktop dashboards unusable on narrow screens or zoomed text.
252
+ - Optimizing visual polish while leaving forms, errors, loading, empty states, and performance broken.
253
+ - Treating retention as more notifications instead of clearer value and easier return paths.
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env python3
2
+ """Static web UX signal scanner.
3
+
4
+ This script is intentionally conservative: it finds review signals, not final
5
+ verdicts. Use the output to decide where to inspect manually.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import re
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Iterable
16
+
17
+
18
+ EXCLUDE_DIRS = {
19
+ ".git",
20
+ ".hg",
21
+ ".svn",
22
+ "node_modules",
23
+ ".next",
24
+ ".nuxt",
25
+ ".svelte-kit",
26
+ "dist",
27
+ "build",
28
+ "coverage",
29
+ ".vercel",
30
+ ".turbo",
31
+ ".cache",
32
+ }
33
+
34
+ EXTENSIONS = {
35
+ ".html",
36
+ ".htm",
37
+ ".jsx",
38
+ ".tsx",
39
+ ".js",
40
+ ".ts",
41
+ ".vue",
42
+ ".svelte",
43
+ ".astro",
44
+ ".css",
45
+ ".scss",
46
+ ".sass",
47
+ }
48
+
49
+
50
+ @dataclass
51
+ class Finding:
52
+ severity: str
53
+ category: str
54
+ title: str
55
+ path: str
56
+ line: int
57
+ evidence: str
58
+ fix: str
59
+
60
+
61
+ PATTERNS = [
62
+ (
63
+ "P1",
64
+ "Accessibility",
65
+ "Non-semantic clickable element",
66
+ re.compile(r"<(div|span)[^>\n]+onClick\s*=", re.I),
67
+ "Use a real button for actions or a real anchor for navigation, then style it.",
68
+ ),
69
+ (
70
+ "P1",
71
+ "Keyboard",
72
+ "Positive tabindex changes natural focus order",
73
+ re.compile(r"tabIndex\s*=\s*[{\"']?\s*[1-9]", re.I),
74
+ "Use DOM order and tabindex 0 or -1 only when focus management requires it.",
75
+ ),
76
+ (
77
+ "P1",
78
+ "Accessibility",
79
+ "Focus outline removed",
80
+ re.compile(r"\boutline\s*:\s*none\b|\boutline-none\b", re.I),
81
+ "Replace removed outlines with visible focus styles that pass contrast.",
82
+ ),
83
+ (
84
+ "P1",
85
+ "Images",
86
+ "Image likely missing alt text",
87
+ re.compile(r"<(?:img|Image)\b(?![^>\n]*\balt\s*=)", re.I),
88
+ "Add useful alt text for meaningful images or alt=\"\" for decorative images.",
89
+ ),
90
+ (
91
+ "P1",
92
+ "Forms",
93
+ "Input likely missing autocomplete",
94
+ re.compile(r"<input\b(?![^>\n]*type\s*=\s*[\"']?(?:button|submit|reset|hidden))(?![^>\n]*\bautoComplete\s*=)(?![^>\n]*\bautocomplete\s*=)", re.I),
95
+ "Add autocomplete/name/type values that work with browser autofill and password managers.",
96
+ ),
97
+ (
98
+ "P2",
99
+ "Forms",
100
+ "Placeholder-only label risk",
101
+ re.compile(r"<(?:input|textarea)\b[^>\n]*placeholder\s*=", re.I),
102
+ "Verify there is a persistent visible label associated with this field.",
103
+ ),
104
+ (
105
+ "P1",
106
+ "Dialogs",
107
+ "Dialog role without aria-modal",
108
+ re.compile(r"role\s*=\s*[\"']dialog[\"'](?![^>\n]*aria-modal)", re.I),
109
+ "Use native dialog or add aria-modal, focus trap, inert background, close, and focus return.",
110
+ ),
111
+ (
112
+ "P2",
113
+ "Motion",
114
+ "Transition-all can hurt performance and predictability",
115
+ re.compile(r"\btransition-all\b|transition\s*:\s*all\b", re.I),
116
+ "Animate specific transform, opacity, color, or shadow properties.",
117
+ ),
118
+ (
119
+ "P2",
120
+ "Responsive",
121
+ "100vh or h-screen can jump on mobile browsers",
122
+ re.compile(r"\bh-screen\b|height\s*:\s*100vh\b", re.I),
123
+ "Prefer min-height: 100dvh or framework equivalent for viewport-height sections.",
124
+ ),
125
+ (
126
+ "P2",
127
+ "Links",
128
+ "Placeholder or empty hash link",
129
+ re.compile(r"<a\b[^>\n]*href\s*=\s*[\"']#(?:[\"'])", re.I),
130
+ "Use a real destination, button semantics, or remove the fake link.",
131
+ ),
132
+ (
133
+ "P2",
134
+ "Forms",
135
+ "Button missing explicit type",
136
+ re.compile(r"<button\b(?![^>\n]*\btype\s*=)", re.I),
137
+ "Set type=\"button\" for non-submit actions and type=\"submit\" for form submits.",
138
+ ),
139
+ (
140
+ "P2",
141
+ "Copy",
142
+ "Generic action label",
143
+ re.compile(r">\s*(Click here|Learn more|Submit|OK)\s*<", re.I),
144
+ "Use verb-object labels such as \"Save changes\" or \"View pricing plans\".",
145
+ ),
146
+ ]
147
+
148
+
149
+ def iter_files(root: Path) -> Iterable[Path]:
150
+ for path in root.rglob("*"):
151
+ if path.is_dir():
152
+ continue
153
+ if any(part in EXCLUDE_DIRS for part in path.parts):
154
+ continue
155
+ if path.suffix.lower() not in EXTENSIONS:
156
+ continue
157
+ if path.stat().st_size > 1_000_000:
158
+ continue
159
+ yield path
160
+
161
+
162
+ def detect_stack(root: Path) -> list[str]:
163
+ stack: list[str] = []
164
+ package_json = root / "package.json"
165
+ if package_json.exists():
166
+ try:
167
+ data = json.loads(package_json.read_text(encoding="utf-8"))
168
+ deps = " ".join(
169
+ list((data.get("dependencies") or {}).keys())
170
+ + list((data.get("devDependencies") or {}).keys())
171
+ )
172
+ checks = [
173
+ ("Next.js", "next"),
174
+ ("React", "react"),
175
+ ("Vue", "vue"),
176
+ ("Nuxt", "nuxt"),
177
+ ("Svelte", "svelte"),
178
+ ("SvelteKit", "@sveltejs/kit"),
179
+ ("Angular", "@angular/core"),
180
+ ("Remix", "@remix-run"),
181
+ ("Astro", "astro"),
182
+ ("Solid", "solid-js"),
183
+ ("Tailwind", "tailwindcss"),
184
+ ("Playwright", "@playwright/test"),
185
+ ("axe", "axe-core"),
186
+ ]
187
+ for label, token in checks:
188
+ if token in deps:
189
+ stack.append(label)
190
+ except Exception:
191
+ stack.append("package.json present, unreadable")
192
+ for marker, label in [
193
+ ("app", "App Router or app directory"),
194
+ ("pages", "Pages directory"),
195
+ ("src", "src directory"),
196
+ ]:
197
+ if (root / marker).exists():
198
+ stack.append(label)
199
+ return sorted(set(stack)) or ["Unknown web stack"]
200
+
201
+
202
+ def scan(root: Path) -> tuple[list[str], list[Finding], int]:
203
+ findings: list[Finding] = []
204
+ files = list(iter_files(root))
205
+ for file_path in files:
206
+ rel = file_path.relative_to(root).as_posix()
207
+ try:
208
+ lines = file_path.read_text(encoding="utf-8", errors="ignore").splitlines()
209
+ except Exception:
210
+ continue
211
+ for idx, line in enumerate(lines, start=1):
212
+ stripped = line.strip()
213
+ if not stripped:
214
+ continue
215
+ for severity, category, title, pattern, fix in PATTERNS:
216
+ if pattern.search(stripped):
217
+ findings.append(
218
+ Finding(
219
+ severity=severity,
220
+ category=category,
221
+ title=title,
222
+ path=rel,
223
+ line=idx,
224
+ evidence=stripped[:180],
225
+ fix=fix,
226
+ )
227
+ )
228
+ return detect_stack(root), findings, len(files)
229
+
230
+
231
+ def render_markdown(root: Path, stack: list[str], findings: list[Finding], file_count: int) -> str:
232
+ counts = {key: sum(1 for item in findings if item.severity == key) for key in ("P0", "P1", "P2", "P3")}
233
+ out = [
234
+ "# Web UX Static Scan",
235
+ "",
236
+ f"Root: `{root}`",
237
+ f"Files scanned: `{file_count}`",
238
+ f"Detected stack: {', '.join(stack)}",
239
+ f"Findings: P0={counts['P0']} P1={counts['P1']} P2={counts['P2']} P3={counts['P3']}",
240
+ "",
241
+ "> Static scan output is a triage signal. Confirm every finding in the UI or code before changing behavior.",
242
+ "",
243
+ ]
244
+ if not findings:
245
+ out.append("No matching static UX signals found.")
246
+ return "\n".join(out)
247
+
248
+ out.extend(["| Severity | Category | Location | Signal | Evidence | Fix |", "| --- | --- | --- | --- | --- | --- |"])
249
+ for item in findings[:120]:
250
+ evidence = item.evidence.replace("|", "\\|")
251
+ out.append(
252
+ f"| {item.severity} | {item.category} | `{item.path}:{item.line}` | {item.title} | `{evidence}` | {item.fix} |"
253
+ )
254
+ if len(findings) > 120:
255
+ out.append(f"\nTruncated to 120 findings out of {len(findings)}. Narrow the scan path for more detail.")
256
+ return "\n".join(out)
257
+
258
+
259
+ def main() -> int:
260
+ parser = argparse.ArgumentParser(description="Scan a web app for static UX review signals.")
261
+ parser.add_argument("root", nargs="?", default=".", help="Project root or subdirectory to scan")
262
+ args = parser.parse_args()
263
+
264
+ root = Path(args.root).resolve()
265
+ if not root.exists():
266
+ raise SystemExit(f"Path does not exist: {root}")
267
+ stack, findings, file_count = scan(root)
268
+ print(render_markdown(root, stack, findings, file_count))
269
+ return 0
270
+
271
+
272
+ if __name__ == "__main__":
273
+ raise SystemExit(main())