@emberkit/cli 0.4.0 → 0.5.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.
@@ -0,0 +1,407 @@
1
+ export const dashboardTemplate = {
2
+ "package.json": `{
3
+ "name": "{{name}}",
4
+ "version": "0.1.0",
5
+ "private": true,
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "emberkit dev",
9
+ "build": "emberkit build",
10
+ "preview": "emberkit preview"
11
+ },
12
+ "dependencies": {
13
+ "@emberkit/core": "^0.2.4",
14
+ "@emberkit/ui": "^0.2.3"
15
+ },
16
+ "devDependencies": {
17
+ "@emberkit/cli": "^0.2.4",
18
+ "typescript": "^5.7.0",
19
+ "vite": "^6.0.0",
20
+ "tailwindcss": "^4.0.0",
21
+ "@tailwindcss/vite": "^4.0.0"
22
+ }
23
+ }`,
24
+ "tsconfig.json": `{
25
+ "compilerOptions": {
26
+ "target": "ES2022",
27
+ "module": "ESNext",
28
+ "moduleResolution": "bundler",
29
+ "jsx": "react-jsx",
30
+ "jsxImportSource": "@emberkit/core",
31
+ "strict": true,
32
+ "esModuleInterop": true,
33
+ "skipLibCheck": true,
34
+ "forceConsistentCasingInFileNames": true,
35
+ "resolveJsonModule": true,
36
+ "isolatedModules": true,
37
+ "noEmit": true,
38
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
39
+ "paths": {
40
+ "@/*": ["./src/*"]
41
+ }
42
+ },
43
+ "include": ["src"],
44
+ "exclude": ["node_modules", "dist"]
45
+ }`,
46
+ "vite.config.ts": `import { defineConfig } from 'vite';
47
+ import { emberkitVitePlugin } from '@emberkit/core/vite-plugin';
48
+ import tailwindcss from '@tailwindcss/vite';
49
+
50
+ export default defineConfig({
51
+ plugins: [emberkitVitePlugin(), tailwindcss()],
52
+ server: {
53
+ port: 3000,
54
+ host: 'localhost',
55
+ },
56
+ esbuild: {
57
+ jsxImportSource: '@emberkit/core',
58
+ },
59
+ });`,
60
+ "index.html": `<!DOCTYPE html>
61
+ <html lang="en">
62
+ <head>
63
+ <meta charset="UTF-8">
64
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
65
+ <title>{{name}}</title>
66
+ <link rel="preconnect" href="https://fonts.googleapis.com">
67
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
68
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
69
+ </head>
70
+ <body id="app">
71
+ <script type="module" src="/src/index.tsx"></script>
72
+ </body>
73
+ </html>`,
74
+ "src/index.tsx": `import { render } from '@emberkit/core';
75
+ import { routes } from 'virtual:emberkit-routes';
76
+ import App from './routes/_layout';
77
+ import './styles.css';
78
+
79
+ const root = document.getElementById('app');
80
+
81
+ if (root) {
82
+ try {
83
+ render(App, root, { routes });
84
+ } catch (error) {
85
+ console.error('[entry] Render error:', error);
86
+ }
87
+ }`,
88
+ "src/styles.css": `@import "tailwindcss";
89
+
90
+ @theme {
91
+ --color-brand-50: #f0fdf4;
92
+ --color-brand-100: #dcfce7;
93
+ --color-brand-200: #bbf7d0;
94
+ --color-brand-300: #86efac;
95
+ --color-brand-400: #4ade80;
96
+ --color-brand-500: #22c55e;
97
+ --color-brand-600: #16a34a;
98
+ --color-brand-700: #15803d;
99
+ --font-sans: 'Inter', system-ui, sans-serif;
100
+ }
101
+
102
+ body {
103
+ @apply bg-gray-50 text-gray-900 font-sans;
104
+ }`,
105
+ "src/routes/_layout.tsx": `import type { RouteComponent } from '@emberkit/core';
106
+ import { signal } from '@emberkit/core';
107
+ import { Sidebar, Header } from '@emberkit/ui';
108
+
109
+ const Layout: RouteComponent = ({ children }) => {
110
+ const sidebarOpen = signal(true);
111
+
112
+ const sidebarItems = [
113
+ { label: 'Dashboard', href: '/dashboard', icon: 'grid' },
114
+ { label: 'Analytics', href: '/analytics', icon: 'chart' },
115
+ { label: 'Users', href: '/users', icon: 'users' },
116
+ { label: 'Projects', href: '/projects', icon: 'folder' },
117
+ { label: 'Settings', href: '/settings', icon: 'settings' },
118
+ ];
119
+
120
+ return (
121
+ <div className="min-h-screen flex">
122
+ <Sidebar
123
+ logo={<span className="font-bold text-lg">&#9889; {{name}}</span>}
124
+ items={sidebarItems}
125
+ collapsed={!sidebarOpen.value}
126
+ onToggle={() => { sidebarOpen.value = !sidebarOpen.value; }}
127
+ className="w-64"
128
+ />
129
+ <div className="flex-1 flex flex-col min-w-0">
130
+ <Header
131
+ title="Dashboard"
132
+ user={{ name: 'User', avatar: '' }}
133
+ onMenuClick={() => { sidebarOpen.value = !sidebarOpen.value; }}
134
+ />
135
+ <main className="flex-1 p-6 overflow-auto">
136
+ {children}
137
+ </main>
138
+ </div>
139
+ </div>
140
+ );
141
+ };
142
+
143
+ export default Layout;`,
144
+ "src/routes/index.tsx": `import type { RouteComponent } from '@emberkit/core';
145
+ import { Card, Badge } from '@emberkit/ui';
146
+
147
+ const DashboardPage: RouteComponent = () => {
148
+ const stats = [
149
+ { label: 'Total Revenue', value: '$45,231', change: '+20.1%', positive: true },
150
+ { label: 'Active Users', value: '2,338', change: '+15.3%', positive: true },
151
+ { label: 'New Signups', value: '1,247', change: '+8.2%', positive: true },
152
+ { label: 'Churn Rate', value: '2.4%', change: '-0.5%', positive: true },
153
+ ];
154
+
155
+ const recentActivity = [
156
+ { user: 'John Doe', action: 'created a new project', time: '2 minutes ago' },
157
+ { user: 'Jane Smith', action: 'upgraded to Pro plan', time: '15 minutes ago' },
158
+ { user: 'Mike Johnson', action: 'invited 3 team members', time: '1 hour ago' },
159
+ { user: 'Sarah Williams', action: 'published a new post', time: '2 hours ago' },
160
+ { user: 'Alex Brown', action: 'deleted an old project', time: '3 hours ago' },
161
+ ];
162
+
163
+ return (
164
+ <div className="space-y-6">
165
+ <div>
166
+ <h1 className="text-2xl font-bold">Welcome back</h1>
167
+ <p className="text-gray-600 mt-1">Here's what's happening with your account today.</p>
168
+ </div>
169
+
170
+ {/* Stats Grid */}
171
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
172
+ {stats.map((stat) => (
173
+ <Card key={stat.label} padding="lg">
174
+ <p className="text-sm text-gray-600">{stat.label}</p>
175
+ <p className="text-2xl font-bold mt-1">{stat.value}</p>
176
+ <Badge variant={stat.positive ? 'success' : 'danger'} size="sm" className="mt-2">
177
+ {stat.change}
178
+ </Badge>
179
+ </Card>
180
+ ))}
181
+ </div>
182
+
183
+ <div className="grid lg:grid-cols-2 gap-6">
184
+ {/* Chart Placeholder */}
185
+ <Card padding="lg">
186
+ <h3 className="font-semibold mb-4">Revenue Overview</h3>
187
+ <div className="h-64 bg-gray-100 rounded-lg flex items-center justify-center text-gray-500">
188
+ Chart placeholder - integrate your preferred charting library
189
+ </div>
190
+ </Card>
191
+
192
+ {/* Recent Activity */}
193
+ <Card padding="lg">
194
+ <h3 className="font-semibold mb-4">Recent Activity</h3>
195
+ <div className="space-y-4">
196
+ {recentActivity.map((item, i) => (
197
+ <div key={i} className="flex items-start gap-3 pb-4 border-b border-gray-100 last:border-0 last:pb-0">
198
+ <div className="w-8 h-8 rounded-full bg-brand-100 flex items-center justify-center text-brand-700 font-medium text-sm">
199
+ {item.user.charAt(0)}
200
+ </div>
201
+ <div className="flex-1 min-w-0">
202
+ <p className="text-sm">
203
+ <span className="font-medium">{item.user}</span>{' '}
204
+ {item.action}
205
+ </p>
206
+ <p className="text-xs text-gray-500 mt-0.5">{item.time}</p>
207
+ </div>
208
+ </div>
209
+ ))}
210
+ </div>
211
+ </Card>
212
+ </div>
213
+
214
+ {/* Quick Actions */}
215
+ <Card padding="lg">
216
+ <h3 className="font-semibold mb-4">Quick Actions</h3>
217
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
218
+ {[
219
+ { label: 'New Project', icon: '+' },
220
+ { label: 'Invite User', icon: '&#128100;' },
221
+ { label: 'View Reports', icon: '&#128202;' },
222
+ { label: 'Settings', icon: '&#9881;' },
223
+ ].map((action) => (
224
+ <button
225
+ key={action.label}
226
+ className="p-4 rounded-lg border border-gray-200 hover:border-brand-300 hover:bg-brand-50 transition-colors text-center"
227
+ >
228
+ <span className="text-2xl block mb-2">{action.icon}</span>
229
+ <span className="text-sm font-medium">{action.label}</span>
230
+ </button>
231
+ ))}
232
+ </div>
233
+ </Card>
234
+ </div>
235
+ );
236
+ };
237
+
238
+ export default DashboardPage;`,
239
+ "src/routes/users.tsx": `import type { RouteComponent } from '@emberkit/core';
240
+ import { Card, Badge, Button, Input } from '@emberkit/ui';
241
+ import { signal } from '@emberkit/core';
242
+
243
+ interface User {
244
+ id: number;
245
+ name: string;
246
+ email: string;
247
+ role: string;
248
+ status: 'active' | 'inactive' | 'pending';
249
+ lastActive: string;
250
+ }
251
+
252
+ const users: User[] = [
253
+ { id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin', status: 'active', lastActive: '2 min ago' },
254
+ { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'Editor', status: 'active', lastActive: '1 hour ago' },
255
+ { id: 3, name: 'Mike Johnson', email: 'mike@example.com', role: 'Viewer', status: 'inactive', lastActive: '3 days ago' },
256
+ { id: 4, name: 'Sarah Williams', email: 'sarah@example.com', role: 'Editor', status: 'active', lastActive: '5 min ago' },
257
+ { id: 5, name: 'Alex Brown', email: 'alex@example.com', role: 'Viewer', status: 'pending', lastActive: 'Never' },
258
+ ];
259
+
260
+ const UsersPage: RouteComponent = () => {
261
+ const search = signal('');
262
+
263
+ const filteredUsers = users.filter(
264
+ (u) =>
265
+ u.name.toLowerCase().includes(search.value.toLowerCase()) ||
266
+ u.email.toLowerCase().includes(search.value.toLowerCase())
267
+ );
268
+
269
+ const statusVariant = (status: User['status']) => {
270
+ switch (status) {
271
+ case 'active': return 'success';
272
+ case 'inactive': return 'default';
273
+ case 'pending': return 'warning';
274
+ }
275
+ };
276
+
277
+ return (
278
+ <div className="space-y-6">
279
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
280
+ <div>
281
+ <h1 className="text-2xl font-bold">Users</h1>
282
+ <p className="text-gray-600 mt-1">Manage your team members and their roles.</p>
283
+ </div>
284
+ <Button variant="primary">Add User</Button>
285
+ </div>
286
+
287
+ <Card padding="lg">
288
+ <div className="mb-4">
289
+ <Input
290
+ placeholder="Search users..."
291
+ value={search.value}
292
+ onInput={(e) => { search.value = e.currentTarget.value; }}
293
+ className="max-w-sm"
294
+ />
295
+ </div>
296
+ <div className="overflow-x-auto">
297
+ <table className="w-full text-sm">
298
+ <thead>
299
+ <tr className="border-b border-gray-200">
300
+ <th className="text-left py-3 px-4 font-medium text-gray-600">Name</th>
301
+ <th className="text-left py-3 px-4 font-medium text-gray-600">Role</th>
302
+ <th className="text-left py-3 px-4 font-medium text-gray-600">Status</th>
303
+ <th className="text-left py-3 px-4 font-medium text-gray-600">Last Active</th>
304
+ <th className="text-right py-3 px-4 font-medium text-gray-600">Actions</th>
305
+ </tr>
306
+ </thead>
307
+ <tbody>
308
+ {filteredUsers.map((user) => (
309
+ <tr key={user.id} className="border-b border-gray-100 hover:bg-gray-50">
310
+ <td className="py-3 px-4">
311
+ <div>
312
+ <p className="font-medium">{user.name}</p>
313
+ <p className="text-gray-500 text-xs">{user.email}</p>
314
+ </div>
315
+ </td>
316
+ <td className="py-3 px-4">{user.role}</td>
317
+ <td className="py-3 px-4">
318
+ <Badge variant={statusVariant(user.status)} size="sm">
319
+ {user.status}
320
+ </Badge>
321
+ </td>
322
+ <td className="py-3 px-4 text-gray-500">{user.lastActive}</td>
323
+ <td className="py-3 px-4 text-right">
324
+ <button className="text-brand-600 hover:underline text-sm">Edit</button>
325
+ </td>
326
+ </tr>
327
+ ))}
328
+ </tbody>
329
+ </table>
330
+ </div>
331
+ </Card>
332
+ </div>
333
+ );
334
+ };
335
+
336
+ export default UsersPage;`,
337
+ "src/routes/settings.tsx": `import type { RouteComponent } from '@emberkit/core';
338
+ import { Card, Input, Button, Alert } from '@emberkit/ui';
339
+ import { signal } from '@emberkit/core';
340
+
341
+ const SettingsPage: RouteComponent = () => {
342
+ const name = signal('John Doe');
343
+ const email = signal('john@example.com');
344
+ const saved = signal(false);
345
+
346
+ const handleSave = (e: Event) => {
347
+ e.preventDefault();
348
+ saved.value = true;
349
+ setTimeout(() => { saved.value = false; }, 3000);
350
+ };
351
+
352
+ return (
353
+ <div className="space-y-6 max-w-2xl">
354
+ <div>
355
+ <h1 className="text-2xl font-bold">Settings</h1>
356
+ <p className="text-gray-600 mt-1">Manage your account preferences.</p>
357
+ </div>
358
+
359
+ {saved.value && (
360
+ <Alert variant="success">Settings saved successfully!</Alert>
361
+ )}
362
+
363
+ <Card padding="lg">
364
+ <h3 className="font-semibold mb-6">Profile</h3>
365
+ <form onSubmit={handleSave} className="space-y-4">
366
+ <Input
367
+ label="Full Name"
368
+ value={name.value}
369
+ onInput={(e) => { name.value = e.currentTarget.value; }}
370
+ />
371
+ <Input
372
+ label="Email"
373
+ type="email"
374
+ value={email.value}
375
+ onInput={(e) => { email.value = e.currentTarget.value; }}
376
+ />
377
+ <div className="flex justify-end">
378
+ <Button variant="primary" type="submit">Save Changes</Button>
379
+ </div>
380
+ </form>
381
+ </Card>
382
+
383
+ <Card padding="lg">
384
+ <h3 className="font-semibold mb-6">Password</h3>
385
+ <form className="space-y-4">
386
+ <Input label="Current Password" type="password" />
387
+ <Input label="New Password" type="password" />
388
+ <Input label="Confirm Password" type="password" />
389
+ <div className="flex justify-end">
390
+ <Button variant="primary">Update Password</Button>
391
+ </div>
392
+ </form>
393
+ </Card>
394
+
395
+ <Card padding="lg" className="border-red-200">
396
+ <h3 className="font-semibold mb-2 text-red-600">Danger Zone</h3>
397
+ <p className="text-sm text-gray-600 mb-4">
398
+ Once you delete your account, there is no going back.
399
+ </p>
400
+ <Button variant="danger">Delete Account</Button>
401
+ </Card>
402
+ </div>
403
+ );
404
+ };
405
+
406
+ export default SettingsPage;`,
407
+ };
@@ -1,6 +1,6 @@
1
- export const routeTemplate = `import type { RouteComponent } from '@emberkit/runtime';
1
+ export const routeTemplate = `import type { RouteComponent } from '@emberkit/core';
2
2
 
3
- const {{name}}Route: RouteComponent = (props) => {
3
+ const {{name}}: RouteComponent = () => {
4
4
  return (
5
5
  <div>
6
6
  <h1>{{name}}</h1>
@@ -8,15 +8,13 @@ const {{name}}Route: RouteComponent = (props) => {
8
8
  );
9
9
  };
10
10
 
11
- export default {{name}}Route;
11
+ export default {{name}};
12
12
  `;
13
- export const componentTemplate = `import type { Component } from '@emberkit/runtime';
14
-
15
- interface {{name}}Props {
13
+ export const componentTemplate = `interface {{name}}Props {
16
14
  className?: string;
17
15
  }
18
16
 
19
- const {{name}}: Component<{{name}}Props> = ({ className = '' }) => {
17
+ const {{name}} = ({ className = '' }: {{name}}Props) => {
20
18
  return (
21
19
  <div className={className}>
22
20
  {{name}} component
@@ -26,15 +24,11 @@ const {{name}}: Component<{{name}}Props> = ({ className = '' }) => {
26
24
 
27
25
  export default {{name}};
28
26
  `;
29
- export const layoutTemplate = `import type { RouteComponent } from '@emberkit/runtime';
30
-
31
- interface {{name}}LayoutProps {
32
- children: JSX.Element;
33
- }
27
+ export const layoutTemplate = `import type { RouteComponent } from '@emberkit/core';
34
28
 
35
- const {{name}}Layout: RouteComponent<{{name}}LayoutProps> = ({ children }) => {
29
+ const {{name}}Layout: RouteComponent = ({ children }) => {
36
30
  return (
37
- <div className="{{kebab-name}}">
31
+ <div>
38
32
  <header>
39
33
  <nav>{{name}} Navigation</nav>
40
34
  </header>
@@ -46,10 +40,13 @@ const {{name}}Layout: RouteComponent<{{name}}LayoutProps> = ({ children }) => {
46
40
 
47
41
  export default {{name}}Layout;
48
42
  `;
49
- export const errorBoundaryTemplate = `import type { RouteComponent } from '@emberkit/runtime';
50
- import { createErrorBoundary } from '@emberkit/runtime';
43
+ export const errorBoundaryTemplate = `import type { RouteComponent } from '@emberkit/core';
44
+
45
+ interface {{name}}ErrorProps {
46
+ error: Error;
47
+ }
51
48
 
52
- const {{name}}Error: RouteComponent<{ error: Error }> = ({ error }) => {
49
+ const {{name}}Error: RouteComponent<{{name}}ErrorProps> = ({ error }) => {
53
50
  return (
54
51
  <div className="error-boundary">
55
52
  <h2>Something went wrong</h2>
@@ -60,21 +57,52 @@ const {{name}}Error: RouteComponent<{ error: Error }> = ({ error }) => {
60
57
 
61
58
  export default {{name}}Error;
62
59
  `;
63
- export const loaderTemplate = `import type { Loader } from '@emberkit/loader';
60
+ export const loaderTemplate = `import type { LoaderFunction, LoaderResult } from '@emberkit/core';
64
61
 
65
- export const loader: Loader = async ({ params, request }) => {
62
+ export const loader: LoaderFunction = async ({ params, query, request }) => {
66
63
  return {
67
- // Add your data here
68
- };
64
+ data: {
65
+ // Add your data here
66
+ },
67
+ } as LoaderResult<unknown>;
68
+ };
69
+ `;
70
+ export const actionTemplate = `import type { ActionFunction, LoaderResult } from '@emberkit/core';
71
+
72
+ export const action: ActionFunction = async ({ params, request }) => {
73
+ const formData = await request.formData();
74
+
75
+ return {
76
+ data: {
77
+ success: true,
78
+ },
79
+ } as LoaderResult<unknown>;
80
+ };
81
+ `;
82
+ export const apiRouteTemplate = `import type { LoaderFunction, LoaderResult } from '@emberkit/core';
83
+
84
+ export const GET: LoaderFunction = async ({ params, query, request }) => {
85
+ return {
86
+ data: {
87
+ message: 'Hello from API',
88
+ },
89
+ } as LoaderResult<unknown>;
90
+ };
91
+
92
+ export const POST: LoaderFunction = async ({ request }) => {
93
+ const body = await request.json();
94
+
95
+ return {
96
+ data: {
97
+ received: body,
98
+ },
99
+ } as LoaderResult<unknown>;
69
100
  };
70
101
  `;
71
102
  export const configTemplate = `import { defineConfig } from '@emberkit/core';
72
103
 
73
104
  export default defineConfig({
74
- mode: 'ssr',
75
- routes: {
76
- output: 'src/generated/routes.ts',
77
- },
105
+ mode: 'hybrid',
78
106
  server: {
79
107
  port: 3000,
80
108
  },
@@ -84,27 +112,134 @@ export default defineConfig({
84
112
  },
85
113
  });
86
114
  `;
87
- export const indexTemplate = `import { render } from '@emberkit/runtime';
115
+ export const indexTemplate = `import { render } from '@emberkit/core';
116
+ import { routes } from 'virtual:emberkit-routes';
88
117
  import App from './routes/_layout';
118
+ import './styles.css';
89
119
 
90
120
  const root = document.getElementById('app');
91
121
 
92
122
  if (root) {
93
- render(App, root);
123
+ try {
124
+ render(App, root, { routes });
125
+ } catch (error) {
126
+ console.error('[entry] Render error:', error);
127
+ }
94
128
  }
95
129
  `;
96
- export const layoutRoutesTemplate = `import { defineRoutes } from '@emberkit/router';
130
+ export const layoutRoutesTemplate = `// EmberKit uses file-based routing.
131
+ // Routes are automatically discovered from the src/routes directory.
132
+ // - src/routes/index.tsx → /
133
+ // - src/routes/about.tsx → /about
134
+ // - src/routes/[slug].tsx → /:slug
135
+ // - src/routes/[...rest].tsx → catch-all
97
136
 
98
- export default defineRoutes([
99
- {
100
- path: '/',
101
- component: () => import('./routes/index'),
102
- },
103
- {
104
- path: '/:slug',
105
- component: () => import('./routes/[slug]'),
106
- },
107
- ]);
137
+ export {};
138
+ `;
139
+ export const signalTemplate = `import { signal, computed, effect } from '@emberkit/core';
140
+
141
+ // Writable signal
142
+ const count = signal(0);
143
+
144
+ // Computed value
145
+ const doubled = computed(() => count.value * 2);
146
+
147
+ // Side effect
148
+ effect(() => {
149
+ console.log('Count changed to:', count.value);
150
+ });
151
+
152
+ // Update
153
+ count.value++;
154
+
155
+ // Batch updates
156
+ import { batch } from '@emberkit/core';
157
+
158
+ batch(() => {
159
+ count.value = 10;
160
+ });
161
+
162
+ export { count, doubled };
163
+ `;
164
+ export const contextTemplate = `import { createContext, useContext } from '@emberkit/core';
165
+
166
+ interface {{name}}Context {
167
+ // Define your context shape
168
+ value: string;
169
+ }
170
+
171
+ const {{name}}Context = createContext<{{name}}Context>({
172
+ value: 'default',
173
+ });
174
+
175
+ // Provider usage:
176
+ // <{{name}}Context.Provider value={{ value: 'hello' }}>
177
+ // {children}
178
+ // </{{name}}Context.Provider>
179
+
180
+ // Consumer usage:
181
+ // const ctx = useContext({{name}}Context);
182
+
183
+ export { {{name}}Context };
184
+ `;
185
+ export const formTemplate = `import { signal } from '@emberkit/core';
186
+
187
+ const {{name}}Form = () => {
188
+ const email = signal('');
189
+ const password = signal('');
190
+ const error = signal<string | null>(null);
191
+ const loading = signal(false);
192
+
193
+ const handleSubmit = async (e: Event) => {
194
+ e.preventDefault();
195
+ error.value = null;
196
+ loading.value = true;
197
+
198
+ try {
199
+ const response = await fetch('/api/auth/login', {
200
+ method: 'POST',
201
+ headers: { 'Content-Type': 'application/json' },
202
+ body: JSON.stringify({
203
+ email: email.value,
204
+ password: password.value,
205
+ }),
206
+ });
207
+
208
+ if (!response.ok) {
209
+ throw new Error('Login failed');
210
+ }
211
+
212
+ // Handle success
213
+ } catch (err) {
214
+ error.value = err instanceof Error ? err.message : 'Unknown error';
215
+ } finally {
216
+ loading.value = false;
217
+ }
218
+ };
219
+
220
+ return (
221
+ <form onSubmit={handleSubmit}>
222
+ <input
223
+ type="email"
224
+ value={email.value}
225
+ onInput={(e) => { email.value = e.currentTarget.value; }}
226
+ placeholder="Email"
227
+ />
228
+ <input
229
+ type="password"
230
+ value={password.value}
231
+ onInput={(e) => { password.value = e.currentTarget.value; }}
232
+ placeholder="Password"
233
+ />
234
+ {error.value && <p className="text-red-500">{error.value}</p>}
235
+ <button type="submit" disabled={loading.value}>
236
+ {loading.value ? 'Loading...' : 'Submit'}
237
+ </button>
238
+ </form>
239
+ );
240
+ };
241
+
242
+ export default {{name}}Form;
108
243
  `;
109
244
  export function getTemplate(type) {
110
245
  const templates = {
@@ -113,7 +248,13 @@ export function getTemplate(type) {
113
248
  layout: layoutTemplate,
114
249
  error: errorBoundaryTemplate,
115
250
  loader: loaderTemplate,
251
+ action: actionTemplate,
252
+ api: apiRouteTemplate,
116
253
  config: configTemplate,
254
+ index: indexTemplate,
255
+ signal: signalTemplate,
256
+ context: contextTemplate,
257
+ form: formTemplate,
117
258
  };
118
259
  return templates[type] ?? routeTemplate;
119
260
  }