@cplieger/web-terminal-ui 3.5.0 → 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.
- package/README.md +52 -23
- package/css/00-tokens.css +8 -2
- package/css/01-scope.css +56 -0
- package/css/{02-app.css → 02-terminal.css} +64 -239
- package/css/{03-primitives.css → 10-primitives.css} +21 -19
- package/css/20-connection-banner.css +42 -0
- package/css/21-scroll-to-bottom.css +65 -0
- package/css/22-context-menu.css +45 -0
- package/css/23-toolbar.css +181 -0
- package/css/{05-tabs.css → 30-tabs.css} +96 -57
- package/css/{06-mobile.css → 31-switcher.css} +66 -81
- package/css/{07-animations.css → 40-animations.css} +15 -15
- package/css/MANIFEST +19 -11
- package/css/MANIFEST.single +12 -0
- package/css/MANIFEST.tabbed +16 -0
- package/css/MANIFEST.touch +12 -0
- package/css/{01-reset.css → page.css} +18 -19
- package/package.json +18 -5
- package/scaffold/index.html +5 -0
- package/src/features/context-menu.ts +6 -33
- package/src/features/menu-position.ts +50 -0
- package/src/features/{tabs.ts → tabs/index.ts} +204 -655
- package/src/features/tabs/model.ts +334 -0
- package/src/features/tabs/strip.ts +101 -0
- package/src/features/tabs/switcher.ts +129 -0
- package/src/index.ts +1 -0
- package/src/kernel/a11y.ts +30 -5
- package/src/kernel/kernel.ts +149 -59
- package/src/kernel/types.ts +55 -14
- package/src/presets/agent-tabbed.ts +15 -0
- package/src/presets/single.ts +27 -0
- package/src/presets/tabbed.ts +42 -0
- package/src/presets/touch.ts +13 -0
- package/src/presets.ts +12 -76
- package/src/viewport.ts +28 -15
- package/css/04-feedback.css +0 -180
package/README.md
CHANGED
|
@@ -49,11 +49,26 @@ pins the engine version explicitly.
|
|
|
49
49
|
|
|
50
50
|
## Usage
|
|
51
51
|
|
|
52
|
-
Serve
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
Serve a CSS bundle matching how you embed the terminal, plus a minimal HTML
|
|
53
|
+
page that has one empty container element, then call
|
|
54
|
+
`createTerminal(root, { features })` from your entry module.
|
|
55
|
+
|
|
56
|
+
**Full-page host** (the terminal IS the page — `web-terminal-server`,
|
|
57
|
+
`web-terminal-kiro`): concatenate `css/MANIFEST` into the `style.css` your page
|
|
58
|
+
links. That reference bundle is `css/page.css` (the page kit: `html/body`
|
|
59
|
+
reset + the terminal web font's `@font-face`, expecting the font files at
|
|
60
|
+
`/vendor/fonts/`) plus the complete component set.
|
|
61
|
+
|
|
62
|
+
**Embedder** (the terminal lives inside your app's layout — a panel, a pane):
|
|
63
|
+
concatenate the per-preset manifest matching your composition instead —
|
|
64
|
+
`css/MANIFEST.single`, `css/MANIFEST.touch`, or `css/MANIFEST.tabbed`. These
|
|
65
|
+
contain ONLY root-scoped component styles: no page reset, no fonts, no
|
|
66
|
+
document-level rules, nothing to quarantine. Pass `layout: "container"` so the
|
|
67
|
+
terminal fills (and positions its chrome against) your container element
|
|
68
|
+
instead of the viewport.
|
|
69
|
+
|
|
70
|
+
Feature bundles (presets) live at the `./presets` sub-path, with per-preset
|
|
71
|
+
entry modules beside it:
|
|
57
72
|
|
|
58
73
|
```html
|
|
59
74
|
<div id="terminal"></div>
|
|
@@ -81,14 +96,22 @@ sub-path:
|
|
|
81
96
|
|
|
82
97
|
`createTerminal(root, opts?)` builds the entire terminal subtree (the kernel
|
|
83
98
|
plus every feature's chrome) inside `root` itself — there is no element-id
|
|
84
|
-
contract for the host page to reproduce
|
|
99
|
+
contract for the host page to reproduce, and every style and CSS custom
|
|
100
|
+
property is scoped to the `wt-root` class it stamps on your element (removed
|
|
101
|
+
again by `destroy()`). Call it exactly once; the engine's
|
|
85
102
|
render/connection/scroll modules are single-instance per page (tabs multiplex
|
|
86
103
|
sessions over the one kernel). `scaffold/index.html` is a complete reference page
|
|
87
104
|
to copy and adapt.
|
|
88
105
|
|
|
89
|
-
Four presets are provided
|
|
90
|
-
|
|
91
|
-
|
|
106
|
+
Four presets are provided; each is a plain feature-array factory, so you can
|
|
107
|
+
spread and edit it. Import the barrel (`@cplieger/web-terminal-ui/presets`) for
|
|
108
|
+
convenience, or a per-preset entry module for the minimal delivered import
|
|
109
|
+
graph — `…/presets/single`, `…/presets/touch`, `…/presets/tabbed`,
|
|
110
|
+
`…/presets/agent-tabbed` (the barrel statically reaches every feature; the
|
|
111
|
+
touch entry, for example, never imports the tabs module). Individual features
|
|
112
|
+
are importable from `…/features/<name>` (`clipboard`, `context-menu`,
|
|
113
|
+
`scroll-to-bottom`, `predictive-echo`, `connection-banner`, `mobile-toolbar`,
|
|
114
|
+
`tabs`, `activity-monitor`, `animations`) for hand-picked compositions:
|
|
92
115
|
|
|
93
116
|
- `presetSingle()` — single-pane desktop UI (context menu, clipboard,
|
|
94
117
|
scroll-to-bottom, predictive echo, connection banner).
|
|
@@ -110,25 +133,31 @@ individual features instead:
|
|
|
110
133
|
|
|
111
134
|
### Options
|
|
112
135
|
|
|
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
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
136
|
+
| Option | Default | Purpose |
|
|
137
|
+
| ----------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
138
|
+
| `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. |
|
|
139
|
+
| `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). |
|
|
140
|
+
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
|
|
141
|
+
| `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. |
|
|
142
|
+
| `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. |
|
|
143
|
+
| `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
144
|
|
|
121
145
|
`createTerminal()` returns a handle: `focus()` re-focuses the terminal input
|
|
122
|
-
(and opens the soft keyboard on touch)
|
|
123
|
-
|
|
146
|
+
(and opens the soft keyboard on touch); `send(bytes)` sends bytes to the active
|
|
147
|
+
session through the kernel's sanitizing input funnel (the supported host path
|
|
148
|
+
for a "type this command" affordance); `reset()` drops the local scrollback and
|
|
149
|
+
screen without injecting keystrokes (send a redraw keystroke yourself if you
|
|
150
|
+
want one — e.g. Ctrl+L); and `destroy()` tears every feature down and releases
|
|
151
|
+
the kernel.
|
|
124
152
|
|
|
125
153
|
## What ships
|
|
126
154
|
|
|
127
|
-
| Path
|
|
128
|
-
|
|
|
129
|
-
| `src/**/*.ts`
|
|
130
|
-
| `css/*.css` + `MANIFEST`
|
|
131
|
-
| `
|
|
155
|
+
| Path | Purpose |
|
|
156
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
157
|
+
| `src/**/*.ts` | The UI modules: the kernel (`kernel/`), opt-in features (`features/`), per-preset entries (`presets/`), IME, predictive echo, viewport. |
|
|
158
|
+
| `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. |
|
|
159
|
+
| `css/page.css` | The page kit (full-page hosts only): `html/body` reset + the terminal web font's `@font-face`. |
|
|
160
|
+
| `scaffold/index.html` | A reference full-page HTML host: `<head>` + one empty root element + importmap. |
|
|
132
161
|
|
|
133
162
|
## Related projects
|
|
134
163
|
|
package/css/00-tokens.css
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
/* ===== Tokens =====
|
|
2
|
-
|
|
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);
|
package/css/01-scope.css
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
}
|
|
37
|
+
|
|
38
|
+
/* stylelint-disable declaration-no-important, time-min-milliseconds --
|
|
39
|
+
Canonical prefers-reduced-motion override. !important and near-zero
|
|
40
|
+
durations are required to forcibly defeat any animation/transition rule
|
|
41
|
+
regardless of specificity, which is the whole point of this a11y reset.
|
|
42
|
+
Scoped to the terminal subtree: the host page's own motion policy is the
|
|
43
|
+
host's business (pre-v4 this hit `*` and silently flattened the embedding
|
|
44
|
+
app's animations). */
|
|
45
|
+
@media (prefers-reduced-motion: reduce) {
|
|
46
|
+
.wt-root,
|
|
47
|
+
.wt-root *,
|
|
48
|
+
.wt-root *::before,
|
|
49
|
+
.wt-root *::after {
|
|
50
|
+
animation-duration: 0.01ms !important;
|
|
51
|
+
animation-iteration-count: 1 !important;
|
|
52
|
+
transition-duration: 0.01ms !important;
|
|
53
|
+
scroll-behavior: auto !important;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/* stylelint-enable declaration-no-important, time-min-milliseconds */
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/* ===== Terminal ===== */
|
|
2
|
-
.term {
|
|
3
|
-
|
|
2
|
+
:where(.wt-root) .term {
|
|
3
|
+
/* Absolute against .wt-root (the positioning boundary in BOTH layout modes):
|
|
4
|
+
in wt-viewport the root is a fixed full-viewport box, so this is
|
|
5
|
+
geometrically identical to the pre-v4 `position: fixed`; in wt-container it
|
|
6
|
+
fills the embedder's panel. viewport.ts still pins top/bottom inline to
|
|
7
|
+
track the visual viewport around the soft keyboard. */
|
|
8
|
+
position: absolute;
|
|
4
9
|
inset: 0;
|
|
5
10
|
overflow: hidden auto;
|
|
6
11
|
/* The scroll controller (vterm scroll.ts) is the single owner of
|
|
@@ -27,7 +32,7 @@
|
|
|
27
32
|
color: var(--text);
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
.term-row {
|
|
35
|
+
:where(.wt-root) .term-row {
|
|
31
36
|
white-space: pre;
|
|
32
37
|
min-height: 17px;
|
|
33
38
|
font-variant-ligatures: none;
|
|
@@ -48,7 +53,7 @@
|
|
|
48
53
|
cell tracks the column on every flush. */
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
.term-row:empty::after {
|
|
56
|
+
:where(.wt-root) .term-row:empty::after {
|
|
52
57
|
content: "\00a0";
|
|
53
58
|
}
|
|
54
59
|
|
|
@@ -56,7 +61,7 @@
|
|
|
56
61
|
as the first row when history older than the buffer holds was evicted —
|
|
57
62
|
client-side cap or a server-reported resume gap). A quiet, centered,
|
|
58
63
|
non-selectable divider so the user knows scrollback doesn't go further up. */
|
|
59
|
-
.term-trim-marker {
|
|
64
|
+
:where(.wt-root) .term-trim-marker {
|
|
60
65
|
color: color-mix(in oklch, var(--text) 60%, transparent);
|
|
61
66
|
font-size: 0.6875rem;
|
|
62
67
|
text-align: center;
|
|
@@ -65,7 +70,7 @@
|
|
|
65
70
|
font-family: var(--font-ui);
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
.term-input {
|
|
73
|
+
:where(.wt-root) .term-input {
|
|
69
74
|
/* The hidden keyboard target. position:absolute (NOT fixed) so it lives in
|
|
70
75
|
the terminal's scroll/content coordinate space alongside .composition-view
|
|
71
76
|
and .pred-cursor: composition.positionCompositionView() moves it to the
|
|
@@ -94,7 +99,7 @@
|
|
|
94
99
|
font-size: 16px;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
.term-output {
|
|
102
|
+
:where(.wt-root) .term-output {
|
|
98
103
|
position: relative;
|
|
99
104
|
outline: none;
|
|
100
105
|
|
|
@@ -128,7 +133,7 @@
|
|
|
128
133
|
so the candidate popup tracks what the user is typing.
|
|
129
134
|
Visibility is gated by the `.active` class so empty state doesn't
|
|
130
135
|
render a phantom box on top of the cursor. */
|
|
131
|
-
.composition-view {
|
|
136
|
+
:where(.wt-root) .composition-view {
|
|
132
137
|
position: absolute;
|
|
133
138
|
pointer-events: none;
|
|
134
139
|
background: var(--surface);
|
|
@@ -143,7 +148,7 @@
|
|
|
143
148
|
display: none;
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
.composition-view.active {
|
|
151
|
+
:where(.wt-root) .composition-view.active {
|
|
147
152
|
display: block;
|
|
148
153
|
}
|
|
149
154
|
|
|
@@ -153,7 +158,7 @@
|
|
|
153
158
|
cursor (predict.ts deactivates on bail and the renderer hides on
|
|
154
159
|
coincidence). Dashed outline + reduced opacity reads as "tentative"
|
|
155
160
|
— distinct from the solid block of the real cursor. */
|
|
156
|
-
.pred-cursor {
|
|
161
|
+
:where(.wt-root) .pred-cursor {
|
|
157
162
|
position: absolute;
|
|
158
163
|
pointer-events: none;
|
|
159
164
|
border: 1px dashed var(--text);
|
|
@@ -163,7 +168,7 @@
|
|
|
163
168
|
box-sizing: border-box;
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
.pred-cursor.visible {
|
|
171
|
+
:where(.wt-root) .pred-cursor.visible {
|
|
167
172
|
display: block;
|
|
168
173
|
}
|
|
169
174
|
|
|
@@ -177,17 +182,17 @@
|
|
|
177
182
|
Auto-detected bare URLs (.term-autolink) are tightly scoped to the URL text and
|
|
178
183
|
never bleed, so they keep a subtle persistent underline. (The engine also only
|
|
179
184
|
anchors runs with real link text, so the underline hugs the text either way.) */
|
|
180
|
-
.term-link {
|
|
185
|
+
:where(.wt-root) .term-link {
|
|
181
186
|
color: inherit;
|
|
182
187
|
text-decoration: none;
|
|
183
188
|
cursor: pointer;
|
|
184
189
|
}
|
|
185
190
|
|
|
186
|
-
.term-autolink {
|
|
191
|
+
:where(.wt-root) .term-autolink {
|
|
187
192
|
text-decoration: underline dotted;
|
|
188
193
|
}
|
|
189
194
|
|
|
190
|
-
.term-link:hover {
|
|
195
|
+
:where(.wt-root) .term-link:hover {
|
|
191
196
|
text-decoration: underline solid;
|
|
192
197
|
}
|
|
193
198
|
|
|
@@ -197,9 +202,9 @@
|
|
|
197
202
|
the viewport. JS-driven instead of CSS animation because the cursor
|
|
198
203
|
span is recreated every row update — a CSS animation would restart on
|
|
199
204
|
each recreation and never reach the "off" frame during streaming. */
|
|
200
|
-
.term-cursor,
|
|
201
|
-
.term-cursor-underline,
|
|
202
|
-
.term-cursor-bar {
|
|
205
|
+
:where(.wt-root) .term-cursor,
|
|
206
|
+
:where(.wt-root) .term-cursor-underline,
|
|
207
|
+
:where(.wt-root) .term-cursor-bar {
|
|
203
208
|
box-sizing: border-box;
|
|
204
209
|
display: inline-block;
|
|
205
210
|
height: 17px;
|
|
@@ -207,35 +212,35 @@
|
|
|
207
212
|
vertical-align: top;
|
|
208
213
|
}
|
|
209
214
|
|
|
210
|
-
.term-cursor {
|
|
215
|
+
:where(.wt-root) .term-cursor {
|
|
211
216
|
background: var(--text);
|
|
212
217
|
color: var(--bg);
|
|
213
218
|
}
|
|
214
219
|
|
|
215
220
|
/* Underline cursor (DECSCUSR 3/4) */
|
|
216
|
-
.term-cursor-underline {
|
|
221
|
+
:where(.wt-root) .term-cursor-underline {
|
|
217
222
|
background: transparent;
|
|
218
223
|
color: var(--text);
|
|
219
224
|
border-bottom: 2px solid var(--text);
|
|
220
225
|
}
|
|
221
226
|
|
|
222
227
|
/* Bar cursor (DECSCUSR 5/6) */
|
|
223
|
-
.term-cursor-bar {
|
|
228
|
+
:where(.wt-root) .term-cursor-bar {
|
|
224
229
|
background: transparent;
|
|
225
230
|
color: var(--text);
|
|
226
231
|
border-left: 2px solid var(--text);
|
|
227
232
|
}
|
|
228
233
|
|
|
229
|
-
.cursor-blink-off .term-cursor {
|
|
234
|
+
:where(.wt-root) .cursor-blink-off .term-cursor {
|
|
230
235
|
background: transparent;
|
|
231
236
|
color: var(--text);
|
|
232
237
|
}
|
|
233
238
|
|
|
234
|
-
.term-blink {
|
|
235
|
-
animation:
|
|
239
|
+
:where(.wt-root) .term-blink {
|
|
240
|
+
animation: wt-blink-anim 1s step-end infinite;
|
|
236
241
|
}
|
|
237
242
|
|
|
238
|
-
@keyframes
|
|
243
|
+
@keyframes wt-blink-anim {
|
|
239
244
|
50% {
|
|
240
245
|
opacity: 0;
|
|
241
246
|
}
|
|
@@ -243,9 +248,9 @@
|
|
|
243
248
|
|
|
244
249
|
/* When the terminal isn't focused, render the cursor as an outline
|
|
245
250
|
rather than a filled block (xterm.js convention). */
|
|
246
|
-
.term:not(.focus) .term-cursor,
|
|
247
|
-
.term:not(.focus) .term-cursor-underline,
|
|
248
|
-
.term:not(.focus) .term-cursor-bar {
|
|
251
|
+
:where(.wt-root) .term:not(.focus) .term-cursor,
|
|
252
|
+
:where(.wt-root) .term:not(.focus) .term-cursor-underline,
|
|
253
|
+
:where(.wt-root) .term:not(.focus) .term-cursor-bar {
|
|
249
254
|
background: transparent;
|
|
250
255
|
color: var(--text);
|
|
251
256
|
border: none;
|
|
@@ -257,280 +262,100 @@
|
|
|
257
262
|
}
|
|
258
263
|
|
|
259
264
|
/* ===== Terminal color classes (basic 16 — themeable) ===== */
|
|
260
|
-
.term-fg-0 {
|
|
265
|
+
:where(.wt-root) .term-fg-0 {
|
|
261
266
|
color: #000;
|
|
262
267
|
}
|
|
263
|
-
.term-fg-1 {
|
|
268
|
+
:where(.wt-root) .term-fg-1 {
|
|
264
269
|
color: #a00;
|
|
265
270
|
}
|
|
266
|
-
.term-fg-2 {
|
|
271
|
+
:where(.wt-root) .term-fg-2 {
|
|
267
272
|
color: #0a0;
|
|
268
273
|
}
|
|
269
|
-
.term-fg-3 {
|
|
274
|
+
:where(.wt-root) .term-fg-3 {
|
|
270
275
|
color: #a50;
|
|
271
276
|
}
|
|
272
|
-
.term-fg-4 {
|
|
277
|
+
:where(.wt-root) .term-fg-4 {
|
|
273
278
|
color: #00a;
|
|
274
279
|
}
|
|
275
|
-
.term-fg-5 {
|
|
280
|
+
:where(.wt-root) .term-fg-5 {
|
|
276
281
|
color: #a0a;
|
|
277
282
|
}
|
|
278
|
-
.term-fg-6 {
|
|
283
|
+
:where(.wt-root) .term-fg-6 {
|
|
279
284
|
color: #0aa;
|
|
280
285
|
}
|
|
281
|
-
.term-fg-7 {
|
|
286
|
+
:where(.wt-root) .term-fg-7 {
|
|
282
287
|
color: #aaa;
|
|
283
288
|
}
|
|
284
|
-
.term-fg-8 {
|
|
289
|
+
:where(.wt-root) .term-fg-8 {
|
|
285
290
|
color: #555;
|
|
286
291
|
}
|
|
287
|
-
.term-fg-9 {
|
|
292
|
+
:where(.wt-root) .term-fg-9 {
|
|
288
293
|
color: #f55;
|
|
289
294
|
}
|
|
290
|
-
.term-fg-10 {
|
|
295
|
+
:where(.wt-root) .term-fg-10 {
|
|
291
296
|
color: #5f5;
|
|
292
297
|
}
|
|
293
|
-
.term-fg-11 {
|
|
298
|
+
:where(.wt-root) .term-fg-11 {
|
|
294
299
|
color: #ff5;
|
|
295
300
|
}
|
|
296
|
-
.term-fg-12 {
|
|
301
|
+
:where(.wt-root) .term-fg-12 {
|
|
297
302
|
color: #55f;
|
|
298
303
|
}
|
|
299
|
-
.term-fg-13 {
|
|
304
|
+
:where(.wt-root) .term-fg-13 {
|
|
300
305
|
color: #f5f;
|
|
301
306
|
}
|
|
302
|
-
.term-fg-14 {
|
|
307
|
+
:where(.wt-root) .term-fg-14 {
|
|
303
308
|
color: #5ff;
|
|
304
309
|
}
|
|
305
|
-
.term-fg-15 {
|
|
310
|
+
:where(.wt-root) .term-fg-15 {
|
|
306
311
|
color: #fff;
|
|
307
312
|
}
|
|
308
313
|
|
|
309
|
-
.term-bg-0 {
|
|
314
|
+
:where(.wt-root) .term-bg-0 {
|
|
310
315
|
background: #000;
|
|
311
316
|
}
|
|
312
|
-
.term-bg-1 {
|
|
317
|
+
:where(.wt-root) .term-bg-1 {
|
|
313
318
|
background: #a00;
|
|
314
319
|
}
|
|
315
|
-
.term-bg-2 {
|
|
320
|
+
:where(.wt-root) .term-bg-2 {
|
|
316
321
|
background: #0a0;
|
|
317
322
|
}
|
|
318
|
-
.term-bg-3 {
|
|
323
|
+
:where(.wt-root) .term-bg-3 {
|
|
319
324
|
background: #a50;
|
|
320
325
|
}
|
|
321
|
-
.term-bg-4 {
|
|
326
|
+
:where(.wt-root) .term-bg-4 {
|
|
322
327
|
background: #00a;
|
|
323
328
|
}
|
|
324
|
-
.term-bg-5 {
|
|
329
|
+
:where(.wt-root) .term-bg-5 {
|
|
325
330
|
background: #a0a;
|
|
326
331
|
}
|
|
327
|
-
.term-bg-6 {
|
|
332
|
+
:where(.wt-root) .term-bg-6 {
|
|
328
333
|
background: #0aa;
|
|
329
334
|
}
|
|
330
|
-
.term-bg-7 {
|
|
335
|
+
:where(.wt-root) .term-bg-7 {
|
|
331
336
|
background: #aaa;
|
|
332
337
|
}
|
|
333
|
-
.term-bg-8 {
|
|
338
|
+
:where(.wt-root) .term-bg-8 {
|
|
334
339
|
background: #555;
|
|
335
340
|
}
|
|
336
|
-
.term-bg-9 {
|
|
341
|
+
:where(.wt-root) .term-bg-9 {
|
|
337
342
|
background: #f55;
|
|
338
343
|
}
|
|
339
|
-
.term-bg-10 {
|
|
344
|
+
:where(.wt-root) .term-bg-10 {
|
|
340
345
|
background: #5f5;
|
|
341
346
|
}
|
|
342
|
-
.term-bg-11 {
|
|
347
|
+
:where(.wt-root) .term-bg-11 {
|
|
343
348
|
background: #ff5;
|
|
344
349
|
}
|
|
345
|
-
.term-bg-12 {
|
|
350
|
+
:where(.wt-root) .term-bg-12 {
|
|
346
351
|
background: #55f;
|
|
347
352
|
}
|
|
348
|
-
.term-bg-13 {
|
|
353
|
+
:where(.wt-root) .term-bg-13 {
|
|
349
354
|
background: #f5f;
|
|
350
355
|
}
|
|
351
|
-
.term-bg-14 {
|
|
356
|
+
:where(.wt-root) .term-bg-14 {
|
|
352
357
|
background: #5ff;
|
|
353
358
|
}
|
|
354
|
-
.term-bg-15 {
|
|
359
|
+
:where(.wt-root) .term-bg-15 {
|
|
355
360
|
background: #fff;
|
|
356
361
|
}
|
|
357
|
-
|
|
358
|
-
/* ===== Key toolbar (touch devices only) =====
|
|
359
|
-
Uses interpolate-size: allow-keywords (on :root) to animate
|
|
360
|
-
width/height between a fixed collapsed size and auto (intrinsic).
|
|
361
|
-
Toggle is first in DOM so it's at top-left of the grid — visible
|
|
362
|
-
when the container clips to 1-cell size. Container is right-anchored
|
|
363
|
-
so it grows leftward on open. */
|
|
364
|
-
.key-toolbar {
|
|
365
|
-
display: contents;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/* Touch-only toolbar children: the on-screen Esc/Tab/arrows/hamburger
|
|
369
|
-
are useless on desktop where the user has a real keyboard. Hidden by
|
|
370
|
-
default; the @media (pointer: coarse) block below restores them. */
|
|
371
|
-
.kb-key,
|
|
372
|
-
.kb-toggle {
|
|
373
|
-
display: none;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
@media (pointer: coarse) {
|
|
377
|
-
.key-toolbar {
|
|
378
|
-
position: fixed;
|
|
379
|
-
top: calc(var(--safe-top) + var(--sp-2) + var(--vv-top, 0px));
|
|
380
|
-
right: var(--sp-2);
|
|
381
|
-
z-index: 10;
|
|
382
|
-
display: grid;
|
|
383
|
-
grid-template-columns: repeat(4, var(--touch-target)) var(--touch-target);
|
|
384
|
-
grid-template-rows: var(--touch-target) var(--touch-target);
|
|
385
|
-
place-content: start end;
|
|
386
|
-
gap: var(--sp-1);
|
|
387
|
-
padding: var(--sp-1);
|
|
388
|
-
background: var(--surface);
|
|
389
|
-
backdrop-filter: var(--blur);
|
|
390
|
-
border: 1px solid var(--border);
|
|
391
|
-
border-radius: var(--radius);
|
|
392
|
-
overflow: hidden;
|
|
393
|
-
width: auto;
|
|
394
|
-
height: auto;
|
|
395
|
-
transition:
|
|
396
|
-
width var(--dur-enter) var(--ease-standard),
|
|
397
|
-
height var(--dur-enter) var(--ease-standard);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/* Collapsed: fixed size = 1 button + padding + border. Content is
|
|
401
|
-
right-aligned (justify-content:end) so the toggle (col 5) stays at
|
|
402
|
-
the right edge, visible within the clipped area. --pill-outer
|
|
403
|
-
includes the 2px border (box-sizing:border-box); omitting it left
|
|
404
|
-
the content box 2px short, pinning the button top-right with
|
|
405
|
-
unequal bottom/left gaps. */
|
|
406
|
-
.key-toolbar.collapsed {
|
|
407
|
-
width: var(--pill-outer);
|
|
408
|
-
height: var(--pill-outer);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/* Grid placement: toggle at col 5 row 1 (top-right). */
|
|
412
|
-
.kb-toggle {
|
|
413
|
-
grid-area: 1 / 5;
|
|
414
|
-
position: relative;
|
|
415
|
-
}
|
|
416
|
-
.kb-r1c1 {
|
|
417
|
-
grid-area: 1 / 1;
|
|
418
|
-
}
|
|
419
|
-
.kb-r1c2 {
|
|
420
|
-
grid-area: 1 / 2;
|
|
421
|
-
}
|
|
422
|
-
.kb-r1c3 {
|
|
423
|
-
grid-area: 1 / 3;
|
|
424
|
-
}
|
|
425
|
-
.kb-r1c4 {
|
|
426
|
-
grid-area: 1 / 4;
|
|
427
|
-
}
|
|
428
|
-
.kb-r2c1 {
|
|
429
|
-
grid-area: 2 / 1;
|
|
430
|
-
}
|
|
431
|
-
.kb-r2c2 {
|
|
432
|
-
grid-area: 2 / 2;
|
|
433
|
-
}
|
|
434
|
-
.kb-r2c3 {
|
|
435
|
-
grid-area: 2 / 3;
|
|
436
|
-
}
|
|
437
|
-
.kb-r2c4 {
|
|
438
|
-
grid-area: 2 / 4;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/* Keys fade in/out during open/close. The container's width/height
|
|
442
|
-
transition handles the background slide; opacity handles the
|
|
443
|
-
individual button appearance. Stagger the fade slightly after
|
|
444
|
-
the container starts growing (delay on open) and before it
|
|
445
|
-
finishes shrinking (no delay on close). */
|
|
446
|
-
.kb-key {
|
|
447
|
-
opacity: 1;
|
|
448
|
-
transition: opacity var(--dur-standard) var(--ease-enter) 0.05s;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
.key-toolbar.collapsed .kb-key {
|
|
452
|
-
opacity: 0;
|
|
453
|
-
transition: opacity var(--dur-exit) var(--ease-exit);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/* Shared button */
|
|
457
|
-
.key-toolbar button {
|
|
458
|
-
width: var(--touch-target);
|
|
459
|
-
height: var(--touch-target);
|
|
460
|
-
background: transparent;
|
|
461
|
-
color: var(--text);
|
|
462
|
-
border: none;
|
|
463
|
-
border-radius: var(--radius-inner);
|
|
464
|
-
font-size: 0.9375rem;
|
|
465
|
-
font-family: var(--font-ui);
|
|
466
|
-
font-weight: 500;
|
|
467
|
-
cursor: pointer;
|
|
468
|
-
touch-action: manipulation;
|
|
469
|
-
user-select: none;
|
|
470
|
-
display: flex;
|
|
471
|
-
align-items: center;
|
|
472
|
-
justify-content: center;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
.key-toolbar button:active {
|
|
476
|
-
background: var(--text-active);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/* Sticky Ctrl, armed: inverted fill so the toggle state reads clearly
|
|
480
|
-
in the monochrome palette (stays filled while pressed). */
|
|
481
|
-
.kb-key.armed,
|
|
482
|
-
.kb-key.armed:active {
|
|
483
|
-
background: var(--text);
|
|
484
|
-
color: var(--bg);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
.key-toolbar button svg {
|
|
488
|
-
width: var(--icon);
|
|
489
|
-
height: var(--icon);
|
|
490
|
-
stroke: currentcolor;
|
|
491
|
-
fill: none;
|
|
492
|
-
stroke-width: 2;
|
|
493
|
-
stroke-linecap: round;
|
|
494
|
-
stroke-linejoin: round;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/* Toggle icon crossfade. Both icons are stacked absolute so they
|
|
498
|
-
can independently fade + rotate during the open<->close
|
|
499
|
-
transition. inset:0 + margin:auto centers them within the
|
|
500
|
-
button (works because the SVGs carry explicit width/height); a
|
|
501
|
-
bare `position: absolute` would pin them at the top-left, giving
|
|
502
|
-
the toggle visibly unequal left/right padding when collapsed. */
|
|
503
|
-
.kb-toggle .icon-hamburger,
|
|
504
|
-
.kb-toggle .icon-close {
|
|
505
|
-
position: absolute;
|
|
506
|
-
inset: 0;
|
|
507
|
-
margin: auto;
|
|
508
|
-
transition:
|
|
509
|
-
opacity var(--dur-standard) linear,
|
|
510
|
-
transform var(--dur-standard) linear;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
.kb-toggle .icon-close {
|
|
514
|
-
opacity: 1;
|
|
515
|
-
transform: rotate(0deg);
|
|
516
|
-
}
|
|
517
|
-
.kb-toggle .icon-hamburger {
|
|
518
|
-
opacity: 0;
|
|
519
|
-
transform: rotate(-90deg);
|
|
520
|
-
}
|
|
521
|
-
.key-toolbar.collapsed .kb-toggle .icon-close {
|
|
522
|
-
opacity: 0;
|
|
523
|
-
transform: rotate(90deg);
|
|
524
|
-
}
|
|
525
|
-
.key-toolbar.collapsed .kb-toggle .icon-hamburger {
|
|
526
|
-
opacity: 1;
|
|
527
|
-
transform: rotate(0deg);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/* No-transition on first render */
|
|
531
|
-
.key-toolbar.no-transition,
|
|
532
|
-
.key-toolbar.no-transition .kb-toggle .icon-hamburger,
|
|
533
|
-
.key-toolbar.no-transition .kb-toggle .icon-close {
|
|
534
|
-
transition: none;
|
|
535
|
-
}
|
|
536
|
-
}
|