@cplieger/web-terminal-ui 2.1.4 → 3.0.1
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 +57 -32
- package/css/03-v3.css +590 -0
- package/css/MANIFEST +1 -0
- package/package.json +9 -5
- package/scaffold/index.html +8 -6
- package/src/composition.ts +15 -0
- package/src/features/activity-monitor.ts +64 -0
- package/src/features/animations.ts +29 -0
- package/src/features/clipboard.ts +98 -0
- package/src/features/connection-banner.ts +53 -0
- package/src/features/context-menu.ts +207 -0
- package/src/features/dom.ts +15 -0
- package/src/features/mobile-toolbar.ts +82 -0
- package/src/features/predictive-echo.ts +63 -0
- package/src/features/scroll-to-bottom.ts +49 -0
- package/src/features/tabs.ts +687 -0
- package/src/index.ts +30 -9
- package/src/kernel/a11y.ts +102 -0
- package/src/kernel/bus.ts +61 -0
- package/src/kernel/conn-state.ts +133 -0
- package/src/kernel/kernel.ts +688 -0
- package/src/kernel/regions.ts +119 -0
- package/src/kernel/types.ts +286 -0
- package/src/presets.ts +46 -0
- package/src/mount.ts +0 -903
- package/src/status.ts +0 -228
package/css/03-v3.css
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/* ===== v3 kernel + feature styles =====
|
|
2
|
+
The v3 kernel builds named layout regions (wt-region-*) as siblings of the
|
|
3
|
+
terminal surface (.term, styled in 02-app.css), and 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 exactly as the
|
|
6
|
+
pre-v3 chrome did; the terminal-core, key-toolbar (.key-toolbar/.kb-*), and
|
|
7
|
+
cursor/color styles in 02-app.css are reused unchanged.
|
|
8
|
+
|
|
9
|
+
Note: this is the pragmatic first-cut v3 stylesheet. The design's section 22.8
|
|
10
|
+
per-feature CSS split (core.css + mobile-toolbar.css/context-menu.css/tabs.css/
|
|
11
|
+
activity.css/animations.css + a css-manifest.mjs assembler) is a tracked
|
|
12
|
+
follow-up; the classes below are the seams that split will carve along. */
|
|
13
|
+
|
|
14
|
+
/* Regions + slots are transparent: they group a feature's DOM without imposing
|
|
15
|
+
layout, so a feature element inside them positions itself (fixed) over the
|
|
16
|
+
terminal, and an absent feature contributes nothing. */
|
|
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
|
+
.wt-toast {
|
|
55
|
+
position: fixed;
|
|
56
|
+
bottom: calc(var(--sp-3) + var(--kb-inset, 0px));
|
|
57
|
+
left: 50%;
|
|
58
|
+
transform: translateX(-50%);
|
|
59
|
+
max-width: calc(100% - 2 * var(--sp-3));
|
|
60
|
+
padding: var(--sp-1) var(--sp-3);
|
|
61
|
+
background: var(--surface);
|
|
62
|
+
backdrop-filter: var(--blur);
|
|
63
|
+
border: 1px solid oklch(70% 0.1 160deg / 30%);
|
|
64
|
+
border-radius: var(--radius);
|
|
65
|
+
color: var(--text);
|
|
66
|
+
font-family: var(--font-ui);
|
|
67
|
+
font-size: 0.8125rem;
|
|
68
|
+
font-weight: 500;
|
|
69
|
+
text-align: center;
|
|
70
|
+
z-index: 40;
|
|
71
|
+
display: none;
|
|
72
|
+
opacity: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.wt-toast.visible {
|
|
76
|
+
display: block;
|
|
77
|
+
opacity: 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ===== Connection banner (connectionBanner feature) ===== */
|
|
81
|
+
.wt-conn-banner {
|
|
82
|
+
position: fixed;
|
|
83
|
+
bottom: calc(var(--sp-3) + var(--kb-inset, 0px));
|
|
84
|
+
left: 50%;
|
|
85
|
+
transform: translateX(-50%);
|
|
86
|
+
max-width: calc(100% - 2 * var(--sp-3));
|
|
87
|
+
padding: var(--sp-1) var(--sp-3);
|
|
88
|
+
background: var(--surface);
|
|
89
|
+
backdrop-filter: var(--blur);
|
|
90
|
+
border: 1px solid var(--border);
|
|
91
|
+
border-radius: var(--radius);
|
|
92
|
+
color: var(--text);
|
|
93
|
+
font-family: var(--font-ui);
|
|
94
|
+
font-size: 0.8125rem;
|
|
95
|
+
font-weight: 500;
|
|
96
|
+
text-align: center;
|
|
97
|
+
z-index: 20;
|
|
98
|
+
display: none;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.wt-conn-banner.visible {
|
|
102
|
+
display: block;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.wt-conn-banner[data-state="reconnecting"] {
|
|
106
|
+
border-color: oklch(70% 0.15 60deg / 40%);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.wt-conn-banner[data-state="offline"] {
|
|
110
|
+
color: #f55;
|
|
111
|
+
border-color: oklch(60% 0.2 25deg / 50%);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.wt-conn-banner[data-state="restarted"] {
|
|
115
|
+
color: #fa5;
|
|
116
|
+
border-color: oklch(70% 0.15 60deg / 40%);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Desktop: move both the banner and toast to top-center so they don't collide
|
|
120
|
+
with the bottom-anchored scroll-to-bottom control. */
|
|
121
|
+
@media (pointer: fine) {
|
|
122
|
+
.wt-conn-banner,
|
|
123
|
+
.wt-toast {
|
|
124
|
+
top: var(--sp-3);
|
|
125
|
+
bottom: auto;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ===== Scroll-to-bottom (scrollToBottom feature) ===== */
|
|
130
|
+
.wt-scroll-bottom {
|
|
131
|
+
position: fixed;
|
|
132
|
+
left: 50%;
|
|
133
|
+
bottom: calc(var(--safe-bottom, 0px) + var(--sp-3) + var(--kb-inset, 0px));
|
|
134
|
+
transform: translateX(-50%);
|
|
135
|
+
gap: var(--sp-1);
|
|
136
|
+
padding: var(--sp-2) var(--sp-3);
|
|
137
|
+
background: var(--surface);
|
|
138
|
+
backdrop-filter: var(--blur);
|
|
139
|
+
border: 1px solid var(--border);
|
|
140
|
+
border-radius: var(--radius);
|
|
141
|
+
font-size: 0.875rem;
|
|
142
|
+
font-weight: 500;
|
|
143
|
+
z-index: 10;
|
|
144
|
+
display: none;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* The scroll slot gets .scrolled-up toggled by the feature; show the button. */
|
|
148
|
+
.wt-slot-scroll.scrolled-up .wt-scroll-bottom {
|
|
149
|
+
display: inline-flex;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ===== Context menu (contextMenu feature) ===== */
|
|
153
|
+
.wt-ctx-menu {
|
|
154
|
+
position: fixed;
|
|
155
|
+
display: none;
|
|
156
|
+
flex-direction: column;
|
|
157
|
+
background: var(--surface);
|
|
158
|
+
backdrop-filter: var(--blur);
|
|
159
|
+
border: 1px solid var(--border);
|
|
160
|
+
border-radius: var(--radius);
|
|
161
|
+
padding: var(--sp-1);
|
|
162
|
+
z-index: 30;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.wt-ctx-menu.visible {
|
|
166
|
+
display: flex;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.wt-ctx-menu button {
|
|
170
|
+
background: transparent;
|
|
171
|
+
color: var(--text);
|
|
172
|
+
border: none;
|
|
173
|
+
border-radius: var(--radius-inner);
|
|
174
|
+
font-family: var(--font-ui);
|
|
175
|
+
font-size: 0.8125rem;
|
|
176
|
+
font-weight: 500;
|
|
177
|
+
padding: var(--sp-1) var(--sp-3);
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
text-align: left;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.wt-ctx-menu button:active {
|
|
183
|
+
background: var(--text-active);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@media (pointer: coarse) {
|
|
187
|
+
.wt-ctx-menu button {
|
|
188
|
+
min-height: var(--touch-target);
|
|
189
|
+
padding-block: var(--sp-2);
|
|
190
|
+
touch-action: manipulation;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* ===== Tab bar (tabs feature) ===== */
|
|
195
|
+
.wt-region-top-bar {
|
|
196
|
+
/* On desktop the tab strip docks to the top; the region is transparent, so
|
|
197
|
+
the bar element below owns its own fixed placement. */
|
|
198
|
+
display: contents;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.wt-tab-bar {
|
|
202
|
+
position: fixed;
|
|
203
|
+
top: calc(var(--safe-top, 0px) + var(--vv-top, 0px));
|
|
204
|
+
left: 0;
|
|
205
|
+
right: 0;
|
|
206
|
+
display: flex;
|
|
207
|
+
gap: 2px;
|
|
208
|
+
padding: var(--sp-1);
|
|
209
|
+
background: var(--surface);
|
|
210
|
+
backdrop-filter: var(--blur);
|
|
211
|
+
border-bottom: 1px solid var(--border);
|
|
212
|
+
overflow-x: auto;
|
|
213
|
+
z-index: 15;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.wt-tab {
|
|
217
|
+
display: inline-flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
gap: var(--sp-1);
|
|
220
|
+
padding: var(--sp-1) var(--sp-2);
|
|
221
|
+
border-radius: var(--radius-inner);
|
|
222
|
+
color: color-mix(in oklch, var(--text) 70%, transparent);
|
|
223
|
+
font-family: var(--font-ui);
|
|
224
|
+
font-size: 0.8125rem;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
white-space: nowrap;
|
|
227
|
+
user-select: none;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.wt-tab-active {
|
|
231
|
+
background: color-mix(in oklch, var(--text) 10%, var(--surface));
|
|
232
|
+
color: var(--text);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.wt-tab-label {
|
|
236
|
+
max-width: 14ch;
|
|
237
|
+
overflow: hidden;
|
|
238
|
+
text-overflow: ellipsis;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.wt-tab-close {
|
|
242
|
+
background: transparent;
|
|
243
|
+
border: none;
|
|
244
|
+
color: inherit;
|
|
245
|
+
cursor: pointer;
|
|
246
|
+
padding: 0 var(--sp-1);
|
|
247
|
+
border-radius: var(--radius-inner);
|
|
248
|
+
opacity: 0.6;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.wt-tab-close:hover {
|
|
252
|
+
opacity: 1;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.wt-tab-new {
|
|
256
|
+
background: transparent;
|
|
257
|
+
border: none;
|
|
258
|
+
color: var(--text);
|
|
259
|
+
cursor: pointer;
|
|
260
|
+
font-size: 1.1rem;
|
|
261
|
+
padding: 0 var(--sp-2);
|
|
262
|
+
border-radius: var(--radius-inner);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Shift the terminal down so the fixed tab bar does not overlap the first
|
|
266
|
+
rows. The tab bar is ~36px tall; the terminal padding-top absorbs it. */
|
|
267
|
+
.wt-tab-bar ~ .term,
|
|
268
|
+
:has(> .wt-tab-bar) .term {
|
|
269
|
+
padding-top: 40px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* ===== Activity dots (activityMonitor via tabs) =====
|
|
273
|
+
Four-state, distinguishable without color (shape + motion, not hue alone),
|
|
274
|
+
per section 12 / WCAG 1.4.1. The one .wt-status-dot vocabulary is shared by
|
|
275
|
+
the desktop strip, the mobile switcher, and the overview sheet rows. */
|
|
276
|
+
.wt-status-dot {
|
|
277
|
+
width: 9px;
|
|
278
|
+
height: 9px;
|
|
279
|
+
border-radius: 50%;
|
|
280
|
+
flex: none;
|
|
281
|
+
background: color-mix(in oklch, var(--text) 40%, transparent);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.wt-status-dot[data-status="working"] {
|
|
285
|
+
background: oklch(75% 0.15 90deg);
|
|
286
|
+
animation: wt-pulse 1.2s ease-in-out infinite;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.wt-status-dot[data-status="idle"] {
|
|
290
|
+
background: color-mix(in oklch, var(--text) 40%, transparent);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.wt-status-dot[data-status="input"] {
|
|
294
|
+
background: oklch(70% 0.18 25deg);
|
|
295
|
+
/* A ring so needs-input reads distinctly without relying on hue alone. */
|
|
296
|
+
box-shadow: 0 0 0 2px oklch(70% 0.18 25deg / 40%);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.wt-status-dot[data-status="exited"] {
|
|
300
|
+
background: transparent;
|
|
301
|
+
border: 1px solid color-mix(in oklch, var(--text) 40%, transparent);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* Reduced-motion (or animations feature off): drop the pulse, keep the color. */
|
|
305
|
+
@keyframes wt-pulse {
|
|
306
|
+
50% {
|
|
307
|
+
opacity: 0.4;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
@media (prefers-reduced-motion: reduce) {
|
|
312
|
+
.wt-status-dot[data-status="working"] {
|
|
313
|
+
animation: none;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* ===== Animations feature =====
|
|
318
|
+
Motion is off unless the animations feature marks the root .wt-animate (and
|
|
319
|
+
the OS is not in reduced-motion). Transitions are declared here, gated on it. */
|
|
320
|
+
.wt-animate .wt-toast,
|
|
321
|
+
.wt-animate .wt-conn-banner,
|
|
322
|
+
.wt-animate .wt-scroll-bottom,
|
|
323
|
+
.wt-animate .wt-ctx-menu {
|
|
324
|
+
transition:
|
|
325
|
+
opacity var(--dur-standard) var(--ease-standard),
|
|
326
|
+
display var(--dur-standard) allow-discrete;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.wt-animate .wt-tab {
|
|
330
|
+
transition: background-color var(--dur-micro) var(--ease-standard);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* ===== Mobile bottom-switcher (tabs feature, section 12) =====
|
|
334
|
+
The switcher is the mobile tab surface: it shows the active tab and a
|
|
335
|
+
position indicator as a tap/swipe target, plus a >=44px overview control that
|
|
336
|
+
carries the aggregate needs-input badge. Hidden on a fine pointer, where the
|
|
337
|
+
top strip is used instead; the top strip is correspondingly hidden on a
|
|
338
|
+
coarse pointer so tabs live in the thumb zone. */
|
|
339
|
+
.wt-switcher {
|
|
340
|
+
display: none;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@media (pointer: coarse) {
|
|
344
|
+
.wt-tab-bar,
|
|
345
|
+
.wt-tab-new {
|
|
346
|
+
display: none;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.wt-switcher {
|
|
350
|
+
position: fixed;
|
|
351
|
+
left: 0;
|
|
352
|
+
right: 0;
|
|
353
|
+
/* Lift above the soft keyboard rather than hiding, so the switch surface
|
|
354
|
+
stays reachable in the terminal's primary working state (section 12). */
|
|
355
|
+
bottom: calc(var(--safe-bottom, 0px) + var(--kb-inset, 0px));
|
|
356
|
+
display: flex;
|
|
357
|
+
align-items: stretch;
|
|
358
|
+
gap: var(--sp-1);
|
|
359
|
+
padding: var(--sp-1);
|
|
360
|
+
background: var(--surface);
|
|
361
|
+
backdrop-filter: var(--blur);
|
|
362
|
+
border-top: 1px solid var(--border);
|
|
363
|
+
z-index: 25;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.wt-switcher-current {
|
|
368
|
+
flex: 1 1 auto;
|
|
369
|
+
display: flex;
|
|
370
|
+
align-items: center;
|
|
371
|
+
gap: var(--sp-2);
|
|
372
|
+
min-height: var(--touch-target);
|
|
373
|
+
padding: 0 var(--sp-2);
|
|
374
|
+
background: transparent;
|
|
375
|
+
color: var(--text);
|
|
376
|
+
border: none;
|
|
377
|
+
border-radius: var(--radius-inner);
|
|
378
|
+
font-family: var(--font-ui);
|
|
379
|
+
font-size: 0.875rem;
|
|
380
|
+
cursor: pointer;
|
|
381
|
+
overflow: hidden;
|
|
382
|
+
/* Read horizontal swipe here; let vertical scrolling pass through. */
|
|
383
|
+
touch-action: pan-y;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.wt-switcher-current:active {
|
|
387
|
+
background: var(--text-active);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.wt-switcher-label {
|
|
391
|
+
flex: 1 1 auto;
|
|
392
|
+
overflow: hidden;
|
|
393
|
+
text-overflow: ellipsis;
|
|
394
|
+
white-space: nowrap;
|
|
395
|
+
text-align: left;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.wt-switcher-pos {
|
|
399
|
+
flex: none;
|
|
400
|
+
color: color-mix(in oklch, var(--text) 60%, transparent);
|
|
401
|
+
font-variant-numeric: tabular-nums;
|
|
402
|
+
font-size: 0.8125rem;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.wt-switcher-overview {
|
|
406
|
+
position: relative;
|
|
407
|
+
flex: none;
|
|
408
|
+
padding: 0 var(--sp-3);
|
|
409
|
+
font-family: var(--font-ui);
|
|
410
|
+
font-size: 0.8125rem;
|
|
411
|
+
font-weight: 600;
|
|
412
|
+
border: 1px solid var(--border);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.wt-switcher-count {
|
|
416
|
+
font-variant-numeric: tabular-nums;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* Aggregate needs-input attention: a background tab is blocked on you. Shape +
|
|
420
|
+
color (a ring plus a corner glyph), never hue alone (WCAG 1.4.1). */
|
|
421
|
+
.wt-switcher-overview[data-attention="input"] {
|
|
422
|
+
border-color: oklch(70% 0.18 25deg);
|
|
423
|
+
box-shadow: 0 0 0 2px oklch(70% 0.18 25deg / 35%);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.wt-switcher-overview[data-attention="input"]::after {
|
|
427
|
+
content: "";
|
|
428
|
+
position: absolute;
|
|
429
|
+
top: 3px;
|
|
430
|
+
right: 3px;
|
|
431
|
+
width: 8px;
|
|
432
|
+
height: 8px;
|
|
433
|
+
border-radius: 50%;
|
|
434
|
+
background: oklch(70% 0.18 25deg);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* ===== Overview sheet (tabs feature, section 12) =====
|
|
438
|
+
A modal, focus-trapped bottom sheet listing every tab; distinct from the
|
|
439
|
+
non-modal overlay (context menu). Opened from the switcher; mobile-only in
|
|
440
|
+
practice (the switcher that opens it is hidden on a fine pointer). */
|
|
441
|
+
.wt-sheet-scrim {
|
|
442
|
+
position: fixed;
|
|
443
|
+
inset: 0;
|
|
444
|
+
background: oklch(0% 0 0deg / 50%);
|
|
445
|
+
z-index: 45;
|
|
446
|
+
display: none;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.wt-sheet-scrim.visible {
|
|
450
|
+
display: block;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.wt-sheet {
|
|
454
|
+
position: fixed;
|
|
455
|
+
left: 0;
|
|
456
|
+
right: 0;
|
|
457
|
+
bottom: 0;
|
|
458
|
+
max-height: 70vh;
|
|
459
|
+
display: flex;
|
|
460
|
+
flex-direction: column;
|
|
461
|
+
gap: var(--sp-2);
|
|
462
|
+
padding: var(--sp-3);
|
|
463
|
+
padding-bottom: calc(var(--sp-3) + var(--safe-bottom, 0px));
|
|
464
|
+
background: var(--surface);
|
|
465
|
+
backdrop-filter: var(--blur);
|
|
466
|
+
border-top: 1px solid var(--border);
|
|
467
|
+
border-radius: var(--radius) var(--radius) 0 0;
|
|
468
|
+
z-index: 46;
|
|
469
|
+
overflow-y: auto;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.wt-sheet[hidden] {
|
|
473
|
+
display: none;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.wt-sheet-header {
|
|
477
|
+
display: flex;
|
|
478
|
+
align-items: center;
|
|
479
|
+
justify-content: space-between;
|
|
480
|
+
gap: var(--sp-2);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.wt-sheet-title {
|
|
484
|
+
font-family: var(--font-ui);
|
|
485
|
+
font-size: 0.9375rem;
|
|
486
|
+
font-weight: 600;
|
|
487
|
+
color: var(--text);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.wt-sheet-new {
|
|
491
|
+
flex: none;
|
|
492
|
+
padding: 0 var(--sp-3);
|
|
493
|
+
font-family: var(--font-ui);
|
|
494
|
+
font-size: 0.8125rem;
|
|
495
|
+
font-weight: 500;
|
|
496
|
+
border: 1px solid var(--border);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.wt-sheet-list {
|
|
500
|
+
list-style: none;
|
|
501
|
+
margin: 0;
|
|
502
|
+
padding: 0;
|
|
503
|
+
display: flex;
|
|
504
|
+
flex-direction: column;
|
|
505
|
+
gap: 2px;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.wt-sheet-row {
|
|
509
|
+
display: flex;
|
|
510
|
+
align-items: stretch;
|
|
511
|
+
gap: var(--sp-1);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.wt-sheet-select {
|
|
515
|
+
flex: 1 1 auto;
|
|
516
|
+
display: flex;
|
|
517
|
+
align-items: center;
|
|
518
|
+
gap: var(--sp-2);
|
|
519
|
+
min-height: var(--touch-target);
|
|
520
|
+
padding: 0 var(--sp-2);
|
|
521
|
+
background: transparent;
|
|
522
|
+
color: var(--text);
|
|
523
|
+
border: none;
|
|
524
|
+
border-radius: var(--radius-inner);
|
|
525
|
+
font-family: var(--font-ui);
|
|
526
|
+
font-size: 0.875rem;
|
|
527
|
+
text-align: left;
|
|
528
|
+
cursor: pointer;
|
|
529
|
+
overflow: hidden;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.wt-sheet-row-active .wt-sheet-select {
|
|
533
|
+
background: color-mix(in oklch, var(--text) 10%, var(--surface));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.wt-sheet-select:active {
|
|
537
|
+
background: var(--text-active);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.wt-sheet-row-label {
|
|
541
|
+
flex: 1 1 auto;
|
|
542
|
+
overflow: hidden;
|
|
543
|
+
text-overflow: ellipsis;
|
|
544
|
+
white-space: nowrap;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.wt-sheet-close {
|
|
548
|
+
flex: none;
|
|
549
|
+
border: 1px solid var(--border);
|
|
550
|
+
font-size: 1rem;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/* ===== Catching-up cue (tabs feature, sections 12/13) =====
|
|
554
|
+
A switched-into tab's cached screen is stale until its resume delta lands, so
|
|
555
|
+
it must not read as live. This unobtrusive cue shows only if the delta has not
|
|
556
|
+
arrived shortly after the switch, and clears on the first screen frame. */
|
|
557
|
+
.wt-catchup {
|
|
558
|
+
position: fixed;
|
|
559
|
+
top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + 44px);
|
|
560
|
+
left: 50%;
|
|
561
|
+
transform: translateX(-50%);
|
|
562
|
+
padding: 2px var(--sp-2);
|
|
563
|
+
background: var(--surface);
|
|
564
|
+
backdrop-filter: var(--blur);
|
|
565
|
+
border: 1px solid var(--border);
|
|
566
|
+
border-radius: var(--radius);
|
|
567
|
+
color: color-mix(in oklch, var(--text) 75%, transparent);
|
|
568
|
+
font-family: var(--font-ui);
|
|
569
|
+
font-size: 0.75rem;
|
|
570
|
+
z-index: 24;
|
|
571
|
+
display: none;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.wt-catchup.visible {
|
|
575
|
+
display: block;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* ===== Switch motion (animations feature) =====
|
|
579
|
+
A subtle content fade on switch, so the cache-first repaint reads as a change
|
|
580
|
+
rather than a flicker. Gated on .wt-animate (and prefers-reduced-motion is
|
|
581
|
+
respected because the animations feature does not set .wt-animate then). */
|
|
582
|
+
.wt-animate .term.wt-switching .term-output {
|
|
583
|
+
animation: wt-switch-in var(--dur-standard) var(--ease-standard);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
@keyframes wt-switch-in {
|
|
587
|
+
from {
|
|
588
|
+
opacity: 0.55;
|
|
589
|
+
}
|
|
590
|
+
}
|
package/css/MANIFEST
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Reference touch-first browser UI for @cplieger/web-terminal-engine:
|
|
5
|
+
"description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./src/index.ts"
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./presets": "./src/presets.ts"
|
|
9
10
|
},
|
|
11
|
+
"sideEffects": [
|
|
12
|
+
"**/*.css"
|
|
13
|
+
],
|
|
10
14
|
"types": "./src/index.ts",
|
|
11
15
|
"scripts": {
|
|
12
16
|
"typecheck": "tsgo -p tsconfig.json",
|
|
@@ -21,7 +25,7 @@
|
|
|
21
25
|
"verify": "bash scripts/verify.sh"
|
|
22
26
|
},
|
|
23
27
|
"peerDependencies": {
|
|
24
|
-
"@cplieger/web-terminal-engine": ">=
|
|
28
|
+
"@cplieger/web-terminal-engine": ">=2.0.0"
|
|
25
29
|
},
|
|
26
30
|
"devDependencies": {
|
|
27
31
|
"@eslint/js": "10.0.1",
|
|
@@ -32,7 +36,7 @@
|
|
|
32
36
|
"happy-dom": "20.10.6",
|
|
33
37
|
"html-validate": "11.5.3",
|
|
34
38
|
"knip": "6.23.0",
|
|
35
|
-
"prettier": "3.9.
|
|
39
|
+
"prettier": "3.9.4",
|
|
36
40
|
"stylelint": "17.14.0",
|
|
37
41
|
"stylelint-config-standard": "40.0.0",
|
|
38
42
|
"typescript": "6.0.3",
|
package/scaffold/index.html
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
</style>
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
19
|
-
<!--
|
|
20
|
-
The loading overlay is a sibling (not a child of the root) so
|
|
21
|
-
|
|
19
|
+
<!-- createTerminal() builds the entire terminal UI inside this single root
|
|
20
|
+
element. The loading overlay is a sibling (not a child of the root) so it is
|
|
21
|
+
never cleared; it is faded out once the first frame renders. -->
|
|
22
22
|
<div id="terminal"></div>
|
|
23
23
|
<div id="loading"><div class="spin"></div>Loading…</div>
|
|
24
24
|
<!-- Deployment-specific: the importmap resolves the bare package specifiers to
|
|
@@ -27,13 +27,15 @@
|
|
|
27
27
|
{
|
|
28
28
|
"imports": {
|
|
29
29
|
"@cplieger/web-terminal-engine": "/vendor/cplieger-web-terminal-engine/index.js",
|
|
30
|
-
"@cplieger/web-terminal-ui": "/vendor/cplieger-web-terminal-ui/index.js"
|
|
30
|
+
"@cplieger/web-terminal-ui": "/vendor/cplieger-web-terminal-ui/index.js",
|
|
31
|
+
"@cplieger/web-terminal-ui/presets": "/vendor/cplieger-web-terminal-ui/presets.js"
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
</script>
|
|
34
35
|
<script type="module">
|
|
35
|
-
import {
|
|
36
|
-
|
|
36
|
+
import { createTerminal } from "@cplieger/web-terminal-ui";
|
|
37
|
+
import { presetTabbed } from "@cplieger/web-terminal-ui/presets";
|
|
38
|
+
createTerminal(document.getElementById("terminal"), { features: presetTabbed(), loading: document.getElementById("loading") });
|
|
37
39
|
</script>
|
|
38
40
|
</body>
|
|
39
41
|
</html>
|
package/src/composition.ts
CHANGED
|
@@ -64,6 +64,21 @@ export function isComposing(): boolean {
|
|
|
64
64
|
return composing || sendingComposition;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Abort any in-flight composition without sending, and clear the textarea.
|
|
68
|
+
* The kernel calls this on a tab switch (detach): committing half-composed IME
|
|
69
|
+
* text to either the outgoing or the incoming session is wrong, so we discard
|
|
70
|
+
* it (design 5.1, "end any composition ... or cancel"). Clearing
|
|
71
|
+
* sendingComposition also neutralizes a just-fired compositionend whose
|
|
72
|
+
* deferred send is still pending on the microtask/timeout queue, so it cannot
|
|
73
|
+
* land on whoever is active after the switch. */
|
|
74
|
+
export function cancelComposition(): void {
|
|
75
|
+
composing = false;
|
|
76
|
+
sendingComposition = false;
|
|
77
|
+
compositionView.textContent = "";
|
|
78
|
+
compositionView.classList.remove("active");
|
|
79
|
+
resetToPlaceholder(textarea);
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
function onStart(): void {
|
|
68
83
|
composing = true;
|
|
69
84
|
const start = textarea.selectionStart;
|