@cplieger/web-terminal-ui 4.2.0 → 4.3.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 +8 -8
- package/css/00-tokens.css +7 -0
- package/css/30-tabs.css +76 -46
- package/css/31-switcher.css +5 -4
- package/package.json +1 -1
- package/src/features/tabs/index.ts +68 -22
package/README.md
CHANGED
|
@@ -137,14 +137,14 @@ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
|
|
|
137
137
|
|
|
138
138
|
### Options
|
|
139
139
|
|
|
140
|
-
| Option | Default | Purpose
|
|
141
|
-
| ----------- | -------------------------- |
|
|
142
|
-
| `features` | _(none — bare kernel)_ | The feature list. Omitted or empty builds only the terminal (no chrome). Use a preset from `./presets` or a hand-picked array.
|
|
143
|
-
| `layout` | `"viewport"` | How the terminal claims space. `"viewport"`: the root becomes a fixed full-viewport box (the full-page product). `"container"`: the root fills your container element, which becomes the styling and positioning boundary (the embedded case).
|
|
144
|
-
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to.
|
|
145
|
-
| `fontReady` | `'14px "MonaspiceNe NFM"'` | CSS font shorthand awaited before the first resize, so the server is sized against the real web font's cell metrics rather than a fallback.
|
|
146
|
-
| `loading` | _(none)_ | A pre-JS loading overlay element (kept in your served HTML so it paints before this module loads); it is faded out and removed once the first frame renders.
|
|
147
|
-
| `theme` | _(none)_ | Theme overrides (CSS custom properties on the terminal root): `--accent`, `--tab-bg`, `--tab-hover-bg`, `--tab-active-bg`, `--tab-active-fg`, `--tab-active-border
|
|
140
|
+
| Option | Default | Purpose |
|
|
141
|
+
| ----------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
142
|
+
| `features` | _(none — bare kernel)_ | The feature list. Omitted or empty builds only the terminal (no chrome). Use a preset from `./presets` or a hand-picked array. |
|
|
143
|
+
| `layout` | `"viewport"` | How the terminal claims space. `"viewport"`: the root becomes a fixed full-viewport box (the full-page product). `"container"`: the root fills your container element, which becomes the styling and positioning boundary (the embedded case). |
|
|
144
|
+
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
|
|
145
|
+
| `fontReady` | `'14px "MonaspiceNe NFM"'` | CSS font shorthand awaited before the first resize, so the server is sized against the real web font's cell metrics rather than a fallback. |
|
|
146
|
+
| `loading` | _(none)_ | A pre-JS loading overlay element (kept in your served HTML so it paints before this module loads); it is faded out and removed once the first frame renders. |
|
|
147
|
+
| `theme` | _(none)_ | Theme overrides (CSS custom properties on the terminal root): `--accent`, `--tab-bg`, `--tab-hover-bg`, `--tab-active-bg`, `--tab-active-fg`, `--tab-active-border`, plus the activity-dot palette `--status-working`, `--status-done`, `--status-input` (the working and input rings derive from their own tokens). The library ships neutral defaults. |
|
|
148
148
|
|
|
149
149
|
`createTerminal()` returns a handle: `focus()` re-focuses the terminal input
|
|
150
150
|
(and opens the soft keyboard on touch); `send(bytes)` sends bytes to the active
|
package/css/00-tokens.css
CHANGED
|
@@ -44,6 +44,13 @@
|
|
|
44
44
|
var(--text) 25%
|
|
45
45
|
); /* subtle edge: the fill lightened + slightly desaturated, matching the inactive chip's faint border rather than a vivid accent outline */
|
|
46
46
|
|
|
47
|
+
/* Status-dot palette (the activity vocabulary painted in 30-tabs.css; the
|
|
48
|
+
switcher notification dot reuses the same rules). The working and input
|
|
49
|
+
rings each derive from their own token in 30-tabs.css, so one override
|
|
50
|
+
recolors dot + ring together. */
|
|
51
|
+
--status-working: #d9f99d; /* agent working (pulses): pale lime */
|
|
52
|
+
--status-done: #22c55e; /* turn finished: solid green */
|
|
53
|
+
--status-input: #fb923c; /* action required: solid orange + ring */
|
|
47
54
|
--radius: 0.75rem;
|
|
48
55
|
--radius-inner: 0.5rem;
|
|
49
56
|
/* Small square icon buttons (e.g. the 24px tab-close x): at that size the
|
package/css/30-tabs.css
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
:where(.wt-root) .wt-tab-bar {
|
|
15
15
|
/* Absolute against .wt-root, like all chrome (identical geometry in
|
|
16
|
-
wt-viewport mode; see 01-scope.css). The bar itself never scrolls:
|
|
17
|
-
tabs
|
|
18
|
-
|
|
19
|
-
edge however many tabs are open. */
|
|
16
|
+
wt-viewport mode; see 01-scope.css). The bar itself never scrolls: only
|
|
17
|
+
the tabs scroll, inside .wt-tab-scroll (below); the "+" and the keyboard
|
|
18
|
+
button are fixed bar items after it, so both stay visible at the bar's
|
|
19
|
+
right edge however many tabs are open. */
|
|
20
20
|
position: absolute;
|
|
21
21
|
bottom: var(--safe-bottom);
|
|
22
22
|
left: 0;
|
|
@@ -32,17 +32,22 @@
|
|
|
32
32
|
z-index: 15;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/* The scrolling tab list inside the bar: the tabs
|
|
36
|
-
|
|
37
|
-
HERE, not on the bar, so
|
|
38
|
-
flex: 1 + min-width: 0
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
/* The scrolling tab list inside the bar: the tabs and nothing else — the "+"
|
|
36
|
+
and the keyboard button are fixed bar siblings after it. Horizontal
|
|
37
|
+
scrolling lives HERE, not on the bar, so neither control ever scrolls away.
|
|
38
|
+
flex: 0 1 auto + min-width: 0 shrink-WRAP the list: while the tabs fit, the
|
|
39
|
+
scroller is exactly as wide as its content and the "+" trails the last tab
|
|
40
|
+
across the bar gap (visually identical to living in the list); once the
|
|
41
|
+
tabs overflow, the scroller caps at the space the fixed controls leave and
|
|
42
|
+
the "+" reads as pinned at the bar's right edge (with the keyboard button,
|
|
43
|
+
when shown on a wide touchscreen, anchored to its right). The tabs feature
|
|
44
|
+
also maps a vertical wheel over the bar to scrollLeft here — the strip has
|
|
45
|
+
no vertical scroll to spend it on. */
|
|
41
46
|
:where(.wt-root) .wt-tab-scroll {
|
|
42
47
|
display: flex;
|
|
43
48
|
align-items: center;
|
|
44
49
|
gap: var(--sp-1);
|
|
45
|
-
flex:
|
|
50
|
+
flex: 0 1 auto;
|
|
46
51
|
min-width: 0;
|
|
47
52
|
overflow-x: auto;
|
|
48
53
|
/* A wheel/trackpad/touch scroll that runs out of tabs must not chain into
|
|
@@ -164,9 +169,12 @@
|
|
|
164
169
|
}
|
|
165
170
|
}
|
|
166
171
|
|
|
167
|
-
/* The add-tab (+) button:
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
/* The add-tab (+) button: a fixed bar item OUTSIDE the tab scroller (it never
|
|
173
|
+
scrolls away with an overflowing list; the shrink-wrapped scroller keeps it
|
|
174
|
+
visually trailing the last tab while the tabs fit — see .wt-tab-scroll). Its
|
|
175
|
+
hover box matches an active tab's fill and stretches to the strip height;
|
|
176
|
+
the bar gap (not a custom margin) spaces it from the list. Its glyph is an
|
|
177
|
+
inline SVG. */
|
|
170
178
|
:where(.wt-root) .wt-tab-new {
|
|
171
179
|
display: inline-flex;
|
|
172
180
|
align-items: center;
|
|
@@ -226,8 +234,10 @@
|
|
|
226
234
|
tab scroller: pinned at the bar's right edge in the same column as the
|
|
227
235
|
scroll-to-bottom button floating above it (both 44px boxes whose right edge
|
|
228
236
|
sits --sp-2 from the root's right edge), where an overflowing tab list can
|
|
229
|
-
never push or scroll it away
|
|
230
|
-
|
|
237
|
+
never push or scroll it away — margin-left: auto (below) keeps it there
|
|
238
|
+
whatever the tab count. The "+" is likewise a fixed bar item outside the
|
|
239
|
+
scroller, but it TRAILS the tab list (the auto margin sits between them)
|
|
240
|
+
and packs up against the kb only when the strip overflows. Hidden on a fine-pointer desktop (a real keyboard is present; the
|
|
231
241
|
scroller then stretches to the full bar width) and until a keyboardToggle
|
|
232
242
|
feature is wired ([hidden]); shown only on a coarse pointer at strip widths
|
|
233
243
|
(the .wt-narrow complement below; the breakpoint itself is the kernel's,
|
|
@@ -235,6 +245,11 @@
|
|
|
235
245
|
:where(.wt-root) .wt-tab-kb {
|
|
236
246
|
display: none;
|
|
237
247
|
flex: none;
|
|
248
|
+
/* Pin at the bar's FAR right edge always: the auto margin absorbs whatever
|
|
249
|
+
width the shrink-wrapped scroller + "+" leave free, so the kb button's
|
|
250
|
+
position is independent of the tab count (the "+" trails the tab list and
|
|
251
|
+
only packs up against the kb once the strip overflows). */
|
|
252
|
+
margin-left: auto;
|
|
238
253
|
}
|
|
239
254
|
|
|
240
255
|
/* Grid open: filled so the toggled state reads at a glance (mirrors the
|
|
@@ -376,33 +391,42 @@
|
|
|
376
391
|
border: 1.5px solid color-mix(in oklch, var(--text) 45%, transparent);
|
|
377
392
|
}
|
|
378
393
|
|
|
379
|
-
/* Agent working: solid pulsing pale lime
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
394
|
+
/* Agent working: solid pulsing dot (--status-working; pale lime by default —
|
|
395
|
+
its high lightness keeps it clear on every tab state) wearing the same 2px
|
|
396
|
+
ring as input, so the two agent-turn states share one silhouette: a live
|
|
397
|
+
turn pulses (dot + ring together), a turn blocked on the user is that same
|
|
398
|
+
silhouette frozen and recolored (input, below). Motion separates working
|
|
399
|
+
from input; the ring separates both from done's bare disc. Server-side this
|
|
400
|
+
is driven by the OSC 9;4 progress signal, so it tracks agent activity, not
|
|
401
|
+
the user typing at the prompt. */
|
|
384
402
|
:where(.wt-root) .wt-status-dot[data-status="working"] {
|
|
385
|
-
background:
|
|
386
|
-
border-color:
|
|
403
|
+
background: var(--status-working);
|
|
404
|
+
border-color: var(--status-working);
|
|
405
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-working) 40%, transparent);
|
|
387
406
|
animation: wt-pulse 1.2s ease-in-out infinite;
|
|
388
407
|
}
|
|
389
408
|
|
|
390
|
-
/* Agent done / ready: solid
|
|
391
|
-
(#22c55e),
|
|
392
|
-
pale lime
|
|
409
|
+
/* Agent done / ready: solid disc (--status-done), no motion. The default green
|
|
410
|
+
matches Kiro IDE's "Ready" (#22c55e), darker and more saturated than
|
|
411
|
+
working's default pale lime so the two states differ by lightness as well as
|
|
412
|
+
motion. */
|
|
393
413
|
:where(.wt-root) .wt-status-dot[data-status="done"] {
|
|
394
|
-
background:
|
|
395
|
-
border-color:
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/* User action required (permission / approval):
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
414
|
+
background: var(--status-done);
|
|
415
|
+
border-color: var(--status-done);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/* User action required (permission / approval): working's exact silhouette —
|
|
419
|
+
solid dot + 2px ring — frozen and recolored (--status-input; the default
|
|
420
|
+
orange is shifted away from the old amber so it separates from both
|
|
421
|
+
default-lime working and green done). The stillness is the message: the
|
|
422
|
+
agent's turn is mid-flight but blocked on you. The ring keeps it distinct
|
|
423
|
+
from done's bare disc without leaning on hue; it derives from the same token
|
|
424
|
+
at 40% alpha, so a theme override recolors dot + ring together. It persists
|
|
425
|
+
until you act on or switch into the tab. */
|
|
402
426
|
:where(.wt-root) .wt-status-dot[data-status="input"] {
|
|
403
|
-
background:
|
|
404
|
-
border-color:
|
|
405
|
-
box-shadow: 0 0 0 2px
|
|
427
|
+
background: var(--status-input);
|
|
428
|
+
border-color: var(--status-input);
|
|
429
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-input) 40%, transparent);
|
|
406
430
|
}
|
|
407
431
|
|
|
408
432
|
/* Exited: a dim hollow dot, static; distinct from idle by its lower opacity. */
|
|
@@ -417,15 +441,18 @@
|
|
|
417
441
|
100% {
|
|
418
442
|
/* Pin the animated layer's paint as well as its opacity. When working begins
|
|
419
443
|
in the same frame that input/done ends, an opacity-only animation can
|
|
420
|
-
pulse the previously rasterized
|
|
421
|
-
|
|
422
|
-
|
|
444
|
+
pulse the previously rasterized input/done paint (the input ring
|
|
445
|
+
included, hence the box-shadow pin). */
|
|
446
|
+
background-color: var(--status-working);
|
|
447
|
+
border-color: var(--status-working);
|
|
448
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-working) 40%, transparent);
|
|
423
449
|
opacity: 1;
|
|
424
450
|
}
|
|
425
451
|
|
|
426
452
|
50% {
|
|
427
|
-
background-color:
|
|
428
|
-
border-color:
|
|
453
|
+
background-color: var(--status-working);
|
|
454
|
+
border-color: var(--status-working);
|
|
455
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-working) 40%, transparent);
|
|
429
456
|
opacity: 0.4;
|
|
430
457
|
}
|
|
431
458
|
}
|
|
@@ -434,10 +461,13 @@
|
|
|
434
461
|
@media (prefers-reduced-motion: reduce) {
|
|
435
462
|
:where(.wt-root) .wt-status-dot[data-status="working"] {
|
|
436
463
|
animation: none;
|
|
437
|
-
/* The pulse is working's
|
|
438
|
-
|
|
439
|
-
from
|
|
440
|
-
|
|
464
|
+
/* The pulse is what separates working from input's frozen silhouette; with
|
|
465
|
+
motion removed, punch an inner hole (dot -> donut) so working differs
|
|
466
|
+
from input (solid ringed disc) and done (bare disc) by shape, not hue
|
|
467
|
+
alone (WCAG 1.4.1). The outer ring stays. */
|
|
468
|
+
box-shadow:
|
|
469
|
+
inset 0 0 0 2px var(--bg),
|
|
470
|
+
0 0 0 2px color-mix(in srgb, var(--status-working) 40%, transparent);
|
|
441
471
|
}
|
|
442
472
|
}
|
|
443
473
|
|
package/css/31-switcher.css
CHANGED
|
@@ -280,10 +280,11 @@
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
/* Latest-wins background-tab notification dot on the switch button — the moved
|
|
283
|
-
attention cue
|
|
284
|
-
turn finished. Reuses the exact .wt-status-dot
|
|
285
|
-
matches the per-tab dots. Hidden
|
|
286
|
-
cleared when the list opens
|
|
283
|
+
attention cue: the input colour when a background terminal needs input, the
|
|
284
|
+
done colour when a background turn finished. Reuses the exact .wt-status-dot
|
|
285
|
+
rules (30-tabs.css, --status-* tokens) so it matches the per-tab dots. Hidden
|
|
286
|
+
until the tabs feature sets data-status; cleared when the list opens
|
|
287
|
+
(expandSwitcher). */
|
|
287
288
|
:where(.wt-root) .wt-switcher-switch-dot {
|
|
288
289
|
position: absolute;
|
|
289
290
|
top: 6px;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"type": "module",
|
|
5
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",
|
|
@@ -225,17 +225,20 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
225
225
|
|
|
226
226
|
// --- Desktop tab strip (top-bar region) ---
|
|
227
227
|
// Two layers: the bar itself never scrolls; an inner scroller
|
|
228
|
-
// (.wt-tab-scroll, the tablist) holds the tabs
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// (
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
228
|
+
// (.wt-tab-scroll, the tablist) holds ONLY the tabs. The "+" and the
|
|
229
|
+
// keyboard button sit OUTSIDE the scroller as fixed bar items in the
|
|
230
|
+
// order [scroller | + | kb], so an overflowing tab list can never push
|
|
231
|
+
// or scroll either control away. The scroller shrink-wraps its content
|
|
232
|
+
// (CSS flex: 0 1 auto), so while the tabs fit the "+" trails the last
|
|
233
|
+
// tab exactly as if it were in the list, while the kb button (a wide
|
|
234
|
+
// touchscreen; hidden on a fine pointer) stays pinned at the bar's FAR
|
|
235
|
+
// right edge via margin-left: auto whatever the tab count. Once the
|
|
236
|
+
// tabs overflow, the scroller caps at the remaining bar width and the
|
|
237
|
+
// "+" packs right, up against the kb. Both controls are built + wired
|
|
238
|
+
// by the same shared factories as the mobile switcher's; the kb button
|
|
239
|
+
// is CSS-gated to a wide touchscreen and un-hidden below only when a
|
|
240
|
+
// keyboardToggle is wired. addTabChrome appends each tab to the
|
|
241
|
+
// scroller's end.
|
|
239
242
|
const slot = ctx.region("top-bar", "tabs");
|
|
240
243
|
const bar = document.createElement("div");
|
|
241
244
|
bar.className = "wt-tab-bar";
|
|
@@ -245,9 +248,38 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
245
248
|
scroller.setAttribute("role", "tablist");
|
|
246
249
|
bar.appendChild(scroller);
|
|
247
250
|
const newBtn = makeNewButton("wt-tab-new");
|
|
248
|
-
|
|
251
|
+
bar.appendChild(newBtn);
|
|
249
252
|
const deskKb = makeKbButton("wt-tab-kb wt-btn");
|
|
250
253
|
bar.appendChild(deskKb);
|
|
254
|
+
// A vertical mouse wheel anywhere over the strip scrolls the tab list
|
|
255
|
+
// horizontally — the strip has no vertical dimension to spend the delta
|
|
256
|
+
// on, and this is the affordance browser tab bars train. Bound on the
|
|
257
|
+
// BAR so the empty strip area and the fixed controls translate too.
|
|
258
|
+
// Horizontal-dominant deltas (a trackpad pan) keep native handling, and
|
|
259
|
+
// a wheel over a non-overflowing strip falls through untouched. The
|
|
260
|
+
// listener is non-passive because a translated tick must preventDefault
|
|
261
|
+
// so an embedding page (wt-container mode) does not also scroll.
|
|
262
|
+
bar.addEventListener(
|
|
263
|
+
"wheel",
|
|
264
|
+
(e) => {
|
|
265
|
+
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) {
|
|
266
|
+
return; // horizontal-dominant: native scroll already handles it
|
|
267
|
+
}
|
|
268
|
+
if (scroller.scrollWidth <= scroller.clientWidth) {
|
|
269
|
+
return; // nothing to scroll: let the page have the wheel
|
|
270
|
+
}
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
// deltaMode: 0 = pixels, 1 = lines (Firefox wheel), 2 = pages.
|
|
273
|
+
const step =
|
|
274
|
+
e.deltaMode === 1
|
|
275
|
+
? e.deltaY * 32
|
|
276
|
+
: e.deltaMode === 2
|
|
277
|
+
? e.deltaY * scroller.clientWidth
|
|
278
|
+
: e.deltaY;
|
|
279
|
+
scroller.scrollLeft += step;
|
|
280
|
+
},
|
|
281
|
+
{ passive: false },
|
|
282
|
+
);
|
|
251
283
|
|
|
252
284
|
// Pull the terminal surface up off the docked BOTTOM strip on desktop so
|
|
253
285
|
// the bar does not overlap the last rows. The surface is absolute
|
|
@@ -515,13 +547,27 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
515
547
|
ctx.surface().querySelector<HTMLElement>(".term-input")?.focus({ preventScroll: true });
|
|
516
548
|
}
|
|
517
549
|
|
|
518
|
-
// paintActive updates the desktop strip's active state.
|
|
550
|
+
// paintActive updates the desktop strip's active state. When the ACTIVE
|
|
551
|
+
// TAB CHANGES (tracked via lastRevealedActive — not on every chrome
|
|
552
|
+
// sync, so a user browsing a scrolled strip is never yanked back by an
|
|
553
|
+
// unrelated repaint), the newly active chip is brought into view in the
|
|
554
|
+
// overflowed scroller; inline: "nearest" is a no-op when it is already
|
|
555
|
+
// visible. The typeof guard covers happy-dom, which lacks
|
|
556
|
+
// scrollIntoView.
|
|
557
|
+
let lastRevealedActive = "";
|
|
519
558
|
function paintActive(): void {
|
|
520
559
|
for (const t of tabList) {
|
|
521
560
|
const on = t.id === activeId;
|
|
522
561
|
t.el.classList.toggle("wt-tab-active", on);
|
|
523
562
|
t.aria.setSelected(on);
|
|
524
563
|
}
|
|
564
|
+
if (activeId && activeId !== lastRevealedActive) {
|
|
565
|
+
lastRevealedActive = activeId;
|
|
566
|
+
const active = tabList.find((t) => t.id === activeId);
|
|
567
|
+
if (active && typeof active.el.scrollIntoView === "function") {
|
|
568
|
+
active.el.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
525
571
|
}
|
|
526
572
|
|
|
527
573
|
// syncMobile updates the bottom bar: active label + dot, and the aggregate
|
|
@@ -868,9 +914,9 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
868
914
|
}
|
|
869
915
|
paintStatusDot(dot, info.status, info.reportsActivity ?? false);
|
|
870
916
|
const aria = tablist.registerTab(el);
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
scroller.
|
|
917
|
+
// Append to the scroller's end: the tab list is ALL the scroller holds
|
|
918
|
+
// (the "+" and keyboard button are fixed bar items outside it).
|
|
919
|
+
scroller.appendChild(el);
|
|
874
920
|
// Runtime-added tabs animate in; initial tabs do not (see `started`).
|
|
875
921
|
// The timer (not animationend) also clears the class on the hidden mobile
|
|
876
922
|
// strip, where the animation never fires.
|
|
@@ -1183,8 +1229,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1183
1229
|
}
|
|
1184
1230
|
|
|
1185
1231
|
// dragTargetBefore returns the first tab whose horizontal midpoint is past
|
|
1186
|
-
// x (the element the dragged tab should sit before), or null to drop at
|
|
1187
|
-
// end
|
|
1232
|
+
// x (the element the dragged tab should sit before), or null to drop at
|
|
1233
|
+
// the end of the tab list. syncOrderFromDom rebuilds tabList to
|
|
1188
1234
|
// match the strip's DOM order after a drag, so position indicators, the
|
|
1189
1235
|
// switcher, and close-to-the-right/left all follow the visible order.
|
|
1190
1236
|
function dragTargetBefore(x: number): HTMLElement | null {
|
|
@@ -1211,8 +1257,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1211
1257
|
}
|
|
1212
1258
|
}
|
|
1213
1259
|
|
|
1214
|
-
// moveTab shifts a tab one slot left or right in tabList and re-
|
|
1215
|
-
// every tab element in list order
|
|
1260
|
+
// moveTab shifts a tab one slot left or right in tabList and re-appends
|
|
1261
|
+
// every tab element in list order so the DOM matches.
|
|
1216
1262
|
// It is the single-pointer / non-drag alternative to the drag-and-drop
|
|
1217
1263
|
// reorder (WCAG 2.5.7), surfaced as Move left / Move right in the tab
|
|
1218
1264
|
// context menu.
|
|
@@ -1228,7 +1274,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1228
1274
|
}
|
|
1229
1275
|
tabList.splice(to, 0, tab);
|
|
1230
1276
|
for (const item of tabList) {
|
|
1231
|
-
scroller.
|
|
1277
|
+
scroller.appendChild(item.el);
|
|
1232
1278
|
}
|
|
1233
1279
|
syncChrome();
|
|
1234
1280
|
ctx.announce(`Moved ${tab.display} to position ${String(to + 1)}`);
|
|
@@ -1694,7 +1740,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1694
1740
|
if (e.dataTransfer) {
|
|
1695
1741
|
e.dataTransfer.dropEffect = "move";
|
|
1696
1742
|
}
|
|
1697
|
-
scroller.insertBefore(draggingEl, dragTargetBefore(e.clientX)
|
|
1743
|
+
scroller.insertBefore(draggingEl, dragTargetBefore(e.clientX));
|
|
1698
1744
|
});
|
|
1699
1745
|
// Active-row close (x): closes the current tab (mirrors a listed row's x).
|
|
1700
1746
|
// stopPropagation so it is not read as a tap/swipe on the row surface.
|