@korajs/cli 0.3.4 → 0.5.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 (104) hide show
  1. package/dist/bin.cjs +1839 -829
  2. package/dist/bin.cjs.map +1 -1
  3. package/dist/bin.js +1107 -146
  4. package/dist/bin.js.map +1 -1
  5. package/dist/{chunk-HNVCNPER.js → chunk-3WNFM3QB.js} +107 -97
  6. package/dist/chunk-3WNFM3QB.js.map +1 -0
  7. package/dist/{chunk-ULTO5NFA.js → chunk-PSRM56B7.js} +2 -2
  8. package/dist/chunk-PSRM56B7.js.map +1 -0
  9. package/dist/{chunk-MIMLCXSX.js → chunk-SOTZIWIF.js} +267 -228
  10. package/dist/chunk-SOTZIWIF.js.map +1 -0
  11. package/dist/create.cjs +58 -48
  12. package/dist/create.cjs.map +1 -1
  13. package/dist/create.js +2 -2
  14. package/dist/index.cjs +322 -273
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +31 -29
  17. package/dist/index.d.ts +31 -29
  18. package/dist/index.js +2 -2
  19. package/package.json +9 -3
  20. package/templates/react-basic/README.md.hbs +10 -1
  21. package/templates/react-basic/src/App.tsx +112 -126
  22. package/templates/react-basic/src/index.css +165 -159
  23. package/templates/react-basic/src/kora-worker.ts +0 -1
  24. package/templates/react-basic/src/main.tsx +12 -12
  25. package/templates/react-basic/src/modules/todos/todo.mutations.ts +25 -0
  26. package/templates/react-basic/src/modules/todos/todo.queries.ts +5 -0
  27. package/templates/react-basic/src/modules/todos/todo.schema.ts +10 -0
  28. package/templates/react-basic/src/modules/todos/useTodos.ts +31 -0
  29. package/templates/react-basic/src/schema.ts +6 -12
  30. package/templates/react-basic/src/vite-env.d.ts +17 -0
  31. package/templates/react-basic/{tsconfig.json → tsconfig.json.hbs} +1 -2
  32. package/templates/react-basic/vite.config.ts +57 -50
  33. package/templates/react-sync/.env.example +19 -0
  34. package/templates/react-sync/README.md.hbs +43 -1
  35. package/templates/react-sync/package.json.hbs +3 -0
  36. package/templates/react-sync/server.ts +90 -26
  37. package/templates/react-sync/src/App.tsx +132 -130
  38. package/templates/react-sync/src/auth.ts +25 -0
  39. package/templates/react-sync/src/index.css +212 -165
  40. package/templates/react-sync/src/kora-worker.ts +0 -1
  41. package/templates/react-sync/src/main.tsx +34 -18
  42. package/templates/react-sync/src/modules/todos/todo.mutations.ts +25 -0
  43. package/templates/react-sync/src/modules/todos/todo.queries.ts +5 -0
  44. package/templates/react-sync/src/modules/todos/todo.schema.ts +10 -0
  45. package/templates/react-sync/src/modules/todos/useTodos.ts +31 -0
  46. package/templates/react-sync/src/schema.ts +6 -12
  47. package/templates/react-sync/src/vite-env.d.ts +17 -0
  48. package/templates/react-sync/{tsconfig.json → tsconfig.json.hbs} +1 -2
  49. package/templates/react-sync/vite.config.ts +67 -60
  50. package/templates/react-tailwind/README.md.hbs +10 -1
  51. package/templates/react-tailwind/src/App.tsx +186 -197
  52. package/templates/react-tailwind/src/index.css +3 -3
  53. package/templates/react-tailwind/src/kora-worker.ts +0 -1
  54. package/templates/react-tailwind/src/main.tsx +19 -12
  55. package/templates/react-tailwind/src/modules/todos/todo.mutations.ts +25 -0
  56. package/templates/react-tailwind/src/modules/todos/todo.queries.ts +5 -0
  57. package/templates/react-tailwind/src/modules/todos/todo.schema.ts +10 -0
  58. package/templates/react-tailwind/src/modules/todos/useTodos.ts +31 -0
  59. package/templates/react-tailwind/src/schema.ts +6 -12
  60. package/templates/react-tailwind/src/vite-env.d.ts +17 -0
  61. package/templates/react-tailwind/{tsconfig.json → tsconfig.json.hbs} +1 -2
  62. package/templates/react-tailwind/vite.config.ts +57 -50
  63. package/templates/react-tailwind-sync/.env.example +19 -0
  64. package/templates/react-tailwind-sync/README.md.hbs +43 -1
  65. package/templates/react-tailwind-sync/package.json.hbs +3 -0
  66. package/templates/react-tailwind-sync/server.ts +90 -26
  67. package/templates/react-tailwind-sync/src/App.tsx +246 -222
  68. package/templates/react-tailwind-sync/src/auth.ts +25 -0
  69. package/templates/react-tailwind-sync/src/index.css +3 -3
  70. package/templates/react-tailwind-sync/src/kora-worker.ts +0 -1
  71. package/templates/react-tailwind-sync/src/main.tsx +48 -18
  72. package/templates/react-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
  73. package/templates/react-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
  74. package/templates/react-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
  75. package/templates/react-tailwind-sync/src/modules/todos/useTodos.ts +31 -0
  76. package/templates/react-tailwind-sync/src/schema.ts +6 -12
  77. package/templates/react-tailwind-sync/src/vite-env.d.ts +17 -0
  78. package/templates/react-tailwind-sync/{tsconfig.json → tsconfig.json.hbs} +1 -2
  79. package/templates/react-tailwind-sync/vite.config.ts +67 -60
  80. package/templates/tauri-react/.env.example +20 -0
  81. package/templates/tauri-react/README.md.hbs +36 -1
  82. package/templates/tauri-react/dev.ts +11 -11
  83. package/templates/tauri-react/package.json.hbs +2 -0
  84. package/templates/tauri-react/server.ts +86 -12
  85. package/templates/tauri-react/src/App.tsx +769 -544
  86. package/templates/tauri-react/src/AppShell.tsx +125 -83
  87. package/templates/tauri-react/src/SetupScreen.tsx +257 -208
  88. package/templates/tauri-react/src/auth.ts +45 -0
  89. package/templates/tauri-react/src/main.tsx +3 -3
  90. package/templates/tauri-react/src/modules/todos/todo.mutations.ts +25 -0
  91. package/templates/tauri-react/src/modules/todos/todo.queries.ts +5 -0
  92. package/templates/tauri-react/src/modules/todos/todo.schema.ts +10 -0
  93. package/templates/tauri-react/src/modules/todos/useTodos.ts +31 -0
  94. package/templates/tauri-react/src/schema.ts +6 -12
  95. package/templates/tauri-react/src/sync-config.ts +47 -47
  96. package/templates/tauri-react/src/updater.ts +15 -15
  97. package/templates/tauri-react/src/vite-env.d.ts +17 -0
  98. package/templates/tauri-react/src-tauri/tauri.conf.json +2 -2
  99. package/templates/tauri-react/tsconfig.json.hbs +14 -0
  100. package/templates/tauri-react/vite.config.ts +9 -9
  101. package/dist/chunk-HNVCNPER.js.map +0 -1
  102. package/dist/chunk-MIMLCXSX.js.map +0 -1
  103. package/dist/chunk-ULTO5NFA.js.map +0 -1
  104. package/templates/tauri-react/tsconfig.json +0 -15
package/dist/index.d.cts CHANGED
@@ -117,6 +117,7 @@ interface ProjectConfig {
117
117
  region: string | null;
118
118
  environment: 'preview' | 'production';
119
119
  confirm: boolean;
120
+ deployTarget?: 'full-stack' | 'sync-server';
120
121
  }
121
122
  interface ProvisionResult {
122
123
  applicationId: string;
@@ -425,7 +426,7 @@ declare function resetDeployState(projectRoot: string): Promise<void>;
425
426
  interface DockerfileOptions {
426
427
  nodeVersion?: string;
427
428
  port?: number;
428
- clientDirectory?: string;
429
+ clientDirectory?: string | null;
429
430
  serverBundleFile?: string;
430
431
  /** Native modules that must be installed at runtime (externalized from bundle). */
431
432
  nativeDependencies?: Record<string, string>;
@@ -569,6 +570,32 @@ declare function isAuthValue(value: string): value is AuthOption;
569
570
  declare function isDatabaseValue(value: string): value is DatabaseOption;
570
571
  declare function isDatabaseProviderValue(value: string): value is DatabaseProviderOption;
571
572
 
573
+ interface CreatePreferences {
574
+ platform: PlatformOption;
575
+ framework: FrameworkOption;
576
+ tailwind: boolean;
577
+ sync: boolean;
578
+ db: DatabaseOption;
579
+ dbProvider: DatabaseProviderOption;
580
+ auth: AuthOption;
581
+ packageManager: PackageManager;
582
+ }
583
+ /**
584
+ * Preference store for scaffold-time defaults in `create-kora-app`.
585
+ */
586
+ declare class PreferenceStore {
587
+ private readonly store;
588
+ constructor();
589
+ getCreatePreferences(): CreatePreferences | null;
590
+ saveCreatePreferences(preferences: CreatePreferences): void;
591
+ clearCreatePreferences(): void;
592
+ }
593
+ /**
594
+ * Gets preferences from storage or returns defaults when not available.
595
+ */
596
+ declare function getCreatePreferencesOrDefault(store: PreferenceStore): CreatePreferences;
597
+ declare function getDefaultCreatePreferences(): CreatePreferences;
598
+
572
599
  interface SelectOption<T extends string> {
573
600
  label: string;
574
601
  value: T;
@@ -615,32 +642,6 @@ declare class ClackPromptClient implements PromptClient {
615
642
  outro(message: string): void;
616
643
  }
617
644
 
618
- interface CreatePreferences {
619
- platform: PlatformOption;
620
- framework: FrameworkOption;
621
- tailwind: boolean;
622
- sync: boolean;
623
- db: DatabaseOption;
624
- dbProvider: DatabaseProviderOption;
625
- auth: AuthOption;
626
- packageManager: PackageManager;
627
- }
628
- /**
629
- * Preference store for scaffold-time defaults in `create-kora-app`.
630
- */
631
- declare class PreferenceStore {
632
- private readonly store;
633
- constructor();
634
- getCreatePreferences(): CreatePreferences | null;
635
- saveCreatePreferences(preferences: CreatePreferences): void;
636
- clearCreatePreferences(): void;
637
- }
638
- /**
639
- * Gets preferences from storage or returns defaults when not available.
640
- */
641
- declare function getCreatePreferencesOrDefault(store: PreferenceStore): CreatePreferences;
642
- declare function getDefaultCreatePreferences(): CreatePreferences;
643
-
644
645
  interface CreateFlags {
645
646
  platform?: string;
646
647
  framework?: string;
@@ -685,8 +686,9 @@ interface SyncProviderPresetOptions {
685
686
  /**
686
687
  * Applies provider-specific sync scaffolding adjustments after template copy.
687
688
  *
688
- * This is a lightweight bridge until template-layer composition lands.
689
- * Today we only specialize the sync server template when Postgres is selected.
689
+ * The sync server template is environment-driven: SQLite is used when
690
+ * DATABASE_URL is absent, and PostgreSQL is used when DATABASE_URL is present.
691
+ * Provider presets only add provider-specific guidance.
690
692
  */
691
693
  declare function applySyncProviderPreset(options: SyncProviderPresetOptions): Promise<void>;
692
694
 
package/dist/index.d.ts CHANGED
@@ -117,6 +117,7 @@ interface ProjectConfig {
117
117
  region: string | null;
118
118
  environment: 'preview' | 'production';
119
119
  confirm: boolean;
120
+ deployTarget?: 'full-stack' | 'sync-server';
120
121
  }
121
122
  interface ProvisionResult {
122
123
  applicationId: string;
@@ -425,7 +426,7 @@ declare function resetDeployState(projectRoot: string): Promise<void>;
425
426
  interface DockerfileOptions {
426
427
  nodeVersion?: string;
427
428
  port?: number;
428
- clientDirectory?: string;
429
+ clientDirectory?: string | null;
429
430
  serverBundleFile?: string;
430
431
  /** Native modules that must be installed at runtime (externalized from bundle). */
431
432
  nativeDependencies?: Record<string, string>;
@@ -569,6 +570,32 @@ declare function isAuthValue(value: string): value is AuthOption;
569
570
  declare function isDatabaseValue(value: string): value is DatabaseOption;
570
571
  declare function isDatabaseProviderValue(value: string): value is DatabaseProviderOption;
571
572
 
573
+ interface CreatePreferences {
574
+ platform: PlatformOption;
575
+ framework: FrameworkOption;
576
+ tailwind: boolean;
577
+ sync: boolean;
578
+ db: DatabaseOption;
579
+ dbProvider: DatabaseProviderOption;
580
+ auth: AuthOption;
581
+ packageManager: PackageManager;
582
+ }
583
+ /**
584
+ * Preference store for scaffold-time defaults in `create-kora-app`.
585
+ */
586
+ declare class PreferenceStore {
587
+ private readonly store;
588
+ constructor();
589
+ getCreatePreferences(): CreatePreferences | null;
590
+ saveCreatePreferences(preferences: CreatePreferences): void;
591
+ clearCreatePreferences(): void;
592
+ }
593
+ /**
594
+ * Gets preferences from storage or returns defaults when not available.
595
+ */
596
+ declare function getCreatePreferencesOrDefault(store: PreferenceStore): CreatePreferences;
597
+ declare function getDefaultCreatePreferences(): CreatePreferences;
598
+
572
599
  interface SelectOption<T extends string> {
573
600
  label: string;
574
601
  value: T;
@@ -615,32 +642,6 @@ declare class ClackPromptClient implements PromptClient {
615
642
  outro(message: string): void;
616
643
  }
617
644
 
618
- interface CreatePreferences {
619
- platform: PlatformOption;
620
- framework: FrameworkOption;
621
- tailwind: boolean;
622
- sync: boolean;
623
- db: DatabaseOption;
624
- dbProvider: DatabaseProviderOption;
625
- auth: AuthOption;
626
- packageManager: PackageManager;
627
- }
628
- /**
629
- * Preference store for scaffold-time defaults in `create-kora-app`.
630
- */
631
- declare class PreferenceStore {
632
- private readonly store;
633
- constructor();
634
- getCreatePreferences(): CreatePreferences | null;
635
- saveCreatePreferences(preferences: CreatePreferences): void;
636
- clearCreatePreferences(): void;
637
- }
638
- /**
639
- * Gets preferences from storage or returns defaults when not available.
640
- */
641
- declare function getCreatePreferencesOrDefault(store: PreferenceStore): CreatePreferences;
642
- declare function getDefaultCreatePreferences(): CreatePreferences;
643
-
644
645
  interface CreateFlags {
645
646
  platform?: string;
646
647
  framework?: string;
@@ -685,8 +686,9 @@ interface SyncProviderPresetOptions {
685
686
  /**
686
687
  * Applies provider-specific sync scaffolding adjustments after template copy.
687
688
  *
688
- * This is a lightweight bridge until template-layer composition lands.
689
- * Today we only specialize the sync server template when Postgres is selected.
689
+ * The sync server template is environment-driven: SQLite is used when
690
+ * DATABASE_URL is absent, and PostgreSQL is used when DATABASE_URL is present.
691
+ * Provider presets only add provider-specific guidance.
690
692
  */
691
693
  declare function applySyncProviderPreset(options: SyncProviderPresetOptions): Promise<void>;
692
694
 
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  writeDockerfileArtifact,
28
28
  writeFlyTomlArtifact,
29
29
  writeRailwayJsonArtifact
30
- } from "./chunk-MIMLCXSX.js";
30
+ } from "./chunk-SOTZIWIF.js";
31
31
  import {
32
32
  ClackPromptClient,
33
33
  CliError,
@@ -54,7 +54,7 @@ import {
54
54
  saveResolvedPreferences,
55
55
  shouldSavePreferences,
56
56
  validateProjectName
57
- } from "./chunk-HNVCNPER.js";
57
+ } from "./chunk-3WNFM3QB.js";
58
58
  export {
59
59
  ClackPromptClient,
60
60
  CliError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korajs/cli",
3
- "version": "0.3.4",
3
+ "version": "0.5.0",
4
4
  "description": "Kora.js CLI tooling and project scaffolding",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -36,14 +36,20 @@
36
36
  "conf": "^15.1.0",
37
37
  "esbuild": "^0.28.0",
38
38
  "validate-npm-package-name": "^7.0.2",
39
- "@korajs/core": "0.3.2"
39
+ "@korajs/core": "0.5.0"
40
40
  },
41
41
  "devDependencies": {
42
+ "@types/better-sqlite3": "^7.6.13",
42
43
  "@types/node": "^25.5.2",
43
44
  "@types/validate-npm-package-name": "^4.0.2",
45
+ "better-sqlite3": "^12.8.0",
44
46
  "tsup": "^8.3.6",
45
47
  "typescript": "^5.7.3",
46
- "vitest": "^3.0.4"
48
+ "vitest": "^3.0.4",
49
+ "@korajs/store": "0.5.0",
50
+ "@korajs/sync": "0.5.0",
51
+ "@korajs/merge": "0.5.0",
52
+ "korajs": "0.5.0"
47
53
  },
48
54
  "license": "MIT",
49
55
  "scripts": {
@@ -14,14 +14,23 @@ npm run dev
14
14
 
15
15
  ```
16
16
  src/
17
- schema.ts # Data schema definition
17
+ schema.ts # Schema entry point
18
18
  main.tsx # App initialization and Kora setup
19
19
  App.tsx # Main application component
20
+ modules/
21
+ todos/
22
+ todo.schema.ts # Todo collection definition
23
+ todo.queries.ts # Reusable todo query builders
24
+ todo.mutations.ts # Todo mutation helpers
25
+ useTodos.ts # React hook connecting todos to the UI
20
26
  kora-worker.ts # SQLite WASM web worker
21
27
  index.css # Styles
22
28
  kora.config.ts # Kora configuration
23
29
  ```
24
30
 
31
+ In a feature module, `schema` defines data shape, `queries` contain reads only, `mutations` contain
32
+ writes, and `useTodos.ts` is the React binding that exposes todo data and actions to components.
33
+
25
34
  ## Scripts
26
35
 
27
36
  | Script | Description |
@@ -1,146 +1,132 @@
1
1
  import { useState } from 'react'
2
- import { useQuery, useMutation, useCollection } from '@korajs/react'
2
+ import { useTodos } from './modules/todos/useTodos'
3
3
 
4
4
  type Filter = 'all' | 'active' | 'completed'
5
5
 
6
6
  export function App() {
7
- const todos = useCollection('todos')
8
- const allTodos = useQuery(todos.where({}).orderBy('createdAt', 'desc'))
9
- const { mutate: addTodo } = useMutation(
10
- (data: { title: string }) => todos.insert(data)
11
- )
12
- const { mutate: toggleTodo } = useMutation(
13
- (id: string, data: { completed: boolean }) => todos.update(id, data)
14
- )
15
- const { mutate: deleteTodo } = useMutation(
16
- (id: string) => todos.delete(id)
17
- )
7
+ const { allTodos, activeTodos, completedTodos, addTodo, toggleTodo, deleteTodo } = useTodos()
18
8
 
19
- const [filter, setFilter] = useState<Filter>('all')
20
- const [input, setInput] = useState('')
9
+ const [filter, setFilter] = useState<Filter>('all')
10
+ const [input, setInput] = useState('')
21
11
 
22
- const activeTodos = allTodos.filter((t) => !t.completed)
23
- const completedTodos = allTodos.filter((t) => !!t.completed)
24
- const filteredTodos =
25
- filter === 'active' ? activeTodos : filter === 'completed' ? completedTodos : allTodos
12
+ const filteredTodos =
13
+ filter === 'active' ? activeTodos : filter === 'completed' ? completedTodos : allTodos
26
14
 
27
- const handleSubmit = (e: React.FormEvent) => {
28
- e.preventDefault()
29
- const title = input.trim()
30
- if (title) {
31
- addTodo({ title })
32
- setInput('')
33
- }
34
- }
15
+ const handleSubmit = (e: React.FormEvent) => {
16
+ e.preventDefault()
17
+ const title = input.trim()
18
+ if (title) {
19
+ addTodo.mutate({ title })
20
+ setInput('')
21
+ }
22
+ }
35
23
 
36
- const clearCompleted = () => {
37
- for (const todo of completedTodos) {
38
- deleteTodo(todo.id)
39
- }
40
- }
24
+ const clearCompleted = () => {
25
+ for (const todo of completedTodos) {
26
+ deleteTodo.mutate(todo.id)
27
+ }
28
+ }
41
29
 
42
- return (
43
- <div className="app">
44
- <div className="header">
45
- <h1>My Tasks</h1>
46
- <span className="local-badge">Local-first</span>
47
- </div>
30
+ return (
31
+ <div className="app">
32
+ <div className="header">
33
+ <h1>My Tasks</h1>
34
+ <span className="local-badge">Local-first</span>
35
+ </div>
48
36
 
49
- <div className="stats">
50
- <div className="stat-card">
51
- <div className="label">Total</div>
52
- <div className="value muted">{allTodos.length}</div>
53
- </div>
54
- <div className="stat-card">
55
- <div className="label">Remaining</div>
56
- <div className="value warning">{activeTodos.length}</div>
57
- </div>
58
- <div className="stat-card">
59
- <div className="label">Done</div>
60
- <div className="value success">{completedTodos.length}</div>
61
- </div>
62
- </div>
37
+ <div className="stats">
38
+ <div className="stat-card">
39
+ <div className="label">Total</div>
40
+ <div className="value muted">{allTodos.length}</div>
41
+ </div>
42
+ <div className="stat-card">
43
+ <div className="label">Remaining</div>
44
+ <div className="value warning">{activeTodos.length}</div>
45
+ </div>
46
+ <div className="stat-card">
47
+ <div className="label">Done</div>
48
+ <div className="value success">{completedTodos.length}</div>
49
+ </div>
50
+ </div>
63
51
 
64
- <form className="add-form" onSubmit={handleSubmit}>
65
- <input
66
- type="text"
67
- value={input}
68
- onChange={(e) => setInput(e.target.value)}
69
- placeholder="What needs to be done?"
70
- />
71
- <button type="submit" disabled={!input.trim()}>
72
- Add
73
- </button>
74
- </form>
52
+ <form className="add-form" onSubmit={handleSubmit}>
53
+ <input
54
+ type="text"
55
+ value={input}
56
+ onChange={(e) => setInput(e.target.value)}
57
+ placeholder="What needs to be done?"
58
+ />
59
+ <button type="submit" disabled={!input.trim()}>
60
+ Add
61
+ </button>
62
+ </form>
75
63
 
76
- <div className="filters">
77
- {(['all', 'active', 'completed'] as const).map((f) => {
78
- const count = f === 'all' ? allTodos.length : f === 'active' ? activeTodos.length : completedTodos.length
79
- return (
80
- <button
81
- key={f}
82
- className={`filter-btn ${filter === f ? 'active' : ''}`}
83
- onClick={() => setFilter(f)}
84
- >
85
- {f.charAt(0).toUpperCase() + f.slice(1)}
86
- <span className="badge">{count}</span>
87
- </button>
88
- )
89
- })}
90
- </div>
64
+ <div className="filters">
65
+ {(['all', 'active', 'completed'] as const).map((f) => {
66
+ const count =
67
+ f === 'all'
68
+ ? allTodos.length
69
+ : f === 'active'
70
+ ? activeTodos.length
71
+ : completedTodos.length
72
+ return (
73
+ <button
74
+ key={f}
75
+ className={`filter-btn ${filter === f ? 'active' : ''}`}
76
+ onClick={() => setFilter(f)}
77
+ >
78
+ {f.charAt(0).toUpperCase() + f.slice(1)}
79
+ <span className="badge">{count}</span>
80
+ </button>
81
+ )
82
+ })}
83
+ </div>
91
84
 
92
- <div className="todo-list">
93
- {filteredTodos.length === 0 ? (
94
- <div className="empty-state">
95
- {filter === 'all' && 'No tasks yet. Add one above!'}
96
- {filter === 'active' && 'All caught up! No active tasks.'}
97
- {filter === 'completed' && 'No completed tasks yet.'}
98
- </div>
99
- ) : (
100
- filteredTodos.map((todo) => (
101
- <div key={todo.id} className="todo-item">
102
- <button
103
- className={`toggle ${todo.completed ? 'checked' : ''}`}
104
- onClick={() => toggleTodo(todo.id, { completed: !todo.completed })}
105
- >
106
- {todo.completed ? '\u2713' : ''}
107
- </button>
108
- <span className={`title ${todo.completed ? 'done' : ''}`}>
109
- {String(todo.title)}
110
- </span>
111
- {todo.createdAt && (
112
- <span className="time">{formatTime(Number(todo.createdAt))}</span>
113
- )}
114
- <button
115
- className="delete-btn"
116
- onClick={() => deleteTodo(todo.id)}
117
- >
118
- \u00d7
119
- </button>
120
- </div>
121
- ))
122
- )}
123
- </div>
85
+ <div className="todo-list">
86
+ {filteredTodos.length === 0 ? (
87
+ <div className="empty-state">
88
+ {filter === 'all' && 'No tasks yet. Add one above!'}
89
+ {filter === 'active' && 'All caught up! No active tasks.'}
90
+ {filter === 'completed' && 'No completed tasks yet.'}
91
+ </div>
92
+ ) : (
93
+ filteredTodos.map((todo) => (
94
+ <div key={todo.id} className="todo-item">
95
+ <button
96
+ className={`toggle ${todo.completed ? 'checked' : ''}`}
97
+ onClick={() => toggleTodo.mutate(todo.id, { completed: !todo.completed })}
98
+ >
99
+ {todo.completed ? '\u2713' : ''}
100
+ </button>
101
+ <span className={`title ${todo.completed ? 'done' : ''}`}>{String(todo.title)}</span>
102
+ {todo.createdAt && <span className="time">{formatTime(Number(todo.createdAt))}</span>}
103
+ <button className="delete-btn" onClick={() => deleteTodo.mutate(todo.id)}>
104
+ \u00d7
105
+ </button>
106
+ </div>
107
+ ))
108
+ )}
109
+ </div>
124
110
 
125
- {allTodos.length > 0 && (
126
- <div className="footer">
127
- <span>{activeTodos.length} item{activeTodos.length !== 1 ? 's' : ''} left</span>
128
- {completedTodos.length > 0 && (
129
- <button onClick={clearCompleted}>Clear completed</button>
130
- )}
131
- </div>
132
- )}
111
+ {allTodos.length > 0 && (
112
+ <div className="footer">
113
+ <span>
114
+ {activeTodos.length} item{activeTodos.length !== 1 ? 's' : ''} left
115
+ </span>
116
+ {completedTodos.length > 0 && <button onClick={clearCompleted}>Clear completed</button>}
117
+ </div>
118
+ )}
133
119
 
134
- <p className="branding">Powered by Kora &mdash; offline-first, real-time sync</p>
135
- </div>
136
- )
120
+ <p className="branding">Powered by Kora &mdash; offline-first, real-time sync</p>
121
+ </div>
122
+ )
137
123
  }
138
124
 
139
125
  function formatTime(timestamp: number): string {
140
- const date = new Date(timestamp)
141
- const now = new Date()
142
- if (date.toDateString() === now.toDateString()) {
143
- return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
144
- }
145
- return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
126
+ const date = new Date(timestamp)
127
+ const now = new Date()
128
+ if (date.toDateString() === now.toDateString()) {
129
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
130
+ }
131
+ return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
146
132
  }