@quario/viewer 0.3.0 → 0.5.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 +73 -0
- package/README.md +84 -75
- package/lib/button.js +11 -4
- package/lib/check.js +23 -63
- package/lib/chrome.js +22 -4
- package/lib/icons.js +25 -0
- package/lib/index.d.ts +37 -29
- package/lib/index.js +68 -63
- package/lib/menu.js +20 -33
- package/lib/panel.js +2 -1
- package/lib/stage.js +410 -235
- package/lib/toolbar.js +17 -36
- package/package.json +6 -4
- package/lib/mark.js +0 -40
- package/lib/style.js +0 -123
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,79 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-09-05
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Previewing with `fonts` now needs `fontkit`, not `@pdf-lib/fontkit`.**
|
|
15
|
+
Install `fontkit` instead. The viewer measures host TrueType faces through
|
|
16
|
+
`@quario/layout`, whose optional parser this is; the old package crashed on
|
|
17
|
+
any OpenType face needing a shaping state machine, so those faces now
|
|
18
|
+
preview where they used to throw.
|
|
19
|
+
|
|
20
|
+
- **The bar's controls are drawn icons.** The zoom trigger, the menu's check
|
|
21
|
+
and the error panel's dismiss are drawn from Lucide's set rather than by
|
|
22
|
+
hand, at one weight and one grid, and the dismiss no longer relies on the
|
|
23
|
+
host's font to draw a multiplication sign. They paint in `currentColor`, so
|
|
24
|
+
`--qv-icon` and `--qv-icon-active` still recolour them.
|
|
25
|
+
|
|
26
|
+
- **An export button names its format in words.** Each one is a download icon
|
|
27
|
+
with `PDF`, `XLSX` or `CSV` beside it, in place of the sheet drawing that
|
|
28
|
+
lettered the format inside itself. The buttons are wider; the accessible
|
|
29
|
+
name is unchanged.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **A zoom no longer leaves a page part-drawn.** Changing the zoom while a
|
|
34
|
+
page was still painting could leave that page carrying content at the old
|
|
35
|
+
scale on a canvas sized for the new one — the rest of it blank — and nothing
|
|
36
|
+
repainted it afterwards: scrolling away and back left it as it was. A page
|
|
37
|
+
whose paint is still in flight is now replaced rather than reused, so the
|
|
38
|
+
superseded paint has nowhere to land.
|
|
39
|
+
|
|
40
|
+
## [0.4.0] - 2026-09-03
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **`fonts`.** The font mapping a host passes to `pdf({ fonts })`, so the
|
|
45
|
+
preview measures and draws in the same TrueType faces the document embeds.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **`renderComplete` and `rendered` settle once the pages on screen are
|
|
50
|
+
painted.** Both already answered for the newest render reaching the sheet;
|
|
51
|
+
what that means is now stated: the pages the reader can see carry their
|
|
52
|
+
pixels, and the pages further down the report do not hold the promise up.
|
|
53
|
+
A screenshot or a pixel-reading test taken at that moment sees what the
|
|
54
|
+
reader sees.
|
|
55
|
+
|
|
56
|
+
- **A split slot's box now fills the split's height.** A slot's background and
|
|
57
|
+
border stopped at that slot's own content and now take the split's height,
|
|
58
|
+
the way a table cell's take their row's. See the `@quario/layout` changelog
|
|
59
|
+
for the rule and for what it costs a report that relied on the short box.
|
|
60
|
+
|
|
61
|
+
- **The sheet has pages, and they break where the PDF's do.** The viewer
|
|
62
|
+
depends on `@quario/layout` and paints the same display list the pdf target
|
|
63
|
+
writes, one canvas per page, in the faces the document itself uses.
|
|
64
|
+
Zoom is a repaint, so text stays crisp at 200%; the menu's first row reads
|
|
65
|
+
"Fit page" and fit sizes one page to the width. Breaking:
|
|
66
|
+
`targets` is exports only — no `"html"` target is required or read — and a
|
|
67
|
+
`page` change re-lays the report out rather than resizing a sheet. The
|
|
68
|
+
unlicensed marking is painted per page from the list; the `--qv-mark`
|
|
69
|
+
token is gone with the DOM stamps.
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- **A long report no longer asks for a backing store per page.** The viewer
|
|
74
|
+
paints the pages near the viewport — the ones on screen and one screenful
|
|
75
|
+
either side — and keeps every other page sized but blank. A report of a
|
|
76
|
+
few hundred pages used to ask the browser for gigabytes of canvas; past
|
|
77
|
+
what it would grant, pages came up blank with nothing reported, which on a
|
|
78
|
+
HiDPI display took only a moderately long report. Every page still holds
|
|
79
|
+
its place, so the scrollbar and the scroll extent are the whole report from
|
|
80
|
+
the first frame, and a zoom step now repaints a handful of pages instead of
|
|
81
|
+
walking the document.
|
|
82
|
+
|
|
10
83
|
## [0.3.0] - 2026-09-02
|
|
11
84
|
|
|
12
85
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
# @quario/viewer
|
|
2
2
|
|
|
3
3
|
**The embeddable report viewer for [quario](https://www.npmjs.com/package/quario), as a custom
|
|
4
|
-
element.** Write `<quario-viewer>`, assign it a compiled report, the targets
|
|
5
|
-
data; it
|
|
6
|
-
downloads for the
|
|
4
|
+
element.** Write `<quario-viewer>`, assign it a compiled report, the export targets you want, and
|
|
5
|
+
data; it lays the report out on pages — the same pages the PDF target writes — paints them on a
|
|
6
|
+
white sheet in its shadow root, and offers export downloads for the targets you passed.
|
|
7
7
|
|
|
8
|
-
The viewer is not a render target and compiles nothing.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
The viewer is not a render target and compiles nothing. The sheet is `@quario/layout`'s display
|
|
9
|
+
list, painted page by page; you pass the export targets you want, and the viewer wires a button
|
|
10
|
+
to each (`"pdf"`, `"xlsx"`, `"csv"`). The bar always carries the zoom control; with no exportable
|
|
11
|
+
target it carries nothing else.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install quario @quario/
|
|
16
|
+
npm install quario @quario/viewer
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
The engine is a peer; the viewer itself depends on no target package. Install the targets you want
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
to export with (`@quario/pdf`, `@quario/xlsx`, `@quario/csv`) and pass them in. Its own runtime
|
|
21
|
+
dependencies are [Lit](https://lit.dev) (`lit` + `@lit/task`) and `@quario/layout`, the paged
|
|
22
|
+
layout it paints, plain ESM like everything else here. ESM-only, and
|
|
22
23
|
browser-only by nature: the element needs a DOM. CSP-safe like the rest of quario. No
|
|
23
24
|
string-to-code paths, chrome styled through constructed stylesheets, so your `style-src` never
|
|
24
25
|
sees a style tag.
|
|
@@ -27,7 +28,6 @@ sees a style tag.
|
|
|
27
28
|
|
|
28
29
|
```js
|
|
29
30
|
import { csv } from "@quario/csv";
|
|
30
|
-
import { html } from "@quario/html";
|
|
31
31
|
import { pdf } from "@quario/pdf";
|
|
32
32
|
import { xlsx } from "@quario/xlsx";
|
|
33
33
|
import { quario } from "quario";
|
|
@@ -35,14 +35,17 @@ import "@quario/viewer/register";
|
|
|
35
35
|
|
|
36
36
|
const view = document.querySelector("quario-viewer");
|
|
37
37
|
view.report = quario().report(schema, funcs);
|
|
38
|
-
view.targets = [
|
|
38
|
+
view.targets = [pdf(), xlsx(), csv()];
|
|
39
39
|
view.data = data;
|
|
40
40
|
view.filename = "sales";
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
with `<quario-viewer></quario-viewer>` in your markup, sized by your own CSS. The element is
|
|
44
|
-
`display: block
|
|
45
|
-
|
|
44
|
+
`display: block`, and its chrome is laid out to fit a width of **320px** — a phone-width embed is
|
|
45
|
+
the narrowest the bar is designed for. Narrower than that and the export buttons push the zoom
|
|
46
|
+
control off the left of the bar, quietly: the bar does not wrap or scroll. Importing
|
|
47
|
+
`@quario/viewer` defines nothing: the main entry exports the `QuarioViewer` class and is
|
|
48
|
+
side-effect-free, and `@quario/viewer/register` performs the one-line
|
|
46
49
|
`customElements.define("quario-viewer", QuarioViewer)`. A host that wants its own tag imports the
|
|
47
50
|
class and defines it itself. The repository ships this wiring as a runnable page at
|
|
48
51
|
`example/viewer.js`.
|
|
@@ -52,50 +55,51 @@ package to install. See [Using with React, Vue and Svelte](#using-with-react-vue
|
|
|
52
55
|
|
|
53
56
|
## Properties
|
|
54
57
|
|
|
55
|
-
| Property | Takes
|
|
56
|
-
| ------------- |
|
|
57
|
-
| `report` | A compiled report from a quario instance
|
|
58
|
-
| `targets` |
|
|
59
|
-
| `data` | The render document
|
|
60
|
-
| `zoom` | `"fit"` or a percentage between 25 and 200
|
|
61
|
-
| `page` |
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`"pdf"`/`"xlsx"`/`"csv"`
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
| Property | Takes | Default |
|
|
59
|
+
| ------------- | ----------------------------------------------- | ----------- |
|
|
60
|
+
| `report` | A compiled report from a quario instance | - |
|
|
61
|
+
| `targets` | Export targets, possibly none | - |
|
|
62
|
+
| `data` | The render document | `undefined` |
|
|
63
|
+
| `zoom` | `"fit"` or a percentage between 25 and 200 | `"fit"` |
|
|
64
|
+
| `page` | Page geometry, as passed to `pdf({ page })` | A4, 54pt |
|
|
65
|
+
| `fonts` | The font mapping, as passed to `pdf({ fonts })` | none |
|
|
66
|
+
| `filename` | Export download name, without extension | `"report"` |
|
|
67
|
+
| `colorScheme` | `"light"`, `"dark"`, or `"auto"` | `"light"` |
|
|
68
|
+
|
|
69
|
+
Properties, not attributes: `report`, `targets`, `data`, `page` and `fonts` are values no attribute
|
|
70
|
+
could carry. `targets` mirrors `report.render(target, data)` for the exports: `"pdf"`/`"xlsx"`/`"csv"`
|
|
71
|
+
targets become export buttons, in the order given, and the sheet needs none of them. The quario
|
|
72
|
+
instance (and with it the license and the registry) stays yours: the element takes the compiled
|
|
73
|
+
report, never a schema.
|
|
70
74
|
|
|
71
75
|
Assigning `data` (or any of the others) re-renders. Rapid successive writes render only the newest
|
|
72
76
|
state: a slow render can never overwrite a newer one, and a superseded render fires no event.
|
|
73
77
|
Assignments are compared by identity, so to re-render from the same object, assign a fresh one
|
|
74
78
|
(`view.data = { ...data }`).
|
|
75
79
|
|
|
76
|
-
`page`
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
`page` and `fonts` are the pdf target's own options: the viewer lays the report out on them, so
|
|
81
|
+
passing the same values to the element and to `pdf({ page, fonts })` is what makes the preview
|
|
82
|
+
page where the document pages, in the same faces. A `page` or `fonts` write re-lays the report
|
|
83
|
+
out.
|
|
80
84
|
|
|
81
85
|
## Events and `renderComplete`
|
|
82
86
|
|
|
83
87
|
```js
|
|
84
88
|
view.addEventListener("rendered", () => {});
|
|
85
89
|
view.addEventListener("error", ({ detail: { error, kind } }) => {});
|
|
86
|
-
await view.renderComplete; // true when the newest render
|
|
90
|
+
await view.renderComplete; // true when the newest render landed on the sheet
|
|
87
91
|
```
|
|
88
92
|
|
|
89
|
-
`rendered` fires each time a render
|
|
93
|
+
`rendered` fires each time a render lands on the sheet; `error` fires for every failure you should
|
|
90
94
|
know about, with `detail.kind` naming which: `"mount-render"` until a render has ever landed,
|
|
91
95
|
`"update-render"` after, `"export"` for a download that could not be produced. Both events are
|
|
92
96
|
non-bubbling, like `<img>`'s. Listen on the element. (Because the event is named `error`, an
|
|
93
97
|
inline `onerror` attribute on the element would fire too; `window.onerror` never sees it.)
|
|
94
98
|
|
|
95
|
-
`renderComplete` awaits the newest render settling: `true` when it
|
|
96
|
-
it failed or there was nothing to render. It never rejects; failures
|
|
97
|
-
|
|
98
|
-
reported to no one.
|
|
99
|
+
`renderComplete` awaits the newest render settling: `true` when it landed on the sheet with the pages
|
|
100
|
+
on screen painted, `false` when it failed or there was nothing to render. It never rejects; failures
|
|
101
|
+
arrive on the `error` event. `rendered` fires at that same moment. Like every outcome here, it
|
|
102
|
+
answers for the newest render only. A superseded render's failure is reported to no one.
|
|
99
103
|
|
|
100
104
|
Your own mistakes surface on the same channel: a report that is not compiled, a missing
|
|
101
105
|
`"html"` target, or a malformed option property becomes a `TypeError` naming the property, on
|
|
@@ -200,54 +204,59 @@ listeners:
|
|
|
200
204
|
|
|
201
205
|
## Zoom
|
|
202
206
|
|
|
203
|
-
The bar's magnifier opens the zoom menu: **Fit
|
|
204
|
-
and 200%. A check marks the current mode — Fit
|
|
207
|
+
The bar's magnifier opens the zoom menu: **Fit page** on its own, then 25%, 50%, 75%, 100%, 150%
|
|
208
|
+
and 200%. A check marks the current mode — Fit page whenever the viewer is fitting, whatever
|
|
205
209
|
percentage that came out at — and the percentage on screen is what the trigger is named after
|
|
206
210
|
("Zoom, 62%"), so nothing in the bar changes width as it moves. The `zoom` property picks the mode
|
|
207
211
|
the viewer opens in: a percentage between 25 and 200 — continuous, not one of the menu's stops — or
|
|
208
212
|
`"fit"`, the default. A percentage the menu does not offer leaves every row unchecked.
|
|
209
213
|
|
|
210
|
-
Fit sizes
|
|
214
|
+
Fit sizes one page to the viewer's width and **only ever shrinks**: given room to spare it stops at
|
|
211
215
|
100%, so the report is shown at its true point size rather than blown up. It has no floor, so a
|
|
212
216
|
narrow pane fits at whatever percentage that takes — below 25% the menu has no row to return to it,
|
|
213
|
-
and **Fit
|
|
217
|
+
and **Fit page** is the only way back. A fitted viewer follows its own box, so a collapsing panel
|
|
214
218
|
or a resized window re-fits on its own.
|
|
215
219
|
|
|
216
|
-
The preview **scales; it never reflows.** Zooming
|
|
217
|
-
photograph. Line breaks, column widths and point sizes stay what they are at
|
|
218
|
-
|
|
219
|
-
they agree.
|
|
220
|
+
The preview **scales; it never reflows.** Zooming repaints the pages larger or smaller, like a
|
|
221
|
+
photograph that stays sharp. Line breaks, column widths and point sizes stay what they are at
|
|
222
|
+
100%, because the layout never changes under a zoom.
|
|
220
223
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
Only the pages near the viewport are painted — the ones on screen and one screenful either side.
|
|
225
|
+
Every page keeps its size, so the scrollbar and the scroll extent are the whole report from the
|
|
226
|
+
start; a page further off is blank paper until you scroll to it, which is why a zoom step costs the
|
|
227
|
+
same on a thousand-page report as on a five-page one.
|
|
225
228
|
|
|
226
|
-
|
|
227
|
-
report shorter than a page sits on paper the shape of the page you configured rather than on a band
|
|
228
|
-
the height of its own content. It is a minimum, so a longer report keeps growing on the one sheet.
|
|
229
|
-
Sizing a container to a viewer showing a short report therefore reserves a full page — set `zoom`
|
|
230
|
-
to a percentage small enough where the box has to stay small. The sheet still draws no page
|
|
231
|
-
boundaries: only the pdf target knows where pages fall.
|
|
232
|
-
|
|
233
|
-
The sheet is the html target's fragment, so a report with image items shows them through that
|
|
234
|
-
target's `data:` URIs. A host page with a Content Security Policy needs `img-src data:` for them
|
|
235
|
-
to display, the same deployment requirement the fragment carries anywhere else.
|
|
229
|
+
## What the preview is
|
|
236
230
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
231
|
+
The pages on screen are the pages the PDF export writes: both consume one layout, `@quario/layout`'s
|
|
232
|
+
display list, laid out on your `page` and `fonts`. A report shorter than a page is one page; a
|
|
233
|
+
longer one is as many as the layout breaks it into, stacked down the sheet. Sizing a container to a
|
|
234
|
+
viewer showing a short report therefore reserves a full page — set `zoom` to a percentage small
|
|
235
|
+
enough where the box has to stay small.
|
|
236
|
+
|
|
237
|
+
Each page is a canvas, and text on it is drawn in the face the document will use. A TrueType
|
|
238
|
+
family you pass as `fonts` is registered from your own bytes and drawn as the browser shapes it —
|
|
239
|
+
the same shaping the PDF gets from the same file, so ligatures, joined scripts and accents look
|
|
240
|
+
here the way they will on paper. Text in the built-in families is drawn character by character at
|
|
241
|
+
the advances the layout measured, because the font a browser has for Helvetica, Times or Courier
|
|
242
|
+
only stands in for the one the PDF writes, and the correction is what makes a line fill the same
|
|
243
|
+
width and break in the same place the document does. Inside one run of a family you supply, a
|
|
244
|
+
browser may kern by a fraction more than the document; where lines break and pages end is the
|
|
245
|
+
layout's, and identical. Images are decoded from their bytes — no `data:` URIs, so a host page's
|
|
246
|
+
Content Security Policy needs no `img-src` grant for them.
|
|
247
|
+
|
|
248
|
+
Rendering an unlicensed evaluation, the marking is painted across every page, the way the PDF
|
|
249
|
+
export marks every page: it rides on the layout, so what you see is what the document carries. A
|
|
250
|
+
licensed render carries none.
|
|
242
251
|
|
|
243
252
|
While a render is in flight a thin indeterminate bar sits on the toolbar's bottom edge, and the
|
|
244
253
|
viewer reads `aria-busy="true"`. It reports that the viewer is working, not how far along. The
|
|
245
254
|
engine streams events and cannot know how many are still coming.
|
|
246
255
|
|
|
247
256
|
There is no Print button, because the sheet is the wrong thing to print. Your page's stylesheets do
|
|
248
|
-
not cross into the shadow root, so `@media print` rules never reach the report; and
|
|
249
|
-
|
|
250
|
-
paper at
|
|
257
|
+
not cross into the shadow root, so `@media print` rules never reach the report; and the pages are
|
|
258
|
+
painted at whatever zoom the reader happened to leave them, so printing the page puts the viewer's
|
|
259
|
+
chrome on paper at that zoom. What a printer wants is the pdf target's
|
|
251
260
|
document, the same bytes the PDF export hands over. A host that wants its own Print button owns
|
|
252
261
|
two lines:
|
|
253
262
|
|
|
@@ -265,7 +274,7 @@ the export is, watermark and all.
|
|
|
265
274
|
|
|
266
275
|
When a render or an export fails, the viewer says so on the **error panel**, a strip across the top
|
|
267
276
|
of the sheet carrying a short label and the error's own message. It replaces rather than stacks, is
|
|
268
|
-
dismissed by its own button, and is cleared by the next render that
|
|
277
|
+
dismissed by its own button, and is cleared by the next render that lands on the sheet. A successful
|
|
269
278
|
export leaves it up, because the panel describes what you are looking at and a download says
|
|
270
279
|
nothing about that.
|
|
271
280
|
|
|
@@ -284,18 +293,18 @@ part of this: `q.report(schema)` raises those before the viewer is ever handed a
|
|
|
284
293
|
|
|
285
294
|
`colorScheme` paints the **chrome** — backdrop, bar, controls, progress strip, error
|
|
286
295
|
panel — and never the sheet. `"light"` (the default) and `"dark"` pin; `"auto"` follows
|
|
287
|
-
the OS via CSS `color-scheme`. The
|
|
296
|
+
the OS via CSS `color-scheme`. The pages stay white, marking included.
|
|
288
297
|
|
|
289
298
|
Chrome styles live on `--qv-*` custom properties under stable `qv-*` class names. A
|
|
290
299
|
token set on the element (or an ancestor) always wins over the pin. The names are a
|
|
291
300
|
reference seam, not a frozen vocabulary: backdrop and bar (`--qv-backdrop`, `--qv-bar`,
|
|
292
301
|
`--qv-border`, `--qv-text`), controls (`--qv-icon`, `--qv-icon-active`, `--qv-hover`,
|
|
293
302
|
`--qv-active`, `--qv-focus`), progress (`--qv-progress`), the error panel (`--qv-error`,
|
|
294
|
-
`--qv-error-text`, `--qv-error-border`, `--qv-error-hover`), and the
|
|
295
|
-
(`--qv-sheet-shadow`).
|
|
303
|
+
`--qv-error-text`, `--qv-error-border`, `--qv-error-hover`), and the pages' edge
|
|
304
|
+
(`--qv-sheet-shadow`).
|
|
296
305
|
|
|
297
|
-
The report itself
|
|
298
|
-
|
|
306
|
+
The report itself is paint, not markup: there is no report stylesheet to restyle, because what
|
|
307
|
+
you see is the layout the PDF target writes.
|
|
299
308
|
|
|
300
309
|
## License
|
|
301
310
|
|
package/lib/button.js
CHANGED
|
@@ -18,6 +18,11 @@ export let BUTTON = css`
|
|
|
18
18
|
border-radius: 5px;
|
|
19
19
|
background: transparent;
|
|
20
20
|
color: var(--_icon);
|
|
21
|
+
/* A form control does not inherit \`font\`, so the chrome's own 13px/1.4
|
|
22
|
+
system-ui stops at every button unless it is restated here. The shorthand
|
|
23
|
+
rather than \`font-family\` alone: the UA sets the shorthand, so restating
|
|
24
|
+
one longhand leaves size and line-height at the UA's. */
|
|
25
|
+
font: inherit;
|
|
21
26
|
cursor: pointer;
|
|
22
27
|
}
|
|
23
28
|
|
|
@@ -63,9 +68,11 @@ let fallback = (value, otherwise) => value ?? otherwise;
|
|
|
63
68
|
* @param {{ title: string, label?: string, disabled?: boolean, name?: string,
|
|
64
69
|
* popover?: string, click?: () => void,
|
|
65
70
|
* content?: import('lit').TemplateResult }} control
|
|
66
|
-
* `
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
|
69
76
|
* `popover` names the menu this button opens — the platform then owns the
|
|
70
77
|
* opening and the invoker's own `aria-expanded`, which is why such a button
|
|
71
78
|
* needs no click of its own.
|
|
@@ -83,6 +90,6 @@ export let button = ({ title, label, disabled, name, popover, click, content })
|
|
|
83
90
|
?disabled=${disabled}
|
|
84
91
|
@click=${click}
|
|
85
92
|
>
|
|
86
|
-
${
|
|
93
|
+
${content ?? nothing}${label ?? nothing}
|
|
87
94
|
</button>
|
|
88
95
|
`;
|
package/lib/check.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What the host got wrong, named. The element's own mistakes — a report that
|
|
3
|
-
* is not compiled, a
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
8
8
|
*
|
|
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
13
|
import { STEPS } from "./zoom.js";
|
|
13
14
|
|
|
14
15
|
/** @type {(message: string) => never} */
|
|
@@ -16,12 +17,6 @@ let fail = (message) => {
|
|
|
16
17
|
throw new TypeError("quario-viewer: " + message);
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
// The PdfPage vocabulary and defaults, mirrored from the pdf target: the
|
|
20
|
-
// sheet is a visual stand-in for the page the host configured there, so the
|
|
21
|
-
// same values must mean the same geometry.
|
|
22
|
-
/** @type {Record<string, [number, number]>} */
|
|
23
|
-
let SIZES = { A4: [595.28, 841.89], letter: [612, 792] };
|
|
24
|
-
|
|
25
20
|
let REPORT = [
|
|
26
21
|
(/** @type {any} */ report) => report && typeof report === "object",
|
|
27
22
|
(/** @type {any} */ report) => typeof report.stream === "function",
|
|
@@ -31,69 +26,33 @@ let REPORT = [
|
|
|
31
26
|
/** @type {(report: any) => boolean} */
|
|
32
27
|
let compiled = (report) => REPORT.every((check) => check(report));
|
|
33
28
|
|
|
34
|
-
/** @type {(target: any) => boolean} */
|
|
35
|
-
let htmlTarget = (target) => target?.name === "html";
|
|
36
|
-
|
|
37
29
|
/**
|
|
38
|
-
* The
|
|
39
|
-
* the
|
|
30
|
+
* The shapes a render needs: a compiled report, and the export targets as an
|
|
31
|
+
* array — possibly empty, since the sheet is the layout's own and needs no
|
|
32
|
+
* target of the host's.
|
|
40
33
|
*
|
|
41
34
|
* @param {any} report
|
|
42
35
|
* @param {any} targets
|
|
43
|
-
* @returns {any} The html target.
|
|
44
36
|
*/
|
|
45
|
-
export let
|
|
37
|
+
export let exports = (report, targets) => {
|
|
46
38
|
if (!compiled(report)) fail("report: expected a compiled report from a quario instance");
|
|
47
39
|
if (!Array.isArray(targets)) fail("targets: expected an array of render targets");
|
|
48
|
-
let found = targets.find(htmlTarget);
|
|
49
|
-
if (!found) fail('targets: no "html" target — include html() to display');
|
|
50
|
-
return found;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/** @type {(size: unknown) => [number, number] | false} */
|
|
54
|
-
let named = (size) =>
|
|
55
|
-
Object.hasOwn(SIZES, /** @type {string} */ (size)) && SIZES[/** @type {string} */ (size)];
|
|
56
|
-
|
|
57
|
-
/** @type {(size: unknown) => any} */
|
|
58
|
-
let measure = (size) => (Array.isArray(size) ? size : named(size));
|
|
59
|
-
|
|
60
|
-
/** @type {(n: number) => boolean} */
|
|
61
|
-
let positive = (n) => Number.isFinite(n) && n > 0;
|
|
62
|
-
|
|
63
|
-
/** @type {(dimensions: any) => { width: number, height: number }} */
|
|
64
|
-
let extents = (dimensions) => {
|
|
65
|
-
let width = +dimensions[0];
|
|
66
|
-
let height = +dimensions[1];
|
|
67
|
-
if (!positive(width) || !positive(height)) fail("page.size: expected finite positive dimensions");
|
|
68
|
-
return { width, height };
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
/** @type {(margin: number, width: number, height: number) => boolean} */
|
|
72
|
-
let fits = (margin, width, height) =>
|
|
73
|
-
[Number.isFinite(margin), margin >= 0, 2 * margin < Math.min(width, height)].every(Boolean);
|
|
74
|
-
|
|
75
|
-
/** @type {(page: any, width: number, height: number) => number} */
|
|
76
|
-
let inset = (page, width, height) => {
|
|
77
|
-
let margin = page?.margin ?? 54;
|
|
78
|
-
if (!fits(margin, width, height))
|
|
79
|
-
fail("page.margin: expected a non-negative number smaller than half the page");
|
|
80
|
-
return margin;
|
|
81
40
|
};
|
|
82
41
|
|
|
83
42
|
/**
|
|
84
|
-
* The
|
|
85
|
-
*
|
|
86
|
-
*
|
|
43
|
+
* The page geometry from the `page` property: the layout's own vocabulary and
|
|
44
|
+
* defaults, so the same values mean the same pages on screen and on paper.
|
|
45
|
+
* The layout names the mistake; this names the property.
|
|
87
46
|
*
|
|
88
47
|
* @param {any} page
|
|
89
48
|
* @returns {{ width: number, height: number, margin: number }}
|
|
90
49
|
*/
|
|
91
50
|
export let geometry = (page) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
51
|
+
try {
|
|
52
|
+
return pageBox(page, "page");
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return fail(/** @type {Error} */ (error).message);
|
|
55
|
+
}
|
|
97
56
|
};
|
|
98
57
|
|
|
99
58
|
/** @type {(mode: any, floor: number, ceiling: number) => boolean} */
|
|
@@ -135,8 +94,9 @@ export let name = (filename) => {
|
|
|
135
94
|
return base;
|
|
136
95
|
};
|
|
137
96
|
|
|
138
|
-
/**
|
|
139
|
-
|
|
97
|
+
/** The schemes the property accepts. Two are their own CSS value; `auto` is
|
|
98
|
+
* the one that is not, so it is the only wording written here. */
|
|
99
|
+
let SCHEMES = ["light", "dark", "auto"];
|
|
140
100
|
|
|
141
101
|
/**
|
|
142
102
|
* The used `color-scheme` from the `colorScheme` property. Light is the
|
|
@@ -149,6 +109,6 @@ let SCHEME = { light: "light", dark: "dark", auto: "light dark" };
|
|
|
149
109
|
*/
|
|
150
110
|
export let scheme = (colorScheme) => {
|
|
151
111
|
let pin = colorScheme ?? "light";
|
|
152
|
-
if (!
|
|
153
|
-
return
|
|
112
|
+
if (!SCHEMES.includes(pin)) fail('colorScheme: expected "light", "dark", or "auto"');
|
|
113
|
+
return pin === "auto" ? "light dark" : pin;
|
|
154
114
|
};
|
package/lib/chrome.js
CHANGED
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
* purpose: `:host` outranks that same host rule, so nothing outside can
|
|
25
25
|
* shadow an alias by accident.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
27
|
+
* One of the fifteen is the sheet's rather than the chrome's: a color scheme
|
|
28
28
|
* paints chrome only (ADR 0018), so `--qv-sheet-shadow` is the sheet's edge
|
|
29
|
-
* against the backdrop
|
|
30
|
-
*
|
|
29
|
+
* against the backdrop. It is in the block because it has no exceptions, not
|
|
30
|
+
* because it follows the pin.
|
|
31
31
|
* `--qv-focus` and `--qv-progress` repeat the accent rather than chain a third
|
|
32
32
|
* alias — a host is free to move one alone.
|
|
33
33
|
*
|
|
@@ -68,7 +68,6 @@ export let CHROME = css`
|
|
|
68
68
|
0 1px 2px light-dark(rgba(0, 0, 0, 0.16), rgba(0, 0, 0, 0.5)),
|
|
69
69
|
0 6px 24px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.45))
|
|
70
70
|
);
|
|
71
|
-
--_mark: var(--qv-mark, #999);
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
.qv-viewer {
|
|
@@ -167,6 +166,25 @@ export let CHROME = css`
|
|
|
167
166
|
display: flex;
|
|
168
167
|
gap: 2px;
|
|
169
168
|
}
|
|
169
|
+
|
|
170
|
+
/* An export button reads as a drawn download and the format in words, so it
|
|
171
|
+
takes the width its text needs rather than the icon-only box. */
|
|
172
|
+
.qv-exports .qv-button {
|
|
173
|
+
gap: 5px;
|
|
174
|
+
width: auto;
|
|
175
|
+
padding: 0 9px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* The base icon size, once per package rather than once per control: an
|
|
179
|
+
inline <svg> carrying a viewBox and no width falls back to 300x150, and
|
|
180
|
+
nothing in the chrome wants that. 18px is 0.75 of Lucide's 24 grid, which
|
|
181
|
+
puts its stroke 2 at 1.5px beside 13px text. The narrower selectors that
|
|
182
|
+
drop to 14px live with the controls they belong to — the menu's tick slot
|
|
183
|
+
in menu.js (ADR 0044). */
|
|
184
|
+
.qv-viewer svg {
|
|
185
|
+
width: 18px;
|
|
186
|
+
height: 18px;
|
|
187
|
+
}
|
|
170
188
|
`;
|
|
171
189
|
|
|
172
190
|
/**
|
package/lib/icons.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The icons @quario/viewer draws, vendored from Lucide: one export per icon,
|
|
3
|
+
* finished templates that carry no size. Chrome CSS decides that — 18px by
|
|
4
|
+
* default, 14px in the gutter and the menu's tick slot — because `x` appears
|
|
5
|
+
* in two contexts at once (ADR 0044).
|
|
6
|
+
*
|
|
7
|
+
* Generated by `scripts/icons.mjs` from lucide 1.40.0. Do not edit:
|
|
8
|
+
* `npm run icons` rewrites it, and `npm run check` regenerates it into a
|
|
9
|
+
* temp directory and compares.
|
|
10
|
+
*
|
|
11
|
+
* Lucide is ISC licensed. Copyright (c) 2026 Lucide Icons and Contributors.
|
|
12
|
+
*/
|
|
13
|
+
import { html, svg } from "lit";
|
|
14
|
+
|
|
15
|
+
/** Upstream's grid and stroke, written once. */
|
|
16
|
+
/** @type {(paths: import('lit').SVGTemplateResult) => import('lit').TemplateResult} */
|
|
17
|
+
let icon = (paths) =>
|
|
18
|
+
html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
|
|
19
|
+
|
|
20
|
+
export let check = icon(svg`<path d="M20 6 9 17l-5-5"/>`);
|
|
21
|
+
export let download = icon(
|
|
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
|
+
);
|
|
24
|
+
export let search = icon(svg`<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>`);
|
|
25
|
+
export let x = icon(svg`<path d="M18 6 6 18"/><path d="m6 6 12 12"/>`);
|