@cplieger/web-terminal-ui 3.5.0 → 4.0.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 +52 -23
- package/css/00-tokens.css +8 -2
- package/css/01-scope.css +56 -0
- package/css/{02-app.css → 02-terminal.css} +64 -239
- package/css/{03-primitives.css → 10-primitives.css} +21 -19
- package/css/20-connection-banner.css +42 -0
- package/css/21-scroll-to-bottom.css +65 -0
- package/css/22-context-menu.css +45 -0
- package/css/23-toolbar.css +181 -0
- package/css/{05-tabs.css → 30-tabs.css} +96 -57
- package/css/{06-mobile.css → 31-switcher.css} +66 -81
- package/css/{07-animations.css → 40-animations.css} +15 -15
- package/css/MANIFEST +19 -11
- package/css/MANIFEST.single +12 -0
- package/css/MANIFEST.tabbed +16 -0
- package/css/MANIFEST.touch +12 -0
- package/css/{01-reset.css → page.css} +18 -19
- package/package.json +18 -5
- package/scaffold/index.html +5 -0
- package/src/features/context-menu.ts +6 -33
- package/src/features/menu-position.ts +50 -0
- package/src/features/{tabs.ts → tabs/index.ts} +204 -655
- package/src/features/tabs/model.ts +334 -0
- package/src/features/tabs/strip.ts +101 -0
- package/src/features/tabs/switcher.ts +129 -0
- package/src/index.ts +1 -0
- package/src/kernel/a11y.ts +30 -5
- package/src/kernel/kernel.ts +149 -59
- package/src/kernel/types.ts +55 -14
- package/src/presets/agent-tabbed.ts +15 -0
- package/src/presets/single.ts +27 -0
- package/src/presets/tabbed.ts +42 -0
- package/src/presets/touch.ts +13 -0
- package/src/presets.ts +12 -76
- package/src/viewport.ts +28 -15
- package/css/04-feedback.css +0 -180
package/src/viewport.ts
CHANGED
|
@@ -20,6 +20,11 @@ import { scroll } from "@cplieger/web-terminal-engine";
|
|
|
20
20
|
const SETTLE_MS = 350;
|
|
21
21
|
|
|
22
22
|
let termWrap: HTMLElement;
|
|
23
|
+
// The terminal root (.wt-root): the geometry CSS vars (--kb-inset, --vv-top)
|
|
24
|
+
// are published here so the terminal subtree — and nothing else on the host
|
|
25
|
+
// page — inherits them. Falls back to termWrap if init was somehow given no
|
|
26
|
+
// root (never in practice; the kernel always passes it).
|
|
27
|
+
let varTarget: HTMLElement;
|
|
23
28
|
let onSettled: ((wasAtBottom: boolean) => void) | null = null;
|
|
24
29
|
let inTransition = false;
|
|
25
30
|
let wasAtBottomAtStart = true;
|
|
@@ -59,6 +64,10 @@ function startTransition(): void {
|
|
|
59
64
|
|
|
60
65
|
export function init(opts: {
|
|
61
66
|
termWrap: HTMLElement;
|
|
67
|
+
/** The terminal root (.wt-root): receives the geometry CSS vars the sibling
|
|
68
|
+
* chrome reads (--kb-inset, --vv-top), so they scope to the terminal
|
|
69
|
+
* subtree instead of leaking onto the host document. */
|
|
70
|
+
root?: HTMLElement;
|
|
62
71
|
onSettled: (wasAtBottom: boolean) => void;
|
|
63
72
|
/** Ignore the visualViewport keyboard geometry (a hardware-keyboard device
|
|
64
73
|
* has no soft keyboard to accommodate); only reserved bottom chrome insets
|
|
@@ -66,6 +75,7 @@ export function init(opts: {
|
|
|
66
75
|
suppressKeyboardInset?: () => boolean;
|
|
67
76
|
}): void {
|
|
68
77
|
termWrap = opts.termWrap;
|
|
78
|
+
varTarget = opts.root ?? opts.termWrap;
|
|
69
79
|
onSettled = opts.onSettled;
|
|
70
80
|
suppressKeyboardInset = opts.suppressKeyboardInset ?? (() => false);
|
|
71
81
|
|
|
@@ -105,12 +115,13 @@ export function init(opts: {
|
|
|
105
115
|
// folds the reserve into the bottom offset here. The reserve excludes the
|
|
106
116
|
// keyboard (it is measured with the keyboard closed), so adding it to
|
|
107
117
|
// bottomInset does not double-count.
|
|
118
|
+
// Read the reserve off the terminal itself: tabs publishes it on the
|
|
119
|
+
// root (.wt-root), and termWrap inherits it — nothing terminal-scoped
|
|
120
|
+
// lives on the document root anymore.
|
|
108
121
|
const rawReserve = Math.max(
|
|
109
122
|
0,
|
|
110
123
|
Math.round(
|
|
111
|
-
parseFloat(
|
|
112
|
-
getComputedStyle(document.documentElement).getPropertyValue("--wt-reserve-bottom"),
|
|
113
|
-
) || 0,
|
|
124
|
+
parseFloat(getComputedStyle(termWrap).getPropertyValue("--wt-reserve-bottom")) || 0,
|
|
114
125
|
),
|
|
115
126
|
);
|
|
116
127
|
// Sanity cap: the reserve is bottom chrome (a tab bar), tens of px. A value
|
|
@@ -122,13 +133,13 @@ export function init(opts: {
|
|
|
122
133
|
const bottom = bottomInset + reserve;
|
|
123
134
|
termWrap.style.top = offsetTop > 0 ? `${offsetTop}px` : "";
|
|
124
135
|
termWrap.style.bottom = bottom > 0 ? `${bottom}px` : "";
|
|
125
|
-
// Expose the same geometry as CSS vars for the sibling
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
// Expose the same geometry as CSS vars for the sibling chrome, on the
|
|
137
|
+
// terminal ROOT (scoped: the host page never sees them): --kb-inset
|
|
138
|
+
// lifts the bottom banner above the keyboard; --vv-top lets the top key
|
|
139
|
+
// toolbar follow the visual viewport's offset (otherwise it scrolls off
|
|
140
|
+
// the top when iOS shifts the layout up).
|
|
141
|
+
varTarget.style.setProperty("--kb-inset", `${bottomInset}px`);
|
|
142
|
+
varTarget.style.setProperty("--vv-top", `${offsetTop}px`);
|
|
132
143
|
startTransition();
|
|
133
144
|
};
|
|
134
145
|
vv.addEventListener("resize", onChange);
|
|
@@ -192,11 +203,13 @@ export function teardown(): void {
|
|
|
192
203
|
clearTimeout(settleTimer);
|
|
193
204
|
settleTimer = null;
|
|
194
205
|
}
|
|
195
|
-
// Clear the
|
|
196
|
-
// a remount (which would recompute them) leaves no stale inset
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
206
|
+
// Clear the CSS vars onChange published on the terminal root, so a destroy
|
|
207
|
+
// without a remount (which would recompute them) leaves no stale inset.
|
|
208
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- teardown may run before any init in tests
|
|
209
|
+
if (varTarget) {
|
|
210
|
+
varTarget.style.removeProperty("--kb-inset");
|
|
211
|
+
varTarget.style.removeProperty("--vv-top");
|
|
212
|
+
}
|
|
200
213
|
inTransition = false;
|
|
201
214
|
onSettled = null;
|
|
202
215
|
suppressKeyboardInset = () => false;
|
package/css/04-feedback.css
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
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
|
-
pointer-events: none; /* passive status surface: never steal taps from the terminal beneath */
|
|
28
|
-
display: none;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.wt-conn-banner.visible {
|
|
32
|
-
display: block;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.wt-conn-banner[data-state="reconnecting"] {
|
|
36
|
-
border-color: oklch(70% 0.15 60deg / 40%);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.wt-conn-banner[data-state="offline"] {
|
|
40
|
-
color: #f55;
|
|
41
|
-
border-color: oklch(60% 0.2 25deg / 50%);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.wt-conn-banner[data-state="restarted"] {
|
|
45
|
-
color: #fa5;
|
|
46
|
-
border-color: oklch(70% 0.15 60deg / 40%);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/* ===== Scroll-to-bottom (scrollToBottom feature) =====
|
|
50
|
-
A compact floating icon button docked in the bottom-right corner, shown only
|
|
51
|
-
while the user has scrolled up. Same button on both form factors (the old
|
|
52
|
-
desktop bottom-centre text pill was dropped in the design-system switch, and
|
|
53
|
-
its dead CSS removed from 02-app.css); each form factor just lifts it above
|
|
54
|
-
its own bottom bar. Size + icon centering come from the .wt-btn primitive (a
|
|
55
|
-
44px icon button); this rule adds only the floating chrome and placement. */
|
|
56
|
-
.wt-scroll-bottom {
|
|
57
|
-
position: fixed;
|
|
58
|
-
right: var(--sp-3);
|
|
59
|
-
bottom: calc(var(--safe-bottom, 0px) + var(--sp-3) + var(--kb-inset, 0px));
|
|
60
|
-
background: var(--surface);
|
|
61
|
-
backdrop-filter: var(--blur);
|
|
62
|
-
border: 1px solid var(--border);
|
|
63
|
-
/* Match the tab "+" buttons (and every other .wt-btn), which round to
|
|
64
|
-
--radius-inner. Both are 44px boxes, so matching the corner radius makes
|
|
65
|
-
the FAB read as the same-sized button as the new-tab "+" beside/above it
|
|
66
|
-
rather than a rounder, different-looking control. */
|
|
67
|
-
border-radius: var(--radius-inner);
|
|
68
|
-
/* Full --text icon, matching the switcher's keyboard/switch/"+" icons (which
|
|
69
|
-
use .wt-btn's default color). Was a muted 70% mix that read as grey next to
|
|
70
|
-
those white bar icons. */
|
|
71
|
-
color: var(--text);
|
|
72
|
-
z-index: 10;
|
|
73
|
-
display: none;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/* The scroll slot gets .scrolled-up toggled by the feature; show the button. */
|
|
77
|
-
.wt-slot-scroll.scrolled-up .wt-scroll-bottom {
|
|
78
|
-
display: inline-flex;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/* Hover brightens the border; press fills the background (the .wt-btn :active
|
|
82
|
-
fill would otherwise be masked by this button's own surface fill). The icon
|
|
83
|
-
is already full --text, so hover no longer needs to change it. */
|
|
84
|
-
.wt-scroll-bottom:hover {
|
|
85
|
-
border-color: color-mix(in oklch, var(--text) 35%, transparent);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.wt-scroll-bottom:active {
|
|
89
|
-
background: var(--text-active);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/* Mobile + tabbed: lift above the switcher bar and the keyboard (WS3), and pull
|
|
93
|
-
the right edge in to --sp-2 so the button sits directly above the switcher's
|
|
94
|
-
"+" (whose right edge is inset by the bar's --sp-2 padding). Both are 44px
|
|
95
|
-
border-box boxes, so matching the right inset lines them up in one vertical
|
|
96
|
-
column (they were 4px out: the FAB's base --sp-3 vs the "+"'s --sp-2). */
|
|
97
|
-
@media (pointer: coarse) {
|
|
98
|
-
.wt-tabbed .wt-scroll-bottom {
|
|
99
|
-
right: var(--sp-2);
|
|
100
|
-
bottom: calc(
|
|
101
|
-
var(--wt-switcher-h) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/* Desktop + tabbed: the tab strip docks at the bottom, so lift the button above
|
|
107
|
-
it with a clear gap. */
|
|
108
|
-
@media (pointer: fine) {
|
|
109
|
-
.wt-tabbed .wt-scroll-bottom {
|
|
110
|
-
bottom: calc(var(--wt-tabbar-h, 44px) + var(--sp-3));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/* ===== Context menu (contextMenu feature) ===== */
|
|
115
|
-
.wt-ctx-menu {
|
|
116
|
-
position: fixed;
|
|
117
|
-
display: none;
|
|
118
|
-
flex-direction: column;
|
|
119
|
-
background: var(--surface);
|
|
120
|
-
backdrop-filter: var(--blur);
|
|
121
|
-
border: 1px solid var(--border);
|
|
122
|
-
border-radius: var(--radius);
|
|
123
|
-
padding: var(--sp-1);
|
|
124
|
-
z-index: 30;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.wt-ctx-menu.visible {
|
|
128
|
-
display: flex;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.wt-ctx-menu button {
|
|
132
|
-
background: transparent;
|
|
133
|
-
color: var(--text);
|
|
134
|
-
border: none;
|
|
135
|
-
border-radius: var(--radius-inner);
|
|
136
|
-
font-family: var(--font-ui);
|
|
137
|
-
font-size: 0.8125rem;
|
|
138
|
-
font-weight: 500;
|
|
139
|
-
padding: var(--sp-1) var(--sp-3);
|
|
140
|
-
cursor: pointer;
|
|
141
|
-
text-align: left;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.wt-ctx-menu button:active {
|
|
145
|
-
background: var(--text-active);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
@media (pointer: coarse) {
|
|
149
|
-
.wt-ctx-menu button {
|
|
150
|
-
min-height: var(--touch-target);
|
|
151
|
-
padding-block: var(--sp-2);
|
|
152
|
-
touch-action: manipulation;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/* ===== Catching-up cue (tabs feature, sections 12/13) =====
|
|
157
|
-
A switched-into tab's cached screen is stale until its resume delta lands, so
|
|
158
|
-
it must not read as live. This unobtrusive cue shows only if the delta has not
|
|
159
|
-
arrived shortly after the switch, and clears on the first screen frame. */
|
|
160
|
-
.wt-catchup {
|
|
161
|
-
position: fixed;
|
|
162
|
-
top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + 44px);
|
|
163
|
-
left: 50%;
|
|
164
|
-
transform: translateX(-50%);
|
|
165
|
-
padding: 2px var(--sp-2);
|
|
166
|
-
background: var(--surface);
|
|
167
|
-
backdrop-filter: var(--blur);
|
|
168
|
-
border: 1px solid var(--border);
|
|
169
|
-
border-radius: var(--radius);
|
|
170
|
-
color: color-mix(in oklch, var(--text) 75%, transparent);
|
|
171
|
-
font-family: var(--font-ui);
|
|
172
|
-
font-size: 0.75rem;
|
|
173
|
-
z-index: 24;
|
|
174
|
-
pointer-events: none; /* passive status surface: never steal taps from the terminal beneath */
|
|
175
|
-
display: none;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
.wt-catchup.visible {
|
|
179
|
-
display: block;
|
|
180
|
-
}
|