@waveso/docs 0.9.1 → 0.11.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.
@@ -110,6 +110,20 @@ function createImage(Image) {
110
110
  * "no tabindex on non-interactive elements", not a violation of it. A labelled
111
111
  * `<section>` is a `region` landmark, so the tab stop announces itself instead
112
112
  * of being a mystery stop in the tab order.
113
+ *
114
+ * ⚠️ ONE FRAME, AND IT WORE `.wave-docs-panel` FOR A WHILE.
115
+ *
116
+ * The panel — an outer frame, a header band, an inset card — exists to separate
117
+ * *chrome* from *content*. "Where to go next" and a code frame both have chrome
118
+ * to put in that band: a title, a language, a copy button. A table's header row
119
+ * is not chrome, it is data, and setting the body into a card away from its own
120
+ * header cost three vertical rules down each side, stopped the row dividers
121
+ * short of the box, and narrowed the reading width — on the densest element on
122
+ * a page, for nothing gained. Full-width dividers are what let an eye track a
123
+ * row across.
124
+ *
125
+ * It keeps the panel's outer radius, so a table and a code block still read as
126
+ * two of one family without the table pretending to have chrome it has not got.
113
127
  */
114
128
  function createTable(label) {
115
129
  return function MarkdownTable({ className, ...rest }) {
@@ -0,0 +1,42 @@
1
+ import { NavStop } from "../nav-order.js";
2
+ import { DocsLinkComponent } from "./markdown-components.js";
3
+ import { ReactNode } from "react";
4
+ //#region src/react/pager.d.ts
5
+ interface DocsPagerProps {
6
+ /** The stop before this page in the reading order. Omit at the beginning. */
7
+ previous?: NavStop | undefined;
8
+ /** The stop after it. Omit at the end. */
9
+ next?: NavStop | undefined;
10
+ /** Client-side router link, e.g. `next/link`. Falls back to `<a>`. */
11
+ Link?: DocsLinkComponent | undefined;
12
+ /** Above the previous page's title. Defaults to `'Previous'`. */
13
+ previousLabel?: string | undefined;
14
+ /** Above the next page's title. Defaults to `'Next'`. */
15
+ nextLabel?: string | undefined;
16
+ /**
17
+ * Accessible name for the landmark. Defaults to `'Pagination'`.
18
+ *
19
+ * Named because a page can hold more than one navigation landmark — this,
20
+ * the sidebar and the table of contents — and "navigation" three times is
21
+ * not a list anyone can steer by.
22
+ */
23
+ label?: string | undefined;
24
+ className?: string | undefined;
25
+ }
26
+ /**
27
+ * Links to the pages either side of this one in the reading order.
28
+ *
29
+ * Derived from the same tree `DocsSidebar` renders, so it cannot disagree with
30
+ * the column beside it — see `nav-order.ts`. Nothing here is authored: a page
31
+ * gets a pager by being in the navigation.
32
+ *
33
+ * ⚠️ NOTHING RENDERS AT ALL WHEN THERE IS NO NEIGHBOUR EITHER SIDE. A single
34
+ * page site, or a route outside the tree, would otherwise get an empty
35
+ * landmark — announced by a screen reader as a navigation region containing
36
+ * nothing, which is worse than the absence it is standing in for.
37
+ *
38
+ * No client JavaScript: two links and a heading, rendered on the server.
39
+ */
40
+ declare function DocsPager({ previous, next, Link, previousLabel, nextLabel, label, className }: DocsPagerProps): ReactNode;
41
+ //#endregion
42
+ export { DocsPager, DocsPagerProps };
@@ -0,0 +1,91 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ //#region src/react/pager.tsx
3
+ /**
4
+ * Links to the pages either side of this one in the reading order.
5
+ *
6
+ * Derived from the same tree `DocsSidebar` renders, so it cannot disagree with
7
+ * the column beside it — see `nav-order.ts`. Nothing here is authored: a page
8
+ * gets a pager by being in the navigation.
9
+ *
10
+ * ⚠️ NOTHING RENDERS AT ALL WHEN THERE IS NO NEIGHBOUR EITHER SIDE. A single
11
+ * page site, or a route outside the tree, would otherwise get an empty
12
+ * landmark — announced by a screen reader as a navigation region containing
13
+ * nothing, which is worse than the absence it is standing in for.
14
+ *
15
+ * No client JavaScript: two links and a heading, rendered on the server.
16
+ */
17
+ function DocsPager({ previous, next, Link, previousLabel = "Previous", nextLabel = "Next", label = "Pagination", className }) {
18
+ if (previous === void 0 && next === void 0) return null;
19
+ return /* @__PURE__ */ jsxs("nav", {
20
+ "aria-label": label,
21
+ className: ["wave-docs-pager", className].filter(Boolean).join(" "),
22
+ children: [/* @__PURE__ */ jsx(PagerLink, {
23
+ stop: previous,
24
+ direction: "previous",
25
+ caption: previousLabel,
26
+ Link
27
+ }), /* @__PURE__ */ jsx(PagerLink, {
28
+ stop: next,
29
+ direction: "next",
30
+ caption: nextLabel,
31
+ Link
32
+ })]
33
+ });
34
+ }
35
+ function PagerLink({ stop, direction, caption, Link }) {
36
+ if (stop === void 0) return /* @__PURE__ */ jsx("div", { className: "wave-docs-pager__gap" });
37
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
38
+ className: "wave-docs-pager__caption",
39
+ children: [
40
+ direction === "previous" ? /* @__PURE__ */ jsx(PagerChevron, {}) : null,
41
+ caption,
42
+ direction === "next" ? /* @__PURE__ */ jsx(PagerChevron, {}) : null
43
+ ]
44
+ }), /* @__PURE__ */ jsx("span", {
45
+ className: "wave-docs-pager__title",
46
+ children: stop.title
47
+ })] });
48
+ const props = {
49
+ className: "wave-docs-pager__link",
50
+ href: stop.href,
51
+ "data-direction": direction,
52
+ "aria-label": `${caption}: ${stop.title}`
53
+ };
54
+ return Link === void 0 ? /* @__PURE__ */ jsx("a", {
55
+ ...props,
56
+ children: body
57
+ }) : /* @__PURE__ */ jsx(Link, {
58
+ ...props,
59
+ children: body
60
+ });
61
+ }
62
+ /**
63
+ * The arrow on the outer edge of each link, pointing the way it goes.
64
+ *
65
+ * Decorative: the link is named "Previous: Installation" by `aria-label`, so
66
+ * this would only repeat a word already in the name — and `⌘`-style symbols
67
+ * read badly when they reach a screen reader at all.
68
+ *
69
+ * Which way it points is the stylesheet's, not this component's: it is a
70
+ * physical direction, and it mirrors under `dir="rtl"` where "previous" is on
71
+ * the right. Rotating it here would put that decision somewhere CSS cannot
72
+ * correct it.
73
+ */
74
+ function PagerChevron() {
75
+ return /* @__PURE__ */ jsx("svg", {
76
+ className: "wave-docs-pager__chevron",
77
+ "aria-hidden": "true",
78
+ focusable: "false",
79
+ viewBox: "0 0 24 24",
80
+ width: "16",
81
+ height: "16",
82
+ fill: "none",
83
+ stroke: "currentColor",
84
+ strokeWidth: "2",
85
+ strokeLinecap: "round",
86
+ strokeLinejoin: "round",
87
+ children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
88
+ });
89
+ }
90
+ //#endregion
91
+ export { DocsPager };
@@ -70,6 +70,30 @@ interface DocsLabels {
70
70
  toc?: string | undefined;
71
71
  /** The link at the end of the TOC. Default `'Back to top'`. */
72
72
  backToTop?: string | undefined;
73
+ /**
74
+ * Above the previous page's title in the pager. Defaults to `'Previous'`.
75
+ *
76
+ * The direction, not the destination: the page's own title is the name, and
77
+ * these two words are what say which way it lies.
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;
86
+ previousPage?: string | undefined;
87
+ /** The same for the next page. Defaults to `'Next'`. */
88
+ nextPage?: string | undefined;
89
+ /**
90
+ * Accessible name for the pager landmark. Defaults to `'Pagination'`.
91
+ *
92
+ * A page carries three navigation landmarks — the sidebar, the table of
93
+ * contents and this — and "navigation" three times is not a list anyone can
94
+ * steer by.
95
+ */
96
+ pagination?: string | undefined;
73
97
  /**
74
98
  * Screen-reader suffix on a link that opens a new tab.
75
99
  * Default `'(opens in a new tab)'`.
@@ -137,7 +161,7 @@ interface DocsLabels {
137
161
  * failure being guarded against is a key that is declared, documented and never
138
162
  * read, which type-checks perfectly.
139
163
  */
140
- 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", "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"];
141
165
  /** The four the shell renders itself, resolved centrally. */
142
166
  type ShellLabelKey = 'nav' | 'openNav' | 'closeNav' | 'skipToContent';
143
167
  /**
@@ -27,6 +27,10 @@ const DOCS_LABEL_KEYS = [
27
27
  "youtubeHide",
28
28
  "copyCode",
29
29
  "copyCodeFrom",
30
+ "explore",
31
+ "previousPage",
32
+ "nextPage",
33
+ "pagination",
30
34
  "copied",
31
35
  "copyFailed"
32
36
  ];
package/dist/react/toc.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { DOCS_CONTENT_ID } from "../docs-content-id.js";
3
- import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/react/toc.tsx
6
6
  const DEFAULT_ROOT_MARGIN = "0px 0px -60% 0px";
@@ -48,14 +48,26 @@ function DocsToc({ entries, label = "On this page", rootMargin = DEFAULT_ROOT_MA
48
48
  lastIds.current = ids;
49
49
  setActiveId(void 0);
50
50
  }
51
+ const inBand = useRef(/* @__PURE__ */ new Set());
52
+ const atBottom = useRef(false);
53
+ const resolveActive = useCallback(() => {
54
+ if (atBottom.current) {
55
+ const last = ids[ids.length - 1];
56
+ if (last !== void 0) {
57
+ setActiveId(last);
58
+ return;
59
+ }
60
+ }
61
+ const next = ids.find((id) => inBand.current.has(id));
62
+ if (next !== void 0) setActiveId(next);
63
+ }, [ids]);
51
64
  useEffect(() => {
52
65
  if (ids.length === 0 || typeof IntersectionObserver === "undefined") return;
53
- const visible = /* @__PURE__ */ new Set();
66
+ const visible = inBand.current;
54
67
  const observer = new IntersectionObserver((records) => {
55
68
  for (const record of records) if (record.isIntersecting) visible.add(record.target.id);
56
69
  else visible.delete(record.target.id);
57
- const next = ids.find((id) => visible.has(id));
58
- if (next !== void 0) setActiveId(next);
70
+ resolveActive();
59
71
  }, {
60
72
  rootMargin,
61
73
  threshold: 0
@@ -81,7 +93,24 @@ function DocsToc({ entries, label = "On this page", rootMargin = DEFAULT_ROOT_MA
81
93
  if (frame !== void 0) cancelAnimationFrame(frame);
82
94
  observer.disconnect();
83
95
  };
84
- }, [ids, rootMargin]);
96
+ }, [
97
+ ids,
98
+ rootMargin,
99
+ resolveActive
100
+ ]);
101
+ useEffect(() => {
102
+ if (ids[ids.length - 1] === void 0 || typeof window === "undefined") return;
103
+ const onScroll = () => {
104
+ const scrollable = document.documentElement.scrollHeight - window.innerHeight;
105
+ const bottom = scrollable > 2 && window.scrollY >= scrollable - 2;
106
+ if (bottom === atBottom.current) return;
107
+ atBottom.current = bottom;
108
+ resolveActive();
109
+ };
110
+ window.addEventListener("scroll", onScroll, { passive: true });
111
+ onScroll();
112
+ return () => window.removeEventListener("scroll", onScroll);
113
+ }, [ids, resolveActive]);
85
114
  if (entries.length === 0) return null;
86
115
  return /* @__PURE__ */ jsxs("nav", {
87
116
  "aria-label": label,