@quario/viewer 0.1.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/CHANGELOG.md +17 -0
- package/LICENSE +219 -0
- package/README.md +303 -0
- package/lib/button.js +88 -0
- package/lib/check.js +154 -0
- package/lib/chrome.js +187 -0
- package/lib/index.d.ts +130 -0
- package/lib/index.js +450 -0
- package/lib/mark.js +40 -0
- package/lib/menu.js +318 -0
- package/lib/panel.js +126 -0
- package/lib/register.d.ts +9 -0
- package/lib/register.js +10 -0
- package/lib/stage.js +361 -0
- package/lib/style.js +94 -0
- package/lib/toolbar.js +95 -0
- package/lib/zoom.js +45 -0
- package/package.json +78 -0
package/lib/menu.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The zoom menu: the magnifier trigger in the bar and the popover it opens —
|
|
3
|
+
* "Fit sheet" above a separator, then the percentages `zoom.js` offers. It is
|
|
4
|
+
* the whole zoom control; the live percentage is never drawn, it is what the
|
|
5
|
+
* trigger is named after ("Zoom, 62%"), so the bar carries one icon rather
|
|
6
|
+
* than a readout that changes width under the reader.
|
|
7
|
+
*
|
|
8
|
+
* Native Popover API, so the top layer and light dismiss are the platform's:
|
|
9
|
+
* the menu escapes the bar without any ancestor having to allow it, and
|
|
10
|
+
* Escape or a click outside closes it. Three things the platform leaves to
|
|
11
|
+
* the author, each handled once here rather than per dismissal: placing the
|
|
12
|
+
* popover against its invoker (`place()` — anchor positioning would decide it
|
|
13
|
+
* in CSS, but not yet everywhere the viewer embeds); dismissing it when the
|
|
14
|
+
* viewport moves under it (`follow()` — a top-layer element does not travel
|
|
15
|
+
* with the invoker it was placed against, and a menu hanging off nothing is
|
|
16
|
+
* worse than one that closed); and handing focus back off a row that is about
|
|
17
|
+
* to stop being displayed (`leaving()`, on `beforetoggle`, the last moment
|
|
18
|
+
* that row is still focusable).
|
|
19
|
+
*
|
|
20
|
+
* Radio semantics, not toggles: exactly one row is the current mode, and
|
|
21
|
+
* choosing the one already checked settles as a no-op in the element's own
|
|
22
|
+
* `#setMode`, so this module reports every row alike. The check follows
|
|
23
|
+
* the mode rather than the measurement — "Fit sheet" is checked whenever the
|
|
24
|
+
* viewer is fitting, whatever percentage that came out at, and a host-set
|
|
25
|
+
* percentage the menu does not offer checks nothing.
|
|
26
|
+
*
|
|
27
|
+
* Labels go in as template values, never as markup, so this module has no
|
|
28
|
+
* markup edge to escape at (hard constraint 4).
|
|
29
|
+
*/
|
|
30
|
+
import { css, html } from "lit";
|
|
31
|
+
import { button } from "./button.js";
|
|
32
|
+
import { STEPS } from "./zoom.js";
|
|
33
|
+
|
|
34
|
+
/** The popover's id, scoped to the shadow root, so two viewers never collide. */
|
|
35
|
+
let ID = "qv-zoom-menu";
|
|
36
|
+
|
|
37
|
+
/** The gap between the trigger and the menu it opens, in px. */
|
|
38
|
+
let OFFSET = 6;
|
|
39
|
+
|
|
40
|
+
export let MENU = css`
|
|
41
|
+
/* Top-layer, so it is placed against the viewport: the UA's centring inset
|
|
42
|
+
is cleared here and \`place()\` writes the corner. Clearing it is also what
|
|
43
|
+
keeps the opening honest — \`place()\` runs from the queued toggle task, and
|
|
44
|
+
until it does, a fixed box with auto insets sits at its static position,
|
|
45
|
+
which is exactly where this element stands in the bar. \`display\` is
|
|
46
|
+
deliberately left alone: an author \`display\` would beat the UA rule that
|
|
47
|
+
hides a closed popover. */
|
|
48
|
+
.qv-menu {
|
|
49
|
+
position: fixed;
|
|
50
|
+
inset: auto;
|
|
51
|
+
margin: 0;
|
|
52
|
+
padding: 4px;
|
|
53
|
+
min-width: 150px;
|
|
54
|
+
border: 1px solid var(--_border);
|
|
55
|
+
border-radius: 8px;
|
|
56
|
+
background: var(--_bar);
|
|
57
|
+
box-shadow: 0 6px 20px light-dark(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.5));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.qv-menuitem {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: 7px;
|
|
64
|
+
width: 100%;
|
|
65
|
+
padding: 5px 12px 5px 6px;
|
|
66
|
+
border: none;
|
|
67
|
+
border-radius: 5px;
|
|
68
|
+
background: transparent;
|
|
69
|
+
color: inherit;
|
|
70
|
+
font: inherit;
|
|
71
|
+
font-variant-numeric: tabular-nums;
|
|
72
|
+
text-align: left;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.qv-menuitem:hover {
|
|
77
|
+
background: var(--_hover);
|
|
78
|
+
color: var(--_icon-active);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Inset, so the ring stays inside the menu's own padding. */
|
|
82
|
+
.qv-menuitem:focus-visible {
|
|
83
|
+
outline: 2px solid var(--_focus);
|
|
84
|
+
outline-offset: -2px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* The check's slot is held whether or not a check is in it, so the labels
|
|
88
|
+
line up down the menu. */
|
|
89
|
+
.qv-tick {
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
flex: none;
|
|
92
|
+
width: 13px;
|
|
93
|
+
height: 13px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.qv-separator {
|
|
97
|
+
margin: 4px 2px;
|
|
98
|
+
border: none;
|
|
99
|
+
border-top: 1px solid var(--_border);
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
/** @type {import('lit').TemplateResult} */
|
|
104
|
+
let magnifier = html`
|
|
105
|
+
<svg viewBox="0 0 20 20" width="19" height="19" aria-hidden="true">
|
|
106
|
+
<g fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round">
|
|
107
|
+
<circle cx="8.75" cy="8.75" r="5.25"></circle>
|
|
108
|
+
<path d="M12.6 12.6 16.4 16.4"></path>
|
|
109
|
+
</g>
|
|
110
|
+
</svg>
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
/** @type {import('lit').TemplateResult} */
|
|
114
|
+
let tick = html`
|
|
115
|
+
<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true">
|
|
116
|
+
<path
|
|
117
|
+
d="M3.4 8.4 6.4 11.4 12.6 4.8"
|
|
118
|
+
fill="none"
|
|
119
|
+
stroke="currentColor"
|
|
120
|
+
stroke-width="1.9"
|
|
121
|
+
stroke-linecap="round"
|
|
122
|
+
stroke-linejoin="round"
|
|
123
|
+
></path>
|
|
124
|
+
</svg>
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The button that opens this menu, found by the invoker relationship itself
|
|
129
|
+
* rather than by where it sits: a wrapper added for layout, or a second
|
|
130
|
+
* control in the group, must not be able to repoint placement or the focus
|
|
131
|
+
* hand-back.
|
|
132
|
+
*
|
|
133
|
+
* @type {(menu: Element) => HTMLElement}
|
|
134
|
+
*/
|
|
135
|
+
let trigger = (menu) =>
|
|
136
|
+
/** @type {any} */ (menu.getRootNode()).querySelector('[popovertarget="' + ID + '"]');
|
|
137
|
+
|
|
138
|
+
/** What has focus in the viewer's shadow root — not necessarily in the menu. */
|
|
139
|
+
/** @type {(menu: Element) => Element | null} */
|
|
140
|
+
let focused = (menu) => /** @type {ShadowRoot} */ (menu.getRootNode()).activeElement;
|
|
141
|
+
|
|
142
|
+
/** @type {(menu: Element) => HTMLElement[]} */
|
|
143
|
+
let items = (menu) => /** @type {HTMLElement[]} */ ([...menu.querySelectorAll(".qv-menuitem")]);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Place the open menu under its trigger, or over it when there is no room
|
|
147
|
+
* below — the viewer is embedded, and a host is free to put its bar near the
|
|
148
|
+
* bottom of the window.
|
|
149
|
+
*
|
|
150
|
+
* @param {HTMLElement} menu
|
|
151
|
+
*/
|
|
152
|
+
let place = (menu) => {
|
|
153
|
+
let box = trigger(menu).getBoundingClientRect();
|
|
154
|
+
let below = box.bottom + OFFSET + menu.offsetHeight <= innerHeight;
|
|
155
|
+
menu.style.left = box.left + "px";
|
|
156
|
+
menu.style.top = (below ? box.bottom + OFFSET : box.top - OFFSET - menu.offsetHeight) + "px";
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Where each key sends focus, from the row that has it.
|
|
161
|
+
*
|
|
162
|
+
* @type {Record<string, (list: HTMLElement[], at: number) => HTMLElement>}
|
|
163
|
+
*/
|
|
164
|
+
let MOVE = {
|
|
165
|
+
ArrowDown: (list, at) => list[(at + 1) % list.length],
|
|
166
|
+
ArrowUp: (list, at) => list[(at - 1 + list.length) % list.length],
|
|
167
|
+
Home: (list) => list[0],
|
|
168
|
+
End: (list) => list[list.length - 1],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Tab is the one key that closes rather than moves: a menu left open behind
|
|
173
|
+
* the focus ring is the state light dismiss does not cover, and closing it
|
|
174
|
+
* is what puts focus back on the trigger to carry on from.
|
|
175
|
+
*
|
|
176
|
+
* @param {KeyboardEvent} event
|
|
177
|
+
*/
|
|
178
|
+
let navigate = (event) => {
|
|
179
|
+
let menu = /** @type {HTMLElement} */ (event.currentTarget);
|
|
180
|
+
let move = MOVE[event.key];
|
|
181
|
+
if (!move && event.key !== "Tab") return;
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
if (!move) return menu.hidePopover();
|
|
184
|
+
let list = items(menu);
|
|
185
|
+
move(list, list.indexOf(/** @type {any} */ (focused(menu)))).focus();
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* What is watching the viewport for each open menu, so closing can stop it.
|
|
190
|
+
* Keyed weakly on the menu, which is the element the listeners close.
|
|
191
|
+
*
|
|
192
|
+
* @type {WeakMap<Element, AbortController>}
|
|
193
|
+
*/
|
|
194
|
+
let watching = new WeakMap();
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Placement is written once, at opening, against the trigger's viewport box —
|
|
198
|
+
* so anything that moves that box while the menu is open leaves it pointing
|
|
199
|
+
* at nothing. Scrolling (in capture, so a scroll anywhere counts) and
|
|
200
|
+
* resizing dismiss it instead of dragging it along: the reader has already
|
|
201
|
+
* looked away from the control.
|
|
202
|
+
*
|
|
203
|
+
* @param {HTMLElement} menu
|
|
204
|
+
*/
|
|
205
|
+
let follow = (menu) => {
|
|
206
|
+
let stop = new AbortController();
|
|
207
|
+
// Guarded and `once`, not merely aborted on close: removing an open popover
|
|
208
|
+
// from the document hides it without firing `beforetoggle`, so a discarded
|
|
209
|
+
// viewer never reaches `leaving()`. `once` is what stops these listeners —
|
|
210
|
+
// and the menu they hold — outliving the first scroll or resize after that.
|
|
211
|
+
let close = () => menu.matches(":popover-open") && menu.hidePopover();
|
|
212
|
+
let once = { capture: true, once: true, signal: stop.signal };
|
|
213
|
+
addEventListener("scroll", close, once);
|
|
214
|
+
addEventListener("resize", close, once);
|
|
215
|
+
watching.set(menu, stop);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Opening puts focus on the row that is checked, so the arrow keys move from
|
|
220
|
+
* where the reader already is. Nothing is checked at an off-list percentage;
|
|
221
|
+
* then the first row takes it.
|
|
222
|
+
*
|
|
223
|
+
* @param {ToggleEvent} event
|
|
224
|
+
*/
|
|
225
|
+
let opened = (event) => {
|
|
226
|
+
if (event.newState !== "open") return;
|
|
227
|
+
let menu = /** @type {HTMLElement} */ (event.currentTarget);
|
|
228
|
+
place(menu);
|
|
229
|
+
follow(menu);
|
|
230
|
+
(/** @type {HTMLElement | null} */
|
|
231
|
+
(menu.querySelector('[aria-checked="true"]')) ?? items(menu)[0]).focus();
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Whatever dismissed the menu — Escape, Tab, a chosen row — focus must not be
|
|
236
|
+
* left on a row that is about to stop being displayed. `beforetoggle` is
|
|
237
|
+
* where this belongs: by the time `toggle` runs the rows are gone and the
|
|
238
|
+
* browser has already put focus back on the document.
|
|
239
|
+
*
|
|
240
|
+
* @param {ToggleEvent} event
|
|
241
|
+
*/
|
|
242
|
+
let leaving = (event) => {
|
|
243
|
+
let menu = /** @type {HTMLElement} */ (event.currentTarget);
|
|
244
|
+
if (event.newState !== "closed") return;
|
|
245
|
+
watching.get(menu)?.abort();
|
|
246
|
+
if (menu.contains(focused(menu))) trigger(menu).focus();
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Choosing a row closes the menu and reports the row. Whether that is a
|
|
251
|
+
* change is the element's to know, not this module's: `#setMode` is where the
|
|
252
|
+
* mode lives, and re-choosing the one already current settles there.
|
|
253
|
+
*
|
|
254
|
+
* @type {(value: "fit" | number, choose: (value: "fit" | number) => void)
|
|
255
|
+
* => (event: Event) => void}
|
|
256
|
+
*/
|
|
257
|
+
let pick = (value, choose) => (event) => {
|
|
258
|
+
let target = /** @type {HTMLElement} */ (event.currentTarget);
|
|
259
|
+
/** @type {HTMLElement} */ (target.closest("[popover]")).hidePopover();
|
|
260
|
+
choose(value);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* One menu row.
|
|
265
|
+
*
|
|
266
|
+
* @type {(label: string, value: "fit" | number, checked: boolean,
|
|
267
|
+
* choose: (value: "fit" | number) => void) => import('lit').TemplateResult}
|
|
268
|
+
*/
|
|
269
|
+
let row = (label, value, checked, choose) => html`
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
class="qv-menuitem"
|
|
273
|
+
role="menuitemradio"
|
|
274
|
+
aria-checked=${String(checked)}
|
|
275
|
+
tabindex="-1"
|
|
276
|
+
data-zoom=${String(value)}
|
|
277
|
+
@click=${pick(value, choose)}
|
|
278
|
+
>
|
|
279
|
+
<span class="qv-tick">${checked ? tick : ""}</span>
|
|
280
|
+
${label}
|
|
281
|
+
</button>
|
|
282
|
+
`;
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The zoom group: the trigger and its menu. The trigger's name carries the
|
|
286
|
+
* percentage on screen, because that is the one control whose state a reader
|
|
287
|
+
* would otherwise have to open the menu to learn — and at an off-list fit
|
|
288
|
+
* percentage the menu could not tell them anyway.
|
|
289
|
+
*
|
|
290
|
+
* @param {{ mode: "fit" | number, percent: number,
|
|
291
|
+
* choose: (value: "fit" | number) => void }} zoom
|
|
292
|
+
* The live mode, the percentage actually on screen, and the element's own
|
|
293
|
+
* handler for a chosen row.
|
|
294
|
+
* @returns {import('lit').TemplateResult}
|
|
295
|
+
*/
|
|
296
|
+
export let zoomMenu = ({ mode, percent, choose }) => html`
|
|
297
|
+
<div class="qv-zoom">
|
|
298
|
+
${button({
|
|
299
|
+
title: "Zoom, " + percent + "%",
|
|
300
|
+
popover: ID,
|
|
301
|
+
content: magnifier,
|
|
302
|
+
})}
|
|
303
|
+
<div
|
|
304
|
+
id=${ID}
|
|
305
|
+
popover
|
|
306
|
+
class="qv-menu"
|
|
307
|
+
role="menu"
|
|
308
|
+
aria-label="Zoom"
|
|
309
|
+
@toggle=${opened}
|
|
310
|
+
@beforetoggle=${leaving}
|
|
311
|
+
@keydown=${navigate}
|
|
312
|
+
>
|
|
313
|
+
${row("Fit sheet", "fit", mode === "fit", choose)}
|
|
314
|
+
<hr class="qv-separator" role="separator" />
|
|
315
|
+
${STEPS.map((step) => row(step + "%", step, mode === step, choose))}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
`;
|
package/lib/panel.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error panel: what a failed render or export looks like to the reader,
|
|
3
|
+
* rather than only to whoever is listening for the event. One panel serves
|
|
4
|
+
* every kind of failure, so the label is what tells them apart.
|
|
5
|
+
*
|
|
6
|
+
* Always the compound — "the error panel", never a bare "panel", which stays
|
|
7
|
+
* the word for the host's own UI regions around the viewer (see CONTEXT.md).
|
|
8
|
+
*
|
|
9
|
+
* It is the one place the viewer states wording of its own. Everything else it
|
|
10
|
+
* shows comes from the report: the fragment is the html target's, and even the
|
|
11
|
+
* unlicensed marking's wording arrives inside it. A label cannot, because no
|
|
12
|
+
* error knows whether it was a mount, an update or an export that failed —
|
|
13
|
+
* which is exactly what the error event's `kind` names.
|
|
14
|
+
*
|
|
15
|
+
* What it prints beside that label is the error's own message, as a template
|
|
16
|
+
* value. That is a markup edge, and it stays one: a message can carry report
|
|
17
|
+
* data, because a host's registry function is free to interpolate a row into
|
|
18
|
+
* whatever it throws (hard constraint 4).
|
|
19
|
+
*/
|
|
20
|
+
import { css, html } from "lit";
|
|
21
|
+
import { button } from "./button.js";
|
|
22
|
+
|
|
23
|
+
export let PANEL = css`
|
|
24
|
+
/* Positioned rather than laid out, for the reason the progress strip is: a
|
|
25
|
+
panel that took layout would push the stage down and shorten it, so raising
|
|
26
|
+
and clearing it would rescale a fitted sheet every time an export failed.
|
|
27
|
+
Over the top of the stage, above the marking watermark's z-index. */
|
|
28
|
+
.qv-error {
|
|
29
|
+
position: absolute;
|
|
30
|
+
inset: 0 0 auto 0;
|
|
31
|
+
z-index: 2;
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: flex-start;
|
|
34
|
+
gap: 6px;
|
|
35
|
+
max-height: 45%;
|
|
36
|
+
padding: 9px 8px 11px 12px;
|
|
37
|
+
background: var(--_error);
|
|
38
|
+
color: var(--_error-text);
|
|
39
|
+
border-bottom: 1px solid var(--_error-border);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* The wording scrolls; the dismiss control does not go with it. */
|
|
43
|
+
.qv-error-body {
|
|
44
|
+
flex: 1;
|
|
45
|
+
min-width: 0;
|
|
46
|
+
overflow: auto;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.qv-error-label {
|
|
50
|
+
margin: 0;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* The error's own words, which for a located diagnostic carry the schema path
|
|
55
|
+
and the offending source. It wraps rather than truncates: the path is the
|
|
56
|
+
most useful part and it is at the front, but the message is what says why. */
|
|
57
|
+
.qv-error-message {
|
|
58
|
+
margin: 3px 0 0;
|
|
59
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
60
|
+
white-space: pre-wrap;
|
|
61
|
+
overflow-wrap: anywhere;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* The shared button is styled for the bar, where it sits on the chrome's own
|
|
65
|
+
palette. In here it takes the panel's. */
|
|
66
|
+
.qv-error .qv-button {
|
|
67
|
+
flex: none;
|
|
68
|
+
color: inherit;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.qv-error .qv-button:hover {
|
|
72
|
+
background: var(--_error-hover);
|
|
73
|
+
color: inherit;
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Which failure this was, in the panel's own wording. The update label earns
|
|
79
|
+
* its length: that failure leaves the previous report on the sheet, and
|
|
80
|
+
* silently stale content is the thing the panel exists to prevent.
|
|
81
|
+
*
|
|
82
|
+
* @param {"mount-render" | "update-render" | "export"} kind
|
|
83
|
+
* @param {string} [format] The export format's name, for that kind alone.
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
export let label = (kind, format) =>
|
|
87
|
+
kind === "export"
|
|
88
|
+
? "Could not export " + format
|
|
89
|
+
: kind === "update-render"
|
|
90
|
+
? "Could not update the report — showing the previous version"
|
|
91
|
+
: "Could not render the report";
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* What an error says. Not every throw is an `Error` — a host's registry
|
|
95
|
+
* function may throw anything, which `locate()` in the engine allows for too —
|
|
96
|
+
* so the value itself answers when there is no message to read, and an `Error`
|
|
97
|
+
* with an empty one falls through to its own `toString`.
|
|
98
|
+
*
|
|
99
|
+
* @param {unknown} error The caught value.
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
let wording = (error) => String(/** @type {any} */ (error)?.message || error);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The error panel, rendered only while there is a failure worth looking at:
|
|
106
|
+
* it is a state, not a log — the error event is the log, and it takes every
|
|
107
|
+
* failure whether or not one is drawn. Assertive rather than polite: a
|
|
108
|
+
* failure means the reader is looking at stale or empty content, which is
|
|
109
|
+
* worth interrupting for. What keeps the announcement rate down is that the
|
|
110
|
+
* panel replaces rather than stacks, and that a superseded render's failure
|
|
111
|
+
* is never drawn.
|
|
112
|
+
*
|
|
113
|
+
* @param {{ label: string, error: unknown }} failure The viewer's own label
|
|
114
|
+
* and the caught value, printed as text and never markup.
|
|
115
|
+
* @param {() => void} dismiss Takes the panel down until the next failure.
|
|
116
|
+
* @returns {import('lit').TemplateResult}
|
|
117
|
+
*/
|
|
118
|
+
export let panel = (failure, dismiss) => html`
|
|
119
|
+
<div class="qv-error" role="alert">
|
|
120
|
+
<div class="qv-error-body">
|
|
121
|
+
<p class="qv-error-label">${failure.label}</p>
|
|
122
|
+
<p class="qv-error-message">${wording(failure.error)}</p>
|
|
123
|
+
</div>
|
|
124
|
+
${button({ title: "Dismiss", label: "×", click: dismiss })}
|
|
125
|
+
</div>
|
|
126
|
+
`;
|
package/lib/register.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-line define, as its own entry so the main module stays
|
|
3
|
+
* side-effect-free: importing `@quario/viewer` gives you the class and
|
|
4
|
+
* defines nothing, importing `@quario/viewer/register` gives the tag to
|
|
5
|
+
* hosts that want the platform default. A host that wants its own tag calls
|
|
6
|
+
* `customElements.define` itself (docs/adr/0005-the-surfaces-are-custom-elements.md).
|
|
7
|
+
*/
|
|
8
|
+
import { QuarioViewer, TAG } from "./index.js";
|
|
9
|
+
|
|
10
|
+
customElements.define(TAG, QuarioViewer);
|