@broadpaper/renderer 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,1109 @@
1
+ // src/dom.ts
2
+ import { classNames, styleString } from "@broadpaper/core";
3
+ var SVG_NS = "http://www.w3.org/2000/svg";
4
+ function vnodeToDom(v, doc = document) {
5
+ if (typeof v === "string") return doc.createTextNode(v);
6
+ const el = v.ns === "svg" ? doc.createElementNS(SVG_NS, v.tag) : doc.createElement(v.tag);
7
+ applyProps(el, v.props);
8
+ for (const c of v.children) el.appendChild(vnodeToDom(c, doc));
9
+ return el;
10
+ }
11
+ function applyProps(el, props) {
12
+ for (const [k, v] of Object.entries(props)) {
13
+ if (v === void 0 || v === null || v === false) continue;
14
+ if (k === "class") {
15
+ const c = classNames(v);
16
+ if (c) el.setAttribute("class", c);
17
+ } else if (k === "style") {
18
+ const s = styleString(v);
19
+ if (s) el.setAttribute("style", s);
20
+ } else if (v === true) el.setAttribute(k, "");
21
+ else el.setAttribute(k, String(v));
22
+ }
23
+ }
24
+ function mountVNodes(container, vnodes) {
25
+ const doc = container.ownerDocument;
26
+ const frag = doc.createDocumentFragment();
27
+ for (const v of vnodes) frag.appendChild(vnodeToDom(v, doc));
28
+ container.replaceChildren(frag);
29
+ }
30
+ function ensureStyle(doc, id, css) {
31
+ let el = doc.getElementById(id);
32
+ if (!el) {
33
+ el = doc.createElement("style");
34
+ el.id = id;
35
+ doc.head.appendChild(el);
36
+ }
37
+ if (el.textContent !== css) el.textContent = css;
38
+ return el;
39
+ }
40
+
41
+ // src/measurer.ts
42
+ import { renderNode } from "@broadpaper/core";
43
+ function hashString(s) {
44
+ let h3 = 2166136261;
45
+ for (let i = 0; i < s.length; i++) {
46
+ h3 ^= s.charCodeAt(i);
47
+ h3 = Math.imul(h3, 16777619);
48
+ }
49
+ return (h3 >>> 0).toString(36) + s.length.toString(36);
50
+ }
51
+ function sectionHash(section, width, mode) {
52
+ return hashString(JSON.stringify(section)) + ":" + Math.round(width * 100) + ":" + mode;
53
+ }
54
+ var DomMeasurer = class {
55
+ constructor(doc, container) {
56
+ this.doc = doc;
57
+ this.host = doc.createElement("div");
58
+ this.host.className = "bp-doc bp-galley";
59
+ this.host.setAttribute("aria-hidden", "true");
60
+ (container ?? doc.body).appendChild(this.host);
61
+ }
62
+ doc;
63
+ host;
64
+ cache = /* @__PURE__ */ new Map();
65
+ opts = null;
66
+ setOptions(opts) {
67
+ this.opts = opts;
68
+ this.host.classList.toggle("bp-doc--design", opts.mode === "design");
69
+ }
70
+ measure(sections, width) {
71
+ if (!this.opts) throw new Error("DomMeasurer.setOptions() must be called before measure()");
72
+ const out = {};
73
+ const pending = [];
74
+ for (const section of sections) {
75
+ const key = sectionHash(section, width, this.opts.mode);
76
+ const hit = this.cache.get(key);
77
+ if (hit) {
78
+ out[section.key] = hit;
79
+ continue;
80
+ }
81
+ const el = this.doc.createElement("div");
82
+ el.className = "bp-galley__section";
83
+ el.style.width = `${width}px`;
84
+ el.style.position = "relative";
85
+ const v = renderNode(section, { ...this.opts, width }, width);
86
+ if (v) el.appendChild(vnodeToDom(v, this.doc));
87
+ pending.push({ section, key, el });
88
+ }
89
+ if (pending.length === 0) return out;
90
+ for (const p of pending) this.host.appendChild(p.el);
91
+ for (const p of pending) {
92
+ const geom = readSectionGeometry(p.el, p.section, this.opts);
93
+ this.cache.set(p.key, geom);
94
+ out[p.section.key] = geom;
95
+ }
96
+ for (const p of pending) p.el.remove();
97
+ if (this.cache.size > 400) {
98
+ const keys = [...this.cache.keys()].slice(0, 200);
99
+ for (const k of keys) this.cache.delete(k);
100
+ }
101
+ return out;
102
+ }
103
+ clearCache() {
104
+ this.cache.clear();
105
+ }
106
+ destroy() {
107
+ this.host.remove();
108
+ this.cache.clear();
109
+ }
110
+ };
111
+ function readSectionGeometry(root, section, opts) {
112
+ const base = root.getBoundingClientRect();
113
+ const nodes = {};
114
+ const byKey = /* @__PURE__ */ new Map();
115
+ const collect = (n) => {
116
+ byKey.set(n.key, n);
117
+ for (const c of n.children) collect(c);
118
+ };
119
+ collect(section);
120
+ const elements = root.querySelectorAll("[data-key]");
121
+ for (const el of elements) {
122
+ const key = el.getAttribute("data-key");
123
+ if (nodes[key]) continue;
124
+ const node = byKey.get(key);
125
+ const r = el.getBoundingClientRect();
126
+ const g = { top: r.top - base.top, height: r.height, left: r.left - base.left, width: r.width };
127
+ if (node) {
128
+ const def = opts.registry.get(node.type);
129
+ const measure = def?.measure ?? (node.flow.split === "lines" ? "text" : node.flow.split === "rows" ? "table" : "none");
130
+ if (measure === "text") g.lines = readLineBoxes(el, base.top, g.top + g.height);
131
+ else if (measure === "table") g.rows = readTableRows(el, base.top);
132
+ }
133
+ nodes[key] = g;
134
+ }
135
+ const sectionRect = root.firstElementChild?.getBoundingClientRect();
136
+ const height = sectionRect ? sectionRect.height : base.height;
137
+ return { height, width: base.width, nodes };
138
+ }
139
+ function readLineBoxes(el, baseTop, nodeBottom) {
140
+ const container = el.querySelector("[data-part='text']") ?? el;
141
+ const doc = el.ownerDocument;
142
+ const rects = [];
143
+ const walker = doc.createTreeWalker(container, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, {
144
+ acceptNode: (n) => {
145
+ if (n.nodeType === Node.TEXT_NODE) return (n.textContent ?? "").trim().length ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
146
+ const tag = n.tagName;
147
+ if (tag === "BR" || tag === "IMG" || tag === "SVG") return NodeFilter.FILTER_ACCEPT;
148
+ const e = n;
149
+ if (e.classList?.contains("bp-p--empty")) return NodeFilter.FILTER_ACCEPT;
150
+ return NodeFilter.FILTER_SKIP;
151
+ }
152
+ });
153
+ let cur = walker.nextNode();
154
+ while (cur) {
155
+ if (cur.nodeType === Node.TEXT_NODE) {
156
+ const range = doc.createRange();
157
+ range.selectNodeContents(cur);
158
+ for (const r of range.getClientRects()) if (r.height > 0 && r.width > 0) rects.push({ top: r.top - baseTop, bottom: r.bottom - baseTop });
159
+ } else {
160
+ const r = cur.getBoundingClientRect();
161
+ if (r.height > 0) rects.push({ top: r.top - baseTop, bottom: r.bottom - baseTop });
162
+ }
163
+ cur = walker.nextNode();
164
+ }
165
+ if (rects.length === 0) return [];
166
+ rects.sort((a, b) => a.top - b.top || a.bottom - b.bottom);
167
+ const lines = [];
168
+ for (const r of rects) {
169
+ const last = lines[lines.length - 1];
170
+ if (last && r.top < last.bottom - 0.5) {
171
+ last.top = Math.min(last.top, r.top);
172
+ last.bottom = Math.max(last.bottom, r.bottom);
173
+ } else lines.push({ ...r });
174
+ }
175
+ const out = [];
176
+ for (let i = 0; i < lines.length - 1; i++) out.push(round2((lines[i].bottom + lines[i + 1].top) / 2));
177
+ out.push(round2(nodeBottom));
178
+ return out;
179
+ }
180
+ function readTableRows(el, baseTop) {
181
+ const table = el.querySelector("table.bp-table");
182
+ if (!table) return [];
183
+ const rows = [];
184
+ const read = (selector, kind) => {
185
+ for (const tr of table.querySelectorAll(`:scope > ${selector} > tr`)) {
186
+ const r = tr.getBoundingClientRect();
187
+ rows.push({ top: round2(r.top - baseTop), height: round2(r.height), kind });
188
+ }
189
+ };
190
+ read("thead", "header");
191
+ read("tbody", "body");
192
+ read("tfoot", "footer");
193
+ return rows;
194
+ }
195
+ function round2(n) {
196
+ return Math.round(n * 100) / 100;
197
+ }
198
+
199
+ // src/layout.ts
200
+ import {
201
+ computePageGeometry,
202
+ mergeTheme,
203
+ resolveValue,
204
+ paginate,
205
+ resolveTemplate,
206
+ renderNode as renderNode2,
207
+ h,
208
+ walkVNodes
209
+ } from "@broadpaper/core";
210
+ function indexNodes(doc) {
211
+ const map = /* @__PURE__ */ new Map();
212
+ const visit = (n) => {
213
+ map.set(n.key, n);
214
+ n.children.forEach(visit);
215
+ };
216
+ for (const s of doc.body) visit(s);
217
+ for (const s of doc.header.default) visit(s);
218
+ for (const s of doc.header.firstPage ?? []) visit(s);
219
+ for (const s of doc.footer.default) visit(s);
220
+ for (const s of doc.footer.firstPage ?? []) visit(s);
221
+ return map;
222
+ }
223
+ function prepareLayout(input) {
224
+ const mode = input.mode ?? "preview";
225
+ const theme = mergeTheme(input.theme);
226
+ const template = input.template;
227
+ const page = computePageGeometry(template.page);
228
+ const now = input.now ?? /* @__PURE__ */ new Date();
229
+ const format = {
230
+ locale: input.locale ?? template.settings?.locale ?? "en-GB",
231
+ currency: input.currency ?? template.settings?.currency ?? "GBP",
232
+ timeZone: input.timeZone ?? template.settings?.timeZone,
233
+ now
234
+ };
235
+ const resolved = resolveTemplate({
236
+ template,
237
+ registry: input.registry,
238
+ theme,
239
+ dataSources: input.dataSources,
240
+ data: input.data,
241
+ sampleData: input.sampleData,
242
+ mode,
243
+ now,
244
+ locale: format.locale,
245
+ currency: format.currency,
246
+ timeZone: format.timeZone,
247
+ designIterations: input.designIterations,
248
+ allowStyleOverrides: input.allowStyleOverrides
249
+ });
250
+ const baseOpts = {
251
+ registry: input.registry,
252
+ theme,
253
+ styles: template.styles,
254
+ mode,
255
+ page: { number: 1, total: 1 },
256
+ format,
257
+ width: page.contentWidth,
258
+ decorate: input.decorate
259
+ };
260
+ const regionSections = [...resolved.header.default, ...resolved.header.firstPage ?? [], ...resolved.footer.default, ...resolved.footer.firstPage ?? []];
261
+ return { input, mode, theme, template, page, format, resolved, baseOpts, regionSections };
262
+ }
263
+ function finishLayout(p, geometry, regionGeometry) {
264
+ const { input, mode, theme, template, page, format, resolved, baseOpts } = p;
265
+ const paged = paginate({ document: resolved, geometry, page, minPages: input.minPages ?? template.page.minPages ?? 1, regionGeometry });
266
+ const nodesByKey = indexNodes(resolved);
267
+ const warnings = [
268
+ ...resolved.warnings.map((w) => ({ source: "resolve", message: w.message, nodeId: w.nodeId })),
269
+ ...paged.warnings.map((w) => ({ source: "layout", message: w.message, nodeId: w.nodeId, page: w.page, code: w.code }))
270
+ ];
271
+ const renderPage = (index) => {
272
+ const p2 = paged.pages[index];
273
+ if (!p2) throw new Error(`Page ${index} does not exist`);
274
+ const opts = { ...baseOpts, page: { number: p2.number, total: paged.totalPages } };
275
+ return composePage(p2.index, p2.number, paged.totalPages, p2.first, resolved, paged, page, opts, nodesByKey, regionGeometry, theme, template.page.background, geometry);
276
+ };
277
+ return {
278
+ resolved,
279
+ paged,
280
+ geometry,
281
+ regionGeometry,
282
+ page,
283
+ theme,
284
+ format,
285
+ mode,
286
+ warnings,
287
+ nodesByKey,
288
+ renderPage,
289
+ renderPages: () => paged.pages.map((_, i) => renderPage(i))
290
+ };
291
+ }
292
+ function layoutDocument(input, measurer) {
293
+ const p = prepareLayout(input);
294
+ measurer.setOptions?.(p.baseOpts);
295
+ const geometry = measurer.measure(p.resolved.body, p.page.contentWidth);
296
+ const regionGeometry = p.regionSections.length ? measurer.measure(p.regionSections, p.page.contentWidth) : {};
297
+ return finishLayout(p, geometry, regionGeometry);
298
+ }
299
+ async function layoutDocumentAsync(input, measurer) {
300
+ const p = prepareLayout(input);
301
+ measurer.setOptions?.(p.baseOpts);
302
+ const geometry = await measurer.measure(p.resolved.body, p.page.contentWidth);
303
+ const regionGeometry = p.regionSections.length ? await measurer.measure(p.regionSections, p.page.contentWidth) : {};
304
+ return finishLayout(p, geometry, regionGeometry);
305
+ }
306
+ function windowSection(root, geom, from, to) {
307
+ if (!geom) return root;
308
+ const visit = (v) => {
309
+ const key = v.props["data-key"];
310
+ if (typeof key === "string") {
311
+ const g = geom.nodes[key];
312
+ if (g) {
313
+ const bottom = g.top + g.height;
314
+ if (bottom <= from - 0.01 || g.top >= to + 0.01) {
315
+ const existing = v.props.style;
316
+ const style = typeof existing === "string" ? `${existing};height:${g.height}px;overflow:hidden` : { ...existing ?? {}, height: `${g.height}px`, overflow: "hidden" };
317
+ return { ...v, props: { ...v.props, style, "data-culled": "" }, children: [] };
318
+ }
319
+ if (g.rows && g.rows.length) return { ...v, children: v.children.map((c) => typeof c === "string" ? c : windowTable(c, g.rows, from, to)) };
320
+ }
321
+ }
322
+ return { ...v, children: v.children.map((c) => typeof c === "string" ? c : visit(c)) };
323
+ };
324
+ return visit(root);
325
+ }
326
+ function windowTable(v, rows, from, to) {
327
+ if (v.tag === "table") {
328
+ const body = rows.filter((r) => r.kind === "body");
329
+ return {
330
+ ...v,
331
+ children: v.children.map((c) => {
332
+ if (typeof c === "string" || c.tag !== "tbody") return c;
333
+ const trs = c.children.filter((x) => typeof x !== "string" && x.tag === "tr");
334
+ if (trs.length !== body.length) return c;
335
+ let leading = 0;
336
+ let trailing = 0;
337
+ const kept = [];
338
+ for (let i = 0; i < trs.length; i++) {
339
+ const r = body[i];
340
+ const bottom = r.top + r.height;
341
+ if (bottom <= from + 0.01) leading += r.height;
342
+ else if (r.top >= to - 0.01) trailing += r.height;
343
+ else kept.push(trs[i]);
344
+ }
345
+ const spacer = (height) => h("tr", { class: "bp-table__spacer" }, h("td", { colspan: "100", style: { padding: "0", border: "0", height: `${height}px` } }));
346
+ return { ...c, children: [...leading > 0 ? [spacer(leading)] : [], ...kept, ...trailing > 0 ? [spacer(trailing)] : []] };
347
+ })
348
+ };
349
+ }
350
+ return { ...v, children: v.children.map((c) => typeof c === "string" ? c : windowTable(c, rows, from, to)) };
351
+ }
352
+ function renderTableHeader(node, opts, width) {
353
+ const full = renderNode2(node, opts, width);
354
+ if (!full) return null;
355
+ walkVNodes(full, (v) => {
356
+ if (v.tag === "table") {
357
+ v.children = v.children.filter((c) => typeof c === "string" || c.tag === "colgroup" || c.tag === "thead");
358
+ return false;
359
+ }
360
+ return true;
361
+ });
362
+ const props = { ...full.props };
363
+ delete props.style;
364
+ full.props = { ...props, class: [String(props.class ?? ""), "bp-repeat-header__node"] };
365
+ return full;
366
+ }
367
+ function composePage(index, number, total, first, resolved, paged, page, opts, nodesByKey, regionGeometry, theme, pageBackground, geometry) {
368
+ const p = paged.pages[index];
369
+ const pageStyle = {
370
+ width: `${page.width}px`,
371
+ height: `${page.height}px`,
372
+ background: resolveValue(pageBackground, theme, "#ffffff")
373
+ };
374
+ const region = (sections, top, height, name, align) => {
375
+ if (!sections || sections.length === 0 || height <= 0) return null;
376
+ const total2 = sections.reduce((a, s) => a + (regionGeometry[s.key]?.height ?? 0), 0);
377
+ const offset = align === "bottom" ? Math.max(0, height - total2) : 0;
378
+ return h(
379
+ "div",
380
+ { class: ["bp-page__region", `bp-page__${name}`], "data-region": name, style: { top: `${top}px`, left: `${page.margins.left}px`, width: `${page.contentWidth}px`, height: `${height}px` } },
381
+ h("div", { class: "bp-page__region-inner", style: { position: "absolute", top: `${offset}px`, left: "0", width: "100%" } }, sections.map((s) => renderNode2(s, opts, page.contentWidth)))
382
+ );
383
+ };
384
+ const headerSections = first && resolved.header.firstPage ? resolved.header.firstPage : resolved.header.default;
385
+ const footerSections = first && resolved.footer.firstPage ? resolved.footer.firstPage : resolved.footer.default;
386
+ const windowed = (section, from, to) => {
387
+ const v = renderNode2(section, opts, page.contentWidth);
388
+ if (!v) return null;
389
+ return windowSection(v, geometry?.[section.key], from, to);
390
+ };
391
+ const fragments = p.fragments.map((f) => {
392
+ const section = nodesByKey.get(f.sectionKey);
393
+ if (!section) return null;
394
+ const extra = f.repeatHeaders.reduce((a, r) => a + r.height, 0);
395
+ const headers = f.repeatHeaders.map((r) => {
396
+ const t = nodesByKey.get(r.tableKey);
397
+ const v = t ? renderTableHeader(t, opts, r.width) : null;
398
+ return h("div", { class: "bp-repeat-header", "data-table": r.tableKey, style: { left: `${r.left}px`, width: `${r.width}px`, height: `${r.height}px` } }, v);
399
+ });
400
+ return h(
401
+ "div",
402
+ { class: ["bp-fragment", f.continued && "bp-fragment--continued", f.continues && "bp-fragment--continues"], "data-section": f.sectionKey, "data-from": String(f.from), "data-to": String(f.to), style: { top: `${f.y}px`, height: `${f.height}px` } },
403
+ headers,
404
+ h("div", { class: "bp-fragment__clip", style: { top: `${extra}px`, height: `${f.to - f.from}px` } }, h("div", { class: "bp-fragment__content", style: { "margin-top": `${-f.from}px`, width: `${page.contentWidth}px` } }, windowed(section, f.from, f.to)))
405
+ );
406
+ });
407
+ return h(
408
+ "div",
409
+ { class: ["bp-page", first && "bp-page--first"], "data-page": String(number), "data-page-index": String(index), style: pageStyle, "aria-label": `Page ${number} of ${total}` },
410
+ region(headerSections, page.headerTop, page.headerHeight, "header", "top"),
411
+ h("div", { class: "bp-page__body", "data-region": "body", style: { top: `${page.contentTop}px`, left: `${page.margins.left}px`, width: `${page.contentWidth}px`, height: `${page.contentHeight}px` } }, fragments),
412
+ region(footerSections, page.footerTop, page.footerHeight, "footer", "bottom")
413
+ );
414
+ }
415
+
416
+ // src/preview.ts
417
+ import { h as h2, toHtml, escapeHtml } from "@broadpaper/core";
418
+
419
+ // src/styles.ts
420
+ var DOCUMENT_CSS = `
421
+ .bp-doc, .bp-doc * { box-sizing: border-box; }
422
+ .bp-doc { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; font-variant-numeric: tabular-nums; }
423
+ .bp-doc p { margin: 0; }
424
+ .bp-doc h1, .bp-doc h2, .bp-doc h3, .bp-doc h4 { margin: 0; font-weight: inherit; }
425
+ .bp-doc img { max-width: 100%; }
426
+ .bp-doc a { color: inherit; }
427
+ .bp-doc table { border-spacing: 0; }
428
+ .bp-doc ul.bp-list, .bp-doc ol.bp-list { margin: 0; padding-left: 1.4em; }
429
+ .bp-doc .bp-list li { margin: 0.15em 0; }
430
+ .bp-doc .bp-code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.92em; background: rgba(0,0,0,0.05); padding: 0 3px; border-radius: 3px; }
431
+ .bp-doc .bp-p--empty { min-height: 1em; }
432
+
433
+ .bp-page { position: relative; overflow: hidden; background: #fff; color: #1a1d21; }
434
+ .bp-page__region { position: absolute; overflow: hidden; }
435
+ .bp-page__body { position: absolute; overflow: hidden; }
436
+ .bp-fragment { position: absolute; left: 0; width: 100%; overflow: hidden; }
437
+ .bp-fragment__clip { position: absolute; left: 0; right: 0; overflow: hidden; }
438
+ .bp-repeat-header { position: absolute; top: 0; overflow: hidden; }
439
+ .bp-repeat-header .bp-table { table-layout: fixed; }
440
+
441
+ .bp-stack { display: flex; flex-direction: column; }
442
+ .bp-stack > .bp-node { min-width: 0; }
443
+ .bp-node--hidden > * { opacity: 0.4; }
444
+ .bp-node--placeholder { opacity: 0.85; }
445
+ .bp-empty-section, .bp-empty-column { display: none; }
446
+ .bp-galley { position: absolute; left: -100000px; top: 0; visibility: hidden; pointer-events: none; }
447
+
448
+ .bp-table { width: 100%; border-collapse: collapse; }
449
+ .bp-table td, .bp-table th { word-wrap: break-word; overflow-wrap: anywhere; }
450
+ .bp-chart__svg text { user-select: none; }
451
+ .bp-page-break--print { height: 0; }
452
+
453
+ /* --- design mode affordances (only when .bp-doc--design) --- */
454
+ .bp-doc--design .bp-token { display: inline; padding: 0 5px; margin: 0 1px; border-radius: 4px; background: rgba(79, 111, 165, 0.14); color: #2f4f7f; font-size: 0.92em; font-weight: 500; white-space: nowrap; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
455
+ .bp-doc--design .bp-token--lg { font-size: 0.65em; vertical-align: middle; }
456
+ .bp-doc--design .bp-token__value { opacity: 0.65; margin-left: 4px; font-weight: 400; }
457
+ .bp-doc--design .bp-cell--bound { text-decoration: underline dotted rgba(79,111,165,0.45); text-underline-offset: 3px; text-decoration-thickness: 1px; }
458
+ .bp-doc--design .bp-placeholder-text { color: #9aa1ab; font-style: italic; }
459
+ .bp-doc--design .bp-empty-section { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; min-height: 96px; border: 1.5px dashed #cfd5dd; border-radius: 8px; color: #6b7280; font-size: 12px; font-family: Inter, system-ui, sans-serif; background: #fafbfc; }
460
+ .bp-doc--design .bp-empty-section__title { font-weight: 600; color: #4b5563; }
461
+ .bp-doc--design .bp-empty-column { display: flex; align-items: center; justify-content: center; min-height: 56px; border: 1.5px dashed #d4d9e0; border-radius: 6px; color: #9aa1ab; font-size: 11px; font-family: Inter, system-ui, sans-serif; }
462
+ .bp-doc--design .bp-spacer--design { background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,0.04) 6px 8px); border-radius: 3px; }
463
+ .bp-doc--design .bp-page-break { display: flex; align-items: center; gap: 8px; margin: 2px 0; color: #b4541f; font-size: 9.5px; font-family: Inter, system-ui, sans-serif; text-transform: uppercase; letter-spacing: 0.06em; }
464
+ .bp-doc--design .bp-page-break::before, .bp-doc--design .bp-page-break::after { content: ""; flex: 1; border-top: 2px dashed rgba(180, 84, 31, 0.5); }
465
+ .bp-doc--design .bp-page-break__label { display: inline-flex; align-items: center; padding: 1px 8px; border-radius: 999px; font-weight: 600; background: rgba(180, 84, 31, 0.12); white-space: nowrap; }
466
+
467
+ /* Structural blocks draw nothing of their own, so design mode outlines them.
468
+ The label is absolutely positioned: naming a box must not resize it. */
469
+ .bp-doc--design .bp-container,
470
+ .bp-doc--design .bp-keep-together { position: relative; outline: 1px dashed rgba(79, 111, 165, 0.32); outline-offset: 1px; border-radius: 3px; }
471
+ .bp-doc--design .bp-container::before,
472
+ .bp-doc--design .bp-keep-together::before {
473
+ content: "Container"; position: absolute; top: -6px; left: 8px; z-index: 1; padding: 0 4px;
474
+ background: #fff; color: #7c8697; font-family: Inter, system-ui, sans-serif; font-size: 8.5px;
475
+ font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; pointer-events: none;
476
+ }
477
+ .bp-doc--design .bp-keep-together::before { content: "Keep together"; }
478
+ .bp-doc--design .bp-repeater__badge, .bp-doc--design .bp-conditional__badge, .bp-doc--design .bp-table__badge, .bp-doc--design .bp-chart__badge { display: inline-flex; align-items: center; gap: 4px; margin-bottom: 4px; padding: 1px 7px; border-radius: 999px; font-size: 9.5px; font-weight: 600; letter-spacing: 0.02em; font-family: Inter, system-ui, sans-serif; background: rgba(79,111,165,0.12); color: #2f4f7f; user-select: none; }
479
+ .bp-doc--design .bp-conditional--false .bp-conditional__badge { background: rgba(199,125,10,0.14); color: #8a5a06; }
480
+ .bp-doc--design .bp-conditional--false > .bp-stack { opacity: 0.45; }
481
+ .bp-doc--design .bp-repeater { border-left: 2px dashed rgba(79,111,165,0.35); padding-left: 8px; }
482
+ .bp-doc--design .bp-conditional { border-left: 2px dashed rgba(199,125,10,0.45); padding-left: 8px; }
483
+ .bp-doc--design .bp-image-placeholder { display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #f1f3f6, #e6e9ee); color: #6b7280; font-size: 11px; font-family: Inter, system-ui, sans-serif; border: 1px dashed #cfd5dd; }
484
+ .bp-doc--design .bp-logo-placeholder { display: inline-flex; align-items: center; padding: 0 12px; border: 1px dashed #cfd5dd; border-radius: 4px; color: #6b7280; font-size: 11px; font-family: Inter, system-ui, sans-serif; }
485
+ .bp-doc--design .bp-node--locked { outline: 1px dashed rgba(0,0,0,0.15); }
486
+
487
+ @media print {
488
+ .bp-page { break-after: page; page-break-after: always; box-shadow: none !important; margin: 0 !important; }
489
+ .bp-page:last-child { break-after: auto; page-break-after: auto; }
490
+ }
491
+ `;
492
+ function fontFaceCss(theme) {
493
+ return (theme.fonts ?? []).map((f) => {
494
+ const src = (Array.isArray(f.src) ? f.src : [f.src]).map((u) => `url("${u.replace(/"/g, "%22")}")`).join(", ");
495
+ return `@font-face{font-family:"${f.family.replace(/"/g, "")}";src:${src};font-weight:${f.weight ?? 400};font-style:${f.style ?? "normal"};font-display:${f.display ?? "block"};}`;
496
+ }).join("\n");
497
+ }
498
+
499
+ // src/preview.ts
500
+ async function waitForFonts(doc, families = []) {
501
+ const fonts = doc.fonts;
502
+ if (!fonts) return;
503
+ try {
504
+ await Promise.all(families.map((f) => fonts.load(`12px "${f}"`).catch(() => void 0)));
505
+ await fonts.ready;
506
+ } catch {
507
+ }
508
+ }
509
+ function createReportPreview(container, options) {
510
+ const doc = container.ownerDocument;
511
+ ensureStyle(doc, "bp-document-css", DOCUMENT_CSS);
512
+ const measurer = new DomMeasurer(doc);
513
+ let current = options;
514
+ let destroyed = false;
515
+ const root = doc.createElement("div");
516
+ root.className = `bp-doc bp-preview ${options.className ?? ""}`.trim();
517
+ container.appendChild(root);
518
+ const api = {
519
+ result: null,
520
+ async update(input) {
521
+ current = { ...current, ...input };
522
+ const theme = current.theme;
523
+ ensureStyle(doc, "bp-theme-fonts", theme ? fontFaceCss({ ...theme, id: theme.id ?? "t", name: theme.name ?? "", typography: theme.typography ?? { headingFont: "", bodyFont: "" }, colors: theme.colors ?? {} }) : "");
524
+ await waitForFonts(doc, (theme?.fonts ?? []).map((f) => f.family));
525
+ if (destroyed) return api.result;
526
+ const result = layoutDocument(current, measurer);
527
+ api.result = result;
528
+ const gap = current.pageGap ?? 24;
529
+ const zoom = current.zoom ?? 1;
530
+ root.classList.toggle("bp-doc--design", result.mode === "design");
531
+ const pages = result.renderPages().map((p, i) => h2("div", { class: "bp-preview__page", style: { "margin-bottom": i < result.paged.totalPages - 1 ? `${gap}px` : "0", "box-shadow": "0 1px 3px rgba(16,24,40,0.12), 0 8px 24px rgba(16,24,40,0.08)" } }, p));
532
+ const inner = h2("div", { class: "bp-preview__pages", style: { transform: zoom !== 1 ? `scale(${zoom})` : void 0, "transform-origin": "top center", width: `${result.page.width}px`, margin: "0 auto" } }, pages);
533
+ mountVNodes(root, [inner]);
534
+ return result;
535
+ },
536
+ goToPage(number) {
537
+ const el = root.querySelector(`[data-page="${number}"]`);
538
+ el?.scrollIntoView({ behavior: "smooth", block: "start" });
539
+ },
540
+ destroy() {
541
+ destroyed = true;
542
+ measurer.destroy();
543
+ root.remove();
544
+ }
545
+ };
546
+ void api.update({});
547
+ return api;
548
+ }
549
+ function documentToHtml(result, opts = {}) {
550
+ const pages = result.renderPages();
551
+ const { width, height } = result.page;
552
+ const fonts = fontFaceCss(result.theme);
553
+ const css = `${fonts}
554
+ ${DOCUMENT_CSS}
555
+ @page{size:${width}px ${height}px;margin:0;}
556
+ html,body{margin:0;padding:0;background:#fff;}
557
+ .bp-page{break-after:page;page-break-after:always;}
558
+ .bp-page:last-child{break-after:auto;page-break-after:auto;}
559
+ ${opts.extraCss ?? ""}`;
560
+ const meta = [
561
+ `<meta charset="utf-8">`,
562
+ `<meta name="viewport" content="width=${width}">`,
563
+ `<meta name="generator" content="BroadPaper">`,
564
+ opts.author ? `<meta name="author" content="${escapeHtml(opts.author)}">` : "",
565
+ opts.subject ? `<meta name="description" content="${escapeHtml(opts.subject)}">` : "",
566
+ opts.keywords?.length ? `<meta name="keywords" content="${escapeHtml(opts.keywords.join(", "))}">` : ""
567
+ ].join("\n");
568
+ const body = pages.map((p) => toHtml(p)).join("\n");
569
+ return `<!doctype html>
570
+ <html lang="${escapeHtml(opts.lang ?? "en")}">
571
+ <head>
572
+ ${meta}
573
+ <title>${escapeHtml(opts.title ?? "Report")}</title>
574
+ <style>${css}</style>
575
+ </head>
576
+ <body class="bp-doc${opts.design ? " bp-doc--design" : ""}" data-pages="${result.paged.totalPages}">
577
+ ${body}
578
+ </body>
579
+ </html>`;
580
+ }
581
+
582
+ // src/chart-interactions.ts
583
+ var STYLE_ID = "bp-chart-interactions";
584
+ var CSS = `
585
+ .bp-chart [data-bp-hit] { cursor: default; transition: opacity 120ms ease, filter 120ms ease; }
586
+ .bp-chart--active [data-bp-hit] { opacity: 0.45; }
587
+ .bp-chart--active [data-bp-hit][data-bp-on] { opacity: 1; filter: brightness(1.06); }
588
+ .bp-chart [data-bp-hit]:focus-visible { outline: none; }
589
+ .bp-chart [data-bp-hit][data-bp-focus] { stroke: #111827; stroke-width: 2; paint-order: stroke; }
590
+ .bp-chart-tip {
591
+ position: fixed; z-index: 2147483000; pointer-events: none;
592
+ max-width: 260px; padding: 6px 9px; border-radius: 6px;
593
+ background: #111827; color: #fff;
594
+ font: 12px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
595
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
596
+ opacity: 0; transform: translateY(2px); transition: opacity 90ms ease, transform 90ms ease;
597
+ }
598
+ .bp-chart-tip[data-bp-shown] { opacity: 1; transform: none; }
599
+ .bp-chart-tip__row { display: flex; align-items: baseline; gap: 6px; white-space: nowrap; }
600
+ .bp-chart-tip__row + .bp-chart-tip__row { margin-top: 2px; }
601
+ .bp-chart-tip__swatch { width: 8px; height: 8px; border-radius: 2px; flex: none; align-self: center; }
602
+ .bp-chart-tip__label { color: #cbd5e1; }
603
+ .bp-chart-tip__value { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; }
604
+ .bp-chart-tip__head { font-weight: 600; margin-bottom: 3px; }
605
+ @media (prefers-reduced-motion: reduce) {
606
+ .bp-chart-tip, .bp-chart [data-bp-hit] { transition: none; }
607
+ }
608
+ @media print { .bp-chart-tip { display: none !important; } }
609
+ `;
610
+ function ensureStyle2(doc) {
611
+ if (doc.getElementById(STYLE_ID)) return;
612
+ const el = doc.createElement("style");
613
+ el.id = STYLE_ID;
614
+ el.textContent = CSS;
615
+ doc.head.appendChild(el);
616
+ }
617
+ function defaultTooltip(hit, ctx, doc) {
618
+ const box = doc.createElement("div");
619
+ const head = doc.createElement("div");
620
+ head.className = "bp-chart-tip__head";
621
+ head.textContent = hit.category || ctx.title;
622
+ box.appendChild(head);
623
+ const rows = ctx.type === "pie" || ctx.type === "donut" ? [hit] : ctx.siblings;
624
+ for (const r of rows) {
625
+ const row = doc.createElement("div");
626
+ row.className = "bp-chart-tip__row";
627
+ const swatch = doc.createElement("span");
628
+ swatch.className = "bp-chart-tip__swatch";
629
+ swatch.style.background = r.color;
630
+ row.appendChild(swatch);
631
+ const label = doc.createElement("span");
632
+ label.className = "bp-chart-tip__label";
633
+ label.textContent = ctx.type === "pie" || ctx.type === "donut" ? r.share !== void 0 ? `${Math.round(r.share * 1e3) / 10}%` : "" : r.series;
634
+ row.appendChild(label);
635
+ const value = doc.createElement("span");
636
+ value.className = "bp-chart-tip__value";
637
+ value.textContent = r.value === null ? "\u2014" : r.formatted;
638
+ if (r === hit && rows.length > 1) value.style.textDecoration = "underline";
639
+ row.appendChild(value);
640
+ box.appendChild(row);
641
+ }
642
+ return box;
643
+ }
644
+ function enableChartInteractions(root, options = {}) {
645
+ const doc = root.ownerDocument;
646
+ if (!doc?.defaultView) return () => void 0;
647
+ ensureStyle2(doc);
648
+ const portal = options.portal ?? doc.body;
649
+ const tip = doc.createElement("div");
650
+ tip.className = "bp-chart-tip";
651
+ tip.setAttribute("role", "tooltip");
652
+ tip.setAttribute("aria-hidden", "true");
653
+ let tipMounted = false;
654
+ const cleanups = [];
655
+ const hideTip = () => {
656
+ tip.removeAttribute("data-bp-shown");
657
+ tip.setAttribute("aria-hidden", "true");
658
+ };
659
+ const showTip = (content, anchor) => {
660
+ if (!tipMounted) {
661
+ portal.appendChild(tip);
662
+ tipMounted = true;
663
+ }
664
+ tip.replaceChildren(typeof content === "string" ? doc.createTextNode(content) : content);
665
+ tip.setAttribute("aria-hidden", "false");
666
+ tip.setAttribute("data-bp-shown", "");
667
+ const box = tip.getBoundingClientRect();
668
+ const view = doc.defaultView;
669
+ const gap = 10;
670
+ let left = anchor.left + anchor.width / 2 - box.width / 2;
671
+ let top = anchor.top - box.height - gap;
672
+ if (top < 4) top = anchor.bottom + gap;
673
+ left = Math.max(4, Math.min(left, view.innerWidth - box.width - 4));
674
+ top = Math.max(4, Math.min(top, view.innerHeight - box.height - 4));
675
+ tip.style.left = `${Math.round(left)}px`;
676
+ tip.style.top = `${Math.round(top)}px`;
677
+ };
678
+ for (const chart of Array.from(root.querySelectorAll(".bp-chart[data-bp-chart]"))) {
679
+ if (chart.dataset.bpInteractive === "1") continue;
680
+ let payload;
681
+ try {
682
+ payload = JSON.parse(chart.dataset.bpChart ?? "");
683
+ } catch {
684
+ continue;
685
+ }
686
+ if (!Array.isArray(payload.hits) || payload.hits.length === 0) continue;
687
+ if (options.onChart?.(chart, payload) === false) continue;
688
+ chart.dataset.bpInteractive = "1";
689
+ const marks = Array.from(chart.querySelectorAll("[data-bp-hit]"));
690
+ if (marks.length === 0) continue;
691
+ const byCategory = /* @__PURE__ */ new Map();
692
+ for (const hit of payload.hits) {
693
+ const list = byCategory.get(hit.categoryIndex) ?? [];
694
+ list.push(hit);
695
+ byCategory.set(hit.categoryIndex, list);
696
+ }
697
+ const activate = (mark, hit, focus) => {
698
+ chart.classList.add("bp-chart--active");
699
+ const siblings = byCategory.get(hit.categoryIndex) ?? [hit];
700
+ for (const m of marks) {
701
+ const h3 = payload.hits[Number(m.getAttribute("data-bp-hit"))];
702
+ if (h3 && siblings.includes(h3)) m.setAttribute("data-bp-on", "");
703
+ else m.removeAttribute("data-bp-on");
704
+ m.removeAttribute("data-bp-focus");
705
+ }
706
+ if (focus) mark.setAttribute("data-bp-focus", "");
707
+ const content = options.tooltip?.(hit, { type: payload.type, title: payload.title, siblings }) ?? defaultTooltip(hit, { type: payload.type, title: payload.title, siblings }, doc);
708
+ showTip(content, mark.getBoundingClientRect());
709
+ };
710
+ const deactivate = () => {
711
+ chart.classList.remove("bp-chart--active");
712
+ for (const m of marks) {
713
+ m.removeAttribute("data-bp-on");
714
+ m.removeAttribute("data-bp-focus");
715
+ }
716
+ hideTip();
717
+ };
718
+ for (const mark of marks) {
719
+ const hit = payload.hits[Number(mark.getAttribute("data-bp-hit"))];
720
+ if (!hit) continue;
721
+ mark.setAttribute("tabindex", "0");
722
+ mark.setAttribute("role", "img");
723
+ const share = hit.share !== void 0 ? `, ${Math.round(hit.share * 1e3) / 10}%` : "";
724
+ mark.setAttribute("aria-label", `${hit.category}${hit.series ? `, ${hit.series}` : ""}: ${hit.value === null ? "no value" : hit.formatted}${share}`);
725
+ const onEnter = () => activate(mark, hit, false);
726
+ const onFocus = () => activate(mark, hit, true);
727
+ const onKey = (e) => {
728
+ const key = e.key;
729
+ if (key === "Enter" || key === " ") {
730
+ e.preventDefault();
731
+ options.onSelect?.(hit, e);
732
+ } else if (key === "Escape") deactivate();
733
+ };
734
+ const onClick = (e) => options.onSelect?.(hit, e);
735
+ mark.addEventListener("pointerenter", onEnter);
736
+ mark.addEventListener("focus", onFocus);
737
+ mark.addEventListener("keydown", onKey);
738
+ if (options.onSelect) mark.addEventListener("click", onClick);
739
+ cleanups.push(() => {
740
+ mark.removeEventListener("pointerenter", onEnter);
741
+ mark.removeEventListener("focus", onFocus);
742
+ mark.removeEventListener("keydown", onKey);
743
+ mark.removeEventListener("click", onClick);
744
+ mark.removeAttribute("tabindex");
745
+ mark.removeAttribute("role");
746
+ mark.removeAttribute("aria-label");
747
+ mark.removeAttribute("data-bp-on");
748
+ mark.removeAttribute("data-bp-focus");
749
+ });
750
+ }
751
+ chart.addEventListener("pointerleave", deactivate);
752
+ chart.addEventListener("focusout", (e) => {
753
+ if (!chart.contains(e.relatedTarget)) deactivate();
754
+ });
755
+ cleanups.push(() => {
756
+ chart.classList.remove("bp-chart--active");
757
+ delete chart.dataset.bpInteractive;
758
+ });
759
+ }
760
+ const onScroll = () => hideTip();
761
+ doc.addEventListener("scroll", onScroll, true);
762
+ cleanups.push(() => doc.removeEventListener("scroll", onScroll, true));
763
+ return () => {
764
+ for (const c of cleanups.splice(0)) c();
765
+ tip.remove();
766
+ tipMounted = false;
767
+ };
768
+ }
769
+
770
+ // src/client.ts
771
+ import { resolveWatermark, watermarkCss } from "@broadpaper/license";
772
+ async function printReport(result, opts = {}) {
773
+ const mark = await resolveWatermark(opts.license !== void 0 ? { license: opts.license } : {});
774
+ const html = documentToHtml(result, { title: opts.title, ...mark ? { extraCss: watermarkCss(mark) } : {} });
775
+ const frame = document.createElement("iframe");
776
+ frame.setAttribute("aria-hidden", "true");
777
+ frame.style.position = "fixed";
778
+ frame.style.right = "0";
779
+ frame.style.bottom = "0";
780
+ frame.style.width = "0";
781
+ frame.style.height = "0";
782
+ frame.style.border = "0";
783
+ document.body.appendChild(frame);
784
+ const doc = frame.contentDocument;
785
+ doc.open();
786
+ doc.write(html);
787
+ doc.close();
788
+ const win = frame.contentWindow;
789
+ const cleanup = () => {
790
+ setTimeout(() => frame.remove(), 1e3);
791
+ opts.onDone?.();
792
+ };
793
+ win.addEventListener("afterprint", cleanup, { once: true });
794
+ const go = () => {
795
+ win.focus();
796
+ win.print();
797
+ };
798
+ const fonts = doc.fonts;
799
+ if (fonts?.ready) fonts.ready.then(() => setTimeout(go, 50)).catch(go);
800
+ else setTimeout(go, 100);
801
+ }
802
+ function createPdfClient(opts) {
803
+ const f = opts.fetch ?? fetch;
804
+ return {
805
+ async render(req) {
806
+ const res = await f(`${opts.baseUrl.replace(/\/$/, "")}/render`, {
807
+ method: "POST",
808
+ headers: { "content-type": "application/json", ...opts.token ? { authorization: `Bearer ${opts.token}` } : {} },
809
+ body: JSON.stringify({ ...req, now: req.now?.toISOString() })
810
+ });
811
+ if (!res.ok) {
812
+ let message = `PDF service responded ${res.status}`;
813
+ try {
814
+ const j = await res.json();
815
+ if (j.error) message = j.error;
816
+ } catch {
817
+ }
818
+ throw new Error(message);
819
+ }
820
+ return { blob: await res.blob(), pages: Number(res.headers.get("x-broadpaper-pages") ?? 0), warnings: Number(res.headers.get("x-broadpaper-warnings") ?? 0) };
821
+ },
822
+ async health() {
823
+ try {
824
+ const res = await f(`${opts.baseUrl.replace(/\/$/, "")}/health`);
825
+ return res.ok;
826
+ } catch {
827
+ return false;
828
+ }
829
+ }
830
+ };
831
+ }
832
+ function downloadBlob(blob, filename) {
833
+ const url = URL.createObjectURL(blob);
834
+ const a = document.createElement("a");
835
+ a.href = url;
836
+ a.download = filename;
837
+ document.body.appendChild(a);
838
+ a.click();
839
+ a.remove();
840
+ setTimeout(() => URL.revokeObjectURL(url), 5e3);
841
+ }
842
+
843
+ // src/viewer.ts
844
+ var VIEWER_CSS = `
845
+ .bp-viewer { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; background: #f1f2f4; color: #1a1d21; font: 13px/1.4 system-ui, -apple-system, "Segoe UI", sans-serif; }
846
+ .bp-viewer *, .bp-viewer *::before, .bp-viewer *::after { box-sizing: border-box; }
847
+ .bp-viewer__scroll { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 24px 16px; scroll-behavior: smooth; }
848
+ .bp-viewer__stage { margin: 0 auto; transform-origin: top left; }
849
+ .bp-viewer .bp-preview__page { box-shadow: 0 1px 3px rgba(16,24,40,0.14), 0 10px 28px rgba(16,24,40,0.10); }
850
+
851
+ .bp-viewer__bar { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #fff; border-bottom: 1px solid #e2e5ea; }
852
+ .bp-viewer__spacer { flex: 1 1 auto; }
853
+ .bp-viewer__btn { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 28px; padding: 0 8px; border: 1px solid #e2e5ea; border-radius: 6px; background: #fff; color: inherit; font: inherit; cursor: pointer; }
854
+ .bp-viewer__btn:hover:not(:disabled) { background: #f5f6f8; }
855
+ .bp-viewer__btn:disabled { opacity: 0.4; cursor: default; }
856
+ .bp-viewer__btn:focus-visible, .bp-viewer__field:focus-visible { outline: 2px solid #4a6fa5; outline-offset: 1px; }
857
+ .bp-viewer__count { font-variant-numeric: tabular-nums; white-space: nowrap; }
858
+ .bp-viewer__field { width: 3.5em; height: 28px; padding: 0 6px; border: 1px solid #e2e5ea; border-radius: 6px; font: inherit; text-align: right; font-variant-numeric: tabular-nums; }
859
+
860
+ /* The reader's own print of the view. The page boxes are already exact, so all
861
+ this has to do is get the ground and the chrome out of the way. */
862
+ @media print {
863
+ .bp-viewer { background: #fff; height: auto; }
864
+ .bp-viewer__bar { display: none; }
865
+ .bp-viewer__scroll { overflow: visible; padding: 0; }
866
+ .bp-viewer__stage { transform: none !important; width: auto !important; }
867
+ .bp-viewer .bp-preview__page { box-shadow: none; margin: 0 !important; break-after: page; page-break-after: always; }
868
+ .bp-viewer .bp-preview__page:last-child { break-after: auto; page-break-after: auto; }
869
+ }
870
+
871
+ @media (prefers-color-scheme: dark) {
872
+ .bp-viewer { background: #22252a; color: #e8eaed; }
873
+ .bp-viewer__bar { background: #2b2f36; border-bottom-color: #3a3f47; }
874
+ .bp-viewer__btn, .bp-viewer__field { background: #2b2f36; border-color: #3a3f47; color: inherit; }
875
+ .bp-viewer__btn:hover:not(:disabled) { background: #343941; }
876
+ }
877
+ `;
878
+ var clamp = (n, lo, hi) => n < lo ? lo : n > hi ? hi : n;
879
+ function button(doc, label, title) {
880
+ const b = doc.createElement("button");
881
+ b.type = "button";
882
+ b.className = "bp-viewer__btn";
883
+ b.textContent = label;
884
+ b.title = title;
885
+ b.setAttribute("aria-label", title);
886
+ return b;
887
+ }
888
+ function createReportViewer(container, options) {
889
+ const doc = container.ownerDocument;
890
+ const win = doc.defaultView;
891
+ ensureStyle(doc, "bp-viewer-css", VIEWER_CSS);
892
+ let opts = { ...options };
893
+ let destroyed = false;
894
+ let page = 1;
895
+ let zoom = 1;
896
+ let stopInteractions = null;
897
+ const root = doc.createElement("div");
898
+ root.className = `bp-viewer ${opts.className ?? ""}`.trim();
899
+ const bar = doc.createElement("div");
900
+ bar.className = "bp-viewer__bar";
901
+ bar.setAttribute("role", "toolbar");
902
+ bar.setAttribute("aria-label", "Report controls");
903
+ const prev = button(doc, "\u2039", "Previous page");
904
+ const next = button(doc, "\u203A", "Next page");
905
+ const pageField = doc.createElement("input");
906
+ pageField.className = "bp-viewer__field";
907
+ pageField.type = "text";
908
+ pageField.inputMode = "numeric";
909
+ pageField.setAttribute("aria-label", "Page number");
910
+ const count = doc.createElement("span");
911
+ count.className = "bp-viewer__count";
912
+ const zoomOut = button(doc, "\u2212", "Zoom out");
913
+ const zoomIn = button(doc, "+", "Zoom in");
914
+ const zoomLabel = doc.createElement("button");
915
+ zoomLabel.type = "button";
916
+ zoomLabel.className = "bp-viewer__btn bp-viewer__count";
917
+ zoomLabel.title = "Fit to width";
918
+ const printBtn = button(doc, "Print", "Print or save as PDF");
919
+ const spacer = doc.createElement("span");
920
+ spacer.className = "bp-viewer__spacer";
921
+ bar.append(prev, pageField, count, next, spacer, zoomOut, zoomLabel, zoomIn, printBtn);
922
+ const scroll = doc.createElement("div");
923
+ scroll.className = "bp-viewer__scroll";
924
+ scroll.tabIndex = 0;
925
+ scroll.setAttribute("role", "region");
926
+ scroll.setAttribute("aria-label", opts.label ?? "Report");
927
+ const stage = doc.createElement("div");
928
+ stage.className = "bp-viewer__stage";
929
+ scroll.appendChild(stage);
930
+ if (opts.toolbar !== false) root.appendChild(bar);
931
+ root.appendChild(scroll);
932
+ container.appendChild(root);
933
+ const preview = createReportPreview(stage, { ...opts, mode: "preview", zoom: 1 });
934
+ function fitZoom() {
935
+ const result = preview.result;
936
+ if (!result) return 1;
937
+ const explicit = opts.zoom;
938
+ if (typeof explicit === "number") return explicit;
939
+ const fit = opts.fit ?? "width";
940
+ if (fit === "actual") return 1;
941
+ const styles = win?.getComputedStyle(scroll);
942
+ const padX = styles ? parseFloat(styles.paddingLeft || "0") + parseFloat(styles.paddingRight || "0") : 32;
943
+ const padY = styles ? parseFloat(styles.paddingTop || "0") + parseFloat(styles.paddingBottom || "0") : 48;
944
+ const availableWidth = scroll.clientWidth - padX;
945
+ if (!(availableWidth > 0)) return 1;
946
+ const byWidth = availableWidth / result.page.width;
947
+ if (fit === "width") {
948
+ return clamp(byWidth, 0.1, 1);
949
+ }
950
+ const availableHeight = scroll.clientHeight - padY;
951
+ if (!(availableHeight > 0)) return clamp(byWidth, 0.1, 1);
952
+ return clamp(Math.min(byWidth, availableHeight / result.page.height), 0.1, 1);
953
+ }
954
+ function applyZoom() {
955
+ zoom = fitZoom();
956
+ const result = preview.result;
957
+ stage.style.transform = zoom === 1 ? "" : `scale(${zoom})`;
958
+ if (result) {
959
+ const unscaled = stage.scrollHeight;
960
+ stage.style.width = `${Math.round(result.page.width * zoom)}px`;
961
+ stage.style.marginBottom = zoom < 1 ? `${-Math.round(unscaled * (1 - zoom))}px` : "";
962
+ }
963
+ zoomLabel.textContent = `${Math.round(zoom * 100)}%`;
964
+ zoomLabel.setAttribute("aria-label", `Zoom, ${Math.round(zoom * 100)} percent. Activate to fit to width.`);
965
+ }
966
+ function visiblePage() {
967
+ const pages = stage.querySelectorAll("[data-page]");
968
+ if (!pages.length) return 1;
969
+ const top = scroll.getBoundingClientRect().top;
970
+ let best = 1;
971
+ let bestDistance = Infinity;
972
+ for (const el of pages) {
973
+ const box = el.getBoundingClientRect();
974
+ const distance = box.bottom < top ? Infinity : Math.abs(box.top - top);
975
+ if (distance < bestDistance) {
976
+ bestDistance = distance;
977
+ best = Number(el.getAttribute("data-page")) || 1;
978
+ }
979
+ }
980
+ return best;
981
+ }
982
+ function syncBar() {
983
+ const total = preview.result?.paged.totalPages ?? 0;
984
+ count.textContent = total ? `/ ${total}` : "";
985
+ if (doc.activeElement !== pageField) pageField.value = total ? String(page) : "";
986
+ pageField.disabled = total < 2;
987
+ prev.disabled = page <= 1;
988
+ next.disabled = total === 0 || page >= total;
989
+ }
990
+ function setPage(next_, scrollTo) {
991
+ const total = preview.result?.paged.totalPages ?? 0;
992
+ const wanted = total ? clamp(Math.round(next_), 1, total) : 1;
993
+ if (scrollTo) {
994
+ const el = stage.querySelector(`[data-page="${wanted}"]`);
995
+ if (el) scroll.scrollTop += el.getBoundingClientRect().top - scroll.getBoundingClientRect().top;
996
+ }
997
+ if (wanted === page) return;
998
+ page = wanted;
999
+ syncBar();
1000
+ opts.onPageChange?.(page);
1001
+ }
1002
+ const onScroll = () => {
1003
+ const seen = visiblePage();
1004
+ if (seen === page) return;
1005
+ page = seen;
1006
+ syncBar();
1007
+ opts.onPageChange?.(page);
1008
+ };
1009
+ scroll.addEventListener("scroll", onScroll, { passive: true });
1010
+ const onKey = (e) => {
1011
+ if (e.target === pageField) return;
1012
+ if (e.key === "PageDown") setPage(page + 1, true);
1013
+ else if (e.key === "PageUp") setPage(page - 1, true);
1014
+ else if (e.key === "Home") setPage(1, true);
1015
+ else if (e.key === "End") setPage(preview.result?.paged.totalPages ?? 1, true);
1016
+ else return;
1017
+ e.preventDefault();
1018
+ };
1019
+ scroll.addEventListener("keydown", onKey);
1020
+ prev.addEventListener("click", () => setPage(page - 1, true));
1021
+ next.addEventListener("click", () => setPage(page + 1, true));
1022
+ pageField.addEventListener("change", () => setPage(Number(pageField.value), true));
1023
+ zoomLabel.addEventListener("click", () => api.setZoom("width"));
1024
+ zoomIn.addEventListener("click", () => api.setZoom(clamp(zoom + 0.1, 0.1, 4)));
1025
+ zoomOut.addEventListener("click", () => api.setZoom(clamp(zoom - 0.1, 0.1, 4)));
1026
+ printBtn.addEventListener("click", () => api.print());
1027
+ const observer = win && "ResizeObserver" in win ? new win.ResizeObserver(() => applyZoom()) : null;
1028
+ observer?.observe(scroll);
1029
+ function afterRender(result) {
1030
+ stopInteractions?.();
1031
+ stopInteractions = null;
1032
+ if (opts.interactions !== false) {
1033
+ stopInteractions = enableChartInteractions(stage, typeof opts.interactions === "object" ? opts.interactions : {});
1034
+ }
1035
+ applyZoom();
1036
+ setPage(page, false);
1037
+ syncBar();
1038
+ opts.onLayout?.(result);
1039
+ }
1040
+ const api = {
1041
+ result: null,
1042
+ page: 1,
1043
+ pageCount: 0,
1044
+ zoom: 1,
1045
+ async update(input) {
1046
+ opts = { ...opts, ...input };
1047
+ const result = await preview.update({ ...input, mode: "preview", zoom: 1 });
1048
+ if (destroyed) return result;
1049
+ afterRender(result);
1050
+ return result;
1051
+ },
1052
+ goToPage(n) {
1053
+ setPage(n, true);
1054
+ },
1055
+ setZoom(value) {
1056
+ if (typeof value === "number") opts = { ...opts, zoom: value };
1057
+ else opts = { ...opts, fit: value, zoom: void 0 };
1058
+ applyZoom();
1059
+ },
1060
+ print() {
1061
+ const result = preview.result;
1062
+ if (!result) return;
1063
+ if (opts.onPrint) opts.onPrint(result);
1064
+ else win?.print();
1065
+ },
1066
+ destroy() {
1067
+ destroyed = true;
1068
+ observer?.disconnect();
1069
+ scroll.removeEventListener("scroll", onScroll);
1070
+ scroll.removeEventListener("keydown", onKey);
1071
+ stopInteractions?.();
1072
+ preview.destroy();
1073
+ root.remove();
1074
+ }
1075
+ };
1076
+ Object.defineProperties(api, {
1077
+ result: { get: () => preview.result, enumerable: true },
1078
+ page: { get: () => page, enumerable: true },
1079
+ pageCount: { get: () => preview.result?.paged.totalPages ?? 0, enumerable: true },
1080
+ zoom: { get: () => zoom, enumerable: true }
1081
+ });
1082
+ syncBar();
1083
+ void api.update({});
1084
+ return api;
1085
+ }
1086
+ export {
1087
+ DOCUMENT_CSS,
1088
+ DomMeasurer,
1089
+ composePage,
1090
+ createPdfClient,
1091
+ createReportPreview,
1092
+ createReportViewer,
1093
+ documentToHtml,
1094
+ downloadBlob,
1095
+ enableChartInteractions,
1096
+ ensureStyle,
1097
+ fontFaceCss,
1098
+ hashString,
1099
+ layoutDocument,
1100
+ layoutDocumentAsync,
1101
+ mountVNodes,
1102
+ printReport,
1103
+ readLineBoxes,
1104
+ readTableRows,
1105
+ sectionHash,
1106
+ vnodeToDom,
1107
+ waitForFonts
1108
+ };
1109
+ //# sourceMappingURL=index.js.map