@waveso/docs 0.4.0 → 0.6.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/CHANGELOG.md +270 -0
- package/README.md +221 -79
- package/dist/anchors.d.ts +44 -0
- package/dist/anchors.js +76 -0
- package/dist/errors.d.ts +4 -0
- package/dist/highlighter.js +2 -1
- package/dist/link-suggestion.d.ts +31 -0
- package/dist/link-suggestion.js +94 -0
- package/dist/meta.js +6 -9
- package/dist/next.d.ts +54 -14
- package/dist/next.js +135 -20
- package/dist/plugins/rehype-code-frame.d.ts +13 -1
- package/dist/plugins/rehype-code-frame.js +2 -1
- package/dist/plugins/rehype-fallback-heading-ids.js +1 -1
- package/dist/plugins/remark-doc-links.d.ts +83 -1
- package/dist/plugins/remark-doc-links.js +50 -23
- package/dist/plugins/remark-youtube.d.ts +18 -3
- package/dist/plugins/remark-youtube.js +57 -9
- package/dist/react/callout.d.ts +13 -1
- package/dist/react/callout.js +2 -2
- package/dist/react/code-runtime.d.ts +12 -2
- package/dist/react/code-runtime.js +28 -4
- package/dist/react/doc-content.d.ts +12 -1
- package/dist/react/doc-content.js +2 -2
- package/dist/react/layout.d.ts +27 -10
- package/dist/react/layout.js +6 -3
- package/dist/react/link-adapter.d.ts +34 -0
- package/dist/react/link-adapter.js +30 -0
- package/dist/react/markdown-components.d.ts +29 -1
- package/dist/react/markdown-components.js +69 -67
- package/dist/react/nav.d.ts +5 -1
- package/dist/react/nav.js +5 -2
- package/dist/react/next-link.d.ts +6 -28
- package/dist/react/next-link.js +45 -24
- package/dist/react/next-nav.d.ts +5 -1
- package/dist/react/next-nav.js +6 -3
- package/dist/react/next-search.js +1 -1
- package/dist/react/search-dialog.d.ts +59 -3
- package/dist/react/search-dialog.js +53 -9
- package/dist/react/shell-labels.d.ts +135 -21
- package/dist/react/shell-labels.js +47 -6
- package/dist/react/sidebar.d.ts +18 -1
- package/dist/react/sidebar.js +59 -23
- package/dist/react/youtube.d.ts +22 -1
- package/dist/react/youtube.js +22 -4
- package/dist/render.d.ts +12 -1
- package/dist/render.js +107 -21
- package/dist/route-path.js +7 -2
- package/dist/safe-href.d.ts +47 -0
- package/dist/safe-href.js +73 -0
- package/dist/search-index.js +1 -1
- package/dist/search-options.d.ts +64 -2
- package/dist/search-options.js +25 -1
- package/dist/semaphore.d.ts +46 -0
- package/dist/semaphore.js +60 -0
- package/dist/source.js +86 -12
- package/dist/types.d.ts +102 -6
- package/package.json +6 -3
|
@@ -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
|
-
/**
|
|
6
|
-
const
|
|
7
|
-
/**
|
|
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
|
|
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 (
|
|
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__ */
|
|
53
|
+
children: [children, /* @__PURE__ */ jsxs("span", {
|
|
92
54
|
className: "wave-docs-sr-only",
|
|
93
|
-
children: "
|
|
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
|
|
150
|
-
return
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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:
|
|
180
|
-
callout: Callout,
|
|
181
|
-
|
|
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>`. */
|
package/dist/react/nav.d.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -1,34 +1,12 @@
|
|
|
1
1
|
import { DocsLinkComponent } from "./markdown-components.js";
|
|
2
|
-
import { ComponentProps, ComponentType } from "react";
|
|
3
2
|
//#region src/react/next-link.d.ts
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
4
|
+
* Module scope, and never inside a render.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* A fresh component identity for a link remounts every link in the document on
|
|
7
|
+
* every render — which is why `next-search.tsx` and `next-nav.tsx` each build
|
|
8
|
+
* theirs at module scope too, and why this is a constant rather than a factory.
|
|
10
9
|
*/
|
|
11
|
-
|
|
12
|
-
href: string;
|
|
13
|
-
prefetch?: boolean | null;
|
|
14
|
-
}>;
|
|
15
|
-
/**
|
|
16
|
-
* Adapt `next/link` to {@link DocsLinkProps}.
|
|
17
|
-
*
|
|
18
|
-
* `next/link` widens `href` to `string | UrlObject` and `prefetch` to
|
|
19
|
-
* `boolean | 'auto' | null`; the React layer promises neither, because it must
|
|
20
|
-
* also run with a plain `<a>`. One wrapper keeps that mismatch in a single
|
|
21
|
-
* place instead of at every call site.
|
|
22
|
-
*
|
|
23
|
-
* `prefetch` is omitted rather than passed as `undefined`, which is not
|
|
24
|
-
* pedantry: under `exactOptionalPropertyTypes` — which this package compiles
|
|
25
|
-
* with, and which any consumer may turn on — `undefined` is not assignable to
|
|
26
|
-
* `boolean | 'auto' | null`, and `<SearchDialog Link={Link} />` written by
|
|
27
|
-
* hand fails to compile for a reason that reads as our bug.
|
|
28
|
-
*
|
|
29
|
-
* Call it once at module scope, never during a render: a fresh component
|
|
30
|
-
* identity for `a` remounts every link in the document on every render.
|
|
31
|
-
*/
|
|
32
|
-
declare function wrapNextLink(NextLink: NextLinkComponent): DocsLinkComponent;
|
|
10
|
+
declare const DocsLink: DocsLinkComponent;
|
|
33
11
|
//#endregion
|
|
34
|
-
export {
|
|
12
|
+
export { DocsLink };
|
package/dist/react/next-link.js
CHANGED
|
@@ -1,30 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
import { wrapNextLink } from "./link-adapter.js";
|
|
3
|
+
import NextLink from "next/link";
|
|
4
|
+
//#region src/react/next-link.tsx
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
6
|
+
* `next/link`, adapted to this package's {@link DocsLinkProps} and ready to pass.
|
|
5
7
|
*
|
|
6
|
-
* `next/link`
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* ⚠️ IT EXISTS BECAUSE PASSING `next/link` DIRECTLY DOES NOT TYPE-CHECK. Under
|
|
9
|
+
* `exactOptionalPropertyTypes` — which this package compiles with, and which any
|
|
10
|
+
* consumer may turn on — Next's `LinkProps` re-declares `onClick?`,
|
|
11
|
+
* `onMouseEnter?` and `onTouchStart?` *without* `| undefined` while React's
|
|
12
|
+
* anchor props include it, so `<DocsSidebar Link={Link} />` fails to compile
|
|
13
|
+
* over three handlers `next/link` accepts perfectly well at run time. It is a
|
|
14
|
+
* disagreement between two dependencies' declaration files, true of every
|
|
15
|
+
* `next/link` call site in a project with that flag on, and nothing the shape of
|
|
16
|
+
* `DocsLinkProps` can fix without breaking the plain-`<a>` fallback that makes
|
|
17
|
+
* these components host-agnostic.
|
|
10
18
|
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* hand fails to compile for a reason that reads as our bug.
|
|
19
|
+
* `docs.Layout` and `DocsSearch` have always absorbed it internally, so it only
|
|
20
|
+
* ever bit someone composing a shell by hand — who was told to write
|
|
21
|
+
* `Link={Link as DocsLinkComponent}` and wait for this module. This is it; the
|
|
22
|
+
* cast is retired.
|
|
16
23
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
24
|
+
* ```tsx
|
|
25
|
+
* 'use client';
|
|
26
|
+
* import { DocsLink } from '@waveso/docs/react/next-link';
|
|
27
|
+
* import { DocsSidebar } from '@waveso/docs/react/sidebar';
|
|
28
|
+
*
|
|
29
|
+
* <DocsSidebar nav={nav} pathname={pathname} Link={DocsLink} />
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* ## Why `'use client'`
|
|
33
|
+
*
|
|
34
|
+
* Not for a hook — there is none. {@link DocsLink} is a *function*, and a
|
|
35
|
+
* function cannot be handed from a Server Component to a Client one: React
|
|
36
|
+
* serialises those props and refuses. Without the directive this module's export
|
|
37
|
+
* would be a server reference, and passing it to `DocsSidebar` — which is itself
|
|
38
|
+
* `'use client'` — would fail `next build` with "Functions cannot be passed
|
|
39
|
+
* directly to Client Components". The directive makes it a client reference,
|
|
40
|
+
* which crosses fine.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Module scope, and never inside a render.
|
|
44
|
+
*
|
|
45
|
+
* A fresh component identity for a link remounts every link in the document on
|
|
46
|
+
* every render — which is why `next-search.tsx` and `next-nav.tsx` each build
|
|
47
|
+
* theirs at module scope too, and why this is a constant rather than a factory.
|
|
19
48
|
*/
|
|
20
|
-
|
|
21
|
-
return function DocsNextLink({ href, prefetch, children, ...rest }) {
|
|
22
|
-
return createElement(NextLink, {
|
|
23
|
-
...rest,
|
|
24
|
-
href,
|
|
25
|
-
...prefetch === void 0 ? {} : { prefetch }
|
|
26
|
-
}, children);
|
|
27
|
-
};
|
|
28
|
-
}
|
|
49
|
+
const DocsLink = wrapNextLink(NextLink);
|
|
29
50
|
//#endregion
|
|
30
|
-
export {
|
|
51
|
+
export { DocsLink };
|
package/dist/react/next-nav.d.ts
CHANGED
|
@@ -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 };
|
package/dist/react/next-nav.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { wrapNextLink } from "./
|
|
2
|
+
import { wrapNextLink } from "./link-adapter.js";
|
|
3
3
|
import { DocsNav } from "./nav.js";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
import NextLink from "next/link";
|
|
@@ -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
|
|
@@ -5,8 +5,15 @@ import { Options } from "minisearch";
|
|
|
5
5
|
//#region src/react/search-dialog.d.ts
|
|
6
6
|
interface SearchDialogProps {
|
|
7
7
|
/**
|
|
8
|
-
* URL of the serialised index, e.g. `/search-index.json`.
|
|
9
|
-
*
|
|
8
|
+
* URL of the serialised index, e.g. `/docs/search-index.json`.
|
|
9
|
+
*
|
|
10
|
+
* `docs.searchIndexUrl` is the value to pass: it is derived from the route's
|
|
11
|
+
* `basePath`, so it is right when the docs are mounted anywhere but the root.
|
|
12
|
+
* `docs.Layout` passes it for you.
|
|
13
|
+
*
|
|
14
|
+
* (This used to say "whatever `writeSearchIndex` wrote". That function was
|
|
15
|
+
* deleted in 0.3.0, along with the build script it needed — the index is a
|
|
16
|
+
* `force-static` route handler now.)
|
|
10
17
|
*/
|
|
11
18
|
indexUrl: string;
|
|
12
19
|
/**
|
|
@@ -61,6 +68,55 @@ interface SearchDialogProps {
|
|
|
61
68
|
minQueryLength?: number | undefined;
|
|
62
69
|
/** Input debounce in milliseconds. Defaults to 120. */
|
|
63
70
|
debounceMs?: number | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Shown before anything is typed. Defaults to
|
|
73
|
+
* `'Start typing to search the documentation.'`
|
|
74
|
+
*/
|
|
75
|
+
hintLabel?: string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Shown while a query is below {@link SearchDialogProps.minQueryLength}.
|
|
78
|
+
* Defaults to `'Keep typing — {min} characters or more.'`
|
|
79
|
+
*
|
|
80
|
+
* `{min}` is replaced with that number. Said rather than silently done: a
|
|
81
|
+
* dialog that answers nothing and explains nothing reads as broken, and this
|
|
82
|
+
* is the state every reader passes through on the way to their real query.
|
|
83
|
+
*/
|
|
84
|
+
shortQueryLabel?: string | undefined;
|
|
85
|
+
/** Shown while the index is being fetched. Defaults to `'Loading the search index…'`. */
|
|
86
|
+
loadingLabel?: string | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Shown when the index cannot be loaded. Defaults to
|
|
89
|
+
* `'Search is unavailable right now. Try reloading the page.'`
|
|
90
|
+
*/
|
|
91
|
+
errorLabel?: string | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Shown when a query matches nothing. Defaults to `'No results for “{query}”.'`
|
|
94
|
+
*
|
|
95
|
+
* `{query}` is replaced with what the reader typed.
|
|
96
|
+
*/
|
|
97
|
+
emptyLabel?: string | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* The live region's announcement, by plural category. `{count}` is the total.
|
|
100
|
+
*
|
|
101
|
+
* Defaults to `{ one: '{count} result', other: '{count} results' }`.
|
|
102
|
+
*
|
|
103
|
+
* ⚠️ KEYED BY CATEGORY RATHER THAN BEING TWO STRINGS, BECAUSE MOST LANGUAGES
|
|
104
|
+
* ARE NOT ENGLISH. Polish takes four forms and Arabic six;
|
|
105
|
+
* `Intl.PluralRules(locale).select(count)` picks, and an unlisted category
|
|
106
|
+
* falls back to `other`. Two props called "singular" and "plural" would have
|
|
107
|
+
* made this package announce a wrong number of results, correctly, in most of
|
|
108
|
+
* the world.
|
|
109
|
+
*/
|
|
110
|
+
resultCountLabels?: Partial<Record<Intl.LDMLPluralRule, string>> | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* Language tag for the plural rules above. Defaults to the document's own
|
|
113
|
+
* `<html lang>`, then to `'en'`.
|
|
114
|
+
*
|
|
115
|
+
* Read at announcement time rather than at render, so it costs nothing on a
|
|
116
|
+
* site that never changes it and needs no prop on a site that sets `lang`
|
|
117
|
+
* correctly — which is every site that should be setting these labels at all.
|
|
118
|
+
*/
|
|
119
|
+
locale?: string | undefined;
|
|
64
120
|
/** Extra class names for the trigger button, e.g. a navbar's own layout. */
|
|
65
121
|
className?: string | undefined;
|
|
66
122
|
/**
|
|
@@ -95,6 +151,6 @@ interface SearchDialogProps {
|
|
|
95
151
|
* portalled to `document.body`, so a navbar's stacking context cannot trap
|
|
96
152
|
* it behind the page.
|
|
97
153
|
*/
|
|
98
|
-
declare function SearchDialog({ indexUrl, navigate, Link, triggerLabel, placeholder, dialogLabel, pageSize, minQueryLength, debounceMs, className, miniSearchOptions }: SearchDialogProps): ReactNode;
|
|
154
|
+
declare function SearchDialog({ indexUrl, navigate, Link, triggerLabel, placeholder, dialogLabel, pageSize, minQueryLength, debounceMs, className, miniSearchOptions, hintLabel, shortQueryLabel, loadingLabel, errorLabel, emptyLabel, resultCountLabels, locale }: SearchDialogProps): ReactNode;
|
|
99
155
|
//#endregion
|
|
100
156
|
export { SearchDialog, SearchDialogProps };
|
|
@@ -26,7 +26,7 @@ const FOCUSABLE_SELECTOR = [
|
|
|
26
26
|
* portalled to `document.body`, so a navbar's stacking context cannot trap
|
|
27
27
|
* it behind the page.
|
|
28
28
|
*/
|
|
29
|
-
function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", placeholder = "Search documentation", dialogLabel = "Search documentation", pageSize = 20, minQueryLength = 2, debounceMs = 120, className, miniSearchOptions }) {
|
|
29
|
+
function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", placeholder = "Search documentation", dialogLabel = "Search documentation", pageSize = 20, minQueryLength = 2, debounceMs = 120, className, miniSearchOptions, hintLabel, shortQueryLabel, loadingLabel, errorLabel, emptyLabel, resultCountLabels, locale }) {
|
|
30
30
|
const [isOpen, setIsOpen] = useState(false);
|
|
31
31
|
const [query, setQuery] = useState("");
|
|
32
32
|
const [hits, setHits] = useState([]);
|
|
@@ -336,7 +336,16 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
|
|
|
336
336
|
status,
|
|
337
337
|
query: query.trim(),
|
|
338
338
|
hitCount: hits.length,
|
|
339
|
-
minQueryLength
|
|
339
|
+
minQueryLength,
|
|
340
|
+
labels: {
|
|
341
|
+
...hintLabel === void 0 ? {} : { hint: hintLabel },
|
|
342
|
+
...shortQueryLabel === void 0 ? {} : { shortQuery: shortQueryLabel },
|
|
343
|
+
...loadingLabel === void 0 ? {} : { loading: loadingLabel },
|
|
344
|
+
...errorLabel === void 0 ? {} : { error: errorLabel },
|
|
345
|
+
...emptyLabel === void 0 ? {} : { empty: emptyLabel }
|
|
346
|
+
},
|
|
347
|
+
resultCountLabels,
|
|
348
|
+
locale
|
|
340
349
|
})
|
|
341
350
|
]
|
|
342
351
|
})
|
|
@@ -382,24 +391,59 @@ function SearchResultOption({ hit, id, isActive, setSize, posInSet, onActivate,
|
|
|
382
391
|
})
|
|
383
392
|
});
|
|
384
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* The wording, so a caller that overrides none of it costs nothing.
|
|
396
|
+
*
|
|
397
|
+
* `Record<keyof …, string>` rather than `as const`: literal types here make
|
|
398
|
+
* every override a type error, and `Required<StatusLabels>` keeps the
|
|
399
|
+
* `| undefined` that `exactOptionalPropertyTypes` needs on the props.
|
|
400
|
+
*/
|
|
401
|
+
const DEFAULT_STATUS_LABELS = {
|
|
402
|
+
hint: "Start typing to search the documentation.",
|
|
403
|
+
shortQuery: "Keep typing — {min} characters or more.",
|
|
404
|
+
loading: "Loading the search index…",
|
|
405
|
+
error: "Search is unavailable right now. Try reloading the page.",
|
|
406
|
+
empty: "No results for “{query}”."
|
|
407
|
+
};
|
|
408
|
+
const DEFAULT_RESULT_COUNT_LABELS = {
|
|
409
|
+
one: "{count} result",
|
|
410
|
+
other: "{count} results"
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* The announcement for `count` hits, in the document's own language.
|
|
414
|
+
*
|
|
415
|
+
* `Intl.PluralRules` rather than an `=== 1` check: Polish takes four plural
|
|
416
|
+
* forms and Arabic six, and a package that ships an English singular/plural pair
|
|
417
|
+
* announces a wrong number of results — correctly, and confidently — in most of
|
|
418
|
+
* the world. An unlisted category falls back to `other`, which is the one every
|
|
419
|
+
* language has.
|
|
420
|
+
*/
|
|
421
|
+
function announceCount(count, labels, locale) {
|
|
422
|
+
const tag = locale ?? (typeof document === "undefined" ? "" : document.documentElement.lang) ?? "";
|
|
423
|
+
let category = "other";
|
|
424
|
+
try {
|
|
425
|
+
category = new Intl.PluralRules(tag === "" ? "en" : tag).select(count);
|
|
426
|
+
} catch {}
|
|
427
|
+
return (labels[category] ?? labels.other ?? "{count}").replace("{count}", String(count));
|
|
428
|
+
}
|
|
385
429
|
/** Loading, failure and empty states, plus a live region for hit counts. */
|
|
386
|
-
function SearchStatus({ status, query, hitCount, minQueryLength }) {
|
|
430
|
+
function SearchStatus({ status, query, hitCount, minQueryLength, labels, resultCountLabels, locale }) {
|
|
387
431
|
let message = null;
|
|
388
432
|
let modifier = "";
|
|
389
433
|
if (status === "error") {
|
|
390
|
-
message =
|
|
434
|
+
message = labels.error ?? DEFAULT_STATUS_LABELS.error;
|
|
391
435
|
modifier = " wave-docs-search-status-error";
|
|
392
436
|
} else if (query === "") {
|
|
393
|
-
message =
|
|
437
|
+
message = labels.hint ?? DEFAULT_STATUS_LABELS.hint;
|
|
394
438
|
modifier = " wave-docs-search-status-hint";
|
|
395
439
|
} else if (query.length < minQueryLength) {
|
|
396
|
-
message =
|
|
440
|
+
message = (labels.shortQuery ?? DEFAULT_STATUS_LABELS.shortQuery).replace("{min}", String(minQueryLength));
|
|
397
441
|
modifier = " wave-docs-search-status-hint";
|
|
398
442
|
} else if (status !== "ready") {
|
|
399
|
-
message =
|
|
443
|
+
message = labels.loading ?? DEFAULT_STATUS_LABELS.loading;
|
|
400
444
|
modifier = " wave-docs-search-status-loading";
|
|
401
445
|
} else if (hitCount === 0) {
|
|
402
|
-
message =
|
|
446
|
+
message = (labels.empty ?? DEFAULT_STATUS_LABELS.empty).replace("{query}", query);
|
|
403
447
|
modifier = " wave-docs-search-status-empty";
|
|
404
448
|
}
|
|
405
449
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [message === null ? null : /* @__PURE__ */ jsx("p", {
|
|
@@ -409,7 +453,7 @@ function SearchStatus({ status, query, hitCount, minQueryLength }) {
|
|
|
409
453
|
className: "wave-docs-search-announcer",
|
|
410
454
|
role: "status",
|
|
411
455
|
"aria-live": "polite",
|
|
412
|
-
children: query === "" || status !== "ready" ? "" :
|
|
456
|
+
children: query === "" || status !== "ready" ? "" : announceCount(hitCount, resultCountLabels ?? DEFAULT_RESULT_COUNT_LABELS, locale)
|
|
413
457
|
})] });
|
|
414
458
|
}
|
|
415
459
|
/**
|