@cplieger/web-terminal-ui 3.4.5 → 4.0.0

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.
Files changed (38) hide show
  1. package/README.md +53 -23
  2. package/css/00-tokens.css +8 -2
  3. package/css/01-scope.css +56 -0
  4. package/css/{02-app.css → 02-terminal.css} +64 -239
  5. package/css/{03-primitives.css → 10-primitives.css} +21 -19
  6. package/css/20-connection-banner.css +42 -0
  7. package/css/21-scroll-to-bottom.css +65 -0
  8. package/css/22-context-menu.css +45 -0
  9. package/css/23-toolbar.css +181 -0
  10. package/css/{05-tabs.css → 30-tabs.css} +96 -57
  11. package/css/{06-mobile.css → 31-switcher.css} +66 -81
  12. package/css/{07-animations.css → 40-animations.css} +15 -15
  13. package/css/MANIFEST +19 -11
  14. package/css/MANIFEST.single +12 -0
  15. package/css/MANIFEST.tabbed +16 -0
  16. package/css/MANIFEST.touch +12 -0
  17. package/css/{01-reset.css → page.css} +18 -19
  18. package/package.json +21 -5
  19. package/scaffold/index.html +5 -0
  20. package/src/features/connection-banner.ts +3 -0
  21. package/src/features/context-menu.ts +6 -33
  22. package/src/features/menu-position.ts +50 -0
  23. package/src/features/{tabs.ts → tabs/index.ts} +207 -636
  24. package/src/features/tabs/model.ts +334 -0
  25. package/src/features/tabs/strip.ts +101 -0
  26. package/src/features/tabs/switcher.ts +129 -0
  27. package/src/index.ts +1 -0
  28. package/src/kernel/a11y.ts +30 -5
  29. package/src/kernel/conn-state.ts +20 -3
  30. package/src/kernel/kernel.ts +162 -59
  31. package/src/kernel/types.ts +65 -15
  32. package/src/presets/agent-tabbed.ts +15 -0
  33. package/src/presets/single.ts +27 -0
  34. package/src/presets/tabbed.ts +42 -0
  35. package/src/presets/touch.ts +13 -0
  36. package/src/presets.ts +12 -76
  37. package/src/viewport.ts +28 -15
  38. package/css/04-feedback.css +0 -180
package/src/presets.ts CHANGED
@@ -1,8 +1,11 @@
1
- // Feature presets (design section 22.6). Presets are plain feature-array
2
- // factories, so they are spreadable and editable: a consumer can drop or add a
3
- // feature with a filter/spread. Importing a preset pulls in every feature it
4
- // lists (convenient, full UI); a consumer wanting a minimal footprint imports
5
- // individual features instead.
1
+ // Feature presets: the barrel. Presets are plain feature-array factories, so
2
+ // they are spreadable and editable: a consumer can drop or add a feature with a
3
+ // filter/spread. This barrel statically imports EVERY preset (and therefore
4
+ // every feature); a consumer that wants the minimal delivered import graph for
5
+ // its composition imports the per-preset entry module instead —
6
+ // "@cplieger/web-terminal-ui/presets/single" | "/presets/touch" |
7
+ // "/presets/tabbed" | "/presets/agent-tabbed" — or hand-picks individual
8
+ // features from "@cplieger/web-terminal-ui/features/<name>".
6
9
  //
7
10
  // The tabbed presets (presetTabbed / presetAgentTabbed) share the same feature
8
11
  // set (both include the activity monitor, whose per-tab dot reveals itself only
@@ -13,74 +16,7 @@
13
16
  // server-side — an agent server sets a classifier mapping OSC 9 notifications to
14
17
  // done/needs-input.
15
18
 
16
- import type { TerminalFeature } from "./kernel/types.js";
17
- import { clipboard } from "./features/clipboard.js";
18
- import { contextMenu } from "./features/context-menu.js";
19
- import { scrollToBottom } from "./features/scroll-to-bottom.js";
20
- import { predictiveEcho } from "./features/predictive-echo.js";
21
- import { connectionBanner } from "./features/connection-banner.js";
22
- import { mobileToolbar } from "./features/mobile-toolbar.js";
23
- import { tabs } from "./features/tabs.js";
24
- import { activityMonitor } from "./features/activity-monitor.js";
25
- import { animations } from "./features/animations.js";
26
-
27
- /** Single-pane desktop UI: context menu, clipboard, scroll-to-bottom, predictive
28
- * echo, and the connection banner. No mobile toolbar, no tabs. */
29
- export function presetSingle(): TerminalFeature<unknown>[] {
30
- // contextMenu offers Copy/Paste through the clipboard feature (ctx.use), so it
31
- // holds the same clipboard value the array includes.
32
- const clip = clipboard();
33
- return [
34
- contextMenu({ clipboard: clip }),
35
- clip,
36
- scrollToBottom(),
37
- predictiveEcho(),
38
- connectionBanner(),
39
- ];
40
- }
41
-
42
- /** Touch-first UI: presetSingle plus the on-screen key toolbar. */
43
- export function presetTouch(): TerminalFeature<unknown>[] {
44
- return [...presetSingle(), mobileToolbar()];
45
- }
46
-
47
- // buildTabbed composes the tabbed UI: the single-pane touch features, an
48
- // externally-driven mobile toolbar (its grid is opened from a keyboard button in
49
- // the tab bar, not its own toggle), the activity monitor (the status-SSE data
50
- // source), tabs wired to both, and animations. The activity dot reveals itself
51
- // per tab only when a session reports activity (OSC 9;4), so the monitor is
52
- // always included — a plain shell just never reveals a dot. The toolbar and
53
- // monitor are ordered before tabs because tabs reads their APIs via ctx.use.
54
- function buildTabbed(preferInputTitle: boolean): TerminalFeature<unknown>[] {
55
- const kb = mobileToolbar({ externalToggle: true });
56
- const monitor = activityMonitor();
57
- return [
58
- ...presetSingle(),
59
- kb,
60
- monitor,
61
- tabs({ keyboardToggle: kb, activityMonitor: monitor, preferInputTitle }),
62
- animations(),
63
- ];
64
- }
65
-
66
- /** Tabbed UI: the touch features, tabs, the mobile keyboard bar, the activity
67
- * monitor, and animations. Requires a server that speaks the session API
68
- * (`/api/sessions`, `/ws?session=`, and the status SSE `/api/sessions/events`),
69
- * such as `web-terminal-server` or `web-terminal-kiro`. Each tab's title follows the
70
- * process OSC 0/2 window title when the program sets one, else the last
71
- * submitted line; each tab's activity dot stays hidden until its session
72
- * reports activity via OSC 9;4 progress (kiro-cli, Claude Code, …), so a plain
73
- * bash/sh keeps clean, label-only tabs. */
74
- export function presetTabbed(): TerminalFeature<unknown>[] {
75
- return buildTabbed(false);
76
- }
77
-
78
- /** Tabbed UI for an agent shell (web-terminal-kiro). Same features as presetTabbed, but
79
- * with `preferInputTitle`: the agent's program (kiro-cli) emits a non-empty but
80
- * useless OSC 0/2 title, so each tab's label follows the latest submitted line
81
- * (persisted server-side and recovered on reload) and the OSC title is ignored.
82
- * The agent-vs-generic status distinction remains server-side (a status
83
- * classifier mapping OSC 9 notifications to done/needs-input). */
84
- export function presetAgentTabbed(): TerminalFeature<unknown>[] {
85
- return buildTabbed(true);
86
- }
19
+ export { presetSingle } from "./presets/single.js";
20
+ export { presetTouch } from "./presets/touch.js";
21
+ export { presetTabbed } from "./presets/tabbed.js";
22
+ export { presetAgentTabbed } from "./presets/agent-tabbed.js";
package/src/viewport.ts CHANGED
@@ -20,6 +20,11 @@ import { scroll } from "@cplieger/web-terminal-engine";
20
20
  const SETTLE_MS = 350;
21
21
 
22
22
  let termWrap: HTMLElement;
23
+ // The terminal root (.wt-root): the geometry CSS vars (--kb-inset, --vv-top)
24
+ // are published here so the terminal subtree — and nothing else on the host
25
+ // page — inherits them. Falls back to termWrap if init was somehow given no
26
+ // root (never in practice; the kernel always passes it).
27
+ let varTarget: HTMLElement;
23
28
  let onSettled: ((wasAtBottom: boolean) => void) | null = null;
24
29
  let inTransition = false;
25
30
  let wasAtBottomAtStart = true;
@@ -59,6 +64,10 @@ function startTransition(): void {
59
64
 
60
65
  export function init(opts: {
61
66
  termWrap: HTMLElement;
67
+ /** The terminal root (.wt-root): receives the geometry CSS vars the sibling
68
+ * chrome reads (--kb-inset, --vv-top), so they scope to the terminal
69
+ * subtree instead of leaking onto the host document. */
70
+ root?: HTMLElement;
62
71
  onSettled: (wasAtBottom: boolean) => void;
63
72
  /** Ignore the visualViewport keyboard geometry (a hardware-keyboard device
64
73
  * has no soft keyboard to accommodate); only reserved bottom chrome insets
@@ -66,6 +75,7 @@ export function init(opts: {
66
75
  suppressKeyboardInset?: () => boolean;
67
76
  }): void {
68
77
  termWrap = opts.termWrap;
78
+ varTarget = opts.root ?? opts.termWrap;
69
79
  onSettled = opts.onSettled;
70
80
  suppressKeyboardInset = opts.suppressKeyboardInset ?? (() => false);
71
81
 
@@ -105,12 +115,13 @@ export function init(opts: {
105
115
  // folds the reserve into the bottom offset here. The reserve excludes the
106
116
  // keyboard (it is measured with the keyboard closed), so adding it to
107
117
  // bottomInset does not double-count.
118
+ // Read the reserve off the terminal itself: tabs publishes it on the
119
+ // root (.wt-root), and termWrap inherits it — nothing terminal-scoped
120
+ // lives on the document root anymore.
108
121
  const rawReserve = Math.max(
109
122
  0,
110
123
  Math.round(
111
- parseFloat(
112
- getComputedStyle(document.documentElement).getPropertyValue("--wt-reserve-bottom"),
113
- ) || 0,
124
+ parseFloat(getComputedStyle(termWrap).getPropertyValue("--wt-reserve-bottom")) || 0,
114
125
  ),
115
126
  );
116
127
  // Sanity cap: the reserve is bottom chrome (a tab bar), tens of px. A value
@@ -122,13 +133,13 @@ export function init(opts: {
122
133
  const bottom = bottomInset + reserve;
123
134
  termWrap.style.top = offsetTop > 0 ? `${offsetTop}px` : "";
124
135
  termWrap.style.bottom = bottom > 0 ? `${bottom}px` : "";
125
- // Expose the same geometry as CSS vars for the sibling fixed chrome:
126
- // --kb-inset lifts the bottom banner above the keyboard; --vv-top lets the
127
- // top key toolbar follow the visual viewport's offset (otherwise it
128
- // scrolls off the top when iOS shifts the layout up).
129
- const root = document.documentElement.style;
130
- root.setProperty("--kb-inset", `${bottomInset}px`);
131
- root.setProperty("--vv-top", `${offsetTop}px`);
136
+ // Expose the same geometry as CSS vars for the sibling chrome, on the
137
+ // terminal ROOT (scoped: the host page never sees them): --kb-inset
138
+ // lifts the bottom banner above the keyboard; --vv-top lets the top key
139
+ // toolbar follow the visual viewport's offset (otherwise it scrolls off
140
+ // the top when iOS shifts the layout up).
141
+ varTarget.style.setProperty("--kb-inset", `${bottomInset}px`);
142
+ varTarget.style.setProperty("--vv-top", `${offsetTop}px`);
132
143
  startTransition();
133
144
  };
134
145
  vv.addEventListener("resize", onChange);
@@ -192,11 +203,13 @@ export function teardown(): void {
192
203
  clearTimeout(settleTimer);
193
204
  settleTimer = null;
194
205
  }
195
- // Clear the global root CSS vars onChange published, so a destroy without
196
- // a remount (which would recompute them) leaves no stale inset on :root.
197
- const rootStyle = document.documentElement.style;
198
- rootStyle.removeProperty("--kb-inset");
199
- rootStyle.removeProperty("--vv-top");
206
+ // Clear the CSS vars onChange published on the terminal root, so a destroy
207
+ // without a remount (which would recompute them) leaves no stale inset.
208
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- teardown may run before any init in tests
209
+ if (varTarget) {
210
+ varTarget.style.removeProperty("--kb-inset");
211
+ varTarget.style.removeProperty("--vv-top");
212
+ }
200
213
  inTransition = false;
201
214
  onSettled = null;
202
215
  suppressKeyboardInset = () => false;
@@ -1,180 +0,0 @@
1
- /* ===== Feedback + overlay chrome =====
2
- Transient, non-tab feature surfaces: the connection banner, the
3
- scroll-to-bottom control, the right-click/long-press context menu, and the
4
- tab catching-up cue. Grouped because they are all fixed, floating, and
5
- short-lived; their motion lives in 07-animations.css. */
6
-
7
- /* ===== Connection banner (connectionBanner feature) =====
8
- Top-centre on every form factor (same rationale as the toast: a bottom bar is
9
- always docked now). */
10
- .wt-conn-banner {
11
- position: fixed;
12
- top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + var(--sp-3));
13
- left: 50%;
14
- transform: translateX(-50%);
15
- max-width: calc(100% - 2 * var(--sp-3));
16
- padding: var(--sp-1) var(--sp-3);
17
- background: var(--surface);
18
- backdrop-filter: var(--blur);
19
- border: 1px solid var(--border);
20
- border-radius: var(--radius);
21
- color: var(--text);
22
- font-family: var(--font-ui);
23
- font-size: 0.8125rem;
24
- font-weight: 500;
25
- text-align: center;
26
- z-index: 20;
27
- pointer-events: none; /* passive status surface: never steal taps from the terminal beneath */
28
- display: none;
29
- }
30
-
31
- .wt-conn-banner.visible {
32
- display: block;
33
- }
34
-
35
- .wt-conn-banner[data-state="reconnecting"] {
36
- border-color: oklch(70% 0.15 60deg / 40%);
37
- }
38
-
39
- .wt-conn-banner[data-state="offline"] {
40
- color: #f55;
41
- border-color: oklch(60% 0.2 25deg / 50%);
42
- }
43
-
44
- .wt-conn-banner[data-state="restarted"] {
45
- color: #fa5;
46
- border-color: oklch(70% 0.15 60deg / 40%);
47
- }
48
-
49
- /* ===== Scroll-to-bottom (scrollToBottom feature) =====
50
- A compact floating icon button docked in the bottom-right corner, shown only
51
- while the user has scrolled up. Same button on both form factors (the old
52
- desktop bottom-centre text pill was dropped in the design-system switch, and
53
- its dead CSS removed from 02-app.css); each form factor just lifts it above
54
- its own bottom bar. Size + icon centering come from the .wt-btn primitive (a
55
- 44px icon button); this rule adds only the floating chrome and placement. */
56
- .wt-scroll-bottom {
57
- position: fixed;
58
- right: var(--sp-3);
59
- bottom: calc(var(--safe-bottom, 0px) + var(--sp-3) + var(--kb-inset, 0px));
60
- background: var(--surface);
61
- backdrop-filter: var(--blur);
62
- border: 1px solid var(--border);
63
- /* Match the tab "+" buttons (and every other .wt-btn), which round to
64
- --radius-inner. Both are 44px boxes, so matching the corner radius makes
65
- the FAB read as the same-sized button as the new-tab "+" beside/above it
66
- rather than a rounder, different-looking control. */
67
- border-radius: var(--radius-inner);
68
- /* Full --text icon, matching the switcher's keyboard/switch/"+" icons (which
69
- use .wt-btn's default color). Was a muted 70% mix that read as grey next to
70
- those white bar icons. */
71
- color: var(--text);
72
- z-index: 10;
73
- display: none;
74
- }
75
-
76
- /* The scroll slot gets .scrolled-up toggled by the feature; show the button. */
77
- .wt-slot-scroll.scrolled-up .wt-scroll-bottom {
78
- display: inline-flex;
79
- }
80
-
81
- /* Hover brightens the border; press fills the background (the .wt-btn :active
82
- fill would otherwise be masked by this button's own surface fill). The icon
83
- is already full --text, so hover no longer needs to change it. */
84
- .wt-scroll-bottom:hover {
85
- border-color: color-mix(in oklch, var(--text) 35%, transparent);
86
- }
87
-
88
- .wt-scroll-bottom:active {
89
- background: var(--text-active);
90
- }
91
-
92
- /* Mobile + tabbed: lift above the switcher bar and the keyboard (WS3), and pull
93
- the right edge in to --sp-2 so the button sits directly above the switcher's
94
- "+" (whose right edge is inset by the bar's --sp-2 padding). Both are 44px
95
- border-box boxes, so matching the right inset lines them up in one vertical
96
- column (they were 4px out: the FAB's base --sp-3 vs the "+"'s --sp-2). */
97
- @media (pointer: coarse) {
98
- .wt-tabbed .wt-scroll-bottom {
99
- right: var(--sp-2);
100
- bottom: calc(
101
- var(--wt-switcher-h) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
102
- );
103
- }
104
- }
105
-
106
- /* Desktop + tabbed: the tab strip docks at the bottom, so lift the button above
107
- it with a clear gap. */
108
- @media (pointer: fine) {
109
- .wt-tabbed .wt-scroll-bottom {
110
- bottom: calc(var(--wt-tabbar-h, 44px) + var(--sp-3));
111
- }
112
- }
113
-
114
- /* ===== Context menu (contextMenu feature) ===== */
115
- .wt-ctx-menu {
116
- position: fixed;
117
- display: none;
118
- flex-direction: column;
119
- background: var(--surface);
120
- backdrop-filter: var(--blur);
121
- border: 1px solid var(--border);
122
- border-radius: var(--radius);
123
- padding: var(--sp-1);
124
- z-index: 30;
125
- }
126
-
127
- .wt-ctx-menu.visible {
128
- display: flex;
129
- }
130
-
131
- .wt-ctx-menu button {
132
- background: transparent;
133
- color: var(--text);
134
- border: none;
135
- border-radius: var(--radius-inner);
136
- font-family: var(--font-ui);
137
- font-size: 0.8125rem;
138
- font-weight: 500;
139
- padding: var(--sp-1) var(--sp-3);
140
- cursor: pointer;
141
- text-align: left;
142
- }
143
-
144
- .wt-ctx-menu button:active {
145
- background: var(--text-active);
146
- }
147
-
148
- @media (pointer: coarse) {
149
- .wt-ctx-menu button {
150
- min-height: var(--touch-target);
151
- padding-block: var(--sp-2);
152
- touch-action: manipulation;
153
- }
154
- }
155
-
156
- /* ===== Catching-up cue (tabs feature, sections 12/13) =====
157
- A switched-into tab's cached screen is stale until its resume delta lands, so
158
- it must not read as live. This unobtrusive cue shows only if the delta has not
159
- arrived shortly after the switch, and clears on the first screen frame. */
160
- .wt-catchup {
161
- position: fixed;
162
- top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + 44px);
163
- left: 50%;
164
- transform: translateX(-50%);
165
- padding: 2px var(--sp-2);
166
- background: var(--surface);
167
- backdrop-filter: var(--blur);
168
- border: 1px solid var(--border);
169
- border-radius: var(--radius);
170
- color: color-mix(in oklch, var(--text) 75%, transparent);
171
- font-family: var(--font-ui);
172
- font-size: 0.75rem;
173
- z-index: 24;
174
- pointer-events: none; /* passive status surface: never steal taps from the terminal beneath */
175
- display: none;
176
- }
177
-
178
- .wt-catchup.visible {
179
- display: block;
180
- }