@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.
Files changed (159) 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 +7 -463
  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/workflow-logs/None.json +49 -0
  9. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  10. package/.skills/adr-deep-research/SKILL.md +351 -0
  11. package/.skills/api-create/SKILL.md +116 -17
  12. package/.skills/api-research/SKILL.md +130 -0
  13. package/.skills/docs-sync/SKILL.md +260 -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 +786 -0
  17. package/.skills/hustle-build-review/SKILL.md +518 -0
  18. package/.skills/parallel-spawn/SKILL.md +212 -0
  19. package/.skills/ralph-continue/SKILL.md +151 -0
  20. package/.skills/ralph-loop/SKILL.md +341 -0
  21. package/.skills/ralph-status/SKILL.md +87 -0
  22. package/.skills/refactor/SKILL.md +59 -0
  23. package/.skills/shadcn/SKILL.md +522 -0
  24. package/.skills/test-all/SKILL.md +210 -0
  25. package/.skills/test-builds/SKILL.md +208 -0
  26. package/.skills/test-debug/SKILL.md +212 -0
  27. package/.skills/test-e2e/SKILL.md +168 -0
  28. package/.skills/test-review/SKILL.md +707 -0
  29. package/.skills/test-unit/SKILL.md +143 -0
  30. package/.skills/test-visual/SKILL.md +301 -0
  31. package/.skills/token-report/SKILL.md +132 -0
  32. package/CHANGELOG.md +575 -0
  33. package/README.md +426 -56
  34. package/bin/cli.js +1538 -88
  35. package/commands/hustle-api-create.md +22 -0
  36. package/commands/hustle-build.md +259 -0
  37. package/commands/hustle-combine.md +81 -2
  38. package/commands/hustle-ui-create-page.md +84 -2
  39. package/commands/hustle-ui-create.md +82 -2
  40. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  95. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  96. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  97. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  98. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  99. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  100. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  101. package/hooks/api-workflow-check.py +34 -0
  102. package/hooks/auto-answer.py +305 -0
  103. package/hooks/check-update.py +132 -0
  104. package/hooks/completion-promise-detector.py +293 -0
  105. package/hooks/context-capacity-warning.py +171 -0
  106. package/hooks/docs-update-check.py +120 -0
  107. package/hooks/enforce-dry-run.py +134 -0
  108. package/hooks/enforce-external-research.py +25 -0
  109. package/hooks/enforce-interview.py +20 -0
  110. package/hooks/generate-adr-options.py +282 -0
  111. package/hooks/hook_utils.py +609 -0
  112. package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  113. package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  114. package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  115. package/hooks/ntfy-on-question.py +240 -0
  116. package/hooks/orchestrator-completion.py +313 -0
  117. package/hooks/orchestrator-handoff.py +267 -0
  118. package/hooks/orchestrator-session-startup.py +146 -0
  119. package/hooks/parallel-orchestrator.py +451 -0
  120. package/hooks/periodic-reground.py +270 -67
  121. package/hooks/project-document-prompt.py +302 -0
  122. package/hooks/remote-question-proxy.py +284 -0
  123. package/hooks/remote-question-server.py +1224 -0
  124. package/hooks/run-code-review.py +176 -29
  125. package/hooks/run-visual-qa.py +338 -0
  126. package/hooks/session-logger.py +27 -1
  127. package/hooks/session-startup.py +113 -0
  128. package/hooks/update-adr-decision.py +236 -0
  129. package/hooks/update-api-showcase.py +13 -1
  130. package/hooks/update-testing-checklist.py +195 -0
  131. package/hooks/update-ui-showcase.py +13 -1
  132. package/package.json +7 -3
  133. package/scripts/extract-schema-docs.cjs +322 -0
  134. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  135. package/templates/CLAUDE-SECTION.md +89 -64
  136. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  137. package/templates/api-dev-state.json +33 -1
  138. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  139. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  140. package/templates/api-showcase/_components/APITester.tsx +367 -58
  141. package/templates/brand-page/page.tsx +645 -0
  142. package/templates/component/Component.visual.spec.ts +30 -24
  143. package/templates/docs/page.tsx +230 -0
  144. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  145. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  146. package/templates/github-workflows/security.yml +274 -0
  147. package/templates/hustle-build-defaults.json +136 -0
  148. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  149. package/templates/page/page.e2e.test.ts +30 -26
  150. package/templates/performance-budgets.json +63 -5
  151. package/templates/playwright-report/page.tsx +258 -0
  152. package/templates/registry.json +279 -3
  153. package/templates/review-dashboard/page.tsx +510 -0
  154. package/templates/settings.json +155 -7
  155. package/templates/test-results/page.tsx +237 -0
  156. package/templates/typedoc.json +19 -0
  157. package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
  158. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  159. 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
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Metadata } from "next";
2
- import { UIShowcase } from "./UIShowcase";
2
+ import { UIShowcase } from "./_components/UIShowcase";
3
3
 
4
4
  export const metadata: Metadata = {
5
5
  title: "UI Showcase",