@cplieger/web-terminal-ui 5.1.0 → 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 +12 -4
- package/css/30-tabs.css +19 -3
- package/package.json +2 -2
- package/src/features/tabs/index.ts +26 -18
- package/src/features/tabs/model.ts +50 -32
- package/src/features/tabs/notify.ts +57 -8
package/README.md
CHANGED
|
@@ -143,12 +143,20 @@ are importable from `…/features/<name>` (`clipboard`, `context-menu`,
|
|
|
143
143
|
(working, warning, error), the frozen ringed needs-input dot, two static discs
|
|
144
144
|
(a green finished turn, a red crashed process), and two hollow ones (idle and a
|
|
145
145
|
dim ended session). A
|
|
146
|
-
reported percentage additionally renders a 2px determinate bar on the chip
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
150
153
|
notification when the user is not already looking at that terminal (permission
|
|
151
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.
|
|
152
160
|
|
|
153
161
|
### Options
|
|
154
162
|
|
package/css/30-tabs.css
CHANGED
|
@@ -81,6 +81,16 @@
|
|
|
81
81
|
overlays (.wt-tab-ghost, .wt-tab-menu) are children of .wt-root, not of a
|
|
82
82
|
chip, so this cannot capture them. */
|
|
83
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;
|
|
84
94
|
/* ONE height for every control in the strip: the chip, the "+" and the
|
|
85
95
|
keyboard button all resolve to --touch-target, so a chip and a button can
|
|
86
96
|
never disagree about how tall the row is. Before this the chip's height was
|
|
@@ -632,7 +642,15 @@
|
|
|
632
642
|
Painted only while a percentage actually exists: the element carries the
|
|
633
643
|
`hidden` attribute otherwise (paintProgress), so an absent value (-1) renders
|
|
634
644
|
NO bar rather than a zero-width or empty one. Its width is the only inline
|
|
635
|
-
style — the percentage is data, not styling.
|
|
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. */
|
|
636
654
|
:where(.wt-root) .wt-progress-bar {
|
|
637
655
|
position: absolute;
|
|
638
656
|
left: 0;
|
|
@@ -640,8 +658,6 @@
|
|
|
640
658
|
height: 2px;
|
|
641
659
|
max-width: 100%;
|
|
642
660
|
background: var(--accent);
|
|
643
|
-
/* Follow the chip's rounded bottom-left corner instead of poking out of it. */
|
|
644
|
-
border-bottom-left-radius: inherit;
|
|
645
661
|
pointer-events: none;
|
|
646
662
|
}
|
|
647
663
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
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": "GPL-3.0-or-later",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"eslint": "10.8.0",
|
|
57
57
|
"fast-check": "4.9.0",
|
|
58
58
|
"happy-dom": "20.11.1",
|
|
59
|
-
"html-validate": "11.
|
|
59
|
+
"html-validate": "11.6.0",
|
|
60
60
|
"knip": "6.29.0",
|
|
61
61
|
"prettier": "3.9.6",
|
|
62
62
|
"stylelint": "17.14.1",
|
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
orderedInsertIndex,
|
|
35
35
|
parseCueSeen,
|
|
36
36
|
parseTabOrder,
|
|
37
|
-
progressLabel,
|
|
38
37
|
renderedProgress,
|
|
39
38
|
sanitizePinnedName,
|
|
40
39
|
serializeCueSeen,
|
|
@@ -760,12 +759,13 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
760
759
|
}
|
|
761
760
|
}
|
|
762
761
|
t.display = display;
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
// cannot tell a working tab from a crashed one
|
|
768
|
-
|
|
762
|
+
t.label.textContent = display;
|
|
763
|
+
// The accessible name carries the state and the percentage as well as
|
|
764
|
+
// the label: the dots are aria-hidden decoration and the progress bar
|
|
765
|
+
// is a 2px line with no text, so without this a screen-reader user
|
|
766
|
+
// cannot tell a working tab from a crashed one, or hear how far along
|
|
767
|
+
// a determinate one is.
|
|
768
|
+
t.aria.setLabel(tabAccessibleName(display, statusOf(t), shownProgress(t)));
|
|
769
769
|
}
|
|
770
770
|
}
|
|
771
771
|
|
|
@@ -777,10 +777,10 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
777
777
|
}
|
|
778
778
|
|
|
779
779
|
/** shownProgress is a tab's percentage as it may currently be DISPLAYED:
|
|
780
|
-
* the last value the server reported, dropped
|
|
781
|
-
* Those are the only two ways a percentage
|
|
782
|
-
* own OSC 9;4;0 (an explicit -1) and
|
|
783
|
-
* renderedProgress for why there is no third. */
|
|
780
|
+
* the last value the server reported, dropped unless the current status is
|
|
781
|
+
* one the progress channel owns. Those are the only two ways a percentage
|
|
782
|
+
* stops showing — the program's own OSC 9;4;0 (an explicit -1) and a status
|
|
783
|
+
* from another channel; see renderedProgress for why there is no third. */
|
|
784
784
|
function shownProgress(t: Tab): number {
|
|
785
785
|
return renderedProgress(statusOf(t), t.progress);
|
|
786
786
|
}
|
|
@@ -876,7 +876,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
876
876
|
function syncMobile(): void {
|
|
877
877
|
const idx = tabList.findIndex((t) => t.id === activeId);
|
|
878
878
|
const active = idx >= 0 ? tabList[idx] : undefined;
|
|
879
|
-
swLabel.textContent = active ?
|
|
879
|
+
swLabel.textContent = active ? active.display : "";
|
|
880
880
|
paintStatusDot(swDot, active ? statusOf(active) : "idle", active?.reports ?? false);
|
|
881
881
|
paintProgress(swProgress, active ? shownProgress(active) : PROGRESS_ABSENT);
|
|
882
882
|
// The aggregate background-notification cue rides the dedicated switch
|
|
@@ -904,10 +904,7 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
904
904
|
function updateRow(row: HTMLElement, t: Tab): void {
|
|
905
905
|
paintStatusDot(pick(row, ".wt-switcher-row-dot"), statusOf(t), t.reports);
|
|
906
906
|
paintProgress(pick(row, ".wt-progress-bar"), shownProgress(t));
|
|
907
|
-
pick(row, ".wt-switcher-row-label").textContent =
|
|
908
|
-
t.display,
|
|
909
|
-
shownProgress(t),
|
|
910
|
-
);
|
|
907
|
+
pick(row, ".wt-switcher-row-label").textContent = t.display;
|
|
911
908
|
}
|
|
912
909
|
// renderSwitcherList reconciles the expanded list to a row per OTHER tab
|
|
913
910
|
// (the active tab lives in the bar row), REUSING existing row elements
|
|
@@ -1309,8 +1306,10 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
1309
1306
|
// Set an initial label immediately (relabelAll refines it with de-dup
|
|
1310
1307
|
// once the tab is in tabList and syncChrome runs).
|
|
1311
1308
|
tab.display = baseLabel(tab).text;
|
|
1312
|
-
label.textContent =
|
|
1313
|
-
aria.setLabel(
|
|
1309
|
+
label.textContent = tab.display;
|
|
1310
|
+
aria.setLabel(
|
|
1311
|
+
tabAccessibleName(tab.display, info.status, renderedProgress(info.status, tab.progress)),
|
|
1312
|
+
);
|
|
1314
1313
|
el.addEventListener("click", (e) => {
|
|
1315
1314
|
if ((e.target as HTMLElement).closest(".wt-tab-close")) {
|
|
1316
1315
|
return; // handled by the close button
|
|
@@ -2575,6 +2574,15 @@ export function tabs(opts: TabsOptions = {}): TerminalFeature<TabsApi> {
|
|
|
2575
2574
|
notifier.deliver(s, {
|
|
2576
2575
|
sessionIsActive: s.id === activeId,
|
|
2577
2576
|
label: tabList.find((t) => t.id === s.id)?.display ?? s.title,
|
|
2577
|
+
// What clicking the notification does. The platform's own click
|
|
2578
|
+
// default focuses the page; this supplies the other half, landing the
|
|
2579
|
+
// user on the session that raised the notification rather than on
|
|
2580
|
+
// whichever tab they last left active. switchTo already no-ops on an
|
|
2581
|
+
// id that is gone, which is the case that matters here: the prompt may
|
|
2582
|
+
// have been answered, or the tab closed, between post and click.
|
|
2583
|
+
activate: () => {
|
|
2584
|
+
switchTo(s.id);
|
|
2585
|
+
},
|
|
2578
2586
|
});
|
|
2579
2587
|
});
|
|
2580
2588
|
offStreamOpen = monitor.onStreamOpen?.(() => {
|
|
@@ -117,53 +117,71 @@ export function normalizeProgress(value: unknown): number {
|
|
|
117
117
|
return Math.min(100, Math.round(value));
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/** statusOwnsProgress reports whether a status is one the program's OSC 9;4
|
|
121
|
+
* progress channel is currently speaking through. Only these may show a bar.
|
|
122
|
+
*
|
|
123
|
+
* The tab keeps the last percentage the server reported, and the server keeps
|
|
124
|
+
* sending it (correctly — a progress state persists until the program changes
|
|
125
|
+
* it), so this predicate is what stops that value from being painted underneath a
|
|
126
|
+
* status that came from somewhere else entirely. `input` and `done` come from the
|
|
127
|
+
* NOTIFICATION channel; `idle` is the absence of a progress state; `exited` and
|
|
128
|
+
* `crashed` are process facts. A percentage under any of those is a claim about
|
|
129
|
+
* a different channel than the one the reader is looking at. */
|
|
130
|
+
export function statusOwnsProgress(status: string): boolean {
|
|
131
|
+
return status === "working" || status === STATUS_FAILED || status === STATUS_WARNING;
|
|
132
|
+
}
|
|
133
|
+
|
|
120
134
|
/** renderedProgress is the percentage a status may actually SHOW. The tab keeps
|
|
121
135
|
* the last value the server reported; this is the one place that decides whether
|
|
122
136
|
* it is still meaningful, so the clearing rule has a single home.
|
|
123
137
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
138
|
+
* Two things clear it, and both are honest — neither invents a state change the
|
|
139
|
+
* program never made:
|
|
126
140
|
*
|
|
127
141
|
* 1. The program's own clear: OSC 9;4;0, or the abbreviated OSC 9;4, which the
|
|
128
142
|
* engine reports as percentage -1. That is the spec path, and the value
|
|
129
143
|
* arriving as PROGRESS_ABSENT is all it takes.
|
|
130
|
-
* 2. The
|
|
131
|
-
*
|
|
132
|
-
*
|
|
144
|
+
* 2. The status is not one the progress channel owns (statusOwnsProgress) —
|
|
145
|
+
* checked here. That covers a dead process, whose progress is meaningless
|
|
146
|
+
* and which nothing will ever clear, and equally a `done` or `input` latch,
|
|
147
|
+
* which is the notification channel talking. Measured: kiro-cli parks
|
|
148
|
+
* progress state 4 at its context-usage percentage when idle, so a finished
|
|
149
|
+
* turn used to paint a green done dot beside a 72% bar — a number about the
|
|
150
|
+
* context window, rendered as how far along the work was.
|
|
133
151
|
*
|
|
134
152
|
* There is deliberately NO third clear. 100% is not special: state 1 at 100 is a
|
|
135
|
-
* STATE that persists, and the progress channel carries no "done" signal at all
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* the program says otherwise" property the indeterminate state already has.
|
|
153
|
+
* STATE that persists, and the progress channel carries no "done" signal at all,
|
|
154
|
+
* so a program that pins 100 and goes quiet keeps its bar — the same "persists
|
|
155
|
+
* until the program says otherwise" property the indeterminate state already has.
|
|
139
156
|
* There is also no TIMEOUT: a timer would assert a change the program never
|
|
140
|
-
* reported
|
|
141
|
-
*
|
|
142
|
-
* program last reported. (In practice this is not a stale-bar risk for the one
|
|
143
|
-
* agent that drives it: kiro-cli never sends state 1, and sends state 0 when it
|
|
144
|
-
* goes idle.) */
|
|
157
|
+
* reported (Ghostty's ~15s one does, which is why it also asks emitters to
|
|
158
|
+
* refresh once a second; nothing here requires a keep-alive). */
|
|
145
159
|
export function renderedProgress(status: string, progress: number): number {
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** progressLabel prefixes a rendered label with its percentage ("78% · one").
|
|
150
|
-
*
|
|
151
|
-
* Applied at RENDER time and never stored: `Tab.title`, `Tab.pinnedTitle` and
|
|
152
|
-
* `Tab.display` stay the plain name, so a manual rename round-trips unprefixed,
|
|
153
|
-
* an automatic title is untouched, de-duplication still compares real labels,
|
|
154
|
-
* and clearing the progress needs no cleanup — the next paint simply stops
|
|
155
|
-
* adding the prefix. */
|
|
156
|
-
export function progressLabel(display: string, progress: number): string {
|
|
157
|
-
return progress < 0 ? display : `${String(progress)}% · ${display}`;
|
|
160
|
+
return statusOwnsProgress(status) ? progress : PROGRESS_ABSENT;
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
/** tabAccessibleName is a tab's announced name: its rendered label plus the
|
|
161
|
-
* session's state
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
* session's state, and the percentage when one is showing.
|
|
165
|
+
*
|
|
166
|
+
* The percentage is announced but never DRAWN as text. No terminal emulator puts
|
|
167
|
+
* a number next to a tab label — Windows Terminal draws a ring over the tab icon,
|
|
168
|
+
* Ghostty and kitty a bar on a window edge, ConEmu a percentage in the WINDOW
|
|
169
|
+
* title — and the visible number cost label width in a chip that already shrinks
|
|
170
|
+
* to a 100px floor. The bar answers "how far" for the eye; this answers it for a
|
|
171
|
+
* screen reader, which cannot see the bar at all (it is decoration with no text).
|
|
172
|
+
*
|
|
173
|
+
* Pass PROGRESS_ABSENT for progress to announce the state alone. Callers hand in
|
|
174
|
+
* renderedProgress's output, so a percentage the tab is not showing is not
|
|
175
|
+
* announced either. */
|
|
176
|
+
export function tabAccessibleName(
|
|
177
|
+
renderedLabel: string,
|
|
178
|
+
status: string,
|
|
179
|
+
progress: number = PROGRESS_ABSENT,
|
|
180
|
+
): string {
|
|
181
|
+
const state = statusPhrase(status);
|
|
182
|
+
return progress < 0
|
|
183
|
+
? `${renderedLabel} — ${state}`
|
|
184
|
+
: `${renderedLabel} — ${state}, ${String(progress)}%`;
|
|
167
185
|
}
|
|
168
186
|
|
|
169
187
|
// localStorage key for the last active session id, so a page reload reopens the
|
|
@@ -2,7 +2,19 @@
|
|
|
2
2
|
// BROWSER notifications. The spec names the display mode ("post a
|
|
3
3
|
// notification"), which is why this one signal is not tab-only chrome: a
|
|
4
4
|
// finished turn or a permission prompt has to be able to reach a user who is
|
|
5
|
-
// looking at another app.
|
|
5
|
+
// looking at another app. Clicking one takes the user to the session that raised
|
|
6
|
+
// it (see deliver).
|
|
7
|
+
//
|
|
8
|
+
// NON-PERSISTENT notifications (the `new Notification()` constructor), which is
|
|
9
|
+
// the standard surface available to a page with no service worker. Their lifetime
|
|
10
|
+
// is tied to the page's, which suits this app exactly: the notification is about
|
|
11
|
+
// a live PTY the page is attached to, and there is nothing useful to click once
|
|
12
|
+
// the page is gone. Per the Notifications API the constructor throws on most
|
|
13
|
+
// mobile browsers, where persistent (service-worker) notifications are the only
|
|
14
|
+
// route; that throw is caught below and degrades to tab-only chrome. Registering
|
|
15
|
+
// a service worker for an app that is otherwise entirely live is a cost this does
|
|
16
|
+
// not pay — mobile support for the standard constructor is the platforms' side of
|
|
17
|
+
// the contract.
|
|
6
18
|
//
|
|
7
19
|
// A notification is an EVENT, not a state (that is the structural difference
|
|
8
20
|
// from OSC 9;4 progress, which is a state and drives the status dot): the engine
|
|
@@ -15,13 +27,20 @@
|
|
|
15
27
|
// browserNotifierEnv() below. The notification text NEVER enters the DOM — see
|
|
16
28
|
// the note on deliver().
|
|
17
29
|
|
|
18
|
-
/** The slice of `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
30
|
+
/** The slice of a non-persistent `Notification` instance this feature uses: the
|
|
31
|
+
* `click` handler and `close()`. The instance is no longer discarded — attaching
|
|
32
|
+
* a click handler is what lets a notification finish its job (see deliver).
|
|
33
|
+
* Module-private: it is the ctor type's return shape, not a consumer surface. */
|
|
34
|
+
interface NotificationLike {
|
|
35
|
+
onclick: ((event: Event) => void) | null;
|
|
36
|
+
close: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The slice of `window.Notification` used here. */
|
|
21
40
|
export type NotificationCtorLike = new (
|
|
22
41
|
title: string,
|
|
23
42
|
options?: { body?: string; tag?: string },
|
|
24
|
-
) =>
|
|
43
|
+
) => NotificationLike;
|
|
25
44
|
|
|
26
45
|
/** The capabilities a notifier needs, all injected. `permission` and `request`
|
|
27
46
|
* are read as plain strings rather than the DOM's NotificationPermission union:
|
|
@@ -71,8 +90,15 @@ export interface Notifier {
|
|
|
71
90
|
/** Deliver one status event's notification, if it carries a new one. Returns
|
|
72
91
|
* true when a browser notification was actually posted (false covers every
|
|
73
92
|
* degraded path: no notification in the event, a repeat, suppression, no API,
|
|
74
|
-
* no permission, a throwing constructor).
|
|
75
|
-
|
|
93
|
+
* no permission, a throwing constructor).
|
|
94
|
+
*
|
|
95
|
+
* `view.activate` is what the notification's click performs in the page. The
|
|
96
|
+
* caller supplies it, so this module still knows nothing about sessions, tabs
|
|
97
|
+
* or the DOM. */
|
|
98
|
+
deliver(
|
|
99
|
+
ev: NotificationEvent,
|
|
100
|
+
view: { sessionIsActive: boolean; label: string; activate: () => void },
|
|
101
|
+
): boolean;
|
|
76
102
|
/** Note that this page has a session capable of notifying, which is what makes
|
|
77
103
|
* a permission prompt worth raising at all. */
|
|
78
104
|
arm(): void;
|
|
@@ -135,7 +161,30 @@ export function createNotifier(env: NotifierEnv): Notifier {
|
|
|
135
161
|
// label. The engine already strips control/bidi runes and clamps the
|
|
136
162
|
// length; that is not widened here. If this ever needs an in-page
|
|
137
163
|
// surface, it must use textContent.
|
|
138
|
-
new Ctor(view.label, { body: text, tag: ev.id });
|
|
164
|
+
const posted = new Ctor(view.label, { body: text, tag: ev.id });
|
|
165
|
+
// A notification the user clicks should land them on the terminal that
|
|
166
|
+
// raised it. Half of that is the click event's OWN default behaviour —
|
|
167
|
+
// per the Notifications API, activating a non-persistent notification
|
|
168
|
+
// moves focus to the viewport of its browsing context — so this handler
|
|
169
|
+
// deliberately does NOT preventDefault() and does NOT call
|
|
170
|
+
// window.focus(): the platform already brings the page forward, and the
|
|
171
|
+
// only missing half is switching to the right session inside it. That
|
|
172
|
+
// asymmetry is the whole bug this fixes: the page came forward and then
|
|
173
|
+
// showed whichever tab the user happened to leave active.
|
|
174
|
+
//
|
|
175
|
+
// NOT the `navigate` option, which is the declarative alternative and
|
|
176
|
+
// bypasses the click event entirely: it NAVIGATES, which for a live
|
|
177
|
+
// terminal means dropping every WebSocket and replaying scrollback to
|
|
178
|
+
// arrive somewhere the page already was. A same-page switch is both
|
|
179
|
+
// cheaper and correct. Do not "modernise" this into `navigate`.
|
|
180
|
+
posted.onclick = (): void => {
|
|
181
|
+
view.activate();
|
|
182
|
+
// The user has just acted on it, so it is no longer relevant — the
|
|
183
|
+
// documented reason to close one. (Closing is only wrong when used as
|
|
184
|
+
// a display timer, which also strips it from the notification tray
|
|
185
|
+
// before the user can interact.)
|
|
186
|
+
posted.close();
|
|
187
|
+
};
|
|
139
188
|
return true;
|
|
140
189
|
} catch {
|
|
141
190
|
// Safari throws on `new Notification` outside an installed PWA even when
|