@obvi/blueprint 1.1.3 → 1.2.0

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/dist/blueprint.js CHANGED
@@ -756,11 +756,31 @@ function injectSidebar(doc, win) {
756
756
  searchHint.append(searchHintCmd, searchHintKey);
757
757
  searchField.append(searchIcon, searchLabel, searchHint);
758
758
  search.append(searchField);
759
- panel.append(search);
760
759
  const switcher = buildDocSwitcher(doc, win);
761
- if (switcher) panel.append(switcher);
760
+ const tocSwitcher = doc.createElement("details");
761
+ tocSwitcher.className = "bp-toc-switcher";
762
+ tocSwitcher.open = true;
763
+ const tocSummary = doc.createElement("summary");
764
+ tocSummary.className = "bp-toc-switcher__current sidebar-field";
765
+ tocSummary.setAttribute("aria-label", "Jump to section");
766
+ const tocLabel = doc.createElement("span");
767
+ tocLabel.className = "bp-toc-switcher__label";
768
+ const tocTitle = doc.createElement("span");
769
+ tocTitle.className = "bp-toc-switcher__title";
770
+ tocTitle.textContent = "Contents";
771
+ tocLabel.append(tocTitle);
772
+ const tocChevron = doc.createElement("span");
773
+ tocChevron.className = "bp-toc-switcher__chevron bp-transition-transform bp-duration-normal bp-ease-in-out";
774
+ tocChevron.setAttribute("aria-hidden", "true");
775
+ tocChevron.append(createChromeSvg(doc, DOC_CHEVRON_ICON));
776
+ tocSummary.append(tocLabel, tocChevron);
762
777
  const list = doc.createElement("ul");
763
- panel.append(list);
778
+ tocSwitcher.append(tocSummary, list);
779
+ const mobileHead = doc.createElement("div");
780
+ mobileHead.className = "sidebar-mobile-head";
781
+ mobileHead.append(tocSwitcher, search);
782
+ panel.append(mobileHead);
783
+ if (switcher) panel.append(switcher);
764
784
  nav.append(panel, buildSidebarFooter(doc));
765
785
  return nav;
766
786
  }
@@ -834,6 +854,7 @@ function setThemeAnimated(doc, win, theme) {
834
854
  transition = doc.startViewTransition(runUpdate);
835
855
  }
836
856
  if (!applied) runUpdate();
857
+ transition?.ready?.catch(clearSwitching);
837
858
  if (transition?.finished) {
838
859
  transition.finished.finally(clearSwitching).catch(clearSwitching);
839
860
  } else {
@@ -931,14 +952,14 @@ function wireSearchPalette(doc, win) {
931
952
  let panel = null;
932
953
  let input = null;
933
954
  let results = null;
934
- let footStatus = null;
955
+ let actionDefs = [];
935
956
  let entries = [];
936
957
  let visible = [];
937
958
  let selected = 0;
938
959
  let returnFocus = null;
939
960
  let prevOverflow = "";
940
961
  const collectEntries = () => {
941
- const list = doc.querySelector(".bp-sidebar .bp-sidebar__panel > ul") ?? doc.querySelector(".bp-toc > ul");
962
+ const list = doc.querySelector(".bp-sidebar .bp-sidebar__panel .bp-toc-switcher > ul") ?? doc.querySelector(".bp-sidebar .bp-sidebar__panel > ul") ?? doc.querySelector(".bp-toc > ul");
942
963
  if (!list) return [];
943
964
  const out = [];
944
965
  let group = "";
@@ -961,16 +982,57 @@ function wireSearchPalette(doc, win) {
961
982
  }
962
983
  return out;
963
984
  };
964
- const keyHint = (caps, labelText) => {
965
- const span = doc.createElement("span");
966
- span.className = "docs-search__key";
967
- for (const cap of caps) {
968
- const kbd = doc.createElement("kbd");
969
- kbd.textContent = cap;
970
- span.append(kbd);
971
- }
972
- span.append(doc.createTextNode(` ${labelText}`));
973
- return span;
985
+ const modGlyph = isMacLike ? "⌘" : "⌃";
986
+ const matchesShortcut = (event, key) => {
987
+ const mod = isMacLike ? event.metaKey : event.ctrlKey;
988
+ if (!mod || !event.shiftKey) return false;
989
+ return event.key.toLowerCase() === key.toLowerCase();
990
+ };
991
+ const buildActionDefs = () => {
992
+ const defs = [];
993
+ if (doc.querySelector(".theme-toggle")) {
994
+ defs.push({
995
+ label: "Toggle theme",
996
+ icon: THEME_MOON_ICON,
997
+ chip: `${modGlyph}⇧L`,
998
+ shortcut: "l",
999
+ run: () => doc.querySelector(".theme-toggle")?.click()
1000
+ });
1001
+ }
1002
+ if (doc.querySelector(".sidebar-toggle")) {
1003
+ defs.push({
1004
+ label: "Toggle sidebar",
1005
+ icon: SIDEBAR_COLLAPSE_ICON,
1006
+ chip: `${modGlyph}⇧B`,
1007
+ shortcut: "b",
1008
+ run: () => doc.querySelector(".sidebar-toggle")?.click()
1009
+ });
1010
+ }
1011
+ return defs;
1012
+ };
1013
+ const buildAction = (def) => {
1014
+ const button = doc.createElement("button");
1015
+ button.type = "button";
1016
+ button.className = "docs-search__action bp-transition-colors bp-ease";
1017
+ const icon = createChromeSvg(doc, def.icon, 16);
1018
+ icon.classList.add("docs-search__action-icon");
1019
+ const label2 = doc.createElement("span");
1020
+ label2.className = "docs-search__action-label";
1021
+ label2.textContent = def.label;
1022
+ button.append(icon, label2);
1023
+ if (def.chip) {
1024
+ const chip = doc.createElement("kbd");
1025
+ chip.className = "docs-search__chip";
1026
+ for (const glyph of Array.from(def.chip)) {
1027
+ const key = doc.createElement("span");
1028
+ key.textContent = glyph;
1029
+ chip.append(key);
1030
+ }
1031
+ button.append(chip);
1032
+ }
1033
+ button.addEventListener("click", () => def.run(button));
1034
+ def.button = button;
1035
+ return button;
974
1036
  };
975
1037
  const highlight = (text, q) => {
976
1038
  if (!q) return [doc.createTextNode(text)];
@@ -1000,6 +1062,11 @@ function wireSearchPalette(doc, win) {
1000
1062
  });
1001
1063
  const mainCol = doc.createElement("span");
1002
1064
  mainCol.className = "docs-search__row-main";
1065
+ const iconBox = doc.createElement("span");
1066
+ iconBox.className = "docs-search__icon-box";
1067
+ iconBox.setAttribute("aria-hidden", "true");
1068
+ iconBox.textContent = "#";
1069
+ mainCol.append(iconBox);
1003
1070
  if (entry.crumb) {
1004
1071
  const crumb = doc.createElement("span");
1005
1072
  crumb.className = "docs-search__crumb";
@@ -1008,11 +1075,7 @@ function wireSearchPalette(doc, win) {
1008
1075
  }
1009
1076
  const title = doc.createElement("span");
1010
1077
  title.className = "docs-search__title";
1011
- const hash = doc.createElement("span");
1012
- hash.className = "docs-search__hash";
1013
- hash.setAttribute("aria-hidden", "true");
1014
- hash.textContent = "#";
1015
- title.append(hash, ...highlight(entry.title, q));
1078
+ title.append(...highlight(entry.title, q));
1016
1079
  mainCol.append(title);
1017
1080
  const enter = doc.createElement("kbd");
1018
1081
  enter.className = "docs-search__enter";
@@ -1020,19 +1083,22 @@ function wireSearchPalette(doc, win) {
1020
1083
  row.append(mainCol, enter);
1021
1084
  return row;
1022
1085
  };
1023
- const buildEmpty = (query) => {
1086
+ const buildEmpty = () => {
1024
1087
  const wrap = doc.createElement("div");
1025
1088
  wrap.className = "docs-search__empty";
1026
- const iconWrap = doc.createElement("span");
1027
- iconWrap.className = "docs-search__empty-icon";
1028
- iconWrap.append(createChromeSvg(doc, SEARCH_ICON, 28));
1029
- const title = doc.createElement("span");
1089
+ const center = doc.createElement("div");
1090
+ center.className = "docs-search__empty-center";
1091
+ const copy = doc.createElement("div");
1092
+ copy.className = "docs-search__empty-copy";
1093
+ const title = doc.createElement("p");
1030
1094
  title.className = "docs-search__empty-title";
1031
- title.textContent = `No results for “${(query || "").trim()}”`;
1032
- const note = doc.createElement("span");
1095
+ title.textContent = "No results found";
1096
+ const note = doc.createElement("p");
1033
1097
  note.className = "docs-search__empty-note";
1034
- note.textContent = "Try a different term, or check your spelling.";
1035
- wrap.append(iconWrap, title, note);
1098
+ note.textContent = "Try searching with different keywords";
1099
+ copy.append(title, note);
1100
+ center.append(copy);
1101
+ wrap.append(center);
1036
1102
  return wrap;
1037
1103
  };
1038
1104
  const applySelection = () => {
@@ -1060,8 +1126,7 @@ function wireSearchPalette(doc, win) {
1060
1126
  selected = 0;
1061
1127
  results.replaceChildren();
1062
1128
  if (visible.length === 0) {
1063
- results.append(buildEmpty(query));
1064
- footStatus.textContent = "0 results";
1129
+ results.append(buildEmpty());
1065
1130
  input.removeAttribute("aria-activedescendant");
1066
1131
  return;
1067
1132
  }
@@ -1075,9 +1140,14 @@ function wireSearchPalette(doc, win) {
1075
1140
  results.append(group);
1076
1141
  visible.forEach((entry, index) => results.append(buildRow(entry, index, q)));
1077
1142
  applySelection();
1078
- footStatus.textContent = q ? `${visible.length} result${visible.length === 1 ? "" : "s"}` : "Jump to a section";
1079
1143
  };
1080
1144
  const onKeydown = (event) => {
1145
+ const action = actionDefs.find((def) => matchesShortcut(event, def.shortcut));
1146
+ if (action) {
1147
+ event.preventDefault();
1148
+ action.run(action.button);
1149
+ return;
1150
+ }
1081
1151
  if (event.key === "Escape") {
1082
1152
  event.preventDefault();
1083
1153
  close();
@@ -1088,6 +1158,7 @@ function wireSearchPalette(doc, win) {
1088
1158
  event.preventDefault();
1089
1159
  move(-1);
1090
1160
  } else if (event.key === "Enter") {
1161
+ if (event.target?.closest?.(".docs-search__action")) return;
1091
1162
  const row = results.querySelector(
1092
1163
  `.docs-search__row[data-index="${selected}"]`
1093
1164
  );
@@ -1125,12 +1196,12 @@ function wireSearchPalette(doc, win) {
1125
1196
  input = doc.createElement("input");
1126
1197
  input.className = "docs-search__input";
1127
1198
  input.type = "search";
1128
- input.placeholder = "Search the docs…";
1199
+ input.placeholder = "Search the blueprint…";
1129
1200
  input.setAttribute("role", "combobox");
1130
1201
  input.setAttribute("aria-autocomplete", "list");
1131
1202
  input.setAttribute("aria-controls", "docs-search-listbox");
1132
1203
  input.setAttribute("aria-expanded", "false");
1133
- input.setAttribute("aria-label", "Search the docs");
1204
+ input.setAttribute("aria-label", "Search the blueprint");
1134
1205
  input.autocomplete = "off";
1135
1206
  input.spellcheck = false;
1136
1207
  const esc = doc.createElement("span");
@@ -1144,15 +1215,11 @@ function wireSearchPalette(doc, win) {
1144
1215
  results.setAttribute("aria-label", "Search results");
1145
1216
  const foot = doc.createElement("div");
1146
1217
  foot.className = "docs-search__foot";
1147
- footStatus = doc.createElement("span");
1148
- const keys = doc.createElement("span");
1149
- keys.className = "docs-search__keys";
1150
- keys.append(
1151
- keyHint(["↑", "↓"], "navigate"),
1152
- keyHint(["↵"], "open"),
1153
- keyHint(["esc"], "close")
1154
- );
1155
- foot.append(footStatus, keys);
1218
+ const actions = doc.createElement("div");
1219
+ actions.className = "docs-search__actions";
1220
+ actionDefs = buildActionDefs();
1221
+ for (const def of actionDefs) actions.append(buildAction(def));
1222
+ foot.append(actions);
1156
1223
  panel.append(field, results, foot);
1157
1224
  overlay.append(panel);
1158
1225
  (doc.body || doc.documentElement).append(overlay);
@@ -1393,7 +1460,7 @@ function initializeBlueprintRuntime(doc = document, win = window) {
1393
1460
  if (!list || !listItem) return true;
1394
1461
  const container = link.closest(".bp-sidebar, .bp-toc, .sidebar");
1395
1462
  if (!container) return true;
1396
- const topList = container.querySelector(":scope > .bp-sidebar__panel > ul") ?? container.querySelector(":scope > ul");
1463
+ const topList = container.querySelector(":scope > .bp-sidebar__panel .bp-toc-switcher > ul") ?? container.querySelector(":scope > .bp-sidebar__panel > ul") ?? container.querySelector(":scope > ul");
1397
1464
  return list === topList;
1398
1465
  };
1399
1466
  const entries = [...doc.querySelectorAll('.bp-sidebar a[href^="#"], .bp-toc a[href^="#"], .sidebar a[href^="#"]')].filter(isPrimaryNavLink).map((link) => ({
@@ -1405,6 +1472,14 @@ function initializeBlueprintRuntime(doc = document, win = window) {
1405
1472
  );
1406
1473
  let scheduled = false;
1407
1474
  let scrollingTo = null;
1475
+ const tocSwitchers = [...doc.querySelectorAll(".bp-toc-switcher")];
1476
+ const tocSwitcherTitles = [...doc.querySelectorAll(".bp-toc-switcher__title")];
1477
+ const railColumnQuery = win.matchMedia("(min-width: 861px)");
1478
+ const syncTocSwitcherOpen = () => {
1479
+ for (const sw of tocSwitchers) sw.open = railColumnQuery.matches;
1480
+ };
1481
+ syncTocSwitcherOpen();
1482
+ railColumnQuery.addEventListener("change", syncTocSwitcherOpen);
1408
1483
  const placeMarker = (link) => {
1409
1484
  const container = link.closest(".bp-sidebar, .bp-toc, .sidebar");
1410
1485
  const list = container?.querySelector(":scope > ul") ?? container?.querySelector("ul");
@@ -1412,18 +1487,59 @@ function initializeBlueprintRuntime(doc = document, win = window) {
1412
1487
  list.style.setProperty("--bp-toc-y", `${link.offsetTop}px`);
1413
1488
  list.style.setProperty("--bp-toc-h", `${link.offsetHeight}px`);
1414
1489
  };
1490
+ const revealInRail = (link) => {
1491
+ const panel = link.closest(".bp-sidebar__panel");
1492
+ if (!panel || panel.scrollHeight <= panel.clientHeight) return;
1493
+ const style = win.getComputedStyle(panel);
1494
+ const padTop = Number.parseFloat(style.scrollPaddingTop) || 0;
1495
+ const padBottom = Number.parseFloat(style.scrollPaddingBottom) || 0;
1496
+ const panelRect = panel.getBoundingClientRect();
1497
+ const linkRect = link.getBoundingClientRect();
1498
+ const safeTop = panelRect.top + padTop;
1499
+ const safeBottom = panelRect.bottom - padBottom;
1500
+ let delta = 0;
1501
+ if (linkRect.top < safeTop) delta = linkRect.top - safeTop;
1502
+ else if (linkRect.bottom > safeBottom) delta = linkRect.bottom - safeBottom;
1503
+ if (delta === 0) return;
1504
+ panel.scrollBy({
1505
+ top: delta,
1506
+ behavior: prefersReducedMotion() ? "auto" : "smooth"
1507
+ });
1508
+ };
1509
+ let lastCurrent = null;
1415
1510
  const setCurrent = (section) => {
1511
+ const changed = section !== lastCurrent;
1512
+ let activeLabel = "";
1416
1513
  for (const item of entries) {
1417
1514
  if (section && item.section === section) {
1418
1515
  item.link.setAttribute("aria-current", "location");
1419
1516
  placeMarker(item.link);
1517
+ if (changed) revealInRail(item.link);
1518
+ if (!activeLabel) activeLabel = item.link.textContent.trim();
1420
1519
  } else {
1421
1520
  item.link.removeAttribute("aria-current");
1422
1521
  }
1423
1522
  }
1523
+ lastCurrent = section;
1524
+ if (changed) {
1525
+ const summaryLabel = activeLabel || "Contents";
1526
+ for (const title of tocSwitcherTitles) title.textContent = summaryLabel;
1527
+ }
1528
+ };
1529
+ const mobileHead = doc.querySelector(".sidebar-mobile-head");
1530
+ const contentsRail = doc.querySelector("#bp-contents-rail");
1531
+ const syncHeadDivider = () => {
1532
+ if (!mobileHead || !contentsRail) return;
1533
+ if (win.getComputedStyle(mobileHead).position !== "fixed") {
1534
+ mobileHead.removeAttribute("data-bp-head-stuck");
1535
+ return;
1536
+ }
1537
+ const stuck = contentsRail.getBoundingClientRect().bottom <= mobileHead.getBoundingClientRect().bottom + 0.5;
1538
+ mobileHead.toggleAttribute("data-bp-head-stuck", stuck);
1424
1539
  };
1425
1540
  const update = () => {
1426
1541
  scheduled = false;
1542
+ syncHeadDivider();
1427
1543
  if (progress) {
1428
1544
  const max = scrollHeight() - clientHeight();
1429
1545
  progress.style.transform = `scaleX(${max > 0 ? scrollTop() / max : 0})`;
@@ -1505,6 +1621,9 @@ function initializeBlueprintRuntime(doc = document, win = window) {
1505
1621
  event.preventDefault();
1506
1622
  if (win.location.hash !== link.hash) win.history.pushState(null, "", link.hash);
1507
1623
  if (entries.some(({ section: target }) => target === section)) setCurrent(section);
1624
+ if (!railColumnQuery.matches) {
1625
+ for (const sw of tocSwitchers) sw.open = false;
1626
+ }
1508
1627
  scrollToSection(section);
1509
1628
  });
1510
1629
  let scrollSource = shellScroller ?? win;
@@ -1765,7 +1884,7 @@ function renderContentsTree(doc, tree) {
1765
1884
  function findSidebarNavLists(doc) {
1766
1885
  const lists = [];
1767
1886
  for (const nav of doc.querySelectorAll(".bp-sidebar:not([data-bp-nav-manual])")) {
1768
- const list = nav.querySelector(":scope > .bp-sidebar__panel > ul") ?? nav.querySelector(":scope > ul");
1887
+ const list = nav.querySelector(":scope > .bp-sidebar__panel .bp-toc-switcher > ul") ?? nav.querySelector(":scope > .bp-sidebar__panel > ul") ?? nav.querySelector(":scope > ul");
1769
1888
  if (list) lists.push(list);
1770
1889
  }
1771
1890
  for (const nav of doc.querySelectorAll(".bp-toc:not([data-bp-nav-manual])")) {