@korajs/cli 0.1.8 → 0.1.11

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 (50) hide show
  1. package/dist/bin.cjs +52 -8
  2. package/dist/bin.cjs.map +1 -1
  3. package/dist/bin.js +2 -2
  4. package/dist/chunk-N36PFOSA.js +0 -0
  5. package/dist/{chunk-AUDUNRYA.js → chunk-SYOFLJLB.js} +33 -4
  6. package/dist/chunk-SYOFLJLB.js.map +1 -0
  7. package/dist/{chunk-ZVB4HAB3.js → chunk-VGOOQ56H.js} +22 -7
  8. package/dist/chunk-VGOOQ56H.js.map +1 -0
  9. package/dist/create.cjs +52 -8
  10. package/dist/create.cjs.map +1 -1
  11. package/dist/create.js +2 -2
  12. package/dist/index.cjs +21 -6
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +1 -1
  17. package/package.json +13 -13
  18. package/templates/react-basic/src/App.tsx +129 -31
  19. package/templates/react-basic/src/index.css +281 -0
  20. package/templates/react-basic/src/main.tsx +2 -0
  21. package/templates/react-basic/vite.config.ts +31 -1
  22. package/templates/react-sync/kora.config.ts +1 -1
  23. package/templates/react-sync/server.ts +13 -2
  24. package/templates/react-sync/src/App.tsx +132 -32
  25. package/templates/react-sync/src/index.css +292 -0
  26. package/templates/react-sync/src/main.tsx +2 -0
  27. package/templates/react-sync/vite.config.ts +31 -1
  28. package/templates/react-tailwind/index.html.hbs +12 -0
  29. package/templates/react-tailwind/kora.config.ts +13 -0
  30. package/templates/react-tailwind/package.json.hbs +31 -0
  31. package/templates/react-tailwind/src/App.tsx +212 -0
  32. package/templates/react-tailwind/src/index.css +7 -0
  33. package/templates/react-tailwind/src/kora-worker.ts +3 -0
  34. package/templates/react-tailwind/src/main.tsx +23 -0
  35. package/templates/react-tailwind/src/schema.ts +15 -0
  36. package/templates/react-tailwind/tsconfig.json +14 -0
  37. package/templates/react-tailwind/vite.config.ts +40 -0
  38. package/templates/react-tailwind-sync/index.html.hbs +12 -0
  39. package/templates/react-tailwind-sync/kora.config.ts +17 -0
  40. package/templates/react-tailwind-sync/package.json.hbs +33 -0
  41. package/templates/react-tailwind-sync/server.ts +22 -0
  42. package/templates/react-tailwind-sync/src/App.tsx +241 -0
  43. package/templates/react-tailwind-sync/src/index.css +7 -0
  44. package/templates/react-tailwind-sync/src/kora-worker.ts +3 -0
  45. package/templates/react-tailwind-sync/src/main.tsx +29 -0
  46. package/templates/react-tailwind-sync/src/schema.ts +15 -0
  47. package/templates/react-tailwind-sync/tsconfig.json +14 -0
  48. package/templates/react-tailwind-sync/vite.config.ts +40 -0
  49. package/dist/chunk-AUDUNRYA.js.map +0 -1
  50. package/dist/chunk-ZVB4HAB3.js.map +0 -1
@@ -1,50 +1,150 @@
1
+ import { useState } from 'react'
1
2
  import { useQuery, useMutation, useSyncStatus, useCollection } from '@korajs/react'
2
3
 
4
+ type Filter = 'all' | 'active' | 'completed'
5
+
3
6
  export function App() {
4
7
  const todos = useCollection('todos')
5
- const activeTodos = useQuery(todos.where({ completed: false }))
8
+ const allTodos = useQuery(todos.orderBy('createdAt', 'desc'))
6
9
  const { mutate: addTodo } = useMutation(
7
10
  (data: { title: string }) => todos.insert(data)
8
11
  )
9
12
  const { mutate: toggleTodo } = useMutation(
10
13
  (id: string, data: { completed: boolean }) => todos.update(id, data)
11
14
  )
15
+ const { mutate: deleteTodo } = useMutation(
16
+ (id: string) => todos.delete(id)
17
+ )
12
18
  const status = useSyncStatus()
13
19
 
20
+ const [filter, setFilter] = useState<Filter>('all')
21
+ const [input, setInput] = useState('')
22
+
23
+ const activeTodos = allTodos.filter((t) => !t.completed)
24
+ const completedTodos = allTodos.filter((t) => !!t.completed)
25
+ const filteredTodos =
26
+ filter === 'active' ? activeTodos : filter === 'completed' ? completedTodos : allTodos
27
+
28
+ const handleSubmit = (e: React.FormEvent) => {
29
+ e.preventDefault()
30
+ const title = input.trim()
31
+ if (title) {
32
+ addTodo({ title })
33
+ setInput('')
34
+ }
35
+ }
36
+
37
+ const clearCompleted = () => {
38
+ for (const todo of completedTodos) {
39
+ deleteTodo(todo.id)
40
+ }
41
+ }
42
+
14
43
  return (
15
- <div style={{ maxWidth: 600, margin: '0 auto', padding: 20 }}>
16
- <h1>Kora Todo App</h1>
17
- <p>Status: {status.status}</p>
18
-
19
- <form
20
- onSubmit={async (e) => {
21
- e.preventDefault()
22
- const form = e.currentTarget
23
- const input = form.elements.namedItem('title') as HTMLInputElement
24
- if (input.value.trim()) {
25
- addTodo({ title: input.value.trim() })
26
- input.value = ''
27
- }
28
- }}
29
- >
30
- <input name="title" placeholder="What needs to be done?" />
31
- <button type="submit">Add</button>
44
+ <div className="app">
45
+ <div className="header">
46
+ <h1>My Tasks</h1>
47
+ <div className="sync-badge">
48
+ <span className={`sync-dot ${status.status}`} />
49
+ <span>{status.status}</span>
50
+ </div>
51
+ </div>
52
+
53
+ <div className="stats">
54
+ <div className="stat-card">
55
+ <div className="label">Total</div>
56
+ <div className="value muted">{allTodos.length}</div>
57
+ </div>
58
+ <div className="stat-card">
59
+ <div className="label">Remaining</div>
60
+ <div className="value warning">{activeTodos.length}</div>
61
+ </div>
62
+ <div className="stat-card">
63
+ <div className="label">Done</div>
64
+ <div className="value success">{completedTodos.length}</div>
65
+ </div>
66
+ </div>
67
+
68
+ <form className="add-form" onSubmit={handleSubmit}>
69
+ <input
70
+ type="text"
71
+ value={input}
72
+ onChange={(e) => setInput(e.target.value)}
73
+ placeholder="What needs to be done?"
74
+ />
75
+ <button type="submit" disabled={!input.trim()}>
76
+ Add
77
+ </button>
32
78
  </form>
33
79
 
34
- <ul>
35
- {activeTodos.map((todo) => (
36
- <li key={todo.id}>
37
- <label>
38
- <input
39
- type="checkbox"
40
- checked={!!todo.completed}
41
- onChange={() => toggleTodo(todo.id, { completed: !todo.completed })}
42
- />
43
- {String(todo.title)}
44
- </label>
45
- </li>
46
- ))}
47
- </ul>
80
+ <div className="filters">
81
+ {(['all', 'active', 'completed'] as const).map((f) => {
82
+ const count = f === 'all' ? allTodos.length : f === 'active' ? activeTodos.length : completedTodos.length
83
+ return (
84
+ <button
85
+ key={f}
86
+ className={`filter-btn ${filter === f ? 'active' : ''}`}
87
+ onClick={() => setFilter(f)}
88
+ >
89
+ {f.charAt(0).toUpperCase() + f.slice(1)}
90
+ <span className="badge">{count}</span>
91
+ </button>
92
+ )
93
+ })}
94
+ </div>
95
+
96
+ <div className="todo-list">
97
+ {filteredTodos.length === 0 ? (
98
+ <div className="empty-state">
99
+ {filter === 'all' && 'No tasks yet. Add one above!'}
100
+ {filter === 'active' && 'All caught up! No active tasks.'}
101
+ {filter === 'completed' && 'No completed tasks yet.'}
102
+ </div>
103
+ ) : (
104
+ filteredTodos.map((todo) => (
105
+ <div key={todo.id} className="todo-item">
106
+ <button
107
+ className={`toggle ${todo.completed ? 'checked' : ''}`}
108
+ onClick={() => toggleTodo(todo.id, { completed: !todo.completed })}
109
+ >
110
+ {todo.completed ? '\u2713' : ''}
111
+ </button>
112
+ <span className={`title ${todo.completed ? 'done' : ''}`}>
113
+ {String(todo.title)}
114
+ </span>
115
+ {todo.createdAt && (
116
+ <span className="time">{formatTime(Number(todo.createdAt))}</span>
117
+ )}
118
+ <button
119
+ className="delete-btn"
120
+ onClick={() => deleteTodo(todo.id)}
121
+ >
122
+ \u00d7
123
+ </button>
124
+ </div>
125
+ ))
126
+ )}
127
+ </div>
128
+
129
+ {allTodos.length > 0 && (
130
+ <div className="footer">
131
+ <span>{activeTodos.length} item{activeTodos.length !== 1 ? 's' : ''} left</span>
132
+ {completedTodos.length > 0 && (
133
+ <button onClick={clearCompleted}>Clear completed</button>
134
+ )}
135
+ </div>
136
+ )}
137
+
138
+ <p className="branding">Powered by Kora &mdash; offline-first, real-time sync</p>
48
139
  </div>
49
140
  )
50
141
  }
142
+
143
+ function formatTime(timestamp: number): string {
144
+ const date = new Date(timestamp)
145
+ const now = new Date()
146
+ if (date.toDateString() === now.toDateString()) {
147
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
148
+ }
149
+ return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
150
+ }
@@ -0,0 +1,292 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ margin: 0;
6
+ padding: 0;
7
+ }
8
+
9
+ :root {
10
+ --color-bg: #0a0a0f;
11
+ --color-surface: #111118;
12
+ --color-border: #1e1e2a;
13
+ --color-border-hover: #2a2a3a;
14
+ --color-text: #e4e4e7;
15
+ --color-text-muted: #71717a;
16
+ --color-text-dim: #3f3f46;
17
+ --color-primary: #6366f1;
18
+ --color-primary-hover: #818cf8;
19
+ --color-success: #34d399;
20
+ --color-warning: #fbbf24;
21
+ --color-danger: #f87171;
22
+ --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
23
+ --radius: 8px;
24
+ }
25
+
26
+ body {
27
+ font-family: var(--font-sans);
28
+ background: var(--color-bg);
29
+ color: var(--color-text);
30
+ line-height: 1.6;
31
+ -webkit-font-smoothing: antialiased;
32
+ }
33
+
34
+ .app {
35
+ max-width: 640px;
36
+ margin: 0 auto;
37
+ padding: 48px 16px;
38
+ }
39
+
40
+ .header {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ margin-bottom: 32px;
45
+ }
46
+
47
+ .header h1 {
48
+ font-size: 1.5rem;
49
+ font-weight: 700;
50
+ }
51
+
52
+ .sync-badge {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ gap: 6px;
56
+ padding: 4px 12px;
57
+ border-radius: 999px;
58
+ background: var(--color-surface);
59
+ font-size: 0.8rem;
60
+ }
61
+
62
+ .sync-dot {
63
+ width: 8px;
64
+ height: 8px;
65
+ border-radius: 50%;
66
+ }
67
+
68
+ .sync-dot.connected,
69
+ .sync-dot.synced { background: var(--color-success); }
70
+ .sync-dot.syncing { background: var(--color-warning); }
71
+ .sync-dot.offline { background: var(--color-text-muted); }
72
+ .sync-dot.error { background: var(--color-danger); }
73
+
74
+ .stats {
75
+ display: grid;
76
+ grid-template-columns: repeat(3, 1fr);
77
+ gap: 12px;
78
+ margin-bottom: 32px;
79
+ }
80
+
81
+ .stat-card {
82
+ background: var(--color-surface);
83
+ border: 1px solid var(--color-border);
84
+ border-radius: var(--radius);
85
+ padding: 16px;
86
+ }
87
+
88
+ .stat-card .label {
89
+ font-size: 0.8rem;
90
+ color: var(--color-text-muted);
91
+ }
92
+
93
+ .stat-card .value {
94
+ font-size: 1.5rem;
95
+ font-weight: 700;
96
+ }
97
+
98
+ .stat-card .value.muted { color: var(--color-text-muted); }
99
+ .stat-card .value.warning { color: var(--color-warning); }
100
+ .stat-card .value.success { color: var(--color-success); }
101
+
102
+ .add-form {
103
+ display: flex;
104
+ gap: 12px;
105
+ margin-bottom: 32px;
106
+ }
107
+
108
+ .add-form input {
109
+ flex: 1;
110
+ padding: 10px 16px;
111
+ border: 1px solid var(--color-border);
112
+ border-radius: var(--radius);
113
+ background: var(--color-surface);
114
+ color: var(--color-text);
115
+ font-size: 0.95rem;
116
+ outline: none;
117
+ }
118
+
119
+ .add-form input:focus {
120
+ border-color: var(--color-primary);
121
+ }
122
+
123
+ .add-form input::placeholder {
124
+ color: var(--color-text-muted);
125
+ }
126
+
127
+ .add-form button {
128
+ padding: 10px 20px;
129
+ border: none;
130
+ border-radius: var(--radius);
131
+ background: var(--color-primary);
132
+ color: white;
133
+ font-size: 0.95rem;
134
+ font-weight: 500;
135
+ cursor: pointer;
136
+ }
137
+
138
+ .add-form button:hover {
139
+ background: var(--color-primary-hover);
140
+ }
141
+
142
+ .add-form button:disabled {
143
+ opacity: 0.5;
144
+ cursor: not-allowed;
145
+ }
146
+
147
+ .filters {
148
+ display: flex;
149
+ gap: 8px;
150
+ margin-bottom: 24px;
151
+ }
152
+
153
+ .filter-btn {
154
+ padding: 6px 16px;
155
+ border: none;
156
+ border-radius: var(--radius);
157
+ background: var(--color-surface);
158
+ color: var(--color-text-muted);
159
+ font-size: 0.85rem;
160
+ font-weight: 500;
161
+ cursor: pointer;
162
+ }
163
+
164
+ .filter-btn:hover {
165
+ background: var(--color-border-hover);
166
+ color: var(--color-text);
167
+ }
168
+
169
+ .filter-btn.active {
170
+ background: var(--color-primary);
171
+ color: white;
172
+ }
173
+
174
+ .filter-btn .badge {
175
+ display: inline-block;
176
+ margin-left: 6px;
177
+ padding: 1px 6px;
178
+ border-radius: 999px;
179
+ font-size: 0.75rem;
180
+ background: rgba(255, 255, 255, 0.15);
181
+ }
182
+
183
+ .filter-btn:not(.active) .badge {
184
+ background: var(--color-border);
185
+ }
186
+
187
+ .todo-list {
188
+ display: flex;
189
+ flex-direction: column;
190
+ gap: 8px;
191
+ }
192
+
193
+ .todo-item {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 12px;
197
+ padding: 12px 16px;
198
+ border: 1px solid var(--color-border);
199
+ border-radius: var(--radius);
200
+ background: var(--color-surface);
201
+ }
202
+
203
+ .todo-item:hover {
204
+ border-color: var(--color-border-hover);
205
+ }
206
+
207
+ .todo-item .toggle {
208
+ background: none;
209
+ border: 2px solid var(--color-text-muted);
210
+ width: 20px;
211
+ height: 20px;
212
+ border-radius: 50%;
213
+ cursor: pointer;
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: center;
217
+ color: transparent;
218
+ font-size: 0.7rem;
219
+ flex-shrink: 0;
220
+ }
221
+
222
+ .todo-item .toggle.checked {
223
+ border-color: var(--color-success);
224
+ background: var(--color-success);
225
+ color: white;
226
+ }
227
+
228
+ .todo-item .title {
229
+ flex: 1;
230
+ }
231
+
232
+ .todo-item .title.done {
233
+ text-decoration: line-through;
234
+ color: var(--color-text-muted);
235
+ }
236
+
237
+ .todo-item .time {
238
+ font-size: 0.75rem;
239
+ color: var(--color-text-dim);
240
+ }
241
+
242
+ .todo-item .delete-btn {
243
+ background: none;
244
+ border: none;
245
+ color: var(--color-text-dim);
246
+ cursor: pointer;
247
+ padding: 4px;
248
+ opacity: 0;
249
+ transition: opacity 0.15s;
250
+ }
251
+
252
+ .todo-item:hover .delete-btn {
253
+ opacity: 1;
254
+ }
255
+
256
+ .todo-item .delete-btn:hover {
257
+ color: var(--color-danger);
258
+ }
259
+
260
+ .empty-state {
261
+ padding: 48px 0;
262
+ text-align: center;
263
+ color: var(--color-text-dim);
264
+ border: 1px dashed var(--color-border);
265
+ border-radius: var(--radius);
266
+ }
267
+
268
+ .footer {
269
+ display: flex;
270
+ justify-content: space-between;
271
+ margin-top: 24px;
272
+ font-size: 0.85rem;
273
+ color: var(--color-text-muted);
274
+ }
275
+
276
+ .footer button {
277
+ background: none;
278
+ border: none;
279
+ color: var(--color-text-muted);
280
+ cursor: pointer;
281
+ }
282
+
283
+ .footer button:hover {
284
+ color: var(--color-text);
285
+ }
286
+
287
+ .branding {
288
+ margin-top: 48px;
289
+ text-align: center;
290
+ font-size: 0.75rem;
291
+ color: var(--color-text-dim);
292
+ }
@@ -4,6 +4,7 @@ import { StrictMode } from 'react'
4
4
  import { createRoot } from 'react-dom/client'
5
5
  import schema from './schema'
6
6
  import { App } from './App'
7
+ import './index.css'
7
8
 
8
9
  const app = createApp({
9
10
  schema,
@@ -13,6 +14,7 @@ const app = createApp({
13
14
  store: {
14
15
  workerUrl: new URL('./kora-worker.ts', import.meta.url),
15
16
  },
17
+ devtools: true,
16
18
  })
17
19
 
18
20
  // Connect to sync server once the app is ready
@@ -1,9 +1,39 @@
1
1
  import react from '@vitejs/plugin-react'
2
+ import type { Plugin } from 'vite'
2
3
  import { defineConfig } from 'vite'
3
4
 
5
+ /**
6
+ * Adds Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers
7
+ * required for SharedArrayBuffer. Without these headers, OPFS SAH Pool VFS
8
+ * cannot be used and SQLite WASM falls back to in-memory storage (no persistence).
9
+ */
10
+ function crossOriginIsolation(): Plugin {
11
+ return {
12
+ name: 'cross-origin-isolation',
13
+ configureServer(server) {
14
+ server.middlewares.use((_req, res, next) => {
15
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
16
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
17
+ next()
18
+ })
19
+ },
20
+ configurePreviewServer(server) {
21
+ server.middlewares.use((_req, res, next) => {
22
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
23
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
24
+ next()
25
+ })
26
+ },
27
+ }
28
+ }
29
+
4
30
  export default defineConfig({
5
- plugins: [react()],
31
+ plugins: [react(), crossOriginIsolation()],
6
32
  optimizeDeps: {
7
33
  exclude: ['@sqlite.org/sqlite-wasm', '@korajs/store'],
34
+ include: ['yjs'],
35
+ },
36
+ resolve: {
37
+ dedupe: ['yjs'],
8
38
  },
9
39
  })
@@ -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>{{projectName}}</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,13 @@
1
+ import { defineConfig } from 'korajs/config'
2
+
3
+ export default defineConfig({
4
+ schema: './src/schema.ts',
5
+ dev: {
6
+ port: 5173,
7
+ watch: {
8
+ enabled: true,
9
+ debounceMs: 300,
10
+ },
11
+ sync: false,
12
+ },
13
+ })
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "kora dev",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "korajs": "{{koraVersion}}",
13
+ "@korajs/react": "{{koraVersion}}",
14
+ "@korajs/store": "{{koraVersion}}",
15
+ "@sqlite.org/sqlite-wasm": ">=3.51.0-build1",
16
+ "lucide-react": "^0.468.0",
17
+ "react": "^19.0.0",
18
+ "react-dom": "^19.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@korajs/cli": "{{koraVersion}}",
22
+ "@tailwindcss/vite": "^4.0.0",
23
+ "@types/react": "^19.0.0",
24
+ "@types/react-dom": "^19.0.0",
25
+ "@vitejs/plugin-react": "^4.3.0",
26
+ "tailwindcss": "^4.0.0",
27
+ "tsx": "^4.19.0",
28
+ "typescript": "^5.7.0",
29
+ "vite": "^6.0.0"
30
+ }
31
+ }