@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 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(in oklch, var(--text) 22%, var(--surface)); /* active tab fill */
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(in oklch, var(--accent) 90%, transparent); /* accent edge */
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. Unlike the keyboard button it
231
- stays visible while the list is expanded — it is the close control. */
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) {
@@ -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.0",
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",
@@ -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 (const b of bytes) {
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
- if (inPaste) {
790
- // A newline INSIDE a bracketed paste is pasted content, not a
791
- // submit: fold it to a single space so the whole paste stays one
792
- // logical line and the title reflects its START (a multi-line
793
- // paste previously left only its LAST line as the title).
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
- const trans = "transform 0.25s cubic-bezier(0.2, 0, 0, 1), opacity 0.2s ease-out";
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
  }