@quario/viewer 0.9.0 → 0.11.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 CHANGED
@@ -1,5 +1,49 @@
1
1
  # @quario/viewer
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **A collapsed group does not draw its content.** The sheet paints the layout's pages, so an instance whose group declares `collapsed` reads as its header and footer alone.
8
+ - **A group's `label` titles its outline row.** The panel lists the layout's marks, so an instance that declares a label reads under it rather than under its first header line.
9
+ - **A linked run is clickable, and reachable with a keyboard.** Every run carrying an `href` now gets a region of its own over the words the page paints, so a reader clicks it or reaches it with Tab and opens it with Enter. A URL is an anchor opening in a tab of its own. An `href` naming a group's `label` is a button that steps the sheet to that instance, the way an outline row does, and a label no instance carries is no region at all. The regions follow the reach as the pages do, and move with the zoom.
10
+
11
+ The size budget rises from 8.5 kB to 9.5 kB to hold them.
12
+
13
+ - **An optional outline panel.** Set `outline` to `true` and the bar carries one more control, which opens a panel beside the sheet. The panel lists the report's group instances, nested by depth, in document order, and a row scrolls the sheet to where that instance begins. The list is the group tree the PDF target writes as bookmarks, so a report declares nothing for it. An instance whose bands all resolve hidden is not in it. The module is off by default. The reader opens and closes the panel.
14
+
15
+ ### Patch Changes
16
+
17
+ - **An error quario replaces keeps the one it replaced, as `cause`.** A failure inside one of your own functions reaches you named where it happened — `detail[0].value [{{ total() }}]: ...` — which is a copy, rebuilt from the class and a prefixed message. The stack that says which line of your function threw used to go with the original. It now rides along: read `error.cause` for the throw as it happened, and the message for where quario was when it happened.
18
+
19
+ The viewer's and the editor's own property checks do the same. A `page` or `fonts` value either element refuses throws a `TypeError` naming the property, and that error now carries the layout's own account of the value behind it.
20
+
21
+ - **`renderComplete` answers for the newest render at the moment it settles.** A render that starts behind a pending `await` is the one the promise then waits for, so a host awaiting it while properties keep changing waits for the last change's render. Before, the promise could settle for the render in flight when it was asked, or miss one that started behind it.
22
+ - **The viewer reports a target it cannot export, rather than dropping it.** A `targets` entry whose name is not `pdf`, `xlsx`, `csv`, `docx` or `html` used to disappear: no button, no error. It is now a host mistake like any other malformed property. The error panel names the entry by its index and the name it carried, and the last good render stays up. An `html` target is still accepted and not read, so one list can serve both `render` and the viewer.
23
+ - Updated dependencies
24
+ - quario@0.11.0
25
+ - @quario/layout@0.8.0
26
+ - @quario/landing@0.3.1
27
+
28
+ ## 0.10.0
29
+
30
+ ### Minor Changes
31
+
32
+ - **A viewer you insert again renders once more, unless its last render landed on the sheet.** It used to do that only for a render that stopped mid-flight, which left three ways for the newest render to go missing with nothing owed.
33
+
34
+ A render that failed while the element was out of the document is one of them. The error panel reaches nobody there, so the verdict reached nobody, and the element came back showing the previous pages with no panel and no re-render owed. A property the host rejected while the element was out of the document behaved the same way. So did a property written while it was out of the document.
35
+
36
+ All three now leave the render owed, and inserting the element collects it. What you see on reconnect is the panel the failure earns, or the pages the newest properties produce.
37
+
38
+ A failure the panel already showed is owed again too. Removing and inserting such an element re-runs the render and reports the same failure once. It is not announced twice.
39
+
40
+ ### Patch Changes
41
+
42
+ - Updated dependencies
43
+ - @quario/layout@0.7.0
44
+ - quario@0.10.0
45
+ - @quario/landing@0.3.0
46
+
3
47
  ## 0.9.0
4
48
 
5
49
  ### Minor Changes
package/README.md CHANGED
@@ -10,6 +10,23 @@ list, painted page by page. You pass the export targets you want, and the viewer
10
10
  to each (`"pdf"`, `"xlsx"`, `"csv"`, `"docx"`). The bar always carries the zoom control. With no
11
11
  exportable target it carries nothing else.
12
12
 
13
+ ## Contents
14
+
15
+ - [Install](#install)
16
+ - [Quick start](#quick-start)
17
+ - [Properties](#properties)
18
+ - [Events and `renderComplete`](#events-and-rendercomplete)
19
+ - [Lifecycle](#lifecycle)
20
+ - [Using with React, Vue and Svelte](#using-with-react-vue-and-svelte)
21
+ - [Zoom](#zoom)
22
+ - [Outline](#outline)
23
+ - [Links](#links)
24
+ - [What the preview is](#what-the-preview-is)
25
+ - [Errors](#errors)
26
+ - [Color scheme](#color-scheme)
27
+ - [Documentation](#documentation)
28
+ - [License](#license)
29
+
13
30
  ## Install
14
31
 
15
32
  ```bash
@@ -66,11 +83,14 @@ package to install. See [Using with React, Vue and Svelte](#using-with-react-vue
66
83
  | `fonts` | The font mapping, as passed to `pdf({ fonts })` | none |
67
84
  | `filename` | Export download name, without extension | `"report"` |
68
85
  | `colorScheme` | `"light"`, `"dark"`, or `"auto"` | `"light"` |
86
+ | `outline` | `true` to offer the outline panel | `false` |
69
87
 
70
88
  Properties, not attributes: `report`, `targets`, `data`, `page` and `fonts` are values no attribute
71
89
  could carry. `targets` mirrors `report.render(target, data)` for the exports:
72
90
  `"pdf"`/`"xlsx"`/`"csv"`/`"docx"` targets become export buttons, in the order given, and the
73
- sheet needs none of them. The quario instance (and with it the license and the registry) stays
91
+ sheet needs none of them. An `"html"` target is accepted and not read, so one list can serve
92
+ `render` and the viewer. Any other name is a host mistake: the viewer reports it on the error panel
93
+ with the entry's index, and renders nothing. The quario instance (and with it the license and the registry) stays
74
94
  yours: the element takes the compiled report, never a schema.
75
95
 
76
96
  Assigning `data` (or any of the others) re-renders. Rapid successive writes render only the newest
@@ -100,8 +120,9 @@ inline `onerror` attribute on the element would fire too. `window.onerror` never
100
120
  `renderComplete` awaits the newest render settling: `true` when it landed on the sheet with the pages
101
121
  on screen finished trying to paint, `false` when it failed or there was nothing to render. It never
102
122
  rejects. Failures arrive on the `error` event. `rendered` fires at that same moment. Like every
103
- outcome here, it answers for the newest render only. A superseded render's failure reaches no
104
- one.
123
+ outcome here, it answers for the newest render only: the render that is newest when the promise
124
+ settles, so a render that starts behind your `await` is the one it then waits for. A superseded
125
+ render's failure reaches no one.
105
126
 
106
127
  An image the browser cannot decode — pixel data corrupt past the size in its header, which is all
107
128
  the engine reads — is drawn as nothing, and the page is drawn around it. That is not a render
@@ -115,11 +136,12 @@ option property, becomes a `TypeError` naming the property, on the `error` event
115
136
 
116
137
  ## Lifecycle
117
138
 
118
- There is no `destroy()`. Removing the element from the DOM abandons in-flight work and releases
119
- its observers. An export that settles after removal downloads nothing. The properties persist, and
120
- re-inserting the element re-renders from them, so reparenting is safe, and discarding a viewer is
121
- just discarding the element. Two viewers, or a viewer beside your own components, coexist: each
122
- element owns its own shadow root.
139
+ There is no `destroy()`. Removal from the DOM stops the work in flight and releases the
140
+ observers. An export that settles after removal downloads nothing. The properties and the pages
141
+ persist. The element still owes a render that did not land on the sheet. It therefore renders
142
+ once more when you insert it, unless its last render landed. Reparenting is safe, and you
143
+ discard a viewer by discarding the element. Two viewers, or a viewer beside your own components,
144
+ coexist: each element owns its own shadow root.
123
145
 
124
146
  ## Using with React, Vue and Svelte
125
147
 
@@ -234,6 +256,18 @@ Every page keeps its size, so the scrollbar and the scroll extent are the whole
234
256
  start. A page further off is blank paper until you scroll to it, which is why a zoom step costs the
235
257
  same on a thousand-page report as on a five-page one.
236
258
 
259
+ ## Links
260
+
261
+ A run that carries an [`href`](https://github.com/getquario/quario/blob/main/SCHEMA.md#style-declarations) gets a region of its own over the words the page
262
+ paints. The reader clicks it, or reaches it with the Tab key and opens it with Enter. You
263
+ declare nothing for this and turn nothing on.
264
+
265
+ A URL opens in a tab of its own, so a reader reading a report never loses it. The engine
266
+ admitted that URL against your instance's `schemes` allowlist before the viewer saw it,
267
+ which is where you decide what a report may point at. An `href` that starts with `#` names
268
+ a group's `label` and steps the sheet to that instance, the way an outline row does. A
269
+ label no instance carries is no region, and the words stay as they are.
270
+
237
271
  ## What the preview is
238
272
 
239
273
  The pages on screen are the pages the PDF export writes: both consume one layout, `@quario/layout`'s
@@ -242,6 +276,8 @@ longer one is as many pages as the layout breaks it into, stacked down the sheet
242
276
  container to a viewer that draws a short report therefore reserves a full page. Set `zoom` to a
243
277
  percentage small enough where the box has to stay small.
244
278
 
279
+ The reader reads and does not reorder. The viewer paints the layout's pages and offers no sort, because a page is not a grid; a host that wants sorted rows renders the report again with them sorted. The XLSX target's `filter` option is where a reader gets a control of their own.
280
+
245
281
  Each page is a canvas, and text on it is drawn in the face the document will use. The viewer
246
282
  registers a TrueType family you pass as `fonts` from your own bytes and draws it as the browser
247
283
  shapes it —
@@ -303,6 +339,19 @@ data is the message itself, if your own registry functions interpolate a row int
303
339
  That is your call, and the panel puts it on screen as text, never as markup. Compile errors are not
304
340
  part of this: `q.report(schema)` raises those before the viewer is ever handed a report.
305
341
 
342
+ ## Outline
343
+
344
+ Set `outline` to `true` and the bar carries one more control. It opens a panel beside the sheet
345
+ that lists the report's group instances, nested by depth, in document order. A row scrolls the
346
+ sheet to where that instance begins. The list is the group tree the PDF target writes as
347
+ bookmarks, and a row reads the group's `label` where one is declared. A group instance whose bands all resolve
348
+ hidden is not in the list. The property turns the module on. The reader opens and closes the
349
+ panel, and the panel opens when the module turns on.
350
+
351
+ ```js
352
+ view.outline = true;
353
+ ```
354
+
306
355
  ## Color scheme
307
356
 
308
357
  `colorScheme` paints the **chrome** — backdrop, bar, controls, progress strip, error
package/lib/button.js CHANGED
@@ -1,8 +1,6 @@
1
1
  /**
2
2
  * The shared `qv-button` template. Every control the viewer renders comes
3
- * from here the zoom menu's trigger, the export downloads, the error
4
- * panel's dismiss — so they agree on the class, the `type` and how they are
5
- * named.
3
+ * from here, so they agree on the class, the `type` and how they are named.
6
4
  */
7
5
  import { css, html, nothing } from "lit";
8
6
 
@@ -26,16 +24,28 @@ export let BUTTON = css`
26
24
  cursor: pointer;
27
25
  }
28
26
 
29
- .qv-button:hover {
27
+ /* The interaction treatment, written once for every <button> the viewer
28
+ draws. A menu row (\`menu.js\`) and an outline row (\`outline.js\`) are
29
+ buttons too: a different shape — full width, left-aligned — but hovering
30
+ and focusing them mean the same thing, and stating that twice is how the
31
+ two drifted apart before the palette landed. Where a row differs it says
32
+ so itself. */
33
+ .qv-button:hover,
34
+ .qv-menuitem:hover,
35
+ .qv-outline-row:hover {
30
36
  background: var(--_hover);
31
37
  color: var(--_icon-active);
32
38
  }
33
39
 
34
- .qv-button:active {
40
+ .qv-button:active,
41
+ .qv-menuitem:active,
42
+ .qv-outline-row:active {
35
43
  background: var(--_active);
36
44
  }
37
45
 
38
- .qv-button:focus-visible {
46
+ .qv-button:focus-visible,
47
+ .qv-menuitem:focus-visible,
48
+ .qv-outline-row:focus-visible {
39
49
  outline: 2px solid var(--_focus);
40
50
  outline-offset: 1px;
41
51
  }
@@ -67,15 +77,12 @@ let fallback = (value, otherwise) => value ?? otherwise;
67
77
  *
68
78
  * @param {{ title: string, label?: string, disabled?: boolean, name?: string,
69
79
  * popover?: string, click?: () => void,
70
- * content?: import('lit').TemplateResult }} control
71
- * `content` is the icon a control draws and `label` the visible text it
72
- * reads as; an export button carries both, the zoom trigger and the panel's
73
- * dismiss the icon alone. Neither is ever the accessible name `title` is,
74
- * which is why the icons are `aria-hidden`. `name` marks an export button
75
- * with the target it downloads, and
76
- * `popover` names the menu this button opens — the platform then owns the
77
- * opening and the invoker's own `aria-expanded`, which is why such a button
78
- * needs no click of its own.
80
+ * content: import('lit').TemplateResult }} control
81
+ * `content` is the icon every control draws and `label` the visible text it
82
+ * reads as. Neither is ever the accessible name `title` is, which is why
83
+ * the icons are `aria-hidden`. `name` marks an export button with the target
84
+ * it downloads. `popover` names the menu this button opens, and the platform
85
+ * then owns the opening and the invoker's `aria-expanded`.
79
86
  * @returns {import('lit').TemplateResult}
80
87
  */
81
88
  export let button = ({ title, label, disabled, name, popover, click, content }) => html`
@@ -90,6 +97,6 @@ export let button = ({ title, label, disabled, name, popover, click, content })
90
97
  ?disabled=${disabled}
91
98
  @click=${click}
92
99
  >
93
- ${content ?? nothing}${label ?? nothing}
100
+ ${content}${label ?? nothing}
94
101
  </button>
95
102
  `;
package/lib/check.js CHANGED
@@ -1,20 +1,26 @@
1
1
  /**
2
- * What the host got wrong, named. The element's own mistakes a report that
3
- * is not compiled, a malformed option property are definition-shaped: they
4
- * describe the embedding, not the report, so each check here throws a
5
- * `TypeError` naming the property and the expectation. The element rethrows
6
- * them from its render task, which is how one channel (the error event and
7
- * the panel) carries every failure a host can cause.
2
+ * What the host got wrong, named. Each check throws a `TypeError` naming the
3
+ * property and the expectation; the element rethrows them from its render
4
+ * task, so one channel carries every failure a host can cause.
8
5
  *
9
6
  * Pure policy, no DOM: this module is what the Node suite pins.
10
7
  */
11
8
 
12
9
  import { checkFonts, pageBox } from "@quario/layout";
10
+ import { EXPORTS } from "./toolbar.js";
13
11
  import { STEPS } from "./zoom.js";
14
12
 
15
- /** @type {(message: string) => never} */
16
- let fail = (message) => {
17
- throw new TypeError("quario-viewer: " + message);
13
+ /**
14
+ * Name the mistake on the property that carries it. An `Error` stands in for
15
+ * its own message and rides along as `cause`: a replacement is a claim about
16
+ * what went wrong, and only the error it replaced can contradict it
17
+ * (docs/adr/0084).
18
+ *
19
+ * @type {(said: string | Error) => never}
20
+ */
21
+ let fail = (said) => {
22
+ let message = said instanceof Error ? said.message : said;
23
+ throw new TypeError("quario-viewer: " + message, said instanceof Error ? { cause: said } : {});
18
24
  };
19
25
 
20
26
  let REPORT = [
@@ -26,10 +32,19 @@ let REPORT = [
26
32
  /** @type {(report: any) => boolean} */
27
33
  let compiled = (report) => REPORT.every((check) => check(report));
28
34
 
35
+ // The one target the viewer accepts and never reads: a host that hands one
36
+ // list to `render` and to the viewer carries it, and the sheet is the
37
+ // layout's own. Every other name the export map does not hold is a mistake,
38
+ // because the viewer would drop it without a button and without a word.
39
+ let UNREAD = ["html"];
40
+ let ACCEPTED = [...Object.keys(EXPORTS), ...UNREAD];
41
+ let NAMES = ACCEPTED.map((name) => '"' + name + '"').join(", ");
42
+
29
43
  /**
30
44
  * The shapes a render needs: a compiled report, and the export targets as an
31
45
  * array — possibly empty, since the sheet is the layout's own and needs no
32
- * target of the host's.
46
+ * target of the host's. Each entry names a target the viewer exports or the
47
+ * one it leaves unread; anything else is named on its index.
33
48
  *
34
49
  * @param {any} report
35
50
  * @param {any} targets
@@ -37,6 +52,19 @@ let compiled = (report) => REPORT.every((check) => check(report));
37
52
  export let exports = (report, targets) => {
38
53
  if (!compiled(report)) fail("report: expected a compiled report from a quario instance");
39
54
  if (!Array.isArray(targets)) fail("targets: expected an array of render targets");
55
+ targets.forEach(named);
56
+ };
57
+
58
+ /** What a rejected entry carried, in the message's words. */
59
+ /** @type {(target: any) => string} */
60
+ let carried = (target) =>
61
+ target?.name === undefined ? "a value with no name" : JSON.stringify(target.name);
62
+
63
+ /** One entry of `targets`, named on its index when the viewer would drop it. */
64
+ /** @type {(target: any, i: number) => void} */
65
+ let named = (target, i) => {
66
+ if (ACCEPTED.includes(target?.name)) return;
67
+ fail("targets[" + i + "]: expected a target named one of " + NAMES + ", got " + carried(target));
40
68
  };
41
69
 
42
70
  /**
@@ -51,15 +79,14 @@ export let geometry = (page) => {
51
79
  try {
52
80
  return pageBox(page, "page");
53
81
  } catch (error) {
54
- return fail(/** @type {Error} */ (error).message);
82
+ return fail(/** @type {Error} */ (error));
55
83
  }
56
84
  };
57
85
 
58
86
  /**
59
87
  * The font mapping from the `fonts` property. Only the shape reaches here: a
60
- * face that will not parse, or a missing parser, is found while the report is
61
- * measured and is a render failure there. The layout names the mistake; this
62
- * names the property, the way `geometry` does.
88
+ * face that will not parse is found while the report is measured and is a
89
+ * render failure there.
63
90
  *
64
91
  * @param {any} fonts
65
92
  */
@@ -67,7 +94,7 @@ export let faces = (fonts) => {
67
94
  try {
68
95
  checkFonts(fonts, "fonts");
69
96
  } catch (error) {
70
- fail(/** @type {Error} */ (error).message);
97
+ fail(/** @type {Error} */ (error));
71
98
  }
72
99
  };
73
100
 
@@ -76,12 +103,10 @@ let inRange = (mode, floor, ceiling) =>
76
103
  [Number.isFinite(mode), mode >= floor, mode <= ceiling].every(Boolean);
77
104
 
78
105
  /**
79
- * The zoom mode from the `zoom` property. Fit is the default: the host owns
80
- * the box, and a report nobody can read until they touch a control is not
81
- * much of a preview. A host may author any percentage from 25 to 200 —
82
- * continuous, not one of the menu's own stops; fit itself computes anything
83
- * at or below 100, so this deliberately validates less than the element can
84
- * display.
106
+ * The zoom mode from the `zoom` property, fit by default. A host may author
107
+ * any percentage from 25 to 200 continuous, not one of the menu's stops.
108
+ * Fit itself computes anything at or below 100, so this deliberately
109
+ * validates less than the element can display.
85
110
  *
86
111
  * @param {any} zoom
87
112
  * @returns {"fit" | number}
@@ -110,15 +135,27 @@ export let name = (filename) => {
110
135
  return base;
111
136
  };
112
137
 
138
+ /**
139
+ * Whether the outline module is on, from the `outline` property, off by
140
+ * default. A flag, so anything but a boolean or nothing is a mistake.
141
+ *
142
+ * @param {any} outline
143
+ * @returns {boolean}
144
+ */
145
+ export let outlined = (outline) => {
146
+ if (outline !== undefined && typeof outline !== "boolean")
147
+ fail("outline: expected true or false");
148
+ return outline === true;
149
+ };
150
+
113
151
  /** The schemes the property accepts. Two are their own CSS value; `auto` is
114
152
  * the one that is not, so it is the only wording written here. */
115
153
  let SCHEMES = ["light", "dark", "auto"];
116
154
 
117
155
  /**
118
- * The used `color-scheme` from the `colorScheme` property. Light is the
119
- * default: embedding is the product, and following the OS would paint a light
120
- * toolbar in a host that already chose dark. `"auto"` is the opt-in that
121
- * follows, as CSS `light dark`.
156
+ * The used `color-scheme` from the `colorScheme` property, light by default:
157
+ * following the OS would paint a light toolbar in a host that already chose
158
+ * dark. `"auto"` is the opt-in that follows, as CSS `light dark`.
122
159
  *
123
160
  * @param {any} colorScheme
124
161
  * @returns {string}
package/lib/chrome.js CHANGED
@@ -4,37 +4,23 @@
4
4
  * `stage.js` keeps it paper.
5
5
  *
6
6
  * This module also owns the **default palette**: the `:host` block below is
7
- * the one place a `--qv-*` fallback is written, and every sheet this one
8
- * included — paints from the `--_*` alias it declares. That is what makes a
9
- * color one edit rather than a hunt, and `viewer.test.js` holds both halves:
10
- * `--qv-` appears in no other module, and the aliases read are exactly the
11
- * ones declared. The fallbacks are `light-dark()`, so a `color-scheme` pin on
12
- * the host paints chrome without a class.
7
+ * the one place a `--qv-*` fallback is written, and every sheet paints from
8
+ * the `--_*` alias it declares. The fallbacks are `light-dark()`, so a
9
+ * `color-scheme` pin on the host paints chrome without a class. The other
10
+ * sheets therefore paint only where this one is adopted too presence, not
11
+ * order, since a custom property resolves down the inherited chain.
13
12
  *
14
- * The cost is that the other sheets no longer stand alone they paint only
15
- * where this one is adopted too. It is presence, not order: a custom property
16
- * resolves down the inherited chain, so where CHROME sits in `static styles`
17
- * does not matter, only that it is there.
13
+ * The two halves are written differently on purpose. A host token must always
14
+ * win, so a default is a `var()` fallback which applies only when the token
15
+ * is unset never a `:host { --qv-border: }` rule, whose pseudo-class
16
+ * specificity would beat the host's own `quario-viewer` rule (ADR 0018). The
17
+ * private aliases invert that: `:host` outranks the host rule, so nothing
18
+ * outside can shadow one by accident.
18
19
  *
19
- * Host tokens always win, and `:host` is what makes that survive here. A
20
- * `:host { --qv-border: }` *default* would carry pseudo-class specificity
21
- * and beat a host's own `quario-viewer` rule the seam ADR 0018 declines to
22
- * make a lie. A `var()` fallback applies only when the token is unset, so it
23
- * loses to a host token at any specificity. The private half inverts that on
24
- * purpose: `:host` outranks that same host rule, so nothing outside can
25
- * shadow an alias by accident.
26
- *
27
- * One of the fifteen is the sheet's rather than the chrome's: a color scheme
28
- * paints chrome only (ADR 0018), so `--qv-sheet-shadow` is the sheet's edge
29
- * against the backdrop. It is in the block because it has no exceptions, not
30
- * because it follows the pin.
31
- * `--qv-focus` and `--qv-progress` repeat the accent rather than chain a third
32
- * alias — a host is free to move one alone.
33
- *
34
- * What is not here is what another module owns: `stage.js` the surface a
35
- * report is scaled on, `panel.js` the error panel, `button.js` the controls
36
- * every group in the bar is built from. The element composes their templates
37
- * and stylesheets; the imports run one way only.
20
+ * `--qv-sheet-shadow` is the sheet's edge against the backdrop rather than
21
+ * chrome (a color scheme paints chrome only, ADR 0018); it sits here because
22
+ * it has no exceptions. `--qv-focus` and `--qv-progress` repeat the accent
23
+ * rather than chain a third alias, so a host can move one alone.
38
24
  */
39
25
  import { css, html, nothing } from "lit";
40
26
 
@@ -80,13 +66,13 @@ export let CHROME = css`
80
66
  font: 13px/1.4 system-ui, sans-serif;
81
67
  }
82
68
 
83
- /* Everything below the bar. It exists to be the box the error panel is
84
- positioned against: anchored to the viewer instead, the panel would cover
85
- the bar's own controls. */
69
+ /* Everything below the bar: the outline panel when it is open, then the
70
+ stage. It exists to be the box the error panel is positioned against:
71
+ anchored to the viewer instead, the panel would cover the bar's own
72
+ controls. */
86
73
  .qv-body {
87
74
  position: relative;
88
75
  display: flex;
89
- flex-direction: column;
90
76
  flex: 1;
91
77
  min-height: 0;
92
78
  }
@@ -162,6 +148,10 @@ export let CHROME = css`
162
148
  margin-right: auto;
163
149
  }
164
150
 
151
+ .qv-outline-toggle {
152
+ display: flex;
153
+ }
154
+
165
155
  .qv-exports {
166
156
  display: flex;
167
157
  gap: 2px;
@@ -188,9 +178,8 @@ export let CHROME = css`
188
178
  `;
189
179
 
190
180
  /**
191
- * The render indicator. Idle it stays out of the accessibility tree an
192
- * unstarted progressbar has nothing to announce and the stylesheet above
193
- * hooks on the viewer's `aria-busy` to show it.
181
+ * The render indicator. Idle it stays out of the accessibility tree, and the
182
+ * stylesheet above hooks on the viewer's `aria-busy` to show it.
194
183
  *
195
184
  * @param {boolean} busy
196
185
  * @returns {import('lit').TemplateResult}
package/lib/icons.js CHANGED
@@ -21,5 +21,8 @@ export let check = icon(svg`<path d="M20 6 9 17l-5-5"/>`);
21
21
  export let download = icon(
22
22
  svg`<path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/>`,
23
23
  );
24
+ export let listTree = icon(
25
+ svg`<path d="M8 5h13"/><path d="M13 12h8"/><path d="M13 19h8"/><path d="M3 10a2 2 0 0 0 2 2h3"/><path d="M3 5v12a2 2 0 0 0 2 2h3"/>`,
26
+ );
24
27
  export let search = icon(svg`<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>`);
25
28
  export let x = icon(svg`<path d="M18 6 6 18"/><path d="m6 6 12 12"/>`);
package/lib/index.d.ts CHANGED
@@ -56,8 +56,9 @@ export interface ViewerErrorDetail {
56
56
  * element up with the panel saying why. A superseded render's failure is
57
57
  * reported to no one.
58
58
  *
59
- * Removing the element abandons in-flight work and releases its observers;
60
- * the properties persist, and reconnecting re-renders from them. There is no
59
+ * Removing the element stops the work in flight and releases its observers;
60
+ * the properties persist, and reconnecting renders them again unless the last
61
+ * render landed. There is no
61
62
  * `destroy()` — removal is destruction only in the garbage-collection sense,
62
63
  * and reparenting is safe. An export settling after removal downloads
63
64
  * nothing.
@@ -96,6 +97,14 @@ export class QuarioViewer extends LitElement {
96
97
  * follows the OS via CSS `color-scheme`. The sheet stays white.
97
98
  */
98
99
  colorScheme: "light" | "dark" | "auto" | undefined;
100
+ /**
101
+ * Whether the outline module is on. Off by default. On, the bar carries a
102
+ * toggle and a panel beside the sheet lists the report's group instances,
103
+ * nested by depth, in document order; a row scrolls to where that instance
104
+ * begins. The list is the one the PDF target writes as bookmarks, so
105
+ * nothing in the report declares it, and a hollow instance is not in it.
106
+ */
107
+ outline: boolean | undefined;
99
108
  /**
100
109
  * The newest render settling: `true` when it landed on the sheet with the
101
110
  * pages on screen finished trying to paint, `false` when it failed or there