@cplieger/web-terminal-ui 5.0.3 → 5.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.
- package/README.md +55 -15
- package/css/00-tokens.css +53 -1
- package/css/30-tabs.css +178 -76
- package/css/31-switcher.css +29 -25
- package/css/page.css +8 -4
- package/package.json +3 -2
- package/src/features/activity-monitor.ts +4 -2
- package/src/features/tabs/index.ts +149 -42
- package/src/features/tabs/model.ts +166 -7
- package/src/features/tabs/notify.ts +216 -0
- package/src/features/tabs/strip.ts +56 -14
- package/src/index.ts +2 -0
- package/src/kernel/style-contract.ts +96 -0
- package/src/kernel/types.ts +13 -6
package/README.md
CHANGED
|
@@ -59,15 +59,19 @@ page that has one empty container element, then call
|
|
|
59
59
|
**Full-page host** (the terminal IS the page, as in `web-terminal-server` and
|
|
60
60
|
`web-terminal-kiro`): concatenate `css/MANIFEST` into the `style.css` your page
|
|
61
61
|
links. That reference bundle is `css/page.css` (the page kit: `html/body`
|
|
62
|
-
reset + the
|
|
63
|
-
`/vendor/fonts/`) plus the complete component set.
|
|
62
|
+
reset + the bundled web font's `@font-face`, expecting the font files at
|
|
63
|
+
`/vendor/fonts/`) plus the complete component set. The bundled font backs both
|
|
64
|
+
the terminal display and the chrome text, so serving those files is not
|
|
65
|
+
optional for a full-page host.
|
|
64
66
|
|
|
65
67
|
**Embedder** (the terminal lives inside your app's layout, as a panel or pane):
|
|
66
68
|
concatenate the per-preset manifest matching your composition instead:
|
|
67
69
|
`css/MANIFEST.single`, `css/MANIFEST.touch`, or `css/MANIFEST.tabbed`. These
|
|
68
70
|
contain ONLY root-scoped component styles: no page reset, no fonts, no
|
|
69
|
-
document-level rules, nothing to quarantine.
|
|
70
|
-
|
|
71
|
+
document-level rules, nothing to quarantine. Chrome text then falls back to your
|
|
72
|
+
platform's monospace unless you declare the `@font-face` yourself; ship it if you
|
|
73
|
+
want the terminal panel to match the full-page product. Pass `layout: "container"`
|
|
74
|
+
so the terminal fills (and positions its chrome against) your container element
|
|
71
75
|
instead of the viewport.
|
|
72
76
|
|
|
73
77
|
Feature bundles (presets) live at the `./presets` sub-path, with per-preset
|
|
@@ -134,20 +138,29 @@ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
|
|
|
134
138
|
label follows the latest submitted line (persisted server-side and recovered
|
|
135
139
|
on reload) and the program's OSC 0/2 title is ignored. With `presumeReports`,
|
|
136
140
|
the idle activity dot shows from tab creation instead of waiting for the
|
|
137
|
-
session's first OSC 9;4 signal. Its status dots
|
|
138
|
-
|
|
141
|
+
session's first OSC 9;4 signal. Its status dots come from the same activity
|
|
142
|
+
monitor and cover eight states: the three animated OSC 9;4 progress states
|
|
143
|
+
(working, warning, error), the frozen ringed needs-input dot, two static discs
|
|
144
|
+
(a green finished turn, a red crashed process), and two hollow ones (idle and a
|
|
145
|
+
dim ended session). A
|
|
146
|
+
reported percentage additionally renders a 2px determinate bar on the chip and
|
|
147
|
+
a `78% ·` prefix on the tab label; it is deliberately never written to the
|
|
148
|
+
browser document title, since one page title cannot represent several
|
|
149
|
+
sessions. An OSC 9 notification is posted as a browser
|
|
150
|
+
notification when the user is not already looking at that terminal (permission
|
|
151
|
+
is requested on a user gesture; a denial degrades silently to the tab dots).
|
|
139
152
|
|
|
140
153
|
### Options
|
|
141
154
|
|
|
142
|
-
| Option | Default | Purpose
|
|
143
|
-
| -------------- | -------------------------- |
|
|
144
|
-
| `features` | _(none; bare kernel)_ | A FUNCTION returning the feature list. Omitted builds only the terminal (no chrome). Pass a preset by name (`features: presetTabbed`) or a factory of your own; it is called inside the startup-failure boundary, so a preset that throws is handled rather than escaping.
|
|
145
|
-
| `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).
|
|
146
|
-
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to.
|
|
147
|
-
| `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.
|
|
148
|
-
| `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.
|
|
149
|
-
| `onFatalError` | _(built-in recovery)_ | Called with a `TerminalStartupFailure` after a fatal startup failure, in either phase (`feature-setup` or `kernel-init`); behavior below.
|
|
150
|
-
| `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`, plus the activity-dot palette `--status-working`, `--status-done`, `--status-input`. The library ships neutral defaults. |
|
|
155
|
+
| Option | Default | Purpose |
|
|
156
|
+
| -------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
157
|
+
| `features` | _(none; bare kernel)_ | A FUNCTION returning the feature list. Omitted builds only the terminal (no chrome). Pass a preset by name (`features: presetTabbed`) or a factory of your own; it is called inside the startup-failure boundary, so a preset that throws is handled rather than escaping. |
|
|
158
|
+
| `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). |
|
|
159
|
+
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
|
|
160
|
+
| `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. |
|
|
161
|
+
| `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. |
|
|
162
|
+
| `onFatalError` | _(built-in recovery)_ | Called with a `TerminalStartupFailure` after a fatal startup failure, in either phase (`feature-setup` or `kernel-init`); behavior below. |
|
|
163
|
+
| `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`, plus the activity-dot palette `--status-working`, `--status-done`, `--status-input`, `--status-warning`, `--status-failed`. The library ships neutral defaults. If you retheme the animated trio (`--status-working` / `--status-warning` / `--status-failed`), keep their LIGHTNESS spread: they differ only in hue, so equal-lightness replacements collapse into one another in greyscale and under deuteranopia. |
|
|
151
164
|
|
|
152
165
|
`createTerminal()` returns a handle: `focus()` re-focuses the terminal input
|
|
153
166
|
(and opens the soft keyboard on touch); `send(bytes)` sends bytes to the active
|
|
@@ -157,6 +170,33 @@ screen without injecting keystrokes (send a redraw keystroke yourself if you
|
|
|
157
170
|
want one, for example Ctrl+L); and `destroy()` tears every feature down and
|
|
158
171
|
releases the kernel.
|
|
159
172
|
|
|
173
|
+
### Themes that provably apply
|
|
174
|
+
|
|
175
|
+
`theme` is an open `Record<string, string>` — the kernel sets every key on the
|
|
176
|
+
terminal root verbatim — so a key this library renamed or retired becomes a live
|
|
177
|
+
declaration nothing reads: no error, just the library's neutral defaults where
|
|
178
|
+
your brand should be. The supported keys are therefore published as data:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { PUBLIC_THEME_TOKENS } from "@cplieger/web-terminal-ui/style-contract";
|
|
182
|
+
// or from the package root; the subpath imports nothing and touches no DOM,
|
|
183
|
+
// so a Node script can read it too.
|
|
184
|
+
|
|
185
|
+
for (const key of Object.keys(MY_THEME)) {
|
|
186
|
+
expect(PUBLIC_THEME_TOKENS).toContain(key); // your test, our list
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Every token on that list is guaranteed by this package's own suite to be both
|
|
191
|
+
declared AND read by a shipped rule, so setting it changes what renders. The
|
|
192
|
+
second half is the one you cannot check from outside: a token that is declared
|
|
193
|
+
but that no rule reads accepts your override and applies it to nothing. Anything
|
|
194
|
+
NOT on the list is internal and may be renamed without a release note.
|
|
195
|
+
|
|
196
|
+
`LOADING_OVERLAY_CLASSES` is published from the same module for the same reason:
|
|
197
|
+
your pre-JS overlay markup opts into `css/page.css`'s styling by class name, and
|
|
198
|
+
that markup usually lives in a static HTML file no compiler reads.
|
|
199
|
+
|
|
160
200
|
### Startup failures
|
|
161
201
|
|
|
162
202
|
You do not need your own startup-failure UI. Every way starting up can fail ends
|
package/css/00-tokens.css
CHANGED
|
@@ -82,6 +82,28 @@
|
|
|
82
82
|
--status-working: #52a9fe; /* agent working (waves): the same blue as the default --accent */
|
|
83
83
|
--status-done: #22c55e; /* turn finished: solid green */
|
|
84
84
|
--status-input: #fb923c; /* action required: solid orange + ring */
|
|
85
|
+
/* OSC 9;4 progress states 4 (warning) and 2 (error). Both render as the SAME
|
|
86
|
+
object as working -- identical glow + travelling wave, one shared rule and
|
|
87
|
+
one shared pair of keyframes (30-tabs.css) -- differing ONLY in hue, because
|
|
88
|
+
the escape code models one progress indicator with several states rather
|
|
89
|
+
than three unrelated dots. Neither wears a ring: the frozen hard ring is
|
|
90
|
+
--status-input's exclusive meaning ("blocked, waiting on you"), and neither
|
|
91
|
+
warning nor error asks the user to act.
|
|
92
|
+
LIGHTNESS SEPARATION IS LOAD-BEARING, NOT DECORATIVE. Three states that
|
|
93
|
+
differ only by hue collapse into one under deuteranopia and in greyscale, so
|
|
94
|
+
the three hues are also spread across lightness. Measured OKLab L (and WCAG
|
|
95
|
+
relative luminance Y, the greyscale read):
|
|
96
|
+
warning #facc15 L 86.1% Y 63.6%
|
|
97
|
+
working #52a9fe L 71.9% Y 37.3%
|
|
98
|
+
failed #dc2626 L 57.7% Y 16.7%
|
|
99
|
+
~14 L points and a wide Y gap between each neighbour, monotonic in both, so
|
|
100
|
+
the three stay ordered and distinguishable with hue discarded entirely. A
|
|
101
|
+
retheme MUST keep that spread: two animated states at the same lightness are
|
|
102
|
+
indistinguishable to a colour-blind user, whatever their hues. warning is
|
|
103
|
+
YELLOW, deliberately not amber -- amber would collide with --status-input's
|
|
104
|
+
orange (L 75.8%), which sits between working and warning. */
|
|
105
|
+
--status-warning: #facc15; /* OSC 9;4 state 4: yellow disc, working's wave */
|
|
106
|
+
--status-failed: #dc2626; /* OSC 9;4 state 2: red disc, working's wave; crashed reuses it solid and static */
|
|
85
107
|
--radius: 0.75rem;
|
|
86
108
|
--radius-inner: 0.5rem;
|
|
87
109
|
/* Small square icon buttons (e.g. the 24px tab-close x): at that size the
|
|
@@ -102,8 +124,38 @@
|
|
|
102
124
|
--sp-1: 0.25rem;
|
|
103
125
|
--sp-2: 0.5rem;
|
|
104
126
|
--sp-3: 0.75rem;
|
|
105
|
-
|
|
127
|
+
/* Chrome text uses the SAME bundled web font as the terminal, deliberately.
|
|
128
|
+
A system-font stack made the tab-title alignment unfixable: every entry of
|
|
129
|
+
the old stack (-apple-system, BlinkMacSystemFont, "SF Pro Text") resolves
|
|
130
|
+
only on Apple platforms, so everywhere else it fell through to system-ui —
|
|
131
|
+
which Chromium resolves through fontconfig and Firefox from the desktop UI
|
|
132
|
+
font setting. Two different typefaces, with two different vertical metrics,
|
|
133
|
+
on ONE machine. Optically centring text needs the font's ascent/descent
|
|
134
|
+
asymmetry as a known constant (--label-ink-shift below), and an unknown
|
|
135
|
+
font has no constant, so no value could ever be right on both.
|
|
136
|
+
|
|
137
|
+
MonaspiceNe NFM is already served at /vendor/fonts by every full-page host
|
|
138
|
+
(page.css declares the faces, the Dockerfiles ship the files), so this adds
|
|
139
|
+
no download. A consumer serving a component-only manifest has no face for
|
|
140
|
+
this family and falls back to the platform monospace — and also ships no tab
|
|
141
|
+
strip (MANIFEST.touch excludes 30-tabs.css), so nothing metric-dependent
|
|
142
|
+
rides on that fallback. */
|
|
143
|
+
--font-ui: "MonaspiceNe NFM", ui-monospace, monospace;
|
|
106
144
|
--font-mono: "MonaspiceNe NFM", monospace;
|
|
145
|
+
/* How far a chip label's INK sits below its line box's centre, so flex
|
|
146
|
+
centring can be corrected by exactly that much (see .wt-tab-label in
|
|
147
|
+
30-tabs.css). Derived from MonaspiceNe NFM's own tables and outlines:
|
|
148
|
+
|
|
149
|
+
(ascent - descent) / 2 = (0.995 - 0.250) / 2 = 0.3725em
|
|
150
|
+
(cap ink - descender ink)/2 = (0.7344 - 0.2188) / 2 = 0.2578em
|
|
151
|
+
difference = 0.1147em
|
|
152
|
+
|
|
153
|
+
A constant is safe here only because the bundled font leaves engines
|
|
154
|
+
nothing to disagree about: hhea, OS/2 typo and usWin all declare the same
|
|
155
|
+
0.995 / 0.250 with lineGap 0, and fsSelection sets USE_TYPO_METRICS, so
|
|
156
|
+
Chromium, Gecko and WebKit all compute the same line box. Re-derive this
|
|
157
|
+
number if the bundled font ever changes; do not nudge it by eye. */
|
|
158
|
+
--label-ink-shift: 0.1147em;
|
|
107
159
|
--dur-micro: 0.1s;
|
|
108
160
|
--dur-standard: 0.2s;
|
|
109
161
|
--dur-enter: 0.25s;
|
package/css/30-tabs.css
CHANGED
|
@@ -75,15 +75,24 @@
|
|
|
75
75
|
width: 300px;
|
|
76
76
|
flex: 0 1 auto;
|
|
77
77
|
min-width: 100px;
|
|
78
|
+
/* Positioning context for the chip's own overlay: the OSC 9;4 determinate
|
|
79
|
+
progress bar sits on its bottom edge (see .wt-progress-bar). Nothing else
|
|
80
|
+
inside a chip is absolutely positioned, and the strip's two root-level
|
|
81
|
+
overlays (.wt-tab-ghost, .wt-tab-menu) are children of .wt-root, not of a
|
|
82
|
+
chip, so this cannot capture them. */
|
|
83
|
+
position: relative;
|
|
78
84
|
/* ONE height for every control in the strip: the chip, the "+" and the
|
|
79
85
|
keyboard button all resolve to --touch-target, so a chip and a button can
|
|
80
86
|
never disagree about how tall the row is. Before this the chip's height was
|
|
81
87
|
whatever its tallest child happened to be, and the "+" hard-coded a COPY of
|
|
82
88
|
that arithmetic (close button + padding + border) — two sources of truth
|
|
83
|
-
for one row, which drifted the moment a child's box grew:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
for one row, which drifted the moment a child's box grew: the label's own
|
|
90
|
+
box once reached 31px, past the 24px close button, and the chip rendered
|
|
91
|
+
49px tall next to a 42px "+" and a 44px keyboard button. (That growth came
|
|
92
|
+
from a text-box trim's descender padding being applied on the untrimmed
|
|
93
|
+
code path, which no longer exists -- the label's geometry is now one
|
|
94
|
+
unconditional rule; see .wt-tab-label. The two-sources-of-truth lesson is
|
|
95
|
+
what survives.) --touch-target is also the tap-target floor the rest of
|
|
87
96
|
the chrome already respects, including this chip's mobile counterpart
|
|
88
97
|
(.wt-switcher-current in 31-switcher.css, same declaration).
|
|
89
98
|
|
|
@@ -174,57 +183,58 @@
|
|
|
174
183
|
}
|
|
175
184
|
|
|
176
185
|
/* The label flexes to fill the fixed-width tab and truncates with an ellipsis
|
|
177
|
-
(min-width: 0 lets a flex item shrink below its content width)
|
|
186
|
+
(min-width: 0 lets a flex item shrink below its content width), and it owns the
|
|
187
|
+
ink-centring geometry the two switcher labels in 31-switcher.css share verbatim.
|
|
188
|
+
|
|
189
|
+
Centre the INK, not the line box. A font's metrics box reaches far higher above
|
|
190
|
+
the baseline than below it (the bundled font: 0.995em over, 0.250em under),
|
|
191
|
+
while the ink of a mixed-case title runs 0.7344em over (cap height) to 0.2188em
|
|
192
|
+
under (descender). Flex centring centres the BOX, which leaves the visible text
|
|
193
|
+
--label-ink-shift low — 0.1147em, 1.5px at this size — reading as
|
|
194
|
+
baseline-aligned with the activity dot's bottom edge instead of level with its
|
|
195
|
+
centre. Two declarations, one job, and a third that must NOT be added:
|
|
196
|
+
|
|
197
|
+
- padding-block is the ink allowance, not spacing. `overflow: hidden` (which
|
|
198
|
+
the ellipsis requires) clips at the padding box, and the line box clears the
|
|
199
|
+
tallest ink by only 0.6px, so an unpadded label shaves the top off every
|
|
200
|
+
acute accent and caron. 0.125em per side buys 2.2px of clearance at 13px, and
|
|
201
|
+
being symmetric it does not move the centred text.
|
|
202
|
+
- margin-block hangs TWICE the shift below the box. Flex centring centres the
|
|
203
|
+
MARGIN box, so a bottom-heavy margin lifts the border box by half of it --
|
|
204
|
+
exactly one shift -- and unlike padding it does not enlarge the clipping box.
|
|
205
|
+
- line-height stays at `normal`, deliberately. Setting it looks like the
|
|
206
|
+
responsible thing to do and it is the trap: `normal` is the ONE value whose
|
|
207
|
+
half-leading is zero by definition, which is what makes the baseline sit at
|
|
208
|
+
exactly the engine's ascent below the box top. Any other value leaves
|
|
209
|
+
(line-height - font box) / 2 to be halved and rounded, and Chromium floors
|
|
210
|
+
it -- measured with an explicit 1.25 the 14px switcher labels came out a
|
|
211
|
+
FULL pixel high (line box 17.5 under an 18px font box, so half-leading
|
|
212
|
+
-0.25 floored to -1) while the 13px strip label was fine, which is exactly
|
|
213
|
+
the kind of works-where-I-looked bug this rule exists to end.
|
|
214
|
+
|
|
215
|
+
What this deliberately does NOT use is `text-box: trim-both`, which all three
|
|
216
|
+
previous attempts were built on. It ships in Blink and WebKit only (Firefox has
|
|
217
|
+
it in Nightly behind layout.css.text-box.enabled), so it can only be applied
|
|
218
|
+
behind an @supports gate, and a gate means two engines render two different
|
|
219
|
+
geometries of which only the one the author is looking at ever gets tuned.
|
|
220
|
+
Measured on the gated version: Chromium centred to 0.02px while Firefox sat
|
|
221
|
+
1.50px low with its baseline half a pixel BELOW the dot. The keyword vocabulary
|
|
222
|
+
is too coarse for this text besides -- `cap alphabetic` centres the cap band and
|
|
223
|
+
reads 1.14px low on lowercase, `ex alphabetic` centres the x-band and reads high
|
|
224
|
+
on capitals, and a mixed-case tab title wants neither edge.
|
|
225
|
+
|
|
226
|
+
The residual error is engines rounding ascent and descent to whole pixels, which
|
|
227
|
+
makes the ideal shift 0.127em at 13px and 0.099em at 14px; the one authored
|
|
228
|
+
0.1147em is the unrounded truth and lands within 0.22px at both sizes, in every
|
|
229
|
+
engine, with no fork. */
|
|
178
230
|
:where(.wt-root) .wt-tab-label {
|
|
179
231
|
flex: 1 1 auto;
|
|
180
232
|
min-width: 0;
|
|
181
233
|
overflow: hidden;
|
|
182
234
|
white-space: nowrap;
|
|
183
235
|
text-overflow: ellipsis;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
/* Center the INK, not the line box: UI fonts carry far more ascent than
|
|
187
|
-
descent (the -apple-system/Noto Sans stack at 13px: 14px over / 4px under
|
|
188
|
-
the baseline), so the flex-centered line box leaves the visible text ~1.5px
|
|
189
|
-
low — reading as baseline-aligned with the activity dot's bottom instead of
|
|
190
|
-
its center. Trimming the box makes flexbox center the glyph band instead of
|
|
191
|
-
the font's metrics box.
|
|
192
|
-
|
|
193
|
-
`ex`, not `cap`, is the over edge, because a tab title is sentence case and
|
|
194
|
-
lowercase-dominant ("workspace", "refactor the auth module"). Centering the
|
|
195
|
-
x-height/baseline band centers the total ink of such a string, since a
|
|
196
|
-
Latin font's ascender overshoot above x-height is about its descender depth
|
|
197
|
-
below the baseline (measured on this stack at 13px: 3.0px over vs 3.0px
|
|
198
|
-
under, ink center 0.02px off the chip's center). `cap alphabetic` — the
|
|
199
|
-
right choice for an ALL-CAPS label — leaves lowercase text 1.14px low,
|
|
200
|
-
which is the original complaint barely moved.
|
|
201
|
-
|
|
202
|
-
padding-block is LOAD-BEARING, not spacing: trimming puts the box's under
|
|
203
|
-
edge ON the baseline, and `overflow: hidden` clips at the padding edge, so
|
|
204
|
-
with no padding every descender and every tall diacritic is cut off (
|
|
205
|
-
measured: 3.3px of p/y/g/j tail and 2.3px of an acute accent, i.e. "pygmy
|
|
206
|
-
jaguar Ápple" rendered as "pyqmy iaquar Apple"). 0.5em restores the ink
|
|
207
|
-
overhang the trim removes from the box; being symmetric it does not move
|
|
208
|
-
the centered band, and being em-relative it tracks the font size.
|
|
209
|
-
|
|
210
|
-
The pair is @supports-GATED because the padding is the trim's allowance and
|
|
211
|
-
nothing else: an untrimmed line box already contains that overhang, so
|
|
212
|
-
adding the padding there is not "harmless slack" (this comment used to
|
|
213
|
-
claim it was) — it grew the label's box to 31px, past the 24px close button,
|
|
214
|
-
and a browser without text-box support rendered 49px tab chips beside a
|
|
215
|
-
42px "+" and a 44px keyboard button. The chip's own --touch-target floor
|
|
216
|
-
pins the row height either way; the gate is what keeps the label from
|
|
217
|
-
pushing PAST it. Gating the shorthand (not the longhands) is deliberate: an
|
|
218
|
-
engine that ships text-box-trim without the shorthand does not apply the
|
|
219
|
-
declaration we actually write, so it must take the untrimmed branch too.
|
|
220
|
-
|
|
221
|
-
Shared by all three chip labels (this one + the two switcher labels in
|
|
222
|
-
31-switcher.css). */
|
|
223
|
-
@supports (text-box: trim-both ex alphabetic) {
|
|
224
|
-
:where(.wt-root) .wt-tab-label {
|
|
225
|
-
text-box: trim-both ex alphabetic;
|
|
226
|
-
padding-block: 0.5em;
|
|
227
|
-
}
|
|
236
|
+
padding-block: 0.125em;
|
|
237
|
+
margin-block: 0 calc(2 * var(--label-ink-shift));
|
|
228
238
|
}
|
|
229
239
|
|
|
230
240
|
/* A little breathing room after the activity dot, before the label. */
|
|
@@ -569,9 +579,13 @@
|
|
|
569
579
|
}
|
|
570
580
|
|
|
571
581
|
/* ===== Activity dots (activityMonitor via tabs) =====
|
|
572
|
-
|
|
573
|
-
per section 12 / WCAG 1.4.1
|
|
574
|
-
|
|
582
|
+
Eight-state, distinguishable without color (shape + motion, not hue alone),
|
|
583
|
+
per section 12 / WCAG 1.4.1: three ANIMATED progress states (working /
|
|
584
|
+
warning / failed — one object, three hues, spread across lightness so they
|
|
585
|
+
survive greyscale and deuteranopia), a frozen ringed disc (input), two static
|
|
586
|
+
discs (green done, red crashed), and two hollow ones (idle, dim exited).
|
|
587
|
+
The one .wt-status-dot vocabulary is shared by the desktop strip, the mobile
|
|
588
|
+
switcher, and the overview sheet rows.
|
|
575
589
|
|
|
576
590
|
Revealed PER TAB: the dot is hidden until its own session reports activity
|
|
577
591
|
(OSC 9;4 progress, or a classified OSC 9 notification), at which point the
|
|
@@ -595,6 +609,7 @@
|
|
|
595
609
|
from the filled done/working/input states (WCAG 1.4.1). */
|
|
596
610
|
background: transparent;
|
|
597
611
|
border: 1.5px solid color-mix(in oklch, var(--text) 45%, transparent);
|
|
612
|
+
|
|
598
613
|
/* Reset every transient visual owned by a status. In particular, a dot that
|
|
599
614
|
leaves working must stop its pulse, and a dot that enters working must not
|
|
600
615
|
carry the input ring or an exited opacity into the animated paint layer. */
|
|
@@ -603,6 +618,33 @@
|
|
|
603
618
|
animation: none;
|
|
604
619
|
}
|
|
605
620
|
|
|
621
|
+
/* ===== Determinate progress bar (OSC 9;4 percentage) =====
|
|
622
|
+
The second display mode the specs name for a progress report (iTerm2 calls it
|
|
623
|
+
a progress bar; ConEmu also puts the value on the taskbar, which is the
|
|
624
|
+
document-title prefix's job). One 2px line on the chip's bottom edge, shared
|
|
625
|
+
by all three chip sites (desktop strip chip, mobile active row, expanded list
|
|
626
|
+
row) because it ships in the one chipContent builder.
|
|
627
|
+
|
|
628
|
+
The ACCENT colour, not a per-state tint: the bar answers "how far", the dot
|
|
629
|
+
answers "in what state", and tinting the bar per state would say the same
|
|
630
|
+
thing twice while making a 2px line carry hue it cannot carry legibly.
|
|
631
|
+
|
|
632
|
+
Painted only while a percentage actually exists: the element carries the
|
|
633
|
+
`hidden` attribute otherwise (paintProgress), so an absent value (-1) renders
|
|
634
|
+
NO bar rather than a zero-width or empty one. Its width is the only inline
|
|
635
|
+
style — the percentage is data, not styling. */
|
|
636
|
+
:where(.wt-root) .wt-progress-bar {
|
|
637
|
+
position: absolute;
|
|
638
|
+
left: 0;
|
|
639
|
+
bottom: 0;
|
|
640
|
+
height: 2px;
|
|
641
|
+
max-width: 100%;
|
|
642
|
+
background: var(--accent);
|
|
643
|
+
/* Follow the chip's rounded bottom-left corner instead of poking out of it. */
|
|
644
|
+
border-bottom-left-radius: inherit;
|
|
645
|
+
pointer-events: none;
|
|
646
|
+
}
|
|
647
|
+
|
|
606
648
|
/* Reveal a dot only once its session has reported activity (the tabs feature
|
|
607
649
|
adds .wt-reports per dot; see paintStatusDot). Without it the dot — and the
|
|
608
650
|
whole activity vocabulary for that tab — stays invisible. */
|
|
@@ -629,14 +671,44 @@
|
|
|
629
671
|
opacity always multiplies onto its pseudos, so true independence requires
|
|
630
672
|
animating disc COLORS, not element opacity. Server-side this is driven by the
|
|
631
673
|
OSC 9;4 progress signal, so it tracks agent activity, not the user typing at
|
|
632
|
-
the prompt.
|
|
633
|
-
|
|
674
|
+
the prompt.
|
|
675
|
+
|
|
676
|
+
WORKING, WARNING AND FAILED SHARE THIS RULE, AND BOTH KEYFRAMES, VERBATIM.
|
|
677
|
+
OSC 9;4 models ONE progress indicator with several states (1/3 active,
|
|
678
|
+
4 warning, 2 error), so it renders as one visual object whose HUE carries the
|
|
679
|
+
state — not three unrelated dots. Each state therefore sets --dot-color and
|
|
680
|
+
changes nothing else: same disc, same glow beat, same travelling wave, same
|
|
681
|
+
period. Do NOT give warning or failed a ring — the frozen hard ring is
|
|
682
|
+
input's exclusive meaning ("blocked, waiting on you"), and neither of these
|
|
683
|
+
asks the user to act (see the input rule). Hue alone would not be enough on
|
|
684
|
+
its own, which is why the three tokens are also spread across lightness
|
|
685
|
+
(measured OKLab L: warning 86.1%, working 71.9%, failed 57.7% — see the
|
|
686
|
+
load-bearing note in 00-tokens.css). */
|
|
687
|
+
:where(.wt-root) .wt-status-dot[data-status="working"],
|
|
688
|
+
:where(.wt-root) .wt-status-dot[data-status="warning"],
|
|
689
|
+
:where(.wt-root) .wt-status-dot[data-status="failed"] {
|
|
634
690
|
position: relative;
|
|
635
|
-
background: var(--
|
|
636
|
-
border-color: var(--
|
|
691
|
+
background: var(--dot-color);
|
|
692
|
+
border-color: var(--dot-color);
|
|
637
693
|
animation: wt-working-glow 1.2s ease-in-out infinite;
|
|
638
694
|
}
|
|
639
695
|
|
|
696
|
+
/* The one difference between the three animated states. Kept as three
|
|
697
|
+
single-declaration rules so the shared paint above reads as literally shared:
|
|
698
|
+
a future change to the disc, the glow or the wave cannot land on one hue and
|
|
699
|
+
miss the other two. */
|
|
700
|
+
:where(.wt-root) .wt-status-dot[data-status="working"] {
|
|
701
|
+
--dot-color: var(--status-working);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
:where(.wt-root) .wt-status-dot[data-status="warning"] {
|
|
705
|
+
--dot-color: var(--status-warning);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
:where(.wt-root) .wt-status-dot[data-status="failed"] {
|
|
709
|
+
--dot-color: var(--status-failed);
|
|
710
|
+
}
|
|
711
|
+
|
|
640
712
|
/* The ripple layer: a feathered radial BAND (not a hairline border) that starts
|
|
641
713
|
hugging the disc's edge (inset -2px, the 9->13px box) and then travels
|
|
642
714
|
outward, dissolving. The band is painted as a radial-gradient — soft on both
|
|
@@ -645,16 +717,18 @@
|
|
|
645
717
|
which is what makes the wave visibly dissipate as it grows. transform +
|
|
646
718
|
opacity only, so it composites without repaints (a gradient background is
|
|
647
719
|
rasterized once, unlike an animated box-shadow spread, which repaints every
|
|
648
|
-
frame). It exists only in the
|
|
649
|
-
dot changes status. (The switcher notification dot never shows
|
|
650
|
-
input/done only — so its position: absolute placement never
|
|
651
|
-
rule's position: relative.)
|
|
720
|
+
frame). It exists only in the three animated states — no stale layer to pin
|
|
721
|
+
when a dot changes status. (The switcher notification dot never shows any of
|
|
722
|
+
them — input/done/crashed only — so its position: absolute placement never
|
|
723
|
+
meets this rule's position: relative.)
|
|
652
724
|
|
|
653
725
|
The final scale is deliberately capped at today's 27px footprint: the mobile
|
|
654
726
|
overview row (.wt-switcher-row-select) clips at overflow: hidden 12.5px from
|
|
655
727
|
the dot's center, and a wave wider than ~25px picks up a visible straight
|
|
656
728
|
edge there. Visibility comes from the band's weight and timing, not size. */
|
|
657
|
-
:where(.wt-root) .wt-status-dot[data-status="working"]::after
|
|
729
|
+
:where(.wt-root) .wt-status-dot[data-status="working"]::after,
|
|
730
|
+
:where(.wt-root) .wt-status-dot[data-status="warning"]::after,
|
|
731
|
+
:where(.wt-root) .wt-status-dot[data-status="failed"]::after {
|
|
658
732
|
content: "";
|
|
659
733
|
position: absolute;
|
|
660
734
|
inset: -2px;
|
|
@@ -662,7 +736,7 @@
|
|
|
662
736
|
background: radial-gradient(
|
|
663
737
|
closest-side,
|
|
664
738
|
transparent 44%,
|
|
665
|
-
var(--
|
|
739
|
+
var(--dot-color) 76%,
|
|
666
740
|
transparent 100%
|
|
667
741
|
);
|
|
668
742
|
animation: wt-working-ripple 1.2s linear infinite;
|
|
@@ -702,23 +776,46 @@
|
|
|
702
776
|
opacity: 0.5;
|
|
703
777
|
}
|
|
704
778
|
|
|
779
|
+
/* Crashed (a non-zero exit status, or a signal the program was not asked for;
|
|
780
|
+
a server-initiated end is "exited", so a routine restart never paints red):
|
|
781
|
+
a SOLID dot in the failed red, static.
|
|
782
|
+
|
|
783
|
+
Solid, not hollow. There is no "hollow means no process" invariant to honour —
|
|
784
|
+
idle is hollow with a perfectly live process — so hollowness would only say
|
|
785
|
+
"unremarkable", and a crash is the least unremarkable thing this vocabulary
|
|
786
|
+
reports. It uses the SAME token as failed, because "the program reported an
|
|
787
|
+
error" and "the program died badly" are one family, and it separates from
|
|
788
|
+
failed by MOTION: failed pulses and emits the travelling wave, crashed is
|
|
789
|
+
completely still (no glow, no wave, no ring — nothing clears it, so any
|
|
790
|
+
animation would nag forever). That is the same axis that already separates
|
|
791
|
+
working from done. Under prefers-reduced-motion the separation survives as
|
|
792
|
+
SHAPE: failed degrades to a ringed donut (see the reduced-motion block), while
|
|
793
|
+
crashed stays a bare solid disc. */
|
|
794
|
+
:where(.wt-root) .wt-status-dot[data-status="crashed"] {
|
|
795
|
+
background: var(--status-failed);
|
|
796
|
+
border-color: var(--status-failed);
|
|
797
|
+
}
|
|
798
|
+
|
|
705
799
|
/* The disc's glow beat: base -> brighter -> base, then a rest until the next
|
|
706
800
|
cycle. Colors are set at EVERY frame, which both drives the glow and pins
|
|
707
801
|
the paint — a dot entering working in the same frame input/done ends can
|
|
708
802
|
never re-pulse the previously rasterized paint (the hazard the old
|
|
709
803
|
opacity-only pulse had to pin box-shadow against). Element opacity stays 1
|
|
710
|
-
throughout; see the layer-independence note on the working rule.
|
|
804
|
+
throughout; see the layer-independence note on the working rule.
|
|
805
|
+
|
|
806
|
+
--dot-color, not --status-working: this beat is shared verbatim by working,
|
|
807
|
+
warning and failed, each of which supplies its own hue. */
|
|
711
808
|
@keyframes wt-working-glow {
|
|
712
809
|
0%,
|
|
713
810
|
70%,
|
|
714
811
|
100% {
|
|
715
|
-
background-color: var(--
|
|
716
|
-
border-color: var(--
|
|
812
|
+
background-color: var(--dot-color);
|
|
813
|
+
border-color: var(--dot-color);
|
|
717
814
|
}
|
|
718
815
|
|
|
719
816
|
35% {
|
|
720
|
-
background-color: color-mix(in oklch, var(--
|
|
721
|
-
border-color: color-mix(in oklch, var(--
|
|
817
|
+
background-color: color-mix(in oklch, var(--dot-color) 78%, #fff);
|
|
818
|
+
border-color: color-mix(in oklch, var(--dot-color) 78%, #fff);
|
|
722
819
|
}
|
|
723
820
|
}
|
|
724
821
|
|
|
@@ -758,22 +855,27 @@
|
|
|
758
855
|
}
|
|
759
856
|
}
|
|
760
857
|
|
|
761
|
-
/* Reduced-motion: drop the
|
|
858
|
+
/* Reduced-motion: drop the animated states' motion (glow + ripple), keep color.
|
|
859
|
+
All three degrade the same way, since they are one object with three hues. */
|
|
762
860
|
@media (prefers-reduced-motion: reduce) {
|
|
763
|
-
:where(.wt-root) .wt-status-dot[data-status="working"]
|
|
861
|
+
:where(.wt-root) .wt-status-dot[data-status="working"],
|
|
862
|
+
:where(.wt-root) .wt-status-dot[data-status="warning"],
|
|
863
|
+
:where(.wt-root) .wt-status-dot[data-status="failed"] {
|
|
764
864
|
animation: none;
|
|
765
|
-
/* Motion is what separates
|
|
766
|
-
removed, punch an inner hole (dot -> donut) and add a static ring
|
|
767
|
-
|
|
768
|
-
shape, not hue alone (WCAG 1.4.1). The ring carries the same 30%
|
|
769
|
-
|
|
865
|
+
/* Motion is what separates the animated states from input's frozen ping;
|
|
866
|
+
with it removed, punch an inner hole (dot -> donut) and add a static ring
|
|
867
|
+
so they differ from input (solid ringed disc) and done (bare disc) by
|
|
868
|
+
shape, not hue alone (WCAG 1.4.1). The ring carries the same 30% weight as
|
|
869
|
+
input's, from whichever hue the state supplied. */
|
|
770
870
|
box-shadow:
|
|
771
871
|
inset 0 0 0 2px var(--bg),
|
|
772
|
-
0 0 0 2px color-mix(in srgb, var(--
|
|
872
|
+
0 0 0 2px color-mix(in srgb, var(--dot-color) 30%, transparent);
|
|
773
873
|
}
|
|
774
874
|
|
|
775
875
|
/* The ripple layer disappears entirely (not just paused mid-wave). */
|
|
776
|
-
:where(.wt-root) .wt-status-dot[data-status="working"]::after
|
|
876
|
+
:where(.wt-root) .wt-status-dot[data-status="working"]::after,
|
|
877
|
+
:where(.wt-root) .wt-status-dot[data-status="warning"]::after,
|
|
878
|
+
:where(.wt-root) .wt-status-dot[data-status="failed"]::after {
|
|
777
879
|
content: none;
|
|
778
880
|
}
|
|
779
881
|
}
|
package/css/31-switcher.css
CHANGED
|
@@ -172,22 +172,20 @@
|
|
|
172
172
|
min-width: 0;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/* Ink-centring geometry, identical to .wt-tab-label: a symmetric ink allowance
|
|
176
|
+
the ellipsis clipping needs, plus a bottom margin of twice --label-ink-shift
|
|
177
|
+
that lifts the ink to the row centre. See 30-tabs.css for the derivation, for
|
|
178
|
+
why line-height must stay `normal` here, and for why this is margin arithmetic
|
|
179
|
+
over the bundled font rather than a text-box trim behind an @supports gate that
|
|
180
|
+
only one engine ever takes. */
|
|
175
181
|
:where(.wt-root) .wt-switcher-label {
|
|
176
182
|
flex: 1 1 auto;
|
|
177
183
|
overflow: hidden;
|
|
178
184
|
text-overflow: ellipsis;
|
|
179
185
|
white-space: nowrap;
|
|
180
186
|
text-align: left;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
/* Ink-centering trim + its mandatory descender allowance, same as .wt-tab-label
|
|
184
|
-
(see 30-tabs.css for why `ex`, why the padding is not optional, and why the
|
|
185
|
-
pair is @supports-gated rather than left to fall back). */
|
|
186
|
-
@supports (text-box: trim-both ex alphabetic) {
|
|
187
|
-
:where(.wt-root) .wt-switcher-label {
|
|
188
|
-
text-box: trim-both ex alphabetic;
|
|
189
|
-
padding-block: 0.5em;
|
|
190
|
-
}
|
|
187
|
+
padding-block: 0.125em;
|
|
188
|
+
margin-block: 0 calc(2 * var(--label-ink-shift));
|
|
191
189
|
}
|
|
192
190
|
|
|
193
191
|
/* Keyboard button: hidden until a keyboardToggle is wired; filled while the key
|
|
@@ -301,9 +299,12 @@
|
|
|
301
299
|
|
|
302
300
|
/* Latest-wins background-tab notification dot on the switch button — the moved
|
|
303
301
|
attention cue: the input colour when a background terminal needs input, the
|
|
304
|
-
done colour when a background turn finished
|
|
305
|
-
|
|
306
|
-
|
|
302
|
+
done colour when a background turn finished, the failed red when a background
|
|
303
|
+
process crashed. Those three only — the rule the user set is "only states that
|
|
304
|
+
want the user, like a notification", so the ongoing progress states (working /
|
|
305
|
+
warning / failed) never raise it. Reuses the exact .wt-status-dot rules
|
|
306
|
+
(30-tabs.css, --status-* tokens) so it matches the per-tab dots. Hidden until
|
|
307
|
+
the tabs feature sets data-status; cleared when the list opens
|
|
307
308
|
(expandSwitcher). */
|
|
308
309
|
:where(.wt-root) .wt-switcher-switch-dot {
|
|
309
310
|
position: absolute;
|
|
@@ -312,8 +313,9 @@
|
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
/* The dot carries the .wt-status-dot class, so its size, shape, and colour come
|
|
315
|
-
straight from the shared .wt-status-dot[data-status="input"|"done"]
|
|
316
|
-
(30-tabs.css) — the SAME dot colours every tab already uses, defined
|
|
316
|
+
straight from the shared .wt-status-dot[data-status="input"|"done"|"crashed"]
|
|
317
|
+
rules (30-tabs.css) — the SAME dot colours every tab already uses, defined
|
|
318
|
+
once. None of the three animates, so nothing here can pin a wave layer.
|
|
317
319
|
.wt-status-dot is display:none by default; force it visible only while a
|
|
318
320
|
notification is set (data-status present). */
|
|
319
321
|
:where(.wt-root) .wt-switcher-switch-dot[data-status] {
|
|
@@ -407,6 +409,10 @@
|
|
|
407
409
|
display: flex;
|
|
408
410
|
align-items: center;
|
|
409
411
|
gap: var(--sp-2);
|
|
412
|
+
/* Positioning context for this row's progress bar (.wt-progress-bar, defined
|
|
413
|
+
once with the shared dot vocabulary in 30-tabs.css), which rides the row's
|
|
414
|
+
own bottom edge. */
|
|
415
|
+
position: relative;
|
|
410
416
|
min-height: var(--touch-target);
|
|
411
417
|
/* Right padding reserves room for the close (x) overlaid inside the box. */
|
|
412
418
|
padding: 0 var(--touch-target) 0 var(--sp-2);
|
|
@@ -435,21 +441,19 @@
|
|
|
435
441
|
background: color-mix(in oklch, var(--tab-bg), var(--text) 15%);
|
|
436
442
|
}
|
|
437
443
|
|
|
444
|
+
/* Ink-centring geometry, identical to .wt-tab-label: a symmetric ink allowance
|
|
445
|
+
the ellipsis clipping needs, plus a bottom margin of twice --label-ink-shift
|
|
446
|
+
that lifts the ink to the row centre. See 30-tabs.css for the derivation, for
|
|
447
|
+
why line-height must stay `normal` here, and for why this is margin arithmetic
|
|
448
|
+
over the bundled font rather than a text-box trim behind an @supports gate that
|
|
449
|
+
only one engine ever takes. */
|
|
438
450
|
:where(.wt-root) .wt-switcher-row-label {
|
|
439
451
|
flex: 1 1 auto;
|
|
440
452
|
overflow: hidden;
|
|
441
453
|
text-overflow: ellipsis;
|
|
442
454
|
white-space: nowrap;
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
/* Ink-centering trim + its mandatory descender allowance, same as .wt-tab-label
|
|
446
|
-
(see 30-tabs.css for why `ex`, why the padding is not optional, and why the
|
|
447
|
-
pair is @supports-gated rather than left to fall back). */
|
|
448
|
-
@supports (text-box: trim-both ex alphabetic) {
|
|
449
|
-
:where(.wt-root) .wt-switcher-row-label {
|
|
450
|
-
text-box: trim-both ex alphabetic;
|
|
451
|
-
padding-block: 0.5em;
|
|
452
|
-
}
|
|
455
|
+
padding-block: 0.125em;
|
|
456
|
+
margin-block: 0 calc(2 * var(--label-ink-shift));
|
|
453
457
|
}
|
|
454
458
|
|
|
455
459
|
/* The close sits flush at the row's right edge (no gap), vertically centered. */
|