shelving 1.255.1 → 1.256.1

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 (51) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/ui/block/Block.d.ts +2 -1
  4. package/ui/block/Block.js +2 -1
  5. package/ui/block/Block.module.css +11 -0
  6. package/ui/block/Block.tsx +10 -1
  7. package/ui/block/Panel.module.css +1 -0
  8. package/ui/block/Section.module.css +12 -0
  9. package/ui/docs/DocumentationApp.d.ts +7 -0
  10. package/ui/docs/DocumentationApp.js +14 -0
  11. package/ui/docs/DocumentationApp.tsx +26 -0
  12. package/ui/docs/DocumentationPage.js +9 -25
  13. package/ui/docs/DocumentationPage.tsx +16 -35
  14. package/ui/{tree/TreeIndexPage.d.ts → docs/DocumentationSearchPage.d.ts} +3 -7
  15. package/ui/{tree/TreeIndexPage.js → docs/DocumentationSearchPage.js} +11 -15
  16. package/ui/{tree/TreeIndexPage.tsx → docs/DocumentationSearchPage.tsx} +15 -20
  17. package/ui/docs/index.d.ts +2 -1
  18. package/ui/docs/index.js +2 -1
  19. package/ui/docs/index.ts +2 -1
  20. package/ui/form/CheckboxInput.js +5 -2
  21. package/ui/form/CheckboxInput.tsx +8 -5
  22. package/ui/form/RadioInput.js +5 -2
  23. package/ui/form/RadioInput.tsx +8 -5
  24. package/ui/inline/Code.module.css +1 -0
  25. package/ui/inline/Mark.module.css +1 -0
  26. package/ui/misc/Tag.module.css +1 -0
  27. package/ui/style/Gap.d.ts +2 -7
  28. package/ui/style/Gap.js +1 -1
  29. package/ui/style/Gap.module.css +49 -9
  30. package/ui/style/Gap.tsx +3 -9
  31. package/ui/style/Indent.module.css +10 -2
  32. package/ui/style/Padding.d.ts +2 -7
  33. package/ui/style/Padding.module.css +10 -2
  34. package/ui/style/Padding.tsx +2 -26
  35. package/ui/style/Space.module.css +10 -9
  36. package/ui/tree/TreeApp.d.ts +5 -3
  37. package/ui/tree/TreeApp.js +4 -6
  38. package/ui/tree/TreeApp.tsx +14 -11
  39. package/ui/tree/TreePage.js +3 -2
  40. package/ui/tree/TreePage.tsx +16 -13
  41. package/ui/tree/TreeSidebar.d.ts +3 -3
  42. package/ui/tree/TreeSidebar.js +2 -4
  43. package/ui/tree/TreeSidebar.tsx +4 -5
  44. package/ui/tree/index.d.ts +1 -1
  45. package/ui/tree/index.js +1 -1
  46. package/ui/tree/index.ts +1 -1
  47. package/ui/docs/DocumentationHomePage.d.ts +0 -14
  48. package/ui/docs/DocumentationHomePage.js +0 -23
  49. package/ui/docs/DocumentationHomePage.md +0 -34
  50. package/ui/docs/DocumentationHomePage.test.tsx +0 -35
  51. package/ui/docs/DocumentationHomePage.tsx +0 -45
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Shelving
2
2
 
3
- [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) [![Release](https://github.com/dhoulb/shelving/actions/workflows/release.yaml/badge.svg)](https://github.com/dhoulb/shelving/actions/workflows/release.yaml) [![npm](https://img.shields.io/npm/dm/shelving.svg)](https://www.npmjs.com/package/shelving)
3
+ [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://www.conventionalcommits.org/) [![Release](https://github.com/dhoulb/shelving/actions/workflows/release.yaml/badge.svg)](https://github.com/dhoulb/shelving/actions/workflows/release.yaml) [![npm](https://img.shields.io/npm/dm/shelving.svg)](https://www.npmjs.com/package/shelving)
4
4
 
5
5
  TypeScript data toolkit — schema validation, database and API providers, observable state stores, React integration, and typed utility functions.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.255.1",
3
+ "version": "1.256.1",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,7 @@
1
1
  import type { ReactElement } from "react";
2
2
  import { type ColorVariants } from "../style/Color.js";
3
3
  import { type IndentVariants } from "../style/Indent.js";
4
+ import { type PaddingVariants } from "../style/Padding.js";
4
5
  import { type SpaceVariants } from "../style/Space.js";
5
6
  import { type TypographyVariants } from "../style/Typography.js";
6
7
  import { type WidthVariants } from "../style/Width.js";
@@ -16,7 +17,7 @@ export type BlockElement = "div" | "section" | "header" | "footer" | "article" |
16
17
  *
17
18
  * @see https://shelving.cc/ui/BlockProps
18
19
  */
19
- export interface BlockProps extends ColorVariants, IndentVariants, SpaceVariants, TypographyVariants, WidthVariants, OptionalChildProps {
20
+ export interface BlockProps extends ColorVariants, PaddingVariants, IndentVariants, SpaceVariants, TypographyVariants, WidthVariants, OptionalChildProps {
20
21
  /**
21
22
  * Element this `<Block>` renders as, e.g. "header" to output a "<header>"
22
23
  * @default "div"
package/ui/block/Block.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { getColorClass } from "../style/Color.js";
3
3
  import { getIndentClass } from "../style/Indent.js";
4
+ import { getPaddingClass } from "../style/Padding.js";
4
5
  import { getSpaceClass } from "../style/Space.js";
5
6
  import { getTypographyClass } from "../style/Typography.js";
6
7
  import { getWidthClass } from "../style/Width.js";
@@ -19,7 +20,7 @@ const BLOCK_CLASS = getModuleClass(BLOCK_CSS, "block");
19
20
  */
20
21
  export function getBlockClass(variants) {
21
22
  return getClass(BLOCK_CLASS, //
22
- getColorClass(variants), getIndentClass(variants), getSpaceClass(variants), getTypographyClass(variants), getWidthClass(variants));
23
+ getColorClass(variants), getIndentClass(variants), getPaddingClass(variants), getSpaceClass(variants), getTypographyClass(variants), getWidthClass(variants));
23
24
  }
24
25
  /**
25
26
  * Plain `<div>` block with block-level spacing.
@@ -14,3 +14,14 @@
14
14
  margin-block: 0;
15
15
  }
16
16
  }
17
+
18
+ @layer overrides {
19
+ .block {
20
+ &:first-child {
21
+ margin-block-start: 0;
22
+ }
23
+ &:last-child {
24
+ margin-block-end: 0;
25
+ }
26
+ }
27
+ }
@@ -1,6 +1,7 @@
1
1
  import type { ReactElement } from "react";
2
2
  import { type ColorVariants, getColorClass } from "../style/Color.js";
3
3
  import { getIndentClass, type IndentVariants } from "../style/Indent.js";
4
+ import { getPaddingClass, type PaddingVariants } from "../style/Padding.js";
4
5
  import { getSpaceClass, type SpaceVariants } from "../style/Space.js";
5
6
  import { getTypographyClass, type TypographyVariants } from "../style/Typography.js";
6
7
  import { getWidthClass, type WidthVariants } from "../style/Width.js";
@@ -22,7 +23,14 @@ export type BlockElement = "div" | "section" | "header" | "footer" | "article" |
22
23
  *
23
24
  * @see https://shelving.cc/ui/BlockProps
24
25
  */
25
- export interface BlockProps extends ColorVariants, IndentVariants, SpaceVariants, TypographyVariants, WidthVariants, OptionalChildProps {
26
+ export interface BlockProps
27
+ extends ColorVariants,
28
+ PaddingVariants,
29
+ IndentVariants,
30
+ SpaceVariants,
31
+ TypographyVariants,
32
+ WidthVariants,
33
+ OptionalChildProps {
26
34
  /**
27
35
  * Element this `<Block>` renders as, e.g. "header" to output a "<header>"
28
36
  * @default "div"
@@ -45,6 +53,7 @@ export function getBlockClass(variants: BlockProps): string {
45
53
  BLOCK_CLASS, //
46
54
  getColorClass(variants),
47
55
  getIndentClass(variants),
56
+ getPaddingClass(variants),
48
57
  getSpaceClass(variants),
49
58
  getTypographyClass(variants),
50
59
  getWidthClass(variants),
@@ -28,6 +28,7 @@
28
28
  inline-size: 100%;
29
29
  margin-inline: 0;
30
30
  margin-block: 0;
31
+ padding-block: var(--panel-padding, var(--space-section));
31
32
  padding-inline: var(--panel-indent, var(--space-normal));
32
33
  border-top: var(--panel-border, var(--panel-stroke, var(--stroke-normal)) solid var(--tint-80));
33
34
  border-bottom: var(--panel-border, var(--panel-stroke, var(--stroke-normal)) solid var(--tint-80));
@@ -18,3 +18,15 @@
18
18
  margin-block: var(--section-space, var(--space-section));
19
19
  }
20
20
  }
21
+
22
+ @layer overrides {
23
+ .prose :where(section, article, aside, nav, header, footer, figure),
24
+ .section {
25
+ &:first-child {
26
+ margin-block-start: 0;
27
+ }
28
+ &:last-child {
29
+ margin-block-end: 0;
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReactElement } from "react";
2
+ import { type TreeAppProps } from "../tree/TreeApp.js";
3
+ /**
4
+ * Documentation app.
5
+ * - Build upon `<TreeApp>` to include a `<DocumentationSearchPage>` at its `/search` URL.
6
+ */
7
+ export declare function DocumentationApp({ tree, routes, sidebar, ...meta }: TreeAppProps): ReactElement;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Menu, MenuItem } from "../menu/Menu.js";
3
+ import { TreeApp } from "../tree/TreeApp.js";
4
+ import { TreeSidebar } from "../tree/TreeSidebar.js";
5
+ import { DocumentationSearchPage } from "./DocumentationSearchPage.js";
6
+ /**
7
+ * Documentation app.
8
+ * - Build upon `<TreeApp>` to include a `<DocumentationSearchPage>` at its `/search` URL.
9
+ */
10
+ export function DocumentationApp({ tree, routes = {
11
+ "/search": DocumentationSearchPage,
12
+ }, sidebar = (_jsx(TreeSidebar, { tree: tree, children: _jsx(Menu, { children: _jsx(MenuItem, { href: "/search", children: "Search" }) }) })), ...meta }) {
13
+ return _jsx(TreeApp, { tree: tree, routes: routes, sidebar: sidebar, ...meta });
14
+ }
@@ -0,0 +1,26 @@
1
+ import type { ReactElement } from "react";
2
+ import { Menu, MenuItem } from "../menu/Menu.js";
3
+ import { TreeApp, type TreeAppProps } from "../tree/TreeApp.js";
4
+ import { TreeSidebar } from "../tree/TreeSidebar.js";
5
+ import { DocumentationSearchPage } from "./DocumentationSearchPage.js";
6
+
7
+ /**
8
+ * Documentation app.
9
+ * - Build upon `<TreeApp>` to include a `<DocumentationSearchPage>` at its `/search` URL.
10
+ */
11
+ export function DocumentationApp({
12
+ tree,
13
+ routes = {
14
+ "/search": DocumentationSearchPage,
15
+ },
16
+ sidebar = (
17
+ <TreeSidebar tree={tree}>
18
+ <Menu>
19
+ <MenuItem href="/search">Search</MenuItem>
20
+ </Menu>
21
+ </TreeSidebar>
22
+ ),
23
+ ...meta
24
+ }: TreeAppProps): ReactElement {
25
+ return <TreeApp tree={tree} routes={routes} sidebar={sidebar} {...meta} />;
26
+ }
@@ -21,7 +21,8 @@ import { DocumentationReturns } from "./DocumentationReturns.js";
21
21
  import { DocumentationSignatures } from "./DocumentationSignatures.js";
22
22
  import { DocumentationThrows } from "./DocumentationThrows.js";
23
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. */
24
- const KIND_SECTIONS = {
24
+ const KINDS = {
25
+ module: "Modules",
25
26
  component: "Components",
26
27
  function: "Functions",
27
28
  class: "Classes",
@@ -31,29 +32,6 @@ const KIND_SECTIONS = {
31
32
  "static method": "Static methods",
32
33
  method: "Methods",
33
34
  };
34
- /** Render a list of tree elements grouped into kind-based card sections, in `KIND_SECTIONS` order. */
35
- function _renderSections(elements) {
36
- return Object.entries(KIND_SECTIONS).map(([kind, label]) => {
37
- const group = elements.filter(el => el.props.kind === kind);
38
- return group.length ? (_jsxs(Section, { children: [_jsx(Heading, { children: label }), _jsx(TreeCards, { children: group })] }, kind)) : null;
39
- });
40
- }
41
- /**
42
- * Children listing for a documentation page — this page's child symbols grouped into kind-based card sections.
43
- *
44
- * - Renders nothing when the page has no children (e.g. a leaf symbol).
45
- * - Cross-tree search and kind filtering live on the index page (`TreeIndexPage`), not here.
46
- *
47
- * @param props The page's child elements.
48
- * @returns The grouped card sections, or `null` when there are no children.
49
- */
50
- function DocumentationChildren({ elements }) {
51
- const childElements = Array.from(walkElements(elements));
52
- // No children → nothing to list.
53
- if (!childElements.length)
54
- return null;
55
- return _renderSections(childElements);
56
- }
57
35
  /**
58
36
  * Page renderer for a `tree-documentation` element — the full detail page for a documented symbol.
59
37
  * - 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.
@@ -69,5 +47,11 @@ function DocumentationChildren({ elements }) {
69
47
  * @see https://shelving.cc/ui/DocumentationPage
70
48
  */
71
49
  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 })] }) }), _jsxs(Block, { indent: "normal", children: [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 })] })] }) }));
50
+ const elements = Array.from(walkElements(children));
51
+ 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 })] }) }), _jsxs(Block, { indent: "normal", padding: "section", children: [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)))] })), elements.length
52
+ ? Object.entries(KINDS).map(([kind, label]) => {
53
+ const group = elements.filter(el => el.props.kind === kind);
54
+ return group.length ? (_jsxs(Section, { children: [_jsx(Heading, { children: label }), _jsx(TreeCards, { children: group })] }, kind)) : null;
55
+ })
56
+ : null] })] }) }));
73
57
  }
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { walkElements } from "../../util/element.js";
3
- import type { DocumentationElementProps, TreeElement, TreeElements } from "../../util/tree.js";
3
+ import type { DocumentationElementProps, TreeElement } from "../../util/tree.js";
4
4
  import { Block } from "../block/Block.js";
5
5
  import { Heading } from "../block/Heading.js";
6
6
  import { Panel } from "../block/Panel.js";
@@ -23,7 +23,8 @@ import { DocumentationSignatures } from "./DocumentationSignatures.js";
23
23
  import { DocumentationThrows } from "./DocumentationThrows.js";
24
24
 
25
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. */
26
- const KIND_SECTIONS = {
26
+ const KINDS = {
27
+ module: "Modules",
27
28
  component: "Components",
28
29
  function: "Functions",
29
30
  class: "Classes",
@@ -34,37 +35,6 @@ const KIND_SECTIONS = {
34
35
  method: "Methods",
35
36
  };
36
37
 
37
- /** Render a list of tree elements grouped into kind-based card sections, in `KIND_SECTIONS` order. */
38
- function _renderSections(elements: readonly TreeElement[]): ReactNode {
39
- return Object.entries(KIND_SECTIONS).map(([kind, label]) => {
40
- const group = elements.filter(el => (el.props as DocumentationElementProps).kind === kind);
41
- return group.length ? (
42
- <Section key={kind}>
43
- <Heading>{label}</Heading>
44
- <TreeCards>{group}</TreeCards>
45
- </Section>
46
- ) : null;
47
- });
48
- }
49
-
50
- /**
51
- * Children listing for a documentation page — this page's child symbols grouped into kind-based card sections.
52
- *
53
- * - Renders nothing when the page has no children (e.g. a leaf symbol).
54
- * - Cross-tree search and kind filtering live on the index page (`TreeIndexPage`), not here.
55
- *
56
- * @param props The page's child elements.
57
- * @returns The grouped card sections, or `null` when there are no children.
58
- */
59
- function DocumentationChildren({ elements }: { readonly elements?: TreeElements }): ReactNode {
60
- const childElements = Array.from(walkElements<TreeElement>(elements));
61
-
62
- // No children → nothing to list.
63
- if (!childElements.length) return null;
64
-
65
- return _renderSections(childElements);
66
- }
67
-
68
38
  /**
69
39
  * Page renderer for a `tree-documentation` element — the full detail page for a documented symbol.
70
40
  * - 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.
@@ -95,6 +65,7 @@ export function DocumentationPage({
95
65
  children,
96
66
  ...props
97
67
  }: DocumentationElementProps): ReactNode {
68
+ const elements = Array.from(walkElements<TreeElement>(children));
98
69
  return (
99
70
  <Page title={title ?? name} description={description}>
100
71
  <Block color={getDocumentationKindColor(kind)}>
@@ -110,7 +81,7 @@ export function DocumentationPage({
110
81
  <DocumentationButtons {...props} />
111
82
  </Header>
112
83
  </Panel>
113
- <Block indent="normal">
84
+ <Block indent="normal" padding="section">
114
85
  {signatures?.length || params?.length || returns?.length || throws?.length || properties?.length || types?.length ? (
115
86
  <Section>
116
87
  <DocumentationSignatures signatures={signatures} />
@@ -136,7 +107,17 @@ export function DocumentationPage({
136
107
  ))}
137
108
  </Section>
138
109
  )}
139
- <DocumentationChildren elements={children} />
110
+ {elements.length
111
+ ? Object.entries(KINDS).map(([kind, label]) => {
112
+ const group = elements.filter(el => (el.props as DocumentationElementProps).kind === kind);
113
+ return group.length ? (
114
+ <Section key={kind}>
115
+ <Heading>{label}</Heading>
116
+ <TreeCards>{group}</TreeCards>
117
+ </Section>
118
+ ) : null;
119
+ })
120
+ : null}
140
121
  </Block>
141
122
  </Block>
142
123
  </Page>
@@ -1,7 +1,4 @@
1
1
  import { type ReactNode } from "react";
2
- import type { AbsolutePath } from "../../util/path.js";
3
- /** Canonical URL path of the `TreeIndexPage`, wired as a `<TreeApp>` fallback route. */
4
- export declare const TREE_INDEX_PATH: AbsolutePath;
5
2
  /**
6
3
  * Page listing every element in the system in one flat, searchable view.
7
4
  *
@@ -9,11 +6,10 @@ export declare const TREE_INDEX_PATH: AbsolutePath;
9
6
  * - The kind checkboxes are multi-select — ticking several narrows to a `kind IN […]` filter; ticking none shows every kind.
10
7
  * - An empty query lists everything (capped at 100); a non-empty query ranks with `searchTree` and caps at 20.
11
8
  * - Reads the whole tree from the surrounding `<TreeProvider>` (the flattened map's root), so it works on every page.
12
- * - Wired as a `<TreeApp>` fallback route at `TREE_INDEX_PATH` (`/all`) — it's not a node in the tree.
13
9
  *
14
10
  * @kind component
15
11
  * @returns A `<Page>` with a search input, kind checkboxes, and a flat card listing of results.
16
- * @example <Router routes={{ [TREE_INDEX_PATH]: TreeIndexPage }} />
17
- * @see https://shelving.cc/ui/TreeIndexPage
12
+ * @example <Router routes={{ search: DocumentationSearchPage }} />
13
+ * @see https://shelving.cc/ui/DocumentationSearchPage
18
14
  */
19
- export declare function TreeIndexPage(): ReactNode;
15
+ export declare function DocumentationSearchPage(): ReactNode;
@@ -3,23 +3,20 @@ import { useMemo, useState } from "react";
3
3
  import { toggleArrayItem } from "../../util/array.js";
4
4
  import { searchTree } from "../../util/tree.js";
5
5
  import { Block } from "../block/Block.js";
6
+ import { Panel } from "../block/Panel.js";
6
7
  import { Header, Section } from "../block/Section.js";
7
8
  import { Title } from "../block/Title.js";
8
- import { DocumentationKind } from "../docs/DocumentationKind.js";
9
9
  import { CheckboxInput } from "../form/CheckboxInput.js";
10
10
  import { TextInput } from "../form/TextInput.js";
11
11
  import { Page } from "../page/Page.js";
12
12
  import { Row } from "../style/Flex.js";
13
- import { TreeCards } from "./TreeCards.js";
14
- import { useTreeMap } from "./TreeContext.js";
15
- /** Canonical URL path of the `TreeIndexPage`, wired as a `<TreeApp>` fallback route. */
16
- export const TREE_INDEX_PATH = "/all";
13
+ import { TreeCards } from "../tree/TreeCards.js";
14
+ import { useTreeMap } from "../tree/TreeContext.js";
15
+ import { DocumentationKind } from "./DocumentationKind.js";
17
16
  /** Title shown for the index page. */
18
- const INDEX_TITLE = "All elements";
19
- /** Description shown for the index page (page `<meta>` description). */
20
- const INDEX_DESCRIPTION = "Search every documented element in the system.";
17
+ const TITLE = "Search...";
21
18
  /** Kinds offered as filter chips, in display order — mirrors `DocumentationPage`'s sections. */
22
- const INDEX_KINDS = ["component", "function", "class", "interface", "type", "constant", "method", "property"];
19
+ const KINDS = ["module", "component", "function", "class", "interface", "type", "constant", "method", "property"];
23
20
  /** Cap on the flat listing when there's no query — keeps "show everything" sane. */
24
21
  const INDEX_LIMIT = 100;
25
22
  /**
@@ -29,14 +26,13 @@ const INDEX_LIMIT = 100;
29
26
  * - The kind checkboxes are multi-select — ticking several narrows to a `kind IN […]` filter; ticking none shows every kind.
30
27
  * - An empty query lists everything (capped at 100); a non-empty query ranks with `searchTree` and caps at 20.
31
28
  * - Reads the whole tree from the surrounding `<TreeProvider>` (the flattened map's root), so it works on every page.
32
- * - Wired as a `<TreeApp>` fallback route at `TREE_INDEX_PATH` (`/all`) — it's not a node in the tree.
33
29
  *
34
30
  * @kind component
35
31
  * @returns A `<Page>` with a search input, kind checkboxes, and a flat card listing of results.
36
- * @example <Router routes={{ [TREE_INDEX_PATH]: TreeIndexPage }} />
37
- * @see https://shelving.cc/ui/TreeIndexPage
32
+ * @example <Router routes={{ search: DocumentationSearchPage }} />
33
+ * @see https://shelving.cc/ui/DocumentationSearchPage
38
34
  */
39
- export function TreeIndexPage() {
35
+ export function DocumentationSearchPage() {
40
36
  const [query, setQuery] = useState("");
41
37
  const [selected, setSelected] = useState([]);
42
38
  const root = useTreeMap().get("/");
@@ -45,12 +41,12 @@ export function TreeIndexPage() {
45
41
  if (!root)
46
42
  return [];
47
43
  const all = searchTree(root, "", { limit: Number.POSITIVE_INFINITY });
48
- return INDEX_KINDS.filter(kind => all.some(el => el.props.kind === kind));
44
+ return KINDS.filter(kind => all.some(el => el.props.kind === kind));
49
45
  }, [root]);
50
46
  const trimmed = query.trim();
51
47
  // Multiple kinds can be ticked at once — an array value decodes to a `kind IN […]` filter.
52
48
  const filter = selected.length ? { kind: selected } : undefined;
53
49
  // Each element's `key` is its unique canonical path (stamped by `flattenTree()`), so this flat cross-tree listing reconciles correctly.
54
50
  const cards = root ? searchTree(root, trimmed, { limit: trimmed ? 20 : INDEX_LIMIT, filter }) : [];
55
- return (_jsx(Page, { title: INDEX_TITLE, description: INDEX_DESCRIPTION, children: _jsxs(Block, { indent: "normal", children: [_jsx(Header, { children: _jsx(Title, { children: INDEX_TITLE }) }), _jsxs(Section, { children: [_jsx(TextInput, { name: "search", title: "Search", placeholder: "Search\u2026", value: query, onValue: v => setQuery(v ?? "") }), !!kinds.length && (_jsx(Row, { left: true, wrap: true, children: kinds.map(kind => (_jsx(CheckboxInput, { name: kind, width: "fit", value: selected.includes(kind), onValue: () => setSelected(s => toggleArrayItem(s, kind)), children: _jsx(DocumentationKind, { kind: kind }) }, kind))) }))] }), _jsx(Section, { children: _jsx(TreeCards, { children: cards }) })] }) }));
51
+ return (_jsxs(Page, { title: TITLE, children: [_jsxs(Panel, { children: [_jsx(Header, { children: _jsx(Title, { center: true, children: TITLE }) }), _jsxs(Section, { children: [_jsx(TextInput, { name: "search", title: "Search", placeholder: "Search\u2026", value: query, onValue: v => setQuery(v ?? "") }), !!kinds.length && (_jsx(Row, { left: true, wrap: true, children: kinds.map(kind => (_jsx(CheckboxInput, { name: kind, width: "fit", value: selected.includes(kind), onValue: () => setSelected(s => toggleArrayItem(s, kind)), children: _jsx(DocumentationKind, { kind: kind }) }, kind))) }))] })] }), _jsx(Block, { indent: "normal", padding: "section", children: _jsx(Section, { children: _jsx(TreeCards, { children: cards }) }) })] }));
56
52
  }
@@ -1,31 +1,25 @@
1
1
  import { type ReactNode, useMemo, useState } from "react";
2
2
  import { type ImmutableArray, toggleArrayItem } from "../../util/array.js";
3
- import type { AbsolutePath } from "../../util/path.js";
4
3
  import type { Query } from "../../util/query.js";
5
4
  import type { DocumentationElementProps } from "../../util/tree.js";
6
5
  import { searchTree } from "../../util/tree.js";
7
6
  import { Block } from "../block/Block.js";
7
+ import { Panel } from "../block/Panel.js";
8
8
  import { Header, Section } from "../block/Section.js";
9
9
  import { Title } from "../block/Title.js";
10
- import { DocumentationKind } from "../docs/DocumentationKind.js";
11
10
  import { CheckboxInput } from "../form/CheckboxInput.js";
12
11
  import { TextInput } from "../form/TextInput.js";
13
12
  import { Page } from "../page/Page.js";
14
13
  import { Row } from "../style/Flex.js";
15
- import { TreeCards } from "./TreeCards.js";
16
- import { useTreeMap } from "./TreeContext.js";
17
-
18
- /** Canonical URL path of the `TreeIndexPage`, wired as a `<TreeApp>` fallback route. */
19
- export const TREE_INDEX_PATH = "/all" as AbsolutePath;
14
+ import { TreeCards } from "../tree/TreeCards.js";
15
+ import { useTreeMap } from "../tree/TreeContext.js";
16
+ import { DocumentationKind } from "./DocumentationKind.js";
20
17
 
21
18
  /** Title shown for the index page. */
22
- const INDEX_TITLE = "All elements";
23
-
24
- /** Description shown for the index page (page `<meta>` description). */
25
- const INDEX_DESCRIPTION = "Search every documented element in the system.";
19
+ const TITLE = "Search...";
26
20
 
27
21
  /** Kinds offered as filter chips, in display order — mirrors `DocumentationPage`'s sections. */
28
- const INDEX_KINDS = ["component", "function", "class", "interface", "type", "constant", "method", "property"];
22
+ const KINDS = ["module", "component", "function", "class", "interface", "type", "constant", "method", "property"];
29
23
 
30
24
  /** Cap on the flat listing when there's no query — keeps "show everything" sane. */
31
25
  const INDEX_LIMIT = 100;
@@ -37,14 +31,13 @@ const INDEX_LIMIT = 100;
37
31
  * - The kind checkboxes are multi-select — ticking several narrows to a `kind IN […]` filter; ticking none shows every kind.
38
32
  * - An empty query lists everything (capped at 100); a non-empty query ranks with `searchTree` and caps at 20.
39
33
  * - Reads the whole tree from the surrounding `<TreeProvider>` (the flattened map's root), so it works on every page.
40
- * - Wired as a `<TreeApp>` fallback route at `TREE_INDEX_PATH` (`/all`) — it's not a node in the tree.
41
34
  *
42
35
  * @kind component
43
36
  * @returns A `<Page>` with a search input, kind checkboxes, and a flat card listing of results.
44
- * @example <Router routes={{ [TREE_INDEX_PATH]: TreeIndexPage }} />
45
- * @see https://shelving.cc/ui/TreeIndexPage
37
+ * @example <Router routes={{ search: DocumentationSearchPage }} />
38
+ * @see https://shelving.cc/ui/DocumentationSearchPage
46
39
  */
47
- export function TreeIndexPage(): ReactNode {
40
+ export function DocumentationSearchPage(): ReactNode {
48
41
  const [query, setQuery] = useState("");
49
42
  const [selected, setSelected] = useState<ImmutableArray<string>>([]);
50
43
 
@@ -54,7 +47,7 @@ export function TreeIndexPage(): ReactNode {
54
47
  const kinds = useMemo(() => {
55
48
  if (!root) return [];
56
49
  const all = searchTree(root, "", { limit: Number.POSITIVE_INFINITY });
57
- return INDEX_KINDS.filter(kind => all.some(el => (el.props as DocumentationElementProps).kind === kind));
50
+ return KINDS.filter(kind => all.some(el => (el.props as DocumentationElementProps).kind === kind));
58
51
  }, [root]);
59
52
 
60
53
  const trimmed = query.trim();
@@ -64,10 +57,10 @@ export function TreeIndexPage(): ReactNode {
64
57
  const cards = root ? searchTree(root, trimmed, { limit: trimmed ? 20 : INDEX_LIMIT, filter }) : [];
65
58
 
66
59
  return (
67
- <Page title={INDEX_TITLE} description={INDEX_DESCRIPTION}>
68
- <Block indent="normal">
60
+ <Page title={TITLE}>
61
+ <Panel>
69
62
  <Header>
70
- <Title>{INDEX_TITLE}</Title>
63
+ <Title center>{TITLE}</Title>
71
64
  </Header>
72
65
  <Section>
73
66
  <TextInput name="search" title="Search" placeholder="Search…" value={query} onValue={v => setQuery(v ?? "")} />
@@ -87,6 +80,8 @@ export function TreeIndexPage(): ReactNode {
87
80
  </Row>
88
81
  )}
89
82
  </Section>
83
+ </Panel>
84
+ <Block indent="normal" padding="section">
90
85
  <Section>
91
86
  <TreeCards>{cards}</TreeCards>
92
87
  </Section>
@@ -1,13 +1,14 @@
1
+ export * from "./DocumentationApp.js";
1
2
  export * from "./DocumentationButtons.js";
2
3
  export * from "./DocumentationCard.js";
3
4
  export * from "./DocumentationDescription.js";
4
- export * from "./DocumentationHomePage.js";
5
5
  export * from "./DocumentationKind.js";
6
6
  export * from "./DocumentationPage.js";
7
7
  export * from "./DocumentationParams.js";
8
8
  export * from "./DocumentationProperties.js";
9
9
  export * from "./DocumentationReferences.js";
10
10
  export * from "./DocumentationReturns.js";
11
+ export * from "./DocumentationSearchPage.js";
11
12
  export * from "./DocumentationSignatures.js";
12
13
  export * from "./DocumentationThrows.js";
13
14
  export * from "./DocumentationType.js";
package/ui/docs/index.js CHANGED
@@ -1,13 +1,14 @@
1
+ export * from "./DocumentationApp.js";
1
2
  export * from "./DocumentationButtons.js";
2
3
  export * from "./DocumentationCard.js";
3
4
  export * from "./DocumentationDescription.js";
4
- export * from "./DocumentationHomePage.js";
5
5
  export * from "./DocumentationKind.js";
6
6
  export * from "./DocumentationPage.js";
7
7
  export * from "./DocumentationParams.js";
8
8
  export * from "./DocumentationProperties.js";
9
9
  export * from "./DocumentationReferences.js";
10
10
  export * from "./DocumentationReturns.js";
11
+ export * from "./DocumentationSearchPage.js";
11
12
  export * from "./DocumentationSignatures.js";
12
13
  export * from "./DocumentationThrows.js";
13
14
  export * from "./DocumentationType.js";
package/ui/docs/index.ts CHANGED
@@ -1,13 +1,14 @@
1
+ export * from "./DocumentationApp.js";
1
2
  export * from "./DocumentationButtons.js";
2
3
  export * from "./DocumentationCard.js";
3
4
  export * from "./DocumentationDescription.js";
4
- export * from "./DocumentationHomePage.js";
5
5
  export * from "./DocumentationKind.js";
6
6
  export * from "./DocumentationPage.js";
7
7
  export * from "./DocumentationParams.js";
8
8
  export * from "./DocumentationProperties.js";
9
9
  export * from "./DocumentationReferences.js";
10
10
  export * from "./DocumentationReturns.js";
11
+ export * from "./DocumentationSearchPage.js";
11
12
  export * from "./DocumentationSignatures.js";
12
13
  export * from "./DocumentationThrows.js";
13
14
  export * from "./DocumentationType.js";
@@ -4,6 +4,8 @@ import { getFlexClass } from "../style/Flex.js";
4
4
  import { getClass, getModuleClass } from "../util/css.js";
5
5
  import { getInputClass } from "./Input.js";
6
6
  import INPUT_CSS from "./Input.module.css";
7
+ const INPUT_LABEL_CLASS = getModuleClass(INPUT_CSS, "label");
8
+ const INPUT_PLACEHOLDER_CLASS = getModuleClass(INPUT_CSS, "placeholder");
7
9
  /**
8
10
  * Checkbox input bound to a `boolean` value, rendered as a labelled `<input type="checkbox">`.
9
11
  * - The label content comes from `children`, falling back to `placeholder`/`title`.
@@ -12,7 +14,8 @@ import INPUT_CSS from "./Input.module.css";
12
14
  * @example <CheckboxInput name="agree" value={agree} onValue={setAgree}>I agree</CheckboxInput>
13
15
  * @see https://shelving.cc/ui/CheckboxInput
14
16
  */
15
- export function CheckboxInput({ name, title, placeholder = title || "Yes", required = false, disabled = false, message = "", value = false, onValue, children, ...variants }) {
17
+ export function CheckboxInput({ name, title, placeholder = "Yes", required = false, disabled = false, message = "", value = false, onValue, children = title, ...variants }) {
16
18
  const hasChildren = notNullish(children);
17
- return (_jsxs("label", { className: getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "label"), getFlexClass(variants), hasChildren && getModuleClass(INPUT_CSS, "placeholder")), "aria-invalid": !!message, children: [_jsx("input", { name: name, type: "checkbox", defaultChecked: !!value, onChange: e => onValue?.(!!e.currentTarget.checked), required: required, disabled: disabled, title: message, className: getModuleClass(INPUT_CSS, "radio") }), _jsx("span", { children: hasChildren ? children : placeholder })] }));
19
+ return (_jsxs("label", { className: getClass(getInputClass(variants), //
20
+ getFlexClass(variants), INPUT_LABEL_CLASS, !hasChildren && INPUT_PLACEHOLDER_CLASS), "aria-invalid": !!message, children: [_jsx("input", { name: name, type: "checkbox", defaultChecked: !!value, onChange: e => onValue?.(!!e.currentTarget.checked), required: required, disabled: disabled, title: message, className: getModuleClass(INPUT_CSS, "radio") }), _jsx("span", { children: hasChildren ? children : placeholder })] }));
18
21
  }
@@ -6,6 +6,9 @@ import type { OptionalChildProps } from "../util/props.js";
6
6
  import { getInputClass, type InputVariants, type ValueInputProps } from "./Input.js";
7
7
  import INPUT_CSS from "./Input.module.css";
8
8
 
9
+ const INPUT_LABEL_CLASS = getModuleClass(INPUT_CSS, "label");
10
+ const INPUT_PLACEHOLDER_CLASS = getModuleClass(INPUT_CSS, "placeholder");
11
+
9
12
  /**
10
13
  * Props for `CheckboxInput`, a boolean-valued checkbox input.
11
14
  *
@@ -24,23 +27,23 @@ export interface CheckboxProps extends ValueInputProps<boolean>, OptionalChildPr
24
27
  export function CheckboxInput({
25
28
  name,
26
29
  title,
27
- placeholder = title || "Yes",
30
+ placeholder = "Yes",
28
31
  required = false,
29
32
  disabled = false,
30
33
  message = "",
31
34
  value = false,
32
35
  onValue,
33
- children,
36
+ children = title,
34
37
  ...variants
35
38
  }: CheckboxProps): ReactElement {
36
39
  const hasChildren = notNullish(children);
37
40
  return (
38
41
  <label
39
42
  className={getClass(
40
- getInputClass(variants),
41
- getModuleClass(INPUT_CSS, "label"),
43
+ getInputClass(variants), //
42
44
  getFlexClass(variants),
43
- hasChildren && getModuleClass(INPUT_CSS, "placeholder"),
45
+ INPUT_LABEL_CLASS,
46
+ !hasChildren && INPUT_PLACEHOLDER_CLASS,
44
47
  )}
45
48
  aria-invalid={!!message}
46
49
  >
@@ -4,6 +4,8 @@ import { getFlexClass } from "../style/Flex.js";
4
4
  import { getClass, getModuleClass } from "../util/css.js";
5
5
  import { getInputClass } from "./Input.js";
6
6
  import INPUT_CSS from "./Input.module.css";
7
+ const INPUT_LABEL_CLASS = getModuleClass(INPUT_CSS, "label");
8
+ const INPUT_PLACEHOLDER_CLASS = getModuleClass(INPUT_CSS, "placeholder");
7
9
  /**
8
10
  * Single `<input type="radio">` wrapped in a `<label>` styled as an `<Input>`.
9
11
  * - Calls `onValue(true)` when selected; label content comes from `children`, falling back to `placeholder`/`title`.
@@ -12,7 +14,8 @@ import INPUT_CSS from "./Input.module.css";
12
14
  * @example <RadioInput name="plan" value={isPro} onValue={selectPro}>Pro</RadioInput>
13
15
  * @see https://shelving.cc/ui/RadioInput
14
16
  */
15
- export function RadioInput({ name, title, placeholder = title || "Choose", required = false, disabled = false, message = "", value = false, onValue, children, ...props }) {
17
+ export function RadioInput({ name, title, placeholder = "Choose", required = false, disabled = false, message = "", value = false, onValue, children = title, ...props }) {
16
18
  const hasChildren = notNullish(children);
17
- return (_jsxs("label", { className: getClass(getInputClass(props), getModuleClass(INPUT_CSS, "label"), getFlexClass(props), hasChildren && getModuleClass(INPUT_CSS, "placeholder")), children: [_jsx("input", { className: getModuleClass(INPUT_CSS, "radio"), type: "radio", name: name, defaultChecked: value, onChange: () => onValue(true), disabled: disabled, required: required, "aria-invalid": !!message }), hasChildren ? children : placeholder] }));
19
+ return (_jsxs("label", { className: getClass(getInputClass(props), //
20
+ getFlexClass(props), INPUT_LABEL_CLASS, !hasChildren && INPUT_PLACEHOLDER_CLASS), children: [_jsx("input", { className: getModuleClass(INPUT_CSS, "radio"), type: "radio", name: name, defaultChecked: value, onChange: () => onValue(true), disabled: disabled, required: required, "aria-invalid": !!message }), hasChildren ? children : placeholder] }));
18
21
  }