@waveso/docs 0.7.1 → 0.9.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.
@@ -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, hintLabel, shortQueryLabel, loadingLabel, errorLabel, emptyLabel, resultCountLabels, locale }) {
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, selectLabel = "Select", openLabel = "Open", closeLabel = "Close", resultCountLabels, locale }) {
30
30
  const [isOpen, setIsOpen] = useState(false);
31
31
  const [query, setQuery] = useState("");
32
32
  const [hits, setHits] = useState([]);
@@ -53,7 +53,18 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
53
53
  const movedByKeyboard = useRef(false);
54
54
  const [activeIndex, setActiveIndex] = useState(0);
55
55
  const [status, setStatus] = useState("idle");
56
- const [shortcutHint, setShortcutHint] = useState("");
56
+ /**
57
+ * The trigger's shortcut, split rather than held as one string.
58
+ *
59
+ * ⚠️ THE `⌘` NEEDS ITS OWN `font-size` AND CSS CANNOT SELECT A CHARACTER.
60
+ * Measured in the shipped mono stack at 12px, its ink is 6.39px tall against
61
+ * the `K`'s 8.75px — so as one string the symbol sits visibly short of the
62
+ * letter beside it. Two nodes is the only way to scale one and not the other.
63
+ *
64
+ * `null` until the effect below resolves the platform: reading it during
65
+ * render would disagree with the server's markup and break hydration.
66
+ */
67
+ const [shortcut, setShortcut] = useState(null);
57
68
  const dialogRef = useRef(null);
58
69
  const triggerRef = useRef(null);
59
70
  const inputRef = useRef(null);
@@ -126,7 +137,10 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
126
137
  }, [isOpen, closeDialog]);
127
138
  useEffect(() => {
128
139
  const isApple = /mac|iphone|ipad|ipod/i.test(navigator.userAgent);
129
- setShortcutHint(isApple ? "⌘K" : "Ctrl K");
140
+ setShortcut({
141
+ modifier: isApple ? "⌘" : "Ctrl",
142
+ isSymbol: isApple
143
+ });
130
144
  }, []);
131
145
  useEffect(() => {
132
146
  if (isOpen) {
@@ -267,13 +281,21 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
267
281
  onClick: openDialog,
268
282
  onPointerEnter: warmIndex,
269
283
  onFocus: warmIndex,
270
- children: [/* @__PURE__ */ jsx("span", {
271
- className: "wave-docs-search-trigger-label",
272
- children: triggerLabel
273
- }), shortcutHint === "" ? null : /* @__PURE__ */ jsx("kbd", {
274
- className: "wave-docs-search-trigger-kbd",
275
- children: shortcutHint
276
- })]
284
+ children: [
285
+ /* @__PURE__ */ jsx(SearchGlyph, {}),
286
+ /* @__PURE__ */ jsx("span", {
287
+ className: "wave-docs-search-trigger-label",
288
+ children: triggerLabel
289
+ }),
290
+ shortcut === null ? null : /* @__PURE__ */ jsxs("kbd", {
291
+ className: "wave-docs-search-trigger-kbd",
292
+ children: [/* @__PURE__ */ jsx("span", {
293
+ className: "wave-docs-search-trigger-mod",
294
+ "data-symbol": shortcut.isSymbol ? "" : void 0,
295
+ children: shortcut.modifier
296
+ }), shortcut.isSymbol ? "K" : " K"]
297
+ })
298
+ ]
277
299
  }), isOpen ? createPortal(/* @__PURE__ */ jsx("div", {
278
300
  className: "wave-docs-search-backdrop",
279
301
  onMouseDown: (event) => {
@@ -288,7 +310,7 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
288
310
  children: [
289
311
  /* @__PURE__ */ jsxs("div", {
290
312
  className: "wave-docs-search-input-row",
291
- children: [/* @__PURE__ */ jsx("input", {
313
+ children: [/* @__PURE__ */ jsx(SearchGlyph, {}), /* @__PURE__ */ jsx("input", {
292
314
  ref: inputRef,
293
315
  className: "wave-docs-search-input",
294
316
  type: "text",
@@ -305,11 +327,6 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
305
327
  autoComplete: "off",
306
328
  autoCorrect: "off",
307
329
  spellCheck: false
308
- }), /* @__PURE__ */ jsx("button", {
309
- type: "button",
310
- className: "wave-docs-search-close",
311
- onClick: closeDialog,
312
- children: "Close"
313
330
  })]
314
331
  }),
315
332
  /* @__PURE__ */ jsx("div", {
@@ -346,11 +363,82 @@ function SearchDialog({ indexUrl, navigate, Link, triggerLabel = "Search", place
346
363
  },
347
364
  resultCountLabels,
348
365
  locale
366
+ }),
367
+ /* @__PURE__ */ jsxs("div", {
368
+ className: "wave-docs-search-footer",
369
+ children: [
370
+ /* @__PURE__ */ jsxs("span", {
371
+ className: "wave-docs-search-hint",
372
+ "aria-hidden": "true",
373
+ children: [
374
+ /* @__PURE__ */ jsx("kbd", {
375
+ className: "wave-docs-search-kbd",
376
+ children: "↑"
377
+ }),
378
+ /* @__PURE__ */ jsx("kbd", {
379
+ className: "wave-docs-search-kbd",
380
+ children: "↓"
381
+ }),
382
+ selectLabel
383
+ ]
384
+ }),
385
+ /* @__PURE__ */ jsxs("span", {
386
+ className: "wave-docs-search-hint",
387
+ "aria-hidden": "true",
388
+ children: [/* @__PURE__ */ jsx("kbd", {
389
+ className: "wave-docs-search-kbd",
390
+ children: "↵"
391
+ }), openLabel]
392
+ }),
393
+ /* @__PURE__ */ jsxs("button", {
394
+ type: "button",
395
+ className: "wave-docs-search-close",
396
+ "aria-label": closeLabel,
397
+ "aria-keyshortcuts": "Escape",
398
+ onClick: closeDialog,
399
+ children: [/* @__PURE__ */ jsx("kbd", {
400
+ className: "wave-docs-search-kbd",
401
+ children: "Esc"
402
+ }), /* @__PURE__ */ jsx("span", { children: closeLabel })]
403
+ })
404
+ ]
349
405
  })
350
406
  ]
351
407
  })
352
408
  }), document.body) : null] });
353
409
  }
410
+ /**
411
+ * The magnifier, on the trigger and in the dialog's input row.
412
+ *
413
+ * Decorative in both places: the trigger carries its name in `aria-label` and
414
+ * the input carries its own, so this glyph would only ever repeat a word that
415
+ * is already there — announced as "search Search" and, on some engines, as the
416
+ * name of the character.
417
+ *
418
+ * Inline SVG rather than a `::before` glyph, matching every other icon in this
419
+ * package: generated content is announced by some screen-reader and browser
420
+ * pairs, which is the one thing `aria-hidden` cannot take back.
421
+ */
422
+ function SearchGlyph() {
423
+ return /* @__PURE__ */ jsxs("svg", {
424
+ className: "wave-docs-search-glyph",
425
+ "aria-hidden": "true",
426
+ focusable: "false",
427
+ viewBox: "0 0 24 24",
428
+ width: "16",
429
+ height: "16",
430
+ fill: "none",
431
+ stroke: "currentColor",
432
+ strokeWidth: "2",
433
+ strokeLinecap: "round",
434
+ strokeLinejoin: "round",
435
+ children: [/* @__PURE__ */ jsx("circle", {
436
+ cx: "11",
437
+ cy: "11",
438
+ r: "7"
439
+ }), /* @__PURE__ */ jsx("path", { d: "m20 20-3.5-3.5" })]
440
+ });
441
+ }
354
442
  /** One result row: a real link, so middle-click and "open in new tab" work. */
355
443
  function SearchResultOption({ hit, id, isActive, setSize, posInSet, onActivate, onSelect, Link }) {
356
444
  function handleClick(event) {
@@ -1,7 +1,17 @@
1
1
  import { DocNavNode } from "../types.js";
2
2
  import { DocsLinkComponent } from "./markdown-components.js";
3
- import { ReactNode } from "react";
3
+ import { ComponentType, ReactNode } from "react";
4
4
  //#region src/react/sidebar.d.ts
5
+ /**
6
+ * `icon` names to components, for `DocsSidebar`'s `icons` prop.
7
+ *
8
+ * The component is rendered with no props: an icon that needs configuration is
9
+ * a closure the host writes, not a contract this package invents. It should
10
+ * draw at the size it is given — the column is `1rem` square and the built-in
11
+ * markers use `currentColor`, so anything following those two conventions sits
12
+ * in line with them.
13
+ */
14
+ type DocsIconMap = Record<string, ComponentType>;
5
15
  interface DocsSidebarProps {
6
16
  /** The tree from `@waveso/docs/source`. */
7
17
  nav: DocNavNode[];
@@ -32,6 +42,44 @@ interface DocsSidebarProps {
32
42
  * The separating space is markup, so this is the sentence and nothing else.
33
43
  */
34
44
  externalLink?: string | undefined;
45
+ /**
46
+ * The marker column. `true` (default), `false`, or your own icons.
47
+ *
48
+ * Weight and a chevron were the only things separating a category from a
49
+ * page, and in a tree where the two interleave — a `Reference` group sitting
50
+ * directly above an `Internals` page — that is not enough to scan. A leading
51
+ * glyph gives the column a shape you read before you read any words.
52
+ *
53
+ * `false` renders no markers at all, for a host whose own navigation has a
54
+ * different vocabulary and does not want a second one. The external-link mark
55
+ * returns to the trailing edge there — turning off a decorative column is not
56
+ * consent to drop a warning.
57
+ *
58
+ * A **map** replaces the defaults with your components, keyed by the `icon`
59
+ * name authored in frontmatter or `meta.json`:
60
+ *
61
+ * ```tsx
62
+ * import { Book, Rocket } from 'lucide-react';
63
+ *
64
+ * <DocsSidebar nav={nav} pathname={pathname}
65
+ * icons={{ book: Book, rocket: Rocket }} />
66
+ * ```
67
+ *
68
+ * ```yaml
69
+ * # content/reference/index.md
70
+ * icon: book
71
+ * ```
72
+ *
73
+ * ⚠️ A NAME THE HOST RESOLVES, NEVER ART THIS PACKAGE SHIPS. Content is
74
+ * authored in YAML and JSON and cannot carry a React element, and a docs
75
+ * package mounted inside someone else's application must not put its
76
+ * iconography beside theirs. Three markers ship; everything else is yours.
77
+ *
78
+ * A name with no entry in the map falls back to the built-in marker for that
79
+ * node's type. A typo in one file leaves a folder where a book should be —
80
+ * not a hole in the column.
81
+ */
82
+ icons?: boolean | DocsIconMap | undefined;
35
83
  className?: string | undefined;
36
84
  }
37
85
  /**
@@ -76,6 +124,6 @@ interface DocsSidebarProps {
76
124
  * nothing prefetches locally whatever this says. Do not "fix" it back because
77
125
  * the network tab looks the same.
78
126
  */
79
- declare function DocsSidebar({ nav, pathname, Link, label, expandGroup, collapseGroup, externalLink, className }: DocsSidebarProps): ReactNode;
127
+ declare function DocsSidebar({ nav, pathname, Link, label, expandGroup, collapseGroup, externalLink, icons, className }: DocsSidebarProps): ReactNode;
80
128
  //#endregion
81
- export { DocsSidebar, DocsSidebarProps };
129
+ export { DocsIconMap, DocsSidebar, DocsSidebarProps };
@@ -15,6 +15,24 @@ function normalizeHref(href) {
15
15
  function isActiveHref(pathname, href) {
16
16
  return normalizeHref(pathname) === normalizeHref(href);
17
17
  }
18
+ /**
19
+ * `{ [key]: true }` for every group the active page lives inside, keyed the way
20
+ * the tree renders — `${prefix}-${index}`, index counted over *all* siblings
21
+ * because that is what `NavList` does.
22
+ *
23
+ * Shaped as the state itself rather than as a list of keys so both callers can
24
+ * spread it: it seeds the map on mount and merges into it on every navigation,
25
+ * and neither needs a loop of its own.
26
+ */
27
+ function openAlong(nodes, pathname, prefix, into = {}) {
28
+ nodes.forEach((node, index) => {
29
+ if (node.type !== "group" || !containsActive(node, pathname)) return;
30
+ const key = `${prefix}-${index}`;
31
+ into[key] = true;
32
+ openAlong(node.children, pathname, key, into);
33
+ });
34
+ return into;
35
+ }
18
36
  /** Whether the active page lives anywhere under this node. */
19
37
  function containsActive(node, pathname) {
20
38
  switch (node.type) {
@@ -66,18 +84,22 @@ function containsActive(node, pathname) {
66
84
  * nothing prefetches locally whatever this says. Do not "fix" it back because
67
85
  * the network tab looks the same.
68
86
  */
69
- function DocsSidebar({ nav, pathname, Link, label = "Docs", expandGroup, collapseGroup, externalLink, className }) {
87
+ function DocsSidebar({ nav, pathname, Link, label = "Docs", expandGroup, collapseGroup, externalLink, icons = true, className }) {
88
+ const iconMap = icons === false ? false : icons === true ? {} : icons;
70
89
  const text = {
71
90
  expandGroup: expandGroup ?? DEFAULT_SIDEBAR_LABELS.expandGroup,
72
91
  collapseGroup: collapseGroup ?? DEFAULT_SIDEBAR_LABELS.collapseGroup,
73
92
  externalLink: externalLink ?? DEFAULT_SIDEBAR_LABELS.externalLink
74
93
  };
75
94
  const baseId = useId();
76
- const [toggled, setToggled] = useState({});
95
+ const [toggled, setToggled] = useState(() => openAlong(nav, pathname, baseId));
77
96
  const lastPathname = useRef(pathname);
78
97
  if (lastPathname.current !== pathname) {
79
98
  lastPathname.current = pathname;
80
- setToggled({});
99
+ setToggled((previous) => ({
100
+ ...previous,
101
+ ...openAlong(nav, pathname, baseId)
102
+ }));
81
103
  }
82
104
  const handleToggle = (key, isOpen) => {
83
105
  setToggled((previous) => ({
@@ -108,6 +130,7 @@ function DocsSidebar({ nav, pathname, Link, label = "Docs", expandGroup, collaps
108
130
  nodes: nav,
109
131
  depth: 0,
110
132
  keyPrefix: baseId,
133
+ icons: iconMap,
111
134
  pathname,
112
135
  Link,
113
136
  toggled,
@@ -157,7 +180,7 @@ function scrollableAncestor(element) {
157
180
  }
158
181
  return null;
159
182
  }
160
- function NavList({ nodes, depth, keyPrefix, pathname, Link, toggled, onToggle, text, id }) {
183
+ function NavList({ nodes, depth, keyPrefix, icons, pathname, Link, toggled, onToggle, text, id }) {
161
184
  const holdsActive = nodes.some((node) => (node.type === "page" || node.type === "link" && !node.external) && isActiveHref(pathname, node.href));
162
185
  return /* @__PURE__ */ jsx("ul", {
163
186
  id,
@@ -175,31 +198,47 @@ function NavList({ nodes, depth, keyPrefix, pathname, Link, toggled, onToggle, t
175
198
  }, key);
176
199
  case "link": return /* @__PURE__ */ jsx("li", {
177
200
  className: "wave-docs-sidebar__item",
178
- children: /* @__PURE__ */ jsx(NavLink, {
201
+ children: /* @__PURE__ */ jsxs(NavLink, {
179
202
  href: node.href,
180
203
  isExternal: node.external,
181
204
  isActive: !node.external && isActiveHref(pathname, node.href),
182
205
  isNearby: holdsActive,
183
206
  Link,
184
207
  externalLink: text.externalLink,
185
- children: node.title
208
+ icons,
209
+ children: [icons === false ? null : /* @__PURE__ */ jsx(NavIcon, {
210
+ type: "external",
211
+ name: node.icon,
212
+ icons
213
+ }), /* @__PURE__ */ jsx("span", {
214
+ className: "wave-docs-sidebar__label",
215
+ children: node.title
216
+ })]
186
217
  })
187
218
  }, key);
188
219
  case "page": return /* @__PURE__ */ jsx("li", {
189
220
  className: "wave-docs-sidebar__item",
190
- children: /* @__PURE__ */ jsx(NavLink, {
221
+ children: /* @__PURE__ */ jsxs(NavLink, {
191
222
  href: node.href,
192
223
  isExternal: false,
193
224
  isActive: isActiveHref(pathname, node.href),
194
225
  isNearby: holdsActive,
195
226
  Link,
196
- children: node.title
227
+ children: [icons === false ? null : /* @__PURE__ */ jsx(NavIcon, {
228
+ type: "file",
229
+ name: node.icon,
230
+ icons
231
+ }), /* @__PURE__ */ jsx("span", {
232
+ className: "wave-docs-sidebar__label",
233
+ children: node.title
234
+ })]
197
235
  })
198
236
  }, key);
199
237
  case "group": return /* @__PURE__ */ jsx(NavGroup, {
200
238
  node,
201
239
  itemKey: key,
202
240
  depth,
241
+ icons,
203
242
  pathname,
204
243
  Link,
205
244
  toggled,
@@ -211,7 +250,7 @@ function NavList({ nodes, depth, keyPrefix, pathname, Link, toggled, onToggle, t
211
250
  })
212
251
  });
213
252
  }
214
- function NavGroup({ node, itemKey, depth, pathname, Link, toggled, onToggle, text }) {
253
+ function NavGroup({ node, itemKey, depth, icons, pathname, Link, toggled, onToggle, text }) {
215
254
  const listId = `${itemKey}-list`;
216
255
  const hasActive = containsActive(node, pathname);
217
256
  const isOpen = toggled[itemKey] ?? hasActive;
@@ -227,17 +266,32 @@ function NavGroup({ node, itemKey, depth, pathname, Link, toggled, onToggle, tex
227
266
  "aria-expanded": isOpen,
228
267
  "aria-controls": isOpen ? listId : void 0,
229
268
  onClick: () => onToggle(itemKey, !isOpen),
230
- children: [/* @__PURE__ */ jsx("span", {
231
- className: "wave-docs-sidebar__group-title",
232
- children: node.title
233
- }), /* @__PURE__ */ jsx(Chevron, { isOpen })]
234
- }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(NavLink, {
269
+ children: [
270
+ icons === false ? null : /* @__PURE__ */ jsx(NavIcon, {
271
+ type: "folder",
272
+ name: node.icon,
273
+ icons
274
+ }),
275
+ /* @__PURE__ */ jsx("span", {
276
+ className: "wave-docs-sidebar__group-title",
277
+ children: node.title
278
+ }),
279
+ /* @__PURE__ */ jsx(Chevron, { isOpen })
280
+ ]
281
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(NavLink, {
235
282
  href: node.href,
236
283
  isExternal: false,
237
284
  isActive: isGroupActive,
238
285
  isNearby: hasActive,
239
286
  Link,
240
- children: node.title
287
+ children: [icons === false ? null : /* @__PURE__ */ jsx(NavIcon, {
288
+ type: "folder",
289
+ name: node.icon,
290
+ icons
291
+ }), /* @__PURE__ */ jsx("span", {
292
+ className: "wave-docs-sidebar__label",
293
+ children: node.title
294
+ })]
241
295
  }), /* @__PURE__ */ jsx("button", {
242
296
  type: "button",
243
297
  className: "wave-docs-sidebar__group-toggle",
@@ -252,6 +306,7 @@ function NavGroup({ node, itemKey, depth, pathname, Link, toggled, onToggle, tex
252
306
  nodes: node.children,
253
307
  depth: depth + 1,
254
308
  keyPrefix: itemKey,
309
+ icons,
255
310
  pathname,
256
311
  Link,
257
312
  toggled,
@@ -260,7 +315,7 @@ function NavGroup({ node, itemKey, depth, pathname, Link, toggled, onToggle, tex
260
315
  }) : null]
261
316
  });
262
317
  }
263
- function NavLink({ href, isExternal, isActive, isNearby = false, Link, externalLink = DEFAULT_SIDEBAR_LABELS.externalLink, children }) {
318
+ function NavLink({ href, isExternal, isActive, isNearby = false, Link, externalLink = DEFAULT_SIDEBAR_LABELS.externalLink, icons = {}, children }) {
264
319
  const className = "wave-docs-sidebar__link";
265
320
  if (isExternal) return /* @__PURE__ */ jsxs("a", {
266
321
  className,
@@ -269,7 +324,7 @@ function NavLink({ href, isExternal, isActive, isNearby = false, Link, externalL
269
324
  rel: "noopener noreferrer",
270
325
  children: [
271
326
  children,
272
- /* @__PURE__ */ jsx("svg", {
327
+ icons === false ? /* @__PURE__ */ jsx("svg", {
273
328
  className: "wave-docs-sidebar__external",
274
329
  "aria-hidden": "true",
275
330
  focusable: "false",
@@ -281,8 +336,8 @@ function NavLink({ href, isExternal, isActive, isNearby = false, Link, externalL
281
336
  strokeWidth: "2",
282
337
  strokeLinecap: "round",
283
338
  strokeLinejoin: "round",
284
- children: /* @__PURE__ */ jsx("path", { d: "M14 4h6v6M20 4l-8 8M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5" })
285
- }),
339
+ children: /* @__PURE__ */ jsx("path", { d: NAV_ICON_PATHS.external[0] })
340
+ }) : null,
286
341
  /* @__PURE__ */ jsxs("span", {
287
342
  className: "wave-docs-sr-only",
288
343
  children: [" ", externalLink]
@@ -303,6 +358,58 @@ function NavLink({ href, isExternal, isActive, isNearby = false, Link, externalL
303
358
  children
304
359
  });
305
360
  }
361
+ /**
362
+ * The glyph at the head of a row: a folder for a group, a page for a page.
363
+ *
364
+ * Weight and a chevron were the only things telling a category from a page, and
365
+ * where the two interleave — a `Reference` group directly above an `Internals`
366
+ * page — that is not enough to scan a column of twenty. A silhouette is read
367
+ * before any word is.
368
+ *
369
+ * An external link takes the third glyph, in the same leading slot. It used to
370
+ * carry that mark at the *far* end of its row, which cost twice: the leading
371
+ * slot then had to be an empty box to keep the column from going ragged, and
372
+ * the trailing edge held two unrelated meanings — "opens elsewhere" on one row,
373
+ * "expands" on the next. Leading is what a row *is*; trailing is what it
374
+ * *does*. With the mark moved, the only thing at the far end of any row is a
375
+ * chevron, which is what makes a group legible from across the column — and
376
+ * leaves that edge free for a status dot or an overflow control later.
377
+ *
378
+ * ⚠️ THE VISUAL MARK MOVED AND THE ANNOUNCED ONE DID NOT. The sr-only "(opens
379
+ * in a new tab)" stays after the link text, so the name is still read as
380
+ * "GitHub, opens in a new tab" rather than the other way round.
381
+ *
382
+ * Inline SVG, matching `Chevron` and the external mark rather than a font or a
383
+ * dependency — the package ships no icon set, and these two are as generic as
384
+ * the chevron beside them.
385
+ */
386
+ const NAV_ICON_PATHS = {
387
+ folder: ["M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"],
388
+ file: ["M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z", "M14 2v4a2 2 0 0 0 2 2h4"],
389
+ external: ["M14 4h6v6M20 4l-8 8M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"]
390
+ };
391
+ function NavIcon({ type, name, icons }) {
392
+ const Custom = name === void 0 ? void 0 : icons[name];
393
+ if (Custom !== void 0) return /* @__PURE__ */ jsx("span", {
394
+ className: "wave-docs-sidebar__icon",
395
+ "aria-hidden": "true",
396
+ children: /* @__PURE__ */ jsx(Custom, {})
397
+ });
398
+ return /* @__PURE__ */ jsx("svg", {
399
+ className: "wave-docs-sidebar__icon",
400
+ "aria-hidden": "true",
401
+ focusable: "false",
402
+ viewBox: "0 0 24 24",
403
+ width: "16",
404
+ height: "16",
405
+ fill: "none",
406
+ stroke: "currentColor",
407
+ strokeWidth: "2",
408
+ strokeLinecap: "round",
409
+ strokeLinejoin: "round",
410
+ children: NAV_ICON_PATHS[type].map((d) => /* @__PURE__ */ jsx("path", { d }, d))
411
+ });
412
+ }
306
413
  function Chevron({ isOpen }) {
307
414
  return /* @__PURE__ */ jsx("svg", {
308
415
  className: "wave-docs-sidebar__chevron",
package/dist/source.js CHANGED
@@ -381,17 +381,20 @@ function buildNav(dir, config) {
381
381
  const visible = index && isVisibleIn(index.doc, config) ? index : void 0;
382
382
  const title = groupTitle(child, visible?.doc);
383
383
  const href = visible?.doc.href;
384
+ const icon = child.meta?.icon ?? visible?.doc.frontmatter.icon;
384
385
  const group = {
385
386
  type: "group",
386
387
  title,
387
388
  children,
388
- ...href !== void 0 ? { href } : {}
389
+ ...href !== void 0 ? { href } : {},
390
+ ...icon !== void 0 ? { icon } : {}
389
391
  };
390
392
  const node = children.length === 0 && visible !== void 0 && href !== void 0 ? {
391
393
  type: "page",
392
394
  title,
393
395
  href,
394
- slug: visible.doc.slug
396
+ slug: visible.doc.slug,
397
+ ...icon !== void 0 ? { icon } : {}
395
398
  } : group;
396
399
  const order = visible?.doc.frontmatter.order;
397
400
  entries.push({
@@ -403,7 +406,8 @@ function buildNav(dir, config) {
403
406
  type: "page",
404
407
  title: navTitle(visible.doc),
405
408
  href,
406
- slug: visible.doc.slug
409
+ slug: visible.doc.slug,
410
+ ...visible.doc.frontmatter.icon !== void 0 ? { icon: visible.doc.frontmatter.icon } : {}
407
411
  } } : {},
408
412
  ...order !== void 0 ? { order } : {}
409
413
  });
@@ -420,7 +424,8 @@ function toPageEntry(page, config) {
420
424
  type: "page",
421
425
  title,
422
426
  href: page.doc.href,
423
- slug: page.doc.slug
427
+ slug: page.doc.slug,
428
+ ...page.doc.frontmatter.icon !== void 0 ? { icon: page.doc.frontmatter.icon } : {}
424
429
  },
425
430
  ...order !== void 0 ? { order } : {},
426
431
  ...isVisibleIn(page.doc, config) ? {} : { hidden: true }