pepperscore 0.5.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1 - 2026-11-12
4
+
5
+ ### Changed
6
+
7
+ - Generated violin and cello finger TAB favours staying in position across a phrase, reducing unnecessary position shifts and labels. This changes display-only fingering choices; sounding pitches and playback are unchanged.
8
+ - PDF export is cooperative and cancellable on large scores: pages are annotated and drawn in slices that yield to the browser, so the page stays responsive (a 30-part, one-system-per-page score previously blocked the main thread for seconds per page). Cancel now takes effect within one slice, and the dialog shows “Building PDF… page N of M”. No change to the PDF produced.
9
+
10
+ ### Fixed
11
+
12
+ - Pitch-coloured notehead outlines use the intended default or host-set outline colour in Safari/WebKit instead of drawing solid black. Playback, cursor, selection, transposed and out-of-range highlights retain their own colours.
13
+ - PDF export is verified in Safari/WebKit as well as Chromium and Firefox. In WebKit a failed download of the dialog's lazy chunk stayed failed until the tab was closed, because WebKit keeps the failed `modulepreload` Vite adds. On Vite and native-ESM hosts the dialog now retries once through a cache-busted address, so reopening it or reloading recovers. On other bundlers (for example Webpack) the retry is skipped and the dialog shows its usual error and Reload page button.
14
+
3
15
  ## 0.5.0 - 2026-11-11
4
16
 
5
17
  ### Added
package/README.md CHANGED
@@ -313,6 +313,55 @@ const skin = mergeSkins(base, { theme: { surface: '#fff' }, pitchColours: { C: '
313
313
  <PepperScore abc={tuneAbc} skin={skin} settings={{ pitchColours: true }} />;
314
314
  ```
315
315
 
316
+ #### Worked example: following a host's accent scopes
317
+
318
+ becreative sets `--accent-ink`, `--accent`, `--accent-wash` and `--accent-wash-strong` on `.accent-blue`, `.accent-orange` and `.accent-pink` section wrappers. Mapping theme roles to those variables (with `accentHover` derived from the ink, since the host has no hover token and an unset `accentHover` falls back to the default green) makes the player change colour with the section, with no re-render or re-engrave. Define the skin once (TypeScript):
319
+
320
+ ```ts
321
+ import { CRF_PITCH_COLOURS, mergeSkins, type PepperScoreSkin } from 'pepperscore';
322
+
323
+ export const becreativeSkin: PepperScoreSkin = {
324
+ theme: {
325
+ accent: 'var(--accent-ink)',
326
+ accentHover: 'color-mix(in srgb, var(--accent-ink) 82%, black)', // becreative has no hover token: darken the section ink
327
+ accentWash: 'var(--accent-wash)',
328
+ accentWashStrong: 'var(--accent-wash-strong)',
329
+ focusRing: 'var(--accent)', // a colour; becreative's --accent-ring is a box-shadow value
330
+ },
331
+ pitchColours: CRF_PITCH_COLOURS,
332
+ bar: ['play', 'tempo', 'instrumentSwitch', 'noteNames', 'counts', 'chordSymbols', 'options'],
333
+ };
334
+ // `static` shows no bar, so give it the theme only.
335
+ export const becreativeStaticSkin: PepperScoreSkin = { theme: becreativeSkin.theme };
336
+ // Same skin with a shorter bar; `mergeSkins` replaces `bar` whole.
337
+ export const quietSkin = mergeSkins(becreativeSkin, { bar: ['play', 'options'] });
338
+ ```
339
+
340
+ Plain JavaScript uses the same exports; the skin is only data, with no types needed:
341
+
342
+ ```js
343
+ import { CRF_PITCH_COLOURS, mergeSkins, THEME_TOKENS, BAR_ITEM_IDS } from 'pepperscore';
344
+
345
+ export const becreativeSkin = {
346
+ theme: { accent: 'var(--accent-ink)', accentHover: 'color-mix(in srgb, var(--accent-ink) 82%, black)', accentWash: 'var(--accent-wash)', accentWashStrong: 'var(--accent-wash-strong)', focusRing: 'var(--accent)' },
347
+ pitchColours: CRF_PITCH_COLOURS,
348
+ bar: ['play', 'tempo', 'instrumentSwitch', 'noteNames', 'counts', 'chordSymbols', 'options'],
349
+ };
350
+ console.log(THEME_TOKENS.includes('accent'), BAR_ITEM_IDS.includes('options')); // true true: valid names at runtime
351
+ ```
352
+
353
+ Then pass it as `skin` on the shapes that carry a bar, and the theme-only skin on the shape that does not:
354
+
355
+ ```tsx
356
+ <section className="accent-orange">
357
+ <PepperScore chrome="full" skin={becreativeSkin} abc={tuneAbc} /> {/* reader */}
358
+ <PepperScore chrome="minimal" skin={becreativeSkin} abc={tuneAbc} /> {/* snippet */}
359
+ <PepperScore chrome="none" skin={becreativeStaticSkin} abc={tuneAbc} /> {/* static: no bar */}
360
+ </section>
361
+ ```
362
+
363
+ Changing the section's class (`accent-blue` to `accent-orange` to `accent-pink`) recolours every shape. The reader's embed test page (`/embed-skin?becreative`) runs this at 1280px and 390px and `npm run test:skin` checks it.
364
+
316
365
  - **Theme:** a partial map of `ThemeToken` roles to CSS colours (including host `var()` references). Roles: `accent`, `accentHover`, `accentWash`, `accentWashStrong`, `backdrop`, `border`, `borderMuted`, `danger`, `focusRing`, `onAccent`, `playback`, `shadow`, `surface`, `surfaceRaised`, `text`, `textFaint`, `textMuted`, `textSecondary`, `warning`. Omitted roles keep their defaults. Theme changes do not re-engrave.
317
366
  - **Bar:** an ordered `BarItemId[]` replacing the full/minimal header controls. Available ids: `play`, `stop`, `tempo`, `transpose`, `keyFlip`, `bars`, `viewToggle`, `tab`, `zoom`, `mixer`, `openFile`, `instrumentSwitch`, `octaveTool`, `options`, `noteNames`, `counts`, `chordSymbols`, `pitchColours`, `pdf`. Duplicates and unknown ids are ignored. Explicit `controls` values of `false` still win; unavailable controls are omitted. A bar with no renderable items restores the preset defaults. `chrome="none"` does not show a skin bar. Without `bar`, the preset header is unchanged.
318
367
  - **Drawer:** `options` opens the drawer. Enabled stop, view, bars, zoom, mixer, file and PDF actions omitted from a composed bar appear in its Playback and view panel. Annotation tiles stay in sync with the drawer and saved preferences; `chordSymbols` toggles authored and inferred symbols together.
@@ -338,11 +387,17 @@ The pitch-colour tile appears in the bar/drawer only when a palette is supplied.
338
387
  | Paper (`pdfPaper`) | `'a4'` | A4, A3, Letter or Legal. Remembered globally in the reader's preferences when persistence is on, not per score. It is not a `settings` prop. |
339
388
  | Bars per line | `4` | Local to the dialog; 1–8, reset on reopen. Does not read or change on-screen `settings.barsPerLine`. Lines may use fewer bars to fit. |
340
389
 
341
- The dialog reports “Checking fit…” while measuring, then how many lines need fewer bars. **Save** can proceed without waiting for that check and captures the current view; **Cancel** or Escape aborts work without downloading. Save downloads `<title>.pdf`, with the title normalized to lowercase hyphen-separated text (or `score.pdf` when no usable title exists). Export does not interrupt playback or change the on-screen layout/scroll position.
390
+ The dialog reports “Checking fit…” while measuring, then how many lines need fewer bars. **Save** can proceed without waiting for that check and captures the current view. While it builds, the dialog shows “Building PDF… page N of M”; **Cancel** (or Escape) stops it, discards the work and downloads nothing. Save downloads `<title>.pdf`, with the title normalized to lowercase hyphen-separated text (or `score.pdf` when no usable title exists). Export does not interrupt playback or change the on-screen layout/scroll position.
391
+
392
+ There is no imperative export call: the button, drawer action, skin bar item and `controls.pdf` are the whole public surface, and `pdfPaper` is the only preference.
342
393
 
343
394
  The PDF has vector notation, embedded music/text fonts, title/credits and page numbers. It carries the current transposition/instrument view, visible staves, notation/TAB choice, note names, counts, chord symbols and active pitch colours, including annotation sizes. Screen controls, playhead and selection marks are excluded. Serif/Mono annotation text uses Poppins in PDF; unsupported text glyphs report an error instead of silently disappearing.
344
395
 
345
- The dialog loads only when opened; the composer, `jspdf` and `svg2pdf.js` load on Save. Those libraries are installed runtime dependencies. The PDF implementation measured about +1.5 KB gzip for initial package JS+CSS; the larger PDF engine/font payload is deferred. PDF export is verified in Chromium and Firefox, including Firefox's corrected notation scale. Safari/WebKit PDF is not yet verified.
396
+ **Large scores.** Engraving runs in its own worker. Drawing needs a DOM, so it runs on the page, one page at a time and in slices of a few dozen elements, yielding to the browser between slices, so the page stays responsive and playback keeps running. On a quiet developer machine the longest main-thread block while exporting a 30-part score (one system per page) was about 200 ms in Chromium and Firefox and about 240 ms in WebKit; export time is dominated by engraving, which takes minutes for hundreds of bars (it is not faster, only non-blocking). Cancel (or Escape) is checked at every slice, so it takes effect within one slice, tens of milliseconds, including while the engraving worker is starting; it terminates the worker and removes the temporary fonts and offscreen page the export added. These figures are from the package's validation fixture, not a guarantee on slow devices.
397
+
398
+ **Browsers.** Chromium, Firefox and Safari/WebKit are covered by the package's validation. If the dialog's lazy chunk fails to download (for example after a network drop), the loader retries once through a cache-busted address of the same chunk, so reopening the dialog or using **Reload page** recovers. This is supported on Vite and native-ESM hosts, where the chunk keeps its name and the retry is a plain `import()` (WebKit otherwise answers every later attempt from the failed `modulepreload` Vite adds, for the life of the tab). On other bundlers such as Webpack, recovery is best effort: the retry is skipped when the chunk's name cannot be found, and the dialog shows its usual error with a **Reload page** button.
399
+
400
+ The dialog loads only when opened; the composer, `jspdf` and `svg2pdf.js` load on Save. Those libraries are installed runtime dependencies. The PDF implementation measured about +1.5 KB gzip for initial package JS+CSS; the larger PDF engine/font payload is deferred.
346
401
 
347
402
  ### String TAB
348
403
 
@@ -40,45 +40,46 @@ async function u({ score: t, prefs: r, hiddenPartIds: a, channel: o, pitchColour
40
40
  }
41
41
  };
42
42
  }
43
- function d({ paper: e, onPaper: n, snapshot: i, revision: d, font: f, verovioBaseUrl: p, onClose: m }) {
44
- let [h, g] = l(4), [_, v] = l(!1), [y, b] = l("4"), x = () => {
45
- let e = Number(y), n = y.trim() && Number.isFinite(e) ? t(e) : h;
46
- return g(n), b(String(n)), n;
47
- }, [S, C] = l("Checking fit…"), [w, T] = l(""), E = c(null), D = c(null), O = c(null), k = c(i);
48
- k.current = i;
49
- let A = c(m);
50
- A.current = m;
51
- let j = p ? {
52
- module: new URL("verovio-module.mjs", new URL(p.replace(/\/?$/, "/"), document.baseURI)).href,
53
- toolkit: new URL("verovio.mjs", new URL(p.replace(/\/?$/, "/"), document.baseURI)).href
43
+ var d = (e) => e && e.pages ? `Building PDF… ${e.phase === "composing" ? "drawing" : "preparing"} page ${e.page} of ${e.pages}` : "Building PDF…";
44
+ function f({ paper: e, onPaper: n, snapshot: i, revision: f, font: p, verovioBaseUrl: m, onClose: h }) {
45
+ let [g, _] = l(4), [v, y] = l(!1), [b, x] = l("4"), S = () => {
46
+ let e = Number(b), n = b.trim() && Number.isFinite(e) ? t(e) : g;
47
+ return _(n), x(String(n)), n;
48
+ }, [C, w] = l("Checking fit…"), [T, E] = l(""), [D, O] = l(null), k = c(null), A = c(null), j = c(null), M = c(i);
49
+ M.current = i;
50
+ let N = c(h);
51
+ N.current = h;
52
+ let P = m ? {
53
+ module: new URL("verovio-module.mjs", new URL(m.replace(/\/?$/, "/"), document.baseURI)).href,
54
+ toolkit: new URL("verovio.mjs", new URL(m.replace(/\/?$/, "/"), document.baseURI)).href
54
55
  } : void 0;
55
56
  s(() => {
56
57
  let e = document.activeElement;
57
- E.current?.querySelector("select")?.focus({ preventScroll: !0 });
58
- let t = E.current?.closest(".pepperscore");
58
+ k.current?.querySelector("select")?.focus({ preventScroll: !0 });
59
+ let t = k.current?.closest(".pepperscore");
59
60
  return () => {
60
- D.current?.abort(), O.current?.abort();
61
+ A.current?.abort(), j.current?.abort();
61
62
  let n = document.activeElement;
62
63
  e?.isConnected && (n === document.body || n && t?.contains(n)) && e.focus({ preventScroll: !0 });
63
64
  };
64
65
  }, []);
65
- let M = (e) => {
66
+ let F = (e) => {
66
67
  if (e.key === "Escape") {
67
- e.preventDefault(), e.stopPropagation(), m();
68
+ e.preventDefault(), e.stopPropagation(), h();
68
69
  return;
69
70
  }
70
71
  if (e.key !== "Tab") return;
71
- let t = Array.from(E.current?.querySelectorAll("button:not(:disabled),select:not(:disabled),input:not(:disabled),a[href]") ?? []), n = t[0], r = t.at(-1);
72
+ let t = Array.from(k.current?.querySelectorAll("button:not(:disabled),select:not(:disabled),input:not(:disabled),a[href]") ?? []), n = t[0], r = t.at(-1);
72
73
  e.shiftKey && document.activeElement === n ? (e.preventDefault(), r?.focus()) : !e.shiftKey && document.activeElement === r && (e.preventDefault(), n?.focus());
73
74
  };
74
75
  s(() => {
75
- if (_) return;
76
+ if (v) return;
76
77
  let t = new AbortController();
77
- return D.current = t, C("Checking fit…"), (async () => {
78
+ return A.current = t, w("Checking fit…"), (async () => {
78
79
  try {
79
- let n = await u(k.current());
80
+ let n = await u(M.current());
80
81
  t.signal.throwIfAborted();
81
- let { engravePages: i } = await import("./engrave-DpVrifuu.js"), a = n.score.parts.filter((e) => !n.visiblePartIds || n.visiblePartIds.includes(e.id)), o = r(a, n.annotations ?? {}), s = [
82
+ let { engravePages: i } = await import("./engrave-DKNPmehA.js"), a = n.score.parts.filter((e) => !n.visiblePartIds || n.visiblePartIds.includes(e.id)), o = r(a, n.annotations ?? {}), s = [
82
83
  n.score.title,
83
84
  n.score.subtitle,
84
85
  n.score.composer,
@@ -94,45 +95,48 @@ function d({ paper: e, onPaper: n, snapshot: i, revision: d, font: f, verovioBas
94
95
  }
95
96
  }, {
96
97
  paper: e,
97
- maxBars: h,
98
- font: f,
99
- files: j,
98
+ maxBars: g,
99
+ font: p,
100
+ files: P,
100
101
  spacingStaff: o.spacingStaff,
101
102
  annotationRoom: o.laneHeight,
102
103
  headingMm: s.length ? 8 + s.length * 4 : 0
103
104
  }, t.signal);
104
105
  if (t.signal.aborted) return;
105
- let l = c.lines.filter((e) => e.count < h).length;
106
- C(`${l} ${l === 1 ? "line will" : "lines will"} have fewer than ${h} bars on ${e.toUpperCase()}.${c.shrunk ? " A tall system is reduced to fit the page." : ""}`);
106
+ let l = c.lines.filter((e) => e.count < g).length;
107
+ w(`${l} ${l === 1 ? "line will" : "lines will"} have fewer than ${g} bars on ${e.toUpperCase()}.${c.shrunk ? " A tall system is reduced to fit the page." : ""}`);
107
108
  } catch (e) {
108
- t.signal.aborted || C(`Fit check unavailable: ${e instanceof Error ? e.message : String(e)}. You can still Save.`);
109
+ t.signal.aborted || w(`Fit check unavailable: ${e instanceof Error ? e.message : String(e)}. You can still Save.`);
109
110
  }
110
111
  })(), () => t.abort();
111
112
  }, [
112
113
  e,
113
- h,
114
- f,
114
+ g,
115
115
  p,
116
- d,
117
- _
116
+ m,
117
+ f,
118
+ v
118
119
  ]);
119
- let N = async () => {
120
- let t = x();
121
- E.current?.querySelector(".pdf-cancel")?.focus({ preventScroll: !0 }), D.current?.abort();
120
+ let I = async () => {
121
+ let t = S();
122
+ k.current?.querySelector(".pdf-cancel")?.focus({ preventScroll: !0 }), A.current?.abort();
122
123
  let n = new AbortController();
123
- O.current = n, v(!0), T("");
124
+ j.current = n, y(!0), E(""), O(null);
124
125
  try {
125
126
  let r = window.frameElement;
126
127
  if (r?.hasAttribute("sandbox") && !r.sandbox.contains("allow-downloads")) throw Error("Downloads are blocked by this embed. Open the score outside the embed or enable downloads.");
127
- let i = await u(k.current());
128
+ let i = await u(M.current());
128
129
  n.signal.throwIfAborted();
129
- let { exportScore: a } = await import("./export-BFhuMW8H.js"), o = await a({
130
+ let { exportScore: a } = await import("./export-Dg2CkokP.js"), o = await a({
130
131
  ...i,
131
132
  options: {
132
133
  paper: e,
133
134
  maxBars: t,
134
- font: f,
135
- files: j
135
+ font: p,
136
+ files: P
137
+ },
138
+ onProgress: (e) => {
139
+ n.signal.aborted || O(e);
136
140
  }
137
141
  }, n.signal);
138
142
  n.signal.throwIfAborted();
@@ -143,26 +147,26 @@ function d({ paper: e, onPaper: n, snapshot: i, revision: d, font: f, verovioBas
143
147
  } finally {
144
148
  c.remove(), window.setTimeout(() => URL.revokeObjectURL(s), 6e4);
145
149
  }
146
- A.current();
150
+ N.current();
147
151
  } catch (e) {
148
- n.signal.aborted || T(e instanceof Error ? e.message : String(e));
152
+ n.signal.aborted || E(e instanceof Error ? e.message : String(e));
149
153
  } finally {
150
- n.signal.aborted || v(!1);
154
+ n.signal.aborted || y(!1);
151
155
  }
152
156
  };
153
157
  return /* @__PURE__ */ a("div", {
154
158
  className: "pdf-backdrop",
155
159
  onMouseDown: (e) => {
156
- e.target === e.currentTarget && m();
160
+ e.target === e.currentTarget && h();
157
161
  },
158
162
  children: /* @__PURE__ */ o("section", {
159
- ref: E,
163
+ ref: k,
160
164
  className: "pdf-panel",
161
165
  role: "dialog",
162
166
  "aria-modal": "true",
163
167
  "aria-label": "Export PDF",
164
- "aria-busy": _,
165
- onKeyDown: M,
168
+ "aria-busy": v,
169
+ onKeyDown: F,
166
170
  children: [
167
171
  /* @__PURE__ */ a("h2", { children: "Export PDF" }),
168
172
  /* @__PURE__ */ o("label", {
@@ -170,7 +174,7 @@ function d({ paper: e, onPaper: n, snapshot: i, revision: d, font: f, verovioBas
170
174
  children: ["Paper size", /* @__PURE__ */ o("select", {
171
175
  className: "pdf-paper",
172
176
  value: e,
173
- disabled: _,
177
+ disabled: v,
174
178
  onChange: (e) => n(e.target.value),
175
179
  children: [
176
180
  /* @__PURE__ */ a("option", {
@@ -199,21 +203,21 @@ function d({ paper: e, onPaper: n, snapshot: i, revision: d, font: f, verovioBas
199
203
  type: "number",
200
204
  min: 1,
201
205
  max: 8,
202
- value: y,
203
- disabled: _,
204
- onChange: (e) => b(e.target.value),
205
- onBlur: x
206
+ value: b,
207
+ disabled: v,
208
+ onChange: (e) => x(e.target.value),
209
+ onBlur: S
206
210
  })]
207
211
  }),
208
212
  /* @__PURE__ */ a("p", {
209
213
  className: "pdf-fit",
210
214
  role: "status",
211
- children: _ ? "Building PDF…" : S
215
+ children: v ? d(D) : C
212
216
  }),
213
- w && /* @__PURE__ */ a("p", {
217
+ T && /* @__PURE__ */ a("p", {
214
218
  className: "pdf-error",
215
219
  role: "alert",
216
- children: w
220
+ children: T
217
221
  }),
218
222
  /* @__PURE__ */ o("div", {
219
223
  className: "pdf-actions",
@@ -221,10 +225,10 @@ function d({ paper: e, onPaper: n, snapshot: i, revision: d, font: f, verovioBas
221
225
  /* @__PURE__ */ a("button", {
222
226
  type: "button",
223
227
  className: "pdf-cancel",
224
- onClick: m,
228
+ onClick: h,
225
229
  children: "Cancel"
226
230
  }),
227
- w && /* @__PURE__ */ a("button", {
231
+ T && /* @__PURE__ */ a("button", {
228
232
  type: "button",
229
233
  className: "pdf-reload",
230
234
  onClick: () => window.history.go(0),
@@ -233,8 +237,8 @@ function d({ paper: e, onPaper: n, snapshot: i, revision: d, font: f, verovioBas
233
237
  /* @__PURE__ */ a("button", {
234
238
  type: "button",
235
239
  className: "pdf-save",
236
- disabled: _,
237
- onClick: () => void N(),
240
+ disabled: v,
241
+ onClick: () => void I(),
238
242
  children: "Save"
239
243
  })
240
244
  ]
@@ -244,6 +248,6 @@ function d({ paper: e, onPaper: n, snapshot: i, revision: d, font: f, verovioBas
244
248
  });
245
249
  }
246
250
  //#endregion
247
- export { d as default };
251
+ export { f as default };
248
252
 
249
- //# sourceMappingURL=PdfOverlay-BmBGsMkr.js.map
253
+ //# sourceMappingURL=PdfOverlay-DUgY9mW-.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PdfOverlay-DUgY9mW-.js","names":["verovioBaseline","clampBarsPerLine","planStaves","ANNOTATION_SCALE","prepareSnapshot","saved","score","hidden","hiddenPartIds","info","channel","planned","part","buildingText","progress","PdfOverlay","bars","setBars","useState","busy","setBusy","barsDraft","setBarsDraft","commitBars","number","next","fit","setFit","error","setError","setProgress","panel","useRef","measure","save","snapshotRef","snapshot","closeRef","onClose","files","verovioBaseUrl","previous","owner","active","onKeyDown","event","items","first","last","controller","request","visible","layout","annotationLayout","metadata","result","engravePages","reduced","line","paper","failure","font","revision","download","maxBars","frame","blob","exportScore","update","url","anchor","onPaper"],"sources":["../src/components/PdfOverlay.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport type { ExportProgress, ExportRequest } from '../lib/pdf/export';\nimport type { Score } from '../lib/score';\nimport type { PitchColourPaint } from '../lib/pitch-colours';\nimport { verovioBaseline } from '../lib/verovio-runtime';\nimport { annotationLayout, planStaves } from '../lib/render';\nimport { ANNOTATION_SCALE, BARS_PER_LINE_MAX, clampBarsPerLine, type Prefs } from '../lib/prefs';\n\ntype Snapshot = Omit<ExportRequest, 'options'>;\nexport type PdfCapture = { score: Score; prefs: Prefs; hiddenPartIds: string[]; channel: number; pitchColours?: PitchColourPaint };\nasync function prepareSnapshot({ score, prefs: saved, hiddenPartIds, channel, pitchColours }: PdfCapture): Promise<Snapshot> {\n if (!score.document) throw new Error('This score has no engraving document.');\n const hidden = new Set(hiddenPartIds);\n const info = await verovioBaseline(score.document, channel);\n const planned = planStaves(info, score, hidden, { view: saved.tabView, tuning: saved.tabTuning, capo: saved.capo });\n return { score, input: { data: score.document.engravingText, view: { breakEvery: 0, hiddenStaves: planned.hiddenStaves,\n spelling: planned.spelling, chordSymbols: saved.chordSymbols, ...(planned.tabPlan.spec ? { tab: planned.tabPlan.spec } : {}) } },\n visiblePartIds: planned.visibleParts.map(part => part.id), tabAliases: planned.tabAlias, tabStaves: planned.tabStaves, pitchColours,\n annotations: { showNames: saved.names, showCounts: saved.counts, subdivisions: saved.subdivisions,\n showChordSymbols: saved.chordSymbols, inferChordSymbols: saved.inferChordSymbols,\n scale: { names: ANNOTATION_SCALE[saved.namesSize], chordSymbols: ANNOTATION_SCALE[saved.chordSymbolsSize], counts: ANNOTATION_SCALE[saved.countsSize], subdivisions: ANNOTATION_SCALE[saved.subdivisionsSize] } } };\n}\n/** Engraving runs in a worker, then each page is drawn in turn: say which. */\nconst buildingText = (progress: ExportProgress | null) => progress && progress.pages ? `Building PDF… ${progress.phase === 'composing' ? 'drawing' : 'preparing'} page ${progress.page} of ${progress.pages}` : 'Building PDF…';\nexport default function PdfOverlay({ paper, onPaper, snapshot, revision, font, verovioBaseUrl, onClose }: {\n paper: Prefs['pdfPaper']; onPaper: (paper: Prefs['pdfPaper']) => void;\n snapshot: () => PdfCapture; revision: unknown; font: string; verovioBaseUrl?: string; onClose: () => void;\n}) {\n const [bars, setBars] = useState(4), [busy, setBusy] = useState(false);\n const [barsDraft, setBarsDraft] = useState('4');\n const commitBars = () => {\n const number = Number(barsDraft);\n const next = barsDraft.trim() && Number.isFinite(number) ? clampBarsPerLine(number) : bars;\n setBars(next); setBarsDraft(String(next));\n return next;\n };\n const [fit, setFit] = useState('Checking fit…'), [error, setError] = useState(''), [progress, setProgress] = useState<ExportProgress | null>(null);\n const panel = useRef<HTMLElement>(null), measure = useRef<AbortController | null>(null), save = useRef<AbortController | null>(null);\n const snapshotRef = useRef(snapshot); snapshotRef.current = snapshot;\n const closeRef = useRef(onClose); closeRef.current = onClose;\n const files = verovioBaseUrl ? { module: new URL('verovio-module.mjs', new URL(verovioBaseUrl.replace(/\\/?$/, '/'), document.baseURI)).href,\n toolkit: new URL('verovio.mjs', new URL(verovioBaseUrl.replace(/\\/?$/, '/'), document.baseURI)).href } : undefined;\n useEffect(() => {\n const previous = document.activeElement as HTMLElement | null;\n panel.current?.querySelector<HTMLElement>('select')?.focus({ preventScroll: true });\n const owner = panel.current?.closest('.pepperscore');\n return () => {\n measure.current?.abort(); save.current?.abort();\n const active = document.activeElement;\n if (previous?.isConnected && (active === document.body || (active && owner?.contains(active)))) previous.focus({ preventScroll: true });\n };\n }, []);\n const onKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); onClose(); return; }\n if (event.key !== 'Tab') return;\n const items = Array.from(panel.current?.querySelectorAll<HTMLElement>('button:not(:disabled),select:not(:disabled),input:not(:disabled),a[href]') ?? []);\n const first = items[0], last = items.at(-1);\n if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }\n else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }\n };\n useEffect(() => {\n if (busy) return;\n const controller = new AbortController(); measure.current = controller;\n setFit('Checking fit…');\n void (async () => {\n try {\n const request = await prepareSnapshot(snapshotRef.current());\n controller.signal.throwIfAborted();\n const { engravePages } = await import('../lib/pdf/engrave');\n const visible = request.score.parts.filter(part => !request.visiblePartIds || request.visiblePartIds.includes(part.id));\n const layout = annotationLayout(visible, request.annotations ?? {});\n const metadata = [request.score.title, request.score.subtitle, request.score.composer, ...(request.score.credits ?? [])].filter(Boolean);\n const result = await engravePages({ ...request.input, view: { breakEvery: 0, hiddenStaves: [], chordSymbols: false, ...request.input.view, harmSize: layout.harmSize } },\n { paper, maxBars: bars, font, files, spacingStaff: layout.spacingStaff, annotationRoom: layout.laneHeight, headingMm: metadata.length ? 8 + metadata.length * 4 : 0 }, controller.signal);\n if (controller.signal.aborted) return;\n const reduced = result.lines.filter(line => line.count < bars).length;\n setFit(`${reduced} ${reduced === 1 ? 'line will' : 'lines will'} have fewer than ${bars} bars on ${paper.toUpperCase()}.${result.shrunk ? ' A tall system is reduced to fit the page.' : ''}`);\n } catch (failure) { if (!controller.signal.aborted) setFit(`Fit check unavailable: ${failure instanceof Error ? failure.message : String(failure)}. You can still Save.`); }\n })();\n return () => controller.abort();\n }, [paper, bars, font, verovioBaseUrl, revision, busy]);\n const download = async () => {\n const maxBars = commitBars();\n panel.current?.querySelector<HTMLButtonElement>('.pdf-cancel')?.focus({ preventScroll: true });\n measure.current?.abort();\n const controller = new AbortController(); save.current = controller;\n setBusy(true); setError(''); setProgress(null);\n try {\n // frameElement belongs to the parent's realm; instanceof against this window's\n // HTMLIFrameElement is false even for a same-origin embed.\n const frame = window.frameElement as HTMLIFrameElement | null;\n if (frame?.hasAttribute('sandbox') && !frame.sandbox.contains('allow-downloads')) throw new Error('Downloads are blocked by this embed. Open the score outside the embed or enable downloads.');\n const request = await prepareSnapshot(snapshotRef.current());\n controller.signal.throwIfAborted();\n const { exportScore } = await import('../lib/pdf/export');\n const blob = await exportScore({ ...request, options: { paper, maxBars, font, files }, onProgress: update => { if (!controller.signal.aborted) setProgress(update); } }, controller.signal);\n controller.signal.throwIfAborted();\n const url = URL.createObjectURL(blob), anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = `${request.score.title?.normalize('NFKD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'score'}.pdf`;\n document.body.append(anchor);\n try { anchor.click(); } finally { anchor.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 60000); }\n closeRef.current();\n } catch (failure) { if (!controller.signal.aborted) setError(failure instanceof Error ? failure.message : String(failure)); }\n finally { if (!controller.signal.aborted) setBusy(false); }\n };\n return <div className=\"pdf-backdrop\" onMouseDown={event => { if (event.target === event.currentTarget) onClose(); }}>\n <section ref={panel} className=\"pdf-panel\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Export PDF\" aria-busy={busy} onKeyDown={onKeyDown}>\n <h2>Export PDF</h2>\n <label className=\"pdf-field\">Paper size<select className=\"pdf-paper\" value={paper} disabled={busy} onChange={event => onPaper(event.target.value as Prefs['pdfPaper'])}>\n <option value=\"a4\">A4</option><option value=\"a3\">A3</option><option value=\"letter\">Letter</option><option value=\"legal\">Legal</option>\n </select></label>\n <label className=\"pdf-field\">Bars per line<input className=\"pdf-bars\" type=\"number\" min={1} max={BARS_PER_LINE_MAX} value={barsDraft} disabled={busy} onChange={event => setBarsDraft(event.target.value)} onBlur={commitBars}/></label>\n <p className=\"pdf-fit\" role=\"status\">{busy ? buildingText(progress) : fit}</p>\n {error && <p className=\"pdf-error\" role=\"alert\">{error}</p>}\n <div className=\"pdf-actions\"><button type=\"button\" className=\"pdf-cancel\" onClick={onClose}>Cancel</button>{error && <button type=\"button\" className=\"pdf-reload\" onClick={() => window.history.go(0)}>Reload page</button>}<button type=\"button\" className=\"pdf-save\" disabled={busy} onClick={() => void download()}>Save</button></div>\n </section>\n </div>;\n}\n"],"mappings":";;;;AAUA,eAAeI,EAAgB,EAAE,UAAO,OAAOC,GAAO,kBAAe,YAAS,mBAA+C;CAC3H,IAAI,CAACC,EAAM,UAAU,MAAU,MAAM,uCAAuC;CAC5E,IAAMC,IAAS,IAAI,IAAIC,CAAa,GAC9BC,IAAO,MAAMT,EAAgBM,EAAM,UAAUI,CAAO,GACpDC,IAAUT,EAAWO,GAAMH,GAAOC,GAAQ;EAAE,MAAMF,EAAM;EAAS,QAAQA,EAAM;EAAW,MAAMA,EAAM;CAAK,CAAC;CAClH,OAAO;EAAE;EAAO,OAAO;GAAE,MAAMC,EAAM,SAAS;GAAe,MAAM;IAAE,YAAY;IAAG,cAAcK,EAAQ;IACxG,UAAUA,EAAQ;IAAU,cAAcN,EAAM;IAAc,GAAIM,EAAQ,QAAQ,OAAO,EAAE,KAAKA,EAAQ,QAAQ,KAAK,IAAI,CAAC;GAAG;EAAE;EAC/H,gBAAgBA,EAAQ,aAAa,KAAI,MAAQC,EAAK,EAAE;EAAG,YAAYD,EAAQ;EAAU,WAAWA,EAAQ;EAAW;EACvH,aAAa;GAAE,WAAWN,EAAM;GAAO,YAAYA,EAAM;GAAQ,cAAcA,EAAM;GACnF,kBAAkBA,EAAM;GAAc,mBAAmBA,EAAM;GAC/D,OAAO;IAAE,OAAOF,EAAiBE,EAAM;IAAY,cAAcF,EAAiBE,EAAM;IAAmB,QAAQF,EAAiBE,EAAM;IAAa,cAAcF,EAAiBE,EAAM;GAAkB;EAAE;CAAE;AACxN;AAEA,IAAMQ,KAAgB,MAAoCC,KAAYA,EAAS,QAAQ,iBAAiBA,EAAS,UAAU,cAAc,YAAY,YAAY,QAAQA,EAAS,KAAK,MAAMA,EAAS,UAAU;AAChN,SAAwBC,EAAW,EAAE,UAAO,YAAS,aAAU,aAAU,SAAM,mBAAgB,cAG5F;CACD,IAAM,CAACC,GAAMC,KAAWC,EAAS,CAAC,GAAG,CAACC,GAAMC,KAAWF,EAAS,EAAK,GAC/D,CAACG,GAAWC,KAAgBJ,EAAS,GAAG,GACxCK,UAAmB;EACvB,IAAMC,IAAS,OAAOH,CAAS,GACzBI,IAAOJ,EAAU,KAAK,KAAK,OAAO,SAASG,CAAM,IAAIvB,EAAiBuB,CAAM,IAAIR;EAEtF,OADAC,EAAQQ,CAAI,GAAGH,EAAa,OAAOG,CAAI,CAAC,GACjCA;CACT,GACM,CAACC,GAAKC,KAAUT,EAAS,eAAe,GAAG,CAACU,GAAOC,KAAYX,EAAS,EAAE,GAAG,CAACJ,GAAUgB,KAAeZ,EAAgC,IAAI,GAC3Ia,IAAQC,EAAoB,IAAI,GAAGC,IAAUD,EAA+B,IAAI,GAAGE,IAAOF,EAA+B,IAAI,GAC7HG,IAAcH,EAAOI,CAAQ;CAAG,EAAY,UAAUA;CAC5D,IAAMC,IAAWL,EAAOM,CAAO;CAAG,EAAS,UAAUA;CACrD,IAAMC,IAAQC,IAAiB;EAAE,QAAQ,IAAI,IAAI,sBAAsB,IAAI,IAAIA,EAAe,QAAQ,QAAQ,GAAG,GAAG,SAAS,OAAO,CAAC,CAAC,CAAC;EACrI,SAAS,IAAI,IAAI,eAAe,IAAI,IAAIA,EAAe,QAAQ,QAAQ,GAAG,GAAG,SAAS,OAAO,CAAC,CAAC,CAAC;CAAK,IAAI,KAAA;CAC3G,QAAgB;EACd,IAAMC,IAAW,SAAS;EAC1B,EAAM,SAAS,cAA2B,QAAQ,CAAC,EAAE,MAAM,EAAE,eAAe,GAAK,CAAC;EAClF,IAAMC,IAAQX,EAAM,SAAS,QAAQ,cAAc;EACnD,aAAa;GACe,AAA1BE,EAAQ,SAAS,MAAM,GAAGC,EAAK,SAAS,MAAM;GAC9C,IAAMS,IAAS,SAAS;GACxB,AAAIF,GAAU,gBAAgBE,MAAW,SAAS,QAASA,KAAUD,GAAO,SAASC,CAAM,MAAKF,EAAS,MAAM,EAAE,eAAe,GAAK,CAAC;EACxI;CACF,GAAG,CAAC,CAAC;CACL,IAAMG,KAAa,MAA4C;EAC7D,IAAIC,EAAM,QAAQ,UAAU;GAAmD,AAAjDA,EAAM,eAAe,GAAGA,EAAM,gBAAgB,GAAGP,EAAQ;GAAG;EAAQ;EAClG,IAAIO,EAAM,QAAQ,OAAO;EACzB,IAAMC,IAAQ,MAAM,KAAKf,EAAM,SAAS,iBAA8B,0EAA0E,KAAK,CAAC,CAAC,GACjJgB,IAAQD,EAAM,IAAIE,IAAOF,EAAM,GAAG,EAAE;EAC1C,AAAID,EAAM,YAAY,SAAS,kBAAkBE,KAASF,EAAM,eAAe,GAAGG,GAAM,MAAM,KACrF,CAACH,EAAM,YAAY,SAAS,kBAAkBG,MAAQH,EAAM,eAAe,GAAGE,GAAO,MAAM;CACtG;CACA,QAAgB;EACd,IAAI5B,GAAM;EACV,IAAM8B,IAAa,IAAI,gBAAgB;EAiBvC,OAjB0C,EAAQ,UAAUA,GAC5DtB,EAAO,eAAe,IAChB,YAAY;GAChB,IAAI;IACF,IAAMuB,IAAU,MAAM9C,EAAgB+B,EAAY,QAAQ,CAAC;IAC3D,EAAW,OAAO,eAAe;IACjC,IAAM,EAAE,oBAAiB,MAAM,OAAO,0BAChCgB,IAAUD,EAAQ,MAAM,MAAM,QAAO,MAAQ,CAACA,EAAQ,kBAAkBA,EAAQ,eAAe,SAAStC,EAAK,EAAE,CAAC,GAChHwC,IAASC,EAAiBF,GAASD,EAAQ,eAAe,CAAC,CAAC,GAC5DI,IAAW;KAACJ,EAAQ,MAAM;KAAOA,EAAQ,MAAM;KAAUA,EAAQ,MAAM;KAAU,GAAIA,EAAQ,MAAM,WAAW,CAAC;IAAE,CAAC,CAAC,OAAO,OAAO,GACjIK,IAAS,MAAMC,EAAa;KAAE,GAAGN,EAAQ;KAAO,MAAM;MAAE,YAAY;MAAG,cAAc,CAAC;MAAG,cAAc;MAAO,GAAGA,EAAQ,MAAM;MAAM,UAAUE,EAAO;KAAS;IAAE,GACrK;KAAE;KAAO,SAASpC;KAAM;KAAM;KAAO,cAAcoC,EAAO;KAAc,gBAAgBA,EAAO;KAAY,WAAWE,EAAS,SAAS,IAAIA,EAAS,SAAS,IAAI;IAAE,GAAGL,EAAW,MAAM;IAC1L,IAAIA,EAAW,OAAO,SAAS;IAC/B,IAAMQ,IAAUF,EAAO,MAAM,QAAO,MAAQG,EAAK,QAAQ1C,CAAI,CAAC,CAAC;IAC/D,EAAO,GAAGyC,EAAQ,GAAGA,MAAY,IAAI,cAAc,aAAa,mBAAmBzC,EAAK,WAAW2C,EAAM,YAAY,EAAE,GAAGJ,EAAO,SAAS,+CAA+C,IAAI;GAC/L,SAASK,GAAS;IAAE,AAAKX,EAAW,OAAO,WAAStB,EAAO,0BAA0BiC,aAAmB,QAAQA,EAAQ,UAAU,OAAOA,CAAO,EAAE,sBAAsB;GAAG;EAC7K,EAAA,CAAG,SACUX,EAAW,MAAM;CAChC,GAAG;EAACU;EAAO3C;EAAM6C;EAAMrB;EAAgBsB;EAAU3C;CAAI,CAAC;CACtD,IAAM4C,IAAW,YAAY;EAC3B,IAAMC,IAAUzC,EAAW;EAE3B,AADAQ,EAAM,SAAS,cAAiC,aAAa,CAAC,EAAE,MAAM,EAAE,eAAe,GAAK,CAAC,GAC7FE,EAAQ,SAAS,MAAM;EACvB,IAAMgB,IAAa,IAAI,gBAAgB;EACV,AADa,EAAK,UAAUA,GACzD7B,EAAQ,EAAI,GAAGS,EAAS,EAAE,GAAGC,EAAY,IAAI;EAC7C,IAAI;GAGF,IAAMmC,IAAQ,OAAO;GACrB,IAAIA,GAAO,aAAa,SAAS,KAAK,CAACA,EAAM,QAAQ,SAAS,iBAAiB,GAAG,MAAU,MAAM,4FAA4F;GAC9L,IAAMf,IAAU,MAAM9C,EAAgB+B,EAAY,QAAQ,CAAC;GAC3D,EAAW,OAAO,eAAe;GACjC,IAAM,EAAE,mBAAgB,MAAM,OAAO,yBAC/B+B,IAAO,MAAMC,EAAY;IAAE,GAAGjB;IAAS,SAAS;KAAE;KAAO;KAAS;KAAM;IAAM;IAAG,aAAY,MAAU;KAAE,AAAKD,EAAW,OAAO,WAASnB,EAAYsC,CAAM;IAAG;GAAE,GAAGnB,EAAW,MAAM;GAC1L,EAAW,OAAO,eAAe;GACjC,IAAMoB,IAAM,IAAI,gBAAgBH,CAAI,GAAGI,IAAS,SAAS,cAAc,GAAG;GAG1E,AAFA,EAAO,OAAOD,GACd,EAAO,WAAW,GAAGnB,EAAQ,MAAM,OAAO,UAAU,MAAM,CAAC,CAAC,QAAQ,oBAAoB,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,QAAQ,eAAe,GAAG,CAAC,CAAC,QAAQ,UAAU,EAAE,KAAK,QAAQ,OACvK,SAAS,KAAK,OAAOoB,CAAM;GAC3B,IAAI;IAAE,EAAO,MAAM;GAAG,UAAU;IAAmB,AAAjBA,EAAO,OAAO,GAAG,OAAO,iBAAiB,IAAI,gBAAgBD,CAAG,GAAG,GAAK;GAAG;GAC7G,EAAS,QAAQ;EACnB,SAAST,GAAS;GAAE,AAAKX,EAAW,OAAO,WAASpB,EAAS+B,aAAmB,QAAQA,EAAQ,UAAU,OAAOA,CAAO,CAAC;EAAG,UACpH;GAAE,AAAKX,EAAW,OAAO,WAAS7B,EAAQ,EAAK;EAAG;CAC5D;CACA,OAAO,kBAAC,OAAD;EAAK,WAAU;EAAe,cAAa,MAAS;GAAE,AAAIyB,EAAM,WAAWA,EAAM,iBAAeP,EAAQ;EAAG;EAChH,UAAA,kBAAC,WAAD;GAAS,KAAKP;GAAO,WAAU;GAAY,MAAK;GAAS,cAAW;GAAO,cAAW;GAAa,aAAWZ;GAAiB;GAA/H,UAAA;IACE,kBAAC,MAAD,EAAA,UAAI,aAAc,CAAA;IAClB,kBAAC,SAAD;KAAO,WAAU;KAAjB,UAAA,CAA6B,cAAU,kBAAC,UAAD;MAAQ,WAAU;MAAY,OAAOwC;MAAO,UAAUxC;MAAM,WAAU,MAASoD,EAAQ1B,EAAM,OAAO,KAA0B;MAA9H,UAAA;OACrC,kBAAC,UAAD;QAAQ,OAAM;QAAK,UAAA;OAAU,CAAA;OAAC,kBAAC,UAAD;QAAQ,OAAM;QAAK,UAAA;OAAU,CAAA;OAAC,kBAAC,UAAD;QAAQ,OAAM;QAAS,UAAA;OAAc,CAAA;OAAC,kBAAC,UAAD;QAAQ,OAAM;QAAQ,UAAA;OAAa,CAAA;MAC/H;KAAQ,CAAA,CAAA;;IAChB,kBAAC,SAAD;KAAO,WAAU;KAAjB,UAAA,CAA6B,iBAAa,kBAAC,SAAD;MAAO,WAAU;MAAW,MAAK;MAAS,KAAK;MAAG,KAAA;MAAwB,OAAOxB;MAAW,UAAUF;MAAM,WAAU,MAASG,EAAauB,EAAM,OAAO,KAAK;MAAG,QAAQtB;KAAY,CAAA,CAAQ;;IACvO,kBAAC,KAAD;KAAG,WAAU;KAAU,MAAK;KAAU,UAAAJ,IAAON,EAAaC,CAAQ,IAAIY;IAAO,CAAA;IAC5EE,KAAS,kBAAC,KAAD;KAAG,WAAU;KAAY,MAAK;KAAS,UAAAA;IAAS,CAAA;IAC1D,kBAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MAA6B,kBAAC,UAAD;OAAQ,MAAK;OAAS,WAAU;OAAa,SAASU;OAAS,UAAA;MAAc,CAAA;MAAEV,KAAS,kBAAC,UAAD;OAAQ,MAAK;OAAS,WAAU;OAAa,eAAe,OAAO,QAAQ,GAAG,CAAC;OAAG,UAAA;MAAmB,CAAA;MAAE,kBAAC,UAAD;OAAQ,MAAK;OAAS,WAAU;OAAW,UAAUT;OAAM,eAAe,KAAK4C,EAAS;OAAG,UAAA;MAAY,CAAA;KAAM;;GAClU;;CACN,CAAA;AACP"}