@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,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 });
|