@cplieger/web-terminal-ui 1.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.
- package/LICENSE +692 -0
- package/README.md +76 -0
- package/css/00-tokens.css +33 -0
- package/css/01-reset.css +73 -0
- package/css/02-app.css +730 -0
- package/css/MANIFEST +5 -0
- package/package.json +58 -0
- package/scaffold/index.html +52 -0
- package/src/composition.ts +171 -0
- package/src/index.ts +10 -0
- package/src/mount.ts +798 -0
- package/src/predict.ts +183 -0
- package/src/status.ts +173 -0
- package/src/viewport.ts +98 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @cplieger/web-terminal-ui
|
|
2
|
+
|
|
3
|
+
The reference touch-first browser UI for
|
|
4
|
+
[`@cplieger/web-terminal-engine`](https://github.com/cplieger/web-terminal-engine) — the part
|
|
5
|
+
that turns the engine's render/scroll/connection/keyboard modules into a
|
|
6
|
+
usable terminal on a phone as well as a desktop.
|
|
7
|
+
|
|
8
|
+
One `mount()` call wires the engine to a small HTML scaffold and owns
|
|
9
|
+
everything above the raw terminal:
|
|
10
|
+
|
|
11
|
+
- a **display-only** `#term-output` (native text selection survives redraws)
|
|
12
|
+
- a hidden `<textarea>` that owns the keyboard, the local typing buffer, and IME
|
|
13
|
+
- a **mobile key toolbar** (Tab / Esc / arrows / Enter / sticky-Ctrl) and a
|
|
14
|
+
scroll-to-bottom control
|
|
15
|
+
- a **viewport-clamped context menu** (Copy / Select All / Paste)
|
|
16
|
+
- **predictive local echo** so the cursor moves on keypress even on a slow link
|
|
17
|
+
- **IME / composition** handling (CJK, dictation, autocorrect)
|
|
18
|
+
- **viewport + keyboard-inset** handling for the iOS soft keyboard, rotation,
|
|
19
|
+
and font-load reflows
|
|
20
|
+
- a connection-status banner and a copy toast
|
|
21
|
+
|
|
22
|
+
It is published as TypeScript source (no build step) to npm and JSR, alongside
|
|
23
|
+
the CSS bundle and an HTML scaffold. Consumers who want a different UI should
|
|
24
|
+
depend on the engine directly and skip this package.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install @cplieger/web-terminal-ui @cplieger/web-terminal-engine
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`@cplieger/web-terminal-engine` is a peer dependency — the UI is built on the engine,
|
|
33
|
+
and both halves of the wire protocol must stay in lockstep, so the consumer
|
|
34
|
+
pins the engine version explicitly.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
Serve the scaffold (`scaffold/index.html` — copy and adapt it) and the bundled
|
|
39
|
+
CSS (`css/` concatenated per `css/MANIFEST` into the `style.css` the scaffold
|
|
40
|
+
links), then call `mount()` from your entry module:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { mount } from "@cplieger/web-terminal-ui";
|
|
44
|
+
|
|
45
|
+
mount();
|
|
46
|
+
// or, for a server that exposes the WebSocket elsewhere / a custom font:
|
|
47
|
+
mount({ wsPath: "/api/shell/ws", fontReady: '14px "MyMono"' });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`mount()` expects the scaffold's element ids to exist in the DOM (`term`,
|
|
51
|
+
`term-output`, `term-input`, `composition-view`, `pred-cursor`, `loading`,
|
|
52
|
+
`conn-banner`, `key-toolbar`, the `kb-*` buttons, `scroll-bottom`, `ctx-menu`)
|
|
53
|
+
and throws a clear error if a required one is missing. Call it exactly once —
|
|
54
|
+
the UI is single-instance per page.
|
|
55
|
+
|
|
56
|
+
### Options
|
|
57
|
+
|
|
58
|
+
| Option | Default | Purpose |
|
|
59
|
+
| ----------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
60
|
+
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
|
|
61
|
+
| `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. |
|
|
62
|
+
|
|
63
|
+
`mount()` returns a small handle: `{ focus() }` re-focuses the terminal input
|
|
64
|
+
(and opens the soft keyboard on touch).
|
|
65
|
+
|
|
66
|
+
## What ships
|
|
67
|
+
|
|
68
|
+
| Path | Purpose |
|
|
69
|
+
| ------------------------ | ----------------------------------------------------------------- |
|
|
70
|
+
| `src/*.ts` | The UI modules (`mount`, IME, predictive echo, viewport, status). |
|
|
71
|
+
| `css/*.css` + `MANIFEST` | The default theme + layout; concatenate in MANIFEST order. |
|
|
72
|
+
| `scaffold/index.html` | A reference HTML scaffold with the required element ids. |
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
GPL-3.0-or-later. See `LICENSE`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* ===== Tokens ===== */
|
|
2
|
+
:root {
|
|
3
|
+
interpolate-size: allow-keywords;
|
|
4
|
+
|
|
5
|
+
--bg: oklch(0% 0 0deg);
|
|
6
|
+
--surface: oklch(16% 0.005 280deg / 70%);
|
|
7
|
+
--border: oklch(100% 0 0deg / 15%);
|
|
8
|
+
--text: oklch(90% 0.005 280deg);
|
|
9
|
+
--text-active: color-mix(in oklch, var(--text) 15%, transparent);
|
|
10
|
+
--radius: 0.75rem;
|
|
11
|
+
--radius-inner: 0.5rem;
|
|
12
|
+
--touch-target: 2.75rem; /* 44px — Apple HIG minimum */
|
|
13
|
+
--pill-outer: calc(
|
|
14
|
+
var(--touch-target) + 2 * var(--sp-1) + 2px
|
|
15
|
+
); /* touch-target + 2×padding + 2×border */
|
|
16
|
+
|
|
17
|
+
--icon: 1.125rem; /* 18px */
|
|
18
|
+
--icon-desktop: 1rem; /* 16px */
|
|
19
|
+
--sp-1: 0.25rem;
|
|
20
|
+
--sp-2: 0.5rem;
|
|
21
|
+
--sp-3: 0.75rem;
|
|
22
|
+
--font-ui: -apple-system, blinkmacsystemfont, "SF Pro Text", system-ui, sans-serif;
|
|
23
|
+
--font-mono: "MonaspiceNe NFM", monospace;
|
|
24
|
+
--dur-micro: 0.1s;
|
|
25
|
+
--dur-standard: 0.2s;
|
|
26
|
+
--dur-enter: 0.25s;
|
|
27
|
+
--dur-exit: 0.15s;
|
|
28
|
+
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
|
|
29
|
+
--ease-enter: cubic-bezier(0, 0, 0, 1);
|
|
30
|
+
--ease-exit: cubic-bezier(0.4, 0, 1, 1);
|
|
31
|
+
--blur: blur(8px);
|
|
32
|
+
--safe-top: env(safe-area-inset-top, 0px);
|
|
33
|
+
}
|
package/css/01-reset.css
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* ===== Reset ===== */
|
|
2
|
+
*,
|
|
3
|
+
*::before,
|
|
4
|
+
*::after {
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
html,
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
height: 100%;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
background: var(--bg);
|
|
15
|
+
color-scheme: dark;
|
|
16
|
+
-webkit-font-smoothing: antialiased;
|
|
17
|
+
text-size-adjust: 100%;
|
|
18
|
+
overscroll-behavior: none;
|
|
19
|
+
/* Disable pinch-to-zoom and double-tap-to-zoom. The viewport meta tag
|
|
20
|
+
blocks browser zoom; touch-action: pan-x pan-y additionally lets only
|
|
21
|
+
scroll/pan gestures through, blocking the pinch-zoom and double-tap-zoom
|
|
22
|
+
gestures iOS Safari otherwise still honors. */
|
|
23
|
+
touch-action: pan-x pan-y;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* stylelint-disable declaration-no-important, time-min-milliseconds --
|
|
27
|
+
Canonical prefers-reduced-motion override. !important and near-zero
|
|
28
|
+
durations are required to forcibly defeat any animation/transition rule
|
|
29
|
+
regardless of specificity, which is the whole point of this a11y reset. */
|
|
30
|
+
@media (prefers-reduced-motion: reduce) {
|
|
31
|
+
*,
|
|
32
|
+
*::before,
|
|
33
|
+
*::after {
|
|
34
|
+
animation-duration: 0.01ms !important;
|
|
35
|
+
animation-iteration-count: 1 !important;
|
|
36
|
+
transition-duration: 0.01ms !important;
|
|
37
|
+
scroll-behavior: auto !important;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/* stylelint-enable declaration-no-important, time-min-milliseconds */
|
|
41
|
+
|
|
42
|
+
/* ===== Fonts ===== */
|
|
43
|
+
@font-face {
|
|
44
|
+
font-family: "MonaspiceNe NFM";
|
|
45
|
+
src: url("/vendor/fonts/MonaspiceNeNerdFontMono-Regular.otf") format("opentype");
|
|
46
|
+
font-weight: 400;
|
|
47
|
+
font-style: normal;
|
|
48
|
+
font-display: block;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@font-face {
|
|
52
|
+
font-family: "MonaspiceNe NFM";
|
|
53
|
+
src: url("/vendor/fonts/MonaspiceNeNerdFontMono-Bold.otf") format("opentype");
|
|
54
|
+
font-weight: 700;
|
|
55
|
+
font-style: normal;
|
|
56
|
+
font-display: block;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@font-face {
|
|
60
|
+
font-family: "MonaspiceNe NFM";
|
|
61
|
+
src: url("/vendor/fonts/MonaspiceNeNerdFontMono-Italic.otf") format("opentype");
|
|
62
|
+
font-weight: 400;
|
|
63
|
+
font-style: italic;
|
|
64
|
+
font-display: block;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@font-face {
|
|
68
|
+
font-family: "MonaspiceNe NFM";
|
|
69
|
+
src: url("/vendor/fonts/MonaspiceNeNerdFontMono-BoldItalic.otf") format("opentype");
|
|
70
|
+
font-weight: 700;
|
|
71
|
+
font-style: italic;
|
|
72
|
+
font-display: block;
|
|
73
|
+
}
|