@cplieger/web-terminal-ui 3.0.3 → 3.1.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.
package/README.md CHANGED
@@ -134,6 +134,12 @@ Consumers that ship this UI:
134
134
  - [`vibekit`](https://github.com/cplieger/vibekit)
135
135
  - [`vibecli`](https://github.com/cplieger/vibecli)
136
136
 
137
+ ## Disclaimer
138
+
139
+ This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.
140
+
141
+ This project was built with AI-assisted tooling using [Claude Opus](https://www.anthropic.com/claude) and [Kiro](https://kiro.dev). The human maintainer defines architecture, supervises implementation, and makes all final decisions.
142
+
137
143
  ## License
138
144
 
139
145
  GPL-3.0-or-later. See `LICENSE`.
package/css/00-tokens.css CHANGED
@@ -7,13 +7,25 @@
7
7
  --border: oklch(100% 0 0deg / 15%);
8
8
  --text: oklch(90% 0.005 280deg);
9
9
  --text-active: color-mix(in oklch, var(--text) 15%, transparent);
10
+ /* ===== Themeable tokens (the consumer-configurable "template") =====
11
+ These default to the neutral built-in look; a consumer recolors the UI by
12
+ overriding them via createTerminal's `theme` option (which sets them on the
13
+ terminal root) or its own CSS. vibecli sets a purple palette. Everything
14
+ above is the fixed design palette; everything here is the public theme API. */
15
+ --accent: #a78bfa; /* accent action colour (mobile "+", toggled key button) */
16
+ --tab-hover-bg: var(--text-active); /* hovered inactive tab / pressed controls */
17
+ --tab-active-bg: color-mix(in oklch, var(--text) 22%, var(--surface)); /* active tab fill */
18
+ --tab-active-fg: var(--text); /* active tab text */
10
19
  --radius: 0.75rem;
11
20
  --radius-inner: 0.5rem;
12
21
  --touch-target: 2.75rem; /* 44px — Apple HIG minimum */
13
22
  --pill-outer: calc(
14
23
  var(--touch-target) + 2 * var(--sp-1) + 2px
15
24
  ); /* touch-target + 2×padding + 2×border */
16
-
25
+ /* Mobile bottom bar height (a 44px control + the bar's 2×sp-2 padding + its
26
+ 1px top border). Bottom-anchored chrome (banner, toast, scroll-to-bottom,
27
+ key grid) lifts by this so it clears the bar. */
28
+ --wt-switcher-h: calc(var(--touch-target) + 2 * var(--sp-2) + 1px);
17
29
  --icon: 1.125rem; /* 18px */
18
30
  --icon-desktop: 1rem; /* 16px */
19
31
  --sp-1: 0.25rem;
package/css/02-app.css CHANGED
@@ -10,6 +10,15 @@
10
10
  above the viewport), so native scroll anchoring is safe to leave on
11
11
  and keeps the read position steady on the rare insert-above. */
12
12
  overflow-anchor: auto;
13
+ /* .term is the scroll container, so the touch-action intersection for a
14
+ touch on the output stops here (the html/body pan-x pan-y does not reach
15
+ inside a nested scroller). Declare it here too: native one/two-finger
16
+ scroll is allowed, double-tap-zoom is blocked (snappy taps, no 300ms
17
+ delay), and — critically — this does NOT affect native long-press text
18
+ selection (touch-action governs scroll/zoom, not selection). Avoid the
19
+ `manipulation` alias: it breaks pointercancel-on-scroll in Safari
20
+ (WebKit bug 240917), which the switcher-bar gesture relies on. */
21
+ touch-action: pan-x pan-y;
13
22
  font-family: var(--font-mono);
14
23
  font-size: 14px;
15
24
  line-height: 17px;
@@ -94,8 +103,22 @@
94
103
  textarea owns input, the keyboard, and IME. Keeping the editable
95
104
  element off the scroll content is what lets the first touch-drag
96
105
  scroll instead of placing a caret (bug 6), and not re-rendering it
97
- as an editable keeps a selection alive across redraws (bug 1). */
106
+ as an editable keeps a selection alive across redraws (bug 1).
107
+
108
+ The two selection properties below are load-bearing on touch: the
109
+ browser's native long-press-to-select-a-word + OS copy/paste callout
110
+ (iOS Safari and Android Chrome) is our selection UX, so we opt IN
111
+ explicitly and must never let an ancestor override them. `user-select`
112
+ is spec'd as non-inherited but WebKit/Chromium treat it as inherited,
113
+ so a stray `user-select: none` on html/body/.term would silently kill
114
+ selection here; keep it `text` (modern Safari honors the unprefixed
115
+ form, so no `-webkit-` pair is needed). `-webkit-touch-callout: default`
116
+ keeps the iOS callout (a page-level `none` suppresses it) and has no
117
+ standard equivalent. No JS may `preventDefault` a touch/pointer/
118
+ contextmenu over this element or the native selection dies (see the
119
+ kernel focus handling + contextMenu feature). */
98
120
  user-select: text;
121
+ -webkit-touch-callout: default;
99
122
  }
100
123
 
101
124
  /* IME composition view: floating overlay positioned at the cursor
@@ -417,70 +440,6 @@
417
440
  display: none;
418
441
  }
419
442
 
420
- /* Desktop: scroll-to-bottom is still genuinely useful for jumping back
421
- after scrolling up to read history, so it surfaces here as a
422
- bottom-center pill with a label. .key-toolbar.scrolled-up is
423
- toggled by app.ts, same as on touch. The display+opacity transition
424
- uses allow-discrete so the fade-out plays cleanly when the user
425
- clicks (scroll-to-bottom clears .scrolled-up, reverting display:none
426
- after the opacity has gone to 0). */
427
- .kb-scroll-bottom {
428
- position: fixed;
429
- left: 50%;
430
- bottom: calc(var(--safe-bottom, 0px) + var(--sp-3));
431
- transform: translateX(-50%);
432
- display: none;
433
- opacity: 0;
434
- align-items: center;
435
- gap: var(--sp-1);
436
- padding: var(--sp-2) var(--sp-3);
437
- background: var(--surface);
438
- backdrop-filter: var(--blur);
439
- border: 1px solid var(--border);
440
- border-radius: var(--radius);
441
- color: var(--text);
442
- font: inherit;
443
- font-size: 0.875rem;
444
- font-weight: 500;
445
- cursor: pointer;
446
- z-index: 10;
447
- transition:
448
- opacity var(--dur-standard) var(--ease-standard),
449
- background-color var(--dur-micro) var(--ease-standard),
450
- display var(--dur-standard) allow-discrete;
451
- }
452
-
453
- .kb-scroll-bottom::after {
454
- content: "Scroll to bottom";
455
- }
456
-
457
- .kb-scroll-bottom svg {
458
- width: var(--icon);
459
- height: var(--icon);
460
- stroke: currentcolor;
461
- fill: none;
462
- stroke-width: 2;
463
- stroke-linecap: round;
464
- stroke-linejoin: round;
465
- }
466
-
467
- .kb-scroll-bottom:hover {
468
- background: color-mix(in oklch, var(--text) 8%, var(--surface));
469
- }
470
-
471
- .kb-scroll-bottom:active {
472
- background: var(--text-active);
473
- }
474
-
475
- .key-toolbar.scrolled-up .kb-scroll-bottom {
476
- display: inline-flex;
477
- opacity: 1;
478
-
479
- @starting-style {
480
- opacity: 0;
481
- }
482
- }
483
-
484
443
  @media (pointer: coarse) {
485
444
  .key-toolbar {
486
445
  position: fixed;
@@ -516,11 +475,6 @@
516
475
  height: var(--pill-outer);
517
476
  }
518
477
 
519
- /* Collapsed + scrolled up: 2 buttons tall (+2px border). */
520
- .key-toolbar.collapsed.scrolled-up {
521
- height: calc(2 * var(--touch-target) + var(--sp-1) + 2 * var(--sp-1) + 2px);
522
- }
523
-
524
478
  /* Grid placement: toggle at col 5 row 1 (top-right). */
525
479
  .kb-toggle {
526
480
  grid-area: 1 / 5;
@@ -551,55 +505,6 @@
551
505
  grid-area: 2 / 4;
552
506
  }
553
507
 
554
- /* Scroll-bottom: col 5 row 2 (below toggle). Resets the desktop
555
- FAB-specific properties (fixed position, pill padding, surface
556
- background, label) so the button collapses back to a grid-area
557
- icon-only key. */
558
- .key-toolbar .kb-scroll-bottom {
559
- position: static;
560
- transform: none;
561
- bottom: auto;
562
- left: auto;
563
- grid-area: 2 / 5;
564
- display: none;
565
- opacity: 0;
566
- padding: 0;
567
- background: transparent;
568
- backdrop-filter: none;
569
- border: none;
570
- border-radius: var(--radius-inner);
571
- font-size: inherit;
572
- /* Touch override drops the desktop label (::after content: "")
573
- so only the SVG remains. The base rule sets align-items:center
574
- but inherits justify-content:flex-start, which would push the
575
- lone icon to the left edge of the touch-target-sized cell —
576
- same unequal-padding issue as the toolbar toggle. Center it. */
577
- justify-content: center;
578
- transition:
579
- opacity var(--dur-standard) var(--ease-standard),
580
- display var(--dur-standard) allow-discrete;
581
- }
582
-
583
- .kb-scroll-bottom::after {
584
- /* No label on touch. Remove the box entirely (not just its text) so
585
- it isn't an empty flex item — combined with the base rule's
586
- gap:var(--sp-1) it would shove the lone icon half a gap off-centre. */
587
- content: "";
588
- display: none;
589
- }
590
- .kb-scroll-bottom:hover {
591
- background: transparent;
592
- }
593
-
594
- .key-toolbar.scrolled-up .kb-scroll-bottom {
595
- display: flex;
596
- opacity: 1;
597
-
598
- @starting-style {
599
- opacity: 0;
600
- }
601
- }
602
-
603
508
  /* Keys fade in/out during open/close. The container's width/height
604
509
  transition handles the background slide; opacity handles the
605
510
  individual button appearance. Stagger the fade slightly after
@@ -691,7 +596,6 @@
691
596
 
692
597
  /* No-transition on first render */
693
598
  .key-toolbar.no-transition,
694
- .key-toolbar.no-transition .kb-scroll-bottom,
695
599
  .key-toolbar.no-transition .kb-toggle .icon-hamburger,
696
600
  .key-toolbar.no-transition .kb-toggle .icon-close {
697
601
  transition: none;
@@ -0,0 +1,82 @@
1
+ /* ===== web-terminal-ui design system: primitives =====
2
+ The v3 kernel builds named layout regions (wt-region-*) as siblings of the
3
+ terminal surface (.term, styled in 02-app.css); features mount chrome into
4
+ region slots. The regions are transparent groupings (display: contents) so
5
+ each feature's element floats over the fixed-inset terminal, and an absent
6
+ feature contributes nothing.
7
+
8
+ The stylesheet is layered by concern and concatenated in css/MANIFEST order:
9
+ tokens (00) -> reset (01) -> terminal core + legacy key toolbar (02) -> these
10
+ shared primitives (03) -> per-feature chrome (04 feedback, 05 tabs, 06 mobile)
11
+ -> animations (07, where every .wt-animate-gated transition/keyframe lives, so
12
+ motion is opt-in from one place). Cascade order is the file order here.
13
+
14
+ Primitives are the building blocks every feature reuses: the transparent
15
+ layout regions, the 44px touch button, and the toast surface. */
16
+
17
+ .wt-region,
18
+ .wt-slot {
19
+ display: contents;
20
+ }
21
+
22
+ /* ===== Shared button primitive (44px touch target) ===== */
23
+ .wt-btn {
24
+ display: inline-flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ min-width: var(--touch-target);
28
+ min-height: var(--touch-target);
29
+ background: transparent;
30
+ color: var(--text);
31
+ border: none;
32
+ border-radius: var(--radius-inner);
33
+ font: inherit;
34
+ cursor: pointer;
35
+ touch-action: manipulation;
36
+ user-select: none;
37
+ }
38
+
39
+ .wt-btn:active {
40
+ background: var(--text-active);
41
+ }
42
+
43
+ .wt-btn svg {
44
+ width: var(--icon);
45
+ height: var(--icon);
46
+ stroke: currentcolor;
47
+ fill: none;
48
+ stroke-width: 2;
49
+ stroke-linecap: round;
50
+ stroke-linejoin: round;
51
+ }
52
+
53
+ /* ===== Toast (kernel shared primitive) =====
54
+ Top-centre on every form factor (both desktop and the tabbed mobile UI dock a
55
+ bar at the bottom, so a bottom toast would collide with it; top-centre is
56
+ clear and the same everywhere). The vv-top/safe-top offset keeps it below the
57
+ notch and follows iOS's visual-viewport shift. */
58
+ .wt-toast {
59
+ position: fixed;
60
+ top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + var(--sp-3));
61
+ left: 50%;
62
+ transform: translateX(-50%);
63
+ max-width: calc(100% - 2 * var(--sp-3));
64
+ padding: var(--sp-1) var(--sp-3);
65
+ background: var(--surface);
66
+ backdrop-filter: var(--blur);
67
+ border: 1px solid oklch(70% 0.1 160deg / 30%);
68
+ border-radius: var(--radius);
69
+ color: var(--text);
70
+ font-family: var(--font-ui);
71
+ font-size: 0.8125rem;
72
+ font-weight: 500;
73
+ text-align: center;
74
+ z-index: 40;
75
+ display: none;
76
+ opacity: 0;
77
+ }
78
+
79
+ .wt-toast.visible {
80
+ display: block;
81
+ opacity: 1;
82
+ }
@@ -0,0 +1,166 @@
1
+ /* ===== Feedback + overlay chrome =====
2
+ Transient, non-tab feature surfaces: the connection banner, the
3
+ scroll-to-bottom control, the right-click/long-press context menu, and the
4
+ tab catching-up cue. Grouped because they are all fixed, floating, and
5
+ short-lived; their motion lives in 07-animations.css. */
6
+
7
+ /* ===== Connection banner (connectionBanner feature) =====
8
+ Top-centre on every form factor (same rationale as the toast: a bottom bar is
9
+ always docked now). */
10
+ .wt-conn-banner {
11
+ position: fixed;
12
+ top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + var(--sp-3));
13
+ left: 50%;
14
+ transform: translateX(-50%);
15
+ max-width: calc(100% - 2 * var(--sp-3));
16
+ padding: var(--sp-1) var(--sp-3);
17
+ background: var(--surface);
18
+ backdrop-filter: var(--blur);
19
+ border: 1px solid var(--border);
20
+ border-radius: var(--radius);
21
+ color: var(--text);
22
+ font-family: var(--font-ui);
23
+ font-size: 0.8125rem;
24
+ font-weight: 500;
25
+ text-align: center;
26
+ z-index: 20;
27
+ display: none;
28
+ }
29
+
30
+ .wt-conn-banner.visible {
31
+ display: block;
32
+ }
33
+
34
+ .wt-conn-banner[data-state="reconnecting"] {
35
+ border-color: oklch(70% 0.15 60deg / 40%);
36
+ }
37
+
38
+ .wt-conn-banner[data-state="offline"] {
39
+ color: #f55;
40
+ border-color: oklch(60% 0.2 25deg / 50%);
41
+ }
42
+
43
+ .wt-conn-banner[data-state="restarted"] {
44
+ color: #fa5;
45
+ border-color: oklch(70% 0.15 60deg / 40%);
46
+ }
47
+
48
+ /* ===== Scroll-to-bottom (scrollToBottom feature) =====
49
+ A compact floating icon button docked in the bottom-right corner, shown only
50
+ while the user has scrolled up. Same button on both form factors (the old
51
+ desktop bottom-centre text pill was dropped in the design-system switch, and
52
+ its dead CSS removed from 02-app.css); each form factor just lifts it above
53
+ its own bottom bar. Size + icon centering come from the .wt-btn primitive (a
54
+ 44px icon button); this rule adds only the floating chrome and placement. */
55
+ .wt-scroll-bottom {
56
+ position: fixed;
57
+ right: var(--sp-3);
58
+ bottom: calc(var(--safe-bottom, 0px) + var(--sp-3) + var(--kb-inset, 0px));
59
+ background: var(--surface);
60
+ backdrop-filter: var(--blur);
61
+ border: 1px solid var(--border);
62
+ border-radius: var(--radius);
63
+ color: color-mix(in oklch, var(--text) 70%, transparent);
64
+ z-index: 10;
65
+ display: none;
66
+ }
67
+
68
+ /* The scroll slot gets .scrolled-up toggled by the feature; show the button. */
69
+ .wt-slot-scroll.scrolled-up .wt-scroll-bottom {
70
+ display: inline-flex;
71
+ }
72
+
73
+ /* Hover brightens the icon + border; press fills the background (the .wt-btn
74
+ :active fill would otherwise be masked by this button's own surface fill). */
75
+ .wt-scroll-bottom:hover {
76
+ border-color: color-mix(in oklch, var(--text) 35%, transparent);
77
+ color: var(--text);
78
+ }
79
+
80
+ .wt-scroll-bottom:active {
81
+ background: var(--text-active);
82
+ }
83
+
84
+ /* Mobile + tabbed: lift above the switcher bar and the keyboard (WS3). */
85
+ @media (pointer: coarse) {
86
+ .wt-tabbed .wt-scroll-bottom {
87
+ bottom: calc(
88
+ var(--wt-switcher-h) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
89
+ );
90
+ }
91
+ }
92
+
93
+ /* Desktop + tabbed: the tab strip docks at the bottom, so lift the button above
94
+ it with a clear gap. */
95
+ @media (pointer: fine) {
96
+ .wt-tabbed .wt-scroll-bottom {
97
+ bottom: calc(var(--wt-tabbar-h, 44px) + var(--sp-3));
98
+ }
99
+ }
100
+
101
+ /* ===== Context menu (contextMenu feature) ===== */
102
+ .wt-ctx-menu {
103
+ position: fixed;
104
+ display: none;
105
+ flex-direction: column;
106
+ background: var(--surface);
107
+ backdrop-filter: var(--blur);
108
+ border: 1px solid var(--border);
109
+ border-radius: var(--radius);
110
+ padding: var(--sp-1);
111
+ z-index: 30;
112
+ }
113
+
114
+ .wt-ctx-menu.visible {
115
+ display: flex;
116
+ }
117
+
118
+ .wt-ctx-menu button {
119
+ background: transparent;
120
+ color: var(--text);
121
+ border: none;
122
+ border-radius: var(--radius-inner);
123
+ font-family: var(--font-ui);
124
+ font-size: 0.8125rem;
125
+ font-weight: 500;
126
+ padding: var(--sp-1) var(--sp-3);
127
+ cursor: pointer;
128
+ text-align: left;
129
+ }
130
+
131
+ .wt-ctx-menu button:active {
132
+ background: var(--text-active);
133
+ }
134
+
135
+ @media (pointer: coarse) {
136
+ .wt-ctx-menu button {
137
+ min-height: var(--touch-target);
138
+ padding-block: var(--sp-2);
139
+ touch-action: manipulation;
140
+ }
141
+ }
142
+
143
+ /* ===== Catching-up cue (tabs feature, sections 12/13) =====
144
+ A switched-into tab's cached screen is stale until its resume delta lands, so
145
+ it must not read as live. This unobtrusive cue shows only if the delta has not
146
+ arrived shortly after the switch, and clears on the first screen frame. */
147
+ .wt-catchup {
148
+ position: fixed;
149
+ top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + 44px);
150
+ left: 50%;
151
+ transform: translateX(-50%);
152
+ padding: 2px var(--sp-2);
153
+ background: var(--surface);
154
+ backdrop-filter: var(--blur);
155
+ border: 1px solid var(--border);
156
+ border-radius: var(--radius);
157
+ color: color-mix(in oklch, var(--text) 75%, transparent);
158
+ font-family: var(--font-ui);
159
+ font-size: 0.75rem;
160
+ z-index: 24;
161
+ display: none;
162
+ }
163
+
164
+ .wt-catchup.visible {
165
+ display: block;
166
+ }