@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.
- package/README.md +63 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.js +83 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +15 -0
- package/dist/migrations.d.ts +19 -0
- package/dist/migrations.js +74 -0
- package/dist/templates.d.ts +33 -0
- package/dist/templates.js +89 -0
- package/dist/tools.d.ts +36 -0
- package/dist/tools.js +22 -0
- package/dist/vite.d.ts +29 -0
- package/dist/vite.js +77 -0
- package/package.json +47 -0
- package/templates/ai-app/KAZZLE.md +21 -0
- package/templates/ai-app/bun.lock +290 -0
- package/templates/ai-app/components/server/index.ts +238 -0
- package/templates/ai-app/components/server/package.json +20 -0
- package/templates/ai-app/components/server/tsconfig.json +11 -0
- package/templates/ai-app/components/ui/index.html +12 -0
- package/templates/ai-app/components/ui/package.json +28 -0
- package/templates/ai-app/components/ui/public/favicon.svg +4 -0
- package/templates/ai-app/components/ui/src/App.tsx +52 -0
- package/templates/ai-app/components/ui/src/api.ts +85 -0
- package/templates/ai-app/components/ui/src/main.tsx +11 -0
- package/templates/ai-app/components/ui/src/styles/app.css +210 -0
- package/templates/ai-app/components/ui/src/styles/theme.css +181 -0
- package/templates/ai-app/components/ui/src/tabs/chat-tab.tsx +75 -0
- package/templates/ai-app/components/ui/src/tabs/extract-tab.tsx +92 -0
- package/templates/ai-app/components/ui/src/tabs/image-tab.tsx +51 -0
- package/templates/ai-app/components/ui/src/tabs/speech-tab.tsx +47 -0
- package/templates/ai-app/components/ui/src/tabs/transcribe-tab.tsx +42 -0
- package/templates/ai-app/components/ui/src/tabs/video-tab.tsx +76 -0
- package/templates/ai-app/components/ui/src/types.ts +53 -0
- package/templates/ai-app/components/ui/src/vite-env.d.ts +1 -0
- package/templates/ai-app/components/ui/tsconfig.json +21 -0
- package/templates/ai-app/components/ui/vite.config.ts +22 -0
- package/templates/ai-app/kazzle.config.ts +9 -0
- package/templates/ai-app/package.json +17 -0
- package/templates/empty-app/KAZZLE.md +10 -0
- package/templates/empty-app/kazzle.config.ts +8 -0
- package/templates/empty-app/package.json +15 -0
- package/templates/manifest.json +10 -0
- package/templates/process-app/KAZZLE.md +10 -0
- package/templates/process-app/components/process/index.ts +155 -0
- package/templates/process-app/components/process/package.json +20 -0
- package/templates/process-app/components/process/tsconfig.json +12 -0
- package/templates/process-app/kazzle.config.ts +32 -0
- package/templates/process-app/package.json +18 -0
- package/templates/process-app/skills/tools/SKILL.md +37 -0
- package/templates/process-app/skills/tools/tools.ts +19 -0
- package/templates/realtime-app/KAZZLE.md +11 -0
- package/templates/realtime-app/bun.lock +887 -0
- package/templates/realtime-app/components/server/deploy.ts +105 -0
- package/templates/realtime-app/components/server/index.ts +40 -0
- package/templates/realtime-app/components/server/migrations/001_items.sql +6 -0
- package/templates/realtime-app/components/server/package.json +20 -0
- package/templates/realtime-app/components/server/routes.ts +137 -0
- package/templates/realtime-app/components/server/tsconfig.json +15 -0
- package/templates/realtime-app/components/ui/index.html +13 -0
- package/templates/realtime-app/components/ui/package.json +36 -0
- package/templates/realtime-app/components/ui/public/favicon.svg +4 -0
- package/templates/realtime-app/components/ui/src/App.tsx +77 -0
- package/templates/realtime-app/components/ui/src/lib/connector.ts +80 -0
- package/templates/realtime-app/components/ui/src/lib/schema.ts +35 -0
- package/templates/realtime-app/components/ui/src/lib/sync.ts +24 -0
- package/templates/realtime-app/components/ui/src/main.tsx +49 -0
- package/templates/realtime-app/components/ui/src/styles/theme.css +181 -0
- package/templates/realtime-app/components/ui/src/vite-env.d.ts +2 -0
- package/templates/realtime-app/components/ui/tsconfig.json +14 -0
- package/templates/realtime-app/components/ui/vite.config.ts +86 -0
- package/templates/realtime-app/kazzle.config.ts +11 -0
- package/templates/realtime-app/package.json +17 -0
- package/templates/shared/icon.svg +4 -0
- package/templates/ui-app/KAZZLE.md +16 -0
- package/templates/ui-app/components/ui/bun.lock +894 -0
- package/templates/ui-app/components/ui/index.html +13 -0
- package/templates/ui-app/components/ui/package.json +30 -0
- package/templates/ui-app/components/ui/public/favicon.svg +4 -0
- package/templates/ui-app/components/ui/src/App.tsx +41 -0
- package/templates/ui-app/components/ui/src/main.tsx +24 -0
- package/templates/ui-app/components/ui/src/styles/theme.css +181 -0
- package/templates/ui-app/components/ui/tsconfig.json +21 -0
- package/templates/ui-app/components/ui/vite.config.ts +97 -0
- package/templates/ui-app/kazzle.config.ts +10 -0
- package/templates/ui-app/package.json +16 -0
- package/templates/ui-db-app/KAZZLE.md +12 -0
- package/templates/ui-db-app/components/server/index.ts +218 -0
- package/templates/ui-db-app/components/server/package.json +21 -0
- package/templates/ui-db-app/components/server/tsconfig.json +11 -0
- package/templates/ui-db-app/components/ui/index.html +12 -0
- package/templates/ui-db-app/components/ui/package.json +28 -0
- package/templates/ui-db-app/components/ui/public/favicon.svg +4 -0
- package/templates/ui-db-app/components/ui/src/App.tsx +221 -0
- package/templates/ui-db-app/components/ui/src/api.ts +79 -0
- package/templates/ui-db-app/components/ui/src/main.tsx +10 -0
- package/templates/ui-db-app/components/ui/src/styles/theme.css +181 -0
- package/templates/ui-db-app/components/ui/src/types.ts +17 -0
- package/templates/ui-db-app/components/ui/src/vite-env.d.ts +1 -0
- package/templates/ui-db-app/components/ui/tsconfig.json +21 -0
- package/templates/ui-db-app/components/ui/vite.config.ts +19 -0
- package/templates/ui-db-app/kazzle.config.ts +9 -0
- 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,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,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
|
+
}
|