create-top-secret-starter 0.8.3 → 0.11.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 (132) hide show
  1. package/backend-overlay/remove.json +5 -2
  2. package/backend-overlay/web/src/lib/{query-client.test.ts → query-client.test.tsx} +12 -11
  3. package/backend-overlay/web/src/lib/query-client.ts +3 -3
  4. package/backend-overlay/web/src/routes/_authenticated/index.tsx +2 -2
  5. package/backend-overlay/web/src/routes/_authenticated.tsx +13 -9
  6. package/backend-overlay/web-router/src/routes/sign-in.tsx +1 -1
  7. package/backend-overlay/web-router/src/routes/sign-up.tsx +1 -1
  8. package/backend-overlay/web-start/src/routes/sign-in.tsx +1 -1
  9. package/backend-overlay/web-start/src/routes/sign-up.tsx +1 -1
  10. package/github-config.js +10 -0
  11. package/index.js +8 -4
  12. package/package.json +1 -1
  13. package/templates/template-router/.claude/skills/project-conventions/SKILL.md +15 -38
  14. package/templates/template-router/.claude/skills/shadcn/SKILL.md +2 -2
  15. package/templates/template-router/.claude/skills/shadcn/rules/composition.md +8 -7
  16. package/templates/template-router/.env.example +0 -2
  17. package/templates/template-router/.mcp.json +2 -2
  18. package/templates/template-router/.oxlintrc.json +71 -1
  19. package/templates/template-router/CLAUDE.md +3 -3
  20. package/templates/template-router/components.json +1 -2
  21. package/templates/template-router/doctor.config.json +4 -0
  22. package/templates/template-router/e2e/a11y.spec.ts +9 -2
  23. package/templates/template-router/e2e/auth.spec.ts +2 -8
  24. package/templates/template-router/e2e/global-setup.ts +1 -5
  25. package/templates/template-router/e2e/keyboard.spec.ts +12 -11
  26. package/templates/template-router/e2e/notes.spec.ts +53 -0
  27. package/templates/template-router/e2e/utils.ts +1 -5
  28. package/templates/template-router/index.html +2 -1
  29. package/templates/template-router/package.json +3 -4
  30. package/templates/template-router/src/api/auth/auth-router-sync.test.ts +32 -0
  31. package/templates/template-router/src/api/auth/auth-router-sync.ts +14 -11
  32. package/templates/template-router/src/api/auth/create-guards.ts +2 -0
  33. package/templates/template-router/src/api/auth/index.ts +2 -0
  34. package/templates/template-router/src/api/auth/schema.ts +3 -1
  35. package/templates/template-router/src/api/index.ts +14 -2
  36. package/templates/template-router/src/api/notes/index.ts +57 -0
  37. package/templates/template-router/src/api/notes/notes.test.ts +29 -0
  38. package/templates/template-router/src/api/notes/schema.ts +23 -0
  39. package/templates/template-router/src/api/refresh.test.ts +28 -0
  40. package/templates/template-router/src/components/error-screen.tsx +45 -0
  41. package/templates/template-router/src/components/root-error-boundary.tsx +15 -32
  42. package/templates/template-router/src/components/theme-toggle.tsx +26 -0
  43. package/templates/template-router/src/components/ui/textarea.tsx +17 -0
  44. package/templates/template-router/src/components/ui/toast.tsx +220 -0
  45. package/templates/template-router/src/env.ts +0 -2
  46. package/templates/{template-start/src/lib/query-client.test.ts → template-router/src/lib/query-client.test.tsx} +15 -16
  47. package/templates/template-router/src/lib/query-client.ts +4 -3
  48. package/templates/template-router/src/lib/storage-store.ts +8 -4
  49. package/templates/template-router/src/main.tsx +1 -3
  50. package/templates/template-router/src/mocks/server.ts +90 -8
  51. package/templates/template-router/src/providers/index.tsx +3 -11
  52. package/templates/template-router/src/providers/theme-anti-flash.test.ts +1 -1
  53. package/templates/template-router/src/providers/theme-constants.ts +5 -0
  54. package/templates/template-router/src/providers/theme.tsx +4 -24
  55. package/templates/template-router/src/routeTree.gen.ts +46 -3
  56. package/templates/template-router/src/routes/-error.tsx +20 -41
  57. package/templates/template-router/src/routes/-not-found.tsx +10 -7
  58. package/templates/template-router/src/routes/-pending.tsx +6 -8
  59. package/templates/template-router/src/routes/__root.tsx +5 -1
  60. package/templates/template-router/src/routes/_authenticated/index.tsx +2 -2
  61. package/templates/template-router/src/routes/_authenticated/notes/$noteId.tsx +44 -0
  62. package/templates/template-router/src/routes/_authenticated/notes/index.tsx +105 -0
  63. package/templates/template-router/src/routes/_authenticated.tsx +23 -10
  64. package/templates/template-router/src/routes/sign-in.tsx +7 -7
  65. package/templates/template-router/src/test/mock-backend.ts +0 -1
  66. package/templates/template-router/tsconfig.app.json +2 -2
  67. package/templates/template-router/tsconfig.json +1 -9
  68. package/templates/template-router/tsconfig.node.json +4 -3
  69. package/templates/template-start/.claude/skills/project-conventions/SKILL.md +15 -38
  70. package/templates/template-start/.claude/skills/shadcn/SKILL.md +2 -2
  71. package/templates/template-start/.claude/skills/shadcn/rules/composition.md +8 -7
  72. package/templates/template-start/.env.example +0 -2
  73. package/templates/template-start/.mcp.json +2 -2
  74. package/templates/template-start/.oxlintrc.json +71 -1
  75. package/templates/template-start/CLAUDE.md +3 -3
  76. package/templates/template-start/components.json +1 -2
  77. package/templates/template-start/doctor.config.json +4 -0
  78. package/templates/template-start/e2e/a11y.spec.ts +9 -2
  79. package/templates/template-start/e2e/auth.spec.ts +2 -8
  80. package/templates/template-start/e2e/global-setup.ts +1 -5
  81. package/templates/template-start/e2e/keyboard.spec.ts +12 -11
  82. package/templates/template-start/e2e/notes.spec.ts +53 -0
  83. package/templates/template-start/e2e/utils.ts +1 -5
  84. package/templates/template-start/package.json +3 -4
  85. package/templates/template-start/src/api/auth/auth-router-sync.test.ts +32 -0
  86. package/templates/template-start/src/api/auth/auth-router-sync.ts +14 -11
  87. package/templates/template-start/src/api/auth/create-guards.ts +2 -0
  88. package/templates/template-start/src/api/auth/index.ts +2 -0
  89. package/templates/template-start/src/api/auth/schema.ts +3 -1
  90. package/templates/template-start/src/api/index.ts +14 -2
  91. package/templates/template-start/src/api/notes/index.ts +57 -0
  92. package/templates/template-start/src/api/notes/notes.test.ts +29 -0
  93. package/templates/template-start/src/api/notes/schema.ts +23 -0
  94. package/templates/template-start/src/api/refresh.test.ts +28 -0
  95. package/templates/template-start/src/components/error-screen.tsx +45 -0
  96. package/templates/template-start/src/components/root-error-boundary.tsx +15 -32
  97. package/templates/template-start/src/components/theme-toggle.tsx +26 -0
  98. package/templates/template-start/src/components/ui/textarea.tsx +17 -0
  99. package/templates/template-start/src/components/ui/toast.tsx +220 -0
  100. package/templates/template-start/src/env.ts +0 -2
  101. package/templates/{template-router/src/lib/query-client.test.ts → template-start/src/lib/query-client.test.tsx} +15 -16
  102. package/templates/template-start/src/lib/query-client.ts +4 -3
  103. package/templates/template-start/src/mocks/server.ts +90 -8
  104. package/templates/template-start/src/providers/index.tsx +3 -11
  105. package/templates/template-start/src/providers/theme-constants.ts +5 -0
  106. package/templates/template-start/src/providers/theme.tsx +4 -24
  107. package/templates/template-start/src/routeTree.gen.ts +48 -3
  108. package/templates/template-start/src/router.tsx +2 -4
  109. package/templates/template-start/src/routes/-error.tsx +20 -41
  110. package/templates/template-start/src/routes/-not-found.tsx +10 -7
  111. package/templates/template-start/src/routes/-pending.tsx +6 -8
  112. package/templates/template-start/src/routes/__root.tsx +5 -3
  113. package/templates/template-start/src/routes/_authenticated/index.tsx +2 -2
  114. package/templates/template-start/src/routes/_authenticated/notes/$noteId.tsx +44 -0
  115. package/templates/template-start/src/routes/_authenticated/notes/index.tsx +105 -0
  116. package/templates/template-start/src/routes/_authenticated.tsx +23 -10
  117. package/templates/template-start/src/routes/about.tsx +1 -1
  118. package/templates/template-start/src/routes/sign-in.tsx +1 -1
  119. package/templates/template-start/src/test/mock-backend.ts +0 -1
  120. package/templates/template-start/tsconfig.app.json +2 -2
  121. package/templates/template-start/tsconfig.json +1 -9
  122. package/templates/template-start/tsconfig.node.json +4 -3
  123. package/templates/template-router/src/components/ui/dialog.tsx +0 -136
  124. package/templates/template-router/src/components/ui/select.tsx +0 -188
  125. package/templates/template-router/src/components/ui/skeleton.tsx +0 -13
  126. package/templates/template-router/src/components/ui/sonner.tsx +0 -43
  127. package/templates/template-router/tsconfig.e2e.json +0 -24
  128. package/templates/template-start/src/components/ui/dialog.tsx +0 -136
  129. package/templates/template-start/src/components/ui/select.tsx +0 -188
  130. package/templates/template-start/src/components/ui/skeleton.tsx +0 -13
  131. package/templates/template-start/src/components/ui/sonner.tsx +0 -43
  132. package/templates/template-start/tsconfig.e2e.json +0 -24
@@ -1,15 +1,18 @@
1
1
  [
2
2
  "src/mocks",
3
+ "src/api/notes/schema.ts",
4
+ "src/api/notes/notes.test.ts",
5
+ "src/routes/_authenticated/notes",
3
6
  "src/api/auth/schema.ts",
4
7
  "src/api/auth/session-store.ts",
5
8
  "src/api/auth/session-store.test.ts",
6
9
  "src/api/auth/auth-router-sync.ts",
10
+ "src/api/auth/auth-router-sync.test.ts",
7
11
  "src/test/mock-backend.ts",
8
12
  "src/api/auth/auth.test.ts",
9
13
  "src/api/refresh.test.ts",
10
14
  "src/lib/single-flight.ts",
11
15
  "src/lib/single-flight.test.ts",
12
16
  "e2e",
13
- "playwright.config.ts",
14
- "tsconfig.e2e.json"
17
+ "playwright.config.ts"
15
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 { queryClient } from '@/lib/query-client'
5
+ import { Toaster } from '@/components/ui/toast'
5
6
 
6
- vi.mock('sonner', () => ({ toast: { error: vi.fn() } }))
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
- // notifyError imports sonner dynamically, so the toast lands a tick later
17
- const flushToasts = () => vi.dynamicImportSettled()
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(toast.error).not.toHaveBeenCalled()
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(toast.error).toHaveBeenCalledTimes(1)
47
- expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('500'))
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(toast.error).toHaveBeenCalledTimes(1)
59
- expect(toast.error).toHaveBeenCalledWith('nope')
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
- const message = getErrorMessage(error)
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
- <div className='w-full max-w-md space-y-4'>
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
- </div>
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
- <div className='flex min-h-svh flex-col'>
25
+ <>
25
26
  <header className='flex h-14 items-center justify-between border-b px-6'>
26
- <span className='text-muted-foreground text-sm'>{session?.user.email}</span>
27
- <Button variant='outline' size='sm' onClick={onSignOut} disabled={signOut.isPending}>
28
- <LogOut data-icon='inline-start' />
29
- Sign out
30
- </Button>
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
- <main className='flex flex-1 items-center justify-center p-6'>
36
+ <div className='flex flex-1 items-center justify-center p-6'>
33
37
  <Outlet />
34
- </main>
35
- </div>
38
+ </div>
39
+ </>
36
40
  )
37
41
  }
@@ -47,7 +47,7 @@ function SignInPage() {
47
47
  })
48
48
 
49
49
  return (
50
- <main className='flex min-h-svh items-center justify-center p-6'>
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>
@@ -50,7 +50,7 @@ function SignUpPage() {
50
50
  })
51
51
 
52
52
  return (
53
- <main className='flex min-h-svh items-center justify-center p-6'>
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>
@@ -51,7 +51,7 @@ function SignInPage() {
51
51
  })
52
52
 
53
53
  return (
54
- <main className='flex min-h-svh items-center justify-center p-6'>
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>
@@ -54,7 +54,7 @@ function SignUpPage() {
54
54
  })
55
55
 
56
56
  return (
57
- <main className='flex min-h-svh items-center justify-center p-6'>
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
  `
package/index.js CHANGED
@@ -317,10 +317,14 @@ const applyBackendOverlay = () => {
317
317
  delete pkg.trustedDependencies
318
318
  })
319
319
 
320
- // textual, not a JSON round-trip: JSON.stringify re-expands `paths` and fails format:check
321
- substitute(path.join(webDir, 'tsconfig.json'), [
322
- [',\n {\n "path": "./tsconfig.e2e.json"\n }', '']
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-top-secret-starter",
3
- "version": "0.8.3",
3
+ "version": "0.11.0",
4
4
  "description": "Scaffold a Vite + React 19 + TypeScript + shadcn starter with TanStack Router",
5
5
  "keywords": [
6
6
  "create",
@@ -5,16 +5,21 @@ 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 `scripts/check-conventions.mjs`;
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
12
12
 
13
- Colocate under `src/api/<feature>/`, mirroring `src/api/auth/`:
13
+ Colocate under `src/api/<feature>/`. **`src/api/notes/` is the reference**: list,
14
+ detail, create and delete over a plain REST resource, with the routes under
15
+ `src/routes/_authenticated/notes/`. `src/api/auth/` is the special case (session, no cache).
14
16
 
15
17
  - `schema.ts` — zod schemas. **Required**: the conventions check fails without it.
16
18
  - data functions using the `api` instance from `src/api/index.ts`
17
19
  - `use<Thing>` hooks (Query/Mutation) in the same folder
20
+ - `xQuery` as `queryOptions` (`notesQuery`, `noteQuery(id)`), detail keys nested under the
21
+ list key. Mutations invalidate the list in `onSuccess`; after a delete, `removeQueries` the
22
+ detail entry instead of invalidating it — a refetch would 404 into a toast.
18
23
 
19
24
  Validate at the boundary, always: `.then(data => Schema.parse(data))`. Never trust
20
25
  raw JSON, even from your own backend — a changed contract must fail loudly at the
@@ -22,7 +27,8 @@ edge, not produce `undefined` three components deep.
22
27
 
23
28
  Never call `fetch` directly. The two ky instances exist for a reason:
24
29
 
25
- - `api` — attaches the bearer token; on 401 refreshes once and retries
30
+ - `api` — attaches the bearer token; refreshes ahead of a request when the session's
31
+ optional `expiresAt` is within 30 s, and on a 401 refreshes once and retries
26
32
  - `publicApi` — no auth header and no refresh hook: sign-in and the refresh call itself
27
33
 
28
34
  Refresh is wrapped in `singleFlight` (`src/lib/single-flight.ts`) so concurrent
@@ -59,8 +65,8 @@ on mount and therefore also listens for `storage`.
59
65
  ## Forms
60
66
 
61
67
  react-hook-form + `standardSchemaResolver` (zod/mini schemas), with shadcn `Field*`
62
- primitives. `routes/sign-in.tsx`
63
- is the reference implementation read it before writing a new form.
68
+ primitives. `routes/sign-in.tsx` is the reference implementation;
69
+ `routes/_authenticated/notes/index.tsx` shows a form that feeds a mutation and resets.
64
70
 
65
71
  Reuse the request schema from the feature's `schema.ts` as the form schema where the
66
72
  shapes match; two schemas for one payload drift apart.
@@ -74,7 +80,9 @@ File-based under `src/routes/`.
74
80
  - `-`-prefixed files (`-error.tsx`, `-not-found.tsx`) are non-route modules
75
81
  - Router context carries `queryClient`; auto code-splitting is on
76
82
  - Data a route needs on first paint: `loader: ({ context }) => context.queryClient.query({ ...xQuery, staleTime: 'static' })`
77
- plus `useSuspenseQuery(xQuery)` in the component. `_authenticated/index.tsx` is the reference.
83
+ plus `useSuspenseQuery(xQuery)` in the component. `_authenticated/notes/index.tsx` is the reference.
84
+ - Detail routes (`$noteId.tsx`) turn a 404 from the loader into `throw notFound()` so the
85
+ not-found page renders instead of the error page.
78
86
  - Sign-in/out mutations touch neither the router nor the Query cache — `auth-router-sync` reacts to the session change
79
87
 
80
88
  ## UI primitives
@@ -107,37 +115,6 @@ Put error-handling changes there, not in individual hooks.
107
115
 
108
116
  On (`babel-plugin-react-compiler`). No manual `useMemo` / `useCallback` — enforced.
109
117
 
110
- ## Backend variant (`apps/api`, `create-cli/backend-overlay`)
111
-
112
- The rules above describe the SPA-only variant. When the backend is on:
113
-
114
- - HTTP goes through Eden (`treaty<App>` in `src/api/index.ts`), types come from
115
- `apps/api/src/app.ts` — no `schema.ts`, no ky.
116
- - Auth is Better Auth cookie sessions via `better-auth/react`. No `session-store`,
117
- no refresh, no `auth-router-sync`; guards `await authClient.getSession()` in
118
- `beforeLoad` and mutations call `router.invalidate()` on success.
119
- - New server resource: table in `apps/api/src/db/schema.ts` → `db:generate` → a
120
- feature folder `apps/api/src/modules/<feature>/` → React Query hooks over
121
- `api.<resource>` (`backend-overlay/web/src/api/notes`). `modules/notes/` is the
122
- reference; it follows Elysia's feature-based layout:
123
-
124
- | File | Holds |
125
- | ------------ | --------------------------------------------------------------------- |
126
- | `model.ts` | `t` schemas (body, params, response) + types via `UnwrapSchema` |
127
- | `service.ts` | `abstract class` with static methods; takes `db`/ids, never `Context` |
128
- | `index.ts` | the Elysia controller — routing, `{ auth: true }`, `response` schemas |
129
-
130
- One Elysia instance is one controller: don't pass `Context` into a class, and don't
131
- type handlers against it. `modules/auth/service.ts` is the exception the docs allow —
132
- a request-dependent service, so it is an Elysia instance with a `macro` and a `name`.
133
-
134
- - `scripts/check-conventions.mjs` scans this repo's `src/` and `e2e/` only, so neither
135
- `apps/api/` nor the overlay is covered — including the rule that every
136
- `src/api/<feature>/` has a `schema.ts`, which the Eden variant deliberately drops.
137
- Generated projects don't ship the script at all. Review those by hand.
138
-
139
118
  ## Mock backend
140
119
 
141
- `src/mocks/server.ts` patches global `fetch` for `/api/*`, dev-only under
142
- `VITE_ENABLE_MOCKS`. Seed sign-in `demo@example.com` / `demo1234`. `bun run graduate <name>`
143
- 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 `sonner`.** Use `toast()` from `sonner`.
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 | `sonner` (toast), `Alert`, `Progress`, `Skeleton`, `Spinner` |
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 sonner
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 sonner
93
+ ## Toast notifications use the Base UI toast manager
94
94
 
95
95
  ```tsx
96
- import { toast } from 'sonner'
96
+ import { toast } from '@/components/ui/toast'
97
97
 
98
- toast.success('Changes saved.')
99
- toast.error('Something went wrong.')
100
- toast('File deleted.', {
101
- action: { label: 'Undo', onClick: () => undoDelete() }
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
 
@@ -1,3 +1 @@
1
1
  VITE_API_URL=/api/
2
- # set to false to hit a real API in dev; mocks never ship in a production build
3
- VITE_ENABLE_MOCKS=true
@@ -2,11 +2,11 @@
2
2
  "mcpServers": {
3
3
  "playwright": {
4
4
  "command": "bunx",
5
- "args": ["@playwright/mcp@latest"]
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 standardSchemaResolver."
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`, `sonner`, time, storage events). Never mock our own modules.
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,13 +48,13 @@ 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. `mocksEnabled` alone is a runtime value and would still ship the mock backend as a lazy chunk.
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 is imported from `src/`; CLIs and build plugins are dev deps so `bun audit --prod` stays readable. `trustedDependencies: []` — don't add entries without checking what the script does.
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.
58
58
 
59
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.
60
60
 
@@ -16,8 +16,7 @@
16
16
  "components": "@/components",
17
17
  "utils": "@/lib/utils",
18
18
  "ui": "@/components/ui",
19
- "lib": "@/lib",
20
- "hooks": "@/hooks"
19
+ "lib": "@/lib"
21
20
  },
22
21
  "menuColor": "default",
23
22
  "menuAccent": "subtle",
@@ -7,6 +7,10 @@
7
7
  "files": ["src/routes/**"],
8
8
  "rules": ["react-doctor/only-export-components"]
9
9
  },
10
+ {
11
+ "files": ["src/api/auth/index.ts"],
12
+ "rules": ["react-doctor/query-mutation-missing-invalidation"]
13
+ },
10
14
  {
11
15
  "files": ["src/api/auth/session-store.ts"],
12
16
  "rules": ["react-doctor/client-localstorage-no-version"]
@@ -3,8 +3,7 @@ import { expect, test, type Page } from '@playwright/test'
3
3
 
4
4
  import { ready, signIn } from './utils'
5
5
 
6
- // Automated rules catch roughly a third of WCAG failures — treat a pass as "no
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
 
@@ -25,3 +24,11 @@ test('the app shell has no detectable violations', async ({ page }) => {
25
24
  const { violations } = await scan(page)
26
25
  expect(violations).toEqual([])
27
26
  })
27
+
28
+ test('the notes list has no detectable violations', async ({ page }) => {
29
+ await signIn(page)
30
+ await page.getByRole('link', { name: 'Notes' }).click()
31
+ await expect(page.getByRole('heading', { name: 'Notes' })).toBeVisible()
32
+ const { violations } = await scan(page)
33
+ expect(violations).toEqual([])
34
+ })
@@ -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
- // A navigation control must expose the link role axe cannot see this, because
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
- // defaultPreload: 'intent' runs beforeLoad on hover; a thrown redirect must
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
- // `reuseExistingServer` makes Playwright adopt whatever already listens on the
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 {