@cplieger/web-terminal-ui 3.5.0 → 4.1.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 (41) hide show
  1. package/README.md +60 -27
  2. package/css/00-tokens.css +18 -2
  3. package/css/01-scope.css +76 -0
  4. package/css/{02-app.css → 02-terminal.css} +113 -239
  5. package/css/10-primitives.css +98 -0
  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} +127 -66
  11. package/css/{06-mobile.css → 31-switcher.css} +85 -80
  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 +19 -6
  19. package/scaffold/index.html +5 -0
  20. package/src/features/activity-monitor.ts +32 -2
  21. package/src/features/connection-banner.ts +1 -0
  22. package/src/features/context-menu.ts +6 -33
  23. package/src/features/menu-position.ts +50 -0
  24. package/src/features/mobile-toolbar.ts +5 -4
  25. package/src/features/{tabs.ts → tabs/index.ts} +310 -721
  26. package/src/features/tabs/model.ts +342 -0
  27. package/src/features/tabs/strip.ts +101 -0
  28. package/src/features/tabs/switcher.ts +129 -0
  29. package/src/index.ts +1 -0
  30. package/src/kernel/a11y.ts +30 -5
  31. package/src/kernel/conn-state.ts +16 -6
  32. package/src/kernel/kernel.ts +170 -59
  33. package/src/kernel/types.ts +68 -15
  34. package/src/presets/agent-tabbed.ts +15 -0
  35. package/src/presets/single.ts +27 -0
  36. package/src/presets/tabbed.ts +42 -0
  37. package/src/presets/touch.ts +13 -0
  38. package/src/presets.ts +12 -76
  39. package/src/viewport.ts +28 -15
  40. package/css/03-primitives.css +0 -83
  41. package/css/04-feedback.css +0 -180
package/README.md CHANGED
@@ -31,7 +31,8 @@ with opt-in feature modules that own everything above the raw terminal:
31
31
  - **predictive local echo**, **IME / composition** (CJK, dictation,
32
32
  autocorrect), and **viewport + keyboard-inset** handling for the iOS soft
33
33
  keyboard, rotation, and font-load reflows
34
- - a connection-status banner and a copy toast
34
+ - a connection-status banner, including a persistent protocol-incompatibility
35
+ state, and a copy toast
35
36
 
36
37
  It is published as TypeScript source (no build step) to npm and JSR, alongside
37
38
  the CSS bundle and a reference HTML page. Consumers who want a different UI should
@@ -43,17 +44,35 @@ depend on the engine directly and skip this package.
43
44
  npm install @cplieger/web-terminal-ui @cplieger/web-terminal-engine
44
45
  ```
45
46
 
46
- `@cplieger/web-terminal-engine` is a peer dependency — the UI is built on the engine,
47
- and both halves of the wire protocol must stay in lockstep, so the consumer
48
- pins the engine version explicitly.
47
+ `@cplieger/web-terminal-engine` is a peer dependency — the UI is built on the
48
+ engine, so the consumer pins the engine version explicitly. Pairing compatibility
49
+ is governed by the engine's
50
+ [directional wire contract](https://github.com/cplieger/web-terminal-engine#wire-protocol),
51
+ not strict package-version equality; the UI surfaces a terminal incompatibility
52
+ through its connection banner.
49
53
 
50
54
  ## Usage
51
55
 
52
- Serve the bundled CSS (`css/` concatenated per `css/MANIFEST` into the
53
- `style.css` your page links) plus a minimal HTML page that has one empty
54
- container element for the terminal, then call `createTerminal(root, { features })`
55
- from your entry module. Feature bundles (presets) live at the `./presets`
56
- sub-path:
56
+ Serve a CSS bundle matching how you embed the terminal, plus a minimal HTML
57
+ page that has one empty container element, then call
58
+ `createTerminal(root, { features })` from your entry module.
59
+
60
+ **Full-page host** (the terminal IS the page — `web-terminal-server`,
61
+ `web-terminal-kiro`): concatenate `css/MANIFEST` into the `style.css` your page
62
+ links. That reference bundle is `css/page.css` (the page kit: `html/body`
63
+ reset + the terminal web font's `@font-face`, expecting the font files at
64
+ `/vendor/fonts/`) plus the complete component set.
65
+
66
+ **Embedder** (the terminal lives inside your app's layout — a panel, a pane):
67
+ concatenate the per-preset manifest matching your composition instead —
68
+ `css/MANIFEST.single`, `css/MANIFEST.touch`, or `css/MANIFEST.tabbed`. These
69
+ contain ONLY root-scoped component styles: no page reset, no fonts, no
70
+ document-level rules, nothing to quarantine. Pass `layout: "container"` so the
71
+ terminal fills (and positions its chrome against) your container element
72
+ instead of the viewport.
73
+
74
+ Feature bundles (presets) live at the `./presets` sub-path, with per-preset
75
+ entry modules beside it:
57
76
 
58
77
  ```html
59
78
  <div id="terminal"></div>
@@ -81,14 +100,22 @@ sub-path:
81
100
 
82
101
  `createTerminal(root, opts?)` builds the entire terminal subtree (the kernel
83
102
  plus every feature's chrome) inside `root` itself — there is no element-id
84
- contract for the host page to reproduce. Call it exactly once; the engine's
103
+ contract for the host page to reproduce, and every style and CSS custom
104
+ property is scoped to the `wt-root` class it stamps on your element (removed
105
+ again by `destroy()`). Call it exactly once; the engine's
85
106
  render/connection/scroll modules are single-instance per page (tabs multiplex
86
107
  sessions over the one kernel). `scaffold/index.html` is a complete reference page
87
108
  to copy and adapt.
88
109
 
89
- Four presets are provided (import from `@cplieger/web-terminal-ui/presets`); each
90
- is a plain feature-array factory, so you can spread and edit it, or hand-pick
91
- individual features instead:
110
+ Four presets are provided; each is a plain feature-array factory, so you can
111
+ spread and edit it. Import the barrel (`@cplieger/web-terminal-ui/presets`) for
112
+ convenience, or a per-preset entry module for the minimal delivered import
113
+ graph — `…/presets/single`, `…/presets/touch`, `…/presets/tabbed`,
114
+ `…/presets/agent-tabbed` (the barrel statically reaches every feature; the
115
+ touch entry, for example, never imports the tabs module). Individual features
116
+ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
117
+ `scroll-to-bottom`, `predictive-echo`, `connection-banner`, `mobile-toolbar`,
118
+ `tabs`, `activity-monitor`, `animations`) for hand-picked compositions:
92
119
 
93
120
  - `presetSingle()` — single-pane desktop UI (context menu, clipboard,
94
121
  scroll-to-bottom, predictive echo, connection banner).
@@ -110,25 +137,31 @@ individual features instead:
110
137
 
111
138
  ### Options
112
139
 
113
- | Option | Default | Purpose |
114
- | ----------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
115
- | `features` | _(none — bare kernel)_ | The feature list. Omitted or empty builds only the terminal (no chrome). Use a preset from `./presets` or a hand-picked array. |
116
- | `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
117
- | `fontReady` | `'14px "MonaspiceNe NFM"'` | CSS font shorthand awaited before the first resize, so the server is sized against the real web font's cell metrics rather than a fallback. |
118
- | `loading` | _(none)_ | A pre-JS loading overlay element (kept in your served HTML so it paints before this module loads); it is faded out and removed once the first frame renders. |
119
- | `theme` | _(none)_ | Theme overrides (CSS custom properties on the terminal root): `--accent`, `--tab-hover-bg`, `--tab-active-bg`, `--tab-active-fg`. The library ships neutral defaults. |
140
+ | Option | Default | Purpose |
141
+ | ----------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
142
+ | `features` | _(none — bare kernel)_ | The feature list. Omitted or empty builds only the terminal (no chrome). Use a preset from `./presets` or a hand-picked array. |
143
+ | `layout` | `"viewport"` | How the terminal claims space. `"viewport"`: the root becomes a fixed full-viewport box (the full-page product). `"container"`: the root fills your container element, which becomes the styling and positioning boundary (the embedded case). |
144
+ | `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
145
+ | `fontReady` | `'14px "MonaspiceNe NFM"'` | CSS font shorthand awaited before the first resize, so the server is sized against the real web font's cell metrics rather than a fallback. |
146
+ | `loading` | _(none)_ | A pre-JS loading overlay element (kept in your served HTML so it paints before this module loads); it is faded out and removed once the first frame renders. |
147
+ | `theme` | _(none)_ | Theme overrides (CSS custom properties on the terminal root): `--accent`, `--tab-bg`, `--tab-hover-bg`, `--tab-active-bg`, `--tab-active-fg`, `--tab-active-border`. The library ships neutral defaults. |
120
148
 
121
149
  `createTerminal()` returns a handle: `focus()` re-focuses the terminal input
122
- (and opens the soft keyboard on touch), and `destroy()` tears every feature down
123
- and releases the kernel.
150
+ (and opens the soft keyboard on touch); `send(bytes)` sends bytes to the active
151
+ session through the kernel's sanitizing input funnel (the supported host path
152
+ for a "type this command" affordance); `reset()` drops the local scrollback and
153
+ screen without injecting keystrokes (send a redraw keystroke yourself if you
154
+ want one — e.g. Ctrl+L); and `destroy()` tears every feature down and releases
155
+ the kernel.
124
156
 
125
157
  ## What ships
126
158
 
127
- | Path | Purpose |
128
- | ------------------------ | --------------------------------------------------------------------------------------------------------------- |
129
- | `src/**/*.ts` | The UI modules: the kernel (`kernel/`), opt-in features (`features/`), presets, IME, predictive echo, viewport. |
130
- | `css/*.css` + `MANIFEST` | The default theme + layout; concatenate in MANIFEST order. |
131
- | `scaffold/index.html` | A reference HTML page: `<head>` + one empty root element + importmap. |
159
+ | Path | Purpose |
160
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
161
+ | `src/**/*.ts` | The UI modules: the kernel (`kernel/`), opt-in features (`features/`), per-preset entries (`presets/`), IME, predictive echo, viewport. |
162
+ | `css/*.css` + manifests | Root-scoped component styles. `MANIFEST` = the reference full-page bundle (`page.css` + the tabbed set); `MANIFEST.single/touch/tabbed` = component-only per-preset bundles for embedders. Concatenate in manifest order. |
163
+ | `css/page.css` | The page kit (full-page hosts only): `html/body` reset + the terminal web font's `@font-face`. |
164
+ | `scaffold/index.html` | A reference full-page HTML host: `<head>` + one empty root element + importmap. |
132
165
 
133
166
  ## Related projects
134
167
 
package/css/00-tokens.css CHANGED
@@ -1,11 +1,27 @@
1
- /* ===== Tokens ===== */
2
- :root {
1
+ /* ===== Tokens =====
2
+ Every token defaults on .wt-root (the class createTerminal stamps on the
3
+ consumer's root element), never on the document :root: the terminal subtree
4
+ inherits them, the host page never does, and a host page's identically-named
5
+ custom properties never reach the terminal (the class definition wins for the
6
+ subtree). The `theme` option overrides them as inline style on the same
7
+ element — the consumer contract is unchanged. */
8
+ .wt-root {
3
9
  interpolate-size: allow-keywords;
4
10
 
5
11
  --bg: oklch(0% 0 0deg);
6
12
  --surface: oklch(16% 0.005 280deg / 70%);
7
13
  --border: oklch(100% 0 0deg / 15%);
8
14
  --text: oklch(90% 0.005 280deg);
15
+
16
+ /* Captured copies of the default color pair, resolved HERE (custom
17
+ properties substitute var() at computed-value time, so descendants
18
+ inherit the resolved colors, not the references). The DECSCNM
19
+ reverse-video rule in 02-terminal.css swaps --text/--bg on .term by
20
+ reading these — a direct self-referential swap (--text: var(--bg))
21
+ would be a cycle and invalidate both. Theme overrides (inline custom
22
+ properties on this element, or a consumer rule) are captured too. */
23
+ --text-default: var(--text);
24
+ --bg-default: var(--bg);
9
25
  --text-active: color-mix(in oklch, var(--text) 15%, transparent);
10
26
  /* ===== Themeable tokens (the consumer-configurable "template") =====
11
27
  These default to the neutral built-in look; a consumer recolors the UI by
@@ -0,0 +1,76 @@
1
+ /* ===== Component scope + layout modes =====
2
+ Everything here is scoped under .wt-root — the class createTerminal stamps on
3
+ the consumer's root element. The library never styles the host document: page
4
+ concerns (html/body reset, fonts, viewport meta behavior) live in page.css,
5
+ served only by full-page hosts. This file owns the boundary itself: the
6
+ scoped box-sizing + font-smoothing baseline, the two layout modes, and the
7
+ scoped reduced-motion kill-switch. */
8
+
9
+ /* Layout mode: how the root claims space (kernel stamps exactly one).
10
+ wt-viewport — the root IS the page surface: a fixed full-viewport box (the
11
+ full-page product; geometrically identical to the pre-v4 fixed-inset .term).
12
+ wt-container — the root fills its parent, which becomes the boundary: the
13
+ embedded case. overflow: clip keeps absolute chrome (menus, banners, the
14
+ switcher) inside the box, so an embedder needs no containment workarounds. */
15
+ .wt-root.wt-viewport {
16
+ position: fixed;
17
+ inset: 0;
18
+ }
19
+
20
+ .wt-root.wt-container {
21
+ position: relative;
22
+ inline-size: 100%;
23
+ block-size: 100%;
24
+ overflow: clip;
25
+ }
26
+
27
+ .wt-root,
28
+ .wt-root *,
29
+ .wt-root *::before,
30
+ .wt-root *::after {
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ .wt-root {
35
+ -webkit-font-smoothing: antialiased;
36
+ /* The terminal subtree is dark regardless of the host page's scheme, and
37
+ UA-rendered bits inside it (classic scrollbars, selection tints) must
38
+ match — page.css declares this on html for full-page hosts, but an
39
+ embedded root (wt-container) inherits the HOST's scheme without this. */
40
+ color-scheme: dark;
41
+ /* Design-system scrollbar COLOR on every scroller in the subtree (the
42
+ terminal, the tab scroller, the expanded switcher list): border-grey
43
+ thumb, no track — the vibekit/subflux treatment. scrollbar-color
44
+ INHERITS, so the boundary is the one place to set it. Where it is not
45
+ supported the color-scheme above still renders the UA fallback dark.
46
+ Overlay-scrollbar platforms are unaffected. */
47
+ scrollbar-color: var(--border) transparent;
48
+ }
49
+
50
+ /* scrollbar-width does NOT inherit (unlike scrollbar-color), so the thin
51
+ variant is applied to every element in the boundary: a no-op for anything
52
+ that does not scroll, and any scroller a future feature adds is covered
53
+ automatically instead of joining an explicit list nobody remembers. */
54
+ .wt-root * {
55
+ scrollbar-width: thin;
56
+ }
57
+
58
+ /* stylelint-disable declaration-no-important, time-min-milliseconds --
59
+ Canonical prefers-reduced-motion override. !important and near-zero
60
+ durations are required to forcibly defeat any animation/transition rule
61
+ regardless of specificity, which is the whole point of this a11y reset.
62
+ Scoped to the terminal subtree: the host page's own motion policy is the
63
+ host's business (pre-v4 this hit `*` and silently flattened the embedding
64
+ app's animations). */
65
+ @media (prefers-reduced-motion: reduce) {
66
+ .wt-root,
67
+ .wt-root *,
68
+ .wt-root *::before,
69
+ .wt-root *::after {
70
+ animation-duration: 0.01ms !important;
71
+ animation-iteration-count: 1 !important;
72
+ transition-duration: 0.01ms !important;
73
+ scroll-behavior: auto !important;
74
+ }
75
+ }
76
+ /* stylelint-enable declaration-no-important, time-min-milliseconds */