parthmax24 commited on
Commit
91db40f
·
1 Parent(s): b3a9c0e

updated the ui

Browse files
Files changed (4) hide show
  1. app.py +26 -4
  2. static/main.css +1390 -768
  3. static/main.js +351 -391
  4. templates/index.html +116 -61
app.py CHANGED
@@ -41,6 +41,10 @@ class ConnectionManager:
41
  "joined_at": datetime.now().isoformat()
42
  }
43
  self.active_connections[room].append(connection_info)
 
 
 
 
44
 
45
  # Send join notification
46
  join_message = {
@@ -50,10 +54,7 @@ class ConnectionManager:
50
  "room": room
51
  }
52
  await self.broadcast_to_room(room, join_message)
53
-
54
- # Send message history to new user
55
- for message in self.message_history[room]:
56
- await websocket.send_text(json.dumps(message))
57
 
58
  def disconnect(self, websocket: WebSocket, room: str):
59
  if room in self.active_connections:
@@ -86,6 +87,26 @@ class ConnectionManager:
86
  # Remove disconnected clients
87
  for conn in disconnected:
88
  self.active_connections[room].remove(conn)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  def get_room_users(self, room: str) -> List[str]:
91
  if room not in self.active_connections:
@@ -203,6 +224,7 @@ async def websocket_endpoint(websocket: WebSocket, room: str, username: str):
203
  "room": room
204
  }
205
  await manager.broadcast_to_room(room, leave_message)
 
206
 
207
  @app.get("/api/rooms")
208
  async def get_active_rooms():
 
41
  "joined_at": datetime.now().isoformat()
42
  }
43
  self.active_connections[room].append(connection_info)
44
+
45
+ # Send message history to new user before broadcasting the fresh join event
46
+ for message in self.message_history[room]:
47
+ await websocket.send_text(json.dumps(message))
48
 
49
  # Send join notification
50
  join_message = {
 
54
  "room": room
55
  }
56
  await self.broadcast_to_room(room, join_message)
57
+ await self.broadcast_user_list(room)
 
 
 
58
 
59
  def disconnect(self, websocket: WebSocket, room: str):
60
  if room in self.active_connections:
 
87
  # Remove disconnected clients
88
  for conn in disconnected:
89
  self.active_connections[room].remove(conn)
90
+
91
+ async def broadcast_user_list(self, room: str):
92
+ if room not in self.active_connections:
93
+ return
94
+
95
+ users_message = {
96
+ "type": "user_list",
97
+ "users": self.get_room_users(room),
98
+ "room": room
99
+ }
100
+
101
+ disconnected = []
102
+ for connection_info in self.active_connections[room]:
103
+ try:
104
+ await connection_info["websocket"].send_text(json.dumps(users_message))
105
+ except:
106
+ disconnected.append(connection_info)
107
+
108
+ for conn in disconnected:
109
+ self.active_connections[room].remove(conn)
110
 
111
  def get_room_users(self, room: str) -> List[str]:
112
  if room not in self.active_connections:
 
224
  "room": room
225
  }
226
  await manager.broadcast_to_room(room, leave_message)
227
+ await manager.broadcast_user_list(room)
228
 
229
  @app.get("/api/rooms")
230
  async def get_active_rooms():
static/main.css CHANGED
@@ -1,908 +1,1530 @@
1
- :root {
2
- --primary-bg: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
3
- --secondary-bg: rgba(255, 255, 255, 0.8);
4
- --card-bg: rgba(255, 255, 255, 0.9);
5
- --text-primary: #1a202c;
6
- --text-secondary: #4a5568;
7
- --text-muted: #718096;
8
- --border-color: rgba(0, 0, 0, 0.08);
9
- --accent-primary: #3b82f6;
10
- --accent-secondary: #60a5fa;
11
- --accent-success: #10b981;
12
- --accent-danger: #ef4444;
13
- --shadow-light: 0 1px 3px rgba(0, 0, 0, 0.05);
14
- --shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.1);
15
- --shadow-heavy: 0 8px 24px rgba(0, 0, 0, 0.12);
16
- --blur-light: blur(10px);
17
- --blur-medium: blur(20px);
18
- }
19
-
20
- * {
21
- margin: 0;
22
- padding: 0;
23
- box-sizing: border-box;
24
- }
25
-
26
- body {
27
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
28
- background: var(--primary-bg);
29
- color: var(--text-primary);
30
- height: 100vh;
31
- display: flex;
32
- flex-direction: column;
33
- font-size: 14px;
34
- line-height: 1.5;
35
- transition: all 0.3s ease;
36
- }
37
-
38
- .header {
39
- background: var(--card-bg);
40
- backdrop-filter: var(--blur-medium);
41
- padding: 1.5rem;
42
- border-bottom: 1px solid var(--border-color);
43
- box-shadow: var(--shadow-light);
44
- position: relative;
45
- }
46
-
47
- .header::before {
48
- content: '';
49
- position: absolute;
50
- top: 0;
51
- left: 0;
52
- right: 0;
53
- height: 3px;
54
- background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
55
- }
56
-
57
- .header-top {
58
- display: flex;
59
- justify-content: space-between;
60
- align-items: center;
61
- margin-bottom: 1.5rem;
62
- }
63
-
64
- .logo {
65
- display: flex;
66
- align-items: center;
67
- gap: 0.75rem;
68
- }
69
-
70
- .logo i {
71
- font-size: 1.5rem;
72
- color: var(--accent-primary);
73
- }
74
-
75
- .header h1 {
76
- color: var(--text-primary);
77
- font-size: 1.5rem;
78
- font-weight: 700;
79
- letter-spacing: -0.025em;
80
- }
81
-
82
- .connection-form {
83
- display: flex;
84
- gap: 1rem;
85
- align-items: center;
86
- flex-wrap: wrap;
87
- }
88
-
89
- .input-group {
90
- display: flex;
91
- flex-direction: column;
92
- gap: 0.25rem;
93
- min-width: 180px;
94
- }
95
-
96
- .input-group label {
97
- font-size: 0.75rem;
98
- font-weight: 500;
99
- color: var(--text-muted);
100
- text-transform: uppercase;
101
- letter-spacing: 0.05em;
102
- }
103
-
104
- .connection-form input {
105
- padding: 0.75rem 1rem;
106
- border: 1px solid var(--border-color);
107
- border-radius: 12px;
108
- background: var(--secondary-bg);
109
- backdrop-filter: var(--blur-light);
110
- color: var(--text-primary);
111
- font-size: 0.875rem;
112
- transition: all 0.2s ease;
113
- outline: none;
114
- }
115
-
116
- .connection-form input:focus {
117
- border-color: var(--accent-primary);
118
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
119
- }
120
-
121
- .connection-form input::placeholder {
122
- color: var(--text-muted);
123
- }
124
-
125
- .btn {
126
- padding: 0.75rem 1.5rem;
127
- border: none;
128
- border-radius: 12px;
129
- background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
130
- color: white;
131
- cursor: pointer;
132
- font-weight: 600;
133
- font-size: 0.875rem;
134
- transition: all 0.2s ease;
135
- position: relative;
136
- overflow: hidden;
137
- display: flex;
138
- align-items: center;
139
- gap: 0.5rem;
140
- box-shadow: var(--shadow-light);
141
- }
142
-
143
- .btn::before {
144
- content: '';
145
- position: absolute;
146
- top: 0;
147
- left: -100%;
148
- width: 100%;
149
- height: 100%;
150
- background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
151
- transition: left 0.5s;
152
- }
153
-
154
- .btn:hover::before {
155
- left: 100%;
156
- }
157
-
158
- .btn:hover {
159
- transform: translateY(-2px);
160
- box-shadow: var(--shadow-medium);
161
- }
162
-
163
- .btn:active {
164
- transform: translateY(0);
165
- }
166
-
167
- .btn:disabled {
168
- background: var(--text-muted);
169
- cursor: not-allowed;
170
- transform: none;
171
- opacity: 0.6;
172
- }
173
-
174
- .btn-secondary {
175
- background: transparent;
176
- border: 1px solid var(--border-color);
177
- color: var(--text-secondary);
178
- }
179
-
180
- .btn-secondary:hover {
181
- background: var(--secondary-bg);
182
  }
183
 
184
  .is-hidden {
185
  display: none !important;
186
  }
187
-
188
- .main-content {
189
- display: flex;
190
- flex: 1;
191
- overflow: hidden;
192
- gap: 1px;
193
- }
194
-
195
- .sidebar {
196
- width: 280px;
197
- background: var(--card-bg);
198
- backdrop-filter: var(--blur-light);
199
- border-right: 1px solid var(--border-color);
200
- display: flex;
201
- flex-direction: column;
202
- transition: all 0.3s ease;
203
- }
204
-
205
- .sidebar-header {
206
- padding: 1.5rem;
207
- border-bottom: 1px solid var(--border-color);
208
- }
209
-
210
- .sidebar-header h3 {
211
- color: var(--text-primary);
212
- font-size: 1rem;
213
- font-weight: 600;
214
- display: flex;
215
- align-items: center;
216
- gap: 0.5rem;
217
- }
218
-
219
- .sidebar-header i {
220
- color: var(--accent-success);
221
- }
222
-
223
- .sidebar-content {
224
- flex: 1;
225
- padding: 1rem;
226
- overflow-y: auto;
227
- }
228
-
229
- .user-list {
230
- list-style: none;
231
- display: flex;
232
- flex-direction: column;
233
- gap: 0.5rem;
234
- }
235
-
236
- .user-list li {
237
- padding: 0.75rem 1rem;
238
- color: var(--text-secondary);
239
- background: var(--secondary-bg);
240
- border-radius: 8px;
241
- transition: all 0.2s ease;
242
- display: flex;
243
- align-items: center;
244
- gap: 0.75rem;
245
- }
246
-
247
- .user-list li:hover {
248
- background: var(--card-bg);
249
- transform: translateX(2px);
250
- }
251
-
252
- .user-list li::before {
253
- content: '';
254
- width: 8px;
255
- height: 8px;
256
- border-radius: 50%;
257
- background: var(--accent-success);
258
- flex-shrink: 0;
259
- }
260
-
261
- .chat-container {
262
- flex: 1;
263
- display: flex;
264
- flex-direction: column;
265
- background: var(--secondary-bg);
266
- backdrop-filter: var(--blur-light);
267
- }
268
-
269
- .messages {
270
- flex: 1;
271
- overflow-y: auto;
272
- padding: 1.5rem;
273
- display: flex;
274
- flex-direction: column;
275
- gap: 1rem;
276
- scroll-behavior: smooth;
277
- }
278
-
279
- .messages::-webkit-scrollbar {
280
- width: 6px;
281
- }
282
-
283
- .messages::-webkit-scrollbar-track {
284
- background: transparent;
285
- }
286
-
287
- .messages::-webkit-scrollbar-thumb {
288
- background: var(--border-color);
289
- border-radius: 3px;
290
- }
291
-
292
- .messages::-webkit-scrollbar-thumb:hover {
293
- background: var(--text-muted);
294
- }
295
-
296
- .message {
297
- background: var(--card-bg);
298
- backdrop-filter: var(--blur-light);
299
- border-radius: 16px;
300
- padding: 1rem 1.25rem;
301
- max-width: 75%;
302
- border: 1px solid var(--border-color);
303
- box-shadow: var(--shadow-light);
304
- position: relative;
305
- animation: messageSlide 0.3s ease-out;
306
- }
307
-
308
- @keyframes messageSlide {
309
- from {
310
- opacity: 0;
311
- transform: translateY(10px);
312
- }
313
- to {
314
- opacity: 1;
315
- transform: translateY(0);
316
- }
317
- }
318
-
319
- .message.own {
320
- align-self: flex-end;
321
- background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
322
- color: white;
323
- border: none;
324
- }
325
-
326
- .message.own .username,
327
- .message.own .timestamp {
328
- color: rgba(255, 255, 255, 0.9);
329
- }
330
-
331
- .message.system {
332
- align-self: center;
333
- background: var(--secondary-bg);
334
- border: 1px solid var(--border-color);
335
- font-style: italic;
336
- color: var(--text-muted);
337
- max-width: 60%;
338
- text-align: center;
339
- font-size: 0.8rem;
340
- }
341
-
342
- .message-header {
343
- display: flex;
344
- justify-content: space-between;
345
- align-items: center;
346
- margin-bottom: 0.5rem;
347
- font-size: 0.75rem;
348
- }
349
-
350
- .username {
351
- font-weight: 600;
352
- color: var(--accent-primary);
353
- }
354
-
355
- .message.own .username {
356
- color: rgba(255, 255, 255, 0.9);
357
- }
358
-
359
- .timestamp {
360
- color: var(--text-muted);
361
- font-size: 0.7rem;
362
- }
363
-
364
- .message-content {
365
- word-wrap: break-word;
366
- line-height: 1.4;
367
- }
368
-
369
- .file-preview img {
370
- max-width: 200px;
371
- max-height: 150px;
372
- border-radius: 8px;
373
- margin-top: 0.75rem;
374
- box-shadow: var(--shadow-light);
375
- }
376
-
377
- .file-download {
378
- display: inline-flex;
379
- align-items: center;
380
- gap: 0.5rem;
381
- margin-top: 0.75rem;
382
- padding: 0.5rem 1rem;
383
- background: var(--secondary-bg);
384
- border-radius: 8px;
385
- color: var(--accent-primary);
386
- text-decoration: none;
387
- font-size: 0.8rem;
388
- transition: all 0.2s ease;
389
- border: 1px solid var(--border-color);
390
- }
391
-
392
- .file-download:hover {
393
  background: var(--card-bg);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  transform: translateY(-1px);
395
- box-shadow: var(--shadow-light);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  }
397
 
398
  .file-summary {
399
  display: flex;
400
  align-items: center;
401
  gap: 0.5rem;
402
- margin-bottom: 0.5rem;
403
  }
404
 
405
  .file-size {
 
406
  color: var(--text-muted);
407
- font-size: 0.8rem;
408
- margin-bottom: 0.5rem;
409
- }
410
-
411
- .input-area {
412
- padding: 1.5rem;
413
- background: var(--card-bg);
414
- backdrop-filter: var(--blur-medium);
415
- border-top: 1px solid var(--border-color);
416
- }
417
-
418
- .input-row {
419
- display: flex;
420
- gap: 1rem;
421
- align-items: flex-end;
422
- }
423
-
424
- #messageInput {
425
- flex: 1;
426
- padding: 1rem 1.25rem;
427
- border: 1px solid var(--border-color);
428
- border-radius: 20px;
429
- background: var(--secondary-bg);
430
- backdrop-filter: var(--blur-light);
431
- color: var(--text-primary);
432
- resize: none;
433
- outline: none;
434
- transition: all 0.2s ease;
435
- font-family: inherit;
436
- max-height: 100px;
437
- min-height: 44px;
438
- }
439
-
440
- #messageInput:focus {
441
- border-color: var(--accent-primary);
442
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
443
- }
444
-
445
- #messageInput::placeholder {
446
- color: var(--text-muted);
447
- }
448
-
449
- .file-input-wrapper {
450
- position: relative;
451
- }
452
-
453
- .file-input-wrapper input[type=file] {
454
- position: absolute;
455
- opacity: 0;
456
- width: 100%;
457
- height: 100%;
458
- cursor: pointer;
459
- }
460
-
461
- .file-input-label {
462
- display: flex;
463
- align-items: center;
464
- justify-content: center;
465
- width: 44px;
466
- height: 44px;
467
- background: var(--secondary-bg);
468
- border: 1px solid var(--border-color);
469
- border-radius: 50%;
470
- cursor: pointer;
471
- transition: all 0.2s ease;
472
- color: var(--text-secondary);
473
- }
474
-
475
- .file-input-label:hover {
476
- background: var(--accent-primary);
477
- color: white;
478
- transform: translateY(-2px);
479
- box-shadow: var(--shadow-light);
480
- }
481
-
482
- .status {
483
- padding: 0.75rem 1rem;
484
- text-align: center;
485
- font-size: 0.8rem;
486
- border-radius: 8px;
487
- margin-top: 1rem;
488
- font-weight: 500;
489
- }
490
-
491
- .status.connected {
492
- color: var(--accent-success);
493
- background: rgba(16, 185, 129, 0.1);
494
- border: 1px solid rgba(16, 185, 129, 0.2);
495
- }
496
-
497
- .status.disconnected {
498
- color: var(--accent-danger);
499
- background: rgba(239, 68, 68, 0.1);
500
- border: 1px solid rgba(239, 68, 68, 0.2);
501
- }
502
-
503
- .status.neutral {
504
- color: var(--text-muted);
505
- background: var(--secondary-bg);
506
- border: 1px solid var(--border-color);
507
- }
508
-
509
- /* Mobile optimizations */
510
- @media (max-width: 768px) {
511
- .header {
512
- padding: 1rem;
513
- }
514
-
515
- .header-top {
516
- margin-bottom: 1rem;
517
- }
518
-
519
- .header h1 {
520
- font-size: 1.25rem;
521
- }
522
-
523
- .connection-form {
524
- flex-direction: column;
525
- gap: 0.75rem;
526
- width: 100%;
527
- }
528
-
529
- .input-group {
530
- width: 100%;
531
- min-width: auto;
532
- }
533
-
534
- .connection-form input,
535
- .btn {
536
- width: 100%;
537
- }
538
-
539
- .main-content {
540
- flex-direction: column;
541
- height: calc(100vh - 200px);
542
- }
543
-
544
- .sidebar {
545
- width: 100%;
546
- height: 120px;
547
- flex-direction: row;
548
- border-right: none;
549
- border-bottom: 1px solid var(--border-color);
550
- }
551
-
552
- .sidebar-header {
553
- min-width: 120px;
554
- border-right: 1px solid var(--border-color);
555
- border-bottom: none;
556
- }
557
-
558
- .sidebar-header h3 {
559
- font-size: 0.9rem;
560
- }
561
-
562
- .sidebar-content {
563
- overflow-x: auto;
564
- overflow-y: hidden;
565
- }
566
-
567
- .user-list {
568
- flex-direction: row;
569
- white-space: nowrap;
570
- padding-bottom: 0.5rem;
571
- }
572
-
573
- .user-list li {
574
- flex-shrink: 0;
575
- font-size: 0.8rem;
576
- padding: 0.5rem 0.75rem;
577
- }
578
-
579
- .message {
580
- max-width: 90%;
581
- padding: 0.75rem 1rem;
582
- }
583
-
584
- .messages {
585
- padding: 1rem;
586
- }
587
-
588
- .input-area {
589
- padding: 1rem;
590
- }
591
-
592
- .input-row {
593
- gap: 0.75rem;
594
- }
595
- }
596
-
597
- @media (max-width: 480px) {
598
- .header {
599
- padding: 0.75rem;
600
- }
601
-
602
- .messages {
603
- padding: 0.75rem;
604
- gap: 0.75rem;
605
- }
606
-
607
- .message {
608
- padding: 0.75rem;
609
- border-radius: 12px;
610
- }
611
-
612
- .input-area {
613
- padding: 0.75rem;
614
- }
615
- }
616
-
617
- * {
618
- transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
619
- }
620
 
621
- :root {
622
- --mobile-safe-bottom: env(safe-area-inset-bottom, 0px);
623
  }
624
 
625
- html,
626
- body {
627
- min-height: 100%;
628
- overflow: hidden;
 
 
629
  }
630
 
631
- button,
632
- input,
633
- textarea {
634
- font: inherit;
 
 
 
 
 
 
 
 
 
 
 
635
  }
636
 
637
- .mobile-users-toggle {
638
- display: none;
 
 
 
 
 
 
 
 
 
 
639
  }
640
 
641
- .mobile-drawer-backdrop {
642
- display: none;
643
  }
644
 
645
- @media (max-width: 768px) {
646
- body {
647
- height: 100dvh;
648
- min-height: 100dvh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
  }
650
 
651
- .header {
652
- z-index: 20;
653
- padding: 0.85rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  }
 
655
 
656
- .header-top {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
  gap: 0.75rem;
658
- margin-bottom: 0.75rem;
659
  }
660
 
661
- .logo {
662
- min-width: 0;
 
663
  }
664
 
665
- .header h1 {
666
- overflow: hidden;
667
- text-overflow: ellipsis;
668
- white-space: nowrap;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
  }
670
 
671
- .mobile-users-toggle {
 
 
 
 
 
672
  width: 42px;
673
  height: 42px;
674
- border-radius: 50%;
675
- flex: 0 0 auto;
676
- display: inline-flex;
677
- align-items: center;
678
- justify-content: center;
679
  }
680
 
681
- .mobile-users-toggle {
682
- background: var(--secondary-bg);
683
- border: 1px solid var(--border-color);
684
- color: var(--text-secondary);
685
- cursor: pointer;
686
- transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.3s ease;
687
  }
688
 
689
- .mobile-users-toggle:active {
690
- transform: scale(0.96);
 
 
691
  }
692
 
693
- .connection-form {
694
- display: grid;
695
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
696
- gap: 0.65rem;
697
- align-items: end;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698
  }
699
 
700
- .connection-form .btn {
 
 
 
 
 
 
 
 
 
 
 
701
  min-height: 44px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  justify-content: center;
703
- grid-column: span 2;
 
 
 
 
 
704
  }
705
 
706
- .connection-form input {
707
- min-height: 44px;
708
- font-size: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
  }
710
 
711
  .status {
712
- margin-top: 0.75rem;
713
- padding: 0.6rem 0.75rem;
714
- text-align: left;
 
 
 
 
 
715
  }
716
 
717
- .main-content {
718
- height: auto;
719
- min-height: 0;
720
- flex: 1;
721
- position: relative;
 
 
 
722
  }
723
 
724
- .chat-container {
725
- min-width: 0;
726
- min-height: 0;
727
  }
728
 
729
- .messages {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
730
  min-height: 0;
731
- padding: 0.85rem;
732
- padding-bottom: 1rem;
733
  }
734
 
735
- .message,
736
- .message.system {
737
- max-width: min(92%, 34rem);
738
- border-radius: 14px;
739
  }
740
 
741
- .message-header {
742
- gap: 0.75rem;
743
  }
744
 
745
- .message-content {
746
- overflow-wrap: anywhere;
747
  }
748
 
749
- .file-preview img {
750
- max-width: 100%;
751
- height: auto;
752
  }
753
 
754
- .sidebar {
755
  position: fixed;
756
  top: 0;
757
  right: 0;
758
  bottom: 0;
759
  z-index: 40;
760
  width: min(84vw, 320px);
761
- height: auto;
762
- flex-direction: column;
763
- transform: translateX(105%);
764
  border-left: 1px solid var(--border-color);
765
  border-right: 0;
766
- border-bottom: 0;
767
  box-shadow: var(--shadow-heavy);
768
- transition: transform 0.25s ease;
 
769
  }
770
 
771
- body.mobile-users-open .sidebar {
772
  transform: translateX(0);
773
  }
774
 
775
- .sidebar-header {
776
- min-width: 0;
777
- padding: 1rem;
778
- border-right: 0;
779
- border-bottom: 1px solid var(--border-color);
780
- }
781
-
782
- .sidebar-content {
783
- overflow-y: auto;
784
- overflow-x: hidden;
785
- padding: 0.85rem;
786
- }
787
-
788
- .user-list {
789
- flex-direction: column;
790
- white-space: normal;
791
- padding-bottom: 0;
792
- }
793
-
794
- .user-list li {
795
- width: 100%;
796
- min-height: 42px;
797
  }
798
 
799
  .mobile-drawer-backdrop {
800
  position: fixed;
801
  inset: 0;
802
  z-index: 35;
803
- display: block;
804
  pointer-events: none;
805
  background: rgba(15, 23, 42, 0);
806
- transition: background-color 0.25s ease;
807
  }
808
 
809
- body.mobile-users-open .mobile-drawer-backdrop {
810
  pointer-events: auto;
811
- background: rgba(15, 23, 42, 0.42);
812
  }
813
 
814
  .input-area {
 
 
815
  padding: 0.75rem;
816
- padding-bottom: calc(0.75rem + var(--mobile-safe-bottom));
817
  }
818
 
819
- .input-row {
820
- display: grid;
821
- grid-template-columns: 44px minmax(0, 1fr) 48px;
822
- gap: 0.55rem;
823
- align-items: end;
824
  }
825
 
826
- #messageInput {
827
- min-height: 44px;
828
- max-height: 132px;
829
- padding: 0.78rem 0.95rem;
830
- border-radius: 18px;
831
- font-size: 16px;
832
  }
 
833
 
834
- #sendBtn {
835
- width: 48px;
836
- height: 44px;
837
- min-width: 48px;
838
- padding: 0;
839
- border-radius: 50%;
840
- justify-content: center;
841
  }
842
 
843
- #sendBtn span {
844
- display: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
845
  }
846
  }
847
 
848
- @media (max-width: 480px) {
849
- .header {
850
- padding: 0.7rem;
 
851
  }
852
 
853
- .connection-form {
854
- grid-template-columns: 1fr;
 
 
 
855
  }
856
 
857
- .connection-form .btn {
858
- grid-column: auto;
859
  }
860
 
861
- .input-group label {
862
- font-size: 0.68rem;
863
  }
864
 
865
- .status {
866
- font-size: 0.75rem;
867
  }
868
 
869
- .messages {
870
- padding: 0.7rem;
871
  }
872
 
873
- .message {
874
- max-width: 94%;
875
- padding: 0.72rem 0.82rem;
876
  }
877
 
878
- .message.system {
879
- max-width: 92%;
880
  }
881
 
882
- .file-download {
883
- width: 100%;
884
- justify-content: center;
885
  }
886
- }
887
 
888
- @media (max-width: 360px) {
889
- .logo i {
890
- display: none;
891
  }
892
 
893
- .mobile-users-toggle {
894
- width: 40px;
895
- height: 40px;
 
896
  }
897
 
898
- .input-row {
899
- grid-template-columns: 40px minmax(0, 1fr) 44px;
 
900
  }
 
901
 
902
- .file-input-label,
903
- #sendBtn {
904
- width: 40px;
905
- min-width: 40px;
 
 
 
906
  }
907
  }
908
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-bg:
3
+ radial-gradient(circle at 16% 12%, rgba(96, 165, 250, 0.2), transparent 28%),
4
+ radial-gradient(circle at 84% 18%, rgba(59, 130, 246, 0.16), transparent 30%),
5
+ radial-gradient(circle at 45% 92%, rgba(16, 185, 129, 0.1), transparent 28%),
6
+ linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
7
+ --secondary-bg: rgba(255, 255, 255, 0.66);
8
+ --card-bg: rgba(255, 255, 255, 0.74);
9
+ --glass-strong: rgba(255, 255, 255, 0.82);
10
+ --glass-soft: rgba(255, 255, 255, 0.48);
11
+ --text-primary: #1a202c;
12
+ --text-secondary: #4a5568;
13
+ --text-muted: #718096;
14
+ --border-color: rgba(148, 163, 184, 0.22);
15
+ --glass-border: rgba(255, 255, 255, 0.76);
16
+ --accent-primary: #3b82f6;
17
+ --accent-secondary: #60a5fa;
18
+ --accent-success: #10b981;
19
+ --accent-danger: #ef4444;
20
+ --shadow-light: 0 10px 24px rgba(15, 23, 42, 0.08);
21
+ --shadow-medium: 0 18px 38px rgba(59, 130, 246, 0.2), 0 6px 16px rgba(15, 23, 42, 0.08);
22
+ --shadow-heavy: 0 34px 90px rgba(15, 23, 42, 0.16), 0 12px 28px rgba(59, 130, 246, 0.1);
23
+ --inner-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.92);
24
+ --inner-depth: inset 0 -18px 42px rgba(96, 165, 250, 0.05);
25
+ --blur-light: blur(14px);
26
+ --blur-medium: blur(26px);
27
+ --safe-bottom: env(safe-area-inset-bottom, 0px);
28
+ }
29
+
30
+ * {
31
+ box-sizing: border-box;
32
+ margin: 0;
33
+ padding: 0;
34
+ }
35
+
36
+ html,
37
+ body {
38
+ min-height: 100%;
39
+ overflow: hidden;
40
+ }
41
+
42
+ body {
43
+ position: relative;
44
+ min-height: 100dvh;
45
+ background: var(--primary-bg);
46
+ color: var(--text-primary);
47
+ font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
48
+ font-size: 14px;
49
+ line-height: 1.5;
50
+ }
51
+
52
+ body::before,
53
+ body::after {
54
+ content: "";
55
+ position: fixed;
56
+ z-index: -1;
57
+ pointer-events: none;
58
+ filter: blur(4px);
59
+ }
60
+
61
+ body::before {
62
+ inset: 4% auto auto 7%;
63
+ width: 18rem;
64
+ height: 18rem;
65
+ border-radius: 50%;
66
+ background: radial-gradient(circle, rgba(96, 165, 250, 0.22), transparent 68%);
67
+ }
68
+
69
+ body::after {
70
+ right: 5%;
71
+ bottom: 5%;
72
+ width: 22rem;
73
+ height: 22rem;
74
+ border-radius: 50%;
75
+ background: radial-gradient(circle, rgba(59, 130, 246, 0.16), transparent 68%);
76
+ }
77
+
78
+ button,
79
+ input,
80
+ textarea {
81
+ font: inherit;
82
+ }
83
+
84
+ button {
85
+ border: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
87
 
88
  .is-hidden {
89
  display: none !important;
90
  }
91
+
92
+ .app-shell {
93
+ display: grid;
94
+ grid-template-columns: minmax(320px, 450px) minmax(0, 760px);
95
+ gap: 1rem;
96
+ width: min(1240px, calc(100vw - 2rem));
97
+ height: calc(100dvh - 2rem);
98
+ max-height: 900px;
99
+ margin: 1rem auto;
100
+ perspective: 1400px;
101
+ }
102
+
103
+ .connect-screen,
104
+ .chat-screen {
105
+ position: relative;
106
+ min-height: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  background: var(--card-bg);
108
+ border: 1px solid var(--glass-border);
109
+ border-radius: 28px;
110
+ box-shadow: var(--shadow-heavy), var(--inner-highlight), var(--inner-depth);
111
+ backdrop-filter: var(--blur-medium);
112
+ -webkit-backdrop-filter: var(--blur-medium);
113
+ transform-style: preserve-3d;
114
+ }
115
+
116
+ .connect-screen::before,
117
+ .chat-screen::before {
118
+ content: "";
119
+ position: absolute;
120
+ inset: 1px;
121
+ z-index: -1;
122
+ border-radius: inherit;
123
+ background:
124
+ linear-gradient(145deg, rgba(255, 255, 255, 0.74), rgba(255, 255, 255, 0.18) 42%, rgba(96, 165, 250, 0.08)),
125
+ radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.95), transparent 28%);
126
+ pointer-events: none;
127
+ }
128
+
129
+ .connect-screen {
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: clamp(1rem, 2dvh, 1.6rem);
133
+ min-height: 0;
134
+ overflow-y: auto;
135
+ overscroll-behavior: contain;
136
+ padding: clamp(1.25rem, 3vw, 2.4rem);
137
+ scrollbar-gutter: stable;
138
+ transform: rotateY(0.5deg) translateZ(0);
139
+ }
140
+
141
+ .chat-screen {
142
+ overflow: hidden;
143
+ transform: rotateY(-0.35deg) translateZ(0);
144
+ }
145
+
146
+ .connect-screen::-webkit-scrollbar {
147
+ width: 6px;
148
+ }
149
+
150
+ .connect-screen::-webkit-scrollbar-track {
151
+ background: transparent;
152
+ }
153
+
154
+ .connect-screen::-webkit-scrollbar-thumb {
155
+ border-radius: 999px;
156
+ background: rgba(100, 116, 139, 0.24);
157
+ }
158
+
159
+ .brand-bar {
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 0.9rem;
163
+ }
164
+
165
+ .brand-mark {
166
+ display: grid;
167
+ place-items: center;
168
+ width: 52px;
169
+ height: 52px;
170
+ border-radius: 18px;
171
+ color: white;
172
+ background:
173
+ radial-gradient(circle at 30% 22%, rgba(255, 255, 255, 0.55), transparent 18%),
174
+ linear-gradient(145deg, var(--accent-secondary), var(--accent-primary) 58%, #2563eb);
175
+ box-shadow:
176
+ 0 18px 34px rgba(59, 130, 246, 0.24),
177
+ inset 0 1px 0 rgba(255, 255, 255, 0.45),
178
+ inset 0 -10px 22px rgba(37, 99, 235, 0.28);
179
+ font-size: 1.55rem;
180
+ }
181
+
182
+ .brand-bar h1 {
183
+ font-size: clamp(1.65rem, 3vw, 2rem);
184
+ line-height: 1.1;
185
+ }
186
+
187
+ .brand-bar p,
188
+ .welcome-copy p,
189
+ .connect-tip,
190
+ .room-meta p {
191
+ color: var(--text-secondary);
192
+ }
193
+
194
+ .welcome-art {
195
+ position: relative;
196
+ display: grid;
197
+ place-items: center;
198
+ width: min(58vw, 250px);
199
+ aspect-ratio: 1;
200
+ margin: 0.5rem auto 0;
201
+ border-radius: 50%;
202
+ background:
203
+ radial-gradient(circle at 30% 22%, rgba(255, 255, 255, 0.86), transparent 24%),
204
+ radial-gradient(circle at 38% 42%, rgba(16, 185, 129, 0.12), transparent 18%),
205
+ radial-gradient(circle at 70% 20%, rgba(96, 165, 250, 0.16), transparent 9%),
206
+ linear-gradient(145deg, rgba(59, 130, 246, 0.08), rgba(96, 165, 250, 0.06));
207
+ border: 1px solid rgba(255, 255, 255, 0.7);
208
+ box-shadow:
209
+ inset 0 1px 0 rgba(255, 255, 255, 0.86),
210
+ inset 18px 24px 42px rgba(255, 255, 255, 0.48),
211
+ 0 22px 48px rgba(59, 130, 246, 0.1);
212
+ }
213
+
214
+ .welcome-art::before,
215
+ .welcome-art::after {
216
+ content: "";
217
+ position: absolute;
218
+ border-radius: 50%;
219
+ background: var(--accent-secondary);
220
+ opacity: 0.65;
221
+ }
222
+
223
+ .welcome-art::before {
224
+ width: 8px;
225
+ height: 8px;
226
+ right: 17%;
227
+ top: 28%;
228
+ }
229
+
230
+ .welcome-art::after {
231
+ width: 10px;
232
+ height: 10px;
233
+ left: 16%;
234
+ bottom: 26%;
235
+ background: var(--accent-primary);
236
+ }
237
+
238
+ .bubble {
239
+ position: absolute;
240
+ display: flex;
241
+ align-items: center;
242
+ justify-content: center;
243
+ gap: 0.8rem;
244
+ border-radius: 28px;
245
+ }
246
+
247
+ .bubble span {
248
+ width: 12px;
249
+ height: 12px;
250
+ border-radius: 50%;
251
+ }
252
+
253
+ .bubble-primary {
254
+ left: 16%;
255
+ top: 30%;
256
+ width: 128px;
257
+ height: 78px;
258
+ color: white;
259
+ background:
260
+ radial-gradient(circle at 28% 18%, rgba(255, 255, 255, 0.55), transparent 20%),
261
+ linear-gradient(145deg, var(--accent-secondary), var(--accent-primary) 62%, #2563eb);
262
+ box-shadow:
263
+ 0 18px 30px rgba(59, 130, 246, 0.26),
264
+ inset 0 1px 0 rgba(255, 255, 255, 0.4),
265
+ inset 0 -12px 24px rgba(37, 99, 235, 0.22);
266
+ animation: floatPrimary 5.5s ease-in-out infinite;
267
+ }
268
+
269
+ .bubble-primary::after {
270
+ content: "";
271
+ position: absolute;
272
+ left: 28px;
273
+ bottom: -18px;
274
+ border-top: 24px solid var(--accent-primary);
275
+ border-right: 24px solid transparent;
276
+ }
277
+
278
+ .bubble-primary span {
279
+ background: white;
280
+ }
281
+
282
+ .bubble-secondary {
283
+ right: 12%;
284
+ bottom: 23%;
285
+ width: 126px;
286
+ height: 72px;
287
+ background: rgba(255, 255, 255, 0.86);
288
+ backdrop-filter: var(--blur-light);
289
+ -webkit-backdrop-filter: var(--blur-light);
290
+ box-shadow:
291
+ 0 20px 42px rgba(15, 23, 42, 0.11),
292
+ inset 0 1px 0 rgba(255, 255, 255, 0.95);
293
+ animation: floatSecondary 6.2s ease-in-out infinite;
294
+ }
295
+
296
+ .bubble-secondary::after {
297
+ content: "";
298
+ position: absolute;
299
+ right: 24px;
300
+ bottom: -16px;
301
+ border-top: 22px solid white;
302
+ border-left: 22px solid transparent;
303
+ }
304
+
305
+ .bubble-secondary span {
306
+ background: #60a5fa;
307
+ }
308
+
309
+ @keyframes floatPrimary {
310
+ 0%,
311
+ 100% {
312
+ transform: translate3d(0, 0, 22px);
313
+ }
314
+
315
+ 50% {
316
+ transform: translate3d(0, -8px, 28px);
317
+ }
318
+ }
319
+
320
+ @keyframes floatSecondary {
321
+ 0%,
322
+ 100% {
323
+ transform: translate3d(0, 0, 34px);
324
+ }
325
+
326
+ 50% {
327
+ transform: translate3d(5px, 7px, 40px);
328
+ }
329
+ }
330
+
331
+ .welcome-copy {
332
+ text-align: center;
333
+ }
334
+
335
+ .welcome-copy h2 {
336
+ margin-bottom: 0.35rem;
337
+ font-size: clamp(1.45rem, 3vw, 1.8rem);
338
+ }
339
+
340
+ .welcome-copy p {
341
+ max-width: 25rem;
342
+ margin: 0 auto;
343
+ font-size: 1rem;
344
+ }
345
+
346
+ .connection-form {
347
+ display: grid;
348
+ gap: 1rem;
349
+ }
350
+
351
+ .input-group {
352
+ display: grid;
353
+ gap: 0.5rem;
354
+ }
355
+
356
+ .input-group label {
357
+ color: #64748b;
358
+ font-size: 0.78rem;
359
+ font-weight: 700;
360
+ letter-spacing: 0.05em;
361
+ text-transform: uppercase;
362
+ }
363
+
364
+ .field-shell {
365
+ display: grid;
366
+ grid-template-columns: 48px minmax(0, 1fr) 30px;
367
+ align-items: center;
368
+ min-height: 64px;
369
+ padding: 0 0.8rem;
370
+ border: 1px solid rgba(255, 255, 255, 0.72);
371
+ border-radius: 18px;
372
+ background: rgba(255, 255, 255, 0.62);
373
+ box-shadow:
374
+ 0 12px 28px rgba(15, 23, 42, 0.06),
375
+ inset 0 1px 0 rgba(255, 255, 255, 0.88);
376
+ backdrop-filter: var(--blur-light);
377
+ -webkit-backdrop-filter: var(--blur-light);
378
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
379
+ }
380
+
381
+ .field-shell:focus-within {
382
+ border-color: rgba(96, 165, 250, 0.76);
383
+ box-shadow:
384
+ 0 18px 38px rgba(59, 130, 246, 0.14),
385
+ 0 0 0 4px rgba(59, 130, 246, 0.08),
386
+ inset 0 1px 0 rgba(255, 255, 255, 0.92);
387
  transform: translateY(-1px);
388
+ }
389
+
390
+ .field-shell > i:first-child {
391
+ display: grid;
392
+ place-items: center;
393
+ width: 38px;
394
+ height: 38px;
395
+ border-radius: 14px;
396
+ color: var(--accent-secondary);
397
+ background: rgba(96, 165, 250, 0.12);
398
+ }
399
+
400
+ .field-shell input {
401
+ min-width: 0;
402
+ border: 0;
403
+ outline: 0;
404
+ background: transparent;
405
+ color: var(--text-primary);
406
+ font-size: 1rem;
407
+ }
408
+
409
+ .field-check {
410
+ display: grid;
411
+ place-items: center;
412
+ width: 22px;
413
+ height: 22px;
414
+ border-radius: 50%;
415
+ color: white;
416
+ background: var(--accent-success);
417
+ font-size: 0.72rem;
418
+ opacity: 0;
419
+ transform: scale(0.8);
420
+ transition: opacity 0.2s ease, transform 0.2s ease;
421
+ }
422
+
423
+ .field-shell.has-value .field-check {
424
+ opacity: 1;
425
+ transform: scale(1);
426
+ }
427
+
428
+ .btn,
429
+ .icon-btn,
430
+ .send-btn {
431
+ cursor: pointer;
432
+ }
433
+
434
+ .btn {
435
+ display: inline-flex;
436
+ align-items: center;
437
+ justify-content: center;
438
+ gap: 0.75rem;
439
+ min-height: 60px;
440
+ padding: 0 1.2rem;
441
+ border-radius: 18px;
442
+ color: white;
443
+ font-size: 1.05rem;
444
+ font-weight: 800;
445
+ background:
446
+ radial-gradient(circle at 35% 10%, rgba(255, 255, 255, 0.45), transparent 18%),
447
+ linear-gradient(145deg, var(--accent-secondary), var(--accent-primary) 58%, #2563eb);
448
+ box-shadow:
449
+ 0 22px 44px rgba(59, 130, 246, 0.28),
450
+ inset 0 1px 0 rgba(255, 255, 255, 0.45),
451
+ inset 0 -12px 24px rgba(37, 99, 235, 0.26);
452
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
453
+ }
454
+
455
+ .btn:hover,
456
+ .send-btn:hover,
457
+ .icon-btn:hover {
458
+ transform: translateY(-2px);
459
+ }
460
+
461
+ .field-shell,
462
+ .btn,
463
+ .icon-btn,
464
+ .send-btn,
465
+ .message,
466
+ .user-list li {
467
+ will-change: transform;
468
+ }
469
+
470
+ .btn:active,
471
+ .send-btn:active,
472
+ .icon-btn:active {
473
+ transform: translateY(0);
474
+ }
475
+
476
+ .status {
477
+ display: flex;
478
+ align-items: center;
479
+ justify-content: center;
480
+ gap: 0.6rem;
481
+ min-height: 56px;
482
+ padding: 0.8rem 1rem;
483
+ border-radius: 18px;
484
+ font-weight: 700;
485
+ backdrop-filter: var(--blur-light);
486
+ -webkit-backdrop-filter: var(--blur-light);
487
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.78);
488
+ }
489
+
490
+ .status.neutral {
491
+ color: var(--text-muted);
492
+ background: rgba(255, 255, 255, 0.68);
493
+ border: 1px solid var(--border-color);
494
+ }
495
+
496
+ .status.connected {
497
+ color: #059669;
498
+ background: rgba(16, 185, 129, 0.1);
499
+ border: 1px solid rgba(16, 185, 129, 0.18);
500
+ }
501
+
502
+ .status.disconnected {
503
+ color: var(--accent-danger);
504
+ background: rgba(239, 68, 68, 0.08);
505
+ border: 1px solid rgba(239, 68, 68, 0.16);
506
+ }
507
+
508
+ .connect-tip {
509
+ display: flex;
510
+ align-items: center;
511
+ gap: 0.8rem;
512
+ margin-top: auto;
513
+ padding: 1rem;
514
+ border-radius: 18px;
515
+ background: linear-gradient(135deg, rgba(96, 165, 250, 0.12), rgba(255, 255, 255, 0.54));
516
+ border: 1px solid rgba(255, 255, 255, 0.66);
517
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.76), 0 12px 26px rgba(15, 23, 42, 0.05);
518
+ }
519
+
520
+ .connect-tip i {
521
+ color: var(--accent-secondary);
522
+ font-size: 1.35rem;
523
+ }
524
+
525
+ .chat-screen {
526
+ display: flex;
527
+ flex-direction: column;
528
+ }
529
+
530
+ .chat-header {
531
+ display: flex;
532
+ align-items: center;
533
+ gap: 1rem;
534
+ padding: 1.15rem 1.25rem;
535
+ color: white;
536
+ background:
537
+ radial-gradient(circle at 12% 0%, rgba(255, 255, 255, 0.28), transparent 28%),
538
+ linear-gradient(145deg, var(--accent-primary), var(--accent-secondary));
539
+ box-shadow:
540
+ inset 0 1px 0 rgba(255, 255, 255, 0.24),
541
+ 0 16px 34px rgba(59, 130, 246, 0.18);
542
+ }
543
+
544
+ .room-meta {
545
+ min-width: 0;
546
+ flex: 1;
547
+ }
548
+
549
+ .room-meta h2 {
550
+ overflow: hidden;
551
+ font-size: 1.3rem;
552
+ line-height: 1.2;
553
+ text-overflow: ellipsis;
554
+ white-space: nowrap;
555
+ }
556
+
557
+ .room-meta p {
558
+ display: flex;
559
+ align-items: center;
560
+ gap: 0.45rem;
561
+ color: rgba(255, 255, 255, 0.86);
562
+ }
563
+
564
+ .online-dot {
565
+ width: 9px;
566
+ height: 9px;
567
+ border-radius: 50%;
568
+ background: var(--accent-success);
569
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.18);
570
+ }
571
+
572
+ .chat-actions {
573
+ display: flex;
574
+ align-items: center;
575
+ gap: 0.65rem;
576
+ }
577
+
578
+ .icon-btn {
579
+ display: inline-grid;
580
+ place-items: center;
581
+ width: 46px;
582
+ height: 46px;
583
+ border-radius: 50%;
584
+ color: inherit;
585
+ background: rgba(255, 255, 255, 0.18);
586
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22), 0 10px 22px rgba(37, 99, 235, 0.12);
587
+ backdrop-filter: var(--blur-light);
588
+ -webkit-backdrop-filter: var(--blur-light);
589
+ transition: transform 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
590
+ }
591
+
592
+ .icon-btn:hover {
593
+ background: rgba(255, 255, 255, 0.24);
594
+ }
595
+
596
+ .mobile-back-btn,
597
+ .users-close {
598
+ display: none;
599
+ }
600
+
601
+ .chat-body {
602
+ display: grid;
603
+ grid-template-columns: 240px minmax(0, 1fr);
604
+ min-height: 0;
605
+ flex: 1;
606
+ }
607
+
608
+ .users-panel {
609
+ display: flex;
610
+ flex-direction: column;
611
+ min-height: 0;
612
+ border-right: 1px solid var(--border-color);
613
+ background: rgba(255, 255, 255, 0.46);
614
+ backdrop-filter: var(--blur-light);
615
+ -webkit-backdrop-filter: var(--blur-light);
616
+ }
617
+
618
+ .users-panel-header {
619
+ display: flex;
620
+ align-items: center;
621
+ justify-content: space-between;
622
+ padding: 1rem;
623
+ border-bottom: 1px solid var(--border-color);
624
+ }
625
+
626
+ .users-panel-header h3 {
627
+ font-size: 1rem;
628
+ }
629
+
630
+ .user-list {
631
+ display: grid;
632
+ gap: 0.55rem;
633
+ padding: 1rem;
634
+ overflow-y: auto;
635
+ list-style: none;
636
+ }
637
+
638
+ .user-list li {
639
+ display: flex;
640
+ align-items: center;
641
+ gap: 0.7rem;
642
+ min-width: 0;
643
+ padding: 0.75rem;
644
+ border-radius: 14px;
645
+ color: var(--text-secondary);
646
+ border: 1px solid rgba(255, 255, 255, 0.68);
647
+ background: rgba(255, 255, 255, 0.62);
648
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 8px 18px rgba(15, 23, 42, 0.04);
649
+ }
650
+
651
+ .user-list li::before {
652
+ content: "";
653
+ flex: 0 0 auto;
654
+ width: 9px;
655
+ height: 9px;
656
+ border-radius: 50%;
657
+ background: var(--accent-success);
658
+ }
659
+
660
+ .empty-users {
661
+ padding: 0 1rem 1rem;
662
+ color: var(--text-muted);
663
+ font-size: 0.9rem;
664
+ }
665
+
666
+ .messages-wrap {
667
+ display: flex;
668
+ flex-direction: column;
669
+ min-width: 0;
670
+ min-height: 0;
671
+ background:
672
+ radial-gradient(circle at 78% 24%, rgba(96, 165, 250, 0.08), transparent 28%),
673
+ radial-gradient(circle at 28% 76%, rgba(16, 185, 129, 0.06), transparent 24%),
674
+ rgba(255, 255, 255, 0.18);
675
+ }
676
+
677
+ .messages {
678
+ display: flex;
679
+ flex: 1;
680
+ flex-direction: column;
681
+ gap: 1rem;
682
+ min-height: 0;
683
+ padding: 1.25rem;
684
+ overflow-y: auto;
685
+ scroll-behavior: smooth;
686
+ }
687
+
688
+ .messages::-webkit-scrollbar,
689
+ .user-list::-webkit-scrollbar {
690
+ width: 6px;
691
+ }
692
+
693
+ .messages::-webkit-scrollbar-track,
694
+ .user-list::-webkit-scrollbar-track {
695
+ background: transparent;
696
+ }
697
+
698
+ .messages::-webkit-scrollbar-thumb,
699
+ .user-list::-webkit-scrollbar-thumb {
700
+ border-radius: 999px;
701
+ background: rgba(100, 116, 139, 0.28);
702
+ }
703
+
704
+ .message {
705
+ max-width: min(72%, 33rem);
706
+ padding: 0.95rem 1.1rem;
707
+ border: 1px solid rgba(255, 255, 255, 0.68);
708
+ border-radius: 18px 18px 18px 6px;
709
+ background: rgba(255, 255, 255, 0.72);
710
+ box-shadow:
711
+ 0 14px 28px rgba(15, 23, 42, 0.08),
712
+ inset 0 1px 0 rgba(255, 255, 255, 0.82);
713
+ backdrop-filter: var(--blur-light);
714
+ -webkit-backdrop-filter: var(--blur-light);
715
+ animation: messageSlide 0.24s ease-out;
716
+ }
717
+
718
+ @keyframes messageSlide {
719
+ from {
720
+ opacity: 0;
721
+ transform: translateY(8px);
722
+ }
723
+
724
+ to {
725
+ opacity: 1;
726
+ transform: translateY(0);
727
+ }
728
+ }
729
+
730
+ .message.own {
731
+ align-self: flex-end;
732
+ border: 0;
733
+ border-radius: 18px 18px 6px 18px;
734
+ color: white;
735
+ background:
736
+ radial-gradient(circle at 24% 12%, rgba(255, 255, 255, 0.32), transparent 18%),
737
+ linear-gradient(145deg, var(--accent-primary), var(--accent-secondary));
738
+ box-shadow:
739
+ 0 18px 36px rgba(59, 130, 246, 0.24),
740
+ inset 0 1px 0 rgba(255, 255, 255, 0.28);
741
+ }
742
+
743
+ .message.system {
744
+ align-self: center;
745
+ max-width: 92%;
746
+ padding: 0.7rem 1rem;
747
+ border-radius: 999px;
748
+ color: var(--text-secondary);
749
+ font-size: 0.9rem;
750
+ font-style: italic;
751
+ border: 1px solid rgba(255, 255, 255, 0.72);
752
+ background: rgba(255, 255, 255, 0.68);
753
+ box-shadow: 0 10px 22px rgba(15, 23, 42, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.86);
754
+ }
755
+
756
+ .message-header {
757
+ display: flex;
758
+ align-items: center;
759
+ justify-content: space-between;
760
+ gap: 1rem;
761
+ margin-bottom: 0.45rem;
762
+ font-size: 0.78rem;
763
+ }
764
+
765
+ .username {
766
+ min-width: 0;
767
+ overflow: hidden;
768
+ color: var(--accent-secondary);
769
+ font-weight: 800;
770
+ text-overflow: ellipsis;
771
+ white-space: nowrap;
772
+ }
773
+
774
+ .timestamp {
775
+ flex: 0 0 auto;
776
+ color: var(--text-muted);
777
+ }
778
+
779
+ .message.own .username,
780
+ .message.own .timestamp {
781
+ color: rgba(255, 255, 255, 0.88);
782
+ }
783
+
784
+ .message-content {
785
+ overflow-wrap: anywhere;
786
+ font-size: 1rem;
787
  }
788
 
789
  .file-summary {
790
  display: flex;
791
  align-items: center;
792
  gap: 0.5rem;
793
+ margin-bottom: 0.45rem;
794
  }
795
 
796
  .file-size {
797
+ margin-bottom: 0.55rem;
798
  color: var(--text-muted);
799
+ font-size: 0.82rem;
800
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
 
802
+ .message.own .file-size {
803
+ color: rgba(255, 255, 255, 0.75);
804
  }
805
 
806
+ .file-preview img {
807
+ max-width: 100%;
808
+ max-height: 220px;
809
+ margin-top: 0.65rem;
810
+ border-radius: 12px;
811
+ box-shadow: var(--shadow-light);
812
  }
813
 
814
+ .file-download {
815
+ display: inline-flex;
816
+ align-items: center;
817
+ justify-content: center;
818
+ gap: 0.5rem;
819
+ margin-top: 0.75rem;
820
+ padding: 0.5rem 0.8rem;
821
+ border: 1px solid rgba(255, 255, 255, 0.7);
822
+ border-radius: 999px;
823
+ color: var(--accent-secondary);
824
+ background: rgba(255, 255, 255, 0.62);
825
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.78);
826
+ font-size: 0.86rem;
827
+ font-weight: 700;
828
+ text-decoration: none;
829
  }
830
 
831
+ .input-area {
832
+ display: grid;
833
+ grid-template-columns: 48px minmax(0, 1fr) 56px;
834
+ gap: 0.8rem;
835
+ align-items: end;
836
+ padding: 1rem;
837
+ padding-bottom: calc(1rem + var(--safe-bottom));
838
+ border-top: 1px solid var(--border-color);
839
+ background: rgba(255, 255, 255, 0.58);
840
+ backdrop-filter: var(--blur-medium);
841
+ -webkit-backdrop-filter: var(--blur-medium);
842
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.84);
843
  }
844
 
845
+ .file-input-wrapper {
846
+ position: relative;
847
  }
848
 
849
+ .file-input-wrapper input[type="file"] {
850
+ position: absolute;
851
+ inset: 0;
852
+ width: 100%;
853
+ height: 100%;
854
+ opacity: 0;
855
+ cursor: pointer;
856
+ }
857
+
858
+ .file-input-label,
859
+ .send-btn {
860
+ display: grid;
861
+ place-items: center;
862
+ border-radius: 50%;
863
+ }
864
+
865
+ .file-input-label {
866
+ width: 48px;
867
+ height: 48px;
868
+ border: 1px solid rgba(255, 255, 255, 0.78);
869
+ color: var(--text-secondary);
870
+ background: rgba(255, 255, 255, 0.68);
871
+ box-shadow: 0 10px 22px rgba(15, 23, 42, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.86);
872
+ backdrop-filter: var(--blur-light);
873
+ -webkit-backdrop-filter: var(--blur-light);
874
+ cursor: pointer;
875
+ }
876
+
877
+ #messageInput {
878
+ min-width: 0;
879
+ min-height: 48px;
880
+ max-height: 136px;
881
+ padding: 0.85rem 1rem;
882
+ resize: none;
883
+ border: 1px solid rgba(255, 255, 255, 0.78);
884
+ border-radius: 999px;
885
+ outline: 0;
886
+ color: var(--text-primary);
887
+ background: rgba(255, 255, 255, 0.72);
888
+ font-size: 1rem;
889
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92), 0 10px 24px rgba(15, 23, 42, 0.05);
890
+ backdrop-filter: var(--blur-light);
891
+ -webkit-backdrop-filter: var(--blur-light);
892
+ }
893
+
894
+ #messageInput:focus {
895
+ border-color: rgba(59, 130, 246, 0.5);
896
+ box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
897
+ }
898
+
899
+ .send-btn {
900
+ width: 56px;
901
+ height: 56px;
902
+ color: white;
903
+ background:
904
+ radial-gradient(circle at 32% 16%, rgba(255, 255, 255, 0.45), transparent 18%),
905
+ linear-gradient(145deg, var(--accent-secondary), var(--accent-primary) 60%, #2563eb);
906
+ box-shadow:
907
+ 0 18px 36px rgba(59, 130, 246, 0.28),
908
+ inset 0 1px 0 rgba(255, 255, 255, 0.42),
909
+ inset 0 -10px 20px rgba(37, 99, 235, 0.24);
910
+ font-size: 1.15rem;
911
+ }
912
+
913
+ @media (min-width: 769px) {
914
+ .connect-screen:hover {
915
+ transform: rotateY(0) translateY(-2px);
916
  }
917
 
918
+ .chat-screen:hover {
919
+ transform: rotateY(0) translateY(-2px);
920
+ }
921
+
922
+ .app-shell.is-chatting {
923
+ grid-template-columns: minmax(0, 1fr);
924
+ }
925
+
926
+ .app-shell.is-chatting .chat-screen {
927
+ grid-column: 1 / -1;
928
+ transform: none;
929
+ }
930
+
931
+ .chat-screen.is-hidden {
932
+ display: flex !important;
933
+ }
934
+
935
+ .chat-screen:not(.is-connected-preview) {
936
+ opacity: 0.96;
937
+ }
938
+
939
+ .chat-screen:not(.is-connected-preview) .messages::before {
940
+ content: "Connect to a room to begin chatting.";
941
+ margin: auto;
942
+ color: var(--text-muted);
943
+ font-weight: 700;
944
+ }
945
+
946
+ .chat-screen:not(.is-connected-preview) .input-area {
947
+ opacity: 0.45;
948
+ pointer-events: none;
949
+ }
950
+ }
951
+
952
+ @media (max-width: 900px) and (min-width: 769px) {
953
+ .app-shell {
954
+ grid-template-columns: minmax(300px, 390px) minmax(0, 1fr);
955
+ }
956
+
957
+ .chat-body {
958
+ grid-template-columns: 200px minmax(0, 1fr);
959
  }
960
+ }
961
 
962
+ @media (min-width: 769px) and (max-height: 860px) {
963
+ .app-shell {
964
+ height: calc(100dvh - 1.5rem);
965
+ margin: 0.75rem auto;
966
+ }
967
+
968
+ .connect-screen {
969
+ gap: 0.85rem;
970
+ padding: 1.6rem;
971
+ }
972
+
973
+ .brand-mark {
974
+ width: 46px;
975
+ height: 46px;
976
+ border-radius: 16px;
977
+ font-size: 1.35rem;
978
+ }
979
+
980
+ .brand-bar h1 {
981
+ font-size: 1.75rem;
982
+ }
983
+
984
+ .brand-bar p {
985
+ font-size: 0.95rem;
986
+ }
987
+
988
+ .welcome-art {
989
+ width: min(42vh, 210px);
990
+ margin-top: 0;
991
+ }
992
+
993
+ .bubble-primary {
994
+ width: 110px;
995
+ height: 68px;
996
+ }
997
+
998
+ .bubble-secondary {
999
+ width: 108px;
1000
+ height: 62px;
1001
+ }
1002
+
1003
+ .welcome-copy h2 {
1004
+ font-size: 1.45rem;
1005
+ }
1006
+
1007
+ .welcome-copy p {
1008
+ font-size: 0.95rem;
1009
+ }
1010
+
1011
+ .connection-form {
1012
  gap: 0.75rem;
 
1013
  }
1014
 
1015
+ .field-shell {
1016
+ min-height: 54px;
1017
+ border-radius: 16px;
1018
  }
1019
 
1020
+ .btn {
1021
+ min-height: 54px;
1022
+ }
1023
+
1024
+ .status {
1025
+ min-height: 48px;
1026
+ padding: 0.65rem 0.9rem;
1027
+ }
1028
+
1029
+ .connect-tip {
1030
+ margin-top: 0;
1031
+ padding: 0.85rem;
1032
+ }
1033
+ }
1034
+
1035
+ @media (min-width: 769px) and (max-height: 760px) {
1036
+ .app-shell {
1037
+ height: calc(100dvh - 1rem);
1038
+ margin: 0.5rem auto;
1039
  }
1040
 
1041
+ .connect-screen {
1042
+ gap: 0.65rem;
1043
+ padding: 1.1rem 1.35rem;
1044
+ }
1045
+
1046
+ .brand-mark {
1047
  width: 42px;
1048
  height: 42px;
1049
+ border-radius: 14px;
1050
+ font-size: 1.2rem;
 
 
 
1051
  }
1052
 
1053
+ .brand-bar h1 {
1054
+ font-size: 1.55rem;
 
 
 
 
1055
  }
1056
 
1057
+ .brand-bar p,
1058
+ .welcome-copy p,
1059
+ .connect-tip {
1060
+ font-size: 0.9rem;
1061
  }
1062
 
1063
+ .welcome-art {
1064
+ width: min(31vh, 170px);
1065
+ }
1066
+
1067
+ .bubble {
1068
+ gap: 0.55rem;
1069
+ }
1070
+
1071
+ .bubble span {
1072
+ width: 9px;
1073
+ height: 9px;
1074
+ }
1075
+
1076
+ .bubble-primary {
1077
+ width: 88px;
1078
+ height: 54px;
1079
+ border-radius: 20px;
1080
+ }
1081
+
1082
+ .bubble-primary::after {
1083
+ left: 20px;
1084
+ bottom: -13px;
1085
+ border-top-width: 18px;
1086
+ border-right-width: 18px;
1087
+ }
1088
+
1089
+ .bubble-secondary {
1090
+ width: 88px;
1091
+ height: 50px;
1092
+ border-radius: 20px;
1093
+ }
1094
+
1095
+ .bubble-secondary::after {
1096
+ right: 18px;
1097
+ bottom: -12px;
1098
+ border-top-width: 17px;
1099
+ border-left-width: 17px;
1100
+ }
1101
+
1102
+ .welcome-copy h2 {
1103
+ font-size: 1.28rem;
1104
+ margin-bottom: 0.15rem;
1105
+ }
1106
+
1107
+ .input-group {
1108
+ gap: 0.35rem;
1109
+ }
1110
+
1111
+ .input-group label {
1112
+ font-size: 0.72rem;
1113
+ }
1114
+
1115
+ .field-shell {
1116
+ grid-template-columns: 42px minmax(0, 1fr) 28px;
1117
+ min-height: 48px;
1118
+ border-radius: 14px;
1119
  }
1120
 
1121
+ .field-shell > i:first-child {
1122
+ width: 32px;
1123
+ height: 32px;
1124
+ border-radius: 12px;
1125
+ }
1126
+
1127
+ .btn {
1128
+ min-height: 48px;
1129
+ border-radius: 14px;
1130
+ }
1131
+
1132
+ .status {
1133
  min-height: 44px;
1134
+ border-radius: 14px;
1135
+ font-size: 0.9rem;
1136
+ }
1137
+
1138
+ .connect-tip {
1139
+ padding: 0.7rem 0.85rem;
1140
+ border-radius: 14px;
1141
+ }
1142
+ }
1143
+
1144
+ @media (max-width: 768px) {
1145
+ body {
1146
+ background:
1147
+ radial-gradient(circle at 50% 0%, rgba(96, 165, 250, 0.22), transparent 34%),
1148
+ radial-gradient(circle at 15% 90%, rgba(59, 130, 246, 0.1), transparent 30%),
1149
+ linear-gradient(155deg, #f8fafc 0%, #e8eef7 100%);
1150
+ }
1151
+
1152
+ .app-shell {
1153
+ display: block;
1154
+ width: 100vw;
1155
+ height: 100dvh;
1156
+ margin: 0;
1157
+ overflow: hidden;
1158
+ }
1159
+
1160
+ .connect-screen,
1161
+ .chat-screen {
1162
+ width: 100%;
1163
+ height: 100%;
1164
+ border: 0;
1165
+ border-radius: 0;
1166
+ box-shadow: none;
1167
+ transform: none;
1168
+ }
1169
+
1170
+ .connect-screen {
1171
+ gap: clamp(0.8rem, 1.65dvh, 1.05rem);
1172
  justify-content: center;
1173
+ padding: clamp(1rem, 2.5dvh, 1.35rem) 1.05rem;
1174
+ overflow-y: auto;
1175
+ scrollbar-gutter: auto;
1176
+ background:
1177
+ radial-gradient(circle at 50% 23%, rgba(96, 165, 250, 0.14), transparent 28%),
1178
+ linear-gradient(150deg, rgba(255, 255, 255, 0.94), rgba(239, 246, 255, 0.78));
1179
  }
1180
 
1181
+ .brand-bar {
1182
+ gap: 0.7rem;
1183
+ padding-top: 0;
1184
+ }
1185
+
1186
+ .brand-mark {
1187
+ width: 42px;
1188
+ height: 42px;
1189
+ border-radius: 14px;
1190
+ font-size: 1.16rem;
1191
+ }
1192
+
1193
+ .brand-bar h1 {
1194
+ font-size: 1.45rem;
1195
+ letter-spacing: -0.02em;
1196
+ }
1197
+
1198
+ .brand-bar p {
1199
+ font-size: 0.82rem;
1200
+ }
1201
+
1202
+ .welcome-art {
1203
+ width: min(52vw, 190px);
1204
+ margin: 0.15rem auto 0;
1205
+ box-shadow:
1206
+ inset 0 1px 0 rgba(255, 255, 255, 0.9),
1207
+ inset 18px 24px 42px rgba(255, 255, 255, 0.5),
1208
+ 0 26px 58px rgba(59, 130, 246, 0.14);
1209
+ }
1210
+
1211
+ .bubble {
1212
+ gap: 0.5rem;
1213
+ }
1214
+
1215
+ .bubble span {
1216
+ width: 9px;
1217
+ height: 9px;
1218
+ }
1219
+
1220
+ .bubble-primary {
1221
+ width: 92px;
1222
+ height: 56px;
1223
+ border-radius: 21px;
1224
+ }
1225
+
1226
+ .bubble-primary::after {
1227
+ left: 18px;
1228
+ bottom: -11px;
1229
+ border-top-width: 16px;
1230
+ border-right-width: 16px;
1231
+ }
1232
+
1233
+ .bubble-secondary {
1234
+ width: 92px;
1235
+ height: 54px;
1236
+ border-radius: 21px;
1237
+ }
1238
+
1239
+ .bubble-secondary::after {
1240
+ right: 17px;
1241
+ bottom: -10px;
1242
+ border-top-width: 15px;
1243
+ border-left-width: 15px;
1244
+ }
1245
+
1246
+ .welcome-copy h2 {
1247
+ margin-bottom: 0.25rem;
1248
+ font-size: 1.42rem;
1249
+ letter-spacing: -0.02em;
1250
+ }
1251
+
1252
+ .welcome-copy p {
1253
+ width: min(100%, 21rem);
1254
+ font-size: 0.9rem;
1255
+ line-height: 1.4;
1256
+ }
1257
+
1258
+ .connection-form {
1259
+ gap: 0.72rem;
1260
+ }
1261
+
1262
+ .input-group {
1263
+ gap: 0.36rem;
1264
+ }
1265
+
1266
+ .input-group label {
1267
+ font-size: 0.7rem;
1268
+ }
1269
+
1270
+ .field-shell {
1271
+ grid-template-columns: 42px minmax(0, 1fr) 28px;
1272
+ min-height: 54px;
1273
+ border-radius: 18px;
1274
+ background: rgba(255, 255, 255, 0.82);
1275
+ box-shadow:
1276
+ 0 14px 30px rgba(15, 23, 42, 0.07),
1277
+ inset 0 1px 0 rgba(255, 255, 255, 0.94);
1278
+ }
1279
+
1280
+ .field-shell > i:first-child {
1281
+ width: 32px;
1282
+ height: 32px;
1283
+ border-radius: 12px;
1284
+ }
1285
+
1286
+ .field-shell input {
1287
+ font-size: 0.95rem;
1288
+ }
1289
+
1290
+ .btn {
1291
+ min-height: 54px;
1292
+ border-radius: 18px;
1293
+ font-size: 0.98rem;
1294
+ box-shadow:
1295
+ 0 18px 34px rgba(59, 130, 246, 0.26),
1296
+ inset 0 1px 0 rgba(255, 255, 255, 0.46),
1297
+ inset 0 -12px 24px rgba(37, 99, 235, 0.24);
1298
  }
1299
 
1300
  .status {
1301
+ justify-content: center;
1302
+ min-height: 42px;
1303
+ padding: 0.58rem 0.75rem;
1304
+ border-radius: 999px;
1305
+ font-size: 0.78rem;
1306
+ text-align: center;
1307
+ line-height: 1.25;
1308
+ white-space: normal;
1309
  }
1310
 
1311
+ .connect-tip {
1312
+ gap: 0.65rem;
1313
+ margin-top: 0;
1314
+ padding: 0.72rem 0.85rem;
1315
+ border-radius: 18px;
1316
+ font-size: 0.8rem;
1317
+ line-height: 1.3;
1318
+ background: linear-gradient(135deg, rgba(96, 165, 250, 0.12), rgba(255, 255, 255, 0.74));
1319
  }
1320
 
1321
+ .connect-tip i {
1322
+ font-size: 1.1rem;
 
1323
  }
1324
 
1325
+ .chat-header {
1326
+ min-height: 92px;
1327
+ padding: 1rem;
1328
+ border-radius: 0 0 22px 22px;
1329
+ box-shadow: var(--shadow-medium);
1330
+ }
1331
+
1332
+ .mobile-back-btn,
1333
+ .users-close {
1334
+ display: inline-grid;
1335
+ }
1336
+
1337
+ .desktop-disconnect-btn {
1338
+ display: none;
1339
+ }
1340
+
1341
+ .chat-body {
1342
+ display: block;
1343
+ position: relative;
1344
+ flex: 1;
1345
  min-height: 0;
 
 
1346
  }
1347
 
1348
+ .messages-wrap {
1349
+ height: calc(100dvh - 92px);
 
 
1350
  }
1351
 
1352
+ .messages {
1353
+ padding: 1rem;
1354
  }
1355
 
1356
+ .message {
1357
+ max-width: 84%;
1358
  }
1359
 
1360
+ .message.system {
1361
+ max-width: 90%;
 
1362
  }
1363
 
1364
+ .users-panel {
1365
  position: fixed;
1366
  top: 0;
1367
  right: 0;
1368
  bottom: 0;
1369
  z-index: 40;
1370
  width: min(84vw, 320px);
 
 
 
1371
  border-left: 1px solid var(--border-color);
1372
  border-right: 0;
1373
+ background: rgba(255, 255, 255, 0.94);
1374
  box-shadow: var(--shadow-heavy);
1375
+ transform: translateX(105%);
1376
+ transition: transform 0.24s ease;
1377
  }
1378
 
1379
+ body.users-open .users-panel {
1380
  transform: translateX(0);
1381
  }
1382
 
1383
+ .users-close {
1384
+ color: var(--text-secondary);
1385
+ background: rgba(15, 23, 42, 0.06);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1386
  }
1387
 
1388
  .mobile-drawer-backdrop {
1389
  position: fixed;
1390
  inset: 0;
1391
  z-index: 35;
 
1392
  pointer-events: none;
1393
  background: rgba(15, 23, 42, 0);
1394
+ transition: background-color 0.24s ease;
1395
  }
1396
 
1397
+ body.users-open .mobile-drawer-backdrop {
1398
  pointer-events: auto;
1399
+ background: rgba(15, 23, 42, 0.34);
1400
  }
1401
 
1402
  .input-area {
1403
+ grid-template-columns: 46px minmax(0, 1fr) 52px;
1404
+ gap: 0.6rem;
1405
  padding: 0.75rem;
1406
+ padding-bottom: calc(0.75rem + var(--safe-bottom));
1407
  }
1408
 
1409
+ .file-input-label {
1410
+ width: 46px;
1411
+ height: 46px;
 
 
1412
  }
1413
 
1414
+ .send-btn {
1415
+ width: 52px;
1416
+ height: 52px;
 
 
 
1417
  }
1418
+ }
1419
 
1420
+ @media (max-width: 420px) {
1421
+ .connect-screen {
1422
+ gap: 0.74rem;
1423
+ padding: 0.85rem 0.9rem;
 
 
 
1424
  }
1425
 
1426
+ .brand-bar h1 {
1427
+ font-size: 1.42rem;
1428
+ }
1429
+
1430
+ .welcome-copy h2 {
1431
+ font-size: 1.32rem;
1432
+ }
1433
+
1434
+ .welcome-copy p {
1435
+ font-size: 0.85rem;
1436
+ }
1437
+
1438
+ .welcome-art {
1439
+ width: min(48vw, 170px);
1440
+ }
1441
+
1442
+ .message {
1443
+ max-width: 90%;
1444
+ padding: 0.82rem 0.92rem;
1445
+ }
1446
+
1447
+ .room-meta h2 {
1448
+ font-size: 1.15rem;
1449
  }
1450
  }
1451
 
1452
+ @media (max-width: 420px) and (max-height: 700px) {
1453
+ .connect-screen {
1454
+ gap: 0.56rem;
1455
+ padding: 0.6rem 0.75rem;
1456
  }
1457
 
1458
+ .brand-mark {
1459
+ width: 38px;
1460
+ height: 38px;
1461
+ border-radius: 13px;
1462
+ font-size: 1.05rem;
1463
  }
1464
 
1465
+ .brand-bar h1 {
1466
+ font-size: 1.35rem;
1467
  }
1468
 
1469
+ .brand-bar p {
1470
+ font-size: 0.8rem;
1471
  }
1472
 
1473
+ .welcome-art {
1474
+ width: min(38vw, 132px);
1475
  }
1476
 
1477
+ .welcome-copy h2 {
1478
+ font-size: 1.18rem;
1479
  }
1480
 
1481
+ .welcome-copy p {
1482
+ font-size: 0.8rem;
 
1483
  }
1484
 
1485
+ .connection-form {
1486
+ gap: 0.5rem;
1487
  }
1488
 
1489
+ .field-shell {
1490
+ min-height: 46px;
1491
+ border-radius: 15px;
1492
  }
 
1493
 
1494
+ .btn {
1495
+ min-height: 46px;
1496
+ font-size: 0.92rem;
1497
  }
1498
 
1499
+ .status {
1500
+ min-height: 40px;
1501
+ padding: 0.5rem 0.65rem;
1502
+ font-size: 0.78rem;
1503
  }
1504
 
1505
+ .connect-tip {
1506
+ padding: 0.55rem 0.65rem;
1507
+ font-size: 0.76rem;
1508
  }
1509
+ }
1510
 
1511
+ @media (max-width: 360px) and (max-height: 640px) {
1512
+ .welcome-art {
1513
+ width: 110px;
1514
+ }
1515
+
1516
+ .connect-tip {
1517
+ display: none;
1518
  }
1519
  }
1520
 
1521
+ @media (prefers-reduced-motion: reduce) {
1522
+ *,
1523
+ *::before,
1524
+ *::after {
1525
+ animation-duration: 0.01ms !important;
1526
+ animation-iteration-count: 1 !important;
1527
+ scroll-behavior: auto !important;
1528
+ transition-duration: 0.01ms !important;
1529
+ }
1530
+ }
static/main.js CHANGED
@@ -1,417 +1,377 @@
1
- class TriChat {
2
- constructor() {
3
- this.ws = null;
4
- this.username = '';
5
- this.room = 'global';
6
- this.isConnected = false;
7
-
8
- this.initElements();
9
- this.bindEvents();
10
- this.setupMessageInputResize();
11
- }
12
-
13
- initElements() {
14
- this.elements = {
15
- usernameInput: document.getElementById('usernameInput'),
16
- roomInput: document.getElementById('roomInput'),
17
- connectBtn: document.getElementById('connectBtn'),
18
- disconnectBtn: document.getElementById('disconnectBtn'),
19
- status: document.getElementById('status'),
20
- messages: document.getElementById('messages'),
21
- messageInput: document.getElementById('messageInput'),
22
- sendBtn: document.getElementById('sendBtn'),
23
- fileInput: document.getElementById('fileInput'),
24
- inputArea: document.getElementById('inputArea'),
25
- userList: document.getElementById('userList')
26
- };
27
- }
28
 
29
- setupMessageInputResize() {
30
- this.elements.messageInput.addEventListener('input', () => {
31
- this.elements.messageInput.style.height = 'auto';
32
- this.elements.messageInput.style.height = Math.min(this.elements.messageInput.scrollHeight, 100) + 'px';
33
- });
34
- }
35
-
36
- bindEvents() {
37
- this.elements.connectBtn.addEventListener('click', () => this.connect());
38
- this.elements.disconnectBtn.addEventListener('click', () => this.disconnect());
39
- this.elements.sendBtn.addEventListener('click', () => this.sendMessage());
40
- this.elements.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
41
-
42
- this.elements.messageInput.addEventListener('keypress', (e) => {
43
- if (e.key === 'Enter' && !e.shiftKey) {
44
- e.preventDefault();
45
- this.sendMessage();
46
- }
47
- });
48
-
49
- this.elements.usernameInput.addEventListener('keypress', (e) => {
50
- if (e.key === 'Enter') {
51
- this.connect();
52
- }
53
- });
54
- }
55
-
56
- async connect() {
57
- const username = this.elements.usernameInput.value.trim();
58
- const room = this.elements.roomInput.value.trim() || 'global';
59
-
60
- if (!username) {
61
- this.showNotification('Please enter your name', 'error');
62
- this.elements.usernameInput.focus();
63
- return;
64
- }
65
-
66
- this.username = username;
67
- this.room = room;
68
-
69
- try {
70
- this.updateStatus('Connecting...', 'neutral');
71
-
72
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
73
- const wsUrl = `${protocol}//${window.location.host}/ws/${room}?username=${encodeURIComponent(username)}`;
74
-
75
- this.ws = new WebSocket(wsUrl);
76
-
77
- this.ws.onopen = () => {
78
- this.isConnected = true;
79
- this.updateUI();
80
- this.updateStatus(`Connected to ${room}`, 'connected');
81
- this.showNotification('Successfully connected!', 'success');
82
- };
83
-
84
- this.ws.onmessage = (event) => {
85
- const message = JSON.parse(event.data);
86
- this.displayMessage(message);
87
- if (message.type === 'user_list') {
88
- this.updateUserList(message.users);
89
- }
90
- };
91
-
92
- this.ws.onclose = () => {
93
- this.isConnected = false;
94
- this.updateUI();
95
- this.updateStatus('Disconnected', 'disconnected');
96
- };
97
-
98
- this.ws.onerror = (error) => {
99
- console.error('WebSocket error:', error);
100
- this.updateStatus('Connection error', 'disconnected');
101
- this.showNotification('Connection failed', 'error');
102
- };
103
-
104
- } catch (error) {
105
- console.error('Connection failed:', error);
106
- this.updateStatus('Failed to connect', 'disconnected');
107
- this.showNotification('Failed to connect', 'error');
108
- }
109
- }
110
-
111
- disconnect() {
112
- if (this.ws) {
113
- this.ws.close();
114
- }
115
- }
116
-
117
- updateUI() {
118
- if (this.isConnected) {
119
- this.elements.connectBtn.classList.add('is-hidden');
120
- this.elements.disconnectBtn.classList.remove('is-hidden');
121
- this.elements.inputArea.classList.remove('is-hidden');
122
- this.elements.usernameInput.disabled = true;
123
- this.elements.roomInput.disabled = true;
124
- this.elements.messageInput.focus();
125
- } else {
126
- this.elements.connectBtn.classList.remove('is-hidden');
127
- this.elements.disconnectBtn.classList.add('is-hidden');
128
- this.elements.inputArea.classList.add('is-hidden');
129
- this.elements.usernameInput.disabled = false;
130
- this.elements.roomInput.disabled = false;
131
- this.elements.messages.innerHTML = '';
132
- this.elements.userList.innerHTML = '';
133
- }
134
- }
135
-
136
- updateStatus(text, type) {
137
- this.elements.status.innerHTML = `
138
- <i class="fas ${type === 'connected' ? 'fa-check-circle' : type === 'disconnected' ? 'fa-times-circle' : 'fa-info-circle'}"></i>
139
- ${text}
140
- `;
141
- this.elements.status.className = 'status ' + type;
142
- }
143
 
144
- showNotification(message, type) {
145
- // Simple notification - you could enhance this with a toast library
146
- console.log(`${type.toUpperCase()}: ${message}`);
147
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
- updateUserList(users) {
150
- this.elements.userList.innerHTML = '';
151
- users.forEach(user => {
152
- const li = document.createElement('li');
153
- li.textContent = user;
154
- this.elements.userList.appendChild(li);
155
- });
156
- }
157
-
158
- sendMessage() {
159
- const text = this.elements.messageInput.value.trim();
160
- if (!text || !this.isConnected) return;
161
-
162
- const message = {
163
- type: 'text',
164
- username: this.username,
165
- text: text,
166
- timestamp: new Date().toISOString(),
167
- room: this.room
168
- };
169
-
170
- this.ws.send(JSON.stringify(message));
171
- this.elements.messageInput.value = '';
172
- this.elements.messageInput.style.height = 'auto';
173
  }
174
-
175
- async handleFileSelect(event) {
176
- const file = event.target.files[0];
177
- if (!file) return;
178
-
179
- if (file.size > 5 * 1024 * 1024) {
180
- this.showNotification('File size must be less than 5MB', 'error');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  return;
182
  }
183
-
184
- try {
185
- const base64Data = await this.fileToBase64(file);
186
-
187
- const message = {
188
- type: 'file',
189
- username: this.username,
190
- fileName: file.name,
191
- fileType: file.type,
192
- fileSize: file.size,
193
- fileData: base64Data,
194
- timestamp: new Date().toISOString(),
195
- room: this.room
196
- };
197
-
198
- this.ws.send(JSON.stringify(message));
199
- event.target.value = '';
200
- this.showNotification('File uploaded successfully', 'success');
201
-
202
- } catch (error) {
203
- console.error('File upload error:', error);
204
- this.showNotification('Failed to upload file', 'error');
205
- }
206
- }
207
-
208
- fileToBase64(file) {
209
- return new Promise((resolve, reject) => {
210
- const reader = new FileReader();
211
- reader.onload = () => resolve(reader.result.split(',')[1]);
212
- reader.onerror = reject;
213
- reader.readAsDataURL(file);
214
- });
215
- }
216
-
217
- displayMessage(message) {
218
- const messageElement = document.createElement('div');
219
- messageElement.className = 'message';
220
-
221
- if (message.type === 'system') {
222
- messageElement.className += ' system';
223
- messageElement.innerHTML = `
224
- <div class="message-content">
225
- <i class="fas fa-info-circle"></i>
226
- ${this.escapeHtml(message.message)}
227
- </div>
228
- `;
229
- } else {
230
- if (message.username === this.username) {
231
- messageElement.className += ' own';
232
- }
233
-
234
- const timestamp = new Date(message.timestamp).toLocaleTimeString([], {
235
- hour: '2-digit',
236
- minute: '2-digit'
237
- });
238
-
239
- if (message.type === 'text') {
240
- messageElement.innerHTML = `
241
- <div class="message-header">
242
- <span class="username">${this.escapeHtml(message.username)}</span>
243
- <span class="timestamp">${timestamp}</span>
244
- </div>
245
- <div class="message-content">${this.escapeHtml(message.text)}</div>
246
- `;
247
- } else if (message.type === 'file') {
248
- const downloadUrl = 'data:' + message.fileType + ';base64,' + message.fileData;
249
- let preview = '';
250
- let fileIcon = 'fas fa-file';
251
-
252
- if (message.fileType.startsWith('image/')) {
253
- preview = `<div class="file-preview"><img src="${downloadUrl}" alt="${message.fileName}"></div>`;
254
- fileIcon = 'fas fa-image';
255
- } else if (message.fileType.startsWith('video/')) {
256
- fileIcon = 'fas fa-video';
257
- } else if (message.fileType.startsWith('audio/')) {
258
- fileIcon = 'fas fa-music';
259
- } else if (message.fileType.includes('pdf')) {
260
- fileIcon = 'fas fa-file-pdf';
261
- } else if (message.fileType.includes('document') || message.fileType.includes('text')) {
262
- fileIcon = 'fas fa-file-alt';
263
- }
264
-
265
- messageElement.innerHTML = `
266
- <div class="message-header">
267
- <span class="username">${this.escapeHtml(message.username)}</span>
268
- <span class="timestamp">${timestamp}</span>
269
- </div>
270
- <div class="message-content">
271
- <div class="file-summary">
272
- <i class="${fileIcon}"></i>
273
- <strong>${this.escapeHtml(message.fileName)}</strong>
274
- </div>
275
- <div class="file-size">
276
- ${this.formatFileSize(message.fileSize)}
277
- </div>
278
- ${preview}
279
- <a href="${downloadUrl}" download="${message.fileName}" class="file-download">
280
- <i class="fas fa-download"></i>
281
- Download
282
- </a>
283
- </div>
284
- `;
285
- }
286
- }
287
-
288
- this.elements.messages.appendChild(messageElement);
289
- this.elements.messages.scrollTop = this.elements.messages.scrollHeight;
290
-
291
- // Add subtle animation
292
- messageElement.style.opacity = '0';
293
- messageElement.style.transform = 'translateY(10px)';
294
-
295
- requestAnimationFrame(() => {
296
- messageElement.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
297
- messageElement.style.opacity = '1';
298
- messageElement.style.transform = 'translateY(0)';
299
- });
300
- }
301
-
302
- escapeHtml(text) {
303
- const div = document.createElement('div');
304
- div.textContent = text;
305
- return div.innerHTML;
306
- }
307
-
308
- formatFileSize(bytes) {
309
- if (bytes === 0) return '0 Bytes';
310
- const k = 1024;
311
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
312
- const i = Math.floor(Math.log(bytes) / Math.log(k));
313
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
314
- }
315
  }
316
-
317
- // Initialize the chat application
318
- document.addEventListener('DOMContentLoaded', () => {
319
- const triChat = new TriChat();
320
-
321
- // Add some welcome messages for demo
322
- if (!triChat.isConnected) {
323
- setTimeout(() => {
324
- const welcomeMessage = document.createElement('div');
325
- welcomeMessage.className = 'message system';
326
- welcomeMessage.innerHTML = `
327
- <div class="message-content">
328
- <i class="fas fa-rocket"></i>
329
- Welcome to Tri-Chat! Connect with your name to start chatting.
330
- </div>
331
- `;
332
- triChat.elements.messages.appendChild(welcomeMessage);
333
- }, 500);
334
- }
335
- });
336
 
337
- (function () {
338
- const mobileQuery = window.matchMedia('(max-width: 768px)');
339
-
340
- function setupMobileControls() {
341
- const headerTop = document.querySelector('.header-top');
342
- const sidebar = document.querySelector('.sidebar');
343
- const sendBtn = document.getElementById('sendBtn');
344
-
345
- if (!headerTop || !sidebar) return;
346
-
347
- let usersToggle = document.querySelector('.mobile-users-toggle');
348
- if (!usersToggle) {
349
- usersToggle = document.createElement('button');
350
- usersToggle.type = 'button';
351
- usersToggle.className = 'mobile-users-toggle';
352
- usersToggle.setAttribute('aria-label', 'Show online users');
353
- usersToggle.setAttribute('aria-controls', 'onlineUsersPanel');
354
- usersToggle.setAttribute('aria-expanded', 'false');
355
- usersToggle.innerHTML = '<i class="fas fa-users"></i>';
356
- headerTop.appendChild(usersToggle);
357
  }
 
 
 
 
 
358
 
359
- let backdrop = document.querySelector('.mobile-drawer-backdrop');
360
- if (!backdrop) {
361
- backdrop = document.createElement('div');
362
- backdrop.className = 'mobile-drawer-backdrop';
363
- document.body.appendChild(backdrop);
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  }
 
365
 
366
- sidebar.id = 'onlineUsersPanel';
 
 
 
 
367
 
368
- if (sendBtn && !sendBtn.querySelector('span')) {
369
- const label = Array.from(sendBtn.childNodes).find((node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim());
370
- if (label) {
371
- const text = label.textContent.trim();
372
- label.textContent = '';
373
- const span = document.createElement('span');
374
- span.textContent = text;
375
- sendBtn.appendChild(span);
376
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  }
378
 
379
- const closeDrawer = () => {
380
- document.body.classList.remove('mobile-users-open');
381
- usersToggle.setAttribute('aria-expanded', 'false');
382
- };
383
 
384
- const toggleDrawer = () => {
385
- const nextState = !document.body.classList.contains('mobile-users-open');
386
- document.body.classList.toggle('mobile-users-open', nextState);
387
- usersToggle.setAttribute('aria-expanded', String(nextState));
388
- };
 
 
 
 
 
389
 
390
- usersToggle.addEventListener('click', toggleDrawer);
391
- backdrop.addEventListener('click', closeDrawer);
392
- document.addEventListener('keydown', (event) => {
393
- if (event.key === 'Escape') closeDrawer();
 
 
 
 
 
 
 
 
 
394
  });
 
395
 
396
- const userList = document.getElementById('userList');
397
- if (userList) {
398
- userList.addEventListener('click', () => {
399
- if (mobileQuery.matches) closeDrawer();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  }
 
 
 
402
  }
403
 
404
- function syncViewportState() {
405
- document.body.classList.toggle('is-mobile-layout', mobileQuery.matches);
406
- if (!mobileQuery.matches) {
407
- document.body.classList.remove('mobile-users-open');
408
- }
 
 
409
  }
410
 
411
- document.addEventListener('DOMContentLoaded', () => {
412
- setupMobileControls();
413
- syncViewportState();
414
- mobileQuery.addEventListener('change', syncViewportState);
415
- });
416
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
 
 
 
 
 
1
+ class TriChat {
2
+ constructor() {
3
+ this.ws = null;
4
+ this.username = "";
5
+ this.room = "global";
6
+ this.isConnected = false;
7
+ this.users = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ this.initElements();
10
+ this.bindEvents();
11
+ this.setupMessageInputResize();
12
+ this.updateFieldStates();
13
+ this.showWelcomeMessage();
14
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ initElements() {
17
+ this.elements = {
18
+ appShell: document.getElementById("appShell"),
19
+ connectScreen: document.getElementById("connectScreen"),
20
+ chatScreen: document.getElementById("chatScreen"),
21
+ connectionForm: document.getElementById("connectionForm"),
22
+ messageForm: document.getElementById("messageForm"),
23
+ usernameInput: document.getElementById("usernameInput"),
24
+ roomInput: document.getElementById("roomInput"),
25
+ connectBtn: document.getElementById("connectBtn"),
26
+ disconnectBtn: document.getElementById("disconnectBtn"),
27
+ mobileBackBtn: document.getElementById("mobileBackBtn"),
28
+ status: document.getElementById("status"),
29
+ roomTitle: document.getElementById("roomTitle"),
30
+ onlineCount: document.getElementById("onlineCount"),
31
+ messages: document.getElementById("messages"),
32
+ messageInput: document.getElementById("messageInput"),
33
+ sendBtn: document.getElementById("sendBtn"),
34
+ fileInput: document.getElementById("fileInput"),
35
+ userList: document.getElementById("userList"),
36
+ emptyUsers: document.getElementById("emptyUsers"),
37
+ usersToggle: document.getElementById("usersToggle"),
38
+ usersClose: document.getElementById("usersClose"),
39
+ drawerBackdrop: document.getElementById("drawerBackdrop")
40
+ };
41
+ }
42
 
43
+ bindEvents() {
44
+ this.elements.connectionForm.addEventListener("submit", (event) => {
45
+ event.preventDefault();
46
+ this.connect();
47
+ });
48
+
49
+ this.elements.messageForm.addEventListener("submit", (event) => {
50
+ event.preventDefault();
51
+ this.sendMessage();
52
+ });
53
+
54
+ this.elements.disconnectBtn.addEventListener("click", () => this.disconnect());
55
+ this.elements.mobileBackBtn.addEventListener("click", () => this.disconnect());
56
+ this.elements.fileInput.addEventListener("change", (event) => this.handleFileSelect(event));
57
+ this.elements.usersToggle.addEventListener("click", () => this.toggleUsersPanel());
58
+ this.elements.usersClose.addEventListener("click", () => this.closeUsersPanel());
59
+ this.elements.drawerBackdrop.addEventListener("click", () => this.closeUsersPanel());
60
+
61
+ this.elements.messageInput.addEventListener("keydown", (event) => {
62
+ if (event.key === "Enter" && !event.shiftKey) {
63
+ event.preventDefault();
64
+ this.sendMessage();
 
 
65
  }
66
+ });
67
+
68
+ this.elements.usernameInput.addEventListener("input", () => this.updateFieldStates());
69
+ this.elements.roomInput.addEventListener("input", () => this.updateFieldStates());
70
+
71
+ document.addEventListener("keydown", (event) => {
72
+ if (event.key === "Escape") this.closeUsersPanel();
73
+ });
74
+ }
75
+
76
+ setupMessageInputResize() {
77
+ this.elements.messageInput.addEventListener("input", () => {
78
+ this.elements.messageInput.style.height = "auto";
79
+ this.elements.messageInput.style.height = `${Math.min(this.elements.messageInput.scrollHeight, 136)}px`;
80
+ });
81
+ }
82
+
83
+ updateFieldStates() {
84
+ const usernameShell = this.elements.usernameInput.closest(".field-shell");
85
+ const roomShell = this.elements.roomInput.closest(".field-shell");
86
+
87
+ usernameShell.classList.toggle("has-value", Boolean(this.elements.usernameInput.value.trim()));
88
+ roomShell.classList.toggle("has-value", Boolean(this.elements.roomInput.value.trim()));
89
+ }
90
+
91
+ async connect() {
92
+ const username = this.elements.usernameInput.value.trim();
93
+ const room = this.elements.roomInput.value.trim() || "global";
94
+
95
+ if (!username) {
96
+ this.updateStatus("Please enter your name", "disconnected");
97
+ this.elements.usernameInput.focus();
98
+ return;
99
+ }
100
+
101
+ this.username = username;
102
+ this.room = room;
103
+ this.elements.roomTitle.textContent = room;
104
+
105
+ try {
106
+ this.updateStatus("Connecting...", "neutral");
107
+ this.elements.connectBtn.disabled = true;
108
+
109
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
110
+ const wsUrl = `${protocol}//${window.location.host}/ws/${encodeURIComponent(room)}?username=${encodeURIComponent(username)}`;
111
+
112
+ this.ws = new WebSocket(wsUrl);
113
+
114
+ this.ws.onopen = () => {
115
+ this.isConnected = true;
116
+ this.elements.connectBtn.disabled = false;
117
+ this.updateStatus(`Connected to ${room}`, "connected");
118
+ this.updateUI();
119
+ this.elements.messageInput.focus();
120
+ };
121
+
122
+ this.ws.onmessage = (event) => {
123
+ const message = JSON.parse(event.data);
124
+
125
+ if (message.type === "user_list") {
126
+ this.updateUserList(message.users || []);
127
  return;
128
  }
129
+
130
+ this.displayMessage(message);
131
+ };
132
+
133
+ this.ws.onclose = () => {
134
+ this.isConnected = false;
135
+ this.elements.connectBtn.disabled = false;
136
+ this.updateStatus("Disconnected", "disconnected");
137
+ this.updateUI();
138
+ };
139
+
140
+ this.ws.onerror = (error) => {
141
+ console.error("WebSocket error:", error);
142
+ this.elements.connectBtn.disabled = false;
143
+ this.updateStatus("Connection error", "disconnected");
144
+ };
145
+ } catch (error) {
146
+ console.error("Connection failed:", error);
147
+ this.elements.connectBtn.disabled = false;
148
+ this.updateStatus("Failed to connect", "disconnected");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  }
150
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ disconnect() {
153
+ if (this.ws) {
154
+ this.ws.close();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  }
156
+ }
157
+
158
+ updateUI() {
159
+ document.body.classList.remove("users-open");
160
+ this.elements.usersToggle.setAttribute("aria-expanded", "false");
161
 
162
+ if (this.isConnected) {
163
+ this.elements.appShell.classList.add("is-chatting");
164
+ this.elements.connectScreen.classList.add("is-hidden");
165
+ this.elements.chatScreen.classList.remove("is-hidden");
166
+ this.elements.chatScreen.classList.add("is-connected-preview");
167
+ this.elements.usernameInput.disabled = true;
168
+ this.elements.roomInput.disabled = true;
169
+ } else {
170
+ this.elements.appShell.classList.remove("is-chatting");
171
+ this.elements.connectScreen.classList.remove("is-hidden");
172
+ this.elements.chatScreen.classList.add("is-hidden");
173
+ this.elements.chatScreen.classList.remove("is-connected-preview");
174
+ this.elements.usernameInput.disabled = false;
175
+ this.elements.roomInput.disabled = false;
176
+ this.elements.messages.innerHTML = "";
177
+ this.users = [];
178
+ this.updateUserList([]);
179
+ this.showWelcomeMessage();
180
  }
181
+ }
182
 
183
+ updateStatus(text, type) {
184
+ const icon = type === "connected" ? "fa-check-circle" : type === "disconnected" ? "fa-times-circle" : "fa-info-circle";
185
+ this.elements.status.innerHTML = `<i class="fas ${icon}"></i><span>${this.escapeHtml(text)}</span>`;
186
+ this.elements.status.className = `status ${type}`;
187
+ }
188
 
189
+ updateUserList(users) {
190
+ this.users = users;
191
+ this.elements.userList.innerHTML = "";
192
+
193
+ users.forEach((user) => {
194
+ const li = document.createElement("li");
195
+ li.textContent = user;
196
+ this.elements.userList.appendChild(li);
197
+ });
198
+
199
+ const count = users.length;
200
+ this.elements.onlineCount.textContent = `${count} ${count === 1 ? "member" : "members"} online`;
201
+ this.elements.emptyUsers.classList.toggle("is-hidden", count > 0);
202
+ }
203
+
204
+ sendMessage() {
205
+ const text = this.elements.messageInput.value.trim();
206
+ if (!text || !this.isConnected) return;
207
+
208
+ this.ws.send(JSON.stringify({
209
+ type: "text",
210
+ username: this.username,
211
+ text,
212
+ timestamp: new Date().toISOString(),
213
+ room: this.room
214
+ }));
215
+
216
+ this.elements.messageInput.value = "";
217
+ this.elements.messageInput.style.height = "auto";
218
+ }
219
+
220
+ async handleFileSelect(event) {
221
+ const file = event.target.files[0];
222
+ if (!file || !this.isConnected) return;
223
+
224
+ if (file.size > 5 * 1024 * 1024) {
225
+ this.updateStatus("File size must be less than 5MB", "disconnected");
226
+ event.target.value = "";
227
+ return;
228
  }
229
 
230
+ try {
231
+ const base64Data = await this.fileToBase64(file);
 
 
232
 
233
+ this.ws.send(JSON.stringify({
234
+ type: "file",
235
+ username: this.username,
236
+ fileName: file.name,
237
+ fileType: file.type,
238
+ fileSize: file.size,
239
+ fileData: base64Data,
240
+ timestamp: new Date().toISOString(),
241
+ room: this.room
242
+ }));
243
 
244
+ event.target.value = "";
245
+ } catch (error) {
246
+ console.error("File upload error:", error);
247
+ this.updateStatus("Failed to upload file", "disconnected");
248
+ }
249
+ }
250
+
251
+ fileToBase64(file) {
252
+ return new Promise((resolve, reject) => {
253
+ const reader = new FileReader();
254
+ reader.onload = () => resolve(reader.result.split(",")[1]);
255
+ reader.onerror = reject;
256
+ reader.readAsDataURL(file);
257
  });
258
+ }
259
 
260
+ displayMessage(message) {
261
+ if (message.type === "error") {
262
+ this.updateStatus(message.message || "Something went wrong", "disconnected");
263
+ return;
264
+ }
265
+
266
+ const messageElement = document.createElement("div");
267
+ messageElement.className = "message";
268
+
269
+ if (message.type === "system") {
270
+ messageElement.classList.add("system");
271
+ messageElement.innerHTML = `
272
+ <div class="message-content">
273
+ <i class="fas fa-info-circle"></i>
274
+ ${this.escapeHtml(message.message)}
275
+ </div>
276
+ `;
277
+ } else {
278
+ if (message.username === this.username) {
279
+ messageElement.classList.add("own");
280
+ }
281
+
282
+ const timestamp = new Date(message.timestamp).toLocaleTimeString([], {
283
+ hour: "2-digit",
284
+ minute: "2-digit"
285
  });
286
+
287
+ if (message.type === "text") {
288
+ messageElement.innerHTML = `
289
+ <div class="message-header">
290
+ <span class="username">${this.escapeHtml(message.username)}</span>
291
+ <span class="timestamp">${timestamp}</span>
292
+ </div>
293
+ <div class="message-content">${this.escapeHtml(message.text)}</div>
294
+ `;
295
+ } else if (message.type === "file") {
296
+ const downloadUrl = `data:${message.fileType};base64,${message.fileData}`;
297
+ const preview = message.fileType.startsWith("image/")
298
+ ? `<div class="file-preview"><img src="${downloadUrl}" alt="${this.escapeHtml(message.fileName)}"></div>`
299
+ : "";
300
+ const fileIcon = this.getFileIcon(message.fileType);
301
+
302
+ messageElement.innerHTML = `
303
+ <div class="message-header">
304
+ <span class="username">${this.escapeHtml(message.username)}</span>
305
+ <span class="timestamp">${timestamp}</span>
306
+ </div>
307
+ <div class="message-content">
308
+ <div class="file-summary">
309
+ <i class="${fileIcon}"></i>
310
+ <strong>${this.escapeHtml(message.fileName)}</strong>
311
+ </div>
312
+ <div class="file-size">${this.formatFileSize(message.fileSize)}</div>
313
+ ${preview}
314
+ <a href="${downloadUrl}" download="${this.escapeHtml(message.fileName)}" class="file-download">
315
+ <i class="fas fa-download"></i>
316
+ Download
317
+ </a>
318
+ </div>
319
+ `;
320
+ }
321
  }
322
+
323
+ this.elements.messages.appendChild(messageElement);
324
+ this.elements.messages.scrollTop = this.elements.messages.scrollHeight;
325
  }
326
 
327
+ getFileIcon(fileType) {
328
+ if (fileType.startsWith("image/")) return "fas fa-image";
329
+ if (fileType.startsWith("video/")) return "fas fa-video";
330
+ if (fileType.startsWith("audio/")) return "fas fa-music";
331
+ if (fileType.includes("pdf")) return "fas fa-file-pdf";
332
+ if (fileType.includes("document") || fileType.includes("text")) return "fas fa-file-alt";
333
+ return "fas fa-file";
334
  }
335
 
336
+ showWelcomeMessage() {
337
+ if (this.elements.messages.children.length > 0) return;
338
+
339
+ const welcomeMessage = document.createElement("div");
340
+ welcomeMessage.className = "message system";
341
+ welcomeMessage.innerHTML = `
342
+ <div class="message-content">
343
+ <i class="fas fa-rocket"></i>
344
+ Connect with your name to start chatting.
345
+ </div>
346
+ `;
347
+ this.elements.messages.appendChild(welcomeMessage);
348
+ }
349
+
350
+ toggleUsersPanel() {
351
+ const isOpen = document.body.classList.toggle("users-open");
352
+ this.elements.usersToggle.setAttribute("aria-expanded", String(isOpen));
353
+ }
354
+
355
+ closeUsersPanel() {
356
+ document.body.classList.remove("users-open");
357
+ this.elements.usersToggle.setAttribute("aria-expanded", "false");
358
+ }
359
+
360
+ escapeHtml(text) {
361
+ const div = document.createElement("div");
362
+ div.textContent = text || "";
363
+ return div.innerHTML;
364
+ }
365
+
366
+ formatFileSize(bytes) {
367
+ if (bytes === 0) return "0 Bytes";
368
+ const k = 1024;
369
+ const sizes = ["Bytes", "KB", "MB", "GB"];
370
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
371
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
372
+ }
373
+ }
374
 
375
+ document.addEventListener("DOMContentLoaded", () => {
376
+ new TriChat();
377
+ });
templates/index.html CHANGED
@@ -1,4 +1,4 @@
1
- <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
@@ -8,74 +8,129 @@
8
  <link href="/static/main.css" rel="stylesheet">
9
  </head>
10
  <body>
11
- <div class="header">
12
- <div class="header-top">
13
- <div class="logo">
14
- <i class="fas fa-comments"></i>
15
- <h1>Tri-Chat</h1>
 
 
 
 
 
16
  </div>
17
- </div>
18
-
19
- <div class="connection-form">
20
- <div class="input-group">
21
- <label for="usernameInput">Your Name</label>
22
- <input type="text" id="usernameInput" placeholder="Enter your name" maxlength="20">
 
 
 
 
 
 
23
  </div>
24
- <div class="input-group">
25
- <label for="roomInput">Room</label>
26
- <input type="text" id="roomInput" placeholder="global" maxlength="30">
 
27
  </div>
28
- <button class="btn" id="connectBtn">
29
- <i class="fas fa-plug"></i>
30
- Connect
31
- </button>
32
- <button class="btn btn-secondary is-hidden" id="disconnectBtn">
33
- <i class="fas fa-sign-out-alt"></i>
34
- Disconnect
35
- </button>
36
- </div>
37
-
38
- <div class="status neutral" id="status">
39
- <i class="fas fa-info-circle"></i>
40
- Enter your name and click Connect to start chatting
41
- </div>
42
- </div>
43
-
44
- <div class="main-content">
45
- <div class="sidebar">
46
- <div class="sidebar-header">
47
- <h3>
48
- <i class="fas fa-users"></i>
49
- Online
50
- </h3>
 
 
 
 
 
 
51
  </div>
52
- <div class="sidebar-content">
53
- <ul class="user-list" id="userList"></ul>
 
 
54
  </div>
55
- </div>
56
-
57
- <div class="chat-container">
58
- <div class="messages" id="messages"></div>
59
-
60
- <div class="input-area is-hidden" id="inputArea">
61
- <div class="input-row">
62
- <div class="file-input-wrapper">
63
- <input type="file" id="fileInput" accept="*/*">
64
- <label for="fileInput" class="file-input-label">
65
- <i class="fas fa-paperclip"></i>
66
- </label>
67
- </div>
68
- <textarea id="messageInput" placeholder="Type your message..." maxlength="500" rows="1"></textarea>
69
- <button class="btn" id="sendBtn">
70
- <i class="fas fa-paper-plane"></i>
71
- Send
 
 
 
 
 
72
  </button>
73
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  </div>
75
- </div>
76
- </div>
 
 
77
  <script src="/static/main.js"></script>
78
  </body>
79
  </html>
80
-
81
-
 
1
+ <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
 
8
  <link href="/static/main.css" rel="stylesheet">
9
  </head>
10
  <body>
11
+ <main class="app-shell" id="appShell">
12
+ <section class="connect-screen" id="connectScreen" aria-labelledby="connectTitle">
13
+ <div class="brand-bar">
14
+ <div class="brand-mark" aria-hidden="true">
15
+ <i class="fas fa-comments"></i>
16
+ </div>
17
+ <div>
18
+ <h1>Tri-Chat</h1>
19
+ <p>Talk together. Stay connected.</p>
20
+ </div>
21
  </div>
22
+
23
+ <div class="welcome-art" aria-hidden="true">
24
+ <div class="bubble bubble-primary">
25
+ <span></span>
26
+ <span></span>
27
+ <span></span>
28
+ </div>
29
+ <div class="bubble bubble-secondary">
30
+ <span></span>
31
+ <span></span>
32
+ <span></span>
33
+ </div>
34
  </div>
35
+
36
+ <div class="welcome-copy">
37
+ <h2 id="connectTitle">Welcome to Tri-Chat!</h2>
38
+ <p>Connect to a room and start chatting with others instantly.</p>
39
  </div>
40
+
41
+ <form class="connection-form" id="connectionForm">
42
+ <div class="input-group">
43
+ <label for="usernameInput">Your Name</label>
44
+ <div class="field-shell">
45
+ <i class="fas fa-user"></i>
46
+ <input type="text" id="usernameInput" placeholder="Enter your name" maxlength="20" autocomplete="name">
47
+ <i class="fas fa-check field-check" aria-hidden="true"></i>
48
+ </div>
49
+ </div>
50
+
51
+ <div class="input-group">
52
+ <label for="roomInput">Room</label>
53
+ <div class="field-shell">
54
+ <i class="fas fa-hashtag"></i>
55
+ <input type="text" id="roomInput" placeholder="global" maxlength="30" autocomplete="off">
56
+ <i class="fas fa-check field-check" aria-hidden="true"></i>
57
+ </div>
58
+ </div>
59
+
60
+ <button class="btn btn-primary" id="connectBtn" type="submit">
61
+ <i class="fas fa-plug"></i>
62
+ <span>Connect</span>
63
+ </button>
64
+ </form>
65
+
66
+ <div class="status neutral" id="status" aria-live="polite">
67
+ <i class="fas fa-info-circle"></i>
68
+ Enter your name and click Connect to start chatting
69
  </div>
70
+
71
+ <div class="connect-tip">
72
+ <i class="fas fa-lightbulb"></i>
73
+ <span>Tip: Share the room name with your friends so they can join too.</span>
74
  </div>
75
+ </section>
76
+
77
+ <section class="chat-screen is-hidden" id="chatScreen" aria-label="Chat room">
78
+ <header class="chat-header">
79
+ <button class="icon-btn mobile-back-btn" id="mobileBackBtn" type="button" aria-label="Disconnect and return to connect screen">
80
+ <i class="fas fa-arrow-left"></i>
81
+ </button>
82
+
83
+ <div class="room-meta">
84
+ <h2 id="roomTitle">global</h2>
85
+ <p>
86
+ <span class="online-dot"></span>
87
+ <span id="onlineCount">0 members online</span>
88
+ </p>
89
+ </div>
90
+
91
+ <div class="chat-actions">
92
+ <button class="icon-btn users-toggle" id="usersToggle" type="button" aria-label="Show online users" aria-controls="usersPanel" aria-expanded="false">
93
+ <i class="fas fa-users"></i>
94
+ </button>
95
+ <button class="icon-btn desktop-disconnect-btn" id="disconnectBtn" type="button" aria-label="Disconnect">
96
+ <i class="fas fa-sign-out-alt"></i>
97
  </button>
98
  </div>
99
+ </header>
100
+
101
+ <div class="chat-body">
102
+ <aside class="users-panel" id="usersPanel">
103
+ <div class="users-panel-header">
104
+ <h3>Online</h3>
105
+ <button class="icon-btn users-close" id="usersClose" type="button" aria-label="Close online users">
106
+ <i class="fas fa-times"></i>
107
+ </button>
108
+ </div>
109
+ <ul class="user-list" id="userList"></ul>
110
+ <p class="empty-users" id="emptyUsers">No one else is here yet.</p>
111
+ </aside>
112
+
113
+ <div class="messages-wrap">
114
+ <div class="messages" id="messages"></div>
115
+
116
+ <form class="input-area" id="messageForm">
117
+ <div class="file-input-wrapper">
118
+ <input type="file" id="fileInput" accept="*/*" aria-label="Attach file">
119
+ <label for="fileInput" class="file-input-label" aria-hidden="true">
120
+ <i class="fas fa-paperclip"></i>
121
+ </label>
122
+ </div>
123
+ <textarea id="messageInput" placeholder="Type a message..." maxlength="500" rows="1"></textarea>
124
+ <button class="send-btn" id="sendBtn" type="submit" aria-label="Send message">
125
+ <i class="fas fa-paper-plane"></i>
126
+ </button>
127
+ </form>
128
+ </div>
129
  </div>
130
+ </section>
131
+ </main>
132
+
133
+ <div class="mobile-drawer-backdrop" id="drawerBackdrop"></div>
134
  <script src="/static/main.js"></script>
135
  </body>
136
  </html>