shelving 1.253.0 → 1.253.2

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 (42) hide show
  1. package/extract/TypescriptExtractor.d.ts +2 -2
  2. package/extract/TypescriptExtractor.js +53 -73
  3. package/package.json +1 -1
  4. package/ui/block/Card.module.css +2 -1
  5. package/ui/docs/DocumentationDescription.d.ts +8 -5
  6. package/ui/docs/DocumentationDescription.js +12 -10
  7. package/ui/docs/DocumentationDescription.tsx +26 -12
  8. package/ui/docs/DocumentationPage.d.ts +5 -5
  9. package/ui/docs/DocumentationPage.js +8 -9
  10. package/ui/docs/DocumentationPage.test.tsx +36 -10
  11. package/ui/docs/DocumentationPage.tsx +9 -8
  12. package/ui/docs/DocumentationParams.js +8 -22
  13. package/ui/docs/DocumentationParams.tsx +15 -29
  14. package/ui/docs/DocumentationProperties.d.ts +26 -0
  15. package/ui/docs/DocumentationProperties.js +32 -0
  16. package/ui/docs/DocumentationProperties.tsx +83 -0
  17. package/ui/docs/DocumentationType.d.ts +33 -0
  18. package/ui/docs/DocumentationType.js +32 -0
  19. package/ui/docs/DocumentationType.tsx +48 -0
  20. package/ui/docs/index.d.ts +2 -0
  21. package/ui/docs/index.js +2 -0
  22. package/ui/docs/index.ts +2 -0
  23. package/ui/layout/CenteredLayout.js +2 -3
  24. package/ui/layout/CenteredLayout.md +1 -1
  25. package/ui/layout/CenteredLayout.module.css +61 -1
  26. package/ui/layout/CenteredLayout.tsx +2 -3
  27. package/ui/layout/Layout.d.ts +0 -7
  28. package/ui/layout/Layout.js +0 -9
  29. package/ui/layout/Layout.ts +0 -11
  30. package/ui/layout/SidebarLayout.js +2 -3
  31. package/ui/layout/SidebarLayout.md +1 -1
  32. package/ui/layout/SidebarLayout.module.css +44 -5
  33. package/ui/layout/SidebarLayout.tsx +2 -3
  34. package/ui/misc/MetaContext.test.tsx +10 -0
  35. package/ui/misc/StatusIcon.md +10 -1
  36. package/ui/misc/StatusIcon.module.css +4 -0
  37. package/ui/misc/Tag.module.css +1 -1
  38. package/ui/router/Router.test.tsx +9 -0
  39. package/util/tree.d.ts +4 -2
  40. package/util/url.d.ts +3 -1
  41. package/util/url.js +4 -1
  42. package/ui/layout/Layout.module.css +0 -73
@@ -9,9 +9,9 @@ import { FileExtractor } from "./FileExtractor.js";
9
9
  * - Class declarations synthesise their `signatures`, `params`, and `returns` from the constructor — `new ClassName<…>(…)` including generics, one signature per constructor overload, with `returns` set to the class type. Param descriptions come from the constructor's `@param` first, then the class's `@param`.
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
- * - Sets `title` on every `tree-documentation` child — `name()` for functions, `Class.name()` for methods, `Class.name` for properties, bare `name` for other kinds.
12
+ * - Sets `title` on every `tree-documentation` child — `name()` for functions, `Class.name()` for methods, bare `name` for other kinds. (Data members are not child elements — see `properties` below.)
13
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.
14
+ * - Records a structured `properties` list for classes, interfaces, and object-literal `type` aliases — each data member's (property/getter/setter) name, type, optionality, default, and description. This is the single source for a type's data members: they render as the type's Properties table and flatten into an options-bag parameter's fields at render time, rather than each becoming its own child element. Methods stay as child elements.
15
15
  * - Names a destructured (binding-pattern) parameter for the Param column — which has no name of its own — from an explicit `@param`, else its rest element (`...options`), else a type-derived fallback (`props` / `options`). The signature still shows the full `{ … }`.
16
16
  * - 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.
17
17
  * - 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.
@@ -10,9 +10,9 @@ import { extractMarkdownProps } from "./MarkupExtractor.js";
10
10
  * - Class declarations synthesise their `signatures`, `params`, and `returns` from the constructor — `new ClassName<…>(…)` including generics, one signature per constructor overload, with `returns` set to the class type. Param descriptions come from the constructor's `@param` first, then the class's `@param`.
11
11
  * - 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.
12
12
  * - Sets `description` (a plain-text summary from the first JSDoc paragraph) on every `tree-documentation` child.
13
- * - Sets `title` on every `tree-documentation` child — `name()` for functions, `Class.name()` for methods, `Class.name` for properties, bare `name` for other kinds.
13
+ * - Sets `title` on every `tree-documentation` child — `name()` for functions, `Class.name()` for methods, bare `name` for other kinds. (Data members are not child elements — see `properties` below.)
14
14
  * - 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`).
15
- * - 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
+ * - Records a structured `properties` list for classes, interfaces, and object-literal `type` aliases — each data member's (property/getter/setter) name, type, optionality, default, and description. This is the single source for a type's data members: they render as the type's Properties table and flatten into an options-bag parameter's fields at render time, rather than each becoming its own child element. Methods stay as child elements.
16
16
  * - Names a destructured (binding-pattern) parameter for the Param column — which has no name of its own — from an explicit `@param`, else its rest element (`...options`), else a type-derived fallback (`props` / `options`). The signature still shows the full `{ … }`.
17
17
  * - 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.
18
18
  * - 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.
@@ -199,12 +199,14 @@ function _getReferencedTypes(statement, source) {
199
199
  return names.length ? names : undefined;
200
200
  }
201
201
  /**
202
- * Extract structured property entries from an interface or object-literal `type` alias, mirroring the `DocumentationParam` shape.
203
- * - 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).
204
- * - Skips private `_`-prefixed members. Descriptions and `@default` values come from each member's own JSDoc.
202
+ * Extract structured property entries from a class, interface, or object-literal `type` alias, mirroring the `DocumentationParam` shape.
203
+ * - This is the single source of truth for a type's data members: properties (and getters/setters) are documented here as a structured list, not as their own child elements. Methods stay as child elements (see `_getClassMembers`).
204
+ * - Used both to render the type's own Properties table and to flatten an options-bag parameter into its individual fields at render time (resolve the param's type in the tree map, then list these).
205
+ * - Skips private/protected, `_`-prefixed, `override`, and `declare` members. Optionality comes from a `?` token; defaults from a class-field initializer or a member's `@default` JSDoc; descriptions from each member's own JSDoc.
206
+ * - Getters/setters fold into a single entry per name (the getter's type wins for a get/set pair).
205
207
  */
206
208
  function _getProperties(statement, source) {
207
- const members = ts.isInterfaceDeclaration(statement)
209
+ const members = ts.isInterfaceDeclaration(statement) || ts.isClassDeclaration(statement)
208
210
  ? statement.members
209
211
  : ts.isTypeAliasDeclaration(statement) && ts.isTypeLiteralNode(statement.type)
210
212
  ? statement.type.members
@@ -213,19 +215,48 @@ function _getProperties(statement, source) {
213
215
  return;
214
216
  const properties = [];
215
217
  for (const member of members) {
216
- if (!ts.isPropertySignature(member))
217
- continue;
218
218
  const name = member.name && ts.isIdentifier(member.name) ? member.name.text : undefined;
219
219
  if (!name || name.startsWith("_"))
220
220
  continue;
221
+ const modifiers = ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined;
222
+ // Skip private/protected (not public API), `override` (documented on the base class), and `declare` (ambient re-declarations).
223
+ if (modifiers?.some(m => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword))
224
+ continue;
225
+ if (modifiers?.some(m => m.kind === ts.SyntaxKind.OverrideKeyword || m.kind === ts.SyntaxKind.DeclareKeyword))
226
+ continue;
221
227
  const jsDoc = _getJSDoc(member, source);
222
- properties.push({
223
- name,
224
- type: member.type?.getText(source),
225
- description: jsDoc?.description,
226
- optional: !!member.questionToken,
227
- default: jsDoc?.default,
228
- });
228
+ if (ts.isPropertySignature(member) || ts.isPropertyDeclaration(member)) {
229
+ // A class field's initializer is its default; otherwise fall back to an explicit `@default` tag.
230
+ const def = (ts.isPropertyDeclaration(member) ? member.initializer?.getText(source) : undefined) ?? jsDoc?.default;
231
+ properties.push({
232
+ name,
233
+ type: member.type?.getText(source),
234
+ description: jsDoc?.description,
235
+ optional: !!member.questionToken,
236
+ default: def,
237
+ readonly: modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) || undefined,
238
+ });
239
+ }
240
+ else if (ts.isGetAccessor(member) || ts.isSetAccessor(member)) {
241
+ const type = ts.isGetAccessor(member) ? member.type?.getText(source) : member.parameters[0]?.type?.getText(source);
242
+ const index = properties.findIndex(p => p.name === name);
243
+ const found = index >= 0 ? properties[index] : undefined;
244
+ // Fold a get/set pair into one entry: the getter's declared type wins, and the matching setter clears read-only.
245
+ if (found) {
246
+ properties[index] = { ...found, type: ts.isGetAccessor(member) && type ? type : found.type, readonly: undefined };
247
+ }
248
+ else {
249
+ // A lone getter is read-only until a setter is seen; a lone setter is writable.
250
+ properties.push({
251
+ name,
252
+ type,
253
+ description: jsDoc?.description,
254
+ optional: false,
255
+ default: jsDoc?.default,
256
+ readonly: ts.isGetAccessor(member) || undefined,
257
+ });
258
+ }
259
+ }
229
260
  }
230
261
  return properties.length ? properties : undefined;
231
262
  }
@@ -434,12 +465,12 @@ function _getReturns(statement, source, jsDocReturns, name) {
434
465
  return [{ type }];
435
466
  }
436
467
  /**
437
- * Extract class or interface members as child elements.
438
- * - `className` is stamped onto every member as its `class` prop (the source of the qualified flat key and the "member of …" affordance) and is prebaked into the member `title` (`Class.name` / `Class.name()`).
468
+ * Extract a class or interface's **methods** as child elements.
469
+ * - Data members (properties, getters/setters) are NOT child elements they're collected as a structured list by [`_getProperties`](#) and rendered as the type's Properties table instead. Only methods, which carry their own params/returns/throws docs, warrant a dedicated page.
470
+ * - `className` is stamped onto every method as its `class` prop (the source of the qualified flat key and the "member of …" affordance) and is prebaked into the member `title` (`Class.name()`).
439
471
  * - Members declared with the `override` modifier are skipped — the base class already documents them, so a subclass page lists only its newly-introduced API.
440
- * - Members declared with the `declare` modifier are skipped — they're ambient type-only re-declarations (e.g. narrowing an inherited property's type), not new API.
441
- * - Getters/setters fold into a single `property` element per name; a getter with no matching setter is `readonly`.
442
- * - `static` members are labelled `static method` / `static property` so the docs site groups them in their own sections, separate from instance `method` / `property`, and their rendered signature is prefixed with the `static ` keyword.
472
+ * - Members declared with the `declare` modifier are skipped — they're ambient type-only re-declarations, not new API.
473
+ * - `static` methods are labelled `static method` so the docs site groups them in their own section, separate from instance `method`s, and their rendered signature is prefixed with the `static ` keyword.
443
474
  */
444
475
  function _getClassMembers(statement, source, className) {
445
476
  if (!ts.isClassDeclaration(statement) && !ts.isInterfaceDeclaration(statement))
@@ -459,7 +490,7 @@ function _getClassMembers(statement, source, className) {
459
490
  // Skip `declare` members — ambient type-only re-declarations (e.g. a subclass narrowing an inherited property's type), not new API.
460
491
  if (modifiers?.some(m => m.kind === ts.SyntaxKind.DeclareKeyword))
461
492
  continue;
462
- // `static` members are grouped and labelled separately from instance members (`static method` / `static property`),
493
+ // `static` methods are grouped and labelled separately from instance methods (`static method`),
463
494
  // and carry the `static ` keyword in their rendered signature.
464
495
  const isStatic = modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword);
465
496
  const staticPrefix = isStatic ? "static " : "";
@@ -495,58 +526,7 @@ function _getClassMembers(statement, source, className) {
495
526
  });
496
527
  }
497
528
  }
498
- else if (ts.isPropertyDeclaration(member) || ts.isPropertySignature(member)) {
499
- const type = member.type?.getText(source);
500
- const readonly = modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) || undefined;
501
- members.push({
502
- type: "tree-documentation",
503
- key: name,
504
- props: {
505
- name,
506
- title: `${className}.${name}`,
507
- description,
508
- content,
509
- kind: isStatic ? "static property" : "property",
510
- class: className,
511
- readonly,
512
- signatures: type ? [`${staticPrefix}${readonly ? "readonly " : ""}${name}: ${type}`] : undefined,
513
- },
514
- });
515
- }
516
- else if (ts.isGetAccessor(member) || ts.isSetAccessor(member)) {
517
- // Getters/setters fold into one `property`. A getter alone (no setter) is read-only; a setter clears that.
518
- const type = ts.isGetAccessor(member) ? member.type?.getText(source) : member.parameters[0]?.type?.getText(source);
519
- const key = name;
520
- const existingIndex = members.findIndex(m => m.key === key);
521
- const existing = members[existingIndex];
522
- if (existing) {
523
- members[existingIndex] = {
524
- ...existing,
525
- props: {
526
- ...existing.props,
527
- // A getter + setter pair is writable — drop the read-only flag and the `readonly ` signature prefix.
528
- readonly: undefined,
529
- signatures: type ? [`${staticPrefix}${name}: ${type}`] : existing.props.signatures,
530
- },
531
- };
532
- }
533
- else {
534
- members.push({
535
- type: "tree-documentation",
536
- key,
537
- props: {
538
- name,
539
- title: `${className}.${name}`,
540
- description,
541
- content,
542
- kind: isStatic ? "static property" : "property",
543
- class: className,
544
- readonly: ts.isGetAccessor(member) || undefined,
545
- signatures: type ? [`${staticPrefix}${ts.isGetAccessor(member) ? "readonly " : ""}${name}: ${type}`] : undefined,
546
- },
547
- });
548
- }
549
- }
529
+ // Properties, getters, and setters are not child elements — `_getProperties` collects them as a structured list instead.
550
530
  }
551
531
  return members.length ? members : undefined;
552
532
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.253.0",
3
+ "version": "1.253.2",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -68,7 +68,8 @@
68
68
 
69
69
  @layer overrides {
70
70
  .card {
71
- &:has(.overlay:not(focus-visible)) {
71
+ /* Show the focus ring only while the navigable overlay is keyboard-focused; hide it otherwise (including non-clickable cards, which have no overlay). */
72
+ &:not(:has(.overlay:focus-visible)) {
72
73
  outline-color: transparent;
73
74
  }
74
75
  }
@@ -11,16 +11,19 @@ export interface DocumentationDescriptionProps {
11
11
  readonly default?: string | undefined;
12
12
  /** Whether the value is optional — pass an explicit boolean to opt into the note: `false` appends `Required.` (when there's no default), `true` appends nothing. Leave `undefined` to suppress the note entirely (e.g. for returns/throws). */
13
13
  readonly optional?: boolean | undefined;
14
+ /** Whether the value is read-only — appends a trailing `Readonly` note at the very end (after any `Defaults to …` / `Required.`). */
15
+ readonly readonly?: boolean | undefined;
14
16
  }
15
17
  /**
16
18
  * Render a documentation table row's description cell.
17
- * - The description is parsed as inline markup (`context="inline"`), so backticks, emphasis and links resolve rather than showing as literal source.
18
- * - When a `default` is given it renders a trailing `Defaults to …` note on the same line (linking the value when it's a documented token); otherwise, when `optional` is explicitly `false`, it renders `Required.` for clarity.
19
+ * - The description is parsed as inline markup via `TreeMarkup` (`context="inline"`), so backticks, emphasis and links resolve rather than showing as literal source — and each inline-code span auto-links to its documented token when one exists.
20
+ * - When a `default` is given it renders a trailing `Defaults to …` note on the same line (linking the value when it's a documented token); otherwise, when `optional` is explicitly `false`, it renders a `required` `<Tag>`. (Optionality has no tag — it's implicit, and an optional value usually carries the longer `Defaults to …` text instead.)
21
+ * - When `readonly` is set, a `readonly` `<Tag>` is appended at the very end (after any default/required note).
19
22
  * - Wrapped in `<Prose>` so markup-produced `<code>` (and other inline) elements pick up the standard prose typography (the chip background, etc.) — a bare `<code>` is only styled inside a `.prose` ancestor.
20
23
  *
21
24
  * @kind component
22
- * @returns The description and any trailing note wrapped in a `<Prose>`, or `null` when both are empty.
23
- * @example <DocumentationDescription description="The `foo` value." optional={false} />
25
+ * @returns The description and any trailing notes wrapped in a `<Prose>`, or `null` when all are empty.
26
+ * @example <DocumentationDescription description="The `foo` value." optional={false} readonly />
24
27
  * @see https://dhoulb.github.io/shelving/ui/docs/DocumentationDescription/DocumentationDescription
25
28
  */
26
- export declare function DocumentationDescription({ description, default: def, optional }: DocumentationDescriptionProps): ReactNode;
29
+ export declare function DocumentationDescription({ description, default: def, optional, readonly }: DocumentationDescriptionProps): ReactNode;
@@ -1,22 +1,24 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Prose } from "../block/Prose.js";
3
- import { Markup } from "../misc/Markup.js";
3
+ import { Tag } from "../misc/Tag.js";
4
4
  import { TreeLink } from "../tree/TreeLink.js";
5
+ import { TreeMarkup } from "../tree/TreeMarkup.js";
5
6
  /**
6
7
  * Render a documentation table row's description cell.
7
- * - The description is parsed as inline markup (`context="inline"`), so backticks, emphasis and links resolve rather than showing as literal source.
8
- * - When a `default` is given it renders a trailing `Defaults to …` note on the same line (linking the value when it's a documented token); otherwise, when `optional` is explicitly `false`, it renders `Required.` for clarity.
8
+ * - The description is parsed as inline markup via `TreeMarkup` (`context="inline"`), so backticks, emphasis and links resolve rather than showing as literal source — and each inline-code span auto-links to its documented token when one exists.
9
+ * - When a `default` is given it renders a trailing `Defaults to …` note on the same line (linking the value when it's a documented token); otherwise, when `optional` is explicitly `false`, it renders a `required` `<Tag>`. (Optionality has no tag — it's implicit, and an optional value usually carries the longer `Defaults to …` text instead.)
10
+ * - When `readonly` is set, a `readonly` `<Tag>` is appended at the very end (after any default/required note).
9
11
  * - Wrapped in `<Prose>` so markup-produced `<code>` (and other inline) elements pick up the standard prose typography (the chip background, etc.) — a bare `<code>` is only styled inside a `.prose` ancestor.
10
12
  *
11
13
  * @kind component
12
- * @returns The description and any trailing note wrapped in a `<Prose>`, or `null` when both are empty.
13
- * @example <DocumentationDescription description="The `foo` value." optional={false} />
14
+ * @returns The description and any trailing notes wrapped in a `<Prose>`, or `null` when all are empty.
15
+ * @example <DocumentationDescription description="The `foo` value." optional={false} readonly />
14
16
  * @see https://dhoulb.github.io/shelving/ui/docs/DocumentationDescription/DocumentationDescription
15
17
  */
16
- export function DocumentationDescription({ description, default: def, optional }) {
17
- const body = description ? _jsx(Markup, { context: "inline", children: description }) : null;
18
- const note = def ? (_jsxs(_Fragment, { children: ["Defaults to ", _jsx(TreeLink, { name: def })] })) : optional === false ? (_jsx(_Fragment, { children: "Required." })) : null;
19
- if (!body && !note)
18
+ export function DocumentationDescription({ description, default: def, optional, readonly }) {
19
+ const body = description ? _jsx(TreeMarkup, { context: "inline", children: description }) : null;
20
+ const requirement = def ? (_jsxs(_Fragment, { children: ["Defaults to ", _jsx(TreeLink, { name: def })] })) : optional === false ? (_jsx(Tag, { children: "required" })) : null;
21
+ if (!body && !requirement && !readonly)
20
22
  return null;
21
- return (_jsxs(Prose, { children: [body, body && note ? " " : null, note] }));
23
+ return (_jsxs(Prose, { children: [body, requirement ? (_jsxs(_Fragment, { children: [body ? " " : null, requirement] })) : null, readonly ? (_jsxs(_Fragment, { children: [body || requirement ? " " : null, _jsx(Tag, { children: "readonly" })] })) : null] }));
22
24
  }
@@ -1,7 +1,8 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { Prose } from "../block/Prose.js";
3
- import { Markup } from "../misc/Markup.js";
3
+ import { Tag } from "../misc/Tag.js";
4
4
  import { TreeLink } from "../tree/TreeLink.js";
5
+ import { TreeMarkup } from "../tree/TreeMarkup.js";
5
6
 
6
7
  /**
7
8
  * Props for `DocumentationDescription` — a description cell's resolved text, plus an optional default/optionality note.
@@ -15,34 +16,47 @@ export interface DocumentationDescriptionProps {
15
16
  readonly default?: string | undefined;
16
17
  /** Whether the value is optional — pass an explicit boolean to opt into the note: `false` appends `Required.` (when there's no default), `true` appends nothing. Leave `undefined` to suppress the note entirely (e.g. for returns/throws). */
17
18
  readonly optional?: boolean | undefined;
19
+ /** Whether the value is read-only — appends a trailing `Readonly` note at the very end (after any `Defaults to …` / `Required.`). */
20
+ readonly readonly?: boolean | undefined;
18
21
  }
19
22
 
20
23
  /**
21
24
  * Render a documentation table row's description cell.
22
- * - The description is parsed as inline markup (`context="inline"`), so backticks, emphasis and links resolve rather than showing as literal source.
23
- * - When a `default` is given it renders a trailing `Defaults to …` note on the same line (linking the value when it's a documented token); otherwise, when `optional` is explicitly `false`, it renders `Required.` for clarity.
25
+ * - The description is parsed as inline markup via `TreeMarkup` (`context="inline"`), so backticks, emphasis and links resolve rather than showing as literal source — and each inline-code span auto-links to its documented token when one exists.
26
+ * - When a `default` is given it renders a trailing `Defaults to …` note on the same line (linking the value when it's a documented token); otherwise, when `optional` is explicitly `false`, it renders a `required` `<Tag>`. (Optionality has no tag — it's implicit, and an optional value usually carries the longer `Defaults to …` text instead.)
27
+ * - When `readonly` is set, a `readonly` `<Tag>` is appended at the very end (after any default/required note).
24
28
  * - Wrapped in `<Prose>` so markup-produced `<code>` (and other inline) elements pick up the standard prose typography (the chip background, etc.) — a bare `<code>` is only styled inside a `.prose` ancestor.
25
29
  *
26
30
  * @kind component
27
- * @returns The description and any trailing note wrapped in a `<Prose>`, or `null` when both are empty.
28
- * @example <DocumentationDescription description="The `foo` value." optional={false} />
31
+ * @returns The description and any trailing notes wrapped in a `<Prose>`, or `null` when all are empty.
32
+ * @example <DocumentationDescription description="The `foo` value." optional={false} readonly />
29
33
  * @see https://dhoulb.github.io/shelving/ui/docs/DocumentationDescription/DocumentationDescription
30
34
  */
31
- export function DocumentationDescription({ description, default: def, optional }: DocumentationDescriptionProps): ReactNode {
32
- const body = description ? <Markup context="inline">{description}</Markup> : null;
33
- const note = def ? (
35
+ export function DocumentationDescription({ description, default: def, optional, readonly }: DocumentationDescriptionProps): ReactNode {
36
+ const body = description ? <TreeMarkup context="inline">{description}</TreeMarkup> : null;
37
+ const requirement = def ? (
34
38
  <>
35
39
  Defaults to <TreeLink name={def} />
36
40
  </>
37
41
  ) : optional === false ? (
38
- <>Required.</>
42
+ <Tag>required</Tag>
39
43
  ) : null;
40
- if (!body && !note) return null;
44
+ if (!body && !requirement && !readonly) return null;
41
45
  return (
42
46
  <Prose>
43
47
  {body}
44
- {body && note ? " " : null}
45
- {note}
48
+ {requirement ? (
49
+ <>
50
+ {body ? " " : null}
51
+ {requirement}
52
+ </>
53
+ ) : null}
54
+ {readonly ? (
55
+ <>
56
+ {body || requirement ? " " : null}
57
+ <Tag>readonly</Tag>
58
+ </>
59
+ ) : null}
46
60
  </Prose>
47
61
  );
48
62
  }
@@ -2,11 +2,11 @@ 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, 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`. A parameter/property default renders as a `Defaults to …` line at the foot of its description cell (linking the value when it's a documented token) rather than in a dedicated column.
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.
5
+ * - Renders breadcrumbs, title (with kind + `readonly` tags), relational links (`member of`, `extends`, `implements`), signatures (one per overload), content, parameters, returns, throws, properties, referenced types, and examples.
6
+ * - In the Parameters / Returns / Throws / Properties 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`. A default renders as a `Defaults to …` note (linking the value when it's a documented token) rather than in a dedicated column.
7
+ * - A class/interface/object-literal type's data members render as the Properties table (see `DocumentationProperties`); an options-bag parameter whose type resolves to one is flattened into indented child rows from the same structured list.
8
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).
9
- * - Child symbols are grouped by `kind` into card sections (Functions, Classes, Methods, Properties, …), each under its own heading.
9
+ * - Child symbols (functions, classes, methods — not data members) are grouped by `kind` into card sections, each under its own heading.
10
10
  * - All sections are conditional — only render when they have entries.
11
11
  *
12
12
  * @kind component
@@ -14,4 +14,4 @@ import type { DocumentationElementProps } from "../../util/tree.js";
14
14
  * @example <DocumentationPage {...element.props} />
15
15
  * @see https://dhoulb.github.io/shelving/ui/docs/DocumentationPage/DocumentationPage
16
16
  */
17
- export declare function DocumentationPage({ title, name, kind, description, content, signatures, params, returns, throws, types, examples, children, ...props }: DocumentationElementProps): ReactNode;
17
+ export declare function DocumentationPage({ title, name, kind, description, content, signatures, params, returns, throws, properties, types, examples, children, ...props }: DocumentationElementProps): ReactNode;
@@ -15,11 +15,12 @@ import { TreeMarkup } from "../tree/TreeMarkup.js";
15
15
  import { DocumentationButtons } from "./DocumentationButtons.js";
16
16
  import { DocumentationKind, getDocumentationKindColor } from "./DocumentationKind.js";
17
17
  import { DocumentationParams } from "./DocumentationParams.js";
18
+ import { DocumentationProperties } from "./DocumentationProperties.js";
18
19
  import { DocumentationReferences } from "./DocumentationReferences.js";
19
20
  import { DocumentationReturns } from "./DocumentationReturns.js";
20
21
  import { DocumentationSignatures } from "./DocumentationSignatures.js";
21
22
  import { DocumentationThrows } from "./DocumentationThrows.js";
22
- /** Documentation `kind`s grouped into card sections, in display order — pluralised, sentence-case headings. */
23
+ /** Documentation `kind`s grouped into card sections, in display order — pluralised, sentence-case headings. Data members (properties) are not child elements — they render as the Properties table instead. */
23
24
  const KIND_SECTIONS = {
24
25
  component: "Components",
25
26
  function: "Functions",
@@ -28,9 +29,7 @@ const KIND_SECTIONS = {
28
29
  type: "Types",
29
30
  constant: "Constants",
30
31
  "static method": "Static methods",
31
- "static property": "Static properties",
32
32
  method: "Methods",
33
- property: "Properties",
34
33
  };
35
34
  /** Render a list of tree elements grouped into kind-based card sections, in `KIND_SECTIONS` order. */
36
35
  function _renderSections(elements) {
@@ -57,11 +56,11 @@ function DocumentationChildren({ elements }) {
57
56
  }
58
57
  /**
59
58
  * 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, referenced types, and examples.
61
- * - 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`. A parameter/property default renders as a `Defaults to …` line at the foot of its description cell (linking the value when it's a documented token) rather than in a dedicated column.
62
- * - 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.
59
+ * - Renders breadcrumbs, title (with kind + `readonly` tags), relational links (`member of`, `extends`, `implements`), signatures (one per overload), content, parameters, returns, throws, properties, referenced types, and examples.
60
+ * - In the Parameters / Returns / Throws / Properties 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`. A default renders as a `Defaults to …` note (linking the value when it's a documented token) rather than in a dedicated column.
61
+ * - A class/interface/object-literal type's data members render as the Properties table (see `DocumentationProperties`); an options-bag parameter whose type resolves to one is flattened into indented child rows from the same structured list.
63
62
  * - A `type` alias's referenced type names render as a linked `Type` table, each row carrying the resolved element's `description` (exact-match only).
64
- * - Child symbols are grouped by `kind` into card sections (Functions, Classes, Methods, Properties, …), each under its own heading.
63
+ * - Child symbols (functions, classes, methods — not data members) are grouped by `kind` into card sections, each under its own heading.
65
64
  * - All sections are conditional — only render when they have entries.
66
65
  *
67
66
  * @kind component
@@ -69,6 +68,6 @@ function DocumentationChildren({ elements }) {
69
68
  * @example <DocumentationPage {...element.props} />
70
69
  * @see https://dhoulb.github.io/shelving/ui/docs/DocumentationPage/DocumentationPage
71
70
  */
72
- export function DocumentationPage({ title, name, kind = "unknown", description, content, signatures, params, returns, throws, types, examples, children, ...props }) {
73
- 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 }), _jsx(DocumentationParams, { params: params }), _jsx(DocumentationReturns, { returns: returns }), _jsx(DocumentationThrows, { throws: throws }), _jsx(DocumentationReferences, { types: types })] })) : null, content && (_jsx(Section, { children: _jsx(Prose, { children: _jsx(TreeMarkup, { children: content }) }) })), examples?.length && (_jsxs(Section, { children: [_jsx(Heading, { children: "Examples" }), examples.map(({ description }) => (_jsx(Preformatted, { children: description }, description)))] })), _jsx(DocumentationChildren, { elements: children })] }) }));
71
+ export function DocumentationPage({ title, name, kind = "unknown", description, content, signatures, params, returns, throws, properties, types, examples, children, ...props }) {
72
+ 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 || properties?.length || types?.length ? (_jsxs(Section, { children: [_jsx(DocumentationSignatures, { signatures: signatures }), _jsx(DocumentationParams, { params: params }), _jsx(DocumentationReturns, { returns: returns }), _jsx(DocumentationThrows, { throws: throws }), _jsx(DocumentationProperties, { properties: properties }), _jsx(DocumentationReferences, { types: types })] })) : null, content && (_jsx(Section, { children: _jsx(Prose, { children: _jsx(TreeMarkup, { children: content }) }) })), examples?.length && (_jsxs(Section, { children: [_jsx(Heading, { children: "Examples" }), examples.map(({ description }) => (_jsx(Preformatted, { children: description }, description)))] })), _jsx(DocumentationChildren, { elements: children })] }) }));
74
73
  }
@@ -32,16 +32,45 @@ describe("DocumentationPage", () => {
32
32
  test("renders a section only for kinds that have children", () => {
33
33
  const html = render(
34
34
  <DocumentationPage path="/Store" name="Store" kind="class">
35
- {[doc("get", "method"), doc("size", "property")]}
35
+ {[doc("get", "method")]}
36
36
  </DocumentationPage>,
37
37
  "./Store",
38
38
  );
39
39
  expect(html).toContain("Methods");
40
- expect(html).toContain("Properties");
41
40
  expect(html).not.toContain("Functions");
42
41
  expect(html).not.toContain("Interfaces");
43
42
  });
44
43
 
44
+ test("renders properties as a table with dot-prefixed names, dropping `| undefined` as optional", () => {
45
+ const html = render(
46
+ <DocumentationPage
47
+ path="/Opts"
48
+ name="Opts"
49
+ kind="interface"
50
+ properties={[
51
+ { name: "caller", type: "AnyCaller | undefined", description: "The caller." },
52
+ { name: "id", type: "string", description: "The id." },
53
+ { name: "timeout", type: "number", description: "The timeout.", default: "20000" },
54
+ { name: "size", type: "number", description: "The size.", readonly: true },
55
+ ]}
56
+ />,
57
+ "./Opts",
58
+ );
59
+ // Dedicated Properties table with dot-prefixed names.
60
+ expect(html).toContain(">Property</th>");
61
+ expect(html).toContain(">.caller</code>");
62
+ expect(html).toContain(">.timeout</code>");
63
+ // `| undefined` is dropped from the type and marks the property optional (no `Required.`).
64
+ expect(html).toContain(">AnyCaller</code>");
65
+ expect(html).not.toContain(">undefined</code>");
66
+ // A property with no default and no optionality gets a `required` tag; one with a default surfaces `Defaults to …`.
67
+ expect(html).toContain(">required</span>");
68
+ expect(html).toContain("Defaults to");
69
+ expect(html).toContain("20000");
70
+ // A read-only property appends a `readonly` tag at the end of its description.
71
+ expect(html).toContain(">readonly</span>");
72
+ });
73
+
45
74
  test("renders parameters as a table, folding any default into the description as a `Defaults to …` line", () => {
46
75
  const html = render(
47
76
  <DocumentationPage
@@ -60,10 +89,10 @@ describe("DocumentationPage", () => {
60
89
  expect(html).toContain(">Param</th>");
61
90
  expect(html).toContain(">Type</th>");
62
91
  expect(html).not.toContain(">Default</th>");
63
- // A param with a default surfaces it as a `Defaults to …` note in its description; a param with neither a default nor optionality is marked `Required.`.
92
+ // A param with a default surfaces it as a `Defaults to …` note in its description; a param with neither a default nor optionality gets a `required` tag.
64
93
  expect(html).toContain("Defaults to");
65
94
  expect(html).toContain("false");
66
- expect(html).toContain("Required.");
95
+ expect(html).toContain(">required</span>");
67
96
  // Returns table headers.
68
97
  expect(html).toContain(">Return</th>");
69
98
  expect(html).toContain("The new thing.");
@@ -110,19 +139,16 @@ describe("DocumentationPage", () => {
110
139
  expect(html).not.toContain("`code`");
111
140
  });
112
141
 
113
- test("groups static members into their own sections, before instance sections", () => {
142
+ test("groups static methods into their own section, before instance methods", () => {
114
143
  const html = render(
115
144
  <DocumentationPage path="/Color" name="Color" kind="class">
116
- {[doc("from", "static method"), doc("DEFAULT", "static property"), doc("toString", "method"), doc("red", "property")]}
145
+ {[doc("from", "static method"), doc("toString", "method")]}
117
146
  </DocumentationPage>,
118
147
  "./Color",
119
148
  );
120
149
  expect(html).toContain("Static methods");
121
- expect(html).toContain("Static properties");
122
150
  expect(html).toContain("Methods");
123
- expect(html).toContain("Properties");
124
- // Static sections render before their instance counterparts (the `Methods` / `Properties` headings use a capital, so they don't match inside `Static methods`).
151
+ // Static methods render before instance methods (the `Methods` heading uses a capital, so it doesn't match inside `Static methods`).
125
152
  expect(html.indexOf("Static methods")).toBeLessThan(html.indexOf("Methods"));
126
- expect(html.indexOf("Static properties")).toBeLessThan(html.indexOf("Properties"));
127
153
  });
128
154
  });
@@ -16,12 +16,13 @@ import { TreeMarkup } from "../tree/TreeMarkup.js";
16
16
  import { DocumentationButtons } from "./DocumentationButtons.js";
17
17
  import { DocumentationKind, getDocumentationKindColor } from "./DocumentationKind.js";
18
18
  import { DocumentationParams } from "./DocumentationParams.js";
19
+ import { DocumentationProperties } from "./DocumentationProperties.js";
19
20
  import { DocumentationReferences } from "./DocumentationReferences.js";
20
21
  import { DocumentationReturns } from "./DocumentationReturns.js";
21
22
  import { DocumentationSignatures } from "./DocumentationSignatures.js";
22
23
  import { DocumentationThrows } from "./DocumentationThrows.js";
23
24
 
24
- /** Documentation `kind`s grouped into card sections, in display order — pluralised, sentence-case headings. */
25
+ /** Documentation `kind`s grouped into card sections, in display order — pluralised, sentence-case headings. Data members (properties) are not child elements — they render as the Properties table instead. */
25
26
  const KIND_SECTIONS = {
26
27
  component: "Components",
27
28
  function: "Functions",
@@ -30,9 +31,7 @@ const KIND_SECTIONS = {
30
31
  type: "Types",
31
32
  constant: "Constants",
32
33
  "static method": "Static methods",
33
- "static property": "Static properties",
34
34
  method: "Methods",
35
- property: "Properties",
36
35
  };
37
36
 
38
37
  /** Render a list of tree elements grouped into kind-based card sections, in `KIND_SECTIONS` order. */
@@ -68,11 +67,11 @@ function DocumentationChildren({ elements }: { readonly elements?: TreeElements
68
67
 
69
68
  /**
70
69
  * Page renderer for a `tree-documentation` element — the full detail page for a documented symbol.
71
- * - 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.
72
- * - 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`. A parameter/property default renders as a `Defaults to …` line at the foot of its description cell (linking the value when it's a documented token) rather than in a dedicated column.
73
- * - 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
+ * - Renders breadcrumbs, title (with kind + `readonly` tags), relational links (`member of`, `extends`, `implements`), signatures (one per overload), content, parameters, returns, throws, properties, referenced types, and examples.
71
+ * - In the Parameters / Returns / Throws / Properties 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`. A default renders as a `Defaults to …` note (linking the value when it's a documented token) rather than in a dedicated column.
72
+ * - A class/interface/object-literal type's data members render as the Properties table (see `DocumentationProperties`); an options-bag parameter whose type resolves to one is flattened into indented child rows from the same structured list.
74
73
  * - A `type` alias's referenced type names render as a linked `Type` table, each row carrying the resolved element's `description` (exact-match only).
75
- * - Child symbols are grouped by `kind` into card sections (Functions, Classes, Methods, Properties, …), each under its own heading.
74
+ * - Child symbols (functions, classes, methods — not data members) are grouped by `kind` into card sections, each under its own heading.
76
75
  * - All sections are conditional — only render when they have entries.
77
76
  *
78
77
  * @kind component
@@ -90,6 +89,7 @@ export function DocumentationPage({
90
89
  params,
91
90
  returns,
92
91
  throws,
92
+ properties,
93
93
  types,
94
94
  examples,
95
95
  children,
@@ -110,12 +110,13 @@ export function DocumentationPage({
110
110
  <DocumentationButtons {...props} />
111
111
  </Header>
112
112
  </Panel>
113
- {signatures?.length || params?.length || returns?.length || throws?.length || types?.length ? (
113
+ {signatures?.length || params?.length || returns?.length || throws?.length || properties?.length || types?.length ? (
114
114
  <Section>
115
115
  <DocumentationSignatures signatures={signatures} />
116
116
  <DocumentationParams params={params} />
117
117
  <DocumentationReturns returns={returns} />
118
118
  <DocumentationThrows throws={throws} />
119
+ <DocumentationProperties properties={properties} />
119
120
  <DocumentationReferences types={types} />
120
121
  </Section>
121
122
  ) : null}