@argus-vrt/web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.cta.json +15 -0
  2. package/DEPLOYMENT.md +154 -0
  3. package/Dockerfile +51 -0
  4. package/README.md +159 -0
  5. package/docker-compose.prod.yml +38 -0
  6. package/docker-compose.yml +15 -0
  7. package/drizzle/0000_slim_makkari.sql +61 -0
  8. package/drizzle/meta/0000_snapshot.json +452 -0
  9. package/drizzle/meta/_journal.json +13 -0
  10. package/drizzle.config.ts +10 -0
  11. package/package.json +60 -0
  12. package/public/favicon.ico +0 -0
  13. package/public/logo-argus.svg +8 -0
  14. package/public/logo-variants/logo-argus-a.svg +9 -0
  15. package/public/logo-variants/logo-argus-modern.svg +11 -0
  16. package/public/logo-variants/logo-argus-peacock.svg +8 -0
  17. package/public/logo192.png +0 -0
  18. package/public/logo512.png +0 -0
  19. package/public/manifest.json +25 -0
  20. package/public/robots.txt +3 -0
  21. package/public/tanstack-circle-logo.png +0 -0
  22. package/public/tanstack-word-logo-white.svg +1 -0
  23. package/scripts/backfill-kind.ts +148 -0
  24. package/src/api-plugin.ts +169 -0
  25. package/src/components/image/ImageCompare.tsx +188 -0
  26. package/src/components/story/StoryFlatList.tsx +67 -0
  27. package/src/components/story/StoryGroupedTree.tsx +273 -0
  28. package/src/components/story/StoryTree.tsx +185 -0
  29. package/src/components/ui/Drawer.tsx +110 -0
  30. package/src/components/ui/SearchInput.tsx +95 -0
  31. package/src/components/ui/StatusBadge.tsx +59 -0
  32. package/src/components/ui/ViewModeToggle.tsx +39 -0
  33. package/src/db/index.ts +27 -0
  34. package/src/db/schema.ts +151 -0
  35. package/src/hooks/useDebounce.ts +23 -0
  36. package/src/hooks/useStoryTree.ts +205 -0
  37. package/src/lib/utils.ts +55 -0
  38. package/src/logo.svg +12 -0
  39. package/src/routeTree.gen.ts +177 -0
  40. package/src/router.tsx +17 -0
  41. package/src/routes/__root.tsx +174 -0
  42. package/src/routes/branches/$name.tsx +171 -0
  43. package/src/routes/branches/index.tsx +104 -0
  44. package/src/routes/index.tsx +178 -0
  45. package/src/routes/tests/$id.tsx +417 -0
  46. package/src/routes/tests/index.tsx +128 -0
  47. package/src/routes/upload.tsx +108 -0
  48. package/src/styles.css +213 -0
  49. package/tsconfig.json +28 -0
  50. package/vite.config.ts +30 -0
@@ -0,0 +1,128 @@
1
+ import { createFileRoute, Link } from '@tanstack/react-router'
2
+ import { createServerFn } from '@tanstack/react-start'
3
+ import { GitBranch, Hash } from 'lucide-react'
4
+ import { desc } from 'drizzle-orm'
5
+ import { getDb, tests } from '../../db'
6
+ import { formatRelativeTime } from '../../lib/utils'
7
+ import { StatusBadge } from '../../components/ui/StatusBadge'
8
+
9
+ const getAllTests = createServerFn({ method: 'GET' }).handler(async () => {
10
+ const db = getDb()
11
+ const allTests = await db
12
+ .select()
13
+ .from(tests)
14
+ .orderBy(desc(tests.createdAt))
15
+ .limit(50)
16
+
17
+ return allTests.map((test) => ({
18
+ ...test,
19
+ createdAt: test.createdAt.toISOString(),
20
+ }))
21
+ })
22
+
23
+ export const Route = createFileRoute('/tests/')({
24
+ component: TestsIndex,
25
+ loader: () => getAllTests(),
26
+ })
27
+
28
+ function TestsIndex() {
29
+ const tests = Route.useLoaderData()
30
+
31
+ return (
32
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
33
+ <div className="mb-8">
34
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-white">All Tests</h1>
35
+ <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
36
+ View all visual regression test runs
37
+ </p>
38
+ </div>
39
+
40
+ <div className="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
41
+ <div className="overflow-x-auto">
42
+ <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
43
+ <thead className="bg-gray-50 dark:bg-gray-900">
44
+ <tr>
45
+ <th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
46
+ Branch
47
+ </th>
48
+ <th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider hidden sm:table-cell">
49
+ Commit
50
+ </th>
51
+ <th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
52
+ Status
53
+ </th>
54
+ <th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider hidden md:table-cell">
55
+ Changes
56
+ </th>
57
+ <th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider hidden lg:table-cell">
58
+ Created
59
+ </th>
60
+ <th className="px-4 sm:px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
61
+ Actions
62
+ </th>
63
+ </tr>
64
+ </thead>
65
+ <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
66
+ {tests.map((test) => (
67
+ <tr key={test.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
68
+ <td className="px-4 sm:px-6 py-4 whitespace-nowrap">
69
+ <div className="flex items-center gap-2">
70
+ <GitBranch className="w-4 h-4 text-gray-400 flex-shrink-0" />
71
+ <span className="text-sm font-medium text-gray-900 dark:text-white truncate max-w-[120px] sm:max-w-none">
72
+ {test.branch}
73
+ </span>
74
+ </div>
75
+ <div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
76
+ vs {test.baseBranch}
77
+ </div>
78
+ </td>
79
+ <td className="px-4 sm:px-6 py-4 whitespace-nowrap hidden sm:table-cell">
80
+ <div className="flex items-center gap-2">
81
+ <Hash className="w-4 h-4 text-gray-400" />
82
+ <span className="text-sm font-mono text-gray-600 dark:text-gray-300">
83
+ {test.commitHash}
84
+ </span>
85
+ </div>
86
+ <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 max-w-xs truncate">
87
+ {test.commitMessage}
88
+ </div>
89
+ </td>
90
+ <td className="px-4 sm:px-6 py-4 whitespace-nowrap">
91
+ <StatusBadge status={test.status} />
92
+ </td>
93
+ <td className="px-4 sm:px-6 py-4 whitespace-nowrap text-sm hidden md:table-cell">
94
+ <div className="flex items-center gap-4">
95
+ <span className="text-warning-600 dark:text-warning-400">
96
+ {test.changedCount} changed
97
+ </span>
98
+ <span className="text-success-600 dark:text-success-400">
99
+ {test.passedCount} passed
100
+ </span>
101
+ {test.failedCount > 0 && (
102
+ <span className="text-error-600 dark:text-error-400">
103
+ {test.failedCount} failed
104
+ </span>
105
+ )}
106
+ </div>
107
+ </td>
108
+ <td className="px-4 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 hidden lg:table-cell">
109
+ {formatRelativeTime(test.createdAt)}
110
+ </td>
111
+ <td className="px-4 sm:px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
112
+ <Link
113
+ to="/tests/$id"
114
+ params={{ id: test.id }}
115
+ className="text-primary-600 dark:text-primary-400 hover:text-primary-900 dark:hover:text-primary-300"
116
+ >
117
+ View Details
118
+ </Link>
119
+ </td>
120
+ </tr>
121
+ ))}
122
+ </tbody>
123
+ </table>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ )
128
+ }
@@ -0,0 +1,108 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { createServerFn } from '@tanstack/react-start'
3
+ import { getDb, tests, storyResults } from '../db'
4
+
5
+ interface UploadPayload {
6
+ branch: string
7
+ baseBranch?: string
8
+ commitHash: string
9
+ commitMessage?: string
10
+ stories: Array<{
11
+ storyId: string
12
+ componentName: string
13
+ storyName: string
14
+ baselineUrl?: string
15
+ currentUrl: string
16
+ diffUrl?: string
17
+ pixelDiff: number
18
+ ssimScore: number
19
+ hasDiff: boolean
20
+ isNew: boolean
21
+ }>
22
+ }
23
+
24
+ // Exported server function for uploading test results
25
+ export const uploadTestResults = createServerFn({ method: 'POST' })
26
+ .inputValidator((data: UploadPayload) => data)
27
+ .handler(async ({ data }) => {
28
+ const db = getDb()
29
+
30
+ const {
31
+ branch,
32
+ baseBranch = 'main',
33
+ commitHash,
34
+ commitMessage,
35
+ stories,
36
+ } = data
37
+
38
+ // Calculate counts
39
+ const totalStories = stories.length
40
+ const changedCount = stories.filter((s) => s.hasDiff || s.isNew).length
41
+ const passedCount = stories.filter((s) => !s.hasDiff && !s.isNew).length
42
+ const failedCount = stories.filter((s) => s.hasDiff).length
43
+
44
+ // Create test record
45
+ const [test] = await db
46
+ .insert(tests)
47
+ .values({
48
+ branch,
49
+ baseBranch,
50
+ commitHash,
51
+ commitMessage,
52
+ status: 'PENDING',
53
+ totalStories,
54
+ changedCount,
55
+ passedCount,
56
+ failedCount,
57
+ })
58
+ .returning()
59
+
60
+ // Create story results
61
+ if (stories.length > 0) {
62
+ await db.insert(storyResults).values(
63
+ stories.map((story) => ({
64
+ testId: test.id,
65
+ storyId: story.storyId,
66
+ componentName: story.componentName,
67
+ storyName: story.storyName,
68
+ baselineUrl: story.baselineUrl,
69
+ currentUrl: story.currentUrl,
70
+ diffUrl: story.diffUrl,
71
+ pixelDiff: story.pixelDiff,
72
+ ssimScore: story.ssimScore,
73
+ hasDiff: story.hasDiff,
74
+ isNew: story.isNew,
75
+ }))
76
+ )
77
+ }
78
+
79
+ return {
80
+ success: true,
81
+ testId: test.id,
82
+ url: `/tests/${test.id}`,
83
+ }
84
+ })
85
+
86
+ export const Route = createFileRoute('/upload')({
87
+ component: UploadPage,
88
+ })
89
+
90
+ function UploadPage() {
91
+ return (
92
+ <div className="max-w-2xl mx-auto px-4 py-8">
93
+ <h1 className="text-2xl font-bold text-gray-900 mb-4">Upload API</h1>
94
+ <p className="text-gray-600 mb-4">
95
+ This endpoint accepts test results from the CLI tool.
96
+ </p>
97
+ <div className="bg-gray-100 rounded-lg p-4">
98
+ <h2 className="font-semibold mb-2">Usage</h2>
99
+ <p className="text-sm text-gray-600 mb-2">
100
+ The CLI uses server functions to upload results. Run:
101
+ </p>
102
+ <code className="block bg-gray-800 text-green-400 p-3 rounded text-sm">
103
+ rn-visual-test upload --api-url http://localhost:3000
104
+ </code>
105
+ </div>
106
+ </div>
107
+ )
108
+ }
package/src/styles.css ADDED
@@ -0,0 +1,213 @@
1
+ @import "tailwindcss";
2
+
3
+ @custom-variant dark (&:where(.dark, .dark *));
4
+
5
+ @theme {
6
+ /* Font families */
7
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
8
+ --font-heading: "Space Grotesk", ui-sans-serif, system-ui, sans-serif;
9
+ --font-mono: "JetBrains Mono", ui-monospace, monospace;
10
+
11
+ /* Primary colors - Indigo */
12
+ --color-primary-50: oklch(0.97 0.01 264);
13
+ --color-primary-100: oklch(0.94 0.03 264);
14
+ --color-primary-200: oklch(0.87 0.06 264);
15
+ --color-primary-300: oklch(0.78 0.10 264);
16
+ --color-primary-400: oklch(0.67 0.15 264);
17
+ --color-primary-500: oklch(0.55 0.19 264);
18
+ --color-primary-600: oklch(0.47 0.20 264);
19
+ --color-primary-700: oklch(0.42 0.17 264);
20
+ --color-primary-800: oklch(0.36 0.14 264);
21
+ --color-primary-900: oklch(0.31 0.11 264);
22
+
23
+ /* Success colors - Green */
24
+ --color-success-50: oklch(0.98 0.02 145);
25
+ --color-success-100: oklch(0.94 0.05 145);
26
+ --color-success-200: oklch(0.87 0.10 145);
27
+ --color-success-300: oklch(0.78 0.14 145);
28
+ --color-success-400: oklch(0.68 0.17 145);
29
+ --color-success-500: oklch(0.59 0.18 145);
30
+ --color-success-600: oklch(0.50 0.16 145);
31
+ --color-success-700: oklch(0.43 0.13 145);
32
+ --color-success-800: oklch(0.37 0.10 145);
33
+ --color-success-900: oklch(0.32 0.08 145);
34
+
35
+ /* Warning colors - Yellow/Amber */
36
+ --color-warning-50: oklch(0.98 0.02 85);
37
+ --color-warning-100: oklch(0.95 0.05 85);
38
+ --color-warning-200: oklch(0.90 0.10 85);
39
+ --color-warning-300: oklch(0.84 0.14 85);
40
+ --color-warning-400: oklch(0.78 0.16 85);
41
+ --color-warning-500: oklch(0.72 0.16 85);
42
+ --color-warning-600: oklch(0.62 0.15 85);
43
+ --color-warning-700: oklch(0.52 0.13 85);
44
+ --color-warning-800: oklch(0.44 0.10 85);
45
+ --color-warning-900: oklch(0.38 0.08 85);
46
+
47
+ /* Error colors - Red */
48
+ --color-error-50: oklch(0.97 0.02 25);
49
+ --color-error-100: oklch(0.94 0.05 25);
50
+ --color-error-200: oklch(0.87 0.10 25);
51
+ --color-error-300: oklch(0.78 0.14 25);
52
+ --color-error-400: oklch(0.68 0.17 25);
53
+ --color-error-500: oklch(0.59 0.20 25);
54
+ --color-error-600: oklch(0.50 0.19 25);
55
+ --color-error-700: oklch(0.43 0.16 25);
56
+ --color-error-800: oklch(0.37 0.13 25);
57
+ --color-error-900: oklch(0.32 0.10 25);
58
+
59
+ /* Info colors - Blue */
60
+ --color-info-50: oklch(0.97 0.01 240);
61
+ --color-info-100: oklch(0.94 0.03 240);
62
+ --color-info-200: oklch(0.87 0.07 240);
63
+ --color-info-300: oklch(0.78 0.11 240);
64
+ --color-info-400: oklch(0.67 0.15 240);
65
+ --color-info-500: oklch(0.55 0.18 240);
66
+ --color-info-600: oklch(0.47 0.17 240);
67
+ --color-info-700: oklch(0.42 0.14 240);
68
+ --color-info-800: oklch(0.36 0.11 240);
69
+ --color-info-900: oklch(0.31 0.09 240);
70
+
71
+ /* Surface colors - Light mode defaults */
72
+ --color-surface-bg: oklch(0.98 0.00 0);
73
+ --color-surface-card: oklch(1.00 0.00 0);
74
+ --color-surface-hover: oklch(0.96 0.00 0);
75
+ --color-surface-border: oklch(0.90 0.00 0);
76
+ --color-surface-border-subtle: oklch(0.94 0.00 0);
77
+
78
+ /* Text colors - Light mode defaults */
79
+ --color-text-primary: oklch(0.15 0.00 0);
80
+ --color-text-secondary: oklch(0.40 0.00 0);
81
+ --color-text-muted: oklch(0.55 0.00 0);
82
+ --color-text-inverted: oklch(1.00 0.00 0);
83
+ }
84
+
85
+ /* Dark mode overrides using CSS custom properties */
86
+ :root {
87
+ --bg: var(--color-surface-bg);
88
+ --card: var(--color-surface-card);
89
+ --card-hover: var(--color-surface-hover);
90
+ --border: var(--color-surface-border);
91
+ --border-subtle: var(--color-surface-border-subtle);
92
+ --text: var(--color-text-primary);
93
+ --text-secondary: var(--color-text-secondary);
94
+ --text-muted: var(--color-text-muted);
95
+ }
96
+
97
+ .dark {
98
+ --bg: oklch(0.15 0.00 0);
99
+ --card: oklch(0.20 0.00 0);
100
+ --card-hover: oklch(0.25 0.00 0);
101
+ --border: oklch(0.30 0.00 0);
102
+ --border-subtle: oklch(0.25 0.00 0);
103
+ --text: oklch(0.95 0.00 0);
104
+ --text-secondary: oklch(0.70 0.00 0);
105
+ --text-muted: oklch(0.55 0.00 0);
106
+ }
107
+
108
+ body {
109
+ @apply m-0;
110
+ font-family: var(--font-sans);
111
+ -webkit-font-smoothing: antialiased;
112
+ -moz-osx-font-smoothing: grayscale;
113
+ }
114
+
115
+ code {
116
+ font-family: var(--font-mono);
117
+ }
118
+
119
+ /* Heading font utility */
120
+ .font-heading {
121
+ font-family: var(--font-heading);
122
+ }
123
+
124
+ /* Apply heading font to h1-h3 by default */
125
+ h1, h2, h3 {
126
+ font-family: var(--font-heading);
127
+ }
128
+
129
+ /* Drawer animations */
130
+ @keyframes drawer-slide-in {
131
+ from {
132
+ transform: translateX(-100%);
133
+ }
134
+ to {
135
+ transform: translateX(0);
136
+ }
137
+ }
138
+
139
+ @keyframes drawer-slide-out {
140
+ from {
141
+ transform: translateX(0);
142
+ }
143
+ to {
144
+ transform: translateX(-100%);
145
+ }
146
+ }
147
+
148
+ @keyframes fade-in {
149
+ from {
150
+ opacity: 0;
151
+ }
152
+ to {
153
+ opacity: 1;
154
+ }
155
+ }
156
+
157
+ @keyframes fade-out {
158
+ from {
159
+ opacity: 1;
160
+ }
161
+ to {
162
+ opacity: 0;
163
+ }
164
+ }
165
+
166
+ .drawer-enter {
167
+ animation: drawer-slide-in 0.3s ease-out forwards;
168
+ }
169
+
170
+ .drawer-exit {
171
+ animation: drawer-slide-out 0.3s ease-in forwards;
172
+ }
173
+
174
+ .backdrop-enter {
175
+ animation: fade-in 0.3s ease-out forwards;
176
+ }
177
+
178
+ .backdrop-exit {
179
+ animation: fade-out 0.3s ease-in forwards;
180
+ }
181
+
182
+ /* Custom scrollbar for story list */
183
+ .custom-scrollbar::-webkit-scrollbar {
184
+ width: 6px;
185
+ }
186
+
187
+ .custom-scrollbar::-webkit-scrollbar-track {
188
+ background: transparent;
189
+ }
190
+
191
+ .custom-scrollbar::-webkit-scrollbar-thumb {
192
+ background-color: oklch(0.70 0.00 0);
193
+ border-radius: 3px;
194
+ }
195
+
196
+ .dark .custom-scrollbar::-webkit-scrollbar-thumb {
197
+ background-color: oklch(0.40 0.00 0);
198
+ }
199
+
200
+ /* Tree view expand/collapse animation */
201
+ .tree-content {
202
+ display: grid;
203
+ grid-template-rows: 0fr;
204
+ transition: grid-template-rows 0.2s ease-out;
205
+ }
206
+
207
+ .tree-content.expanded {
208
+ grid-template-rows: 1fr;
209
+ }
210
+
211
+ .tree-content > div {
212
+ overflow: hidden;
213
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "include": ["**/*.ts", "**/*.tsx"],
3
+ "compilerOptions": {
4
+ "target": "ES2022",
5
+ "jsx": "react-jsx",
6
+ "module": "ESNext",
7
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
8
+ "types": ["vite/client"],
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": false,
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "skipLibCheck": true,
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedSideEffectImports": true,
23
+ "baseUrl": ".",
24
+ "paths": {
25
+ "@/*": ["./src/*"]
26
+ }
27
+ }
28
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { defineConfig } from 'vite'
2
+ import { devtools } from '@tanstack/devtools-vite'
3
+ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
4
+ import viteReact from '@vitejs/plugin-react'
5
+ import viteTsConfigPaths from 'vite-tsconfig-paths'
6
+ import { fileURLToPath, URL } from 'url'
7
+
8
+ import tailwindcss from '@tailwindcss/vite'
9
+ import { apiPlugin } from './src/api-plugin'
10
+
11
+ const config = defineConfig({
12
+ resolve: {
13
+ alias: {
14
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
15
+ },
16
+ },
17
+ plugins: [
18
+ apiPlugin(), // API endpoints for CLI
19
+ devtools(),
20
+ // this is the plugin that enables path aliases
21
+ viteTsConfigPaths({
22
+ projects: ['./tsconfig.json'],
23
+ }),
24
+ tailwindcss(),
25
+ tanstackStart(),
26
+ viteReact(),
27
+ ],
28
+ })
29
+
30
+ export default config