create-top-secret-starter 0.9.0 → 0.12.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/backend-overlay/remove.json +2 -2
- package/backend-overlay/web/src/lib/{query-client.test.ts → query-client.test.tsx} +12 -11
- package/backend-overlay/web/src/lib/query-client.ts +3 -3
- package/backend-overlay/web/src/routes/_authenticated/index.tsx +2 -2
- package/backend-overlay/web/src/routes/_authenticated.tsx +13 -9
- package/backend-overlay/web-router/src/routes/sign-in.tsx +3 -3
- package/backend-overlay/web-router/src/routes/sign-up.tsx +3 -3
- package/backend-overlay/web-start/src/routes/sign-in.tsx +3 -3
- package/backend-overlay/web-start/src/routes/sign-up.tsx +3 -3
- package/github-config.js +10 -6
- package/index.js +8 -4
- package/package.json +1 -1
- package/templates/template-router/.claude/skills/project-conventions/SKILL.md +3 -34
- package/templates/template-router/.claude/skills/shadcn/SKILL.md +2 -2
- package/templates/template-router/.claude/skills/shadcn/rules/composition.md +8 -7
- package/templates/template-router/.env.example +0 -2
- package/templates/template-router/.mcp.json +2 -2
- package/templates/template-router/.oxlintrc.json +71 -1
- package/templates/template-router/CLAUDE.md +3 -5
- package/templates/template-router/components.json +1 -2
- package/templates/template-router/e2e/a11y.spec.ts +1 -2
- package/templates/template-router/e2e/auth.spec.ts +2 -8
- package/templates/template-router/e2e/global-setup.ts +1 -5
- package/templates/template-router/e2e/keyboard.spec.ts +7 -11
- package/templates/template-router/e2e/utils.ts +1 -5
- package/templates/template-router/index.html +2 -1
- package/templates/template-router/package.json +5 -8
- package/templates/template-router/src/api/auth/auth-router-sync.test.ts +32 -0
- package/templates/template-router/src/api/auth/auth-router-sync.ts +14 -11
- package/templates/template-router/src/api/auth/index.ts +2 -0
- package/templates/template-router/src/api/index.ts +6 -1
- package/templates/template-router/src/api/notes/index.ts +17 -1
- package/templates/template-router/src/api/refresh.test.ts +11 -0
- package/templates/template-router/src/components/error-screen.tsx +45 -0
- package/templates/template-router/src/components/root-error-boundary.tsx +15 -32
- package/templates/template-router/src/components/theme-toggle.tsx +26 -0
- package/templates/template-router/src/components/ui/textarea.tsx +17 -0
- package/templates/template-router/src/components/ui/toast.tsx +220 -0
- package/templates/template-router/src/env.ts +0 -2
- package/templates/{template-start/src/lib/query-client.test.ts → template-router/src/lib/query-client.test.tsx} +15 -16
- package/templates/template-router/src/lib/query-client.ts +4 -3
- package/templates/template-router/src/lib/storage-store.ts +8 -4
- package/templates/template-router/src/main.tsx +1 -3
- package/templates/template-router/src/mocks/server.ts +14 -8
- package/templates/template-router/src/providers/index.tsx +3 -11
- package/templates/template-router/src/providers/theme-anti-flash.test.ts +1 -1
- package/templates/template-router/src/providers/theme-constants.ts +5 -0
- package/templates/template-router/src/providers/theme.tsx +4 -24
- package/templates/template-router/src/routes/-error.tsx +20 -41
- package/templates/template-router/src/routes/-not-found.tsx +10 -7
- package/templates/template-router/src/routes/-pending.tsx +6 -8
- package/templates/template-router/src/routes/__root.tsx +5 -1
- package/templates/template-router/src/routes/_authenticated/index.tsx +2 -2
- package/templates/template-router/src/routes/_authenticated/notes/$noteId.tsx +2 -2
- package/templates/template-router/src/routes/_authenticated/notes/index.tsx +14 -14
- package/templates/template-router/src/routes/_authenticated.tsx +7 -4
- package/templates/template-router/src/routes/sign-in.tsx +9 -9
- package/templates/template-router/src/test/mock-backend.ts +0 -1
- package/templates/template-router/tsconfig.app.json +2 -2
- package/templates/template-router/tsconfig.json +1 -9
- package/templates/template-router/tsconfig.node.json +4 -3
- package/templates/template-start/.claude/skills/project-conventions/SKILL.md +3 -34
- package/templates/template-start/.claude/skills/shadcn/SKILL.md +2 -2
- package/templates/template-start/.claude/skills/shadcn/rules/composition.md +8 -7
- package/templates/template-start/.env.example +0 -2
- package/templates/template-start/.mcp.json +2 -2
- package/templates/template-start/.oxlintrc.json +71 -1
- package/templates/template-start/CLAUDE.md +3 -5
- package/templates/template-start/components.json +1 -2
- package/templates/template-start/e2e/a11y.spec.ts +1 -2
- package/templates/template-start/e2e/auth.spec.ts +2 -8
- package/templates/template-start/e2e/global-setup.ts +1 -5
- package/templates/template-start/e2e/keyboard.spec.ts +7 -11
- package/templates/template-start/e2e/utils.ts +1 -5
- package/templates/template-start/package.json +5 -8
- package/templates/template-start/src/api/auth/auth-router-sync.test.ts +32 -0
- package/templates/template-start/src/api/auth/auth-router-sync.ts +14 -11
- package/templates/template-start/src/api/auth/index.ts +2 -0
- package/templates/template-start/src/api/index.ts +6 -1
- package/templates/template-start/src/api/notes/index.ts +17 -1
- package/templates/template-start/src/api/refresh.test.ts +11 -0
- package/templates/template-start/src/components/error-screen.tsx +45 -0
- package/templates/template-start/src/components/root-error-boundary.tsx +15 -32
- package/templates/template-start/src/components/theme-toggle.tsx +26 -0
- package/templates/template-start/src/components/ui/textarea.tsx +17 -0
- package/templates/template-start/src/components/ui/toast.tsx +220 -0
- package/templates/template-start/src/env.ts +0 -2
- package/templates/{template-router/src/lib/query-client.test.ts → template-start/src/lib/query-client.test.tsx} +15 -16
- package/templates/template-start/src/lib/query-client.ts +4 -3
- package/templates/template-start/src/mocks/server.ts +14 -8
- package/templates/template-start/src/providers/index.tsx +3 -11
- package/templates/template-start/src/providers/theme-constants.ts +5 -0
- package/templates/template-start/src/providers/theme.tsx +4 -24
- package/templates/template-start/src/router.tsx +2 -4
- package/templates/template-start/src/routes/-error.tsx +20 -41
- package/templates/template-start/src/routes/-not-found.tsx +10 -7
- package/templates/template-start/src/routes/-pending.tsx +6 -8
- package/templates/template-start/src/routes/__root.tsx +5 -3
- package/templates/template-start/src/routes/_authenticated/index.tsx +2 -2
- package/templates/template-start/src/routes/_authenticated/notes/$noteId.tsx +2 -2
- package/templates/template-start/src/routes/_authenticated/notes/index.tsx +14 -14
- package/templates/template-start/src/routes/_authenticated.tsx +7 -4
- package/templates/template-start/src/routes/about.tsx +1 -1
- package/templates/template-start/src/routes/sign-in.tsx +3 -3
- package/templates/template-start/src/test/mock-backend.ts +0 -1
- package/templates/template-start/tsconfig.app.json +2 -2
- package/templates/template-start/tsconfig.json +1 -9
- package/templates/template-start/tsconfig.node.json +4 -3
- package/templates/template-router/src/components/ui/dialog.tsx +0 -136
- package/templates/template-router/src/components/ui/select.tsx +0 -188
- package/templates/template-router/src/components/ui/skeleton.tsx +0 -13
- package/templates/template-router/src/components/ui/sonner.tsx +0 -43
- package/templates/template-router/tsconfig.e2e.json +0 -24
- package/templates/template-start/src/components/ui/dialog.tsx +0 -136
- package/templates/template-start/src/components/ui/select.tsx +0 -188
- package/templates/template-start/src/components/ui/skeleton.tsx +0 -13
- package/templates/template-start/src/components/ui/sonner.tsx +0 -43
- package/templates/template-start/tsconfig.e2e.json +0 -24
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"src/api/auth/session-store.ts",
|
|
8
8
|
"src/api/auth/session-store.test.ts",
|
|
9
9
|
"src/api/auth/auth-router-sync.ts",
|
|
10
|
+
"src/api/auth/auth-router-sync.test.ts",
|
|
10
11
|
"src/test/mock-backend.ts",
|
|
11
12
|
"src/api/auth/auth.test.ts",
|
|
12
13
|
"src/api/refresh.test.ts",
|
|
13
14
|
"src/lib/single-flight.ts",
|
|
14
15
|
"src/lib/single-flight.test.ts",
|
|
15
16
|
"e2e",
|
|
16
|
-
"playwright.config.ts"
|
|
17
|
-
"tsconfig.e2e.json"
|
|
17
|
+
"playwright.config.ts"
|
|
18
18
|
]
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { EdenFetchError } from '@elysia/eden'
|
|
2
|
+
import { act, render, screen } from '@testing-library/react'
|
|
2
3
|
import { beforeEach, expect, it, vi } from 'vitest'
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { Toaster } from '@/components/ui/toast'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
const { toast } = await import('sonner')
|
|
7
|
+
import { queryClient } from '@/lib/query-client'
|
|
8
8
|
|
|
9
9
|
const httpError = (status: number) => new EdenFetchError(status, { message: `HTTP ${status}` })
|
|
10
10
|
|
|
11
11
|
beforeEach(() => {
|
|
12
|
-
vi.mocked(toast.error).mockClear()
|
|
13
12
|
queryClient.clear()
|
|
13
|
+
render(<Toaster />)
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const flushToasts = () => act(() => new Promise(resolve => setTimeout(resolve)))
|
|
17
|
+
|
|
18
|
+
const toasts = () => screen.queryAllByRole('dialog')
|
|
18
19
|
|
|
19
20
|
it('stays silent when a query fails with no data on screen', async () => {
|
|
20
21
|
await queryClient
|
|
@@ -27,7 +28,7 @@ it('stays silent when a query fails with no data on screen', async () => {
|
|
|
27
28
|
await flushToasts()
|
|
28
29
|
|
|
29
30
|
// the route's error component already tells the user; a toast would double up
|
|
30
|
-
expect(
|
|
31
|
+
expect(toasts()).toHaveLength(0)
|
|
31
32
|
})
|
|
32
33
|
|
|
33
34
|
it('toasts when a background refetch fails over data already shown', async () => {
|
|
@@ -43,8 +44,8 @@ it('toasts when a background refetch fails over data already shown', async () =>
|
|
|
43
44
|
.catch(() => {})
|
|
44
45
|
await flushToasts()
|
|
45
46
|
|
|
46
|
-
expect(
|
|
47
|
-
expect(
|
|
47
|
+
expect(toasts()).toHaveLength(1)
|
|
48
|
+
expect(toasts()[0]).toHaveTextContent('500')
|
|
48
49
|
})
|
|
49
50
|
|
|
50
51
|
it('always toasts a failed mutation', async () => {
|
|
@@ -55,8 +56,8 @@ it('always toasts a failed mutation', async () => {
|
|
|
55
56
|
.catch(() => {})
|
|
56
57
|
await flushToasts()
|
|
57
58
|
|
|
58
|
-
expect(
|
|
59
|
-
expect(
|
|
59
|
+
expect(toasts()).toHaveLength(1)
|
|
60
|
+
expect(toasts()[0]).toHaveTextContent('nope')
|
|
60
61
|
})
|
|
61
62
|
|
|
62
63
|
it('does not retry a 4xx query', async () => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { EdenFetchError } from '@elysia/eden'
|
|
2
2
|
import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query'
|
|
3
3
|
|
|
4
|
+
import { toast } from '@/components/ui/toast'
|
|
5
|
+
|
|
4
6
|
const isClientError = (error: unknown) =>
|
|
5
7
|
error instanceof EdenFetchError && error.status >= 400 && error.status < 500
|
|
6
8
|
|
|
@@ -13,10 +15,8 @@ const getErrorMessage = (error: unknown) => {
|
|
|
13
15
|
return error instanceof Error ? error.message : 'Something went wrong'
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
// Load toast code only when an error needs notification.
|
|
17
18
|
const notifyError = (error: unknown) => {
|
|
18
|
-
|
|
19
|
-
void import('sonner').then(({ toast }) => toast.error(message))
|
|
19
|
+
toast.add({ type: 'error', title: getErrorMessage(error) })
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export const queryClient = new QueryClient({
|
|
@@ -26,7 +26,7 @@ function NotesPage() {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
|
-
<
|
|
29
|
+
<main className='w-full max-w-md space-y-4'>
|
|
30
30
|
<h1 className='text-2xl font-semibold'>PLACEHOLDER</h1>
|
|
31
31
|
|
|
32
32
|
<form onSubmit={onSubmit} className='flex gap-2'>
|
|
@@ -60,6 +60,6 @@ function NotesPage() {
|
|
|
60
60
|
</li>
|
|
61
61
|
))}
|
|
62
62
|
</ul>
|
|
63
|
-
</
|
|
63
|
+
</main>
|
|
64
64
|
)
|
|
65
65
|
}
|
|
@@ -3,6 +3,7 @@ import { LogOut } from 'lucide-react'
|
|
|
3
3
|
|
|
4
4
|
import { useSession, useSignOut } from '@/api/auth'
|
|
5
5
|
import { requireSession } from '@/api/auth/guards'
|
|
6
|
+
import { ThemeToggle } from '@/components/theme-toggle'
|
|
6
7
|
import { Button } from '@/components/ui/button'
|
|
7
8
|
|
|
8
9
|
export const Route = createFileRoute('/_authenticated')({
|
|
@@ -21,17 +22,20 @@ function AuthenticatedLayout() {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
return (
|
|
24
|
-
|
|
25
|
+
<>
|
|
25
26
|
<header className='flex h-14 items-center justify-between border-b px-6'>
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
<div className='flex items-center gap-4'>
|
|
28
|
+
<span className='text-muted-foreground text-sm'>{session?.user.email}</span>
|
|
29
|
+
<ThemeToggle />
|
|
30
|
+
<Button variant='outline' size='sm' onClick={onSignOut} disabled={signOut.isPending}>
|
|
31
|
+
<LogOut data-icon='inline-start' />
|
|
32
|
+
Sign out
|
|
33
|
+
</Button>
|
|
34
|
+
</div>
|
|
31
35
|
</header>
|
|
32
|
-
<
|
|
36
|
+
<div className='flex flex-1 items-center justify-center p-6'>
|
|
33
37
|
<Outlet />
|
|
34
|
-
</
|
|
35
|
-
|
|
38
|
+
</div>
|
|
39
|
+
</>
|
|
36
40
|
)
|
|
37
41
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
2
|
import { Link, createFileRoute, useRouter } from '@tanstack/react-router'
|
|
3
3
|
import { useForm } from 'react-hook-form'
|
|
4
4
|
import * as z from 'zod/mini'
|
|
@@ -37,7 +37,7 @@ function SignInPage() {
|
|
|
37
37
|
const signIn = useSignIn()
|
|
38
38
|
|
|
39
39
|
const { register, handleSubmit, formState } = useForm({
|
|
40
|
-
resolver:
|
|
40
|
+
resolver: zodResolver(SignInFormSchema),
|
|
41
41
|
defaultValues: { email: '', password: '' }
|
|
42
42
|
})
|
|
43
43
|
|
|
@@ -47,7 +47,7 @@ function SignInPage() {
|
|
|
47
47
|
})
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
|
-
<main className='flex
|
|
50
|
+
<main className='flex flex-1 items-center justify-center p-6'>
|
|
51
51
|
<form onSubmit={onSubmit} className='w-full max-w-sm'>
|
|
52
52
|
<FieldGroup>
|
|
53
53
|
<h1 className='text-center text-xl font-semibold'>Sign in</h1>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
2
|
import { Link, createFileRoute, useRouter } from '@tanstack/react-router'
|
|
3
3
|
import { useForm } from 'react-hook-form'
|
|
4
4
|
import * as z from 'zod/mini'
|
|
@@ -40,7 +40,7 @@ function SignUpPage() {
|
|
|
40
40
|
const signUp = useSignUp()
|
|
41
41
|
|
|
42
42
|
const { register, handleSubmit, formState } = useForm({
|
|
43
|
-
resolver:
|
|
43
|
+
resolver: zodResolver(SignUpFormSchema),
|
|
44
44
|
defaultValues: { name: '', email: '', password: '' }
|
|
45
45
|
})
|
|
46
46
|
|
|
@@ -50,7 +50,7 @@ function SignUpPage() {
|
|
|
50
50
|
})
|
|
51
51
|
|
|
52
52
|
return (
|
|
53
|
-
<main className='flex
|
|
53
|
+
<main className='flex flex-1 items-center justify-center p-6'>
|
|
54
54
|
<form onSubmit={onSubmit} className='w-full max-w-sm'>
|
|
55
55
|
<FieldGroup>
|
|
56
56
|
<h1 className='text-center text-xl font-semibold'>Create an account</h1>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
2
|
import { Link, createFileRoute, useRouter } from '@tanstack/react-router'
|
|
3
3
|
import { useForm } from 'react-hook-form'
|
|
4
4
|
import * as z from 'zod/mini'
|
|
@@ -41,7 +41,7 @@ function SignInPage() {
|
|
|
41
41
|
const hydrated = useHydrated()
|
|
42
42
|
|
|
43
43
|
const { register, handleSubmit, formState } = useForm({
|
|
44
|
-
resolver:
|
|
44
|
+
resolver: zodResolver(SignInFormSchema),
|
|
45
45
|
defaultValues: { email: '', password: '' }
|
|
46
46
|
})
|
|
47
47
|
|
|
@@ -51,7 +51,7 @@ function SignInPage() {
|
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
return (
|
|
54
|
-
<main className='flex
|
|
54
|
+
<main className='flex flex-1 items-center justify-center p-6'>
|
|
55
55
|
<form onSubmit={onSubmit} className='w-full max-w-sm'>
|
|
56
56
|
<FieldGroup>
|
|
57
57
|
<h1 className='text-center text-xl font-semibold'>Sign in</h1>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
2
|
import { Link, createFileRoute, useRouter } from '@tanstack/react-router'
|
|
3
3
|
import { useForm } from 'react-hook-form'
|
|
4
4
|
import * as z from 'zod/mini'
|
|
@@ -44,7 +44,7 @@ function SignUpPage() {
|
|
|
44
44
|
const hydrated = useHydrated()
|
|
45
45
|
|
|
46
46
|
const { register, handleSubmit, formState } = useForm({
|
|
47
|
-
resolver:
|
|
47
|
+
resolver: zodResolver(SignUpFormSchema),
|
|
48
48
|
defaultValues: { name: '', email: '', password: '' }
|
|
49
49
|
})
|
|
50
50
|
|
|
@@ -54,7 +54,7 @@ function SignUpPage() {
|
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
|
-
<main className='flex
|
|
57
|
+
<main className='flex flex-1 items-center justify-center p-6'>
|
|
58
58
|
<form onSubmit={onSubmit} className='w-full max-w-sm'>
|
|
59
59
|
<FieldGroup>
|
|
60
60
|
<h1 className='text-center text-xl font-semibold'>Create an account</h1>
|
package/github-config.js
CHANGED
|
@@ -73,8 +73,18 @@ ${
|
|
|
73
73
|
${setup}
|
|
74
74
|
- name: Install dependencies
|
|
75
75
|
run: ${install}
|
|
76
|
+
- name: Cache Playwright browsers
|
|
77
|
+
uses: actions/cache@v6
|
|
78
|
+
id: playwright-cache
|
|
79
|
+
with:
|
|
80
|
+
path: ~/.cache/ms-playwright
|
|
81
|
+
key: playwright-chromium-webkit-\${{ runner.os }}-\${{ hashFiles('${lockfile}') }}
|
|
76
82
|
- name: Install browsers
|
|
77
83
|
run: ${exec} playwright install --with-deps chromium webkit
|
|
84
|
+
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
85
|
+
- name: Install browser system deps
|
|
86
|
+
run: ${exec} playwright install-deps chromium webkit
|
|
87
|
+
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
|
78
88
|
- name: End-to-end tests
|
|
79
89
|
run: ${pm} run test:e2e
|
|
80
90
|
`
|
|
@@ -93,12 +103,6 @@ updates:
|
|
|
93
103
|
groups:
|
|
94
104
|
minor-and-patch:
|
|
95
105
|
update-types: [minor, patch]
|
|
96
|
-
# vite is pinned: 8.2+ collapses router code splitting and grows the entry chunk
|
|
97
|
-
ignore:
|
|
98
|
-
- dependency-name: vite
|
|
99
|
-
update-types:
|
|
100
|
-
- version-update:semver-major
|
|
101
|
-
- version-update:semver-minor
|
|
102
106
|
- package-ecosystem: github-actions
|
|
103
107
|
directory: /
|
|
104
108
|
schedule:
|
package/index.js
CHANGED
|
@@ -317,10 +317,14 @@ const applyBackendOverlay = () => {
|
|
|
317
317
|
delete pkg.trustedDependencies
|
|
318
318
|
})
|
|
319
319
|
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
320
|
+
// the e2e suite is gone with the mock backend
|
|
321
|
+
const tsconfigNode = path.join(webDir, 'tsconfig.node.json')
|
|
322
|
+
fs.writeFileSync(
|
|
323
|
+
tsconfigNode,
|
|
324
|
+
fs
|
|
325
|
+
.readFileSync(tsconfigNode, 'utf8')
|
|
326
|
+
.replace(/"include": \[[^\]]*\]/, '"include": ["vite.config.ts"]')
|
|
327
|
+
)
|
|
324
328
|
|
|
325
329
|
const apiDir = path.join(targetDir, 'apps', 'api')
|
|
326
330
|
fs.cpSync(path.join(cliRoot, 'templates', 'template-api'), apiDir, { recursive: true })
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ description: How to add a feature to this codebase — API modules, forms, route
|
|
|
5
5
|
|
|
6
6
|
# Project conventions
|
|
7
7
|
|
|
8
|
-
Loaded on demand. Invariants here are **enforced** by `
|
|
8
|
+
Loaded on demand. Invariants here are **enforced** by oxlint `no-restricted-*` overrides;
|
|
9
9
|
the rest is the shape to follow.
|
|
10
10
|
|
|
11
11
|
## New API module
|
|
@@ -64,7 +64,7 @@ on mount and therefore also listens for `storage`.
|
|
|
64
64
|
|
|
65
65
|
## Forms
|
|
66
66
|
|
|
67
|
-
react-hook-form + `
|
|
67
|
+
react-hook-form + `zodResolver` (zod/mini schemas), with shadcn `Field*`
|
|
68
68
|
primitives. `routes/sign-in.tsx` is the reference implementation;
|
|
69
69
|
`routes/_authenticated/notes/index.tsx` shows a form that feeds a mutation and resets.
|
|
70
70
|
|
|
@@ -115,37 +115,6 @@ Put error-handling changes there, not in individual hooks.
|
|
|
115
115
|
|
|
116
116
|
On (`babel-plugin-react-compiler`). No manual `useMemo` / `useCallback` — enforced.
|
|
117
117
|
|
|
118
|
-
## Backend variant (`apps/api`, `create-cli/backend-overlay`)
|
|
119
|
-
|
|
120
|
-
The rules above describe the SPA-only variant. When the backend is on:
|
|
121
|
-
|
|
122
|
-
- HTTP goes through Eden (`treaty<App>` in `src/api/index.ts`), types come from
|
|
123
|
-
`apps/api/src/app.ts` — no `schema.ts`, no ky.
|
|
124
|
-
- Auth is Better Auth cookie sessions via `better-auth/react`. No `session-store`,
|
|
125
|
-
no refresh, no `auth-router-sync`; guards `await authClient.getSession()` in
|
|
126
|
-
`beforeLoad` and mutations call `router.invalidate()` on success.
|
|
127
|
-
- New server resource: table in `apps/api/src/db/schema.ts` → `db:generate` → a
|
|
128
|
-
feature folder `apps/api/src/modules/<feature>/` → React Query hooks over
|
|
129
|
-
`api.<resource>` (`backend-overlay/web/src/api/notes`). `modules/notes/` is the
|
|
130
|
-
reference; it follows Elysia's feature-based layout:
|
|
131
|
-
|
|
132
|
-
| File | Holds |
|
|
133
|
-
| ------------ | --------------------------------------------------------------------- |
|
|
134
|
-
| `model.ts` | `t` schemas (body, params, response) + types via `UnwrapSchema` |
|
|
135
|
-
| `service.ts` | `abstract class` with static methods; takes `db`/ids, never `Context` |
|
|
136
|
-
| `index.ts` | the Elysia controller — routing, `{ auth: true }`, `response` schemas |
|
|
137
|
-
|
|
138
|
-
One Elysia instance is one controller: don't pass `Context` into a class, and don't
|
|
139
|
-
type handlers against it. `modules/auth/service.ts` is the exception the docs allow —
|
|
140
|
-
a request-dependent service, so it is an Elysia instance with a `macro` and a `name`.
|
|
141
|
-
|
|
142
|
-
- `scripts/check-conventions.mjs` scans this repo's `src/` and `e2e/` only, so neither
|
|
143
|
-
`apps/api/` nor the overlay is covered — including the rule that every
|
|
144
|
-
`src/api/<feature>/` has a `schema.ts`, which the Eden variant deliberately drops.
|
|
145
|
-
Generated projects don't ship the script at all. Review those by hand.
|
|
146
|
-
|
|
147
118
|
## Mock backend
|
|
148
119
|
|
|
149
|
-
`src/mocks/server.ts` patches global `fetch` for `/api
|
|
150
|
-
`VITE_ENABLE_MOCKS`. Seed sign-in `demo@example.com` / `demo1234`. `bun run graduate <name>`
|
|
151
|
-
removes it along with the demo account (see root `CLAUDE.md`, Starter state).
|
|
120
|
+
`src/mocks/server.ts` patches global `fetch` for `/api/*` in dev builds only. Seed sign-in `demo@example.com` / `demo1234`. Drop it: delete `src/mocks/`, `src/test/mock-backend.ts` and the DEV branch that installs it.
|
|
@@ -64,7 +64,7 @@ These rules are **always enforced**. Each links to a file with Incorrect/Correct
|
|
|
64
64
|
- **Use existing components before custom markup.** Check if a component exists before writing a styled `div`.
|
|
65
65
|
- **Callouts use `Alert`.** Don't build custom styled divs.
|
|
66
66
|
- **Empty states use `Empty`.** Don't build custom empty state markup.
|
|
67
|
-
- **Toast via `
|
|
67
|
+
- **Toast via `toast.add()`** from `@/components/ui/toast` (Base UI).
|
|
68
68
|
- **Use `Separator`** instead of `<hr>` or `<div className="border-t">`.
|
|
69
69
|
- **Use `Skeleton`** for loading placeholders. No custom `animate-pulse` divs.
|
|
70
70
|
- **Use `Badge`** instead of custom styled spans.
|
|
@@ -135,7 +135,7 @@ These are the most common patterns that differentiate correct shadcn/ui code. Fo
|
|
|
135
135
|
| Data display | `Table`, `Card`, `Badge`, `Avatar` |
|
|
136
136
|
| Navigation | `Sidebar`, `NavigationMenu`, `Breadcrumb`, `Tabs`, `Pagination` |
|
|
137
137
|
| Overlays | `Dialog` (modal), `Sheet` (side panel), `Drawer` (bottom sheet), `AlertDialog` (confirmation) |
|
|
138
|
-
| Feedback | `
|
|
138
|
+
| Feedback | `Toast`, `Alert` , `Progress`, `Skeleton`, `Spinner` |
|
|
139
139
|
| Command palette | `Command` inside `Dialog` |
|
|
140
140
|
| Charts | `Chart` (wraps Recharts) |
|
|
141
141
|
| Layout | `Card`, `Separator`, `Resizable`, `ScrollArea`, `Accordion`, `Collapsible` |
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
- Items always inside their Group component
|
|
6
6
|
- Callouts use Alert
|
|
7
7
|
- Empty states use Empty component
|
|
8
|
-
- Toast notifications use
|
|
8
|
+
- Toast notifications use the Base UI toast manager
|
|
9
9
|
- Choosing between overlay components
|
|
10
10
|
- Dialog, Sheet, and Drawer always need a Title
|
|
11
11
|
- Card structure
|
|
@@ -90,15 +90,16 @@ Chat components nest in a fixed order (`MessageScrollerProvider` → `MessageScr
|
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
93
|
-
## Toast notifications use
|
|
93
|
+
## Toast notifications use the Base UI toast manager
|
|
94
94
|
|
|
95
95
|
```tsx
|
|
96
|
-
import { toast } from '
|
|
96
|
+
import { toast } from '@/components/ui/toast'
|
|
97
97
|
|
|
98
|
-
toast.
|
|
99
|
-
toast.
|
|
100
|
-
toast(
|
|
101
|
-
|
|
98
|
+
toast.add({ type: 'success', title: 'Changes saved.' })
|
|
99
|
+
toast.add({ type: 'error', title: 'Something went wrong.' })
|
|
100
|
+
toast.add({
|
|
101
|
+
title: 'File deleted.',
|
|
102
|
+
actionProps: { children: 'Undo', onClick: () => undoDelete() }
|
|
102
103
|
})
|
|
103
104
|
```
|
|
104
105
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"playwright": {
|
|
4
4
|
"command": "bunx",
|
|
5
|
-
"args": ["@playwright/mcp@
|
|
5
|
+
"args": ["@playwright/mcp@0.0.80"]
|
|
6
6
|
},
|
|
7
7
|
"context7": {
|
|
8
8
|
"command": "bunx",
|
|
9
|
-
"args": ["@upstash/context7-mcp"]
|
|
9
|
+
"args": ["@upstash/context7-mcp@4.0.7"]
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -51,5 +51,75 @@
|
|
|
51
51
|
],
|
|
52
52
|
"vitest/require-mock-type-parameters": "off",
|
|
53
53
|
"vitest/require-to-throw-message": "off"
|
|
54
|
-
}
|
|
54
|
+
},
|
|
55
|
+
"overrides": [
|
|
56
|
+
{
|
|
57
|
+
"files": ["src/**/*.{ts,tsx}"],
|
|
58
|
+
"rules": {
|
|
59
|
+
"no-restricted-globals": [
|
|
60
|
+
"error",
|
|
61
|
+
{
|
|
62
|
+
"name": "fetch",
|
|
63
|
+
"message": "Use the `api` ky instance from src/api: it owns the bearer header and the single-flight token refresh."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "localStorage",
|
|
67
|
+
"message": "Persisted state goes through createStorageStore (src/lib/storage-store.ts): it owns decoding and cross-tab sync."
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"no-restricted-imports": [
|
|
71
|
+
"error",
|
|
72
|
+
{
|
|
73
|
+
"paths": [
|
|
74
|
+
{
|
|
75
|
+
"name": "zod",
|
|
76
|
+
"message": "Import from 'zod/mini'. Mixing the two ships both builds (+15.8 kB gzip). For forms use zodResolver (typed against zod/v4/core, works with mini)."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "react",
|
|
80
|
+
"importNames": ["useMemo", "useCallback"],
|
|
81
|
+
"message": "React Compiler is on: manual memoization is noise."
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"files": ["src/mocks/**"],
|
|
90
|
+
"rules": {
|
|
91
|
+
"no-restricted-globals": [
|
|
92
|
+
"error",
|
|
93
|
+
{
|
|
94
|
+
"name": "localStorage",
|
|
95
|
+
"message": "Persisted state goes through createStorageStore (src/lib/storage-store.ts): it owns decoding and cross-tab sync."
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"files": ["src/lib/storage-store.ts", "src/**/*.test.{ts,tsx}"],
|
|
102
|
+
"rules": {
|
|
103
|
+
"no-restricted-globals": [
|
|
104
|
+
"error",
|
|
105
|
+
{
|
|
106
|
+
"name": "fetch",
|
|
107
|
+
"message": "Use the `api` ky instance from src/api: it owns the bearer header and the single-flight token refresh."
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"files": ["src/api/**/*.test.ts"],
|
|
114
|
+
"rules": {
|
|
115
|
+
"no-restricted-globals": "off"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"files": ["src/components/ui/**"],
|
|
120
|
+
"rules": {
|
|
121
|
+
"no-restricted-imports": "off"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
]
|
|
55
125
|
}
|
|
@@ -21,7 +21,7 @@ Comments are a last resort, for genuinely non-obvious cases. Keep them to one sh
|
|
|
21
21
|
|
|
22
22
|
Don't write tests that merely restate the implementation — zero confidence value.
|
|
23
23
|
|
|
24
|
-
- Mock only at system boundaries (HTTP via the mock server or `fetch`,
|
|
24
|
+
- Mock only at system boundaries (HTTP via the mock server or `fetch`, time, storage events). Never mock our own modules.
|
|
25
25
|
- Verify through the public interface, not a side channel: reload the store and call `get()`, don't read `localStorage` directly. A test that breaks on a behaviour-preserving refactor is wrong.
|
|
26
26
|
- Call counts are not a result — unless the count _is_ the contract (one refresh per burst of 401s, retry limit).
|
|
27
27
|
- Expected values are independent literals, never recomputed the way the code does it.
|
|
@@ -48,14 +48,12 @@ For how to add an API module, a form, a route or a UI primitive, use the **proje
|
|
|
48
48
|
|
|
49
49
|
Persisted state goes through `createStorageStore` in `src/lib/storage-store.ts` — the only module allowed to touch `localStorage`. It owns decoding and cross-tab sync.
|
|
50
50
|
|
|
51
|
-
Dev-only code must put `import.meta.env.DEV` **first** in the condition so Vite drops the branch at build time.
|
|
51
|
+
Dev-only code must put `import.meta.env.DEV` **first** in the condition so Vite drops the branch at build time.
|
|
52
52
|
|
|
53
53
|
<important if="you are adding, removing, or upgrading a dependency">
|
|
54
54
|
|
|
55
55
|
## Dependencies
|
|
56
56
|
|
|
57
|
-
`dependencies` holds only what
|
|
58
|
-
|
|
59
|
-
`vite` is **pinned to 8.1.x** (8.1.5). Every 8.2.x and 8.3.0-beta.0 collapse router `autoCodeSplitting` from 13 chunks to 10 and grow the entry from 81 to 115 kB gzip (measured on 8.2.2). Re-test before unpinning.
|
|
57
|
+
`dependencies` holds only what's imported as JS from `src/` (runtime); CSS-only imports and other build inputs stay dev deps. CLIs and build plugins are dev deps so `bun audit --prod` stays readable. `trustedDependencies: []` — don't add entries without checking what the script does.
|
|
60
58
|
|
|
61
59
|
</important>
|
|
@@ -3,8 +3,7 @@ import { expect, test, type Page } from '@playwright/test'
|
|
|
3
3
|
|
|
4
4
|
import { ready, signIn } from './utils'
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
// known machine-detectable violations", not as "accessible".
|
|
6
|
+
// axe catches roughly a third of WCAG failures; a pass is not "accessible"
|
|
8
7
|
const scan = (page: Page) =>
|
|
9
8
|
new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze()
|
|
10
9
|
|
|
@@ -2,10 +2,6 @@ import { expect, test } from '@playwright/test'
|
|
|
2
2
|
|
|
3
3
|
import { ready, signIn } from './utils'
|
|
4
4
|
|
|
5
|
-
// Playwright gives each test its own context, so localStorage does not leak
|
|
6
|
-
// between them. That matters here: the session lives in localStorage, and a
|
|
7
|
-
// shared context would make a signed-out test pass for the wrong reason.
|
|
8
|
-
|
|
9
5
|
test('a guest is redirected away from a protected route', async ({ page }) => {
|
|
10
6
|
await ready(page, '/')
|
|
11
7
|
await expect(page).toHaveURL(/\/sign-in/)
|
|
@@ -71,13 +67,11 @@ test('a theme change in one tab reaches the other', async ({ context }) => {
|
|
|
71
67
|
|
|
72
68
|
test('preloading a protected route on hover does not navigate a guest', async ({ page }) => {
|
|
73
69
|
await ready(page, '/does-not-exist')
|
|
74
|
-
//
|
|
75
|
-
// role="button" on an anchor is valid ARIA
|
|
70
|
+
// role query on purpose: axe accepts role="button" on an anchor
|
|
76
71
|
const backHome = page.getByRole('link', { name: 'Back home' })
|
|
77
72
|
await expect(backHome).toBeVisible()
|
|
78
73
|
|
|
79
|
-
//
|
|
80
|
-
// abort the preload, not move the user
|
|
74
|
+
// hover preloads beforeLoad; its redirect must abort the preload, not navigate
|
|
81
75
|
await backHome.hover()
|
|
82
76
|
await page.waitForTimeout(500)
|
|
83
77
|
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type { FullConfig } from '@playwright/test'
|
|
2
2
|
|
|
3
|
-
// `
|
|
4
|
-
// port, so a dev server from another project would silently serve the whole suite
|
|
5
|
-
// (the failures then look like missing UI). Vite serves `/@fs` only for paths
|
|
6
|
-
// inside its own root, so a 200 here proves the server was started from this
|
|
7
|
-
// checkout — a foreign server answers 403.
|
|
3
|
+
// Vite serves `/@fs` only under its own root: 200 proves the reused server is this checkout
|
|
8
4
|
const identityProbe = async (baseURL: string) => {
|
|
9
5
|
const url = `${baseURL}/@fs${process.cwd()}/package.json`
|
|
10
6
|
try {
|