@cplieger/web-terminal-ui 5.0.0 → 5.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/css/30-tabs.css CHANGED
@@ -75,6 +75,22 @@
75
75
  width: 300px;
76
76
  flex: 0 1 auto;
77
77
  min-width: 100px;
78
+ /* ONE height for every control in the strip: the chip, the "+" and the
79
+ keyboard button all resolve to --touch-target, so a chip and a button can
80
+ never disagree about how tall the row is. Before this the chip's height was
81
+ whatever its tallest child happened to be, and the "+" hard-coded a COPY of
82
+ that arithmetic (close button + padding + border) — two sources of truth
83
+ for one row, which drifted the moment a child's box grew: a browser without
84
+ text-box support kept the label's untrimmed line box (31px, past the 24px
85
+ close button), so the chip rendered 49px tall next to a 42px "+" and a 44px
86
+ keyboard button. --touch-target is also the tap-target floor the rest of
87
+ the chrome already respects, including this chip's mobile counterpart
88
+ (.wt-switcher-current in 31-switcher.css, same declaration).
89
+
90
+ min-height, not height: the floor governs today (the tallest child is the
91
+ 1.5rem close button, which fits with room to spare), and taller future
92
+ content grows the row rather than overflowing a fixed box. */
93
+ min-height: var(--touch-target);
78
94
  padding: var(--sp-2);
79
95
  /* A transparent border is reserved on every tab so toggling the inactive
80
96
  outline (below) never shifts the strip's layout. */
@@ -112,9 +128,8 @@
112
128
 
113
129
  /* Press (mousedown / touch-hold): one rung past the hover lift — same recipe,
114
130
  deeper mix — so hover -> press reads as a single dimension. After the hover
115
- rule so it wins while both apply. Scoped like hover: clicking the active tab
116
- selects nothing (it is already active), so it presses like the static surface
117
- it is. */
131
+ rule so it wins while both apply. The active chip has its own pair (above),
132
+ mixed against the active fill instead of the inactive one. */
118
133
  :where(.wt-root) .wt-tab:not(.wt-tab-active):active {
119
134
  background: color-mix(in oklch, var(--tab-bg), var(--text) 25%);
120
135
  color: var(--tab-active-fg);
@@ -129,6 +144,34 @@
129
144
  color: var(--tab-active-fg);
130
145
  }
131
146
 
147
+ /* The active tab answers a press too. Clicking it selects nothing, which is why
148
+ it was originally left inert — but selection is not the only thing a chip does:
149
+ it is a drag handle for reordering, a right-click target for the tab menu, a
150
+ double-tap target for rename, and it carries a close button. A control with
151
+ four affordances and no pointer feedback reads as decoration, and it was the
152
+ only chip in the strip that gave none (the same gap that got the keyboard
153
+ button its hover wash).
154
+
155
+ The lift is mixed with --tab-active-fg rather than --text: this is a lighter
156
+ press over an already-filled control (the .wt-switcher-current / .wt-tab-new
157
+ recipe), not a wash over a transparent one, so it follows the filled-surface
158
+ vocabulary and stays themeable through one token. It is a rung subtler than the
159
+ inactive chip's 15%/25% because it starts from an already-lifted fill.
160
+
161
+ Hover is gated to real hover pointers. Tapping an INACTIVE chip makes it
162
+ active, so a touchscreen's sticky :hover leaves with the class; tapping the
163
+ ACTIVE chip changes nothing, so an ungated hover would stay lit until the next
164
+ tap somewhere else. Touch feedback there is :active, which is correct. */
165
+ @media (hover: hover) {
166
+ :where(.wt-root) .wt-tab-active:hover {
167
+ background: color-mix(in oklch, var(--tab-active-bg), var(--tab-active-fg) 12%);
168
+ }
169
+ }
170
+
171
+ :where(.wt-root) .wt-tab-active:active {
172
+ background: color-mix(in oklch, var(--tab-active-bg), var(--tab-active-fg) 22%);
173
+ }
174
+
132
175
  /* The label flexes to fill the fixed-width tab and truncates with an ellipsis
133
176
  (min-width: 0 lets a flex item shrink below its content width). */
134
177
  :where(.wt-root) .wt-tab-label {
@@ -137,7 +180,9 @@
137
180
  overflow: hidden;
138
181
  white-space: nowrap;
139
182
  text-overflow: ellipsis;
140
- /* Center the INK, not the line box: UI fonts carry far more ascent than
183
+ }
184
+
185
+ /* Center the INK, not the line box: UI fonts carry far more ascent than
141
186
  descent (the -apple-system/Noto Sans stack at 13px: 14px over / 4px under
142
187
  the baseline), so the flex-centered line box leaves the visible text ~1.5px
143
188
  low — reading as baseline-aligned with the activity dot's bottom instead of
@@ -159,15 +204,26 @@
159
204
  measured: 3.3px of p/y/g/j tail and 2.3px of an acute accent, i.e. "pygmy
160
205
  jaguar Ápple" rendered as "pyqmy iaquar Apple"). 0.5em restores the ink
161
206
  overhang the trim removes from the box; being symmetric it does not move
162
- the centered band, and being em-relative it tracks the font size. The
163
- grown box (~20px) still sits inside the close button's 1.5rem, so the tab
164
- height is unchanged. A browser without text-box support keeps the untrimmed
165
- box, where the padding is harmless slack.
207
+ the centered band, and being em-relative it tracks the font size.
208
+
209
+ The pair is @supports-GATED because the padding is the trim's allowance and
210
+ nothing else: an untrimmed line box already contains that overhang, so
211
+ adding the padding there is not "harmless slack" (this comment used to
212
+ claim it was) — it grew the label's box to 31px, past the 24px close button,
213
+ and a browser without text-box support rendered 49px tab chips beside a
214
+ 42px "+" and a 44px keyboard button. The chip's own --touch-target floor
215
+ pins the row height either way; the gate is what keeps the label from
216
+ pushing PAST it. Gating the shorthand (not the longhands) is deliberate: an
217
+ engine that ships text-box-trim without the shorthand does not apply the
218
+ declaration we actually write, so it must take the untrimmed branch too.
166
219
 
167
220
  Shared by all three chip labels (this one + the two switcher labels in
168
221
  31-switcher.css). */
169
- text-box: trim-both ex alphabetic;
170
- padding-block: 0.5em;
222
+ @supports (text-box: trim-both ex alphabetic) {
223
+ :where(.wt-root) .wt-tab-label {
224
+ text-box: trim-both ex alphabetic;
225
+ padding-block: 0.5em;
226
+ }
171
227
  }
172
228
 
173
229
  /* A little breathing room after the activity dot, before the label. */
@@ -241,12 +297,16 @@
241
297
  align-items: center;
242
298
  justify-content: center;
243
299
  flex: none;
244
- /* Square box that matches the tab height (a stretched flex item ignores
245
- aspect-ratio against its SVG's intrinsic width, so size it explicitly). The
246
- tab height is the close-button (1.5rem) + vertical padding (2×sp-2) +
247
- border. */
248
- width: calc(1.5rem + 2 * var(--sp-2) + 2px);
249
- height: calc(1.5rem + 2 * var(--sp-2) + 2px);
300
+ /* A SQUARE box of exactly the strip's control height, so it lines up with the
301
+ tab chips beside it and with the keyboard button after it (the .wt-btn
302
+ primitive's 44px box, same token). Sized explicitly on both axes rather
303
+ than by aspect-ratio: a stretched flex item ignores aspect-ratio against
304
+ its SVG's intrinsic width. It used to restate the chip's height arithmetic
305
+ (close button + padding + border) as a calc(), which is how it ended up
306
+ 7px short of a chip whose content grew — see the min-height note on
307
+ .wt-tab. */
308
+ width: var(--touch-target);
309
+ height: var(--touch-target);
250
310
  padding: 0;
251
311
  background: transparent;
252
312
  border: none;
@@ -178,11 +178,16 @@
178
178
  text-overflow: ellipsis;
179
179
  white-space: nowrap;
180
180
  text-align: left;
181
- /* Ink-centering trim + its mandatory descender allowance, same as
182
- .wt-tab-label (see 30-tabs.css for why `ex` and why the padding is not
183
- optional). */
184
- text-box: trim-both ex alphabetic;
185
- padding-block: 0.5em;
181
+ }
182
+
183
+ /* Ink-centering trim + its mandatory descender allowance, same as .wt-tab-label
184
+ (see 30-tabs.css for why `ex`, why the padding is not optional, and why the
185
+ pair is @supports-gated rather than left to fall back). */
186
+ @supports (text-box: trim-both ex alphabetic) {
187
+ :where(.wt-root) .wt-switcher-label {
188
+ text-box: trim-both ex alphabetic;
189
+ padding-block: 0.5em;
190
+ }
186
191
  }
187
192
 
188
193
  /* Keyboard button: hidden until a keyboardToggle is wired; filled while the key
@@ -432,11 +437,16 @@
432
437
  overflow: hidden;
433
438
  text-overflow: ellipsis;
434
439
  white-space: nowrap;
435
- /* Ink-centering trim + its mandatory descender allowance, same as
436
- .wt-tab-label (see 30-tabs.css for why `ex` and why the padding is not
437
- optional). */
438
- text-box: trim-both ex alphabetic;
439
- padding-block: 0.5em;
440
+ }
441
+
442
+ /* Ink-centering trim + its mandatory descender allowance, same as .wt-tab-label
443
+ (see 30-tabs.css for why `ex`, why the padding is not optional, and why the
444
+ pair is @supports-gated rather than left to fall back). */
445
+ @supports (text-box: trim-both ex alphabetic) {
446
+ :where(.wt-root) .wt-switcher-row-label {
447
+ text-box: trim-both ex alphabetic;
448
+ padding-block: 0.5em;
449
+ }
440
450
  }
441
451
 
442
452
  /* The close sits flush at the row's right edge (no gap), vertically centered. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "5.0.0",
3
+ "version": "5.0.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",
@@ -2445,8 +2445,8 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
2445
2445
  void close_(activeId);
2446
2446
  }
2447
2447
  });
2448
- // Dismiss the tab context menu on an outside click, on Escape, and on
2449
- // scroll (it is anchored to a viewport point, so a scroll would detach it).
2448
+ // Dismiss the tab context menu on an outside click, on Escape, and on a
2449
+ // scroll of the strip itself (which moves the chip the menu is anchored to).
2450
2450
  const onDocClickMenu = (): void => {
2451
2451
  // The trailing click of the long-press that just opened the menu is that
2452
2452
  // gesture's own release, not a click-away.
@@ -2466,8 +2466,23 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
2466
2466
  }
2467
2467
  };
2468
2468
  document.addEventListener("contextmenu", onDocContextMenu);
2469
- const onScrollMenu = (): void => {
2470
- hideTabMenu();
2469
+ // Only a scroll that moves the menu's ANCHOR dismisses it, which is the
2470
+ // strip's own horizontal scroller and nothing else: the menu is placed
2471
+ // root-relative (menu-position.ts rebases onto the offsetParent), so any
2472
+ // scroll that moves .wt-root moves the menu with it, and the terminal's
2473
+ // scroller does not move the chip at all.
2474
+ //
2475
+ // The target check is load-bearing, not a micro-optimization. This listener
2476
+ // is capture-phase on window, so it sees EVERY scroll in the document —
2477
+ // including the terminal surface auto-scrolling to the bottom on each chunk
2478
+ // of output. Without the check, an agent printing into the TUI closed the
2479
+ // tab menu within a frame of it opening, which is unusable on exactly the
2480
+ // screen the menu exists for (a busy multi-agent strip).
2481
+ const onScrollMenu = (e: Event): void => {
2482
+ const target = e.target;
2483
+ if (target instanceof Element && target.closest(".wt-tab-scroll")) {
2484
+ hideTabMenu();
2485
+ }
2471
2486
  };
2472
2487
  window.addEventListener("scroll", onScrollMenu, true);
2473
2488
  const offMenuKey = ctx.registerKeydown((ev) => {