pyjs-examples 0.0.1__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 (71) hide show
  1. pyjs_examples/__init__.py +1 -0
  2. pyjs_examples/calculator/README.md +55 -0
  3. pyjs_examples/calculator/__init__.py +1 -0
  4. pyjs_examples/calculator/__main__.py +9 -0
  5. pyjs_examples/calculator/calculator.py +325 -0
  6. pyjs_examples/calculator/tests/test_browser_calculator.py +67 -0
  7. pyjs_examples/calculator/tests/test_calculator.py +72 -0
  8. pyjs_examples/catalog.py +1433 -0
  9. pyjs_examples/chatroom/README.md +50 -0
  10. pyjs_examples/chatroom/__init__.py +1 -0
  11. pyjs_examples/chatroom/__main__.py +12 -0
  12. pyjs_examples/chatroom/chat_server.py +156 -0
  13. pyjs_examples/chatroom/chatroom.py +704 -0
  14. pyjs_examples/chatroom/tests/test_browser_chatroom.py +59 -0
  15. pyjs_examples/chatroom/tests/test_chat_server.py +53 -0
  16. pyjs_examples/chatroom/visual_test.py +156 -0
  17. pyjs_examples/counter.py +73 -0
  18. pyjs_examples/kanban/README.md +31 -0
  19. pyjs_examples/kanban/__init__.py +1 -0
  20. pyjs_examples/kanban/__main__.py +8 -0
  21. pyjs_examples/kanban/kanban.py +581 -0
  22. pyjs_examples/kanban/tests/renders/kanban-board.png +0 -0
  23. pyjs_examples/kanban/tests/test_browser_kanban.py +150 -0
  24. pyjs_examples/kanban/tests/test_kanban.py +38 -0
  25. pyjs_examples/kanban/visual_test.py +108 -0
  26. pyjs_examples/nativescript_counter/README.md +31 -0
  27. pyjs_examples/nativescript_counter/__init__.py +5 -0
  28. pyjs_examples/nativescript_counter/__main__.py +17 -0
  29. pyjs_examples/nativescript_counter/counter.py +54 -0
  30. pyjs_examples/nativescript_counter/tests/test_android_counter.py +31 -0
  31. pyjs_examples/nativescript_counter/tests/test_counter.py +11 -0
  32. pyjs_examples/samples/__init__.py +1 -0
  33. pyjs_examples/samples/state_counter/README.md +19 -0
  34. pyjs_examples/samples/state_counter/__init__.py +1 -0
  35. pyjs_examples/samples/state_counter/__main__.py +3 -0
  36. pyjs_examples/samples/state_counter/state_counter.py +60 -0
  37. pyjs_examples/samples/state_counter/tests/test_browser_state_counter.py +18 -0
  38. pyjs_examples/samples/state_counter/tests/test_state_counter.py +17 -0
  39. pyjs_examples/samples/ui_dialog/README.md +19 -0
  40. pyjs_examples/samples/ui_dialog/__init__.py +1 -0
  41. pyjs_examples/samples/ui_dialog/__main__.py +3 -0
  42. pyjs_examples/samples/ui_dialog/tests/test_browser_ui_dialog.py +21 -0
  43. pyjs_examples/samples/ui_dialog/tests/test_ui_dialog.py +10 -0
  44. pyjs_examples/samples/ui_dialog/ui_dialog.py +65 -0
  45. pyjs_examples/space_invaders/README.md +45 -0
  46. pyjs_examples/space_invaders/__init__.py +1 -0
  47. pyjs_examples/space_invaders/__main__.py +3 -0
  48. pyjs_examples/space_invaders/space_invaders.py +1025 -0
  49. pyjs_examples/space_invaders/tests/renders/action_scene.png +0 -0
  50. pyjs_examples/space_invaders/tests/renders/initial_wave.png +0 -0
  51. pyjs_examples/space_invaders/tests/test_browser_game.py +86 -0
  52. pyjs_examples/space_invaders/tests/test_game.py +83 -0
  53. pyjs_examples/todomvc/README.md +50 -0
  54. pyjs_examples/todomvc/__init__.py +1 -0
  55. pyjs_examples/todomvc/__main__.py +12 -0
  56. pyjs_examples/todomvc/base.css +141 -0
  57. pyjs_examples/todomvc/index.css +393 -0
  58. pyjs_examples/todomvc/tests/test_browser_todomvc.py +145 -0
  59. pyjs_examples/todomvc/tests/test_todomvc.py +84 -0
  60. pyjs_examples/todomvc/todomvc.py +426 -0
  61. pyjs_examples/workspace/README.md +25 -0
  62. pyjs_examples/workspace/__init__.py +1 -0
  63. pyjs_examples/workspace/__main__.py +3 -0
  64. pyjs_examples/workspace/tests/test_browser_workspace.py +109 -0
  65. pyjs_examples/workspace/tests/test_workspace.py +27 -0
  66. pyjs_examples/workspace/workspace.py +749 -0
  67. pyjs_examples-0.0.1.dist-info/METADATA +71 -0
  68. pyjs_examples-0.0.1.dist-info/RECORD +71 -0
  69. pyjs_examples-0.0.1.dist-info/WHEEL +5 -0
  70. pyjs_examples-0.0.1.dist-info/entry_points.txt +11 -0
  71. pyjs_examples-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,704 @@
1
+ from pyjs import effect, js, nojs, state
2
+ from pyjs.web.dom import location, sessionStorage
3
+ from pyjs.web.domx import button, form, input, ref, tag, tw
4
+ from pyjs.web.events import Event, MessageEvent
5
+ from pyjs.web.javascript import JSON
6
+ from pyjs.web.network import WebSocket
7
+ from pyjs.web.ui import Widget
8
+
9
+
10
+ @js
11
+ class ChatMessage:
12
+ def __init__(self):
13
+ self.id = 0
14
+ self.room = ""
15
+ self.sender = ""
16
+ self.text = ""
17
+ self.time = ""
18
+
19
+
20
+ @js
21
+ class ChatSnapshot:
22
+ def __init__(self):
23
+ self.type = ""
24
+ self.room = ""
25
+ self.rooms: list[str] = []
26
+ self.messages: list[ChatMessage] = []
27
+ self.users: list[str] = []
28
+ self.message = ""
29
+
30
+
31
+ @js
32
+ class OutgoingChatMessage:
33
+ def __init__(self, message_type: str, name: str, room: str, text: str):
34
+ self.type = message_type
35
+ self.name = name
36
+ self.room = room
37
+ self.text = text
38
+
39
+
40
+ @js
41
+ class NameSubmitListener:
42
+ def __init__(self, app: ChatRoomApp):
43
+ self.app = app
44
+
45
+ @js(include=True)
46
+ def handleEvent(self, event: Event):
47
+ event.preventDefault()
48
+ self.app.submit_name()
49
+
50
+
51
+ @js
52
+ class RoomCreateListener:
53
+ def __init__(self, app: ChatRoomApp):
54
+ self.app = app
55
+
56
+ @js(include=True)
57
+ def handleEvent(self, event: Event):
58
+ event.preventDefault()
59
+ self.app.create_room()
60
+
61
+
62
+ @js
63
+ class MessageSubmitListener:
64
+ def __init__(self, app: ChatRoomApp):
65
+ self.app = app
66
+
67
+ @js(include=True)
68
+ def handleEvent(self, event: Event):
69
+ event.preventDefault()
70
+ self.app.submit_message()
71
+
72
+
73
+ @js
74
+ class RoomClickListener:
75
+ def __init__(self, app: ChatRoomApp, room: str):
76
+ self.app = app
77
+ self.room = room
78
+
79
+ @js(include=True)
80
+ def handleEvent(self, event: Event):
81
+ self.app.switch_room(self.room)
82
+
83
+
84
+ @js
85
+ class ReconnectClickListener:
86
+ def __init__(self, app: ChatRoomApp):
87
+ self.app = app
88
+
89
+ @js(include=True)
90
+ def handleEvent(self, event: Event):
91
+ self.app.connect()
92
+
93
+
94
+ @js
95
+ class SignOutClickListener:
96
+ def __init__(self, app: ChatRoomApp):
97
+ self.app = app
98
+
99
+ @js(include=True)
100
+ def handleEvent(self, event: Event):
101
+ self.app.sign_out()
102
+
103
+
104
+ @js
105
+ class SocketOpenListener:
106
+ def __init__(self, app: ChatRoomApp):
107
+ self.app = app
108
+
109
+ @js(include=True)
110
+ def handleEvent(self, event: Event):
111
+ self.app.socket_opened()
112
+
113
+
114
+ @js
115
+ class SocketMessageListener:
116
+ def __init__(self, app: ChatRoomApp):
117
+ self.app = app
118
+
119
+ @js(include=True)
120
+ def handleEvent(self, event: MessageEvent):
121
+ message: ChatSnapshot = JSON.parse(event.data)
122
+ self.app.receive(message)
123
+
124
+
125
+ @js
126
+ class SocketCloseListener:
127
+ def __init__(self, app: ChatRoomApp):
128
+ self.app = app
129
+
130
+ @js(include=True)
131
+ def handleEvent(self, event: Event):
132
+ self.app.socket_closed()
133
+
134
+
135
+ @js
136
+ class SocketErrorListener:
137
+ def __init__(self, app: ChatRoomApp):
138
+ self.app = app
139
+
140
+ @js(include=True)
141
+ def handleEvent(self, event: Event):
142
+ self.app.show_error("The chat server could not be reached.")
143
+
144
+
145
+ @js
146
+ class ChatRoomApp(Widget):
147
+ name = state("")
148
+ room = state("general")
149
+ connection = state("signed-out")
150
+ socket_path = state("")
151
+
152
+ def __init__(self, socket_path: str = ""):
153
+ super().__init__()
154
+ self.socket_path = socket_path
155
+
156
+ self.name_input = ref(
157
+ input(
158
+ tw(
159
+ "w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-base "
160
+ "text-slate-950 outline-none placeholder:text-slate-400 "
161
+ "focus:border-violet-500 focus:ring-4 focus:ring-violet-100"
162
+ ),
163
+ {
164
+ "autocomplete": "nickname",
165
+ "data-testid": "name-input",
166
+ "maxlength": "32",
167
+ "placeholder": "How should people know you?",
168
+ },
169
+ )
170
+ )
171
+ self.name_form = ref(
172
+ form(
173
+ tw("mt-6 space-y-3"),
174
+ self.name_input,
175
+ button(
176
+ tw(
177
+ "w-full rounded-xl bg-violet-600 px-4 py-3 font-semibold text-white "
178
+ "shadow-sm hover:bg-violet-700 focus:outline-none focus:ring-4 "
179
+ "focus:ring-violet-200"
180
+ ),
181
+ {"type": "submit"},
182
+ "Enter workspace",
183
+ ),
184
+ )
185
+ )
186
+ self.name_form.addEventListener("submit", NameSubmitListener(self))
187
+ self.sign_in = ref(
188
+ tag(
189
+ "section",
190
+ tw(
191
+ "fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-6 "
192
+ "backdrop-blur-sm"
193
+ ),
194
+ {"data-testid": "sign-in"},
195
+ tag(
196
+ "div",
197
+ tw("w-full max-w-md rounded-3xl bg-white p-8 shadow-2xl"),
198
+ tag(
199
+ "div",
200
+ tw(
201
+ "mb-5 flex h-12 w-12 items-center justify-center rounded-2xl "
202
+ "bg-violet-600 text-xl font-bold text-white"
203
+ ),
204
+ "P",
205
+ ),
206
+ tag(
207
+ "h1",
208
+ tw("text-2xl font-bold tracking-tight text-slate-950"),
209
+ "Join PyJS Workspace",
210
+ ),
211
+ tag(
212
+ "p",
213
+ tw("mt-2 text-sm leading-6 text-slate-500"),
214
+ "Choose a display name for this tab. Nothing is saved when the "
215
+ "server stops.",
216
+ ),
217
+ self.name_form,
218
+ ),
219
+ )
220
+ )
221
+
222
+ self.room_input = ref(
223
+ input(
224
+ tw(
225
+ "min-w-0 flex-1 rounded-lg border border-white/15 bg-white/10 px-3 py-2 "
226
+ "text-sm text-white outline-none placeholder:text-violet-200/70 "
227
+ "focus:border-white/40 focus:bg-white/15"
228
+ ),
229
+ {
230
+ "data-testid": "room-input",
231
+ "maxlength": "32",
232
+ "placeholder": "new-room",
233
+ },
234
+ )
235
+ )
236
+ self.room_form = ref(
237
+ form(
238
+ tw("flex gap-2 px-3 pb-4"),
239
+ self.room_input,
240
+ button(
241
+ tw(
242
+ "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-white/15 "
243
+ "text-lg font-medium text-white hover:bg-white/25 focus:outline-none "
244
+ "focus:ring-2 focus:ring-white/50"
245
+ ),
246
+ {"aria-label": "Create room", "data-testid": "create-room", "type": "submit"},
247
+ "+",
248
+ ),
249
+ )
250
+ )
251
+ self.room_form.addEventListener("submit", RoomCreateListener(self))
252
+ self.room_list = ref(
253
+ tag(
254
+ "nav",
255
+ tw("min-h-0 flex-1 space-y-1 overflow-y-auto px-2 pb-4"),
256
+ {"aria-label": "Rooms", "data-testid": "room-list"},
257
+ )
258
+ )
259
+ self.profile_name = ref(tag("span", tw("truncate font-medium"), "Guest"))
260
+ self.sign_out_button = ref(
261
+ button(
262
+ tw(
263
+ "rounded-md px-2 py-1 text-xs text-violet-200 hover:bg-white/10 "
264
+ "hover:text-white focus:outline-none focus:ring-2 focus:ring-white/40"
265
+ ),
266
+ {"data-testid": "sign-out", "type": "button"},
267
+ "Change",
268
+ )
269
+ )
270
+ self.sign_out_button.addEventListener("click", SignOutClickListener(self))
271
+
272
+ self.current_room = ref(tag("h2", tw("text-lg font-bold text-slate-950"), "# general"))
273
+ self.connection_status = ref(
274
+ tag("span", tw("text-xs font-medium text-slate-500"), "Not connected")
275
+ )
276
+ self.reconnect_button = ref(
277
+ button(
278
+ tw(
279
+ "hidden rounded-lg border border-slate-200 px-3 py-1.5 text-xs font-semibold "
280
+ "text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 "
281
+ "focus:ring-violet-300"
282
+ ),
283
+ {"data-testid": "reconnect", "type": "button"},
284
+ "Reconnect",
285
+ )
286
+ )
287
+ self.reconnect_button.addEventListener("click", ReconnectClickListener(self))
288
+ self.error_banner = ref(
289
+ tag(
290
+ "div",
291
+ tw(
292
+ "hidden border-b border-red-200 bg-red-50 px-5 py-3 text-sm font-medium "
293
+ "text-red-700"
294
+ ),
295
+ {"data-testid": "chat-error", "role": "alert"},
296
+ )
297
+ )
298
+ self.message_list = ref(
299
+ tag(
300
+ "section",
301
+ tw("min-h-0 flex-1 space-y-1 overflow-y-auto px-5 py-6"),
302
+ {"aria-label": "Messages", "data-testid": "message-list"},
303
+ )
304
+ )
305
+ self.message_input = ref(
306
+ input(
307
+ tw(
308
+ "min-w-0 flex-1 bg-transparent px-4 py-3 text-sm text-slate-950 outline-none "
309
+ "placeholder:text-slate-400"
310
+ ),
311
+ {
312
+ "autocomplete": "off",
313
+ "data-testid": "message-input",
314
+ "maxlength": "2000",
315
+ "placeholder": "Message #general",
316
+ },
317
+ )
318
+ )
319
+ self.message_form = ref(
320
+ form(
321
+ tw(
322
+ "flex items-center rounded-xl border border-slate-300 bg-white shadow-sm "
323
+ "focus-within:border-violet-500 focus-within:ring-4 "
324
+ "focus-within:ring-violet-100"
325
+ ),
326
+ self.message_input,
327
+ button(
328
+ tw(
329
+ "mr-2 rounded-lg bg-violet-600 px-4 py-2 text-sm font-semibold text-white "
330
+ "hover:bg-violet-700 focus:outline-none focus:ring-2 focus:ring-violet-300"
331
+ ),
332
+ {"data-testid": "send-message", "type": "submit"},
333
+ "Send",
334
+ ),
335
+ )
336
+ )
337
+ self.message_form.addEventListener("submit", MessageSubmitListener(self))
338
+
339
+ self.member_count = ref(
340
+ tag(
341
+ "span",
342
+ tw("text-xs text-slate-500"),
343
+ {"data-testid": "member-count"},
344
+ "0 people",
345
+ )
346
+ )
347
+ self.member_list = ref(
348
+ tag(
349
+ "div",
350
+ tw("min-h-0 flex-1 space-y-1 overflow-y-auto p-3"),
351
+ {"data-testid": "member-list"},
352
+ )
353
+ )
354
+
355
+ self.workspace = ref(
356
+ tag(
357
+ "div",
358
+ tw("hidden h-screen min-h-[36rem] overflow-hidden bg-slate-100 text-slate-950"),
359
+ {"data-testid": "workspace"},
360
+ tag(
361
+ "div",
362
+ tw(
363
+ "grid h-full grid-cols-[16rem_minmax(0,1fr)] "
364
+ "lg:grid-cols-[16rem_minmax(0,1fr)_15rem]"
365
+ ),
366
+ tag(
367
+ "aside",
368
+ tw("flex min-h-0 flex-col bg-violet-950 text-white"),
369
+ tag(
370
+ "header",
371
+ tw("border-b border-white/10 px-5 py-4"),
372
+ tag("p", tw("text-xs font-medium text-violet-300"), "WORKSPACE"),
373
+ tag(
374
+ "h1", tw("mt-1 text-xl font-bold tracking-tight"), "PyJS Community"
375
+ ),
376
+ ),
377
+ tag(
378
+ "div",
379
+ tw("flex items-center justify-between px-4 pb-2 pt-5"),
380
+ tag(
381
+ "h2",
382
+ tw(
383
+ "text-xs font-semibold uppercase tracking-wider text-violet-300"
384
+ ),
385
+ "Rooms",
386
+ ),
387
+ tag("span", tw("text-violet-300"), "+"),
388
+ ),
389
+ self.room_form,
390
+ self.room_list,
391
+ tag(
392
+ "footer",
393
+ tw(
394
+ "flex items-center justify-between border-t border-white/10 "
395
+ "px-4 py-3 text-sm"
396
+ ),
397
+ tag(
398
+ "div",
399
+ tw("flex min-w-0 items-center gap-2"),
400
+ tag("span", tw("h-2.5 w-2.5 shrink-0 rounded-full bg-emerald-400")),
401
+ self.profile_name,
402
+ ),
403
+ self.sign_out_button,
404
+ ),
405
+ ),
406
+ tag(
407
+ "main",
408
+ tw("flex min-h-0 min-w-0 flex-col bg-white"),
409
+ tag(
410
+ "header",
411
+ tw(
412
+ "flex h-[4.5rem] shrink-0 items-center justify-between border-b "
413
+ "border-slate-200 px-5 shadow-sm"
414
+ ),
415
+ tag("div", self.current_room, self.connection_status),
416
+ self.reconnect_button,
417
+ ),
418
+ self.error_banner,
419
+ self.message_list,
420
+ tag("div", tw("shrink-0 border-t border-slate-100 p-4"), self.message_form),
421
+ ),
422
+ tag(
423
+ "aside",
424
+ tw("hidden min-h-0 flex-col border-l border-slate-200 bg-slate-50 lg:flex"),
425
+ tag(
426
+ "header",
427
+ tw(
428
+ "flex h-[4.5rem] shrink-0 items-center justify-between "
429
+ "border-b border-slate-200 px-4"
430
+ ),
431
+ tag("h2", tw("font-bold text-slate-900"), "People"),
432
+ self.member_count,
433
+ ),
434
+ self.member_list,
435
+ ),
436
+ ),
437
+ )
438
+ )
439
+ self.add(self.workspace, self.sign_in)
440
+
441
+ def initialize(self):
442
+ self.socket: WebSocket | None = None
443
+ name = sessionStorage.getItem("pyjs-chatroom-display-name")
444
+ if name is None:
445
+ self.show_sign_in()
446
+ return
447
+ if name == "":
448
+ self.show_sign_in()
449
+ return
450
+ self.name = name
451
+ self.show_workspace()
452
+ self.connect()
453
+
454
+ @effect(name)
455
+ def render_name(self):
456
+ self.profile_name.textContent = self.name if self.name != "" else "Guest"
457
+
458
+ @effect(room)
459
+ def render_room(self):
460
+ self.current_room.textContent = "# " + self.room
461
+ self.message_input.setAttribute("placeholder", "Message #" + self.room)
462
+
463
+ @effect(connection)
464
+ def render_connection(self):
465
+ if self.connection == "connecting":
466
+ self.connection_status.textContent = "Connecting…"
467
+ elif self.connection == "connected":
468
+ self.connection_status.textContent = "Connected"
469
+ elif self.connection == "disconnected":
470
+ self.connection_status.textContent = "Disconnected"
471
+ elif self.connection == "preview":
472
+ self.connection_status.textContent = "Preview mode"
473
+ else:
474
+ self.connection_status.textContent = "Not connected"
475
+ if self.connection == "disconnected":
476
+ self.reconnect_button.classList.remove("hidden")
477
+ else:
478
+ self.reconnect_button.classList.add("hidden")
479
+
480
+ def show_sign_in(self):
481
+ self.workspace.classList.add("hidden")
482
+ self.sign_in.classList.remove("hidden")
483
+ self.connection = "signed-out"
484
+ self.name_input.focus()
485
+
486
+ def show_workspace(self):
487
+ self.sign_in.classList.add("hidden")
488
+ self.workspace.classList.remove("hidden")
489
+
490
+ def submit_name(self):
491
+ name = self.name_input.value.strip()
492
+ if name == "":
493
+ self.show_error("Enter a display name to join the workspace.")
494
+ return
495
+ sessionStorage.setItem("pyjs-chatroom-display-name", name)
496
+ self.name = name
497
+ self.show_workspace()
498
+ self.connect()
499
+
500
+ def sign_out(self):
501
+ sessionStorage.removeItem("pyjs-chatroom-display-name")
502
+ socket = self.socket
503
+ if self.connection == "connected":
504
+ if socket is not None:
505
+ socket.close()
506
+ self.name = ""
507
+ self.name_input.value = ""
508
+ self.show_sign_in()
509
+
510
+ def connect(self):
511
+ if self.socket_path == "":
512
+ self.connection = "preview"
513
+ return
514
+ self.connection = "connecting"
515
+ socket = WebSocket("ws://" + location.host + self.socket_path)
516
+ self.socket = socket
517
+ socket.addEventListener("open", SocketOpenListener(self))
518
+ socket.addEventListener("message", SocketMessageListener(self))
519
+ socket.addEventListener("close", SocketCloseListener(self))
520
+ socket.addEventListener("error", SocketErrorListener(self))
521
+
522
+ def socket_opened(self):
523
+ self.connection = "connected"
524
+ self.send(OutgoingChatMessage("join", self.name, self.room, ""))
525
+
526
+ def socket_closed(self):
527
+ if self.connection == "signed-out":
528
+ return
529
+ self.connection = "disconnected"
530
+
531
+ def send(self, message: OutgoingChatMessage):
532
+ socket = self.socket
533
+ if self.connection == "connected":
534
+ if socket is not None:
535
+ socket.send(JSON.stringify(message))
536
+
537
+ def receive(self, message: ChatSnapshot):
538
+ if message.type == "error":
539
+ self.show_error(message.message)
540
+ return
541
+ self.hide_error()
542
+ self.room = message.room
543
+ self.render_rooms(message.rooms)
544
+ self.render_messages(message.messages)
545
+ self.render_members(message.users)
546
+
547
+ def render_rooms(self, rooms: list[str]):
548
+ self.room_list.replaceChildren()
549
+ current_room = self.room
550
+ for room in rooms:
551
+ if room == current_room:
552
+ room_button = button(
553
+ tw(
554
+ "flex w-full items-center gap-2 rounded-lg bg-white/15 px-3 py-2 "
555
+ "text-left text-sm font-semibold text-white"
556
+ ),
557
+ {"data-room": room, "type": "button"},
558
+ tag("span", tw("text-violet-300"), "#"),
559
+ room,
560
+ )
561
+ else:
562
+ room_button = button(
563
+ tw(
564
+ "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm "
565
+ "font-medium text-violet-100 hover:bg-white/10 hover:text-white"
566
+ ),
567
+ {"data-room": room, "type": "button"},
568
+ tag("span", tw("text-violet-400"), "#"),
569
+ room,
570
+ )
571
+ room_button.addEventListener("click", RoomClickListener(self, room))
572
+ self.room_list.append(room_button)
573
+
574
+ def render_messages(self, messages: list[ChatMessage]):
575
+ self.message_list.replaceChildren()
576
+ if len(messages) == 0:
577
+ self.message_list.append(
578
+ tag(
579
+ "div",
580
+ tw("flex h-full min-h-64 flex-col items-center justify-center text-center"),
581
+ tag(
582
+ "div",
583
+ tw(
584
+ "flex h-14 w-14 items-center justify-center rounded-2xl bg-violet-100 "
585
+ "text-2xl font-bold text-violet-700"
586
+ ),
587
+ "#",
588
+ ),
589
+ tag(
590
+ "h3",
591
+ tw("mt-4 text-lg font-bold text-slate-900"),
592
+ "This is the start of #" + self.room,
593
+ ),
594
+ tag(
595
+ "p",
596
+ tw("mt-1 max-w-sm text-sm text-slate-500"),
597
+ "Say hello and start the conversation.",
598
+ ),
599
+ )
600
+ )
601
+ return
602
+ for message in messages:
603
+ self.message_list.append(
604
+ tag(
605
+ "article",
606
+ tw("group flex gap-3 rounded-lg px-2 py-2 hover:bg-slate-50"),
607
+ {"data-message-id": str(message.id)},
608
+ tag(
609
+ "div",
610
+ tw(
611
+ "mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg "
612
+ "bg-violet-100 text-sm font-bold text-violet-700"
613
+ ),
614
+ "●",
615
+ ),
616
+ tag(
617
+ "div",
618
+ tw("min-w-0 flex-1"),
619
+ tag(
620
+ "div",
621
+ tw("flex items-baseline gap-2"),
622
+ tag("strong", tw("text-sm font-bold text-slate-900"), message.sender),
623
+ tag("time", tw("text-[0.7rem] text-slate-400"), message.time),
624
+ ),
625
+ tag(
626
+ "p",
627
+ tw(
628
+ "mt-0.5 whitespace-pre-wrap break-words text-sm leading-6 "
629
+ "text-slate-700"
630
+ ),
631
+ message.text,
632
+ ),
633
+ ),
634
+ )
635
+ )
636
+ self.message_list.scrollTop = self.message_list.scrollHeight
637
+
638
+ def render_members(self, users: list[str]):
639
+ self.member_list.replaceChildren()
640
+ count = len(users)
641
+ self.member_count.textContent = str(count) + (" person" if count == 1 else " people")
642
+ for user in users:
643
+ self.member_list.append(
644
+ tag(
645
+ "div",
646
+ tw("flex items-center gap-3 rounded-lg px-2 py-2 text-sm hover:bg-white"),
647
+ {"data-user": user},
648
+ tag("span", tw("h-2.5 w-2.5 rounded-full bg-emerald-500")),
649
+ tag("span", tw("truncate font-medium text-slate-700"), user),
650
+ )
651
+ )
652
+
653
+ def switch_room(self, room: str):
654
+ self.send(OutgoingChatMessage("switch_room", self.name, room, ""))
655
+
656
+ def create_room(self):
657
+ room = self.room_input.value.strip()
658
+ if room == "":
659
+ return
660
+ self.room_input.value = ""
661
+ self.send(OutgoingChatMessage("create_room", self.name, room, ""))
662
+
663
+ def submit_message(self):
664
+ text = self.message_input.value.strip()
665
+ if text == "":
666
+ return
667
+ self.message_input.value = ""
668
+ self.send(OutgoingChatMessage("message", self.name, self.room, text))
669
+
670
+ def show_error(self, message: str):
671
+ self.error_banner.textContent = message
672
+ self.error_banner.classList.remove("hidden")
673
+
674
+ def hide_error(self):
675
+ self.error_banner.classList.add("hidden")
676
+
677
+
678
+ def main(socket_path: str = ""):
679
+ return ChatRoomApp(socket_path=socket_path)
680
+
681
+
682
+ @nojs
683
+ def render(request):
684
+ return main(socket_path="/ws")
685
+
686
+
687
+ @nojs
688
+ async def websocket(connection):
689
+ from .chat_server import chat_server
690
+
691
+ await chat_server.handle(connection)
692
+
693
+
694
+ if __name__ == "__main__":
695
+ import os
696
+
697
+ from pyjs.server import serve
698
+
699
+ serve(
700
+ "chatroom",
701
+ render=render,
702
+ websocket_routes={"/ws": websocket},
703
+ port=int(os.environ.get("PYJS_PORT", "8000")),
704
+ )