shelving 1.246.0 → 1.247.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.
@@ -9,7 +9,9 @@ import { FileExtractor } from "./FileExtractor.js";
9
9
  * - A `@kind` tag in a symbol's JSDoc overrides the inferred kind — e.g. `@kind component` relabels a React component (otherwise a `function`) so the docs site groups and colours it as a component. The override also drops the trailing `()` from the title, since a non-function kind reads as a bare name.
10
10
  * - Sets `description` (a plain-text summary from the first JSDoc paragraph) on every `tree-documentation` child.
11
11
  * - Sets `title` on every `tree-documentation` child — `name()` for functions, `Class.name()` for methods, `Class.name` for properties, bare `name` for other kinds.
12
- * - Records relational metadata as raw strings for render-time linking: `class` (owning class), `readonly`, `extends`, `implements`.
12
+ * - Records relational metadata as raw strings for render-time linking: `class` (owning class), `readonly`, `extends` / `implements` (full heritage type text including generic arguments, e.g. `AbstractStore<string>` or `Omit<StringSchemaOptions, "value">`), and `types` (the type names a `type` alias's body references, e.g. `OtherType` in `string | OtherType`).
13
+ * - Records a structured `properties` list for interfaces and object-literal `type` aliases — each member's name, type, optionality, `@default`, and description — so an options-bag parameter can be flattened into its fields at render time.
14
+ * - Pretty-prints object-literal signatures (interfaces and object-literal `type` aliases) as multi-line `{ … }` blocks, one member per line; other type bodies (`string | null`, mapped types, …) are emitted verbatim.
13
15
  * - Members declared with the `override` or `declare` modifier are skipped — the base class already documents overrides, and `declare` members are ambient type-only re-declarations rather than new API.
14
16
  * - Keys are the raw declared `name` (case-preserving) so case-distinct exports like `Collection` and `COLLECTION` stay separate.
15
17
  * - The file element itself has no `title` — a TS source file has no confident title source; renderers fall back to `name`.
@@ -10,7 +10,9 @@ import { extractMarkdownProps } from "./MarkupExtractor.js";
10
10
  * - A `@kind` tag in a symbol's JSDoc overrides the inferred kind — e.g. `@kind component` relabels a React component (otherwise a `function`) so the docs site groups and colours it as a component. The override also drops the trailing `()` from the title, since a non-function kind reads as a bare name.
11
11
  * - Sets `description` (a plain-text summary from the first JSDoc paragraph) on every `tree-documentation` child.
12
12
  * - Sets `title` on every `tree-documentation` child — `name()` for functions, `Class.name()` for methods, `Class.name` for properties, bare `name` for other kinds.
13
- * - Records relational metadata as raw strings for render-time linking: `class` (owning class), `readonly`, `extends`, `implements`.
13
+ * - Records relational metadata as raw strings for render-time linking: `class` (owning class), `readonly`, `extends` / `implements` (full heritage type text including generic arguments, e.g. `AbstractStore<string>` or `Omit<StringSchemaOptions, "value">`), and `types` (the type names a `type` alias's body references, e.g. `OtherType` in `string | OtherType`).
14
+ * - Records a structured `properties` list for interfaces and object-literal `type` aliases — each member's name, type, optionality, `@default`, and description — so an options-bag parameter can be flattened into its fields at render time.
15
+ * - Pretty-prints object-literal signatures (interfaces and object-literal `type` aliases) as multi-line `{ … }` blocks, one member per line; other type bodies (`string | null`, mapped types, …) are emitted verbatim.
14
16
  * - Members declared with the `override` or `declare` modifier are skipped — the base class already documents overrides, and `declare` members are ambient type-only re-declarations rather than new API.
15
17
  * - Keys are the raw declared `name` (case-preserving) so case-distinct exports like `Collection` and `COLLECTION` stay separate.
16
18
  * - The file element itself has no `title` — a TS source file has no confident title source; renderers fall back to `name`.
@@ -114,6 +116,9 @@ function _extractStatement(statement, source) {
114
116
  const examples = jsDoc?.examples;
115
117
  // Heritage (`extends` / `implements`) is only meaningful for classes and interfaces.
116
118
  const heritage = _getHeritage(statement, source);
119
+ // Referenced type names (type aliases) and structured property lists (interfaces / object-literal types) — both resolved to links at render time.
120
+ const types = _getReferencedTypes(statement, source);
121
+ const properties = _getProperties(statement, source);
117
122
  const children = _getClassMembers(statement, source, name);
118
123
  return {
119
124
  type: "tree-documentation",
@@ -132,18 +137,21 @@ function _extractStatement(statement, source) {
132
137
  examples,
133
138
  extends: heritage?.extends,
134
139
  implements: heritage?.implements,
140
+ types,
141
+ properties,
135
142
  children,
136
143
  },
137
144
  };
138
145
  }
139
- /** Extract the `extends` (single base type) and `implements` (interface list) names from a class or interface declaration. */
146
+ /** Extract the `extends` (single base type) and `implements` (interface list) heritage from a class or interface declaration, as full type text including any generic arguments (e.g. `AbstractStore<string>`, `Omit<StringSchemaOptions, "value">`). */
140
147
  function _getHeritage(statement, source) {
141
148
  if (!ts.isClassDeclaration(statement) && !ts.isInterfaceDeclaration(statement))
142
149
  return;
143
150
  let extendsName;
144
151
  const implementsNames = [];
145
152
  for (const clause of statement.heritageClauses ?? []) {
146
- const names = clause.types.map(t => t.expression.getText(source));
153
+ // Full text — keep generic arguments (`Foo<T>`) and wrappers (`Omit<…>`) intact; render-time lookup trims them to the bare name to resolve a link.
154
+ const names = clause.types.map(t => t.getText(source));
147
155
  // `extends` keeps the first base type; an interface extending several still surfaces its primary base.
148
156
  if (clause.token === ts.SyntaxKind.ExtendsKeyword)
149
157
  extendsName ??= names[0];
@@ -154,6 +162,62 @@ function _getHeritage(statement, source) {
154
162
  return;
155
163
  return { extends: extendsName, implements: implementsNames.length ? implementsNames : undefined };
156
164
  }
165
+ /**
166
+ * Collect the type names a `type` alias's body references (e.g. `OtherType` in `type X = string | OtherType`), for render-time linking.
167
+ * - Walks the whole type expression so names nested inside generics, unions, arrays, etc. are all caught.
168
+ * - Primitive keyword types (`string`, `number`, …) aren't type references so they're naturally excluded; the alias's own generic parameters are filtered out explicitly.
169
+ * - Order-preserving and de-duplicated. Unresolved names (builtins like `Record`, externals) simply stay as plain text at render time.
170
+ */
171
+ function _getReferencedTypes(statement, source) {
172
+ if (!ts.isTypeAliasDeclaration(statement))
173
+ return;
174
+ const generics = new Set(statement.typeParameters?.map(t => t.name.text) ?? []);
175
+ const names = [];
176
+ const seen = new Set();
177
+ const visit = (node) => {
178
+ if (ts.isTypeReferenceNode(node)) {
179
+ const name = node.typeName.getText(source);
180
+ if (!generics.has(name) && !seen.has(name)) {
181
+ seen.add(name);
182
+ names.push(name);
183
+ }
184
+ }
185
+ node.forEachChild(visit);
186
+ };
187
+ visit(statement.type);
188
+ return names.length ? names : undefined;
189
+ }
190
+ /**
191
+ * Extract structured property entries from an interface or object-literal `type` alias, mirroring the `DocumentationParam` shape.
192
+ * - Lets a consumer flatten an options-bag parameter into its individual fields at render time (resolve the param's type in the tree map, then list these).
193
+ * - Skips private `_`-prefixed members. Descriptions and `@default` values come from each member's own JSDoc.
194
+ */
195
+ function _getProperties(statement, source) {
196
+ const members = ts.isInterfaceDeclaration(statement)
197
+ ? statement.members
198
+ : ts.isTypeAliasDeclaration(statement) && ts.isTypeLiteralNode(statement.type)
199
+ ? statement.type.members
200
+ : undefined;
201
+ if (!members)
202
+ return;
203
+ const properties = [];
204
+ for (const member of members) {
205
+ if (!ts.isPropertySignature(member))
206
+ continue;
207
+ const name = member.name && ts.isIdentifier(member.name) ? member.name.text : undefined;
208
+ if (!name || name.startsWith("_"))
209
+ continue;
210
+ const jsDoc = _getJSDoc(member, source);
211
+ properties.push({
212
+ name,
213
+ type: member.type?.getText(source),
214
+ description: jsDoc?.description,
215
+ optional: !!member.questionToken,
216
+ default: jsDoc?.default,
217
+ });
218
+ }
219
+ return properties.length ? properties : undefined;
220
+ }
157
221
  /**
158
222
  * Combine the JSDoc leading-description text and any unhandled `@rule` blocks into a single markup content string.
159
223
  * - Unhandled rules (anything not `@param`/`@returns`/`@throws`/`@example`/`@see`) are appended after the description, separated by blank lines, with their `@name` preserved.
@@ -211,12 +275,13 @@ function _getSignatures(statement, source, name) {
211
275
  return _getConstructorSignatures(statement, source, name);
212
276
  }
213
277
  if (ts.isInterfaceDeclaration(statement)) {
214
- // Emit `{ member; member }` — the same shape a `type` object body produces, distinguished only by the `kind` badge.
215
- const members = statement.members.map(m => m.getText(source).replace(/;\s*$/, "").trim()).join("; ");
216
- return [members ? `{ ${members} }` : "{}"];
278
+ // Emit a pretty-printed `{ member; member }` block — the same shape a `type` object body produces, distinguished only by the `kind` badge.
279
+ return [_formatObjectSignature(statement.members, source)];
217
280
  }
218
281
  if (ts.isTypeAliasDeclaration(statement)) {
219
- // Emit only the type body (e.g. `{ a: string }` or `string | null`) the alias name is already the page title.
282
+ // Pretty-print object-literal aliases multi-line like an interface; emit other bodies (`string | null`, mapped types, …) verbatim. The alias name is already the page title.
283
+ if (ts.isTypeLiteralNode(statement.type))
284
+ return [_formatObjectSignature(statement.type.members, source)];
220
285
  return [statement.type.getText(source)];
221
286
  }
222
287
  if (ts.isVariableStatement(statement)) {
@@ -225,6 +290,13 @@ function _getSignatures(statement, source, name) {
225
290
  return [`${name}: ${declaration.type.getText(source)}`];
226
291
  }
227
292
  }
293
+ /** Pretty-print object-type members as a multi-line `{ … }` block — one member per tab-indented line ending in `;` — or `{}` when empty. */
294
+ function _formatObjectSignature(members, source) {
295
+ if (!members.length)
296
+ return "{}";
297
+ const lines = members.map(m => `\t${m.getText(source).replace(/;\s*$/, "").trim()};`);
298
+ return `{\n${lines.join("\n")}\n}`;
299
+ }
228
300
  /** Render a class's generic parameter names as `<P, R>` (names only, no constraints), or `""` when non-generic. */
229
301
  function _getTypeParamNames(statement, source) {
230
302
  const { typeParameters } = statement;
@@ -438,6 +510,7 @@ function _getJSDoc(node, source) {
438
510
  return;
439
511
  const description = ts.getTextOfJSDocComment(jsDoc.comment)?.trim();
440
512
  let kind;
513
+ let def;
441
514
  const params = [];
442
515
  const returns = [];
443
516
  const throws = [];
@@ -469,6 +542,9 @@ function _getJSDoc(node, source) {
469
542
  // `@kind <name>` overrides the AST-inferred kind (e.g. `@kind component` for a React component declared as a function).
470
543
  if (name === "kind")
471
544
  kind = comment?.match(/^[\w-]+/)?.[0];
545
+ // `@default <value>` documents a property's default — surfaced structurally (e.g. on `properties`) rather than rendered into the body.
546
+ else if (name === "default")
547
+ def = comment || undefined;
472
548
  else if (name === "example") {
473
549
  if (comment)
474
550
  examples.push({ description: comment });
@@ -480,6 +556,7 @@ function _getJSDoc(node, source) {
480
556
  return {
481
557
  description: description || undefined,
482
558
  kind,
559
+ default: def,
483
560
  params: params.length ? params : undefined,
484
561
  returns: returns.length ? returns : undefined,
485
562
  throws: throws.length ? throws : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.246.0",
3
+ "version": "1.247.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,8 +1,11 @@
1
- import type { ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
2
  import type { DocumentationElementProps } from "../../util/tree.js";
3
3
  /**
4
4
  * Page renderer for a `tree-documentation` element — the full detail page for a documented symbol.
5
- * - Renders breadcrumbs, title (with kind + `readonly` tags), relational links (`member of`, `extends`, `implements`), signatures (one per overload), content, parameters, returns, throws, and examples.
5
+ * - Renders breadcrumbs, title (with kind + `readonly` tags), relational links (`member of`, `extends`, `implements`), signatures (one per overload), content, parameters, returns, throws, referenced types, and examples.
6
+ * - In the Parameters / Returns / Throws tables the `Type` column links each type to its documented page via `TreeLink` (exact-match only; compound or builtin types stay plain text), and a row with no hand-written description falls back to the referenced type's own `description`.
7
+ * - An options-bag parameter whose type resolves to a documented interface/object type is flattened into indented child rows (one per property), so readers see the individual fields inline.
8
+ * - A `type` alias's referenced type names render as a linked `Type` table, each row carrying the resolved element's `description` (exact-match only).
6
9
  * - Child symbols are grouped by `kind` into card sections (Functions, Classes, Methods, Properties, …), each under its own heading.
7
10
  * - All sections are conditional — only render when they have entries.
8
11
  *
@@ -12,4 +15,4 @@ import type { DocumentationElementProps } from "../../util/tree.js";
12
15
  * @example <DocumentationPage {...element.props} />
13
16
  * @see https://dhoulb.github.io/shelving/ui/docs/DocumentationPage/DocumentationPage
14
17
  */
15
- export declare function DocumentationPage({ title, name, kind, description, content, signatures, params, returns, throws, examples, children, ...props }: DocumentationElementProps): ReactNode;
18
+ export declare function DocumentationPage({ title, name, kind, description, content, signatures, params, returns, throws, types, examples, children, ...props }: DocumentationElementProps): ReactNode;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment } from "react";
2
3
  import { walkElements } from "../../util/element.js";
3
4
  import { Block } from "../block/Block.js";
4
5
  import { Heading } from "../block/Heading.js";
@@ -15,10 +16,16 @@ import { Row } from "../style/Flex.js";
15
16
  import { Scroll } from "../style/Scroll.js";
16
17
  import { TreeBreadcrumbs } from "../tree/TreeBreadcrumbs.js";
17
18
  import { TreeCards } from "../tree/TreeCards.js";
19
+ import { getTreeElement, useTreeMap } from "../tree/TreeContext.js";
20
+ import { TreeLink } from "../tree/TreeLink.js";
18
21
  import { DocumentationButtons } from "./DocumentationButtons.js";
19
22
  import { DocumentationKind, getDocumentationKindColor } from "./DocumentationKind.js";
20
23
  import { DocumentationSignatures } from "./DocumentationSignatures.js";
21
24
  const DEFAULT_TYPE = "unknown";
25
+ /** Resolve a table row's description — the manually-written one, falling back to the referenced type's own `description` from the tree map (exact-match only). */
26
+ function _getRowDescription(map, type, description) {
27
+ return description || getTreeElement(map, type)?.props.description || "";
28
+ }
22
29
  /** Documentation `kind`s grouped into card sections, in display order — pluralised, sentence-case headings. */
23
30
  const KIND_SECTIONS = {
24
31
  component: "Components",
@@ -57,7 +64,10 @@ function DocumentationChildren({ elements }) {
57
64
  }
58
65
  /**
59
66
  * Page renderer for a `tree-documentation` element — the full detail page for a documented symbol.
60
- * - Renders breadcrumbs, title (with kind + `readonly` tags), relational links (`member of`, `extends`, `implements`), signatures (one per overload), content, parameters, returns, throws, and examples.
67
+ * - Renders breadcrumbs, title (with kind + `readonly` tags), relational links (`member of`, `extends`, `implements`), signatures (one per overload), content, parameters, returns, throws, referenced types, and examples.
68
+ * - In the Parameters / Returns / Throws tables the `Type` column links each type to its documented page via `TreeLink` (exact-match only; compound or builtin types stay plain text), and a row with no hand-written description falls back to the referenced type's own `description`.
69
+ * - An options-bag parameter whose type resolves to a documented interface/object type is flattened into indented child rows (one per property), so readers see the individual fields inline.
70
+ * - A `type` alias's referenced type names render as a linked `Type` table, each row carrying the resolved element's `description` (exact-match only).
61
71
  * - Child symbols are grouped by `kind` into card sections (Functions, Classes, Methods, Properties, …), each under its own heading.
62
72
  * - All sections are conditional — only render when they have entries.
63
73
  *
@@ -67,6 +77,11 @@ function DocumentationChildren({ elements }) {
67
77
  * @example <DocumentationPage {...element.props} />
68
78
  * @see https://dhoulb.github.io/shelving/ui/docs/DocumentationPage/DocumentationPage
69
79
  */
70
- export function DocumentationPage({ title, name, kind = "unknown", description, content, signatures, params, returns, throws, examples, children, ...props }) {
71
- return (_jsx(Page, { title: title ?? name, description: description, children: _jsxs(Block, { color: getDocumentationKindColor(kind), children: [_jsx(Panel, { children: _jsxs(Header, { children: [_jsx(TreeBreadcrumbs, {}), _jsx(Title, { children: _jsxs(Row, { left: true, wrap: true, children: [title ?? name, kind && _jsx(DocumentationKind, { kind: kind, size: "normal" })] }) }), _jsx(DocumentationButtons, { ...props })] }) }), signatures?.length || params?.length || returns?.length || throws?.length ? (_jsxs(Section, { children: [_jsx(DocumentationSignatures, { signatures: signatures }), params?.length && (_jsx(Section, { children: _jsx(Scroll, { horizontal: true, children: _jsxs(Table, { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Parameter" }), _jsx("th", { children: "Type" }), _jsx("th", { children: "Default" }), _jsx("th", { children: "Description" })] }) }), _jsx("tbody", { children: params.map(({ name, type = DEFAULT_TYPE, description = "", default: def }) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(Code, { children: name }) }), _jsx("td", { children: _jsx(Code, { children: type }) }), _jsx("td", { children: def ? _jsx(Code, { children: def }) : "-" }), _jsx("td", { children: description })] }, `${name}-${type}-${description}`))) })] }) }) })), returns?.length && (_jsx(Section, { children: _jsx(Scroll, { horizontal: true, children: _jsxs(Table, { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Return" }), _jsx("th", { children: "Description" })] }) }), _jsx("tbody", { children: returns.map(({ type = DEFAULT_TYPE, description = "" }) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(Code, { children: type }) }), _jsx("td", { children: description })] }, `${type}-${description}`))) })] }) }) })), throws?.length && (_jsx(Section, { children: _jsx(Scroll, { horizontal: true, children: _jsxs(Table, { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Throws" }), _jsx("th", { children: "Description" })] }) }), _jsx("tbody", { children: throws.map(({ type = DEFAULT_TYPE, description = "" }) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(Code, { children: type }) }), _jsx("td", { children: description })] }, `${type}-${description}`))) })] }) }) }))] })) : null, content && (_jsx(Section, { children: _jsx(Prose, { children: _jsx(Markup, { children: content }) }) })), examples?.length && (_jsxs(Section, { children: [_jsx(Heading, { children: "Examples" }), examples.map(({ description }) => (_jsx(Preformatted, { children: description }, description)))] })), _jsx(DocumentationChildren, { elements: children })] }) }));
80
+ export function DocumentationPage({ title, name, kind = "unknown", description, content, signatures, params, returns, throws, types, examples, children, ...props }) {
81
+ const map = useTreeMap();
82
+ return (_jsx(Page, { title: title ?? name, description: description, children: _jsxs(Block, { color: getDocumentationKindColor(kind), children: [_jsx(Panel, { children: _jsxs(Header, { children: [_jsx(TreeBreadcrumbs, {}), _jsx(Title, { children: _jsxs(Row, { left: true, wrap: true, children: [title ?? name, kind && _jsx(DocumentationKind, { kind: kind, size: "normal" })] }) }), _jsx(DocumentationButtons, { ...props })] }) }), signatures?.length || params?.length || returns?.length || throws?.length || types?.length ? (_jsxs(Section, { children: [_jsx(DocumentationSignatures, { signatures: signatures }), params?.length && (_jsx(Section, { children: _jsx(Scroll, { horizontal: true, children: _jsxs(Table, { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Parameter" }), _jsx("th", { children: "Type" }), _jsx("th", { children: "Default" })] }) }), _jsx("tbody", { children: params.map(({ name, type = DEFAULT_TYPE, description, default: def }) => {
83
+ // An options-bag param whose type resolves to a documented interface/object type is flattened into its individual fields as indented child rows.
84
+ const resolved = getTreeElement(map, type)?.props;
85
+ return (_jsxs(Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { children: _jsx(Code, { children: name }) }), _jsx("td", { children: _jsx(TreeLink, { name: type }) }), _jsx("td", { children: def ? _jsx(Code, { children: def }) : "-" }), _jsx("td", { children: description || resolved?.description || "" })] }), resolved?.properties?.map(prop => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(Code, { children: `.${prop.name}` }) }), _jsx("td", { children: _jsx(TreeLink, { name: prop.type ?? DEFAULT_TYPE }) }), _jsx("td", { children: prop.default ? _jsx(Code, { children: prop.default }) : "-" }), _jsx("td", { children: _getRowDescription(map, prop.type ?? DEFAULT_TYPE, prop.description) })] }, `${name}.${prop.name}`)))] }, `${name}-${type}`));
86
+ }) })] }) }) })), returns?.length && (_jsx(Section, { children: _jsx(Scroll, { horizontal: true, children: _jsxs(Table, { children: [_jsx("thead", { children: _jsx("tr", { children: _jsx("th", { children: "Return" }) }) }), _jsx("tbody", { children: returns.map(({ type = DEFAULT_TYPE, description }) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(TreeLink, { name: type }) }), _jsx("td", { children: _getRowDescription(map, type, description) })] }, `${type}-${description}`))) })] }) }) })), throws?.length && (_jsx(Section, { children: _jsx(Scroll, { horizontal: true, children: _jsxs(Table, { children: [_jsx("thead", { children: _jsx("tr", { children: _jsx("th", { children: "Throws" }) }) }), _jsx("tbody", { children: throws.map(({ type = DEFAULT_TYPE, description }) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(TreeLink, { name: type }) }), _jsx("td", { children: _getRowDescription(map, type, description) })] }, `${type}-${description}`))) })] }) }) })), types?.length && (_jsx(Section, { children: _jsx(Scroll, { horizontal: true, children: _jsxs(Table, { children: [_jsx("thead", { children: _jsx("tr", { children: _jsx("th", { children: "Type" }) }) }), _jsx("tbody", { children: types.map(type => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(TreeLink, { name: type }) }), _jsx("td", { children: _getRowDescription(map, type) })] }, type))) })] }) }) }))] })) : null, content && (_jsx(Section, { children: _jsx(Prose, { children: _jsx(Markup, { children: content }) }) })), examples?.length && (_jsxs(Section, { children: [_jsx(Heading, { children: "Examples" }), examples.map(({ description }) => (_jsx(Preformatted, { children: description }, description)))] })), _jsx(DocumentationChildren, { elements: children })] }) }));
72
87
  }
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
1
+ import { Fragment, type ReactNode } from "react";
2
2
  import { walkElements } from "../../util/element.js";
3
3
  import type { DocumentationElementProps, TreeElement, TreeElements } from "../../util/tree.js";
4
4
  import { Block } from "../block/Block.js";
@@ -16,12 +16,19 @@ import { Row } from "../style/Flex.js";
16
16
  import { Scroll } from "../style/Scroll.js";
17
17
  import { TreeBreadcrumbs } from "../tree/TreeBreadcrumbs.js";
18
18
  import { TreeCards } from "../tree/TreeCards.js";
19
+ import { getTreeElement, useTreeMap } from "../tree/TreeContext.js";
20
+ import { TreeLink } from "../tree/TreeLink.js";
19
21
  import { DocumentationButtons } from "./DocumentationButtons.js";
20
22
  import { DocumentationKind, getDocumentationKindColor } from "./DocumentationKind.js";
21
23
  import { DocumentationSignatures } from "./DocumentationSignatures.js";
22
24
 
23
25
  const DEFAULT_TYPE = "unknown";
24
26
 
27
+ /** Resolve a table row's description — the manually-written one, falling back to the referenced type's own `description` from the tree map (exact-match only). */
28
+ function _getRowDescription(map: ReadonlyMap<string, TreeElement>, type: string, description?: string | undefined): string {
29
+ return description || getTreeElement(map, type)?.props.description || "";
30
+ }
31
+
25
32
  /** Documentation `kind`s grouped into card sections, in display order — pluralised, sentence-case headings. */
26
33
  const KIND_SECTIONS = {
27
34
  component: "Components",
@@ -69,7 +76,10 @@ function DocumentationChildren({ elements }: { readonly elements?: TreeElements
69
76
 
70
77
  /**
71
78
  * Page renderer for a `tree-documentation` element — the full detail page for a documented symbol.
72
- * - Renders breadcrumbs, title (with kind + `readonly` tags), relational links (`member of`, `extends`, `implements`), signatures (one per overload), content, parameters, returns, throws, and examples.
79
+ * - Renders breadcrumbs, title (with kind + `readonly` tags), relational links (`member of`, `extends`, `implements`), signatures (one per overload), content, parameters, returns, throws, referenced types, and examples.
80
+ * - In the Parameters / Returns / Throws tables the `Type` column links each type to its documented page via `TreeLink` (exact-match only; compound or builtin types stay plain text), and a row with no hand-written description falls back to the referenced type's own `description`.
81
+ * - An options-bag parameter whose type resolves to a documented interface/object type is flattened into indented child rows (one per property), so readers see the individual fields inline.
82
+ * - A `type` alias's referenced type names render as a linked `Type` table, each row carrying the resolved element's `description` (exact-match only).
73
83
  * - Child symbols are grouped by `kind` into card sections (Functions, Classes, Methods, Properties, …), each under its own heading.
74
84
  * - All sections are conditional — only render when they have entries.
75
85
  *
@@ -89,10 +99,12 @@ export function DocumentationPage({
89
99
  params,
90
100
  returns,
91
101
  throws,
102
+ types,
92
103
  examples,
93
104
  children,
94
105
  ...props
95
106
  }: DocumentationElementProps): ReactNode {
107
+ const map = useTreeMap();
96
108
  return (
97
109
  <Page title={title ?? name} description={description}>
98
110
  <Block color={getDocumentationKindColor(kind)}>
@@ -108,7 +120,7 @@ export function DocumentationPage({
108
120
  <DocumentationButtons {...props} />
109
121
  </Header>
110
122
  </Panel>
111
- {signatures?.length || params?.length || returns?.length || throws?.length ? (
123
+ {signatures?.length || params?.length || returns?.length || throws?.length || types?.length ? (
112
124
  <Section>
113
125
  <DocumentationSignatures signatures={signatures} />
114
126
  {params?.length && (
@@ -120,22 +132,39 @@ export function DocumentationPage({
120
132
  <th>Parameter</th>
121
133
  <th>Type</th>
122
134
  <th>Default</th>
123
- <th>Description</th>
124
135
  </tr>
125
136
  </thead>
126
137
  <tbody>
127
- {params.map(({ name, type = DEFAULT_TYPE, description = "", default: def }) => (
128
- <tr key={`${name}-${type}-${description}`}>
129
- <td>
130
- <Code>{name}</Code>
131
- </td>
132
- <td>
133
- <Code>{type}</Code>
134
- </td>
135
- <td>{def ? <Code>{def}</Code> : "-"}</td>
136
- <td>{description}</td>
137
- </tr>
138
- ))}
138
+ {params.map(({ name, type = DEFAULT_TYPE, description, default: def }) => {
139
+ // An options-bag param whose type resolves to a documented interface/object type is flattened into its individual fields as indented child rows.
140
+ const resolved = getTreeElement(map, type)?.props as DocumentationElementProps | undefined;
141
+ return (
142
+ <Fragment key={`${name}-${type}`}>
143
+ <tr>
144
+ <td>
145
+ <Code>{name}</Code>
146
+ </td>
147
+ <td>
148
+ <TreeLink name={type} />
149
+ </td>
150
+ <td>{def ? <Code>{def}</Code> : "-"}</td>
151
+ <td>{description || resolved?.description || ""}</td>
152
+ </tr>
153
+ {resolved?.properties?.map(prop => (
154
+ <tr key={`${name}.${prop.name}`}>
155
+ <td>
156
+ <Code>{`.${prop.name}`}</Code>
157
+ </td>
158
+ <td>
159
+ <TreeLink name={prop.type ?? DEFAULT_TYPE} />
160
+ </td>
161
+ <td>{prop.default ? <Code>{prop.default}</Code> : "-"}</td>
162
+ <td>{_getRowDescription(map, prop.type ?? DEFAULT_TYPE, prop.description)}</td>
163
+ </tr>
164
+ ))}
165
+ </Fragment>
166
+ );
167
+ })}
139
168
  </tbody>
140
169
  </Table>
141
170
  </Scroll>
@@ -148,16 +177,15 @@ export function DocumentationPage({
148
177
  <thead>
149
178
  <tr>
150
179
  <th>Return</th>
151
- <th>Description</th>
152
180
  </tr>
153
181
  </thead>
154
182
  <tbody>
155
- {returns.map(({ type = DEFAULT_TYPE, description = "" }) => (
183
+ {returns.map(({ type = DEFAULT_TYPE, description }) => (
156
184
  <tr key={`${type}-${description}`}>
157
185
  <td>
158
- <Code>{type}</Code>
186
+ <TreeLink name={type} />
159
187
  </td>
160
- <td>{description}</td>
188
+ <td>{_getRowDescription(map, type, description)}</td>
161
189
  </tr>
162
190
  ))}
163
191
  </tbody>
@@ -172,16 +200,38 @@ export function DocumentationPage({
172
200
  <thead>
173
201
  <tr>
174
202
  <th>Throws</th>
175
- <th>Description</th>
176
203
  </tr>
177
204
  </thead>
178
205
  <tbody>
179
- {throws.map(({ type = DEFAULT_TYPE, description = "" }) => (
206
+ {throws.map(({ type = DEFAULT_TYPE, description }) => (
180
207
  <tr key={`${type}-${description}`}>
181
208
  <td>
182
- <Code>{type}</Code>
209
+ <TreeLink name={type} />
210
+ </td>
211
+ <td>{_getRowDescription(map, type, description)}</td>
212
+ </tr>
213
+ ))}
214
+ </tbody>
215
+ </Table>
216
+ </Scroll>
217
+ </Section>
218
+ )}
219
+ {types?.length && (
220
+ <Section>
221
+ <Scroll horizontal>
222
+ <Table>
223
+ <thead>
224
+ <tr>
225
+ <th>Type</th>
226
+ </tr>
227
+ </thead>
228
+ <tbody>
229
+ {types.map(type => (
230
+ <tr key={type}>
231
+ <td>
232
+ <TreeLink name={type} />
183
233
  </td>
184
- <td>{description}</td>
234
+ <td>{_getRowDescription(map, type)}</td>
185
235
  </tr>
186
236
  ))}
187
237
  </tbody>
@@ -14,8 +14,8 @@ export interface TreeButtonProps extends ButtonVariants {
14
14
  /**
15
15
  * Small button linking to a specific tree element, resolved by reference string.
16
16
  *
17
- * - Looks `name` up in the flattened tree map (`useTreeMap()`) — by flat key (`"Store"`, `"Store.get"`) or canonical path (`"/schema/BooleanSchema"`) — and links to the element's canonical `path`.
18
- * - A hit becomes an `<a>` link; a miss (e.g. a builtin like `Serializable`) stays a plain non-link label so it still reads as text.
17
+ * - Resolves `name` via `getTreeElement()` — by flat key (`"Store"`, `"Store.get"`) or canonical path (`"/schema/BooleanSchema"`), falling back to the bare name for a single generic type (`"Schema<T>"` → `"Schema"`) — and links to the element's canonical `path`.
18
+ * - A hit becomes an `<a>` link; a miss (e.g. a builtin like `Serializable`, or a compound type like `Foo | null`) stays a plain non-link label so it still reads as text.
19
19
  * - Defaults to `small plain` styling; pass other `ButtonVariants` to override.
20
20
  *
21
21
  * @param props The element reference `name`, optional `children` label, and button variants.
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Button } from "../form/Button.js";
3
- import { useTreeMap } from "./TreeContext.js";
3
+ import { getTreeElement, useTreeMap } from "./TreeContext.js";
4
4
  /**
5
5
  * Small button linking to a specific tree element, resolved by reference string.
6
6
  *
7
- * - Looks `name` up in the flattened tree map (`useTreeMap()`) — by flat key (`"Store"`, `"Store.get"`) or canonical path (`"/schema/BooleanSchema"`) — and links to the element's canonical `path`.
8
- * - A hit becomes an `<a>` link; a miss (e.g. a builtin like `Serializable`) stays a plain non-link label so it still reads as text.
7
+ * - Resolves `name` via `getTreeElement()` — by flat key (`"Store"`, `"Store.get"`) or canonical path (`"/schema/BooleanSchema"`), falling back to the bare name for a single generic type (`"Schema<T>"` → `"Schema"`) — and links to the element's canonical `path`.
8
+ * - A hit becomes an `<a>` link; a miss (e.g. a builtin like `Serializable`, or a compound type like `Foo | null`) stays a plain non-link label so it still reads as text.
9
9
  * - Defaults to `small plain` styling; pass other `ButtonVariants` to override.
10
10
  *
11
11
  * @param props The element reference `name`, optional `children` label, and button variants.
@@ -14,7 +14,7 @@ import { useTreeMap } from "./TreeContext.js";
14
14
  * @see https://dhoulb.github.io/shelving/ui/tree/TreeButton/TreeButton
15
15
  */
16
16
  export function TreeButton({ name, children, small = true, plain = true, ...variants }) {
17
- const element = useTreeMap().get(name);
17
+ const element = getTreeElement(useTreeMap(), name);
18
18
  const href = element?.props.path;
19
19
  // A resolved element links via its canonical `path`; an unresolved reference is disabled (no `href`) so it renders as a non-link label rather than an empty `<a>`.
20
20
  return (_jsx(Button, { small: small, plain: plain, ...variants, ...(href ? { href } : { disabled: true }), children: children ?? element?.props.title ?? name }));
@@ -60,4 +60,15 @@ describe("TreeButton", () => {
60
60
  expect(html).toContain("Serializable"); // still reads as text
61
61
  expect(html).not.toContain("Serializable</a>"); // but is not a link
62
62
  });
63
+
64
+ test("links a generic reference by its bare type name", () => {
65
+ // `Store<T>` isn't a key, but stripping the generics resolves `Store`.
66
+ const html = render(<TreeButton name="Store<T>" />);
67
+ expect(html).toContain('href="http://x.com/Store"');
68
+ });
69
+
70
+ test("leaves a compound generic reference unresolved", () => {
71
+ const html = render(<TreeButton name="Store<T> | null" />);
72
+ expect(html).not.toContain("</a>"); // not `Identifier<…>`-shaped, so no link
73
+ });
63
74
  });
@@ -1,6 +1,6 @@
1
1
  import type { ReactElement, ReactNode } from "react";
2
2
  import { Button, type ButtonVariants } from "../form/Button.js";
3
- import { useTreeMap } from "./TreeContext.js";
3
+ import { getTreeElement, useTreeMap } from "./TreeContext.js";
4
4
 
5
5
  /**
6
6
  * Props for the `TreeButton` component — the element reference plus button variants.
@@ -17,8 +17,8 @@ export interface TreeButtonProps extends ButtonVariants {
17
17
  /**
18
18
  * Small button linking to a specific tree element, resolved by reference string.
19
19
  *
20
- * - Looks `name` up in the flattened tree map (`useTreeMap()`) — by flat key (`"Store"`, `"Store.get"`) or canonical path (`"/schema/BooleanSchema"`) — and links to the element's canonical `path`.
21
- * - A hit becomes an `<a>` link; a miss (e.g. a builtin like `Serializable`) stays a plain non-link label so it still reads as text.
20
+ * - Resolves `name` via `getTreeElement()` — by flat key (`"Store"`, `"Store.get"`) or canonical path (`"/schema/BooleanSchema"`), falling back to the bare name for a single generic type (`"Schema<T>"` → `"Schema"`) — and links to the element's canonical `path`.
21
+ * - A hit becomes an `<a>` link; a miss (e.g. a builtin like `Serializable`, or a compound type like `Foo | null`) stays a plain non-link label so it still reads as text.
22
22
  * - Defaults to `small plain` styling; pass other `ButtonVariants` to override.
23
23
  *
24
24
  * @param props The element reference `name`, optional `children` label, and button variants.
@@ -27,7 +27,7 @@ export interface TreeButtonProps extends ButtonVariants {
27
27
  * @see https://dhoulb.github.io/shelving/ui/tree/TreeButton/TreeButton
28
28
  */
29
29
  export function TreeButton({ name, children, small = true, plain = true, ...variants }: TreeButtonProps): ReactElement {
30
- const element = useTreeMap().get(name);
30
+ const element = getTreeElement(useTreeMap(), name);
31
31
  const href = element?.props.path;
32
32
  // A resolved element links via its canonical `path`; an unresolved reference is disabled (no `href`) so it renders as a non-link label rather than an empty `<a>`.
33
33
  return (
@@ -33,3 +33,17 @@ export declare function TreeProvider({ tree, children }: {
33
33
  * @see https://dhoulb.github.io/shelving/ui/tree/TreeContext/useTreeMap
34
34
  */
35
35
  export declare function useTreeMap(): ReadonlyMap<string, TreeElement>;
36
+ /**
37
+ * Resolve a reference string to its tree element — by exact key first, then by the bare type name when the whole reference is a single generic type.
38
+ *
39
+ * - Exact lookup handles flat keys (`"BooleanSchema"`, `"Store.get"`) and canonical paths (`"/schema/BooleanSchema"`).
40
+ * - On a miss, a `Foo<…>`-shaped reference retries with the generics stripped (`"Schema<T>"` → `"Schema"`), so a generic type name still links without the extractor storing a separate un-generic'd key — the generics stay in the displayed label.
41
+ * - A compound reference (`"Schema<T> | null"`, `"Omit<X, 'k'>"`) only retries on the leading identifier when the whole string is `Identifier<…>`; otherwise it stays a miss and the caller falls back to plain text.
42
+ *
43
+ * @param map The flattened tree lookup map (from `useTreeMap()`).
44
+ * @param ref The reference string — a flat key, canonical path, or raw type expression.
45
+ * @returns The resolved element, or `undefined` on a miss.
46
+ * @example getTreeElement(useTreeMap(), "Schema<T>") // the `Schema` element
47
+ * @see https://dhoulb.github.io/shelving/ui/tree/TreeContext/getTreeElement
48
+ */
49
+ export declare function getTreeElement(map: ReadonlyMap<string, TreeElement>, ref: string): TreeElement | undefined;
@@ -38,3 +38,23 @@ export function TreeProvider({ tree, children }) {
38
38
  export function useTreeMap() {
39
39
  return use(TreeContext);
40
40
  }
41
+ /**
42
+ * Resolve a reference string to its tree element — by exact key first, then by the bare type name when the whole reference is a single generic type.
43
+ *
44
+ * - Exact lookup handles flat keys (`"BooleanSchema"`, `"Store.get"`) and canonical paths (`"/schema/BooleanSchema"`).
45
+ * - On a miss, a `Foo<…>`-shaped reference retries with the generics stripped (`"Schema<T>"` → `"Schema"`), so a generic type name still links without the extractor storing a separate un-generic'd key — the generics stay in the displayed label.
46
+ * - A compound reference (`"Schema<T> | null"`, `"Omit<X, 'k'>"`) only retries on the leading identifier when the whole string is `Identifier<…>`; otherwise it stays a miss and the caller falls back to plain text.
47
+ *
48
+ * @param map The flattened tree lookup map (from `useTreeMap()`).
49
+ * @param ref The reference string — a flat key, canonical path, or raw type expression.
50
+ * @returns The resolved element, or `undefined` on a miss.
51
+ * @example getTreeElement(useTreeMap(), "Schema<T>") // the `Schema` element
52
+ * @see https://dhoulb.github.io/shelving/ui/tree/TreeContext/getTreeElement
53
+ */
54
+ export function getTreeElement(map, ref) {
55
+ const exact = map.get(ref);
56
+ if (exact || !ref.includes("<"))
57
+ return exact;
58
+ // Strip a whole-string generic wrapper (`Foo<…>` → `Foo`) and retry; leave compound expressions (unions, etc.) unresolved.
59
+ return map.get(ref.replace(/^([\w$.]+)<.*>$/s, "$1"));
60
+ }
@@ -40,3 +40,23 @@ export function TreeProvider({ tree, children }: { readonly tree: TreeElement; r
40
40
  export function useTreeMap(): ReadonlyMap<string, TreeElement> {
41
41
  return use(TreeContext);
42
42
  }
43
+
44
+ /**
45
+ * Resolve a reference string to its tree element — by exact key first, then by the bare type name when the whole reference is a single generic type.
46
+ *
47
+ * - Exact lookup handles flat keys (`"BooleanSchema"`, `"Store.get"`) and canonical paths (`"/schema/BooleanSchema"`).
48
+ * - On a miss, a `Foo<…>`-shaped reference retries with the generics stripped (`"Schema<T>"` → `"Schema"`), so a generic type name still links without the extractor storing a separate un-generic'd key — the generics stay in the displayed label.
49
+ * - A compound reference (`"Schema<T> | null"`, `"Omit<X, 'k'>"`) only retries on the leading identifier when the whole string is `Identifier<…>`; otherwise it stays a miss and the caller falls back to plain text.
50
+ *
51
+ * @param map The flattened tree lookup map (from `useTreeMap()`).
52
+ * @param ref The reference string — a flat key, canonical path, or raw type expression.
53
+ * @returns The resolved element, or `undefined` on a miss.
54
+ * @example getTreeElement(useTreeMap(), "Schema<T>") // the `Schema` element
55
+ * @see https://dhoulb.github.io/shelving/ui/tree/TreeContext/getTreeElement
56
+ */
57
+ export function getTreeElement(map: ReadonlyMap<string, TreeElement>, ref: string): TreeElement | undefined {
58
+ const exact = map.get(ref);
59
+ if (exact || !ref.includes("<")) return exact;
60
+ // Strip a whole-string generic wrapper (`Foo<…>` → `Foo`) and retry; leave compound expressions (unions, etc.) unresolved.
61
+ return map.get(ref.replace(/^([\w$.]+)<.*>$/s, "$1"));
62
+ }
@@ -0,0 +1,26 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ /**
3
+ * Props for the `TreeLink` component — the element reference plus an optional label.
4
+ *
5
+ * @see https://dhoulb.github.io/shelving/ui/tree/TreeLink/TreeLinkProps
6
+ */
7
+ export interface TreeLinkProps {
8
+ /** Reference to an element in the tree — a flat key (`"BooleanSchema"`, `"Store.get"`) or a canonical path (`"/schema/BooleanSchema"`). */
9
+ readonly name: string;
10
+ /** Visible label — defaults to `name` (typically a raw type expression like `"SlugSchemaOptions"`). */
11
+ readonly children?: ReactNode | undefined;
12
+ }
13
+ /**
14
+ * Inline `Code` token linking to a specific tree element, resolved by reference string — the link-styled counterpart of `TreeButton`.
15
+ *
16
+ * - Resolves `name` via `getTreeElement()` — by flat key or canonical path, falling back to the bare name for a single generic type (`Schema<T>` → `Schema`) — and links to the element's canonical `path`.
17
+ * - A hit becomes a `<Link>` wrapping a `<Code>` token; a miss (e.g. a builtin like `string` or a compound type like `T | null` that isn't an exact token) stays a plain `<Code>` so it still reads as code.
18
+ * - Designed for the `Type` column of the documentation Parameters / Returns / Throws / Types tables, where only exact-match type names should link.
19
+ *
20
+ * @kind component
21
+ * @param props The element reference `name` and optional `children` label.
22
+ * @returns A `<Code>` token, wrapped in a `<Link>` when the reference resolves.
23
+ * @example <TreeLink name="BooleanSchema" />
24
+ * @see https://dhoulb.github.io/shelving/ui/tree/TreeLink/TreeLink
25
+ */
26
+ export declare function TreeLink({ name, children }: TreeLinkProps): ReactElement;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Code } from "../inline/Code.js";
3
+ import { Link } from "../inline/Link.js";
4
+ import { getTreeElement, useTreeMap } from "./TreeContext.js";
5
+ /**
6
+ * Inline `Code` token linking to a specific tree element, resolved by reference string — the link-styled counterpart of `TreeButton`.
7
+ *
8
+ * - Resolves `name` via `getTreeElement()` — by flat key or canonical path, falling back to the bare name for a single generic type (`Schema<T>` → `Schema`) — and links to the element's canonical `path`.
9
+ * - A hit becomes a `<Link>` wrapping a `<Code>` token; a miss (e.g. a builtin like `string` or a compound type like `T | null` that isn't an exact token) stays a plain `<Code>` so it still reads as code.
10
+ * - Designed for the `Type` column of the documentation Parameters / Returns / Throws / Types tables, where only exact-match type names should link.
11
+ *
12
+ * @kind component
13
+ * @param props The element reference `name` and optional `children` label.
14
+ * @returns A `<Code>` token, wrapped in a `<Link>` when the reference resolves.
15
+ * @example <TreeLink name="BooleanSchema" />
16
+ * @see https://dhoulb.github.io/shelving/ui/tree/TreeLink/TreeLink
17
+ */
18
+ export function TreeLink({ name, children }) {
19
+ const href = getTreeElement(useTreeMap(), name)?.props.path;
20
+ const code = _jsx(Code, { children: children ?? name });
21
+ // A resolved reference links via its canonical `path`; an unresolved one stays a plain code token rather than an empty link.
22
+ return href ? _jsx(Link, { href: href, children: code }) : code;
23
+ }
@@ -0,0 +1,36 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import { Code } from "../inline/Code.js";
3
+ import { Link } from "../inline/Link.js";
4
+ import { getTreeElement, useTreeMap } from "./TreeContext.js";
5
+
6
+ /**
7
+ * Props for the `TreeLink` component — the element reference plus an optional label.
8
+ *
9
+ * @see https://dhoulb.github.io/shelving/ui/tree/TreeLink/TreeLinkProps
10
+ */
11
+ export interface TreeLinkProps {
12
+ /** Reference to an element in the tree — a flat key (`"BooleanSchema"`, `"Store.get"`) or a canonical path (`"/schema/BooleanSchema"`). */
13
+ readonly name: string;
14
+ /** Visible label — defaults to `name` (typically a raw type expression like `"SlugSchemaOptions"`). */
15
+ readonly children?: ReactNode | undefined;
16
+ }
17
+
18
+ /**
19
+ * Inline `Code` token linking to a specific tree element, resolved by reference string — the link-styled counterpart of `TreeButton`.
20
+ *
21
+ * - Resolves `name` via `getTreeElement()` — by flat key or canonical path, falling back to the bare name for a single generic type (`Schema<T>` → `Schema`) — and links to the element's canonical `path`.
22
+ * - A hit becomes a `<Link>` wrapping a `<Code>` token; a miss (e.g. a builtin like `string` or a compound type like `T | null` that isn't an exact token) stays a plain `<Code>` so it still reads as code.
23
+ * - Designed for the `Type` column of the documentation Parameters / Returns / Throws / Types tables, where only exact-match type names should link.
24
+ *
25
+ * @kind component
26
+ * @param props The element reference `name` and optional `children` label.
27
+ * @returns A `<Code>` token, wrapped in a `<Link>` when the reference resolves.
28
+ * @example <TreeLink name="BooleanSchema" />
29
+ * @see https://dhoulb.github.io/shelving/ui/tree/TreeLink/TreeLink
30
+ */
31
+ export function TreeLink({ name, children }: TreeLinkProps): ReactElement {
32
+ const href = getTreeElement(useTreeMap(), name)?.props.path;
33
+ const code = <Code>{children ?? name}</Code>;
34
+ // A resolved reference links via its canonical `path`; an unresolved one stays a plain code token rather than an empty link.
35
+ return href ? <Link href={href}>{code}</Link> : code;
36
+ }
@@ -5,6 +5,7 @@ export * from "./TreeCard.js";
5
5
  export * from "./TreeCards.js";
6
6
  export * from "./TreeContext.js";
7
7
  export * from "./TreeIndexPage.js";
8
+ export * from "./TreeLink.js";
8
9
  export * from "./TreeMenu.js";
9
10
  export * from "./TreePage.js";
10
11
  export * from "./TreeRouter.js";
package/ui/tree/index.js CHANGED
@@ -5,6 +5,7 @@ export * from "./TreeCard.js";
5
5
  export * from "./TreeCards.js";
6
6
  export * from "./TreeContext.js";
7
7
  export * from "./TreeIndexPage.js";
8
+ export * from "./TreeLink.js";
8
9
  export * from "./TreeMenu.js";
9
10
  export * from "./TreePage.js";
10
11
  export * from "./TreeRouter.js";
package/ui/tree/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from "./TreeCard.js";
5
5
  export * from "./TreeCards.js";
6
6
  export * from "./TreeContext.js";
7
7
  export * from "./TreeIndexPage.js";
8
+ export * from "./TreeLink.js";
8
9
  export * from "./TreeMenu.js";
9
10
  export * from "./TreePage.js";
10
11
  export * from "./TreeRouter.js";
package/util/tree.d.ts CHANGED
@@ -127,10 +127,20 @@ export interface DocumentationElementProps extends TreeElementProps {
127
127
  readonly class?: string | undefined;
128
128
  /** Whether the property is read-only — a `readonly` field, or a getter with no matching setter. */
129
129
  readonly readonly?: boolean | undefined;
130
- /** Name of the class/interface this class/interface extends (e.g. `"AbstractStore"`). Raw string — resolved to a link at render time. */
130
+ /** Full type text of the class/interface this extends, including any generic arguments (e.g. `"AbstractStore<string>"`, `"Omit<StringSchemaOptions, 'value'>"`). Resolved to a link at render time, trimming generics to the bare name; builtins/wrappers that don't resolve stay as text. */
131
131
  readonly extends?: string | undefined;
132
- /** Names of the interfaces this class/interface implements (e.g. `["Serializable"]`). Raw strings — resolved to links at render time; builtins simply stay as text. */
132
+ /** Full type text of the interfaces this implements, including generic arguments (e.g. `["Serializable<string>"]`). Resolved to links at render time (generics trimmed for lookup); builtins simply stay as text. */
133
133
  readonly implements?: ImmutableArray<string> | undefined;
134
+ /**
135
+ * Type names referenced by a `type` alias's body (e.g. `["OtherType"]` for `type X = string | OtherType`).
136
+ * - Raw identifier strings — resolved to links at render time; the alias's own generic parameters and primitive keywords are excluded, and unresolved names stay as plain text.
137
+ */
138
+ readonly types?: ImmutableArray<string> | undefined;
139
+ /**
140
+ * Structured member list for an `interface` or object-literal `type` — each property's `name`, `type`, optionality, `default`, and `description`.
141
+ * - Reuses the `DocumentationParam` shape so an options-bag parameter can be flattened into its individual fields at render time.
142
+ */
143
+ readonly properties?: ImmutableArray<DocumentationParam> | undefined;
134
144
  }
135
145
  /**
136
146
  * Element representing a documented code symbol.
@@ -185,8 +195,9 @@ export interface SearchTreeOptions {
185
195
  * Search the descendants of a tree element and return the best-ranked matches.
186
196
  *
187
197
  * - Walks every descendant of `scope` (depth-first; `scope` itself is not a candidate), optionally narrowed by `options.filter`.
188
- * - Tokenises `query` with `getWords()` so quoted phrases match literally: `searchTree(root, '"hello world" foo')` scores the phrase `hello world` *and* the word `foo` independently, stacking their scores.
189
- * - Ranks each candidate (case-insensitive) per token, summing: `name` exact > `name` starts-with > `name` includes > `title` includes > `description` includes > `content` includes. A `name` match always outranks a content-only match.
198
+ * - Tokenises `query` with `getWords()` so quoted phrases match literally: `searchTree(root, '"hello world" foo')` matches the phrase `hello world` *and* the word `foo`.
199
+ * - Requires *every* token to match (AND, not OR): a candidate is only returned when each token matches at least one of its props. So `date util` returns only elements matching both `date` and `util`, not either alone.
200
+ * - Ranks each candidate (case-insensitive) per token, summing each token's best tier: `name` exact > `name` starts-with > `name` includes > `title` includes > `description` includes > `content` includes. A `name` match always outranks a content-only match.
190
201
  * - An empty `query` returns the (filtered) candidates in tree order — useful for a filter-only or "show everything" listing.
191
202
  *
192
203
  * @param scope The element whose descendants are searched.
package/util/tree.js CHANGED
@@ -47,8 +47,9 @@ function _flatKey(element) {
47
47
  * Search the descendants of a tree element and return the best-ranked matches.
48
48
  *
49
49
  * - Walks every descendant of `scope` (depth-first; `scope` itself is not a candidate), optionally narrowed by `options.filter`.
50
- * - Tokenises `query` with `getWords()` so quoted phrases match literally: `searchTree(root, '"hello world" foo')` scores the phrase `hello world` *and* the word `foo` independently, stacking their scores.
51
- * - Ranks each candidate (case-insensitive) per token, summing: `name` exact > `name` starts-with > `name` includes > `title` includes > `description` includes > `content` includes. A `name` match always outranks a content-only match.
50
+ * - Tokenises `query` with `getWords()` so quoted phrases match literally: `searchTree(root, '"hello world" foo')` matches the phrase `hello world` *and* the word `foo`.
51
+ * - Requires *every* token to match (AND, not OR): a candidate is only returned when each token matches at least one of its props. So `date util` returns only elements matching both `date` and `util`, not either alone.
52
+ * - Ranks each candidate (case-insensitive) per token, summing each token's best tier: `name` exact > `name` starts-with > `name` includes > `title` includes > `description` includes > `content` includes. A `name` match always outranks a content-only match.
52
53
  * - An empty `query` returns the (filtered) candidates in tree order — useful for a filter-only or "show everything" listing.
53
54
  *
54
55
  * @param scope The element whose descendants are searched.
@@ -91,7 +92,7 @@ const _SCORE_NAME_INCLUDES = 100;
91
92
  const _SCORE_TITLE = 10;
92
93
  const _SCORE_DESCRIPTION = 4;
93
94
  const _SCORE_CONTENT = 1;
94
- /** Score one element's props against the (already lower-cased) query tokens — each token contributes its best-matching tier, summed. */
95
+ /** Score one element's props against the (already lower-cased) query tokens — every token must match (AND), each contributing its best-matching tier, summed. */
95
96
  function _scoreElement(props, tokens) {
96
97
  const name = props.name.toLowerCase();
97
98
  const title = props.title?.toLowerCase() ?? "";
@@ -99,6 +100,7 @@ function _scoreElement(props, tokens) {
99
100
  const content = props.content?.toLowerCase() ?? "";
100
101
  let score = 0;
101
102
  for (const token of tokens) {
103
+ // Every token must match somewhere — a single token that matches nothing drops the candidate entirely (AND, not OR).
102
104
  if (name === token)
103
105
  score += _SCORE_NAME_EXACT;
104
106
  else if (name.startsWith(token))
@@ -111,6 +113,8 @@ function _scoreElement(props, tokens) {
111
113
  score += _SCORE_DESCRIPTION;
112
114
  else if (content.includes(token))
113
115
  score += _SCORE_CONTENT;
116
+ else
117
+ return 0;
114
118
  }
115
119
  return score;
116
120
  }