@cplieger/web-terminal-ui 5.0.3 → 5.2.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 +63 -15
- package/css/00-tokens.css +53 -1
- package/css/30-tabs.css +194 -76
- package/css/31-switcher.css +29 -25
- package/css/page.css +8 -4
- package/package.json +4 -3
- package/src/features/activity-monitor.ts +4 -2
- package/src/features/tabs/index.ts +154 -39
- package/src/features/tabs/model.ts +184 -7
- package/src/features/tabs/notify.ts +265 -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,37 @@ 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,
|
|
147
|
+
shown only while one of those three progress states is the session's current
|
|
148
|
+
status. The number itself is announced in the tab's accessible name and drawn
|
|
149
|
+
nowhere: no terminal emulator puts a percentage next to a tab label, and a chip
|
|
150
|
+
that shrinks toward a 100px floor has no width to spare. It is likewise never
|
|
151
|
+
written to the browser document title, since one page title cannot represent
|
|
152
|
+
several sessions. An OSC 9 notification is posted as a browser
|
|
153
|
+
notification when the user is not already looking at that terminal (permission
|
|
154
|
+
is requested on a user gesture; a denial degrades silently to the tab dots).
|
|
155
|
+
Clicking one switches to the session that raised it: the Notifications API's
|
|
156
|
+
own click default already focuses the page, and the handler supplies the
|
|
157
|
+
in-page half. These are non-persistent notifications, so per the API the
|
|
158
|
+
constructor throws on most mobile browsers, where the notification degrades to
|
|
159
|
+
the tab dots.
|
|
139
160
|
|
|
140
161
|
### Options
|
|
141
162
|
|
|
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. |
|
|
163
|
+
| Option | Default | Purpose |
|
|
164
|
+
| -------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
165
|
+
| `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. |
|
|
166
|
+
| `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). |
|
|
167
|
+
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
|
|
168
|
+
| `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. |
|
|
169
|
+
| `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. |
|
|
170
|
+
| `onFatalError` | _(built-in recovery)_ | Called with a `TerminalStartupFailure` after a fatal startup failure, in either phase (`feature-setup` or `kernel-init`); behavior below. |
|
|
171
|
+
| `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
172
|
|
|
152
173
|
`createTerminal()` returns a handle: `focus()` re-focuses the terminal input
|
|
153
174
|
(and opens the soft keyboard on touch); `send(bytes)` sends bytes to the active
|
|
@@ -157,6 +178,33 @@ screen without injecting keystrokes (send a redraw keystroke yourself if you
|
|
|
157
178
|
want one, for example Ctrl+L); and `destroy()` tears every feature down and
|
|
158
179
|
releases the kernel.
|
|
159
180
|
|
|
181
|
+
### Themes that provably apply
|
|
182
|
+
|
|
183
|
+
`theme` is an open `Record<string, string>` — the kernel sets every key on the
|
|
184
|
+
terminal root verbatim — so a key this library renamed or retired becomes a live
|
|
185
|
+
declaration nothing reads: no error, just the library's neutral defaults where
|
|
186
|
+
your brand should be. The supported keys are therefore published as data:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { PUBLIC_THEME_TOKENS } from "@cplieger/web-terminal-ui/style-contract";
|
|
190
|
+
// or from the package root; the subpath imports nothing and touches no DOM,
|
|
191
|
+
// so a Node script can read it too.
|
|
192
|
+
|
|
193
|
+
for (const key of Object.keys(MY_THEME)) {
|
|
194
|
+
expect(PUBLIC_THEME_TOKENS).toContain(key); // your test, our list
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Every token on that list is guaranteed by this package's own suite to be both
|
|
199
|
+
declared AND read by a shipped rule, so setting it changes what renders. The
|
|
200
|
+
second half is the one you cannot check from outside: a token that is declared
|
|
201
|
+
but that no rule reads accepts your override and applies it to nothing. Anything
|
|
202
|
+
NOT on the list is internal and may be renamed without a release note.
|
|
203
|
+
|
|
204
|
+
`LOADING_OVERLAY_CLASSES` is published from the same module for the same reason:
|
|
205
|
+
your pre-JS overlay markup opts into `css/page.css`'s styling by class name, and
|
|
206
|
+
that markup usually lives in a static HTML file no compiler reads.
|
|
207
|
+
|
|
160
208
|
### Startup failures
|
|
161
209
|
|
|
162
210
|
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,34 @@
|
|
|
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;
|
|
84
|
+
/* ...and the clip that keeps that bar inside the chip. A 2px line at
|
|
85
|
+
`left: 0; bottom: 0` has square ends, so without this it pokes out through
|
|
86
|
+
both rounded bottom corners and reads as running edge to edge. The clip is
|
|
87
|
+
applied to the PADDING box with the chip's inner radius (--radius-inner less
|
|
88
|
+
the 1px border), which is exactly the shape the bar must follow, and it is
|
|
89
|
+
what the other two chip sites already do (.wt-switcher-current,
|
|
90
|
+
.wt-switcher-row-select). Nothing else in the chip needs to escape: the
|
|
91
|
+
dot's ripple stays within the --sp-2 padding at its visible amplitude, and
|
|
92
|
+
the rename field's 2px/1px-offset focus ring sits well inside it. */
|
|
93
|
+
overflow: hidden;
|
|
78
94
|
/* ONE height for every control in the strip: the chip, the "+" and the
|
|
79
95
|
keyboard button all resolve to --touch-target, so a chip and a button can
|
|
80
96
|
never disagree about how tall the row is. Before this the chip's height was
|
|
81
97
|
whatever its tallest child happened to be, and the "+" hard-coded a COPY of
|
|
82
98
|
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
|
-
|
|
99
|
+
for one row, which drifted the moment a child's box grew: the label's own
|
|
100
|
+
box once reached 31px, past the 24px close button, and the chip rendered
|
|
101
|
+
49px tall next to a 42px "+" and a 44px keyboard button. (That growth came
|
|
102
|
+
from a text-box trim's descender padding being applied on the untrimmed
|
|
103
|
+
code path, which no longer exists -- the label's geometry is now one
|
|
104
|
+
unconditional rule; see .wt-tab-label. The two-sources-of-truth lesson is
|
|
105
|
+
what survives.) --touch-target is also the tap-target floor the rest of
|
|
87
106
|
the chrome already respects, including this chip's mobile counterpart
|
|
88
107
|
(.wt-switcher-current in 31-switcher.css, same declaration).
|
|
89
108
|
|
|
@@ -174,57 +193,58 @@
|
|
|
174
193
|
}
|
|
175
194
|
|
|
176
195
|
/* 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)
|
|
196
|
+
(min-width: 0 lets a flex item shrink below its content width), and it owns the
|
|
197
|
+
ink-centring geometry the two switcher labels in 31-switcher.css share verbatim.
|
|
198
|
+
|
|
199
|
+
Centre the INK, not the line box. A font's metrics box reaches far higher above
|
|
200
|
+
the baseline than below it (the bundled font: 0.995em over, 0.250em under),
|
|
201
|
+
while the ink of a mixed-case title runs 0.7344em over (cap height) to 0.2188em
|
|
202
|
+
under (descender). Flex centring centres the BOX, which leaves the visible text
|
|
203
|
+
--label-ink-shift low — 0.1147em, 1.5px at this size — reading as
|
|
204
|
+
baseline-aligned with the activity dot's bottom edge instead of level with its
|
|
205
|
+
centre. Two declarations, one job, and a third that must NOT be added:
|
|
206
|
+
|
|
207
|
+
- padding-block is the ink allowance, not spacing. `overflow: hidden` (which
|
|
208
|
+
the ellipsis requires) clips at the padding box, and the line box clears the
|
|
209
|
+
tallest ink by only 0.6px, so an unpadded label shaves the top off every
|
|
210
|
+
acute accent and caron. 0.125em per side buys 2.2px of clearance at 13px, and
|
|
211
|
+
being symmetric it does not move the centred text.
|
|
212
|
+
- margin-block hangs TWICE the shift below the box. Flex centring centres the
|
|
213
|
+
MARGIN box, so a bottom-heavy margin lifts the border box by half of it --
|
|
214
|
+
exactly one shift -- and unlike padding it does not enlarge the clipping box.
|
|
215
|
+
- line-height stays at `normal`, deliberately. Setting it looks like the
|
|
216
|
+
responsible thing to do and it is the trap: `normal` is the ONE value whose
|
|
217
|
+
half-leading is zero by definition, which is what makes the baseline sit at
|
|
218
|
+
exactly the engine's ascent below the box top. Any other value leaves
|
|
219
|
+
(line-height - font box) / 2 to be halved and rounded, and Chromium floors
|
|
220
|
+
it -- measured with an explicit 1.25 the 14px switcher labels came out a
|
|
221
|
+
FULL pixel high (line box 17.5 under an 18px font box, so half-leading
|
|
222
|
+
-0.25 floored to -1) while the 13px strip label was fine, which is exactly
|
|
223
|
+
the kind of works-where-I-looked bug this rule exists to end.
|
|
224
|
+
|
|
225
|
+
What this deliberately does NOT use is `text-box: trim-both`, which all three
|
|
226
|
+
previous attempts were built on. It ships in Blink and WebKit only (Firefox has
|
|
227
|
+
it in Nightly behind layout.css.text-box.enabled), so it can only be applied
|
|
228
|
+
behind an @supports gate, and a gate means two engines render two different
|
|
229
|
+
geometries of which only the one the author is looking at ever gets tuned.
|
|
230
|
+
Measured on the gated version: Chromium centred to 0.02px while Firefox sat
|
|
231
|
+
1.50px low with its baseline half a pixel BELOW the dot. The keyword vocabulary
|
|
232
|
+
is too coarse for this text besides -- `cap alphabetic` centres the cap band and
|
|
233
|
+
reads 1.14px low on lowercase, `ex alphabetic` centres the x-band and reads high
|
|
234
|
+
on capitals, and a mixed-case tab title wants neither edge.
|
|
235
|
+
|
|
236
|
+
The residual error is engines rounding ascent and descent to whole pixels, which
|
|
237
|
+
makes the ideal shift 0.127em at 13px and 0.099em at 14px; the one authored
|
|
238
|
+
0.1147em is the unrounded truth and lands within 0.22px at both sizes, in every
|
|
239
|
+
engine, with no fork. */
|
|
178
240
|
:where(.wt-root) .wt-tab-label {
|
|
179
241
|
flex: 1 1 auto;
|
|
180
242
|
min-width: 0;
|
|
181
243
|
overflow: hidden;
|
|
182
244
|
white-space: nowrap;
|
|
183
245
|
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
|
-
}
|
|
246
|
+
padding-block: 0.125em;
|
|
247
|
+
margin-block: 0 calc(2 * var(--label-ink-shift));
|
|
228
248
|
}
|
|
229
249
|
|
|
230
250
|
/* A little breathing room after the activity dot, before the label. */
|
|
@@ -569,9 +589,13 @@
|
|
|
569
589
|
}
|
|
570
590
|
|
|
571
591
|
/* ===== Activity dots (activityMonitor via tabs) =====
|
|
572
|
-
|
|
573
|
-
per section 12 / WCAG 1.4.1
|
|
574
|
-
|
|
592
|
+
Eight-state, distinguishable without color (shape + motion, not hue alone),
|
|
593
|
+
per section 12 / WCAG 1.4.1: three ANIMATED progress states (working /
|
|
594
|
+
warning / failed — one object, three hues, spread across lightness so they
|
|
595
|
+
survive greyscale and deuteranopia), a frozen ringed disc (input), two static
|
|
596
|
+
discs (green done, red crashed), and two hollow ones (idle, dim exited).
|
|
597
|
+
The one .wt-status-dot vocabulary is shared by the desktop strip, the mobile
|
|
598
|
+
switcher, and the overview sheet rows.
|
|
575
599
|
|
|
576
600
|
Revealed PER TAB: the dot is hidden until its own session reports activity
|
|
577
601
|
(OSC 9;4 progress, or a classified OSC 9 notification), at which point the
|
|
@@ -595,6 +619,7 @@
|
|
|
595
619
|
from the filled done/working/input states (WCAG 1.4.1). */
|
|
596
620
|
background: transparent;
|
|
597
621
|
border: 1.5px solid color-mix(in oklch, var(--text) 45%, transparent);
|
|
622
|
+
|
|
598
623
|
/* Reset every transient visual owned by a status. In particular, a dot that
|
|
599
624
|
leaves working must stop its pulse, and a dot that enters working must not
|
|
600
625
|
carry the input ring or an exited opacity into the animated paint layer. */
|
|
@@ -603,6 +628,39 @@
|
|
|
603
628
|
animation: none;
|
|
604
629
|
}
|
|
605
630
|
|
|
631
|
+
/* ===== Determinate progress bar (OSC 9;4 percentage) =====
|
|
632
|
+
The second display mode the specs name for a progress report (iTerm2 calls it
|
|
633
|
+
a progress bar; ConEmu also puts the value on the taskbar, which is the
|
|
634
|
+
document-title prefix's job). One 2px line on the chip's bottom edge, shared
|
|
635
|
+
by all three chip sites (desktop strip chip, mobile active row, expanded list
|
|
636
|
+
row) because it ships in the one chipContent builder.
|
|
637
|
+
|
|
638
|
+
The ACCENT colour, not a per-state tint: the bar answers "how far", the dot
|
|
639
|
+
answers "in what state", and tinting the bar per state would say the same
|
|
640
|
+
thing twice while making a 2px line carry hue it cannot carry legibly.
|
|
641
|
+
|
|
642
|
+
Painted only while a percentage actually exists: the element carries the
|
|
643
|
+
`hidden` attribute otherwise (paintProgress), so an absent value (-1) renders
|
|
644
|
+
NO bar rather than a zero-width or empty one. Its width is the only inline
|
|
645
|
+
style — the percentage is data, not styling.
|
|
646
|
+
|
|
647
|
+
The rounded ENDS are the chip's job, not this rule's: each chip site clips its
|
|
648
|
+
own overflow (.wt-tab here, .wt-switcher-current and .wt-switcher-row-select in
|
|
649
|
+
31-switcher.css), so the bar is cut to the chip's inner corner radius at both
|
|
650
|
+
ends. A radius on the bar itself cannot do that job — a 2px-tall box scales
|
|
651
|
+
every radius down to 2px under the CSS overlap rule, so it would trace a 2px
|
|
652
|
+
curve against the chip's --radius-inner one and still square off wherever the
|
|
653
|
+
percentage stops short of the edge. */
|
|
654
|
+
:where(.wt-root) .wt-progress-bar {
|
|
655
|
+
position: absolute;
|
|
656
|
+
left: 0;
|
|
657
|
+
bottom: 0;
|
|
658
|
+
height: 2px;
|
|
659
|
+
max-width: 100%;
|
|
660
|
+
background: var(--accent);
|
|
661
|
+
pointer-events: none;
|
|
662
|
+
}
|
|
663
|
+
|
|
606
664
|
/* Reveal a dot only once its session has reported activity (the tabs feature
|
|
607
665
|
adds .wt-reports per dot; see paintStatusDot). Without it the dot — and the
|
|
608
666
|
whole activity vocabulary for that tab — stays invisible. */
|
|
@@ -629,14 +687,44 @@
|
|
|
629
687
|
opacity always multiplies onto its pseudos, so true independence requires
|
|
630
688
|
animating disc COLORS, not element opacity. Server-side this is driven by the
|
|
631
689
|
OSC 9;4 progress signal, so it tracks agent activity, not the user typing at
|
|
632
|
-
the prompt.
|
|
633
|
-
|
|
690
|
+
the prompt.
|
|
691
|
+
|
|
692
|
+
WORKING, WARNING AND FAILED SHARE THIS RULE, AND BOTH KEYFRAMES, VERBATIM.
|
|
693
|
+
OSC 9;4 models ONE progress indicator with several states (1/3 active,
|
|
694
|
+
4 warning, 2 error), so it renders as one visual object whose HUE carries the
|
|
695
|
+
state — not three unrelated dots. Each state therefore sets --dot-color and
|
|
696
|
+
changes nothing else: same disc, same glow beat, same travelling wave, same
|
|
697
|
+
period. Do NOT give warning or failed a ring — the frozen hard ring is
|
|
698
|
+
input's exclusive meaning ("blocked, waiting on you"), and neither of these
|
|
699
|
+
asks the user to act (see the input rule). Hue alone would not be enough on
|
|
700
|
+
its own, which is why the three tokens are also spread across lightness
|
|
701
|
+
(measured OKLab L: warning 86.1%, working 71.9%, failed 57.7% — see the
|
|
702
|
+
load-bearing note in 00-tokens.css). */
|
|
703
|
+
:where(.wt-root) .wt-status-dot[data-status="working"],
|
|
704
|
+
:where(.wt-root) .wt-status-dot[data-status="warning"],
|
|
705
|
+
:where(.wt-root) .wt-status-dot[data-status="failed"] {
|
|
634
706
|
position: relative;
|
|
635
|
-
background: var(--
|
|
636
|
-
border-color: var(--
|
|
707
|
+
background: var(--dot-color);
|
|
708
|
+
border-color: var(--dot-color);
|
|
637
709
|
animation: wt-working-glow 1.2s ease-in-out infinite;
|
|
638
710
|
}
|
|
639
711
|
|
|
712
|
+
/* The one difference between the three animated states. Kept as three
|
|
713
|
+
single-declaration rules so the shared paint above reads as literally shared:
|
|
714
|
+
a future change to the disc, the glow or the wave cannot land on one hue and
|
|
715
|
+
miss the other two. */
|
|
716
|
+
:where(.wt-root) .wt-status-dot[data-status="working"] {
|
|
717
|
+
--dot-color: var(--status-working);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
:where(.wt-root) .wt-status-dot[data-status="warning"] {
|
|
721
|
+
--dot-color: var(--status-warning);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
:where(.wt-root) .wt-status-dot[data-status="failed"] {
|
|
725
|
+
--dot-color: var(--status-failed);
|
|
726
|
+
}
|
|
727
|
+
|
|
640
728
|
/* The ripple layer: a feathered radial BAND (not a hairline border) that starts
|
|
641
729
|
hugging the disc's edge (inset -2px, the 9->13px box) and then travels
|
|
642
730
|
outward, dissolving. The band is painted as a radial-gradient — soft on both
|
|
@@ -645,16 +733,18 @@
|
|
|
645
733
|
which is what makes the wave visibly dissipate as it grows. transform +
|
|
646
734
|
opacity only, so it composites without repaints (a gradient background is
|
|
647
735
|
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.)
|
|
736
|
+
frame). It exists only in the three animated states — no stale layer to pin
|
|
737
|
+
when a dot changes status. (The switcher notification dot never shows any of
|
|
738
|
+
them — input/done/crashed only — so its position: absolute placement never
|
|
739
|
+
meets this rule's position: relative.)
|
|
652
740
|
|
|
653
741
|
The final scale is deliberately capped at today's 27px footprint: the mobile
|
|
654
742
|
overview row (.wt-switcher-row-select) clips at overflow: hidden 12.5px from
|
|
655
743
|
the dot's center, and a wave wider than ~25px picks up a visible straight
|
|
656
744
|
edge there. Visibility comes from the band's weight and timing, not size. */
|
|
657
|
-
:where(.wt-root) .wt-status-dot[data-status="working"]::after
|
|
745
|
+
:where(.wt-root) .wt-status-dot[data-status="working"]::after,
|
|
746
|
+
:where(.wt-root) .wt-status-dot[data-status="warning"]::after,
|
|
747
|
+
:where(.wt-root) .wt-status-dot[data-status="failed"]::after {
|
|
658
748
|
content: "";
|
|
659
749
|
position: absolute;
|
|
660
750
|
inset: -2px;
|
|
@@ -662,7 +752,7 @@
|
|
|
662
752
|
background: radial-gradient(
|
|
663
753
|
closest-side,
|
|
664
754
|
transparent 44%,
|
|
665
|
-
var(--
|
|
755
|
+
var(--dot-color) 76%,
|
|
666
756
|
transparent 100%
|
|
667
757
|
);
|
|
668
758
|
animation: wt-working-ripple 1.2s linear infinite;
|
|
@@ -702,23 +792,46 @@
|
|
|
702
792
|
opacity: 0.5;
|
|
703
793
|
}
|
|
704
794
|
|
|
795
|
+
/* Crashed (a non-zero exit status, or a signal the program was not asked for;
|
|
796
|
+
a server-initiated end is "exited", so a routine restart never paints red):
|
|
797
|
+
a SOLID dot in the failed red, static.
|
|
798
|
+
|
|
799
|
+
Solid, not hollow. There is no "hollow means no process" invariant to honour —
|
|
800
|
+
idle is hollow with a perfectly live process — so hollowness would only say
|
|
801
|
+
"unremarkable", and a crash is the least unremarkable thing this vocabulary
|
|
802
|
+
reports. It uses the SAME token as failed, because "the program reported an
|
|
803
|
+
error" and "the program died badly" are one family, and it separates from
|
|
804
|
+
failed by MOTION: failed pulses and emits the travelling wave, crashed is
|
|
805
|
+
completely still (no glow, no wave, no ring — nothing clears it, so any
|
|
806
|
+
animation would nag forever). That is the same axis that already separates
|
|
807
|
+
working from done. Under prefers-reduced-motion the separation survives as
|
|
808
|
+
SHAPE: failed degrades to a ringed donut (see the reduced-motion block), while
|
|
809
|
+
crashed stays a bare solid disc. */
|
|
810
|
+
:where(.wt-root) .wt-status-dot[data-status="crashed"] {
|
|
811
|
+
background: var(--status-failed);
|
|
812
|
+
border-color: var(--status-failed);
|
|
813
|
+
}
|
|
814
|
+
|
|
705
815
|
/* The disc's glow beat: base -> brighter -> base, then a rest until the next
|
|
706
816
|
cycle. Colors are set at EVERY frame, which both drives the glow and pins
|
|
707
817
|
the paint — a dot entering working in the same frame input/done ends can
|
|
708
818
|
never re-pulse the previously rasterized paint (the hazard the old
|
|
709
819
|
opacity-only pulse had to pin box-shadow against). Element opacity stays 1
|
|
710
|
-
throughout; see the layer-independence note on the working rule.
|
|
820
|
+
throughout; see the layer-independence note on the working rule.
|
|
821
|
+
|
|
822
|
+
--dot-color, not --status-working: this beat is shared verbatim by working,
|
|
823
|
+
warning and failed, each of which supplies its own hue. */
|
|
711
824
|
@keyframes wt-working-glow {
|
|
712
825
|
0%,
|
|
713
826
|
70%,
|
|
714
827
|
100% {
|
|
715
|
-
background-color: var(--
|
|
716
|
-
border-color: var(--
|
|
828
|
+
background-color: var(--dot-color);
|
|
829
|
+
border-color: var(--dot-color);
|
|
717
830
|
}
|
|
718
831
|
|
|
719
832
|
35% {
|
|
720
|
-
background-color: color-mix(in oklch, var(--
|
|
721
|
-
border-color: color-mix(in oklch, var(--
|
|
833
|
+
background-color: color-mix(in oklch, var(--dot-color) 78%, #fff);
|
|
834
|
+
border-color: color-mix(in oklch, var(--dot-color) 78%, #fff);
|
|
722
835
|
}
|
|
723
836
|
}
|
|
724
837
|
|
|
@@ -758,22 +871,27 @@
|
|
|
758
871
|
}
|
|
759
872
|
}
|
|
760
873
|
|
|
761
|
-
/* Reduced-motion: drop the
|
|
874
|
+
/* Reduced-motion: drop the animated states' motion (glow + ripple), keep color.
|
|
875
|
+
All three degrade the same way, since they are one object with three hues. */
|
|
762
876
|
@media (prefers-reduced-motion: reduce) {
|
|
763
|
-
:where(.wt-root) .wt-status-dot[data-status="working"]
|
|
877
|
+
:where(.wt-root) .wt-status-dot[data-status="working"],
|
|
878
|
+
:where(.wt-root) .wt-status-dot[data-status="warning"],
|
|
879
|
+
:where(.wt-root) .wt-status-dot[data-status="failed"] {
|
|
764
880
|
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
|
-
|
|
881
|
+
/* Motion is what separates the animated states from input's frozen ping;
|
|
882
|
+
with it removed, punch an inner hole (dot -> donut) and add a static ring
|
|
883
|
+
so they differ from input (solid ringed disc) and done (bare disc) by
|
|
884
|
+
shape, not hue alone (WCAG 1.4.1). The ring carries the same 30% weight as
|
|
885
|
+
input's, from whichever hue the state supplied. */
|
|
770
886
|
box-shadow:
|
|
771
887
|
inset 0 0 0 2px var(--bg),
|
|
772
|
-
0 0 0 2px color-mix(in srgb, var(--
|
|
888
|
+
0 0 0 2px color-mix(in srgb, var(--dot-color) 30%, transparent);
|
|
773
889
|
}
|
|
774
890
|
|
|
775
891
|
/* The ripple layer disappears entirely (not just paused mid-wave). */
|
|
776
|
-
:where(.wt-root) .wt-status-dot[data-status="working"]::after
|
|
892
|
+
:where(.wt-root) .wt-status-dot[data-status="working"]::after,
|
|
893
|
+
:where(.wt-root) .wt-status-dot[data-status="warning"]::after,
|
|
894
|
+
:where(.wt-root) .wt-status-dot[data-status="failed"]::after {
|
|
777
895
|
content: none;
|
|
778
896
|
}
|
|
779
897
|
}
|