@cplieger/web-terminal-ui 7.3.2 → 8.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.
Files changed (48) hide show
  1. package/README.md +114 -18
  2. package/css/30-tabs.css +21 -0
  3. package/css/31-switcher.css +15 -0
  4. package/css/32-split.css +176 -0
  5. package/css/MANIFEST +1 -0
  6. package/css/MANIFEST.tabbed +1 -0
  7. package/package.json +13 -12
  8. package/src/composition.ts +165 -237
  9. package/src/features/activity-monitor.ts +20 -55
  10. package/src/features/animations.ts +12 -30
  11. package/src/features/clipboard.ts +36 -64
  12. package/src/features/connection-banner.ts +10 -19
  13. package/src/features/context-menu.ts +70 -145
  14. package/src/features/dom.ts +44 -54
  15. package/src/features/menu-position.ts +24 -28
  16. package/src/features/mobile-toolbar.ts +32 -62
  17. package/src/features/predictive-echo.ts +15 -49
  18. package/src/features/scroll-to-bottom.ts +12 -24
  19. package/src/features/tabs/index.ts +1398 -1806
  20. package/src/features/tabs/ink-centre.ts +45 -54
  21. package/src/features/tabs/model.ts +301 -383
  22. package/src/features/tabs/strip.ts +45 -97
  23. package/src/features/tabs/switcher.ts +70 -48
  24. package/src/index.ts +13 -13
  25. package/src/kernel/a11y.ts +106 -27
  26. package/src/kernel/attention.ts +157 -0
  27. package/src/kernel/conn-state.ts +28 -22
  28. package/src/kernel/fatal.ts +76 -0
  29. package/src/kernel/feature-host.ts +136 -0
  30. package/src/kernel/gesture.ts +9 -13
  31. package/src/kernel/kernel.ts +57 -2139
  32. package/src/kernel/layout-policy.ts +44 -0
  33. package/src/kernel/loading-status.ts +30 -88
  34. package/src/kernel/navigation.ts +11 -17
  35. package/src/kernel/notify.ts +158 -0
  36. package/src/kernel/pane.ts +1415 -0
  37. package/src/kernel/realm.ts +11 -0
  38. package/src/kernel/regions.ts +8 -11
  39. package/src/kernel/scrollback.ts +88 -168
  40. package/src/kernel/selection.ts +25 -0
  41. package/src/kernel/shell.ts +1434 -0
  42. package/src/kernel/split-handle.ts +280 -0
  43. package/src/kernel/status-share.ts +100 -0
  44. package/src/kernel/types.ts +457 -441
  45. package/src/predict.ts +144 -170
  46. package/src/viewport.ts +124 -197
  47. package/src/features/tabs/attention.ts +0 -250
  48. package/src/features/tabs/notify.ts +0 -265
@@ -1,11 +1,9 @@
1
- // Optical centring for the three chip labels, measured in the engine rather than
2
- // predicted from the font's tables. Flex centring centres the LINE BOX, and a
3
- // font's box reaches further above the baseline than below it, so the text sits
4
- // low; the correction is the gap from the box's centre to the CAP band's (cap
5
- // top to baseline, the `text-box-edge: cap alphabetic` edges; a band including
6
- // descender ink reads high beside the dot and close glyph). Measured, not a
7
- // constant, because engines round ascent and descent to whole CSS pixels before
8
- // building the line box, and round differently per engine and platform.
1
+ // Optical centring for the chip labels, MEASURED in the engine: flex centring
2
+ // centres the LINE BOX, whose font reaches further above the baseline than below,
3
+ // so text sits low, and the correction is the gap from the box's centre to the cap
4
+ // band's. Measured because engines round ascent and descent to whole CSS pixels
5
+ // before building the line box, differently per engine and platform.
6
+ import { windowOf } from "../../kernel/realm.js";
9
7
  import { fromHTML } from "../dom.js";
10
8
 
11
9
  /** Ink extents are measured at this size and scaled back to em: WebKit and Blink
@@ -50,18 +48,25 @@ export function inkShiftPx(m: InkMetrics): number {
50
48
  return capCentre - m.fontBoxPx / 2;
51
49
  }
52
50
 
53
- let ctx2d: CanvasRenderingContext2D | null | undefined;
51
+ // One measuring canvas per document: a canvas resolves font families against
52
+ // its own document's faces, so an outer canvas would not see a face the terminal's
53
+ // document loaded.
54
+ const canvases = new WeakMap<Document, CanvasRenderingContext2D | null>();
54
55
 
55
- function canvas2d(): CanvasRenderingContext2D | null {
56
- ctx2d ??= document.createElement("canvas").getContext("2d");
57
- return ctx2d;
56
+ function canvas2d(doc: Document): CanvasRenderingContext2D | null {
57
+ let ctx = canvases.get(doc);
58
+ if (ctx === undefined) {
59
+ ctx = doc.createElement("canvas").getContext("2d");
60
+ canvases.set(doc, ctx);
61
+ }
62
+ return ctx;
58
63
  }
59
64
 
60
65
  /** The font's own cap ink, in em. Canvas rather than the DOM because only
61
66
  * TextMetrics reports where the glyphs' ink actually starts and stops; a DOM
62
67
  * rect only ever reports the box around it. */
63
- function inkExtents(font: string): Pick<InkMetrics, "capInkEm"> | null {
64
- const ctx = canvas2d();
68
+ function inkExtents(doc: Document, font: string): Pick<InkMetrics, "capInkEm"> | null {
69
+ const ctx = canvas2d(doc);
65
70
  if (!ctx) {
66
71
  return null;
67
72
  }
@@ -78,12 +83,13 @@ function inkExtents(font: string): Pick<InkMetrics, "capInkEm"> | null {
78
83
  * sits ON the baseline by definition — the only thing in the DOM that reports
79
84
  * a baseline position. */
80
85
  function lineBox(label: HTMLElement): Pick<InkMetrics, "fontBoxPx" | "baselinePx"> | null {
81
- const line = document.createElement("span");
86
+ const doc = label.ownerDocument;
87
+ const line = doc.createElement("span");
82
88
  line.style.position = "absolute";
83
89
  line.style.whiteSpace = "pre";
84
90
  line.style.font = "inherit";
85
91
  line.textContent = PROBE_TEXT;
86
- const strut = document.createElement("span");
92
+ const strut = doc.createElement("span");
87
93
  strut.style.display = "inline-block";
88
94
  strut.style.width = "0";
89
95
  strut.style.height = "0";
@@ -99,37 +105,32 @@ function lineBox(label: HTMLElement): Pick<InkMetrics, "fontBoxPx" | "baselinePx
99
105
  }
100
106
 
101
107
  /** Measures one site and returns its shift plus the font signature it belongs
102
- * to. Returns null when the resolved font is unchanged since `cached` (the
103
- * resize path, which then does no ink or line-box work at all) and when the
104
- * site cannot be measured at all (no layout, no canvas, a display: none
105
- * probe) — callers leave the CSS default in place either way rather than
106
- * writing a wrong number.
107
- *
108
- * The probe lives only for the duration of this synchronous call, deliberately.
109
- * Keeping a permanent hidden one would save a DOM mutation per resize, and it
110
- * would also put a second `.wt-tab` inside the root forever — which the strip's
111
- * test suite counts as a tab (`root.querySelectorAll(".wt-tab")`). Transient
112
- * means no other code, and no assertion, can ever observe it. */
108
+ * to; null when the resolved font is unchanged since `cached` or the site
109
+ * cannot be measured (no layout, no canvas, a display: none probe), and callers
110
+ * then leave the CSS default in place. The probe lives only for this
111
+ * synchronous call: a permanent hidden one would be a second `.wt-tab` inside
112
+ * the root that every `querySelectorAll(".wt-tab")` counts. */
113
113
  function measureSite(
114
114
  varRoot: HTMLElement,
115
115
  probeHTML: string,
116
116
  cached: string | undefined,
117
117
  ): { shiftPx: number; signature: string } | null {
118
- const wrap = document.createElement("div");
118
+ const doc = varRoot.ownerDocument;
119
+ const wrap = doc.createElement("div");
119
120
  wrap.setAttribute("aria-hidden", "true");
120
121
  wrap.style.position = "absolute";
121
122
  wrap.style.top = "0";
122
123
  wrap.style.left = "0";
123
124
  wrap.style.visibility = "hidden";
124
125
  wrap.style.pointerEvents = "none";
125
- wrap.appendChild(fromHTML(probeHTML));
126
+ wrap.appendChild(fromHTML(doc, probeHTML));
126
127
  varRoot.appendChild(wrap);
127
128
  try {
128
129
  const label = wrap.querySelector<HTMLElement>(`.${PROBE_CLASS}`);
129
130
  if (!label) {
130
131
  return null;
131
132
  }
132
- const cs = getComputedStyle(label);
133
+ const cs = windowOf(doc).getComputedStyle(label);
133
134
  const signature = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
134
135
  if (signature === cached) {
135
136
  return null;
@@ -138,7 +139,7 @@ function measureSite(
138
139
  if (!(fontSizePx > 0)) {
139
140
  return null;
140
141
  }
141
- const ink = inkExtents(`${cs.fontStyle} ${cs.fontWeight} ${REF_PX}px ${cs.fontFamily}`);
142
+ const ink = inkExtents(doc, `${cs.fontStyle} ${cs.fontWeight} ${REF_PX}px ${cs.fontFamily}`);
142
143
  const box = lineBox(label);
143
144
  if (!ink || !box) {
144
145
  return null;
@@ -162,26 +163,14 @@ interface Site {
162
163
  readonly probeHTML: string;
163
164
  }
164
165
 
165
- /**
166
- * Writes a measured --label-ink-shift onto the tab strip and the mobile
167
- * switcher, and keeps it current.
168
- *
169
- * Re-measures on two kinds of event, which need two different policies:
170
- *
171
- * - A font finishing (or failing) to load. The first pass can run inside
172
- * `font-display: block`, where the line box is still the fallback's, and the
173
- * real face can change every number here. These FORCE a re-measure, because
174
- * the signature below cannot see them: computed style reports the DECLARED
175
- * font-family list whether or not the webfont ever arrived, so a cached
176
- * signature would match and pin the fallback's shift permanently. (It did,
177
- * until scripts/verify-chip-geometry.mjs caught it.)
178
- * - A resize. rem-relative label sizes move with Safari's per-site page zoom,
179
- * and a new size means new rounding; but most resizes change neither, so this
180
- * path keeps the signature check and costs one computed-style read per site
181
- * when nothing moved.
182
- *
183
- * @returns teardown that drops the listeners and the written properties.
184
- */
166
+ /** Writes a measured --label-ink-shift onto the tab strip and the mobile
167
+ * switcher, and keeps it current. A font load or failure FORCES a re-measure:
168
+ * the first pass can run inside `font-display: block` on the fallback's line
169
+ * box, and computed style reports the DECLARED font-family list either way, so
170
+ * a cached signature would pin the fallback's shift for good. A resize keeps
171
+ * the signature check (rem sizes move with Safari's per-site zoom, but most
172
+ * resizes change nothing) and costs one computed-style read per site.
173
+ * @returns teardown that drops the listeners and the written properties. */
185
174
  export function centreChipLabels(
186
175
  varRoot: HTMLElement,
187
176
  hosts: { readonly strip: HTMLElement; readonly switcher: HTMLElement },
@@ -219,20 +208,22 @@ export function centreChipLabels(
219
208
  };
220
209
 
221
210
  remeasure(false);
211
+ const doc = varRoot.ownerDocument;
212
+ const win = windowOf(doc);
222
213
  // The DOM lib types document.fonts as always present; an engine without the CSS
223
214
  // Font Loading API ships no FontFaceSet at all, so the honest type is the
224
215
  // optional one.
225
- const fonts = document.fonts as FontFaceSet | undefined;
216
+ const fonts = doc.fonts as FontFaceSet | undefined;
226
217
  // `ready` settles the initial load (including a failed one); `loadingdone`
227
218
  // covers a face the host page adds afterwards, which `ready` never reports.
228
219
  void fonts?.ready.then(onFontsDone);
229
220
  fonts?.addEventListener("loadingdone", onFontsDone);
230
- window.addEventListener("resize", onResize);
221
+ win.addEventListener("resize", onResize);
231
222
 
232
223
  return () => {
233
224
  disposed = true;
234
225
  fonts?.removeEventListener("loadingdone", onFontsDone);
235
- window.removeEventListener("resize", onResize);
226
+ win.removeEventListener("resize", onResize);
236
227
  for (const site of sites) {
237
228
  site.host.style.removeProperty("--label-ink-shift");
238
229
  }