@hustle-together/api-dev-tools 3.12.3 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/adr-requests/.gitkeep +10 -0
- package/.claude/agents/adr-researcher.md +109 -0
- package/.claude/agents/visual-analyzer.md +183 -0
- package/.claude/api-dev-state.json +7 -463
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- package/.claude/workflow-logs/None.json +49 -0
- package/.claude/workflow-logs/session-20251230-143727.json +106 -0
- package/.skills/adr-deep-research/SKILL.md +351 -0
- package/.skills/api-create/SKILL.md +116 -17
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-sync/SKILL.md +260 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +786 -0
- package/.skills/hustle-build-review/SKILL.md +518 -0
- package/.skills/parallel-spawn/SKILL.md +212 -0
- package/.skills/ralph-continue/SKILL.md +151 -0
- package/.skills/ralph-loop/SKILL.md +341 -0
- package/.skills/ralph-status/SKILL.md +87 -0
- package/.skills/refactor/SKILL.md +59 -0
- package/.skills/shadcn/SKILL.md +522 -0
- package/.skills/test-all/SKILL.md +210 -0
- package/.skills/test-builds/SKILL.md +208 -0
- package/.skills/test-debug/SKILL.md +212 -0
- package/.skills/test-e2e/SKILL.md +168 -0
- package/.skills/test-review/SKILL.md +707 -0
- package/.skills/test-unit/SKILL.md +143 -0
- package/.skills/test-visual/SKILL.md +301 -0
- package/.skills/token-report/SKILL.md +132 -0
- package/CHANGELOG.md +575 -0
- package/README.md +426 -56
- package/bin/cli.js +1538 -88
- package/commands/hustle-api-create.md +22 -0
- package/commands/hustle-build.md +259 -0
- package/commands/hustle-combine.md +81 -2
- package/commands/hustle-ui-create-page.md +84 -2
- package/commands/hustle-ui-create.md +82 -2
- package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
- package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
- package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
- package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
- package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
- package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
- package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
- package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
- package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
- package/hooks/api-workflow-check.py +34 -0
- package/hooks/auto-answer.py +305 -0
- package/hooks/check-update.py +132 -0
- package/hooks/completion-promise-detector.py +293 -0
- package/hooks/context-capacity-warning.py +171 -0
- package/hooks/docs-update-check.py +120 -0
- package/hooks/enforce-dry-run.py +134 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/hooks/generate-adr-options.py +282 -0
- package/hooks/hook_utils.py +609 -0
- package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- package/hooks/ntfy-on-question.py +240 -0
- package/hooks/orchestrator-completion.py +313 -0
- package/hooks/orchestrator-handoff.py +267 -0
- package/hooks/orchestrator-session-startup.py +146 -0
- package/hooks/parallel-orchestrator.py +451 -0
- package/hooks/periodic-reground.py +270 -67
- package/hooks/project-document-prompt.py +302 -0
- package/hooks/remote-question-proxy.py +284 -0
- package/hooks/remote-question-server.py +1224 -0
- package/hooks/run-code-review.py +176 -29
- package/hooks/run-visual-qa.py +338 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/hooks/update-adr-decision.py +236 -0
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-testing-checklist.py +195 -0
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/CLAUDE-SECTION.md +89 -64
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- package/templates/api-showcase/_components/APIModal.tsx +100 -8
- package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
- package/templates/api-showcase/_components/APITester.tsx +367 -58
- package/templates/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- package/templates/docs/page.tsx +230 -0
- package/templates/eslint-plugin-zod-schema/index.js +446 -0
- package/templates/eslint-plugin-zod-schema/package.json +26 -0
- package/templates/github-workflows/security.yml +274 -0
- package/templates/hustle-build-defaults.json +136 -0
- package/templates/hustle-dev-dashboard/page.tsx +365 -0
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/playwright-report/page.tsx +258 -0
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +155 -7
- package/templates/test-results/page.tsx +237 -0
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- package/templates/ui-showcase/page.tsx +1 -1
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from "react";
|
|
4
|
+
|
|
5
|
+
// Import registry for component/page data
|
|
6
|
+
import registry from "@/../.claude/registry.json";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 7 Viewport Definitions
|
|
10
|
+
* These match the viewports defined in performance-budgets.json
|
|
11
|
+
*/
|
|
12
|
+
const VIEWPORTS = [
|
|
13
|
+
{ id: "mobile-portrait", label: "Mobile", width: 375, height: 667, icon: "📱" },
|
|
14
|
+
{ id: "mobile-notch", label: "Notch", width: 393, height: 852, icon: "📲" },
|
|
15
|
+
{ id: "mobile-landscape", label: "M-Land", width: 667, height: 375, icon: "📱" },
|
|
16
|
+
{ id: "tablet-portrait", label: "Tablet", width: 768, height: 1024, icon: "📋" },
|
|
17
|
+
{ id: "tablet-landscape", label: "T-Land", width: 1024, height: 768, icon: "📋" },
|
|
18
|
+
{ id: "small-desktop", label: "Laptop", width: 1280, height: 720, icon: "💻" },
|
|
19
|
+
{ id: "desktop", label: "Desktop", width: 1920, height: 1080, icon: "🖥️" },
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
type TestStatus = "pass" | "warning" | "fail" | "pending" | "skipped";
|
|
23
|
+
|
|
24
|
+
interface ViewportResult {
|
|
25
|
+
status: TestStatus;
|
|
26
|
+
screenshot?: string;
|
|
27
|
+
issues?: Array<{
|
|
28
|
+
type: string;
|
|
29
|
+
severity: "error" | "warning" | "info";
|
|
30
|
+
element?: string;
|
|
31
|
+
detail: string;
|
|
32
|
+
}>;
|
|
33
|
+
haikuAnalysis?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ComponentTestResult {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
type: "component" | "page";
|
|
40
|
+
state: string;
|
|
41
|
+
route?: string;
|
|
42
|
+
file?: string;
|
|
43
|
+
viewports: Record<string, ViewportResult>;
|
|
44
|
+
lastTested?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Mock data generator - in production, this would come from actual test results
|
|
48
|
+
function generateMockResults(): ComponentTestResult[] {
|
|
49
|
+
const typedRegistry = registry as any;
|
|
50
|
+
const results: ComponentTestResult[] = [];
|
|
51
|
+
|
|
52
|
+
// Add components with their variants as states
|
|
53
|
+
Object.entries(typedRegistry.components || {}).forEach(([id, data]: [string, any]) => {
|
|
54
|
+
const variants = data.variants || ["default"];
|
|
55
|
+
variants.forEach((variant: string) => {
|
|
56
|
+
results.push({
|
|
57
|
+
id: `${id}-${variant}`,
|
|
58
|
+
name: data.name || id,
|
|
59
|
+
type: "component",
|
|
60
|
+
state: variant,
|
|
61
|
+
file: data.file,
|
|
62
|
+
viewports: generateViewportResults(),
|
|
63
|
+
lastTested: new Date().toISOString(),
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Add pages
|
|
69
|
+
Object.entries(typedRegistry.pages || {}).forEach(([id, data]: [string, any]) => {
|
|
70
|
+
results.push({
|
|
71
|
+
id,
|
|
72
|
+
name: data.name || id,
|
|
73
|
+
type: "page",
|
|
74
|
+
state: "default",
|
|
75
|
+
route: data.route,
|
|
76
|
+
file: data.file,
|
|
77
|
+
viewports: generateViewportResults(),
|
|
78
|
+
lastTested: new Date().toISOString(),
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function generateViewportResults(): Record<string, ViewportResult> {
|
|
86
|
+
const results: Record<string, ViewportResult> = {};
|
|
87
|
+
VIEWPORTS.forEach((vp) => {
|
|
88
|
+
// Random status for demo - in production, read from actual test results
|
|
89
|
+
const statuses: TestStatus[] = ["pass", "pass", "pass", "warning", "fail"];
|
|
90
|
+
const status = statuses[Math.floor(Math.random() * statuses.length)];
|
|
91
|
+
|
|
92
|
+
results[vp.id] = {
|
|
93
|
+
status,
|
|
94
|
+
screenshot: `/__snapshots__/${vp.id}.png`,
|
|
95
|
+
issues: status !== "pass" ? [{
|
|
96
|
+
type: status === "fail" ? "touch-target" : "contrast",
|
|
97
|
+
severity: status === "fail" ? "error" : "warning",
|
|
98
|
+
element: "button.primary",
|
|
99
|
+
detail: status === "fail"
|
|
100
|
+
? "Touch target too small (32x32px, min 44x44px)"
|
|
101
|
+
: "Contrast ratio 3.8:1 (min 4.5:1 for AA)",
|
|
102
|
+
}] : [],
|
|
103
|
+
haikuAnalysis: status !== "pass"
|
|
104
|
+
? `${status === "fail" ? "Critical" : "Minor"} accessibility issue detected in ${vp.label} viewport.`
|
|
105
|
+
: undefined,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Visual Testing Dashboard
|
|
113
|
+
*
|
|
114
|
+
* Displays a matrix of all components × states × 7 viewports
|
|
115
|
+
* with pass/warning/fail indicators and AI analysis results.
|
|
116
|
+
*
|
|
117
|
+
* Features:
|
|
118
|
+
* - Table with components/states as rows
|
|
119
|
+
* - 7 viewport columns with status indicators
|
|
120
|
+
* - Click any cell to see screenshot and Haiku analysis
|
|
121
|
+
* - Filter by status (all, passing, issues)
|
|
122
|
+
* - Links to component files and routes
|
|
123
|
+
*/
|
|
124
|
+
export function VisualTestingDashboard() {
|
|
125
|
+
const [selectedCell, setSelectedCell] = useState<{
|
|
126
|
+
result: ComponentTestResult;
|
|
127
|
+
viewport: typeof VIEWPORTS[number];
|
|
128
|
+
data: ViewportResult;
|
|
129
|
+
} | null>(null);
|
|
130
|
+
const [statusFilter, setStatusFilter] = useState<"all" | "pass" | "issues">("all");
|
|
131
|
+
const [typeFilter, setTypeFilter] = useState<"all" | "component" | "page">("all");
|
|
132
|
+
|
|
133
|
+
// Get test results (mock for now, would be from file in production)
|
|
134
|
+
const testResults = useMemo(() => generateMockResults(), []);
|
|
135
|
+
|
|
136
|
+
// Filter results
|
|
137
|
+
const filteredResults = useMemo(() => {
|
|
138
|
+
return testResults.filter((result) => {
|
|
139
|
+
// Type filter
|
|
140
|
+
if (typeFilter !== "all" && result.type !== typeFilter) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Status filter
|
|
145
|
+
if (statusFilter === "all") return true;
|
|
146
|
+
|
|
147
|
+
const hasIssues = Object.values(result.viewports).some(
|
|
148
|
+
(vp) => vp.status === "fail" || vp.status === "warning"
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return statusFilter === "issues" ? hasIssues : !hasIssues;
|
|
152
|
+
});
|
|
153
|
+
}, [testResults, statusFilter, typeFilter]);
|
|
154
|
+
|
|
155
|
+
// Summary stats
|
|
156
|
+
const stats = useMemo(() => {
|
|
157
|
+
let total = 0;
|
|
158
|
+
let passing = 0;
|
|
159
|
+
let warnings = 0;
|
|
160
|
+
let failing = 0;
|
|
161
|
+
|
|
162
|
+
testResults.forEach((result) => {
|
|
163
|
+
Object.values(result.viewports).forEach((vp) => {
|
|
164
|
+
total++;
|
|
165
|
+
if (vp.status === "pass") passing++;
|
|
166
|
+
else if (vp.status === "warning") warnings++;
|
|
167
|
+
else if (vp.status === "fail") failing++;
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return { total, passing, warnings, failing };
|
|
172
|
+
}, [testResults]);
|
|
173
|
+
|
|
174
|
+
const getStatusColor = (status: TestStatus) => {
|
|
175
|
+
switch (status) {
|
|
176
|
+
case "pass": return "bg-green-500";
|
|
177
|
+
case "warning": return "bg-yellow-500";
|
|
178
|
+
case "fail": return "bg-red-500";
|
|
179
|
+
case "pending": return "bg-gray-400";
|
|
180
|
+
case "skipped": return "bg-gray-300";
|
|
181
|
+
default: return "bg-gray-300";
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const getStatusIcon = (status: TestStatus) => {
|
|
186
|
+
switch (status) {
|
|
187
|
+
case "pass": return "✓";
|
|
188
|
+
case "warning": return "⚠";
|
|
189
|
+
case "fail": return "✗";
|
|
190
|
+
case "pending": return "○";
|
|
191
|
+
case "skipped": return "−";
|
|
192
|
+
default: return "?";
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div className="min-h-screen bg-white dark:bg-[#050505]">
|
|
198
|
+
{/* Header */}
|
|
199
|
+
<div className="border-b-2 border-black bg-white dark:border-gray-600 dark:bg-[#050505]">
|
|
200
|
+
<div className="container mx-auto px-4 py-6">
|
|
201
|
+
<h1 className="text-2xl font-bold text-black dark:text-white">
|
|
202
|
+
Visual Testing Dashboard
|
|
203
|
+
</h1>
|
|
204
|
+
<p className="mt-1 text-gray-600 dark:text-gray-400">
|
|
205
|
+
Screenshot analysis across 7 viewports with AI-powered issue detection
|
|
206
|
+
</p>
|
|
207
|
+
|
|
208
|
+
{/* Stats Bar */}
|
|
209
|
+
<div className="mt-4 flex flex-wrap gap-4">
|
|
210
|
+
<div className="flex items-center gap-2 border-2 border-black px-3 py-1.5 dark:border-gray-600">
|
|
211
|
+
<span className="h-3 w-3 rounded-full bg-green-500" />
|
|
212
|
+
<span className="text-sm font-bold">{stats.passing}</span>
|
|
213
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">passing</span>
|
|
214
|
+
</div>
|
|
215
|
+
<div className="flex items-center gap-2 border-2 border-black px-3 py-1.5 dark:border-gray-600">
|
|
216
|
+
<span className="h-3 w-3 rounded-full bg-yellow-500" />
|
|
217
|
+
<span className="text-sm font-bold">{stats.warnings}</span>
|
|
218
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">warnings</span>
|
|
219
|
+
</div>
|
|
220
|
+
<div className="flex items-center gap-2 border-2 border-black px-3 py-1.5 dark:border-gray-600">
|
|
221
|
+
<span className="h-3 w-3 rounded-full bg-red-500" />
|
|
222
|
+
<span className="text-sm font-bold">{stats.failing}</span>
|
|
223
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">failing</span>
|
|
224
|
+
</div>
|
|
225
|
+
<div className="ml-auto text-sm text-gray-600 dark:text-gray-400">
|
|
226
|
+
{stats.total} tests across {testResults.length} elements
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Filter Bar */}
|
|
233
|
+
<div className="sticky top-0 z-10 border-b-2 border-black bg-white/95 backdrop-blur dark:border-gray-600 dark:bg-[#050505]/95">
|
|
234
|
+
<div className="container mx-auto flex flex-wrap gap-4 px-4 py-3">
|
|
235
|
+
{/* Status Filter */}
|
|
236
|
+
<div className="flex gap-2">
|
|
237
|
+
<button
|
|
238
|
+
onClick={() => setStatusFilter("all")}
|
|
239
|
+
className={`border-2 px-3 py-1 text-sm font-bold transition-colors ${
|
|
240
|
+
statusFilter === "all"
|
|
241
|
+
? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
|
|
242
|
+
: "border-black bg-white hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
243
|
+
}`}
|
|
244
|
+
>
|
|
245
|
+
All
|
|
246
|
+
</button>
|
|
247
|
+
<button
|
|
248
|
+
onClick={() => setStatusFilter("pass")}
|
|
249
|
+
className={`border-2 px-3 py-1 text-sm font-bold transition-colors ${
|
|
250
|
+
statusFilter === "pass"
|
|
251
|
+
? "border-green-600 bg-green-600 text-white"
|
|
252
|
+
: "border-black bg-white hover:border-green-600 dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
253
|
+
}`}
|
|
254
|
+
>
|
|
255
|
+
Passing
|
|
256
|
+
</button>
|
|
257
|
+
<button
|
|
258
|
+
onClick={() => setStatusFilter("issues")}
|
|
259
|
+
className={`border-2 px-3 py-1 text-sm font-bold transition-colors ${
|
|
260
|
+
statusFilter === "issues"
|
|
261
|
+
? "border-red-600 bg-red-600 text-white"
|
|
262
|
+
: "border-black bg-white hover:border-red-600 dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
263
|
+
}`}
|
|
264
|
+
>
|
|
265
|
+
Issues
|
|
266
|
+
</button>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Type Filter */}
|
|
270
|
+
<div className="flex gap-2">
|
|
271
|
+
<button
|
|
272
|
+
onClick={() => setTypeFilter("all")}
|
|
273
|
+
className={`border-2 px-3 py-1 text-sm font-bold transition-colors ${
|
|
274
|
+
typeFilter === "all"
|
|
275
|
+
? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
|
|
276
|
+
: "border-black bg-white hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
277
|
+
}`}
|
|
278
|
+
>
|
|
279
|
+
All Types
|
|
280
|
+
</button>
|
|
281
|
+
<button
|
|
282
|
+
onClick={() => setTypeFilter("component")}
|
|
283
|
+
className={`border-2 px-3 py-1 text-sm font-bold transition-colors ${
|
|
284
|
+
typeFilter === "component"
|
|
285
|
+
? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
|
|
286
|
+
: "border-black bg-white hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
287
|
+
}`}
|
|
288
|
+
>
|
|
289
|
+
Components
|
|
290
|
+
</button>
|
|
291
|
+
<button
|
|
292
|
+
onClick={() => setTypeFilter("page")}
|
|
293
|
+
className={`border-2 px-3 py-1 text-sm font-bold transition-colors ${
|
|
294
|
+
typeFilter === "page"
|
|
295
|
+
? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
|
|
296
|
+
: "border-black bg-white hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
297
|
+
}`}
|
|
298
|
+
>
|
|
299
|
+
Pages
|
|
300
|
+
</button>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
{/* Matrix Table */}
|
|
306
|
+
<main className="container mx-auto px-4 py-6">
|
|
307
|
+
<div className="overflow-x-auto">
|
|
308
|
+
<table className="w-full border-collapse border-2 border-black dark:border-gray-600">
|
|
309
|
+
{/* Header */}
|
|
310
|
+
<thead>
|
|
311
|
+
<tr className="bg-gray-100 dark:bg-gray-800">
|
|
312
|
+
<th className="border-2 border-black px-4 py-3 text-left font-bold dark:border-gray-600">
|
|
313
|
+
Element
|
|
314
|
+
</th>
|
|
315
|
+
<th className="border-2 border-black px-4 py-3 text-left font-bold dark:border-gray-600">
|
|
316
|
+
State
|
|
317
|
+
</th>
|
|
318
|
+
{VIEWPORTS.map((vp) => (
|
|
319
|
+
<th
|
|
320
|
+
key={vp.id}
|
|
321
|
+
className="border-2 border-black px-2 py-3 text-center font-bold dark:border-gray-600"
|
|
322
|
+
title={`${vp.width}×${vp.height}`}
|
|
323
|
+
>
|
|
324
|
+
<div className="flex flex-col items-center gap-1">
|
|
325
|
+
<span>{vp.icon}</span>
|
|
326
|
+
<span className="text-xs">{vp.label}</span>
|
|
327
|
+
<span className="text-[10px] text-gray-500">{vp.width}px</span>
|
|
328
|
+
</div>
|
|
329
|
+
</th>
|
|
330
|
+
))}
|
|
331
|
+
<th className="border-2 border-black px-4 py-3 text-left font-bold dark:border-gray-600">
|
|
332
|
+
Links
|
|
333
|
+
</th>
|
|
334
|
+
</tr>
|
|
335
|
+
</thead>
|
|
336
|
+
|
|
337
|
+
{/* Body */}
|
|
338
|
+
<tbody>
|
|
339
|
+
{filteredResults.length === 0 ? (
|
|
340
|
+
<tr>
|
|
341
|
+
<td
|
|
342
|
+
colSpan={VIEWPORTS.length + 3}
|
|
343
|
+
className="border-2 border-black px-4 py-8 text-center text-gray-500 dark:border-gray-600"
|
|
344
|
+
>
|
|
345
|
+
No test results found. Run /test-visual to generate results.
|
|
346
|
+
</td>
|
|
347
|
+
</tr>
|
|
348
|
+
) : (
|
|
349
|
+
filteredResults.map((result) => (
|
|
350
|
+
<tr
|
|
351
|
+
key={result.id}
|
|
352
|
+
className="hover:bg-gray-50 dark:hover:bg-gray-900"
|
|
353
|
+
>
|
|
354
|
+
{/* Element Name */}
|
|
355
|
+
<td className="border-2 border-black px-4 py-2 dark:border-gray-600">
|
|
356
|
+
<div className="flex items-center gap-2">
|
|
357
|
+
<span
|
|
358
|
+
className={`rounded px-1.5 py-0.5 text-xs font-bold ${
|
|
359
|
+
result.type === "component"
|
|
360
|
+
? "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
|
361
|
+
: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"
|
|
362
|
+
}`}
|
|
363
|
+
>
|
|
364
|
+
{result.type === "component" ? "C" : "P"}
|
|
365
|
+
</span>
|
|
366
|
+
<span className="font-medium">{result.name}</span>
|
|
367
|
+
</div>
|
|
368
|
+
</td>
|
|
369
|
+
|
|
370
|
+
{/* State */}
|
|
371
|
+
<td className="border-2 border-black px-4 py-2 text-sm text-gray-600 dark:border-gray-600 dark:text-gray-400">
|
|
372
|
+
{result.state}
|
|
373
|
+
</td>
|
|
374
|
+
|
|
375
|
+
{/* Viewport Results */}
|
|
376
|
+
{VIEWPORTS.map((vp) => {
|
|
377
|
+
const vpResult = result.viewports[vp.id];
|
|
378
|
+
return (
|
|
379
|
+
<td
|
|
380
|
+
key={vp.id}
|
|
381
|
+
className="border-2 border-black px-2 py-2 text-center dark:border-gray-600"
|
|
382
|
+
>
|
|
383
|
+
<button
|
|
384
|
+
onClick={() =>
|
|
385
|
+
setSelectedCell({
|
|
386
|
+
result,
|
|
387
|
+
viewport: vp,
|
|
388
|
+
data: vpResult,
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
className={`inline-flex h-8 w-8 items-center justify-center rounded-full text-white transition-transform hover:scale-110 ${getStatusColor(
|
|
392
|
+
vpResult.status
|
|
393
|
+
)}`}
|
|
394
|
+
title={`${vpResult.status} - Click for details`}
|
|
395
|
+
>
|
|
396
|
+
{getStatusIcon(vpResult.status)}
|
|
397
|
+
</button>
|
|
398
|
+
</td>
|
|
399
|
+
);
|
|
400
|
+
})}
|
|
401
|
+
|
|
402
|
+
{/* Links */}
|
|
403
|
+
<td className="border-2 border-black px-4 py-2 dark:border-gray-600">
|
|
404
|
+
<div className="flex gap-2">
|
|
405
|
+
{result.file && (
|
|
406
|
+
<a
|
|
407
|
+
href={`vscode://file/${result.file}`}
|
|
408
|
+
className="text-xs text-blue-600 hover:underline dark:text-blue-400"
|
|
409
|
+
title="Open in VS Code"
|
|
410
|
+
>
|
|
411
|
+
Code
|
|
412
|
+
</a>
|
|
413
|
+
)}
|
|
414
|
+
{result.route && (
|
|
415
|
+
<a
|
|
416
|
+
href={result.route}
|
|
417
|
+
target="_blank"
|
|
418
|
+
rel="noopener noreferrer"
|
|
419
|
+
className="text-xs text-green-600 hover:underline dark:text-green-400"
|
|
420
|
+
title="View page"
|
|
421
|
+
>
|
|
422
|
+
View
|
|
423
|
+
</a>
|
|
424
|
+
)}
|
|
425
|
+
</div>
|
|
426
|
+
</td>
|
|
427
|
+
</tr>
|
|
428
|
+
))
|
|
429
|
+
)}
|
|
430
|
+
</tbody>
|
|
431
|
+
</table>
|
|
432
|
+
</div>
|
|
433
|
+
</main>
|
|
434
|
+
|
|
435
|
+
{/* Detail Modal */}
|
|
436
|
+
{selectedCell && (
|
|
437
|
+
<div
|
|
438
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
|
439
|
+
onClick={() => setSelectedCell(null)}
|
|
440
|
+
>
|
|
441
|
+
<div
|
|
442
|
+
className="max-h-[90vh] w-full max-w-2xl overflow-y-auto border-4 border-black bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] dark:border-gray-600 dark:bg-[#050505]"
|
|
443
|
+
onClick={(e) => e.stopPropagation()}
|
|
444
|
+
>
|
|
445
|
+
{/* Modal Header */}
|
|
446
|
+
<div className="flex items-center justify-between border-b-2 border-black px-4 py-3 dark:border-gray-600">
|
|
447
|
+
<div>
|
|
448
|
+
<h2 className="text-lg font-bold">
|
|
449
|
+
{selectedCell.result.name} - {selectedCell.result.state}
|
|
450
|
+
</h2>
|
|
451
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
452
|
+
{selectedCell.viewport.icon} {selectedCell.viewport.label} ({selectedCell.viewport.width}×{selectedCell.viewport.height})
|
|
453
|
+
</p>
|
|
454
|
+
</div>
|
|
455
|
+
<button
|
|
456
|
+
onClick={() => setSelectedCell(null)}
|
|
457
|
+
className="border-2 border-black p-2 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800"
|
|
458
|
+
>
|
|
459
|
+
✕
|
|
460
|
+
</button>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
{/* Status Badge */}
|
|
464
|
+
<div className="border-b-2 border-black px-4 py-3 dark:border-gray-600">
|
|
465
|
+
<span
|
|
466
|
+
className={`inline-flex items-center gap-2 rounded px-3 py-1 text-sm font-bold text-white ${getStatusColor(
|
|
467
|
+
selectedCell.data.status
|
|
468
|
+
)}`}
|
|
469
|
+
>
|
|
470
|
+
{getStatusIcon(selectedCell.data.status)} {selectedCell.data.status.toUpperCase()}
|
|
471
|
+
</span>
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
{/* Screenshot Placeholder */}
|
|
475
|
+
<div className="border-b-2 border-black p-4 dark:border-gray-600">
|
|
476
|
+
<div
|
|
477
|
+
className="flex items-center justify-center border-2 border-dashed border-gray-400 bg-gray-100 dark:bg-gray-800"
|
|
478
|
+
style={{
|
|
479
|
+
aspectRatio: `${selectedCell.viewport.width}/${selectedCell.viewport.height}`,
|
|
480
|
+
maxHeight: "300px",
|
|
481
|
+
}}
|
|
482
|
+
>
|
|
483
|
+
<div className="text-center text-gray-500">
|
|
484
|
+
<p className="text-4xl">🖼️</p>
|
|
485
|
+
<p className="mt-2 text-sm">Screenshot</p>
|
|
486
|
+
<p className="text-xs">{selectedCell.data.screenshot}</p>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
{/* Issues */}
|
|
492
|
+
{selectedCell.data.issues && selectedCell.data.issues.length > 0 && (
|
|
493
|
+
<div className="border-b-2 border-black p-4 dark:border-gray-600">
|
|
494
|
+
<h3 className="mb-2 font-bold">Issues Found</h3>
|
|
495
|
+
<ul className="space-y-2">
|
|
496
|
+
{selectedCell.data.issues.map((issue, i) => (
|
|
497
|
+
<li
|
|
498
|
+
key={i}
|
|
499
|
+
className={`border-l-4 p-2 ${
|
|
500
|
+
issue.severity === "error"
|
|
501
|
+
? "border-red-500 bg-red-50 dark:bg-red-900/20"
|
|
502
|
+
: issue.severity === "warning"
|
|
503
|
+
? "border-yellow-500 bg-yellow-50 dark:bg-yellow-900/20"
|
|
504
|
+
: "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
|
|
505
|
+
}`}
|
|
506
|
+
>
|
|
507
|
+
<div className="flex items-center gap-2 text-sm font-bold">
|
|
508
|
+
<span className="uppercase">{issue.type}</span>
|
|
509
|
+
{issue.element && (
|
|
510
|
+
<code className="rounded bg-gray-200 px-1 text-xs dark:bg-gray-700">
|
|
511
|
+
{issue.element}
|
|
512
|
+
</code>
|
|
513
|
+
)}
|
|
514
|
+
</div>
|
|
515
|
+
<p className="mt-1 text-sm">{issue.detail}</p>
|
|
516
|
+
</li>
|
|
517
|
+
))}
|
|
518
|
+
</ul>
|
|
519
|
+
</div>
|
|
520
|
+
)}
|
|
521
|
+
|
|
522
|
+
{/* Haiku Analysis */}
|
|
523
|
+
{selectedCell.data.haikuAnalysis && (
|
|
524
|
+
<div className="p-4">
|
|
525
|
+
<h3 className="mb-2 font-bold">AI Analysis (Haiku)</h3>
|
|
526
|
+
<p className="rounded border-2 border-black bg-gray-50 p-3 text-sm dark:border-gray-600 dark:bg-gray-800">
|
|
527
|
+
{selectedCell.data.haikuAnalysis}
|
|
528
|
+
</p>
|
|
529
|
+
</div>
|
|
530
|
+
)}
|
|
531
|
+
|
|
532
|
+
{/* Actions */}
|
|
533
|
+
<div className="flex gap-2 border-t-2 border-black p-4 dark:border-gray-600">
|
|
534
|
+
<button className="border-2 border-black px-4 py-2 font-bold hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800">
|
|
535
|
+
View Full Screenshot
|
|
536
|
+
</button>
|
|
537
|
+
<button className="border-2 border-black px-4 py-2 font-bold hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800">
|
|
538
|
+
Re-run Test
|
|
539
|
+
</button>
|
|
540
|
+
{selectedCell.result.file && (
|
|
541
|
+
<a
|
|
542
|
+
href={`vscode://file/${selectedCell.result.file}`}
|
|
543
|
+
className="border-2 border-[#BA0C2F] bg-[#BA0C2F] px-4 py-2 font-bold text-white hover:bg-[#8A0921]"
|
|
544
|
+
>
|
|
545
|
+
Open in VS Code
|
|
546
|
+
</a>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
)}
|
|
552
|
+
|
|
553
|
+
{/* Legend */}
|
|
554
|
+
<div className="border-t-2 border-black bg-gray-50 py-4 dark:border-gray-600 dark:bg-gray-900">
|
|
555
|
+
<div className="container mx-auto px-4">
|
|
556
|
+
<div className="flex flex-wrap items-center gap-6 text-sm">
|
|
557
|
+
<span className="font-bold">Legend:</span>
|
|
558
|
+
<div className="flex items-center gap-2">
|
|
559
|
+
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-green-500 text-white">✓</span>
|
|
560
|
+
<span>Pass</span>
|
|
561
|
+
</div>
|
|
562
|
+
<div className="flex items-center gap-2">
|
|
563
|
+
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-yellow-500 text-white">⚠</span>
|
|
564
|
+
<span>Warning (accessibility/contrast)</span>
|
|
565
|
+
</div>
|
|
566
|
+
<div className="flex items-center gap-2">
|
|
567
|
+
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-white">✗</span>
|
|
568
|
+
<span>Fail (layout/touch target)</span>
|
|
569
|
+
</div>
|
|
570
|
+
<div className="flex items-center gap-2">
|
|
571
|
+
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-gray-400 text-white">○</span>
|
|
572
|
+
<span>Pending</span>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
);
|
|
579
|
+
}
|