@quario/viewer 0.6.0 → 0.8.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 +82 -44
- package/README.md +7 -0
- package/lib/check.js +17 -1
- package/lib/icons.js +1 -1
- package/lib/index.d.ts +5 -4
- package/lib/index.js +79 -86
- package/lib/panel.js +11 -4
- package/lib/stage.js +65 -361
- package/package.json +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,75 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
# @quario/viewer
|
|
2
|
+
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **An image's box is now as wide as the container it sits in.** A
|
|
8
|
+
`background` or `border*` on an image item hugged the picture and now spans
|
|
9
|
+
the content width, or the slot's share inside a split; the picture itself
|
|
10
|
+
does not move. See the `@quario/layout` changelog for the rule and for what
|
|
11
|
+
it costs a report that relied on a border hugging a logo.
|
|
12
|
+
- **Only the pages you can see are on the sheet.** A page outside the reach
|
|
13
|
+
used to be an empty canvas holding its place; now it has no element at all,
|
|
14
|
+
and the sheet carries the document's extent itself — its height and width are
|
|
15
|
+
written from the layout, and it paints the page silhouettes. The stretch a
|
|
16
|
+
reader scrolls through is exactly what it was, and a scale change no longer
|
|
17
|
+
costs the browser a relayout of every page: that was around 10 microseconds a
|
|
18
|
+
page, so a five-thousand-page report paid roughly 50 ms of it each time.
|
|
19
|
+
|
|
20
|
+
This changes the sheet's markup, which matters if you style it. `.qv-page` is
|
|
21
|
+
now absolutely positioned inside `.qv-sheet`, and is present only for the pages
|
|
22
|
+
on screen and one viewport height either side. A rule that gave a page a
|
|
23
|
+
margin, or that relied on the pages being a flow of siblings, no longer
|
|
24
|
+
applies — the sheet places its own pages, and does it with inline styles a
|
|
25
|
+
stylesheet rule cannot override. `--qv-sheet-shadow` is unchanged.
|
|
26
|
+
|
|
27
|
+
One cost, stated: a page outside the reach has no `role="img"` node, so
|
|
28
|
+
assistive technology sees the few images of the reach renumbering as the
|
|
29
|
+
reader scrolls rather than a document of named pages.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- **Every published README says where the documentation is.** Each package now
|
|
34
|
+
carries a Documentation section pointing at the reference, at the report schema
|
|
35
|
+
that normatively specifies what a report may declare, and at the package's own
|
|
36
|
+
API. The paragraphs that used to end on an unstated contract — the event
|
|
37
|
+
stream's field semantics, the style vocabulary, page columns, the Content
|
|
38
|
+
Security Policy a fragment with images needs, the formula mangling, and each
|
|
39
|
+
target's own contract — link the page that states it. Every link is an absolute
|
|
40
|
+
URL, so it resolves from the npm package page as readily as from an installed
|
|
41
|
+
copy.
|
|
42
|
+
- Updated dependencies
|
|
43
|
+
- @quario/layout@0.5.0
|
|
44
|
+
- @quario/landing@0.2.0
|
|
45
|
+
- quario@0.8.0
|
|
46
|
+
|
|
47
|
+
## 0.7.0
|
|
48
|
+
|
|
49
|
+
### Minor Changes
|
|
50
|
+
|
|
51
|
+
- **`fonts` is validated as a host property.** It was the one the viewer never
|
|
52
|
+
checked, so a malformed record reached the target inside the render and came
|
|
53
|
+
back as a render failure about a report that was never at fault. Its shape is
|
|
54
|
+
now checked with the other host properties and named on `fonts` rather than
|
|
55
|
+
on the target's `options.fonts`. A face that will not parse, or a missing
|
|
56
|
+
parser, is still found while the report is measured and remains a render
|
|
57
|
+
failure there.
|
|
58
|
+
- **A rejected host property reports as `host-option`.** `ViewerErrorKind`
|
|
59
|
+
gains a fourth member, so a host switching exhaustively over the kind needs
|
|
60
|
+
a case for it. A `page`, `zoom`, `filename`, `colorScheme` or `fonts` the
|
|
61
|
+
viewer rejected was reported as `mount-render` or `update-render` — a render
|
|
62
|
+
that was never attempted — and the panel said "Could not render the report",
|
|
63
|
+
which reads as the report being at fault. It now says the viewer is
|
|
64
|
+
misconfigured and that the report was not the problem.
|
|
65
|
+
|
|
66
|
+
A malformed `report` or `targets` still reports as a render failure: those
|
|
67
|
+
are refused where the render begins rather than when the property is written,
|
|
68
|
+
so nothing yet tells them apart from a render that failed.
|
|
69
|
+
|
|
70
|
+
## 0.6.0
|
|
71
|
+
|
|
72
|
+
### Minor Changes
|
|
13
73
|
|
|
14
74
|
- **Numbers presented through `format` now show a fixed two fraction digits**,
|
|
15
75
|
the same as every other target: `1,000.00` where the preview showed `1,000`,
|
|
@@ -17,9 +77,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
77
|
a universal two. The digits come from the engine, so a page on screen and
|
|
18
78
|
the PDF of it agree. A formatted cell is also up to three characters wider,
|
|
19
79
|
so a line that just fitted can wrap and move a page break.
|
|
20
|
-
|
|
21
|
-
### Fixed
|
|
22
|
-
|
|
23
80
|
- **A page the browser cannot fully paint no longer fails the render.** An
|
|
24
81
|
image whose pixel data is corrupt past the size in its header left that page
|
|
25
82
|
blank _and_ rejected `renderComplete`, so a host awaiting it saw an unhandled
|
|
@@ -29,29 +86,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
29
86
|
fires, and no `error` event is raised. It never rejects for this reason
|
|
30
87
|
again: a page that cannot be drawn is blank, not a failed render.
|
|
31
88
|
|
|
32
|
-
##
|
|
89
|
+
## 0.5.0
|
|
33
90
|
|
|
34
|
-
###
|
|
91
|
+
### Minor Changes
|
|
35
92
|
|
|
36
93
|
- **Previewing with `fonts` now needs `fontkit`, not `@pdf-lib/fontkit`.**
|
|
37
94
|
Install `fontkit` instead. The viewer measures host TrueType faces through
|
|
38
95
|
`@quario/layout`, whose optional parser this is; the old package crashed on
|
|
39
96
|
any OpenType face needing a shaping state machine, so those faces now
|
|
40
97
|
preview where they used to throw.
|
|
41
|
-
|
|
42
98
|
- **The bar's controls are drawn icons.** The zoom trigger, the menu's check
|
|
43
99
|
and the error panel's dismiss are drawn from Lucide's set rather than by
|
|
44
100
|
hand, at one weight and one grid, and the dismiss no longer relies on the
|
|
45
101
|
host's font to draw a multiplication sign. They paint in `currentColor`, so
|
|
46
102
|
`--qv-icon` and `--qv-icon-active` still recolour them.
|
|
47
|
-
|
|
48
103
|
- **An export button names its format in words.** Each one is a download icon
|
|
49
104
|
with `PDF`, `XLSX` or `CSV` beside it, in place of the sheet drawing that
|
|
50
105
|
lettered the format inside itself. The buttons are wider; the accessible
|
|
51
106
|
name is unchanged.
|
|
52
|
-
|
|
53
|
-
### Fixed
|
|
54
|
-
|
|
55
107
|
- **A zoom no longer leaves a page part-drawn.** Changing the zoom while a
|
|
56
108
|
page was still painting could leave that page carrying content at the old
|
|
57
109
|
scale on a canvas sized for the new one — the rest of it blank — and nothing
|
|
@@ -59,27 +111,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
59
111
|
whose paint is still in flight is now replaced rather than reused, so the
|
|
60
112
|
superseded paint has nowhere to land.
|
|
61
113
|
|
|
62
|
-
##
|
|
114
|
+
## 0.4.0
|
|
63
115
|
|
|
64
|
-
###
|
|
116
|
+
### Minor Changes
|
|
65
117
|
|
|
66
118
|
- **`fonts`.** The font mapping a host passes to `pdf({ fonts })`, so the
|
|
67
119
|
preview measures and draws in the same TrueType faces the document embeds.
|
|
68
|
-
|
|
69
|
-
### Changed
|
|
70
|
-
|
|
71
120
|
- **`renderComplete` and `rendered` settle once the pages on screen are
|
|
72
121
|
painted.** Both already answered for the newest render reaching the sheet;
|
|
73
122
|
what that means is now stated: the pages the reader can see carry their
|
|
74
123
|
pixels, and the pages further down the report do not hold the promise up.
|
|
75
124
|
A screenshot or a pixel-reading test taken at that moment sees what the
|
|
76
125
|
reader sees.
|
|
77
|
-
|
|
78
126
|
- **A split slot's box now fills the split's height.** A slot's background and
|
|
79
127
|
border stopped at that slot's own content and now take the split's height,
|
|
80
128
|
the way a table cell's take their row's. See the `@quario/layout` changelog
|
|
81
129
|
for the rule and for what it costs a report that relied on the short box.
|
|
82
|
-
|
|
83
130
|
- **The sheet has pages, and they break where the PDF's do.** The viewer
|
|
84
131
|
depends on `@quario/layout` and paints the same display list the pdf target
|
|
85
132
|
writes, one canvas per page, in the faces the document itself uses.
|
|
@@ -89,9 +136,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
89
136
|
`page` change re-lays the report out rather than resizing a sheet. The
|
|
90
137
|
unlicensed marking is painted per page from the list; the `--qv-mark`
|
|
91
138
|
token is gone with the DOM stamps.
|
|
92
|
-
|
|
93
|
-
### Fixed
|
|
94
|
-
|
|
95
139
|
- **A long report no longer asks for a backing store per page.** The viewer
|
|
96
140
|
paints the pages near the viewport — the ones on screen and one screenful
|
|
97
141
|
either side — and keeps every other page sized but blank. A report of a
|
|
@@ -102,23 +146,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
102
146
|
the first frame, and a zoom step now repaints a handful of pages instead of
|
|
103
147
|
walking the document.
|
|
104
148
|
|
|
105
|
-
##
|
|
149
|
+
## 0.3.0
|
|
106
150
|
|
|
107
|
-
###
|
|
151
|
+
### Minor Changes
|
|
108
152
|
|
|
109
153
|
- **Report and group containers are flex columns.** Adjacent authored
|
|
110
154
|
`spaceBefore` / `spaceAfter` margins add rather than collapse, matching
|
|
111
155
|
the html reference stylesheet.
|
|
112
|
-
|
|
113
|
-
### Fixed
|
|
114
|
-
|
|
115
156
|
- **The adopted sheet occupies a line and preserves authored newlines.**
|
|
116
157
|
Same `min-height: 1lh` and `white-space: pre-line` the html reference
|
|
117
158
|
stylesheet now carries on `.q-item`, and `pre-line` on table cells.
|
|
118
159
|
|
|
119
|
-
##
|
|
160
|
+
## 0.2.0
|
|
120
161
|
|
|
121
|
-
###
|
|
162
|
+
### Minor Changes
|
|
122
163
|
|
|
123
164
|
- **The sheet is now page-shaped, so short reports reserve a full page.**
|
|
124
165
|
`page` sized the sheet's width and padding and ignored its height, so a
|
|
@@ -137,9 +178,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
137
178
|
- **The adopted sheet's baseline face moved onto `.q-report`.** It was on
|
|
138
179
|
`.q-item` and `.q-table`, where a direct rule would have beaten a report's
|
|
139
180
|
own declared family. Same look; the fragment's new root is what carries it.
|
|
140
|
-
|
|
141
|
-
### Fixed
|
|
142
|
-
|
|
143
181
|
- **The sheet no longer draws report text in the platform's own face.** It
|
|
144
182
|
hard-coded `system-ui`, which is SF Pro on macOS, Segoe UI on Windows and
|
|
145
183
|
Roboto on Android — so the same report showed a different typeface to every
|
|
@@ -148,9 +186,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
148
186
|
resolves to Helvetica. The viewer's own chrome keeps `system-ui`, which is
|
|
149
187
|
what a toolbar should do.
|
|
150
188
|
|
|
151
|
-
##
|
|
189
|
+
## 0.1.0
|
|
152
190
|
|
|
153
|
-
###
|
|
191
|
+
### Minor Changes
|
|
154
192
|
|
|
155
193
|
- **An embeddable `<quario-viewer>`.** Assign a compiled report, the targets
|
|
156
194
|
to render with, and data; it shows the HTML report on a continuous sheet,
|
package/README.md
CHANGED
|
@@ -314,6 +314,13 @@ reference seam, not a frozen vocabulary: backdrop and bar (`--qv-backdrop`, `--q
|
|
|
314
314
|
The report itself is paint, not markup: there is no report stylesheet to restyle, because what
|
|
315
315
|
you see is the layout the PDF target writes.
|
|
316
316
|
|
|
317
|
+
## Documentation
|
|
318
|
+
|
|
319
|
+
[The quario documentation](https://getquario.com/docs/) is the reference.
|
|
320
|
+
The [report schema](https://getquario.com/docs/reference/report-schema/) is the normative
|
|
321
|
+
specification of what a report may declare, and
|
|
322
|
+
[`@quario/viewer`](https://getquario.com/docs/reference/viewer/) is this element's own API.
|
|
323
|
+
|
|
317
324
|
## License
|
|
318
325
|
|
|
319
326
|
Commercial software with readable source. Free, unlimited, watermarked evaluation; per-developer
|
package/lib/check.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Pure policy, no DOM: this module is what the Node suite pins.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { pageBox } from "@quario/layout";
|
|
12
|
+
import { checkFonts, pageBox } from "@quario/layout";
|
|
13
13
|
import { STEPS } from "./zoom.js";
|
|
14
14
|
|
|
15
15
|
/** @type {(message: string) => never} */
|
|
@@ -55,6 +55,22 @@ export let geometry = (page) => {
|
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* 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.
|
|
63
|
+
*
|
|
64
|
+
* @param {any} fonts
|
|
65
|
+
*/
|
|
66
|
+
export let faces = (fonts) => {
|
|
67
|
+
try {
|
|
68
|
+
checkFonts(fonts, "fonts");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
fail(/** @type {Error} */ (error).message);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
58
74
|
/** @type {(mode: any, floor: number, ceiling: number) => boolean} */
|
|
59
75
|
let inRange = (mode, floor, ceiling) =>
|
|
60
76
|
[Number.isFinite(mode), mode >= floor, mode <= ceiling].every(Boolean);
|
package/lib/icons.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* default, 14px in the gutter and the menu's tick slot — because `x` appears
|
|
5
5
|
* in two contexts at once (ADR 0044).
|
|
6
6
|
*
|
|
7
|
-
* Generated by `scripts/icons.mjs` from lucide 1.
|
|
7
|
+
* Generated by `scripts/icons.mjs` from lucide 1.42.0. Do not edit:
|
|
8
8
|
* `npm run icons` rewrites it, and `npm run check` regenerates it into a
|
|
9
9
|
* temp directory and compares.
|
|
10
10
|
*
|
package/lib/index.d.ts
CHANGED
|
@@ -24,16 +24,17 @@ export type ViewerFonts = LayoutFonts;
|
|
|
24
24
|
export type ViewableReport = CompiledReport;
|
|
25
25
|
|
|
26
26
|
/** Which failure an `error` event names. */
|
|
27
|
-
export type ViewerErrorKind = "mount-render" | "update-render" | "export";
|
|
27
|
+
export type ViewerErrorKind = "mount-render" | "update-render" | "export" | "host-option";
|
|
28
28
|
|
|
29
29
|
/** The `error` event's payload. */
|
|
30
30
|
export interface ViewerErrorDetail {
|
|
31
31
|
/** The caught value, exactly as thrown. */
|
|
32
32
|
error: unknown;
|
|
33
33
|
/**
|
|
34
|
-
* Which failure occurred: `
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Which failure occurred: `host-option` for a property the viewer rejected,
|
|
35
|
+
* which reached no render at all; otherwise `mount-render` until a render
|
|
36
|
+
* has ever landed on the sheet, `update-render` after, and `export` for a
|
|
37
|
+
* download that could not be produced.
|
|
37
38
|
*/
|
|
38
39
|
kind: ViewerErrorKind;
|
|
39
40
|
}
|
package/lib/index.js
CHANGED
|
@@ -26,10 +26,11 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { Task, TaskStatus } from "@lit/task";
|
|
28
28
|
import { LitElement, html } from "lit";
|
|
29
|
+
import { Landing, failures, options } from "@quario/landing";
|
|
29
30
|
import { layout } from "@quario/layout";
|
|
30
31
|
import { BUTTON } from "./button.js";
|
|
31
32
|
import { CHROME, progress } from "./chrome.js";
|
|
32
|
-
import { exports, geometry, level, name, scheme } from "./check.js";
|
|
33
|
+
import { exports, faces, geometry, level, name, scheme } from "./check.js";
|
|
33
34
|
import { MENU, zoomMenu } from "./menu.js";
|
|
34
35
|
import { PANEL, label, panel } from "./panel.js";
|
|
35
36
|
import { SURFACE, stage } from "./stage.js";
|
|
@@ -39,12 +40,6 @@ import { wanted } from "./zoom.js";
|
|
|
39
40
|
/** @type {(report: unknown, targets: unknown) => boolean} */
|
|
40
41
|
let vacant = (report, targets) => report === undefined && targets === undefined;
|
|
41
42
|
|
|
42
|
-
/** @type {(error: unknown) => unknown} */
|
|
43
|
-
let said = (error) => {
|
|
44
|
-
// oxlint-disable-next-line typescript/no-base-to-string
|
|
45
|
-
return error && String(error);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
43
|
// The properties are described once, in the hand-written public declarations,
|
|
49
44
|
// and read back here — a second copy in JSDoc is a copy that drifts.
|
|
50
45
|
/** @import { ViewableReport, ViewerFonts, ViewerPage } from './index.d.ts' */
|
|
@@ -89,23 +84,23 @@ export class QuarioViewer extends LitElement {
|
|
|
89
84
|
// fails and a disconnect that does not. The template interpolates its
|
|
90
85
|
// element as a node, which lit-html leaves untouched across re-renders.
|
|
91
86
|
#stage = stage();
|
|
92
|
-
/** Whether a render has ever landed on the sheet — the mount/update boundary. */
|
|
93
|
-
#landed = false;
|
|
94
|
-
/** Whether the sheet stopped matching the properties while disconnected. */
|
|
95
|
-
#stale = false;
|
|
96
87
|
/** @type {"fit" | number} The live zoom mode; the controls move it. */
|
|
97
88
|
#mode = "fit";
|
|
98
89
|
/** @type {{ width: number, height: number, margin: number }} */
|
|
99
90
|
#box = geometry(undefined);
|
|
100
91
|
#name = "report";
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
#
|
|
108
|
-
|
|
92
|
+
/** The option check across one update cycle: first-failure-wins, the
|
|
93
|
+
* re-validate-everything rule, and the commit gate that keeps a `filename`
|
|
94
|
+
* write from clobbering the mode a reader clicked to. */
|
|
95
|
+
#options = options();
|
|
96
|
+
/** The render boundary — landed, stale, and the epoch that re-runs the task
|
|
97
|
+
* when its real arguments did not change. */
|
|
98
|
+
#landing = new Landing(this, { abort: () => this.#task.abort() });
|
|
99
|
+
/** The panel's model, with this element's own announce policy: every
|
|
100
|
+
* failure is drawn, because the viewer only re-runs when a host property or
|
|
101
|
+
* the epoch moves, so its rate is already bounded. The panel is a state and
|
|
102
|
+
* the error event is the log. */
|
|
103
|
+
#failures = failures();
|
|
109
104
|
/** Re-applies geometry and scale after the update that changed them. */
|
|
110
105
|
#reapply = false;
|
|
111
106
|
/** @type {Set<string>} The exports in flight; their buttons disable. */
|
|
@@ -130,7 +125,7 @@ export class QuarioViewer extends LitElement {
|
|
|
130
125
|
// show" result: unlike the task primitive's own initial-state symbol it
|
|
131
126
|
// settles `taskComplete`, which is what lets `renderComplete` always answer.
|
|
132
127
|
#task = new Task(this, {
|
|
133
|
-
args: () => [this.report, this.targets, this.data, this.page, this.fonts, this.#epoch],
|
|
128
|
+
args: () => [this.report, this.targets, this.data, this.page, this.fonts, this.#landing.epoch],
|
|
134
129
|
task: async ([report, targets, data, page, fonts], { signal }) => {
|
|
135
130
|
if (!this.#begin(report, targets)) return null;
|
|
136
131
|
// The sheet's own target: the layout list, on the host's page and
|
|
@@ -141,7 +136,7 @@ export class QuarioViewer extends LitElement {
|
|
|
141
136
|
// The engine takes no signal, so abandonment is the guards around this
|
|
142
137
|
// body; the check only spares the swap when the answer arrives after a
|
|
143
138
|
// disconnect mid-render.
|
|
144
|
-
if (signal
|
|
139
|
+
if (this.#landing.dropped(signal, this.isConnected)) return this.#landing.abandon();
|
|
145
140
|
return { list, fonts };
|
|
146
141
|
},
|
|
147
142
|
onComplete: (result) => {
|
|
@@ -149,14 +144,12 @@ export class QuarioViewer extends LitElement {
|
|
|
149
144
|
// The swap sizes every page before it settles, so the reader's place
|
|
150
145
|
// is held; the paint it awaits is what `rendered` waits for.
|
|
151
146
|
this.#painting = this.#stage.swap(result.list, result.fonts).then(() => {
|
|
152
|
-
this.#
|
|
153
|
-
this.#stale = false;
|
|
147
|
+
this.#landing.land();
|
|
154
148
|
// A render that landed on the sheet takes the panel down: the panel
|
|
155
149
|
// says what is wrong with what the reader is looking at, and this is
|
|
156
150
|
// the moment that stops being true. A successful export is not that
|
|
157
151
|
// moment.
|
|
158
|
-
this.#
|
|
159
|
-
this.#dismissed = false;
|
|
152
|
+
this.#failures.clear();
|
|
160
153
|
// After the paint, so the update the task queued has already run.
|
|
161
154
|
this.requestUpdate();
|
|
162
155
|
this.dispatchEvent(new CustomEvent("rendered"));
|
|
@@ -164,7 +157,7 @@ export class QuarioViewer extends LitElement {
|
|
|
164
157
|
},
|
|
165
158
|
onError: (error) => {
|
|
166
159
|
if (!this.isConnected) return;
|
|
167
|
-
this.#announce(error, this.#
|
|
160
|
+
this.#announce(error, this.#kindOf(error));
|
|
168
161
|
},
|
|
169
162
|
});
|
|
170
163
|
|
|
@@ -205,9 +198,9 @@ export class QuarioViewer extends LitElement {
|
|
|
205
198
|
get renderComplete() {
|
|
206
199
|
return this.updateComplete.then(() =>
|
|
207
200
|
this.#task.status === TaskStatus.INITIAL
|
|
208
|
-
? this.#landed
|
|
201
|
+
? this.#landing.landed
|
|
209
202
|
: this.#task.taskComplete.then(
|
|
210
|
-
() => this.#painting.then(() => this.#landed),
|
|
203
|
+
() => this.#painting.then(() => this.#landing.landed),
|
|
211
204
|
() => false,
|
|
212
205
|
),
|
|
213
206
|
);
|
|
@@ -227,7 +220,7 @@ export class QuarioViewer extends LitElement {
|
|
|
227
220
|
this.#checkOptions(changed);
|
|
228
221
|
// A new document is a new subject: dismissing the last failure said
|
|
229
222
|
// nothing about this one.
|
|
230
|
-
if (["data", "report"].some((key) => changed.has(key))) this.#
|
|
223
|
+
if (["data", "report"].some((key) => changed.has(key))) this.#failures.reopen();
|
|
231
224
|
}
|
|
232
225
|
|
|
233
226
|
render() {
|
|
@@ -247,7 +240,7 @@ export class QuarioViewer extends LitElement {
|
|
|
247
240
|
})}
|
|
248
241
|
</div>
|
|
249
242
|
<div class="qv-body">
|
|
250
|
-
${this.#
|
|
243
|
+
${this.#failures.showing ? panel(this.#failures.showing, () => this.#dismiss()) : ""}
|
|
251
244
|
${this.#stage.element}
|
|
252
245
|
</div>
|
|
253
246
|
</div>
|
|
@@ -276,14 +269,12 @@ export class QuarioViewer extends LitElement {
|
|
|
276
269
|
// property write landed while disconnected. The task's arguments did not
|
|
277
270
|
// change, so the epoch is what re-runs it; left alone, reparenting a
|
|
278
271
|
// settled viewer costs nothing.
|
|
279
|
-
this.#wake();
|
|
280
272
|
}
|
|
281
273
|
|
|
282
274
|
disconnectedCallback() {
|
|
283
275
|
super.disconnectedCallback();
|
|
284
276
|
// Abandon in-flight work and release the observer. The properties and the
|
|
285
277
|
// sheet persist: removal is destruction only in the collector's sense.
|
|
286
|
-
this.#task.abort();
|
|
287
278
|
this.#unwatch?.();
|
|
288
279
|
this.#unwatch = undefined;
|
|
289
280
|
}
|
|
@@ -299,27 +290,13 @@ export class QuarioViewer extends LitElement {
|
|
|
299
290
|
* @param {unknown} targets
|
|
300
291
|
*/
|
|
301
292
|
#begin(report, targets) {
|
|
302
|
-
if (this.#invalid) throw this.#invalid;
|
|
293
|
+
if (this.#options.invalid) throw this.#options.invalid;
|
|
303
294
|
if (vacant(report, targets)) return null;
|
|
304
|
-
if (!this.isConnected) return this.#abandon();
|
|
295
|
+
if (!this.isConnected) return this.#landing.abandon();
|
|
305
296
|
exports(report, targets);
|
|
306
297
|
return true;
|
|
307
298
|
}
|
|
308
299
|
|
|
309
|
-
#abandon() {
|
|
310
|
-
this.#stale = true;
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** @param {() => void} run */
|
|
315
|
-
#take(run) {
|
|
316
|
-
try {
|
|
317
|
-
run();
|
|
318
|
-
} catch (error) {
|
|
319
|
-
this.#invalid ??= error;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
300
|
/**
|
|
324
301
|
* Each property validates and commits on its own, stashing the first
|
|
325
302
|
* failure: one bad property must not block a good write to another. All
|
|
@@ -336,42 +313,59 @@ export class QuarioViewer extends LitElement {
|
|
|
336
313
|
* @param {Map<string, unknown>} changed
|
|
337
314
|
*/
|
|
338
315
|
#checkOptions(changed) {
|
|
339
|
-
let
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
316
|
+
let moved = this.#options.recheck(this.hasUpdated, changed, [
|
|
317
|
+
(gate) => {
|
|
318
|
+
let box = geometry(this.page);
|
|
319
|
+
if (gate("page")) {
|
|
320
|
+
this.#box = box;
|
|
321
|
+
this.#reapply = true;
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
(gate) => {
|
|
325
|
+
// Committed only when the host wrote `zoom`, so re-validating on a
|
|
326
|
+
// filename change cannot clobber the mode the reader clicked to.
|
|
327
|
+
let mode = level(this.zoom);
|
|
328
|
+
if (gate("zoom")) {
|
|
329
|
+
this.#mode = mode;
|
|
330
|
+
this.#reapply = true;
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
() => void (this.#name = name(this.filename)),
|
|
334
|
+
// Nothing to commit: the shape is the whole answer, and the faces
|
|
335
|
+
// themselves are loaded inside the render where a bad one is a render
|
|
336
|
+
// failure. Checked here so a malformed record is a mistake named on the
|
|
337
|
+
// property rather than one the target names on `options.fonts`.
|
|
338
|
+
() => faces(this.fonts),
|
|
339
|
+
(gate) => {
|
|
340
|
+
// Same gate as zoom/page: re-validate always, write the CSSOM pin only
|
|
341
|
+
// when this property changed, so a filename write does not dirty
|
|
342
|
+
// inherited color-scheme before layout.
|
|
343
|
+
let used = scheme(this.colorScheme);
|
|
344
|
+
if (gate("colorScheme")) this.style.colorScheme = used;
|
|
345
|
+
},
|
|
346
|
+
]);
|
|
347
|
+
if (moved) this.#landing.epoch++;
|
|
368
348
|
}
|
|
369
349
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Which failure a throw from the task is. Identity, not a tag: `#begin`
|
|
352
|
+
* throws the very value `#checkOptions` stashed, so the one it stashed is
|
|
353
|
+
* the one host mistake this can name for certain — and a property the
|
|
354
|
+
* viewer rejected reached no render at all, so it carries no
|
|
355
|
+
* mount-versus-update distinction.
|
|
356
|
+
*
|
|
357
|
+
* `exports()`'s `report` and `targets` are host mistakes too and still
|
|
358
|
+
* report as a render: they are thrown from `#begin` rather than stashed, so
|
|
359
|
+
* nothing here tells them from a render that failed. Closing that means the
|
|
360
|
+
* task returning failures instead of throwing them, which is the channel
|
|
361
|
+
* quario-70jg.27 reworks — not worth doing twice.
|
|
362
|
+
*
|
|
363
|
+
* @param {unknown} error
|
|
364
|
+
* @returns {import('./index.d.ts').ViewerErrorKind}
|
|
365
|
+
*/
|
|
366
|
+
#kindOf(error) {
|
|
367
|
+
if (error !== undefined && error === this.#options.invalid) return "host-option";
|
|
368
|
+
return this.#landing.landed ? "update-render" : "mount-render";
|
|
375
369
|
}
|
|
376
370
|
|
|
377
371
|
/** @param {() => void} run */
|
|
@@ -408,7 +402,7 @@ export class QuarioViewer extends LitElement {
|
|
|
408
402
|
}
|
|
409
403
|
|
|
410
404
|
#dismiss() {
|
|
411
|
-
this.#
|
|
405
|
+
this.#failures.dismiss();
|
|
412
406
|
this.requestUpdate();
|
|
413
407
|
}
|
|
414
408
|
|
|
@@ -418,12 +412,11 @@ export class QuarioViewer extends LitElement {
|
|
|
418
412
|
* or anything after a disconnect — never come through here.
|
|
419
413
|
*
|
|
420
414
|
* @param {unknown} error The caught value.
|
|
421
|
-
* @param {
|
|
415
|
+
* @param {import('./index.d.ts').ViewerErrorKind} kind
|
|
422
416
|
* @param {string} [format] The export format's name, for that kind alone.
|
|
423
417
|
*/
|
|
424
418
|
#announce(error, kind, format) {
|
|
425
|
-
this.#
|
|
426
|
-
this.#dismissed = false;
|
|
419
|
+
this.#failures.announce(label(kind, format), error);
|
|
427
420
|
this.requestUpdate();
|
|
428
421
|
this.dispatchEvent(new CustomEvent("error", { detail: { error, kind } }));
|
|
429
422
|
}
|
package/lib/panel.js
CHANGED
|
@@ -80,16 +80,23 @@ export let PANEL = css`
|
|
|
80
80
|
* its length: that failure leaves the previous report on the sheet, and
|
|
81
81
|
* silently stale content is the thing the panel exists to prevent.
|
|
82
82
|
*
|
|
83
|
-
* @param {
|
|
83
|
+
* @param {import('./index.d.ts').ViewerErrorKind} kind
|
|
84
84
|
* @param {string} [format] The export format's name, for that kind alone.
|
|
85
85
|
* @returns {string}
|
|
86
86
|
*/
|
|
87
87
|
export let label = (kind, format) =>
|
|
88
88
|
kind === "export"
|
|
89
89
|
? "Could not export " + format
|
|
90
|
-
: kind === "
|
|
91
|
-
?
|
|
92
|
-
|
|
90
|
+
: kind === "host-option"
|
|
91
|
+
? // Named apart because the panel speaks to the reader, and where that
|
|
92
|
+
// reader is a playground's visitor the panel is the product rather
|
|
93
|
+
// than chrome (CONTEXT.md, "Error panel"). Told the report could not
|
|
94
|
+
// be rendered, they read it as their own document failing. A property
|
|
95
|
+
// they cannot see and cannot fix is where that reading is wrong.
|
|
96
|
+
"This viewer is misconfigured — the report itself was not the problem."
|
|
97
|
+
: kind === "update-render"
|
|
98
|
+
? "Could not update the report — showing the previous version"
|
|
99
|
+
: "Could not render the report";
|
|
93
100
|
|
|
94
101
|
/**
|
|
95
102
|
* What an error says. Not every throw is an `Error` — a host's registry
|
package/lib/stage.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The stage: the surface the pages are painted on. It owns the scroll
|
|
3
3
|
* container the reader looks through, the wrapper that carries the gutter,
|
|
4
|
-
* and the sheet —
|
|
5
|
-
* `@quario/layout`'s `paint` — their elements, their
|
|
6
|
-
* relating them.
|
|
4
|
+
* and the sheet — the layout list's whole extent, with a canvas for each page
|
|
5
|
+
* of the reach painted by `@quario/layout`'s `paint` — their elements, their
|
|
6
|
+
* CSS, and every number relating them.
|
|
7
7
|
*
|
|
8
8
|
* Zoom is a repaint, not a transform: each page canvas is sized to the
|
|
9
9
|
* percentage in CSS pixels and painted at that scale times the device pixel
|
|
@@ -11,15 +11,17 @@
|
|
|
11
11
|
* reader saw. The list never changes under a zoom — the preview scales and
|
|
12
12
|
* never reflows (zoom.js says why).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* [reach](../../../CONTEXT.md#reach) carries pixels — the pages
|
|
16
|
-
* one viewport height either side. The extent the reader scrolls
|
|
17
|
-
* whole and synchronous whatever is
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* the
|
|
14
|
+
* The sheet carries the whole list's extent, but only the
|
|
15
|
+
* [reach](../../../CONTEXT.md#reach) carries elements and pixels — the pages
|
|
16
|
+
* on screen and one viewport height either side. The extent the reader scrolls
|
|
17
|
+
* through is whole and synchronous whatever is standing, because the sheet is
|
|
18
|
+
* given its height and width inline and paints the page silhouettes itself; a
|
|
19
|
+
* page leaving the reach is removed, which takes its backing store with it
|
|
20
|
+
* (ADR 0065). A scroll or a resize runs the same arithmetic over the list's own
|
|
21
|
+
* geometry, coalesced to one pass a frame. ADR 0043 says what the reach is
|
|
22
|
+
* worth: painting the whole sheet asked a thousand-page report for gigabytes of
|
|
23
|
+
* backing store, and past what the browser would grant the pages simply came up
|
|
24
|
+
* blank.
|
|
23
25
|
*
|
|
24
26
|
* **A page the browser will not paint is blank, not a failure.** `start`
|
|
25
27
|
* swallows and nothing here rejects: the report laid out and the list reached
|
|
@@ -39,13 +41,14 @@
|
|
|
39
41
|
* *retired* rather than painted over or emptied: it comes off the sheet, its
|
|
40
42
|
* pixels go back, and a fresh one stands in its place, so the superseded
|
|
41
43
|
* paint draws into an element nobody is looking at. A canvas whose paint has
|
|
42
|
-
* settled has nothing that could land late and is re-sized
|
|
43
|
-
*
|
|
44
|
+
* settled has nothing that could land late and is re-sized in place as before;
|
|
45
|
+
* a page leaving the reach is removed either way, which takes its pixels with
|
|
46
|
+
* it whether that paint has landed or not. Without this a zoom landing mid-paint could leave a page
|
|
44
47
|
* carrying old-scale content on a new-scale canvas, with the memo below
|
|
45
48
|
* calling it painted so that nothing repainted it again (ADR 0046).
|
|
46
49
|
*
|
|
47
|
-
* Retirement is lazy, and only the paint is guarded. A scale change sizes
|
|
48
|
-
*
|
|
50
|
+
* Retirement is lazy, and only the paint is guarded. A scale change sizes the
|
|
51
|
+
* reach's CSS boxes at once but retires a page only when the repaint loop
|
|
49
52
|
* reaches it, so a page further down the reach shows its old pixels stretched
|
|
50
53
|
* into the new box until its turn comes — the ordinary look of a zoom in
|
|
51
54
|
* progress, not the artefact above. The cost is that a retired page is a
|
|
@@ -70,17 +73,14 @@
|
|
|
70
73
|
*/
|
|
71
74
|
|
|
72
75
|
import { css } from "lit";
|
|
73
|
-
import {
|
|
76
|
+
import { GUTTER, sheet } from "@quario/landing";
|
|
77
|
+
import { PX_PER_POINT } from "@quario/layout";
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
let GUTTER = 28;
|
|
81
|
-
|
|
82
|
-
/** The gap between two pages, in px. */
|
|
83
|
-
let GAP = 16;
|
|
79
|
+
// `GUTTER` comes from `@quario/landing`, which walks the reach over it: the CSS
|
|
80
|
+
// below and the sheet's own arithmetic have to agree about where a page sits,
|
|
81
|
+
// and two copies of that is a reach that paints the wrong pages. The gap
|
|
82
|
+
// between pages is that package's too, and is not named here at all — the sheet
|
|
83
|
+
// positions its own pages now, so this file has nothing to space.
|
|
84
84
|
|
|
85
85
|
export let SURFACE = css`
|
|
86
86
|
/* Where scrollbars take width, the gutter is held whether one is showing or
|
|
@@ -106,16 +106,17 @@ export let SURFACE = css`
|
|
|
106
106
|
margin: ${GUTTER}px auto;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/* The sheet is the
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
and being painted
|
|
109
|
+
/* The sheet is the whole document's extent, and the pages of the reach are
|
|
110
|
+
positioned inside it. It has no rule here because it has no look: its
|
|
111
|
+
position, its height, its width and its page silhouettes are all written
|
|
112
|
+
inline by \`@quario/landing\` from the list's own geometry, which is the one
|
|
113
|
+
place any of them is decided (ADR 0065). A page outside the reach has no
|
|
114
|
+
element, so what shows there is the sheet's own paint.
|
|
115
|
+
|
|
116
|
+
One page of the reach, at the size and offset the sheet gives it. The
|
|
117
|
+
painter fills it white; the background here is what it shows between being
|
|
118
|
+
placed and being painted, and the shadow is what lifts it off the sheet —
|
|
119
|
+
the two this file does decide. */
|
|
119
120
|
.qv-page {
|
|
120
121
|
display: block;
|
|
121
122
|
background: #fff;
|
|
@@ -150,319 +151,24 @@ export let stage = () => {
|
|
|
150
151
|
// inside the viewer. The `qv-*` names are documented as stable, so the
|
|
151
152
|
// narrower one keeps its name.
|
|
152
153
|
wrapper.className = "qv-stage";
|
|
153
|
-
let
|
|
154
|
-
|
|
155
|
-
wrapper.append(
|
|
154
|
+
let sheetEl = document.createElement("div");
|
|
155
|
+
sheetEl.className = "qv-sheet";
|
|
156
|
+
wrapper.append(sheetEl);
|
|
156
157
|
scroll.append(wrapper);
|
|
157
158
|
|
|
158
159
|
/** The page width to fit against before a list arrives, in points. */
|
|
159
160
|
let fallback = 0;
|
|
160
|
-
/** @type {any} */
|
|
161
|
-
let list = null;
|
|
162
|
-
/** @type {any} */
|
|
163
|
-
let fonts;
|
|
164
161
|
/** The percentage currently on screen. */
|
|
165
162
|
let applied = 100;
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* What each backed page is carrying: the scale it was painted at, so a page
|
|
169
|
-
* re-entering the reach at that scale is not repainted and a scale change
|
|
170
|
-
* invalidates it, and the paint itself, so whoever asks second waits behind
|
|
171
|
-
* the paint the first caller started rather than being told it is done.
|
|
172
|
-
* Keyed by the canvas, so a swap's new pages start with nothing remembered
|
|
173
|
-
* and the pages it replaced take their entries with them — which is why the
|
|
174
|
-
* map is weak: nothing has to remember to forget them.
|
|
175
|
-
*
|
|
176
|
-
* @type {WeakMap<HTMLCanvasElement, { scale: number, painted: Promise<void> }>}
|
|
177
|
-
*/
|
|
178
|
-
let backed = new WeakMap();
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* The canvases whose paint has finished. A promise's settled state is not
|
|
182
|
-
* synchronously observable and this is the one question the rule below
|
|
183
|
-
* turns on: a canvas in here has nothing left that could draw into it, so
|
|
184
|
-
* it can be emptied or re-sized in place; one that is backed but absent
|
|
185
|
-
* here is still being painted, and must be retired instead. Weak on the
|
|
186
|
-
* same key as `backed`, so a retired canvas takes its membership with it.
|
|
187
|
-
*
|
|
188
|
-
* Membership is per paint, not per canvas — `start` takes a canvas out
|
|
189
|
-
* before painting it again, or a page settled at one scale would count as
|
|
190
|
-
* settled the moment it began painting at the next.
|
|
191
|
-
*
|
|
192
|
-
* @type {WeakSet<HTMLCanvasElement>}
|
|
193
|
-
*/
|
|
194
|
-
let settled = new WeakSet();
|
|
195
|
-
|
|
196
163
|
/** CSS pixels per point at the applied percentage. */
|
|
197
164
|
let ratio = () => (PX_PER_POINT * applied) / 100;
|
|
198
165
|
|
|
199
|
-
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
|
|
205
|
-
*/
|
|
206
|
-
let pageAt = (i) => /** @type {HTMLCanvasElement} */ (sheet.children[i]);
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Give a page's canvas its size on screen. The one answer to how big page
|
|
210
|
-
* `i` is at the applied percentage: `pageOf` builds a canvas with it and
|
|
211
|
-
* `sizeAll` writes it over the sheet after a scale change, so a page
|
|
212
|
-
* retired between the two cannot arrive sizeless and move the extent the
|
|
213
|
-
* reader is scrolling through.
|
|
214
|
-
*
|
|
215
|
-
* @type {(canvas: HTMLElement, i: number, px?: number) => void}
|
|
216
|
-
*/
|
|
217
|
-
let sizePage = (canvas, i, px = ratio()) => {
|
|
218
|
-
let each = list.pages[i];
|
|
219
|
-
canvas.style.width = each.width * px + "px";
|
|
220
|
-
canvas.style.height = each.height * px + "px";
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* One page of the list as a canvas: sized on screen, carrying no pixels
|
|
225
|
-
* yet, and named for a screen reader. Every canvas on the sheet is built
|
|
226
|
-
* here — a swap's and a retirement's alike — so a replacement is the same
|
|
227
|
-
* element in every respect but identity.
|
|
228
|
-
*
|
|
229
|
-
* @type {(i: number) => HTMLCanvasElement}
|
|
230
|
-
*/
|
|
231
|
-
let pageOf = (i) => {
|
|
232
|
-
let canvas = document.createElement("canvas");
|
|
233
|
-
canvas.className = "qv-page";
|
|
234
|
-
// A canvas is 300 x 150 until told otherwise, and that store counts. A
|
|
235
|
-
// page starts with none and takes one once it is inside the reach.
|
|
236
|
-
canvas.width = 0;
|
|
237
|
-
canvas.height = 0;
|
|
238
|
-
canvas.setAttribute("role", "img");
|
|
239
|
-
canvas.setAttribute("aria-label", "Page " + (i + 1) + " of " + list.pages.length);
|
|
240
|
-
sizePage(canvas, i);
|
|
241
|
-
return canvas;
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Take page `i`'s canvas off the sheet and stand a fresh one in its place,
|
|
246
|
-
* answering with the replacement. A paint still in flight holds the old
|
|
247
|
-
* canvas's context and draws into something nobody is looking at, which is
|
|
248
|
-
* how a superseded paint is stopped here — by construction, rather than by
|
|
249
|
-
* a check the painter would have to make above its own draw (ADR 0046).
|
|
250
|
-
* The retired canvas takes its entries in `backed` and `settled` with it.
|
|
251
|
-
*
|
|
252
|
-
* Its pixels go back at once, by the same 0 × 0 idiom `release` uses: the
|
|
253
|
-
* store is what ADR 0043 rations, and leaving it to be collected whenever
|
|
254
|
-
* the superseded paint lets go of the context is the timing that ADR
|
|
255
|
-
* refuses. `paint()` does its whole `save`/draw/`restore` after its awaits,
|
|
256
|
-
* so emptying the canvas between them unbalances nothing — it just leaves
|
|
257
|
-
* every op clipped to nothing, which spares the raster work too.
|
|
258
|
-
*
|
|
259
|
-
* @type {(i: number) => HTMLCanvasElement}
|
|
260
|
-
*/
|
|
261
|
-
let retire = (i) => {
|
|
262
|
-
let old = pageAt(i);
|
|
263
|
-
let fresh = pageOf(i);
|
|
264
|
-
old.replaceWith(fresh);
|
|
265
|
-
old.width = 0;
|
|
266
|
-
old.height = 0;
|
|
267
|
-
return fresh;
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Give page `i`'s pixels back. A page whose paint has settled is emptied in
|
|
272
|
-
* place: sizing the canvas to 0 × 0 is the one idiom that frees the store
|
|
273
|
-
* synchronously in every engine the viewer runs in, and the CSS size is
|
|
274
|
-
* untouched, so the page keeps its place in the extent and shows the
|
|
275
|
-
* sheet's white. A page still painting is retired instead — emptying it
|
|
276
|
-
* would leave that paint pointed at a canvas the next pass over the reach
|
|
277
|
-
* re-sizes and re-paints.
|
|
278
|
-
*
|
|
279
|
-
* @type {(i: number) => void}
|
|
280
|
-
*/
|
|
281
|
-
let release = (i) => {
|
|
282
|
-
let canvas = pageAt(i);
|
|
283
|
-
if (!backed.has(canvas)) return;
|
|
284
|
-
if (!settled.has(canvas)) return void retire(i);
|
|
285
|
-
backed.delete(canvas);
|
|
286
|
-
canvas.width = 0;
|
|
287
|
-
canvas.height = 0;
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
/** Device pixels per point at the applied percentage: what a page is
|
|
291
|
-
* painted at, and what its backing store is sized in. */
|
|
292
|
-
let deviceScale = () => ratio() * (globalThis.devicePixelRatio || 1);
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Start painting page `i`, and answer with that paint. **The one place a
|
|
296
|
-
* paint begins, and the one place the rule is enforced**: a canvas whose
|
|
297
|
-
* paint is still in flight is retired here before a second one is pointed
|
|
298
|
-
* at it, so no caller can reach a live canvas with a second paint by
|
|
299
|
-
* forgetting to ask first (ADR 0046). A paint that failed is forgotten, so
|
|
300
|
-
* the next pass over the reach tries again instead of counting the page as
|
|
301
|
-
* painted; one that settles joins `settled`.
|
|
302
|
-
*
|
|
303
|
-
* **The paint this answers with never rejects**, which is the whole of why
|
|
304
|
-
* `swap` does not either. A page that could not be drawn is not a render
|
|
305
|
-
* that failed: the report laid out and the list reached the sheet, so there
|
|
306
|
-
* is nothing for the error panel to say and nothing to report through the
|
|
307
|
-
* error event. Swallowed here, the one place a paint begins, so no caller
|
|
308
|
-
* has to remember to; one bad page then costs its own pixels rather than
|
|
309
|
-
* every page after it in the reach, which this loop awaits one at a time.
|
|
310
|
-
*
|
|
311
|
-
* See the header for how little can still reach it — the layout absorbs a
|
|
312
|
-
* bad image and refuses a bad face while measuring — and why it stays
|
|
313
|
-
* anyway. A paint that failed is forgotten so the next pass over the reach
|
|
314
|
-
* tries again, which is what makes a failure for a passing reason
|
|
315
|
-
* recoverable.
|
|
316
|
-
*
|
|
317
|
-
* @type {(i: number) => Promise<void>}
|
|
318
|
-
*/
|
|
319
|
-
let start = (i) => {
|
|
320
|
-
let canvas = pageAt(i);
|
|
321
|
-
if (backed.has(canvas) && !settled.has(canvas)) canvas = retire(i);
|
|
322
|
-
settled.delete(canvas);
|
|
323
|
-
let scale = deviceScale();
|
|
324
|
-
let each = list.pages[i];
|
|
325
|
-
canvas.width = Math.round(each.width * scale);
|
|
326
|
-
canvas.height = Math.round(each.height * scale);
|
|
327
|
-
let ctx = /** @type {CanvasRenderingContext2D} */ (canvas.getContext("2d"));
|
|
328
|
-
let painted = paint(ctx, each, { scale, fonts }).then(
|
|
329
|
-
() => {
|
|
330
|
-
settled.add(canvas);
|
|
331
|
-
},
|
|
332
|
-
() => {
|
|
333
|
-
backed.delete(canvas);
|
|
334
|
-
},
|
|
335
|
-
);
|
|
336
|
-
backed.set(canvas, { scale, painted });
|
|
337
|
-
return painted;
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Paint one page and answer with that paint — or with the paint already
|
|
342
|
-
* under way at this scale, which is what makes scrolling back over ground
|
|
343
|
-
* already covered free. Answering with the paint rather than with a
|
|
344
|
-
* resolved promise is what lets two callers share one page's paint and both
|
|
345
|
-
* settle behind its pixels.
|
|
346
|
-
*
|
|
347
|
-
* Takes the index, not the canvas: the page it paints may be retired out
|
|
348
|
-
* from under a caller, so a caller that handed one in would be left holding
|
|
349
|
-
* an element that is no longer on the sheet.
|
|
350
|
-
*
|
|
351
|
-
* @type {(i: number) => Promise<void>}
|
|
352
|
-
*/
|
|
353
|
-
let paintPage = (i) => {
|
|
354
|
-
let carrying = backed.get(pageAt(i));
|
|
355
|
-
if (carrying && carrying.scale === deviceScale()) return carrying.painted;
|
|
356
|
-
return start(i);
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Bumped per repaint, so a repaint overtaken by the next stops walking.
|
|
361
|
-
* What that guards is the backing store, not the pixels: a superseded loop
|
|
362
|
-
* would paint at the *current* scale — `paintPage` reads it afresh — but
|
|
363
|
-
* onto pages the newer reach has since dropped, re-backing pages that
|
|
364
|
-
* should be blank (ADR 0043). Stale pixels are `retire`'s business, not
|
|
365
|
-
* this one, so neither guard stands in for the other.
|
|
366
|
-
*
|
|
367
|
-
* It guards repaint against repaint, and nothing else. A repaint chooses
|
|
368
|
-
* its pages once and holds that list across its awaits, so a scroll pass
|
|
369
|
-
* releasing a page mid-repaint is one this loop will paint anyway; the
|
|
370
|
-
* store that leaves behind is bounded by the reach and goes back on the
|
|
371
|
-
* next pass over it.
|
|
372
|
-
*/
|
|
373
|
-
let epoch = 0;
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Give every page its CSS size. Callers run this before writing the scroll
|
|
377
|
-
* offsets back: the extent those offsets are clamped against is this one,
|
|
378
|
-
* and the reach below is read from the offsets once they are in.
|
|
379
|
-
*/
|
|
380
|
-
let sizeAll = () => {
|
|
381
|
-
if (!list) return;
|
|
382
|
-
let px = ratio();
|
|
383
|
-
for (let i = 0; i < list.pages.length; i++) sizePage(pageAt(i), i, px);
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* The indices of the pages in the reach — the ones the viewport shows, and
|
|
388
|
-
* everything within one viewport height above or below. Walked over the
|
|
389
|
-
* list's own geometry (the gutter, each page's height at the applied
|
|
390
|
-
* scale, the gap), never a layout read, so it costs nothing to ask
|
|
391
|
-
* mid-scroll — the whole list is walked because a multiply and an add per
|
|
392
|
-
* page is beneath measuring, and stopping early would be a second rule
|
|
393
|
-
* about where the reach ends. `clientHeight` is read afresh each time, so a
|
|
394
|
-
* pane that changed size changes the reach with it.
|
|
395
|
-
*
|
|
396
|
-
* @returns {number[]}
|
|
397
|
-
*/
|
|
398
|
-
let inReach = () => {
|
|
399
|
-
let view = scroll.clientHeight;
|
|
400
|
-
let top = scroll.scrollTop - view;
|
|
401
|
-
let bottom = scroll.scrollTop + 2 * view;
|
|
402
|
-
let px = ratio();
|
|
403
|
-
let y = GUTTER;
|
|
404
|
-
let found = [];
|
|
405
|
-
for (let [i, each] of list.pages.entries()) {
|
|
406
|
-
let height = each.height * px;
|
|
407
|
-
if (y + height >= top && y <= bottom) found.push(i);
|
|
408
|
-
y += height + GAP;
|
|
409
|
-
}
|
|
410
|
-
return found;
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Release every page the reach has left behind, and answer with the indices
|
|
415
|
-
* of the ones to keep, in the order they are painted in. The single
|
|
416
|
-
* definition of which pages carry pixels: the repaint below and the scroll
|
|
417
|
-
* pass both go through here, and neither holds a page the other released.
|
|
418
|
-
*
|
|
419
|
-
* Indices rather than elements, for the reason `paintPage` takes one: a
|
|
420
|
-
* page can be retired between this pass and the paint that follows it.
|
|
421
|
-
*
|
|
422
|
-
* @type {() => number[]}
|
|
423
|
-
*/
|
|
424
|
-
let keep = () => {
|
|
425
|
-
let wanted = new Set(inReach());
|
|
426
|
-
let kept = [];
|
|
427
|
-
// `retire` replaces one for one, so the count is fixed across the walk.
|
|
428
|
-
for (let i = 0, total = sheet.children.length; i < total; i++) {
|
|
429
|
-
if (wanted.has(i)) kept.push(i);
|
|
430
|
-
else release(i);
|
|
431
|
-
}
|
|
432
|
-
return kept;
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
// Paint the reach in order, yielding between pages and giving way to any
|
|
436
|
-
// repaint that started since. Sizing is the caller's, and comes first.
|
|
437
|
-
let repaint = async () => {
|
|
438
|
-
if (!list) return;
|
|
439
|
-
let mine = ++epoch;
|
|
440
|
-
for (let i of keep()) {
|
|
441
|
-
if (mine !== epoch) return;
|
|
442
|
-
await paintPage(i);
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* One reach pass a frame, however many scrolls and resizes ask for one. It
|
|
448
|
-
* keeps no epoch of its own, so a swap painting behind it is never cut off
|
|
449
|
-
* part-painted, and a page it starts is one a swap arriving at the same page
|
|
450
|
-
* waits behind rather than skips. Nothing awaits these paints here: a scroll
|
|
451
|
-
* is not a render, and a page that will not paint is blank whoever asked for
|
|
452
|
-
* it — `start` settles either way, so there is nothing here to catch.
|
|
453
|
-
*/
|
|
454
|
-
let pending = false;
|
|
455
|
-
let follow = () => {
|
|
456
|
-
if (pending) return;
|
|
457
|
-
pending = true;
|
|
458
|
-
requestAnimationFrame(() => {
|
|
459
|
-
pending = false;
|
|
460
|
-
if (!list) return;
|
|
461
|
-
for (let i of keep()) void paintPage(i);
|
|
462
|
-
});
|
|
463
|
-
};
|
|
464
|
-
scroll.addEventListener("scroll", follow);
|
|
465
|
-
|
|
166
|
+
/** The paged sheet: the page canvases, their backing-store lifetime, the
|
|
167
|
+
* reach and its painting, in `@quario/landing`. It asks `ratio()` afresh,
|
|
168
|
+
* so the percentage this element applies is the scale it paints at — and
|
|
169
|
+
* the elements above stay this file's, because the CSS is the viewer's own
|
|
170
|
+
* public surface. */
|
|
171
|
+
let paged = sheet(scroll, sheetEl, ratio);
|
|
466
172
|
return {
|
|
467
173
|
element: scroll,
|
|
468
174
|
|
|
@@ -475,7 +181,7 @@ export let stage = () => {
|
|
|
475
181
|
fit: () => {
|
|
476
182
|
let usable = scroll.clientWidth - 2 * GUTTER;
|
|
477
183
|
// The list's page width, or the geometry's before one arrives.
|
|
478
|
-
let width = (
|
|
184
|
+
let width = (paged.width() ?? fallback) * PX_PER_POINT;
|
|
479
185
|
if (usable <= 0 || !width) return null;
|
|
480
186
|
return (usable / width) * 100;
|
|
481
187
|
},
|
|
@@ -491,14 +197,17 @@ export let stage = () => {
|
|
|
491
197
|
* offset; horizontally the wrapper is centred by auto margins while it
|
|
492
198
|
* fits — which is exactly when `scrollLeft` is 0 anyway — and its margins
|
|
493
199
|
* are 0 once it overflows, so the plain ratio holds wherever it can be
|
|
494
|
-
* seen. The browser clamps whatever it cannot honour.
|
|
495
|
-
* no view to hold, which is what mounting at an authored zoom
|
|
200
|
+
* seen. The browser clamps whatever it cannot honour. A sheet with no list
|
|
201
|
+
* on it has no view to hold, which is what mounting at an authored zoom
|
|
202
|
+
* takes. Asked of the list rather than of the sheet's children, which
|
|
203
|
+
* since ADR 0065 are the reach's and can be none of them while a list is
|
|
204
|
+
* standing — and it saves a DOM read besides.
|
|
496
205
|
*
|
|
497
206
|
* Only the reach is repainted, and only where the scale actually changed,
|
|
498
207
|
* so a zoom step costs a handful of pages however long the report is.
|
|
499
208
|
*/
|
|
500
209
|
scale: (percent) => {
|
|
501
|
-
let held =
|
|
210
|
+
let held = paged.count() > 0 && {
|
|
502
211
|
top: scroll.scrollTop,
|
|
503
212
|
left: scroll.scrollLeft,
|
|
504
213
|
height: scroll.clientHeight,
|
|
@@ -506,13 +215,13 @@ export let stage = () => {
|
|
|
506
215
|
};
|
|
507
216
|
let ratioOf = percent / applied;
|
|
508
217
|
applied = percent;
|
|
509
|
-
sizeAll();
|
|
218
|
+
paged.sizeAll();
|
|
510
219
|
if (held) {
|
|
511
220
|
let middle = held.top + held.height / 2 - GUTTER;
|
|
512
221
|
scroll.scrollTop = GUTTER + middle * ratioOf - held.height / 2;
|
|
513
222
|
scroll.scrollLeft = (held.left + held.width / 2) * ratioOf - held.width / 2;
|
|
514
223
|
}
|
|
515
|
-
void repaint();
|
|
224
|
+
void paged.repaint();
|
|
516
225
|
},
|
|
517
226
|
|
|
518
227
|
/**
|
|
@@ -528,12 +237,12 @@ export let stage = () => {
|
|
|
528
237
|
|
|
529
238
|
/**
|
|
530
239
|
* Put a laid-out report on the sheet, keeping the reader where they
|
|
531
|
-
* were:
|
|
532
|
-
* scale. The order is the point of the method: the
|
|
533
|
-
*
|
|
534
|
-
* back against the extent the sheet will have rather than
|
|
535
|
-
* — and the reach is read from those offsets, so it is
|
|
536
|
-
* are in. What the
|
|
240
|
+
* were: the extent whole at once, with the reach's pages painted at the
|
|
241
|
+
* applied scale. The order is the point of the method: the sheet takes its
|
|
242
|
+
* height from the list before anything else, so the browser clamps the
|
|
243
|
+
* offsets going back against the extent the sheet will have rather than
|
|
244
|
+
* the one it had — and the reach is read from those offsets, so it is
|
|
245
|
+
* chosen after they are in. What the
|
|
537
246
|
* returned promise settles behind is the reach, which is what the caller's
|
|
538
247
|
* `renderComplete` means by "the pages on screen have finished trying to
|
|
539
248
|
* paint" — and **it never rejects**, because `start` swallows a page the
|
|
@@ -541,21 +250,16 @@ export let stage = () => {
|
|
|
541
250
|
*/
|
|
542
251
|
swap: async (next, faces) => {
|
|
543
252
|
// Reading the offsets flushes layout, so only an actual reswap pays for
|
|
544
|
-
// it:
|
|
545
|
-
let held =
|
|
253
|
+
// it: with no list standing there is nothing scrolled to preserve.
|
|
254
|
+
let held = paged.count() > 0 && {
|
|
546
255
|
top: scroll.scrollTop,
|
|
547
256
|
left: scroll.scrollLeft,
|
|
548
257
|
};
|
|
549
|
-
|
|
550
|
-
fonts = faces;
|
|
551
|
-
sheet.replaceChildren(
|
|
552
|
-
...next.pages.map((/** @type {any} */ _, /** @type {number} */ i) => pageOf(i)),
|
|
553
|
-
);
|
|
258
|
+
await paged.swap(next, faces);
|
|
554
259
|
if (held) {
|
|
555
260
|
scroll.scrollTop = held.top;
|
|
556
261
|
scroll.scrollLeft = held.left;
|
|
557
262
|
}
|
|
558
|
-
await repaint();
|
|
559
263
|
},
|
|
560
264
|
|
|
561
265
|
/**
|
|
@@ -568,7 +272,7 @@ export let stage = () => {
|
|
|
568
272
|
let observer = new ResizeObserver(() => {
|
|
569
273
|
// A resize that changes the fit repaints through `changed()`; one
|
|
570
274
|
// that does not still moved the viewport the reach is measured in.
|
|
571
|
-
follow();
|
|
275
|
+
paged.follow();
|
|
572
276
|
changed();
|
|
573
277
|
});
|
|
574
278
|
observer.observe(scroll);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "The embeddable report viewer shell for quario — in the makings, not yet released",
|
|
5
5
|
"homepage": "https://getquario.com",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@lit/task": "^1.0.3",
|
|
47
|
-
"@quario/
|
|
47
|
+
"@quario/landing": "^0.2.0",
|
|
48
|
+
"@quario/layout": "^0.5.0",
|
|
48
49
|
"lit": "^3.3.3"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
@@ -53,18 +54,19 @@
|
|
|
53
54
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
54
55
|
"esbuild": "^0.28.2",
|
|
55
56
|
"exceljs": "^4.4.0",
|
|
56
|
-
"quario": "^0.
|
|
57
|
+
"quario": "^0.8.0",
|
|
57
58
|
"size-limit": "^13.0.3",
|
|
58
59
|
"typescript": "^7.0.2"
|
|
59
60
|
},
|
|
60
61
|
"peerDependencies": {
|
|
61
|
-
"quario": "^0.
|
|
62
|
+
"quario": "^0.8.0"
|
|
62
63
|
},
|
|
63
64
|
"size-limit": [
|
|
64
65
|
{
|
|
65
66
|
"path": "lib/index.js",
|
|
66
67
|
"ignore": [
|
|
67
68
|
"quario",
|
|
69
|
+
"@quario/landing",
|
|
68
70
|
"@quario/layout",
|
|
69
71
|
"lit",
|
|
70
72
|
"@lit/task"
|