zsbqb-static 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/css/app.css CHANGED
@@ -804,21 +804,26 @@ th { color: var(--muted); font-weight: 600; font-size: 13px; background: var(--b
804
804
  align-items: start;
805
805
  }
806
806
  .alpha-rail {
807
- /* 热区相对 28px 栏宽向外扩一截,便于鼠标/触屏滑动选字母 */
807
+ /* 热区相对 28px 栏宽向外扩一截,便于鼠标/触屏滑动选字母。
808
+ 高度由 JS 按「当前位置到视口底」写入 --alpha-rail-fit,避免 100vh
809
+ 从标题下方起算把字母拉长,点 B 实际打到 G。 */
808
810
  --alpha-rail-pad-start: 14px;
809
811
  --alpha-rail-pad-end: 20px;
810
812
  --alpha-rail-pad-y: 10px;
811
813
  position: sticky;
812
- top: 0;
814
+ top: 8px;
813
815
  z-index: 15;
814
816
  display: flex;
815
817
  flex-direction: column;
816
- gap: 4px;
818
+ justify-content: space-between;
819
+ gap: 0;
817
820
  width: 28px;
818
- box-sizing: content-box;
819
- max-height: calc(100vh - 40px);
820
- overflow-y: auto;
821
- overflow-x: hidden;
821
+ box-sizing: border-box;
822
+ height: var(--alpha-rail-fit, auto);
823
+ max-height: calc(100vh - 16px);
824
+ max-height: calc(100svh - 16px);
825
+ max-height: calc(100dvh - 16px);
826
+ overflow: hidden;
822
827
  overscroll-behavior: contain;
823
828
  padding: var(--alpha-rail-pad-y) var(--alpha-rail-pad-end) var(--alpha-rail-pad-y) var(--alpha-rail-pad-start);
824
829
  margin: calc(var(--alpha-rail-pad-y) * -1) calc(var(--alpha-rail-pad-end) * -1) calc(var(--alpha-rail-pad-y) * -1) calc(var(--alpha-rail-pad-start) * -1);
@@ -846,17 +851,17 @@ th { color: var(--muted); font-weight: 600; font-size: 13px; background: var(--b
846
851
  display: grid;
847
852
  place-items: center;
848
853
  width: 28px;
849
- min-height: 22px;
850
- padding: 2px 0;
854
+ flex: 1 1 auto;
855
+ min-height: 1em;
856
+ padding: 0;
851
857
  border-radius: 5px;
852
858
  color: var(--muted);
853
- font-size: 13px;
859
+ font-size: clamp(8px, calc((var(--alpha-rail-fit, 70svh) - 20px) / 36), 13px);
854
860
  font-weight: 600;
855
861
  font-family: var(--font-sans);
856
862
  font-variant-numeric: tabular-nums;
857
863
  line-height: 1;
858
864
  letter-spacing: 0;
859
- flex: 0 0 auto;
860
865
  }
861
866
  .alpha-rail__item:hover,
862
867
  .alpha-rail__item.is-scrub {
package/js/app.js CHANGED
@@ -1416,11 +1416,11 @@ function bindClientQueryFilters() {
1416
1416
 
1417
1417
  window.__setQueryLetterFilter = function (label) {
1418
1418
  const next = (label || "").trim();
1419
- if (filterState.letter.toLowerCase() === next.toLowerCase()) {
1420
- filterState.letter = "";
1421
- } else {
1422
- filterState.letter = next;
1423
- }
1419
+ if (!next) return;
1420
+ const sameLetter = filterState.letter.toLowerCase() === next.toLowerCase();
1421
+ const qDisplay = (searchInput?.value || filterState.q || "").trim();
1422
+ if (sameLetter && !qDisplay) return;
1423
+ filterState.letter = next;
1424
1424
  // 选字母时清空搜索关键词
1425
1425
  if (searchInput) {
1426
1426
  searchInput.value = "";
@@ -1561,9 +1561,35 @@ function bindAlphaRail() {
1561
1561
 
1562
1562
  let dragging = false;
1563
1563
  let current = null;
1564
+ let downItem = null;
1565
+ let downY = 0;
1564
1566
  let navigating = false;
1567
+ let pointerHandled = false;
1568
+ let fitFrame = 0;
1569
+
1570
+ function fitRail() {
1571
+ const top = rail.getBoundingClientRect().top;
1572
+ const fit = Math.max(280, Math.round(window.innerHeight - top - 8));
1573
+ const next = `${fit}px`;
1574
+ if (rail.style.getPropertyValue("--alpha-rail-fit") !== next) {
1575
+ rail.style.setProperty("--alpha-rail-fit", next);
1576
+ }
1577
+ }
1578
+
1579
+ function requestFit() {
1580
+ if (fitFrame) return;
1581
+ fitFrame = window.requestAnimationFrame(() => {
1582
+ fitFrame = 0;
1583
+ fitRail();
1584
+ });
1585
+ }
1586
+
1587
+ function letterOf(item) {
1588
+ if (!item) return "";
1589
+ return (item.getAttribute("data-letter") || item.textContent || "").trim();
1590
+ }
1565
1591
 
1566
- function pickItem(clientY) {
1592
+ function pickItemByY(clientY) {
1567
1593
  let best = null;
1568
1594
  let bestDist = Infinity;
1569
1595
  for (const item of items) {
@@ -1579,6 +1605,29 @@ function bindAlphaRail() {
1579
1605
  return best;
1580
1606
  }
1581
1607
 
1608
+ function itemFromPoint(clientX, clientY) {
1609
+ const stack =
1610
+ typeof document.elementsFromPoint === "function"
1611
+ ? document.elementsFromPoint(clientX, clientY)
1612
+ : [document.elementFromPoint(clientX, clientY)];
1613
+ for (const node of stack) {
1614
+ if (!node || !node.closest) continue;
1615
+ const item = node.closest("[data-alpha-item]");
1616
+ if (item && rail.contains(item)) return item;
1617
+ }
1618
+ return pickItemByY(clientY);
1619
+ }
1620
+
1621
+ function itemFromEvent(event) {
1622
+ const raw = event.target;
1623
+ const el = raw && raw.nodeType === 1 ? raw : raw && raw.parentElement;
1624
+ if (el && el.closest) {
1625
+ const item = el.closest("[data-alpha-item]");
1626
+ if (item && rail.contains(item)) return item;
1627
+ }
1628
+ return itemFromPoint(event.clientX, event.clientY);
1629
+ }
1630
+
1582
1631
  function setScrub(item) {
1583
1632
  if (current === item) return;
1584
1633
  current = item;
@@ -1592,9 +1641,9 @@ function bindAlphaRail() {
1592
1641
 
1593
1642
  function goTo(item) {
1594
1643
  if (!item || navigating) return;
1595
- if (document.querySelector(".song-list")) {
1596
- const label = item.textContent.trim();
1597
- window.__setQueryLetterFilter?.(label);
1644
+ const label = letterOf(item);
1645
+ if (typeof window.__setQueryLetterFilter === "function") {
1646
+ window.__setQueryLetterFilter(label);
1598
1647
  return;
1599
1648
  }
1600
1649
  const next = item.href;
@@ -1606,7 +1655,9 @@ function bindAlphaRail() {
1606
1655
  function onPointerDown(event) {
1607
1656
  if (event.pointerType === "mouse" && event.button !== 0) return;
1608
1657
  dragging = true;
1609
- setScrub(pickItem(event.clientY));
1658
+ downY = event.clientY;
1659
+ downItem = itemFromEvent(event);
1660
+ setScrub(downItem);
1610
1661
  try {
1611
1662
  rail.setPointerCapture(event.pointerId);
1612
1663
  } catch (err) {
@@ -1617,20 +1668,27 @@ function bindAlphaRail() {
1617
1668
 
1618
1669
  function onPointerMove(event) {
1619
1670
  if (!dragging) return;
1620
- setScrub(pickItem(event.clientY));
1671
+ setScrub(itemFromPoint(event.clientX, event.clientY));
1621
1672
  event.preventDefault();
1622
1673
  }
1623
1674
 
1624
1675
  function onPointerUp(event) {
1625
1676
  if (!dragging) return;
1626
1677
  dragging = false;
1627
- const target = pickItem(event.clientY) || current;
1678
+ const moved = Math.abs(event.clientY - downY) > 8;
1679
+ const target = moved ? itemFromPoint(event.clientX, event.clientY) || current : downItem || itemFromEvent(event);
1680
+ downItem = null;
1628
1681
  clearScrub();
1682
+ pointerHandled = true;
1629
1683
  goTo(target);
1684
+ window.setTimeout(() => {
1685
+ pointerHandled = false;
1686
+ }, 400);
1630
1687
  }
1631
1688
 
1632
1689
  function onPointerCancel() {
1633
1690
  dragging = false;
1691
+ downItem = null;
1634
1692
  clearScrub();
1635
1693
  }
1636
1694
 
@@ -1642,8 +1700,15 @@ function bindAlphaRail() {
1642
1700
  items.forEach((item) => {
1643
1701
  item.addEventListener("click", (event) => {
1644
1702
  event.preventDefault();
1703
+ if (pointerHandled) return;
1704
+ goTo(item);
1645
1705
  });
1646
1706
  });
1707
+
1708
+ window.__fitQueryAlphaRail = fitRail;
1709
+ fitRail();
1710
+ window.addEventListener("resize", requestFit);
1711
+ window.addEventListener("scroll", requestFit, { passive: true });
1647
1712
  }
1648
1713
 
1649
1714
  function bindSearchClear() {
@@ -263,10 +263,15 @@
263
263
  const letter = String((filters && filters.letter) || "").trim();
264
264
  const bpmBucket = String((filters && filters.bpmBucket) || "").trim();
265
265
  const bpmExact = (filters && filters.bpmExact) || "";
266
+ const browseDefault = !filters || filters.browseDefault !== false;
267
+ // 与 search_index/catalog.search_catalog 一致:无关键词、无字母/星级/BPM 时只展示 # 桶。
268
+ const browseOnly = browseDefault && !needle && !letter && !star && !bpmBucket && !String(bpmExact || "").trim();
266
269
  const exact = [];
267
270
  list.forEach((entry) => {
268
271
  if (star && String(entry.star) !== star) return;
269
- if (!letterMatches(entry, letter)) return;
272
+ if (browseOnly) {
273
+ if (!entry.title_lead_non_zh_en) return;
274
+ } else if (!letterMatches(entry, letter)) return;
270
275
  if (!bpmMatches(entry, bpmBucket, bpmExact)) return;
271
276
  if (!needle) {
272
277
  exact.push({ entry, score: 0, fuzzy: false });
package/js/query-app.js CHANGED
@@ -185,6 +185,30 @@
185
185
  >${inner}</button>`;
186
186
  }
187
187
 
188
+ function letterHintLabel(letter) {
189
+ const key = String(letter || "").trim();
190
+ if (!key) return "";
191
+ return key === "#" ? "特殊" : key.toUpperCase();
192
+ }
193
+
194
+ function syncLetterHint(letter) {
195
+ const el = document.querySelector("[data-query-letter-hint]");
196
+ if (!el) return;
197
+ const label = letterHintLabel(letter);
198
+ el.textContent = label ? `${label}开头` : "";
199
+ el.hidden = !label;
200
+ }
201
+
202
+ function syncAlphaRail(letter) {
203
+ const key = String(letter || "").trim().toLowerCase();
204
+ document.querySelectorAll("[data-alpha-item]").forEach((el) => {
205
+ const active = Boolean(key) && el.textContent.trim().toLowerCase() === key;
206
+ el.classList.toggle("is-on", active);
207
+ if (active) el.setAttribute("aria-current", "true");
208
+ else el.removeAttribute("aria-current");
209
+ });
210
+ }
211
+
188
212
  function syncHint(visibleCount) {
189
213
  if (countEl) countEl.textContent = String(visibleCount);
190
214
  const qDisplay = (searchInput && searchInput.value ? searchInput.value : filterState.q || "").trim();
@@ -192,6 +216,7 @@
192
216
  qHint.textContent = qDisplay ? `[${qDisplay}]` : "";
193
217
  qHint.hidden = !qDisplay;
194
218
  }
219
+ syncLetterHint(qDisplay ? "" : filterState.letter);
195
220
  if (hintTail) {
196
221
  if (fuzzySuggest && qDisplay) {
197
222
  hintTail.textContent = "。未精确匹配,是否你想找以下结果?";
@@ -245,11 +270,7 @@
245
270
  }
246
271
  syncHint(ranked.length);
247
272
  syncLetterField(filterState.letter);
248
- document.querySelectorAll("[data-alpha-item]").forEach((el) => {
249
- const active =
250
- filterState.letter && el.textContent.trim().toLowerCase() === filterState.letter.toLowerCase();
251
- el.classList.toggle("is-on", Boolean(active));
252
- });
273
+ syncAlphaRail(filterState.letter);
253
274
  const params = new URLSearchParams(window.location.search);
254
275
  if (family) params.set("family", family);
255
276
  if (filterState.q) params.set("q", filterState.q);
@@ -276,8 +297,11 @@
276
297
 
277
298
  window.__setQueryLetterFilter = function (label) {
278
299
  const next = (label || "").trim();
279
- if (filterState.letter.toLowerCase() === next.toLowerCase()) filterState.letter = "";
280
- else filterState.letter = next;
300
+ if (!next) return;
301
+ const sameLetter = filterState.letter.toLowerCase() === next.toLowerCase();
302
+ const qDisplay = (searchInput && searchInput.value ? searchInput.value : filterState.q || "").trim();
303
+ if (sameLetter && !qDisplay) return;
304
+ filterState.letter = next;
281
305
  if (searchInput) {
282
306
  searchInput.value = "";
283
307
  filterState.q = "";
@@ -285,6 +309,16 @@
285
309
  if (clearBtn) clearBtn.classList.remove("is-on");
286
310
  }
287
311
  applyFilters();
312
+ const toolbar = document.querySelector(".query-toolbar");
313
+ if (toolbar) {
314
+ const nextTop = toolbar.getBoundingClientRect().top + window.scrollY;
315
+ if (Math.abs(window.scrollY - nextTop) > 8) {
316
+ window.scrollTo(0, Math.max(0, nextTop));
317
+ }
318
+ }
319
+ window.requestAnimationFrame(function () {
320
+ if (typeof window.__fitQueryAlphaRail === "function") window.__fitQueryAlphaRail();
321
+ });
288
322
  };
289
323
 
290
324
  window.__queryLoadFamily = function (nextFamily, nextStar) {