hop2 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.hop-tmux.conf +18 -0
  2. package/INTEGRATION.md +92 -0
  3. package/README.md +260 -0
  4. package/hay/CHANGELOG.md +46 -0
  5. package/hay/DEV_NOTES.md +39 -0
  6. package/hay/README.md +290 -0
  7. package/hay/apps/cli/package.json +25 -0
  8. package/hay/apps/cli/src/index.ts +1250 -0
  9. package/hay/apps/cli/tsconfig.json +8 -0
  10. package/hay/apps/server/package.json +37 -0
  11. package/hay/apps/server/src/index.ts +103 -0
  12. package/hay/apps/server/src/lib.ts +161 -0
  13. package/hay/apps/server/src/pty.ts +68 -0
  14. package/hay/apps/server/src/rooms.ts +421 -0
  15. package/hay/apps/server/src/utils.ts +20 -0
  16. package/hay/apps/server/test/rooms.test.ts +146 -0
  17. package/hay/apps/server/tsconfig.json +9 -0
  18. package/hay/apps/web/index.html +12 -0
  19. package/hay/apps/web/package.json +32 -0
  20. package/hay/apps/web/scripts/run-e2e.mjs +130 -0
  21. package/hay/apps/web/src/App.tsx +1302 -0
  22. package/hay/apps/web/src/components/MobileKeyboard.tsx +701 -0
  23. package/hay/apps/web/src/main.tsx +13 -0
  24. package/hay/apps/web/src/setupTests.ts +1 -0
  25. package/hay/apps/web/src/styles.css +1198 -0
  26. package/hay/apps/web/src/utils/optimisticEcho.ts +115 -0
  27. package/hay/apps/web/src/utils/presence.ts +24 -0
  28. package/hay/apps/web/src/vite-env.d.ts +10 -0
  29. package/hay/apps/web/test/optimisticEcho.test.ts +39 -0
  30. package/hay/apps/web/test/presence.test.ts +18 -0
  31. package/hay/apps/web/tsconfig.json +8 -0
  32. package/hay/apps/web/vite.config.ts +13 -0
  33. package/hay/apps/web/vitest.config.ts +12 -0
  34. package/hay/package-lock.json +5988 -0
  35. package/hay/package.json +18 -0
  36. package/hay/packages/shared/package.json +27 -0
  37. package/hay/packages/shared/src/colors.ts +16 -0
  38. package/hay/packages/shared/src/index.ts +2 -0
  39. package/hay/packages/shared/src/protocol.ts +90 -0
  40. package/hay/packages/shared/test/protocol.test.ts +33 -0
  41. package/hay/packages/shared/tsconfig.json +7 -0
  42. package/hay/tsconfig.base.json +15 -0
  43. package/hay-web/assets/index-CTTzApRp.css +32 -0
  44. package/hay-web/assets/index-FDfdolCs.js +60 -0
  45. package/hay-web/assets/index-FDfdolCs.js.map +1 -0
  46. package/hay-web/index.html +13 -0
  47. package/hop +3717 -0
  48. package/package.json +52 -0
  49. package/sessions.html +243 -0
  50. package/tunnel-shell.js +254 -0
@@ -0,0 +1,1198 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Space+Grotesk:wght@400;500;600&display=swap");
2
+
3
+ :root {
4
+ color-scheme: light;
5
+ --bg: #f3eee5;
6
+ --bg-alt: #f8f4ee;
7
+ --ink: #1f1b16;
8
+ --ink-muted: #6b6257;
9
+ --accent: #d97706;
10
+ --accent-2: #0f766e;
11
+ --line: #d6cbb9;
12
+ --panel: #fffdf9;
13
+ --shadow: 0 18px 50px rgba(31, 27, 22, 0.08);
14
+ }
15
+
16
+ * {
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ body {
21
+ margin: 0;
22
+ font-family: "Space Grotesk", "Segoe UI", sans-serif;
23
+ background: radial-gradient(circle at top left, #fdf9f2 0%, var(--bg) 45%, #efe5d7 100%);
24
+ color: var(--ink);
25
+ min-height: 100vh;
26
+ }
27
+
28
+ #root {
29
+ min-height: 100vh;
30
+ }
31
+
32
+ .app {
33
+ min-height: 100vh;
34
+ display: flex;
35
+ flex-direction: column;
36
+ }
37
+
38
+ /* Mobile: lock to exactly one screen, no body scroll */
39
+ @media (max-width: 768px) {
40
+ body {
41
+ height: 100dvh;
42
+ overflow: hidden;
43
+ }
44
+
45
+ #root {
46
+ height: 100dvh;
47
+ overflow: hidden;
48
+ }
49
+
50
+ .app {
51
+ height: 100dvh;
52
+ min-height: unset;
53
+ overflow: hidden;
54
+ }
55
+ }
56
+
57
+ .topbar {
58
+ display: flex;
59
+ justify-content: space-between;
60
+ align-items: center;
61
+ padding: 20px 32px;
62
+ gap: 24px;
63
+ border-bottom: 1px solid var(--line);
64
+ background: rgba(255, 255, 255, 0.6);
65
+ backdrop-filter: blur(16px);
66
+ }
67
+
68
+ .brand {
69
+ display: flex;
70
+ gap: 14px;
71
+ align-items: center;
72
+ }
73
+
74
+ .brand-mark {
75
+ display: grid;
76
+ place-items: center;
77
+ width: 44px;
78
+ height: 44px;
79
+ border-radius: 16px;
80
+ background: linear-gradient(140deg, var(--accent), #fbbf24);
81
+ color: #fffaf0;
82
+ font-size: 20px;
83
+ box-shadow: 0 12px 24px rgba(217, 119, 6, 0.25);
84
+ }
85
+
86
+ .brand-title {
87
+ margin: 0;
88
+ font-weight: 600;
89
+ font-size: 18px;
90
+ }
91
+
92
+ .brand-subtitle {
93
+ margin: 2px 0 0;
94
+ color: var(--ink-muted);
95
+ font-size: 13px;
96
+ }
97
+
98
+ .presence-strip {
99
+ display: flex;
100
+ flex-wrap: wrap;
101
+ gap: 10px;
102
+ align-items: center;
103
+ justify-content: flex-end;
104
+ max-width: 55%;
105
+ }
106
+
107
+ .presence-chip {
108
+ display: inline-flex;
109
+ align-items: center;
110
+ gap: 8px;
111
+ padding: 6px 12px;
112
+ border-radius: 999px;
113
+ background: rgba(255, 255, 255, 0.9);
114
+ border: 1px solid var(--line);
115
+ font-size: 13px;
116
+ color: var(--ink);
117
+ transition: transform 0.2s ease;
118
+ }
119
+
120
+ .presence-chip.self {
121
+ border-color: var(--accent-2);
122
+ box-shadow: 0 8px 20px rgba(15, 118, 110, 0.15);
123
+ }
124
+
125
+ .presence-chip:hover {
126
+ transform: translateY(-1px);
127
+ }
128
+
129
+ .presence-dot {
130
+ width: 10px;
131
+ height: 10px;
132
+ border-radius: 50%;
133
+ display: inline-block;
134
+ }
135
+
136
+ .presence-name {
137
+ font-weight: 500;
138
+ }
139
+
140
+ .presence-status {
141
+ font-size: 11px;
142
+ text-transform: uppercase;
143
+ letter-spacing: 0.08em;
144
+ color: var(--ink-muted);
145
+ }
146
+
147
+ .presence-status.typing {
148
+ color: var(--accent-2);
149
+ }
150
+
151
+ .presence-empty {
152
+ font-size: 12px;
153
+ color: var(--ink-muted);
154
+ }
155
+
156
+ .join {
157
+ flex: 1;
158
+ display: grid;
159
+ place-items: center;
160
+ padding: 40px 20px 80px;
161
+ }
162
+
163
+ .join-card {
164
+ width: min(520px, 92vw);
165
+ background: var(--panel);
166
+ padding: 32px 36px;
167
+ border-radius: 28px;
168
+ box-shadow: var(--shadow);
169
+ animation: lift 0.6s ease;
170
+ border: 1px solid var(--line);
171
+ }
172
+
173
+ @keyframes lift {
174
+ from {
175
+ opacity: 0;
176
+ transform: translateY(16px);
177
+ }
178
+ to {
179
+ opacity: 1;
180
+ transform: translateY(0);
181
+ }
182
+ }
183
+
184
+ .join-card h1 {
185
+ margin-top: 0;
186
+ font-size: 28px;
187
+ }
188
+
189
+ .join-card p {
190
+ color: var(--ink-muted);
191
+ margin-bottom: 24px;
192
+ }
193
+
194
+ .join-form {
195
+ display: grid;
196
+ gap: 16px;
197
+ }
198
+
199
+ .join-form label {
200
+ display: grid;
201
+ gap: 8px;
202
+ font-size: 14px;
203
+ color: var(--ink-muted);
204
+ }
205
+
206
+ input {
207
+ border: 1px solid var(--line);
208
+ border-radius: 14px;
209
+ padding: 12px 14px;
210
+ font-size: 15px;
211
+ font-family: inherit;
212
+ background: #fff;
213
+ }
214
+
215
+ button {
216
+ border: none;
217
+ border-radius: 14px;
218
+ padding: 12px 16px;
219
+ font-size: 14px;
220
+ font-weight: 600;
221
+ cursor: pointer;
222
+ background: #f1e7d8;
223
+ color: var(--ink);
224
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
225
+ }
226
+
227
+ button:hover {
228
+ transform: translateY(-1px);
229
+ box-shadow: 0 12px 24px rgba(31, 27, 22, 0.12);
230
+ }
231
+
232
+ button.primary {
233
+ background: linear-gradient(120deg, var(--accent), #f59e0b);
234
+ color: #fffef8;
235
+ }
236
+
237
+ .join-links {
238
+ margin-top: 26px;
239
+ display: grid;
240
+ gap: 10px;
241
+ font-size: 13px;
242
+ color: var(--ink-muted);
243
+ }
244
+
245
+ .share-link {
246
+ display: flex;
247
+ gap: 10px;
248
+ align-items: center;
249
+ }
250
+
251
+ .share-link input {
252
+ flex: 1;
253
+ font-size: 12px;
254
+ }
255
+
256
+ .session {
257
+ flex: 1;
258
+ display: grid;
259
+ grid-template-columns: 320px 1fr;
260
+ gap: 28px;
261
+ padding: 30px 32px 50px;
262
+ position: relative;
263
+ }
264
+
265
+ .drawer-toggle {
266
+ display: none;
267
+ }
268
+
269
+ .drawer-close {
270
+ display: none;
271
+ }
272
+
273
+ .drawer-overlay {
274
+ display: none;
275
+ }
276
+
277
+ .controls {
278
+ display: flex;
279
+ flex-direction: column;
280
+ gap: 12px;
281
+ }
282
+
283
+ .room-info {
284
+ background: var(--panel);
285
+ padding: 16px;
286
+ border-radius: 16px;
287
+ border: 1px solid var(--line);
288
+ box-shadow: var(--shadow);
289
+ }
290
+
291
+ .room-label {
292
+ margin: 0;
293
+ text-transform: uppercase;
294
+ font-size: 11px;
295
+ letter-spacing: 0.2em;
296
+ color: var(--ink-muted);
297
+ }
298
+
299
+ .room-info h2 {
300
+ margin: 6px 0 2px;
301
+ font-size: 20px;
302
+ }
303
+
304
+ .room-meta {
305
+ margin: 0;
306
+ color: var(--ink-muted);
307
+ font-size: 12px;
308
+ }
309
+
310
+ .control-actions {
311
+ display: grid;
312
+ gap: 8px;
313
+ }
314
+
315
+ .status-card {
316
+ background: var(--panel);
317
+ padding: 20px;
318
+ border-radius: 20px;
319
+ border: 1px solid var(--line);
320
+ box-shadow: var(--shadow);
321
+ }
322
+
323
+ .status-title {
324
+ margin: 0;
325
+ font-size: 12px;
326
+ text-transform: uppercase;
327
+ letter-spacing: 0.12em;
328
+ color: var(--ink-muted);
329
+ }
330
+
331
+ .status-main {
332
+ margin: 10px 0 8px;
333
+ font-size: 16px;
334
+ font-weight: 500;
335
+ }
336
+
337
+ .toggle label {
338
+ display: flex;
339
+ align-items: center;
340
+ gap: 10px;
341
+ font-size: 13px;
342
+ color: var(--ink-muted);
343
+ }
344
+
345
+ .toggle {
346
+ display: grid;
347
+ gap: 6px;
348
+ }
349
+
350
+ .toggle-hint {
351
+ margin: 0;
352
+ font-size: 12px;
353
+ color: var(--ink-muted);
354
+ }
355
+
356
+ .notice {
357
+ margin: 0;
358
+ background: rgba(15, 118, 110, 0.12);
359
+ padding: 10px 12px;
360
+ border-radius: 12px;
361
+ font-size: 13px;
362
+ color: var(--accent-2);
363
+ }
364
+
365
+ .font-size-control {
366
+ display: flex;
367
+ align-items: center;
368
+ justify-content: space-between;
369
+ padding: 8px 0;
370
+ }
371
+
372
+ .font-size-control label {
373
+ font-size: 12px;
374
+ color: var(--ink-muted);
375
+ line-height: 1.2;
376
+ display: flex;
377
+ align-items: center;
378
+ }
379
+
380
+ .font-size-buttons {
381
+ display: flex;
382
+ align-items: center;
383
+ gap: 8px;
384
+ }
385
+
386
+ .font-size-buttons button {
387
+ width: 28px;
388
+ height: 28px;
389
+ border-radius: 7px;
390
+ border: 1px solid var(--line);
391
+ background: var(--panel);
392
+ font-size: 16px;
393
+ cursor: pointer;
394
+ display: flex;
395
+ align-items: center;
396
+ justify-content: center;
397
+ }
398
+
399
+ .font-size-buttons button:active {
400
+ background: var(--line);
401
+ }
402
+
403
+ .font-size-buttons span {
404
+ min-width: 40px;
405
+ text-align: center;
406
+ font-size: 13px;
407
+ font-weight: 500;
408
+ }
409
+
410
+ .view-mode-control,
411
+ .selection-mode-control,
412
+ .haptics-control {
413
+ display: flex;
414
+ align-items: center;
415
+ justify-content: space-between;
416
+ padding: 8px 0;
417
+ min-height: 32px;
418
+ }
419
+
420
+ .view-mode-control label,
421
+ .selection-mode-control label,
422
+ .haptics-control label {
423
+ font-size: 12px;
424
+ color: var(--ink-muted);
425
+ line-height: 1.2;
426
+ display: flex;
427
+ align-items: center;
428
+ }
429
+
430
+ .view-mode-buttons {
431
+ display: flex;
432
+ gap: 4px;
433
+ }
434
+
435
+ .view-mode-buttons button {
436
+ padding: 4px 12px;
437
+ border-radius: 6px;
438
+ border: 1px solid var(--line);
439
+ background: var(--panel);
440
+ font-size: 12px;
441
+ line-height: 1;
442
+ cursor: pointer;
443
+ color: var(--ink-muted);
444
+ transition: all 0.15s ease;
445
+ }
446
+
447
+ .view-mode-buttons button.active {
448
+ background: var(--accent);
449
+ border-color: var(--accent);
450
+ color: white;
451
+ }
452
+
453
+ .view-mode-buttons button:not(.active):active {
454
+ background: var(--line);
455
+ }
456
+
457
+ .terminal {
458
+ display: flex;
459
+ flex-direction: column;
460
+ gap: 12px;
461
+ min-width: 0;
462
+ overflow: hidden;
463
+ }
464
+
465
+ .terminal-frame {
466
+ height: calc(100vh - 250px);
467
+ height: calc(100dvh - 250px);
468
+ min-height: 300px;
469
+ border-radius: 24px;
470
+ overflow: hidden;
471
+ border: 1px solid #1f2937;
472
+ box-shadow: 0 22px 60px rgba(9, 15, 23, 0.35);
473
+ background: #0b1117;
474
+ animation: lift 0.6s ease;
475
+ display: flex;
476
+ flex-direction: column;
477
+ }
478
+
479
+ .terminal-scroll {
480
+ flex: 1;
481
+ min-height: 0;
482
+ overflow: auto;
483
+ padding: 16px 16px 20px 16px;
484
+ touch-action: pan-x pan-y;
485
+ }
486
+
487
+ .terminal-inner {
488
+ width: max-content;
489
+ height: max-content;
490
+ min-width: 100%;
491
+ min-height: 100%;
492
+ }
493
+
494
+ /* Ensure xterm elements don't block our touch handling */
495
+ .terminal-frame .xterm,
496
+ .terminal-frame .xterm-screen,
497
+ .terminal-frame .xterm-viewport {
498
+ touch-action: pan-x pan-y;
499
+ }
500
+
501
+ /* Ensure xterm container has proper background to cover old content on resize */
502
+ .terminal-frame .xterm {
503
+ background: #0d1117;
504
+ overflow: hidden;
505
+ }
506
+
507
+ .terminal-frame .xterm-viewport {
508
+ background: #0d1117 !important;
509
+ overflow: hidden !important;
510
+ }
511
+
512
+ .terminal-frame .xterm-screen {
513
+ overflow: hidden;
514
+ }
515
+
516
+ /* Force canvas to not exceed viewport */
517
+ .terminal-frame .xterm-screen canvas {
518
+ position: relative !important;
519
+ }
520
+
521
+ .terminal-footer {
522
+ display: flex;
523
+ justify-content: space-between;
524
+ font-size: 12px;
525
+ color: var(--ink-muted);
526
+ padding: 0 4px;
527
+ }
528
+
529
+ @media (max-width: 960px) {
530
+ .topbar {
531
+ flex-direction: column;
532
+ align-items: flex-start;
533
+ }
534
+
535
+ .presence-strip {
536
+ max-width: 100%;
537
+ justify-content: flex-start;
538
+ }
539
+
540
+ .session {
541
+ grid-template-columns: 1fr;
542
+ }
543
+ }
544
+
545
+ @media (max-width: 768px) {
546
+ /* Mobile: full bleed terminal, no chrome */
547
+ body {
548
+ background: #0b1117;
549
+ }
550
+
551
+ .app {
552
+ background: #0b1117;
553
+ }
554
+
555
+ .topbar {
556
+ display: none;
557
+ }
558
+
559
+ .session {
560
+ padding: 0;
561
+ gap: 0;
562
+ flex: 1;
563
+ display: flex;
564
+ flex-direction: column;
565
+ overflow: hidden;
566
+ background: #0b1117;
567
+ }
568
+
569
+ .session .terminal {
570
+ flex: 1;
571
+ min-height: 0;
572
+ overflow: hidden;
573
+ gap: 0;
574
+ }
575
+
576
+ .session .terminal-frame {
577
+ flex: 1;
578
+ height: 100% !important;
579
+ min-height: 0;
580
+ border: none;
581
+ border-radius: 0;
582
+ box-shadow: none;
583
+ padding: 0;
584
+ }
585
+
586
+ .session .terminal .terminal-scroll {
587
+ width: 100%;
588
+ height: 100%;
589
+ overflow: hidden;
590
+ overscroll-behavior: none;
591
+ touch-action: none;
592
+ -webkit-overflow-scrolling: auto;
593
+ padding: 0;
594
+ }
595
+
596
+ .session .terminal-frame .xterm,
597
+ .session .terminal-frame .xterm-screen,
598
+ .session .terminal-frame .xterm-viewport {
599
+ touch-action: none;
600
+ }
601
+
602
+ /* Mobile touch scrolling fix:
603
+ - We disable native touch handling on xterm text nodes so our custom
604
+ touch scrolling (App.tsx) receives events without iOS text selection.
605
+ - In selection mode we override these to re-enable native selection. */
606
+ .session .terminal-frame .xterm-rows,
607
+ .session .terminal-frame .xterm-rows > div,
608
+ .session .terminal-frame .xterm-rows span {
609
+ pointer-events: none;
610
+ }
611
+
612
+ /* Selection mode: re-enable native text selection and selection handles. */
613
+ .session.selection-mode .terminal .terminal-scroll {
614
+ overflow: hidden;
615
+ overscroll-behavior: none;
616
+ touch-action: auto;
617
+ }
618
+
619
+ .session.selection-mode .terminal-frame .xterm,
620
+ .session.selection-mode .terminal-frame .xterm-screen,
621
+ .session.selection-mode .terminal-frame .xterm-viewport {
622
+ touch-action: auto;
623
+ }
624
+
625
+ .session.selection-mode .terminal-frame .xterm {
626
+ -webkit-user-select: text;
627
+ user-select: text;
628
+ -webkit-touch-callout: default;
629
+ }
630
+
631
+ .session.selection-mode .terminal-frame .xterm-viewport {
632
+ overflow: hidden !important;
633
+ }
634
+
635
+ .session.selection-mode .terminal-frame .xterm-screen canvas {
636
+ pointer-events: none;
637
+ }
638
+
639
+ .session.selection-mode .terminal-frame .xterm-rows,
640
+ .session.selection-mode .terminal-frame .xterm-rows > div,
641
+ .session.selection-mode .terminal-frame .xterm-rows span {
642
+ pointer-events: auto;
643
+ -webkit-user-select: text;
644
+ user-select: text;
645
+ -webkit-touch-callout: default;
646
+ }
647
+
648
+ .session .terminal-footer {
649
+ display: none;
650
+ }
651
+
652
+ /* When keyboard is visible, remove any bottom padding from terminal */
653
+ .session.has-keyboard .terminal-frame {
654
+ padding-bottom: 0;
655
+ }
656
+
657
+ .drawer-toggle {
658
+ display: flex;
659
+ align-items: center;
660
+ justify-content: center;
661
+ position: fixed;
662
+ z-index: 100;
663
+ width: 56px;
664
+ height: 56px;
665
+ padding: 0;
666
+ border-radius: 50%;
667
+ background: var(--panel);
668
+ border: 1px solid var(--line);
669
+ box-shadow: 0 8px 24px rgba(31, 27, 22, 0.15);
670
+ cursor: grab;
671
+ touch-action: none;
672
+ user-select: none;
673
+ }
674
+
675
+ .drawer-toggle:active {
676
+ cursor: grabbing;
677
+ }
678
+
679
+ .drawer-overlay {
680
+ display: block;
681
+ position: fixed;
682
+ inset: 0;
683
+ background: rgba(0, 0, 0, 0.4);
684
+ z-index: 200;
685
+ }
686
+
687
+ .controls {
688
+ position: fixed;
689
+ top: 0;
690
+ left: 0;
691
+ bottom: 0;
692
+ width: min(320px, 85vw);
693
+ background: var(--bg);
694
+ z-index: 300;
695
+ padding: 20px;
696
+ overflow-y: auto;
697
+ transform: translateX(-100%);
698
+ transition: transform 0.3s ease;
699
+ }
700
+
701
+ .controls.open {
702
+ transform: translateX(0);
703
+ }
704
+
705
+ .drawer-close {
706
+ display: block;
707
+ position: absolute;
708
+ top: 12px;
709
+ right: 12px;
710
+ width: 36px;
711
+ height: 36px;
712
+ border-radius: 50%;
713
+ background: var(--panel);
714
+ border: 1px solid var(--line);
715
+ font-size: 18px;
716
+ line-height: 1;
717
+ cursor: pointer;
718
+ display: flex;
719
+ align-items: center;
720
+ justify-content: center;
721
+ }
722
+
723
+ .terminal-frame {
724
+ height: 100%;
725
+ }
726
+
727
+ .room-info {
728
+ padding: 16px;
729
+ border-radius: 16px;
730
+ }
731
+
732
+ .room-info h2 {
733
+ font-size: 16px;
734
+ margin: 4px 0 2px;
735
+ }
736
+
737
+ .room-label {
738
+ font-size: 10px;
739
+ }
740
+
741
+ .room-meta {
742
+ font-size: 12px;
743
+ }
744
+ }
745
+
746
+ @media (max-width: 480px) {
747
+ .topbar {
748
+ padding: 16px;
749
+ }
750
+
751
+ .brand-subtitle {
752
+ display: none;
753
+ }
754
+
755
+ .session {
756
+ padding: 0;
757
+ }
758
+
759
+ .terminal-frame {
760
+ height: 100% !important;
761
+ border-radius: 0;
762
+ padding: 0;
763
+ }
764
+ }
765
+
766
+ /* ===========================================
767
+ Mobile Keyboard - iOS-style virtual keyboard
768
+ =========================================== */
769
+
770
+ :root {
771
+ --kb-scale: 1;
772
+ --kb-key-height: calc(46px * var(--kb-scale));
773
+ --kb-key-margin: calc(3px * var(--kb-scale));
774
+ --kb-row-gap: calc(5.4px * var(--kb-scale));
775
+ --kb-acc-key-height: calc(36px * var(--kb-scale));
776
+ --kb-key-font: calc(25px * var(--kb-scale));
777
+ --kb-fn-font: calc(16px * var(--kb-scale));
778
+ --kb-acc-font: calc(14px * var(--kb-scale));
779
+ --kb-icon-size: calc(24px * var(--kb-scale));
780
+ }
781
+
782
+ .mobile-keyboard {
783
+ background: #d1d3d9;
784
+ padding: 2px max(2px, env(safe-area-inset-right)) calc(6px + env(safe-area-inset-bottom, 0px)) max(2px, env(safe-area-inset-left));
785
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
786
+ box-sizing: border-box;
787
+ overflow-x: hidden;
788
+ -webkit-user-select: none;
789
+ user-select: none;
790
+ flex-shrink: 0;
791
+ }
792
+
793
+ .kb-acc-row {
794
+ display: flex;
795
+ margin: 0 2px calc(5px * var(--kb-scale));
796
+ padding: calc(3.6px * var(--kb-scale)) calc(3.6px * var(--kb-scale)) calc(5.4px * var(--kb-scale));
797
+ background: #d1d3d9;
798
+ border-radius: 8px;
799
+ }
800
+
801
+ .kb-keys {
802
+ display: flex;
803
+ flex-direction: column;
804
+ gap: var(--kb-row-gap);
805
+ padding: 0 4px;
806
+ width: 100%;
807
+ box-sizing: border-box;
808
+ }
809
+
810
+ .kb-row {
811
+ display: flex;
812
+ justify-content: center;
813
+ width: 100%;
814
+ box-sizing: border-box;
815
+ }
816
+
817
+ .kb-row.kb-r2 {
818
+ padding: 0 5%;
819
+ }
820
+
821
+ .kb-row.kb-r3 {
822
+ padding: 0 1.5%;
823
+ }
824
+
825
+ .kb-row.kb-r3 .kb-gap {
826
+ width: 1%;
827
+ flex-shrink: 0;
828
+ }
829
+
830
+ /* Base key - iOS styling */
831
+ .kb-key {
832
+ display: flex;
833
+ align-items: center;
834
+ justify-content: center;
835
+ height: var(--kb-key-height);
836
+ border-radius: 6px;
837
+ margin: var(--kb-key-margin);
838
+ font-size: var(--kb-key-font);
839
+ font-weight: 400;
840
+ letter-spacing: -0.5px;
841
+ position: relative;
842
+ background: #ffffff;
843
+ color: #000;
844
+ border: none;
845
+ cursor: pointer;
846
+ flex-grow: 1;
847
+ flex-basis: 0;
848
+ -webkit-tap-highlight-color: transparent;
849
+ /* Long transition for fade-out effect - gives user visual confirmation */
850
+ transition: background-color 1s ease-out, transform 1s ease-out, box-shadow 1s ease-out, filter 1s ease-out;
851
+ /* iOS key shadow (the "chin") */
852
+ box-shadow: 0 1px 0 #898c94;
853
+ padding: 0;
854
+ font-family: inherit;
855
+ }
856
+
857
+ /* Extend touch target to cover gaps between keys */
858
+ .kb-key::before {
859
+ content: '';
860
+ position: absolute;
861
+ top: calc(-1 * var(--kb-key-margin));
862
+ left: calc(-1 * var(--kb-key-margin));
863
+ right: calc(-1 * var(--kb-key-margin));
864
+ bottom: calc(-1 * var(--kb-key-margin) - var(--kb-row-gap) / 2);
865
+ }
866
+
867
+ /* Pressed state - fast transition in, slow fade out */
868
+ /* Don't scale! Shrinking the element shrinks the touch target (::before), causing dropped keys on rapid taps */
869
+ .kb-key.kb-pressed {
870
+ background: #d0d0d0 !important;
871
+ box-shadow: none !important;
872
+ transform: translateY(3px);
873
+ filter: brightness(0.85);
874
+ transition: background-color 50ms, transform 50ms, box-shadow 50ms, filter 50ms;
875
+ }
876
+
877
+ /* Fallback for :active (non-JS) */
878
+ .kb-key:active:not(.kb-pressed) {
879
+ background: #d0d0d0;
880
+ box-shadow: none;
881
+ transform: translateY(3px);
882
+ filter: brightness(0.85);
883
+ }
884
+
885
+ /* Function keys - darker background */
886
+ .kb-key.kb-act {
887
+ background: #b2b6bf;
888
+ font-size: var(--kb-fn-font);
889
+ font-weight: 400;
890
+ letter-spacing: -0.2px;
891
+ }
892
+
893
+ .kb-key.kb-act.kb-pressed {
894
+ background: #9ea2ab !important;
895
+ }
896
+
897
+ .kb-key.kb-act:active:not(.kb-pressed) {
898
+ background: #9ea2ab;
899
+ }
900
+
901
+ /* Accessory row keys */
902
+ .kb-key.kb-acc {
903
+ height: var(--kb-acc-key-height);
904
+ font-size: var(--kb-acc-font);
905
+ }
906
+
907
+ /* Primary key (return) - iOS blue */
908
+ .kb-key.kb-pri {
909
+ background: #007aff;
910
+ color: #fff;
911
+ }
912
+
913
+ .kb-key.kb-pri.kb-pressed {
914
+ background: #0062cc !important;
915
+ }
916
+
917
+ .kb-key.kb-pri:active:not(.kb-pressed) {
918
+ background: #0062cc;
919
+ }
920
+
921
+ /* Active modifier state */
922
+ .kb-key.kb-on {
923
+ background: #000;
924
+ color: #fff;
925
+ box-shadow: none;
926
+ transform: translateY(1px);
927
+ }
928
+
929
+ /* Bottom row larger font */
930
+ .kb-key.kb-bottom {
931
+ font-size: calc(16px * var(--kb-scale));
932
+ }
933
+
934
+ .kb-key svg {
935
+ width: var(--kb-icon-size);
936
+ height: var(--kb-icon-size);
937
+ }
938
+
939
+ .kb-gap {
940
+ width: 1%;
941
+ flex-shrink: 0;
942
+ }
943
+
944
+ /* System keyboard button pulse animation */
945
+ .kb-key.kb-syskb {
946
+ background: #3b82f6;
947
+ color: #fff;
948
+ }
949
+
950
+ .kb-key.kb-syskb svg {
951
+ stroke: #fff;
952
+ }
953
+
954
+ @keyframes kbPulse {
955
+ 0%, 100% {
956
+ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
957
+ transform: scale(1);
958
+ }
959
+ 50% {
960
+ box-shadow: 0 0 0 8px rgba(59, 130, 246, 0);
961
+ transform: scale(1.1);
962
+ }
963
+ }
964
+
965
+ .kb-key.kb-pulse {
966
+ animation: kbPulse 1s ease-in-out 5;
967
+ }
968
+
969
+ /* System keyboard hint overlay */
970
+ .kb-hint-overlay {
971
+ position: fixed;
972
+ top: 0;
973
+ left: 0;
974
+ right: 0;
975
+ bottom: 0;
976
+ background: rgba(0, 0, 0, 0.5);
977
+ z-index: 99998;
978
+ display: flex;
979
+ align-items: center;
980
+ justify-content: center;
981
+ animation: kbFadeIn 0.4s ease-out;
982
+ }
983
+
984
+ .kb-hint {
985
+ background: rgba(0, 0, 0, 0.92);
986
+ color: #fff;
987
+ padding: 28px 36px;
988
+ border-radius: 20px;
989
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
990
+ font-size: 18px;
991
+ text-align: center;
992
+ line-height: 1.5;
993
+ max-width: 300px;
994
+ box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4);
995
+ animation: kbHintIn 0.4s ease-out;
996
+ }
997
+
998
+ @keyframes kbFadeIn {
999
+ from { opacity: 0; }
1000
+ to { opacity: 1; }
1001
+ }
1002
+
1003
+ @keyframes kbHintIn {
1004
+ from { opacity: 0; transform: scale(0.9); }
1005
+ to { opacity: 1; transform: scale(1); }
1006
+ }
1007
+
1008
+ /* System keyboard dialog */
1009
+ .kb-sys-overlay {
1010
+ position: fixed;
1011
+ top: 0;
1012
+ left: 0;
1013
+ right: 0;
1014
+ bottom: 0;
1015
+ background: rgba(0, 0, 0, 0.5);
1016
+ z-index: 99999;
1017
+ display: flex;
1018
+ align-items: flex-start;
1019
+ justify-content: center;
1020
+ padding-top: calc(env(safe-area-inset-top) + 12px);
1021
+ }
1022
+
1023
+ .kb-sys-dialog {
1024
+ background: transparent;
1025
+ backdrop-filter: blur(25px);
1026
+ -webkit-backdrop-filter: blur(25px);
1027
+ padding: 4px;
1028
+ border-radius: 14px;
1029
+ display: flex;
1030
+ gap: 8px;
1031
+ width: calc(100% - 24px);
1032
+ max-width: 500px;
1033
+ border: none;
1034
+ box-shadow: none;
1035
+ animation: kbDialogIn 0.35s cubic-bezier(0.19, 1, 0.22, 1);
1036
+ }
1037
+
1038
+ @keyframes kbDialogIn {
1039
+ from { transform: translateY(-100%); opacity: 0; }
1040
+ to { transform: translateY(0); opacity: 1; }
1041
+ }
1042
+
1043
+ .kb-sys-input {
1044
+ flex: 1;
1045
+ min-height: 34px;
1046
+ max-height: 240px;
1047
+ border-radius: 12px;
1048
+ border: 1px solid rgba(0, 0, 0, 0.1);
1049
+ box-shadow: none;
1050
+ padding: 6px 10px;
1051
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
1052
+ font-size: 16px;
1053
+ line-height: 1.35;
1054
+ resize: none;
1055
+ overflow: hidden;
1056
+ background: rgba(255, 255, 255, 0.9);
1057
+ color: #000;
1058
+ }
1059
+
1060
+ .kb-sys-input:focus {
1061
+ outline: none;
1062
+ box-shadow: none;
1063
+ }
1064
+
1065
+
1066
+ /* Keyboard visibility state - when keyboard is shown, maximize terminal space */
1067
+ .session.has-keyboard {
1068
+ padding-bottom: 0;
1069
+ padding-top: 8px;
1070
+ }
1071
+
1072
+ .session.has-keyboard .terminal-frame {
1073
+ height: auto;
1074
+ flex: 1;
1075
+ min-height: 80px;
1076
+ }
1077
+
1078
+ .session.has-keyboard .terminal-footer {
1079
+ display: none;
1080
+ }
1081
+
1082
+ /* Hide topbar when keyboard visible on mobile for more terminal space */
1083
+ @media (max-width: 768px) {
1084
+ .app:has(.session.has-keyboard) .topbar {
1085
+ display: none;
1086
+ }
1087
+ }
1088
+
1089
+ @media (orientation: landscape) {
1090
+ :root {
1091
+ --kb-scale: 0.7;
1092
+ }
1093
+ }
1094
+
1095
+ @media (max-width: 480px) {
1096
+ :root {
1097
+ --kb-scale: 0.9;
1098
+ }
1099
+ }
1100
+
1101
+ /* ===========================================
1102
+ Session Switcher - Side pane session list
1103
+ =========================================== */
1104
+
1105
+ .session-switcher {
1106
+ background: var(--panel);
1107
+ padding: 12px;
1108
+ border-radius: 14px;
1109
+ border: 1px solid var(--line);
1110
+ box-shadow: var(--shadow);
1111
+ }
1112
+
1113
+ .session-switcher-label {
1114
+ margin: 0 0 8px;
1115
+ font-size: 10px;
1116
+ text-transform: uppercase;
1117
+ letter-spacing: 0.15em;
1118
+ color: var(--ink-muted);
1119
+ }
1120
+
1121
+ .session-list {
1122
+ display: flex;
1123
+ flex-direction: column;
1124
+ gap: 6px;
1125
+ }
1126
+
1127
+ .session-list-loading,
1128
+ .session-list-empty {
1129
+ font-size: 13px;
1130
+ color: var(--ink-muted);
1131
+ padding: 8px 0;
1132
+ }
1133
+
1134
+ .session-list-item {
1135
+ display: flex;
1136
+ align-items: center;
1137
+ justify-content: space-between;
1138
+ gap: 10px;
1139
+ width: 100%;
1140
+ padding: 10px 12px;
1141
+ border-radius: 10px;
1142
+ background: var(--bg-alt);
1143
+ border: 1px solid transparent;
1144
+ cursor: pointer;
1145
+ text-align: left;
1146
+ transition: background-color 0.15s, border-color 0.15s, transform 0.15s;
1147
+ }
1148
+
1149
+ .session-list-item:hover {
1150
+ background: #fff;
1151
+ border-color: var(--line);
1152
+ transform: translateY(-1px);
1153
+ }
1154
+
1155
+ .session-list-item.active {
1156
+ border-color: #22c55e40;
1157
+ background: #22c55e10;
1158
+ }
1159
+
1160
+ .session-list-item.starting {
1161
+ border-color: #f59e0b40;
1162
+ background: #f59e0b10;
1163
+ }
1164
+
1165
+ .session-list-name {
1166
+ flex: 1;
1167
+ font-size: 13px;
1168
+ font-weight: 500;
1169
+ color: var(--ink);
1170
+ overflow: hidden;
1171
+ text-overflow: ellipsis;
1172
+ white-space: nowrap;
1173
+ }
1174
+
1175
+ .session-badge {
1176
+ flex-shrink: 0;
1177
+ padding: 3px 7px;
1178
+ border-radius: 5px;
1179
+ font-size: 9px;
1180
+ font-weight: 600;
1181
+ text-transform: uppercase;
1182
+ letter-spacing: 0.05em;
1183
+ }
1184
+
1185
+ .session-badge.live {
1186
+ background: #22c55e;
1187
+ color: #fff;
1188
+ }
1189
+
1190
+ .session-badge.starting {
1191
+ background: #f59e0b;
1192
+ color: #111827;
1193
+ }
1194
+
1195
+ .session-badge.port {
1196
+ background: #3b82f6;
1197
+ color: #fff;
1198
+ }