@waveso/docs 0.3.0 → 0.5.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +160 -39
  3. package/dist/errors.d.ts +2 -0
  4. package/dist/highlighter.js +2 -1
  5. package/dist/meta.js +6 -9
  6. package/dist/next.d.ts +54 -14
  7. package/dist/next.js +115 -18
  8. package/dist/plugins/rehype-code-frame.d.ts +13 -1
  9. package/dist/plugins/rehype-code-frame.js +2 -1
  10. package/dist/plugins/remark-doc-links.d.ts +51 -1
  11. package/dist/plugins/remark-doc-links.js +27 -16
  12. package/dist/plugins/remark-youtube.d.ts +18 -3
  13. package/dist/plugins/remark-youtube.js +57 -9
  14. package/dist/react/callout.d.ts +13 -1
  15. package/dist/react/callout.js +2 -2
  16. package/dist/react/code-runtime.d.ts +12 -2
  17. package/dist/react/code-runtime.js +28 -4
  18. package/dist/react/doc-content.d.ts +12 -1
  19. package/dist/react/doc-content.js +2 -2
  20. package/dist/react/layout.d.ts +27 -10
  21. package/dist/react/layout.js +6 -3
  22. package/dist/react/markdown-components.d.ts +29 -1
  23. package/dist/react/markdown-components.js +69 -67
  24. package/dist/react/nav.d.ts +5 -1
  25. package/dist/react/nav.js +5 -2
  26. package/dist/react/next-nav.d.ts +5 -1
  27. package/dist/react/next-nav.js +5 -2
  28. package/dist/react/search-dialog.d.ts +92 -5
  29. package/dist/react/search-dialog.js +182 -43
  30. package/dist/react/shell-labels.d.ts +135 -21
  31. package/dist/react/shell-labels.js +47 -6
  32. package/dist/react/sidebar.d.ts +18 -1
  33. package/dist/react/sidebar.js +59 -23
  34. package/dist/react/youtube.d.ts +22 -1
  35. package/dist/react/youtube.js +22 -4
  36. package/dist/render.d.ts +11 -0
  37. package/dist/render.js +38 -11
  38. package/dist/route-path.js +7 -2
  39. package/dist/safe-href.d.ts +47 -0
  40. package/dist/safe-href.js +73 -0
  41. package/dist/search-index.js +1 -1
  42. package/dist/search-options.d.ts +64 -2
  43. package/dist/search-options.js +25 -1
  44. package/dist/semaphore.d.ts +46 -0
  45. package/dist/semaphore.js +60 -0
  46. package/dist/source.js +80 -10
  47. package/dist/styles.css +62 -13
  48. package/dist/types.d.ts +30 -0
  49. package/package.json +1 -1
@@ -16,6 +16,18 @@ interface CalloutProps {
16
16
  type?: string | undefined;
17
17
  /** Overrides the default label ("Note", "Warning", …). */
18
18
  title?: string | undefined;
19
+ /**
20
+ * Default headings per type, for a site that is not in English.
21
+ *
22
+ * `title` still wins — it is what a single callout in the markdown asked for,
23
+ * and this is what every callout on the site is called otherwise.
24
+ *
25
+ * Here rather than resolved by the caller so that {@link normalizeCalloutType}
26
+ * stays the one place that decides what an unrecognised type falls back to. A
27
+ * caller picking the heading itself would need that rule too, and a second
28
+ * copy of it is a second thing to keep in step.
29
+ */
30
+ labels?: Partial<Record<CalloutType, string>> | undefined;
19
31
  className?: string | undefined;
20
32
  children?: ReactNode;
21
33
  }
@@ -32,6 +44,6 @@ interface CalloutProps {
32
44
  * All styling lives in `@waveso/docs/styles.css` under `.wave-docs-callout`,
33
45
  * so consumers can restyle it without forking the component.
34
46
  */
35
- declare function Callout({ type, title, className, children }: CalloutProps): ReactNode;
47
+ declare function Callout({ type, title, labels, className, children }: CalloutProps): ReactNode;
36
48
  //#endregion
37
49
  export { CALLOUT_TYPES, Callout, CalloutProps, CalloutType };
@@ -69,9 +69,9 @@ function normalizeCalloutType(value) {
69
69
  * All styling lives in `@waveso/docs/styles.css` under `.wave-docs-callout`,
70
70
  * so consumers can restyle it without forking the component.
71
71
  */
72
- function Callout({ type, title, className, children }) {
72
+ function Callout({ type, title, labels, className, children }) {
73
73
  const kind = normalizeCalloutType(type);
74
- const label = title?.trim() || CALLOUT_LABELS[kind];
74
+ const label = title?.trim() || labels?.[kind] || CALLOUT_LABELS[kind];
75
75
  return /* @__PURE__ */ jsxs("aside", {
76
76
  role: "note",
77
77
  "aria-label": label,
@@ -1,5 +1,15 @@
1
1
  import { ReactNode } from "react";
2
2
  //#region src/react/code-runtime.d.ts
3
+ /** The two announcements, for a site that is not in English. */
4
+ interface CodeRuntimeLabels {
5
+ /** Announced after a successful copy. Default `'Copied to the clipboard.'` */
6
+ copied?: string | undefined;
7
+ /**
8
+ * Announced after a failed one. Default
9
+ * `'Copy failed. Select the code and press Control or Command + C.'`
10
+ */
11
+ copyFailed?: string | undefined;
12
+ }
3
13
  /**
4
14
  * Mount the copy runtime. Renders nothing.
5
15
  *
@@ -9,6 +19,6 @@ import { ReactNode } from "react";
9
19
  * re-renders and there is no state to get out of step with a page that was
10
20
  * server-rendered.
11
21
  */
12
- declare function DocsCodeRuntime(): ReactNode;
22
+ declare function DocsCodeRuntime({ copied, copyFailed }?: CodeRuntimeLabels): ReactNode;
13
23
  //#endregion
14
- export { DocsCodeRuntime };
24
+ export { CodeRuntimeLabels, DocsCodeRuntime };
@@ -40,6 +40,24 @@ const SKIP_LINE_CLASSES = [];
40
40
  const TRIMMED_LINE_CLASSES = [];
41
41
  let refCount = 0;
42
42
  let detach;
43
+ const DEFAULT_COPIED = "Copied to the clipboard.";
44
+ const DEFAULT_COPY_FAILED = "Copy failed. Select the code and press Control or Command + C.";
45
+ /**
46
+ * Module scope, beside `refCount`, because the listener is a singleton too.
47
+ *
48
+ * The runtime installs once per page however many `DocContent`s mount it, so
49
+ * the messages belong to the installation rather than to a component — and two
50
+ * mounts with different labels would be a page with two languages in it, which
51
+ * is not a case worth code. First one in wins, and `refCount` says which.
52
+ *
53
+ * Spelled out rather than `Required<CodeRuntimeLabels>`: the props are declared
54
+ * `string | undefined` for `exactOptionalPropertyTypes`, and `Required` strips
55
+ * the `?` while leaving the `undefined` in the value type.
56
+ */
57
+ let messages = {
58
+ copied: DEFAULT_COPIED,
59
+ copyFailed: DEFAULT_COPY_FAILED
60
+ };
43
61
  /**
44
62
  * Mount the copy runtime. Renders nothing.
45
63
  *
@@ -49,10 +67,16 @@ let detach;
49
67
  * re-renders and there is no state to get out of step with a page that was
50
68
  * server-rendered.
51
69
  */
52
- function DocsCodeRuntime() {
70
+ function DocsCodeRuntime({ copied, copyFailed } = {}) {
53
71
  useEffect(() => {
54
72
  refCount += 1;
55
- if (refCount === 1) detach = install();
73
+ if (refCount === 1) {
74
+ messages = {
75
+ copied: copied ?? DEFAULT_COPIED,
76
+ copyFailed: copyFailed ?? DEFAULT_COPY_FAILED
77
+ };
78
+ detach = install();
79
+ }
56
80
  return () => {
57
81
  refCount -= 1;
58
82
  if (refCount === 0) {
@@ -60,7 +84,7 @@ function DocsCodeRuntime() {
60
84
  detach = void 0;
61
85
  }
62
86
  };
63
- }, []);
87
+ }, [copied, copyFailed]);
64
88
  return null;
65
89
  }
66
90
  function install() {
@@ -110,7 +134,7 @@ function readCode(pre) {
110
134
  async function copy(text, button, status) {
111
135
  const copied = await writeClipboard(text) ? "true" : "false";
112
136
  button.dataset.copied = copied;
113
- status.textContent = copied === "true" ? "Copied to the clipboard." : "Copy failed. Select the code and press Control or Command + C.";
137
+ status.textContent = copied === "true" ? messages.copied : messages.copyFailed;
114
138
  const existing = timers.get(button);
115
139
  if (existing !== void 0) window.clearTimeout(existing);
116
140
  timers.set(button, window.setTimeout(() => {
@@ -1,4 +1,5 @@
1
1
  import { MarkdownComponents } from "./markdown-components.js";
2
+ import { CodeRuntimeLabels } from "./code-runtime.js";
2
3
  import { ReactNode } from "react";
3
4
  import { Root } from "hast";
4
5
  //#region src/react/doc-content.d.ts
@@ -20,6 +21,16 @@ interface DocContentProps {
20
21
  * choice rather than as a mistake.
21
22
  */
22
23
  className?: string | undefined;
24
+ /**
25
+ * The two things the copy runtime announces, for a site that is not in
26
+ * English.
27
+ *
28
+ * Here rather than on `docs.Layout` because this is the component that mounts
29
+ * the runtime, and it is the one no consumer can avoid — the hand-rolled route
30
+ * in the README renders it directly. Forwarded only when set, so a site that
31
+ * overrides nothing sends no extra props across the boundary.
32
+ */
33
+ labels?: CodeRuntimeLabels | undefined;
23
34
  }
24
35
  /**
25
36
  * Render a hast tree as React elements, inside the prose wrapper.
@@ -61,6 +72,6 @@ interface DocContentProps {
61
72
  * you, because `node` is a legal prop on the component and an unknown attribute
62
73
  * on the element.
63
74
  */
64
- declare function DocContent({ hast, components, className }: DocContentProps): ReactNode;
75
+ declare function DocContent({ hast, components, className, labels }: DocContentProps): ReactNode;
65
76
  //#endregion
66
77
  export { DocContent, DocContentProps };
@@ -44,10 +44,10 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
44
44
  * you, because `node` is a legal prop on the component and an unknown attribute
45
45
  * on the element.
46
46
  */
47
- function DocContent({ hast, components, className }) {
47
+ function DocContent({ hast, components, className, labels }) {
48
48
  return /* @__PURE__ */ jsxs("div", {
49
49
  className: className === void 0 || className === "" ? "wave-docs-prose" : `wave-docs-prose ${className}`,
50
- children: [hasCodeFrame(hast) ? /* @__PURE__ */ jsx(DocsCodeRuntime, {}) : null, toJsxRuntime(hast, {
50
+ children: [hasCodeFrame(hast) ? /* @__PURE__ */ jsx(DocsCodeRuntime, { ...labels }) : null, toJsxRuntime(hast, {
51
51
  Fragment,
52
52
  jsx,
53
53
  jsxs,
@@ -1,6 +1,7 @@
1
1
  import { DocNavNode } from "../types.js";
2
- import { DocsSearchProps } from "./next-search.js";
2
+ import { SerializableSearchOptions } from "../search-options.js";
3
3
  import { DocsLabels } from "./shell-labels.js";
4
+ import { DocsSearchProps } from "./next-search.js";
4
5
  import { ReactNode } from "react";
5
6
  //#region src/react/layout.d.ts
6
7
  /**
@@ -9,8 +10,19 @@ import { ReactNode } from "react";
9
10
  * `indexUrl` is derived from `basePath` and is not negotiable here: the whole
10
11
  * reason `docs.Layout` exists is that nobody should have to know the index's
11
12
  * address, and a hand-passed one is wrong under every non-root `basePath`.
13
+ *
14
+ * `miniSearchOptions` is narrower than the one `DocsSearch` itself takes, and
15
+ * has to be. `docs.Layout` is a Server Component and `DocsSearch` is a Client
16
+ * Component, so everything here is serialised on its way across — a `tokenize`
17
+ * or a `processTerm` passed at this seam fails `next build` with *"Functions
18
+ * cannot be passed directly to Client Components"*. {@link
19
+ * SerializableSearchOptions} documents the escape hatch: a `'use client'`
20
+ * wrapper of your own, where the function is a module import on both sides
21
+ * rather than a prop between them.
12
22
  */
13
- type DocsLayoutSearchProps = Omit<DocsSearchProps, 'indexUrl'>;
23
+ type DocsLayoutSearchProps = Omit<DocsSearchProps, 'indexUrl' | 'miniSearchOptions'> & {
24
+ miniSearchOptions?: SerializableSearchOptions | undefined;
25
+ };
14
26
  interface DocsLayoutShellProps {
15
27
  children: ReactNode;
16
28
  nav: DocNavNode[];
@@ -20,14 +32,19 @@ interface DocsLayoutShellProps {
20
32
  /**
21
33
  * `false` to omit the trigger; an object to configure it.
22
34
  *
23
- * ⚠️ AN OBJECT IS WHAT MAKES `miniSearchOptions` REACHABLE. MiniSearch reads
24
- * `tokenize` and `processTerm` both when indexing and when querying, so the
25
- * object `createDocsRoute` built the index with has to be the object the
26
- * dialog queries it with — and while this was a bare boolean there was no
27
- * channel for it at all. Configuring the route and rendering `docs.Layout`
28
- * produced an index whose terms no query could spell: zero results, no error,
29
- * nothing in the console, and the option's own docstring warning about
30
- * exactly that.
35
+ * ⚠️ AN OBJECT IS WHAT MAKES `miniSearchOptions` REACHABLE, AND ONLY THE
36
+ * SERIALISABLE PART OF IT. MiniSearch reads `tokenize` and `processTerm` both
37
+ * when indexing and when querying, so the object `createDocsRoute` built the
38
+ * index with has to be the object the dialog queries it with — and while this
39
+ * was a bare boolean there was no channel for it at all. Configuring the
40
+ * route and rendering `docs.Layout` produced an index whose terms no query
41
+ * could spell: zero results, no error, nothing in the console.
42
+ *
43
+ * Widening it to a boolean-or-object fixed that for data overrides and broke
44
+ * the function ones, which is the harder half: this prop is serialised on its
45
+ * way from a Server Component to a Client one, so a function in it is a build
46
+ * failure rather than a silent miss. `createDocsRoute` refuses to forward one
47
+ * and says so; {@link DocsLayoutSearchProps} carries the remedy.
31
48
  */
32
49
  search?: boolean | DocsLayoutSearchProps | undefined;
33
50
  /**
@@ -38,8 +38,8 @@ function DocsLayoutShell({ children, nav, searchIndexUrl, title, actions, search
38
38
  }),
39
39
  search === false ? null : /* @__PURE__ */ jsx(DocsSearch, {
40
40
  indexUrl: searchIndexUrl,
41
- className: "wave-docs-layout__search",
42
- ...search === true ? {} : search
41
+ ...search === true ? {} : search,
42
+ className: ["wave-docs-layout__search", search === true ? void 0 : search?.className].filter(Boolean).join(" ")
43
43
  }),
44
44
  actions === void 0 ? null : /* @__PURE__ */ jsx("div", {
45
45
  className: "wave-docs-layout__actions",
@@ -55,7 +55,10 @@ function DocsLayoutShell({ children, nav, searchIndexUrl, title, actions, search
55
55
  children: /* @__PURE__ */ jsx(DocsNextNav, {
56
56
  nav,
57
57
  label: text.nav,
58
- closeLabel: text.closeNav
58
+ closeLabel: text.closeNav,
59
+ ...labels?.expandGroup === void 0 ? {} : { expandGroup: labels.expandGroup },
60
+ ...labels?.collapseGroup === void 0 ? {} : { collapseGroup: labels.collapseGroup },
61
+ ...labels?.externalLink === void 0 ? {} : { externalLink: labels.externalLink }
59
62
  })
60
63
  }), children]
61
64
  })
@@ -1,4 +1,5 @@
1
1
  import { CalloutProps } from "./callout.js";
2
+ import { DocsLabels } from "./shell-labels.js";
2
3
  import { YouTubeProps } from "./youtube.js";
3
4
  import { ComponentProps, ComponentType, JSX, ReactNode } from "react";
4
5
  //#region src/react/markdown-components.d.ts
@@ -53,6 +54,20 @@ interface DocsImageProps {
53
54
  className?: string | undefined;
54
55
  sizes?: string | undefined;
55
56
  loading?: 'eager' | 'lazy' | undefined;
57
+ /**
58
+ * Passed through to the image. Defaults to `'async'` on the plain `<img>`.
59
+ *
60
+ * ⚠️ DECLARED BECAUSE THE COMMENT BELOW PROMISED IT AND THE CODE DROPPED IT.
61
+ * `createImage` spreads the tree's own attributes into whichever component it
62
+ * was given, with a comment saying `decoding` and `fetchPriority` survive into
63
+ * the optimising branch — and `wrapNextImage` destructures a fixed list, so
64
+ * they did not. A closed props interface is the right shape for this seam, so
65
+ * the two the comment named are members of it now rather than a promise it
66
+ * could not keep.
67
+ */
68
+ decoding?: 'async' | 'auto' | 'sync' | undefined;
69
+ /** Passed through to the image. `'high'` on a hero image is the usual reason. */
70
+ fetchPriority?: 'high' | 'low' | 'auto' | undefined;
56
71
  }
57
72
  /** A `next/image`-compatible component. */
58
73
  type DocsImageComponent = ComponentType<DocsImageProps>;
@@ -61,7 +76,20 @@ interface MarkdownComponentsOptions {
61
76
  Link?: DocsLinkComponent | undefined;
62
77
  /** Optimising image component, e.g. `next/image`. Falls back to `<img>`. */
63
78
  Image?: DocsImageComponent | undefined;
79
+ /**
80
+ * Overrides for the strings this map renders itself.
81
+ *
82
+ * Five callout headings, the external-link suffix, a wide table's region
83
+ * name and the YouTube facade's three — every one of them hardcoded English
84
+ * until this existed, on a shell whose `labels` prop claimed to be the whole
85
+ * of a site's translatable chrome.
86
+ *
87
+ * All server-rendered, so overriding them costs no client bytes.
88
+ */
89
+ labels?: MarkdownLabels | undefined;
64
90
  }
91
+ /** The subset of `DocsLabels` this map is responsible for. */
92
+ type MarkdownLabels = Pick<DocsLabels, 'externalLink' | 'table' | 'calloutNote' | 'calloutTip' | 'calloutImportant' | 'calloutWarning' | 'calloutCaution' | 'youtubeTitle' | 'youtubePlay' | 'youtubeHide'>;
65
93
  /**
66
94
  * Build the default component map, optionally injecting host-specific link and
67
95
  * image components.
@@ -81,4 +109,4 @@ declare function createMarkdownComponents(options?: MarkdownComponentsOptions):
81
109
  /** The map used when a caller supplies none. Plain `<a>` and `<img>`. */
82
110
  declare const defaultMarkdownComponents: MarkdownComponents;
83
111
  //#endregion
84
- export { DocsImageComponent, DocsImageProps, DocsLinkComponent, DocsLinkProps, MarkdownComponents, MarkdownComponentsOptions, createMarkdownComponents, defaultMarkdownComponents };
112
+ export { DocsImageComponent, DocsImageProps, DocsLinkComponent, DocsLinkProps, MarkdownComponents, MarkdownComponentsOptions, MarkdownLabels, createMarkdownComponents, defaultMarkdownComponents };
@@ -1,24 +1,14 @@
1
+ import { isSafeHref, opensInNewTab } from "../safe-href.js";
1
2
  import { Callout } from "./callout.js";
2
3
  import { YouTube } from "./youtube.js";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  //#region src/react/markdown-components.tsx
5
- /** Schemes we send to a new tab. `mailto:`/`tel:` are left to the OS. */
6
- const HTTP_SCHEME = /^https?:\/\//i;
7
- /** Any URL with a scheme, or protocol-relative. */
6
+ /** Default, and the only string here a reader sees without a screen reader. */
7
+ const DEFAULT_EXTERNAL_LINK = "(opens in a new tab)";
8
+ /** Default name for a wide table's scroll region. */
9
+ const DEFAULT_TABLE = "Table";
10
+ /** Any URL with a scheme, or protocol-relative. Decides router vs plain `<a>`. */
8
11
  const ABSOLUTE_URL = /^([a-z][a-z0-9+.-]*:|\/\/)/i;
9
- /**
10
- * The schemes a markdown link may carry.
11
- *
12
- * GitHub's own allowlist, which is the bar to match: documentation links to
13
- * `sms:`, `ftp:` and `irc:` are ordinary, and an allowlist of three silently
14
- * deleted them. The point of the check is to stop `javascript:`, `data:` and
15
- * `vbscript:` reaching an `href`, not to have an opinion about protocols.
16
- *
17
- * A scheme not listed here — `vscode:`, `obsidian:`, `slack:` — is dropped
18
- * rather than rendered. That is deliberate: an allowlist that grows on request
19
- * is safe, one that guesses is not. {@link warnDroppedHref} makes it visible.
20
- */
21
- const SAFE_SCHEME = /^(https?|mailto|tel|sms|ftp|ftps|irc|ircs|xmpp|news|nntp|feed|git|matrix):/i;
22
12
  /** Hrefs already reported, so a re-render does not repeat the warning. */
23
13
  const warnedHrefs = /* @__PURE__ */ new Set();
24
14
  /**
@@ -32,35 +22,7 @@ const warnedHrefs = /* @__PURE__ */ new Set();
32
22
  function warnDroppedHref(href) {
33
23
  if (process.env.NODE_ENV === "production" || warnedHrefs.has(href)) return;
34
24
  warnedHrefs.add(href);
35
- console.warn(`@waveso/docs: dropped a link to '${href}' — its URL scheme is not in the allowlist, so the text was kept and the destination removed. Use http, https, mailto, tel, sms, ftp, irc, xmpp or matrix, or render the link yourself with a custom \`a\` component.`);
36
- }
37
- /**
38
- * A copy of `href` as a browser will parse it.
39
- *
40
- * ASCII control characters and spaces are stripped before parsing, so
41
- * ` javascript:` and `java<TAB>script:` both navigate where the raw string
42
- * matches no scheme at all — which is how a scheme check gets walked around.
43
- */
44
- function normaliseUrl(href) {
45
- return [...href].filter((char) => (char.codePointAt(0) ?? 0) > 32).join("");
46
- }
47
- /**
48
- * Would this href navigate somewhere we are willing to send a reader?
49
- *
50
- * Nothing upstream filters it: `remarkDocLinks` skips every href with a scheme
51
- * (`isRelativeLink` is false for it), so `assertLinks` never sees one either,
52
- * and `remarkRehype` runs with `allowDangerousHtml` off but passes a link's own
53
- * url through untouched. Verified against React 19: it neutralises
54
- * `javascript:` in every obfuscated form, silently — but it lets `vbscript:`
55
- * and `data:text/html;base64,…` reach the DOM verbatim. So the allowlist is
56
- * ours to keep.
57
- *
58
- * Tested against {@link normaliseUrl}, not the raw string.
59
- */
60
- function isSafeHref(href) {
61
- const normalised = normaliseUrl(href);
62
- if (!ABSOLUTE_URL.test(normalised)) return true;
63
- return normalised.startsWith("//") || SAFE_SCHEME.test(normalised);
25
+ console.warn(`@waveso/docs: dropped a link to '${href}' — its scheme is not in the allowlist. Use http(s), mailto, tel or another documented scheme.`);
64
26
  }
65
27
  function joinClassNames(...values) {
66
28
  const joined = values.filter(Boolean).join(" ");
@@ -73,7 +35,7 @@ function toDimension(value) {
73
35
  return Number.isFinite(parsed) ? parsed : void 0;
74
36
  }
75
37
  }
76
- function createAnchor(Link) {
38
+ function createAnchor(Link, externalLink) {
77
39
  return function MarkdownAnchor({ href, children, ...rest }) {
78
40
  if (href === void 0) return /* @__PURE__ */ jsx("a", {
79
41
  ...rest,
@@ -83,14 +45,14 @@ function createAnchor(Link) {
83
45
  warnDroppedHref(href);
84
46
  return /* @__PURE__ */ jsx("span", { children });
85
47
  }
86
- if (HTTP_SCHEME.test(href) || href.startsWith("//")) return /* @__PURE__ */ jsxs("a", {
48
+ if (opensInNewTab(href)) return /* @__PURE__ */ jsxs("a", {
87
49
  ...rest,
88
50
  href,
89
51
  target: "_blank",
90
52
  rel: "noopener noreferrer",
91
- children: [children, /* @__PURE__ */ jsx("span", {
53
+ children: [children, /* @__PURE__ */ jsxs("span", {
92
54
  className: "wave-docs-sr-only",
93
- children: " (opens in a new tab)"
55
+ children: [" ", externalLink]
94
56
  })]
95
57
  });
96
58
  if (href.startsWith("#") || ABSOLUTE_URL.test(href) || Link === void 0) return /* @__PURE__ */ jsx("a", {
@@ -106,7 +68,7 @@ function createAnchor(Link) {
106
68
  };
107
69
  }
108
70
  function createImage(Image) {
109
- return function MarkdownImage({ src, alt, width, height, title, className, sizes, loading, ...rest }) {
71
+ return function MarkdownImage({ src, alt, width, height, title, className, sizes, loading, decoding, fetchPriority, ...rest }) {
110
72
  const resolvedWidth = toDimension(width);
111
73
  const resolvedHeight = toDimension(height);
112
74
  const resolvedLoading = loading ?? "lazy";
@@ -120,10 +82,11 @@ function createImage(Image) {
120
82
  title,
121
83
  className: resolvedClassName,
122
84
  sizes,
123
- loading: resolvedLoading
85
+ loading: resolvedLoading,
86
+ decoding: decoding ?? "async",
87
+ fetchPriority
124
88
  });
125
89
  return /* @__PURE__ */ jsx("img", {
126
- decoding: "async",
127
90
  ...rest,
128
91
  src,
129
92
  alt: alt ?? "",
@@ -132,7 +95,9 @@ function createImage(Image) {
132
95
  title,
133
96
  className: resolvedClassName,
134
97
  sizes,
135
- loading: resolvedLoading
98
+ loading: resolvedLoading,
99
+ decoding: decoding ?? "async",
100
+ fetchPriority
136
101
  });
137
102
  };
138
103
  }
@@ -146,16 +111,18 @@ function createImage(Image) {
146
111
  * `<section>` is a `region` landmark, so the tab stop announces itself instead
147
112
  * of being a mystery stop in the tab order.
148
113
  */
149
- function MarkdownTable({ className, ...rest }) {
150
- return /* @__PURE__ */ jsx("section", {
151
- className: "wave-docs-table-scroll",
152
- "aria-label": "Table",
153
- tabIndex: 0,
154
- children: /* @__PURE__ */ jsx("table", {
155
- ...rest,
156
- className: joinClassNames("wave-docs-table", className)
157
- })
158
- });
114
+ function createTable(label) {
115
+ return function MarkdownTable({ className, ...rest }) {
116
+ return /* @__PURE__ */ jsx("section", {
117
+ className: "wave-docs-table-scroll",
118
+ "aria-label": label,
119
+ tabIndex: 0,
120
+ children: /* @__PURE__ */ jsx("table", {
121
+ ...rest,
122
+ className: joinClassNames("wave-docs-table", className)
123
+ })
124
+ });
125
+ };
159
126
  }
160
127
  /**
161
128
  * Build the default component map, optionally injecting host-specific link and
@@ -173,12 +140,47 @@ function MarkdownTable({ className, ...rest }) {
173
140
  * ```
174
141
  */
175
142
  function createMarkdownComponents(options = {}) {
143
+ const labels = options.labels ?? {};
144
+ const calloutTitles = calloutTitleMap(labels);
176
145
  return {
177
- a: createAnchor(options.Link),
146
+ a: createAnchor(options.Link, labels.externalLink ?? DEFAULT_EXTERNAL_LINK),
178
147
  img: createImage(options.Image),
179
- table: MarkdownTable,
180
- callout: Callout,
181
- youtube: YouTube
148
+ table: createTable(labels.table ?? DEFAULT_TABLE),
149
+ callout: (props) => /* @__PURE__ */ jsx(Callout, {
150
+ ...props,
151
+ ...calloutTitles === void 0 ? {} : { labels: calloutTitles }
152
+ }),
153
+ youtube: (props) => /* @__PURE__ */ jsx(YouTube, { ...youtubeDefaults(labels, props) })
154
+ };
155
+ }
156
+ /** The five headings as `Callout` wants them, or `undefined` if none are set. */
157
+ function calloutTitleMap(labels) {
158
+ const titles = {};
159
+ let found = false;
160
+ for (const [type, key] of Object.entries(CALLOUT_LABEL_KEYS)) {
161
+ const value = labels[key];
162
+ if (value !== void 0) {
163
+ titles[type] = value;
164
+ found = true;
165
+ }
166
+ }
167
+ return found ? titles : void 0;
168
+ }
169
+ /** Which `DocsLabels` key names a given callout type's heading. */
170
+ const CALLOUT_LABEL_KEYS = {
171
+ note: "calloutNote",
172
+ tip: "calloutTip",
173
+ important: "calloutImportant",
174
+ warning: "calloutWarning",
175
+ caution: "calloutCaution"
176
+ };
177
+ /** `props`, with the site's YouTube strings filled in. */
178
+ function youtubeDefaults(labels, props) {
179
+ return {
180
+ ...props,
181
+ ...props.title === void 0 && labels.youtubeTitle !== void 0 ? { title: labels.youtubeTitle } : {},
182
+ ...labels.youtubePlay === void 0 ? {} : { playLabel: labels.youtubePlay },
183
+ ...labels.youtubeHide === void 0 ? {} : { hideLabel: labels.youtubeHide }
182
184
  };
183
185
  }
184
186
  /** The map used when a caller supplies none. Plain `<a>` and `<img>`. */
@@ -22,7 +22,11 @@ interface DocsNavProps {
22
22
  label?: string | undefined;
23
23
  /** Accessible name for the close button. */
24
24
  closeLabel?: string | undefined;
25
+ /** Passed through to the tree. See `DocsSidebarProps.expandGroup`. */
26
+ expandGroup?: string | undefined;
27
+ collapseGroup?: string | undefined;
28
+ externalLink?: string | undefined;
25
29
  }
26
- declare function DocsNav({ nav, pathname, Link, label, closeLabel }: DocsNavProps): ReactNode;
30
+ declare function DocsNav({ nav, pathname, Link, label, closeLabel, expandGroup, collapseGroup, externalLink }: DocsNavProps): ReactNode;
27
31
  //#endregion
28
32
  export { DOCS_NAV_ID, DocsNav, DocsNavProps };
package/dist/react/nav.js CHANGED
@@ -12,7 +12,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
12
12
  * the button does nothing on the first tap.
13
13
  */
14
14
  const DOCS_NAV_ID = "wave-docs-nav";
15
- function DocsNav({ nav, pathname, Link, label = "Documentation", closeLabel = "Close navigation" }) {
15
+ function DocsNav({ nav, pathname, Link, label = "Documentation", closeLabel = "Close navigation", expandGroup, collapseGroup, externalLink }) {
16
16
  const ref = useRef(null);
17
17
  useEffect(() => {
18
18
  ref.current?.close?.();
@@ -62,7 +62,10 @@ function DocsNav({ nav, pathname, Link, label = "Documentation", closeLabel = "C
62
62
  nav,
63
63
  pathname,
64
64
  label,
65
- Link
65
+ Link,
66
+ ...expandGroup === void 0 ? {} : { expandGroup },
67
+ ...collapseGroup === void 0 ? {} : { collapseGroup },
68
+ ...externalLink === void 0 ? {} : { externalLink }
66
69
  })]
67
70
  });
68
71
  }
@@ -5,7 +5,11 @@ interface DocsNextNavProps {
5
5
  nav: DocNavNode[];
6
6
  label?: string | undefined;
7
7
  closeLabel?: string | undefined;
8
+ /** Passed through to the tree. See `DocsSidebarProps.expandGroup`. */
9
+ expandGroup?: string | undefined;
10
+ collapseGroup?: string | undefined;
11
+ externalLink?: string | undefined;
8
12
  }
9
- declare function DocsNextNav({ nav, label, closeLabel }: DocsNextNavProps): ReactNode;
13
+ declare function DocsNextNav({ nav, label, closeLabel, expandGroup, collapseGroup, externalLink }: DocsNextNavProps): ReactNode;
10
14
  //#endregion
11
15
  export { DocsNextNav, DocsNextNavProps };
@@ -19,13 +19,16 @@ import { usePathname } from "next/navigation";
19
19
  */
20
20
  /** Module scope: a fresh identity here remounts every nav link on every render. */
21
21
  const Link = wrapNextLink(NextLink);
22
- function DocsNextNav({ nav, label, closeLabel }) {
22
+ function DocsNextNav({ nav, label, closeLabel, expandGroup, collapseGroup, externalLink }) {
23
23
  return /* @__PURE__ */ jsx(DocsNav, {
24
24
  nav,
25
25
  pathname: usePathname(),
26
26
  Link,
27
27
  ...label === void 0 ? {} : { label },
28
- ...closeLabel === void 0 ? {} : { closeLabel }
28
+ ...closeLabel === void 0 ? {} : { closeLabel },
29
+ ...expandGroup === void 0 ? {} : { expandGroup },
30
+ ...collapseGroup === void 0 ? {} : { collapseGroup },
31
+ ...externalLink === void 0 ? {} : { externalLink }
29
32
  });
30
33
  }
31
34
  //#endregion