@cplieger/web-terminal-engine 1.2.0 → 1.3.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/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "exports": {
5
5
  ".": "./src/index.ts"
6
6
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "typecheck": "tsgo -p tsconfig.json",
@@ -10,10 +10,10 @@
10
10
  "devDependencies": {
11
11
  "@eslint/js": "10.0.1",
12
12
  "@vitest/coverage-v8": "4.1.9",
13
- "eslint": "10.5.0",
13
+ "eslint": "10.6.0",
14
14
  "fast-check": "4.8.0",
15
15
  "happy-dom": "20.10.6",
16
- "prettier": "3.8.4",
16
+ "prettier": "3.8.5",
17
17
  "typescript": "6.0.3",
18
18
  "typescript-eslint": "8.62.0",
19
19
  "vitest": "4.1.9"
package/src/keyboard.ts CHANGED
@@ -470,7 +470,7 @@ export interface MobileToolbarController {
470
470
  * would garble it.
471
471
  */
472
472
  applyStickyCtrl(text: string): string;
473
- /** Programmatically arm/disarm Ctrl. Updates the toolbar button visuals + fires `onCtrlChange`. */
473
+ /** Programmatically arm/disarm Ctrl. Updates the toolbar button visuals (`.armed` class + `aria-pressed`) + fires `onCtrlChange`. */
474
474
  setCtrlArmed(on: boolean): void;
475
475
  /** Whether sticky-Ctrl is currently armed. */
476
476
  isCtrlArmed(): boolean;
@@ -513,20 +513,28 @@ export function bindMobileToolbar(opts: BindMobileToolbarOptions): MobileToolbar
513
513
  const enterBtn = findBtn(ids.enter);
514
514
  const escBtn = findBtn(ids.esc);
515
515
 
516
+ function paintCtrlBtn(on: boolean): void {
517
+ if (!ctrlBtn) {
518
+ return;
519
+ }
520
+ ctrlBtn.classList.toggle("armed", on);
521
+ // Keep the ARIA toggle state in sync with the visual `.armed` class so
522
+ // assistive tech announces the sticky-Ctrl button as pressed/unpressed.
523
+ // The scaffold ships kb-ctrl as `aria-pressed="false"`; consumers used to
524
+ // own this in their own setCtrlArmed before delegating the toolbar here.
525
+ ctrlBtn.setAttribute("aria-pressed", on ? "true" : "false");
526
+ }
527
+
516
528
  function setCtrlArmed(on: boolean): void {
517
529
  if (armed === on) {
518
530
  // Still update visuals defensively (e.g. after dispose was called
519
531
  // and someone re-armed via setCtrlArmed) — but skip the change
520
532
  // notification to keep onCtrlChange edge-triggered.
521
- if (ctrlBtn) {
522
- ctrlBtn.classList.toggle("armed", on);
523
- }
533
+ paintCtrlBtn(on);
524
534
  return;
525
535
  }
526
536
  armed = on;
527
- if (ctrlBtn) {
528
- ctrlBtn.classList.toggle("armed", on);
529
- }
537
+ paintCtrlBtn(on);
530
538
  opts.onCtrlChange?.(on);
531
539
  }
532
540
 
package/src/render.ts CHANGED
@@ -178,6 +178,12 @@ export function init(opts: {
178
178
  rowEls.clear();
179
179
  renderQueue.clear();
180
180
  trimMarkerEl = null;
181
+ // Drop the predicted-cursor overlay (re-created lazily against the new
182
+ // termWrap on the next setPredictedCursor call) so re-init starts clean.
183
+ if (predCursorEl) {
184
+ predCursorEl.remove();
185
+ predCursorEl = null;
186
+ }
181
187
  output.replaceChildren();
182
188
  cursorAbs = -1;
183
189
  if (pendingFrame !== undefined) {
@@ -785,16 +791,28 @@ export function getCursorPx(): { left: number; top: number; cellH: number } {
785
791
 
786
792
  let predCursorEl: HTMLElement | null = null;
787
793
 
794
+ // Create the predicted-cursor overlay the renderer owns. Appended to termWrap
795
+ // (the positioned scroll container) so its absolute left/top math matches the
796
+ // row offsets. Styled by the `.pred-cursor` class from the UI CSS bundle. The
797
+ // renderer owning this means the engine never depends on a host-provided
798
+ // `#pred-cursor` scaffold element.
799
+ function createPredCursorEl(): HTMLElement {
800
+ const el = document.createElement("div");
801
+ el.className = "pred-cursor";
802
+ el.setAttribute("aria-hidden", "true");
803
+ termWrap.appendChild(el);
804
+ return el;
805
+ }
806
+
788
807
  /**
789
808
  * Show or hide a "predicted" cursor overlay at window-relative (row, col).
790
809
  * Useful for client-side echo of typed characters before the server
791
- * acknowledges them, over high-latency connections.
810
+ * acknowledges them, over high-latency connections. The overlay element is
811
+ * created lazily on first use (a consumer that never predicts never creates
812
+ * it).
792
813
  */
793
814
  export function setPredictedCursor(row: number, col: number, active: boolean): void {
794
- const el = predCursorEl ?? (predCursorEl = document.getElementById("pred-cursor"));
795
- if (!el) {
796
- return;
797
- }
815
+ const el = predCursorEl ?? (predCursorEl = createPredCursorEl());
798
816
  const win = store.getWindow();
799
817
  const predAbs = win.base + row;
800
818
  if (!active || (predAbs === cursorAbs && col === cursorCol)) {