@ugfoundation/swaralipi-js 0.9.2 → 0.10.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 (47) hide show
  1. package/README.md +17 -0
  2. package/dist/{batch-IHEXFEGC.js → batch-5ZFEKXKS.js} +5 -5
  3. package/dist/{batch-IHEXFEGC.js.map → batch-5ZFEKXKS.js.map} +1 -1
  4. package/dist/{chunk-TZBDIRIA.js → chunk-R6VNIBMG.js} +7 -7
  5. package/dist/chunk-R6VNIBMG.js.map +1 -0
  6. package/dist/{chunk-MYVIQU4O.js → chunk-SR7NFEGL.js} +5 -3
  7. package/dist/chunk-SR7NFEGL.js.map +1 -0
  8. package/dist/{chunk-F23LZMIU.js → chunk-UPJYLXYX.js} +3 -3
  9. package/dist/{chunk-F23LZMIU.js.map → chunk-UPJYLXYX.js.map} +1 -1
  10. package/dist/{chunk-SEN266BY.js → chunk-VCLLSNAK.js} +27 -4
  11. package/dist/chunk-VCLLSNAK.js.map +1 -0
  12. package/dist/cli.cjs +7 -5
  13. package/dist/cli.cjs.map +1 -1
  14. package/dist/cli.js +3 -3
  15. package/dist/convert/index.cjs +7 -5
  16. package/dist/convert/index.cjs.map +1 -1
  17. package/dist/convert/index.js +3 -3
  18. package/dist/index.cjs +4 -2
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +2 -2
  23. package/dist/{lay-CZIzR9cM.d.ts → lay-0LnKv1LL.d.ts} +7 -1
  24. package/dist/{lay-CfDlPhH2.d.cts → lay-J17-gS6O.d.cts} +7 -1
  25. package/dist/player/index.cjs +439 -51
  26. package/dist/player/index.cjs.map +1 -1
  27. package/dist/player/index.d.cts +78 -13
  28. package/dist/player/index.d.ts +78 -13
  29. package/dist/player/index.js +414 -53
  30. package/dist/player/index.js.map +1 -1
  31. package/dist/{schedule-CU-VECDH.d.cts → schedule-B1S53131.d.cts} +28 -2
  32. package/dist/{schedule-BaovCtmf.d.ts → schedule-DtcYB-o4.d.ts} +28 -2
  33. package/dist/spec/index.cjs +4 -2
  34. package/dist/spec/index.cjs.map +1 -1
  35. package/dist/spec/index.d.cts +2 -2
  36. package/dist/spec/index.d.ts +2 -2
  37. package/dist/spec/index.js +2 -2
  38. package/dist/viewer/index.cjs +148 -7
  39. package/dist/viewer/index.cjs.map +1 -1
  40. package/dist/viewer/index.d.cts +22 -5
  41. package/dist/viewer/index.d.ts +22 -5
  42. package/dist/viewer/index.js +146 -8
  43. package/dist/viewer/index.js.map +1 -1
  44. package/package.json +1 -1
  45. package/dist/chunk-MYVIQU4O.js.map +0 -1
  46. package/dist/chunk-SEN266BY.js.map +0 -1
  47. package/dist/chunk-TZBDIRIA.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { O as Octave, a as SvaraLetter, N as Note, M as Meta, S as Swaralipi, T as Taal } from '../schema-Cf_epUIC.cjs';
2
- import { L as Lay } from '../lay-CfDlPhH2.cjs';
3
- import { D as DrumVoice, S as Schedule, P as PlayEvent } from '../schedule-CU-VECDH.cjs';
2
+ import { L as Lay } from '../lay-J17-gS6O.cjs';
3
+ import { D as DrumVoice, S as Schedule, P as PlayEvent } from '../schedule-B1S53131.cjs';
4
4
  import 'zod';
5
5
  import 'tone';
6
6
 
@@ -34,6 +34,13 @@ declare const LAY_LABELS: Record<GridLang, Record<Lay, string>>;
34
34
  /** Printed where a row switches to free rhythm (`row.taal: null`) with no
35
35
  * `taalName` of its own. bn is the converter's canonical মুক্তছন্দ. */
36
36
  declare const FREE_TAAL_LABELS: Record<GridLang, string>;
37
+ /** The practice surface's words: the sheet's own accessible name (it says
38
+ * what a tap does), a repeat being on, and the learner's turn to sing. */
39
+ declare const PRACTICE_LABELS: Record<GridLang, {
40
+ sheet: string;
41
+ repeat: string;
42
+ turn: string;
43
+ }>;
37
44
  /** A composed notation cluster: the font's ASCII text plus any residual CSS
38
45
  * classes the font itself cannot express (only the bn ati-octave dot). */
39
46
  interface Cluster {
@@ -209,6 +216,9 @@ declare const ZOOM_STEP = 1.2;
209
216
  * themselves: `<style>${SWARALIPI_CSS}</style>` next to `renderToHtml()`.
210
217
  *
211
218
  * Theming hooks (CSS custom properties, all optional):
219
+ * --sw-loop repeated-line band; --sw-loop-edge its edge, the learner's
220
+ * hollow highlight and "your turn" tag (--sw-turn-ink on it)
221
+ * --sw-row-hover hover tint of a tappable line; --sw-focus the focus ring
212
222
  * --sw-font font stack (default: Noto Sans Bengali + system Bengali)
213
223
  * --sw-size base font size
214
224
  * --sw-cell-gap horizontal gap between source cells
@@ -501,9 +511,16 @@ interface SwaralipiSheetElement extends HTMLElement {
501
511
  doc: Swaralipi | undefined;
502
512
  /** Parse documents against the spec schema before rendering. */
503
513
  validate: boolean;
504
- /** Add the player-highlight class to the note at "part.row.note". */
505
- highlight(path: string): HTMLElement | null;
514
+ /** Add the player-highlight class to the note at "part.row.note" — drawn
515
+ * hollow when it is the learner's turn to sing it. */
516
+ highlight(path: string, yourTurn?: boolean): HTMLElement | null;
506
517
  clearHighlights(): void;
518
+ /** Shade the repeated lines (`part.row`, inclusive; null clears) and mark
519
+ * the learner's turn on them. The player's bindSheet calls this. */
520
+ markLoop(loop: {
521
+ from: string;
522
+ to: string;
523
+ } | null, yourTurn?: boolean): void;
507
524
  /** Open the print dialog (→ "Save as PDF") for the current document. */
508
525
  print(options?: PrintOptions): void;
509
526
  /** Options for the `staff` language (instrument label, tonic, bpm…). */
@@ -551,4 +568,4 @@ type Transliterator = (bengaliText: string) => string;
551
568
  */
552
569
  declare function loadLyricTransliterator(lang: ViewerLang): Promise<Transliterator | undefined>;
553
570
 
554
- export { type AbcMeter, type Arc, type Cell, type Cluster, type ComposeOptions, DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, FREE_TAAL_LABELS, type GridLang, LABELS, LAY_LABELS, type Layout, MARKS, MAX_ZOOM, MIN_ZOOM, NOTATION_FONT_URLS, type PrintOptions, type RenderOptions, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, type StaffOptions, type SwaralipiSheetElement, type TaalStaffOptions, type Transliterator, type ViewerLang, ZOOM_STEP, abcDuration, abcToSvg, alignRows, buildAbc, buildTaalAbc, cellMatra, composeCluster, defineSwaralipiSheet, docToAbc, fitSwaralipi, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
571
+ export { type AbcMeter, type Arc, type Cell, type Cluster, type ComposeOptions, DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, FREE_TAAL_LABELS, type GridLang, LABELS, LAY_LABELS, type Layout, MARKS, MAX_ZOOM, MIN_ZOOM, NOTATION_FONT_URLS, PRACTICE_LABELS, type PrintOptions, type RenderOptions, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, type StaffOptions, type SwaralipiSheetElement, type TaalStaffOptions, type Transliterator, type ViewerLang, ZOOM_STEP, abcDuration, abcToSvg, alignRows, buildAbc, buildTaalAbc, cellMatra, composeCluster, defineSwaralipiSheet, docToAbc, fitSwaralipi, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
@@ -1,6 +1,6 @@
1
1
  import { O as Octave, a as SvaraLetter, N as Note, M as Meta, S as Swaralipi, T as Taal } from '../schema-Cf_epUIC.js';
2
- import { L as Lay } from '../lay-CZIzR9cM.js';
3
- import { D as DrumVoice, S as Schedule, P as PlayEvent } from '../schedule-BaovCtmf.js';
2
+ import { L as Lay } from '../lay-0LnKv1LL.js';
3
+ import { D as DrumVoice, S as Schedule, P as PlayEvent } from '../schedule-DtcYB-o4.js';
4
4
  import 'zod';
5
5
  import 'tone';
6
6
 
@@ -34,6 +34,13 @@ declare const LAY_LABELS: Record<GridLang, Record<Lay, string>>;
34
34
  /** Printed where a row switches to free rhythm (`row.taal: null`) with no
35
35
  * `taalName` of its own. bn is the converter's canonical মুক্তছন্দ. */
36
36
  declare const FREE_TAAL_LABELS: Record<GridLang, string>;
37
+ /** The practice surface's words: the sheet's own accessible name (it says
38
+ * what a tap does), a repeat being on, and the learner's turn to sing. */
39
+ declare const PRACTICE_LABELS: Record<GridLang, {
40
+ sheet: string;
41
+ repeat: string;
42
+ turn: string;
43
+ }>;
37
44
  /** A composed notation cluster: the font's ASCII text plus any residual CSS
38
45
  * classes the font itself cannot express (only the bn ati-octave dot). */
39
46
  interface Cluster {
@@ -209,6 +216,9 @@ declare const ZOOM_STEP = 1.2;
209
216
  * themselves: `<style>${SWARALIPI_CSS}</style>` next to `renderToHtml()`.
210
217
  *
211
218
  * Theming hooks (CSS custom properties, all optional):
219
+ * --sw-loop repeated-line band; --sw-loop-edge its edge, the learner's
220
+ * hollow highlight and "your turn" tag (--sw-turn-ink on it)
221
+ * --sw-row-hover hover tint of a tappable line; --sw-focus the focus ring
212
222
  * --sw-font font stack (default: Noto Sans Bengali + system Bengali)
213
223
  * --sw-size base font size
214
224
  * --sw-cell-gap horizontal gap between source cells
@@ -501,9 +511,16 @@ interface SwaralipiSheetElement extends HTMLElement {
501
511
  doc: Swaralipi | undefined;
502
512
  /** Parse documents against the spec schema before rendering. */
503
513
  validate: boolean;
504
- /** Add the player-highlight class to the note at "part.row.note". */
505
- highlight(path: string): HTMLElement | null;
514
+ /** Add the player-highlight class to the note at "part.row.note" — drawn
515
+ * hollow when it is the learner's turn to sing it. */
516
+ highlight(path: string, yourTurn?: boolean): HTMLElement | null;
506
517
  clearHighlights(): void;
518
+ /** Shade the repeated lines (`part.row`, inclusive; null clears) and mark
519
+ * the learner's turn on them. The player's bindSheet calls this. */
520
+ markLoop(loop: {
521
+ from: string;
522
+ to: string;
523
+ } | null, yourTurn?: boolean): void;
507
524
  /** Open the print dialog (→ "Save as PDF") for the current document. */
508
525
  print(options?: PrintOptions): void;
509
526
  /** Options for the `staff` language (instrument label, tonic, bpm…). */
@@ -551,4 +568,4 @@ type Transliterator = (bengaliText: string) => string;
551
568
  */
552
569
  declare function loadLyricTransliterator(lang: ViewerLang): Promise<Transliterator | undefined>;
553
570
 
554
- export { type AbcMeter, type Arc, type Cell, type Cluster, type ComposeOptions, DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, FREE_TAAL_LABELS, type GridLang, LABELS, LAY_LABELS, type Layout, MARKS, MAX_ZOOM, MIN_ZOOM, NOTATION_FONT_URLS, type PrintOptions, type RenderOptions, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, type StaffOptions, type SwaralipiSheetElement, type TaalStaffOptions, type Transliterator, type ViewerLang, ZOOM_STEP, abcDuration, abcToSvg, alignRows, buildAbc, buildTaalAbc, cellMatra, composeCluster, defineSwaralipiSheet, docToAbc, fitSwaralipi, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
571
+ export { type AbcMeter, type Arc, type Cell, type Cluster, type ComposeOptions, DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, FREE_TAAL_LABELS, type GridLang, LABELS, LAY_LABELS, type Layout, MARKS, MAX_ZOOM, MIN_ZOOM, NOTATION_FONT_URLS, PRACTICE_LABELS, type PrintOptions, type RenderOptions, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, type StaffOptions, type SwaralipiSheetElement, type TaalStaffOptions, type Transliterator, type ViewerLang, ZOOM_STEP, abcDuration, abcToSvg, alignRows, buildAbc, buildTaalAbc, cellMatra, composeCluster, defineSwaralipiSheet, docToAbc, fitSwaralipi, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
@@ -1,5 +1,5 @@
1
- import { isArcRaw, glideTargets, buildSchedule, drumPattern } from '../chunk-SEN266BY.js';
2
- import { hasKomal, isTaalFerta, sameTaal, TAALS, graceNotes } from '../chunk-MYVIQU4O.js';
1
+ import { isArcRaw, glideTargets, buildSchedule, drumPattern } from '../chunk-VCLLSNAK.js';
2
+ import { hasKomal, isTaalFerta, sameTaal, TAALS, graceNotes } from '../chunk-SR7NFEGL.js';
3
3
 
4
4
  // src/viewer/glyphs.ts
5
5
  var SWARABITAN_SHUDDHA = {
@@ -54,6 +54,11 @@ var FREE_TAAL_LABELS = {
54
54
  hi: "\u092E\u0941\u0915\u094D\u0924\u091B\u0902\u0926",
55
55
  en: "Free rhythm"
56
56
  };
57
+ var PRACTICE_LABELS = {
58
+ bn: { sheet: "\u09B8\u09CD\u09AC\u09B0\u09B2\u09BF\u09AA\u09BF \u2014 \u09AF\u09C7 \u09B2\u09BE\u0987\u09A8 \u09A5\u09C7\u0995\u09C7 \u09B6\u09C1\u09A8\u09A4\u09C7 \u099A\u09BE\u09A8, \u09B8\u09C7\u0996\u09BE\u09A8\u09C7 \u099B\u09C1\u0981\u09AF\u09BC\u09C7 \u09A6\u09BF\u09A8", repeat: "\u09B2\u09BE\u0987\u09A8\u099F\u09BF \u09AC\u09BE\u09B0\u09AC\u09BE\u09B0 \u09AC\u09BE\u099C\u09AC\u09C7", turn: "\u098F\u09AC\u09BE\u09B0 \u0986\u09AA\u09A8\u09BF \u0997\u09BE\u09A8" },
59
+ hi: { sheet: "\u0938\u094D\u0935\u0930\u0932\u093F\u092A\u093F \u2014 \u091C\u093F\u0938 \u092A\u0902\u0915\u094D\u0924\u093F \u0938\u0947 \u0938\u0941\u0928\u0928\u093E \u0939\u094B, \u0909\u0938\u0947 \u091B\u0941\u090F\u0901", repeat: "\u092A\u0902\u0915\u094D\u0924\u093F \u0926\u094B\u0939\u0930\u093E\u0908 \u091C\u093E \u0930\u0939\u0940 \u0939\u0948", turn: "\u0905\u092C \u0906\u092A \u0917\u093E\u0907\u090F" },
60
+ en: { sheet: "Swaralipi \u2014 tap a line to play from it", repeat: "Repeating the line", turn: "Your turn \u2014 sing" }
61
+ };
57
62
  function composeCluster(letter, opts) {
58
63
  const classes = [];
59
64
  if (opts.lang === "bn") {
@@ -496,7 +501,7 @@ function renderRow(row, partIdx, rowIdx, offset, ctx) {
496
501
  if (below.length > 0)
497
502
  out.push(`<span class="sw-below" style="grid-column:${col}">${renderAnnotation(below, ctx)}</span>`);
498
503
  });
499
- return `<div class="sw-row">${out.join("")}</div>`;
504
+ return `<div class="sw-row" data-row="${partIdx}.${rowIdx}">${out.join("")}</div>`;
500
505
  }
501
506
  function renderHeader(meta, ctx) {
502
507
  return renderMetaHeader(meta, ctx.lang, ctx.tr);
@@ -868,6 +873,55 @@ ${FONT_FACES}
868
873
  glyph fill/stroke instead (abcjs draws note heads/stems as filled paths). */
869
874
  .sw-staff .sw-hl, .sw-staff .sw-hl * { fill: var(--sw-hl-note, #d68400); stroke: var(--sw-hl-note, #d68400); }
870
875
 
876
+ /* ---- practice: tap a line, repeat it, sing it back ----
877
+ * Only a sheet a player drives is [seekable]. The whole line is the tap
878
+ * target, so it is the line that answers the pointer. A repeat shades its
879
+ * lines and fades the rest (focus, as Soundslice fades outside a loop); on the
880
+ * learner's pass the highlight goes hollow \u2014 it marks where to sing, not what
881
+ * is sounding \u2014 and the repeat's first line wears a "your turn" tag. None of
882
+ * it moves a column: backgrounds, opacity and an out-of-flow tag only. */
883
+ :host([seekable]) .sw-row { cursor: pointer; }
884
+ :host(:focus-visible) { outline: 2px solid var(--sw-focus, currentColor); outline-offset: 0.3em; border-radius: 0.2em; }
885
+ .sw-row { border-radius: 0.3em; }
886
+ @media (hover: hover) {
887
+ :host([seekable]) .sw-row:hover { background: var(--sw-row-hover, rgba(128, 128, 128, 0.08)); }
888
+ }
889
+ .sw-looping .sw-row:not(.sw-loop) { opacity: 0.6; }
890
+ .sw-row.sw-loop {
891
+ background: var(--sw-loop, rgba(255, 196, 64, 0.13));
892
+ box-shadow: inset 0.2em 0 var(--sw-loop-edge, #a85a00);
893
+ }
894
+ .sw-hl.sw-hl-turn { background: none; box-shadow: inset 0 0 0 0.09em var(--sw-loop-edge, #a85a00); }
895
+ /* The tag rides the line's above-band at its right end \u2014 a grid item spanning
896
+ every column, so it sizes no track \u2014 and is sticky, so on a phone where the
897
+ line runs past the screen it waits at the visible edge instead. */
898
+ .sw-row[data-turn]::after {
899
+ content: attr(data-turn);
900
+ grid-column: 1 / -1;
901
+ grid-row: 1;
902
+ justify-self: end;
903
+ align-self: start;
904
+ position: sticky;
905
+ right: 0.2em;
906
+ translate: 0 -0.3em;
907
+ font-size: max(0.6em, 0.7rem);
908
+ font-weight: 600;
909
+ line-height: 1.6;
910
+ padding: 0 0.7em;
911
+ border-radius: 1em;
912
+ color: var(--sw-turn-ink, #fff);
913
+ background: var(--sw-loop-edge, #a85a00);
914
+ }
915
+ .sw-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
916
+ @media (prefers-reduced-motion: no-preference) {
917
+ .sw-row { transition: opacity 0.25s, background-color 0.25s; }
918
+ }
919
+ @media print {
920
+ .sw-row { opacity: 1 !important; background: none !important; box-shadow: none !important; }
921
+ .sw-row[data-turn]::after { content: none; }
922
+ .sw-hl { background: none !important; box-shadow: none !important; }
923
+ }
924
+
871
925
  /* ---- element error box ---- */
872
926
  .sw-error {
873
927
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
@@ -1303,6 +1357,17 @@ async function loadLyricTransliterator(lang) {
1303
1357
 
1304
1358
  // src/viewer/element.ts
1305
1359
  var LANGS = /* @__PURE__ */ new Set(["bn", "hi", "en", "staff"]);
1360
+ var KEYS = "Space ArrowUp ArrowDown ArrowLeft ArrowRight Home End R T Escape Shift+ArrowUp Shift+ArrowDown";
1361
+ function nearest(els, x) {
1362
+ let best;
1363
+ let gap = Infinity;
1364
+ for (const el of els) {
1365
+ const r = el.getBoundingClientRect();
1366
+ const d = Math.abs(r.left + r.width / 2 - x);
1367
+ if (d < gap) [best, gap] = [el, d];
1368
+ }
1369
+ return best;
1370
+ }
1306
1371
  function ensureNotationFonts() {
1307
1372
  if (typeof FontFace === "undefined" || typeof document === "undefined") return;
1308
1373
  const present = /* @__PURE__ */ new Set();
@@ -1315,7 +1380,7 @@ function ensureNotationFonts() {
1315
1380
  var sharedSheet;
1316
1381
  function createElementClass() {
1317
1382
  return class SwaralipiSheet extends HTMLElement {
1318
- static observedAttributes = ["src", "lang", "hide-header", "min-spacing", "min-size"];
1383
+ static observedAttributes = ["src", "lang", "hide-header", "min-spacing", "min-size", "seekable"];
1319
1384
  #doc;
1320
1385
  #root;
1321
1386
  #adopted = false;
@@ -1327,10 +1392,20 @@ function createElementClass() {
1327
1392
  #resize;
1328
1393
  #fittedAt = -1;
1329
1394
  #zoom = 1;
1395
+ /** Where the pointer went down, to tell a tap from a drag. */
1396
+ #down;
1397
+ #marks = { loop: null, turn: false };
1398
+ /** Attributes `seekable` added, and so takes away again. */
1399
+ #owned = [];
1330
1400
  validate = false;
1331
1401
  constructor() {
1332
1402
  super();
1333
1403
  this.#root = this.attachShadow({ mode: "open" });
1404
+ this.#root.addEventListener("pointerdown", (e) => {
1405
+ const { clientX, clientY } = e;
1406
+ this.#down = [clientX, clientY];
1407
+ });
1408
+ this.#root.addEventListener("click", (e) => this.#onClick(e));
1334
1409
  if (typeof CSSStyleSheet !== "undefined" && "replaceSync" in CSSStyleSheet.prototype && "adoptedStyleSheets" in this.#root) {
1335
1410
  if (!sharedSheet) {
1336
1411
  sharedSheet = new CSSStyleSheet();
@@ -1364,6 +1439,27 @@ function createElementClass() {
1364
1439
  const attr = this.getAttribute("lang");
1365
1440
  return attr !== null && LANGS.has(attr) ? attr : "bn";
1366
1441
  }
1442
+ get #practice() {
1443
+ const lang = this.#lang;
1444
+ return PRACTICE_LABELS[lang === "staff" ? "en" : lang];
1445
+ }
1446
+ /** A tap on a line plays from the note nearest the tap: the whole line is
1447
+ * the target (WCAG 2.5.8 counts a position inside one target as picking
1448
+ * a value), fired on the up-event like any click (2.5.2). The player bound
1449
+ * by bindSheet answers it; a host can veto it with preventDefault(). */
1450
+ #onClick(e) {
1451
+ if (!this.hasAttribute("seekable") || e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey) return;
1452
+ if (this.#down !== void 0 && Math.hypot(e.clientX - this.#down[0], e.clientY - this.#down[1]) > 6) return;
1453
+ const target = e.target;
1454
+ const row = target.closest(".sw-row");
1455
+ const notes = row === null ? [] : [...row.querySelectorAll("[data-note]:not(.sw-bar)")];
1456
+ const hit = target.closest("[data-note]:not(.sw-bar)") ?? nearest(notes, e.clientX);
1457
+ const path = hit?.getAttribute("data-note");
1458
+ if (path == null) return;
1459
+ this.dispatchEvent(
1460
+ new CustomEvent("swaralipi-seek", { detail: { path, extend: e.shiftKey }, bubbles: true, cancelable: true })
1461
+ );
1462
+ }
1367
1463
  attributeChangedCallback(name, oldValue, newValue) {
1368
1464
  if (oldValue === newValue) return;
1369
1465
  if (name === "src") {
@@ -1374,6 +1470,19 @@ function createElementClass() {
1374
1470
  this.fit();
1375
1471
  return;
1376
1472
  }
1473
+ if (name === "seekable") {
1474
+ for (const a of this.#owned) this.removeAttribute(a);
1475
+ this.#owned = [];
1476
+ if (newValue === null) return;
1477
+ const want = { tabindex: "0", role: "region", "aria-label": this.#practice.sheet, "aria-keyshortcuts": KEYS };
1478
+ for (const [a, v] of Object.entries(want)) {
1479
+ if (this.hasAttribute(a)) continue;
1480
+ this.setAttribute(a, v);
1481
+ this.#owned.push(a);
1482
+ }
1483
+ return;
1484
+ }
1485
+ if (name === "lang" && this.#owned.includes("aria-label")) this.setAttribute("aria-label", this.#practice.sheet);
1377
1486
  if (name === "lang") this.#ensureTransliterator();
1378
1487
  this.#render();
1379
1488
  }
@@ -1486,7 +1595,9 @@ function createElementClass() {
1486
1595
  if (lang === "staff") {
1487
1596
  const doc = this.#doc;
1488
1597
  void renderStaffHtml(doc, this.staffOptions).then((html2) => {
1489
- if (seq === this.#renderSeq) this.#root.innerHTML = this.#styleFallback() + html2;
1598
+ if (seq !== this.#renderSeq) return;
1599
+ this.#root.innerHTML = this.#styleFallback() + html2;
1600
+ this.#applyMarks();
1490
1601
  }).catch(() => void 0);
1491
1602
  return;
1492
1603
  }
@@ -1496,8 +1607,34 @@ function createElementClass() {
1496
1607
  transliterate: this.#translitLang === lang ? this.#translit : void 0
1497
1608
  });
1498
1609
  this.#root.innerHTML = this.#styleFallback() + html;
1610
+ this.#applyMarks();
1499
1611
  this.#fit();
1500
1612
  }
1613
+ markLoop(loop, yourTurn = false) {
1614
+ this.#marks = { loop, turn: yourTurn };
1615
+ this.#applyMarks();
1616
+ }
1617
+ /** Shade the repeated lines and dim the rest; the first of them carries
1618
+ * the "your turn" cue. A polite live region says the same aloud. */
1619
+ #applyMarks() {
1620
+ const { loop, turn } = this.#marks;
1621
+ const rows = [...this.#root.querySelectorAll(".sw-row")];
1622
+ const at = (row) => rows.findIndex((r) => r.dataset.row === row);
1623
+ const [a, b] = loop === null ? [0, -1] : [at(loop.from), at(loop.to)];
1624
+ rows.forEach((r, i) => {
1625
+ r.classList.toggle("sw-loop", i >= a && i <= b);
1626
+ if (turn && i === a) r.dataset.turn = this.#practice.turn;
1627
+ else delete r.dataset.turn;
1628
+ });
1629
+ this.#root.querySelector(".sw-block")?.classList.toggle("sw-looping", loop !== null);
1630
+ let live = this.#root.querySelector(".sw-live");
1631
+ if (live === null) {
1632
+ live = Object.assign(document.createElement("div"), { className: "sw-live" });
1633
+ live.setAttribute("role", "status");
1634
+ this.#root.append(live);
1635
+ }
1636
+ live.textContent = turn ? this.#practice.turn : loop !== null ? this.#practice.repeat : "";
1637
+ }
1501
1638
  #renderError(message) {
1502
1639
  const div = document.createElement("div");
1503
1640
  div.className = "sw-error";
@@ -1505,14 +1642,15 @@ function createElementClass() {
1505
1642
  this.#root.innerHTML = this.#styleFallback();
1506
1643
  this.#root.appendChild(div);
1507
1644
  }
1508
- highlight(path) {
1645
+ highlight(path, yourTurn = false) {
1509
1646
  if (!/^\d+\.\d+\.\d+$/.test(path)) return null;
1510
1647
  const el = this.#root.querySelector(`[data-note="${path}"]`);
1511
1648
  el?.classList.add("sw-hl");
1649
+ el?.classList.toggle("sw-hl-turn", yourTurn);
1512
1650
  return el;
1513
1651
  }
1514
1652
  clearHighlights() {
1515
- for (const el of this.#root.querySelectorAll(".sw-hl")) el.classList.remove("sw-hl");
1653
+ for (const el of this.#root.querySelectorAll(".sw-hl")) el.classList.remove("sw-hl", "sw-hl-turn");
1516
1654
  }
1517
1655
  print(options = {}) {
1518
1656
  if (this.#doc === void 0) return;
@@ -1541,6 +1679,6 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
1541
1679
  return ctor;
1542
1680
  }
1543
1681
 
1544
- export { DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, FREE_TAAL_LABELS, LABELS, LAY_LABELS, MARKS, MAX_ZOOM, MIN_ZOOM, NOTATION_FONT_URLS, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, ZOOM_STEP, abcDuration, abcToSvg, alignRows, buildAbc, buildTaalAbc, cellMatra, composeCluster, defineSwaralipiSheet, docToAbc, fitSwaralipi, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
1682
+ export { DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, FREE_TAAL_LABELS, LABELS, LAY_LABELS, MARKS, MAX_ZOOM, MIN_ZOOM, NOTATION_FONT_URLS, PRACTICE_LABELS, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, ZOOM_STEP, abcDuration, abcToSvg, alignRows, buildAbc, buildTaalAbc, cellMatra, composeCluster, defineSwaralipiSheet, docToAbc, fitSwaralipi, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
1545
1683
  //# sourceMappingURL=index.js.map
1546
1684
  //# sourceMappingURL=index.js.map