@ugfoundation/swaralipi-js 0.7.0 → 0.7.2

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.
@@ -184,22 +184,33 @@ declare function renderToHtml(doc: Swaralipi, opts?: RenderOptions): string;
184
184
  * top.
185
185
  *
186
186
  * The whole document is one grid (see render.ts), so a sheet has exactly one
187
- * natural width — a wide তাল overruns a phone or an A4 page. Everything
188
- * inside is em-relative, so scaling the sheet's `font-size` scales the
189
- * notation without disturbing the column alignment; a `transform` would
190
- * scale it too, but would also break print pagination.
187
+ * natural width — a wide তাল overruns a phone or an A4 page. Two levers, in
188
+ * this order, because they cost different things:
189
+ *
190
+ * 1. **Spacing.** `--sw-cell-gap` and the `--sw-matra` column floor are
191
+ * 25–35 % of that natural width (measured over the corpus), and giving
192
+ * them up costs only air between the columns — the alignment is
193
+ * untouched, since every row still borrows the same tracks. Compressed
194
+ * only as far as the container demands, down to `minSpacing`.
195
+ * 2. **Type size.** Everything inside is em-relative, so scaling the sheet's
196
+ * `font-size` scales the notation without disturbing the columns (a
197
+ * `transform` would scale it too, but would also break print
198
+ * pagination). This is what actually makes the glyphs smaller, so it
199
+ * runs second and stops at `minSize` px — past that the block scrolls
200
+ * inside `.sw-scroll` rather than becoming unreadable. It also runs the
201
+ * other way: a sheet narrower than its container grows (to 2x) rather
202
+ * than leaving dead space to the right of every row.
191
203
  *
192
- * Free-scaling by default (no floor): the sheet should always fit on first
193
- * paint rather than fall back to `.sw-scroll`'s horizontal scrollbar — a
194
- * `minScale` floor exists only for print, where paper cannot scroll.
195
204
  * `zoom` (default 1) multiplies the fit scale so callers can zoom in past
196
- * fit-to-width; past that point `.sw-scroll` panning is expected, same as a
197
- * photo viewer.
205
+ * fit-to-width, or below the readability floor deliberately; past that point
206
+ * `.sw-scroll` panning is expected, same as a photo viewer. `minScale` is a
207
+ * second, relative floor used by print, where paper cannot scroll.
198
208
  *
199
209
  * Self-contained on purpose — print.ts serialises this function into the
200
- * print page, so it must not close over anything or import anything.
210
+ * print page, so it must not close over anything or import anything (the
211
+ * defaults below are literals for the same reason).
201
212
  */
202
- declare function fitSwaralipi(root: Document | ShadowRoot | HTMLElement, minScale?: number, zoom?: number): void;
213
+ declare function fitSwaralipi(root: Document | ShadowRoot | HTMLElement, minScale?: number, zoom?: number, minSpacing?: number, minSize?: number): void;
203
214
  /** Zoom bounds/step for zoomIn()/zoomOut()/the `zoom` property — exported so
204
215
  * a consumer's own zoom UI (e.g. a slider) can match the element's clamp. */
205
216
  declare const MIN_ZOOM = 0.5;
@@ -215,6 +226,11 @@ declare const ZOOM_STEP = 1.2;
215
226
  * --sw-font font stack (default: Noto Sans Bengali + system Bengali)
216
227
  * --sw-size base font size
217
228
  * --sw-cell-gap horizontal gap between source cells
229
+ * --sw-tight 0–1 multiplier on the column spacing (gap + matra floor);
230
+ * set by fitSwaralipi to compress before shrinking type
231
+ * --sw-matra minimum width of one matra column
232
+ * --sw-header-min floor for the নাম/তাল/আবর্তন header (it must stay legible
233
+ * when the notation is scaled down to fit a phone)
218
234
  * --sw-hl player-highlight background (P2.3)
219
235
  * --sw-rule header rule / muted line color
220
236
  *
@@ -492,6 +508,13 @@ interface SwaralipiSheetElement extends HTMLElement {
492
508
  staffOptions: StaffOptions;
493
509
  /** Re-fit the notation to the element's current width. */
494
510
  fit(): void;
511
+ /** How far the column spacing may be compressed to fit a narrow container
512
+ * before the type is scaled down at all, 0–1 (attribute `min-spacing`;
513
+ * undefined = the library default). Lower = tighter columns, bigger glyphs. */
514
+ minSpacing: number | undefined;
515
+ /** Smallest notation size in px the fit may produce (attribute `min-size`);
516
+ * past it the sheet scrolls horizontally instead of shrinking further. */
517
+ minSize: number | undefined;
495
518
  /** Zoom factor on top of fit-to-width (1 = fit; clamped to
496
519
  * [MIN_ZOOM, MAX_ZOOM]). Assigning re-fits immediately. Resets to 1 on the
497
520
  * next doc/lang re-render — a resize alone (fit()) does not reset it. */
@@ -184,22 +184,33 @@ declare function renderToHtml(doc: Swaralipi, opts?: RenderOptions): string;
184
184
  * top.
185
185
  *
186
186
  * The whole document is one grid (see render.ts), so a sheet has exactly one
187
- * natural width — a wide তাল overruns a phone or an A4 page. Everything
188
- * inside is em-relative, so scaling the sheet's `font-size` scales the
189
- * notation without disturbing the column alignment; a `transform` would
190
- * scale it too, but would also break print pagination.
187
+ * natural width — a wide তাল overruns a phone or an A4 page. Two levers, in
188
+ * this order, because they cost different things:
189
+ *
190
+ * 1. **Spacing.** `--sw-cell-gap` and the `--sw-matra` column floor are
191
+ * 25–35 % of that natural width (measured over the corpus), and giving
192
+ * them up costs only air between the columns — the alignment is
193
+ * untouched, since every row still borrows the same tracks. Compressed
194
+ * only as far as the container demands, down to `minSpacing`.
195
+ * 2. **Type size.** Everything inside is em-relative, so scaling the sheet's
196
+ * `font-size` scales the notation without disturbing the columns (a
197
+ * `transform` would scale it too, but would also break print
198
+ * pagination). This is what actually makes the glyphs smaller, so it
199
+ * runs second and stops at `minSize` px — past that the block scrolls
200
+ * inside `.sw-scroll` rather than becoming unreadable. It also runs the
201
+ * other way: a sheet narrower than its container grows (to 2x) rather
202
+ * than leaving dead space to the right of every row.
191
203
  *
192
- * Free-scaling by default (no floor): the sheet should always fit on first
193
- * paint rather than fall back to `.sw-scroll`'s horizontal scrollbar — a
194
- * `minScale` floor exists only for print, where paper cannot scroll.
195
204
  * `zoom` (default 1) multiplies the fit scale so callers can zoom in past
196
- * fit-to-width; past that point `.sw-scroll` panning is expected, same as a
197
- * photo viewer.
205
+ * fit-to-width, or below the readability floor deliberately; past that point
206
+ * `.sw-scroll` panning is expected, same as a photo viewer. `minScale` is a
207
+ * second, relative floor used by print, where paper cannot scroll.
198
208
  *
199
209
  * Self-contained on purpose — print.ts serialises this function into the
200
- * print page, so it must not close over anything or import anything.
210
+ * print page, so it must not close over anything or import anything (the
211
+ * defaults below are literals for the same reason).
201
212
  */
202
- declare function fitSwaralipi(root: Document | ShadowRoot | HTMLElement, minScale?: number, zoom?: number): void;
213
+ declare function fitSwaralipi(root: Document | ShadowRoot | HTMLElement, minScale?: number, zoom?: number, minSpacing?: number, minSize?: number): void;
203
214
  /** Zoom bounds/step for zoomIn()/zoomOut()/the `zoom` property — exported so
204
215
  * a consumer's own zoom UI (e.g. a slider) can match the element's clamp. */
205
216
  declare const MIN_ZOOM = 0.5;
@@ -215,6 +226,11 @@ declare const ZOOM_STEP = 1.2;
215
226
  * --sw-font font stack (default: Noto Sans Bengali + system Bengali)
216
227
  * --sw-size base font size
217
228
  * --sw-cell-gap horizontal gap between source cells
229
+ * --sw-tight 0–1 multiplier on the column spacing (gap + matra floor);
230
+ * set by fitSwaralipi to compress before shrinking type
231
+ * --sw-matra minimum width of one matra column
232
+ * --sw-header-min floor for the নাম/তাল/আবর্তন header (it must stay legible
233
+ * when the notation is scaled down to fit a phone)
218
234
  * --sw-hl player-highlight background (P2.3)
219
235
  * --sw-rule header rule / muted line color
220
236
  *
@@ -492,6 +508,13 @@ interface SwaralipiSheetElement extends HTMLElement {
492
508
  staffOptions: StaffOptions;
493
509
  /** Re-fit the notation to the element's current width. */
494
510
  fit(): void;
511
+ /** How far the column spacing may be compressed to fit a narrow container
512
+ * before the type is scaled down at all, 0–1 (attribute `min-spacing`;
513
+ * undefined = the library default). Lower = tighter columns, bigger glyphs. */
514
+ minSpacing: number | undefined;
515
+ /** Smallest notation size in px the fit may produce (attribute `min-size`);
516
+ * past it the sheet scrolls horizontally instead of shrinking further. */
517
+ minSize: number | undefined;
495
518
  /** Zoom factor on top of fit-to-width (1 = fit; clamped to
496
519
  * [MIN_ZOOM, MAX_ZOOM]). Assigning re-fits immediately. Resets to 1 on the
497
520
  * next doc/lang re-render — a resize alone (fit()) does not reset it. */
@@ -464,7 +464,7 @@ function renderToHtml(doc, opts = {}) {
464
464
  const tr = lang !== "bn" && opts.transliterate ? opts.transliterate : (s) => s;
465
465
  const ctx = { lang, marks: MARKS[lang], tr };
466
466
  const layout = alignRows(doc.parts.flatMap((p) => p.rows.map((r) => groupRowIntoCells(r.notes))));
467
- const tracks = `grid-template-columns:max-content repeat(${layout.width},minmax(var(--sw-matra, 1.4em),max-content))`;
467
+ const tracks = `grid-template-columns:max-content repeat(${layout.width},minmax(calc(var(--sw-matra, 1.4em) * var(--sw-tight, 1)),max-content))`;
468
468
  const out = [`<div class="sw-sheet" lang="${lang}">`];
469
469
  if (opts.showHeader !== false) out.push(renderHeader(doc.meta, ctx));
470
470
  out.push(`<div class="sw-scroll"><div class="sw-block" style="${tracks}">`);
@@ -481,20 +481,35 @@ function renderToHtml(doc, opts = {}) {
481
481
  }
482
482
 
483
483
  // src/viewer/fit.ts
484
- function fitSwaralipi(root, minScale = 0, zoom = 1) {
484
+ function fitSwaralipi(root, minScale = 0, zoom = 1, minSpacing = 0.35, minSize = 12) {
485
485
  const sheets = root.querySelectorAll(".sw-sheet");
486
486
  for (const sheet of sheets) {
487
487
  const scroll = sheet.querySelector(".sw-scroll");
488
488
  const block = sheet.querySelector(".sw-block");
489
489
  if (scroll === null || block === null) continue;
490
490
  sheet.style.removeProperty("--sw-size");
491
+ sheet.style.removeProperty("--sw-tight");
491
492
  const available = scroll.clientWidth;
492
- const natural = block.scrollWidth;
493
+ let natural = block.scrollWidth;
493
494
  if (available === 0 || natural === 0) continue;
494
- const fit = natural > available ? Math.max(minScale, available / natural) : 1;
495
+ if (natural > available && minSpacing < 1) {
496
+ let loose = minSpacing;
497
+ let tight = 1;
498
+ for (let i = 0; i < 6; i++) {
499
+ const mid = (loose + tight) / 2;
500
+ sheet.style.setProperty("--sw-tight", String(mid));
501
+ if (block.scrollWidth <= available) loose = mid;
502
+ else tight = mid;
503
+ }
504
+ sheet.style.setProperty("--sw-tight", String(loose));
505
+ natural = block.scrollWidth;
506
+ }
507
+ const base = parseFloat(getComputedStyle(sheet).fontSize);
508
+ if (!(base > 0)) continue;
509
+ let fit = Math.max(minScale, Math.min((available - 1) / natural, 2));
510
+ if (minSize > 0) fit = Math.max(fit, Math.min(1, minSize / base));
495
511
  const scale = fit * zoom;
496
512
  if (scale === 1) continue;
497
- const base = parseFloat(getComputedStyle(sheet).fontSize);
498
513
  sheet.style.setProperty("--sw-size", `${(base * scale).toFixed(3)}px`);
499
514
  }
500
515
  }
@@ -531,7 +546,10 @@ ${FONT_FACES}
531
546
  margin: 0 0 0.75em;
532
547
  padding: 0 0 0.5em;
533
548
  border-bottom: 1px solid var(--sw-rule, rgba(128, 128, 128, 0.35));
534
- font-size: 0.95em;
549
+ /* Scales with the sheet, but never below the floor: fitSwaralipi shrinks
550
+ .sw-sheet's font-size to fit the notation, and the metadata is prose \u2014
551
+ it has no columns to align and must stay readable on a phone. */
552
+ font-size: max(0.95em, var(--sw-header-min, 0.95rem));
535
553
  }
536
554
  .sw-meta-row { display: flex; gap: 0.3em; }
537
555
  .sw-meta-label { font-weight: 600; min-width: 4.5em; }
@@ -547,7 +565,7 @@ ${FONT_FACES}
547
565
  .sw-scroll { overflow-x: auto; }
548
566
  .sw-block {
549
567
  display: grid;
550
- column-gap: var(--sw-cell-gap, 0.4em);
568
+ column-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1));
551
569
  align-items: end;
552
570
  width: max-content;
553
571
  }
@@ -1030,7 +1048,7 @@ body { width: 178mm; color: #111; background: #fff; -webkit-print-color-adjust:
1030
1048
  </style>
1031
1049
  </head>
1032
1050
  <body>${top}${bodyHtml}${bottom}
1033
- <script>window.__swFit=function(){(${fitSwaralipi.toString()})(document,0.45)};window.__swFit()</script>
1051
+ <script>window.__swFit=function(){(${fitSwaralipi.toString()})(document,0.45,1,undefined,0)};window.__swFit()</script>
1034
1052
  </body>
1035
1053
  </html>`;
1036
1054
  }
@@ -1121,7 +1139,7 @@ function ensureNotationFonts() {
1121
1139
  var sharedSheet;
1122
1140
  function createElementClass() {
1123
1141
  return class SwaralipiSheet extends HTMLElement {
1124
- static observedAttributes = ["src", "lang", "hide-header"];
1142
+ static observedAttributes = ["src", "lang", "hide-header", "min-spacing", "min-size"];
1125
1143
  #doc;
1126
1144
  #root;
1127
1145
  #adopted = false;
@@ -1176,6 +1194,10 @@ function createElementClass() {
1176
1194
  if (newValue !== null) void this.#load(newValue);
1177
1195
  return;
1178
1196
  }
1197
+ if (name === "min-spacing" || name === "min-size") {
1198
+ this.fit();
1199
+ return;
1200
+ }
1179
1201
  if (name === "lang") this.#ensureTransliterator();
1180
1202
  this.#render();
1181
1203
  }
@@ -1199,7 +1221,28 @@ function createElementClass() {
1199
1221
  }
1200
1222
  /** Scale the notation to fit the available width, times the zoom factor. */
1201
1223
  #fit() {
1202
- fitSwaralipi(this.#root, 0, this.#zoom);
1224
+ fitSwaralipi(this.#root, 0, this.#zoom, this.minSpacing, this.minSize);
1225
+ }
1226
+ /** Numeric attribute, or undefined for fitSwaralipi's own default. */
1227
+ #attrNum(name) {
1228
+ const value = parseFloat(this.getAttribute(name) ?? "");
1229
+ return Number.isFinite(value) ? value : void 0;
1230
+ }
1231
+ #setAttrNum(name, value) {
1232
+ if (value === void 0) this.removeAttribute(name);
1233
+ else this.setAttribute(name, String(value));
1234
+ }
1235
+ get minSpacing() {
1236
+ return this.#attrNum("min-spacing");
1237
+ }
1238
+ set minSpacing(value) {
1239
+ this.#setAttrNum("min-spacing", value);
1240
+ }
1241
+ get minSize() {
1242
+ return this.#attrNum("min-size");
1243
+ }
1244
+ set minSize(value) {
1245
+ this.#setAttrNum("min-size", value);
1203
1246
  }
1204
1247
  /** Re-fit the current render (public for hosts that resize the sheet
1205
1248
  * themselves, e.g. a tab that was hidden while the document loaded). */
@@ -1273,6 +1316,9 @@ function createElementClass() {
1273
1316
  });
1274
1317
  this.#root.innerHTML = this.#styleFallback() + html;
1275
1318
  this.#fit();
1319
+ void document.fonts?.ready.then(() => {
1320
+ if (seq === this.#renderSeq) this.fit();
1321
+ });
1276
1322
  }
1277
1323
  #renderError(message) {
1278
1324
  const div = document.createElement("div");