@plures/praxis 1.1.0 → 1.1.1
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/LICENSE +21 -21
- package/core/codegen/ts-generator.ts +15 -15
- package/dist/node/cli/index.cjs +3 -2282
- package/dist/node/cli/index.js +1 -1
- package/dist/node/{verify-YBZ7W24H.js → verify-QRYKRIDU.js} +3 -2282
- package/docs/REACTIVE_REDESIGN.md +132 -132
- package/docs/SVELTE_INTEGRATION_STRATEGY.md +68 -68
- package/package.json +132 -132
- package/src/components/TerminalNode.svelte +457 -457
- package/src/core/reactive-engine.svelte.ts +65 -65
- package/src/core/reactive-engine.ts +67 -67
- package/src/core/schema/loader.common.ts +150 -150
- package/src/examples/advanced-todo/App.svelte +506 -506
- package/src/index.browser.ts +204 -204
|
@@ -1,506 +1,506 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Advanced Todo App - Svelte 5 Component
|
|
3
|
-
|
|
4
|
-
This component demonstrates:
|
|
5
|
-
- Svelte 5 runes integration with Praxis
|
|
6
|
-
- History state pattern with undo/redo
|
|
7
|
-
- Time-travel debugging
|
|
8
|
-
- Reactive derived values
|
|
9
|
-
-->
|
|
10
|
-
|
|
11
|
-
<script lang="ts">
|
|
12
|
-
import { usePraxisEngine } from '@plures/praxis/svelte';
|
|
13
|
-
import {
|
|
14
|
-
createTodoEngine,
|
|
15
|
-
getFilteredTodos,
|
|
16
|
-
getStats,
|
|
17
|
-
AddTodo,
|
|
18
|
-
ToggleTodo,
|
|
19
|
-
RemoveTodo,
|
|
20
|
-
SetFilter,
|
|
21
|
-
CompleteAll,
|
|
22
|
-
ClearCompleted,
|
|
23
|
-
type TodoItem,
|
|
24
|
-
} from './index';
|
|
25
|
-
|
|
26
|
-
// Create engine with history enabled
|
|
27
|
-
const engine = createTodoEngine();
|
|
28
|
-
const {
|
|
29
|
-
context,
|
|
30
|
-
dispatch,
|
|
31
|
-
undo,
|
|
32
|
-
redo,
|
|
33
|
-
canUndo,
|
|
34
|
-
canRedo,
|
|
35
|
-
snapshots,
|
|
36
|
-
historyIndex,
|
|
37
|
-
goToSnapshot,
|
|
38
|
-
} = usePraxisEngine(engine, {
|
|
39
|
-
enableHistory: true,
|
|
40
|
-
maxHistorySize: 50,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// Local UI state
|
|
44
|
-
let newTodoText = $state('');
|
|
45
|
-
let showDebugger = $state(false);
|
|
46
|
-
|
|
47
|
-
// Derived reactive values
|
|
48
|
-
$: filteredTodos = getFilteredTodos(context.todos, context.filter);
|
|
49
|
-
$: stats = getStats(context.todos);
|
|
50
|
-
$: allCompleted = stats.total > 0 && stats.completed === stats.total;
|
|
51
|
-
|
|
52
|
-
// Event handlers
|
|
53
|
-
function handleAddTodo() {
|
|
54
|
-
if (newTodoText.trim()) {
|
|
55
|
-
dispatch([AddTodo.create({ text: newTodoText })], 'Add Todo');
|
|
56
|
-
newTodoText = '';
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function handleToggle(id: string) {
|
|
61
|
-
dispatch([ToggleTodo.create({ id })], 'Toggle Todo');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function handleRemove(id: string) {
|
|
65
|
-
dispatch([RemoveTodo.create({ id })], 'Remove Todo');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function handleFilterChange(filter: 'all' | 'active' | 'completed') {
|
|
69
|
-
dispatch([SetFilter.create({ filter })], 'Change Filter');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function handleCompleteAll() {
|
|
73
|
-
dispatch([CompleteAll.create({})], 'Complete All');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function handleClearCompleted() {
|
|
77
|
-
if (stats.completed > 0) {
|
|
78
|
-
dispatch([ClearCompleted.create({})], 'Clear Completed');
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Keyboard shortcuts
|
|
83
|
-
function handleKeydown(event: KeyboardEvent) {
|
|
84
|
-
if (event.ctrlKey || event.metaKey) {
|
|
85
|
-
if (event.key === 'z' && canUndo) {
|
|
86
|
-
event.preventDefault();
|
|
87
|
-
undo();
|
|
88
|
-
} else if (event.key === 'y' && canRedo) {
|
|
89
|
-
event.preventDefault();
|
|
90
|
-
redo();
|
|
91
|
-
} else if (event.key === 'd') {
|
|
92
|
-
event.preventDefault();
|
|
93
|
-
showDebugger = !showDebugger;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
</script>
|
|
98
|
-
|
|
99
|
-
<svelte:window onkeydown={handleKeydown} />
|
|
100
|
-
|
|
101
|
-
<div class="todo-app">
|
|
102
|
-
<!-- Header -->
|
|
103
|
-
<header class="header">
|
|
104
|
-
<h1>Praxis Todos</h1>
|
|
105
|
-
<div class="history-controls">
|
|
106
|
-
<button onclick={undo} disabled={!canUndo} title="Undo (Ctrl+Z)">
|
|
107
|
-
⟲ Undo
|
|
108
|
-
</button>
|
|
109
|
-
<span class="history-info">
|
|
110
|
-
{historyIndex + 1} / {snapshots.length}
|
|
111
|
-
</span>
|
|
112
|
-
<button onclick={redo} disabled={!canRedo} title="Redo (Ctrl+Y)">
|
|
113
|
-
⟳ Redo
|
|
114
|
-
</button>
|
|
115
|
-
<button
|
|
116
|
-
onclick={() => showDebugger = !showDebugger}
|
|
117
|
-
class:active={showDebugger}
|
|
118
|
-
title="Toggle Debugger (Ctrl+D)"
|
|
119
|
-
>
|
|
120
|
-
🐛 Debug
|
|
121
|
-
</button>
|
|
122
|
-
</div>
|
|
123
|
-
</header>
|
|
124
|
-
|
|
125
|
-
<!-- Main Content -->
|
|
126
|
-
<main class="main">
|
|
127
|
-
<!-- Input -->
|
|
128
|
-
<div class="input-section">
|
|
129
|
-
<input
|
|
130
|
-
type="text"
|
|
131
|
-
bind:value={newTodoText}
|
|
132
|
-
onkeypress={(e) => e.key === 'Enter' && handleAddTodo()}
|
|
133
|
-
placeholder="What needs to be done?"
|
|
134
|
-
class="new-todo"
|
|
135
|
-
/>
|
|
136
|
-
<button onclick={handleAddTodo} class="add-btn">
|
|
137
|
-
Add
|
|
138
|
-
</button>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
{#if stats.total > 0}
|
|
142
|
-
<!-- Filters -->
|
|
143
|
-
<div class="filters">
|
|
144
|
-
<button
|
|
145
|
-
class:active={context.filter === 'all'}
|
|
146
|
-
onclick={() => handleFilterChange('all')}
|
|
147
|
-
>
|
|
148
|
-
All ({stats.total})
|
|
149
|
-
</button>
|
|
150
|
-
<button
|
|
151
|
-
class:active={context.filter === 'active'}
|
|
152
|
-
onclick={() => handleFilterChange('active')}
|
|
153
|
-
>
|
|
154
|
-
Active ({stats.active})
|
|
155
|
-
</button>
|
|
156
|
-
<button
|
|
157
|
-
class:active={context.filter === 'completed'}
|
|
158
|
-
onclick={() => handleFilterChange('completed')}
|
|
159
|
-
>
|
|
160
|
-
Completed ({stats.completed})
|
|
161
|
-
</button>
|
|
162
|
-
</div>
|
|
163
|
-
|
|
164
|
-
<!-- Todo List -->
|
|
165
|
-
<ul class="todo-list">
|
|
166
|
-
{#each filteredTodos as todo (todo.id)}
|
|
167
|
-
<li class="todo-item" class:completed={todo.completed}>
|
|
168
|
-
<input
|
|
169
|
-
type="checkbox"
|
|
170
|
-
checked={todo.completed}
|
|
171
|
-
onchange={() => handleToggle(todo.id)}
|
|
172
|
-
/>
|
|
173
|
-
<span class="todo-text">{todo.text}</span>
|
|
174
|
-
<button onclick={() => handleRemove(todo.id)} class="remove-btn">
|
|
175
|
-
✕
|
|
176
|
-
</button>
|
|
177
|
-
</li>
|
|
178
|
-
{/each}
|
|
179
|
-
</ul>
|
|
180
|
-
|
|
181
|
-
<!-- Footer -->
|
|
182
|
-
<footer class="footer">
|
|
183
|
-
<span class="stats">
|
|
184
|
-
{stats.active} {stats.active === 1 ? 'item' : 'items'} left
|
|
185
|
-
</span>
|
|
186
|
-
|
|
187
|
-
<div class="actions">
|
|
188
|
-
<button onclick={handleCompleteAll} disabled={allCompleted}>
|
|
189
|
-
Complete All
|
|
190
|
-
</button>
|
|
191
|
-
<button onclick={handleClearCompleted} disabled={stats.completed === 0}>
|
|
192
|
-
Clear Completed
|
|
193
|
-
</button>
|
|
194
|
-
</div>
|
|
195
|
-
</footer>
|
|
196
|
-
{:else}
|
|
197
|
-
<div class="empty-state">
|
|
198
|
-
<p>No todos yet. Add one above!</p>
|
|
199
|
-
</div>
|
|
200
|
-
{/if}
|
|
201
|
-
</main>
|
|
202
|
-
|
|
203
|
-
<!-- Time-Travel Debugger -->
|
|
204
|
-
{#if showDebugger}
|
|
205
|
-
<aside class="debugger">
|
|
206
|
-
<h2>Time-Travel Debugger</h2>
|
|
207
|
-
|
|
208
|
-
<div class="timeline">
|
|
209
|
-
{#each snapshots as snapshot, index}
|
|
210
|
-
<button
|
|
211
|
-
class="snapshot"
|
|
212
|
-
class:active={index === historyIndex}
|
|
213
|
-
onclick={() => goToSnapshot(index)}
|
|
214
|
-
title={`Snapshot ${index + 1}`}
|
|
215
|
-
>
|
|
216
|
-
<div class="snapshot-time">
|
|
217
|
-
{new Date(snapshot.timestamp).toLocaleTimeString()}
|
|
218
|
-
</div>
|
|
219
|
-
<div class="snapshot-events">
|
|
220
|
-
{snapshot.events.length} events
|
|
221
|
-
</div>
|
|
222
|
-
</button>
|
|
223
|
-
{/each}
|
|
224
|
-
</div>
|
|
225
|
-
|
|
226
|
-
<div class="state-viewer">
|
|
227
|
-
<h3>Current State</h3>
|
|
228
|
-
<pre>{JSON.stringify(context, null, 2)}</pre>
|
|
229
|
-
</div>
|
|
230
|
-
</aside>
|
|
231
|
-
{/if}
|
|
232
|
-
</div>
|
|
233
|
-
|
|
234
|
-
<style>
|
|
235
|
-
.todo-app {
|
|
236
|
-
max-width: 800px;
|
|
237
|
-
margin: 0 auto;
|
|
238
|
-
padding: 2rem;
|
|
239
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
.header {
|
|
243
|
-
display: flex;
|
|
244
|
-
justify-content: space-between;
|
|
245
|
-
align-items: center;
|
|
246
|
-
margin-bottom: 2rem;
|
|
247
|
-
padding-bottom: 1rem;
|
|
248
|
-
border-bottom: 2px solid #e0e0e0;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
.header h1 {
|
|
252
|
-
margin: 0;
|
|
253
|
-
font-size: 2rem;
|
|
254
|
-
color: #333;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
.history-controls {
|
|
258
|
-
display: flex;
|
|
259
|
-
gap: 0.5rem;
|
|
260
|
-
align-items: center;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
.history-controls button {
|
|
264
|
-
padding: 0.5rem 1rem;
|
|
265
|
-
border: 1px solid #ddd;
|
|
266
|
-
background: white;
|
|
267
|
-
border-radius: 4px;
|
|
268
|
-
cursor: pointer;
|
|
269
|
-
font-size: 0.9rem;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
.history-controls button:hover:not(:disabled) {
|
|
273
|
-
background: #f5f5f5;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
.history-controls button:disabled {
|
|
277
|
-
opacity: 0.5;
|
|
278
|
-
cursor: not-allowed;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
.history-controls button.active {
|
|
282
|
-
background: #e3f2fd;
|
|
283
|
-
border-color: #2196f3;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
.history-info {
|
|
287
|
-
padding: 0 0.5rem;
|
|
288
|
-
color: #666;
|
|
289
|
-
font-size: 0.9rem;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
.input-section {
|
|
293
|
-
display: flex;
|
|
294
|
-
gap: 0.5rem;
|
|
295
|
-
margin-bottom: 1rem;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
.new-todo {
|
|
299
|
-
flex: 1;
|
|
300
|
-
padding: 0.75rem;
|
|
301
|
-
border: 2px solid #ddd;
|
|
302
|
-
border-radius: 4px;
|
|
303
|
-
font-size: 1rem;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
.new-todo:focus {
|
|
307
|
-
outline: none;
|
|
308
|
-
border-color: #2196f3;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
.add-btn {
|
|
312
|
-
padding: 0.75rem 1.5rem;
|
|
313
|
-
background: #2196f3;
|
|
314
|
-
color: white;
|
|
315
|
-
border: none;
|
|
316
|
-
border-radius: 4px;
|
|
317
|
-
cursor: pointer;
|
|
318
|
-
font-size: 1rem;
|
|
319
|
-
font-weight: 500;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
.add-btn:hover {
|
|
323
|
-
background: #1976d2;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
.filters {
|
|
327
|
-
display: flex;
|
|
328
|
-
gap: 0.5rem;
|
|
329
|
-
margin-bottom: 1rem;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
.filters button {
|
|
333
|
-
flex: 1;
|
|
334
|
-
padding: 0.5rem;
|
|
335
|
-
border: 1px solid #ddd;
|
|
336
|
-
background: white;
|
|
337
|
-
border-radius: 4px;
|
|
338
|
-
cursor: pointer;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
.filters button.active {
|
|
342
|
-
background: #2196f3;
|
|
343
|
-
color: white;
|
|
344
|
-
border-color: #2196f3;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
.todo-list {
|
|
348
|
-
list-style: none;
|
|
349
|
-
padding: 0;
|
|
350
|
-
margin: 0;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
.todo-item {
|
|
354
|
-
display: flex;
|
|
355
|
-
align-items: center;
|
|
356
|
-
gap: 0.75rem;
|
|
357
|
-
padding: 1rem;
|
|
358
|
-
border: 1px solid #ddd;
|
|
359
|
-
border-radius: 4px;
|
|
360
|
-
margin-bottom: 0.5rem;
|
|
361
|
-
background: white;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
.todo-item.completed {
|
|
365
|
-
background: #f5f5f5;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
.todo-item.completed .todo-text {
|
|
369
|
-
text-decoration: line-through;
|
|
370
|
-
color: #999;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
.todo-item input[type="checkbox"] {
|
|
374
|
-
width: 1.25rem;
|
|
375
|
-
height: 1.25rem;
|
|
376
|
-
cursor: pointer;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
.todo-text {
|
|
380
|
-
flex: 1;
|
|
381
|
-
font-size: 1rem;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
.remove-btn {
|
|
385
|
-
padding: 0.25rem 0.5rem;
|
|
386
|
-
background: transparent;
|
|
387
|
-
border: none;
|
|
388
|
-
color: #f44336;
|
|
389
|
-
cursor: pointer;
|
|
390
|
-
font-size: 1.25rem;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
.remove-btn:hover {
|
|
394
|
-
color: #d32f2f;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
.footer {
|
|
398
|
-
display: flex;
|
|
399
|
-
justify-content: space-between;
|
|
400
|
-
align-items: center;
|
|
401
|
-
margin-top: 1rem;
|
|
402
|
-
padding-top: 1rem;
|
|
403
|
-
border-top: 1px solid #e0e0e0;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
.stats {
|
|
407
|
-
color: #666;
|
|
408
|
-
font-size: 0.9rem;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
.actions {
|
|
412
|
-
display: flex;
|
|
413
|
-
gap: 0.5rem;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
.actions button {
|
|
417
|
-
padding: 0.5rem 1rem;
|
|
418
|
-
border: 1px solid #ddd;
|
|
419
|
-
background: white;
|
|
420
|
-
border-radius: 4px;
|
|
421
|
-
cursor: pointer;
|
|
422
|
-
font-size: 0.9rem;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
.actions button:hover:not(:disabled) {
|
|
426
|
-
background: #f5f5f5;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
.actions button:disabled {
|
|
430
|
-
opacity: 0.5;
|
|
431
|
-
cursor: not-allowed;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
.empty-state {
|
|
435
|
-
text-align: center;
|
|
436
|
-
padding: 3rem;
|
|
437
|
-
color: #999;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
.debugger {
|
|
441
|
-
margin-top: 2rem;
|
|
442
|
-
padding: 1rem;
|
|
443
|
-
border: 2px solid #2196f3;
|
|
444
|
-
border-radius: 4px;
|
|
445
|
-
background: #f5f5f5;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
.debugger h2 {
|
|
449
|
-
margin-top: 0;
|
|
450
|
-
color: #2196f3;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
.timeline {
|
|
454
|
-
display: flex;
|
|
455
|
-
gap: 0.5rem;
|
|
456
|
-
overflow-x: auto;
|
|
457
|
-
padding: 1rem 0;
|
|
458
|
-
margin-bottom: 1rem;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
.snapshot {
|
|
462
|
-
min-width: 100px;
|
|
463
|
-
padding: 0.5rem;
|
|
464
|
-
border: 2px solid #ddd;
|
|
465
|
-
background: white;
|
|
466
|
-
border-radius: 4px;
|
|
467
|
-
cursor: pointer;
|
|
468
|
-
text-align: center;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
.snapshot.active {
|
|
472
|
-
border-color: #2196f3;
|
|
473
|
-
background: #e3f2fd;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
.snapshot-time {
|
|
477
|
-
font-size: 0.8rem;
|
|
478
|
-
font-weight: 500;
|
|
479
|
-
margin-bottom: 0.25rem;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
.snapshot-events {
|
|
483
|
-
font-size: 0.75rem;
|
|
484
|
-
color: #666;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
.state-viewer {
|
|
488
|
-
background: white;
|
|
489
|
-
border: 1px solid #ddd;
|
|
490
|
-
border-radius: 4px;
|
|
491
|
-
padding: 1rem;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
.state-viewer h3 {
|
|
495
|
-
margin-top: 0;
|
|
496
|
-
margin-bottom: 0.5rem;
|
|
497
|
-
font-size: 1rem;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
.state-viewer pre {
|
|
501
|
-
margin: 0;
|
|
502
|
-
overflow-x: auto;
|
|
503
|
-
font-size: 0.85rem;
|
|
504
|
-
line-height: 1.5;
|
|
505
|
-
}
|
|
506
|
-
</style>
|
|
1
|
+
<!--
|
|
2
|
+
Advanced Todo App - Svelte 5 Component
|
|
3
|
+
|
|
4
|
+
This component demonstrates:
|
|
5
|
+
- Svelte 5 runes integration with Praxis
|
|
6
|
+
- History state pattern with undo/redo
|
|
7
|
+
- Time-travel debugging
|
|
8
|
+
- Reactive derived values
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { usePraxisEngine } from '@plures/praxis/svelte';
|
|
13
|
+
import {
|
|
14
|
+
createTodoEngine,
|
|
15
|
+
getFilteredTodos,
|
|
16
|
+
getStats,
|
|
17
|
+
AddTodo,
|
|
18
|
+
ToggleTodo,
|
|
19
|
+
RemoveTodo,
|
|
20
|
+
SetFilter,
|
|
21
|
+
CompleteAll,
|
|
22
|
+
ClearCompleted,
|
|
23
|
+
type TodoItem,
|
|
24
|
+
} from './index';
|
|
25
|
+
|
|
26
|
+
// Create engine with history enabled
|
|
27
|
+
const engine = createTodoEngine();
|
|
28
|
+
const {
|
|
29
|
+
context,
|
|
30
|
+
dispatch,
|
|
31
|
+
undo,
|
|
32
|
+
redo,
|
|
33
|
+
canUndo,
|
|
34
|
+
canRedo,
|
|
35
|
+
snapshots,
|
|
36
|
+
historyIndex,
|
|
37
|
+
goToSnapshot,
|
|
38
|
+
} = usePraxisEngine(engine, {
|
|
39
|
+
enableHistory: true,
|
|
40
|
+
maxHistorySize: 50,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Local UI state
|
|
44
|
+
let newTodoText = $state('');
|
|
45
|
+
let showDebugger = $state(false);
|
|
46
|
+
|
|
47
|
+
// Derived reactive values
|
|
48
|
+
$: filteredTodos = getFilteredTodos(context.todos, context.filter);
|
|
49
|
+
$: stats = getStats(context.todos);
|
|
50
|
+
$: allCompleted = stats.total > 0 && stats.completed === stats.total;
|
|
51
|
+
|
|
52
|
+
// Event handlers
|
|
53
|
+
function handleAddTodo() {
|
|
54
|
+
if (newTodoText.trim()) {
|
|
55
|
+
dispatch([AddTodo.create({ text: newTodoText })], 'Add Todo');
|
|
56
|
+
newTodoText = '';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleToggle(id: string) {
|
|
61
|
+
dispatch([ToggleTodo.create({ id })], 'Toggle Todo');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleRemove(id: string) {
|
|
65
|
+
dispatch([RemoveTodo.create({ id })], 'Remove Todo');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleFilterChange(filter: 'all' | 'active' | 'completed') {
|
|
69
|
+
dispatch([SetFilter.create({ filter })], 'Change Filter');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleCompleteAll() {
|
|
73
|
+
dispatch([CompleteAll.create({})], 'Complete All');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleClearCompleted() {
|
|
77
|
+
if (stats.completed > 0) {
|
|
78
|
+
dispatch([ClearCompleted.create({})], 'Clear Completed');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Keyboard shortcuts
|
|
83
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
84
|
+
if (event.ctrlKey || event.metaKey) {
|
|
85
|
+
if (event.key === 'z' && canUndo) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
undo();
|
|
88
|
+
} else if (event.key === 'y' && canRedo) {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
redo();
|
|
91
|
+
} else if (event.key === 'd') {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
showDebugger = !showDebugger;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
100
|
+
|
|
101
|
+
<div class="todo-app">
|
|
102
|
+
<!-- Header -->
|
|
103
|
+
<header class="header">
|
|
104
|
+
<h1>Praxis Todos</h1>
|
|
105
|
+
<div class="history-controls">
|
|
106
|
+
<button onclick={undo} disabled={!canUndo} title="Undo (Ctrl+Z)">
|
|
107
|
+
⟲ Undo
|
|
108
|
+
</button>
|
|
109
|
+
<span class="history-info">
|
|
110
|
+
{historyIndex + 1} / {snapshots.length}
|
|
111
|
+
</span>
|
|
112
|
+
<button onclick={redo} disabled={!canRedo} title="Redo (Ctrl+Y)">
|
|
113
|
+
⟳ Redo
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
onclick={() => showDebugger = !showDebugger}
|
|
117
|
+
class:active={showDebugger}
|
|
118
|
+
title="Toggle Debugger (Ctrl+D)"
|
|
119
|
+
>
|
|
120
|
+
🐛 Debug
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
</header>
|
|
124
|
+
|
|
125
|
+
<!-- Main Content -->
|
|
126
|
+
<main class="main">
|
|
127
|
+
<!-- Input -->
|
|
128
|
+
<div class="input-section">
|
|
129
|
+
<input
|
|
130
|
+
type="text"
|
|
131
|
+
bind:value={newTodoText}
|
|
132
|
+
onkeypress={(e) => e.key === 'Enter' && handleAddTodo()}
|
|
133
|
+
placeholder="What needs to be done?"
|
|
134
|
+
class="new-todo"
|
|
135
|
+
/>
|
|
136
|
+
<button onclick={handleAddTodo} class="add-btn">
|
|
137
|
+
Add
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{#if stats.total > 0}
|
|
142
|
+
<!-- Filters -->
|
|
143
|
+
<div class="filters">
|
|
144
|
+
<button
|
|
145
|
+
class:active={context.filter === 'all'}
|
|
146
|
+
onclick={() => handleFilterChange('all')}
|
|
147
|
+
>
|
|
148
|
+
All ({stats.total})
|
|
149
|
+
</button>
|
|
150
|
+
<button
|
|
151
|
+
class:active={context.filter === 'active'}
|
|
152
|
+
onclick={() => handleFilterChange('active')}
|
|
153
|
+
>
|
|
154
|
+
Active ({stats.active})
|
|
155
|
+
</button>
|
|
156
|
+
<button
|
|
157
|
+
class:active={context.filter === 'completed'}
|
|
158
|
+
onclick={() => handleFilterChange('completed')}
|
|
159
|
+
>
|
|
160
|
+
Completed ({stats.completed})
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<!-- Todo List -->
|
|
165
|
+
<ul class="todo-list">
|
|
166
|
+
{#each filteredTodos as todo (todo.id)}
|
|
167
|
+
<li class="todo-item" class:completed={todo.completed}>
|
|
168
|
+
<input
|
|
169
|
+
type="checkbox"
|
|
170
|
+
checked={todo.completed}
|
|
171
|
+
onchange={() => handleToggle(todo.id)}
|
|
172
|
+
/>
|
|
173
|
+
<span class="todo-text">{todo.text}</span>
|
|
174
|
+
<button onclick={() => handleRemove(todo.id)} class="remove-btn">
|
|
175
|
+
✕
|
|
176
|
+
</button>
|
|
177
|
+
</li>
|
|
178
|
+
{/each}
|
|
179
|
+
</ul>
|
|
180
|
+
|
|
181
|
+
<!-- Footer -->
|
|
182
|
+
<footer class="footer">
|
|
183
|
+
<span class="stats">
|
|
184
|
+
{stats.active} {stats.active === 1 ? 'item' : 'items'} left
|
|
185
|
+
</span>
|
|
186
|
+
|
|
187
|
+
<div class="actions">
|
|
188
|
+
<button onclick={handleCompleteAll} disabled={allCompleted}>
|
|
189
|
+
Complete All
|
|
190
|
+
</button>
|
|
191
|
+
<button onclick={handleClearCompleted} disabled={stats.completed === 0}>
|
|
192
|
+
Clear Completed
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
</footer>
|
|
196
|
+
{:else}
|
|
197
|
+
<div class="empty-state">
|
|
198
|
+
<p>No todos yet. Add one above!</p>
|
|
199
|
+
</div>
|
|
200
|
+
{/if}
|
|
201
|
+
</main>
|
|
202
|
+
|
|
203
|
+
<!-- Time-Travel Debugger -->
|
|
204
|
+
{#if showDebugger}
|
|
205
|
+
<aside class="debugger">
|
|
206
|
+
<h2>Time-Travel Debugger</h2>
|
|
207
|
+
|
|
208
|
+
<div class="timeline">
|
|
209
|
+
{#each snapshots as snapshot, index}
|
|
210
|
+
<button
|
|
211
|
+
class="snapshot"
|
|
212
|
+
class:active={index === historyIndex}
|
|
213
|
+
onclick={() => goToSnapshot(index)}
|
|
214
|
+
title={`Snapshot ${index + 1}`}
|
|
215
|
+
>
|
|
216
|
+
<div class="snapshot-time">
|
|
217
|
+
{new Date(snapshot.timestamp).toLocaleTimeString()}
|
|
218
|
+
</div>
|
|
219
|
+
<div class="snapshot-events">
|
|
220
|
+
{snapshot.events.length} events
|
|
221
|
+
</div>
|
|
222
|
+
</button>
|
|
223
|
+
{/each}
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div class="state-viewer">
|
|
227
|
+
<h3>Current State</h3>
|
|
228
|
+
<pre>{JSON.stringify(context, null, 2)}</pre>
|
|
229
|
+
</div>
|
|
230
|
+
</aside>
|
|
231
|
+
{/if}
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<style>
|
|
235
|
+
.todo-app {
|
|
236
|
+
max-width: 800px;
|
|
237
|
+
margin: 0 auto;
|
|
238
|
+
padding: 2rem;
|
|
239
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.header {
|
|
243
|
+
display: flex;
|
|
244
|
+
justify-content: space-between;
|
|
245
|
+
align-items: center;
|
|
246
|
+
margin-bottom: 2rem;
|
|
247
|
+
padding-bottom: 1rem;
|
|
248
|
+
border-bottom: 2px solid #e0e0e0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.header h1 {
|
|
252
|
+
margin: 0;
|
|
253
|
+
font-size: 2rem;
|
|
254
|
+
color: #333;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.history-controls {
|
|
258
|
+
display: flex;
|
|
259
|
+
gap: 0.5rem;
|
|
260
|
+
align-items: center;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.history-controls button {
|
|
264
|
+
padding: 0.5rem 1rem;
|
|
265
|
+
border: 1px solid #ddd;
|
|
266
|
+
background: white;
|
|
267
|
+
border-radius: 4px;
|
|
268
|
+
cursor: pointer;
|
|
269
|
+
font-size: 0.9rem;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.history-controls button:hover:not(:disabled) {
|
|
273
|
+
background: #f5f5f5;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.history-controls button:disabled {
|
|
277
|
+
opacity: 0.5;
|
|
278
|
+
cursor: not-allowed;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.history-controls button.active {
|
|
282
|
+
background: #e3f2fd;
|
|
283
|
+
border-color: #2196f3;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.history-info {
|
|
287
|
+
padding: 0 0.5rem;
|
|
288
|
+
color: #666;
|
|
289
|
+
font-size: 0.9rem;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.input-section {
|
|
293
|
+
display: flex;
|
|
294
|
+
gap: 0.5rem;
|
|
295
|
+
margin-bottom: 1rem;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.new-todo {
|
|
299
|
+
flex: 1;
|
|
300
|
+
padding: 0.75rem;
|
|
301
|
+
border: 2px solid #ddd;
|
|
302
|
+
border-radius: 4px;
|
|
303
|
+
font-size: 1rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.new-todo:focus {
|
|
307
|
+
outline: none;
|
|
308
|
+
border-color: #2196f3;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.add-btn {
|
|
312
|
+
padding: 0.75rem 1.5rem;
|
|
313
|
+
background: #2196f3;
|
|
314
|
+
color: white;
|
|
315
|
+
border: none;
|
|
316
|
+
border-radius: 4px;
|
|
317
|
+
cursor: pointer;
|
|
318
|
+
font-size: 1rem;
|
|
319
|
+
font-weight: 500;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.add-btn:hover {
|
|
323
|
+
background: #1976d2;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.filters {
|
|
327
|
+
display: flex;
|
|
328
|
+
gap: 0.5rem;
|
|
329
|
+
margin-bottom: 1rem;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.filters button {
|
|
333
|
+
flex: 1;
|
|
334
|
+
padding: 0.5rem;
|
|
335
|
+
border: 1px solid #ddd;
|
|
336
|
+
background: white;
|
|
337
|
+
border-radius: 4px;
|
|
338
|
+
cursor: pointer;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.filters button.active {
|
|
342
|
+
background: #2196f3;
|
|
343
|
+
color: white;
|
|
344
|
+
border-color: #2196f3;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.todo-list {
|
|
348
|
+
list-style: none;
|
|
349
|
+
padding: 0;
|
|
350
|
+
margin: 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.todo-item {
|
|
354
|
+
display: flex;
|
|
355
|
+
align-items: center;
|
|
356
|
+
gap: 0.75rem;
|
|
357
|
+
padding: 1rem;
|
|
358
|
+
border: 1px solid #ddd;
|
|
359
|
+
border-radius: 4px;
|
|
360
|
+
margin-bottom: 0.5rem;
|
|
361
|
+
background: white;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.todo-item.completed {
|
|
365
|
+
background: #f5f5f5;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.todo-item.completed .todo-text {
|
|
369
|
+
text-decoration: line-through;
|
|
370
|
+
color: #999;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.todo-item input[type="checkbox"] {
|
|
374
|
+
width: 1.25rem;
|
|
375
|
+
height: 1.25rem;
|
|
376
|
+
cursor: pointer;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.todo-text {
|
|
380
|
+
flex: 1;
|
|
381
|
+
font-size: 1rem;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.remove-btn {
|
|
385
|
+
padding: 0.25rem 0.5rem;
|
|
386
|
+
background: transparent;
|
|
387
|
+
border: none;
|
|
388
|
+
color: #f44336;
|
|
389
|
+
cursor: pointer;
|
|
390
|
+
font-size: 1.25rem;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.remove-btn:hover {
|
|
394
|
+
color: #d32f2f;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.footer {
|
|
398
|
+
display: flex;
|
|
399
|
+
justify-content: space-between;
|
|
400
|
+
align-items: center;
|
|
401
|
+
margin-top: 1rem;
|
|
402
|
+
padding-top: 1rem;
|
|
403
|
+
border-top: 1px solid #e0e0e0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.stats {
|
|
407
|
+
color: #666;
|
|
408
|
+
font-size: 0.9rem;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.actions {
|
|
412
|
+
display: flex;
|
|
413
|
+
gap: 0.5rem;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.actions button {
|
|
417
|
+
padding: 0.5rem 1rem;
|
|
418
|
+
border: 1px solid #ddd;
|
|
419
|
+
background: white;
|
|
420
|
+
border-radius: 4px;
|
|
421
|
+
cursor: pointer;
|
|
422
|
+
font-size: 0.9rem;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.actions button:hover:not(:disabled) {
|
|
426
|
+
background: #f5f5f5;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.actions button:disabled {
|
|
430
|
+
opacity: 0.5;
|
|
431
|
+
cursor: not-allowed;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.empty-state {
|
|
435
|
+
text-align: center;
|
|
436
|
+
padding: 3rem;
|
|
437
|
+
color: #999;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.debugger {
|
|
441
|
+
margin-top: 2rem;
|
|
442
|
+
padding: 1rem;
|
|
443
|
+
border: 2px solid #2196f3;
|
|
444
|
+
border-radius: 4px;
|
|
445
|
+
background: #f5f5f5;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.debugger h2 {
|
|
449
|
+
margin-top: 0;
|
|
450
|
+
color: #2196f3;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.timeline {
|
|
454
|
+
display: flex;
|
|
455
|
+
gap: 0.5rem;
|
|
456
|
+
overflow-x: auto;
|
|
457
|
+
padding: 1rem 0;
|
|
458
|
+
margin-bottom: 1rem;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.snapshot {
|
|
462
|
+
min-width: 100px;
|
|
463
|
+
padding: 0.5rem;
|
|
464
|
+
border: 2px solid #ddd;
|
|
465
|
+
background: white;
|
|
466
|
+
border-radius: 4px;
|
|
467
|
+
cursor: pointer;
|
|
468
|
+
text-align: center;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.snapshot.active {
|
|
472
|
+
border-color: #2196f3;
|
|
473
|
+
background: #e3f2fd;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.snapshot-time {
|
|
477
|
+
font-size: 0.8rem;
|
|
478
|
+
font-weight: 500;
|
|
479
|
+
margin-bottom: 0.25rem;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.snapshot-events {
|
|
483
|
+
font-size: 0.75rem;
|
|
484
|
+
color: #666;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.state-viewer {
|
|
488
|
+
background: white;
|
|
489
|
+
border: 1px solid #ddd;
|
|
490
|
+
border-radius: 4px;
|
|
491
|
+
padding: 1rem;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.state-viewer h3 {
|
|
495
|
+
margin-top: 0;
|
|
496
|
+
margin-bottom: 0.5rem;
|
|
497
|
+
font-size: 1rem;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.state-viewer pre {
|
|
501
|
+
margin: 0;
|
|
502
|
+
overflow-x: auto;
|
|
503
|
+
font-size: 0.85rem;
|
|
504
|
+
line-height: 1.5;
|
|
505
|
+
}
|
|
506
|
+
</style>
|