@tanstack/cta-framework-react-cra 0.28.0 → 0.29.1

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.
@@ -193,3 +193,171 @@ If you don't want a header link you can omit the `url` and `name` properties.
193
193
  You **MUST** specify routes in the `info.json` file if your add-on supports the `code-router` mode. This is because the `code-routers` setup needs to import the routes in order to add them to the router.
194
194
 
195
195
  By convension you should prefix demo routes with `demo` to make it clear that they are demo routes so they can be easily identified and removed.
196
+
197
+ # Add-on Options
198
+
199
+ The CTA framework supports configurable add-ons through an options system that allows users to customize add-on behavior during creation. This enables more flexible and reusable add-ons that can adapt to different use cases.
200
+
201
+ ## Overview
202
+
203
+ Add-on options allow developers to create configurable add-ons where users can select from predefined choices that affect:
204
+
205
+ - Which files are included in the generated project
206
+ - Template variable values used during file generation
207
+ - Package dependencies that get installed
208
+ - Configuration file contents
209
+
210
+ ## Configuration Format
211
+
212
+ Options are defined in the `info.json` file using the following schema:
213
+
214
+ ```json
215
+ {
216
+ "name": "My Add-on",
217
+ "description": "A configurable add-on",
218
+ "options": {
219
+ "optionName": {
220
+ "type": "select",
221
+ "label": "Display Label",
222
+ "description": "Optional description shown to users",
223
+ "default": "defaultValue",
224
+ "options": [
225
+ { "value": "option1", "label": "Option 1" },
226
+ { "value": "option2", "label": "Option 2" }
227
+ ]
228
+ }
229
+ }
230
+ }
231
+ ```
232
+
233
+ ### Option Types
234
+
235
+ #### Select Options
236
+
237
+ The `select` type allows users to choose from a predefined list of options:
238
+
239
+ ```json
240
+ "database": {
241
+ "type": "select",
242
+ "label": "Database Provider",
243
+ "description": "Choose your database provider",
244
+ "default": "postgres",
245
+ "options": [
246
+ { "value": "postgres", "label": "PostgreSQL" },
247
+ { "value": "mysql", "label": "MySQL" },
248
+ { "value": "sqlite", "label": "SQLite" }
249
+ ]
250
+ }
251
+ ```
252
+
253
+ **Properties:**
254
+
255
+ - `type`: Must be `"select"`
256
+ - `label`: Display text shown to users
257
+ - `description`: Optional help text
258
+ - `default`: Default value that must match one of the option values
259
+ - `options`: Array of value/label pairs
260
+
261
+ ## Template Usage
262
+
263
+ Option values are available in EJS templates through the `addOnOption` variable:
264
+
265
+ ```ejs
266
+ <!-- Access option value -->
267
+ <% if (addOnOption.myAddOnId.database === 'postgres') { %>
268
+ PostgreSQL specific code
269
+ <% } %>
270
+
271
+ <!-- Use option value in output -->
272
+ const driver = '<%= addOnOption.myAddOnId.database %>'
273
+ ```
274
+
275
+ The structure is: `addOnOption.{addOnId}.{optionName}`
276
+
277
+ ### Template Conditional Logic
278
+
279
+ Within template files, use `ignoreFile()` to skip file generation:
280
+
281
+ ```ejs
282
+ <% if (addOnOption.prisma.database !== 'postgres') { ignoreFile() } %>
283
+ import { PrismaClient } from '@prisma/client'
284
+
285
+ declare global {
286
+ var __prisma: PrismaClient | undefined
287
+ }
288
+
289
+ export const prisma = globalThis.__prisma || new PrismaClient()
290
+
291
+ if (process.env.NODE_ENV !== 'production') {
292
+ globalThis.__prisma = prisma
293
+ }
294
+ ```
295
+
296
+ ## Complete Example: Prisma Add-on
297
+
298
+ Here's how the Prisma add-on implements configurable database support:
299
+
300
+ ### Examples
301
+
302
+ Configuration in `info.json`:
303
+
304
+ ```json
305
+ {
306
+ "name": "Prisma ORM",
307
+ "description": "Add Prisma ORM with configurable database support to your application.",
308
+ "options": {
309
+ "database": {
310
+ "type": "select",
311
+ "label": "Database Provider",
312
+ "description": "Choose your database provider",
313
+ "default": "postgres",
314
+ "options": [
315
+ { "value": "postgres", "label": "PostgreSQL" },
316
+ { "value": "mysql", "label": "MySQL" },
317
+ { "value": "sqlite", "label": "SQLite" }
318
+ ]
319
+ }
320
+ }
321
+ }
322
+ ```
323
+
324
+ Code in `package.json.ejs`:
325
+
326
+ ```ejs
327
+ {
328
+ "prisma": "^6.16.3",
329
+ "@prisma/client": "^6.16.3"<% if (addOnOption.prisma.database === 'postgres') { %>,
330
+ "pg": "^8.11.0",
331
+ "@types/pg": "^8.10.0"<% } else if (addOnOption.prisma.database === 'mysql') { %>,
332
+ "mysql2": "^3.6.0"<% } else if (addOnOption.prisma.database === 'sqlite') { %><% } %>
333
+ }
334
+ ```
335
+
336
+ ## CLI Usage
337
+
338
+ ### Interactive Mode
339
+
340
+ When using the CLI interactively, users are prompted for each option:
341
+
342
+ ```bash
343
+ create-tsrouter-app my-app
344
+ # User selects Prisma add-on
345
+ # CLI prompts: "Prisma ORM: Database Provider" with options
346
+ ```
347
+
348
+ ### Non-Interactive Mode
349
+
350
+ Options can be specified via JSON configuration:
351
+
352
+ ```bash
353
+ create-tsrouter-app my-app --add-ons prisma --add-on-config '{"prisma":{"database":"mysql"}}'
354
+ ```
355
+
356
+ ## Best Practices
357
+
358
+ 1. **Use descriptive labels** - Make option purposes clear to users
359
+ 2. **Provide sensible defaults** - Choose the most common use case
360
+ 3. **Group related files** - Use consistent prefixing for option-specific files
361
+ 4. **Document options** - Include descriptions to help users understand choices
362
+ 5. **Test all combinations** - Ensure each option value generates working code
363
+ 6. **Use validation** - The system validates options against the schema automatically
@@ -0,0 +1,7 @@
1
+ <% if (addOnOption.prisma.database === 'postgres') { %>
2
+ # Database URL for PostgreSQL
3
+ DATABASE_URL="postgresql://username:password@localhost:5432/mydb"<% } else if (addOnOption.prisma.database === 'mysql') { %>
4
+ # Database URL for MySQL
5
+ DATABASE_URL="mysql://username:password@localhost:3306/mydb"<% } else if (addOnOption.prisma.database === 'sqlite') { %>
6
+ # Database URL for SQLite
7
+ DATABASE_URL="file:./dev.db"<% } %>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 256 310" width="256" height="310" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path fill="#fff" d="M254.313 235.519L148 9.749A17.063 17.063 0 00133.473.037a16.87 16.87 0 00-15.533 8.052L2.633 194.848a17.465 17.465 0 00.193 18.747L59.2 300.896a18.13 18.13 0 0020.363 7.489l163.599-48.392a17.929 17.929 0 0011.26-9.722 17.542 17.542 0 00-.101-14.76l-.008.008zm-23.802 9.683l-138.823 41.05c-4.235 1.26-8.3-2.411-7.419-6.685l49.598-237.484c.927-4.443 7.063-5.147 9.003-1.035l91.814 194.973a6.63 6.63 0 01-4.18 9.18h.007z"/></svg>
@@ -0,0 +1,14 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "<%= addOnOption.prisma.database === "postgres" ? "postgresql" : addOnOption.prisma.database %>"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ model Todo {
11
+ id Int @id @default(autoincrement())
12
+ title String
13
+ createdAt DateTime @default(now())
14
+ }
@@ -0,0 +1,30 @@
1
+ import { PrismaClient } from '@prisma/client'
2
+
3
+ const prisma = new PrismaClient()
4
+
5
+ async function main() {
6
+ console.log('🌱 Seeding database...')
7
+
8
+ // Clear existing todos
9
+ await prisma.todo.deleteMany()
10
+
11
+ // Create example todos
12
+ const todos = await prisma.todo.createMany({
13
+ data: [
14
+ { title: 'Buy groceries' },
15
+ { title: 'Read a book' },
16
+ { title: 'Workout' },
17
+ ],
18
+ })
19
+
20
+ console.log(`✅ Created ${todos.count} todos`)
21
+ }
22
+
23
+ main()
24
+ .catch((e) => {
25
+ console.error('❌ Error seeding database:', e)
26
+ process.exit(1)
27
+ })
28
+ .finally(async () => {
29
+ await prisma.$disconnect()
30
+ })
@@ -0,0 +1,11 @@
1
+ import { PrismaClient } from '@prisma/client'
2
+
3
+ declare global {
4
+ var __prisma: PrismaClient | undefined
5
+ }
6
+
7
+ export const prisma = globalThis.__prisma || new PrismaClient()
8
+
9
+ if (process.env.NODE_ENV !== 'production') {
10
+ globalThis.__prisma = prisma
11
+ }
@@ -0,0 +1,186 @@
1
+ import { createFileRoute, useRouter } from '@tanstack/react-router'
2
+ import { createServerFn } from '@tanstack/react-start'
3
+ import { prisma } from '@/db'
4
+
5
+ const getTodos = createServerFn({
6
+ method: 'GET',
7
+ }).handler(async () => {
8
+ return await prisma.todo.findMany({
9
+ orderBy: { createdAt: 'desc' },
10
+ })
11
+ })
12
+
13
+ const createTodo = createServerFn({
14
+ method: 'POST',
15
+ })
16
+ .inputValidator((data: { title: string }) => data)
17
+ .handler(async ({ data }) => {
18
+ return await prisma.todo.create({
19
+ data,
20
+ })
21
+ })
22
+
23
+ export const Route = createFileRoute('/demo/prisma')({
24
+ component: DemoPrisma,
25
+ loader: async () => await getTodos(),
26
+ })
27
+
28
+ function DemoPrisma() {
29
+ const router = useRouter()
30
+ const todos = Route.useLoaderData()
31
+
32
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
33
+ e.preventDefault()
34
+ const formData = new FormData(e.target as HTMLFormElement)
35
+ const title = formData.get('title') as string
36
+
37
+ if (!title) return
38
+
39
+ try {
40
+ await createTodo({ data: { title } })
41
+ router.invalidate()
42
+ ;(e.target as HTMLFormElement).reset()
43
+ } catch (error) {
44
+ console.error('Failed to create todo:', error)
45
+ }
46
+ }
47
+
48
+ return (
49
+ <div
50
+ className="flex items-center justify-center min-h-screen p-4 text-white"
51
+ style={{
52
+ background:
53
+ 'linear-gradient(135deg, #0c1a2b 0%, #1a2332 50%, #16202e 100%)',
54
+ }}
55
+ >
56
+ <div
57
+ className="w-full max-w-2xl p-8 rounded-xl shadow-2xl border border-white/10"
58
+ style={{
59
+ background:
60
+ 'linear-gradient(135deg, rgba(22, 32, 46, 0.95) 0%, rgba(12, 26, 43, 0.95) 100%)',
61
+ backdropFilter: 'blur(10px)',
62
+ }}
63
+ >
64
+ <div
65
+ className="flex items-center justify-center gap-4 mb-8 p-4 rounded-lg"
66
+ style={{
67
+ background:
68
+ 'linear-gradient(90deg, rgba(93, 103, 227, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%)',
69
+ border: '1px solid rgba(93, 103, 227, 0.2)',
70
+ }}
71
+ >
72
+ <div className="relative group">
73
+ <div className="absolute -inset-2 bg-gradient-to-r from-indigo-500 via-purple-500 to-indigo-500 rounded-lg blur-lg opacity-60 group-hover:opacity-100 transition duration-500"></div>
74
+ <div className="relative bg-gradient-to-br from-indigo-600 to-purple-600 p-3 rounded-lg">
75
+ <img
76
+ src="/prisma.svg"
77
+ alt="Prisma Logo"
78
+ className="w-8 h-8 transform group-hover:scale-110 transition-transform duration-300"
79
+ />
80
+ </div>
81
+ </div>
82
+ <h1 className="text-3xl font-bold bg-gradient-to-r from-indigo-300 via-purple-300 to-indigo-300 text-transparent bg-clip-text">
83
+ Prisma Database Demo
84
+ </h1>
85
+ </div>
86
+
87
+ <h2 className="text-2xl font-bold mb-4 text-indigo-200">Todos</h2>
88
+
89
+ <ul className="space-y-3 mb-6">
90
+ {todos.map((todo) => (
91
+ <li
92
+ key={todo.id}
93
+ className="rounded-lg p-4 shadow-md border transition-all hover:scale-[1.02] cursor-pointer group"
94
+ style={{
95
+ background:
96
+ 'linear-gradient(135deg, rgba(93, 103, 227, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%)',
97
+ borderColor: 'rgba(93, 103, 227, 0.3)',
98
+ }}
99
+ >
100
+ <div className="flex items-center justify-between">
101
+ <span className="text-lg font-medium text-white group-hover:text-indigo-200 transition-colors">
102
+ {todo.title}
103
+ </span>
104
+ <span className="text-xs text-indigo-300/70">#{todo.id}</span>
105
+ </div>
106
+ </li>
107
+ ))}
108
+ {todos.length === 0 && (
109
+ <li className="text-center py-8 text-indigo-300/70">
110
+ No todos yet. Create one below!
111
+ </li>
112
+ )}
113
+ </ul>
114
+
115
+ <form onSubmit={handleSubmit} className="flex gap-2">
116
+ <input
117
+ type="text"
118
+ name="title"
119
+ placeholder="Add a new todo..."
120
+ className="flex-1 px-4 py-3 rounded-lg border focus:outline-none focus:ring-2 transition-all text-white placeholder-indigo-300/50"
121
+ style={{
122
+ background: 'rgba(93, 103, 227, 0.1)',
123
+ borderColor: 'rgba(93, 103, 227, 0.3)',
124
+ focusRing: 'rgba(93, 103, 227, 0.5)',
125
+ }}
126
+ />
127
+ <button
128
+ type="submit"
129
+ className="px-6 py-3 font-semibold rounded-lg shadow-lg transition-all duration-200 hover:shadow-xl hover:scale-105 active:scale-95 whitespace-nowrap"
130
+ style={{
131
+ background: 'linear-gradient(135deg, #5d67e3 0%, #8b5cf6 100%)',
132
+ color: 'white',
133
+ }}
134
+ >
135
+ Add Todo
136
+ </button>
137
+ </form>
138
+
139
+ <div
140
+ className="mt-8 p-6 rounded-lg border"
141
+ style={{
142
+ background: 'rgba(93, 103, 227, 0.05)',
143
+ borderColor: 'rgba(93, 103, 227, 0.2)',
144
+ }}
145
+ >
146
+ <h3 className="text-lg font-semibold mb-2 text-indigo-200">
147
+ Powered by Prisma ORM
148
+ </h3>
149
+ <p className="text-sm text-indigo-300/80 mb-4">
150
+ Next-generation ORM for Node.js & TypeScript with PostgreSQL
151
+ </p>
152
+ <div className="space-y-2 text-sm">
153
+ <p className="text-indigo-200 font-medium">Setup Instructions:</p>
154
+ <ol className="list-decimal list-inside space-y-2 text-indigo-300/80">
155
+ <li>
156
+ Configure your{' '}
157
+ <code className="px-2 py-1 rounded bg-black/30 text-purple-300">
158
+ DATABASE_URL
159
+ </code>{' '}
160
+ in .env.local
161
+ </li>
162
+ <li>
163
+ Run:{' '}
164
+ <code className="px-2 py-1 rounded bg-black/30 text-purple-300">
165
+ npx prisma generate
166
+ </code>
167
+ </li>
168
+ <li>
169
+ Run:{' '}
170
+ <code className="px-2 py-1 rounded bg-black/30 text-purple-300">
171
+ npx prisma db push
172
+ </code>
173
+ </li>
174
+ <li>
175
+ Optional:{' '}
176
+ <code className="px-2 py-1 rounded bg-black/30 text-purple-300">
177
+ npx prisma studio
178
+ </code>
179
+ </li>
180
+ </ol>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ )
186
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "Prisma",
3
+ "description": "Add Prisma Postgres, or Prisma ORM with other DBs to your application.",
4
+ "phase": "add-on",
5
+ "type": "add-on",
6
+ "link": "https://www.prisma.io/",
7
+ "modes": ["file-router"],
8
+ "dependsOn": ["start"],
9
+ "postInitSpecialSteps": ["post-init-script"],
10
+ "options": {
11
+ "database": {
12
+ "type": "select",
13
+ "label": "Database Provider",
14
+ "description": "Choose your database provider",
15
+ "default": "postgres",
16
+ "options": [
17
+ { "value": "postgres", "label": "Prisma PostgreSQL" },
18
+ { "value": "sqlite", "label": "SQLite" },
19
+ { "value": "mysql", "label": "MySQL" }
20
+ ]
21
+ }
22
+ },
23
+ "routes": [
24
+ {
25
+ "icon": "Database",
26
+ "url": "/demo/prisma",
27
+ "name": "Prisma",
28
+ "path": "src/routes/demo/prisma.tsx",
29
+ "jsName": "DemoPrisma"
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "dependencies": {
3
+ "prisma": "^6.16.3",
4
+ "@prisma/client": "^6.16.3"<% if (addOnOption.prisma.database === 'postgres') { %>,
5
+ "pg": "^8.11.0"<% } %><% if (addOnOption.prisma.database === 'mysql') { %>,
6
+ "mysql2": "^3.6.0"<% } %>
7
+ },
8
+ "devDependencies": {
9
+ "dotenv-cli": "^10.0.0",
10
+ "ts-node": "^10.9.2",
11
+ <% if (addOnOption.prisma.database === 'postgres') { %>
12
+ "@types/pg": "^8.10.0"<% } %><% if (addOnOption.prisma.database === 'mysql') { %>
13
+ "@types/mysql2": "^3.6.0"<% } %><% if (addOnOption.prisma.database === 'sqlite') { %>
14
+ "@types/better-sqlite3": "^7.6.0"<% } %>
15
+ },
16
+ "scripts": {<% if (addOnOption.prisma.database === 'postgres') { %>
17
+ "post-cta-init": "npx create-db@latest",<% } %>
18
+ "db:generate": "dotenv -e .env.local -- prisma generate",
19
+ "db:push": "dotenv -e .env.local -- prisma db push",
20
+ "db:migrate": "dotenv -e .env.local -- prisma migrate dev",
21
+ "db:studio": "dotenv -e .env.local -- prisma studio",
22
+ "db:seed": "dotenv -e .env.local -- prisma db seed"
23
+ },
24
+ "prisma": {
25
+ "seed": "ts-node seed.ts"
26
+ }
27
+ }
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 256 310" width="256" height="310" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path fill="#000" d="M254.313 235.519L148 9.749A17.063 17.063 0 00133.473.037a16.87 16.87 0 00-15.533 8.052L2.633 194.848a17.465 17.465 0 00.193 18.747L59.2 300.896a18.13 18.13 0 0020.363 7.489l163.599-48.392a17.929 17.929 0 0011.26-9.722 17.542 17.542 0 00-.101-14.76l-.008.008zm-23.802 9.683l-138.823 41.05c-4.235 1.26-8.3-2.411-7.419-6.685l49.598-237.484c.927-4.443 7.063-5.147 9.003-1.035l91.814 194.973a6.63 6.63 0 01-4.18 9.18h.007z"/></svg>
@@ -1,4 +1,45 @@
1
- import fs from 'node:fs'
1
+ <% if (addOnEnabled.cloudflare) { %>import { useState } from "react";
2
+ import { createFileRoute } from "@tanstack/react-router";
3
+ import { createServerFn } from "@tanstack/react-start";
4
+
5
+ const getCurrentServerTime = createServerFn({
6
+ method: "GET",
7
+ }).handler(async () => await new Date().toISOString());
8
+
9
+ export const Route = createFileRoute("/demo/start/server-funcs")({
10
+ component: Home,
11
+ loader: async () => await getCurrentServerTime(),
12
+ });
13
+
14
+ function Home() {
15
+ const originalTime = Route.useLoaderData();
16
+ const [time, setTime] = useState(originalTime);
17
+
18
+ return (
19
+ <div
20
+ className="flex items-center justify-center min-h-screen bg-gradient-to-br from-zinc-800 to-black p-4 text-white"
21
+ style={{
22
+ backgroundImage:
23
+ "radial-gradient(50% 50% at 20% 60%, #23272a 0%, #18181b 50%, #000000 100%)",
24
+ }}
25
+ >
26
+ <div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
27
+ <h1 className="text-2xl mb-4">Start Server Functions - Server Time</h1>
28
+ <div className="flex flex-col gap-2">
29
+ <div className="text-xl">Starting Time: {originalTime}</div>
30
+ <div className="text-xl">Current Time: {time}</div>
31
+ <button
32
+ className="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-500/50 disabled:cursor-not-allowed text-white font-bold py-3 px-4 rounded-lg transition-colors"
33
+ onClick={async () => setTime(await getCurrentServerTime())}
34
+ >
35
+ Refresh
36
+ </button>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ );
41
+ }
42
+ <% } else { %>import fs from 'node:fs'
2
43
  import { useCallback, useState } from 'react'
3
44
  import { createFileRoute, useRouter } from '@tanstack/react-router'
4
45
  import { createServerFn } from '@tanstack/react-start'
@@ -95,3 +136,4 @@ function Home() {
95
136
  </div>
96
137
  )
97
138
  }
139
+ <% } %>
@@ -7,42 +7,42 @@
7
7
  "type": "add-on",
8
8
  "routes": [
9
9
  {
10
- "icon": "Server",
10
+ "icon": "SquareFunction",
11
11
  "url": "/demo/start/server-funcs",
12
12
  "name": "Start - Server Functions",
13
13
  "path": "src/routes/demo.start.server-funcs.tsx",
14
14
  "jsName": "StartServerFuncsDemo"
15
15
  },
16
16
  {
17
- "icon": "Server",
17
+ "icon": "Network",
18
18
  "url": "/demo/start/api-request",
19
19
  "name": "Start - API Request",
20
20
  "path": "src/routes/demo.start.api-request.tsx",
21
21
  "jsName": "StartApiRequestDemo"
22
22
  },
23
23
  {
24
- "icon": "Server",
24
+ "icon": "StickyNote",
25
25
  "url": "/demo/start/ssr",
26
26
  "name": "Start - SSR Demos",
27
27
  "path": "src/routes/demo.start.ssr.index.tsx",
28
28
  "jsName": "StartSSRDemo",
29
29
  "children": [
30
30
  {
31
- "icon": "Server",
31
+ "icon": "StickyNote",
32
32
  "url": "/demo/start/ssr/spa-mode",
33
33
  "name": "SPA Mode",
34
34
  "path": "src/routes/demo.start.ssr.spa-mode.tsx",
35
35
  "jsName": "StartSSRSpamodeDemo"
36
36
  },
37
37
  {
38
- "icon": "Server",
38
+ "icon": "StickyNote",
39
39
  "url": "/demo/start/ssr/full-ssr",
40
40
  "name": "Full SSR",
41
41
  "path": "src/routes/demo.start.ssr.full-ssr.tsx",
42
42
  "jsName": "StartSSRFullSsrDemo"
43
43
  },
44
44
  {
45
- "icon": "Server",
45
+ "icon": "StickyNote",
46
46
  "url": "/demo/start/ssr/data-only",
47
47
  "name": "Data Only",
48
48
  "path": "src/routes/demo.start.ssr.data-only.tsx",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cta-framework-react-cra",
3
- "version": "0.28.0",
3
+ "version": "0.29.1",
4
4
  "description": "CTA Framework for React (Create React App)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "author": "Jack Herrington <jherr@pobox.com>",
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
- "@tanstack/cta-engine": "0.28.0"
26
+ "@tanstack/cta-engine": "0.29.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^24.6.0",
@@ -6,10 +6,8 @@ const icons = new Set([
6
6
  "Menu",
7
7
  "X",
8
8
  "Home",
9
- "Globe",
10
9
  "ChevronDown",
11
10
  "ChevronRight",
12
- "Layers",
13
11
  ])
14
12
 
15
13
  for(const addOn of addOns) {
@@ -5,7 +5,7 @@
5
5
  "/.vscode/settings.json": "{\n \"files.watcherExclude\": {\n \"**/routeTree.gen.ts\": true\n },\n \"search.exclude\": {\n \"**/routeTree.gen.ts\": true\n },\n \"files.readonlyInclude\": {\n \"**/routeTree.gen.ts\": true\n }\n}\n",
6
6
  "/public/manifest.json": "{\n \"short_name\": \"TanStack App\",\n \"name\": \"Create TanStack App Sample\",\n \"icons\": [\n {\n \"src\": \"favicon.ico\",\n \"sizes\": \"64x64 32x32 24x24 16x16\",\n \"type\": \"image/x-icon\"\n },\n {\n \"src\": \"logo192.png\",\n \"type\": \"image/png\",\n \"sizes\": \"192x192\"\n },\n {\n \"src\": \"logo512.png\",\n \"type\": \"image/png\",\n \"sizes\": \"512x512\"\n }\n ],\n \"start_url\": \".\",\n \"display\": \"standalone\",\n \"theme_color\": \"#000000\",\n \"background_color\": \"#ffffff\"\n}\n",
7
7
  "/public/robots.txt": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n",
8
- "/src/components/Header.tsx": "import { Link } from '@tanstack/react-router'\n\nimport { useState } from 'react'\nimport {\n ChevronDown,\n ChevronRight,\n Globe,\n Home,\n Layers,\n Menu,\n Server,\n X,\n} from 'lucide-react'\n\nexport default function Header() {\n const [isOpen, setIsOpen] = useState(false)\n const [groupedExpanded, setGroupedExpanded] = useState<\n Record<string, boolean>\n >({})\n\n return (\n <>\n <header className=\"p-4 flex items-center bg-gray-800 text-white shadow-lg\">\n <button\n onClick={() => setIsOpen(true)}\n className=\"p-2 hover:bg-gray-700 rounded-lg transition-colors\"\n aria-label=\"Open menu\"\n >\n <Menu size={24} />\n </button>\n <h1 className=\"ml-4 text-xl font-semibold\">\n <Link to=\"/\">\n <img\n src=\"/tanstack-word-logo-white.svg\"\n alt=\"TanStack Logo\"\n className=\"h-10\"\n />\n </Link>\n </h1>\n </header>\n\n <aside\n className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${\n isOpen ? 'translate-x-0' : '-translate-x-full'\n }`}\n >\n <div className=\"flex items-center justify-between p-4 border-b border-gray-700\">\n <h2 className=\"text-xl font-bold\">Navigation</h2>\n <button\n onClick={() => setIsOpen(false)}\n className=\"p-2 hover:bg-gray-800 rounded-lg transition-colors\"\n aria-label=\"Close menu\"\n >\n <X size={24} />\n </button>\n </div>\n\n <nav className=\"flex-1 p-4 overflow-y-auto\">\n <Link\n to=\"/\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Home size={20} />\n <span className=\"font-medium\">Home</span>\n </Link>\n\n {/* Demo Links Start */}\n\n <Link\n to=\"/demo/start/server-funcs\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Server size={20} />\n <span className=\"font-medium\">Start - Server Functions</span>\n </Link>\n\n <Link\n to=\"/demo/start/api-request\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Server size={20} />\n <span className=\"font-medium\">Start - API Request</span>\n </Link>\n\n <div className=\"flex flex-row justify-between\">\n <Link\n to=\"/demo/start/ssr\"\n onClick={() => setIsOpen(false)}\n className=\"flex-1 flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex-1 flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Server size={20} />\n <span className=\"font-medium\">Start - SSR Demos</span>\n </Link>\n <button\n className=\"p-2 hover:bg-gray-800 rounded-lg transition-colors\"\n onClick={() =>\n setGroupedExpanded((prev) => ({\n ...prev,\n StartSSRDemo: !prev.StartSSRDemo,\n }))\n }\n >\n {groupedExpanded.StartSSRDemo ? (\n <ChevronDown size={20} />\n ) : (\n <ChevronRight size={20} />\n )}\n </button>\n </div>\n {groupedExpanded.StartSSRDemo && (\n <div className=\"flex flex-col ml-4\">\n <Link\n to=\"/demo/start/ssr/spa-mode\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Server size={20} />\n <span className=\"font-medium\">SPA Mode</span>\n </Link>\n\n <Link\n to=\"/demo/start/ssr/full-ssr\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Server size={20} />\n <span className=\"font-medium\">Full SSR</span>\n </Link>\n\n <Link\n to=\"/demo/start/ssr/data-only\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Server size={20} />\n <span className=\"font-medium\">Data Only</span>\n </Link>\n </div>\n )}\n\n {/* Demo Links End */}\n </nav>\n </aside>\n </>\n )\n}\n",
8
+ "/src/components/Header.tsx": "import { Link } from '@tanstack/react-router'\n\nimport { useState } from 'react'\nimport {\n ChevronDown,\n ChevronRight,\n Home,\n Menu,\n Network,\n SquareFunction,\n StickyNote,\n X,\n} from 'lucide-react'\n\nexport default function Header() {\n const [isOpen, setIsOpen] = useState(false)\n const [groupedExpanded, setGroupedExpanded] = useState<\n Record<string, boolean>\n >({})\n\n return (\n <>\n <header className=\"p-4 flex items-center bg-gray-800 text-white shadow-lg\">\n <button\n onClick={() => setIsOpen(true)}\n className=\"p-2 hover:bg-gray-700 rounded-lg transition-colors\"\n aria-label=\"Open menu\"\n >\n <Menu size={24} />\n </button>\n <h1 className=\"ml-4 text-xl font-semibold\">\n <Link to=\"/\">\n <img\n src=\"/tanstack-word-logo-white.svg\"\n alt=\"TanStack Logo\"\n className=\"h-10\"\n />\n </Link>\n </h1>\n </header>\n\n <aside\n className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${\n isOpen ? 'translate-x-0' : '-translate-x-full'\n }`}\n >\n <div className=\"flex items-center justify-between p-4 border-b border-gray-700\">\n <h2 className=\"text-xl font-bold\">Navigation</h2>\n <button\n onClick={() => setIsOpen(false)}\n className=\"p-2 hover:bg-gray-800 rounded-lg transition-colors\"\n aria-label=\"Close menu\"\n >\n <X size={24} />\n </button>\n </div>\n\n <nav className=\"flex-1 p-4 overflow-y-auto\">\n <Link\n to=\"/\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Home size={20} />\n <span className=\"font-medium\">Home</span>\n </Link>\n\n {/* Demo Links Start */}\n\n <Link\n to=\"/demo/start/server-funcs\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <SquareFunction size={20} />\n <span className=\"font-medium\">Start - Server Functions</span>\n </Link>\n\n <Link\n to=\"/demo/start/api-request\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Network size={20} />\n <span className=\"font-medium\">Start - API Request</span>\n </Link>\n\n <div className=\"flex flex-row justify-between\">\n <Link\n to=\"/demo/start/ssr\"\n onClick={() => setIsOpen(false)}\n className=\"flex-1 flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex-1 flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <StickyNote size={20} />\n <span className=\"font-medium\">Start - SSR Demos</span>\n </Link>\n <button\n className=\"p-2 hover:bg-gray-800 rounded-lg transition-colors\"\n onClick={() =>\n setGroupedExpanded((prev) => ({\n ...prev,\n StartSSRDemo: !prev.StartSSRDemo,\n }))\n }\n >\n {groupedExpanded.StartSSRDemo ? (\n <ChevronDown size={20} />\n ) : (\n <ChevronRight size={20} />\n )}\n </button>\n </div>\n {groupedExpanded.StartSSRDemo && (\n <div className=\"flex flex-col ml-4\">\n <Link\n to=\"/demo/start/ssr/spa-mode\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <StickyNote size={20} />\n <span className=\"font-medium\">SPA Mode</span>\n </Link>\n\n <Link\n to=\"/demo/start/ssr/full-ssr\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <StickyNote size={20} />\n <span className=\"font-medium\">Full SSR</span>\n </Link>\n\n <Link\n to=\"/demo/start/ssr/data-only\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <StickyNote size={20} />\n <span className=\"font-medium\">Data Only</span>\n </Link>\n </div>\n )}\n\n {/* Demo Links End */}\n </nav>\n </aside>\n </>\n )\n}\n",
9
9
  "/src/data/demo.punk-songs.ts": "import { createServerFn } from '@tanstack/react-start'\n\nexport const getPunkSongs = createServerFn({\n method: 'GET',\n}).handler(async () => [\n { id: 1, name: 'Teenage Dirtbag', artist: 'Wheatus' },\n { id: 2, name: 'Smells Like Teen Spirit', artist: 'Nirvana' },\n { id: 3, name: 'The Middle', artist: 'Jimmy Eat World' },\n { id: 4, name: 'My Own Worst Enemy', artist: 'Lit' },\n { id: 5, name: 'Fat Lip', artist: 'Sum 41' },\n { id: 6, name: 'All the Small Things', artist: 'blink-182' },\n { id: 7, name: 'Beverly Hills', artist: 'Weezer' },\n])\n",
10
10
  "/src/router.tsx": "import { createRouter } from '@tanstack/react-router'\n\n// Import the generated route tree\nimport { routeTree } from './routeTree.gen'\n\n// Create a new router instance\nexport const getRouter = () => {\n return createRouter({\n routeTree,\n scrollRestoration: true,\n defaultPreloadStaleTime: 0,\n })\n}\n",
11
11
  "/src/routes/__root.tsx": "import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'\nimport { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'\nimport { TanStackDevtools } from '@tanstack/react-devtools'\n\nimport Header from '../components/Header'\n\nimport appCss from '../styles.css?url'\n\nexport const Route = createRootRoute({\n head: () => ({\n meta: [\n {\n charSet: 'utf-8',\n },\n {\n name: 'viewport',\n content: 'width=device-width, initial-scale=1',\n },\n {\n title: 'TanStack Start Starter',\n },\n ],\n links: [\n {\n rel: 'stylesheet',\n href: appCss,\n },\n ],\n }),\n\n shellComponent: RootDocument,\n})\n\nfunction RootDocument({ children }: { children: React.ReactNode }) {\n return (\n <html lang=\"en\">\n <head>\n <HeadContent />\n </head>\n <body>\n <Header />\n {children}\n <TanStackDevtools\n config={{\n position: 'bottom-right',\n }}\n plugins={[\n {\n name: 'Tanstack Router',\n render: <TanStackRouterDevtoolsPanel />,\n },\n ]}\n />\n <Scripts />\n </body>\n </html>\n )\n}\n",
@@ -5,7 +5,7 @@
5
5
  "/.vscode/settings.json": "{\n \"files.watcherExclude\": {\n \"**/routeTree.gen.ts\": true\n },\n \"search.exclude\": {\n \"**/routeTree.gen.ts\": true\n },\n \"files.readonlyInclude\": {\n \"**/routeTree.gen.ts\": true\n }\n}\n",
6
6
  "/public/manifest.json": "{\n \"short_name\": \"TanStack App\",\n \"name\": \"Create TanStack App Sample\",\n \"icons\": [\n {\n \"src\": \"favicon.ico\",\n \"sizes\": \"64x64 32x32 24x24 16x16\",\n \"type\": \"image/x-icon\"\n },\n {\n \"src\": \"logo192.png\",\n \"type\": \"image/png\",\n \"sizes\": \"192x192\"\n },\n {\n \"src\": \"logo512.png\",\n \"type\": \"image/png\",\n \"sizes\": \"512x512\"\n }\n ],\n \"start_url\": \".\",\n \"display\": \"standalone\",\n \"theme_color\": \"#000000\",\n \"background_color\": \"#ffffff\"\n}\n",
7
7
  "/public/robots.txt": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n",
8
- "/src/components/Header.tsx": "import { Link } from '@tanstack/react-router'\n\nimport { useState } from 'react'\nimport {\n ChevronDown,\n ChevronRight,\n Globe,\n Home,\n Layers,\n Menu,\n Network,\n Server,\n X,\n} from 'lucide-react'\n\nexport default function Header() {\n const [isOpen, setIsOpen] = useState(false)\n const [groupedExpanded, setGroupedExpanded] = useState<\n Record<string, boolean>\n >({})\n\n return (\n <>\n <header className=\"p-4 flex items-center bg-gray-800 text-white shadow-lg\">\n <button\n onClick={() => setIsOpen(true)}\n className=\"p-2 hover:bg-gray-700 rounded-lg transition-colors\"\n aria-label=\"Open menu\"\n >\n <Menu size={24} />\n </button>\n <h1 className=\"ml-4 text-xl font-semibold\">\n <Link to=\"/\">\n <img\n src=\"/tanstack-word-logo-white.svg\"\n alt=\"TanStack Logo\"\n className=\"h-10\"\n />\n </Link>\n </h1>\n </header>\n\n <aside\n className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${\n isOpen ? 'translate-x-0' : '-translate-x-full'\n }`}\n >\n <div className=\"flex items-center justify-between p-4 border-b border-gray-700\">\n <h2 className=\"text-xl font-bold\">Navigation</h2>\n <button\n onClick={() => setIsOpen(false)}\n className=\"p-2 hover:bg-gray-800 rounded-lg transition-colors\"\n aria-label=\"Close menu\"\n >\n <X size={24} />\n </button>\n </div>\n\n <nav className=\"flex-1 p-4 overflow-y-auto\">\n <Link\n to=\"/\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Home size={20} />\n <span className=\"font-medium\">Home</span>\n </Link>\n\n {/* Demo Links Start */}\n\n <Link\n to=\"/demo/start/server-funcs\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Server size={20} />\n <span className=\"font-medium\">Start - Server Functions</span>\n </Link>\n\n <Link\n to=\"/demo/start/api-request\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Server size={20} />\n <span className=\"font-medium\">Start - API Request</span>\n </Link>\n\n <div className=\"flex flex-row justify-between\">\n <Link\n to=\"/demo/start/ssr\"\n onClick={() => setIsOpen(false)}\n className=\"flex-1 flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex-1 flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Server size={20} />\n <span className=\"font-medium\">Start - SSR Demos</span>\n </Link>\n <button\n className=\"p-2 hover:bg-gray-800 rounded-lg transition-colors\"\n onClick={() =>\n setGroupedExpanded((prev) => ({\n ...prev,\n StartSSRDemo: !prev.StartSSRDemo,\n }))\n }\n >\n {groupedExpanded.StartSSRDemo ? (\n <ChevronDown size={20} />\n ) : (\n <ChevronRight size={20} />\n )}\n </button>\n </div>\n {groupedExpanded.StartSSRDemo && (\n <div className=\"flex flex-col ml-4\">\n <Link\n to=\"/demo/start/ssr/spa-mode\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Server size={20} />\n <span className=\"font-medium\">SPA Mode</span>\n </Link>\n\n <Link\n to=\"/demo/start/ssr/full-ssr\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Server size={20} />\n <span className=\"font-medium\">Full SSR</span>\n </Link>\n\n <Link\n to=\"/demo/start/ssr/data-only\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Server size={20} />\n <span className=\"font-medium\">Data Only</span>\n </Link>\n </div>\n )}\n\n <Link\n to=\"/demo/tanstack-query\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Network size={20} />\n <span className=\"font-medium\">TanStack Query</span>\n </Link>\n\n {/* Demo Links End */}\n </nav>\n </aside>\n </>\n )\n}\n",
8
+ "/src/components/Header.tsx": "import { Link } from '@tanstack/react-router'\n\nimport { useState } from 'react'\nimport {\n ChevronDown,\n ChevronRight,\n Home,\n Menu,\n Network,\n SquareFunction,\n StickyNote,\n X,\n} from 'lucide-react'\n\nexport default function Header() {\n const [isOpen, setIsOpen] = useState(false)\n const [groupedExpanded, setGroupedExpanded] = useState<\n Record<string, boolean>\n >({})\n\n return (\n <>\n <header className=\"p-4 flex items-center bg-gray-800 text-white shadow-lg\">\n <button\n onClick={() => setIsOpen(true)}\n className=\"p-2 hover:bg-gray-700 rounded-lg transition-colors\"\n aria-label=\"Open menu\"\n >\n <Menu size={24} />\n </button>\n <h1 className=\"ml-4 text-xl font-semibold\">\n <Link to=\"/\">\n <img\n src=\"/tanstack-word-logo-white.svg\"\n alt=\"TanStack Logo\"\n className=\"h-10\"\n />\n </Link>\n </h1>\n </header>\n\n <aside\n className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${\n isOpen ? 'translate-x-0' : '-translate-x-full'\n }`}\n >\n <div className=\"flex items-center justify-between p-4 border-b border-gray-700\">\n <h2 className=\"text-xl font-bold\">Navigation</h2>\n <button\n onClick={() => setIsOpen(false)}\n className=\"p-2 hover:bg-gray-800 rounded-lg transition-colors\"\n aria-label=\"Close menu\"\n >\n <X size={24} />\n </button>\n </div>\n\n <nav className=\"flex-1 p-4 overflow-y-auto\">\n <Link\n to=\"/\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Home size={20} />\n <span className=\"font-medium\">Home</span>\n </Link>\n\n {/* Demo Links Start */}\n\n <Link\n to=\"/demo/start/server-funcs\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <SquareFunction size={20} />\n <span className=\"font-medium\">Start - Server Functions</span>\n </Link>\n\n <Link\n to=\"/demo/start/api-request\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Network size={20} />\n <span className=\"font-medium\">Start - API Request</span>\n </Link>\n\n <div className=\"flex flex-row justify-between\">\n <Link\n to=\"/demo/start/ssr\"\n onClick={() => setIsOpen(false)}\n className=\"flex-1 flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex-1 flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <StickyNote size={20} />\n <span className=\"font-medium\">Start - SSR Demos</span>\n </Link>\n <button\n className=\"p-2 hover:bg-gray-800 rounded-lg transition-colors\"\n onClick={() =>\n setGroupedExpanded((prev) => ({\n ...prev,\n StartSSRDemo: !prev.StartSSRDemo,\n }))\n }\n >\n {groupedExpanded.StartSSRDemo ? (\n <ChevronDown size={20} />\n ) : (\n <ChevronRight size={20} />\n )}\n </button>\n </div>\n {groupedExpanded.StartSSRDemo && (\n <div className=\"flex flex-col ml-4\">\n <Link\n to=\"/demo/start/ssr/spa-mode\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <StickyNote size={20} />\n <span className=\"font-medium\">SPA Mode</span>\n </Link>\n\n <Link\n to=\"/demo/start/ssr/full-ssr\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <StickyNote size={20} />\n <span className=\"font-medium\">Full SSR</span>\n </Link>\n\n <Link\n to=\"/demo/start/ssr/data-only\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <StickyNote size={20} />\n <span className=\"font-medium\">Data Only</span>\n </Link>\n </div>\n )}\n\n <Link\n to=\"/demo/tanstack-query\"\n onClick={() => setIsOpen(false)}\n className=\"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2\"\n activeProps={{\n className:\n 'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',\n }}\n >\n <Network size={20} />\n <span className=\"font-medium\">TanStack Query</span>\n </Link>\n\n {/* Demo Links End */}\n </nav>\n </aside>\n </>\n )\n}\n",
9
9
  "/src/data/demo.punk-songs.ts": "import { createServerFn } from '@tanstack/react-start'\n\nexport const getPunkSongs = createServerFn({\n method: 'GET',\n}).handler(async () => [\n { id: 1, name: 'Teenage Dirtbag', artist: 'Wheatus' },\n { id: 2, name: 'Smells Like Teen Spirit', artist: 'Nirvana' },\n { id: 3, name: 'The Middle', artist: 'Jimmy Eat World' },\n { id: 4, name: 'My Own Worst Enemy', artist: 'Lit' },\n { id: 5, name: 'Fat Lip', artist: 'Sum 41' },\n { id: 6, name: 'All the Small Things', artist: 'blink-182' },\n { id: 7, name: 'Beverly Hills', artist: 'Weezer' },\n])\n",
10
10
  "/src/integrations/tanstack-query/devtools.tsx": "import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'\n\nexport default {\n name: 'Tanstack Query',\n render: <ReactQueryDevtoolsPanel />,\n}\n",
11
11
  "/src/integrations/tanstack-query/root-provider.tsx": "import { QueryClient, QueryClientProvider } from '@tanstack/react-query'\n\nexport function getContext() {\n const queryClient = new QueryClient()\n return {\n queryClient,\n }\n}\n\nexport function Provider({\n children,\n queryClient,\n}: {\n children: React.ReactNode\n queryClient: QueryClient\n}) {\n return (\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n )\n}\n",