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.
- package/extract/TypescriptExtractor.d.ts +3 -1
- package/extract/TypescriptExtractor.js +84 -7
- package/package.json +1 -1
- package/ui/docs/DocumentationPage.d.ts +6 -3
- package/ui/docs/DocumentationPage.js +18 -3
- package/ui/docs/DocumentationPage.tsx +74 -24
- package/ui/tree/TreeButton.d.ts +2 -2
- package/ui/tree/TreeButton.js +4 -4
- package/ui/tree/TreeButton.test.tsx +11 -0
- package/ui/tree/TreeButton.tsx +4 -4
- package/ui/tree/TreeContext.d.ts +14 -0
- package/ui/tree/TreeContext.js +20 -0
- package/ui/tree/TreeContext.tsx +20 -0
- package/ui/tree/TreeLink.d.ts +26 -0
- package/ui/tree/TreeLink.js +23 -0
- package/ui/tree/TreeLink.tsx +36 -0
- package/ui/tree/index.d.ts +1 -0
- package/ui/tree/index.js +1 -0
- package/ui/tree/index.ts +1 -0
- package/util/tree.d.ts +15 -4
- package/util/tree.js +7 -3
|
@@ -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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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,8 +1,11 @@
|
|
|
1
|
-
import type
|
|
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
|
-
|
|
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
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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>{
|
|
234
|
+
<td>{_getRowDescription(map, type)}</td>
|
|
185
235
|
</tr>
|
|
186
236
|
))}
|
|
187
237
|
</tbody>
|
package/ui/tree/TreeButton.d.ts
CHANGED
|
@@ -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
|
-
* -
|
|
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.
|
package/ui/tree/TreeButton.js
CHANGED
|
@@ -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
|
-
* -
|
|
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()
|
|
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
|
});
|
package/ui/tree/TreeButton.tsx
CHANGED
|
@@ -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
|
-
* -
|
|
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()
|
|
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 (
|
package/ui/tree/TreeContext.d.ts
CHANGED
|
@@ -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;
|
package/ui/tree/TreeContext.js
CHANGED
|
@@ -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
|
+
}
|
package/ui/tree/TreeContext.tsx
CHANGED
|
@@ -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
|
+
}
|
package/ui/tree/index.d.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/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
|
-
/**
|
|
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
|
-
/**
|
|
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')`
|
|
189
|
-
* -
|
|
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')`
|
|
51
|
-
* -
|
|
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 —
|
|
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
|
}
|