jac-client 0.1.0__py3-none-any.whl

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 (33) hide show
  1. jac_client/docs/README.md +629 -0
  2. jac_client/docs/advanced-state.md +706 -0
  3. jac_client/docs/imports.md +650 -0
  4. jac_client/docs/lifecycle-hooks.md +554 -0
  5. jac_client/docs/routing.md +530 -0
  6. jac_client/examples/little-x/app.jac +615 -0
  7. jac_client/examples/little-x/package-lock.json +2840 -0
  8. jac_client/examples/little-x/package.json +23 -0
  9. jac_client/examples/little-x/submit-button.jac +8 -0
  10. jac_client/examples/todo-app/README.md +82 -0
  11. jac_client/examples/todo-app/app.jac +683 -0
  12. jac_client/examples/todo-app/package-lock.json +999 -0
  13. jac_client/examples/todo-app/package.json +22 -0
  14. jac_client/plugin/cli.py +328 -0
  15. jac_client/plugin/client.py +41 -0
  16. jac_client/plugin/client_runtime.jac +941 -0
  17. jac_client/plugin/vite_client_bundle.py +470 -0
  18. jac_client/tests/__init__.py +2 -0
  19. jac_client/tests/fixtures/button.jac +6 -0
  20. jac_client/tests/fixtures/client_app.jac +18 -0
  21. jac_client/tests/fixtures/client_app_with_antd.jac +21 -0
  22. jac_client/tests/fixtures/js_import.jac +30 -0
  23. jac_client/tests/fixtures/package-lock.json +329 -0
  24. jac_client/tests/fixtures/package.json +11 -0
  25. jac_client/tests/fixtures/relative_import.jac +13 -0
  26. jac_client/tests/fixtures/test_fragments_spread.jac +44 -0
  27. jac_client/tests/fixtures/utils.js +22 -0
  28. jac_client/tests/test_cl.py +360 -0
  29. jac_client/tests/test_create_jac_app.py +139 -0
  30. jac_client-0.1.0.dist-info/METADATA +126 -0
  31. jac_client-0.1.0.dist-info/RECORD +33 -0
  32. jac_client-0.1.0.dist-info/WHEEL +4 -0
  33. jac_client-0.1.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,683 @@
1
+ node Todo {
2
+ has text: str;
3
+ has done: bool = False;
4
+ }
5
+
6
+ walker create_todo {
7
+ can create with `root entry {
8
+ new_todo = here ++> Todo(text="Example Todo");
9
+ report new_todo;
10
+ }
11
+ }
12
+ walker toggle_todo {
13
+ can toggle with Todo entry {
14
+ here.done = not here.done;
15
+ report here;
16
+ }
17
+ }
18
+ walker read_todos {
19
+ can read with `root entry {
20
+ visit [-->(`?Todo)];
21
+ }
22
+
23
+ can report_todos with exit {
24
+ report here;
25
+ }
26
+ }
27
+
28
+ cl {
29
+ let [todoState, setTodoState] = createState({
30
+ "items": [],
31
+ "filter": "all",
32
+ "input": ""
33
+ });
34
+
35
+ # -----------------------------
36
+ # State helpers
37
+ # -----------------------------
38
+ def onInputChange(e: any) -> None {
39
+ setTodoState({"input": e.target.value});
40
+ }
41
+
42
+ async def onAddTodo(e: any) -> None {
43
+ e.preventDefault();
44
+ inputEl = document.getElementById("todo-input");
45
+ text = (inputEl.value if inputEl and inputEl.value else "").trim();
46
+ if not text { return; }
47
+
48
+ new_todo = await __jacSpawn("create_todo", {"text": text});
49
+
50
+ s = todoState();
51
+ newItem = {"id": new_todo._jac_id, "text": new_todo.text, "done": new_todo.done};
52
+ setTodoState({"items": s.items.concat([newItem])});
53
+
54
+ # Clear the input visually
55
+ if inputEl { inputEl.value = ""; }
56
+ }
57
+
58
+ def setFilter(next: str) -> None {
59
+ setTodoState({"filter": next});
60
+ }
61
+
62
+ async def toggleTodo(id: any) -> None {
63
+ # we need to pass the id to the toggle_todo walker
64
+ toggled_todo = await __jacSpawn("toggle_todo", {}, id);
65
+ s = todoState();
66
+ updated = [];
67
+ for item in s.items {
68
+ if item.id == id {
69
+ updated.push({"id": item.id, "text": item.text, "done": not item.done});
70
+ } else {
71
+ updated.push(item);
72
+ }
73
+ }
74
+ setTodoState({"items": updated});
75
+ }
76
+
77
+ def removeTodo(id: any) -> None {
78
+ s = todoState();
79
+ remaining = [];
80
+ for item in s.items {
81
+ if item.id != id { remaining.push(item); }
82
+ }
83
+ setTodoState({"items": remaining});
84
+ }
85
+
86
+ def clearCompleted() -> None {
87
+ s = todoState();
88
+ remaining = [];
89
+ for item in s.items {
90
+ if not item.done { remaining.push(item); }
91
+ }
92
+ setTodoState({"items": remaining});
93
+ }
94
+
95
+ def filteredItems() -> list {
96
+ s = todoState();
97
+ result = [];
98
+ if s.filter == "active" {
99
+ for it in s.items { if not it.done { result.push(it); } }
100
+ return result;
101
+ } elif s.filter == "completed" {
102
+ for it in s.items { if it.done { result.push(it); } }
103
+ return result;
104
+ }
105
+ return s.items;
106
+ }
107
+
108
+ # -----------------------------
109
+ # UI pieces
110
+ # -----------------------------
111
+ def TodoItem(item: dict) -> any {
112
+ console.log(item.text,"-",typeof (item.done)== 'boolean');
113
+ return <li key={item.id} style={{
114
+ "display": "flex",
115
+ "gap": "12px",
116
+ "alignItems": "center",
117
+ "background": "#FFFFFF",
118
+ "padding": "12px 16px",
119
+ "borderRadius": "10px",
120
+ "marginBottom": "8px",
121
+ "boxShadow": "0 1px 2px rgba(17,24,39,0.06)",
122
+ "border": "1px solid #E5E7EB"
123
+ }}>
124
+ <input
125
+ type="checkbox"
126
+ checked={item.done}
127
+
128
+ onChange={lambda -> None { toggleTodo(item.id); }}
129
+ style={{
130
+ "width": "18px",
131
+ "height": "18px",
132
+ "accentColor": "#7C3AED",
133
+ "cursor": "pointer"
134
+ }}
135
+ />
136
+ <span style={{
137
+ "textDecoration": ("line-through" if item.done else "none"),
138
+ "flex": "1",
139
+ "fontSize": "16px",
140
+ "color": ("#9CA3AF" if item.done else "#111827")
141
+ }}>
142
+ {item.text}
143
+ </span>
144
+ <button
145
+ style={{
146
+ "marginLeft": "auto",
147
+ "padding": "6px 12px",
148
+ "background": "#FFFFFF",
149
+ "color": "#EF4444",
150
+ "border": "1px solid #FCA5A5",
151
+ "borderRadius": "6px",
152
+ "fontSize": "12px",
153
+ "fontWeight": "600",
154
+ "cursor": "pointer",
155
+ "boxShadow": "none",
156
+ "transition": "all 0.2s ease"
157
+ }}
158
+ onClick={lambda -> None { removeTodo(item.id); }}>
159
+ Remove
160
+ </button>
161
+ </li>;
162
+ }
163
+
164
+ def RenderUl(children: list, style: dict = {}) -> any {
165
+ return __jacJsx("ul", {"style": style}, children);
166
+ }
167
+
168
+ async def read_todos_action() -> any {
169
+ todos = await __jacSpawn("read_todos");
170
+ for todo in todos.reports {
171
+ console.log("Todo read:", todo);
172
+ setTodoState({"items": todoState().items.concat([{"id": todo._jac_id, "text": todo.text, "done": todo.done}])});
173
+ }
174
+ }
175
+
176
+ def TodoApp() -> any {
177
+ s = todoState();
178
+
179
+ onMount(lambda -> None {
180
+ read_todos_action();
181
+ });
182
+
183
+
184
+ itemsArr = filteredItems();
185
+ if not Array.isArray(itemsArr) {
186
+ console.warn("filteredItems() did not return an array; coercing to []", itemsArr);
187
+ itemsArr = [];
188
+ }
189
+
190
+ activeCount = 0;
191
+ for it in s.items { if not it.done { activeCount += 1; } }
192
+
193
+ children = [];
194
+ for it in itemsArr { children.push(TodoItem(it)); }
195
+
196
+
197
+
198
+
199
+ return <div style={{
200
+ "maxWidth": "640px",
201
+ "margin": "24px auto",
202
+ "padding": "24px",
203
+ "background": "#FFFFFF",
204
+ "borderRadius": "12px",
205
+ "fontFamily": "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
206
+ "boxShadow": "0 10px 20px rgba(17,24,39,0.06)",
207
+ "border": "1px solid #E5E7EB",
208
+ "color": "#111827"
209
+ }}>
210
+ <h2 style={{
211
+ "marginTop": "0",
212
+ "color": "#111827",
213
+ "textAlign": "left",
214
+ "fontSize": "1.5rem",
215
+ "fontWeight": "700",
216
+ "marginBottom": "16px"
217
+ }}>My Todos</h2>
218
+
219
+ <form id="todo_submit" onSubmit={onAddTodo} style={{
220
+ "display": "flex",
221
+ "gap": "12px",
222
+ "marginBottom": "16px",
223
+ "background": "#FFFFFF",
224
+ "padding": "16px",
225
+ "borderRadius": "10px",
226
+ "border": "1px solid #E5E7EB",
227
+ "boxShadow": "0 1px 2px rgba(17,24,39,0.04)"
228
+ }}>
229
+ <input
230
+ id="todo-input"
231
+ type="text"
232
+ placeholder="What needs to be done?"
233
+ style={{
234
+ "flex": "1",
235
+ "padding": "12px 14px",
236
+ "border": "1px solid #E5E7EB",
237
+ "borderRadius": "8px",
238
+ "fontSize": "16px",
239
+ "outline": "none",
240
+ "background": "#FFFFFF",
241
+ "color": "#111827"
242
+ }}
243
+ />
244
+ <button type="submit" style={{
245
+ "padding": "12px 18px",
246
+ "background": "#7C3AED",
247
+ "color": "#FFFFFF",
248
+ "border": "1px solid #6D28D9",
249
+ "borderRadius": "8px",
250
+ "fontSize": "15px",
251
+ "fontWeight": "600",
252
+ "cursor": "pointer",
253
+ "boxShadow": "0 1px 2px rgba(124,58,237,0.3)",
254
+ "transition": "all 0.2s ease"
255
+ }}>Add Todo</button>
256
+ </form>
257
+
258
+ <div style={{
259
+ "display": "flex",
260
+ "gap": "8px",
261
+ "marginTop": "8px",
262
+ "background": "#FFFFFF",
263
+ "padding": "10px",
264
+ "borderRadius": "10px",
265
+ "border": "1px solid #E5E7EB",
266
+ "flexWrap": "wrap"
267
+ }}>
268
+ <button
269
+ onClick={lambda -> None { setFilter("all"); }}
270
+ style={{
271
+ "padding": "8px 14px",
272
+ "background": ("#7C3AED" if s.filter == "all" else "#FFFFFF"),
273
+ "color": ("#FFFFFF" if s.filter == "all" else "#7C3AED"),
274
+ "border": ("1px solid #6D28D9" if s.filter == "all" else "1px solid #E5E7EB"),
275
+ "borderRadius": "20px",
276
+ "fontSize": "14px",
277
+ "fontWeight": ("700" if s.filter == "all" else "500"),
278
+ "cursor": "pointer",
279
+ "boxShadow": ("0 1px 2px rgba(124,58,237,0.25)" if s.filter == "all" else "none"),
280
+ "transition": "all 0.2s ease"
281
+ }}>
282
+ All
283
+ </button>
284
+ <button
285
+ onClick={lambda -> None { setFilter("active"); }}
286
+ style={{
287
+ "padding": "8px 14px",
288
+ "background": ("#7C3AED" if s.filter == "active" else "#FFFFFF"),
289
+ "color": ("#FFFFFF" if s.filter == "active" else "#7C3AED"),
290
+ "border": ("1px solid #6D28D9" if s.filter == "active" else "1px solid #E5E7EB"),
291
+ "borderRadius": "20px",
292
+ "fontSize": "14px",
293
+ "fontWeight": ("700" if s.filter == "active" else "500"),
294
+ "cursor": "pointer",
295
+ "boxShadow": ("0 1px 2px rgba(124,58,237,0.25)" if s.filter == "active" else "none"),
296
+ "transition": "all 0.2s ease"
297
+ }}>
298
+ Active
299
+ </button>
300
+ <button
301
+ onClick={lambda -> None { setFilter("completed"); }}
302
+ style={{
303
+ "padding": "8px 14px",
304
+ "background": ("#7C3AED" if s.filter == "completed" else "#FFFFFF"),
305
+ "color": ("#FFFFFF" if s.filter == "completed" else "#7C3AED"),
306
+ "border": ("1px solid #6D28D9" if s.filter == "completed" else "1px solid #E5E7EB"),
307
+ "borderRadius": "20px",
308
+ "fontSize": "14px",
309
+ "fontWeight": ("700" if s.filter == "completed" else "500"),
310
+ "cursor": "pointer",
311
+ "boxShadow": ("0 1px 2px rgba(124,58,237,0.25)" if s.filter == "completed" else "none"),
312
+ "transition": "all 0.2s ease"
313
+ }}>
314
+ Completed
315
+ </button>
316
+ <button
317
+ style={{
318
+ "marginLeft": "auto",
319
+ "padding": "8px 14px",
320
+ "background": "#F9FAFB",
321
+ "color": "#374151",
322
+ "border": "1px solid #E5E7EB",
323
+ "borderRadius": "20px",
324
+ "fontSize": "14px",
325
+ "fontWeight": "600",
326
+ "cursor": "pointer",
327
+ "boxShadow": "none",
328
+ "transition": "all 0.2s ease"
329
+ }}
330
+ onClick={clearCompleted}>
331
+ Clear Completed
332
+ </button>
333
+ </div>
334
+
335
+ {RenderUl(children, {
336
+ "listStyle": "none",
337
+ "padding": "0",
338
+ "marginTop": "12px",
339
+ "background": "#FFFFFF",
340
+ "borderRadius": "12px",
341
+ "padding": "12px",
342
+ "border": "1px solid #E5E7EB",
343
+ "max-height": "366px",
344
+ "overflowY": "auto"
345
+ })}
346
+ {[TodoItem(item) for item in items]}
347
+ <div style={{
348
+ "marginTop": "12px",
349
+ "color": "#374151",
350
+ "textAlign": "center",
351
+ "fontSize": "14px",
352
+ "fontWeight": "500",
353
+ "background": "#F3F4F6",
354
+ "padding": "8px 12px",
355
+ "borderRadius": "10px",
356
+ "border": "1px solid #E5E7EB"
357
+ }}>
358
+ {s.items.length} total, {activeCount} active
359
+ </div>
360
+ </div>;
361
+ }
362
+
363
+ # -----------------------------
364
+ # Auth pages
365
+ # -----------------------------
366
+ def LoginForm() -> any {
367
+ return <div style={{
368
+ "maxWidth": "420px",
369
+ "margin": "60px auto",
370
+ "padding": "28px",
371
+ "background": "#FFFFFF",
372
+ "borderRadius": "12px",
373
+ "fontFamily": "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
374
+ "boxShadow": "0 10px 20px rgba(17,24,39,0.06)",
375
+ "border": "1px solid #E5E7EB",
376
+ "color": "#111827"
377
+ }}>
378
+ <h2 style={{
379
+ "marginTop": "0",
380
+ "color": "#111827",
381
+ "textAlign": "center",
382
+ "fontSize": "1.5rem",
383
+ "fontWeight": "700",
384
+ "marginBottom": "20px"
385
+ }}>Welcome Back</h2>
386
+ <form onSubmit={handle_login}>
387
+ <div style={{"marginBottom": "20px"}}>
388
+ <label style={{
389
+ "display": "block",
390
+ "marginBottom": "8px",
391
+ "color": "#374151",
392
+ "fontSize": "14px",
393
+ "fontWeight": "600"
394
+ }}>Username</label>
395
+ <input id="login-username" type="text" style={{
396
+ "width": "100%",
397
+ "padding": "12px 16px",
398
+ "border": "1px solid #E5E7EB",
399
+ "borderRadius": "8px",
400
+ "fontSize": "16px",
401
+ "background": "#FFFFFF",
402
+ "color": "#111827",
403
+ "outline": "none",
404
+ "boxSizing": "border-box"
405
+ }}/>
406
+ </div>
407
+ <div style={{"marginBottom": "24px"}}>
408
+ <label style={{
409
+ "display": "block",
410
+ "marginBottom": "8px",
411
+ "color": "#374151",
412
+ "fontSize": "14px",
413
+ "fontWeight": "600"
414
+ }}>Password</label>
415
+ <input id="login-password" type="password" style={{
416
+ "width": "100%",
417
+ "padding": "12px 16px",
418
+ "border": "1px solid #E5E7EB",
419
+ "borderRadius": "8px",
420
+ "fontSize": "16px",
421
+ "background": "#FFFFFF",
422
+ "color": "#111827",
423
+ "outline": "none",
424
+ "boxSizing": "border-box"
425
+ }}/>
426
+ </div>
427
+ <button type="submit" style={{
428
+ "width": "100%",
429
+ "padding": "12px",
430
+ "background": "#7C3AED",
431
+ "color": "#FFFFFF",
432
+ "border": "1px solid #6D28D9",
433
+ "borderRadius": "8px",
434
+ "fontSize": "15px",
435
+ "fontWeight": "600",
436
+ "cursor": "pointer",
437
+ "boxShadow": "0 1px 2px rgba(124,58,237,0.3)",
438
+ "transition": "all 0.2s ease"
439
+ }}>Sign In</button>
440
+ </form>
441
+ <div style={{
442
+ "marginTop": "16px",
443
+ "textAlign": "center",
444
+ "background": "#F9FAFB",
445
+ "padding": "10px",
446
+ "borderRadius": "8px",
447
+ "border": "1px solid #E5E7EB"
448
+ }}>
449
+ <Link href="/signup" style={{
450
+ "color": "#7C3AED",
451
+ "textDecoration": "none",
452
+ "fontWeight": "500"
453
+ }}>Create an account</Link>
454
+ </div>
455
+ </div>;
456
+ }
457
+
458
+ async def handle_login(e: any) -> None {
459
+ e.preventDefault();
460
+ username = document.getElementById("login-username").value;
461
+ password = document.getElementById("login-password").value;
462
+ success = await jacLogin(username, password);
463
+ if success { navigate("/todos"); } else { alert("Login failed"); }
464
+ }
465
+
466
+ def SignupForm() -> any {
467
+ return <div style={{
468
+ "maxWidth": "420px",
469
+ "margin": "60px auto",
470
+ "padding": "28px",
471
+ "background": "#FFFFFF",
472
+ "borderRadius": "12px",
473
+ "fontFamily": "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
474
+ "boxShadow": "0 10px 20px rgba(17,24,39,0.06)",
475
+ "border": "1px solid #E5E7EB",
476
+ "color": "#111827"
477
+ }}>
478
+ <h2 style={{
479
+ "marginTop": "0",
480
+ "color": "#111827",
481
+ "textAlign": "center",
482
+ "fontSize": "1.5rem",
483
+ "fontWeight": "700",
484
+ "marginBottom": "20px"
485
+ }}>Join Us</h2>
486
+ <form onSubmit={handle_signup}>
487
+ <div style={{"marginBottom": "20px"}}>
488
+ <label style={{
489
+ "display": "block",
490
+ "marginBottom": "8px",
491
+ "color": "#374151",
492
+ "fontSize": "14px",
493
+ "fontWeight": "600"
494
+ }}>Username</label>
495
+ <input id="signup-username" type="text" required style={{
496
+ "width": "100%",
497
+ "padding": "12px 16px",
498
+ "border": "1px solid #E5E7EB",
499
+ "borderRadius": "8px",
500
+ "fontSize": "16px",
501
+ "background": "#FFFFFF",
502
+ "color": "#111827",
503
+ "outline": "none",
504
+ "boxSizing": "border-box"
505
+ }}/>
506
+ </div>
507
+ <div style={{"marginBottom": "20px"}}>
508
+ <label style={{
509
+ "display": "block",
510
+ "marginBottom": "8px",
511
+ "color": "#374151",
512
+ "fontSize": "14px",
513
+ "fontWeight": "600"
514
+ }}>Password</label>
515
+ <input id="signup-password" type="password" required style={{
516
+ "width": "100%",
517
+ "padding": "12px 16px",
518
+ "border": "1px solid #E5E7EB",
519
+ "borderRadius": "8px",
520
+ "fontSize": "16px",
521
+ "background": "#FFFFFF",
522
+ "color": "#111827",
523
+ "outline": "none",
524
+ "boxSizing": "border-box"
525
+ }}/>
526
+ </div>
527
+ <div style={{"marginBottom": "24px"}}>
528
+ <label style={{
529
+ "display": "block",
530
+ "marginBottom": "8px",
531
+ "color": "#374151",
532
+ "fontSize": "14px",
533
+ "fontWeight": "600"
534
+ }}>Confirm Password</label>
535
+ <input id="signup-password-confirm" type="password" required style={{
536
+ "width": "100%",
537
+ "padding": "12px 16px",
538
+ "border": "1px solid #E5E7EB",
539
+ "borderRadius": "8px",
540
+ "fontSize": "16px",
541
+ "background": "#FFFFFF",
542
+ "color": "#111827",
543
+ "outline": "none",
544
+ "boxSizing": "border-box"
545
+ }}/>
546
+ </div>
547
+ <button type="submit" style={{
548
+ "width": "100%",
549
+ "padding": "12px",
550
+ "background": "#7C3AED",
551
+ "color": "#FFFFFF",
552
+ "border": "1px solid #6D28D9",
553
+ "borderRadius": "8px",
554
+ "fontSize": "15px",
555
+ "fontWeight": "600",
556
+ "cursor": "pointer",
557
+ "boxShadow": "0 1px 2px rgba(124,58,237,0.3)",
558
+ "transition": "all 0.2s ease"
559
+ }}>Create Account</button>
560
+ </form>
561
+ <div style={{
562
+ "marginTop": "16px",
563
+ "textAlign": "center",
564
+ "background": "#F9FAFB",
565
+ "padding": "10px",
566
+ "borderRadius": "8px",
567
+ "border": "1px solid #E5E7EB"
568
+ }}>
569
+ <Link href="/login" style={{
570
+ "color": "#7C3AED",
571
+ "textDecoration": "none",
572
+ "fontWeight": "500"
573
+ }}>Already have an account? Login</Link>
574
+ </div>
575
+ </div>;
576
+ }
577
+
578
+ async def handle_signup(e: any) -> None {
579
+ e.preventDefault();
580
+ username = document.getElementById("signup-username").value;
581
+ password = document.getElementById("signup-password").value;
582
+ confirm = document.getElementById("signup-password-confirm").value;
583
+
584
+ if password != confirm { alert("Passwords do not match"); return; }
585
+ if username.length < 3 { alert("Username must be at least 3 characters"); return; }
586
+ if password.length < 6 { alert("Password must be at least 6 characters"); return; }
587
+
588
+ result = await jacSignup(username, password);
589
+
590
+ if result["success"] if "success" in result else False {
591
+ alert("Account created successfully! Welcome to My Todo!");
592
+ navigate("/todos");
593
+ } else {
594
+ alert(result["error"] if "error" in result else "Signup failed");
595
+ }
596
+ }
597
+
598
+ def logout_action() -> None {
599
+ jacLogout();
600
+ setTodoState({"items": [
601
+ {"id": 1, "text": "Sign up for an account", "done": False},
602
+ {"id": 2, "text": "Log in", "done": False},
603
+ {"id": 3, "text": "Add a new todo", "done": False},
604
+ {"id": 4, "text": "Toggle a todo", "done": False},
605
+ {"id": 5, "text": "Filter active/completed", "done": False},
606
+ {"id": 6, "text": "Clear completed", "done": False}
607
+ ], "filter": "all", "input": ""});
608
+ navigate("/login");
609
+ }
610
+
611
+ # -----------------------------
612
+ # Shell + Router
613
+ # -----------------------------
614
+ def Nav(route: str) -> any {
615
+ if not jacIsLoggedIn() or route == "/login" or route == "/signup" { return None; }
616
+ return <nav style={{
617
+ "background": "#FFFFFF",
618
+ "padding": "12px",
619
+ "boxShadow": "0 1px 2px rgba(17,24,39,0.06)",
620
+ "border": "1px solid #E5E7EB",
621
+ "borderRadius": "10px"
622
+ }}>
623
+ <div style={{
624
+ "maxWidth": "960px",
625
+ "margin": "0 auto",
626
+ "display": "flex",
627
+ "gap": "16px",
628
+ "alignItems": "center",
629
+ "padding": "0 12px"
630
+ }}>
631
+ <Link href="/todos" style={{"textDecoration": "none"}}>
632
+ <span style={{
633
+ "color": "#111827",
634
+ "fontWeight": "800",
635
+ "fontSize": "18px"
636
+ }}>📝 My Todos</span>
637
+ </Link>
638
+ <button
639
+ onClick={logout_action}
640
+ style={{
641
+ "marginLeft": "auto",
642
+ "padding": "8px 12px",
643
+ "background": "#FFFFFF",
644
+ "color": "#374151",
645
+ "border": "1px solid #E5E7EB",
646
+ "borderRadius": "18px",
647
+ "cursor": "pointer",
648
+ "fontSize": "14px",
649
+ "fontWeight": "600",
650
+ "boxShadow": "none",
651
+ "transition": "all 0.2s ease"
652
+ }}
653
+ >
654
+ Logout
655
+ </button>
656
+
657
+ </div>
658
+ </nav>;
659
+ }
660
+
661
+ def App() -> any {
662
+ login_route = {"path": "/login", "component": lambda -> any { return LoginForm(); }, "guard": None};
663
+ signup_route = {"path": "/signup", "component": lambda -> any { return SignupForm(); }, "guard": None};
664
+ todos_route = {"path": "/todos", "component": lambda -> any { return TodoApp(); }, "guard": jacIsLoggedIn};
665
+
666
+ routes = [login_route, signup_route, todos_route];
667
+ router = initRouter(routes, "/login"); # changed from createRouter
668
+
669
+ currentPath = router.path();
670
+ return <div style={{
671
+ "minHeight": "95vh",
672
+ "background": "#F7F8FA",
673
+ "padding": "24px"
674
+ }}>
675
+ {Nav(currentPath)}
676
+ <div style={{"maxWidth": "960px", "margin": "0 auto", "padding": "20px"}}>
677
+ {router.render()}
678
+ </div>
679
+ </div>;
680
+ }
681
+
682
+ def jac_app() -> any { return App(); }
683
+ }