@waveso/docs 0.5.0 → 0.7.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.
@@ -0,0 +1,34 @@
1
+ import { DocsLinkComponent } from "./markdown-components.js";
2
+ import { ComponentProps, ComponentType } from "react";
3
+ //#region src/react/link-adapter.d.ts
4
+ /**
5
+ * The part of `next/link` this package uses.
6
+ *
7
+ * Declared structurally rather than imported: `next` is an optional peer, and
8
+ * a type-only import of it would still be a hard resolution requirement for
9
+ * anyone type-checking against our `.d.ts`.
10
+ */
11
+ type NextLinkComponent = ComponentType<Omit<ComponentProps<'a'>, 'href' | 'ref'> & {
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;
33
+ //#endregion
34
+ export { NextLinkComponent, wrapNextLink };
@@ -0,0 +1,30 @@
1
+ import { createElement } from "react";
2
+ //#region src/react/link-adapter.ts
3
+ /**
4
+ * Adapt `next/link` to {@link DocsLinkProps}.
5
+ *
6
+ * `next/link` widens `href` to `string | UrlObject` and `prefetch` to
7
+ * `boolean | 'auto' | null`; the React layer promises neither, because it must
8
+ * also run with a plain `<a>`. One wrapper keeps that mismatch in a single
9
+ * place instead of at every call site.
10
+ *
11
+ * `prefetch` is omitted rather than passed as `undefined`, which is not
12
+ * pedantry: under `exactOptionalPropertyTypes` — which this package compiles
13
+ * with, and which any consumer may turn on — `undefined` is not assignable to
14
+ * `boolean | 'auto' | null`, and `<SearchDialog Link={Link} />` written by
15
+ * hand fails to compile for a reason that reads as our bug.
16
+ *
17
+ * Call it once at module scope, never during a render: a fresh component
18
+ * identity for `a` remounts every link in the document on every render.
19
+ */
20
+ function wrapNextLink(NextLink) {
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
+ }
29
+ //#endregion
30
+ export { wrapNextLink };
@@ -2,14 +2,7 @@ import { DocNavNode } from "../types.js";
2
2
  import { DocsLinkComponent } from "./markdown-components.js";
3
3
  import { ReactNode } from "react";
4
4
  //#region src/react/nav.d.ts
5
- /**
6
- * The drawer's `id`, and the header trigger's `commandfor`.
7
- *
8
- * A constant rather than a `useId`, for two reasons that both matter: the
9
- * trigger is rendered on the server in a different subtree and cannot see a
10
- * hook's value, and `command`/`commandfor` must agree before React hydrates or
11
- * the button does nothing on the first tap.
12
- */
5
+ /** The navigation's `id`, and the trigger's `aria-controls`. */
13
6
  declare const DOCS_NAV_ID = "wave-docs-nav";
14
7
  interface DocsNavProps {
15
8
  /** The tree from `docs.source.nav()`. */
@@ -18,15 +11,22 @@ interface DocsNavProps {
18
11
  pathname: string;
19
12
  /** Client-side router link, e.g. `next/link`. Falls back to `<a>`. */
20
13
  Link?: DocsLinkComponent | undefined;
21
- /** Accessible name for the nav landmark and the drawer. */
14
+ /** Accessible name for the nav landmark. */
22
15
  label?: string | undefined;
23
- /** Accessible name for the close button. */
16
+ /** Accessible name for the trigger while the sidebar is open. */
24
17
  closeLabel?: string | undefined;
18
+ /** Accessible name for the trigger while the sidebar is closed. */
19
+ openLabel?: string | undefined;
20
+ /**
21
+ * Rendered above the tree. The search trigger goes here — it belongs to the
22
+ * navigation, so it moves with it and is never a second thing to place.
23
+ */
24
+ children?: ReactNode;
25
25
  /** Passed through to the tree. See `DocsSidebarProps.expandGroup`. */
26
26
  expandGroup?: string | undefined;
27
27
  collapseGroup?: string | undefined;
28
28
  externalLink?: string | undefined;
29
29
  }
30
- declare function DocsNav({ nav, pathname, Link, label, closeLabel, expandGroup, collapseGroup, externalLink }: DocsNavProps): ReactNode;
30
+ declare function DocsNav({ nav, pathname, Link, label, children, closeLabel, openLabel, expandGroup, collapseGroup, externalLink }: DocsNavProps): ReactNode;
31
31
  //#endregion
32
32
  export { DOCS_NAV_ID, DocsNav, DocsNavProps };
package/dist/react/nav.js CHANGED
@@ -1,73 +1,121 @@
1
1
  "use client";
2
2
  import { DocsSidebar } from "./sidebar.js";
3
- import { useEffect, useRef } from "react";
4
- import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/react/nav.tsx
6
- /**
7
- * The drawer's `id`, and the header trigger's `commandfor`.
8
- *
9
- * A constant rather than a `useId`, for two reasons that both matter: the
10
- * trigger is rendered on the server in a different subtree and cannot see a
11
- * hook's value, and `command`/`commandfor` must agree before React hydrates or
12
- * the button does nothing on the first tap.
13
- */
6
+ /** The navigation's `id`, and the trigger's `aria-controls`. */
14
7
  const DOCS_NAV_ID = "wave-docs-nav";
15
- function DocsNav({ nav, pathname, Link, label = "Documentation", closeLabel = "Close navigation", expandGroup, collapseGroup, externalLink }) {
16
- const ref = useRef(null);
8
+ function DocsNav({ nav, pathname, Link, label = "Documentation", children, closeLabel = "Close navigation", openLabel = "Open navigation", expandGroup, collapseGroup, externalLink }) {
9
+ const shellRef = useRef(null);
10
+ const navRef = useRef(null);
11
+ const returnFocusRef = useRef(null);
12
+ /**
13
+ * ⚠️ THREE STATES, AND THE THIRD ONE IS WHY THERE IS NO FLASH.
14
+ *
15
+ * `null` is "nobody has chosen yet". The server renders it, no `data-state`
16
+ * reaches the DOM, and the stylesheet decides per mode — closed where the
17
+ * navigation would cover the article, open where it would sit beside it. So
18
+ * the first paint is already right at both shapes, with no JavaScript and
19
+ * nothing to correct afterwards.
20
+ *
21
+ * A boolean would have to be picked before the container's width is known,
22
+ * which is either a flash on a phone or a flash on a desktop.
23
+ */
24
+ const [choice, setChoice] = useState(null);
25
+ const [mode, setMode] = useState(null);
26
+ const [ready, setReady] = useState(false);
27
+ const open = choice ?? mode === "push";
28
+ const covering = mode === "cover" && open;
17
29
  useEffect(() => {
18
- ref.current?.close?.();
30
+ const shell = shellRef.current;
31
+ const layout = shell?.parentElement;
32
+ if (shell == null || layout == null) return;
33
+ const read = () => {
34
+ const value = getComputedStyle(shell).getPropertyValue("--wave-docs-sidebar-mode").trim();
35
+ setMode(value === "push" ? "push" : "cover");
36
+ };
37
+ read();
38
+ const frame = requestAnimationFrame(() => {
39
+ setReady(true);
40
+ });
41
+ const observer = new ResizeObserver(read);
42
+ observer.observe(layout);
43
+ return () => {
44
+ cancelAnimationFrame(frame);
45
+ observer.disconnect();
46
+ };
47
+ }, []);
48
+ const close = useCallback(() => {
49
+ setChoice(false);
50
+ }, []);
51
+ useEffect(() => {
52
+ if (covering) setChoice(false);
19
53
  }, [pathname]);
20
54
  useEffect(() => {
21
- if ("command" in HTMLButtonElement.prototype) return;
22
- const onClick = (event) => {
23
- const target = event.target;
24
- if (!(target instanceof Element)) return;
25
- const button = target.closest("button[commandfor]");
26
- if (button === null) return;
27
- if (button.getAttribute("commandfor") !== "wave-docs-nav") return;
28
- const dialog = ref.current;
29
- if (dialog === null) return;
30
- if (button.getAttribute("command") === "close") dialog.close();
31
- else dialog.showModal();
55
+ if (!covering) return;
56
+ const onKeyDown = (event) => {
57
+ if (event.key !== "Escape") return;
58
+ event.stopPropagation();
59
+ close();
32
60
  };
33
- document.addEventListener("click", onClick);
61
+ document.addEventListener("keydown", onKeyDown);
34
62
  return () => {
35
- document.removeEventListener("click", onClick);
63
+ document.removeEventListener("keydown", onKeyDown);
36
64
  };
37
- }, []);
38
- return /* @__PURE__ */ jsxs("dialog", {
39
- ref,
40
- id: DOCS_NAV_ID,
41
- className: "wave-docs-layout__drawer",
42
- closedby: "any",
43
- "aria-label": label,
44
- children: [/* @__PURE__ */ jsx("button", {
65
+ }, [covering, close]);
66
+ useEffect(() => {
67
+ const shell = shellRef.current;
68
+ const layout = shell?.parentElement;
69
+ if (shell == null || layout == null) return;
70
+ const outside = [...layout.children].filter((child) => child !== shell && !child.classList.contains(SCRIM_CLASS));
71
+ if (!covering) {
72
+ for (const element of outside) element.removeAttribute("inert");
73
+ return;
74
+ }
75
+ returnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
76
+ for (const element of outside) element.setAttribute("inert", "");
77
+ navRef.current?.focus();
78
+ return () => {
79
+ for (const element of outside) element.removeAttribute("inert");
80
+ returnFocusRef.current?.focus({ preventScroll: true });
81
+ };
82
+ }, [covering]);
83
+ const state = choice === null && mode === null ? void 0 : open;
84
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
85
+ ref: shellRef,
86
+ className: "wave-docs-layout__sidebar",
87
+ ...state === void 0 ? {} : { "data-state": state ? "open" : "closed" },
88
+ ...ready ? { "data-ready": "" } : {},
89
+ children: [/* @__PURE__ */ jsxs("div", {
90
+ ref: navRef,
91
+ id: DOCS_NAV_ID,
92
+ className: "wave-docs-layout__sidebar-nav",
93
+ tabIndex: -1,
94
+ children: [children, /* @__PURE__ */ jsx(DocsSidebar, {
95
+ nav,
96
+ pathname,
97
+ label,
98
+ Link,
99
+ ...expandGroup === void 0 ? {} : { expandGroup },
100
+ ...collapseGroup === void 0 ? {} : { collapseGroup },
101
+ ...externalLink === void 0 ? {} : { externalLink }
102
+ })]
103
+ }), /* @__PURE__ */ jsx("button", {
45
104
  type: "button",
46
- className: "wave-docs-layout__drawer-close",
47
- "aria-label": closeLabel,
48
- command: "close",
49
- commandfor: DOCS_NAV_ID,
50
- children: /* @__PURE__ */ jsx("svg", {
51
- "aria-hidden": "true",
52
- viewBox: "0 0 16 16",
53
- width: "16",
54
- height: "16",
55
- fill: "none",
56
- stroke: "currentColor",
57
- strokeWidth: "1.5",
58
- strokeLinecap: "round",
59
- children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" })
60
- })
61
- }), /* @__PURE__ */ jsx(DocsSidebar, {
62
- nav,
63
- pathname,
64
- label,
65
- Link,
66
- ...expandGroup === void 0 ? {} : { expandGroup },
67
- ...collapseGroup === void 0 ? {} : { collapseGroup },
68
- ...externalLink === void 0 ? {} : { externalLink }
105
+ className: "wave-docs-layout__sidebar-trigger",
106
+ "aria-controls": DOCS_NAV_ID,
107
+ "aria-label": open ? closeLabel : openLabel,
108
+ ...state === void 0 ? {} : { "aria-expanded": state },
109
+ onClick: () => {
110
+ setChoice(!open);
111
+ }
69
112
  })]
70
- });
113
+ }), /* @__PURE__ */ jsx("div", {
114
+ className: SCRIM_CLASS,
115
+ "aria-hidden": "true",
116
+ onClick: close
117
+ })] });
71
118
  }
119
+ const SCRIM_CLASS = "wave-docs-layout__sidebar-scrim";
72
120
  //#endregion
73
121
  export { DOCS_NAV_ID, DocsNav };
@@ -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
- * The part of `next/link` this package uses.
4
+ * Module scope, and never inside a render.
6
5
  *
7
- * Declared structurally rather than imported: `next` is an optional peer, and
8
- * a type-only import of it would still be a hard resolution requirement for
9
- * anyone type-checking against our `.d.ts`.
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
- type NextLinkComponent = ComponentType<Omit<ComponentProps<'a'>, 'href' | 'ref'> & {
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 { NextLinkComponent, wrapNextLink };
12
+ export { DocsLink };
@@ -1,30 +1,51 @@
1
- import { createElement } from "react";
2
- //#region src/react/next-link.ts
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
- * Adapt `next/link` to {@link DocsLinkProps}.
6
+ * `next/link`, adapted to this package's {@link DocsLinkProps} and ready to pass.
5
7
  *
6
- * `next/link` widens `href` to `string | UrlObject` and `prefetch` to
7
- * `boolean | 'auto' | null`; the React layer promises neither, because it must
8
- * also run with a plain `<a>`. One wrapper keeps that mismatch in a single
9
- * place instead of at every call site.
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
- * `prefetch` is omitted rather than passed as `undefined`, which is not
12
- * pedantry: under `exactOptionalPropertyTypes`which this package compiles
13
- * with, and which any consumer may turn on `undefined` is not assignable to
14
- * `boolean | 'auto' | null`, and `<SearchDialog Link={Link} />` written by
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
- * Call it once at module scope, never during a render: a fresh component
18
- * identity for `a` remounts every link in the document on every render.
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
- function wrapNextLink(NextLink) {
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 { wrapNextLink };
51
+ export { DocsLink };
@@ -5,11 +5,15 @@ interface DocsNextNavProps {
5
5
  nav: DocNavNode[];
6
6
  label?: string | undefined;
7
7
  closeLabel?: string | undefined;
8
+ /** Accessible name for the trigger while the sidebar is closed. */
9
+ openLabel?: string | undefined;
10
+ /** Rendered above the tree. See `DocsNavProps.children`. */
11
+ children?: ReactNode;
8
12
  /** Passed through to the tree. See `DocsSidebarProps.expandGroup`. */
9
13
  expandGroup?: string | undefined;
10
14
  collapseGroup?: string | undefined;
11
15
  externalLink?: string | undefined;
12
16
  }
13
- declare function DocsNextNav({ nav, label, closeLabel, expandGroup, collapseGroup, externalLink }: DocsNextNavProps): ReactNode;
17
+ declare function DocsNextNav({ nav, label, closeLabel, openLabel, children, expandGroup, collapseGroup, externalLink }: DocsNextNavProps): ReactNode;
14
18
  //#endregion
15
19
  export { DocsNextNav, DocsNextNavProps };
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { wrapNextLink } from "./next-link.js";
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,16 +19,18 @@ 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, expandGroup, collapseGroup, externalLink }) {
22
+ function DocsNextNav({ nav, label, closeLabel, openLabel, children, 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
28
  ...closeLabel === void 0 ? {} : { closeLabel },
29
+ ...openLabel === void 0 ? {} : { openLabel },
29
30
  ...expandGroup === void 0 ? {} : { expandGroup },
30
31
  ...collapseGroup === void 0 ? {} : { collapseGroup },
31
- ...externalLink === void 0 ? {} : { externalLink }
32
+ ...externalLink === void 0 ? {} : { externalLink },
33
+ children
32
34
  });
33
35
  }
34
36
  //#endregion
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { wrapNextLink } from "./next-link.js";
2
+ import { wrapNextLink } from "./link-adapter.js";
3
3
  import { SearchDialog } from "./search-dialog.js";
4
4
  import { useCallback } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
@@ -44,9 +44,15 @@
44
44
  interface DocsLabels {
45
45
  /** The navigation landmark's accessible name. Default `'Documentation'`. */
46
46
  nav?: string | undefined;
47
- /** The header button that opens the drawer. Default `'Open navigation'`. */
47
+ /**
48
+ * The sidebar's trigger, named for what pressing it does next. One button
49
+ * carries both strings: `openNav` while the sidebar is closed, `closeNav`
50
+ * while it is open.
51
+ *
52
+ * Default `'Open navigation'`.
53
+ */
48
54
  openNav?: string | undefined;
49
- /** The button that closes the drawer. Default `'Close navigation'`. */
55
+ /** The same button, while the sidebar is open. Default `'Close navigation'`. */
50
56
  closeNav?: string | undefined;
51
57
  /** The skip link's visible text. Default `'Skip to content'`. */
52
58
  skipToContent?: string | undefined;
@@ -8,9 +8,19 @@ interface DocsTocProps {
8
8
  label?: string | undefined;
9
9
  /**
10
10
  * Region of the viewport that counts as "current", as an
11
- * `IntersectionObserver` root margin. The default reserves 80px for a sticky
12
- * header and ignores the bottom 60% of the screen, so the active entry
13
- * tracks what you are reading rather than what has scrolled into view.
11
+ * `IntersectionObserver` root margin. The default ignores the bottom 60% of
12
+ * the screen, so the active entry tracks what you are reading rather than
13
+ * what has scrolled into view.
14
+ *
15
+ * ⚠️ IT USED TO RESERVE 80px FOR A STICKY HEADER, AND TWO THINGS WERE WRONG
16
+ * WITH THAT. There is no header — above 80rem, where this component
17
+ * is rendered at all, nothing of ours overlays the content. And 80px never
18
+ * matched the header anyway: the token was `3.5rem`, which is 56px, so the
19
+ * figure was never derived from the thing it claimed to reserve for.
20
+ *
21
+ * This component is exported standalone, so its default cannot assume our
22
+ * shell. It assumes nothing overlays the content; a host whose own chrome
23
+ * does sets this prop, which is what the prop is for.
14
24
  *
15
25
  * Only `px` and `%` are legal here — `IntersectionObserver` throws on any
16
26
  * other unit, `rem` included.
package/dist/react/toc.js CHANGED
@@ -3,7 +3,7 @@ import { DOCS_CONTENT_ID } from "../docs-content-id.js";
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/react/toc.tsx
6
- const DEFAULT_ROOT_MARGIN = "-80px 0px -60% 0px";
6
+ const DEFAULT_ROOT_MARGIN = "0px 0px -60% 0px";
7
7
  /**
8
8
  * How many frames to keep looking for headings that are not in the document
9
9
  * yet. ~1s at 60Hz, which covers a `<Suspense>` boundary resolving or a
@@ -86,15 +86,22 @@ function DocsToc({ entries, label = "On this page", rootMargin = DEFAULT_ROOT_MA
86
86
  return /* @__PURE__ */ jsxs("nav", {
87
87
  "aria-label": label,
88
88
  className: ["wave-docs-toc", className].filter(Boolean).join(" "),
89
- children: [/* @__PURE__ */ jsx(TocList, {
90
- entries,
91
- activeId,
92
- onSelect: setActiveId
93
- }), /* @__PURE__ */ jsx("a", {
94
- className: "wave-docs-toc__top",
95
- href: `#${DOCS_CONTENT_ID}`,
96
- children: topLabel
97
- })]
89
+ children: [
90
+ /* @__PURE__ */ jsx("p", {
91
+ className: "wave-docs-toc__title",
92
+ children: label
93
+ }),
94
+ /* @__PURE__ */ jsx(TocList, {
95
+ entries,
96
+ activeId,
97
+ onSelect: setActiveId
98
+ }),
99
+ /* @__PURE__ */ jsx("a", {
100
+ className: "wave-docs-toc__top",
101
+ href: `#${DOCS_CONTENT_ID}`,
102
+ children: topLabel
103
+ })
104
+ ]
98
105
  });
99
106
  }
100
107
  function TocList({ entries, activeId, onSelect }) {
package/dist/render.d.ts CHANGED
@@ -12,7 +12,7 @@ import { PluggableList } from "unified";
12
12
  * same object to the source walk and to the renderer. Nothing here re-applies
13
13
  * defaults, so the two cannot drift.
14
14
  */
15
- type DocsRendererConfig = Pick<ResolvedDocsConfig, 'basePath' | 'assertLinks'>;
15
+ type DocsRendererConfig = Pick<ResolvedDocsConfig, 'basePath' | 'onBrokenLinks' | 'onBrokenAnchors' | 'externalRoutes'>;
16
16
  interface DocsRendererOptions {
17
17
  config: DocsRendererConfig;
18
18
  /**