@cplieger/web-terminal-ui 3.4.0 → 3.4.2
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/css/00-tokens.css +12 -2
- package/css/06-mobile.css +23 -3
- package/css/07-animations.css +11 -0
- package/package.json +1 -1
- package/src/features/tabs.ts +68 -7
package/css/00-tokens.css
CHANGED
|
@@ -15,9 +15,19 @@
|
|
|
15
15
|
--accent: oklch(72% 0.15 250deg); /* light blue; themed per consumer */
|
|
16
16
|
--tab-bg: oklch(30% 0 0deg / 90%); /* inactive-tab chip (translucent) */
|
|
17
17
|
--tab-hover-bg: var(--text-active); /* pressed controls / mobile switcher hovers */
|
|
18
|
-
--tab-active-bg: color-mix(
|
|
18
|
+
--tab-active-bg: color-mix(
|
|
19
|
+
in oklch,
|
|
20
|
+
var(--accent) 30%,
|
|
21
|
+
var(--surface)
|
|
22
|
+
); /* active tab fill: accent-derived (a blue by default; themed per consumer), so the active tab reads blue with a subtle lighter edge below */
|
|
23
|
+
|
|
19
24
|
--tab-active-fg: var(--text); /* active tab text */
|
|
20
|
-
--tab-active-border: color-mix(
|
|
25
|
+
--tab-active-border: color-mix(
|
|
26
|
+
in oklch,
|
|
27
|
+
var(--tab-active-bg),
|
|
28
|
+
var(--text) 25%
|
|
29
|
+
); /* subtle edge: the fill lightened + slightly desaturated, matching the inactive chip's faint border rather than a vivid accent outline */
|
|
30
|
+
|
|
21
31
|
--radius: 0.75rem;
|
|
22
32
|
--radius-inner: 0.5rem;
|
|
23
33
|
/* Small square icon buttons (e.g. the 24px tab-close x): at that size the
|
package/css/06-mobile.css
CHANGED
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
/* Cap against the DYNAMIC viewport so a shown mobile browser toolbar can't push the list
|
|
78
78
|
(and the bar above it) off the top; the switcher's own bottom: kb-inset already lifts it
|
|
79
79
|
clear of the soft keyboard. */
|
|
80
|
-
max-height: 50dvh;
|
|
80
|
+
max-height: var(--wt-list-h, 50dvh);
|
|
81
81
|
overflow-y: auto;
|
|
82
82
|
/* No top padding: the bar's --sp-2 bottom padding already sits between the
|
|
83
83
|
active tab and the first row, so the seam equals the --sp-2 inter-row gap. */
|
|
@@ -227,11 +227,31 @@
|
|
|
227
227
|
|
|
228
228
|
/* Tab-switcher button: toggles the list open/closed, sitting between the
|
|
229
229
|
keyboard and "+" buttons. position: relative anchors its notification dot,
|
|
230
|
-
absolutely placed in the top-right corner.
|
|
231
|
-
|
|
230
|
+
absolutely placed in the top-right corner. It only earns its place with ≥2
|
|
231
|
+
tabs (a single tab has nothing to switch to), so it is COLLAPSED to zero width
|
|
232
|
+
by default and revealed by .wt-switcher-multi (the tabs feature sets it on tab
|
|
233
|
+
count); the appear/collapse is animated in 07-animations.css and the active
|
|
234
|
+
chip shrinks/grows to fill the freed space in lockstep (flex layout). */
|
|
232
235
|
.wt-switcher-switch {
|
|
233
236
|
position: relative;
|
|
234
237
|
flex: none;
|
|
238
|
+
width: 0;
|
|
239
|
+
min-width: 0;
|
|
240
|
+
padding: 0;
|
|
241
|
+
opacity: 0;
|
|
242
|
+
overflow: hidden;
|
|
243
|
+
pointer-events: none;
|
|
244
|
+
/* Absorb the flex gap the zero-width button would otherwise leave between the
|
|
245
|
+
active row and the "+". */
|
|
246
|
+
margin-inline-start: calc(-1 * var(--sp-1));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* Revealed once a second tab exists: full 44px box, interactive, no gap eat. */
|
|
250
|
+
.wt-switcher-multi .wt-switcher-switch {
|
|
251
|
+
width: var(--touch-target);
|
|
252
|
+
opacity: 1;
|
|
253
|
+
pointer-events: auto;
|
|
254
|
+
margin-inline-start: 0;
|
|
235
255
|
}
|
|
236
256
|
|
|
237
257
|
@media (hover: hover) {
|
package/css/07-animations.css
CHANGED
|
@@ -74,6 +74,17 @@
|
|
|
74
74
|
opacity var(--dur-standard) var(--ease-standard);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/* The dedicated switch button appears when a second tab opens and collapses back
|
|
78
|
+
at one tab (.wt-switcher-multi, tabs feature). Animate its width + margin +
|
|
79
|
+
opacity so it slides in from zero width — the active-tab chip shrinks to make
|
|
80
|
+
room in lockstep (flex) — rather than popping. Mirrors the keyboard button. */
|
|
81
|
+
.wt-animate .wt-switcher-switch {
|
|
82
|
+
transition:
|
|
83
|
+
width var(--dur-enter) var(--ease-standard),
|
|
84
|
+
margin var(--dur-enter) var(--ease-standard),
|
|
85
|
+
opacity var(--dur-standard) var(--ease-standard);
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
/* Switch motion: a subtle content fade on switch, so the cache-first repaint
|
|
78
89
|
reads as a change rather than a flicker. A swipe additionally slides the
|
|
79
90
|
content in from the swipe's direction (next from the right, prev from the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.2",
|
|
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",
|
package/src/features/tabs.ts
CHANGED
|
@@ -755,7 +755,11 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
755
755
|
if (!t) {
|
|
756
756
|
return; // no active tab
|
|
757
757
|
}
|
|
758
|
-
for (
|
|
758
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
759
|
+
const b = bytes[i];
|
|
760
|
+
if (b === undefined) {
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
759
763
|
if (escState === 1) {
|
|
760
764
|
escState = b === 0x5b ? 2 : b === 0x4f ? 3 : 0; // ESC [ = CSI, ESC O = SS3
|
|
761
765
|
continue;
|
|
@@ -786,11 +790,32 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
786
790
|
if (b === 0x1b) {
|
|
787
791
|
escState = 1; // ESC: start of an escape sequence
|
|
788
792
|
} else if (b === 0x0d || b === 0x0a) {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
793
|
+
// A newline is a line SUBMIT only when it terminates the input. It is
|
|
794
|
+
// folded to a single space (keeping the current line going) when it is
|
|
795
|
+
// instead a paste-internal break, so a multi-line message becomes one
|
|
796
|
+
// logical line whose title reflects its START rather than only its
|
|
797
|
+
// LAST line. Two cases fold:
|
|
798
|
+
// - inside a bracketed paste (ESC[200~ … ESC[201~), and
|
|
799
|
+
// - a newline FOLLOWED by more printable input in this SAME chunk:
|
|
800
|
+
// a human pressing Enter sends the newline as the end of its own
|
|
801
|
+
// input event, whereas a paste (even one sent WITHOUT bracketed-
|
|
802
|
+
// paste guards — e.g. an agent shell like kiro-cli that keeps a
|
|
803
|
+
// pasted multi-line message as one prompt) delivers
|
|
804
|
+
// text + newline + text together, so trailing content marks the
|
|
805
|
+
// newline as a soft break, not a submit. Without this, such a
|
|
806
|
+
// paste left only its last line as the title (the reported
|
|
807
|
+
// "the title cut off the start of my message").
|
|
808
|
+
let softBreak = inPaste;
|
|
809
|
+
if (!softBreak) {
|
|
810
|
+
for (let j = i + 1; j < bytes.length; j++) {
|
|
811
|
+
const nb = bytes[j];
|
|
812
|
+
if (nb !== undefined && nb >= 0x20) {
|
|
813
|
+
softBreak = true;
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (softBreak) {
|
|
794
819
|
if (lineBytes.length > 0 && lineBytes[lineBytes.length - 1] !== 0x20) {
|
|
795
820
|
lineBytes.push(0x20);
|
|
796
821
|
}
|
|
@@ -972,6 +997,22 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
972
997
|
animateRowIn(row); // grow + fade in (the tray height follows)
|
|
973
998
|
}
|
|
974
999
|
}
|
|
1000
|
+
// Publish the measured content height so the expanded list animates its
|
|
1001
|
+
// max-height between 0 and the REAL content height (--wt-list-h in
|
|
1002
|
+
// 06-mobile.css), not a fixed 50dvh far larger than the content — which
|
|
1003
|
+
// made the open finish early and the close start late (box height =
|
|
1004
|
+
// min(content, max-height), so the transition's stretch past the content
|
|
1005
|
+
// moved nothing: the asymmetric, choppy toggle). scrollHeight is the full
|
|
1006
|
+
// content height regardless of the collapsed max-height:0 clip, so this
|
|
1007
|
+
// is valid whether measured while collapsed (on open, before the expanded
|
|
1008
|
+
// class) or already open (a tab added/closed). Capped at 50dvh (then
|
|
1009
|
+
// overflow-y:auto scrolls). This works without interpolate-size (iOS
|
|
1010
|
+
// Safari lacks it), unlike a height:auto transition.
|
|
1011
|
+
const visH = window.visualViewport?.height ?? window.innerHeight;
|
|
1012
|
+
switcher.style.setProperty(
|
|
1013
|
+
"--wt-list-h",
|
|
1014
|
+
`${String(Math.min(swList.scrollHeight, Math.round(visH * 0.5)))}px`,
|
|
1015
|
+
);
|
|
975
1016
|
}
|
|
976
1017
|
|
|
977
1018
|
// clearRows empties the list and drops the reused-row cache (after a
|
|
@@ -1065,7 +1106,17 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1065
1106
|
}
|
|
1066
1107
|
const exit = dir === "next" ? -pitch : pitch; // leaving row off the leading edge
|
|
1067
1108
|
requestAnimationFrame(() => {
|
|
1068
|
-
|
|
1109
|
+
// Couple opacity to the SAME easing + duration as the transform so a
|
|
1110
|
+
// row's fade tracks its DISTANCE from its target slot (every reel row
|
|
1111
|
+
// travels one pitch): opacity is a linear function of remaining
|
|
1112
|
+
// distance, so a row is transparent a pitch away (at the clipped
|
|
1113
|
+
// edge) and only fully opaque once it settles. Entering rows fade IN
|
|
1114
|
+
// as they rotate in and the leaving row fades OUT as it exits — no
|
|
1115
|
+
// hard black cutoff at the list edges, and no permanent edge fade.
|
|
1116
|
+
// (Was opacity 0.2s ease-out, decoupled from the motion, so a row hit
|
|
1117
|
+
// full opacity while still at the edge — the reported hard cut.)
|
|
1118
|
+
const trans =
|
|
1119
|
+
"transform 0.25s cubic-bezier(0.2, 0, 0, 1), opacity 0.25s cubic-bezier(0.2, 0, 0, 1)";
|
|
1069
1120
|
for (const el of rowEls.values()) {
|
|
1070
1121
|
el.style.transition = trans;
|
|
1071
1122
|
el.style.transform = "translateY(0)";
|
|
@@ -1084,6 +1135,16 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1084
1135
|
relabelAll();
|
|
1085
1136
|
paintActive();
|
|
1086
1137
|
syncMobile();
|
|
1138
|
+
// The dedicated switch button only earns its place once there are ≥2
|
|
1139
|
+
// tabs (a single tab has nothing to switch to; expandSwitcher no-ops
|
|
1140
|
+
// there). .wt-switcher-multi drives its collapse-when-single / animate-in
|
|
1141
|
+
// -when-a-second-opens motion in CSS (the active chip shrinks to make
|
|
1142
|
+
// room in lockstep, via the flex layout); aria-hidden + tabindex keep the
|
|
1143
|
+
// collapsed button out of the a11y tree and tab order.
|
|
1144
|
+
const multiTab = tabList.length >= 2;
|
|
1145
|
+
switcher.classList.toggle("wt-switcher-multi", multiTab);
|
|
1146
|
+
swSwitch.setAttribute("aria-hidden", multiTab ? "false" : "true");
|
|
1147
|
+
swSwitch.tabIndex = multiTab ? 0 : -1;
|
|
1087
1148
|
if (expanded) {
|
|
1088
1149
|
renderSwitcherList();
|
|
1089
1150
|
}
|