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 +1 -1
- package/public/app.js +77 -5
- package/public/index.html +268 -7
- package/server.ts +10 -4
package/package.json
CHANGED
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
|
-
|
|
376
|
+
const width = window.innerWidth;
|
|
377
|
+
const height = window.innerHeight;
|
|
378
|
+
camera.aspect = width / height;
|
|
358
379
|
camera.updateProjectionMatrix();
|
|
359
|
-
renderer.setSize(
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
239
|
+
app.listen(port, host, () => {
|
|
234
240
|
console.log('\n✅ Watercooler running!');
|
|
235
241
|
});
|