@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,13 @@
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
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+ <title>{{APP_NAME}}</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "kazzle-ui-app-template",
3
+ "private": true,
4
+ "version": "0.0.1",
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
+ "react": "^19.1.0",
16
+ "react-dom": "^19.1.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^25.1.0",
20
+ "@tailwindcss/vite": "^4.1.0",
21
+ "@types/react": "^19.1.0",
22
+ "@types/react-dom": "^19.1.0",
23
+ "@vitejs/plugin-react": "^4.5.2",
24
+ "tailwindcss": "^4.1.0",
25
+ "typescript": "^5.8.3",
26
+ "vite": "^6.3.5",
27
+ "vite-plugin-pwa": "^0.21.1",
28
+ "workbox-window": "^7.3.0"
29
+ }
30
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2
+ <rect width="64" height="64" rx="14" fill="hsl(230,65%,88%)"/>
3
+ <text x="32" y="46" text-anchor="middle" font-family="system-ui,sans-serif" font-size="40" font-weight="600" fill="hsl(230,50%,25%)">K</text>
4
+ </svg>
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Stub App component. Replace this with whatever your app actually does.
3
+ *
4
+ * The counter exists so the template ships with something visibly interactive
5
+ * out of the box — handy for sanity-checking that React mounted, that HMR
6
+ * works, and that the build pipeline produces a working bundle.
7
+ *
8
+ * STYLING: use Tailwind utility classes in `className` for layout/spacing, and
9
+ * the ready-made primitives (`.card`, `.btn`, `.btn-primary`, `.muted`, …)
10
+ * from `src/styles/theme.css` for common building blocks. See that file to
11
+ * re-skin via design tokens or add new primitives.
12
+ */
13
+
14
+ import { useState } from 'react';
15
+
16
+ export default function App() {
17
+ const [count, setCount] = useState(0);
18
+
19
+ return (
20
+ <div className="page">
21
+ <header className="stack">
22
+ <h1>{{APP_NAME}}</h1>
23
+ <p className="muted">Edit src/App.tsx to get started</p>
24
+ </header>
25
+
26
+ <div className="card row justify-between">
27
+ <span className="muted">Count</span>
28
+ <span className="text-2xl font-semibold tabular-nums">{count}</span>
29
+ </div>
30
+
31
+ <div className="row">
32
+ <button className="btn btn-primary" onClick={() => setCount(c => c + 1)}>
33
+ Increment
34
+ </button>
35
+ <button className="btn" onClick={() => setCount(0)} disabled={count === 0}>
36
+ Reset
37
+ </button>
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * UI-only app entry point.
3
+ *
4
+ * Nothing fancy: mount React into `#root` and let it take over. This template
5
+ * has no server, no database, no sync — everything is local component state.
6
+ * If you need persistence, the most common upgrades are:
7
+ *
8
+ * - "I want to talk to an API" → add a server component (see `ui-db-app`).
9
+ * - "I want offline-first reads + multi-device sync" → see `realtime-app`.
10
+ *
11
+ * `StrictMode` double-invokes effects in dev to catch bugs from accidental
12
+ * state mutation. It has no effect in production builds.
13
+ */
14
+
15
+ import { StrictMode } from 'react';
16
+ import { createRoot } from 'react-dom/client';
17
+ import App from './App';
18
+ import './styles/theme.css';
19
+
20
+ createRoot(document.getElementById('root')!).render(
21
+ <StrictMode>
22
+ <App />
23
+ </StrictMode>
24
+ );
@@ -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,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "types": ["node"]
19
+ },
20
+ "include": ["src", "vite.config.ts"]
21
+ }
@@ -0,0 +1,97 @@
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 file only declares plugins and spreads
7
+ * `kazzleAppVite(command)`. This template has no backend component, so no
8
+ * proxy options are passed; if you add one later, pass
9
+ * `{ apiComponent, proxyPaths }` (see @kazzle/app/vite).
10
+ *
11
+ * The one non-obvious thing left here: we register a service worker via
12
+ * `vite-plugin-pwa`, but ONLY for production builds. In dev, a service worker
13
+ * would aggressively cache module responses and break Vite's HMR. The
14
+ * `devUnregisterServiceWorker` plugin below cleans up stale SWs left by a
15
+ * previous built visit to the same origin. Do not enable `devOptions`.
16
+ */
17
+
18
+ import { defineConfig, type Plugin } from 'vite';
19
+ import react from '@vitejs/plugin-react';
20
+ import tailwindcss from '@tailwindcss/vite';
21
+ import { VitePWA } from 'vite-plugin-pwa';
22
+ import { kazzleAppVite } from '@kazzle/app/vite';
23
+
24
+ /**
25
+ * Dev-only: any browser that previously visited a built/PWA-enabled version
26
+ * of this app at the same origin still has a service worker installed. With
27
+ * `devOptions.enabled: false` we don't serve a SW in dev, so the stale SW
28
+ * keeps intercepting requests with its old precache and the page renders
29
+ * without current CSS/JS. This plugin responds to /sw.js (and the PWA
30
+ * register helper) with a tiny script that unregisters every SW, clears all
31
+ * caches, and reloads the page once. Once cleaned, the SW is gone and Vite
32
+ * HMR works normally. Safe to keep enabled — production builds skip this
33
+ * plugin (the `serve` command guard).
34
+ */
35
+ function devUnregisterServiceWorker(): Plugin {
36
+ const body = [
37
+ "self.addEventListener('install', e => self.skipWaiting());",
38
+ "self.addEventListener('activate', e => e.waitUntil((async () => {",
39
+ " try { await self.registration.unregister(); } catch {}",
40
+ " try { const keys = await caches.keys(); await Promise.all(keys.map(k => caches.delete(k))); } catch {}",
41
+ " const clients = await self.clients.matchAll({ type: 'window' });",
42
+ " for (const c of clients) { try { c.navigate(c.url); } catch {} }",
43
+ "})());",
44
+ ].join('\n');
45
+ return {
46
+ name: 'kazzle:dev-unregister-sw',
47
+ apply: 'serve',
48
+ configureServer(server) {
49
+ server.middlewares.use((req, res, next) => {
50
+ if (req.method !== 'GET') return next();
51
+ const url = req.url?.split('?')[0];
52
+ if (url !== '/sw.js' && url !== '/registerSW.js') return next();
53
+ res.setHeader('content-type', 'application/javascript; charset=utf-8');
54
+ res.setHeader('cache-control', 'no-store');
55
+ res.end(body);
56
+ });
57
+ },
58
+ };
59
+ }
60
+
61
+ export default defineConfig(({ command }) => ({
62
+ plugins: [
63
+ react(),
64
+ // Tailwind v4 — scans your JSX for utility classes and emits CSS. No
65
+ // tailwind.config.js or PostCSS setup needed; tokens/primitives live in
66
+ // src/styles/theme.css.
67
+ tailwindcss(),
68
+ devUnregisterServiceWorker(),
69
+ VitePWA({
70
+ registerType: 'autoUpdate',
71
+ workbox: {
72
+ // Precache the static asset bundle. Network requests to anything
73
+ // ending in these extensions get served from cache first.
74
+ globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
75
+ runtimeCaching: [
76
+ {
77
+ // External API responses get cached briefly and revalidated in
78
+ // the background. Edit this matcher if your app calls a
79
+ // different API host.
80
+ urlPattern: /^https:\/\/api\./i,
81
+ handler: 'NetworkFirst',
82
+ options: {
83
+ cacheName: 'api-cache',
84
+ expiration: { maxEntries: 50, maxAgeSeconds: 300 },
85
+ },
86
+ },
87
+ ],
88
+ },
89
+ // Keep disabled in dev — see header comment.
90
+ devOptions: {
91
+ enabled: false,
92
+ },
93
+ }),
94
+ ],
95
+ // HOST/PORT/strictPort/allowedHosts — the port contract. See @kazzle/app/vite.
96
+ ...kazzleAppVite(command),
97
+ }));
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from '@kazzle/app';
2
+
3
+ export default defineConfig({
4
+ /** Path to the app icon relative to the repo root. Uploaded to CDN on publish. */
5
+ icon: 'components/ui/public/favicon.svg',
6
+ /** App components — each entry defines a deployable unit. Max 1 UI component. */
7
+ components: [
8
+ { name: '{{APP_NAME}}', type: 'ui', path: './components/ui' },
9
+ ],
10
+ });
@@ -0,0 +1,16 @@
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
+ "check": "bun run --filter='./components/*' check"
12
+ },
13
+ "dependencies": {
14
+ "@kazzle/app": "{{KAZZLE_APP_VERSION}}"
15
+ }
16
+ }
@@ -0,0 +1,12 @@
1
+ # Kazzle template context
2
+
3
+ This app was created from the `ui-db` template. It already includes a React UI, a Hono process API, and ordinary Postgres wiring.
4
+
5
+ Template-owned setup:
6
+ - `kazzle.config.ts` declares the UI and server components. Database env vars are injected by `app.create`.
7
+ - `components/server/index.ts` owns the Todo/category API and reads `DATABASE_URL` on the server only.
8
+ - `components/ui/src/api.ts` is the typed browser client for the server API.
9
+ - `components/ui/package.json` and `components/server/package.json` already contain the install, build, and check scripts.
10
+ - The database schema is bootstrapped by template setup before the app is handed back.
11
+
12
+ Do not rerun `db.connect`, rewrite manifest env refs, recreate server boilerplate, or reinstall dependencies as setup repair after `app.create` reports template setup complete. Change these files only when the user asks for behavior beyond this template.
@@ -0,0 +1,218 @@
1
+ /**
2
+ * {{APP_NAME}} — server component for the UI + database template.
3
+ *
4
+ * Plain HTTP CRUD over Postgres. No realtime sync — the UI fetches on mount
5
+ * and re-fetches after mutations. If you outgrow that pattern (multiple
6
+ * clients, offline support, low-latency reads), graduate to the realtime
7
+ * template.
8
+ *
9
+ * The endpoints mirror the two resources used by the example UI: categories
10
+ * and the todos that belong to them. The shape is intentionally simple so
11
+ * customers can use it as a starting point — copy a route, change the table
12
+ * name, change the columns, ship.
13
+ *
14
+ * Notes on the SQL:
15
+ * - We use postgres.js's tagged-template form everywhere (sql`...`). Every
16
+ * interpolated value is bound as a parameter, not concatenated — this
17
+ * is the SQL-injection-safe pattern. Never use `sql.unsafe` here.
18
+ * - `RETURNING *` plus a CTE-join pattern lets the API return the same
19
+ * denormalised shape (todo + category name + category color) the UI
20
+ * reads on GET. The UI never has to reconcile two response shapes.
21
+ * - DELETEs of categories null out the foreign key on their todos rather
22
+ * than cascading. That's a UX choice; flip it if you prefer cascade.
23
+ */
24
+
25
+ import { serve } from '@hono/node-server';
26
+ import { Hono } from 'hono';
27
+ import { cors } from 'hono/cors';
28
+ import postgres from 'postgres';
29
+
30
+ const databaseUrl = process.env.DATABASE_URL;
31
+ if (!databaseUrl) throw new Error('DATABASE_URL is required');
32
+
33
+ const sql = postgres(databaseUrl);
34
+ const app = new Hono();
35
+
36
+ // CORS is wide-open here because the UI is served from the same origin in
37
+ // production (the platform routes both surfaces under one host). Tighten
38
+ // this if you ever expose the API to third-party clients.
39
+ app.use('*', cors());
40
+
41
+ // Liveness probe. Stays cheap on purpose — no DB query — so a slow query
42
+ // doesn't get this endpoint marked unhealthy.
43
+ app.get('/health', (c) => c.json({ ok: true }));
44
+
45
+ // ── Categories ───────────────────────────────────────────────────────────
46
+
47
+ app.get('/categories', async (c) => {
48
+ // LEFT JOIN + COUNT lets us return the sidebar count alongside the
49
+ // category in one round-trip — the UI doesn't have to make N+1 calls.
50
+ const rows = await sql`
51
+ SELECT c.id, c.name, c.color, COUNT(t.id)::int AS todo_count
52
+ FROM categories c
53
+ LEFT JOIN todos t ON t.category_id = c.id
54
+ GROUP BY c.id
55
+ ORDER BY c.name
56
+ `;
57
+ return c.json(rows);
58
+ });
59
+
60
+ app.post('/categories', async (c) => {
61
+ const body = await c.req.json<{ name?: string; color?: string }>();
62
+ const name = body.name?.trim();
63
+ if (!name) return c.json({ error: 'Name is required' }, 400);
64
+
65
+ const [category] = await sql`
66
+ INSERT INTO categories (name, color)
67
+ VALUES (${name}, ${body.color || '#6366f1'})
68
+ RETURNING id, name, color, 0::int AS todo_count
69
+ `;
70
+ return c.json(category, 201);
71
+ });
72
+
73
+ app.patch('/categories/:id', async (c) => {
74
+ const id = Number(c.req.param('id'));
75
+ if (!Number.isFinite(id)) return c.json({ error: 'Invalid category id' }, 400);
76
+
77
+ const body = await c.req.json<{ name?: string; color?: string }>();
78
+ const name = body.name?.trim();
79
+ const color = body.color?.trim();
80
+ if (name === undefined && color === undefined) {
81
+ return c.json({ error: 'No fields to update' }, 400);
82
+ }
83
+
84
+ // COALESCE(${value ?? null}, name) means "use the value if the client
85
+ // sent one, otherwise keep what's already there". This is the cleanest
86
+ // way to support partial updates without writing one route per field.
87
+ const [category] = await sql`
88
+ UPDATE categories
89
+ SET
90
+ name = COALESCE(${name ?? null}, name),
91
+ color = COALESCE(${color ?? null}, color)
92
+ WHERE id = ${id}
93
+ RETURNING id, name, color, (
94
+ SELECT COUNT(*)::int FROM todos WHERE category_id = ${id}
95
+ ) AS todo_count
96
+ `;
97
+ if (!category) return c.json({ error: 'Category not found' }, 404);
98
+ return c.json(category);
99
+ });
100
+
101
+ app.delete('/categories/:id', async (c) => {
102
+ const id = Number(c.req.param('id'));
103
+ if (!Number.isFinite(id)) return c.json({ error: 'Invalid category id' }, 400);
104
+
105
+ // Detach todos from the category instead of deleting them. Swap to
106
+ // `DELETE FROM todos WHERE category_id = ${id}` if you prefer cascade.
107
+ await sql`UPDATE todos SET category_id = NULL, updated_at = now() WHERE category_id = ${id}`;
108
+ await sql`DELETE FROM categories WHERE id = ${id}`;
109
+ return c.json({ ok: true });
110
+ });
111
+
112
+ // ── Todos ────────────────────────────────────────────────────────────────
113
+
114
+ app.get('/todos', async (c) => {
115
+ const categoryId = c.req.query('category_id');
116
+ // Same denormalised shape regardless of whether the UI filtered by
117
+ // category. The UI doesn't need to know two response variants.
118
+ const rows = categoryId
119
+ ? await sql`
120
+ SELECT t.*, c.name AS category_name, c.color AS category_color
121
+ FROM todos t
122
+ LEFT JOIN categories c ON c.id = t.category_id
123
+ WHERE t.category_id = ${Number(categoryId)}
124
+ ORDER BY t.completed ASC, t.created_at DESC
125
+ `
126
+ : await sql`
127
+ SELECT t.*, c.name AS category_name, c.color AS category_color
128
+ FROM todos t
129
+ LEFT JOIN categories c ON c.id = t.category_id
130
+ ORDER BY t.completed ASC, t.created_at DESC
131
+ `;
132
+ return c.json(rows);
133
+ });
134
+
135
+ app.post('/todos', async (c) => {
136
+ const body = await c.req.json<{ title?: string; category_id?: number | null }>();
137
+ const title = body.title?.trim();
138
+ if (!title) return c.json({ error: 'Title is required' }, 400);
139
+
140
+ // CTE + JOIN returns the inserted todo plus its category metadata in one
141
+ // statement. Keeps the response shape identical to GET /todos.
142
+ const [todo] = await sql`
143
+ WITH inserted AS (
144
+ INSERT INTO todos (title, category_id)
145
+ VALUES (${title}, ${body.category_id ?? null})
146
+ RETURNING *
147
+ )
148
+ SELECT i.*, c.name AS category_name, c.color AS category_color
149
+ FROM inserted i
150
+ LEFT JOIN categories c ON c.id = i.category_id
151
+ `;
152
+ return c.json(todo, 201);
153
+ });
154
+
155
+ app.patch('/todos/:id', async (c) => {
156
+ const id = Number(c.req.param('id'));
157
+ if (!Number.isFinite(id)) return c.json({ error: 'Invalid todo id' }, 400);
158
+
159
+ const body = await c.req.json<{ title?: string; completed?: boolean; category_id?: number | null }>();
160
+ const title = body.title?.trim();
161
+
162
+ // category_id needs its own branch because `null` is a valid value
163
+ // (clearing the category) but `undefined` means "don't touch". COALESCE
164
+ // doesn't distinguish those, so we pick the query at the TS level.
165
+ const [todo] = body.category_id === undefined
166
+ ? await sql`
167
+ WITH updated AS (
168
+ UPDATE todos
169
+ SET
170
+ title = COALESCE(${title ?? null}, title),
171
+ completed = COALESCE(${body.completed ?? null}, completed),
172
+ updated_at = now()
173
+ WHERE id = ${id}
174
+ RETURNING *
175
+ )
176
+ SELECT u.*, c.name AS category_name, c.color AS category_color
177
+ FROM updated u
178
+ LEFT JOIN categories c ON c.id = u.category_id
179
+ `
180
+ : await sql`
181
+ WITH updated AS (
182
+ UPDATE todos
183
+ SET
184
+ title = COALESCE(${title ?? null}, title),
185
+ completed = COALESCE(${body.completed ?? null}, completed),
186
+ category_id = ${body.category_id},
187
+ updated_at = now()
188
+ WHERE id = ${id}
189
+ RETURNING *
190
+ )
191
+ SELECT u.*, c.name AS category_name, c.color AS category_color
192
+ FROM updated u
193
+ LEFT JOIN categories c ON c.id = u.category_id
194
+ `;
195
+ if (!todo) return c.json({ error: 'Todo not found' }, 404);
196
+ return c.json(todo);
197
+ });
198
+
199
+ app.delete('/todos/:id', async (c) => {
200
+ const id = Number(c.req.param('id'));
201
+ if (!Number.isFinite(id)) return c.json({ error: 'Invalid todo id' }, 400);
202
+
203
+ await sql`DELETE FROM todos WHERE id = ${id}`;
204
+ return c.json({ ok: true });
205
+ });
206
+
207
+ // PORT and HOST come from the platform. The fallback is for direct local
208
+ // invocations.
209
+ // ─────────────────────────────────────────────────────────────────────────
210
+ // DO NOT CHANGE THE NEXT 4 LINES. DO NOT ADD FALLBACKS. DO NOT HARDCODE.
211
+ // PORT and HOST are injected by `kazzle run`. Missing values must throw.
212
+ // ─────────────────────────────────────────────────────────────────────────
213
+ if (!process.env.PORT || !process.env.HOST) {
214
+ throw new Error('PORT and HOST must be set by "kazzle run" — never hardcode them.');
215
+ }
216
+ const port = Number(process.env.PORT);
217
+ const hostname = process.env.HOST;
218
+ serve({ fetch: app.fetch, port, hostname });