@hustle-together/api-dev-tools 3.12.16 → 4.5.3

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.
Files changed (180) hide show
  1. package/.claude/adr-requests/.gitkeep +10 -0
  2. package/.claude/agents/adr-researcher.md +109 -0
  3. package/.claude/agents/visual-analyzer.md +183 -0
  4. package/.claude/api-dev-state.json +10 -0
  5. package/.claude/documentation-audit.json +114 -0
  6. package/.claude/registry.json +289 -0
  7. package/.claude/settings.json +45 -1
  8. package/.claude/settings.local.json +1 -7
  9. package/.claude/workflow-logs/None.json +49 -0
  10. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  11. package/.skills/adr-deep-research/SKILL.md +351 -0
  12. package/.skills/api-create/SKILL.md +34 -20
  13. package/.skills/api-research/SKILL.md +130 -0
  14. package/.skills/docs-update/SKILL.md +205 -0
  15. package/.skills/hustle-brand/SKILL.md +368 -0
  16. package/.skills/hustle-build/SKILL.md +365 -38
  17. package/.skills/parallel-spawn/SKILL.md +212 -0
  18. package/.skills/ralph-continue/SKILL.md +151 -0
  19. package/.skills/ralph-loop/SKILL.md +341 -0
  20. package/.skills/ralph-status/SKILL.md +87 -0
  21. package/.skills/refactor/SKILL.md +59 -0
  22. package/.skills/shadcn/SKILL.md +522 -0
  23. package/.skills/test-all/SKILL.md +210 -0
  24. package/.skills/test-builds/SKILL.md +208 -0
  25. package/.skills/test-debug/SKILL.md +212 -0
  26. package/.skills/test-e2e/SKILL.md +168 -0
  27. package/.skills/test-review/SKILL.md +707 -0
  28. package/.skills/test-unit/SKILL.md +143 -0
  29. package/.skills/test-visual/SKILL.md +301 -0
  30. package/.skills/token-report/SKILL.md +132 -0
  31. package/CHANGELOG.md +488 -0
  32. package/README.md +346 -53
  33. package/bin/cli.js +359 -123
  34. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  35. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  36. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  37. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  38. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  39. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  40. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  95. package/hooks/api-workflow-check.py +34 -0
  96. package/hooks/auto-answer.py +97 -20
  97. package/{.claude/hooks → hooks}/completion-promise-detector.py +0 -0
  98. package/{.claude/hooks → hooks}/context-capacity-warning.py +0 -0
  99. package/{.claude/hooks → hooks}/docs-update-check.py +0 -0
  100. package/{.claude/hooks → hooks}/enforce-dry-run.py +0 -0
  101. package/hooks/enforce-external-research.py +25 -0
  102. package/hooks/enforce-interview.py +20 -0
  103. package/{.claude/hooks → hooks}/generate-adr-options.py +0 -0
  104. package/{.claude/hooks → hooks}/hook_utils.py +0 -0
  105. package/hooks/ntfy-on-question.py +15 -2
  106. package/hooks/orchestrator-handoff.py +81 -3
  107. package/{.claude/hooks → hooks}/parallel-orchestrator.py +0 -0
  108. package/hooks/periodic-reground.py +40 -0
  109. package/{.claude/hooks → hooks}/remote-question-server.py +0 -0
  110. package/hooks/run-code-review.py +176 -29
  111. package/{.claude/hooks → hooks}/run-visual-qa.py +0 -0
  112. package/hooks/session-logger.py +27 -1
  113. package/hooks/session-startup.py +113 -0
  114. package/{.claude/hooks → hooks}/update-adr-decision.py +0 -0
  115. package/package.json +1 -1
  116. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  117. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  118. package/templates/api-dev-state.json +33 -1
  119. package/templates/brand-page/page.tsx +645 -0
  120. package/templates/component/Component.visual.spec.ts +30 -24
  121. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  122. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  123. package/templates/github-workflows/security.yml +274 -0
  124. package/templates/hustle-build-defaults.json +53 -1
  125. package/templates/page/page.e2e.test.ts +30 -26
  126. package/templates/performance-budgets.json +63 -5
  127. package/templates/registry.json +279 -3
  128. package/templates/review-dashboard/page.tsx +510 -0
  129. package/templates/settings.json +74 -7
  130. package/templates/ui-showcase/_components/UIShowcase.tsx +47 -0
  131. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  132. package/.claude/commands/hustle-combine.md +0 -1089
  133. package/.claude/commands/hustle-ui-create-page.md +0 -1078
  134. package/.claude/commands/hustle-ui-create.md +0 -1058
  135. package/.claude/hooks/auto-answer.py +0 -305
  136. package/.claude/hooks/cache-research.py +0 -337
  137. package/.claude/hooks/check-api-routes.py +0 -168
  138. package/.claude/hooks/check-playwright-setup.py +0 -103
  139. package/.claude/hooks/check-storybook-setup.py +0 -81
  140. package/.claude/hooks/check-update.py +0 -132
  141. package/.claude/hooks/detect-interruption.py +0 -165
  142. package/.claude/hooks/enforce-a11y-audit.py +0 -202
  143. package/.claude/hooks/enforce-brand-guide.py +0 -241
  144. package/.claude/hooks/enforce-component-type-confirm.py +0 -97
  145. package/.claude/hooks/enforce-freshness.py +0 -184
  146. package/.claude/hooks/enforce-page-components.py +0 -186
  147. package/.claude/hooks/enforce-page-data-schema.py +0 -155
  148. package/.claude/hooks/enforce-questions-sourced.py +0 -146
  149. package/.claude/hooks/enforce-schema-from-interview.py +0 -248
  150. package/.claude/hooks/enforce-ui-disambiguation.py +0 -108
  151. package/.claude/hooks/enforce-ui-interview.py +0 -130
  152. package/.claude/hooks/generate-manifest-entry.py +0 -1161
  153. package/.claude/hooks/lib/__init__.py +0 -1
  154. package/.claude/hooks/lib/greptile.py +0 -355
  155. package/.claude/hooks/lib/ntfy.py +0 -209
  156. package/.claude/hooks/notify-input-needed.py +0 -73
  157. package/.claude/hooks/notify-phase-complete.py +0 -90
  158. package/.claude/hooks/ntfy-on-question.py +0 -240
  159. package/.claude/hooks/orchestrator-completion.py +0 -313
  160. package/.claude/hooks/orchestrator-handoff.py +0 -267
  161. package/.claude/hooks/orchestrator-session-startup.py +0 -146
  162. package/.claude/hooks/run-code-review.py +0 -393
  163. package/.claude/hooks/session-logger.py +0 -323
  164. package/.claude/hooks/test-orchestrator-reground.py +0 -248
  165. package/.claude/hooks/track-scope-coverage.py +0 -220
  166. package/.claude/hooks/track-token-usage.py +0 -121
  167. package/.claude/hooks/update-api-showcase.py +0 -161
  168. package/.claude/hooks/update-registry.py +0 -352
  169. package/.claude/hooks/update-ui-showcase.py +0 -224
  170. package/.claude/test-auto-answer-bot.py +0 -183
  171. package/.claude/test-completion-detector.py +0 -263
  172. package/.claude/test-orchestrator-state.json +0 -20
  173. package/.claude/test-orchestrator.sh +0 -271
  174. /package/{.claude/commands → commands}/hustle-build.md +0 -0
  175. /package/{.claude/hooks → hooks}/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  176. /package/{.claude/hooks → hooks}/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  177. /package/{.claude/hooks → hooks}/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  178. /package/{.claude/hooks → hooks}/project-document-prompt.py +0 -0
  179. /package/{.claude/hooks → hooks}/remote-question-proxy.py +0 -0
  180. /package/{.claude/hooks → hooks}/update-testing-checklist.py +0 -0
@@ -4,11 +4,13 @@ import { useState, useMemo } from "react";
4
4
  import { HeroHeader } from "../../shared/HeroHeader";
5
5
  import { PreviewCard } from "./PreviewCard";
6
6
  import { PreviewModal } from "./PreviewModal";
7
+ import { VisualTestingDashboard } from "./VisualTestingDashboard";
7
8
 
8
9
  // Import registry - this will be updated by the CLI when components are created
9
10
  // Note: In production, this could be fetched from an API route
10
11
  import registry from "@/../.claude/registry.json";
11
12
 
13
+ type ViewType = "gallery" | "visual-testing";
12
14
  type FilterType = "all" | "components" | "pages";
13
15
 
14
16
  interface RegistryItem {
@@ -48,6 +50,7 @@ interface Registry {
48
50
  * Created with Hustle API Dev Tools (v3.9.2)
49
51
  */
50
52
  export function UIShowcase() {
53
+ const [view, setView] = useState<ViewType>("gallery");
51
54
  const [filter, setFilter] = useState<FilterType>("all");
52
55
  const [searchQuery, setSearchQuery] = useState("");
53
56
  const [selectedItem, setSelectedItem] = useState<{
@@ -106,8 +109,52 @@ export function UIShowcase() {
106
109
  const componentCount = Object.keys(typedRegistry.components || {}).length;
107
110
  const pageCount = Object.keys(typedRegistry.pages || {}).length;
108
111
 
112
+ // If viewing visual testing dashboard, render that instead
113
+ if (view === "visual-testing") {
114
+ return (
115
+ <div className="min-h-screen bg-white dark:bg-[#050505]">
116
+ {/* View Switcher */}
117
+ <div className="border-b-2 border-black bg-gray-100 dark:border-gray-600 dark:bg-gray-900">
118
+ <div className="container mx-auto flex gap-0 px-4">
119
+ <button
120
+ onClick={() => setView("gallery")}
121
+ className="border-b-4 border-transparent px-6 py-3 font-bold text-gray-600 hover:text-black dark:text-gray-400 dark:hover:text-white"
122
+ >
123
+ Gallery View
124
+ </button>
125
+ <button
126
+ onClick={() => setView("visual-testing")}
127
+ className="border-b-4 border-[#BA0C2F] px-6 py-3 font-bold text-[#BA0C2F]"
128
+ >
129
+ Visual Testing
130
+ </button>
131
+ </div>
132
+ </div>
133
+ <VisualTestingDashboard />
134
+ </div>
135
+ );
136
+ }
137
+
109
138
  return (
110
139
  <div className="min-h-screen bg-white dark:bg-[#050505]">
140
+ {/* View Switcher */}
141
+ <div className="border-b-2 border-black bg-gray-100 dark:border-gray-600 dark:bg-gray-900">
142
+ <div className="container mx-auto flex gap-0 px-4">
143
+ <button
144
+ onClick={() => setView("gallery")}
145
+ className="border-b-4 border-[#BA0C2F] px-6 py-3 font-bold text-[#BA0C2F]"
146
+ >
147
+ Gallery View
148
+ </button>
149
+ <button
150
+ onClick={() => setView("visual-testing")}
151
+ className="border-b-4 border-transparent px-6 py-3 font-bold text-gray-600 hover:text-black dark:text-gray-400 dark:hover:text-white"
152
+ >
153
+ Visual Testing
154
+ </button>
155
+ </div>
156
+ </div>
157
+
111
158
  {/* Hero Header */}
112
159
  <HeroHeader
113
160
  title="UI Showcase"
@@ -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
+ }