watercooler 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watercooler",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "A beautiful 3D visualization of your mailbox messages as a village of coworkers",
5
5
  "type": "module",
6
6
  "main": "server.ts",
package/public/app.js CHANGED
@@ -47,6 +47,13 @@ function init() {
47
47
  controls.maxPolarAngle = Math.PI / 2 - 0.1;
48
48
  controls.minDistance = 20;
49
49
  controls.maxDistance = 80;
50
+ controls.enableZoom = true;
51
+ controls.zoomSpeed = 0.8;
52
+ controls.enablePan = false; // Disable pan on touch for better mobile UX
53
+ controls.touches = {
54
+ ONE: THREE.TOUCH.ROTATE,
55
+ TWO: THREE.TOUCH.DOLLY_PAN
56
+ };
50
57
 
51
58
  // Lighting
52
59
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
@@ -90,6 +97,18 @@ function init() {
90
97
  mouse = new THREE.Vector2();
91
98
  renderer.domElement.addEventListener('click', onHouseClick);
92
99
 
100
+ // Add touch support for mobile
101
+ renderer.domElement.addEventListener('touchstart', onHouseTouchStart, { passive: false });
102
+ renderer.domElement.addEventListener('touchend', onHouseTouchEnd, { passive: false });
103
+
104
+ // Disable context menu on mobile for better UX
105
+ renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault());
106
+
107
+ // Handle orientation change
108
+ window.addEventListener('orientationchange', () => {
109
+ setTimeout(onWindowResize, 100);
110
+ });
111
+
93
112
  animate();
94
113
  }
95
114
 
@@ -354,9 +373,21 @@ function animate() {
354
373
  }
355
374
 
356
375
  function onWindowResize() {
357
- camera.aspect = window.innerWidth / window.innerHeight;
376
+ const width = window.innerWidth;
377
+ const height = window.innerHeight;
378
+ camera.aspect = width / height;
358
379
  camera.updateProjectionMatrix();
359
- renderer.setSize(window.innerWidth, window.innerHeight);
380
+ renderer.setSize(width, height);
381
+
382
+ // Adjust camera position for better mobile view
383
+ if (width < 768) {
384
+ // On mobile, position camera slightly higher and further back
385
+ camera.position.y = Math.max(camera.position.y, 35);
386
+ camera.position.z = Math.max(camera.position.z, 45);
387
+ controls.minDistance = 30; // Prevent zooming too close on mobile
388
+ } else {
389
+ controls.minDistance = 20;
390
+ }
360
391
  }
361
392
 
362
393
  // API and UI Functions
@@ -396,7 +427,10 @@ window.toggleSendPanel = function() {
396
427
 
397
428
  window.toggleMessagesPanel = function() {
398
429
  const panel = document.getElementById('messages-panel');
430
+ const btn = document.getElementById('toggle-messages-btn');
399
431
  panel.classList.toggle('open');
432
+ btn.style.opacity = panel.classList.contains('open') ? '0' : '1';
433
+ btn.style.pointerEvents = panel.classList.contains('open') ? 'none' : 'auto';
400
434
  };
401
435
 
402
436
  function updateUI() {
@@ -529,9 +563,47 @@ async function sendMessage() {
529
563
 
530
564
  // House click handler
531
565
  function onHouseClick(event) {
532
- // Calculate mouse position
533
- mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
534
- mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
566
+ handleHouseInteraction(event.clientX, event.clientY);
567
+ }
568
+
569
+ // Touch handlers for mobile
570
+ let touchStartX = 0;
571
+ let touchStartY = 0;
572
+
573
+ function onHouseTouchStart(event) {
574
+ if (event.touches.length === 1) {
575
+ touchStartX = event.touches[0].clientX;
576
+ touchStartY = event.touches[0].clientY;
577
+ }
578
+ }
579
+
580
+ function onHouseTouchEnd(event) {
581
+ if (event.changedTouches.length === 1) {
582
+ const touchEndX = event.changedTouches[0].clientX;
583
+ const touchEndY = event.changedTouches[0].clientY;
584
+
585
+ // Check if touch moved significantly (if so, it's a drag/pan, not a tap)
586
+ const moveDistance = Math.sqrt(
587
+ Math.pow(touchEndX - touchStartX, 2) +
588
+ Math.pow(touchEndY - touchStartY, 2)
589
+ );
590
+
591
+ // Only trigger if touch didn't move much (tap vs swipe)
592
+ if (moveDistance < 20) {
593
+ handleHouseInteraction(touchEndX, touchEndY);
594
+ }
595
+ }
596
+ }
597
+
598
+ // Common house interaction handler
599
+ function handleHouseInteraction(clientX, clientY) {
600
+ // Calculate normalized device coordinates
601
+ const rect = renderer.domElement.getBoundingClientRect();
602
+ const x = ((clientX - rect.left) / rect.width) * 2 - 1;
603
+ const y = -((clientY - rect.top) / rect.height) * 2 + 1;
604
+
605
+ mouse.x = x;
606
+ mouse.y = y;
535
607
 
536
608
  raycaster.setFromCamera(mouse, camera);
537
609
 
package/public/index.html CHANGED
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
6
6
  <title>Watercooler</title>
7
7
  <style>
8
8
  * {
@@ -320,7 +320,7 @@
320
320
  font-size: 0.9rem;
321
321
  font-weight: 600;
322
322
  cursor: pointer;
323
- z-index: 99;
323
+ z-index: 101;
324
324
  transition: all 0.3s ease;
325
325
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
326
326
  }
@@ -382,6 +382,239 @@
382
382
  border-radius: 3px;
383
383
  }
384
384
 
385
+ /* Mobile responsive styles */
386
+ @media (max-width: 768px) {
387
+ /* Send panel - full width at bottom */
388
+ .send-panel {
389
+ top: auto;
390
+ bottom: 0;
391
+ left: 0;
392
+ right: 0;
393
+ width: 100%;
394
+ max-width: 100%;
395
+ border-radius: 16px 16px 0 0;
396
+ border-bottom: none;
397
+ border-left: none;
398
+ border-right: none;
399
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
400
+ }
401
+
402
+ .send-panel.collapsed {
403
+ width: 100%;
404
+ min-width: 100%;
405
+ transform: translateY(calc(100% - 75px));
406
+ }
407
+
408
+ .send-header {
409
+ padding: 14px 20px;
410
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
411
+ }
412
+
413
+ .send-header h2 {
414
+ font-size: 1rem;
415
+ }
416
+
417
+ .collapse-btn {
418
+ font-size: 1.4rem;
419
+ padding: 8px 16px;
420
+ }
421
+
422
+ .send-form {
423
+ padding: 20px;
424
+ max-height: 50vh;
425
+ overflow-y: auto;
426
+ }
427
+
428
+ .recipient-select, .message-input, .send-btn {
429
+ font-size: 16px; /* Prevents zoom on iOS */
430
+ padding: 14px 16px;
431
+ margin-bottom: 12px;
432
+ }
433
+
434
+ .message-input {
435
+ min-height: 80px;
436
+ }
437
+
438
+ /* Messages panel - nearly full screen on mobile */
439
+ .messages-panel {
440
+ top: auto;
441
+ bottom: 0;
442
+ left: 0;
443
+ right: 0;
444
+ width: 100%;
445
+ max-width: 100%;
446
+ max-height: 90vh;
447
+ height: 90vh;
448
+ border-radius: 16px 16px 0 0;
449
+ transform: translateY(100%);
450
+ transition: transform 0.3s ease;
451
+ display: flex;
452
+ flex-direction: column;
453
+ }
454
+
455
+ .messages-panel.open {
456
+ right: 0;
457
+ transform: translateY(0);
458
+ }
459
+
460
+ .messages-header {
461
+ padding: 20px 24px;
462
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
463
+ flex-shrink: 0;
464
+ }
465
+
466
+ .messages-header h2 {
467
+ font-size: 1.2rem;
468
+ }
469
+
470
+ .close-btn {
471
+ font-size: 1.8rem;
472
+ padding: 8px;
473
+ }
474
+
475
+ .messages-container {
476
+ padding: 20px;
477
+ flex: 1;
478
+ max-height: calc(90vh - 80px);
479
+ }
480
+
481
+ .message-card {
482
+ padding: 16px 16px 16px 24px;
483
+ margin-bottom: 12px;
484
+ }
485
+
486
+ .message-sender {
487
+ font-size: 0.9rem;
488
+ }
489
+
490
+ .message-time {
491
+ font-size: 0.75rem;
492
+ }
493
+
494
+ .message-text {
495
+ font-size: 0.9rem;
496
+ }
497
+
498
+ /* Toggle messages button - repositioned */
499
+ .toggle-messages-btn {
500
+ top: 20px;
501
+ right: 20px;
502
+ padding: 14px 18px;
503
+ font-size: 1rem;
504
+ border-radius: 30px;
505
+ z-index: 101;
506
+ }
507
+
508
+ /* Toast notification - centered */
509
+ .toast {
510
+ left: 20px;
511
+ right: 20px;
512
+ bottom: auto;
513
+ top: 50%;
514
+ transform: translateY(-50%) translateY(20px);
515
+ text-align: center;
516
+ padding: 16px 24px;
517
+ font-size: 1rem;
518
+ }
519
+
520
+ .toast.show {
521
+ transform: translateY(-50%) translateY(0);
522
+ }
523
+
524
+ /* House dialog - full screen on mobile */
525
+ .house-dialog-content {
526
+ width: 100%;
527
+ max-width: 100%;
528
+ max-height: 95vh;
529
+ height: 95vh;
530
+ border-radius: 16px 16px 0 0;
531
+ margin: 0;
532
+ position: fixed;
533
+ bottom: 0;
534
+ left: 0;
535
+ right: 0;
536
+ display: flex;
537
+ flex-direction: column;
538
+ }
539
+
540
+ .house-dialog-header {
541
+ padding: 20px 24px;
542
+ flex-shrink: 0;
543
+ }
544
+
545
+ .house-dialog-header h2 {
546
+ font-size: 1.2rem;
547
+ }
548
+
549
+ .house-dialog-tabs {
550
+ padding: 0 12px;
551
+ flex-shrink: 0;
552
+ }
553
+
554
+ .tab-btn {
555
+ padding: 16px 12px;
556
+ font-size: 0.9rem;
557
+ }
558
+
559
+ .house-dialog-body {
560
+ padding: 20px;
561
+ max-height: calc(95vh - 140px);
562
+ flex: 1;
563
+ overflow-y: auto;
564
+ }
565
+
566
+ .house-dialog-body .message-card {
567
+ padding: 16px 16px 16px 24px;
568
+ }
569
+
570
+ /* Empty state */
571
+ .empty-state {
572
+ padding: 60px 20px;
573
+ }
574
+
575
+ .empty-state p {
576
+ font-size: 1rem;
577
+ }
578
+ }
579
+
580
+ /* Extra small screens */
581
+ @media (max-width: 480px) {
582
+ .send-panel.collapsed {
583
+ transform: translateY(calc(100% - 70px));
584
+ }
585
+
586
+ .send-header {
587
+ padding: 12px 16px;
588
+ }
589
+
590
+ .send-header h2 {
591
+ font-size: 0.9rem;
592
+ }
593
+
594
+ .send-form {
595
+ padding: 16px;
596
+ }
597
+
598
+ .recipient-select, .message-input, .send-btn {
599
+ font-size: 16px;
600
+ padding: 12px 14px;
601
+ }
602
+
603
+ .toggle-messages-btn {
604
+ padding: 12px 16px;
605
+ font-size: 0.9rem;
606
+ }
607
+
608
+ .messages-panel {
609
+ max-height: 75vh;
610
+ }
611
+
612
+ .tab-btn {
613
+ font-size: 0.8rem;
614
+ padding: 12px 8px;
615
+ }
616
+ }
617
+
385
618
  /* House Dialog */
386
619
  .house-dialog {
387
620
  display: none;
@@ -392,7 +625,7 @@
392
625
  bottom: 0;
393
626
  background: rgba(0, 0, 0, 0.7);
394
627
  z-index: 300;
395
- align-items: center;
628
+ align-items: flex-end;
396
629
  justify-content: center;
397
630
  }
398
631
 
@@ -400,15 +633,43 @@
400
633
  display: flex;
401
634
  }
402
635
 
636
+ /* Desktop override for house dialog */
637
+ @media (min-width: 769px) {
638
+ .house-dialog {
639
+ align-items: center;
640
+ }
641
+
642
+ .house-dialog-content {
643
+ position: relative;
644
+ bottom: auto;
645
+ left: auto;
646
+ right: auto;
647
+ border-radius: 20px;
648
+ }
649
+ }
650
+
651
+ /* Desktop house dialog styles */
652
+ @media (min-width: 769px) {
653
+ .house-dialog-content {
654
+ background: rgba(255, 255, 255, 0.15);
655
+ backdrop-filter: blur(20px);
656
+ border-radius: 20px;
657
+ border: 1px solid rgba(255, 255, 255, 0.2);
658
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
659
+ width: 500px;
660
+ max-width: 90%;
661
+ max-height: 80vh;
662
+ display: flex;
663
+ flex-direction: column;
664
+ }
665
+ }
666
+
667
+ /* Base house dialog styles (shared) */
403
668
  .house-dialog-content {
404
669
  background: rgba(255, 255, 255, 0.15);
405
670
  backdrop-filter: blur(20px);
406
- border-radius: 20px;
407
671
  border: 1px solid rgba(255, 255, 255, 0.2);
408
672
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
409
- width: 500px;
410
- max-width: 90%;
411
- max-height: 80vh;
412
673
  display: flex;
413
674
  flex-direction: column;
414
675
  }
package/server.ts CHANGED
@@ -7,13 +7,14 @@ const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
8
 
9
9
  const app = express();
10
- const PORT = process.env.PORT || 3000;
11
10
 
12
11
  // Parse CLI args
13
12
  const args = process.argv.slice(2);
14
13
  let user: string | null = null;
15
14
  let mailboxPath: string | null = null;
16
15
  let coworkerPath: string | null = null;
16
+ let port: number = parseInt(process.env.PORT || '3000', 10);
17
+ let host: string = process.env.HOST || '0.0.0.0';
17
18
 
18
19
  for (let i = 0; i < args.length; i++) {
19
20
  if (args[i] === '--user' || args[i] === '-u') {
@@ -22,11 +23,16 @@ for (let i = 0; i < args.length; i++) {
22
23
  mailboxPath = args[++i];
23
24
  } else if (args[i] === '--coworkers' || args[i] === '-c') {
24
25
  coworkerPath = args[++i];
26
+ } else if (args[i] === '--port' || args[i] === '-p') {
27
+ const p = parseInt(args[++i], 10);
28
+ if (!isNaN(p)) port = p;
29
+ } else if (args[i] === '--host' || args[i] === '-h') {
30
+ host = args[++i];
25
31
  }
26
32
  }
27
33
 
28
34
  if (!user || !mailboxPath) {
29
- console.error('Usage: watercooler --user <name> --mailbox <path> [--coworkers <path>]');
35
+ console.error('Usage: watercooler --user <name> --mailbox <path> [--coworkers <path>] [--port <number>] [--host <address>]');
30
36
  process.exit(1);
31
37
  }
32
38
 
@@ -35,7 +41,7 @@ console.log(` Mailbox: ${mailboxPath}`);
35
41
  if (coworkerPath) {
36
42
  console.log(` Coworker DB: ${coworkerPath}`);
37
43
  }
38
- console.log(` URL: http://localhost:${PORT}`);
44
+ console.log(` URL: http://${host}:${port}`);
39
45
 
40
46
  // Databases
41
47
  let db: Database.Database | null = null;
@@ -230,6 +236,6 @@ app.get('/api/config', (req, res) => {
230
236
  res.json({ user, mailbox: mailboxPath, coworker: coworkerPath });
231
237
  });
232
238
 
233
- app.listen(PORT, () => {
239
+ app.listen(port, host, () => {
234
240
  console.log('\n✅ Watercooler running!');
235
241
  });