@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/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
+ }
@@ -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
+ }