@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 +13 -2
- package/css/10-primitives.css +50 -4
- package/package.json +4 -4
- package/src/kernel/kernel.ts +25 -25
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.
|
|
362
|
-
|
|
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
|
package/css/10-primitives.css
CHANGED
|
@@ -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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
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.
|
|
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
|
+
"playwright": "1.63.0",
|
|
63
63
|
"prettier": "3.9.6",
|
|
64
|
-
"stylelint": "17.
|
|
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",
|
package/src/kernel/kernel.ts
CHANGED
|
@@ -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("
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
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 = {},
|