@cplieger/web-terminal-ui 8.3.3 → 8.3.4

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.
@@ -1,182 +1,195 @@
1
- /* ===== Key toolbar (mobileToolbar feature; touch devices only) =====
1
+ /* ===== Key toolbar (mobileToolbar feature) =====
2
2
  Uses interpolate-size: allow-keywords (on .wt-root) to animate
3
3
  width/height between a fixed collapsed size and auto (intrinsic).
4
- Toggle is first in DOM so it's at top-left of the grid — visible
5
- when the container clips to 1-cell size. Container is right-anchored
4
+ The toggle sits top-right (col 5, row 1), so it stays visible when the
5
+ container clips to 1-cell size. Container is right-anchored
6
6
  so it grows leftward on open. */
7
- :where(.wt-root) .key-toolbar {
8
- display: contents;
7
+
8
+ /* A self-toggled toolbar shows only under a coarse primary pointer. An
9
+ externally toggled one (`externalToggle`) exists whenever its host opens it,
10
+ because the host owns the trigger and may offer it on a touch-capable device
11
+ whose primary pointer reports fine. */
12
+ @media not (pointer: coarse) {
13
+ :where(.wt-root) .key-toolbar:not(.wt-toolbar-external) {
14
+ display: none;
15
+ }
9
16
  }
10
17
 
11
- /* Touch-only toolbar children: the on-screen Esc/Tab/arrows/hamburger
12
- are useless on desktop where the user has a real keyboard. Hidden by
13
- default; the @media (pointer: coarse) block below restores them. */
14
- :where(.wt-root) .kb-key,
15
- :where(.wt-root) .kb-toggle {
18
+ /* Externally toggled: the host's trigger replaces the in-grid toggle, so its
19
+ column goes, and collapsed means gone — with no toggle left to keep in view,
20
+ a collapsed pill is just a box parked over the terminal. (0,2,0) beats the
21
+ shared button rule's (0,1,1). */
22
+ :where(.wt-root) .wt-toolbar-external .kb-toggle {
16
23
  display: none;
17
24
  }
18
25
 
19
- @media (pointer: coarse) {
20
- :where(.wt-root) .key-toolbar {
21
- /* Absolute against .wt-root (identical geometry in wt-viewport mode; see
22
- 01-scope.css). --vv-top keeps it following iOS's visual-viewport shift. */
23
- position: absolute;
24
- top: calc(var(--safe-top) + var(--sp-2) + var(--vv-top, 0px));
25
- right: var(--sp-2);
26
- z-index: 10;
27
- display: grid;
28
- grid-template-columns: repeat(4, var(--touch-target)) var(--touch-target);
29
- grid-template-rows: var(--touch-target) var(--touch-target);
30
- place-content: start end;
31
- gap: var(--sp-1);
32
- padding: var(--sp-1);
33
- background: var(--surface);
34
- backdrop-filter: var(--blur);
35
- border: 1px solid var(--border);
36
- border-radius: var(--radius);
37
- overflow: hidden;
38
- width: auto;
39
- height: auto;
40
- transition:
41
- width var(--dur-enter) var(--ease-standard),
42
- height var(--dur-enter) var(--ease-standard);
43
- }
26
+ :where(.wt-root) .key-toolbar.wt-toolbar-external {
27
+ grid-template-columns: repeat(4, var(--touch-target));
28
+ }
44
29
 
45
- /* Collapsed: fixed size = 1 button + padding + border. Content is
46
- right-aligned (justify-content:end) so the toggle (col 5) stays at
47
- the right edge, visible within the clipped area. --pill-outer
48
- includes the 2px border (box-sizing:border-box); omitting it left
49
- the content box 2px short, pinning the button top-right with
50
- unequal bottom/left gaps. */
51
- :where(.wt-root) .key-toolbar.collapsed {
52
- width: var(--pill-outer);
53
- height: var(--pill-outer);
54
- }
30
+ :where(.wt-root) .key-toolbar.wt-toolbar-external.collapsed {
31
+ display: none;
32
+ }
55
33
 
56
- /* Grid placement: toggle at col 5 row 1 (top-right). */
57
- :where(.wt-root) .kb-toggle {
58
- grid-area: 1 / 5;
59
- position: relative;
60
- }
61
- :where(.wt-root) .kb-r1c1 {
62
- grid-area: 1 / 1;
63
- }
64
- :where(.wt-root) .kb-r1c2 {
65
- grid-area: 1 / 2;
66
- }
67
- :where(.wt-root) .kb-r1c3 {
68
- grid-area: 1 / 3;
69
- }
70
- :where(.wt-root) .kb-r1c4 {
71
- grid-area: 1 / 4;
72
- }
73
- :where(.wt-root) .kb-r2c1 {
74
- grid-area: 2 / 1;
75
- }
76
- :where(.wt-root) .kb-r2c2 {
77
- grid-area: 2 / 2;
78
- }
79
- :where(.wt-root) .kb-r2c3 {
80
- grid-area: 2 / 3;
81
- }
82
- :where(.wt-root) .kb-r2c4 {
83
- grid-area: 2 / 4;
84
- }
34
+ :where(.wt-root) .key-toolbar {
35
+ /* Absolute against .wt-root (identical geometry in wt-viewport mode; see
36
+ 01-scope.css). --vv-top keeps it following iOS's visual-viewport shift. */
37
+ position: absolute;
38
+ top: calc(var(--safe-top) + var(--sp-2) + var(--vv-top, 0px));
39
+ right: var(--sp-2);
40
+ z-index: 10;
41
+ display: grid;
42
+ grid-template-columns: repeat(4, var(--touch-target)) var(--touch-target);
43
+ grid-template-rows: var(--touch-target) var(--touch-target);
44
+ place-content: start end;
45
+ gap: var(--sp-1);
46
+ padding: var(--sp-1);
47
+ background: var(--surface);
48
+ backdrop-filter: var(--blur);
49
+ border: 1px solid var(--border);
50
+ border-radius: var(--radius);
51
+ overflow: hidden;
52
+ width: auto;
53
+ height: auto;
54
+ transition:
55
+ width var(--dur-enter) var(--ease-standard),
56
+ height var(--dur-enter) var(--ease-standard);
57
+ }
85
58
 
86
- /* Keys fade in/out during open/close. The container's width/height
87
- transition handles the background slide; opacity handles the
88
- individual button appearance. Stagger the fade slightly after
89
- the container starts growing (delay on open) and before it
90
- finishes shrinking (no delay on close). */
91
- :where(.wt-root) .kb-key {
92
- opacity: 1;
93
- transition: opacity var(--dur-standard) var(--ease-enter) 0.05s;
94
- }
59
+ /* Collapsed: fixed size = 1 button + padding + border. Content is
60
+ right-aligned (justify-content:end) so the toggle (col 5) stays at
61
+ the right edge, visible within the clipped area. --pill-outer
62
+ includes the 2px border (box-sizing:border-box); omitting it left
63
+ the content box 2px short, pinning the button top-right with
64
+ unequal bottom/left gaps. */
65
+ :where(.wt-root) .key-toolbar.collapsed {
66
+ width: var(--pill-outer);
67
+ height: var(--pill-outer);
68
+ }
95
69
 
96
- :where(.wt-root) .key-toolbar.collapsed .kb-key {
97
- opacity: 0;
98
- transition: opacity var(--dur-exit) var(--ease-exit);
99
- }
70
+ /* Grid placement: toggle at col 5 row 1 (top-right). */
71
+ :where(.wt-root) .kb-toggle {
72
+ grid-area: 1 / 5;
73
+ position: relative;
74
+ }
75
+ :where(.wt-root) .kb-r1c1 {
76
+ grid-area: 1 / 1;
77
+ }
78
+ :where(.wt-root) .kb-r1c2 {
79
+ grid-area: 1 / 2;
80
+ }
81
+ :where(.wt-root) .kb-r1c3 {
82
+ grid-area: 1 / 3;
83
+ }
84
+ :where(.wt-root) .kb-r1c4 {
85
+ grid-area: 1 / 4;
86
+ }
87
+ :where(.wt-root) .kb-r2c1 {
88
+ grid-area: 2 / 1;
89
+ }
90
+ :where(.wt-root) .kb-r2c2 {
91
+ grid-area: 2 / 2;
92
+ }
93
+ :where(.wt-root) .kb-r2c3 {
94
+ grid-area: 2 / 3;
95
+ }
96
+ :where(.wt-root) .kb-r2c4 {
97
+ grid-area: 2 / 4;
98
+ }
100
99
 
101
- /* Shared button */
102
- :where(.wt-root) .key-toolbar button {
103
- width: var(--touch-target);
104
- height: var(--touch-target);
105
- background: transparent;
106
- color: var(--text);
107
- border: none;
108
- border-radius: var(--radius-inner);
109
- font-size: 0.9375rem;
110
- font-family: var(--font-ui);
111
- font-weight: 500;
112
- cursor: pointer;
113
- touch-action: manipulation;
114
- -webkit-user-select: none;
115
- user-select: none;
116
- display: flex;
117
- align-items: center;
118
- justify-content: center;
119
- }
100
+ /* Keys fade in/out during open/close. The container's width/height
101
+ transition handles the background slide; opacity handles the
102
+ individual button appearance. Stagger the fade slightly after
103
+ the container starts growing (delay on open) and before it
104
+ finishes shrinking (no delay on close). */
105
+ :where(.wt-root) .kb-key {
106
+ opacity: 1;
107
+ transition: opacity var(--dur-standard) var(--ease-enter) 0.05s;
108
+ }
120
109
 
121
- :where(.wt-root) .key-toolbar button:active {
122
- background: var(--text-active);
123
- }
110
+ :where(.wt-root) .key-toolbar.collapsed .kb-key {
111
+ opacity: 0;
112
+ transition: opacity var(--dur-exit) var(--ease-exit);
113
+ }
124
114
 
125
- /* Sticky Ctrl, armed: inverted fill so the toggle state reads clearly
126
- in the monochrome palette (stays filled while pressed). */
127
- :where(.wt-root) .kb-key.armed,
128
- :where(.wt-root) .kb-key.armed:active {
129
- background: var(--text);
130
- color: var(--bg);
131
- }
115
+ /* Shared button */
116
+ :where(.wt-root) .key-toolbar button {
117
+ width: var(--touch-target);
118
+ height: var(--touch-target);
119
+ background: transparent;
120
+ color: var(--text);
121
+ border: none;
122
+ border-radius: var(--radius-inner);
123
+ font-size: 0.9375rem;
124
+ font-family: var(--font-ui);
125
+ font-weight: 500;
126
+ cursor: pointer;
127
+ touch-action: manipulation;
128
+ -webkit-user-select: none;
129
+ user-select: none;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ }
132
134
 
133
- :where(.wt-root) .key-toolbar button svg {
134
- width: var(--icon);
135
- height: var(--icon);
136
- stroke: currentcolor;
137
- fill: none;
138
- stroke-width: 2;
139
- stroke-linecap: round;
140
- stroke-linejoin: round;
141
- }
135
+ :where(.wt-root) .key-toolbar button:active {
136
+ background: var(--text-active);
137
+ }
142
138
 
143
- /* Toggle icon crossfade. Both icons are stacked absolute so they
144
- can independently fade + rotate during the open<->close
145
- transition. inset:0 + margin:auto centers them within the
146
- button (works because the SVGs carry explicit width/height); a
147
- bare `position: absolute` would pin them at the top-left, giving
148
- the toggle visibly unequal left/right padding when collapsed. */
149
- :where(.wt-root) .kb-toggle .icon-hamburger,
150
- :where(.wt-root) .kb-toggle .icon-close {
151
- position: absolute;
152
- inset: 0;
153
- margin: auto;
154
- transition:
155
- opacity var(--dur-standard) linear,
156
- transform var(--dur-standard) linear;
157
- }
139
+ /* Sticky Ctrl, armed: inverted fill so the toggle state reads clearly
140
+ in the monochrome palette (stays filled while pressed). */
141
+ :where(.wt-root) .kb-key.armed,
142
+ :where(.wt-root) .kb-key.armed:active {
143
+ background: var(--text);
144
+ color: var(--bg);
145
+ }
158
146
 
159
- :where(.wt-root) .kb-toggle .icon-close {
160
- opacity: 1;
161
- transform: rotate(0deg);
162
- }
163
- :where(.wt-root) .kb-toggle .icon-hamburger {
164
- opacity: 0;
165
- transform: rotate(-90deg);
166
- }
167
- :where(.wt-root) .key-toolbar.collapsed .kb-toggle .icon-close {
168
- opacity: 0;
169
- transform: rotate(90deg);
170
- }
171
- :where(.wt-root) .key-toolbar.collapsed .kb-toggle .icon-hamburger {
172
- opacity: 1;
173
- transform: rotate(0deg);
174
- }
147
+ :where(.wt-root) .key-toolbar button svg {
148
+ width: var(--icon);
149
+ height: var(--icon);
150
+ stroke: currentcolor;
151
+ fill: none;
152
+ stroke-width: 2;
153
+ stroke-linecap: round;
154
+ stroke-linejoin: round;
155
+ }
175
156
 
176
- /* No-transition on first render */
177
- :where(.wt-root) .key-toolbar.no-transition,
178
- :where(.wt-root) .key-toolbar.no-transition .kb-toggle .icon-hamburger,
179
- :where(.wt-root) .key-toolbar.no-transition .kb-toggle .icon-close {
180
- transition: none;
181
- }
157
+ /* Toggle icon crossfade. Both icons are stacked absolute so they
158
+ can independently fade + rotate during the open<->close
159
+ transition. inset:0 + margin:auto centers them within the
160
+ button (works because the SVGs carry explicit width/height); a
161
+ bare `position: absolute` would pin them at the top-left, giving
162
+ the toggle visibly unequal left/right padding when collapsed. */
163
+ :where(.wt-root) .kb-toggle .icon-hamburger,
164
+ :where(.wt-root) .kb-toggle .icon-close {
165
+ position: absolute;
166
+ inset: 0;
167
+ margin: auto;
168
+ transition:
169
+ opacity var(--dur-standard) linear,
170
+ transform var(--dur-standard) linear;
171
+ }
172
+
173
+ :where(.wt-root) .kb-toggle .icon-close {
174
+ opacity: 1;
175
+ transform: rotate(0deg);
176
+ }
177
+ :where(.wt-root) .kb-toggle .icon-hamburger {
178
+ opacity: 0;
179
+ transform: rotate(-90deg);
180
+ }
181
+ :where(.wt-root) .key-toolbar.collapsed .kb-toggle .icon-close {
182
+ opacity: 0;
183
+ transform: rotate(90deg);
184
+ }
185
+ :where(.wt-root) .key-toolbar.collapsed .kb-toggle .icon-hamburger {
186
+ opacity: 1;
187
+ transform: rotate(0deg);
188
+ }
189
+
190
+ /* No-transition on first render */
191
+ :where(.wt-root) .key-toolbar.no-transition,
192
+ :where(.wt-root) .key-toolbar.no-transition .kb-toggle .icon-hamburger,
193
+ :where(.wt-root) .key-toolbar.no-transition .kb-toggle .icon-close {
194
+ transition: none;
182
195
  }
@@ -432,39 +432,21 @@
432
432
  background: var(--tab-hover-bg);
433
433
  }
434
434
 
435
- /* ===== Externally-driven key grid (tabbed presets, WS3) =====
436
- In the tabbed presets the mobile toolbar hides its own toggle (a tab-chrome
437
- keyboard button drives it — the switcher's on a narrow phone, the strip's
438
- .wt-tab-kb on a wide touchscreen) and the grid docks bottom-right above the
439
- tab chrome, its RIGHT edge aligned with the keyboard button that opened it,
440
- so the popup visually hangs off its toggle in every mode (it may overlap the
441
- scroll-to-bottom button — deliberate: the grid is transient and on top). It
442
- collapses to fully hidden, since there is no in-grid toggle to keep visible.
443
- The BASE rules here are the wide-touchscreen (strip) placement — above
444
- --wt-tabbar-h, right-aligned to the strip's .wt-tab-kb (the bar's last flex
445
- item, inset --sp-2 by the bar padding); the .wt-narrow overrides below
446
- re-anchor above the switcher bar (--wt-switcher-h) and shift left past the
447
- bar controls sitting right of the switcher's kb button: the "+" (always)
448
- and, once a second tab reveals it, the switch button (.wt-switcher-multi
449
- from the tabs feature, read here via :has() since the grid is a sibling
450
- region of the switcher). Everything else (toggle hidden, grid columns,
451
- collapsed fully hidden) is identical in all modes. */
435
+ /* ===== Externally-driven key grid: tab-chrome docking (tabbed presets) =====
436
+ A tab-chrome keyboard button drives the grid (the switcher's on a narrow
437
+ phone, the strip's .wt-tab-kb on a wide touchscreen; both exist only under
438
+ a coarse pointer), so the grid docks bottom-right above the tab chrome with
439
+ its RIGHT edge on that button. The mode's own rules (no in-grid toggle, four
440
+ columns, collapsed means gone) are 23-toolbar.css's. The .wt-narrow
441
+ overrides re-anchor above the switcher bar and shift left past the "+" and,
442
+ once a second tab reveals it, the switch button. */
452
443
  @media (pointer: coarse) {
453
- :where(.wt-root) .wt-toolbar-external .kb-toggle {
454
- display: none;
455
- }
456
-
457
444
  :where(.wt-root) .key-toolbar.wt-toolbar-external {
458
445
  top: auto;
459
446
  right: var(--sp-2);
460
447
  bottom: calc(
461
448
  var(--wt-tabbar-h, 44px) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
462
449
  );
463
- grid-template-columns: repeat(4, var(--touch-target));
464
- }
465
-
466
- :where(.wt-root) .key-toolbar.wt-toolbar-external.collapsed {
467
- display: none;
468
450
  }
469
451
 
470
452
  /* Narrow phone (mobile switcher mode): dock above the switcher bar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "8.3.3",
3
+ "version": "8.3.4",
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": "MPL-2.0",
@@ -57,7 +57,7 @@
57
57
  "@vitest/coverage-v8": "5.0.3",
58
58
  "eslint": "10.12.0",
59
59
  "fast-check": "4.10.2",
60
- "html-validate": "11.16.1",
60
+ "html-validate": "11.16.2",
61
61
  "knip": "6.39.0",
62
62
  "playwright": "1.63.0",
63
63
  "prettier": "3.9.9",
@@ -20,8 +20,9 @@ export interface MobileToolbarApi {
20
20
 
21
21
  /** Options for the mobileToolbar feature. */
22
22
  export interface MobileToolbarOptions {
23
- /** Hide the toolbar's own toggle and open the grid above the mobile tab bar;
24
- * the grid is then driven through the returned API. The tabbed presets set it. */
23
+ /** Hide the toolbar's own toggle; the grid is then driven through the returned
24
+ * API and renders whenever it is open, whatever the primary pointer, so the
25
+ * host's trigger decides where a grid exists. The tabbed presets set it. */
25
26
  externalToggle?: boolean;
26
27
  }
27
28
 
@@ -38,12 +39,12 @@ const TOOLBAR_HTML = `
38
39
  <button type="button" id="kb-right" class="kb-key kb-r2c4" aria-label="Right"><svg viewBox="0 0 24 24"><polyline points="9 6 15 12 9 18"/></svg></button>
39
40
  </div>`;
40
41
 
41
- /** Build the mobileToolbar feature. The chrome is always built; only a coarse
42
- * pointer makes it visible (`css/23-toolbar.css`). Order it before `tabs` when
43
- * `externalToggle` is set, since tabs reads the API through `ctx.use`.
44
- * Sticky-Ctrl is a kernel INPUT TRANSFORM rather than a toolbar click path, so
45
- * it rewrites a character typed on the soft keyboard as well as one from these
46
- * buttons. */
42
+ /** Build the mobileToolbar feature. The chrome is always built; a self-toggled
43
+ * toolbar is visible only under a coarse pointer (`css/23-toolbar.css`). Order
44
+ * it before `tabs` when `externalToggle` is set, since tabs reads the API
45
+ * through `ctx.use`. Sticky-Ctrl is a kernel INPUT TRANSFORM rather than a
46
+ * toolbar click path, so it rewrites a character typed on the soft keyboard as
47
+ * well as one from these buttons. */
47
48
  export function mobileToolbar(opts: MobileToolbarOptions = {}): TerminalFeature<MobileToolbarApi> {
48
49
  return {
49
50
  name: "mobileToolbar",
@@ -639,6 +639,18 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
639
639
  };
640
640
  reflectArmed(kbApi.isCtrlArmed());
641
641
  ctx.defer(kbApi.onCtrlArmedChange(reflectArmed));
642
+ // The keyboard buttons render only under a coarse pointer, so a grid
643
+ // still open when the primary pointer turns fine would have no closer.
644
+ const coarse = win.matchMedia("(pointer: coarse)");
645
+ const onPointerChange = (): void => {
646
+ if (!coarse.matches) {
647
+ closeKeyGrid();
648
+ }
649
+ };
650
+ coarse.addEventListener("change", onPointerChange);
651
+ ctx.defer(() => {
652
+ coarse.removeEventListener("change", onPointerChange);
653
+ });
642
654
  }
643
655
  }
644
656
  // Measured optical centring for every chip label in both layouts: writes