@cplieger/web-terminal-ui 7.0.6 → 7.0.7

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 CHANGED
@@ -358,8 +358,19 @@ completed feature and core listener, clears the broken subtree, and shows the
358
358
  panel. If `createTerminal` itself throws (`phase: "kernel-init"`: an
359
359
  unresolvable mount selector, a preset that throws, an invalid feature list, a DOM
360
360
  invariant), it shows the same panel and then rethrows, so a caller with its own
361
- error handling still sees the error. Either way the panel is modal when the
362
- terminal owns the viewport and non-modal when it fills an embedded container.
361
+ error handling still sees the error.
362
+
363
+ The panel is a native `<dialog>`. In `viewport` layout it opens with
364
+ `showModal()`, so the rest of your document is inert while it is up; in
365
+ `container` layout it opens non-modally, so the application around an embedded
366
+ terminal keeps working and keeps its focusables. Escape does not dismiss it in
367
+ either mode: the terminal is already gone, so there would be nothing behind it
368
+ and no way back. Reload is the recovery.
369
+
370
+ One visible consequence in `viewport` layout: a modal dialog is in the browser's
371
+ top layer, above every `z-index` in the document, so the panel now paints over
372
+ your loading overlay instead of under it. It appears at once and the overlay's
373
+ fade-out finishes behind it.
363
374
 
364
375
  **This is why `target` takes a selector and `features` takes a function.** Both
365
376
  are resolved INSIDE that boundary. Written the other way round
@@ -133,13 +133,26 @@
133
133
  Feature setup is part of the kernel composition contract. If it fails, the
134
134
  kernel removes the half-live terminal and fills only its own layout boundary
135
135
  with this recovery surface. In container mode that leaves the host app
136
- outside the terminal fully visible and interactive. */
136
+ outside the terminal fully visible and interactive.
137
+
138
+ The surface is a native <dialog>: showModal() in wt-viewport, the bare `open`
139
+ attribute in wt-container. Modality is the only difference between them. */
137
140
  :where(.wt-root) .wt-fatal {
138
141
  position: absolute;
139
142
  inset: 0;
140
- z-index: 100;
141
- display: grid;
142
- place-items: center;
143
+ /* The size resets are what let the insets stretch the panel: a <dialog> is
144
+ fit-content with the UA sheet's own `margin: auto`, and :modal additionally
145
+ caps both axes at calc(100% - 38px). margin, border-radius and box-shadow
146
+ answer an EMBEDDER's own `dialog { ... }` base rule rather than the UA sheet —
147
+ margin is the member that moves the box, the other two are cosmetic. */
148
+ margin: 0;
149
+ border: none;
150
+ border-radius: 0;
151
+ box-shadow: none;
152
+ inline-size: auto;
153
+ block-size: auto;
154
+ max-inline-size: none;
155
+ max-block-size: none;
143
156
  overflow: auto;
144
157
  padding: calc(var(--safe-top) + var(--sp-3)) var(--sp-3) calc(var(--safe-bottom) + var(--sp-3));
145
158
  background: var(--bg);
@@ -148,6 +161,39 @@
148
161
  text-align: center;
149
162
  }
150
163
 
164
+ /* Scoped to [open]: an author `display` beats the UA's `dialog { display: none }`
165
+ at any specificity, so declared unconditionally a CLOSED panel would still
166
+ paint its full-surface background over the root. */
167
+ :where(.wt-root) .wt-fatal[open] {
168
+ display: grid;
169
+ place-items: center;
170
+ }
171
+
172
+ /* The base rule's `position: absolute` serves the container half (it resolves
173
+ against .wt-root.wt-container, the embedder's box); this gives the viewport back
174
+ to the modal half. A top-layer absolute resolves against the initial containing
175
+ block — viewport-SIZED but canvas-anchored, so equivalent to fixed only at
176
+ scrollY 0 — and Blink does not lock document scrolling under a modal dialog.
177
+ Pinned by fatal-panel.test.ts's scrolled-document test. */
178
+ :where(.wt-root) .wt-fatal:modal {
179
+ position: fixed;
180
+ }
181
+
182
+ /* The stacking order the top layer supplies for free, so it is declared only for
183
+ the half that is not in it. .wt-root.wt-container is the EMBEDDER's element and
184
+ forms no stacking context, so the panel competes in the host's. */
185
+ :where(.wt-root) .wt-fatal:not(:modal) {
186
+ z-index: 100;
187
+ }
188
+
189
+ /* The panel IS the dim — it fills its whole surface in --bg — so the backdrop
190
+ agrees with it instead of adding a second, differently coloured layer. The UA's
191
+ rgba(0, 0, 0, 0.1) would show as a lighter seam if the panel were ever smaller
192
+ than the viewport. */
193
+ :where(.wt-root) .wt-fatal::backdrop {
194
+ background: var(--bg);
195
+ }
196
+
151
197
  :where(.wt-root) .wt-fatal-card {
152
198
  inline-size: min(100%, 28rem);
153
199
  padding: clamp(1.25rem, 5vi, 2rem);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "7.0.6",
3
+ "version": "7.0.7",
4
4
  "type": "module",
5
5
  "description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
6
6
  "license": "MPL-2.0",
@@ -55,13 +55,13 @@
55
55
  "@vitest/browser": "5.0.0",
56
56
  "@vitest/browser-playwright": "5.0.0",
57
57
  "@vitest/coverage-v8": "5.0.0",
58
- "eslint": "10.9.1",
58
+ "eslint": "10.10.0",
59
59
  "fast-check": "4.9.0",
60
60
  "html-validate": "11.14.0",
61
61
  "knip": "6.34.0",
62
- "playwright": "1.62.1",
62
+ "playwright": "1.63.0",
63
63
  "prettier": "3.9.6",
64
- "stylelint": "17.14.1",
64
+ "stylelint": "17.15.0",
65
65
  "stylelint-config-standard": "40.0.0",
66
66
  "typescript": "npm:@typescript/typescript6@6.0.2",
67
67
  "typescript-eslint": "8.69.0",
@@ -168,17 +168,18 @@ function renderFatalStartupInto(
168
168
  layoutMode: "viewport" | "container",
169
169
  message: string = STARTUP_FAILURE_COPY.message,
170
170
  ): void {
171
- const surface = document.createElement("section");
171
+ const surface = document.createElement("dialog");
172
172
  surface.className = "wt-fatal";
173
+ // alertdialog over the implicit `dialog` role: an urgent message to act on. No
174
+ // `aria-modal` — showModal() conveys modality; an authored value can contradict the UA.
173
175
  surface.setAttribute("role", "alertdialog");
174
176
  surface.setAttribute("aria-labelledby", "wt-fatal-title");
175
177
  surface.setAttribute("aria-describedby", "wt-fatal-message");
176
- if (layoutMode === "viewport") {
177
- // A full-page terminal has no usable host UI behind it, so this is the
178
- // page's modal recovery state. An embedded terminal is only one panel in
179
- // a larger app and must not claim that the rest of the app is inert.
180
- surface.setAttribute("aria-modal", "true");
181
- }
178
+ // showModal() makes Escape a close request, and the kernel has already cleared
179
+ // the root: nothing is behind this panel, so reload is the only recovery.
180
+ surface.addEventListener("cancel", (ev) => {
181
+ ev.preventDefault();
182
+ });
182
183
 
183
184
  const card = document.createElement("div");
184
185
  card.className = "wt-fatal-card";
@@ -201,9 +202,19 @@ function renderFatalStartupInto(
201
202
  surface.appendChild(card);
202
203
  root.replaceChildren(surface);
203
204
 
204
- // The terminal input held focus before setup failed. Move that focus to the
205
- // only recovery action; container mode remains non-modal because no trap or
206
- // aria-modal claim prevents the user from leaving the terminal panel.
205
+ // Viewport: the terminal IS the page, so the top layer and an inert document
206
+ // are both accurate; container gets the bare `open` attribute, which is neither
207
+ // and is what an embedded panel wants. `isConnected` is showModal()'s own
208
+ // precondition and a detached root arrives here from a typed call, where a
209
+ // throw would bury the real startup cause under a second error out of the catch.
210
+ if (layoutMode === "viewport" && surface.isConnected) {
211
+ surface.showModal();
212
+ } else {
213
+ surface.open = true;
214
+ }
215
+
216
+ // The terminal input held focus before setup failed; move it to the only
217
+ // recovery action. showModal() lands there itself, the non-modal half does not.
207
218
  reloadButton.focus();
208
219
  }
209
220
 
@@ -1888,21 +1899,6 @@ function buildTerminal(
1888
1899
  };
1889
1900
  }
1890
1901
 
1891
- /** Build the terminal UI inside `root`. Call exactly once.
1892
- *
1893
- * Wraps buildTerminal so that a SYNCHRONOUS startup failure gets the same
1894
- * treatment an asynchronous one already got. Before this, the two halves of
1895
- * "the terminal did not start" were handled inconsistently: an async
1896
- * feature-setup rejection ran enterFatalStartup (overlay dismissed,
1897
- * onFatalError delivered, recovery surface rendered), while a synchronous throw
1898
- * propagated bare, leaving the consumer's loading overlay spinning forever over
1899
- * a page with no terminal and no explanation. Consumers were left hand-building
1900
- * their own surface to cover the gap, diverging from this one in shape and copy.
1901
- *
1902
- * The error still propagates: a consumer with its own handling sees it exactly
1903
- * as before, only now against a page that is no longer stuck on a spinner. A
1904
- * handler returning `true` suppresses the built-in surface, same contract as the
1905
- * async phase. */
1906
1902
  /** Resolve the mount target. A selector is looked up here, INSIDE
1907
1903
  * createTerminal's try, which is the whole point: a consumer that passed
1908
1904
  * `document.getElementById("terminal")` had to null-check it first (tsc forces
@@ -1959,6 +1955,10 @@ function createFallbackHost(): HTMLElement {
1959
1955
  return host;
1960
1956
  }
1961
1957
 
1958
+ /** Build the terminal UI inside `target`. Call exactly once. Wraps buildTerminal so
1959
+ * a SYNCHRONOUS startup failure gets the same recovery surface an async one does
1960
+ * and still propagates; that surface is modal in viewport layout, so
1961
+ * `onFatalError` returning `true` is the path that leaves the page interactive. */
1962
1962
  export function createTerminal(
1963
1963
  target: HTMLElement | string,
1964
1964
  opts: CreateTerminalOptions = {},