shelving 1.246.1 → 1.248.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.
Files changed (39) hide show
  1. package/extract/TypescriptExtractor.d.ts +3 -1
  2. package/extract/TypescriptExtractor.js +84 -7
  3. package/package.json +1 -1
  4. package/ui/block/Table.md +2 -0
  5. package/ui/block/TableCell.d.ts +23 -0
  6. package/ui/block/TableCell.js +18 -0
  7. package/ui/block/TableCell.md +35 -0
  8. package/ui/block/TableCell.tsx +27 -0
  9. package/ui/block/TableHeader.d.ts +23 -0
  10. package/ui/block/TableHeader.js +18 -0
  11. package/ui/block/TableHeader.md +43 -0
  12. package/ui/block/TableHeader.tsx +27 -0
  13. package/ui/block/index.d.ts +2 -0
  14. package/ui/block/index.js +2 -0
  15. package/ui/block/index.ts +2 -0
  16. package/ui/docs/DocumentationPage.d.ts +6 -3
  17. package/ui/docs/DocumentationPage.js +20 -3
  18. package/ui/docs/DocumentationPage.md +1 -1
  19. package/ui/docs/DocumentationPage.test.tsx +1 -1
  20. package/ui/docs/DocumentationPage.tsx +95 -33
  21. package/ui/style/Width.d.ts +18 -11
  22. package/ui/style/Width.js +9 -6
  23. package/ui/style/Width.module.css +107 -11
  24. package/ui/style/Width.tsx +53 -12
  25. package/ui/style/getWidthClass.md +11 -2
  26. package/ui/tree/TreeButton.d.ts +2 -2
  27. package/ui/tree/TreeButton.js +4 -4
  28. package/ui/tree/TreeButton.test.tsx +11 -0
  29. package/ui/tree/TreeButton.tsx +4 -4
  30. package/ui/tree/TreeContext.d.ts +14 -0
  31. package/ui/tree/TreeContext.js +20 -0
  32. package/ui/tree/TreeContext.tsx +20 -0
  33. package/ui/tree/TreeLink.d.ts +26 -0
  34. package/ui/tree/TreeLink.js +23 -0
  35. package/ui/tree/TreeLink.tsx +36 -0
  36. package/ui/tree/index.d.ts +1 -0
  37. package/ui/tree/index.js +1 -0
  38. package/ui/tree/index.ts +1 -0
  39. package/util/tree.d.ts +12 -2
@@ -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.1",
3
+ "version": "1.248.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
package/ui/block/Table.md CHANGED
@@ -9,6 +9,7 @@ A data table — renders a `<table>`. Compose the usual `<thead>` / `<tbody>` /
9
9
  - Wrap a wide table in a horizontally scrollable container if it may exceed the content width on small screens.
10
10
  - Like the other block components it collapses its outer block margin when it is the first or last child.
11
11
  - Spans the full width of its container by default; set the `width` variant (`narrow` / `normal` / `wide` / `full` / `fit`) to constrain it.
12
+ - Size columns with [`<TableHeader>`](/ui/TableHeader) / [`<TableCell>`](/ui/TableCell) and the `width` variant — `width="fit"` hugs the content, and `width="12x" grow` gives a column a hard minimum it can grow past (cells honour `min-width`, so the table scrolls rather than collapsing the column on a narrow screen). A column's width is the widest of its cells.
12
13
  - Inside [`Prose`](/ui/Prose) a raw `<table>` picks up the same styling, so Markdown-rendered tables match component ones.
13
14
 
14
15
  ## Usage
@@ -50,6 +51,7 @@ import { Table } from "shelving/ui";
50
51
 
51
52
  ## See also
52
53
 
54
+ - [`TableHeader`](/ui/TableHeader) / [`TableCell`](/ui/TableCell) — `<th>` / `<td>` cells that set column widths and typography via variants.
53
55
  - [`Definitions`](/ui/Definitions) — term/value pairs when a two-column key/value layout fits better than a grid.
54
56
  - [`Prose`](/ui/Prose) — styles raw `<table>` inside longform content.
55
57
  - [`ui`](/ui) — the styling system: tint ladder, label tokens, and theming.
@@ -0,0 +1,23 @@
1
+ import type { ReactElement } from "react";
2
+ import { type TypographyVariants } from "../style/Typography.js";
3
+ import { type WidthVariants } from "../style/Width.js";
4
+ import type { ChildProps } from "../util/props.js";
5
+ /**
6
+ * Props for `TableCell` — width and typography variants plus `children`.
7
+ *
8
+ * @see https://dhoulb.github.io/shelving/ui/block/TableCell/TableCellProps
9
+ */
10
+ export interface TableCellProps extends WidthVariants, TypographyVariants, ChildProps {
11
+ }
12
+ /**
13
+ * Table body cell — rendered as `<td>`.
14
+ * - Sets its column's width via the `width` variant. Unlike a `<col>`, a cell honours `min-width`, so `width="12x" grow` gives the column a hard minimum that fills the remaining space and keeps the table from collapsing the column on a narrow viewport.
15
+ * - A column's width is the widest of its cells, so use this to size a column that has no header to set the width on.
16
+ *
17
+ * @kind component
18
+ * @param props Width and typography variant props plus `children`.
19
+ * @returns Rendered `<td>` element.
20
+ * @example <TableCell width="12x" grow>A longer description that wants a sensible minimum width.</TableCell>
21
+ * @see https://dhoulb.github.io/shelving/ui/block/TableCell/TableCell
22
+ */
23
+ export declare function TableCell({ children, ...props }: TableCellProps): ReactElement;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getTypographyClass } from "../style/Typography.js";
3
+ import { getWidthClass } from "../style/Width.js";
4
+ import { getClass } from "../util/css.js";
5
+ /**
6
+ * Table body cell — rendered as `<td>`.
7
+ * - Sets its column's width via the `width` variant. Unlike a `<col>`, a cell honours `min-width`, so `width="12x" grow` gives the column a hard minimum that fills the remaining space and keeps the table from collapsing the column on a narrow viewport.
8
+ * - A column's width is the widest of its cells, so use this to size a column that has no header to set the width on.
9
+ *
10
+ * @kind component
11
+ * @param props Width and typography variant props plus `children`.
12
+ * @returns Rendered `<td>` element.
13
+ * @example <TableCell width="12x" grow>A longer description that wants a sensible minimum width.</TableCell>
14
+ * @see https://dhoulb.github.io/shelving/ui/block/TableCell/TableCell
15
+ */
16
+ export function TableCell({ children, ...props }) {
17
+ return _jsx("td", { className: getClass(getWidthClass(props), getTypographyClass(props)) || undefined, children: children });
18
+ }
@@ -0,0 +1,35 @@
1
+ # TableCell
2
+
3
+ A table body cell — renders a `<td>`. Use it for the body rows of a [`Table`](/ui/Table); it sets the column's width and cell typography.
4
+
5
+ **Things to know:**
6
+
7
+ - Set the column's width with the `width` variant. Because a cell (unlike a `<col>`) honours `min-width`, `width="12x" grow` gives the column a *hard* minimum that fills the remaining space and keeps the table from collapsing the column on a narrow screen — the table scrolls instead. See [`getWidthClass`](/ui/getWidthClass).
8
+ - A column's width is the widest of its cells, so this is how you size a column that has no header to set the width on (e.g. a trailing description column).
9
+ - Carries the same [typography variants](/ui/getTypographyClass) as the prose components (`size`, `weight`, `font`, `tint`, alignment).
10
+ - For a column that does have a header, prefer setting the width once on its [`TableHeader`](/ui/TableHeader).
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Table, TableCell } from "shelving/ui";
16
+
17
+ <Table>
18
+ <tbody>
19
+ <tr>
20
+ <TableCell>name</TableCell>
21
+ <TableCell width="12x" grow>A longer description that should keep a sensible minimum width.</TableCell>
22
+ </tr>
23
+ </tbody>
24
+ </Table>
25
+ ```
26
+
27
+ ## Styling
28
+
29
+ `TableCell` paints nothing of its own — it composes the [`width`](/ui/getWidthClass) and [`typography`](/ui/getTypographyClass) variants onto a `<td>`, and inherits the cell styling (borders, padding) from the surrounding [`Table`](/ui/Table). It exposes no hooks of its own.
30
+
31
+ ## See also
32
+
33
+ - [`TableHeader`](/ui/TableHeader) — the `<th>` equivalent for header cells.
34
+ - [`Table`](/ui/Table) — the table the cells belong to.
35
+ - [`getWidthClass`](/ui/getWidthClass) — the full `width` / `grow` variant reference.
@@ -0,0 +1,27 @@
1
+ import type { ReactElement } from "react";
2
+ import { getTypographyClass, type TypographyVariants } from "../style/Typography.js";
3
+ import { getWidthClass, type WidthVariants } from "../style/Width.js";
4
+ import { getClass } from "../util/css.js";
5
+ import type { ChildProps } from "../util/props.js";
6
+
7
+ /**
8
+ * Props for `TableCell` — width and typography variants plus `children`.
9
+ *
10
+ * @see https://dhoulb.github.io/shelving/ui/block/TableCell/TableCellProps
11
+ */
12
+ export interface TableCellProps extends WidthVariants, TypographyVariants, ChildProps {}
13
+
14
+ /**
15
+ * Table body cell — rendered as `<td>`.
16
+ * - Sets its column's width via the `width` variant. Unlike a `<col>`, a cell honours `min-width`, so `width="12x" grow` gives the column a hard minimum that fills the remaining space and keeps the table from collapsing the column on a narrow viewport.
17
+ * - A column's width is the widest of its cells, so use this to size a column that has no header to set the width on.
18
+ *
19
+ * @kind component
20
+ * @param props Width and typography variant props plus `children`.
21
+ * @returns Rendered `<td>` element.
22
+ * @example <TableCell width="12x" grow>A longer description that wants a sensible minimum width.</TableCell>
23
+ * @see https://dhoulb.github.io/shelving/ui/block/TableCell/TableCell
24
+ */
25
+ export function TableCell({ children, ...props }: TableCellProps): ReactElement {
26
+ return <td className={getClass(getWidthClass(props), getTypographyClass(props)) || undefined}>{children}</td>;
27
+ }
@@ -0,0 +1,23 @@
1
+ import type { ReactElement } from "react";
2
+ import { type TypographyVariants } from "../style/Typography.js";
3
+ import { type WidthVariants } from "../style/Width.js";
4
+ import type { ChildProps } from "../util/props.js";
5
+ /**
6
+ * Props for `TableHeader` — width and typography variants plus `children`.
7
+ *
8
+ * @see https://dhoulb.github.io/shelving/ui/block/TableHeader/TableHeaderProps
9
+ */
10
+ export interface TableHeaderProps extends WidthVariants, TypographyVariants, ChildProps {
11
+ }
12
+ /**
13
+ * Table header cell — rendered as `<th>`.
14
+ * - Sets its column's width via the `width` variant (e.g. `width="fit"` to hug the content). Unlike a `<col>`, a cell honours `min-width`, so `width="12x" grow` gives the column a hard minimum it can grow past.
15
+ * - A column's width is the widest of its cells, so sizing the `<th>` sizes the whole column.
16
+ *
17
+ * @kind component
18
+ * @param props Width and typography variant props plus `children`.
19
+ * @returns Rendered `<th>` element.
20
+ * @example <TableHeader width="fit">Parameter</TableHeader>
21
+ * @see https://dhoulb.github.io/shelving/ui/block/TableHeader/TableHeader
22
+ */
23
+ export declare function TableHeader({ children, ...props }: TableHeaderProps): ReactElement;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getTypographyClass } from "../style/Typography.js";
3
+ import { getWidthClass } from "../style/Width.js";
4
+ import { getClass } from "../util/css.js";
5
+ /**
6
+ * Table header cell — rendered as `<th>`.
7
+ * - Sets its column's width via the `width` variant (e.g. `width="fit"` to hug the content). Unlike a `<col>`, a cell honours `min-width`, so `width="12x" grow` gives the column a hard minimum it can grow past.
8
+ * - A column's width is the widest of its cells, so sizing the `<th>` sizes the whole column.
9
+ *
10
+ * @kind component
11
+ * @param props Width and typography variant props plus `children`.
12
+ * @returns Rendered `<th>` element.
13
+ * @example <TableHeader width="fit">Parameter</TableHeader>
14
+ * @see https://dhoulb.github.io/shelving/ui/block/TableHeader/TableHeader
15
+ */
16
+ export function TableHeader({ children, ...props }) {
17
+ return _jsx("th", { className: getClass(getWidthClass(props), getTypographyClass(props)) || undefined, children: children });
18
+ }
@@ -0,0 +1,43 @@
1
+ # TableHeader
2
+
3
+ A table header cell — renders a `<th>`. Use it for the header row of a [`Table`](/ui/Table); it sets the column's width and label typography.
4
+
5
+ **Things to know:**
6
+
7
+ - Set the column's width with the `width` variant — `width="fit"` hugs the content. Because a cell (unlike a `<col>`) honours `min-width`, `width="12x" grow` gives the column a *hard* minimum it can grow past, so the table scrolls rather than collapsing the column on a narrow screen. See [`getWidthClass`](/ui/getWidthClass).
8
+ - A column's width is the widest of its cells, so sizing the `<th>` sizes the whole column — set the width once on the header instead of on every body cell.
9
+ - Carries the same [typography variants](/ui/getTypographyClass) as the prose components (`size`, `weight`, `font`, `tint`, alignment), though the `Table` already styles header cells in the label style by default.
10
+ - For a column that has no header, set the width on its [`TableCell`](/ui/TableCell)s instead.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Table, TableCell, TableHeader } from "shelving/ui";
16
+
17
+ <Table>
18
+ <thead>
19
+ <tr>
20
+ <TableHeader width="fit">Parameter</TableHeader>
21
+ <TableHeader width="fit">Type</TableHeader>
22
+ <TableHeader width="12x" grow>Description</TableHeader>
23
+ </tr>
24
+ </thead>
25
+ <tbody>
26
+ <tr>
27
+ <TableCell>name</TableCell>
28
+ <TableCell>string</TableCell>
29
+ <TableCell>The name of the thing.</TableCell>
30
+ </tr>
31
+ </tbody>
32
+ </Table>
33
+ ```
34
+
35
+ ## Styling
36
+
37
+ `TableHeader` paints nothing of its own — it composes the [`width`](/ui/getWidthClass) and [`typography`](/ui/getTypographyClass) variants onto a `<th>`, and inherits the header styling (label font, borders, padding) from the surrounding [`Table`](/ui/Table). It exposes no hooks of its own.
38
+
39
+ ## See also
40
+
41
+ - [`TableCell`](/ui/TableCell) — the `<td>` equivalent for body cells.
42
+ - [`Table`](/ui/Table) — the table the cells belong to.
43
+ - [`getWidthClass`](/ui/getWidthClass) — the full `width` / `grow` variant reference.
@@ -0,0 +1,27 @@
1
+ import type { ReactElement } from "react";
2
+ import { getTypographyClass, type TypographyVariants } from "../style/Typography.js";
3
+ import { getWidthClass, type WidthVariants } from "../style/Width.js";
4
+ import { getClass } from "../util/css.js";
5
+ import type { ChildProps } from "../util/props.js";
6
+
7
+ /**
8
+ * Props for `TableHeader` — width and typography variants plus `children`.
9
+ *
10
+ * @see https://dhoulb.github.io/shelving/ui/block/TableHeader/TableHeaderProps
11
+ */
12
+ export interface TableHeaderProps extends WidthVariants, TypographyVariants, ChildProps {}
13
+
14
+ /**
15
+ * Table header cell — rendered as `<th>`.
16
+ * - Sets its column's width via the `width` variant (e.g. `width="fit"` to hug the content). Unlike a `<col>`, a cell honours `min-width`, so `width="12x" grow` gives the column a hard minimum it can grow past.
17
+ * - A column's width is the widest of its cells, so sizing the `<th>` sizes the whole column.
18
+ *
19
+ * @kind component
20
+ * @param props Width and typography variant props plus `children`.
21
+ * @returns Rendered `<th>` element.
22
+ * @example <TableHeader width="fit">Parameter</TableHeader>
23
+ * @see https://dhoulb.github.io/shelving/ui/block/TableHeader/TableHeader
24
+ */
25
+ export function TableHeader({ children, ...props }: TableHeaderProps): ReactElement {
26
+ return <th className={getClass(getWidthClass(props), getTypographyClass(props)) || undefined}>{children}</th>;
27
+ }
@@ -14,5 +14,7 @@ export * from "./Preformatted.js";
14
14
  export * from "./Prose.js";
15
15
  export * from "./Subheading.js";
16
16
  export * from "./Table.js";
17
+ export * from "./TableCell.js";
18
+ export * from "./TableHeader.js";
17
19
  export * from "./Title.js";
18
20
  export * from "./Video.js";
package/ui/block/index.js CHANGED
@@ -14,5 +14,7 @@ export * from "./Preformatted.js";
14
14
  export * from "./Prose.js";
15
15
  export * from "./Subheading.js";
16
16
  export * from "./Table.js";
17
+ export * from "./TableCell.js";
18
+ export * from "./TableHeader.js";
17
19
  export * from "./Title.js";
18
20
  export * from "./Video.js";
package/ui/block/index.ts CHANGED
@@ -14,5 +14,7 @@ export * from "./Preformatted.js";
14
14
  export * from "./Prose.js";
15
15
  export * from "./Subheading.js";
16
16
  export * from "./Table.js";
17
+ export * from "./TableCell.js";
18
+ export * from "./TableHeader.js";
17
19
  export * from "./Title.js";
18
20
  export * from "./Video.js";
@@ -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";
@@ -7,6 +8,8 @@ import { Preformatted } from "../block/Preformatted.js";
7
8
  import { Prose } from "../block/Prose.js";
8
9
  import { Header, Section } from "../block/Section.js";
9
10
  import { Table } from "../block/Table.js";
11
+ import { TableCell } from "../block/TableCell.js";
12
+ import { TableHeader } from "../block/TableHeader.js";
10
13
  import { Title } from "../block/Title.js";
11
14
  import { Code } from "../inline/Code.js";
12
15
  import { Markup } from "../misc/Markup.js";
@@ -15,10 +18,16 @@ import { Row } from "../style/Flex.js";
15
18
  import { Scroll } from "../style/Scroll.js";
16
19
  import { TreeBreadcrumbs } from "../tree/TreeBreadcrumbs.js";
17
20
  import { TreeCards } from "../tree/TreeCards.js";
21
+ import { getTreeElement, useTreeMap } from "../tree/TreeContext.js";
22
+ import { TreeLink } from "../tree/TreeLink.js";
18
23
  import { DocumentationButtons } from "./DocumentationButtons.js";
19
24
  import { DocumentationKind, getDocumentationKindColor } from "./DocumentationKind.js";
20
25
  import { DocumentationSignatures } from "./DocumentationSignatures.js";
21
26
  const DEFAULT_TYPE = "unknown";
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, type, description) {
29
+ return description || getTreeElement(map, type)?.props.description || "";
30
+ }
22
31
  /** Documentation `kind`s grouped into card sections, in display order — pluralised, sentence-case headings. */
23
32
  const KIND_SECTIONS = {
24
33
  component: "Components",
@@ -57,7 +66,10 @@ function DocumentationChildren({ elements }) {
57
66
  }
58
67
  /**
59
68
  * 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.
69
+ * - 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.
70
+ * - 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`.
71
+ * - 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.
72
+ * - 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
73
  * - Child symbols are grouped by `kind` into card sections (Functions, Classes, Methods, Properties, …), each under its own heading.
62
74
  * - All sections are conditional — only render when they have entries.
63
75
  *
@@ -67,6 +79,11 @@ function DocumentationChildren({ elements }) {
67
79
  * @example <DocumentationPage {...element.props} />
68
80
  * @see https://dhoulb.github.io/shelving/ui/docs/DocumentationPage/DocumentationPage
69
81
  */
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 })] }) }));
82
+ export function DocumentationPage({ title, name, kind = "unknown", description, content, signatures, params, returns, throws, types, examples, children, ...props }) {
83
+ const map = useTreeMap();
84
+ 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(TableHeader, { width: "fit", children: "Param" }), _jsx(TableHeader, { width: "fit", children: "Type" }), _jsx(TableHeader, { width: "fit", children: "Default" })] }) }), _jsx("tbody", { children: params.map(({ name, type = DEFAULT_TYPE, description, default: def }) => {
85
+ // An options-bag param whose type resolves to a documented interface/object type is flattened into its individual fields as indented child rows.
86
+ const resolved = getTreeElement(map, type)?.props;
87
+ return (_jsxs(Fragment, { children: [_jsxs("tr", { children: [_jsx(TableCell, { children: _jsx(Code, { children: name }) }), _jsx(TableCell, { children: _jsx(TreeLink, { name: type }) }), _jsx(TableCell, { children: def ? _jsx(Code, { children: def }) : "-" }), _jsx(TableCell, { width: "20x", grow: true, children: description || resolved?.description || "" })] }), resolved?.properties?.map(prop => (_jsxs("tr", { children: [_jsx(TableCell, { children: _jsx(Code, { children: `.${prop.name}` }) }), _jsx(TableCell, { children: _jsx(TreeLink, { name: prop.type ?? DEFAULT_TYPE }) }), _jsx(TableCell, { children: prop.default ? _jsx(Code, { children: prop.default }) : "-" }), _jsx(TableCell, { width: "20x", grow: true, children: _getRowDescription(map, prop.type ?? DEFAULT_TYPE, prop.description) })] }, `${name}.${prop.name}`)))] }, `${name}-${type}`));
88
+ }) })] }) }) })), returns?.length && (_jsx(Section, { children: _jsx(Scroll, { horizontal: true, children: _jsxs(Table, { children: [_jsx("thead", { children: _jsx("tr", { children: _jsx(TableHeader, { width: "fit", children: "Return" }) }) }), _jsx("tbody", { children: returns.map(({ type = DEFAULT_TYPE, description }) => (_jsxs("tr", { children: [_jsx(TableCell, { children: _jsx(TreeLink, { name: type }) }), _jsx(TableCell, { width: "20x", grow: true, 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(TableHeader, { width: "fit", children: "Throws" }) }) }), _jsx("tbody", { children: throws.map(({ type = DEFAULT_TYPE, description }) => (_jsxs("tr", { children: [_jsx(TableCell, { children: _jsx(TreeLink, { name: type }) }), _jsx(TableCell, { width: "20x", grow: true, 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(TableHeader, { width: "fit", children: "Type" }) }) }), _jsx("tbody", { children: types.map(type => (_jsxs("tr", { children: [_jsx(TableCell, { children: _jsx(TreeLink, { name: type }) }), _jsx(TableCell, { width: "20x", grow: true, 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
89
  }
@@ -6,7 +6,7 @@ The full detail page for a documented symbol — the default renderer for `tree-
6
6
 
7
7
  - Above the title it renders an ancestor trail via [`TreeBreadcrumbs`](/ui/TreeBreadcrumbs), so it needs a [`TreeProvider`](/ui/TreeProvider) above it to resolve the ancestors.
8
8
  - The title carries a [`DocumentationKind`](/ui/DocumentationKind) tag, and below it [`DocumentationButtons`](/ui/DocumentationButtons) lists the symbol's relations (`member of`, `extends`, `implements`) as links.
9
- - It renders the signature(s) via [`DocumentationSignatures`](/ui/DocumentationSignatures) (one block per overload, each carrying the symbol's name), then prose content, then conditional `Parameters` / `Returns` / `Throws` / `Examples` sections — each only appears when it has entries. Parameters render as a [`Table`](/ui/Table) (`Parameter` / `Type` / `Default` / `Description` columns, a `-` standing in where a parameter has no default); Returns and Throws render as two-column tables (`Return` / `Throws` plus `Description`), with the type in the first column.
9
+ - It renders the signature(s) via [`DocumentationSignatures`](/ui/DocumentationSignatures) (one block per overload, each carrying the symbol's name), then prose content, then conditional `Parameters` / `Returns` / `Throws` / `Examples` sections — each only appears when it has entries. Parameters render as a [`Table`](/ui/Table) (`Param` / `Type` / `Default` / `Description` columns, a `-` standing in where a parameter has no default); Returns and Throws render as two-column tables (`Return` / `Throws` plus `Description`), with the type in the first column.
10
10
  - Child symbols follow, grouped by `kind` into card sections (`Components`, `Functions`, `Classes`, `Interfaces`, `Types`, `Constants`, `Methods`, `Properties`) rendered as [`DocumentationCard`](/ui/DocumentationCard)s inside a [`TreeCards`](/ui/TreeCards) listing. A new documented kind needs an entry in `KIND_SECTIONS` here (and a colour in [`DocumentationKind`](/ui/DocumentationKind)).
11
11
 
12
12
  ## Usage
@@ -57,7 +57,7 @@ describe("DocumentationPage", () => {
57
57
  "./makeThing",
58
58
  );
59
59
  // Parameters table headers — name, type, default, description in separate columns.
60
- expect(html).toContain("<th>Parameter</th>");
60
+ expect(html).toContain("<th>Param</th>");
61
61
  expect(html).toContain("<th>Type</th>");
62
62
  expect(html).toContain("<th>Default</th>");
63
63
  // A param with a default renders it; one without gets a hyphen.