@waveso/docs 0.10.0 → 0.12.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.
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { docsError } from "../docs-error.js";
3
3
  import { mergeSearchOptions } from "../search-options.js";
4
- import { useCallback, useEffect, useId, useRef, useState } from "react";
4
+ import { Fragment, useCallback, useEffect, useId, useRef, useState } from "react";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { createPortal } from "react-dom";
7
7
  //#region src/react/search-dialog.tsx
@@ -51,7 +51,7 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
51
51
  * not itself cause a render.
52
52
  */
53
53
  const movedByKeyboard = useRef(false);
54
- const [activeIndex, setActiveIndex] = useState(0);
54
+ const [activeIndex, setActiveIndex] = useState(-1);
55
55
  const [status, setStatus] = useState("idle");
56
56
  /**
57
57
  * The trigger's shortcut, split rather than held as one string.
@@ -166,13 +166,13 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
166
166
  if (isOpen) return;
167
167
  setQuery("");
168
168
  setHits([]);
169
- setActiveIndex(0);
169
+ setActiveIndex(-1);
170
170
  }, [isOpen]);
171
171
  useEffect(() => {
172
172
  const trimmed = query.trim();
173
173
  if (trimmed.length < minQueryLength) {
174
174
  setHits([]);
175
- setActiveIndex(0);
175
+ setActiveIndex(-1);
176
176
  setVisibleCount(pageSize);
177
177
  return;
178
178
  }
@@ -182,7 +182,7 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
182
182
  if (isCancelled) return;
183
183
  setStatus("ready");
184
184
  setHits(index.search(trimmed).map(toSearchHit).filter(isSearchHit));
185
- setActiveIndex(0);
185
+ setActiveIndex(-1);
186
186
  setVisibleCount(pageSize);
187
187
  }, () => {
188
188
  if (!isCancelled) setStatus("error");
@@ -261,7 +261,10 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
261
261
  event.preventDefault();
262
262
  const delta = event.key === "ArrowDown" ? 1 : -1;
263
263
  movedByKeyboard.current = true;
264
- setActiveIndex((index) => (index + delta + hits.length) % hits.length);
264
+ setActiveIndex((index) => {
265
+ if (index < 0) return delta > 0 ? 0 : hits.length - 1;
266
+ return (index + delta + hits.length) % hits.length;
267
+ });
265
268
  return;
266
269
  }
267
270
  if (event.key === "Enter") {
@@ -446,13 +449,16 @@ function SearchResultOption({ hit, id, isActive, setSize, posInSet, onActivate,
446
449
  event.preventDefault();
447
450
  onSelect(hit);
448
451
  }
449
- const body = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
450
- className: "wave-docs-search-result-heading",
451
- children: hit.heading
452
- }), /* @__PURE__ */ jsx("span", {
453
- className: "wave-docs-search-result-location",
454
- "aria-hidden": "true",
455
- children: toDisplayPath(hit.href)
452
+ const body = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ResultIcon, { section: hit.href.includes("#") }), /* @__PURE__ */ jsxs("span", {
453
+ className: "wave-docs-search-result-text",
454
+ children: [/* @__PURE__ */ jsx("span", {
455
+ className: "wave-docs-search-result-heading",
456
+ children: hit.heading
457
+ }), /* @__PURE__ */ jsx("span", {
458
+ className: "wave-docs-search-result-location",
459
+ "aria-hidden": "true",
460
+ children: toDisplaySegments(hit.href).map((segment, index, all) => /* @__PURE__ */ jsxs(Fragment, { children: [index === 0 ? null : /* @__PURE__ */ jsx(PathChevron, {}), segment] }, all.slice(0, index + 1).join("/")))
461
+ })]
456
462
  })] });
457
463
  return /* @__PURE__ */ jsx("div", {
458
464
  id,
@@ -599,9 +605,10 @@ function isSearchHit(hit) {
599
605
  * Display only. `hit.href` keeps the anchor, so the link still deep-links to
600
606
  * the section — that is the whole point of section-scoped records.
601
607
  */
602
- function toDisplayPath(href) {
608
+ function toDisplaySegments(href) {
603
609
  const hash = href.indexOf("#");
604
- return hash === -1 ? href : href.slice(0, hash);
610
+ const segments = (hash === -1 ? href : href.slice(0, hash)).split("/").filter(Boolean);
611
+ return segments.length === 0 ? ["/"] : segments;
605
612
  }
606
613
  /**
607
614
  * What a result is called when it is read aloud.
@@ -618,6 +625,71 @@ function toDisplayPath(href) {
618
625
  * A page's own record carries `heading === title` and no ancestors, so its name
619
626
  * is the heading alone — "Styling, Styling" is not a path, it is a stutter.
620
627
  */
628
+ /**
629
+ * A page or a section, as one Lucide glyph.
630
+ *
631
+ * ⚠️ THE PATHS ARE WRITTEN OUT RATHER THAN IMPORTED FROM THE SIDEBAR, WHICH
632
+ * HAS THE SAME PAGE ICON. `NAV_ICON_PATHS` lives in a different client
633
+ * component, and importing across the two would pull the whole navigation
634
+ * tree into the search dialog's bundle to reuse one string. The duplication is
635
+ * two path strings; the alternative is kilobytes.
636
+ */
637
+ const RESULT_ICON_PATHS = {
638
+ page: ["M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z", "M14 2v4a2 2 0 0 0 2 2h4"],
639
+ section: [
640
+ "M4 9h16",
641
+ "M4 15h16",
642
+ "m10 3-2 18",
643
+ "m16 3-2 18"
644
+ ]
645
+ };
646
+ /**
647
+ * The separator between two route segments.
648
+ *
649
+ * The same path the sidebar's group toggle and the pager draw, at the same
650
+ * `1rem` they draw it. Decorative, and inside a span that is already
651
+ * `aria-hidden` — the route is announced as words by `spokenName`, never read
652
+ * out as punctuation.
653
+ *
654
+ * ⚠️ THE `viewBox` IS CROPPED HORIZONTALLY AND THAT IS NOT A DIFFERENT GLYPH.
655
+ * `d` is Lucide's chevron unchanged and `8 0 8 24` keeps its vertical grid, so
656
+ * the stroke ratio and the ink are exactly what the sidebar and the pager
657
+ * paint. What it drops is the padding either side that the path never reaches:
658
+ * on the full grid a 24-unit box held six units of chevron, so five-sixths of
659
+ * this element was air and the stylesheet had to subtract it back with
660
+ * negative margins. Cropped, the box is the ink and the spacing beside it is
661
+ * one positive value. See `.wave-docs-search-result-sep`.
662
+ */
663
+ function PathChevron() {
664
+ return /* @__PURE__ */ jsx("svg", {
665
+ className: "wave-docs-search-result-sep",
666
+ "aria-hidden": "true",
667
+ focusable: "false",
668
+ viewBox: "8 5 8 14",
669
+ fill: "none",
670
+ stroke: "currentColor",
671
+ strokeWidth: "2",
672
+ strokeLinecap: "round",
673
+ strokeLinejoin: "round",
674
+ children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
675
+ });
676
+ }
677
+ function ResultIcon({ section }) {
678
+ return /* @__PURE__ */ jsx("svg", {
679
+ className: "wave-docs-search-result-icon",
680
+ "aria-hidden": "true",
681
+ focusable: "false",
682
+ viewBox: "0 0 24 24",
683
+ width: "16",
684
+ height: "16",
685
+ fill: "none",
686
+ stroke: "currentColor",
687
+ strokeWidth: "2",
688
+ strokeLinecap: "round",
689
+ strokeLinejoin: "round",
690
+ children: RESULT_ICON_PATHS[section ? "section" : "page"].map((d) => /* @__PURE__ */ jsx("path", { d }, d))
691
+ });
692
+ }
621
693
  function spokenName(hit) {
622
694
  if (hit.ancestors.length === 0 && hit.heading === hit.title) return hit.heading;
623
695
  return [
@@ -76,6 +76,13 @@ interface DocsLabels {
76
76
  * The direction, not the destination: the page's own title is the name, and
77
77
  * these two words are what say which way it lies.
78
78
  */
79
+ /**
80
+ * The "where to go next" block's heading. Defaults to `'Where to go next'`.
81
+ *
82
+ * A page opts into that block with `explore` in its frontmatter; this is the one
83
+ * string the package supplies for it, and the questions are the author's.
84
+ */
85
+ explore?: string | undefined;
79
86
  previousPage?: string | undefined;
80
87
  /** The same for the next page. Defaults to `'Next'`. */
81
88
  nextPage?: string | undefined;
@@ -154,7 +161,7 @@ interface DocsLabels {
154
161
  * failure being guarded against is a key that is declared, documented and never
155
162
  * read, which type-checks perfectly.
156
163
  */
157
- declare const DOCS_LABEL_KEYS: readonly ["nav", "openNav", "closeNav", "skipToContent", "expandGroup", "collapseGroup", "toc", "backToTop", "externalLink", "table", "calloutNote", "calloutTip", "calloutImportant", "calloutWarning", "calloutCaution", "youtubeTitle", "youtubePlay", "youtubeHide", "copyCode", "copyCodeFrom", "previousPage", "nextPage", "pagination", "copied", "copyFailed"];
164
+ declare const DOCS_LABEL_KEYS: readonly ["nav", "openNav", "closeNav", "skipToContent", "expandGroup", "collapseGroup", "toc", "backToTop", "externalLink", "table", "calloutNote", "calloutTip", "calloutImportant", "calloutWarning", "calloutCaution", "youtubeTitle", "youtubePlay", "youtubeHide", "copyCode", "copyCodeFrom", "explore", "previousPage", "nextPage", "pagination", "copied", "copyFailed"];
158
165
  /** The four the shell renders itself, resolved centrally. */
159
166
  type ShellLabelKey = 'nav' | 'openNav' | 'closeNav' | 'skipToContent';
160
167
  /**
@@ -27,6 +27,7 @@ const DOCS_LABEL_KEYS = [
27
27
  "youtubeHide",
28
28
  "copyCode",
29
29
  "copyCodeFrom",
30
+ "explore",
30
31
  "previousPage",
31
32
  "nextPage",
32
33
  "pagination",