@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.
- package/.cta.json +15 -0
- package/DEPLOYMENT.md +154 -0
- package/Dockerfile +51 -0
- package/README.md +159 -0
- package/docker-compose.prod.yml +38 -0
- package/docker-compose.yml +15 -0
- package/drizzle/0000_slim_makkari.sql +61 -0
- package/drizzle/meta/0000_snapshot.json +452 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/package.json +60 -0
- package/public/favicon.ico +0 -0
- package/public/logo-argus.svg +8 -0
- package/public/logo-variants/logo-argus-a.svg +9 -0
- package/public/logo-variants/logo-argus-modern.svg +11 -0
- package/public/logo-variants/logo-argus-peacock.svg +8 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/robots.txt +3 -0
- package/public/tanstack-circle-logo.png +0 -0
- package/public/tanstack-word-logo-white.svg +1 -0
- package/scripts/backfill-kind.ts +148 -0
- package/src/api-plugin.ts +169 -0
- package/src/components/image/ImageCompare.tsx +188 -0
- package/src/components/story/StoryFlatList.tsx +67 -0
- package/src/components/story/StoryGroupedTree.tsx +273 -0
- package/src/components/story/StoryTree.tsx +185 -0
- package/src/components/ui/Drawer.tsx +110 -0
- package/src/components/ui/SearchInput.tsx +95 -0
- package/src/components/ui/StatusBadge.tsx +59 -0
- package/src/components/ui/ViewModeToggle.tsx +39 -0
- package/src/db/index.ts +27 -0
- package/src/db/schema.ts +151 -0
- package/src/hooks/useDebounce.ts +23 -0
- package/src/hooks/useStoryTree.ts +205 -0
- package/src/lib/utils.ts +55 -0
- package/src/logo.svg +12 -0
- package/src/routeTree.gen.ts +177 -0
- package/src/router.tsx +17 -0
- package/src/routes/__root.tsx +174 -0
- package/src/routes/branches/$name.tsx +171 -0
- package/src/routes/branches/index.tsx +104 -0
- package/src/routes/index.tsx +178 -0
- package/src/routes/tests/$id.tsx +417 -0
- package/src/routes/tests/index.tsx +128 -0
- package/src/routes/upload.tsx +108 -0
- package/src/styles.css +213 -0
- package/tsconfig.json +28 -0
- 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
|