@korajs/cli 0.5.0 → 0.6.0

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 (169) hide show
  1. package/dist/bin.cjs +76 -7
  2. package/dist/bin.cjs.map +1 -1
  3. package/dist/bin.js +3 -3
  4. package/dist/{chunk-PSRM56B7.js → chunk-EEZNRI5W.js} +51 -6
  5. package/dist/chunk-EEZNRI5W.js.map +1 -0
  6. package/dist/{chunk-SOTZIWIF.js → chunk-Q2FBCOQD.js} +2 -2
  7. package/dist/{chunk-3WNFM3QB.js → chunk-VLTPEATY.js} +70 -4
  8. package/dist/chunk-VLTPEATY.js.map +1 -0
  9. package/dist/create.cjs +76 -7
  10. package/dist/create.cjs.map +1 -1
  11. package/dist/create.js +2 -2
  12. package/dist/index.cjs +65 -3
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +2 -2
  17. package/package.json +6 -6
  18. package/templates/svelte-basic/.env.example +3 -0
  19. package/templates/svelte-basic/README.md.hbs +62 -0
  20. package/templates/svelte-basic/index.html.hbs +12 -0
  21. package/templates/svelte-basic/kora.config.ts +12 -0
  22. package/templates/svelte-basic/package.json.hbs +27 -0
  23. package/templates/svelte-basic/src/App.svelte +131 -0
  24. package/templates/svelte-basic/src/Root.svelte +14 -0
  25. package/templates/svelte-basic/src/index.css +287 -0
  26. package/templates/svelte-basic/src/kora-worker.ts +9 -0
  27. package/templates/svelte-basic/src/main.ts +16 -0
  28. package/templates/svelte-basic/src/modules/todos/todo.mutations.ts +25 -0
  29. package/templates/svelte-basic/src/modules/todos/todo.queries.ts +5 -0
  30. package/templates/svelte-basic/src/modules/todos/todo.schema.ts +10 -0
  31. package/templates/svelte-basic/src/modules/todos/useTodos.ts +25 -0
  32. package/templates/svelte-basic/src/schema.ts +9 -0
  33. package/templates/svelte-basic/src/vite-env.d.ts +12 -0
  34. package/templates/svelte-basic/svelte.config.js +5 -0
  35. package/templates/svelte-basic/tsconfig.json.hbs +14 -0
  36. package/templates/svelte-basic/vite.config.ts +52 -0
  37. package/templates/svelte-sync/.env.example +25 -0
  38. package/templates/svelte-sync/README.md.hbs +105 -0
  39. package/templates/svelte-sync/index.html.hbs +12 -0
  40. package/templates/svelte-sync/kora.config.ts +17 -0
  41. package/templates/svelte-sync/package.json.hbs +43 -0
  42. package/templates/svelte-sync/server.ts +90 -0
  43. package/templates/svelte-sync/src/App.svelte +154 -0
  44. package/templates/svelte-sync/src/Root.svelte +22 -0
  45. package/templates/svelte-sync/src/auth.ts +25 -0
  46. package/templates/svelte-sync/src/index.css +339 -0
  47. package/templates/svelte-sync/src/kora-worker.ts +9 -0
  48. package/templates/svelte-sync/src/main.ts +32 -0
  49. package/templates/svelte-sync/src/modules/todos/todo.mutations.ts +25 -0
  50. package/templates/svelte-sync/src/modules/todos/todo.queries.ts +5 -0
  51. package/templates/svelte-sync/src/modules/todos/todo.schema.ts +10 -0
  52. package/templates/svelte-sync/src/modules/todos/useTodos.ts +25 -0
  53. package/templates/svelte-sync/src/schema.ts +9 -0
  54. package/templates/svelte-sync/src/vite-env.d.ts +21 -0
  55. package/templates/svelte-sync/svelte.config.js +5 -0
  56. package/templates/svelte-sync/tsconfig.json.hbs +14 -0
  57. package/templates/svelte-sync/vite.config.ts +79 -0
  58. package/templates/svelte-tailwind/README.md.hbs +62 -0
  59. package/templates/svelte-tailwind/index.html.hbs +12 -0
  60. package/templates/svelte-tailwind/kora.config.ts +12 -0
  61. package/templates/svelte-tailwind/package.json.hbs +30 -0
  62. package/templates/svelte-tailwind/src/App.svelte +79 -0
  63. package/templates/svelte-tailwind/src/Root.svelte +14 -0
  64. package/templates/svelte-tailwind/src/index.css +7 -0
  65. package/templates/svelte-tailwind/src/kora-worker.ts +9 -0
  66. package/templates/svelte-tailwind/src/main.ts +16 -0
  67. package/templates/svelte-tailwind/src/modules/todos/todo.mutations.ts +25 -0
  68. package/templates/svelte-tailwind/src/modules/todos/todo.queries.ts +5 -0
  69. package/templates/svelte-tailwind/src/modules/todos/todo.schema.ts +10 -0
  70. package/templates/svelte-tailwind/src/modules/todos/useTodos.ts +25 -0
  71. package/templates/svelte-tailwind/src/schema.ts +9 -0
  72. package/templates/svelte-tailwind/src/vite-env.d.ts +12 -0
  73. package/templates/svelte-tailwind/svelte.config.js +5 -0
  74. package/templates/svelte-tailwind/tsconfig.json.hbs +14 -0
  75. package/templates/svelte-tailwind/vite.config.ts +53 -0
  76. package/templates/svelte-tailwind-sync/.env.example +25 -0
  77. package/templates/svelte-tailwind-sync/README.md.hbs +105 -0
  78. package/templates/svelte-tailwind-sync/index.html.hbs +12 -0
  79. package/templates/svelte-tailwind-sync/kora.config.ts +17 -0
  80. package/templates/svelte-tailwind-sync/package.json.hbs +46 -0
  81. package/templates/svelte-tailwind-sync/server.ts +90 -0
  82. package/templates/svelte-tailwind-sync/src/App.svelte +196 -0
  83. package/templates/svelte-tailwind-sync/src/Root.svelte +26 -0
  84. package/templates/svelte-tailwind-sync/src/auth.ts +25 -0
  85. package/templates/svelte-tailwind-sync/src/index.css +7 -0
  86. package/templates/svelte-tailwind-sync/src/kora-worker.ts +9 -0
  87. package/templates/svelte-tailwind-sync/src/main.ts +32 -0
  88. package/templates/svelte-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
  89. package/templates/svelte-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
  90. package/templates/svelte-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
  91. package/templates/svelte-tailwind-sync/src/modules/todos/useTodos.ts +25 -0
  92. package/templates/svelte-tailwind-sync/src/schema.ts +9 -0
  93. package/templates/svelte-tailwind-sync/src/vite-env.d.ts +21 -0
  94. package/templates/svelte-tailwind-sync/svelte.config.js +5 -0
  95. package/templates/svelte-tailwind-sync/tsconfig.json.hbs +14 -0
  96. package/templates/svelte-tailwind-sync/vite.config.ts +80 -0
  97. package/templates/vue-basic/README.md.hbs +62 -0
  98. package/templates/vue-basic/index.html.hbs +12 -0
  99. package/templates/vue-basic/kora.config.ts +12 -0
  100. package/templates/vue-basic/package.json.hbs +26 -0
  101. package/templates/vue-basic/src/App.vue +117 -0
  102. package/templates/vue-basic/src/index.css +287 -0
  103. package/templates/vue-basic/src/kora-worker.ts +9 -0
  104. package/templates/vue-basic/src/main.ts +20 -0
  105. package/templates/vue-basic/src/modules/todos/todo.mutations.ts +25 -0
  106. package/templates/vue-basic/src/modules/todos/todo.queries.ts +5 -0
  107. package/templates/vue-basic/src/modules/todos/todo.schema.ts +10 -0
  108. package/templates/vue-basic/src/modules/todos/useTodos.ts +32 -0
  109. package/templates/vue-basic/src/schema.ts +9 -0
  110. package/templates/vue-basic/src/vite-env.d.ts +17 -0
  111. package/templates/vue-basic/tsconfig.json.hbs +15 -0
  112. package/templates/vue-basic/vite.config.ts +69 -0
  113. package/templates/vue-sync/.env.example +25 -0
  114. package/templates/vue-sync/README.md.hbs +105 -0
  115. package/templates/vue-sync/index.html.hbs +12 -0
  116. package/templates/vue-sync/kora.config.ts +17 -0
  117. package/templates/vue-sync/package.json.hbs +42 -0
  118. package/templates/vue-sync/server.ts +90 -0
  119. package/templates/vue-sync/src/App.vue +135 -0
  120. package/templates/vue-sync/src/auth.ts +25 -0
  121. package/templates/vue-sync/src/index.css +339 -0
  122. package/templates/vue-sync/src/kora-worker.ts +9 -0
  123. package/templates/vue-sync/src/main.ts +43 -0
  124. package/templates/vue-sync/src/modules/todos/todo.mutations.ts +25 -0
  125. package/templates/vue-sync/src/modules/todos/todo.queries.ts +5 -0
  126. package/templates/vue-sync/src/modules/todos/todo.schema.ts +10 -0
  127. package/templates/vue-sync/src/modules/todos/useTodos.ts +32 -0
  128. package/templates/vue-sync/src/schema.ts +9 -0
  129. package/templates/vue-sync/src/vite-env.d.ts +26 -0
  130. package/templates/vue-sync/tsconfig.json.hbs +15 -0
  131. package/templates/vue-sync/vite.config.ts +79 -0
  132. package/templates/vue-tailwind/README.md.hbs +62 -0
  133. package/templates/vue-tailwind/index.html.hbs +12 -0
  134. package/templates/vue-tailwind/kora.config.ts +12 -0
  135. package/templates/vue-tailwind/package.json.hbs +29 -0
  136. package/templates/vue-tailwind/src/App.vue +142 -0
  137. package/templates/vue-tailwind/src/index.css +7 -0
  138. package/templates/vue-tailwind/src/kora-worker.ts +9 -0
  139. package/templates/vue-tailwind/src/main.ts +31 -0
  140. package/templates/vue-tailwind/src/modules/todos/todo.mutations.ts +25 -0
  141. package/templates/vue-tailwind/src/modules/todos/todo.queries.ts +5 -0
  142. package/templates/vue-tailwind/src/modules/todos/todo.schema.ts +10 -0
  143. package/templates/vue-tailwind/src/modules/todos/useTodos.ts +32 -0
  144. package/templates/vue-tailwind/src/schema.ts +9 -0
  145. package/templates/vue-tailwind/src/vite-env.d.ts +17 -0
  146. package/templates/vue-tailwind/tsconfig.json.hbs +15 -0
  147. package/templates/vue-tailwind/vite.config.ts +70 -0
  148. package/templates/vue-tailwind-sync/.env.example +25 -0
  149. package/templates/vue-tailwind-sync/README.md.hbs +105 -0
  150. package/templates/vue-tailwind-sync/index.html.hbs +12 -0
  151. package/templates/vue-tailwind-sync/kora.config.ts +17 -0
  152. package/templates/vue-tailwind-sync/package.json.hbs +45 -0
  153. package/templates/vue-tailwind-sync/server.ts +90 -0
  154. package/templates/vue-tailwind-sync/src/App.vue +245 -0
  155. package/templates/vue-tailwind-sync/src/auth.ts +25 -0
  156. package/templates/vue-tailwind-sync/src/index.css +7 -0
  157. package/templates/vue-tailwind-sync/src/kora-worker.ts +9 -0
  158. package/templates/vue-tailwind-sync/src/main.ts +56 -0
  159. package/templates/vue-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
  160. package/templates/vue-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
  161. package/templates/vue-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
  162. package/templates/vue-tailwind-sync/src/modules/todos/useTodos.ts +32 -0
  163. package/templates/vue-tailwind-sync/src/schema.ts +9 -0
  164. package/templates/vue-tailwind-sync/src/vite-env.d.ts +26 -0
  165. package/templates/vue-tailwind-sync/tsconfig.json.hbs +15 -0
  166. package/templates/vue-tailwind-sync/vite.config.ts +80 -0
  167. package/dist/chunk-3WNFM3QB.js.map +0 -1
  168. package/dist/chunk-PSRM56B7.js.map +0 -1
  169. /package/dist/{chunk-SOTZIWIF.js.map → chunk-Q2FBCOQD.js.map} +0 -0
@@ -0,0 +1,90 @@
1
+ import {
2
+ type CreateKoraAuthServerOptions,
3
+ createKoraAuthServer,
4
+ createPostgresOAuthStores,
5
+ createSqliteOAuthStores,
6
+ googleProvider,
7
+ } from '@korajs/auth/server'
8
+ import {
9
+ createPostgresServerStore,
10
+ createProductionServer,
11
+ createSqliteServerStore,
12
+ } from '@korajs/server'
13
+ import schema from './src/schema'
14
+
15
+ async function createStore() {
16
+ if (process.env.DATABASE_URL) {
17
+ return createPostgresServerStore({
18
+ connectionString: process.env.DATABASE_URL,
19
+ })
20
+ }
21
+
22
+ return createSqliteServerStore({
23
+ filename: process.env.KORA_SERVER_DB || './kora-server.db',
24
+ })
25
+ }
26
+
27
+ async function start() {
28
+ const store = await createStore()
29
+ await store.setSchema(schema)
30
+
31
+ const syncPath = process.env.KORA_SYNC_PATH || '/kora-sync'
32
+ const auth = await createAuth()
33
+ const server = createProductionServer({
34
+ store,
35
+ port: Number(process.env.PORT) || 3001,
36
+ staticDir: './dist',
37
+ syncPath,
38
+ httpRoutes: auth ? [{ path: '/auth', handle: auth.handleRequest }] : undefined,
39
+ syncOptions: auth ? { auth: auth.auth } : undefined,
40
+ operationalAuth: {
41
+ adminToken: process.env.KORA_ADMIN_TOKEN,
42
+ metricsToken: process.env.KORA_METRICS_TOKEN,
43
+ backupToken: process.env.KORA_BACKUP_TOKEN,
44
+ },
45
+ })
46
+
47
+ const url = await server.start()
48
+ console.log(`Kora app running at ${url}`)
49
+ console.log(` Sync endpoint: ${url.replace('http', 'ws')}${syncPath}`)
50
+ if (auth) console.log(` Auth endpoint: ${url}/auth`)
51
+ }
52
+
53
+ void start()
54
+
55
+ async function createAuth() {
56
+ if (!process.env.KORA_AUTH_SECRET) {
57
+ return null
58
+ }
59
+
60
+ const oauth = await createOAuthConfig()
61
+ return createKoraAuthServer({
62
+ jwtSecret: process.env.KORA_AUTH_SECRET,
63
+ ...(oauth ? { oauth } : {}),
64
+ })
65
+ }
66
+
67
+ async function createOAuthConfig(): Promise<CreateKoraAuthServerOptions['oauth'] | undefined> {
68
+ if (!process.env.KORA_GOOGLE_CLIENT_ID || !process.env.KORA_GOOGLE_REDIRECT_URI) {
69
+ return undefined
70
+ }
71
+
72
+ const oauthStores = process.env.DATABASE_URL
73
+ ? await createPostgresOAuthStores({ connectionString: process.env.DATABASE_URL })
74
+ : await createSqliteOAuthStores({
75
+ filename: process.env.KORA_AUTH_DB || './kora-auth.db',
76
+ })
77
+
78
+ return {
79
+ providers: [
80
+ googleProvider({
81
+ clientId: process.env.KORA_GOOGLE_CLIENT_ID,
82
+ clientSecret: process.env.KORA_GOOGLE_CLIENT_SECRET,
83
+ redirectUri: process.env.KORA_GOOGLE_REDIRECT_URI,
84
+ pkce: process.env.KORA_OAUTH_PKCE === 'true' || !process.env.KORA_GOOGLE_CLIENT_SECRET,
85
+ }),
86
+ ],
87
+ stateStore: oauthStores.stateStore,
88
+ linkedIdentityStore: oauthStores.linkedIdentityStore,
89
+ }
90
+ }
@@ -0,0 +1,245 @@
1
+ <script setup lang="ts">
2
+ import { useAuth } from '@korajs/auth/vue'
3
+ import { useSyncStatus } from '@korajs/vue'
4
+ import {
5
+ AlertCircle,
6
+ CheckCircle2,
7
+ Circle,
8
+ ClipboardList,
9
+ Loader2,
10
+ Plus,
11
+ Trash2,
12
+ Wifi,
13
+ WifiOff,
14
+ } from 'lucide-vue-next'
15
+ import { computed, ref } from 'vue'
16
+ import { useTodos } from './modules/todos/useTodos'
17
+
18
+ type Filter = 'all' | 'active' | 'completed'
19
+
20
+ const { allTodos, activeTodos, completedTodos, addTodo, toggleTodo, deleteTodo } = useTodos()
21
+ const syncStatus = useSyncStatus()
22
+ const { user, isAuthenticated, signInWithOAuth, signOut, error } = useAuth()
23
+
24
+ const filter = ref<Filter>('all')
25
+ const input = ref('')
26
+
27
+ const filteredTodos = computed(() => {
28
+ if (filter.value === 'active') return activeTodos.value
29
+ if (filter.value === 'completed') return completedTodos.value
30
+ return allTodos.value
31
+ })
32
+
33
+ const syncBadge = computed(() => {
34
+ const status = syncStatus.value
35
+ const s = status.status
36
+ const pending = status.pendingOperations ?? 0
37
+ const map: Record<string, { icon: typeof Wifi; color: string; label: string }> = {
38
+ connected: { icon: Wifi, color: 'text-emerald-400', label: 'Connected' },
39
+ syncing: { icon: Wifi, color: 'text-amber-400', label: 'Syncing' },
40
+ synced: { icon: Wifi, color: 'text-emerald-400', label: 'Synced' },
41
+ offline: { icon: WifiOff, color: 'text-gray-500', label: 'Offline' },
42
+ error: { icon: AlertCircle, color: 'text-red-400', label: 'Error' },
43
+ }
44
+ return { ...(map[s] ?? map.offline), pending }
45
+ })
46
+
47
+ function handleSubmit(): void {
48
+ const title = input.value.trim()
49
+ if (!title) return
50
+ addTodo.mutate({ title })
51
+ input.value = ''
52
+ }
53
+
54
+ function clearCompleted(): void {
55
+ for (const todo of completedTodos.value) {
56
+ deleteTodo.mutate(todo.id)
57
+ }
58
+ }
59
+
60
+ function emptyMessage(f: Filter): string {
61
+ if (f === 'all') return 'No tasks yet. Add one above!'
62
+ if (f === 'active') return 'All caught up! No active tasks.'
63
+ return 'No completed tasks yet.'
64
+ }
65
+
66
+ function formatTime(timestamp: number): string {
67
+ const date = new Date(timestamp)
68
+ const now = new Date()
69
+ if (date.toDateString() === now.toDateString()) {
70
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
71
+ }
72
+ return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
73
+ }
74
+ </script>
75
+
76
+ <template>
77
+ <div class="min-h-screen bg-gray-950 text-gray-100">
78
+ <div class="mx-auto max-w-2xl px-4 py-12">
79
+ <div class="mb-8 flex items-center justify-between">
80
+ <div class="flex items-center gap-3">
81
+ <ClipboardList class="h-8 w-8 text-indigo-400" />
82
+ <h1 class="text-2xl font-bold">My Tasks</h1>
83
+ </div>
84
+ <div class="flex items-center gap-2">
85
+ <div class="flex items-center gap-2 rounded-full bg-gray-800 px-3 py-1.5 text-sm">
86
+ <component :is="syncBadge.icon" :class="['h-4 w-4', syncBadge.color]" />
87
+ <span :class="syncBadge.color">{{ syncBadge.label }}</span>
88
+ <span
89
+ v-if="syncBadge.pending > 0"
90
+ class="rounded-full bg-gray-700 px-2 py-0.5 text-xs text-gray-400"
91
+ >
92
+ {{ syncBadge.pending }} pending
93
+ </span>
94
+ </div>
95
+ <button
96
+ v-if="isAuthenticated"
97
+ type="button"
98
+ class="max-w-44 truncate rounded-full border border-gray-700 bg-gray-900 px-3 py-1.5 text-sm text-gray-300 transition hover:border-gray-600"
99
+ @click="signOut()"
100
+ >
101
+ {{ user?.email || 'Sign out' }}
102
+ </button>
103
+ <button
104
+ v-else
105
+ type="button"
106
+ class="rounded-full border border-gray-700 bg-gray-900 px-3 py-1.5 text-sm text-gray-300 transition hover:border-gray-600"
107
+ @click="signInWithOAuth('google')"
108
+ >
109
+ Sign in
110
+ </button>
111
+ </div>
112
+ </div>
113
+
114
+ <div v-if="error" class="mb-4 rounded-lg border border-red-400/30 px-3 py-2 text-sm text-red-300">
115
+ {{ error }}
116
+ </div>
117
+
118
+ <div class="mb-8 grid grid-cols-3 gap-4">
119
+ <div class="rounded-lg border border-gray-800 bg-gray-900 p-4">
120
+ <p class="text-sm text-gray-500">Total</p>
121
+ <p class="text-2xl font-bold text-gray-300">{{ allTodos.length }}</p>
122
+ </div>
123
+ <div class="rounded-lg border border-gray-800 bg-gray-900 p-4">
124
+ <p class="text-sm text-gray-500">Remaining</p>
125
+ <p class="text-2xl font-bold text-amber-400">{{ activeTodos.length }}</p>
126
+ </div>
127
+ <div class="rounded-lg border border-gray-800 bg-gray-900 p-4">
128
+ <p class="text-sm text-gray-500">Done</p>
129
+ <p class="text-2xl font-bold text-emerald-400">{{ completedTodos.length }}</p>
130
+ </div>
131
+ </div>
132
+
133
+ <form class="mb-8 flex gap-3" @submit.prevent="handleSubmit">
134
+ <input
135
+ v-model="input"
136
+ type="text"
137
+ placeholder="What needs to be done?"
138
+ class="flex-1 rounded-lg border border-gray-700 bg-gray-900 px-4 py-3 text-gray-100 placeholder-gray-500 outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
139
+ />
140
+ <button
141
+ type="submit"
142
+ :disabled="addTodo.isLoading.value || !input.trim()"
143
+ class="flex items-center gap-2 rounded-lg bg-indigo-600 px-5 py-3 font-medium text-white transition hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-50"
144
+ >
145
+ <Loader2 v-if="addTodo.isLoading.value" class="h-4 w-4 animate-spin" />
146
+ <Plus v-else class="h-4 w-4" />
147
+ Add
148
+ </button>
149
+ </form>
150
+
151
+ <div class="mb-6 flex gap-2">
152
+ <button
153
+ v-for="f in (['all', 'active', 'completed'] as const)"
154
+ :key="f"
155
+ type="button"
156
+ :class="[
157
+ 'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition',
158
+ filter === f
159
+ ? 'bg-indigo-600 text-white'
160
+ : 'bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-gray-200',
161
+ ]"
162
+ @click="filter = f"
163
+ >
164
+ {{ f.charAt(0).toUpperCase() + f.slice(1) }}
165
+ <span
166
+ :class="[
167
+ 'rounded-full px-2 py-0.5 text-xs',
168
+ filter === f ? 'bg-indigo-500 text-white' : 'bg-gray-700 text-gray-400',
169
+ ]"
170
+ >
171
+ {{
172
+ f === 'all'
173
+ ? allTodos.length
174
+ : f === 'active'
175
+ ? activeTodos.length
176
+ : completedTodos.length
177
+ }}
178
+ </span>
179
+ </button>
180
+ </div>
181
+
182
+ <div class="space-y-2">
183
+ <div
184
+ v-if="filteredTodos.length === 0"
185
+ class="rounded-lg border border-dashed border-gray-800 py-12 text-center text-gray-600"
186
+ >
187
+ {{ emptyMessage(filter) }}
188
+ </div>
189
+ <template v-else>
190
+ <div
191
+ v-for="todo in filteredTodos"
192
+ :key="todo.id"
193
+ class="group flex items-center gap-3 rounded-lg border border-gray-800 bg-gray-900 px-4 py-3 transition hover:border-gray-700"
194
+ >
195
+ <button
196
+ type="button"
197
+ class="shrink-0 text-gray-500 transition hover:text-indigo-400"
198
+ @click="toggleTodo.mutate(todo.id, { completed: !todo.completed })"
199
+ >
200
+ <CheckCircle2 v-if="todo.completed" class="h-5 w-5 text-emerald-400" />
201
+ <Circle v-else class="h-5 w-5" />
202
+ </button>
203
+ <span
204
+ :class="[
205
+ 'flex-1',
206
+ todo.completed ? 'text-gray-500 line-through' : 'text-gray-100',
207
+ ]"
208
+ >
209
+ {{ String(todo.title) }}
210
+ </span>
211
+ <span v-if="todo.createdAt" class="text-xs text-gray-600">
212
+ {{ formatTime(Number(todo.createdAt)) }}
213
+ </span>
214
+ <button
215
+ type="button"
216
+ class="shrink-0 text-gray-600 opacity-0 transition hover:text-red-400 group-hover:opacity-100"
217
+ @click="deleteTodo.mutate(todo.id)"
218
+ >
219
+ <Trash2 class="h-4 w-4" />
220
+ </button>
221
+ </div>
222
+ </template>
223
+ </div>
224
+
225
+ <div
226
+ v-if="allTodos.length > 0"
227
+ class="mt-6 flex items-center justify-between text-sm text-gray-500"
228
+ >
229
+ <span>{{ activeTodos.length }} item{{ activeTodos.length !== 1 ? 's' : '' }} left</span>
230
+ <button
231
+ v-if="completedTodos.length > 0"
232
+ type="button"
233
+ class="text-gray-500 transition hover:text-gray-300"
234
+ @click="clearCompleted"
235
+ >
236
+ Clear completed
237
+ </button>
238
+ </div>
239
+
240
+ <p class="mt-12 text-center text-xs text-gray-700">
241
+ Powered by Kora — offline-first, real-time sync
242
+ </p>
243
+ </div>
244
+ </div>
245
+ </template>
@@ -0,0 +1,25 @@
1
+ import { createKoraAuth } from '@korajs/auth'
2
+
3
+ export const authServerUrl =
4
+ import.meta.env.VITE_AUTH_URL || `${window.location.protocol}//${window.location.host}`
5
+
6
+ export const authClient = createKoraAuth({
7
+ serverUrl: authServerUrl,
8
+ })
9
+
10
+ export async function completeOAuthCallbackFromLocation(): Promise<void> {
11
+ const url = new URL(window.location.href)
12
+ const code = url.searchParams.get('code')
13
+ const state = url.searchParams.get('state')
14
+ const provider = url.searchParams.get('provider') || 'google'
15
+
16
+ if (!code || !state) {
17
+ return
18
+ }
19
+
20
+ await authClient.completeOAuthSignIn(provider, { code, state })
21
+ url.searchParams.delete('code')
22
+ url.searchParams.delete('state')
23
+ url.searchParams.delete('provider')
24
+ window.history.replaceState({}, document.title, `${url.pathname}${url.search}${url.hash}`)
25
+ }
@@ -0,0 +1,7 @@
1
+ @import "tailwindcss";
2
+
3
+ @layer base {
4
+ body {
5
+ @apply bg-gray-950 text-gray-100 antialiased;
6
+ }
7
+ }
@@ -0,0 +1,9 @@
1
+ // Web Worker entry point for SQLite WASM.
2
+ // Loaded automatically by the Kora store to run SQLite in a background thread.
3
+
4
+ // Import the WASM binary URL so Vite resolves it with the correct content hash.
5
+ // Without this, production builds fail because sqlite3 looks for the unhashed filename.
6
+ import sqliteWasmUrl from '@sqlite.org/sqlite-wasm/sqlite3.wasm?url'
7
+ ;(globalThis as Record<string, unknown>).__KORA_SQLITE_WASM_URL = sqliteWasmUrl
8
+
9
+ import '@korajs/store/sqlite-wasm/worker'
@@ -0,0 +1,56 @@
1
+ import { createKoraAuthSync } from '@korajs/auth'
2
+ import { AuthProvider } from '@korajs/auth/vue'
3
+ import { KoraProvider } from '@korajs/vue'
4
+ import { createApp as createKoraApp } from 'korajs'
5
+ import { createApp, h } from 'vue'
6
+ import App from './App.vue'
7
+ import { authClient, completeOAuthCallbackFromLocation } from './auth'
8
+ import schema from './schema'
9
+ import './index.css'
10
+ import koraWorkerUrl from './kora-worker.ts?worker&url'
11
+
12
+ const syncUrl =
13
+ import.meta.env.VITE_SYNC_URL ||
14
+ `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/kora-sync`
15
+
16
+ const kora = createKoraApp({
17
+ schema,
18
+ sync: {
19
+ url: syncUrl,
20
+ authClient: createKoraAuthSync({ authClient, schema }),
21
+ },
22
+ store: {
23
+ workerUrl: koraWorkerUrl,
24
+ },
25
+ devtools: true,
26
+ })
27
+
28
+ kora.ready.then(() => kora.sync?.connect())
29
+ void completeOAuthCallbackFromLocation().catch((error) => {
30
+ console.error('[Kora Auth] OAuth callback failed:', error)
31
+ })
32
+
33
+ const loadingFallback = h(
34
+ 'div',
35
+ { class: 'flex h-screen items-center justify-center bg-gray-950 text-gray-400' },
36
+ 'Loading...',
37
+ )
38
+
39
+ createApp({
40
+ render: () =>
41
+ h(
42
+ AuthProvider,
43
+ {
44
+ client: authClient,
45
+ fallback: h(
46
+ 'div',
47
+ { class: 'flex h-screen items-center justify-center bg-gray-950 text-gray-400' },
48
+ 'Restoring session...',
49
+ ),
50
+ },
51
+ {
52
+ default: () =>
53
+ h(KoraProvider, { app: kora, fallback: loadingFallback }, { default: () => h(App) }),
54
+ },
55
+ ),
56
+ }).mount('#app')
@@ -0,0 +1,25 @@
1
+ import type { CollectionAccessor } from 'korajs'
2
+
3
+ export interface CreateTodoInput {
4
+ title: string
5
+ }
6
+
7
+ export interface UpdateTodoStatusInput {
8
+ completed: boolean
9
+ }
10
+
11
+ export function createTodo(todos: CollectionAccessor, data: CreateTodoInput) {
12
+ return todos.insert({ title: data.title })
13
+ }
14
+
15
+ export function updateTodoStatus(
16
+ todos: CollectionAccessor,
17
+ id: string,
18
+ data: UpdateTodoStatusInput,
19
+ ) {
20
+ return todos.update(id, { completed: data.completed })
21
+ }
22
+
23
+ export function deleteTodo(todos: CollectionAccessor, id: string) {
24
+ return todos.delete(id)
25
+ }
@@ -0,0 +1,5 @@
1
+ import type { CollectionAccessor } from 'korajs'
2
+
3
+ export function orderedTodos(todos: CollectionAccessor) {
4
+ return todos.where({}).orderBy('createdAt', 'desc')
5
+ }
@@ -0,0 +1,10 @@
1
+ import { t } from 'korajs'
2
+
3
+ export const todos = {
4
+ fields: {
5
+ title: t.string(),
6
+ completed: t.boolean().default(false),
7
+ createdAt: t.timestamp().auto(),
8
+ },
9
+ indexes: ['completed', 'createdAt'],
10
+ }
@@ -0,0 +1,32 @@
1
+ import { computed } from 'vue'
2
+ import { useCollection, useMutation, useQuery } from '@korajs/vue'
3
+ import {
4
+ type CreateTodoInput,
5
+ type UpdateTodoStatusInput,
6
+ createTodo,
7
+ deleteTodo,
8
+ updateTodoStatus,
9
+ } from './todo.mutations'
10
+ import { orderedTodos } from './todo.queries'
11
+
12
+ export function useTodos() {
13
+ const todos = useCollection('todos')
14
+ const allTodos = useQuery(orderedTodos(todos))
15
+ const addTodo = useMutation((data: CreateTodoInput) => createTodo(todos, data))
16
+ const toggleTodo = useMutation((id: string, data: UpdateTodoStatusInput) =>
17
+ updateTodoStatus(todos, id, data),
18
+ )
19
+ const removeTodo = useMutation((id: string) => deleteTodo(todos, id))
20
+
21
+ const activeTodos = computed(() => allTodos.value.filter((todo) => !todo.completed))
22
+ const completedTodos = computed(() => allTodos.value.filter((todo) => !!todo.completed))
23
+
24
+ return {
25
+ allTodos,
26
+ activeTodos,
27
+ completedTodos,
28
+ addTodo,
29
+ toggleTodo,
30
+ deleteTodo: removeTodo,
31
+ }
32
+ }
@@ -0,0 +1,9 @@
1
+ import { defineSchema } from 'korajs'
2
+ import { todos } from './modules/todos/todo.schema'
3
+
4
+ export default defineSchema({
5
+ version: 1,
6
+ collections: {
7
+ todos,
8
+ },
9
+ })
@@ -0,0 +1,26 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.vue' {
4
+ import type { DefineComponent } from 'vue'
5
+ const component: DefineComponent<object, object, unknown>
6
+ export default component
7
+ }
8
+
9
+ interface ImportMetaEnv {
10
+ readonly VITE_SYNC_URL?: string
11
+ readonly VITE_AUTH_URL?: string
12
+ }
13
+
14
+ interface ImportMeta {
15
+ readonly env: ImportMetaEnv
16
+ }
17
+
18
+ declare module '*?worker&url' {
19
+ const url: string
20
+ export default url
21
+ }
22
+
23
+ declare module '*?url' {
24
+ const url: string
25
+ export default url
26
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "noUncheckedIndexedAccess": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "jsx": "preserve",
11
+ "lib": ["ES2022", "DOM"],
12
+ "types": ["vite/client"]
13
+ },
14
+ "include": ["src/**/*.ts", "src/**/*.vue"]
15
+ }
@@ -0,0 +1,80 @@
1
+ import { copyFileSync, existsSync, readdirSync } from 'node:fs'
2
+ import { join, resolve } from 'node:path'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+ import vue from '@vitejs/plugin-vue'
5
+ import type { Plugin } from 'vite'
6
+ import { defineConfig } from 'vite'
7
+
8
+ function crossOriginIsolation(): Plugin {
9
+ return {
10
+ name: 'cross-origin-isolation',
11
+ configureServer(server) {
12
+ server.middlewares.use((_req, res, next) => {
13
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
14
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
15
+ next()
16
+ })
17
+ },
18
+ configurePreviewServer(server) {
19
+ server.middlewares.use((_req, res, next) => {
20
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
21
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
22
+ next()
23
+ })
24
+ },
25
+ }
26
+ }
27
+
28
+ function sqliteWasmHotfix(): Plugin {
29
+ return {
30
+ name: 'sqlite-wasm-hotfix',
31
+ apply: 'build',
32
+ closeBundle() {
33
+ const assetsDir = resolve('dist', 'assets')
34
+ if (!existsSync(assetsDir)) return
35
+
36
+ for (const file of readdirSync(assetsDir)) {
37
+ if (/^sqlite3-.+\.wasm$/.test(file)) {
38
+ copyFileSync(join(assetsDir, file), join(assetsDir, 'sqlite3.wasm'))
39
+ break
40
+ }
41
+ }
42
+
43
+ const proxyFile = resolve(
44
+ 'node_modules',
45
+ '@sqlite.org',
46
+ 'sqlite-wasm',
47
+ 'sqlite-wasm',
48
+ 'jswasm',
49
+ 'sqlite3-opfs-async-proxy.js',
50
+ )
51
+ if (existsSync(proxyFile)) {
52
+ copyFileSync(proxyFile, join(assetsDir, 'sqlite3-opfs-async-proxy.js'))
53
+ }
54
+ },
55
+ }
56
+ }
57
+
58
+ export default defineConfig({
59
+ plugins: [vue(), tailwindcss(), crossOriginIsolation(), sqliteWasmHotfix()],
60
+ worker: {
61
+ format: 'es',
62
+ },
63
+ optimizeDeps: {
64
+ exclude: ['@sqlite.org/sqlite-wasm', '@korajs/store'],
65
+ include: ['yjs'],
66
+ },
67
+ resolve: {
68
+ dedupe: ['yjs'],
69
+ },
70
+ server: {
71
+ allowedHosts: true,
72
+ proxy: {
73
+ '/kora-sync': {
74
+ target: 'ws://localhost:3001',
75
+ ws: true,
76
+ rewriteWsOrigin: true,
77
+ },
78
+ },
79
+ },
80
+ })