@quario/viewer 0.2.0 → 0.4.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/lib/index.js CHANGED
@@ -1,22 +1,24 @@
1
1
  /**
2
2
  * @quario/viewer — the embeddable viewer element. Not a render target: it
3
3
  * compiles nothing and never touches the event stream. `<quario-viewer>`
4
- * consumes a compiled report plus the host's configured targets through
5
- * properties, displays the html target's fragment on a shadow-DOM sheet, and
6
- * offers export downloads for whichever exportable targets the host passed.
7
- * Outcomes reach the host as `rendered` and `error` events, plus
8
- * `renderComplete`, the one awaitable for the newest render settling.
4
+ * consumes a compiled report plus the host's configured export targets
5
+ * through properties, lays the report out on pages through `@quario/layout`
6
+ * and paints them on a shadow-DOM sheet the same list the pdf target
7
+ * writes, so the preview breaks its pages where the document does
8
+ * (docs/adr/0039) and offers export downloads for whichever exportable
9
+ * targets the host passed. Outcomes reach the host as `rendered` and `error`
10
+ * events, plus `renderComplete`, the one awaitable for the newest render
11
+ * settling.
9
12
  *
10
13
  * Importing this module defines nothing: the element class is the export,
11
14
  * and `@quario/viewer/register` performs the one-line define for hosts that
12
15
  * want it (docs/adr/0005-the-surfaces-are-custom-elements.md).
13
16
  *
14
- * Escaping stays exclusively the html target's job (its documented output is
15
- * injected the documented way), and the viewer adds no report content of its
16
- * own. The unlicensed marking is the one thing it presents rather than merely
17
- * styles: the wording still arrives inside the fragment, and the stage draws
18
- * repeating watermarks on the scaled sheet — see
19
- * docs/adr/0017-the-viewer-watermarks-the-sheet.md.
17
+ * Nothing here is markup: the sheet is a stack of canvases, and the list's
18
+ * text reaches them as strings the painter draws, so the viewer has no
19
+ * markup edge and adds no report content of its own. The unlicensed marking
20
+ * rides on the list, one op per page, painted last — the same placement the
21
+ * pdf target writes (docs/adr/0017-the-viewer-watermarks-the-sheet.md).
20
22
  *
21
23
  * The error panel is the only wording the viewer authors, and it is not
22
24
  * report content: it names which failure occurred, because no error knows
@@ -24,13 +26,13 @@
24
26
  */
25
27
  import { Task, TaskStatus } from "@lit/task";
26
28
  import { LitElement, html } from "lit";
29
+ import { layout } from "@quario/layout";
27
30
  import { BUTTON } from "./button.js";
28
31
  import { CHROME, progress } from "./chrome.js";
29
- import { display, geometry, level, name, scheme } from "./check.js";
32
+ import { exports, geometry, level, name, scheme } from "./check.js";
30
33
  import { MENU, zoomMenu } from "./menu.js";
31
34
  import { PANEL, label, panel } from "./panel.js";
32
35
  import { SURFACE, stage } from "./stage.js";
33
- import { REPORT } from "./style.js";
34
36
  import { EXPORTS, download, exportGroup } from "./toolbar.js";
35
37
  import { wanted } from "./zoom.js";
36
38
 
@@ -45,18 +47,19 @@ let said = (error) => {
45
47
 
46
48
  // The properties are described once, in the hand-written public declarations,
47
49
  // and read back here — a second copy in JSDoc is a copy that drifts.
48
- /** @import { ViewableReport, ViewerPage } from './index.d.ts' */
50
+ /** @import { ViewableReport, ViewerFonts, ViewerPage } from './index.d.ts' */
49
51
 
50
52
  /** The tag `@quario/viewer/register` defines the element under. */
51
53
  export const TAG = "quario-viewer";
52
54
 
53
55
  /**
54
56
  * The viewer element. Hosts assign `report`, `targets` and `data` (plus the
55
- * option properties `zoom`, `page`, `filename` and `colorScheme`) and listen for `rendered`
56
- * and `error`; rapid successive writes render only the newest state, because
57
- * the render pipeline is a task keyed on those properties and the task's own
58
- * call-id guard drops every superseded run whole — including its failure,
59
- * which is reported to no one (ADR 0005's narrowed guarantee).
57
+ * option properties `zoom`, `page`, `fonts`, `filename` and `colorScheme`)
58
+ * and listen for `rendered` and `error`; rapid successive writes render only
59
+ * the newest state, because the render pipeline is a task keyed on those
60
+ * properties and the task's own call-id guard drops every superseded run
61
+ * whole — including its failure, which is reported to no one (ADR 0005's
62
+ * narrowed guarantee).
60
63
  *
61
64
  * Removal is not destruction: disconnecting abandons in-flight work and
62
65
  * releases the resize observer, the properties persist, and reconnecting
@@ -66,7 +69,7 @@ export class QuarioViewer extends LitElement {
66
69
  // CHROME declares the default palette every other sheet paints from, so it
67
70
  // has to be in this list -- but anywhere in it: a custom property resolves
68
71
  // down the inherited chain, not by stylesheet order.
69
- static styles = [CHROME, BUTTON, MENU, SURFACE, PANEL, REPORT];
72
+ static styles = [CHROME, BUTTON, MENU, SURFACE, PANEL];
70
73
 
71
74
  // Properties only, no attributes: `report`, `targets`, `data` and `page`
72
75
  // are values no attribute could carry, and one door beats two.
@@ -76,6 +79,7 @@ export class QuarioViewer extends LitElement {
76
79
  data: { attribute: false },
77
80
  zoom: { attribute: false },
78
81
  page: { attribute: false },
82
+ fonts: { attribute: false },
79
83
  filename: { attribute: false },
80
84
  colorScheme: { attribute: false },
81
85
  };
@@ -108,37 +112,49 @@ export class QuarioViewer extends LitElement {
108
112
  #exporting = new Set();
109
113
  /** @type {(() => void) | undefined} */
110
114
  #unwatch;
115
+ /** @type {Promise<void>} The newest swap's paint, which `renderComplete` waits behind. */
116
+ #painting = Promise.resolve();
111
117
 
112
118
  // The whole async pipeline: keyed on the render properties, re-run when one
113
- // changes. The task's call-id guard is the latest-wins machinery; side
119
+ // changes `page` and `fonts` among them, since either moves where the
120
+ // pages break. The task's call-id guard is the latest-wins machinery; side
114
121
  // effects — the stage swap and both events — live only in the callbacks
115
122
  // below, which that guard restricts to the newest run, never in the task
116
123
  // body a stale run still executes to completion. `null` is the "nothing to
117
124
  // show" result: unlike the task primitive's own initial-state symbol it
118
125
  // settles `taskComplete`, which is what lets `renderComplete` always answer.
119
126
  #task = new Task(this, {
120
- args: () => [this.report, this.targets, this.data, this.#epoch],
121
- task: async ([report, targets, data], { signal }) => {
127
+ args: () => [this.report, this.targets, this.data, this.page, this.fonts, this.#epoch],
128
+ task: async ([report, targets, data, page, fonts], { signal }) => {
122
129
  if (!this.#begin(report, targets)) return null;
123
- let target = display(report, targets);
124
- let fragment = await /** @type {any} */ (report).render(target, data);
130
+ // The sheet's own target: the layout list, on the host's page and
131
+ // fonts the same two a host passes to `pdf()`, which is what makes
132
+ // the preview page where the document pages.
133
+ let target = layout({ page: /** @type {any} */ (page), fonts: /** @type {any} */ (fonts) });
134
+ let list = await /** @type {any} */ (report).render(target, data);
125
135
  // The engine takes no signal, so abandonment is the guards around this
126
136
  // body; the check only spares the swap when the answer arrives after a
127
137
  // disconnect mid-render.
128
138
  if (this.#drop(signal)) return this.#abandon();
129
- return fragment;
139
+ return { list, fonts };
130
140
  },
131
- onComplete: (fragment) => {
132
- if (fragment === null || !this.isConnected) return;
133
- this.#stage.swap(fragment);
134
- this.#landed = true;
135
- this.#stale = false;
136
- // A render that reached the sheet takes the panel down: the panel says
137
- // what is wrong with what the reader is looking at, and this is the
138
- // moment that stops being true. A successful export is not that moment.
139
- this.#failure = undefined;
140
- this.#dismissed = false;
141
- this.dispatchEvent(new CustomEvent("rendered"));
141
+ onComplete: (result) => {
142
+ if (result === null || !this.isConnected) return;
143
+ // The swap sizes every page before it settles, so the reader's place
144
+ // is held; the paint it awaits is what `rendered` waits for.
145
+ this.#painting = this.#stage.swap(result.list, result.fonts).then(() => {
146
+ this.#landed = true;
147
+ this.#stale = false;
148
+ // A render that reached the sheet takes the panel down: the panel
149
+ // says what is wrong with what the reader is looking at, and this is
150
+ // the moment that stops being true. A successful export is not that
151
+ // moment.
152
+ this.#failure = undefined;
153
+ this.#dismissed = false;
154
+ // After the paint, so the update the task queued has already run.
155
+ this.requestUpdate();
156
+ this.dispatchEvent(new CustomEvent("rendered"));
157
+ });
142
158
  },
143
159
  onError: (error) => {
144
160
  if (!this.isConnected) return;
@@ -161,6 +177,8 @@ export class QuarioViewer extends LitElement {
161
177
  this.zoom = undefined;
162
178
  /** @type {ViewerPage | undefined} */
163
179
  this.page = undefined;
180
+ /** @type {ViewerFonts | undefined} */
181
+ this.fonts = undefined;
164
182
  /** @type {string | undefined} */
165
183
  this.filename = undefined;
166
184
  /** @type {"light" | "dark" | "auto" | undefined} */
@@ -180,7 +198,7 @@ export class QuarioViewer extends LitElement {
180
198
  this.#task.status === TaskStatus.INITIAL
181
199
  ? this.#landed
182
200
  : this.#task.taskComplete.then(
183
- () => this.#landed,
201
+ () => this.#painting.then(() => this.#landed),
184
202
  () => false,
185
203
  ),
186
204
  );
@@ -273,6 +291,7 @@ export class QuarioViewer extends LitElement {
273
291
  if (this.#invalid) throw this.#invalid;
274
292
  if (vacant(report, targets)) return null;
275
293
  if (!this.isConnected) return this.#abandon();
294
+ exports(report, targets);
276
295
  return true;
277
296
  }
278
297
 
package/lib/menu.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
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
3
+ * "Fit page" above a separator, then the percentages `zoom.js` offers. It is
4
4
  * the whole zoom control; the live percentage is never drawn, it is what the
5
5
  * trigger is named after ("Zoom, 62%"), so the bar carries one icon rather
6
6
  * than a readout that changes width under the reader.
@@ -20,7 +20,7 @@
20
20
  * Radio semantics, not toggles: exactly one row is the current mode, and
21
21
  * choosing the one already checked settles as a no-op in the element's own
22
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
23
+ * the mode rather than the measurement — "Fit page" is checked whenever the
24
24
  * viewer is fitting, whatever percentage that came out at, and a host-set
25
25
  * percentage the menu does not offer checks nothing.
26
26
  *
@@ -310,7 +310,7 @@ export let zoomMenu = ({ mode, percent, choose }) => html`
310
310
  @beforetoggle=${leaving}
311
311
  @keydown=${navigate}
312
312
  >
313
- ${row("Fit sheet", "fit", mode === "fit", choose)}
313
+ ${row("Fit page", "fit", mode === "fit", choose)}
314
314
  <hr class="qv-separator" role="separator" />
315
315
  ${STEPS.map((step) => row(step + "%", step, mode === step, choose))}
316
316
  </div>