xote 1.0.1 → 1.0.2

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 CHANGED
@@ -1,23 +1,36 @@
1
1
  # xote (pronounced [ˈʃɔtʃi])
2
+ [![npm version](https://img.shields.io/npm/v/xote.svg)](https://www.npmjs.com/package/xote)
2
3
 
3
- A lightweight, zero-dependency UI library for ReScript with fine-grained reactivity powered by signals. Build reactive web applications with automatic dependency tracking and efficient updates.
4
+ A lightweight, zero-dependency UI library for ReScript with fine-grained reactivity based on the [TC39 Signals proposal](https://github.com/tc39/proposal-signals). Build reactive web applications with automatic dependency tracking and efficient updates.
4
5
 
5
6
  ## Features
6
7
 
7
- - **Zero dependencies** - No runtime dependencies, pure ReScript implementation
8
- - **Reactive primitives** - Signals, computed values, and effects
9
- - **Component system** - Declarative UI components with automatic reactive updates
10
- - **Automatic dependency tracking** - No manual subscription management
11
- - **Batching and control** - Support for untracked reads and batched updates
12
- - **Lightweight** - Small bundle size, minimal overhead
8
+ - Zero dependencies: pure ReScript implementation
9
+ - Lightweight (~1kb) and efficient runtime
10
+ - Declarative components for building reactive UIs (JSX support comming up soon)
11
+ - Reactive primitives: signals, computed values, and effects
12
+ - Automatic dependency tracking: no manual subscription management
13
+ - Fine-grained updates: direct DOM updates without a virtual DOM
13
14
 
14
15
  ## Getting Started
15
16
 
16
- Comming soon.
17
-
18
17
  ### Installation
19
18
 
20
- Comming soon.
19
+ ```bash
20
+ npm install xote
21
+ # or
22
+ yarn add xote
23
+ # or
24
+ pnpm add xote
25
+ ```
26
+
27
+ Then, add it to your ReScript project’s dependencies in `rescript.json`:
28
+
29
+ ```json
30
+ {
31
+ "bs-dependencies": ["xote"]
32
+ }
33
+ ```
21
34
 
22
35
  ### Quick Example
23
36
 
@@ -27,13 +40,13 @@ open Xote
27
40
  // Create reactive state
28
41
  let count = Signal.make(0)
29
42
 
30
- // Create computed values
43
+ // Derived value
31
44
  let doubled = Computed.make(() => Signal.get(count) * 2)
32
45
 
33
- // Define event handlers
46
+ // Event handler
34
47
  let increment = (_evt: Dom.event) => Signal.update(count, n => n + 1)
35
48
 
36
- // Build your UI
49
+ // Build the UI
37
50
  let app = Component.div(
38
51
  ~children=[
39
52
  Component.h1(~children=[Component.text("Counter")], ()),
@@ -51,266 +64,26 @@ let app = Component.div(
51
64
  ()
52
65
  )
53
66
 
54
- // Mount to DOM
67
+ // Mount to the DOM
55
68
  Component.mountById(app, "app")
56
69
  ```
57
70
 
58
- ## API Reference
59
-
60
- ### Signals - Reactive State
61
-
62
- Signals are the foundation of reactive state management in Xote.
63
-
64
- ```rescript
65
- // Create a signal with initial value
66
- let count = Signal.make(0)
67
-
68
- // Read signal value (tracks dependencies)
69
- let value = Signal.get(count) // => 0
70
-
71
- // Read without tracking
72
- let value = Signal.peek(count) // => 0
73
-
74
- // Update signal value
75
- Signal.set(count, 1)
76
-
77
- // Update with a function
78
- Signal.update(count, n => n + 1)
79
- ```
80
-
81
- ### Computed - Derived State
82
-
83
- Computed values automatically update when their dependencies change.
84
-
85
- ```rescript
86
- let count = Signal.make(5)
87
-
88
- // Computed values are signals that derive from other signals
89
- let doubled = Computed.make(() => Signal.get(count) * 2)
90
-
91
- Signal.get(doubled) // => 10
92
-
93
- Signal.set(count, 10)
94
- Signal.get(doubled) // => 20
95
- ```
96
-
97
- ### Effects - Side Effects
98
-
99
- Effects run automatically when their dependencies change.
100
-
101
- ```rescript
102
- let count = Signal.make(0)
103
-
104
- // Effect runs immediately and re-runs when count changes
105
- let disposer = Effect.run(() => {
106
- Console.log("Count is now: " ++ Int.toString(Signal.get(count)))
107
- })
108
-
109
- Signal.set(count, 1) // Logs: "Count is now: 1"
110
-
111
- // Clean up effect
112
- disposer.dispose()
113
- ```
114
-
115
- ### Components - Building UI
116
-
117
- #### Basic Elements
118
-
119
- ```rescript
120
- // Text nodes
121
- Component.text("Hello, world!")
122
-
123
- // Reactive text from signals
124
- let name = Signal.make("Alice")
125
- Component.textSignal(
126
- Computed.make(() => "Hello, " ++ Signal.get(name))
127
- )
128
-
129
- // HTML elements
130
- Component.div(
131
- ~attrs=[("class", "container"), ("id", "main")],
132
- ~events=[("click", handleClick)],
133
- ~children=[
134
- Component.h1(~children=[Component.text("Title")], ()),
135
- Component.p(~children=[Component.text("Content")], ())
136
- ],
137
- ()
138
- )
139
- ```
140
-
141
- #### Reactive Lists
142
-
143
- Render lists that automatically update when data changes:
144
-
145
- ```rescript
146
- let todos = Signal.make([
147
- {id: 1, text: "Learn Xote"},
148
- {id: 2, text: "Build an app"}
149
- ])
150
-
151
- let todoItem = (todo) => {
152
- Component.li(~children=[Component.text(todo.text)], ())
153
- }
154
-
155
- Component.ul(
156
- ~children=[Component.list(todos, todoItem)],
157
- ()
158
- )
159
- // List updates automatically when todos signal changes!
160
- ```
161
-
162
- #### Event Handling
163
-
164
- ```rescript
165
- let handleClick = (evt: Dom.event) => {
166
- Console.log("Clicked!")
167
- }
168
-
169
- Component.button(
170
- ~events=[("click", handleClick), ("mouseenter", handleHover)],
171
- ~children=[Component.text("Click me")],
172
- ()
173
- )
174
- ```
175
-
176
- ### Utilities
177
-
178
- #### Untracked Reads
179
-
180
- Read signals without creating dependencies.
181
-
182
- ```rescript
183
- Core.untrack(() => {
184
- let value = Signal.get(count) // Won't track this read
185
- Console.log(value)
186
- })
187
- ```
188
-
189
- #### Batching Updates
190
-
191
- Group multiple updates to run effects only once.
192
-
193
- ```rescript
194
- Core.batch(() => {
195
- Signal.set(count1, 10)
196
- Signal.set(count2, 20)
197
- Signal.set(count3, 30)
198
- })
199
- // Effects run once after all updates
200
- ```
201
-
202
- ## Complete Example
203
-
204
- ### Counter Application
205
-
206
- ```rescript
207
- open Xote
208
-
209
- // State management
210
- let counterValue = Signal.make(0)
211
-
212
- // Derived state
213
- let counterDisplay = Computed.make(() =>
214
- "Count: " ++ Int.toString(Signal.get(counterValue))
215
- )
216
-
217
- let isEven = Computed.make(() =>
218
- mod(Signal.get(counterValue), 2) == 0
219
- )
220
-
221
- // Event handlers
222
- let increment = (_evt: Dom.event) => {
223
- Signal.update(counterValue, n => n + 1)
224
- }
225
-
226
- let decrement = (_evt: Dom.event) => {
227
- Signal.update(counterValue, n => n - 1)
228
- }
229
-
230
- let reset = (_evt: Dom.event) => {
231
- Signal.set(counterValue, 0)
232
- }
233
-
234
- // Side effects
235
- let _ = Effect.run(() => {
236
- Console.log("Counter changed: " ++ Int.toString(Signal.get(counterValue)))
237
- })
238
-
239
- // UI Component
240
- let app = Component.div(
241
- ~attrs=[("class", "app")],
242
- ~children=[
243
- Component.h1(~children=[Component.text("Xote Counter")], ()),
244
-
245
- Component.div(
246
- ~attrs=[("class", "counter-display")],
247
- ~children=[
248
- Component.h2(~children=[Component.textSignal(counterDisplay)], ()),
249
- Component.p(~children=[
250
- Component.textSignal(
251
- Computed.make(() =>
252
- Signal.get(isEven) ? "Even" : "Odd"
253
- )
254
- )
255
- ], ())
256
- ],
257
- ()
258
- ),
259
-
260
- Component.div(
261
- ~attrs=[("class", "controls")],
262
- ~children=[
263
- Component.button(
264
- ~events=[("click", decrement)],
265
- ~children=[Component.text("-")],
266
- ()
267
- ),
268
- Component.button(
269
- ~events=[("click", reset)],
270
- ~children=[Component.text("Reset")],
271
- ()
272
- ),
273
- Component.button(
274
- ~events=[("click", increment)],
275
- ~children=[Component.text("+")],
276
- ()
277
- )
278
- ],
279
- ()
280
- )
281
- ],
282
- ()
283
- )
284
-
285
- // Mount to DOM
286
- Component.mountById(app, "app")
287
- ```
71
+ Classic counter: when you click the button, the counter updates reactively.
288
72
 
289
- ## How It Works
73
+ ## Philosophy
290
74
 
291
- ### Reactive System
75
+ Xote focuses on clarity, control, and performance. It brings reactive programming to ReScript with minimal abstractions and no runtime dependencies. The goal is to offer precise, fine-grained updates and predictable behavior without a virtual DOM.
292
76
 
293
- - **Automatic dependency tracking** - When you read a signal with `Signal.get()` inside a computed or effect, it automatically tracks that dependency
294
- - **Fine-grained updates** - Only the specific computeds and effects that depend on changed signals are re-executed
295
- - **Synchronous by default** - Updates happen immediately and synchronously for predictable behavior
296
- - **Batching support** - Group multiple updates to minimize re-computation
77
+ ## Core Concepts
297
78
 
298
- ### Component Rendering
79
+ - **Signal**: Reactive state container
80
+ - **Computed**: Derived reactive value that updates automatically
81
+ - **Effect**: Function that re-runs when dependencies change
82
+ - **Component**: Declarative UI builder using ReScript functions
299
83
 
300
- - **Initial render** - Components are rendered to real DOM elements on mount
301
- - **Reactive text** - `textSignal()` creates text nodes that automatically update when their signal changes
302
- - **Reactive lists** - `Component.list()` creates lists that automatically update when data changes
303
- - **Direct DOM manipulation** - No virtual DOM diffing, updates are precise and efficient
304
- - **Effect-based** - Signal changes trigger effects that update specific DOM nodes automatically
84
+ For a more complete example, see the full [Todo App example](https://github.com/brnrdog/xote/blob/main/src/demo/Xote__TodoApp.res).
305
85
 
306
- ## Best Practices
307
86
 
308
- 1. **Keep signals at the top level** - Define signals outside component definitions for proper reactivity
309
- 2. **Use computed for derived state** - Don't repeat calculations, use `Computed.make()`
310
- 3. **Use `Component.list()` for reactive arrays** - Let the framework handle list updates automatically
311
- 4. **Batch related updates** - Use `Core.batch()` when updating multiple signals together
312
- 5. **Dispose effects when done** - Call `disposer.dispose()` for effects you no longer need
313
- 6. **Use `peek()` when you don't want tracking** - Read signal values without creating dependencies
314
87
 
315
88
  ## License
316
89
 
package/docs/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [1.0.2](https://github.com/brnrdog/xote/compare/v1.0.1...v1.0.2) (2025-10-30)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * version bump ([2794ae6](https://github.com/brnrdog/xote/commit/2794ae697f5c3448d946b9fc5c7d1c0defa8be1a))
7
+
1
8
  ## [1.0.1](https://github.com/brnrdog/xote/compare/v1.0.0...v1.0.1) (2025-10-30)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xote",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "repository": {
5
5
  "url": "https://github.com/brnrdog/xote"
6
6
  },
@@ -14,7 +14,9 @@
14
14
  },
15
15
  "keywords": [
16
16
  "rescript",
17
- "signals"
17
+ "signals",
18
+ "xote",
19
+ "reactivity"
18
20
  ],
19
21
  "release": {
20
22
  "branches": [
@@ -0,0 +1,289 @@
1
+ open Xote
2
+
3
+ type todo = {
4
+ id: int,
5
+ text: string,
6
+ completed: bool,
7
+ }
8
+
9
+ let todos = Signal.make([{id: 1, text: "Buy Milk", completed: true}])
10
+ let nextId = ref(0)
11
+ let inputValue = Signal.make("")
12
+ let darkMode = Signal.make(false)
13
+
14
+ // Computed values derived from todos
15
+ let completedCount = Computed.make(() => {
16
+ Signal.get(todos)->Array.filter(todo => todo.completed)->Array.length
17
+ })
18
+
19
+ let activeCount = Computed.make(() => {
20
+ Signal.get(todos)->Array.filter(todo => !todo.completed)->Array.length
21
+ })
22
+
23
+ let totalCount = Computed.make(() => {
24
+ Signal.get(todos)->Array.length
25
+ })
26
+
27
+ let addTodo = (text: string) => {
28
+ if String.trim(text) != "" {
29
+ Signal.update(todos, list => {
30
+ Array.concat(list, [{id: nextId.contents, text, completed: false}])
31
+ })
32
+ nextId := nextId.contents + 1
33
+ Signal.set(inputValue, "")
34
+ }
35
+ }
36
+
37
+ let toggleTodo = (id: int) => {
38
+ todos->Signal.update(todos => {
39
+ let markAsComplete = todo => todo.id == id ? {...todo, completed: !todo.completed} : todo
40
+ todos->Array.map(markAsComplete)
41
+ })
42
+ }
43
+
44
+ let removeTodo = (id: int) => {
45
+ todos->Signal.update(todos => todos->Array.filter(todo => todo.id != id))
46
+ }
47
+
48
+ @val @scope("document") external querySelector: string => Nullable.t<Dom.element> = "querySelector"
49
+ @get external target: Dom.event => Dom.element = "target"
50
+ @get external value: Dom.element => string = "value"
51
+ @set external setValue: (Dom.element, string) => unit = "value"
52
+ @get external key: Dom.event => string = "key"
53
+
54
+ let clearInput = () => {
55
+ switch querySelector(".todo-input")->Nullable.toOption {
56
+ | Some(input) => setValue(input, "")
57
+ | None => ()
58
+ }
59
+ }
60
+
61
+ let handleInput = (evt: Dom.event) => {
62
+ let newValue = evt->target->value
63
+ Signal.set(inputValue, newValue)
64
+ }
65
+
66
+ let getInputValue = () => {
67
+ switch querySelector(".todo-input")->Nullable.toOption {
68
+ | Some(input) => input->value
69
+ | None => ""
70
+ }
71
+ }
72
+
73
+ let handleKeyDown = (evt: Dom.event) => {
74
+ if evt->key == "Enter" {
75
+ addTodo(getInputValue())
76
+ clearInput()
77
+ }
78
+ }
79
+
80
+ let handleAddClick = (_evt: Dom.event) => {
81
+ addTodo(getInputValue())
82
+ clearInput()
83
+ }
84
+
85
+ let toggleTheme = (_evt: Dom.event) => {
86
+ Signal.update(darkMode, mode => !mode)
87
+ }
88
+
89
+ // Effect to sync dark mode with HTML class
90
+ let _ = Effect.run(() => {
91
+ let isDark = Signal.get(darkMode)
92
+ if isDark {
93
+ %raw(`document.documentElement.classList.add('dark')`)
94
+ } else {
95
+ %raw(`document.documentElement.classList.remove('dark')`)
96
+ }
97
+ })
98
+
99
+ let todoItem = (todo: todo) => {
100
+ let checkboxAttrs = todo.completed
101
+ ? [("type", "checkbox"), ("checked", "checked"), ("class", "w-5 h-5 cursor-pointer")]
102
+ : [("type", "checkbox"), ("class", "w-5 h-5 cursor-pointer")]
103
+
104
+ Component.li(
105
+ ~attrs=[
106
+ (
107
+ "class",
108
+ "flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 mb-2 " ++ (
109
+ todo.completed ? "completed" : ""
110
+ ),
111
+ ),
112
+ ],
113
+ ~children=[
114
+ Component.input(~attrs=checkboxAttrs, ~events=[("change", _ => toggleTodo(todo.id))], ()),
115
+ Component.span(
116
+ ~attrs=[("class", "flex-1 text-gray-900 dark:text-gray-100")],
117
+ ~children=[Component.text(todo.text)],
118
+ (),
119
+ ),
120
+ Component.button(
121
+ ~events=[("click", _ => removeTodo(todo.id))],
122
+ ~children=[Component.Text("Delete")],
123
+ (),
124
+ ),
125
+ ],
126
+ (),
127
+ )
128
+ }
129
+
130
+ let inputElement = Component.input(
131
+ ~attrs=[
132
+ ("type", "text"),
133
+ ("placeholder", "What needs to be done?"),
134
+ (
135
+ "class",
136
+ "todo-input flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500",
137
+ ),
138
+ ],
139
+ ~events=[("input", handleInput), ("keydown", handleKeyDown)],
140
+ (),
141
+ )
142
+
143
+ let app = Component.div(
144
+ ~attrs=[("class", "max-w-2xl mx-auto p-6 space-y-6")],
145
+ ~children=[
146
+ Component.div(
147
+ ~attrs=[("class", "bg-white dark:bg-gray-800 rounded-xl shadow-lg p-8")],
148
+ ~children=[
149
+ Component.div(
150
+ ~attrs=[("class", "flex items-center justify-between mb-6")],
151
+ ~children=[
152
+ Component.h1(
153
+ ~attrs=[("class", "text-3xl font-bold text-gray-900 dark:text-white")],
154
+ ~children=[Component.text("Todo List")],
155
+ (),
156
+ ),
157
+ Component.button(
158
+ ~attrs=[
159
+ (
160
+ "class",
161
+ "px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors",
162
+ ),
163
+ ],
164
+ ~events=[("click", toggleTheme)],
165
+ ~children=[
166
+ Component.textSignal(
167
+ Computed.make(() => Signal.get(darkMode) ? "☀️ Light" : "🌙 Dark"),
168
+ ),
169
+ ],
170
+ (),
171
+ ),
172
+ ],
173
+ (),
174
+ ),
175
+ Component.div(
176
+ ~attrs=[
177
+ (
178
+ "class",
179
+ "grid grid-cols-3 gap-4 mb-6 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700",
180
+ ),
181
+ ],
182
+ ~children=[
183
+ Component.div(
184
+ ~attrs=[("class", "flex flex-col items-center")],
185
+ ~children=[
186
+ Component.span(
187
+ ~attrs=[
188
+ (
189
+ "class",
190
+ "text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1",
191
+ ),
192
+ ],
193
+ ~children=[Component.text("Total")],
194
+ (),
195
+ ),
196
+ Component.span(
197
+ ~attrs=[("class", "text-2xl font-bold text-gray-900 dark:text-white")],
198
+ ~children=[
199
+ Component.textSignal(Computed.make(() => Int.toString(Signal.get(totalCount)))),
200
+ ],
201
+ (),
202
+ ),
203
+ ],
204
+ (),
205
+ ),
206
+ Component.div(
207
+ ~attrs=[("class", "flex flex-col items-center")],
208
+ ~children=[
209
+ Component.span(
210
+ ~attrs=[
211
+ (
212
+ "class",
213
+ "text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1",
214
+ ),
215
+ ],
216
+ ~children=[Component.text("Active")],
217
+ (),
218
+ ),
219
+ Component.span(
220
+ ~attrs=[("class", "text-2xl font-bold text-blue-600 dark:text-blue-400")],
221
+ ~children=[
222
+ Component.textSignal(
223
+ Computed.make(() => Int.toString(Signal.get(activeCount))),
224
+ ),
225
+ ],
226
+ (),
227
+ ),
228
+ ],
229
+ (),
230
+ ),
231
+ Component.div(
232
+ ~attrs=[("class", "flex flex-col items-center")],
233
+ ~children=[
234
+ Component.span(
235
+ ~attrs=[
236
+ (
237
+ "class",
238
+ "text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1",
239
+ ),
240
+ ],
241
+ ~children=[Component.text("Completed")],
242
+ (),
243
+ ),
244
+ Component.span(
245
+ ~attrs=[("class", "text-2xl font-bold text-green-600 dark:text-green-400")],
246
+ ~children=[
247
+ Component.textSignal(
248
+ Computed.make(() => Int.toString(Signal.get(completedCount))),
249
+ ),
250
+ ],
251
+ (),
252
+ ),
253
+ ],
254
+ (),
255
+ ),
256
+ ],
257
+ (),
258
+ ),
259
+ Component.div(
260
+ ~attrs=[("class", "flex gap-2 mb-6")],
261
+ ~children=[
262
+ inputElement,
263
+ Component.button(
264
+ ~attrs=[
265
+ (
266
+ "class",
267
+ "px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500",
268
+ ),
269
+ ],
270
+ ~events=[("click", handleAddClick)],
271
+ ~children=[Component.text("Add")],
272
+ (),
273
+ ),
274
+ ],
275
+ (),
276
+ ),
277
+ Component.ul(
278
+ ~attrs=[("class", "todo-list space-y-2")],
279
+ ~children=[Component.list(todos, todoItem)],
280
+ (),
281
+ ),
282
+ ],
283
+ (),
284
+ ),
285
+ ],
286
+ (),
287
+ )
288
+
289
+ Component.mountById(app, "app")
@@ -0,0 +1,326 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Xote__Effect from "../Xote__Effect.res.mjs";
4
+ import * as Xote__Signal from "../Xote__Signal.res.mjs";
5
+ import * as Xote__Computed from "../Xote__Computed.res.mjs";
6
+ import * as Xote__Component from "../Xote__Component.res.mjs";
7
+
8
+ var todos = Xote__Signal.make([{
9
+ id: 1,
10
+ text: "Buy Milk",
11
+ completed: true
12
+ }]);
13
+
14
+ var nextId = {
15
+ contents: 0
16
+ };
17
+
18
+ var inputValue = Xote__Signal.make("");
19
+
20
+ var darkMode = Xote__Signal.make(false);
21
+
22
+ var completedCount = Xote__Computed.make(function () {
23
+ return Xote__Signal.get(todos).filter(function (todo) {
24
+ return todo.completed;
25
+ }).length;
26
+ });
27
+
28
+ var activeCount = Xote__Computed.make(function () {
29
+ return Xote__Signal.get(todos).filter(function (todo) {
30
+ return !todo.completed;
31
+ }).length;
32
+ });
33
+
34
+ var totalCount = Xote__Computed.make(function () {
35
+ return Xote__Signal.get(todos).length;
36
+ });
37
+
38
+ function addTodo(text) {
39
+ if (text.trim() !== "") {
40
+ Xote__Signal.update(todos, (function (list) {
41
+ return list.concat([{
42
+ id: nextId.contents,
43
+ text: text,
44
+ completed: false
45
+ }]);
46
+ }));
47
+ nextId.contents = nextId.contents + 1 | 0;
48
+ return Xote__Signal.set(inputValue, "");
49
+ }
50
+
51
+ }
52
+
53
+ function toggleTodo(id) {
54
+ Xote__Signal.update(todos, (function (todos) {
55
+ var markAsComplete = function (todo) {
56
+ if (todo.id === id) {
57
+ return {
58
+ id: todo.id,
59
+ text: todo.text,
60
+ completed: !todo.completed
61
+ };
62
+ } else {
63
+ return todo;
64
+ }
65
+ };
66
+ return todos.map(markAsComplete);
67
+ }));
68
+ }
69
+
70
+ function removeTodo(id) {
71
+ Xote__Signal.update(todos, (function (todos) {
72
+ return todos.filter(function (todo) {
73
+ return todo.id !== id;
74
+ });
75
+ }));
76
+ }
77
+
78
+ function clearInput() {
79
+ var input = document.querySelector(".todo-input");
80
+ if (!(input == null)) {
81
+ input.value = "";
82
+ return ;
83
+ }
84
+
85
+ }
86
+
87
+ function handleInput(evt) {
88
+ var newValue = evt.target.value;
89
+ Xote__Signal.set(inputValue, newValue);
90
+ }
91
+
92
+ function getInputValue() {
93
+ var input = document.querySelector(".todo-input");
94
+ if (input == null) {
95
+ return "";
96
+ } else {
97
+ return input.value;
98
+ }
99
+ }
100
+
101
+ function handleKeyDown(evt) {
102
+ if (evt.key === "Enter") {
103
+ addTodo(getInputValue());
104
+ return clearInput();
105
+ }
106
+
107
+ }
108
+
109
+ function handleAddClick(_evt) {
110
+ addTodo(getInputValue());
111
+ clearInput();
112
+ }
113
+
114
+ function toggleTheme(_evt) {
115
+ Xote__Signal.update(darkMode, (function (mode) {
116
+ return !mode;
117
+ }));
118
+ }
119
+
120
+ Xote__Effect.run(function () {
121
+ var isDark = Xote__Signal.get(darkMode);
122
+ if (isDark) {
123
+ return (document.documentElement.classList.add('dark'));
124
+ } else {
125
+ return (document.documentElement.classList.remove('dark'));
126
+ }
127
+ });
128
+
129
+ function todoItem(todo) {
130
+ var checkboxAttrs = todo.completed ? [
131
+ [
132
+ "type",
133
+ "checkbox"
134
+ ],
135
+ [
136
+ "checked",
137
+ "checked"
138
+ ],
139
+ [
140
+ "class",
141
+ "w-5 h-5 cursor-pointer"
142
+ ]
143
+ ] : [
144
+ [
145
+ "type",
146
+ "checkbox"
147
+ ],
148
+ [
149
+ "class",
150
+ "w-5 h-5 cursor-pointer"
151
+ ]
152
+ ];
153
+ return Xote__Component.li([[
154
+ "class",
155
+ "flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 mb-2 " + (
156
+ todo.completed ? "completed" : ""
157
+ )
158
+ ]], undefined, [
159
+ Xote__Component.input(checkboxAttrs, [[
160
+ "change",
161
+ (function (param) {
162
+ toggleTodo(todo.id);
163
+ })
164
+ ]], undefined),
165
+ Xote__Component.span([[
166
+ "class",
167
+ "flex-1 text-gray-900 dark:text-gray-100"
168
+ ]], undefined, [Xote__Component.text(todo.text)], undefined),
169
+ Xote__Component.button(undefined, [[
170
+ "click",
171
+ (function (param) {
172
+ removeTodo(todo.id);
173
+ })
174
+ ]], [{
175
+ TAG: "Text",
176
+ _0: "Delete"
177
+ }], undefined)
178
+ ], undefined);
179
+ }
180
+
181
+ var inputElement = Xote__Component.input([
182
+ [
183
+ "type",
184
+ "text"
185
+ ],
186
+ [
187
+ "placeholder",
188
+ "What needs to be done?"
189
+ ],
190
+ [
191
+ "class",
192
+ "todo-input flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
193
+ ]
194
+ ], [
195
+ [
196
+ "input",
197
+ handleInput
198
+ ],
199
+ [
200
+ "keydown",
201
+ handleKeyDown
202
+ ]
203
+ ], undefined);
204
+
205
+ var app = Xote__Component.div([[
206
+ "class",
207
+ "max-w-2xl mx-auto p-6 space-y-6"
208
+ ]], undefined, [Xote__Component.div([[
209
+ "class",
210
+ "bg-white dark:bg-gray-800 rounded-xl shadow-lg p-8"
211
+ ]], undefined, [
212
+ Xote__Component.div([[
213
+ "class",
214
+ "flex items-center justify-between mb-6"
215
+ ]], undefined, [
216
+ Xote__Component.h1([[
217
+ "class",
218
+ "text-3xl font-bold text-gray-900 dark:text-white"
219
+ ]], undefined, [Xote__Component.text("Todo List")], undefined),
220
+ Xote__Component.button([[
221
+ "class",
222
+ "px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
223
+ ]], [[
224
+ "click",
225
+ toggleTheme
226
+ ]], [Xote__Component.textSignal(Xote__Computed.make(function () {
227
+ if (Xote__Signal.get(darkMode)) {
228
+ return "☀️ Light";
229
+ } else {
230
+ return "🌙 Dark";
231
+ }
232
+ }))], undefined)
233
+ ], undefined),
234
+ Xote__Component.div([[
235
+ "class",
236
+ "grid grid-cols-3 gap-4 mb-6 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700"
237
+ ]], undefined, [
238
+ Xote__Component.div([[
239
+ "class",
240
+ "flex flex-col items-center"
241
+ ]], undefined, [
242
+ Xote__Component.span([[
243
+ "class",
244
+ "text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1"
245
+ ]], undefined, [Xote__Component.text("Total")], undefined),
246
+ Xote__Component.span([[
247
+ "class",
248
+ "text-2xl font-bold text-gray-900 dark:text-white"
249
+ ]], undefined, [Xote__Component.textSignal(Xote__Computed.make(function () {
250
+ return Xote__Signal.get(totalCount).toString();
251
+ }))], undefined)
252
+ ], undefined),
253
+ Xote__Component.div([[
254
+ "class",
255
+ "flex flex-col items-center"
256
+ ]], undefined, [
257
+ Xote__Component.span([[
258
+ "class",
259
+ "text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1"
260
+ ]], undefined, [Xote__Component.text("Active")], undefined),
261
+ Xote__Component.span([[
262
+ "class",
263
+ "text-2xl font-bold text-blue-600 dark:text-blue-400"
264
+ ]], undefined, [Xote__Component.textSignal(Xote__Computed.make(function () {
265
+ return Xote__Signal.get(activeCount).toString();
266
+ }))], undefined)
267
+ ], undefined),
268
+ Xote__Component.div([[
269
+ "class",
270
+ "flex flex-col items-center"
271
+ ]], undefined, [
272
+ Xote__Component.span([[
273
+ "class",
274
+ "text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1"
275
+ ]], undefined, [Xote__Component.text("Completed")], undefined),
276
+ Xote__Component.span([[
277
+ "class",
278
+ "text-2xl font-bold text-green-600 dark:text-green-400"
279
+ ]], undefined, [Xote__Component.textSignal(Xote__Computed.make(function () {
280
+ return Xote__Signal.get(completedCount).toString();
281
+ }))], undefined)
282
+ ], undefined)
283
+ ], undefined),
284
+ Xote__Component.div([[
285
+ "class",
286
+ "flex gap-2 mb-6"
287
+ ]], undefined, [
288
+ inputElement,
289
+ Xote__Component.button([[
290
+ "class",
291
+ "px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500"
292
+ ]], [[
293
+ "click",
294
+ handleAddClick
295
+ ]], [Xote__Component.text("Add")], undefined)
296
+ ], undefined),
297
+ Xote__Component.ul([[
298
+ "class",
299
+ "todo-list space-y-2"
300
+ ]], undefined, [Xote__Component.list(todos, todoItem)], undefined)
301
+ ], undefined)], undefined);
302
+
303
+ Xote__Component.mountById(app, "app");
304
+
305
+ export {
306
+ todos ,
307
+ nextId ,
308
+ inputValue ,
309
+ darkMode ,
310
+ completedCount ,
311
+ activeCount ,
312
+ totalCount ,
313
+ addTodo ,
314
+ toggleTodo ,
315
+ removeTodo ,
316
+ clearInput ,
317
+ handleInput ,
318
+ getInputValue ,
319
+ handleKeyDown ,
320
+ handleAddClick ,
321
+ toggleTheme ,
322
+ todoItem ,
323
+ inputElement ,
324
+ app ,
325
+ }
326
+ /* todos Not a pure module */