@kazzle/app 0.1.693

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 (103) hide show
  1. package/README.md +63 -0
  2. package/dist/client.d.ts +25 -0
  3. package/dist/client.js +83 -0
  4. package/dist/index.d.ts +86 -0
  5. package/dist/index.js +15 -0
  6. package/dist/migrations.d.ts +19 -0
  7. package/dist/migrations.js +74 -0
  8. package/dist/templates.d.ts +33 -0
  9. package/dist/templates.js +89 -0
  10. package/dist/tools.d.ts +36 -0
  11. package/dist/tools.js +22 -0
  12. package/dist/vite.d.ts +29 -0
  13. package/dist/vite.js +77 -0
  14. package/package.json +47 -0
  15. package/templates/ai-app/KAZZLE.md +21 -0
  16. package/templates/ai-app/bun.lock +290 -0
  17. package/templates/ai-app/components/server/index.ts +238 -0
  18. package/templates/ai-app/components/server/package.json +20 -0
  19. package/templates/ai-app/components/server/tsconfig.json +11 -0
  20. package/templates/ai-app/components/ui/index.html +12 -0
  21. package/templates/ai-app/components/ui/package.json +28 -0
  22. package/templates/ai-app/components/ui/public/favicon.svg +4 -0
  23. package/templates/ai-app/components/ui/src/App.tsx +52 -0
  24. package/templates/ai-app/components/ui/src/api.ts +85 -0
  25. package/templates/ai-app/components/ui/src/main.tsx +11 -0
  26. package/templates/ai-app/components/ui/src/styles/app.css +210 -0
  27. package/templates/ai-app/components/ui/src/styles/theme.css +181 -0
  28. package/templates/ai-app/components/ui/src/tabs/chat-tab.tsx +75 -0
  29. package/templates/ai-app/components/ui/src/tabs/extract-tab.tsx +92 -0
  30. package/templates/ai-app/components/ui/src/tabs/image-tab.tsx +51 -0
  31. package/templates/ai-app/components/ui/src/tabs/speech-tab.tsx +47 -0
  32. package/templates/ai-app/components/ui/src/tabs/transcribe-tab.tsx +42 -0
  33. package/templates/ai-app/components/ui/src/tabs/video-tab.tsx +76 -0
  34. package/templates/ai-app/components/ui/src/types.ts +53 -0
  35. package/templates/ai-app/components/ui/src/vite-env.d.ts +1 -0
  36. package/templates/ai-app/components/ui/tsconfig.json +21 -0
  37. package/templates/ai-app/components/ui/vite.config.ts +22 -0
  38. package/templates/ai-app/kazzle.config.ts +9 -0
  39. package/templates/ai-app/package.json +17 -0
  40. package/templates/empty-app/KAZZLE.md +10 -0
  41. package/templates/empty-app/kazzle.config.ts +8 -0
  42. package/templates/empty-app/package.json +15 -0
  43. package/templates/manifest.json +10 -0
  44. package/templates/process-app/KAZZLE.md +10 -0
  45. package/templates/process-app/components/process/index.ts +155 -0
  46. package/templates/process-app/components/process/package.json +20 -0
  47. package/templates/process-app/components/process/tsconfig.json +12 -0
  48. package/templates/process-app/kazzle.config.ts +32 -0
  49. package/templates/process-app/package.json +18 -0
  50. package/templates/process-app/skills/tools/SKILL.md +37 -0
  51. package/templates/process-app/skills/tools/tools.ts +19 -0
  52. package/templates/realtime-app/KAZZLE.md +11 -0
  53. package/templates/realtime-app/bun.lock +887 -0
  54. package/templates/realtime-app/components/server/deploy.ts +105 -0
  55. package/templates/realtime-app/components/server/index.ts +40 -0
  56. package/templates/realtime-app/components/server/migrations/001_items.sql +6 -0
  57. package/templates/realtime-app/components/server/package.json +20 -0
  58. package/templates/realtime-app/components/server/routes.ts +137 -0
  59. package/templates/realtime-app/components/server/tsconfig.json +15 -0
  60. package/templates/realtime-app/components/ui/index.html +13 -0
  61. package/templates/realtime-app/components/ui/package.json +36 -0
  62. package/templates/realtime-app/components/ui/public/favicon.svg +4 -0
  63. package/templates/realtime-app/components/ui/src/App.tsx +77 -0
  64. package/templates/realtime-app/components/ui/src/lib/connector.ts +80 -0
  65. package/templates/realtime-app/components/ui/src/lib/schema.ts +35 -0
  66. package/templates/realtime-app/components/ui/src/lib/sync.ts +24 -0
  67. package/templates/realtime-app/components/ui/src/main.tsx +49 -0
  68. package/templates/realtime-app/components/ui/src/styles/theme.css +181 -0
  69. package/templates/realtime-app/components/ui/src/vite-env.d.ts +2 -0
  70. package/templates/realtime-app/components/ui/tsconfig.json +14 -0
  71. package/templates/realtime-app/components/ui/vite.config.ts +86 -0
  72. package/templates/realtime-app/kazzle.config.ts +11 -0
  73. package/templates/realtime-app/package.json +17 -0
  74. package/templates/shared/icon.svg +4 -0
  75. package/templates/ui-app/KAZZLE.md +16 -0
  76. package/templates/ui-app/components/ui/bun.lock +894 -0
  77. package/templates/ui-app/components/ui/index.html +13 -0
  78. package/templates/ui-app/components/ui/package.json +30 -0
  79. package/templates/ui-app/components/ui/public/favicon.svg +4 -0
  80. package/templates/ui-app/components/ui/src/App.tsx +41 -0
  81. package/templates/ui-app/components/ui/src/main.tsx +24 -0
  82. package/templates/ui-app/components/ui/src/styles/theme.css +181 -0
  83. package/templates/ui-app/components/ui/tsconfig.json +21 -0
  84. package/templates/ui-app/components/ui/vite.config.ts +97 -0
  85. package/templates/ui-app/kazzle.config.ts +10 -0
  86. package/templates/ui-app/package.json +16 -0
  87. package/templates/ui-db-app/KAZZLE.md +12 -0
  88. package/templates/ui-db-app/components/server/index.ts +218 -0
  89. package/templates/ui-db-app/components/server/package.json +21 -0
  90. package/templates/ui-db-app/components/server/tsconfig.json +11 -0
  91. package/templates/ui-db-app/components/ui/index.html +12 -0
  92. package/templates/ui-db-app/components/ui/package.json +28 -0
  93. package/templates/ui-db-app/components/ui/public/favicon.svg +4 -0
  94. package/templates/ui-db-app/components/ui/src/App.tsx +221 -0
  95. package/templates/ui-db-app/components/ui/src/api.ts +79 -0
  96. package/templates/ui-db-app/components/ui/src/main.tsx +10 -0
  97. package/templates/ui-db-app/components/ui/src/styles/theme.css +181 -0
  98. package/templates/ui-db-app/components/ui/src/types.ts +17 -0
  99. package/templates/ui-db-app/components/ui/src/vite-env.d.ts +1 -0
  100. package/templates/ui-db-app/components/ui/tsconfig.json +21 -0
  101. package/templates/ui-db-app/components/ui/vite.config.ts +19 -0
  102. package/templates/ui-db-app/kazzle.config.ts +9 -0
  103. package/templates/ui-db-app/package.json +17 -0
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "{{APP_SLUG}}-server",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "bun run index.ts",
8
+ "dev": "kazzle run -- bun --watch index.ts",
9
+ "typecheck": "tsc --noEmit --allowImportingTsExtensions",
10
+ "check": "bun run typecheck"
11
+ },
12
+ "dependencies": {
13
+ "@hono/node-server": "^1.13.0",
14
+ "hono": "^4.12.16",
15
+ "postgres": "^3.4.5"
16
+ },
17
+ "devDependencies": {
18
+ "@types/bun": "^1.3.6",
19
+ "typescript": "^5.8.3"
20
+ }
21
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "types": ["bun"]
9
+ },
10
+ "include": ["*.ts"]
11
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{APP_NAME}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "{{APP_SLUG}}",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "kazzle run -- vite",
8
+ "typecheck": "tsc --noEmit",
9
+ "build": "vite build",
10
+ "preview": "kazzle run -- vite preview",
11
+ "check": "bun run typecheck && bun run build"
12
+ },
13
+ "dependencies": {
14
+ "@kazzle/app": "{{KAZZLE_APP_VERSION}}",
15
+ "@vitejs/plugin-react": "^4.5.2",
16
+ "react": "^19.1.0",
17
+ "react-dom": "^19.1.0",
18
+ "vite": "^6.3.5"
19
+ },
20
+ "devDependencies": {
21
+ "@tailwindcss/vite": "^4.1.0",
22
+ "@types/node": "^25.1.0",
23
+ "@types/react": "^19.1.0",
24
+ "@types/react-dom": "^19.1.0",
25
+ "tailwindcss": "^4.1.0",
26
+ "typescript": "^5.8.3"
27
+ }
28
+ }
@@ -0,0 +1,4 @@
1
+ <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="64" height="64" rx="18" fill="#6366F1"/>
3
+ <path d="M19 33.5L28 42L46 22" stroke="white" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
4
+ </svg>
@@ -0,0 +1,221 @@
1
+ import { FormEvent, useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ createCategory,
4
+ createTodo,
5
+ deleteCategory,
6
+ deleteTodo,
7
+ getCategories,
8
+ getTodos,
9
+ updateCategory,
10
+ updateTodo,
11
+ } from './api';
12
+ import type { Category, Todo } from './types';
13
+
14
+ const DEFAULT_COLOR = '#6366f1';
15
+ const COLORS = [DEFAULT_COLOR, '#0ea5e9', '#f97316', '#10b981', '#ec4899'];
16
+
17
+ export function App() {
18
+ const [categories, setCategories] = useState<Category[]>([]);
19
+ const [todos, setTodos] = useState<Todo[]>([]);
20
+ const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null);
21
+ const [draft, setDraft] = useState('');
22
+ const [draftCategoryId, setDraftCategoryId] = useState<number | null>(null);
23
+ const [categoryName, setCategoryName] = useState('');
24
+ const [categoryColor, setCategoryColor] = useState<string>(DEFAULT_COLOR);
25
+ const [editingCategoryId, setEditingCategoryId] = useState<number | null>(null);
26
+ const [error, setError] = useState<string | null>(null);
27
+
28
+ const selectedCategory = useMemo(
29
+ () => categories.find(category => category.id === selectedCategoryId) ?? null,
30
+ [categories, selectedCategoryId],
31
+ );
32
+
33
+ const totalCount = useMemo(
34
+ () => categories.reduce((total, category) => total + category.todo_count, 0),
35
+ [categories],
36
+ );
37
+
38
+ async function load() {
39
+ const [nextCategories, nextTodos] = await Promise.all([
40
+ getCategories(),
41
+ getTodos(selectedCategoryId),
42
+ ]);
43
+ setCategories(nextCategories);
44
+ setTodos(nextTodos);
45
+ }
46
+
47
+ useEffect(() => {
48
+ load().catch(handleError);
49
+ }, [selectedCategoryId]);
50
+
51
+ function handleError(err: unknown) {
52
+ setError(err instanceof Error ? err.message : String(err));
53
+ }
54
+
55
+ async function onSubmit(event: FormEvent) {
56
+ event.preventDefault();
57
+ const title = draft.trim();
58
+ if (!title) return;
59
+ try {
60
+ await createTodo(title, selectedCategoryId ?? draftCategoryId);
61
+ setDraft('');
62
+ await load();
63
+ } catch (err) {
64
+ handleError(err);
65
+ }
66
+ }
67
+
68
+ async function onCategorySubmit(event: FormEvent) {
69
+ event.preventDefault();
70
+ const name = categoryName.trim();
71
+ if (!name) return;
72
+ try {
73
+ if (editingCategoryId !== null) {
74
+ await updateCategory(editingCategoryId, { name, color: categoryColor });
75
+ } else {
76
+ await createCategory(name, categoryColor);
77
+ }
78
+ setCategoryName('');
79
+ setCategoryColor(DEFAULT_COLOR);
80
+ setEditingCategoryId(null);
81
+ await load();
82
+ } catch (err) {
83
+ handleError(err);
84
+ }
85
+ }
86
+
87
+ function startEditCategory(category: Category) {
88
+ setEditingCategoryId(category.id);
89
+ setCategoryName(category.name);
90
+ setCategoryColor(category.color);
91
+ }
92
+
93
+ async function removeCategory(categoryId: number) {
94
+ try {
95
+ await deleteCategory(categoryId);
96
+ if (selectedCategoryId === categoryId) setSelectedCategoryId(null);
97
+ await load();
98
+ } catch (err) {
99
+ handleError(err);
100
+ }
101
+ }
102
+
103
+ return (
104
+ <main className="grid min-h-screen grid-cols-[280px_1fr]">
105
+ <aside className="flex flex-col gap-2 border-r border-border bg-surface p-8">
106
+ <p className="muted text-sm">{{APP_NAME}}</p>
107
+ <h1 className="mb-4">Categories</h1>
108
+
109
+ <button
110
+ className={`btn justify-start ${!selectedCategoryId ? 'btn-primary' : ''}`}
111
+ onClick={() => setSelectedCategoryId(null)}
112
+ >
113
+ <span className="size-2.5 rounded-full bg-fg" />
114
+ <span className="flex-1 text-left">All tasks</span>
115
+ <small className="muted">{totalCount}</small>
116
+ </button>
117
+
118
+ {categories.map(category => (
119
+ <div className="row gap-1.5" key={category.id}>
120
+ <button
121
+ className={`btn flex-1 justify-start ${selectedCategoryId === category.id ? 'btn-primary' : ''}`}
122
+ onClick={() => setSelectedCategoryId(category.id)}
123
+ >
124
+ <span className="size-2.5 rounded-full" style={{ background: category.color }} />
125
+ <span className="flex-1 text-left">{category.name}</span>
126
+ <small className="muted">{category.todo_count}</small>
127
+ </button>
128
+ <button className="btn px-2.5" onClick={() => startEditCategory(category)}>Edit</button>
129
+ </div>
130
+ ))}
131
+
132
+ <form className="stack mt-4 border-t border-border pt-4" onSubmit={onCategorySubmit}>
133
+ <input
134
+ className="input"
135
+ value={categoryName}
136
+ onChange={event => setCategoryName(event.target.value)}
137
+ placeholder={editingCategoryId ? 'Rename category' : 'New category'}
138
+ />
139
+ <div className="row">
140
+ {COLORS.map(color => (
141
+ <button
142
+ key={color}
143
+ type="button"
144
+ className={`size-6 rounded-full border-2 ${categoryColor === color ? 'border-fg' : 'border-transparent'}`}
145
+ style={{ background: color }}
146
+ onClick={() => setCategoryColor(color)}
147
+ />
148
+ ))}
149
+ </div>
150
+ <div className="row">
151
+ <button type="submit" className="btn btn-primary">{editingCategoryId ? 'Save' : 'Add category'}</button>
152
+ {editingCategoryId !== null ? (
153
+ <button type="button" className="btn" onClick={() => removeCategory(editingCategoryId)}>
154
+ Delete
155
+ </button>
156
+ ) : null}
157
+ </div>
158
+ </form>
159
+ </aside>
160
+
161
+ <section className="flex flex-col gap-6 p-12">
162
+ <header className="stack gap-1">
163
+ <p className="muted text-sm">{selectedCategory?.name ?? 'All categories'}</p>
164
+ <h2>{selectedCategory ? `${selectedCategory.name} tasks` : 'Today\'s tasks'}</h2>
165
+ </header>
166
+
167
+ <form className="row" onSubmit={onSubmit}>
168
+ <input
169
+ className="input flex-1"
170
+ value={draft}
171
+ onChange={event => setDraft(event.target.value)}
172
+ placeholder="Add a task..."
173
+ />
174
+ {!selectedCategoryId ? (
175
+ <select
176
+ className="input w-auto"
177
+ value={draftCategoryId ?? ''}
178
+ onChange={event => setDraftCategoryId(event.target.value ? Number(event.target.value) : null)}
179
+ >
180
+ <option value="">No category</option>
181
+ {categories.map(category => (
182
+ <option key={category.id} value={category.id}>{category.name}</option>
183
+ ))}
184
+ </select>
185
+ ) : null}
186
+ <button type="submit" className="btn btn-primary">Add</button>
187
+ </form>
188
+
189
+ {error ? <p className="text-danger">{error}</p> : null}
190
+
191
+ <div className="stack">
192
+ {todos.map(todo => (
193
+ <article
194
+ key={todo.id}
195
+ className={`card row ${todo.completed ? 'opacity-70' : ''}`}
196
+ >
197
+ <label className="row flex-1 cursor-pointer">
198
+ <input
199
+ type="checkbox"
200
+ checked={todo.completed}
201
+ onChange={() => updateTodo(todo.id, { completed: !todo.completed }).then(load).catch(handleError)}
202
+ />
203
+ <span className={todo.completed ? 'muted line-through' : ''}>{todo.title}</span>
204
+ </label>
205
+ {todo.category_name ? (
206
+ <span
207
+ className="rounded-full px-2.5 py-1 text-xs text-accent-fg"
208
+ style={{ background: todo.category_color ?? '#6366f1' }}
209
+ >
210
+ {todo.category_name}
211
+ </span>
212
+ ) : null}
213
+ <button className="btn px-3 py-1.5" onClick={() => deleteTodo(todo.id).then(load).catch(handleError)}>Delete</button>
214
+ </article>
215
+ ))}
216
+ {todos.length === 0 ? <p className="muted">No tasks yet. Add one above.</p> : null}
217
+ </div>
218
+ </section>
219
+ </main>
220
+ );
221
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Thin wrappers over the server's REST API.
3
+ *
4
+ * Pattern: one async function per endpoint. Each one builds the request,
5
+ * awaits the response, throws on non-2xx, and parses the JSON body. The UI
6
+ * imports these by name, so changing a URL or shape only ever touches one
7
+ * file.
8
+ *
9
+ * The `request` helper centralises the fetch + JSON + error-handling
10
+ * boilerplate so the individual functions stay one-liners. Add a header
11
+ * (auth tokens, etc.) here and every endpoint picks it up.
12
+ *
13
+ * Errors are surfaced as `Error` instances with the status code and body in
14
+ * the message — the App component catches them and shows the message in the
15
+ * UI. If you need structured error objects (with codes the UI can branch
16
+ * on), augment `request` to JSON-parse non-2xx bodies and throw a typed
17
+ * subclass.
18
+ */
19
+
20
+ import type { Category, Todo } from './types';
21
+
22
+ async function request<T>(path: string, init?: RequestInit): Promise<T> {
23
+ const res = await fetch(path, {
24
+ ...init,
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ ...init?.headers,
28
+ },
29
+ });
30
+ if (!res.ok) {
31
+ throw new Error(`Request failed: ${res.status} ${await res.text()}`);
32
+ }
33
+ return res.json() as Promise<T>;
34
+ }
35
+
36
+ export function getCategories(): Promise<Category[]> {
37
+ return request<Category[]>('/categories');
38
+ }
39
+
40
+ export function createCategory(name: string, color: string): Promise<Category> {
41
+ return request<Category>('/categories', {
42
+ method: 'POST',
43
+ body: JSON.stringify({ name, color }),
44
+ });
45
+ }
46
+
47
+ export function updateCategory(id: number, data: Partial<Pick<Category, 'name' | 'color'>>): Promise<Category> {
48
+ return request<Category>(`/categories/${id}`, {
49
+ method: 'PATCH',
50
+ body: JSON.stringify(data),
51
+ });
52
+ }
53
+
54
+ export function deleteCategory(id: number): Promise<{ ok: true }> {
55
+ return request<{ ok: true }>(`/categories/${id}`, { method: 'DELETE' });
56
+ }
57
+
58
+ export function getTodos(categoryId: number | null): Promise<Todo[]> {
59
+ const query = categoryId ? `?category_id=${categoryId}` : '';
60
+ return request<Todo[]>(`/todos${query}`);
61
+ }
62
+
63
+ export function createTodo(title: string, categoryId: number | null): Promise<Todo> {
64
+ return request<Todo>('/todos', {
65
+ method: 'POST',
66
+ body: JSON.stringify({ title, category_id: categoryId }),
67
+ });
68
+ }
69
+
70
+ export function updateTodo(id: number, data: Partial<Pick<Todo, 'title' | 'completed' | 'category_id'>>): Promise<Todo> {
71
+ return request<Todo>(`/todos/${id}`, {
72
+ method: 'PATCH',
73
+ body: JSON.stringify(data),
74
+ });
75
+ }
76
+
77
+ export function deleteTodo(id: number): Promise<{ ok: true }> {
78
+ return request<{ ok: true }>(`/todos/${id}`, { method: 'DELETE' });
79
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { App } from './App';
4
+ import './styles/theme.css';
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
@@ -0,0 +1,181 @@
1
+ /* ─────────────────────────────────────────────────────────────────────────
2
+ * Kazzle app theme — the ONE styling entry point for this app.
3
+ *
4
+ * This file is the design system. It is imported once (in `main.tsx`) and
5
+ * gives you three things, in order:
6
+ *
7
+ * 1. Tailwind v4 — every utility class (`flex`, `gap-4`,
8
+ * `rounded-xl`, `text-zinc-500`, …) works in
9
+ * your JSX. No config file, no PostCSS setup.
10
+ * 2. Design tokens (@theme) — colors, radius, fonts as CSS variables. Edit
11
+ * these to re-skin the whole app at once.
12
+ * 3. Primitives (@layer) — a few ready-made classes (`.btn`, `.card`,
13
+ * `.input`, `.field`, `.page`, `.stack`,
14
+ * `.row`) so common UI looks good with zero
15
+ * effort.
16
+ *
17
+ * HOW TO STYLE — pick whichever is easier, mix freely:
18
+ * • Tailwind utilities in `className` for one-off layout/spacing.
19
+ * • A primitive class (`.btn`, `.card`) for the common building blocks.
20
+ *
21
+ * WHY IT'S BUILT THIS WAY: styles live in the markup (utilities) or in named
22
+ * primitives here — never in a separate per-screen stylesheet keyed to class
23
+ * names you have to keep in sync. Rewriting a component can't silently strip
24
+ * its styling, because the styling travels with the elements.
25
+ *
26
+ * TO EXTEND: add new design tokens under `@theme`, and add new reusable
27
+ * building blocks under `@layer components`. Keep app-specific one-offs as
28
+ * inline Tailwind utilities in the component instead of adding a class here.
29
+ * ───────────────────────────────────────────────────────────────────────── */
30
+
31
+ @import "tailwindcss";
32
+
33
+ /* ── Design tokens ──────────────────────────────────────────────────────────
34
+ * Exposed to Tailwind as `bg-surface`, `text-fg`, `text-muted`,
35
+ * `bg-accent`, `rounded-app`, etc. Change a value here and every utility and
36
+ * primitive that references it updates at once. */
37
+ @theme {
38
+ --color-bg: #0b0f1a;
39
+ --color-surface: #131a2a;
40
+ --color-surface-raised: #1b2438;
41
+ --color-fg: #e8edf7;
42
+ --color-muted: #94a3b8;
43
+ --color-border: rgba(148, 163, 184, 0.18);
44
+ --color-accent: #6366f1;
45
+ --color-accent-hover: #4f46e5;
46
+ --color-accent-fg: #ffffff;
47
+ --color-danger: #f87171;
48
+
49
+ --radius-app: 0.875rem;
50
+
51
+ --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system,
52
+ BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
53
+ }
54
+
55
+ /* ── Base ────────────────────────────────────────────────────────────────────
56
+ * Sensible document defaults so even unstyled markup reads well. */
57
+ @layer base {
58
+ html,
59
+ body,
60
+ #root {
61
+ min-height: 100%;
62
+ }
63
+
64
+ body {
65
+ margin: 0;
66
+ background: var(--color-bg);
67
+ color: var(--color-fg);
68
+ font-family: var(--font-sans);
69
+ -webkit-font-smoothing: antialiased;
70
+ }
71
+
72
+ h1 {
73
+ font-size: 1.875rem;
74
+ font-weight: 700;
75
+ letter-spacing: -0.02em;
76
+ margin: 0;
77
+ }
78
+
79
+ h2 {
80
+ font-size: 1.25rem;
81
+ font-weight: 600;
82
+ margin: 0;
83
+ }
84
+ }
85
+
86
+ /* ── Primitives ──────────────────────────────────────────────────────────────
87
+ * Reusable building blocks. Use them directly (`<button class="btn btn-primary">`)
88
+ * or layer Tailwind utilities on top. Add new ones here when a pattern repeats. */
89
+ @layer components {
90
+ /* Page wrapper — centers content with comfortable gutters. */
91
+ .page {
92
+ width: min(100% - 2rem, 48rem);
93
+ margin-inline: auto;
94
+ padding-block: 3rem;
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: 1.5rem;
98
+ }
99
+
100
+ /* Vertical / horizontal stacks. */
101
+ .stack {
102
+ display: flex;
103
+ flex-direction: column;
104
+ gap: 0.75rem;
105
+ }
106
+ .row {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 0.75rem;
110
+ }
111
+
112
+ /* Card surface. */
113
+ .card {
114
+ background: var(--color-surface);
115
+ border: 1px solid var(--color-border);
116
+ border-radius: var(--radius-app);
117
+ padding: 1.25rem;
118
+ }
119
+
120
+ /* Buttons. `.btn` is the neutral base; add `.btn-primary` for the accent. */
121
+ .btn {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ gap: 0.5rem;
126
+ padding: 0.625rem 1rem;
127
+ border-radius: var(--radius-app);
128
+ border: 1px solid var(--color-border);
129
+ background: var(--color-surface-raised);
130
+ color: var(--color-fg);
131
+ font: inherit;
132
+ font-weight: 500;
133
+ cursor: pointer;
134
+ transition: background 0.15s, border-color 0.15s, opacity 0.15s;
135
+ }
136
+ .btn:hover {
137
+ background: color-mix(in srgb, var(--color-surface-raised) 80%, white);
138
+ }
139
+ .btn:disabled {
140
+ opacity: 0.5;
141
+ cursor: not-allowed;
142
+ }
143
+ .btn-primary {
144
+ background: var(--color-accent);
145
+ border-color: transparent;
146
+ color: var(--color-accent-fg);
147
+ }
148
+ .btn-primary:hover {
149
+ background: var(--color-accent-hover);
150
+ }
151
+
152
+ /* Text input / select. */
153
+ .input {
154
+ width: 100%;
155
+ min-width: 0;
156
+ padding: 0.625rem 0.875rem;
157
+ border-radius: var(--radius-app);
158
+ border: 1px solid var(--color-border);
159
+ background: var(--color-surface-raised);
160
+ color: var(--color-fg);
161
+ font: inherit;
162
+ outline: none;
163
+ }
164
+ .input:focus {
165
+ border-color: var(--color-accent);
166
+ }
167
+
168
+ /* Labelled field: <label class="field"><span>Label</span><input class="input"/></label> */
169
+ .field {
170
+ display: flex;
171
+ flex-direction: column;
172
+ gap: 0.375rem;
173
+ font-size: 0.875rem;
174
+ color: var(--color-muted);
175
+ }
176
+
177
+ /* Muted helper / secondary text. */
178
+ .muted {
179
+ color: var(--color-muted);
180
+ }
181
+ }
@@ -0,0 +1,17 @@
1
+ export interface Category {
2
+ id: number;
3
+ name: string;
4
+ color: string;
5
+ todo_count: number;
6
+ }
7
+
8
+ export interface Todo {
9
+ id: number;
10
+ title: string;
11
+ completed: boolean;
12
+ category_id: number | null;
13
+ category_name: string | null;
14
+ category_color: string | null;
15
+ created_at: string;
16
+ updated_at: string;
17
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "Node",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "jsx": "react-jsx"
18
+ },
19
+ "include": ["src"],
20
+ "references": []
21
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Vite config for the UI component.
3
+ *
4
+ * Kazzle preview/runtime wiring (HOST/PORT binding, strictPort, allowedHosts,
5
+ * backend proxy) lives in @kazzle/app/vite — the ONE shared place that must
6
+ * never be reinvented. This template declares a backend `server` component, so
7
+ * we pass its name plus the relative routes the browser calls; the helper
8
+ * proxies them to the sibling component and refuses to serve until Kazzle has
9
+ * injected the backend URL. Add new backend routes to `proxyPaths`.
10
+ */
11
+ import { defineConfig } from 'vite';
12
+ import react from '@vitejs/plugin-react';
13
+ import tailwindcss from '@tailwindcss/vite';
14
+ import { kazzleAppVite } from '@kazzle/app/vite';
15
+
16
+ export default defineConfig(({ command }) => ({
17
+ plugins: [react(), tailwindcss()],
18
+ ...kazzleAppVite(command, { apiComponent: 'server', proxyPaths: ['/categories', '/todos'] }),
19
+ }));
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from '@kazzle/app';
2
+
3
+ export default defineConfig({
4
+ icon: 'components/ui/public/favicon.svg',
5
+ components: [
6
+ { name: 'ui', type: 'ui', path: './components/ui' },
7
+ { name: 'server', type: 'process', path: './components/server/index.ts', runtime: { dev: 'bun run dev', run: 'bun run start' } },
8
+ ],
9
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "{{APP_SLUG}}",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "workspaces": [
7
+ "components/*"
8
+ ],
9
+ "scripts": {
10
+ "dev": "bun run --cwd components/ui dev",
11
+ "start": "bun run --cwd components/server start",
12
+ "check": "bun run --filter='./components/*' check"
13
+ },
14
+ "dependencies": {
15
+ "@kazzle/app": "{{KAZZLE_APP_VERSION}}"
16
+ }
17
+ }