@waveso/docs 0.10.0 → 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.
package/dist/next.js CHANGED
@@ -3,12 +3,13 @@ import { docsError } from "./docs-error.js";
3
3
  import { DOCS_CONTENT_ID } from "./docs-content-id.js";
4
4
  import { describeSuggestion } from "./link-suggestion.js";
5
5
  import { mapPooled } from "./map-pooled.js";
6
- import { neighbours } from "./nav-order.js";
6
+ import { neighbours, readingOrder, stepTitle } from "./nav-order.js";
7
7
  import { findFunctionValuedOptions } from "./search-options.js";
8
8
  import { createMarkdownComponents } from "./react/markdown-components.js";
9
9
  import { DocContent } from "./react/doc-content.js";
10
10
  import { DocsHero } from "./react/hero.js";
11
11
  import { DocsPager } from "./react/pager.js";
12
+ import { DocsExplore } from "./react/explore.js";
12
13
  import { DocsToc } from "./react/toc.js";
13
14
  import { wrapNextLink } from "./react/link-adapter.js";
14
15
  import { createDocsRenderer } from "./render.js";
@@ -429,6 +430,25 @@ function createDocsRoute(options) {
429
430
  etag: `"${createHash("sha1").update(json).digest("hex")}"`
430
431
  } });
431
432
  };
433
+ /**
434
+ * A page's `next` rows with their link text filled in.
435
+ *
436
+ * ⚠️ THE ERROR IS THE POINT. Falling back to the href renders a URL where a
437
+ * sentence should be, on a page that builds cleanly — so an unresolvable row
438
+ * stops the build and names both fixes.
439
+ */
440
+ async function resolveExplore(doc) {
441
+ const stops = readingOrder(await requestScopedSource.nav());
442
+ return (doc.frontmatter.explore ?? []).map((step) => {
443
+ const title = stepTitle(stops, step);
444
+ if (title === void 0) throw docsError("invalid-frontmatter", `${doc.href}: the "explore" entry pointing at "${step.href}" has no title, and no page in the navigation owns that route. Point it at a page in the tree, or give the entry its own \`title\` — which is what an external link or a page kept out of the navigation needs.`);
445
+ return {
446
+ question: step.question,
447
+ href: step.href,
448
+ title
449
+ };
450
+ });
451
+ }
432
452
  async function renderRoute(segments) {
433
453
  const doc = await getPage(segments);
434
454
  if (doc === void 0) return (await loadNotFound())();
@@ -450,6 +470,11 @@ function createDocsRoute(options) {
450
470
  ...options.components
451
471
  },
452
472
  ...copyLabels === void 0 ? {} : { labels: copyLabels }
473
+ }), (doc.frontmatter.explore?.length ?? 0) === 0 ? null : createElement(DocsExplore, {
474
+ links: await resolveExplore(doc),
475
+ Link: link,
476
+ ...routeLabels?.explore === void 0 ? {} : { heading: routeLabels.explore },
477
+ ...routeLabels?.externalLink === void 0 ? {} : { externalLabel: routeLabels.externalLink }
453
478
  }), options.pager === false ? null : createElement(DocsPager, {
454
479
  ...neighbours(await requestScopedSource.nav(), doc.href),
455
480
  Link: link,
@@ -1,4 +1,4 @@
1
- import { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE } from "../code-frame.js";
1
+ import { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_ICON_PATHS, CODE_ICON_STATES } from "../code-frame.js";
2
2
  import { parseCodeMeta } from "../code-meta.js";
3
3
  import { CONTINUE, SKIP, visit } from "unist-util-visit";
4
4
  //#region src/plugins/rehype-code-frame.ts
@@ -28,12 +28,17 @@ const rehypeCodeFrame = (options = {}) => {
28
28
  value: title
29
29
  }]
30
30
  });
31
- children.push(copyButton(title === void 0 ? copyLabel : copyFromLabel.replace("{title}", title)), node);
31
+ children.push(copyButton(title === void 0 ? copyLabel : copyFromLabel.replace("{title}", title)), {
32
+ type: "element",
33
+ tagName: "div",
34
+ properties: { className: ["wave-docs-panel__body", "wave-docs-code__body"] },
35
+ children: [node]
36
+ });
32
37
  parent.children[index] = {
33
38
  type: "element",
34
39
  tagName: "figure",
35
40
  properties: {
36
- className: ["wave-docs-code"],
41
+ className: ["wave-docs-panel", "wave-docs-code"],
37
42
  [CODE_FRAME_ATTRIBUTE]: "",
38
43
  ...language === void 0 ? {} : { "data-lang": language }
39
44
  },
@@ -60,15 +65,40 @@ function copyButton(label) {
60
65
  [CODE_COPY_ATTRIBUTE]: "",
61
66
  "aria-label": label
62
67
  },
63
- children: [{
68
+ children: CODE_ICON_STATES.map(([state, name]) => stateIcon(state, name))
69
+ };
70
+ }
71
+ /**
72
+ * One Lucide glyph, as hast.
73
+ *
74
+ * Decoration, so it is out of the accessibility tree — the button reads as its
75
+ * `aria-label` — and out of the search index by the same rule that drops the
76
+ * heading anchor icons.
77
+ */
78
+ function stateIcon(state, name) {
79
+ return {
80
+ type: "element",
81
+ tagName: "svg",
82
+ properties: {
83
+ className: ["wave-docs-code__copy-icon"],
84
+ "data-state": state,
85
+ "aria-hidden": "true",
86
+ focusable: "false",
87
+ viewBox: "0 0 24 24",
88
+ width: "16",
89
+ height: "16",
90
+ fill: "none",
91
+ stroke: "currentColor",
92
+ strokeWidth: "2",
93
+ strokeLinecap: "round",
94
+ strokeLinejoin: "round"
95
+ },
96
+ children: CODE_ICON_PATHS[name].map((d) => ({
64
97
  type: "element",
65
- tagName: "span",
66
- properties: { "aria-hidden": "true" },
67
- children: [{
68
- type: "text",
69
- value: "⧉"
70
- }]
71
- }]
98
+ tagName: "path",
99
+ properties: { d },
100
+ children: []
101
+ }))
72
102
  };
73
103
  }
74
104
  /** `code.data.meta` — read, never written. */
@@ -0,0 +1,53 @@
1
+ import { DocsLinkComponent } from "./markdown-components.js";
2
+ import { ReactNode } from "react";
3
+ //#region src/react/explore.d.ts
4
+ /** One row: a question, and the page that answers it. */
5
+ interface DocsExploreLink {
6
+ question: string;
7
+ href: string;
8
+ /** Resolved by the caller — from the navigation, or from the frontmatter. */
9
+ title: string;
10
+ }
11
+ interface DocsExploreProps {
12
+ links: DocsExploreLink[];
13
+ /** The block's heading. Defaults to `'Where to go next'`. */
14
+ heading?: string | undefined;
15
+ /** Client-side router link, e.g. `next/link`. Falls back to `<a>`. */
16
+ Link?: DocsLinkComponent | undefined;
17
+ /**
18
+ * Screen-reader suffix on a row whose answer is off-site. Defaults to
19
+ * `'(opens in a new tab)'`.
20
+ */
21
+ externalLabel?: string | undefined;
22
+ className?: string | undefined;
23
+ }
24
+ /**
25
+ * "Where to go next" — a question per row, and the page that answers it.
26
+ *
27
+ * The sidebar is a structure and this is a router: it says *why* a reader would
28
+ * go somewhere, which no tree of titles can. A page opts in by declaring
29
+ * `explore` in its frontmatter, so the same component is a landing page's
30
+ * onboarding and an ordinary page's footnote.
31
+ *
32
+ * ⚠️ `explore`, NOT `next` — AND NOT `steps` EITHER.
33
+ *
34
+ * `next` is unusable in this package: `src/next.ts` is the Next.js adapter, so
35
+ * two files one directory apart would carry the same name for entirely
36
+ * different things, and `doc.frontmatter.next` would read like a routing hook.
37
+ *
38
+ * `steps` is wrong for a second reason: these rows are a *branch*, not a
39
+ * sequence. A reader picks one and ignores the rest; none of them is first. A
40
+ * numbered "1 → 2 → 3" component is a real and separate thing worth building,
41
+ * and `steps` is the name it will need.
42
+ *
43
+ * ⚠️ A LIST, NOT A TABLE — which is what the markdown this replaces had to be.
44
+ * A screen reader announces "table, 2 columns, 7 rows" for what is a list of
45
+ * links with descriptions, and asks the reader to navigate it by cell. Two
46
+ * columns of sentence-length questions are also cramped on a phone, where this
47
+ * stacks instead.
48
+ *
49
+ * No client JavaScript: a heading and a list of links, rendered on the server.
50
+ */
51
+ declare function DocsExplore({ links, heading, Link, externalLabel, className }: DocsExploreProps): ReactNode;
52
+ //#endregion
53
+ export { DocsExplore, DocsExploreLink, DocsExploreProps };
@@ -0,0 +1,86 @@
1
+ import { opensInNewTab } from "../safe-href.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/react/explore.tsx
4
+ /**
5
+ * "Where to go next" — a question per row, and the page that answers it.
6
+ *
7
+ * The sidebar is a structure and this is a router: it says *why* a reader would
8
+ * go somewhere, which no tree of titles can. A page opts in by declaring
9
+ * `explore` in its frontmatter, so the same component is a landing page's
10
+ * onboarding and an ordinary page's footnote.
11
+ *
12
+ * ⚠️ `explore`, NOT `next` — AND NOT `steps` EITHER.
13
+ *
14
+ * `next` is unusable in this package: `src/next.ts` is the Next.js adapter, so
15
+ * two files one directory apart would carry the same name for entirely
16
+ * different things, and `doc.frontmatter.next` would read like a routing hook.
17
+ *
18
+ * `steps` is wrong for a second reason: these rows are a *branch*, not a
19
+ * sequence. A reader picks one and ignores the rest; none of them is first. A
20
+ * numbered "1 → 2 → 3" component is a real and separate thing worth building,
21
+ * and `steps` is the name it will need.
22
+ *
23
+ * ⚠️ A LIST, NOT A TABLE — which is what the markdown this replaces had to be.
24
+ * A screen reader announces "table, 2 columns, 7 rows" for what is a list of
25
+ * links with descriptions, and asks the reader to navigate it by cell. Two
26
+ * columns of sentence-length questions are also cramped on a phone, where this
27
+ * stacks instead.
28
+ *
29
+ * No client JavaScript: a heading and a list of links, rendered on the server.
30
+ */
31
+ function DocsExplore({ links, heading = "Where to go next", Link, externalLabel = "(opens in a new tab)", className }) {
32
+ if (links.length === 0) return null;
33
+ return /* @__PURE__ */ jsxs("section", {
34
+ className: [
35
+ "wave-docs-panel",
36
+ "wave-docs-explore",
37
+ className
38
+ ].filter(Boolean).join(" "),
39
+ "aria-labelledby": "wave-docs-explore-heading",
40
+ children: [/* @__PURE__ */ jsx("div", {
41
+ className: "wave-docs-panel__header",
42
+ children: /* @__PURE__ */ jsx("h2", {
43
+ className: "wave-docs-panel__title",
44
+ id: "wave-docs-explore-heading",
45
+ children: heading
46
+ })
47
+ }), /* @__PURE__ */ jsx("ul", {
48
+ className: "wave-docs-panel__body wave-docs-explore__list",
49
+ children: links.map((row) => /* @__PURE__ */ jsxs("li", {
50
+ className: "wave-docs-explore__item",
51
+ children: [/* @__PURE__ */ jsx("span", {
52
+ className: "wave-docs-explore__question",
53
+ children: row.question
54
+ }), /* @__PURE__ */ jsx(ExploreAnswer, {
55
+ row,
56
+ Link,
57
+ externalLabel
58
+ })]
59
+ }, row.href))
60
+ })]
61
+ });
62
+ }
63
+ function ExploreAnswer({ row, Link, externalLabel }) {
64
+ const className = "wave-docs-explore__answer";
65
+ if (opensInNewTab(row.href)) return /* @__PURE__ */ jsxs("a", {
66
+ className,
67
+ href: row.href,
68
+ target: "_blank",
69
+ rel: "noopener noreferrer",
70
+ children: [row.title, /* @__PURE__ */ jsxs("span", {
71
+ className: "wave-docs-sr-only",
72
+ children: [" ", externalLabel]
73
+ })]
74
+ });
75
+ return Link === void 0 ? /* @__PURE__ */ jsx("a", {
76
+ className,
77
+ href: row.href,
78
+ children: row.title
79
+ }) : /* @__PURE__ */ jsx(Link, {
80
+ className,
81
+ href: row.href,
82
+ children: row.title
83
+ });
84
+ }
85
+ //#endregion
86
+ export { DocsExplore };
@@ -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 }) {
@@ -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",