pepperscore 0.4.1 → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +245 -10
  3. package/dist/PdfOverlay-DUgY9mW-.js +253 -0
  4. package/dist/PdfOverlay-DUgY9mW-.js.map +1 -0
  5. package/dist/THIRD_PARTY_NOTICES.txt +1234 -1
  6. package/dist/assets/engrave.worker-B0WfvX6e.js +5 -0
  7. package/dist/assets/{import.worker-Rop-z7az.js → import.worker-BAr_ZqtQ.js} +2 -2
  8. package/dist/assets/verovio.worker-cxTTfdKx.js +7 -0
  9. package/dist/components/DrawerControls.d.ts +7 -0
  10. package/dist/components/PdfOverlay.d.ts +19 -0
  11. package/dist/components/PepperScore.d.ts +6 -1
  12. package/dist/components/PitchColourTile.d.ts +4 -0
  13. package/dist/components/SkinBar.d.ts +12 -0
  14. package/dist/engrave-DKNPmehA.js +32 -0
  15. package/dist/engrave-DKNPmehA.js.map +1 -0
  16. package/dist/export-Dg2CkokP.js +392 -0
  17. package/dist/export-Dg2CkokP.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.js +3563 -2588
  20. package/dist/index.js.map +1 -1
  21. package/dist/lib/bar-items.d.ts +31 -0
  22. package/dist/lib/chrome.d.ts +6 -4
  23. package/dist/lib/file-state.d.ts +1 -0
  24. package/dist/lib/header-controls.d.ts +2 -2
  25. package/dist/lib/lazy-import.d.ts +19 -0
  26. package/dist/lib/notation/mei-tab.d.ts +8 -1
  27. package/dist/lib/notation/mei-view.d.ts +2 -0
  28. package/dist/lib/notation/musicxml-octave-clef.d.ts +5 -0
  29. package/dist/lib/pdf/annotations.d.ts +16 -0
  30. package/dist/lib/pdf/compose.d.ts +11 -0
  31. package/dist/lib/pdf/cooperative.d.ts +23 -0
  32. package/dist/lib/pdf/engrave.d.ts +29 -0
  33. package/dist/lib/pdf/export.d.ts +22 -0
  34. package/dist/lib/pdf/fit.d.ts +2 -0
  35. package/dist/lib/pdf/fonts.d.ts +3 -0
  36. package/dist/lib/pdf/normalize.d.ts +9 -0
  37. package/dist/lib/pdf/paper.d.ts +32 -0
  38. package/dist/lib/pdf/slices.d.ts +14 -0
  39. package/dist/lib/pitch-colours.d.ts +16 -0
  40. package/dist/lib/prefs.d.ts +7 -2
  41. package/dist/lib/render.d.ts +52 -0
  42. package/dist/lib/score.d.ts +6 -0
  43. package/dist/lib/skin.d.ts +25 -0
  44. package/dist/lib/tab/string-instruments.d.ts +65 -0
  45. package/dist/lib/tab/strings/charts.d.ts +29 -0
  46. package/dist/lib/tab/strings/phrase.d.ts +16 -0
  47. package/dist/lib/tab/strings/solver.d.ts +29 -0
  48. package/dist/lib/tab/strings/svg.d.ts +3 -0
  49. package/dist/lib/tab/tab-parts.d.ts +2 -5
  50. package/dist/style.css +1 -1
  51. package/package.json +5 -2
  52. package/dist/assets/verovio.worker-DBJku5Ng.js +0 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
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
+
15
+ ## 0.5.0 - 2026-11-11
16
+
17
+ ### Added
18
+
19
+ - `skin` prop (`PepperScoreSkin`): theme colours by interface role, an ordered `bar` of built-in controls, and an optional complete 12-pitch palette. Exports `mergeSkins`, `CRF_PITCH_COLOURS`, `ThemeToken`, `BarItemId`, `PitchClass` and `PitchPalette`. Theme values can reference host CSS variables; changing colours does not re-engrave the score. Without a skin, existing colours and header defaults are retained.
20
+ - Composable bars in full and minimal chrome, including note-name, beat-count and chord-symbol toggles and the instrument switcher. Explicit `controls` values of `false` still hide their controls. Annotation toggles stay in sync with the drawer and saved preferences; the chord-symbol tile switches authored and inferred symbols together.
21
+ - A Playback and view drawer panel for stop, view mode, bars per line, zoom, mixer and file opening when those actions are omitted from the header. On phones, full chrome's playback controls move to a wrapping bottom toolbar.
22
+ - Pitch-coloured noteheads from `skin.pitchColours`, enabled with `settings.pitchColours`, the bar/drawer tile or `handle.setPitchColours(on)`. Off by default, saved per score, and based on displayed pitch after transposition; enharmonic equivalents share a colour. Playback, selection and range marks retain priority. Colour and palette changes do not re-engrave.
23
+ - Generated fret-number TAB for guitar, four-/five-string bass guitar and soprano, tenor, baritone and bass ukulele, with instrument-specific tunings and re-entrant string order. `settings.tabInstruments` maps exact `onReady` part ids to a TAB identity (for example `bass5`) or `'none'`; it changes TAB identity without changing sound. A reader instrument switch replaces that initial override. Unqualified bass uses four strings; unqualified ukulele uses soprano GCEA.
24
+ - Violin and cello get four-line finger + position TAB in TAB/Both views: finger numbers on the string lines, inline L/H for low/high finger placement, and position marks such as `I pos.` and `pos. ½`. Cello positions follow Davidoff. Finger numbers are display only; playback uses the source pitches. Finger TAB uses instrument charts, ignoring fretted tuning/capo settings. Viola and double bass still get no generated TAB.
25
+ - ABC authoring rules and copyable templates in the package README, covering voice/header order, clefs, transposition, MIDI directives, grouping and current limitations.
26
+ - PDF export: a secondary header button in `full` (icon-only at reader widths 761–900px; secondary row/More menu on narrow screens), a drawer action and skin bar item `pdf`. `controls.pdf` enables/disables it (resolved as `chrome.pdf`; on for `full`, off otherwise). The dialog offers A4/A3/Letter/Legal, remembered `pdfPaper`, bars per line defaulting to 4, “Checking fit…”, Save and Cancel. Save downloads `<title>.pdf` (a normalized title, or `score.pdf`), with vector notation, embedded fonts, annotations and active pitch colours from the current view. The overlay and PDF engine are lazy-loaded; the implementation measured approximately +1.5 KB gzip initial package JS+CSS. `jspdf` and `svg2pdf.js` remain runtime dependencies, imported dynamically on Save. PDF export is verified in Chromium and Firefox (including corrected Firefox notation scale); Safari/WebKit PDF is not yet verified.
27
+
28
+ ### Changed
29
+
30
+ - Instrument names without an explicit MIDI program now distinguish voices from instruments: Alto/Tenor labels (including numbering and divisi) are voices unless an instrument noun such as Sax, Flute or Trombone is present. Choir labels include solo, plural, Mezzo-Soprano, Contralto, Countertenor and split slash/ampersand staves. Treble needs a vocal hint or another named choir range; Treble/Bass alone stays piano plus bass guitar. Three distinct S/A/T/B letters establish choir context when no guitar/drum evidence is present. Explicit MIDI programs retain precedence.
31
+ - Name-only ukulele parts use nylon-guitar sound. Horn names distinguish English Horn/Cor Anglais, Basset Horn and Flugelhorn/Flügelhorn from French Horn; soprano sax, recorder and banjo names select their corresponding GM sounds.
32
+
33
+ ### Fixed
34
+
35
+ - ABC accidentals follow bar state and ties across barlines; E♯/C♭ and other source spellings survive engraving. Key changes, including mid-bar changes, draw at their onset once per staff, including when layer 1 has no onset there.
36
+ - ABC positioned text (`"^Swing"`, `"_rit."`) stays separate from chord symbols; final and double barlines, alto/tenor clefs and octave clefs are preserved.
37
+ - ABC `%%score`/`%%staves` grouping produces grand staves. Per-voice `transpose=` and `%%MIDI transpose|program|channel` affect sounding pitch and sound; channel 10 or a drum name selects percussion. Only the first tune is imported. Directives work with body-only voice declarations, a file-header `K:` no longer ends the tune header, and `V:` defaults before `X:` keep their clef/name/transposition while preserving grouping.
38
+ - Octave-clef ABC and MusicXML now agree on staff position, playback and sounding-pitch TAB. MusicXML written-octave notes with a matching clef/transpose octave are normalized in the import copy while preserving original source bytes and residual instrument transposition. Switching to guitar retains the octave clef's 8.
39
+
40
+ ### Behaviour changes
41
+
42
+ - Bare **Bass** now means bass guitar with four-string TAB, except in choir context without guitar/drums or with an explicit vocal label/program. Explicit bass-guitar names remain instrumental; Double Bass/Contrabass/Upright/String Bass remain double bass. **Bass Solo** and **Basses** are voices even in a band: supply a MIDI program for an instrumental bass solo.
43
+ - **Alto 1** / **Tenor II** now mean voices; include **Sax** for saxophones. Instrument nouns and explicit MIDI programs take precedence over voice-range words.
44
+ - Generated TAB is restricted to supported string parts: the first-pitched-part fallback is removed, so piano and other non-string parts no longer acquire guitar TAB. Authored imported TAB remains available. Violin/cello use finger + position TAB; viola/double-bass TAB remains unavailable.
45
+ - `tabTuning` (including saved tunings) applies only to generated fretted parts with the same string count; other parts keep their instrument defaults. A six-string tuning no longer replaces bass/ukulele defaults. String order is physical, not sorted by pitch.
46
+ - Octave-clef ABC letters are stored at **sounding pitch**: `G,` with `clef=treble-8` sounds G3, draws as G4 and generates sounding-pitch TAB. Do not add `transpose=-12` to compensate for the clef; an explicit instrument transpose still applies independently.
47
+
3
48
  ## 0.4.1 - 2026-10-30
4
49
 
5
50
  ### Added
package/README.md CHANGED
@@ -195,12 +195,13 @@ export default defineConfig({
195
195
  | `scoreUrl` | `string` | | A score to download. Relative URLs resolve against the page. Cross-origin URLs need CORS. |
196
196
  | `abc` | `string` | | ABC notation. |
197
197
  | `chrome` | `'full' \| 'minimal' \| 'none'` | `'full'` | See the chrome table below. |
198
- | `controls` | `{ transport?, viewToggle?, openFile?, mixer?, drawer?, zoom?, tabToggle?, instrumentSwitch?, octaveTool? }` (booleans) | from `chrome` | Per-control overrides of the preset, see below. |
198
+ | `controls` | `{ transport?, viewToggle?, openFile?, mixer?, drawer?, zoom?, tabToggle?, instrumentSwitch?, octaveTool?, pdf? }` (booleans) | from `chrome` | Per-control overrides of the preset, see below. |
199
+ | `skin` | `PepperScoreSkin` | | Theme colours, ordered built-in bar and optional pitch palette; see [Skins and pitch colours](#skins-and-pitch-colours). |
199
200
  | `view` | `'vertical' \| 'endless'` | saved or `'vertical'` | Vertical scrolls down, Endless scrolls sideways. Changing the prop re-applies it. |
200
201
  | `height` | `string` | natural | `"fill"`: the parent's height. Any CSS length (`"420px"`, `"60vh"`, `"100dvh"`) fixes it. Omitted: `full` fills its parent with a 480px floor, and `minimal`/`none` take the height of their score. |
201
202
  | `className` | `string` | | Extra classes on the `.pepperscore` root. |
202
203
  | `ariaLabel` | `string` | `"Score: <title>"`, or `"Score reader"` | Accessible name of the reader's landmark region. By default it is `Score: ` and the loaded score's title, or `Score reader` while there is no titled score. Give each snippet on a page a distinct name when titles repeat. |
203
- | `settings` | `PepperScoreSettings` | | **Initial** reader settings: `noteNames`, `chordSymbols`, `inferChords`, `beatCounts`, `subdivisions` (booleans); `musicFont` (`'Bravura' \| 'Petaluma'`); `staveFont` (`'Sans' \| 'Serif' \| 'Mono'`); `barsPerLine` (`'auto' \| 1–8`, Vertical view; without a `zoom`, an explicit count also sets the on-load size: that many bars fill the width, see below); `zoom` (stave scale, `null` = device default; with a `zoom`, `barsPerLine` only sets the line breaks); `semitones` (−12…12); `concert` (concert vs written pitch); `keyFlip` (spell a transposed view in the enharmonic key, for example Gb major for F# major); `bpm` (initial tempo, 30–240; the score's own tempo otherwise); `overlap` (0–3 lines kept on a page turn); `tabView`, `tabTuning`, `capo` (guitar TAB, see [Guitar TAB](#guitar-tab)); `annotationSizes` (text size per add-on, see below); `instrument`, `instruments`, `rangeLevel`, `autoRange` (the instrument switch, see below). Changing a key in the prop re-applies that key. Drawer edits still work in between. With `persist` on, **a saved per-score record overrides `settings` and `view`** when that score is opened again. The record holds the settings, view, zoom, transposition, part sounds, hidden staves and instrument switches the reader stored for that exact file. The instrument settings are the exception: when they differ from the ones the record was saved under, they apply again (see [Instrument switch](#instrument-switch)). |
204
+ | `settings` | `PepperScoreSettings` | | **Initial** reader settings: `noteNames`, `chordSymbols`, `inferChords`, `beatCounts`, `subdivisions`, `pitchColours` (booleans; pitch colours need a skin palette); `musicFont` (`'Bravura' \| 'Petaluma'`); `staveFont` (`'Sans' \| 'Serif' \| 'Mono'`); `barsPerLine` (`'auto' \| 1–8`, Vertical view; without a `zoom`, an explicit count also sets the on-load size: that many bars fill the width, see below); `zoom` (stave scale, `null` = device default; with a `zoom`, `barsPerLine` only sets the line breaks); `semitones` (−12…12); `concert` (concert vs written pitch); `keyFlip` (spell a transposed view in the enharmonic key, for example Gb major for F# major); `bpm` (initial tempo, 30–240; the score's own tempo otherwise); `overlap` (0–3 lines kept on a page turn); `tabView`, `tabInstruments`, `tabTuning`, `capo` (see [String TAB](#string-tab)); `annotationSizes` (text size per add-on, see below); `instrument`, `instruments`, `rangeLevel`, `autoRange` (the instrument switch, see below). Changing a key in the prop re-applies that key. Drawer edits still work in between. With `persist` on, **a saved per-score record overrides `settings` and `view`** when that score is opened again. The record holds the settings, view, zoom, transposition, part sounds, hidden staves and instrument switches the reader stored for that exact file. Host-only `tabInstruments` is not saved per score. The instrument-switch settings are the other exception: when they differ from the ones the record was saved under, they apply again (see [Instrument switch](#instrument-switch)). |
204
205
  | `persist` | `string \| false` | `'pepperscore'` for `full`, `false` for `minimal`/`none` | Storage namespace, see below. `false` reads and writes nothing. |
205
206
  | `assetBaseUrl` | `string` | jsDelivr copy of `pepperscore-sounds-<soundPack>` | Root of a sounds-package layout. Relative values resolve against the page, and a missing trailing slash is added. |
206
207
  | `verovioBaseUrl` | `string` | the copy in `dist/verovio/` | A directory with an interface-compatible Verovio build to engrave with, laid out like the `verovio` npm package's `dist/` (`verovio-module.mjs`, `verovio.mjs`), e.g. `https://cdn.jsdelivr.net/npm/verovio@6.3.0/dist/`. Shared by every reader on the page: set it before the first score engraves (a reader applies it before it warms Verovio up on mount; with `preloadVerovio`, pass it the same value). See [Verovio](#verovio). |
@@ -217,7 +218,7 @@ export default defineConfig({
217
218
  | `highlights` | `PepperScoreHighlight[]` | | Classes on notes and measures, see "Host hooks". |
218
219
  | `autoFocus` | `boolean` | `false` | Focus the reader on mount so the page-turn keys work before any click. Keyboard shortcuts belong to the focused reader only. Each reader is one Tab stop, and keyboard focus shows a focus ring on it (clicks do not). |
219
220
  | `paintTransition` | `'reveal' \| 'none'` | `'reveal'` | How a new `musicXml` or `abc` replaces a score already on screen. `'reveal'` fades it out, shows the loading overlay, scrolls to the top and fades the new score in. `'none'` keeps the current engraving and scroll position until the new one replaces it in place, with no fade or overlay (for a host that edits the score it shows). The first paint, other sources and setting changes always reveal. |
220
- | `ref` | `Ref<PepperScoreHandle>` | | `{ play(): Promise<void>; stop(): void; transpose(semitones: number): void; seek(seconds: number): void; currentTime(): number; scrollToMeasure(index: number): Promise<void>; seekToMeasure(index: number): void }`. `play` does nothing while playing, before a score has loaded, with `chrome="none"`, or with a `clock`. `stop` does nothing with a `clock`. `transpose` sets the **absolute** transposition (clamped to −12…12), the same as `settings.semitones`. It does not add to the current value. `seek` moves the playhead (and keeps playing from there when playing). With a `clock` it calls `onSeek` instead. `currentTime` returns the playhead seconds, or `clock.now()` with a `clock`. |
221
+ | `ref` | `Ref<PepperScoreHandle>` | | `{ play(): Promise<void>; stop(): void; setPitchColours(on: boolean): void; transpose(semitones: number): void; seek(seconds: number): void; currentTime(): number; scrollToMeasure(index: number): Promise<void>; seekToMeasure(index: number): void }`. `play` does nothing while playing, before a score has loaded, with `chrome="none"`, or with a `clock`. `stop` does nothing with a `clock`. `setPitchColours(on)` toggles palette noteheads without re-engraving (requires `skin.pitchColours`); see below. `transpose` sets the **absolute** transposition (clamped to −12…12), the same as `settings.semitones`. It does not add to the current value. `seek` moves the playhead (and keeps playing from there when playing). With a `clock` it calls `onSeek` instead. `currentTime` returns the playhead seconds, or `clock.now()` with a `clock`. |
221
222
 
222
223
  **The instrument switch.** `settings.instrument` (a `SwitchInstrumentId`: any `InstrumentId` but `'drum-kit'`) shows the first switchable part as that instrument: its clef, written pitch, range and sound. `settings.instruments` sets it per part and wins over `instrument`. A key is either the 1-based source part number as a string (`{ '2': 'trumpet' }`: parts in score order, a grand staff counted once, a drum kit counted too) or an `onReady` part id; a staff id of a grand staff switches the whole part. `onReady`'s `parts[].sourcePart` and `parts[].id` are both valid keys, and parts with `switchable: false` (drum kits, percussion) are ignored. `rangeLevel` (`RangeLevel`: `'beginner' | 'intermediate' | 'advanced'`, default `'intermediate'`) and `autoRange` (default `true`: phrases outside the range move by an octave) set how switched parts fit. They apply to a score without a saved record, and to a saved one when they differ from the host settings it was saved under (see [Instrument switch](#instrument-switch)). In `minimal` and `none` they switch the part without showing a switcher; set `controls.instrumentSwitch` for one.
223
224
 
@@ -270,6 +271,7 @@ For hosts that build on the reader (selection, cursors, navigation):
270
271
  | Control | `full` | `minimal` | `none` |
271
272
  | --- | --- | --- | --- |
272
273
  | Header transport (stop, play, BPM, view toggle, open, mixer) | yes | no | no |
274
+ | PDF export | yes | no | no |
273
275
  | Compact play/stop button (corner overlay) | no | yes | no |
274
276
  | Playhead, note highlight, follow | yes | yes | no (yes with a `clock`) |
275
277
  | Bottom drawer and handle | yes | no | no |
@@ -290,22 +292,130 @@ For hosts that build on the reader (selection, cursors, navigation):
290
292
  | `drawer` | The bottom drawer and its handle. |
291
293
  | `instrumentSwitch` | The instrument switcher: the Instrument choice in the mixer and the embed popover, the range-level and auto-range controls, and the phone bottom toolbar of embeds. On in `full`; off in `minimal` and `none` unless you set it. `settings.instrument` / `settings.instruments` alone still switch the part, but show no switcher. |
292
294
  | `octaveTool` | The octave tool (select a note, passage or phrase and move it 8va/8vb). On in `full`; off in `minimal` and `none` unless you set it. |
293
- | `tabToggle` | The Notation / TAB / Both switch, in the header (in the More panel on a phone) and in the drawer. With the drawer on, the header copy is left out between 761 and 1260px, where the full header has no room for it. It shows only for a score with a pitched part. |
295
+ | `pdf` | PDF export button, drawer action and dialog; on in `full`, off otherwise. Explicit `false` also removes skin bar item `pdf`. See [PDF export](#pdf-export). |
296
+ | `tabToggle` | The Notation / TAB / Both switch, in the header (in the More panel on a phone) and in the drawer. With the drawer on, the header copy is left out between 761 and 1260px, where the full header has no room for it. It shows only for a score with available TAB. |
294
297
 
295
- The header appears when any of `transport`, `viewToggle`, `openFile`, `mixer`, `zoom` or `tabToggle` is on. For example, `<PepperScore chrome="minimal" controls={{ viewToggle: true }} … />` keeps the compact play button and adds a small header with only the view toggle.
298
+ The header appears when any of `transport`, `viewToggle`, `openFile`, `mixer`, `zoom`, `tabToggle` or `pdf` is on. For example, `<PepperScore chrome="minimal" controls={{ viewToggle: true }} … />` keeps the compact play button and adds a small header with only the view toggle.
296
299
 
297
- ### Guitar TAB
300
+ ### Skins and pitch colours
298
301
 
299
- `settings.tabView` sets which staves guitar parts show: `'notation'` (default), `'tab'` or `'both'`. Readers switch with `controls.tabToggle` (on in `full`).
302
+ `skin` is plain data (`PepperScoreSkin`), so it also works across an Astro island boundary:
303
+
304
+ ```tsx
305
+ import { PepperScore, mergeSkins, CRF_PITCH_COLOURS, type PepperScoreSkin } from 'pepperscore';
306
+
307
+ const base: PepperScoreSkin = {
308
+ theme: { accent: 'var(--brand)', focusRing: '#245fa5' },
309
+ bar: ['play', 'tempo', 'instrumentSwitch', 'noteNames', 'pitchColours', 'options'],
310
+ pitchColours: CRF_PITCH_COLOURS,
311
+ };
312
+ const skin = mergeSkins(base, { theme: { surface: '#fff' }, pitchColours: { C: '#ed7777' } });
313
+ <PepperScore abc={tuneAbc} skin={skin} settings={{ pitchColours: true }} />;
314
+ ```
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
+
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.
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.
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.
368
+ - **Pitch palette:** `PitchPalette` maps all 12 sharp-spelled `PitchClass` keys (`C`, `C#`, `D`, `D#`, `E`, `F`, `F#`, `G`, `G#`, `A`, `A#`, `B`) to colours. Enharmonic equivalents share a colour. `CRF_PITCH_COLOURS` is a ready-made palette. `mergeSkins(base, ...overrides)` merges theme/palette keys and replaces the bar with the last supplied array; the resulting palette must be complete or it throws.
369
+
370
+ | Setting / handle | Type | Default | Meaning |
371
+ | --- | --- | --- | --- |
372
+ | `settings.pitchColours` | `boolean` | `false` | Enable palette noteheads; needs `skin.pitchColours`. Saved per score when persistence is on. |
373
+ | `handle.setPitchColours(on)` | `(on: boolean) => void` | | Toggle the same preference without re-engraving. |
374
+
375
+ The pitch-colour tile appears in the bar/drawer only when a palette is supplied. Colours follow displayed pitch after transposition, including individual chord heads; percussion and TAB are not coloured. Playback, cursor, selection and range marks retain priority. Palette changes and toggles do not re-engrave. The package exports `PepperScoreSkin`, `ThemeToken`, `BarItemId`, `PitchClass` and `PitchPalette` types alongside the helpers above.
376
+
377
+ ### PDF export
378
+
379
+ `full` shows a PDF button in the header's secondary controls and the drawer. The header button is icon-only at reader widths 761–900px and appears in the secondary row/More menu on narrow screens. Use `controls={{ pdf: true }}` to enable it in other presets, `controls={{ pdf: false }}` to hide it, or include `'pdf'` in `skin.bar`. The resolved internal flag is `chrome.pdf`; the public `chrome` prop remains a preset string. Export becomes available once a score is loaded.
380
+
381
+ ```tsx
382
+ <PepperScore musicXml={xml} chrome="minimal" controls={{ pdf: true }} />
383
+ ```
384
+
385
+ | Dialog option / preference | Default | Meaning |
386
+ | --- | --- | --- |
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. |
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. |
389
+
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.
393
+
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.
395
+
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.
401
+
402
+ ### String TAB
403
+
404
+ `settings.tabView` sets which staves supported string parts show: `'notation'` (default), `'tab'` or `'both'`. Readers switch with `controls.tabToggle` (on in `full`).
300
405
 
301
406
  | Setting | Type | Default | Meaning |
302
407
  | --- | --- | --- | --- |
303
408
  | `tabView` | `'notation' \| 'tab' \| 'both'` | `'notation'` | Notation only, TAB only (the notation staff is hidden and its chord symbols, tempo and directions move onto the TAB staff), or both. |
304
- | `tabTuning` | `number[]` | `[40, 45, 50, 55, 59, 64]` | Open-string MIDI pitches, low to high, of generated TAB (EADGBE). |
305
- | `capo` | `0`–`12` | `0` | Capo fret of generated TAB. Frets are relative to it. |
409
+ | `tabInstruments` | `PepperScoreSettings['tabInstruments']` | detected per part | Host TAB identity keyed by exact `onReady` part id; `'none'` disables it. Changes TAB, not playback. See the identity list below. |
410
+ | `tabTuning` | `number[]` | instrument defaults | 4–8 open-string MIDI pitches in physical bottom-to-top string order (re-entrant allowed). Applies only to generated fretted parts with the matching string count; finger TAB uses its chart tuning. |
411
+ | `capo` | `0`–`12` | `0` | Capo fret of generated fretted TAB. Frets are relative to it; ignored for finger TAB. |
306
412
 
307
413
  - **A score with a TAB staff** (MusicXML `<clef><sign>TAB</sign>` with `<staff-details>`, as MuseScore and Guitar Pro export) shows that staff in the TAB and Both views. The reader treats it as a view of its notation part: it is played once, has no note-name or count lanes, and the part controls and mixer list the instrument once.
308
- - **Any other score** gets TAB generated from its pitches: for every guitar part (General MIDI programs 25–32, the guitar sound, or "guitar" in the part name), or else for the first pitched part's top staff. A fingering solver picks strings and frets for the whole passage (open strings, few position shifts, playable stretches). Notes out of range are moved by an octave. Chord notes that cannot all be fretted lose doubled notes first, then inner voices. MusicXML `<technical><string>`/`<fret>` on a note is kept as a fixed fingering.
414
+ - **Generated TAB** is available for guitar, four-/five-string bass guitar and soprano/tenor/baritone/bass ukulele, using sounding pitches (including ABC octave clefs and MusicXML octave transposition). GM programs take precedence: 25–40 identify fretted strings, with explicit names refining ukulele and bass identities; without a program, string-instrument names are used. Bare Bass normally means bass guitar, but choir Bass and bass winds do not get TAB. There is no first-pitched-part fallback: piano, flute, harp and string ensembles get no generated TAB. Viola and double bass get no generated TAB.
415
+ - **Finger + position TAB:** violin and cello get four string lines in TAB/Both views, with finger numbers on the lines and inline `L`/`H` for low/high placement (for example `L2`, `H4`). Position marks include `I pos.` and `pos. ½`; cello positions follow Davidoff (½, I, I½, II, III, III½, IV, IV½; no II½). Finger numbers are display only: playback uses source pitches. Chart-defined strings and pitches are used; `tabTuning` and `capo` do not apply. Unsupported or impossible events are left unlabelled. Viola and double bass remain notation only.
416
+ - **Identity and tuning:** `settings.tabInstruments: { '<onReady-part-id>': 'bass5' }` selects five-string bass; supported fretted ids are `guitar`, `bass4`, `bass5`, `ukulele-soprano`, `ukulele-tenor`, `ukulele-baritone`, `ukulele-bass`, plus `'none'` to suppress TAB. Use `violin` or `cello` for finger + position TAB. The accepted `viola` and `double-bass` identities still produce no generated TAB. Invalid keys/values are ignored. A reader instrument switch replaces the initial host override; an octave-only edit retains it. This host setting is not saved per score and does not change sound.
417
+ - **Defaults:** guitar EADGBE `[40,45,50,55,59,64]`; bass4 `[28,33,38,43]`; bass5 `[23,28,33,38,43]`; soprano/tenor ukulele re-entrant GCEA `[67,60,64,69]`; baritone ukulele `[50,55,59,64]`; bass ukulele `[28,33,38,43]`. Unqualified Bass uses four strings; Ukulele uses soprano tuning and nylon-guitar sound. Omit `tabTuning` to use these per-part fretted defaults. A supplied or saved tuning applies only to fretted parts with matching string counts: six-note Drop D affects guitars while bass/ukulele retain their defaults; a four-note tuning affects four-string parts, not guitar or bass5. Preserve physical order; do not sort re-entrant pitches.
418
+ - For fretted TAB, a fingering solver picks strings and frets for the whole passage (open strings, few position shifts, playable stretches). Notes out of range are moved by an octave. Chord notes that cannot all be fretted lose doubled notes first, then inner voices. MusicXML `<technical><string>`/`<fret>` on a note is kept as a fixed fingering.
309
419
  - **Transposed views** regenerate TAB from the transposed pitches. An imported TAB staff is hidden then, because its frets no longer match.
310
420
  - Tied continuations keep their rhythm on the TAB staff but show no fret.
311
421
  - **Baked fingerings.** The repository's `npm run tab:bake` (`scripts/tab-bake.mjs`, see the root README) writes the solver's string/fret onto a score's notes ahead of time, and can add a TAB staff. The reader keeps those fingerings as they are.
@@ -502,3 +612,128 @@ The package licence is `MIT AND LGPL-3.0-or-later`:
502
612
  - Bravura, Petaluma and Petaluma Script fonts: SIL Open Font License 1.1 (`fonts/Bravura-OFL.txt`, `fonts/Petaluma-OFL.txt`)
503
613
  - Perfect Drums samples: attribution in `soundfonts/perfect-drums/ATTRIBUTION.txt` (the source artifact states WTFPL)
504
614
  - Instrument icons: MIT
615
+
616
+
617
+ ## ABC headers
618
+
619
+ ABC is parsed with abcjs and engraved through PepperScore's score model. Chord symbols, text annotations, accidentals, ties, beams and final/double barlines are retained. Use the checklist, rules and templates below when generating ABC for `<PepperScore abc={…}>`.
620
+
621
+ ### Quick checklist
622
+
623
+ 1. One tune per string. Start with `X:1`. Only the first tune is imported.
624
+ 2. Header order: `X:` → `T:` → `C:` → `M:` → `L:` → `Q:` → `%%score`/`%%staves` (if grouping) → `V:` lines (with their `%%MIDI` lines right after each) → `K:` last. Put `%%score`/`%%staves` **before** the `V:` lines they group.
625
+ 3. Keep `K:` after `X:` (a file-level `K:` before `X:` is tolerated but has no effect on voices).
626
+ 4. Declare every voice with `V:<id> name="…" clef=…` (preferably in the tune header; body-only declarations also work), even a single voice, if you need a specific instrument, clef or transposition. `V:` lines before `X:` also work as file-wide defaults, but prefer the tune header.
627
+ 5. Put `%%MIDI program|channel|transpose` **directly after the `V:` line it belongs to**. In the header, a `%%MIDI` line before any `V:` is a default for every voice.
628
+ 6. Transposing instruments: write the notes **as the player reads them** and add `transpose=<semitones written→sounding>` on the `V:` line.
629
+ 7. End the piece with `|]`.
630
+ 8. Chord symbols in plain quotes (`"C7"`); text in quotes with a position prefix (`"^Swing"`, `"_rit."`).
631
+
632
+ ### Rules
633
+
634
+ #### Tune and header
635
+
636
+ - Only the first tune (`X:` to the next `X:`) is read. Before the first `X:`, only `%%MIDI` and `V:` lines apply (as defaults); `M:`, `L:`, `Q:`, `C:` there are ignored, so put them after `X:`. A `K:` there doesn't end the tune header; the tune's own first `K:` does. A `V:` line before `X:` supplies defaults (clef, name, transposition) for that voice; a tune-header `V:` for the same id overrides each property it sets. If the body has music before any `[V:…]` selector, it goes to the first declared voice — which may be a file-header voice — so select voices explicitly in the body.
637
+ - Honoured: `T:` title, `C:` composer, `M:` meter, `L:` default length, `Q:` tempo, `K:` key (with `m`/`min` for minor).
638
+
639
+ #### Keys and accidentals
640
+
641
+ - Change key mid-tune with a `K:` line or inline `[K:Gb]`, including mid-bar. The new signature is drawn where it occurs.
642
+ - Write accidentals normally (`^F`, `_B`, `=F`). Per-bar rules apply: an accidental carries to later notes of the same pitch in that bar, and a tie (`^F- | F`) carries it into the next bar.
643
+ - Spelling is kept as written: `^E` in F♯ major and `_C` in G♭ major stay E♯ and C♭.
644
+
645
+ #### Clefs
646
+
647
+ - `clef=treble|bass|alto|tenor|treble-8|treble+8|bass-8|perc` on `V:` (or `K:`).
648
+ - Octave clefs: ABC note letters are the **sounding** pitch (ABC 2.1); `-8` draws them an octave higher and `+8` an octave lower. Guitar: `clef=treble-8`, and `G,` sounds G3 and is drawn as G4 on the second staff line, with the clef’s "8" retained when switching to guitar. TAB and playback use G3. Don't also add `transpose=-12`; the letters already specify sounding pitch.
649
+
650
+ #### Instruments and sound
651
+
652
+ - `name="…"` selects the instrument from its name (e.g. "Flute", "Cello", "Alto Saxophone") when no `%%MIDI program` is given. A name never implies transposition.
653
+ - `%%MIDI program N`: **zero-based** GM number (0–127), e.g. 0 piano, 40 violin, 42 cello, 65 alto sax, 73 flute. PepperScore stores GM programs as 1–128 internally.
654
+ - `%%MIDI channel 10` selects the drum kit and unpitched notation, taking precedence over the MIDI program. A voice named like "Drums" also imports as unpitched percussion when no MIDI program is supplied. Use `clef=perc`.
655
+ - Header MIDI directives before any `V:` declaration are tune-wide defaults; after a `V:` declaration, they apply only to that voice. Each voice inherits default properties it has not overridden. Body directives override that voice’s earlier MIDI settings, but explicit `V: transpose=` still wins.
656
+ - Body-only voices: a `%%MIDI` line after `[V:id]` / `V:id` in the body applies to that voice. One before any voice selector applies to the first voice, unless notes or a barline come before every selector: then it belongs to that implicit default voice and later voices don't inherit it. Safest: put each `%%MIDI` line right after its voice's `V:` line.
657
+
658
+ #### Transposing instruments
659
+
660
+ - `V:sax name="Alto Saxophone" transpose=-9` → written A4 sounds C4. Values are written→sounding semitones: B♭ clarinet/trumpet -2, tenor sax -14, alto sax -9, horn in F -7, baritone sax -21.
661
+ - `transpose=` wins over `%%MIDI transpose`, including an explicit `transpose=0`. Prefer `transpose=`.
662
+ - `%%MIDI transpose N` supplies written-to-sounding semitones when the voice has no explicit `transpose=`. Instrument intervals add to octave-clef intervals; instrument switching and concert-pitch display use the combined interval. Diatonic steps follow conventional intervals within each octave (0–11 semitones: unison, minor/major second, minor/major third, fourth, augmented fourth, fifth, minor/major sixth, minor/major seventh), with the same sign and seven steps per octave. Thus -9 maps to -5 diatonic steps.
663
+ - `score=` is **not** supported; don't use it.
664
+ - The written key goes in `K:`: for alto sax in concert C, write `K:A`.
665
+ - Don't send concert-pitch ABC *and* `transpose=`; pick written pitch + `transpose=`. (Alternative: concert-pitch ABC with no `transpose=`, and the host sets `settings.instrument`.)
666
+
667
+ #### Grand staff (piano, harp, organ)
668
+
669
+ - Group voices with braces using `%%score {RH | LH}` **before** their `V:` declarations; putting the grouping after the declarations leaves the staves separate. They become one part with one name and a brace. Name the first voice (`name="Piano"`); the second inherits the instrument. Pitched staves in a brace share one instrument and sound, taking an explicit MIDI sound when present, otherwise the named pitched voice’s instrument. Percussion remains specific to its own staff and never changes a pitched partner into drums.
670
+ - Without braces, voices stay separate parts.
671
+
672
+ #### Chord symbols, text and dyads
673
+
674
+ - `"C7"`, `"Am7b5"`, `"C/E"`: chord symbols (default position).
675
+ - `"^Swing"` above, `"_rit."` below: text, never a chord symbol. You can put text and a chord on the same note: `"^Swing""C7"c`.
676
+ - Dyads/chords: `[CE]` gives one stem with two pitches.
677
+
678
+ #### Barlines
679
+
680
+ - `|]` final barline, `||` double barline, `|:` `:|` `::` repeats, `[1` `[2` endings.
681
+
682
+ ### Templates
683
+
684
+ **Single melody, concert pitch**
685
+
686
+ ```abc
687
+ X:1
688
+ T:Scale study
689
+ M:4/4
690
+ L:1/8
691
+ Q:1/4=90
692
+ V:1 name="Flute"
693
+ K:D
694
+ DEFG ABcd | d2c2 B2A2 |]
695
+ ```
696
+
697
+ **Transposing instrument (alto sax, concert C major)**
698
+
699
+ ```abc
700
+ X:1
701
+ M:4/4
702
+ L:1/4
703
+ V:sax name="Alto Saxophone" transpose=-9
704
+ %%MIDI program 65
705
+ K:A
706
+ A B c d | e2 A2 |]
707
+ ```
708
+
709
+ **Piano grand staff with text and chords**
710
+
711
+ ```abc
712
+ X:1
713
+ M:4/4
714
+ L:1/4
715
+ %%score {RH | LH}
716
+ V:RH name="Piano" clef=treble
717
+ V:LH clef=bass
718
+ K:C
719
+ [V:RH] "^Swing""C7"c e g e | c4 |]
720
+ [V:LH] C, G, E, G, | C,4 |]
721
+ ```
722
+
723
+ **Guitar**
724
+
725
+ ```abc
726
+ X:1
727
+ M:4/4
728
+ L:1/4
729
+ V:gtr name="Guitar" clef=treble-8
730
+ %%MIDI program 24
731
+ K:G
732
+ G, A, B, C | D4 |]
733
+ ```
734
+
735
+ ### Known limits (don't rely on these)
736
+
737
+ - Lyrics, decorations, tuplet engraving, multi-tune files: outside the parity guarantee.
738
+ - `score=` on `V:` is ignored.
739
+ - Saved per-part reader settings reset for piano ABC scores first shown before 2026-11-10 (grand-staff regrouping).
@@ -0,0 +1,253 @@
1
+ import { c as e, d as t, o as n, r, u as i } from "./index.js";
2
+ import { jsx as a, jsxs as o } from "react/jsx-runtime";
3
+ import { useEffect as s, useRef as c, useState as l } from "react";
4
+ //#region src/components/PdfOverlay.tsx
5
+ async function u({ score: t, prefs: r, hiddenPartIds: a, channel: o, pitchColours: s }) {
6
+ if (!t.document) throw Error("This score has no engraving document.");
7
+ let c = new Set(a), l = await e(t.document, o), u = n(l, t, c, {
8
+ view: r.tabView,
9
+ tuning: r.tabTuning,
10
+ capo: r.capo
11
+ });
12
+ return {
13
+ score: t,
14
+ input: {
15
+ data: t.document.engravingText,
16
+ view: {
17
+ breakEvery: 0,
18
+ hiddenStaves: u.hiddenStaves,
19
+ spelling: u.spelling,
20
+ chordSymbols: r.chordSymbols,
21
+ ...u.tabPlan.spec ? { tab: u.tabPlan.spec } : {}
22
+ }
23
+ },
24
+ visiblePartIds: u.visibleParts.map((e) => e.id),
25
+ tabAliases: u.tabAlias,
26
+ tabStaves: u.tabStaves,
27
+ pitchColours: s,
28
+ annotations: {
29
+ showNames: r.names,
30
+ showCounts: r.counts,
31
+ subdivisions: r.subdivisions,
32
+ showChordSymbols: r.chordSymbols,
33
+ inferChordSymbols: r.inferChordSymbols,
34
+ scale: {
35
+ names: i[r.namesSize],
36
+ chordSymbols: i[r.chordSymbolsSize],
37
+ counts: i[r.countsSize],
38
+ subdivisions: i[r.subdivisionsSize]
39
+ }
40
+ }
41
+ };
42
+ }
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
55
+ } : void 0;
56
+ s(() => {
57
+ let e = document.activeElement;
58
+ k.current?.querySelector("select")?.focus({ preventScroll: !0 });
59
+ let t = k.current?.closest(".pepperscore");
60
+ return () => {
61
+ A.current?.abort(), j.current?.abort();
62
+ let n = document.activeElement;
63
+ e?.isConnected && (n === document.body || n && t?.contains(n)) && e.focus({ preventScroll: !0 });
64
+ };
65
+ }, []);
66
+ let F = (e) => {
67
+ if (e.key === "Escape") {
68
+ e.preventDefault(), e.stopPropagation(), h();
69
+ return;
70
+ }
71
+ if (e.key !== "Tab") return;
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);
73
+ e.shiftKey && document.activeElement === n ? (e.preventDefault(), r?.focus()) : !e.shiftKey && document.activeElement === r && (e.preventDefault(), n?.focus());
74
+ };
75
+ s(() => {
76
+ if (v) return;
77
+ let t = new AbortController();
78
+ return A.current = t, w("Checking fit…"), (async () => {
79
+ try {
80
+ let n = await u(M.current());
81
+ t.signal.throwIfAborted();
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 = [
83
+ n.score.title,
84
+ n.score.subtitle,
85
+ n.score.composer,
86
+ ...n.score.credits ?? []
87
+ ].filter(Boolean), c = await i({
88
+ ...n.input,
89
+ view: {
90
+ breakEvery: 0,
91
+ hiddenStaves: [],
92
+ chordSymbols: !1,
93
+ ...n.input.view,
94
+ harmSize: o.harmSize
95
+ }
96
+ }, {
97
+ paper: e,
98
+ maxBars: g,
99
+ font: p,
100
+ files: P,
101
+ spacingStaff: o.spacingStaff,
102
+ annotationRoom: o.laneHeight,
103
+ headingMm: s.length ? 8 + s.length * 4 : 0
104
+ }, t.signal);
105
+ if (t.signal.aborted) return;
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." : ""}`);
108
+ } catch (e) {
109
+ t.signal.aborted || w(`Fit check unavailable: ${e instanceof Error ? e.message : String(e)}. You can still Save.`);
110
+ }
111
+ })(), () => t.abort();
112
+ }, [
113
+ e,
114
+ g,
115
+ p,
116
+ m,
117
+ f,
118
+ v
119
+ ]);
120
+ let I = async () => {
121
+ let t = S();
122
+ k.current?.querySelector(".pdf-cancel")?.focus({ preventScroll: !0 }), A.current?.abort();
123
+ let n = new AbortController();
124
+ j.current = n, y(!0), E(""), O(null);
125
+ try {
126
+ let r = window.frameElement;
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.");
128
+ let i = await u(M.current());
129
+ n.signal.throwIfAborted();
130
+ let { exportScore: a } = await import("./export-Dg2CkokP.js"), o = await a({
131
+ ...i,
132
+ options: {
133
+ paper: e,
134
+ maxBars: t,
135
+ font: p,
136
+ files: P
137
+ },
138
+ onProgress: (e) => {
139
+ n.signal.aborted || O(e);
140
+ }
141
+ }, n.signal);
142
+ n.signal.throwIfAborted();
143
+ let s = URL.createObjectURL(o), c = document.createElement("a");
144
+ c.href = s, c.download = `${i.score.title?.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "score"}.pdf`, document.body.append(c);
145
+ try {
146
+ c.click();
147
+ } finally {
148
+ c.remove(), window.setTimeout(() => URL.revokeObjectURL(s), 6e4);
149
+ }
150
+ N.current();
151
+ } catch (e) {
152
+ n.signal.aborted || E(e instanceof Error ? e.message : String(e));
153
+ } finally {
154
+ n.signal.aborted || y(!1);
155
+ }
156
+ };
157
+ return /* @__PURE__ */ a("div", {
158
+ className: "pdf-backdrop",
159
+ onMouseDown: (e) => {
160
+ e.target === e.currentTarget && h();
161
+ },
162
+ children: /* @__PURE__ */ o("section", {
163
+ ref: k,
164
+ className: "pdf-panel",
165
+ role: "dialog",
166
+ "aria-modal": "true",
167
+ "aria-label": "Export PDF",
168
+ "aria-busy": v,
169
+ onKeyDown: F,
170
+ children: [
171
+ /* @__PURE__ */ a("h2", { children: "Export PDF" }),
172
+ /* @__PURE__ */ o("label", {
173
+ className: "pdf-field",
174
+ children: ["Paper size", /* @__PURE__ */ o("select", {
175
+ className: "pdf-paper",
176
+ value: e,
177
+ disabled: v,
178
+ onChange: (e) => n(e.target.value),
179
+ children: [
180
+ /* @__PURE__ */ a("option", {
181
+ value: "a4",
182
+ children: "A4"
183
+ }),
184
+ /* @__PURE__ */ a("option", {
185
+ value: "a3",
186
+ children: "A3"
187
+ }),
188
+ /* @__PURE__ */ a("option", {
189
+ value: "letter",
190
+ children: "Letter"
191
+ }),
192
+ /* @__PURE__ */ a("option", {
193
+ value: "legal",
194
+ children: "Legal"
195
+ })
196
+ ]
197
+ })]
198
+ }),
199
+ /* @__PURE__ */ o("label", {
200
+ className: "pdf-field",
201
+ children: ["Bars per line", /* @__PURE__ */ a("input", {
202
+ className: "pdf-bars",
203
+ type: "number",
204
+ min: 1,
205
+ max: 8,
206
+ value: b,
207
+ disabled: v,
208
+ onChange: (e) => x(e.target.value),
209
+ onBlur: S
210
+ })]
211
+ }),
212
+ /* @__PURE__ */ a("p", {
213
+ className: "pdf-fit",
214
+ role: "status",
215
+ children: v ? d(D) : C
216
+ }),
217
+ T && /* @__PURE__ */ a("p", {
218
+ className: "pdf-error",
219
+ role: "alert",
220
+ children: T
221
+ }),
222
+ /* @__PURE__ */ o("div", {
223
+ className: "pdf-actions",
224
+ children: [
225
+ /* @__PURE__ */ a("button", {
226
+ type: "button",
227
+ className: "pdf-cancel",
228
+ onClick: h,
229
+ children: "Cancel"
230
+ }),
231
+ T && /* @__PURE__ */ a("button", {
232
+ type: "button",
233
+ className: "pdf-reload",
234
+ onClick: () => window.history.go(0),
235
+ children: "Reload page"
236
+ }),
237
+ /* @__PURE__ */ a("button", {
238
+ type: "button",
239
+ className: "pdf-save",
240
+ disabled: v,
241
+ onClick: () => void I(),
242
+ children: "Save"
243
+ })
244
+ ]
245
+ })
246
+ ]
247
+ })
248
+ });
249
+ }
250
+ //#endregion
251
+ export { f as default };
252
+
253
+ //# sourceMappingURL=PdfOverlay-DUgY9mW-.js.map