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 +36 -263
- package/docs/CHANGELOG.md +7 -0
- package/package.json +4 -2
- package/src/demo/TodoApp.res +289 -0
- package/src/demo/TodoApp.res.mjs +326 -0
package/README.md
CHANGED
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
# xote (pronounced [ˈʃɔtʃi])
|
|
2
|
+
[](https://www.npmjs.com/package/xote)
|
|
2
3
|
|
|
3
|
-
A lightweight, zero-dependency UI library for ReScript with fine-grained reactivity
|
|
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
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
-
|
|
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
|
-
//
|
|
43
|
+
// Derived value
|
|
31
44
|
let doubled = Computed.make(() => Signal.get(count) * 2)
|
|
32
45
|
|
|
33
|
-
//
|
|
46
|
+
// Event handler
|
|
34
47
|
let increment = (_evt: Dom.event) => Signal.update(count, n => n + 1)
|
|
35
48
|
|
|
36
|
-
// Build
|
|
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
|
-
|
|
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
|
-
##
|
|
73
|
+
## Philosophy
|
|
290
74
|
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 */
|