@ugfoundation/swaralipi-js 0.7.0 → 0.7.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.
@@ -184,22 +184,31 @@ 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.
191
201
  *
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
202
  * `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.
203
+ * fit-to-width, or below the readability floor deliberately; past that point
204
+ * `.sw-scroll` panning is expected, same as a photo viewer. `minScale` is a
205
+ * second, relative floor used by print, where paper cannot scroll.
198
206
  *
199
207
  * Self-contained on purpose — print.ts serialises this function into the
200
- * print page, so it must not close over anything or import anything.
208
+ * print page, so it must not close over anything or import anything (the
209
+ * defaults below are literals for the same reason).
201
210
  */
202
- declare function fitSwaralipi(root: Document | ShadowRoot | HTMLElement, minScale?: number, zoom?: number): void;
211
+ declare function fitSwaralipi(root: Document | ShadowRoot | HTMLElement, minScale?: number, zoom?: number, minSpacing?: number, minSize?: number): void;
203
212
  /** Zoom bounds/step for zoomIn()/zoomOut()/the `zoom` property — exported so
204
213
  * a consumer's own zoom UI (e.g. a slider) can match the element's clamp. */
205
214
  declare const MIN_ZOOM = 0.5;
@@ -215,6 +224,11 @@ declare const ZOOM_STEP = 1.2;
215
224
  * --sw-font font stack (default: Noto Sans Bengali + system Bengali)
216
225
  * --sw-size base font size
217
226
  * --sw-cell-gap horizontal gap between source cells
227
+ * --sw-tight 0–1 multiplier on the column spacing (gap + matra floor);
228
+ * set by fitSwaralipi to compress before shrinking type
229
+ * --sw-matra minimum width of one matra column
230
+ * --sw-header-min floor for the নাম/তাল/আবর্তন header (it must stay legible
231
+ * when the notation is scaled down to fit a phone)
218
232
  * --sw-hl player-highlight background (P2.3)
219
233
  * --sw-rule header rule / muted line color
220
234
  *
@@ -492,6 +506,13 @@ interface SwaralipiSheetElement extends HTMLElement {
492
506
  staffOptions: StaffOptions;
493
507
  /** Re-fit the notation to the element's current width. */
494
508
  fit(): void;
509
+ /** How far the column spacing may be compressed to fit a narrow container
510
+ * before the type is scaled down at all, 0–1 (attribute `min-spacing`;
511
+ * undefined = the library default). Lower = tighter columns, bigger glyphs. */
512
+ minSpacing: number | undefined;
513
+ /** Smallest notation size in px the fit may produce (attribute `min-size`);
514
+ * past it the sheet scrolls horizontally instead of shrinking further. */
515
+ minSize: number | undefined;
495
516
  /** Zoom factor on top of fit-to-width (1 = fit; clamped to
496
517
  * [MIN_ZOOM, MAX_ZOOM]). Assigning re-fits immediately. Resets to 1 on the
497
518
  * next doc/lang re-render — a resize alone (fit()) does not reset it. */
@@ -184,22 +184,31 @@ 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.
191
201
  *
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
202
  * `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.
203
+ * fit-to-width, or below the readability floor deliberately; past that point
204
+ * `.sw-scroll` panning is expected, same as a photo viewer. `minScale` is a
205
+ * second, relative floor used by print, where paper cannot scroll.
198
206
  *
199
207
  * Self-contained on purpose — print.ts serialises this function into the
200
- * print page, so it must not close over anything or import anything.
208
+ * print page, so it must not close over anything or import anything (the
209
+ * defaults below are literals for the same reason).
201
210
  */
202
- declare function fitSwaralipi(root: Document | ShadowRoot | HTMLElement, minScale?: number, zoom?: number): void;
211
+ declare function fitSwaralipi(root: Document | ShadowRoot | HTMLElement, minScale?: number, zoom?: number, minSpacing?: number, minSize?: number): void;
203
212
  /** Zoom bounds/step for zoomIn()/zoomOut()/the `zoom` property — exported so
204
213
  * a consumer's own zoom UI (e.g. a slider) can match the element's clamp. */
205
214
  declare const MIN_ZOOM = 0.5;
@@ -215,6 +224,11 @@ declare const ZOOM_STEP = 1.2;
215
224
  * --sw-font font stack (default: Noto Sans Bengali + system Bengali)
216
225
  * --sw-size base font size
217
226
  * --sw-cell-gap horizontal gap between source cells
227
+ * --sw-tight 0–1 multiplier on the column spacing (gap + matra floor);
228
+ * set by fitSwaralipi to compress before shrinking type
229
+ * --sw-matra minimum width of one matra column
230
+ * --sw-header-min floor for the নাম/তাল/আবর্তন header (it must stay legible
231
+ * when the notation is scaled down to fit a phone)
218
232
  * --sw-hl player-highlight background (P2.3)
219
233
  * --sw-rule header rule / muted line color
220
234
  *
@@ -492,6 +506,13 @@ interface SwaralipiSheetElement extends HTMLElement {
492
506
  staffOptions: StaffOptions;
493
507
  /** Re-fit the notation to the element's current width. */
494
508
  fit(): void;
509
+ /** How far the column spacing may be compressed to fit a narrow container
510
+ * before the type is scaled down at all, 0–1 (attribute `min-spacing`;
511
+ * undefined = the library default). Lower = tighter columns, bigger glyphs. */
512
+ minSpacing: number | undefined;
513
+ /** Smallest notation size in px the fit may produce (attribute `min-size`);
514
+ * past it the sheet scrolls horizontally instead of shrinking further. */
515
+ minSize: number | undefined;
495
516
  /** Zoom factor on top of fit-to-width (1 = fit; clamped to
496
517
  * [MIN_ZOOM, MAX_ZOOM]). Assigning re-fits immediately. Resets to 1 on the
497
518
  * 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,27 @@ 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
+ for (const step of [0.75, 0.5, 0.25, 0]) {
496
+ if (natural <= available || minSpacing >= 1) break;
497
+ sheet.style.setProperty("--sw-tight", String(minSpacing + (1 - minSpacing) * step));
498
+ natural = block.scrollWidth;
499
+ }
500
+ const base = parseFloat(getComputedStyle(sheet).fontSize);
501
+ let fit = natural > available ? Math.max(minScale, available / natural) : 1;
502
+ if (base > 0 && minSize > 0) fit = Math.max(fit, Math.min(1, minSize / base));
495
503
  const scale = fit * zoom;
496
504
  if (scale === 1) continue;
497
- const base = parseFloat(getComputedStyle(sheet).fontSize);
498
505
  sheet.style.setProperty("--sw-size", `${(base * scale).toFixed(3)}px`);
499
506
  }
500
507
  }
@@ -531,7 +538,10 @@ ${FONT_FACES}
531
538
  margin: 0 0 0.75em;
532
539
  padding: 0 0 0.5em;
533
540
  border-bottom: 1px solid var(--sw-rule, rgba(128, 128, 128, 0.35));
534
- font-size: 0.95em;
541
+ /* Scales with the sheet, but never below the floor: fitSwaralipi shrinks
542
+ .sw-sheet's font-size to fit the notation, and the metadata is prose \u2014
543
+ it has no columns to align and must stay readable on a phone. */
544
+ font-size: max(0.95em, var(--sw-header-min, 0.95rem));
535
545
  }
536
546
  .sw-meta-row { display: flex; gap: 0.3em; }
537
547
  .sw-meta-label { font-weight: 600; min-width: 4.5em; }
@@ -547,7 +557,7 @@ ${FONT_FACES}
547
557
  .sw-scroll { overflow-x: auto; }
548
558
  .sw-block {
549
559
  display: grid;
550
- column-gap: var(--sw-cell-gap, 0.4em);
560
+ column-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1));
551
561
  align-items: end;
552
562
  width: max-content;
553
563
  }
@@ -1030,7 +1040,7 @@ body { width: 178mm; color: #111; background: #fff; -webkit-print-color-adjust:
1030
1040
  </style>
1031
1041
  </head>
1032
1042
  <body>${top}${bodyHtml}${bottom}
1033
- <script>window.__swFit=function(){(${fitSwaralipi.toString()})(document,0.45)};window.__swFit()</script>
1043
+ <script>window.__swFit=function(){(${fitSwaralipi.toString()})(document,0.45,1,undefined,0)};window.__swFit()</script>
1034
1044
  </body>
1035
1045
  </html>`;
1036
1046
  }
@@ -1121,7 +1131,7 @@ function ensureNotationFonts() {
1121
1131
  var sharedSheet;
1122
1132
  function createElementClass() {
1123
1133
  return class SwaralipiSheet extends HTMLElement {
1124
- static observedAttributes = ["src", "lang", "hide-header"];
1134
+ static observedAttributes = ["src", "lang", "hide-header", "min-spacing", "min-size"];
1125
1135
  #doc;
1126
1136
  #root;
1127
1137
  #adopted = false;
@@ -1176,6 +1186,10 @@ function createElementClass() {
1176
1186
  if (newValue !== null) void this.#load(newValue);
1177
1187
  return;
1178
1188
  }
1189
+ if (name === "min-spacing" || name === "min-size") {
1190
+ this.fit();
1191
+ return;
1192
+ }
1179
1193
  if (name === "lang") this.#ensureTransliterator();
1180
1194
  this.#render();
1181
1195
  }
@@ -1199,7 +1213,28 @@ function createElementClass() {
1199
1213
  }
1200
1214
  /** Scale the notation to fit the available width, times the zoom factor. */
1201
1215
  #fit() {
1202
- fitSwaralipi(this.#root, 0, this.#zoom);
1216
+ fitSwaralipi(this.#root, 0, this.#zoom, this.minSpacing, this.minSize);
1217
+ }
1218
+ /** Numeric attribute, or undefined for fitSwaralipi's own default. */
1219
+ #attrNum(name) {
1220
+ const value = parseFloat(this.getAttribute(name) ?? "");
1221
+ return Number.isFinite(value) ? value : void 0;
1222
+ }
1223
+ #setAttrNum(name, value) {
1224
+ if (value === void 0) this.removeAttribute(name);
1225
+ else this.setAttribute(name, String(value));
1226
+ }
1227
+ get minSpacing() {
1228
+ return this.#attrNum("min-spacing");
1229
+ }
1230
+ set minSpacing(value) {
1231
+ this.#setAttrNum("min-spacing", value);
1232
+ }
1233
+ get minSize() {
1234
+ return this.#attrNum("min-size");
1235
+ }
1236
+ set minSize(value) {
1237
+ this.#setAttrNum("min-size", value);
1203
1238
  }
1204
1239
  /** Re-fit the current render (public for hosts that resize the sheet
1205
1240
  * themselves, e.g. a tab that was hidden while the document loaded). */