@quario/viewer 0.1.0 → 0.3.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 +46 -0
- package/README.md +9 -2
- package/lib/index.js +22 -33
- package/lib/stage.js +20 -5
- package/lib/style.js +29 -0
- package/package.json +7 -9
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-09-02
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Report and group containers are flex columns.** Adjacent authored
|
|
15
|
+
`spaceBefore` / `spaceAfter` margins add rather than collapse, matching
|
|
16
|
+
the html reference stylesheet.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **The adopted sheet occupies a line and preserves authored newlines.**
|
|
21
|
+
Same `min-height: 1lh` and `white-space: pre-line` the html reference
|
|
22
|
+
stylesheet now carries on `.q-item`, and `pre-line` on table cells.
|
|
23
|
+
|
|
24
|
+
## [0.2.0] - 2026-09-01
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **The sheet is now page-shaped, so short reports reserve a full page.**
|
|
29
|
+
`page` sized the sheet's width and padding and ignored its height, so a
|
|
30
|
+
report shorter than a page was drawn on paper the shape of its own content —
|
|
31
|
+
an A4 half-page invoice came out twice as wide as it was tall while the PDF
|
|
32
|
+
export was a true A4 page. The sheet now takes at least one page of height as
|
|
33
|
+
well. It remains a minimum, so a longer report keeps growing on the one
|
|
34
|
+
continuous sheet, and nothing about a report of a page or more changes.
|
|
35
|
+
|
|
36
|
+
**This is visible if you size a container to the viewer.** An A4 sheet at
|
|
37
|
+
fit-width is roughly a thousand pixels tall against five hundred before, so a
|
|
38
|
+
box fitted to a short report roughly doubles. Set `zoom` to a percentage small
|
|
39
|
+
enough where the box has to stay small. The sheet still draws no page
|
|
40
|
+
boundaries.
|
|
41
|
+
|
|
42
|
+
- **The adopted sheet's baseline face moved onto `.q-report`.** It was on
|
|
43
|
+
`.q-item` and `.q-table`, where a direct rule would have beaten a report's
|
|
44
|
+
own declared family. Same look; the fragment's new root is what carries it.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **The sheet no longer draws report text in the platform's own face.** It
|
|
49
|
+
hard-coded `system-ui`, which is SF Pro on macOS, Segoe UI on Windows and
|
|
50
|
+
Roboto on Android — so the same report showed a different typeface to every
|
|
51
|
+
visitor, and none of them matched what the PDF exported. Text declaring no
|
|
52
|
+
`family` now renders in `sans-serif`, the same declaration the PDF target
|
|
53
|
+
resolves to Helvetica. The viewer's own chrome keeps `system-ui`, which is
|
|
54
|
+
what a toolbar should do.
|
|
55
|
+
|
|
10
56
|
## [0.1.0] - 2026-08-27
|
|
11
57
|
|
|
12
58
|
### Added
|
package/README.md
CHANGED
|
@@ -73,8 +73,8 @@ state: a slow render can never overwrite a newer one, and a superseded render fi
|
|
|
73
73
|
Assignments are compared by identity, so to re-render from the same object, assign a fresh one
|
|
74
74
|
(`view.data = { ...data }`).
|
|
75
75
|
|
|
76
|
-
`page` is the pdf target's vocabulary and defaults, and it is **purely visual**: it
|
|
77
|
-
sheet
|
|
76
|
+
`page` is the pdf target's vocabulary and defaults, and it is **purely visual**: it gives the white
|
|
77
|
+
sheet the browser lays the report out on its width, its padding, and at least one page of height. The PDF target's own geometry lives
|
|
78
78
|
in the host's `pdf({ page })` configuration. Pass the same values to both so preview and export
|
|
79
79
|
agree.
|
|
80
80
|
|
|
@@ -223,6 +223,13 @@ they agree.
|
|
|
223
223
|
On-screen layout is the browser's: one continuous sheet, no pagination. The PDF export paginates
|
|
224
224
|
independently in the pdf target. The preview approximates; the exports are exact.
|
|
225
225
|
|
|
226
|
+
The sheet is page-shaped: `page` gives it both its width and at least one page of height, so a
|
|
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
|
+
|
|
226
233
|
The sheet is the html target's fragment, so a report with image items shows them through that
|
|
227
234
|
target's `data:` URIs. A host page with a Content Security Policy needs `img-src data:` for them
|
|
228
235
|
to display, the same deployment requirement the fragment carries anywhere else.
|
package/lib/index.js
CHANGED
|
@@ -34,15 +34,6 @@ import { REPORT } from "./style.js";
|
|
|
34
34
|
import { EXPORTS, download, exportGroup } from "./toolbar.js";
|
|
35
35
|
import { wanted } from "./zoom.js";
|
|
36
36
|
|
|
37
|
-
let OPTION = ["page", "zoom", "filename", "colorScheme"];
|
|
38
|
-
let SUBJECT = ["data", "report"];
|
|
39
|
-
|
|
40
|
-
/** @type {(el: LitElement, changed: Map<string, unknown>) => boolean} */
|
|
41
|
-
let optionWrite = (el, changed) => !el.hasUpdated || OPTION.some((key) => changed.has(key));
|
|
42
|
-
|
|
43
|
-
/** @type {(changed: Map<string, unknown>) => boolean} */
|
|
44
|
-
let subjectWrite = (changed) => SUBJECT.some((key) => changed.has(key));
|
|
45
|
-
|
|
46
37
|
/** @type {(report: unknown, targets: unknown) => boolean} */
|
|
47
38
|
let vacant = (report, targets) => report === undefined && targets === undefined;
|
|
48
39
|
|
|
@@ -202,10 +193,14 @@ export class QuarioViewer extends LitElement {
|
|
|
202
193
|
// last good values and is stashed for the task, whose next run reports it
|
|
203
194
|
// through the one failure channel; flipping validity re-runs the task by
|
|
204
195
|
// epoch, because the render arguments themselves did not change.
|
|
205
|
-
if (
|
|
196
|
+
if (
|
|
197
|
+
!this.hasUpdated ||
|
|
198
|
+
["page", "zoom", "filename", "colorScheme"].some((key) => changed.has(key))
|
|
199
|
+
)
|
|
200
|
+
this.#checkOptions(changed);
|
|
206
201
|
// A new document is a new subject: dismissing the last failure said
|
|
207
202
|
// nothing about this one.
|
|
208
|
-
if (
|
|
203
|
+
if (["data", "report"].some((key) => changed.has(key))) this.#dismissed = false;
|
|
209
204
|
}
|
|
210
205
|
|
|
211
206
|
render() {
|
|
@@ -300,26 +295,6 @@ export class QuarioViewer extends LitElement {
|
|
|
300
295
|
}
|
|
301
296
|
}
|
|
302
297
|
|
|
303
|
-
/** @param {Map<string, unknown>} changed */
|
|
304
|
-
#commitPage(changed) {
|
|
305
|
-
let box = geometry(this.page);
|
|
306
|
-
if (!this.hasUpdated || changed.has("page")) {
|
|
307
|
-
this.#box = box;
|
|
308
|
-
this.#reapply = true;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/** @param {Map<string, unknown>} changed */
|
|
313
|
-
#commitZoom(changed) {
|
|
314
|
-
let mode = level(this.zoom);
|
|
315
|
-
// Committed only when the host wrote `zoom`, so re-validating on a
|
|
316
|
-
// filename change cannot clobber the mode the reader clicked to.
|
|
317
|
-
if (!this.hasUpdated || changed.has("zoom")) {
|
|
318
|
-
this.#mode = mode;
|
|
319
|
-
this.#reapply = true;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
298
|
/**
|
|
324
299
|
* Each property validates and commits on its own, stashing the first
|
|
325
300
|
* failure: one bad property must not block a good write to another. All
|
|
@@ -338,8 +313,22 @@ export class QuarioViewer extends LitElement {
|
|
|
338
313
|
#checkOptions(changed) {
|
|
339
314
|
let was = this.#invalid;
|
|
340
315
|
this.#invalid = undefined;
|
|
341
|
-
this.#take(() =>
|
|
342
|
-
|
|
316
|
+
this.#take(() => {
|
|
317
|
+
let box = geometry(this.page);
|
|
318
|
+
if (!this.hasUpdated || changed.has("page")) {
|
|
319
|
+
this.#box = box;
|
|
320
|
+
this.#reapply = true;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
this.#take(() => {
|
|
324
|
+
let mode = level(this.zoom);
|
|
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
|
+
if (!this.hasUpdated || changed.has("zoom")) {
|
|
328
|
+
this.#mode = mode;
|
|
329
|
+
this.#reapply = true;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
343
332
|
this.#take(() => {
|
|
344
333
|
this.#name = name(this.filename);
|
|
345
334
|
});
|
package/lib/stage.js
CHANGED
|
@@ -58,7 +58,12 @@ export let SURFACE = css`
|
|
|
58
58
|
transform-origin: 0 0;
|
|
59
59
|
background: #fff;
|
|
60
60
|
color: #000;
|
|
61
|
-
|
|
61
|
+
/* Sheet geometry only. The report text's own family comes from the shared
|
|
62
|
+
report stylesheet, which the html target ships byte-identically -- a
|
|
63
|
+
preview and an export that disagree on typeface is the bug this splits
|
|
64
|
+
apart (docs/adr/0014). */
|
|
65
|
+
font-size: 10pt;
|
|
66
|
+
line-height: 1.4;
|
|
62
67
|
box-shadow: var(--_sheet-shadow);
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -107,7 +112,9 @@ let measure = (sheet, text, pxPerPt) => {
|
|
|
107
112
|
probe.style.cssText =
|
|
108
113
|
"position:absolute;visibility:hidden;white-space:nowrap;font:" +
|
|
109
114
|
FACE +
|
|
110
|
-
|
|
115
|
+
// The face the marking is actually drawn in, or the measurement sizes
|
|
116
|
+
// wording that will be laid out in something else.
|
|
117
|
+
"pt sans-serif";
|
|
111
118
|
probe.textContent = text;
|
|
112
119
|
sheet.append(probe);
|
|
113
120
|
let width = probe.offsetWidth / pxPerPt;
|
|
@@ -154,7 +161,9 @@ let stampAt = (face, y) => {
|
|
|
154
161
|
mark.style.top = y + "pt";
|
|
155
162
|
let wording = document.createElement("span");
|
|
156
163
|
wording.textContent = face.text;
|
|
157
|
-
|
|
164
|
+
// The same face the PDF target draws the marking in (`families.sans[0]`,
|
|
165
|
+
// Helvetica): quario's own wording, so the two must not disagree either.
|
|
166
|
+
wording.style.font = face.fontSize + "pt sans-serif";
|
|
158
167
|
wording.style.transform = "translate(-50%, -50%) rotate(" + face.turn + "deg)";
|
|
159
168
|
mark.append(wording);
|
|
160
169
|
return mark;
|
|
@@ -296,14 +305,20 @@ export let stage = () => {
|
|
|
296
305
|
/**
|
|
297
306
|
* Give the sheet its point geometry. Page points are CSS points, so the
|
|
298
307
|
* host's values transfer unit-for-unit; the wrapper re-sizes against the
|
|
299
|
-
* result so the scroll extent keeps describing what is there.
|
|
300
|
-
*
|
|
308
|
+
* result so the scroll extent keeps describing what is there.
|
|
309
|
+
*
|
|
310
|
+
* The sheet is the page's shape: its width, and at least one page tall,
|
|
311
|
+
* the same rule the editor's sheet follows. A minimum rather than a
|
|
312
|
+
* height, because the sheet stays one continuous surface -- so the height
|
|
313
|
+
* goes on serving as the watermark stamp period along it too
|
|
314
|
+
* (docs/adr/0035).
|
|
301
315
|
*
|
|
302
316
|
* @param {PageBox} next
|
|
303
317
|
*/
|
|
304
318
|
resize: (next) => {
|
|
305
319
|
page = next;
|
|
306
320
|
sheet.style.width = next.width + "pt";
|
|
321
|
+
sheet.style.minHeight = next.height + "pt";
|
|
307
322
|
sheet.style.padding = next.margin + "pt";
|
|
308
323
|
marking(true);
|
|
309
324
|
paint();
|
package/lib/style.js
CHANGED
|
@@ -23,15 +23,44 @@ import { css } from "lit";
|
|
|
23
23
|
|
|
24
24
|
export let REPORT = css`
|
|
25
25
|
/* shared:start */
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* The baseline face hangs off the report root rather than off .q-item and
|
|
29
|
+
* .q-table: a direct rule beats an inherited one, so a rule on the items
|
|
30
|
+
* themselves would override the report default an author writes on the
|
|
31
|
+
* document, which the root carries as an inline font-family. The marking is
|
|
32
|
+
* outside the root and keeps its own rule for that reason.
|
|
33
|
+
*/
|
|
34
|
+
.q-report,
|
|
35
|
+
.q-unlicensed {
|
|
36
|
+
font-family: sans-serif;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.q-report,
|
|
40
|
+
.q-group {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
.q-table {
|
|
27
46
|
width: 100%;
|
|
28
47
|
border-collapse: collapse;
|
|
29
48
|
}
|
|
30
49
|
|
|
50
|
+
/* A visible text item occupies a line, and a literal newline is a break.
|
|
51
|
+
The class is the hook; a host overrides these the ordinary way, as with
|
|
52
|
+
.q-break. Empty table cells stay contentless, so they take only the
|
|
53
|
+
newline mapping. */
|
|
54
|
+
.q-item {
|
|
55
|
+
min-height: 1lh;
|
|
56
|
+
white-space: pre-line;
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
.q-table th,
|
|
32
60
|
.q-table td {
|
|
33
61
|
padding: 2pt 6pt;
|
|
34
62
|
text-align: left;
|
|
63
|
+
white-space: pre-line;
|
|
35
64
|
}
|
|
36
65
|
|
|
37
66
|
.q-table thead th {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -48,28 +48,26 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@arethetypeswrong/cli": "^0.18.3",
|
|
51
|
-
"@quario/csv": "^0.1.0",
|
|
52
|
-
"@quario/html": "^0.1.0",
|
|
53
|
-
"@quario/pdf": "^0.1.0",
|
|
54
|
-
"@quario/xlsx": "^0.1.0",
|
|
55
51
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
56
52
|
"esbuild": "^0.28.2",
|
|
57
53
|
"exceljs": "^4.4.0",
|
|
58
54
|
"pdf-lib": "^1.17.1",
|
|
59
|
-
"quario": "^0.
|
|
55
|
+
"quario": "^0.3.0",
|
|
60
56
|
"size-limit": "^13.0.3",
|
|
61
57
|
"typescript": "^7.0.2"
|
|
62
58
|
},
|
|
63
59
|
"peerDependencies": {
|
|
64
|
-
"quario": "^0.
|
|
60
|
+
"quario": "^0.3.0"
|
|
65
61
|
},
|
|
66
62
|
"size-limit": [
|
|
67
63
|
{
|
|
68
64
|
"path": "lib/index.js",
|
|
69
65
|
"ignore": [
|
|
70
|
-
"quario"
|
|
66
|
+
"quario",
|
|
67
|
+
"lit",
|
|
68
|
+
"@lit/task"
|
|
71
69
|
],
|
|
72
|
-
"limit": "
|
|
70
|
+
"limit": "8.5 kB"
|
|
73
71
|
}
|
|
74
72
|
],
|
|
75
73
|
"engines": {
|