@prmichaelsen/acp-visualizer 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/package.json +8 -10
  2. package/src/components/ExtraFieldsBadge.tsx +1 -1
  3. package/src/components/FilterBar.tsx +1 -1
  4. package/src/components/Header.tsx +1 -1
  5. package/src/components/MilestoneTable.tsx +1 -1
  6. package/src/components/MilestoneTree.tsx +2 -2
  7. package/src/components/StatusBadge.tsx +1 -1
  8. package/src/components/StatusDot.tsx +1 -1
  9. package/src/components/TaskList.tsx +1 -1
  10. package/src/routes/__root.tsx +5 -5
  11. package/src/routes/api/watch.ts +1 -1
  12. package/src/routes/index.tsx +2 -2
  13. package/src/routes/milestones.tsx +7 -7
  14. package/src/routes/search.tsx +4 -4
  15. package/src/routes/tasks.tsx +3 -3
  16. package/src/services/progress-database.service.ts +3 -3
  17. package/agent/commands/acp.clarification-address.md +0 -417
  18. package/agent/commands/acp.clarification-capture.md +0 -386
  19. package/agent/commands/acp.clarification-create.md +0 -437
  20. package/agent/commands/acp.clarifications-research.md +0 -326
  21. package/agent/commands/acp.command-create.md +0 -432
  22. package/agent/commands/acp.design-create.md +0 -286
  23. package/agent/commands/acp.design-reference.md +0 -355
  24. package/agent/commands/acp.handoff.md +0 -270
  25. package/agent/commands/acp.index.md +0 -423
  26. package/agent/commands/acp.init.md +0 -546
  27. package/agent/commands/acp.package-create.md +0 -895
  28. package/agent/commands/acp.package-info.md +0 -212
  29. package/agent/commands/acp.package-install.md +0 -539
  30. package/agent/commands/acp.package-list.md +0 -280
  31. package/agent/commands/acp.package-publish.md +0 -541
  32. package/agent/commands/acp.package-remove.md +0 -293
  33. package/agent/commands/acp.package-search.md +0 -307
  34. package/agent/commands/acp.package-update.md +0 -361
  35. package/agent/commands/acp.package-validate.md +0 -540
  36. package/agent/commands/acp.pattern-create.md +0 -386
  37. package/agent/commands/acp.plan.md +0 -587
  38. package/agent/commands/acp.proceed.md +0 -882
  39. package/agent/commands/acp.project-create.md +0 -675
  40. package/agent/commands/acp.project-info.md +0 -312
  41. package/agent/commands/acp.project-list.md +0 -226
  42. package/agent/commands/acp.project-remove.md +0 -379
  43. package/agent/commands/acp.project-set.md +0 -227
  44. package/agent/commands/acp.project-update.md +0 -307
  45. package/agent/commands/acp.projects-restore.md +0 -228
  46. package/agent/commands/acp.projects-sync.md +0 -347
  47. package/agent/commands/acp.report.md +0 -407
  48. package/agent/commands/acp.resume.md +0 -239
  49. package/agent/commands/acp.sessions.md +0 -301
  50. package/agent/commands/acp.status.md +0 -293
  51. package/agent/commands/acp.sync.md +0 -364
  52. package/agent/commands/acp.task-create.md +0 -500
  53. package/agent/commands/acp.update.md +0 -302
  54. package/agent/commands/acp.validate.md +0 -466
  55. package/agent/commands/acp.version-check-for-updates.md +0 -276
  56. package/agent/commands/acp.version-check.md +0 -191
  57. package/agent/commands/acp.version-update.md +0 -289
  58. package/agent/commands/command.template.md +0 -339
  59. package/agent/commands/git.commit.md +0 -526
  60. package/agent/commands/git.init.md +0 -514
  61. package/agent/commands/tanstack-cloudflare.deploy.md +0 -272
  62. package/agent/commands/tanstack-cloudflare.tail.md +0 -275
  63. package/agent/design/.gitkeep +0 -0
  64. package/agent/design/design.template.md +0 -154
  65. package/agent/design/local.dashboard-layout-routing.md +0 -288
  66. package/agent/design/local.data-model-yaml-parsing.md +0 -310
  67. package/agent/design/local.search-filtering.md +0 -331
  68. package/agent/design/local.server-api-auto-refresh.md +0 -235
  69. package/agent/design/local.table-tree-views.md +0 -299
  70. package/agent/design/local.visualizer-requirements.md +0 -349
  71. package/agent/design/requirements.template.md +0 -387
  72. package/agent/index/.gitkeep +0 -0
  73. package/agent/index/acp.core.yaml +0 -137
  74. package/agent/index/local.main.template.yaml +0 -37
  75. package/agent/manifest.template.yaml +0 -13
  76. package/agent/manifest.yaml +0 -302
  77. package/agent/milestones/.gitkeep +0 -0
  78. package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +0 -67
  79. package/agent/milestones/milestone-1-{title}.template.md +0 -206
  80. package/agent/milestones/milestone-2-dashboard-views-interaction.md +0 -79
  81. package/agent/package.template.yaml +0 -86
  82. package/agent/patterns/.gitkeep +0 -0
  83. package/agent/patterns/bootstrap.template.md +0 -1237
  84. package/agent/patterns/pattern.template.md +0 -382
  85. package/agent/patterns/tanstack-cloudflare.acl-permissions.md +0 -332
  86. package/agent/patterns/tanstack-cloudflare.action-bar-item.md +0 -416
  87. package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +0 -401
  88. package/agent/patterns/tanstack-cloudflare.auth-session-management.md +0 -387
  89. package/agent/patterns/tanstack-cloudflare.card-and-list.md +0 -271
  90. package/agent/patterns/tanstack-cloudflare.chat-engine.md +0 -353
  91. package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +0 -346
  92. package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +0 -516
  93. package/agent/patterns/tanstack-cloudflare.email-service.md +0 -431
  94. package/agent/patterns/tanstack-cloudflare.expander.md +0 -98
  95. package/agent/patterns/tanstack-cloudflare.fcm-push.md +0 -115
  96. package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +0 -441
  97. package/agent/patterns/tanstack-cloudflare.firebase-auth.md +0 -348
  98. package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +0 -550
  99. package/agent/patterns/tanstack-cloudflare.firebase-storage.md +0 -369
  100. package/agent/patterns/tanstack-cloudflare.form-controls.md +0 -145
  101. package/agent/patterns/tanstack-cloudflare.global-search-context.md +0 -93
  102. package/agent/patterns/tanstack-cloudflare.image-carousel.md +0 -126
  103. package/agent/patterns/tanstack-cloudflare.library-services.md +0 -553
  104. package/agent/patterns/tanstack-cloudflare.lightbox.md +0 -169
  105. package/agent/patterns/tanstack-cloudflare.markdown-content.md +0 -115
  106. package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +0 -98
  107. package/agent/patterns/tanstack-cloudflare.modal.md +0 -156
  108. package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +0 -461
  109. package/agent/patterns/tanstack-cloudflare.notifications-engine.md +0 -151
  110. package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +0 -90
  111. package/agent/patterns/tanstack-cloudflare.og-metadata.md +0 -296
  112. package/agent/patterns/tanstack-cloudflare.pagination.md +0 -442
  113. package/agent/patterns/tanstack-cloudflare.pill-input.md +0 -220
  114. package/agent/patterns/tanstack-cloudflare.provider-adapter.md +0 -401
  115. package/agent/patterns/tanstack-cloudflare.rate-limiting.md +0 -323
  116. package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +0 -338
  117. package/agent/patterns/tanstack-cloudflare.searchable-settings.md +0 -375
  118. package/agent/patterns/tanstack-cloudflare.slide-over.md +0 -129
  119. package/agent/patterns/tanstack-cloudflare.ssr-preload.md +0 -571
  120. package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +0 -508
  121. package/agent/patterns/tanstack-cloudflare.toast-system.md +0 -142
  122. package/agent/patterns/tanstack-cloudflare.unified-header.md +0 -280
  123. package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +0 -628
  124. package/agent/patterns/tanstack-cloudflare.websocket-manager.md +0 -237
  125. package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +0 -358
  126. package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +0 -336
  127. package/agent/progress.template.yaml +0 -161
  128. package/agent/progress.yaml +0 -145
  129. package/agent/schemas/package.schema.yaml +0 -276
  130. package/agent/scripts/acp.common.sh +0 -1781
  131. package/agent/scripts/acp.install.sh +0 -333
  132. package/agent/scripts/acp.package-create.sh +0 -924
  133. package/agent/scripts/acp.package-info.sh +0 -288
  134. package/agent/scripts/acp.package-install.sh +0 -893
  135. package/agent/scripts/acp.package-list.sh +0 -311
  136. package/agent/scripts/acp.package-publish.sh +0 -420
  137. package/agent/scripts/acp.package-remove.sh +0 -348
  138. package/agent/scripts/acp.package-search.sh +0 -156
  139. package/agent/scripts/acp.package-update.sh +0 -517
  140. package/agent/scripts/acp.package-validate.sh +0 -1018
  141. package/agent/scripts/acp.uninstall.sh +0 -85
  142. package/agent/scripts/acp.version-check-for-updates.sh +0 -98
  143. package/agent/scripts/acp.version-check.sh +0 -47
  144. package/agent/scripts/acp.version-update.sh +0 -176
  145. package/agent/scripts/acp.yaml-parser.sh +0 -985
  146. package/agent/scripts/acp.yaml-validate.sh +0 -205
  147. package/agent/tasks/.gitkeep +0 -0
  148. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +0 -210
  149. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +0 -294
  150. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +0 -193
  151. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +0 -262
  152. package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +0 -156
  153. package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +0 -178
  154. package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +0 -141
  155. package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +0 -153
  156. package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +0 -174
  157. package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +0 -233
  158. package/agent/tasks/task-1-{title}.template.md +0 -244
  159. package/vitest.config.ts +0 -27
@@ -1,387 +0,0 @@
1
- # Auth Session Management Pattern
2
-
3
- **Category**: Architecture
4
- **Applicable To**: TanStack Start + Cloudflare Workers applications with authentication
5
- **Status**: Stable
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- This pattern provides cookie-based session management for TanStack Start applications running on Cloudflare Workers. It uses Firebase Admin SDK for token verification, HTTP-only session cookies for security, and a server function (`getAuthSession`) that can be called from any component, route, or API handler to get the current authenticated user.
12
-
13
- The pattern enforces server-side-only authentication — all auth checks happen on the server, never on the client. This prevents token exposure and ensures that authentication state is always authoritative.
14
-
15
- ---
16
-
17
- ## When to Use This Pattern
18
-
19
- ✅ **Use this pattern when:**
20
- - Building authenticated TanStack Start applications
21
- - Need cookie-based session management
22
- - Using Firebase Authentication as the identity provider
23
- - Need a universal `getAuthSession()` function callable from any context
24
- - Want server-side auth enforcement (never client-side token verification)
25
-
26
- ❌ **Don't use this pattern when:**
27
- - Building public-only applications with no authentication
28
- - Using a different auth provider that handles sessions differently (Auth0, Clerk)
29
- - Building static sites
30
-
31
- ---
32
-
33
- ## Core Principles
34
-
35
- 1. **Server-Side Only**: All authentication verification happens on the server — never verify tokens client-side
36
- 2. **Cookie-Based Sessions**: Use HTTP-only cookies to store session tokens (not localStorage)
37
- 3. **Universal Server Function**: `getAuthSession()` is a `createServerFn` callable from any context
38
- 4. **Graceful Fallback**: Auth failures return `null`, never throw — callers decide how to handle
39
- 5. **Session Cookie Exchange**: Exchange short-lived ID tokens for long-lived session cookies (14 days)
40
- 6. **Request-Based Verification**: `getServerSession(request)` extracts and verifies the cookie from the request
41
-
42
- ---
43
-
44
- ## Implementation
45
-
46
- ### Structure
47
-
48
- ```
49
- src/
50
- ├── lib/
51
- │ └── auth/
52
- │ ├── session.ts # Server-side session verification
53
- │ └── server-fn.ts # createServerFn wrapper for components
54
- ├── types/
55
- │ └── auth.ts # User and ServerSession types
56
- ├── components/
57
- │ └── auth/
58
- │ ├── AuthContext.tsx # React context for auth state
59
- │ └── AuthForm.tsx # Login/register UI
60
- └── routes/
61
- ├── __root.tsx # Root layout with auth initialization
62
- ├── auth.tsx # Auth page
63
- └── api/
64
- └── auth/
65
- ├── session.tsx # POST: Create session cookie
66
- └── logout.tsx # POST: Destroy session
67
- ```
68
-
69
- ### Code Example
70
-
71
- #### Step 1: Define Auth Types
72
-
73
- ```typescript
74
- // src/types/auth.ts
75
- export interface User {
76
- uid: string
77
- email: string | null
78
- displayName: string | null
79
- photoURL: string | null
80
- emailVerified: boolean
81
- }
82
-
83
- export interface ServerSession {
84
- user: User
85
- }
86
- ```
87
-
88
- #### Step 2: Server-Side Session Verification
89
-
90
- ```typescript
91
- // src/lib/auth/session.ts
92
- import { verifyIdToken, verifySessionCookie, createSessionCookie as createFirebaseSessionCookie }
93
- from '@prmichaelsen/firebase-admin-sdk-v8'
94
- import type { User, ServerSession } from '@/types/auth'
95
-
96
- /**
97
- * Extract session cookie from request headers
98
- */
99
- function getSessionCookie(request: Request): string | undefined {
100
- const cookieHeader = request.headers.get('cookie')
101
- if (!cookieHeader) return undefined
102
-
103
- const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
104
- const [name, value] = cookie.trim().split('=')
105
- acc[name] = value
106
- return acc
107
- }, {} as Record<string, string>)
108
-
109
- return cookies.session
110
- }
111
-
112
- /**
113
- * Get authenticated user from request
114
- * Returns null if not authenticated — never throws
115
- */
116
- export async function getServerSession(request: Request): Promise<ServerSession | null> {
117
- try {
118
- const sessionCookie = getSessionCookie(request)
119
- if (!sessionCookie) return null
120
-
121
- // Verify session cookie (try session cookie first, fallback to ID token)
122
- let decodedToken
123
- try {
124
- decodedToken = await verifySessionCookie(sessionCookie)
125
- } catch {
126
- decodedToken = await verifyIdToken(sessionCookie)
127
- }
128
-
129
- const user: User = {
130
- uid: decodedToken.sub,
131
- email: decodedToken.email || null,
132
- displayName: decodedToken.name || null,
133
- photoURL: decodedToken.picture || null,
134
- emailVerified: decodedToken.email_verified || false,
135
- }
136
-
137
- return { user }
138
- } catch (error) {
139
- console.error('Failed to get server session', error)
140
- return null
141
- }
142
- }
143
-
144
- /**
145
- * Create a long-lived session cookie from a Firebase ID token
146
- */
147
- export async function createSessionCookie(idToken: string): Promise<string> {
148
- return createFirebaseSessionCookie(idToken, {
149
- expiresIn: 60 * 60 * 24 * 14 * 1000 // 14 days
150
- })
151
- }
152
- ```
153
-
154
- #### Step 3: Universal Server Function
155
-
156
- ```typescript
157
- // src/lib/auth/server-fn.ts
158
- import { createServerFn } from '@tanstack/react-start'
159
- import { getRequest } from '@tanstack/react-start/server'
160
- import { getServerSession } from '@/lib/auth/session'
161
- import { initFirebaseAdmin } from '@/lib/firebase-admin'
162
-
163
- /**
164
- * Server function to get current auth session.
165
- * Callable from any component, route, or server context.
166
- */
167
- export const getAuthSession = createServerFn({ method: 'GET' }).handler(async () => {
168
- try {
169
- initFirebaseAdmin()
170
- const session = await getServerSession(getRequest())
171
- return session?.user || null
172
- } catch (error) {
173
- console.error('[getAuthSession] Error:', error)
174
- return null
175
- }
176
- })
177
- ```
178
-
179
- #### Step 4: Root Layout with Auth Initialization
180
-
181
- ```typescript
182
- // src/routes/__root.tsx
183
- import { createRootRouteWithContext } from '@tanstack/react-router'
184
- import { getAuthSession } from '@/lib/auth/server-fn'
185
- import { AuthProvider } from '@/components/auth/AuthContext'
186
-
187
- export const Route = createRootRouteWithContext()({
188
- beforeLoad: async () => {
189
- // Fetch auth session server-side (SSR)
190
- const user = await getAuthSession()
191
- return { user }
192
- },
193
- component: RootLayout,
194
- })
195
-
196
- function RootLayout() {
197
- const { user } = Route.useRouteContext()
198
-
199
- return (
200
- <AuthProvider initialUser={user}>
201
- <Outlet />
202
- </AuthProvider>
203
- )
204
- }
205
- ```
206
-
207
- #### Step 5: Auth Context Provider
208
-
209
- ```typescript
210
- // src/components/auth/AuthContext.tsx
211
- import { createContext, useContext, useState } from 'react'
212
- import type { User } from '@/types/auth'
213
-
214
- interface AuthContextType {
215
- user: User | null
216
- setUser: (user: User | null) => void
217
- }
218
-
219
- const AuthContext = createContext<AuthContextType | null>(null)
220
-
221
- export function AuthProvider({ initialUser, children }: {
222
- initialUser: User | null
223
- children: React.ReactNode
224
- }) {
225
- const [user, setUser] = useState<User | null>(initialUser)
226
-
227
- return (
228
- <AuthContext.Provider value={{ user, setUser }}>
229
- {children}
230
- </AuthContext.Provider>
231
- )
232
- }
233
-
234
- export function useAuth(): AuthContextType {
235
- const ctx = useContext(AuthContext)
236
- if (!ctx) throw new Error('useAuth must be used within AuthProvider')
237
- return ctx
238
- }
239
- ```
240
-
241
- #### Step 6: Protected Route with Redirect
242
-
243
- ```typescript
244
- // src/routes/profile.tsx
245
- import { createFileRoute, redirect } from '@tanstack/react-router'
246
- import { getAuthSession } from '@/lib/auth/server-fn'
247
-
248
- export const Route = createFileRoute('/profile')({
249
- beforeLoad: async () => {
250
- const user = await getAuthSession()
251
-
252
- if (!user) {
253
- throw redirect({
254
- to: '/auth',
255
- search: { redirect_url: '/profile' },
256
- })
257
- }
258
-
259
- return { user }
260
- },
261
- component: ProfilePage,
262
- })
263
- ```
264
-
265
- #### Step 7: Session Creation API Route
266
-
267
- ```typescript
268
- // src/routes/api/auth/session.tsx
269
- import { createFileRoute } from '@tanstack/react-router'
270
- import { createSessionCookie } from '@/lib/auth/session'
271
-
272
- export const Route = createFileRoute('/api/auth/session')({
273
- server: {
274
- handlers: {
275
- POST: async ({ request }) => {
276
- try {
277
- const { idToken } = await request.json()
278
- const sessionCookie = await createSessionCookie(idToken)
279
-
280
- return new Response(JSON.stringify({ success: true }), {
281
- status: 200,
282
- headers: {
283
- 'Content-Type': 'application/json',
284
- 'Set-Cookie': `session=${sessionCookie}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${60 * 60 * 24 * 14}`,
285
- },
286
- })
287
- } catch (error) {
288
- return new Response(JSON.stringify({ error: 'Failed to create session' }), {
289
- status: 401,
290
- headers: { 'Content-Type': 'application/json' },
291
- })
292
- }
293
- },
294
- },
295
- },
296
- })
297
- ```
298
-
299
- ---
300
-
301
- ## Benefits
302
-
303
- ### 1. Universal Auth Check
304
- `getAuthSession()` works in components, `beforeLoad`, API routes, and server functions — one function everywhere.
305
-
306
- ### 2. Secure by Default
307
- HTTP-only cookies prevent XSS attacks. Server-side verification prevents token tampering.
308
-
309
- ### 3. SSR Compatible
310
- Auth state is available during server-side rendering via `beforeLoad`, enabling instant authenticated content.
311
-
312
- ### 4. Graceful Degradation
313
- Auth failures return `null` instead of throwing, preventing cascading errors.
314
-
315
- ---
316
-
317
- ## Trade-offs
318
-
319
- ### 1. Firebase Dependency
320
- **Downside**: Tightly coupled to Firebase Admin SDK for token verification.
321
- **Mitigation**: Wrap in an interface if you anticipate switching auth providers.
322
-
323
- ### 2. Cookie Size Limits
324
- **Downside**: Session cookies have size limits (~4KB).
325
- **Mitigation**: Store minimal data in the cookie (just the session token), not user profile data.
326
-
327
- ---
328
-
329
- ## Anti-Patterns
330
-
331
- ### ❌ Anti-Pattern 1: Client-Side Token Verification
332
-
333
- ```typescript
334
- // ❌ BAD: Verifying tokens on the client
335
- function MyComponent() {
336
- const token = localStorage.getItem('token')
337
- const user = jwt.decode(token) // Client-side decode — not verified!
338
- }
339
-
340
- // ✅ GOOD: Always verify on server
341
- const user = await getAuthSession()
342
- ```
343
-
344
- ### ❌ Anti-Pattern 2: Throwing on Auth Failure
345
-
346
- ```typescript
347
- // ❌ BAD: Throwing prevents page from loading
348
- export async function getServerSession(request) {
349
- const cookie = getSessionCookie(request)
350
- if (!cookie) throw new Error('Not authenticated') // Crashes!
351
- }
352
-
353
- // ✅ GOOD: Return null, let callers decide
354
- export async function getServerSession(request) {
355
- const cookie = getSessionCookie(request)
356
- if (!cookie) return null // Graceful
357
- }
358
- ```
359
-
360
- ---
361
-
362
- ## Related Patterns
363
-
364
- - **[API Route Handlers](./tanstack-cloudflare.api-route-handlers.md)**: API routes use `getAuthSession()` for auth
365
- - **[SSR Preload Pattern](./tanstack-cloudflare.ssr-preload.md)**: `beforeLoad` uses auth for user-specific data
366
- - **[Rate Limiting](./tanstack-cloudflare.rate-limiting.md)**: Rate limit auth endpoints
367
-
368
- ---
369
-
370
- ## Checklist for Implementation
371
-
372
- - [ ] `getServerSession(request)` returns `ServerSession | null`
373
- - [ ] `getAuthSession` is a `createServerFn` wrapper
374
- - [ ] Session cookie is HTTP-only, Secure, SameSite=Lax
375
- - [ ] Root layout fetches auth in `beforeLoad`
376
- - [ ] AuthProvider wraps app with `initialUser` from SSR
377
- - [ ] Protected routes use `redirect` to `/auth` when not authenticated
378
- - [ ] API routes check `getAuthSession()` before processing
379
- - [ ] Auth failures never throw — always return null
380
- - [ ] Firebase Admin SDK initialized before verification
381
-
382
- ---
383
-
384
- **Status**: Stable - Proven pattern for TanStack Start authentication
385
- **Recommendation**: Use for all authenticated TanStack Start applications
386
- **Last Updated**: 2026-02-28
387
- **Contributors**: Patrick Michaelsen
@@ -1,271 +0,0 @@
1
- # Card, NotificationCard & CardList
2
-
3
- **Category**: Design
4
- **Applicable To**: All card-based data display, notification items with swipe-to-dismiss, and generic feed/list rendering
5
- **Status**: Stable
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- Cards are the primary data display unit across the app. This pattern covers the standard card styling, NotificationCard with swipe-to-dismiss gesture, and CardList (FeedList) — a generic list component that handles loading skeletons, empty states, and error banners for any card type.
12
-
13
- ---
14
-
15
- ## Implementation
16
-
17
- ### Card (Standard Styling)
18
-
19
- All cards in the app follow consistent styling:
20
-
21
- ```typescript
22
- // Standard card container
23
- <div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-4">
24
- {/* Card content */}
25
- </div>
26
-
27
- // With hover effect
28
- <div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-4
29
- hover:border-blue-500/50 transition-colors cursor-pointer">
30
- {/* Clickable card */}
31
- </div>
32
-
33
- // Highlighted/active
34
- <div className="bg-purple-900/20 border border-purple-400/60 rounded-xl p-4 shadow-purple-500/10">
35
- {/* Active card */}
36
- </div>
37
- ```
38
-
39
- **Text Hierarchy**:
40
- - Title: `text-white font-semibold`
41
- - Subtitle/secondary: `text-gray-400 text-sm`
42
- - Meta/timestamp: `text-gray-500 text-xs`
43
-
44
- **Action Buttons** (icon buttons within cards):
45
- - Base: `p-2 text-gray-400 hover:text-white hover:bg-gray-700/50 rounded transition-colors`
46
- - Danger: `text-red-400 hover:text-red-300`
47
- - Disabled: `opacity-50 cursor-not-allowed`
48
-
49
- **State Variants**:
50
- - Deleted/faded: `opacity-20 transition-opacity duration-200`
51
- - Loading: `animate-pulse bg-gray-800`
52
-
53
- ---
54
-
55
- ### NotificationCard
56
-
57
- **File**: `src/components/notifications/NotificationCard.tsx`
58
-
59
- ```typescript
60
- interface NotificationCardProps {
61
- notification: Notification
62
- onMarkAsRead: (id: string) => void
63
- onOpenFriendRequest?: (notification: Notification) => void
64
- }
65
- ```
66
-
67
- **Features**:
68
- - Unread indicator: blue dot (w-2 h-2) on left side
69
- - Avatar circle (w-10 h-10) or type-specific icon with colored background
70
- - Content: title (truncated), message (line-clamp-2), relative timestamp
71
- - Message count badge (top-right, blue pill)
72
- - **Swipe-to-dismiss**: horizontal swipe >80px threshold
73
- - Reveal background: `bg-blue-600/30`
74
- - Smooth dismiss animation (300ms translateX)
75
- - Calls `onMarkAsRead` on dismiss
76
- - Type-specific icons: `friend_request`, `friend_accepted`, `group_invite`, `new_message`, `system`, `memory_published`, `memory_comment`, `organize_nudge`
77
-
78
- ---
79
-
80
- ### CardList (Generic Feed Primitive)
81
-
82
- **File**: `src/components/feed/FeedList.tsx`
83
-
84
- ```typescript
85
- interface CardListProps<T> {
86
- items: T[]
87
- loading: boolean
88
- error: string | null
89
- renderItem: (item: T, index: number) => ReactNode
90
- emptyIcon: ReactNode
91
- emptyMessage: string
92
- skeletonCount?: number // default: 4
93
- }
94
-
95
- export function FeedList<T>({ items, loading, error, renderItem, emptyIcon, emptyMessage, skeletonCount = 4 }: CardListProps<T>) {
96
- // Error state: red banner
97
- if (error) return <div className="text-red-400 p-4 ...">{error}</div>
98
-
99
- // Loading state: skeleton cards
100
- if (loading) return (
101
- <div className="space-y-2">
102
- {Array.from({ length: skeletonCount }).map((_, i) => (
103
- <div key={i} className="bg-gray-800 rounded-xl h-32 animate-pulse" />
104
- ))}
105
- </div>
106
- )
107
-
108
- // Empty state: centered icon + message
109
- if (items.length === 0) return (
110
- <div className="flex flex-col items-center justify-center py-12 text-gray-500">
111
- {emptyIcon}
112
- <p className="mt-2">{emptyMessage}</p>
113
- </div>
114
- )
115
-
116
- // Items
117
- return <div className="space-y-2">{items.map(renderItem)}</div>
118
- }
119
- ```
120
-
121
- **Usage**:
122
-
123
- ```typescript
124
- <FeedList
125
- items={conversations}
126
- loading={loading}
127
- error={error}
128
- renderItem={(conv, i) => <ConversationCard key={conv.id} conversation={conv} />}
129
- emptyIcon={<MessageSquare className="w-12 h-12 text-gray-600" />}
130
- emptyMessage="No conversations yet"
131
- skeletonCount={6}
132
- />
133
- ```
134
-
135
- ---
136
-
137
- ### Virtualized Lists (react-virtuoso)
138
-
139
- For feeds with many items, use `react-virtuoso` instead of mapping all items to DOM. Two usage patterns exist:
140
-
141
- #### Pattern A: Window-Scroll Feed (Memories, Spaces, Groups, Profiles)
142
-
143
- Used for page-level feeds where the entire page scrolls. `useWindowScroll` delegates scrolling to the browser window.
144
-
145
- ```typescript
146
- import { Virtuoso } from 'react-virtuoso'
147
-
148
- <Virtuoso
149
- useWindowScroll
150
- data={memories}
151
- endReached={() => {
152
- if (hasMore && !loading && !loadingMore) {
153
- loadFeed(false) // Append next page
154
- }
155
- }}
156
- itemContent={(index, memory) => (
157
- <div className="pb-2">
158
- <MemoryCard memory={memory} source={source} />
159
- </div>
160
- )}
161
- components={{
162
- Footer: () =>
163
- loadingMore ? (
164
- <div className="flex justify-center py-4">
165
- <Loader2 className="w-5 h-5 animate-spin text-gray-500" />
166
- </div>
167
- ) : null,
168
- }}
169
- />
170
- ```
171
-
172
- **Key props**:
173
- - `useWindowScroll`: Scroll container is the browser window, not Virtuoso's own div
174
- - `data`: The array of items to render
175
- - `endReached`: Callback when user scrolls to the bottom — trigger load-more
176
- - `itemContent`: Render function per item (wraps card in `pb-2` for gap)
177
- - `components.Footer`: Loading spinner while fetching next page
178
-
179
- **Used in**: `/memories`, `SpacesFeed`, `ProfileMemoriesFeed`, `GroupMemories`
180
-
181
- #### Pattern B: Container-Scroll Chat (MessageList)
182
-
183
- Used for chat where messages prepend from the top and the container has a fixed height.
184
-
185
- ```typescript
186
- import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
187
-
188
- const virtuosoRef = useRef<VirtuosoHandle>(null)
189
-
190
- <Virtuoso
191
- ref={virtuosoRef}
192
- className="flex-grow h-0"
193
- firstItemIndex={firstItemIndex}
194
- initialTopMostItemIndex={items.length - 1}
195
- data={items}
196
- startReached={() => {
197
- if (!isLoadingMore && hasMore && onLoadMore) {
198
- setIsLoadingMore(true)
199
- onLoadMore()
200
- }
201
- }}
202
- itemContent={(index, item) => <Message ... />}
203
- />
204
-
205
- // Programmatic scroll to bottom
206
- virtuosoRef.current?.scrollToIndex({ index: 'LAST', behavior: 'smooth' })
207
- ```
208
-
209
- **Key props**:
210
- - `firstItemIndex`: Enables stable prepend — set to a large number minus item count, decrement as older messages load
211
- - `initialTopMostItemIndex`: Start at bottom (`items.length - 1`)
212
- - `startReached`: Callback when user scrolls to the top — load older messages
213
- - `ref` (`VirtuosoHandle`): Exposes `scrollToIndex` for programmatic scroll (new message arrival, search result navigation)
214
-
215
- **Used in**: `MessageList`
216
-
217
- #### When to Use Each
218
-
219
- | Pattern | When | Scroll Container |
220
- |---|---|---|
221
- | FeedList (non-virtualized) | < ~50 items, simple lists | Parent div |
222
- | Virtuoso `useWindowScroll` | Feed pages with infinite scroll | Browser window |
223
- | Virtuoso container-scroll | Chat with prepend, fixed height | Virtuoso div |
224
-
225
- ---
226
-
227
- ## Anti-Patterns
228
-
229
- ### Inconsistent Card Styling
230
-
231
- ```typescript
232
- // Bad: Custom card styling that doesn't match the system
233
- <div className="bg-white rounded-md p-2 shadow">{content}</div>
234
-
235
- // Good: Use standard card classes
236
- <div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-4">
237
- {content}
238
- </div>
239
- ```
240
-
241
- ### Reimplementing Loading/Empty/Error States
242
-
243
- ```typescript
244
- // Bad: Custom loading/empty per page
245
- {loading ? <Spinner /> : items.length === 0 ? <p>Empty</p> : items.map(...)}
246
-
247
- // Good: Use CardList/FeedList
248
- <FeedList items={items} loading={loading} error={error}
249
- renderItem={(item) => <MyCard item={item} />}
250
- emptyIcon={<Icon />} emptyMessage="Nothing here" />
251
- ```
252
-
253
- ---
254
-
255
- ## Checklist
256
-
257
- - [ ] Cards use `bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-4`
258
- - [ ] Text hierarchy follows: white (title), gray-400 (subtitle), gray-500 (meta)
259
- - [ ] Use `FeedList` for small static lists with loading/empty/error states
260
- - [ ] Use `Virtuoso` with `useWindowScroll` for feed pages with infinite scroll
261
- - [ ] Use `Virtuoso` container-scroll with `firstItemIndex` for chat-style prepend lists
262
- - [ ] Wrap each Virtuoso item in `<div className="pb-2">` for consistent card gap
263
- - [ ] Provide `components.Footer` with loading spinner for load-more feedback
264
- - [ ] Swipe-to-dismiss uses 80px threshold with reveal background
265
- - [ ] Deleted/faded items use `opacity-20` transition
266
-
267
- ---
268
-
269
- **Status**: Stable
270
- **Last Updated**: 2026-03-14
271
- **Contributors**: Community