@cplieger/web-terminal-ui 2.1.4 → 3.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 +57 -32
- package/css/03-v3.css +590 -0
- package/css/MANIFEST +1 -0
- package/package.json +9 -5
- package/scaffold/index.html +8 -6
- package/src/composition.ts +15 -0
- package/src/features/activity-monitor.ts +64 -0
- package/src/features/animations.ts +29 -0
- package/src/features/clipboard.ts +98 -0
- package/src/features/connection-banner.ts +53 -0
- package/src/features/context-menu.ts +186 -0
- package/src/features/dom.ts +15 -0
- package/src/features/mobile-toolbar.ts +82 -0
- package/src/features/predictive-echo.ts +63 -0
- package/src/features/scroll-to-bottom.ts +49 -0
- package/src/features/tabs.ts +687 -0
- package/src/index.ts +30 -9
- package/src/kernel/a11y.ts +102 -0
- package/src/kernel/bus.ts +61 -0
- package/src/kernel/conn-state.ts +133 -0
- package/src/kernel/kernel.ts +688 -0
- package/src/kernel/regions.ts +119 -0
- package/src/kernel/types.ts +286 -0
- package/src/presets.ts +46 -0
- package/src/mount.ts +0 -903
- package/src/status.ts +0 -228
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Layout regions (design section 22.13).
|
|
2
|
+
//
|
|
3
|
+
// The kernel owns a small layout skeleton: named regions with fixed geometry,
|
|
4
|
+
// stacking, and keyboard-inset behavior. Features contribute chrome into a
|
|
5
|
+
// region via ctx.region(name, slot); the region owns position, spacing, stack
|
|
6
|
+
// direction, z-order, and the keyboard lift, so features cannot invent their own
|
|
7
|
+
// placement or collide. DOM order within a region always equals visual order, so
|
|
8
|
+
// focus order never diverges from reading order (WCAG 2.4.3): each slot is a
|
|
9
|
+
// wrapper inserted in a declared order, and a feature appends into its slot.
|
|
10
|
+
|
|
11
|
+
import type { RegionName, RegionSlot } from "./types.js";
|
|
12
|
+
|
|
13
|
+
/** The regions the kernel builds, in DOM order. */
|
|
14
|
+
const REGION_NAMES: readonly RegionName[] = [
|
|
15
|
+
"top-bar",
|
|
16
|
+
"banner",
|
|
17
|
+
"bottom-inset-end",
|
|
18
|
+
"bottom-switcher",
|
|
19
|
+
"overlay",
|
|
20
|
+
"sheet",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/** Declared slot order per region. A slot not listed here sorts after the known
|
|
24
|
+
* ones, in first-request order, so an unanticipated slot still composes
|
|
25
|
+
* predictably rather than throwing. */
|
|
26
|
+
const SLOT_ORDER: Record<RegionName, readonly RegionSlot[]> = {
|
|
27
|
+
"top-bar": ["tabs"],
|
|
28
|
+
banner: ["status"],
|
|
29
|
+
"bottom-inset-end": ["keys", "scroll"],
|
|
30
|
+
"bottom-switcher": ["switcher"],
|
|
31
|
+
overlay: ["menu"],
|
|
32
|
+
sheet: ["overview"],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const DEFAULT_SLOT = "default";
|
|
36
|
+
|
|
37
|
+
export interface Regions {
|
|
38
|
+
/** Return the live slot element within a region for a feature to append into.
|
|
39
|
+
* Created once per (region, slot) and inserted in the region's declared slot
|
|
40
|
+
* order. */
|
|
41
|
+
region(name: RegionName, slot?: RegionSlot): HTMLElement;
|
|
42
|
+
/** Remove all region containers from the root. */
|
|
43
|
+
destroy(): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** slotRank returns a sort key for a slot within a region: its index in the
|
|
47
|
+
* declared order, or a large base offset plus a per-region monotonic counter
|
|
48
|
+
* for unlisted slots (stable first-request order). */
|
|
49
|
+
function slotRank(name: RegionName, slot: RegionSlot, seen: Map<string, number>): number {
|
|
50
|
+
const order = SLOT_ORDER[name];
|
|
51
|
+
const idx = order.indexOf(slot);
|
|
52
|
+
if (idx >= 0) {
|
|
53
|
+
return idx;
|
|
54
|
+
}
|
|
55
|
+
const key = `${name}:${slot}`;
|
|
56
|
+
let rank = seen.get(key);
|
|
57
|
+
if (rank === undefined) {
|
|
58
|
+
rank = 1000 + seen.size;
|
|
59
|
+
seen.set(key, rank);
|
|
60
|
+
}
|
|
61
|
+
return rank;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Build the region skeleton inside root and return a Regions accessor. */
|
|
65
|
+
export function createRegions(root: HTMLElement): Regions {
|
|
66
|
+
const containers = new Map<RegionName, HTMLElement>();
|
|
67
|
+
const slots = new Map<string, HTMLElement>(); // "region:slot" -> slot element
|
|
68
|
+
const unlistedSeen = new Map<string, number>();
|
|
69
|
+
|
|
70
|
+
for (const name of REGION_NAMES) {
|
|
71
|
+
const el = document.createElement("div");
|
|
72
|
+
el.className = `wt-region wt-region-${name}`;
|
|
73
|
+
el.dataset["region"] = name;
|
|
74
|
+
root.appendChild(el);
|
|
75
|
+
containers.set(name, el);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function region(name: RegionName, slot: RegionSlot = DEFAULT_SLOT): HTMLElement {
|
|
79
|
+
const key = `${name}:${slot}`;
|
|
80
|
+
const existing = slots.get(key);
|
|
81
|
+
if (existing) {
|
|
82
|
+
return existing;
|
|
83
|
+
}
|
|
84
|
+
const container = containers.get(name);
|
|
85
|
+
if (!container) {
|
|
86
|
+
throw new Error(`web-terminal-ui: unknown region ${name}`);
|
|
87
|
+
}
|
|
88
|
+
const el = document.createElement("div");
|
|
89
|
+
el.className = `wt-slot wt-slot-${slot}`;
|
|
90
|
+
el.dataset["slot"] = slot;
|
|
91
|
+
// Insert in declared slot order: before the first existing child whose rank
|
|
92
|
+
// is greater than ours.
|
|
93
|
+
const myRank = slotRank(name, slot, unlistedSeen);
|
|
94
|
+
let inserted = false;
|
|
95
|
+
for (const child of Array.from(container.children)) {
|
|
96
|
+
const childSlot = (child as HTMLElement).dataset["slot"] ?? DEFAULT_SLOT;
|
|
97
|
+
if (slotRank(name, childSlot, unlistedSeen) > myRank) {
|
|
98
|
+
container.insertBefore(el, child);
|
|
99
|
+
inserted = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!inserted) {
|
|
104
|
+
container.appendChild(el);
|
|
105
|
+
}
|
|
106
|
+
slots.set(key, el);
|
|
107
|
+
return el;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function destroy(): void {
|
|
111
|
+
for (const el of containers.values()) {
|
|
112
|
+
el.remove();
|
|
113
|
+
}
|
|
114
|
+
containers.clear();
|
|
115
|
+
slots.clear();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { region, destroy };
|
|
119
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// The v3 kernel/feature contract (design section 22.4).
|
|
2
|
+
//
|
|
3
|
+
// A terminal is a small always-present kernel plus opt-in feature modules. This
|
|
4
|
+
// module is the typed spine both sides hang off: the feature interface, the
|
|
5
|
+
// context the kernel hands each feature, the typed event bus payloads, and the
|
|
6
|
+
// layout-region vocabulary. It is fully typed (no `any`, no stringly-typed
|
|
7
|
+
// capability lookup) so a feature's public API is held by reference and a peer
|
|
8
|
+
// reads it through a typed token.
|
|
9
|
+
|
|
10
|
+
import type { ScreenMessage, ModesMessage, LineStore } from "@cplieger/web-terminal-engine";
|
|
11
|
+
|
|
12
|
+
/** Cancels a subscription or registration. Idempotent by convention. */
|
|
13
|
+
export type Unsubscribe = () => void;
|
|
14
|
+
|
|
15
|
+
// --- Layout regions (section 22.13) ---
|
|
16
|
+
|
|
17
|
+
/** The named layout regions the kernel owns. A feature mounts its chrome into
|
|
18
|
+
* one of these; the region owns position, spacing, stack direction, z-order,
|
|
19
|
+
* and the keyboard-inset lift. */
|
|
20
|
+
export type RegionName =
|
|
21
|
+
| "top-bar" // desktop tab strip
|
|
22
|
+
| "bottom-inset-end" // thumb-zone control stack (keys, scroll-to-bottom), lifts above the keyboard
|
|
23
|
+
| "bottom-switcher" // mobile tab switcher, lifted above the keyboard
|
|
24
|
+
| "overlay" // viewport-clamped non-modal popovers (context menu)
|
|
25
|
+
| "sheet" // modal bottom sheet (tab overview): focus-trapped, scrim
|
|
26
|
+
| "banner"; // connection status + toasts, one shared stacking context
|
|
27
|
+
|
|
28
|
+
/** A named ordering slot within a region, so features compose in a defined
|
|
29
|
+
* order instead of guessing z-indices. DOM order within a region always equals
|
|
30
|
+
* visual order (WCAG 2.4.3). */
|
|
31
|
+
export type RegionSlot = string;
|
|
32
|
+
|
|
33
|
+
// --- Session references ---
|
|
34
|
+
|
|
35
|
+
/** A reference to a session, carried on switch events and onSwitch. */
|
|
36
|
+
export interface SessionRef {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Read-only view of the active session for features (active id, size, resume
|
|
41
|
+
* bounds). Features never touch the raw connection/outbox/resume layer. */
|
|
42
|
+
export interface SessionView {
|
|
43
|
+
/** Active session id, or null in the unmanaged single-terminal case. */
|
|
44
|
+
readonly id: string | null;
|
|
45
|
+
/** Current terminal size in cells. */
|
|
46
|
+
size(): { cols: number; rows: number };
|
|
47
|
+
/** Highest absolute line index the active store holds (-1 if empty). */
|
|
48
|
+
highestIndex(): number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- Connection state (mirrors the kernel's connection-state machine) ---
|
|
52
|
+
|
|
53
|
+
/** Connection state the kernel owns and broadcasts on `connection:state`. */
|
|
54
|
+
export type ConnState = "open" | "connecting" | "reconnecting" | "offline" | "restarted";
|
|
55
|
+
|
|
56
|
+
// --- Engine drive handles ---
|
|
57
|
+
//
|
|
58
|
+
// Features drive the engine's render and scroll surfaces through these handles:
|
|
59
|
+
// the subset of the engine's render/scroll modules features legitimately need.
|
|
60
|
+
// The kernel assigns the real engine namespaces here, so TS verifies they
|
|
61
|
+
// satisfy the subset (drift is caught at the kernel). The connection layer is
|
|
62
|
+
// deliberately NOT exposed: features send only through ctx.send / ctx.paste (the
|
|
63
|
+
// sanitizing funnel) and never touch the raw outbox/resume layer or the binary
|
|
64
|
+
// frame format (section 22.9).
|
|
65
|
+
|
|
66
|
+
/** The render methods features may drive. */
|
|
67
|
+
export interface RenderHandle {
|
|
68
|
+
setPredictedCursor(row: number, col: number, active: boolean): void;
|
|
69
|
+
getCursorPx(): { left: number; top: number; cellH: number };
|
|
70
|
+
computeSize(): { cols: number; rows: number };
|
|
71
|
+
/** Point the renderer at a store and rebuild from it (tabs, on switch). */
|
|
72
|
+
bind(store: LineStore): void;
|
|
73
|
+
/** The store the renderer is currently bound to. */
|
|
74
|
+
boundStore(): LineStore;
|
|
75
|
+
/** Highest absolute line index the active store holds (-1 if empty). */
|
|
76
|
+
getHighestIndex(): number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The scroll methods features may drive. */
|
|
80
|
+
export interface ScrollHandle {
|
|
81
|
+
scrollToBottom(): void;
|
|
82
|
+
isUserScrolledUp(): boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- Typed event bus payloads (section 22.4) ---
|
|
86
|
+
|
|
87
|
+
/** The inbound-wire + lifecycle events features can subscribe to via ctx.on.
|
|
88
|
+
* No `unknown`: each event carries a typed payload. */
|
|
89
|
+
export interface TerminalEvents {
|
|
90
|
+
/** A tab switch completed; the payload is the newly-active session. */
|
|
91
|
+
"session:switch": SessionRef;
|
|
92
|
+
/** The kernel's connection state changed. */
|
|
93
|
+
"connection:state": ConnState;
|
|
94
|
+
/** A session's process exited (from the status SSE; reaches background tabs). */
|
|
95
|
+
"session:exited": { session: string };
|
|
96
|
+
/** An OSC title change for a session. */
|
|
97
|
+
"wire:title": { session: string; title: string };
|
|
98
|
+
/** Inbound OSC 52 clipboard content. */
|
|
99
|
+
"wire:clipboard": string;
|
|
100
|
+
/** A modes frame (mouse-mode gating, reverse video). */
|
|
101
|
+
"wire:modes": ModesMessage;
|
|
102
|
+
/** A screen frame (first-paint / activity hooks). */
|
|
103
|
+
"wire:screen": ScreenMessage;
|
|
104
|
+
/** The layout breakpoint changed (desktop <-> mobile), so a feature re-homes
|
|
105
|
+
* its chrome across regions (section 22.13). */
|
|
106
|
+
"layout:change": { mobile: boolean };
|
|
107
|
+
/** The user scrolled away from / back to the bottom (drives the
|
|
108
|
+
* scroll-to-bottom affordance). */
|
|
109
|
+
"scroll:state": { scrolledUp: boolean };
|
|
110
|
+
/** The rendered cursor position settled after a flush, so an overlay (the
|
|
111
|
+
* predicted cursor) can re-position against fresh row geometry. Payload-less. */
|
|
112
|
+
"render:cursor": undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Accessibility primitives (kernel-owned, single source) ---
|
|
116
|
+
|
|
117
|
+
/** Controls the ARIA tablist/tabpanel seam on the kernel's output surface. The
|
|
118
|
+
* kernel owns the tabpanel (its output surface); `tabs` registers each tab
|
|
119
|
+
* button through this so it never crosses into kernel-owned DOM. */
|
|
120
|
+
export interface TablistController {
|
|
121
|
+
/** The output surface's element id, for a tab's aria-controls. */
|
|
122
|
+
panelId(): string;
|
|
123
|
+
/** Register a tab button: sets role=tab + aria-controls on it and returns a
|
|
124
|
+
* handle to update its selected state and label, or remove it. */
|
|
125
|
+
registerTab(tab: HTMLElement): TabHandle;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Handle to one registered tab's ARIA state. */
|
|
129
|
+
export interface TabHandle {
|
|
130
|
+
/** Mark this tab selected; the kernel points the panel's aria-labelledby at it. */
|
|
131
|
+
setSelected(selected: boolean): void;
|
|
132
|
+
/** Set the accessible label (used for aria-labelledby on the panel). */
|
|
133
|
+
setLabel(text: string): void;
|
|
134
|
+
/** Deregister this tab. */
|
|
135
|
+
remove(): void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- The context handed to each feature's setup ---
|
|
139
|
+
|
|
140
|
+
/** Everything a feature is given at setup. The only surface features use to
|
|
141
|
+
* affect the terminal; it closes the round-3 holes (typed APIs by reference,
|
|
142
|
+
* ctx.use tokens, a typed bus, a defined lifecycle, kernel-owned a11y). */
|
|
143
|
+
export interface TerminalContext {
|
|
144
|
+
/** Mount chrome into a named kernel region; DOM order in a region equals
|
|
145
|
+
* visual order. Returns a live element the feature appends into. */
|
|
146
|
+
region(name: RegionName, slot?: RegionSlot): HTMLElement;
|
|
147
|
+
/** The terminal scroll surface, for features that attach surface-level
|
|
148
|
+
* gestures (contextMenu right-click / long-press, the tabs swipe) or scope a
|
|
149
|
+
* selection to the output. Read-only use; features own only their region
|
|
150
|
+
* chrome (section 22.9, single-operator trust). */
|
|
151
|
+
surface(): HTMLElement;
|
|
152
|
+
|
|
153
|
+
/** The single sanitizing, session-routed input path: bracket, strip control
|
|
154
|
+
* bytes, normalize NBSP, apply the col-0 backspace brake. Features never
|
|
155
|
+
* touch the raw socket. */
|
|
156
|
+
send(bytes: Uint8Array): void;
|
|
157
|
+
/** Paste text through the sanitizing funnel (bracketed-paste + newline
|
|
158
|
+
* normalization). */
|
|
159
|
+
paste(text: string): void;
|
|
160
|
+
/** Register an outbound-byte transform (e.g. mobileToolbar sticky-Ctrl). The
|
|
161
|
+
* kernel composes transforms in registration order around send; a transform
|
|
162
|
+
* returning an empty array drops the input (e.g. the col-0 backspace brake). */
|
|
163
|
+
registerInputTransform(fn: (bytes: Uint8Array) => Uint8Array): Unsubscribe;
|
|
164
|
+
/** Observe accepted outbound bytes (e.g. predictiveEcho advances its cursor). */
|
|
165
|
+
registerInputObserver(fn: (bytes: Uint8Array) => void): Unsubscribe;
|
|
166
|
+
/** Register a keydown handler that runs before the kernel's default key
|
|
167
|
+
* mapping. Return true to consume the event (the handler owns
|
|
168
|
+
* preventDefault); the kernel then sends nothing for it. Used by clipboard
|
|
169
|
+
* (Ctrl+Shift+C/V) and contextMenu (Escape-to-close). Runs in registration
|
|
170
|
+
* order; the first to return true wins. */
|
|
171
|
+
registerKeydown(fn: (ev: KeyboardEvent) => boolean): Unsubscribe;
|
|
172
|
+
|
|
173
|
+
/** Drive the engine renderer (never the raw resume layer). */
|
|
174
|
+
readonly render: RenderHandle;
|
|
175
|
+
/** Drive the engine scroll controller. */
|
|
176
|
+
readonly scroll: ScrollHandle;
|
|
177
|
+
/** Read-only active-session view. */
|
|
178
|
+
readonly session: SessionView;
|
|
179
|
+
|
|
180
|
+
/** Subscribe to a typed terminal event. Auto-disposed on destroy. */
|
|
181
|
+
on<K extends keyof TerminalEvents>(e: K, fn: (p: TerminalEvents[K]) => void): Unsubscribe;
|
|
182
|
+
|
|
183
|
+
/** Look up a peer feature's typed API by its factory value, or undefined if
|
|
184
|
+
* that feature is absent or not yet set up. Read lazily (at interaction
|
|
185
|
+
* time) so ordering within the feature list does not matter at runtime. */
|
|
186
|
+
use<A>(feature: TerminalFeature<A>): A | undefined;
|
|
187
|
+
|
|
188
|
+
/** Show a transient toast on the kernel-owned toast surface (a shared
|
|
189
|
+
* primitive, section 22.3), so a feature signals "Copied" etc. without
|
|
190
|
+
* owning its own surface and without needing connectionBanner present. */
|
|
191
|
+
toast(message: string, ms?: number): void;
|
|
192
|
+
/** Announce a message on the single kernel-owned polite (or assertive) live
|
|
193
|
+
* region, so features do not spawn competing aria-live regions. */
|
|
194
|
+
announce(message: string, politeness?: "polite" | "assertive"): void;
|
|
195
|
+
/** The kernel's tablist/tabpanel ARIA controller (used by tabs). */
|
|
196
|
+
tablist(): TablistController;
|
|
197
|
+
|
|
198
|
+
/** Switch the live terminal to a session. tabs binds the renderer to the
|
|
199
|
+
* session's cached store (ctx.render.bind) first, then calls this; the kernel
|
|
200
|
+
* reconnects the terminal WS to that session (connection.setSession, using
|
|
201
|
+
* its per-tab resume state), invokes every feature's onSwitch (ordered,
|
|
202
|
+
* before input resumes), and emits session:switch for pure observers
|
|
203
|
+
* (sections 5, 22.4). Updates the active session the SessionView reports.
|
|
204
|
+
* This is how tabs drives the reconnect-on-switch swap without touching the
|
|
205
|
+
* raw connection layer (section 22.9). */
|
|
206
|
+
notifySwitch(session: SessionRef): void;
|
|
207
|
+
/** Drop a session's per-tab resume state (on tab close), so its outbox and
|
|
208
|
+
* byte counters are released. The kernel routes this to the connection
|
|
209
|
+
* layer; features never touch it directly. */
|
|
210
|
+
dropSession(id: string): void;
|
|
211
|
+
|
|
212
|
+
/** Observe feature errors (a feature's runtime callback threw). */
|
|
213
|
+
onError(fn: (feature: string, err: unknown) => void): Unsubscribe;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- The feature interface ---
|
|
217
|
+
|
|
218
|
+
/** A feature is a factory value implementing this: a name and a setup the
|
|
219
|
+
* kernel runs once. `Api` is the feature's own typed public API (void when it
|
|
220
|
+
* exposes none). */
|
|
221
|
+
export interface TerminalFeature<Api = void> {
|
|
222
|
+
readonly name: string;
|
|
223
|
+
setup(ctx: TerminalContext): FeatureInstance<Api> | Promise<FeatureInstance<Api>>;
|
|
224
|
+
/** Populated by the kernel after setup with the instance's api, so a consumer
|
|
225
|
+
* holding the feature value can read it (e.g. `tabs.api?.create()`). Read
|
|
226
|
+
* it lazily; it is undefined until the kernel has run this feature's setup.
|
|
227
|
+
* Readonly so a feature is covariant in Api (a TerminalFeature<X> is a
|
|
228
|
+
* TerminalFeature<unknown>); the kernel sets it through a narrow cast. */
|
|
229
|
+
readonly api?: Api;
|
|
230
|
+
/** True if this feature owns session selection and drives the first
|
|
231
|
+
* connection itself (via ctx.notifySwitch once it has resolved a session id).
|
|
232
|
+
* The kernel reads this synchronously from the feature list and, when any
|
|
233
|
+
* feature sets it, SKIPS its own startup connect: connecting to the bare
|
|
234
|
+
* wsPath before a session exists would hit a session-gated /ws (the
|
|
235
|
+
* SessionManager 404s a /ws with no ?session=) and churn the reconnect
|
|
236
|
+
* backoff until the feature set a session. `tabs` sets this; single-terminal
|
|
237
|
+
* presets leave it unset so the kernel connects to the bare wsPath as before. */
|
|
238
|
+
readonly managesSessions?: boolean;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** What a feature's setup returns: its optional typed API, a teardown, and an
|
|
242
|
+
* optional onSwitch the kernel invokes on a tab switch. */
|
|
243
|
+
export interface FeatureInstance<Api = void> {
|
|
244
|
+
/** This feature's public API, surfaced on the feature value and via ctx.use. */
|
|
245
|
+
readonly api?: Api;
|
|
246
|
+
/** Remove this feature's DOM and listeners. Run in reverse order on destroy. */
|
|
247
|
+
teardown(): void;
|
|
248
|
+
/** Called by the kernel at the START of a tab switch (detach), before the
|
|
249
|
+
* connection is re-pointed at the incoming session, for features holding
|
|
250
|
+
* latched input state that must not fire against the next session: the mobile
|
|
251
|
+
* toolbar disarms its one-shot sticky-Ctrl here so a pending Ctrl cannot
|
|
252
|
+
* become an accidental Ctrl+C to the wrong agent (design 5.1). Runs for every
|
|
253
|
+
* feature before any onSwitch. */
|
|
254
|
+
onDetach?(): void;
|
|
255
|
+
/** Called by the kernel on a tab switch (attach), after the connection is
|
|
256
|
+
* re-pointed, for features that must re-point before input resumes
|
|
257
|
+
* (session:switch on the bus is for pure observers). */
|
|
258
|
+
onSwitch?(session: SessionRef): void;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// --- Entry point ---
|
|
262
|
+
|
|
263
|
+
/** Options for createTerminal. */
|
|
264
|
+
export interface CreateTerminalOptions {
|
|
265
|
+
/** The feature list; omitted or empty means the bare kernel (no chrome).
|
|
266
|
+
* Heterogeneous feature APIs are held as unknown here; a consumer reads a
|
|
267
|
+
* specific feature's api off the feature value it holds. */
|
|
268
|
+
features?: readonly TerminalFeature<unknown>[];
|
|
269
|
+
/** WebSocket endpoint path (default "/ws"). */
|
|
270
|
+
wsPath?: string;
|
|
271
|
+
/** CSS font shorthand awaited before the first resize. */
|
|
272
|
+
fontReady?: string;
|
|
273
|
+
/** Optional pre-JS loading overlay the kernel fades out on first paint. */
|
|
274
|
+
loading?: HTMLElement;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** The handle createTerminal returns. Feature APIs are not materialized here
|
|
278
|
+
* (that could not be typed soundly); a consumer holds the feature value and
|
|
279
|
+
* reads its `api`. */
|
|
280
|
+
export interface TerminalHandle {
|
|
281
|
+
/** Focus the terminal input (opens the soft keyboard on touch). */
|
|
282
|
+
focus(): void;
|
|
283
|
+
/** Tear down every feature in reverse order, dispose all subscriptions, and
|
|
284
|
+
* release the kernel's DOM and engine wiring. */
|
|
285
|
+
destroy(): void;
|
|
286
|
+
}
|
package/src/presets.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
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.
|
|
6
|
+
//
|
|
7
|
+
// presetTabbed (tabs + activityMonitor) is added once those features land.
|
|
8
|
+
|
|
9
|
+
import type { TerminalFeature } from "./kernel/types.js";
|
|
10
|
+
import { clipboard } from "./features/clipboard.js";
|
|
11
|
+
import { contextMenu } from "./features/context-menu.js";
|
|
12
|
+
import { scrollToBottom } from "./features/scroll-to-bottom.js";
|
|
13
|
+
import { predictiveEcho } from "./features/predictive-echo.js";
|
|
14
|
+
import { connectionBanner } from "./features/connection-banner.js";
|
|
15
|
+
import { mobileToolbar } from "./features/mobile-toolbar.js";
|
|
16
|
+
import { tabs } from "./features/tabs.js";
|
|
17
|
+
import { activityMonitor } from "./features/activity-monitor.js";
|
|
18
|
+
import { animations } from "./features/animations.js";
|
|
19
|
+
|
|
20
|
+
/** Single-pane desktop UI: context menu, clipboard, scroll-to-bottom, predictive
|
|
21
|
+
* echo, and the connection banner. No mobile toolbar, no tabs. */
|
|
22
|
+
export function presetSingle(): TerminalFeature<unknown>[] {
|
|
23
|
+
// contextMenu offers Copy/Paste through the clipboard feature (ctx.use), so it
|
|
24
|
+
// holds the same clipboard value the array includes.
|
|
25
|
+
const clip = clipboard();
|
|
26
|
+
return [
|
|
27
|
+
contextMenu({ clipboard: clip }),
|
|
28
|
+
clip,
|
|
29
|
+
scrollToBottom(),
|
|
30
|
+
predictiveEcho(),
|
|
31
|
+
connectionBanner(),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Touch-first UI: presetSingle plus the on-screen key toolbar. */
|
|
36
|
+
export function presetTouch(): TerminalFeature<unknown>[] {
|
|
37
|
+
return [...presetSingle(), mobileToolbar()];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Full reference UI: presetTouch plus tabs, the activity monitor, and
|
|
41
|
+
* animations. activityMonitor is ordered before tabs because tabs reads its
|
|
42
|
+
* API via ctx.use during setup (to wire status dots). */
|
|
43
|
+
export function presetTabbed(): TerminalFeature<unknown>[] {
|
|
44
|
+
const monitor = activityMonitor();
|
|
45
|
+
return [...presetTouch(), monitor, tabs({ activityMonitor: monitor }), animations()];
|
|
46
|
+
}
|