shelving 1.257.1 → 1.258.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/ui/docs/DocumentationApp.js +1 -1
- package/ui/docs/DocumentationApp.tsx +1 -1
- package/ui/misc/Mapper.d.ts +12 -16
- package/ui/misc/Mapper.js +6 -9
- package/ui/misc/Mapper.md +0 -10
- package/ui/misc/Mapper.tsx +15 -21
- package/ui/tree/TreeApp.d.ts +1 -0
- package/ui/tree/TreeApp.js +4 -2
- package/ui/tree/TreeApp.tsx +8 -4
- package/ui/tree/TreeCards.d.ts +1 -3
- package/ui/tree/TreeMenu.d.ts +10 -18
- package/ui/tree/TreeMenu.js +10 -11
- package/ui/tree/TreeMenu.md +4 -3
- package/ui/tree/TreeMenu.tsx +13 -22
- package/ui/tree/TreeRouter.d.ts +4 -9
- package/ui/tree/TreeRouter.js +5 -6
- package/ui/tree/TreeRouter.test.tsx +7 -2
- package/ui/tree/TreeRouter.tsx +5 -12
- package/ui/tree/TreeSidebar.d.ts +5 -11
- package/ui/tree/TreeSidebar.js +11 -10
- package/ui/tree/TreeSidebar.md +4 -3
- package/ui/tree/TreeSidebar.tsx +15 -20
- package/util/ansi.d.ts +17 -26
- package/util/ansi.js +46 -26
package/package.json
CHANGED
|
@@ -9,6 +9,6 @@ import { DocumentationSearchPage } from "./DocumentationSearchPage.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export function DocumentationApp({ tree, routes = {
|
|
11
11
|
"/search": DocumentationSearchPage,
|
|
12
|
-
}, sidebar = (_jsx(TreeSidebar, {
|
|
12
|
+
}, sidebar = (_jsx(TreeSidebar, { children: _jsx(Menu, { children: _jsx(MenuItem, { href: "/search", children: "Search" }) }) })), ...meta }) {
|
|
13
13
|
return _jsx(TreeApp, { tree: tree, routes: routes, sidebar: sidebar, ...meta });
|
|
14
14
|
}
|
package/ui/misc/Mapper.d.ts
CHANGED
|
@@ -4,32 +4,31 @@ import type { ChildProps } from "../util/props.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Dispatch table from a `JSX.IntrinsicElements` key to a renderer component.
|
|
6
6
|
* - Each entry is optional — unmapped elements fall through and render as themselves (e.g. an unmapped `<tree-foo>` appears as a raw `<tree-foo>` HTML element).
|
|
7
|
-
* - Per-entry component receives `JSX.IntrinsicElements[K]
|
|
7
|
+
* - Per-entry component receives `JSX.IntrinsicElements[K]` — the declared props for that element type.
|
|
8
8
|
*
|
|
9
9
|
* @see https://shelving.cc/ui/Mapping
|
|
10
10
|
*/
|
|
11
|
-
export type Mapping
|
|
12
|
-
[K in keyof JSX.IntrinsicElements]?: ComponentType<JSX.IntrinsicElements[K]
|
|
11
|
+
export type Mapping = {
|
|
12
|
+
[K in keyof JSX.IntrinsicElements]?: ComponentType<JSX.IntrinsicElements[K]>;
|
|
13
13
|
};
|
|
14
14
|
/**
|
|
15
15
|
* Props for the `Mapping` component returned by `createMapper()`.
|
|
16
16
|
*
|
|
17
17
|
* @see https://shelving.cc/ui/MappingProps
|
|
18
18
|
*/
|
|
19
|
-
export interface MappingProps
|
|
19
|
+
export interface MappingProps extends ChildProps {
|
|
20
20
|
/** Mapping entries that extend or override the inherited mapping inside this subtree. */
|
|
21
|
-
readonly mapping: Mapping
|
|
21
|
+
readonly mapping: Mapping;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Props for the `Mapper` component returned by `createMapper()`.
|
|
25
25
|
* - `children` holds the pre-walked elements to dispatch.
|
|
26
|
-
* - All other props are spread onto every mapped child as additional props (`E`).
|
|
27
26
|
*
|
|
28
27
|
* @see https://shelving.cc/ui/MapperProps
|
|
29
28
|
*/
|
|
30
|
-
export
|
|
29
|
+
export interface MapperProps {
|
|
31
30
|
readonly children?: Elements;
|
|
32
|
-
}
|
|
31
|
+
}
|
|
33
32
|
/**
|
|
34
33
|
* Create a `[Mapping, Mapper]` pair of components backed by their own private React context.
|
|
35
34
|
*
|
|
@@ -38,24 +37,21 @@ export type MapperProps<E = unknown> = E & {
|
|
|
38
37
|
*
|
|
39
38
|
* Each call creates its own context — independent mappers don't interfere with each other.
|
|
40
39
|
*
|
|
41
|
-
* @typeParam E The shape of any extra props the mapper threads through to every dispatched child. Defaults to `unknown` (no extras).
|
|
42
40
|
* @param defaults The initial mapping of element types to renderer components.
|
|
43
41
|
* @returns A `[Mapping, Mapper]` tuple of components sharing a private context.
|
|
44
42
|
*
|
|
45
43
|
* @example
|
|
46
|
-
* // No extras:
|
|
47
44
|
* const [TreeCardMapping, TreeCardMapper] = createMapper({
|
|
48
45
|
* "tree-element": TreeCard,
|
|
49
46
|
* });
|
|
50
47
|
* <TreeCardMapper>{walkElements(children)}</TreeCardMapper>
|
|
51
48
|
*
|
|
52
49
|
* @example
|
|
53
|
-
* //
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* <TreeMenuMapper path="/foo">{queryElements(children, query)}</TreeMenuMapper>
|
|
50
|
+
* // Override one entry inside a subtree:
|
|
51
|
+
* <TreeCardMapping mapping={{ "tree-element": SpecialTreeCard }}>
|
|
52
|
+
* <TreeCardMapper>{walkElements(children)}</TreeCardMapper>
|
|
53
|
+
* </TreeCardMapping>
|
|
58
54
|
*
|
|
59
55
|
* @see https://shelving.cc/ui/createMapper
|
|
60
56
|
*/
|
|
61
|
-
export declare function createMapper
|
|
57
|
+
export declare function createMapper(defaults?: Mapping): [Mapping: FunctionComponent<MappingProps>, Mapper: FunctionComponent<MapperProps>];
|
package/ui/misc/Mapper.js
CHANGED
|
@@ -9,23 +9,20 @@ import { walkElements } from "../../util/element.js";
|
|
|
9
9
|
*
|
|
10
10
|
* Each call creates its own context — independent mappers don't interfere with each other.
|
|
11
11
|
*
|
|
12
|
-
* @typeParam E The shape of any extra props the mapper threads through to every dispatched child. Defaults to `unknown` (no extras).
|
|
13
12
|
* @param defaults The initial mapping of element types to renderer components.
|
|
14
13
|
* @returns A `[Mapping, Mapper]` tuple of components sharing a private context.
|
|
15
14
|
*
|
|
16
15
|
* @example
|
|
17
|
-
* // No extras:
|
|
18
16
|
* const [TreeCardMapping, TreeCardMapper] = createMapper({
|
|
19
17
|
* "tree-element": TreeCard,
|
|
20
18
|
* });
|
|
21
19
|
* <TreeCardMapper>{walkElements(children)}</TreeCardMapper>
|
|
22
20
|
*
|
|
23
21
|
* @example
|
|
24
|
-
* //
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* <TreeMenuMapper path="/foo">{queryElements(children, query)}</TreeMenuMapper>
|
|
22
|
+
* // Override one entry inside a subtree:
|
|
23
|
+
* <TreeCardMapping mapping={{ "tree-element": SpecialTreeCard }}>
|
|
24
|
+
* <TreeCardMapper>{walkElements(children)}</TreeCardMapper>
|
|
25
|
+
* </TreeCardMapping>
|
|
29
26
|
*
|
|
30
27
|
* @see https://shelving.cc/ui/createMapper
|
|
31
28
|
*/
|
|
@@ -35,7 +32,7 @@ export function createMapper(defaults = {}) {
|
|
|
35
32
|
const inherited = use(Context);
|
|
36
33
|
return _jsx(Context, { value: { ...inherited, ...mapping }, children: children });
|
|
37
34
|
}
|
|
38
|
-
function Mapper({ children
|
|
35
|
+
function Mapper({ children }) {
|
|
39
36
|
const mapping = use(Context);
|
|
40
37
|
const items = [];
|
|
41
38
|
for (const element of walkElements(children)) {
|
|
@@ -45,7 +42,7 @@ export function createMapper(defaults = {}) {
|
|
|
45
42
|
}
|
|
46
43
|
const Component = mapping[element.type];
|
|
47
44
|
if (Component) {
|
|
48
|
-
items.push(_jsx(Component, { ...
|
|
45
|
+
items.push(_jsx(Component, { ...element.props }, element.key));
|
|
49
46
|
}
|
|
50
47
|
else {
|
|
51
48
|
items.push(element);
|
package/ui/misc/Mapper.md
CHANGED
|
@@ -6,7 +6,6 @@ Creates a `[Mapping, Mapper]` component pair backed by a private React context.
|
|
|
6
6
|
|
|
7
7
|
- Each call to `createMapper()` creates its own context — independent mappers don't interfere with each other.
|
|
8
8
|
- Unmapped element types fall through and render as themselves (e.g. an unmapped `<tree-foo>` appears as a raw `<tree-foo>` element).
|
|
9
|
-
- Any extra props passed to `Mapper` (the type parameter `E`) are spread onto every dispatched child, so you can thread shared context like a `path` into each renderer.
|
|
10
9
|
|
|
11
10
|
## Usage
|
|
12
11
|
|
|
@@ -25,12 +24,3 @@ const [TreeMapping, TreeMapper] = createMapper({
|
|
|
25
24
|
<TreeMapper>{walkElements(children)}</TreeMapper>
|
|
26
25
|
</TreeMapping>
|
|
27
26
|
```
|
|
28
|
-
|
|
29
|
-
```tsx
|
|
30
|
-
// With extra props threaded into every dispatched child.
|
|
31
|
-
const [TreeMenuMapping, TreeMenuMapper] = createMapper<{ path?: AbsolutePath }>({
|
|
32
|
-
"tree-element": TreeMenuItem,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
<TreeMenuMapper path="/foo">{queryElements(children, query)}</TreeMenuMapper>
|
|
36
|
-
```
|
package/ui/misc/Mapper.tsx
CHANGED
|
@@ -6,12 +6,12 @@ import type { ChildProps } from "../util/props.js";
|
|
|
6
6
|
/**
|
|
7
7
|
* Dispatch table from a `JSX.IntrinsicElements` key to a renderer component.
|
|
8
8
|
* - Each entry is optional — unmapped elements fall through and render as themselves (e.g. an unmapped `<tree-foo>` appears as a raw `<tree-foo>` HTML element).
|
|
9
|
-
* - Per-entry component receives `JSX.IntrinsicElements[K]
|
|
9
|
+
* - Per-entry component receives `JSX.IntrinsicElements[K]` — the declared props for that element type.
|
|
10
10
|
*
|
|
11
11
|
* @see https://shelving.cc/ui/Mapping
|
|
12
12
|
*/
|
|
13
|
-
export type Mapping
|
|
14
|
-
[K in keyof JSX.IntrinsicElements]?: ComponentType<JSX.IntrinsicElements[K]
|
|
13
|
+
export type Mapping = {
|
|
14
|
+
[K in keyof JSX.IntrinsicElements]?: ComponentType<JSX.IntrinsicElements[K]>;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -19,21 +19,20 @@ export type Mapping<E = unknown> = {
|
|
|
19
19
|
*
|
|
20
20
|
* @see https://shelving.cc/ui/MappingProps
|
|
21
21
|
*/
|
|
22
|
-
export interface MappingProps
|
|
22
|
+
export interface MappingProps extends ChildProps {
|
|
23
23
|
/** Mapping entries that extend or override the inherited mapping inside this subtree. */
|
|
24
|
-
readonly mapping: Mapping
|
|
24
|
+
readonly mapping: Mapping;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Props for the `Mapper` component returned by `createMapper()`.
|
|
29
29
|
* - `children` holds the pre-walked elements to dispatch.
|
|
30
|
-
* - All other props are spread onto every mapped child as additional props (`E`).
|
|
31
30
|
*
|
|
32
31
|
* @see https://shelving.cc/ui/MapperProps
|
|
33
32
|
*/
|
|
34
|
-
export
|
|
33
|
+
export interface MapperProps {
|
|
35
34
|
readonly children?: Elements;
|
|
36
|
-
}
|
|
35
|
+
}
|
|
37
36
|
|
|
38
37
|
// Indexing the heterogeneous `Mapping` by an arbitrary string is unsafe by design — per-key value types diverge.
|
|
39
38
|
// biome-ignore lint/suspicious/noExplicitAny: Each mapping value is a `ComponentType<P>` with its own `P`; we accept `any` for dispatch.
|
|
@@ -47,37 +46,32 @@ type AnyMapping = Record<string, ComponentType<any> | undefined>;
|
|
|
47
46
|
*
|
|
48
47
|
* Each call creates its own context — independent mappers don't interfere with each other.
|
|
49
48
|
*
|
|
50
|
-
* @typeParam E The shape of any extra props the mapper threads through to every dispatched child. Defaults to `unknown` (no extras).
|
|
51
49
|
* @param defaults The initial mapping of element types to renderer components.
|
|
52
50
|
* @returns A `[Mapping, Mapper]` tuple of components sharing a private context.
|
|
53
51
|
*
|
|
54
52
|
* @example
|
|
55
|
-
* // No extras:
|
|
56
53
|
* const [TreeCardMapping, TreeCardMapper] = createMapper({
|
|
57
54
|
* "tree-element": TreeCard,
|
|
58
55
|
* });
|
|
59
56
|
* <TreeCardMapper>{walkElements(children)}</TreeCardMapper>
|
|
60
57
|
*
|
|
61
58
|
* @example
|
|
62
|
-
* //
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* <TreeMenuMapper path="/foo">{queryElements(children, query)}</TreeMenuMapper>
|
|
59
|
+
* // Override one entry inside a subtree:
|
|
60
|
+
* <TreeCardMapping mapping={{ "tree-element": SpecialTreeCard }}>
|
|
61
|
+
* <TreeCardMapper>{walkElements(children)}</TreeCardMapper>
|
|
62
|
+
* </TreeCardMapping>
|
|
67
63
|
*
|
|
68
64
|
* @see https://shelving.cc/ui/createMapper
|
|
69
65
|
*/
|
|
70
|
-
export function createMapper
|
|
71
|
-
defaults: Mapping<E> = {},
|
|
72
|
-
): [Mapping: FunctionComponent<MappingProps<E>>, Mapper: FunctionComponent<MapperProps<E>>] {
|
|
66
|
+
export function createMapper(defaults: Mapping = {}): [Mapping: FunctionComponent<MappingProps>, Mapper: FunctionComponent<MapperProps>] {
|
|
73
67
|
const Context = createContext<AnyMapping>(defaults);
|
|
74
68
|
|
|
75
|
-
function Mapping({ mapping, children }: MappingProps
|
|
69
|
+
function Mapping({ mapping, children }: MappingProps): ReactNode {
|
|
76
70
|
const inherited = use(Context);
|
|
77
71
|
return <Context value={{ ...inherited, ...mapping }}>{children}</Context>;
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
function Mapper({ children
|
|
74
|
+
function Mapper({ children }: MapperProps): ReactNode {
|
|
81
75
|
const mapping = use(Context);
|
|
82
76
|
const items: ReactNode[] = [];
|
|
83
77
|
for (const element of walkElements(children)) {
|
|
@@ -87,7 +81,7 @@ export function createMapper<E = unknown>(
|
|
|
87
81
|
}
|
|
88
82
|
const Component = mapping[element.type];
|
|
89
83
|
if (Component) {
|
|
90
|
-
items.push(<Component key={element.key} {...
|
|
84
|
+
items.push(<Component key={element.key} {...element.props} />);
|
|
91
85
|
} else {
|
|
92
86
|
items.push(element);
|
|
93
87
|
}
|
package/ui/tree/TreeApp.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface TreeAppProps extends PossibleMeta {
|
|
|
22
22
|
* Top-level app component for a tree-based documentation site.
|
|
23
23
|
*
|
|
24
24
|
* - Wraps `<App>` with error catching and a sidebar layout.
|
|
25
|
+
* - Flattens `tree` once via `<TreeProvider>` wrapping both the sidebar and the router, so everything below shares a single `useTreeMap()` lookup — nothing re-flattens.
|
|
25
26
|
* - The sidebar shows a `<TreeSidebar>` (root as a home link + a menu of its children).
|
|
26
27
|
* - `/` renders the root via `<TreePage>`; `/**` catches every deeper path and feeds the full sub-path into `<TreePage>`.
|
|
27
28
|
* - Element rendering uses the default mappings on `<TreePage>`, `<TreeMenu>`, `<TreeCards>`.
|
package/ui/tree/TreeApp.js
CHANGED
|
@@ -4,12 +4,14 @@ import { SidebarLayout } from "../layout/SidebarLayout.js";
|
|
|
4
4
|
import { PageCatcher } from "../misc/Catcher.js";
|
|
5
5
|
import { Navigation } from "../router/Navigation.js";
|
|
6
6
|
import { Router } from "../router/Router.js";
|
|
7
|
+
import { TreeProvider } from "./TreeContext.js";
|
|
7
8
|
import { TreeRouter } from "./TreeRouter.js";
|
|
8
9
|
import { TreeSidebar } from "./TreeSidebar.js";
|
|
9
10
|
/**
|
|
10
11
|
* Top-level app component for a tree-based documentation site.
|
|
11
12
|
*
|
|
12
13
|
* - Wraps `<App>` with error catching and a sidebar layout.
|
|
14
|
+
* - Flattens `tree` once via `<TreeProvider>` wrapping both the sidebar and the router, so everything below shares a single `useTreeMap()` lookup — nothing re-flattens.
|
|
13
15
|
* - The sidebar shows a `<TreeSidebar>` (root as a home link + a menu of its children).
|
|
14
16
|
* - `/` renders the root via `<TreePage>`; `/**` catches every deeper path and feeds the full sub-path into `<TreePage>`.
|
|
15
17
|
* - Element rendering uses the default mappings on `<TreePage>`, `<TreeMenu>`, `<TreeCards>`.
|
|
@@ -20,7 +22,7 @@ import { TreeSidebar } from "./TreeSidebar.js";
|
|
|
20
22
|
* @example <TreeApp tree={tree} title="Docs" />
|
|
21
23
|
* @see https://shelving.cc/ui/TreeApp
|
|
22
24
|
*/
|
|
23
|
-
export function TreeApp({ tree, routes, sidebar = _jsx(TreeSidebar, {
|
|
25
|
+
export function TreeApp({ tree, routes, sidebar = _jsx(TreeSidebar, {}), ...meta }) {
|
|
24
26
|
const fallback = routes && _jsx(Router, { routes: routes });
|
|
25
|
-
return (_jsx(App, { ...meta, children: _jsx(Navigation, { children: _jsx(PageCatcher, { children: _jsx(SidebarLayout, { sidebar: sidebar, children: _jsx(TreeRouter, {
|
|
27
|
+
return (_jsx(App, { ...meta, children: _jsx(Navigation, { children: _jsx(PageCatcher, { children: _jsx(TreeProvider, { tree: tree, children: _jsx(SidebarLayout, { sidebar: sidebar, children: _jsx(TreeRouter, { fallback: fallback }) }) }) }) }) }));
|
|
26
28
|
}
|
package/ui/tree/TreeApp.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { Navigation } from "../router/Navigation.js";
|
|
|
7
7
|
import { Router } from "../router/Router.js";
|
|
8
8
|
import type { Routes } from "../router/Routes.js";
|
|
9
9
|
import type { PossibleMeta } from "../util/index.js";
|
|
10
|
+
import { TreeProvider } from "./TreeContext.js";
|
|
10
11
|
import { TreeRouter } from "./TreeRouter.js";
|
|
11
12
|
import { TreeSidebar } from "./TreeSidebar.js";
|
|
12
13
|
|
|
@@ -31,6 +32,7 @@ export interface TreeAppProps extends PossibleMeta {
|
|
|
31
32
|
* Top-level app component for a tree-based documentation site.
|
|
32
33
|
*
|
|
33
34
|
* - Wraps `<App>` with error catching and a sidebar layout.
|
|
35
|
+
* - Flattens `tree` once via `<TreeProvider>` wrapping both the sidebar and the router, so everything below shares a single `useTreeMap()` lookup — nothing re-flattens.
|
|
34
36
|
* - The sidebar shows a `<TreeSidebar>` (root as a home link + a menu of its children).
|
|
35
37
|
* - `/` renders the root via `<TreePage>`; `/**` catches every deeper path and feeds the full sub-path into `<TreePage>`.
|
|
36
38
|
* - Element rendering uses the default mappings on `<TreePage>`, `<TreeMenu>`, `<TreeCards>`.
|
|
@@ -41,15 +43,17 @@ export interface TreeAppProps extends PossibleMeta {
|
|
|
41
43
|
* @example <TreeApp tree={tree} title="Docs" />
|
|
42
44
|
* @see https://shelving.cc/ui/TreeApp
|
|
43
45
|
*/
|
|
44
|
-
export function TreeApp({ tree, routes, sidebar = <TreeSidebar
|
|
46
|
+
export function TreeApp({ tree, routes, sidebar = <TreeSidebar />, ...meta }: TreeAppProps): ReactElement {
|
|
45
47
|
const fallback = routes && <Router routes={routes} />;
|
|
46
48
|
return (
|
|
47
49
|
<App {...meta}>
|
|
48
50
|
<Navigation>
|
|
49
51
|
<PageCatcher>
|
|
50
|
-
<
|
|
51
|
-
<
|
|
52
|
-
|
|
52
|
+
<TreeProvider tree={tree}>
|
|
53
|
+
<SidebarLayout sidebar={sidebar}>
|
|
54
|
+
<TreeRouter fallback={fallback} />
|
|
55
|
+
</SidebarLayout>
|
|
56
|
+
</TreeProvider>
|
|
53
57
|
</PageCatcher>
|
|
54
58
|
</Navigation>
|
|
55
59
|
</App>
|
package/ui/tree/TreeCards.d.ts
CHANGED
|
@@ -5,9 +5,7 @@ import type { TreeElements } from "../../util/tree.js";
|
|
|
5
5
|
*
|
|
6
6
|
* @see https://shelving.cc/ui/TreeCardMapping
|
|
7
7
|
*/
|
|
8
|
-
export declare const TreeCardMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps
|
|
9
|
-
readonly children?: import("../../util/element.js").Elements;
|
|
10
|
-
}>;
|
|
8
|
+
export declare const TreeCardMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps>, TreeCardMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps>;
|
|
11
9
|
/**
|
|
12
10
|
* Props for the `TreeCards` component — the tree elements to render as cards.
|
|
13
11
|
*
|
package/ui/tree/TreeMenu.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { type Element } from "../../util/element.js";
|
|
3
|
-
import { type AbsolutePath } from "../../util/path.js";
|
|
4
3
|
import type { TreeElement, TreeElementProps } from "../../util/tree.js";
|
|
5
4
|
/**
|
|
6
5
|
* Match an element that should appear in the sidebar menu.
|
|
@@ -14,52 +13,45 @@ import type { TreeElement, TreeElementProps } from "../../util/tree.js";
|
|
|
14
13
|
* @see https://shelving.cc/ui/matchMenuElement
|
|
15
14
|
*/
|
|
16
15
|
export declare function matchMenuElement(element: Element): boolean;
|
|
17
|
-
/** Extras threaded through `TreeMenuMapper` to every menu item — the parent's URL path. */
|
|
18
|
-
interface TreeMenuExtras {
|
|
19
|
-
/** URL path of the parent element. Each item appends its own `name` to compute its own path. Defaults to `/`. */
|
|
20
|
-
readonly path: AbsolutePath;
|
|
21
|
-
}
|
|
22
16
|
/**
|
|
23
17
|
* Default menu item renderer for any `tree-*` element.
|
|
24
18
|
*
|
|
25
|
-
* -
|
|
19
|
+
* - Links straight to the element's own canonical `path` (stamped by `flattenTree()`), so the menu must be fed the flattened tree's elements.
|
|
26
20
|
* - Passes both the label and the nested `<TreeMenuMapper>` to `<MenuItem>`; `<MenuItem>` itself decides whether to reveal the nested submenu based on the current URL.
|
|
27
21
|
*
|
|
28
|
-
* @param props The tree element props
|
|
22
|
+
* @param props The tree element props — `path` is the canonical URL to link to.
|
|
29
23
|
* @returns A `<MenuItem>` for the element, with a nested `<Menu>` when it has menu-eligible children.
|
|
30
24
|
* @kind component
|
|
31
|
-
* @example <TreeMenuItem {...element.props}
|
|
25
|
+
* @example <TreeMenuItem {...element.props} />
|
|
32
26
|
* @see https://shelving.cc/ui/TreeMenuItem
|
|
33
27
|
*/
|
|
34
|
-
export declare function TreeMenuItem({ path, name, title, children }: TreeElementProps
|
|
28
|
+
export declare function TreeMenuItem({ path, name, title, children }: TreeElementProps): ReactNode;
|
|
35
29
|
/**
|
|
36
30
|
* Mapping + Mapper pair for the menu — wrap children in `<TreeMenuMapping>` to override the per-type menu-item renderers.
|
|
37
31
|
*
|
|
38
32
|
* @see https://shelving.cc/ui/TreeMenuMapping
|
|
39
33
|
*/
|
|
40
|
-
export declare const TreeMenuMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps
|
|
34
|
+
export declare const TreeMenuMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps>, TreeMenuMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps>;
|
|
41
35
|
/**
|
|
42
36
|
* Props for the `TreeMenu` component — the root tree element plus its URL path.
|
|
43
37
|
*
|
|
44
38
|
* @see https://shelving.cc/ui/TreeMenuProps
|
|
45
39
|
*/
|
|
46
40
|
export interface TreeMenuProps {
|
|
47
|
-
/** Root element whose children become the navigation links. */
|
|
41
|
+
/** Root element whose children become the navigation links. Must be a flattened element (from `useTreeMap()`) so each child carries its canonical `path`. */
|
|
48
42
|
readonly tree: TreeElement;
|
|
49
|
-
/** URL path of the root — children get `path + their.name`. Defaults to `/`. */
|
|
50
|
-
readonly path?: AbsolutePath | undefined;
|
|
51
43
|
}
|
|
52
44
|
/**
|
|
53
45
|
* Sidebar navigation menu built from the children of a root tree element.
|
|
54
46
|
*
|
|
55
|
-
* - Renders each child via `<TreeMenuItem>` (the default mapping for `tree-element`)
|
|
47
|
+
* - Renders each child via `<TreeMenuItem>` (the default mapping for `tree-element`), linking to each child's stamped `path`.
|
|
48
|
+
* - Pass a flattened element (e.g. `useTreeMap().get("/")`) so its children carry their canonical `path`.
|
|
56
49
|
* - To customise renderers for specific types, wrap in `<TreeMenuMapping mapping={…}>`.
|
|
57
50
|
* - Only directories and files appear — code symbols are kept off the navigation.
|
|
58
51
|
*
|
|
59
52
|
* @kind component
|
|
60
53
|
* @returns A `<Menu>` of navigation links to the root's children.
|
|
61
|
-
* @example <TreeMenu tree={
|
|
54
|
+
* @example <TreeMenu tree={useTreeMap().get("/")} />
|
|
62
55
|
* @see https://shelving.cc/ui/TreeMenu
|
|
63
56
|
*/
|
|
64
|
-
export declare function TreeMenu({
|
|
65
|
-
export {};
|
|
57
|
+
export declare function TreeMenu({ tree }: TreeMenuProps): ReactNode;
|
package/ui/tree/TreeMenu.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { filterElements } from "../../util/element.js";
|
|
3
|
-
import { joinPath } from "../../util/path.js";
|
|
4
3
|
import { Menu, MenuItem } from "../menu/Menu.js";
|
|
5
4
|
import { createMapper } from "../misc/Mapper.js";
|
|
6
5
|
/**
|
|
@@ -25,19 +24,18 @@ export function matchMenuElement(element) {
|
|
|
25
24
|
/**
|
|
26
25
|
* Default menu item renderer for any `tree-*` element.
|
|
27
26
|
*
|
|
28
|
-
* -
|
|
27
|
+
* - Links straight to the element's own canonical `path` (stamped by `flattenTree()`), so the menu must be fed the flattened tree's elements.
|
|
29
28
|
* - Passes both the label and the nested `<TreeMenuMapper>` to `<MenuItem>`; `<MenuItem>` itself decides whether to reveal the nested submenu based on the current URL.
|
|
30
29
|
*
|
|
31
|
-
* @param props The tree element props
|
|
30
|
+
* @param props The tree element props — `path` is the canonical URL to link to.
|
|
32
31
|
* @returns A `<MenuItem>` for the element, with a nested `<Menu>` when it has menu-eligible children.
|
|
33
32
|
* @kind component
|
|
34
|
-
* @example <TreeMenuItem {...element.props}
|
|
33
|
+
* @example <TreeMenuItem {...element.props} />
|
|
35
34
|
* @see https://shelving.cc/ui/TreeMenuItem
|
|
36
35
|
*/
|
|
37
|
-
export function TreeMenuItem({ path
|
|
38
|
-
const href = joinPath(path, name);
|
|
36
|
+
export function TreeMenuItem({ path, name, title, children }) {
|
|
39
37
|
const submenu = Array.from(filterElements(children, matchMenuElement));
|
|
40
|
-
return (_jsxs(MenuItem, { href:
|
|
38
|
+
return (_jsxs(MenuItem, { href: path, children: [title ?? name, submenu.length ? (_jsx(Menu, { children: _jsx(TreeMenuMapper, { children: submenu }) })) : null] }));
|
|
41
39
|
}
|
|
42
40
|
/**
|
|
43
41
|
* Mapping + Mapper pair for the menu — wrap children in `<TreeMenuMapping>` to override the per-type menu-item renderers.
|
|
@@ -51,15 +49,16 @@ export const [TreeMenuMapping, TreeMenuMapper] = createMapper({
|
|
|
51
49
|
/**
|
|
52
50
|
* Sidebar navigation menu built from the children of a root tree element.
|
|
53
51
|
*
|
|
54
|
-
* - Renders each child via `<TreeMenuItem>` (the default mapping for `tree-element`)
|
|
52
|
+
* - Renders each child via `<TreeMenuItem>` (the default mapping for `tree-element`), linking to each child's stamped `path`.
|
|
53
|
+
* - Pass a flattened element (e.g. `useTreeMap().get("/")`) so its children carry their canonical `path`.
|
|
55
54
|
* - To customise renderers for specific types, wrap in `<TreeMenuMapping mapping={…}>`.
|
|
56
55
|
* - Only directories and files appear — code symbols are kept off the navigation.
|
|
57
56
|
*
|
|
58
57
|
* @kind component
|
|
59
58
|
* @returns A `<Menu>` of navigation links to the root's children.
|
|
60
|
-
* @example <TreeMenu tree={
|
|
59
|
+
* @example <TreeMenu tree={useTreeMap().get("/")} />
|
|
61
60
|
* @see https://shelving.cc/ui/TreeMenu
|
|
62
61
|
*/
|
|
63
|
-
export function TreeMenu({
|
|
64
|
-
return (_jsx(Menu, { children: _jsx(TreeMenuMapper, {
|
|
62
|
+
export function TreeMenu({ tree }) {
|
|
63
|
+
return (_jsx(Menu, { children: _jsx(TreeMenuMapper, { children: filterElements(tree.props.children, matchMenuElement) }) }));
|
|
65
64
|
}
|
package/ui/tree/TreeMenu.md
CHANGED
|
@@ -5,7 +5,7 @@ A sidebar navigation menu built from the children of a root tree element. Each c
|
|
|
5
5
|
**Things to know:**
|
|
6
6
|
|
|
7
7
|
- Only directories and files appear, plus documentation symbols of `kind: "module"` — functions, classes, methods, properties, etc. are kept off the navigation (they still get their own pages via `<TreeApp>`).
|
|
8
|
-
- Each item
|
|
8
|
+
- Each item links straight to its element's own canonical `path` (stamped by `flattenTree()`), so feed it a flattened element — e.g. `useTreeMap().get("/")` — rather than a raw extracted tree.
|
|
9
9
|
- It is a `[Mapping, Mapper]` pair: wrap any subtree in `<TreeMenuMapping mapping={…}>` to swap the per-type menu-item renderer without touching the rest of the site. `<TreeSidebar>` shares this same mapper.
|
|
10
10
|
- Use it directly for finer layout control; otherwise `<TreeApp>` wires a `<TreeSidebar>` (a home link plus this menu) for you.
|
|
11
11
|
|
|
@@ -13,9 +13,10 @@ A sidebar navigation menu built from the children of a root tree element. Each c
|
|
|
13
13
|
|
|
14
14
|
```tsx
|
|
15
15
|
import { TreeMenu } from "shelving/ui";
|
|
16
|
+
import { useTreeMap } from "shelving/ui";
|
|
16
17
|
|
|
17
|
-
// Just the navigation menu from
|
|
18
|
-
<TreeMenu tree={
|
|
18
|
+
// Just the navigation menu from the flattened root's children.
|
|
19
|
+
<TreeMenu tree={useTreeMap().get("/")} />
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
Override the menu-item renderer for one element type:
|
package/ui/tree/TreeMenu.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { type Element, filterElements } from "../../util/element.js";
|
|
3
|
-
import { type AbsolutePath, joinPath } from "../../util/path.js";
|
|
4
3
|
import type { TreeElement, TreeElementProps } from "../../util/tree.js";
|
|
5
4
|
import { Menu, MenuItem } from "../menu/Menu.js";
|
|
6
5
|
import { createMapper } from "../misc/Mapper.js";
|
|
@@ -23,33 +22,26 @@ export function matchMenuElement(element: Element): boolean {
|
|
|
23
22
|
return false;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
/** Extras threaded through `TreeMenuMapper` to every menu item — the parent's URL path. */
|
|
27
|
-
interface TreeMenuExtras {
|
|
28
|
-
/** URL path of the parent element. Each item appends its own `name` to compute its own path. Defaults to `/`. */
|
|
29
|
-
readonly path: AbsolutePath;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
/**
|
|
33
26
|
* Default menu item renderer for any `tree-*` element.
|
|
34
27
|
*
|
|
35
|
-
* -
|
|
28
|
+
* - Links straight to the element's own canonical `path` (stamped by `flattenTree()`), so the menu must be fed the flattened tree's elements.
|
|
36
29
|
* - Passes both the label and the nested `<TreeMenuMapper>` to `<MenuItem>`; `<MenuItem>` itself decides whether to reveal the nested submenu based on the current URL.
|
|
37
30
|
*
|
|
38
|
-
* @param props The tree element props
|
|
31
|
+
* @param props The tree element props — `path` is the canonical URL to link to.
|
|
39
32
|
* @returns A `<MenuItem>` for the element, with a nested `<Menu>` when it has menu-eligible children.
|
|
40
33
|
* @kind component
|
|
41
|
-
* @example <TreeMenuItem {...element.props}
|
|
34
|
+
* @example <TreeMenuItem {...element.props} />
|
|
42
35
|
* @see https://shelving.cc/ui/TreeMenuItem
|
|
43
36
|
*/
|
|
44
|
-
export function TreeMenuItem({ path
|
|
45
|
-
const href = joinPath(path, name);
|
|
37
|
+
export function TreeMenuItem({ path, name, title, children }: TreeElementProps): ReactNode {
|
|
46
38
|
const submenu = Array.from(filterElements(children, matchMenuElement));
|
|
47
39
|
return (
|
|
48
|
-
<MenuItem href={
|
|
40
|
+
<MenuItem href={path}>
|
|
49
41
|
{title ?? name}
|
|
50
42
|
{submenu.length ? (
|
|
51
43
|
<Menu>
|
|
52
|
-
<TreeMenuMapper
|
|
44
|
+
<TreeMenuMapper>{submenu}</TreeMenuMapper>
|
|
53
45
|
</Menu>
|
|
54
46
|
) : null}
|
|
55
47
|
</MenuItem>
|
|
@@ -61,7 +53,7 @@ export function TreeMenuItem({ path = "/", name, title, children }: TreeElementP
|
|
|
61
53
|
*
|
|
62
54
|
* @see https://shelving.cc/ui/TreeMenuMapping
|
|
63
55
|
*/
|
|
64
|
-
export const [TreeMenuMapping, TreeMenuMapper] = createMapper
|
|
56
|
+
export const [TreeMenuMapping, TreeMenuMapper] = createMapper({
|
|
65
57
|
"tree-element": TreeMenuItem,
|
|
66
58
|
"tree-documentation": TreeMenuItem,
|
|
67
59
|
});
|
|
@@ -72,28 +64,27 @@ export const [TreeMenuMapping, TreeMenuMapper] = createMapper<TreeMenuExtras>({
|
|
|
72
64
|
* @see https://shelving.cc/ui/TreeMenuProps
|
|
73
65
|
*/
|
|
74
66
|
export interface TreeMenuProps {
|
|
75
|
-
/** Root element whose children become the navigation links. */
|
|
67
|
+
/** Root element whose children become the navigation links. Must be a flattened element (from `useTreeMap()`) so each child carries its canonical `path`. */
|
|
76
68
|
readonly tree: TreeElement;
|
|
77
|
-
/** URL path of the root — children get `path + their.name`. Defaults to `/`. */
|
|
78
|
-
readonly path?: AbsolutePath | undefined;
|
|
79
69
|
}
|
|
80
70
|
|
|
81
71
|
/**
|
|
82
72
|
* Sidebar navigation menu built from the children of a root tree element.
|
|
83
73
|
*
|
|
84
|
-
* - Renders each child via `<TreeMenuItem>` (the default mapping for `tree-element`)
|
|
74
|
+
* - Renders each child via `<TreeMenuItem>` (the default mapping for `tree-element`), linking to each child's stamped `path`.
|
|
75
|
+
* - Pass a flattened element (e.g. `useTreeMap().get("/")`) so its children carry their canonical `path`.
|
|
85
76
|
* - To customise renderers for specific types, wrap in `<TreeMenuMapping mapping={…}>`.
|
|
86
77
|
* - Only directories and files appear — code symbols are kept off the navigation.
|
|
87
78
|
*
|
|
88
79
|
* @kind component
|
|
89
80
|
* @returns A `<Menu>` of navigation links to the root's children.
|
|
90
|
-
* @example <TreeMenu tree={
|
|
81
|
+
* @example <TreeMenu tree={useTreeMap().get("/")} />
|
|
91
82
|
* @see https://shelving.cc/ui/TreeMenu
|
|
92
83
|
*/
|
|
93
|
-
export function TreeMenu({
|
|
84
|
+
export function TreeMenu({ tree }: TreeMenuProps): ReactNode {
|
|
94
85
|
return (
|
|
95
86
|
<Menu>
|
|
96
|
-
<TreeMenuMapper
|
|
87
|
+
<TreeMenuMapper>{filterElements(tree.props.children, matchMenuElement)}</TreeMenuMapper>
|
|
97
88
|
</Menu>
|
|
98
89
|
);
|
|
99
90
|
}
|
package/ui/tree/TreeRouter.d.ts
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
import type { ReactElement, ReactNode } from "react";
|
|
2
|
-
import type { TreeElement } from "../../util/tree.js";
|
|
3
2
|
import type { PossibleMeta } from "../util/index.js";
|
|
4
3
|
/**
|
|
5
4
|
* Mapping + Mapper pair for tree routers — wrap children in `<TreeRouterMapping>` to override the per-type page renderers.
|
|
6
5
|
*
|
|
7
6
|
* @see https://shelving.cc/ui/TreeRouterMapping
|
|
8
7
|
*/
|
|
9
|
-
export declare const TreeRouterMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps
|
|
10
|
-
readonly children?: import("../../index.js").Elements;
|
|
11
|
-
}>;
|
|
8
|
+
export declare const TreeRouterMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps>, TreeRouterMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps>;
|
|
12
9
|
/**
|
|
13
10
|
* Props for the `TreeRouter` component — the tree to route plus an optional fallback and app meta.
|
|
14
11
|
*
|
|
15
12
|
* @see https://shelving.cc/ui/TreeRouterProps
|
|
16
13
|
*/
|
|
17
14
|
export interface TreeRouterProps extends PossibleMeta {
|
|
18
|
-
/** The tree of elements to match routes for. */
|
|
19
|
-
readonly tree: TreeElement;
|
|
20
15
|
/**
|
|
21
16
|
* Optional fallback element.
|
|
22
17
|
* - Explicit `null` means fallback to nothing (router will not throw `NotFoundError`).
|
|
@@ -26,7 +21,7 @@ export interface TreeRouterProps extends PossibleMeta {
|
|
|
26
21
|
/**
|
|
27
22
|
* Resolve a URL path to a tree element and render it as a full page.
|
|
28
23
|
*
|
|
29
|
-
* -
|
|
24
|
+
* - Reads the flattened `path` → element map from the surrounding `<TreeProvider>` (`useTreeMap()`), then resolves the current URL with a single `map.get(path)`.
|
|
30
25
|
* - `/` renders the root itself; deeper paths render the matching descendant (composite module names like `/util/string` resolve for free — they're whole keys in the map).
|
|
31
26
|
* - The resolved element is already stamped with its canonical `path`, so the page and its cards link straight to their own paths — nothing needs threading.
|
|
32
27
|
* - To override the renderer for a specific element type, wrap in `<TreeRouterMapping mapping={…}>`.
|
|
@@ -34,7 +29,7 @@ export interface TreeRouterProps extends PossibleMeta {
|
|
|
34
29
|
* @returns The resolved element rendered as a page, or the `fallback`.
|
|
35
30
|
* @throws `NotFoundError` When no element matches the URL and no `fallback` is given.
|
|
36
31
|
* @kind component
|
|
37
|
-
* @example <
|
|
32
|
+
* @example <TreeProvider tree={tree}><TreeRouter /></TreeProvider>
|
|
38
33
|
* @see https://shelving.cc/ui/TreeRouter
|
|
39
34
|
*/
|
|
40
|
-
export declare function TreeRouter({
|
|
35
|
+
export declare function TreeRouter({ fallback, ...meta }: TreeRouterProps): ReactNode;
|
package/ui/tree/TreeRouter.js
CHANGED
|
@@ -3,7 +3,7 @@ import { NotFoundError } from "../../error/RequestError.js";
|
|
|
3
3
|
import { DocumentationPage } from "../docs/DocumentationPage.js";
|
|
4
4
|
import { createMapper } from "../misc/Mapper.js";
|
|
5
5
|
import { MetaContext, requireMetaURL } from "../misc/MetaContext.js";
|
|
6
|
-
import {
|
|
6
|
+
import { useTreeMap } from "./TreeContext.js";
|
|
7
7
|
import { TreePage } from "./TreePage.js";
|
|
8
8
|
/**
|
|
9
9
|
* Mapping + Mapper pair for tree routers — wrap children in `<TreeRouterMapping>` to override the per-type page renderers.
|
|
@@ -17,7 +17,7 @@ export const [TreeRouterMapping, TreeRouterMapper] = createMapper({
|
|
|
17
17
|
/**
|
|
18
18
|
* Resolve a URL path to a tree element and render it as a full page.
|
|
19
19
|
*
|
|
20
|
-
* -
|
|
20
|
+
* - Reads the flattened `path` → element map from the surrounding `<TreeProvider>` (`useTreeMap()`), then resolves the current URL with a single `map.get(path)`.
|
|
21
21
|
* - `/` renders the root itself; deeper paths render the matching descendant (composite module names like `/util/string` resolve for free — they're whole keys in the map).
|
|
22
22
|
* - The resolved element is already stamped with its canonical `path`, so the page and its cards link straight to their own paths — nothing needs threading.
|
|
23
23
|
* - To override the renderer for a specific element type, wrap in `<TreeRouterMapping mapping={…}>`.
|
|
@@ -25,13 +25,12 @@ export const [TreeRouterMapping, TreeRouterMapper] = createMapper({
|
|
|
25
25
|
* @returns The resolved element rendered as a page, or the `fallback`.
|
|
26
26
|
* @throws `NotFoundError` When no element matches the URL and no `fallback` is given.
|
|
27
27
|
* @kind component
|
|
28
|
-
* @example <
|
|
28
|
+
* @example <TreeProvider tree={tree}><TreeRouter /></TreeProvider>
|
|
29
29
|
* @see https://shelving.cc/ui/TreeRouter
|
|
30
30
|
*/
|
|
31
|
-
export function TreeRouter({
|
|
31
|
+
export function TreeRouter({ fallback, ...meta }) {
|
|
32
32
|
const { path, ...combined } = requireMetaURL(meta);
|
|
33
|
-
|
|
34
|
-
return (_jsx(MetaContext, { value: combined, children: _jsx(TreeProvider, { tree: tree, children: _jsx(TreeRoute, { path: path, fallback: fallback }) }) }));
|
|
33
|
+
return (_jsx(MetaContext, { value: combined, children: _jsx(TreeRoute, { path: path, fallback: fallback }) }));
|
|
35
34
|
}
|
|
36
35
|
/** Resolve the current URL `path` to a tree element via the flattened map and render it; otherwise fall back, or throw `NotFoundError`. */
|
|
37
36
|
function TreeRoute({ path, fallback }) {
|
|
@@ -3,6 +3,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
|
|
3
3
|
import type { TreeElement } from "../../util/tree.js";
|
|
4
4
|
import { MetaContext } from "../misc/MetaContext.js";
|
|
5
5
|
import { createMeta } from "../util/meta.js";
|
|
6
|
+
import { TreeProvider } from "./TreeContext.js";
|
|
6
7
|
import { TreeRouter } from "./TreeRouter.js";
|
|
7
8
|
|
|
8
9
|
/** Minimal tree: root → `util` directory → `array` file. */
|
|
@@ -26,7 +27,9 @@ describe("TreeRouter", () => {
|
|
|
26
27
|
test("card links include an APP_URL subfolder exactly once", () => {
|
|
27
28
|
const html = renderToStaticMarkup(
|
|
28
29
|
<MetaContext value={createMeta({ root: "http://x.com/sub/", url: "./util" })}>
|
|
29
|
-
<
|
|
30
|
+
<TreeProvider tree={tree}>
|
|
31
|
+
<TreeRouter />
|
|
32
|
+
</TreeProvider>
|
|
30
33
|
</MetaContext>,
|
|
31
34
|
);
|
|
32
35
|
expect(html).toContain('href="http://x.com/sub/util/array"');
|
|
@@ -36,7 +39,9 @@ describe("TreeRouter", () => {
|
|
|
36
39
|
test("root page card links include an APP_URL subfolder exactly once", () => {
|
|
37
40
|
const html = renderToStaticMarkup(
|
|
38
41
|
<MetaContext value={createMeta({ root: "http://x.com/sub/", url: "./" })}>
|
|
39
|
-
<
|
|
42
|
+
<TreeProvider tree={tree}>
|
|
43
|
+
<TreeRouter />
|
|
44
|
+
</TreeProvider>
|
|
40
45
|
</MetaContext>,
|
|
41
46
|
);
|
|
42
47
|
expect(html).toContain('href="http://x.com/sub/util"');
|
package/ui/tree/TreeRouter.tsx
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { ReactElement, ReactNode } from "react";
|
|
2
2
|
import { NotFoundError } from "../../error/RequestError.js";
|
|
3
3
|
import type { AbsolutePath } from "../../util/path.js";
|
|
4
|
-
import type { TreeElement } from "../../util/tree.js";
|
|
5
4
|
import { DocumentationPage } from "../docs/DocumentationPage.js";
|
|
6
5
|
import { createMapper } from "../misc/Mapper.js";
|
|
7
6
|
import { MetaContext, requireMetaURL } from "../misc/MetaContext.js";
|
|
8
7
|
import type { PossibleMeta } from "../util/index.js";
|
|
9
|
-
import {
|
|
8
|
+
import { useTreeMap } from "./TreeContext.js";
|
|
10
9
|
import { TreePage } from "./TreePage.js";
|
|
11
10
|
|
|
12
11
|
/**
|
|
@@ -25,9 +24,6 @@ export const [TreeRouterMapping, TreeRouterMapper] = createMapper({
|
|
|
25
24
|
* @see https://shelving.cc/ui/TreeRouterProps
|
|
26
25
|
*/
|
|
27
26
|
export interface TreeRouterProps extends PossibleMeta {
|
|
28
|
-
/** The tree of elements to match routes for. */
|
|
29
|
-
readonly tree: TreeElement;
|
|
30
|
-
|
|
31
27
|
/**
|
|
32
28
|
* Optional fallback element.
|
|
33
29
|
* - Explicit `null` means fallback to nothing (router will not throw `NotFoundError`).
|
|
@@ -38,7 +34,7 @@ export interface TreeRouterProps extends PossibleMeta {
|
|
|
38
34
|
/**
|
|
39
35
|
* Resolve a URL path to a tree element and render it as a full page.
|
|
40
36
|
*
|
|
41
|
-
* -
|
|
37
|
+
* - Reads the flattened `path` → element map from the surrounding `<TreeProvider>` (`useTreeMap()`), then resolves the current URL with a single `map.get(path)`.
|
|
42
38
|
* - `/` renders the root itself; deeper paths render the matching descendant (composite module names like `/util/string` resolve for free — they're whole keys in the map).
|
|
43
39
|
* - The resolved element is already stamped with its canonical `path`, so the page and its cards link straight to their own paths — nothing needs threading.
|
|
44
40
|
* - To override the renderer for a specific element type, wrap in `<TreeRouterMapping mapping={…}>`.
|
|
@@ -46,17 +42,14 @@ export interface TreeRouterProps extends PossibleMeta {
|
|
|
46
42
|
* @returns The resolved element rendered as a page, or the `fallback`.
|
|
47
43
|
* @throws `NotFoundError` When no element matches the URL and no `fallback` is given.
|
|
48
44
|
* @kind component
|
|
49
|
-
* @example <
|
|
45
|
+
* @example <TreeProvider tree={tree}><TreeRouter /></TreeProvider>
|
|
50
46
|
* @see https://shelving.cc/ui/TreeRouter
|
|
51
47
|
*/
|
|
52
|
-
export function TreeRouter({
|
|
48
|
+
export function TreeRouter({ fallback, ...meta }: TreeRouterProps): ReactNode {
|
|
53
49
|
const { path, ...combined } = requireMetaURL(meta);
|
|
54
|
-
// `<TreeProvider>` flattens the tree (memoised) and exposes it as a lookup map so descendants — the route resolver below, plus `<TreeButton>` / breadcrumbs — all resolve against the one map.
|
|
55
50
|
return (
|
|
56
51
|
<MetaContext value={combined}>
|
|
57
|
-
<
|
|
58
|
-
<TreeRoute path={path} fallback={fallback} />
|
|
59
|
-
</TreeProvider>
|
|
52
|
+
<TreeRoute path={path} fallback={fallback} />
|
|
60
53
|
</MetaContext>
|
|
61
54
|
);
|
|
62
55
|
}
|
package/ui/tree/TreeSidebar.d.ts
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import type { AbsolutePath } from "../../util/path.js";
|
|
3
|
-
import { type TreeElement } from "../../util/tree.js";
|
|
4
2
|
import type { OptionalChildProps } from "../util/index.js";
|
|
5
3
|
/**
|
|
6
|
-
* Props for the `TreeSidebar` component
|
|
4
|
+
* Props for the `TreeSidebar` component.
|
|
7
5
|
*
|
|
8
6
|
* @see https://shelving.cc/ui/TreeSidebarProps
|
|
9
7
|
*/
|
|
10
8
|
export interface TreeSidebarProps extends OptionalChildProps {
|
|
11
|
-
/** Root element of the tree. */
|
|
12
|
-
readonly tree: TreeElement;
|
|
13
|
-
/** URL path of the root — defaults to `/`. Children are rendered with `path + their.name`. */
|
|
14
|
-
readonly path?: AbsolutePath | undefined;
|
|
15
9
|
}
|
|
16
10
|
/**
|
|
17
|
-
* Sidebar built from
|
|
11
|
+
* Sidebar built from the surrounding tree, in three sections separated by dividers.
|
|
18
12
|
*
|
|
19
13
|
* - **Middle:** a `<TextInput>` search-as-you-type filter.
|
|
20
14
|
* - **Bottom:** the root's children as a `<TreeMenuMapper>` — swapped for a flat ranked list of results (capped at 20) while the search holds a query.
|
|
21
15
|
*
|
|
22
|
-
*
|
|
16
|
+
* Reads the flattened tree from the surrounding `<TreeProvider>` (`useTreeMap().get("/")`), so child and result hrefs use each element's stamped canonical `path`. To customise child renderers wrap in `<TreeMenuMapping mapping={…}>` (same context as `<TreeMenu>`).
|
|
23
17
|
*
|
|
24
18
|
* @kind component
|
|
25
19
|
* @returns The sectioned sidebar — home/index links, a search input, and either the tree menu or search results.
|
|
26
|
-
* @example <TreeSidebar
|
|
20
|
+
* @example <TreeSidebar />
|
|
27
21
|
* @see https://shelving.cc/ui/TreeSidebar
|
|
28
22
|
*/
|
|
29
|
-
export declare function TreeSidebar({
|
|
23
|
+
export declare function TreeSidebar({ children }: TreeSidebarProps): ReactNode;
|
package/ui/tree/TreeSidebar.js
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useState } from "react";
|
|
3
3
|
import { filterElements } from "../../util/element.js";
|
|
4
|
-
import {
|
|
4
|
+
import { searchTree } from "../../util/tree.js";
|
|
5
5
|
import { Divider } from "../block/Divider.js";
|
|
6
6
|
import { TextInput } from "../form/TextInput.js";
|
|
7
7
|
import { Menu, MenuItem } from "../menu/Menu.js";
|
|
8
|
+
import { useTreeMap } from "./TreeContext.js";
|
|
8
9
|
import { matchMenuElement, TreeMenuMapper } from "./TreeMenu.js";
|
|
9
10
|
/**
|
|
10
|
-
* Sidebar built from
|
|
11
|
+
* Sidebar built from the surrounding tree, in three sections separated by dividers.
|
|
11
12
|
*
|
|
12
13
|
* - **Middle:** a `<TextInput>` search-as-you-type filter.
|
|
13
14
|
* - **Bottom:** the root's children as a `<TreeMenuMapper>` — swapped for a flat ranked list of results (capped at 20) while the search holds a query.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
+
* Reads the flattened tree from the surrounding `<TreeProvider>` (`useTreeMap().get("/")`), so child and result hrefs use each element's stamped canonical `path`. To customise child renderers wrap in `<TreeMenuMapping mapping={…}>` (same context as `<TreeMenu>`).
|
|
16
17
|
*
|
|
17
18
|
* @kind component
|
|
18
19
|
* @returns The sectioned sidebar — home/index links, a search input, and either the tree menu or search results.
|
|
19
|
-
* @example <TreeSidebar
|
|
20
|
+
* @example <TreeSidebar />
|
|
20
21
|
* @see https://shelving.cc/ui/TreeSidebar
|
|
21
22
|
*/
|
|
22
|
-
export function TreeSidebar({
|
|
23
|
+
export function TreeSidebar({ children }) {
|
|
23
24
|
const [query, setQuery] = useState("");
|
|
24
25
|
const trimmed = query.trim();
|
|
25
|
-
//
|
|
26
|
-
const root =
|
|
27
|
-
const results = trimmed ? searchTree(root, trimmed, { limit: 20 }) : null;
|
|
28
|
-
return (_jsxs(_Fragment, { children: [_jsx(Menu, { children: _jsx(MenuItem, { href:
|
|
26
|
+
// The flattened root from context — its children (and descendants) already carry their canonical `path` and unique `key`.
|
|
27
|
+
const root = useTreeMap().get("/");
|
|
28
|
+
const results = root && trimmed ? searchTree(root, trimmed, { limit: 20 }) : null;
|
|
29
|
+
return (_jsxs(_Fragment, { children: [_jsx(Menu, { children: _jsx(MenuItem, { href: "/", children: "Home" }) }), children, _jsx(Divider, {}), _jsx(TextInput, { name: "search", title: "Search", placeholder: "Search\u2026", value: query, onValue: v => setQuery(v ?? "") }), _jsx(Divider, {}), _jsx(Menu, { children: results ? (results.map(el => (_jsx(MenuItem, { href: el.props.path ?? "/", children: el.props.title ?? el.props.name }, el.key)))) : (_jsx(TreeMenuMapper, { children: filterElements(root?.props.children, matchMenuElement) })) })] }));
|
|
29
30
|
}
|
package/ui/tree/TreeSidebar.md
CHANGED
|
@@ -4,10 +4,11 @@ The default sidebar for a tree-based site: a single "home" link for the root ele
|
|
|
4
4
|
|
|
5
5
|
**Things to know:**
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- Reads the flattened tree from the surrounding `<TreeProvider>` (`useTreeMap().get("/")`), so it takes no `tree` prop — mount it anywhere inside a `<TreeApp>` / `<TreeProvider>`.
|
|
8
|
+
- The home link points at `/`; children's hrefs use each element's stamped canonical `path`.
|
|
8
9
|
- The children render through the same mapper as `<TreeMenu>`, so customise them by wrapping in `<TreeMenuMapping mapping={…}>`.
|
|
9
10
|
- Only directories, files, and `kind: "module"` symbols appear — code symbols are kept off the navigation.
|
|
10
|
-
- Use it directly for finer layout control outside `<TreeApp
|
|
11
|
+
- Use it directly for finer layout control outside the default `<TreeApp>` sidebar.
|
|
11
12
|
|
|
12
13
|
## Usage
|
|
13
14
|
|
|
@@ -15,5 +16,5 @@ The default sidebar for a tree-based site: a single "home" link for the root ele
|
|
|
15
16
|
import { TreeSidebar } from "shelving/ui";
|
|
16
17
|
|
|
17
18
|
// A home link + children menu combined — the default sidebar.
|
|
18
|
-
<TreeSidebar
|
|
19
|
+
<TreeSidebar />
|
|
19
20
|
```
|
package/ui/tree/TreeSidebar.tsx
CHANGED
|
@@ -1,50 +1,45 @@
|
|
|
1
|
-
import { type ReactNode,
|
|
1
|
+
import { type ReactNode, useState } from "react";
|
|
2
2
|
import { filterElements } from "../../util/element.js";
|
|
3
|
-
import
|
|
4
|
-
import { flattenTree, searchTree, type TreeElement } from "../../util/tree.js";
|
|
3
|
+
import { searchTree } from "../../util/tree.js";
|
|
5
4
|
import { Divider } from "../block/Divider.js";
|
|
6
5
|
import { TextInput } from "../form/TextInput.js";
|
|
7
6
|
import { Menu, MenuItem } from "../menu/Menu.js";
|
|
8
7
|
import type { OptionalChildProps } from "../util/index.js";
|
|
8
|
+
import { useTreeMap } from "./TreeContext.js";
|
|
9
9
|
import { matchMenuElement, TreeMenuMapper } from "./TreeMenu.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Props for the `TreeSidebar` component
|
|
12
|
+
* Props for the `TreeSidebar` component.
|
|
13
13
|
*
|
|
14
14
|
* @see https://shelving.cc/ui/TreeSidebarProps
|
|
15
15
|
*/
|
|
16
|
-
export interface TreeSidebarProps extends OptionalChildProps {
|
|
17
|
-
/** Root element of the tree. */
|
|
18
|
-
readonly tree: TreeElement;
|
|
19
|
-
/** URL path of the root — defaults to `/`. Children are rendered with `path + their.name`. */
|
|
20
|
-
readonly path?: AbsolutePath | undefined;
|
|
21
|
-
}
|
|
16
|
+
export interface TreeSidebarProps extends OptionalChildProps {}
|
|
22
17
|
|
|
23
18
|
/**
|
|
24
|
-
* Sidebar built from
|
|
19
|
+
* Sidebar built from the surrounding tree, in three sections separated by dividers.
|
|
25
20
|
*
|
|
26
21
|
* - **Middle:** a `<TextInput>` search-as-you-type filter.
|
|
27
22
|
* - **Bottom:** the root's children as a `<TreeMenuMapper>` — swapped for a flat ranked list of results (capped at 20) while the search holds a query.
|
|
28
23
|
*
|
|
29
|
-
*
|
|
24
|
+
* Reads the flattened tree from the surrounding `<TreeProvider>` (`useTreeMap().get("/")`), so child and result hrefs use each element's stamped canonical `path`. To customise child renderers wrap in `<TreeMenuMapping mapping={…}>` (same context as `<TreeMenu>`).
|
|
30
25
|
*
|
|
31
26
|
* @kind component
|
|
32
27
|
* @returns The sectioned sidebar — home/index links, a search input, and either the tree menu or search results.
|
|
33
|
-
* @example <TreeSidebar
|
|
28
|
+
* @example <TreeSidebar />
|
|
34
29
|
* @see https://shelving.cc/ui/TreeSidebar
|
|
35
30
|
*/
|
|
36
|
-
export function TreeSidebar({
|
|
31
|
+
export function TreeSidebar({ children }: TreeSidebarProps): ReactNode {
|
|
37
32
|
const [query, setQuery] = useState("");
|
|
38
33
|
const trimmed = query.trim();
|
|
39
34
|
|
|
40
|
-
//
|
|
41
|
-
const root =
|
|
42
|
-
const results = trimmed ? searchTree(root, trimmed, { limit: 20 }) : null;
|
|
35
|
+
// The flattened root from context — its children (and descendants) already carry their canonical `path` and unique `key`.
|
|
36
|
+
const root = useTreeMap().get("/");
|
|
37
|
+
const results = root && trimmed ? searchTree(root, trimmed, { limit: 20 }) : null;
|
|
43
38
|
|
|
44
39
|
return (
|
|
45
40
|
<>
|
|
46
41
|
<Menu>
|
|
47
|
-
<MenuItem href=
|
|
42
|
+
<MenuItem href="/">Home</MenuItem>
|
|
48
43
|
</Menu>
|
|
49
44
|
{children}
|
|
50
45
|
<Divider />
|
|
@@ -53,12 +48,12 @@ export function TreeSidebar({ tree, path = "/", children }: TreeSidebarProps): R
|
|
|
53
48
|
<Menu>
|
|
54
49
|
{results ? (
|
|
55
50
|
results.map(el => (
|
|
56
|
-
<MenuItem key={el.key} href={el.props.path ??
|
|
51
|
+
<MenuItem key={el.key} href={el.props.path ?? "/"}>
|
|
57
52
|
{el.props.title ?? el.props.name}
|
|
58
53
|
</MenuItem>
|
|
59
54
|
))
|
|
60
55
|
) : (
|
|
61
|
-
<TreeMenuMapper
|
|
56
|
+
<TreeMenuMapper>{filterElements(root?.props.children, matchMenuElement)}</TreeMenuMapper>
|
|
62
57
|
)}
|
|
63
58
|
</Menu>
|
|
64
59
|
</>
|
package/util/ansi.d.ts
CHANGED
|
@@ -92,64 +92,55 @@ export declare const ANSI_RESET: "\u001B[0m";
|
|
|
92
92
|
/**
|
|
93
93
|
* Wrap a string in the ANSI color/style codes (at the start), and `ANSI_RESET` at the end.
|
|
94
94
|
*
|
|
95
|
-
* -
|
|
95
|
+
* - Colour is only emitted when the runtime supports it, resolved once at module load into `_USES_COLOR` — `FORCE_COLOR` > `NO_COLOR` > TTY detection > default-off.
|
|
96
|
+
* - The default is *off* for non-interactive sinks (files, log aggregators, Workers), so escape codes never pollute non-TTY output unless `FORCE_COLOR` opts back in.
|
|
96
97
|
*
|
|
97
98
|
* @param input The string to wrap in ANSI codes.
|
|
98
99
|
* @param wrappers Any number of ANSI escape codes (e.g. `ANSI_RED`, `ANSI_BOLD`) to prepend before `input`.
|
|
99
|
-
* @returns The wrapped string, or `input` unchanged when
|
|
100
|
+
* @returns The wrapped string, or `input` unchanged when colour is not supported (see precedence above).
|
|
100
101
|
* @example ansiWrap("hello", ANSI_RED, ANSI_BOLD) // "\x1b[31m\x1b[1mhello\x1b[0m"
|
|
101
102
|
* @see https://shelving.cc/util/ansi/ansiWrap
|
|
102
103
|
*/
|
|
103
104
|
export declare function ansiWrap(input: string, ...wrappers: ImmutableArray<string>): string;
|
|
104
105
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* - Used directly inside template literals (`${ANSI_SUCCESS}`), where JavaScript invokes `toString()` automatically, so the icon is coloured at use-time, not at module-load time.
|
|
108
|
-
*
|
|
109
|
-
* @see https://shelving.cc/util/ansi/AnsiIcon
|
|
110
|
-
*/
|
|
111
|
-
export type AnsiIcon = {
|
|
112
|
-
toString(): string;
|
|
113
|
-
};
|
|
114
|
-
/**
|
|
115
|
-
* Lazily blue-coloured waiting icon (`⋯`) for use in template literals.
|
|
106
|
+
* Blue-coloured waiting icon (`⋯`) for use in template literals, resolved once at module load.
|
|
116
107
|
*
|
|
117
108
|
* @see https://shelving.cc/util/ansi/ANSI_WAITING
|
|
118
109
|
*/
|
|
119
|
-
export declare const ANSI_WAITING:
|
|
110
|
+
export declare const ANSI_WAITING: string;
|
|
120
111
|
/**
|
|
121
|
-
*
|
|
112
|
+
* Green-coloured success icon (`✓`) for use in template literals, resolved once at module load.
|
|
122
113
|
*
|
|
123
114
|
* @see https://shelving.cc/util/ansi/ANSI_SUCCESS
|
|
124
115
|
*/
|
|
125
|
-
export declare const ANSI_SUCCESS:
|
|
116
|
+
export declare const ANSI_SUCCESS: string;
|
|
126
117
|
/**
|
|
127
|
-
*
|
|
118
|
+
* Red-coloured failure icon (`✗`) for use in template literals, resolved once at module load.
|
|
128
119
|
*
|
|
129
120
|
* @see https://shelving.cc/util/ansi/ANSI_FAILURE
|
|
130
121
|
*/
|
|
131
|
-
export declare const ANSI_FAILURE:
|
|
122
|
+
export declare const ANSI_FAILURE: string;
|
|
132
123
|
/**
|
|
133
|
-
*
|
|
124
|
+
* Blue-coloured up arrow icon (`↑`) for use in template literals, resolved once at module load.
|
|
134
125
|
*
|
|
135
126
|
* @see https://shelving.cc/util/ansi/ANSI_UP
|
|
136
127
|
*/
|
|
137
|
-
export declare const ANSI_UP:
|
|
128
|
+
export declare const ANSI_UP: string;
|
|
138
129
|
/**
|
|
139
|
-
*
|
|
130
|
+
* Blue-coloured down arrow icon (`↓`) for use in template literals, resolved once at module load.
|
|
140
131
|
*
|
|
141
132
|
* @see https://shelving.cc/util/ansi/ANSI_DOWN
|
|
142
133
|
*/
|
|
143
|
-
export declare const ANSI_DOWN:
|
|
134
|
+
export declare const ANSI_DOWN: string;
|
|
144
135
|
/**
|
|
145
|
-
*
|
|
136
|
+
* Blue-coloured right arrow icon (`→`) for use in template literals, resolved once at module load.
|
|
146
137
|
*
|
|
147
138
|
* @see https://shelving.cc/util/ansi/ANSI_RIGHT
|
|
148
139
|
*/
|
|
149
|
-
export declare const ANSI_RIGHT:
|
|
140
|
+
export declare const ANSI_RIGHT: string;
|
|
150
141
|
/**
|
|
151
|
-
*
|
|
142
|
+
* Blue-coloured left arrow icon (`←`) for use in template literals, resolved once at module load.
|
|
152
143
|
*
|
|
153
144
|
* @see https://shelving.cc/util/ansi/ANSI_LEFT
|
|
154
145
|
*/
|
|
155
|
-
export declare const ANSI_LEFT:
|
|
146
|
+
export declare const ANSI_LEFT: string;
|
package/util/ansi.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DOWN, FAILURE, LEFT, RIGHT, SUCCESS, UP, WAITING } from "./constants.js";
|
|
2
|
-
import {
|
|
2
|
+
import { getEnv } from "./env.js";
|
|
3
3
|
// Colors.
|
|
4
4
|
/**
|
|
5
5
|
* ANSI escape code that resets the foreground colour to the terminal default.
|
|
@@ -93,70 +93,90 @@ export const ANSI_INVERSE = "\x1b[7m";
|
|
|
93
93
|
* @see https://shelving.cc/util/ansi/ANSI_RESET
|
|
94
94
|
*/
|
|
95
95
|
export const ANSI_RESET = "\x1b[0m";
|
|
96
|
+
/**
|
|
97
|
+
* Whether ANSI colour should be emitted, resolved once at module load the way the broader CLI ecosystem does.
|
|
98
|
+
*
|
|
99
|
+
* Precedence, highest first (mirrors the `supports-color` resolution order):
|
|
100
|
+
* 1. `FORCE_COLOR` — override on for any value except `0` / `false` (which forces off). An empty value counts as on.
|
|
101
|
+
* 2. `NO_COLOR` — override off for any non-empty value, per [no-color.org](https://no-color.org).
|
|
102
|
+
* 3. TTY detection — on only when `process.stdout` is an interactive TTY and `TERM` is not `dumb`.
|
|
103
|
+
* 4. Otherwise off — non-interactive sinks (files, log aggregators, serverless platforms like Cloudflare Workers) get no escape codes by default.
|
|
104
|
+
*
|
|
105
|
+
* Resolved once at import rather than on every `ansiWrap()` call: TTY detection makes the load-time value correct in every sink, and the worst case (e.g. `process.env` populated after load) simply falls back to the no-colour default.
|
|
106
|
+
*/
|
|
107
|
+
const _USES_COLOR = (() => {
|
|
108
|
+
// `FORCE_COLOR` overrides everything: "0"/"false" forces off, any other value (including empty) forces on.
|
|
109
|
+
const force = getEnv("FORCE_COLOR");
|
|
110
|
+
if (force !== undefined)
|
|
111
|
+
return force !== "0" && force.toLowerCase() !== "false";
|
|
112
|
+
// `NO_COLOR` with any non-empty value disables colour.
|
|
113
|
+
if (getEnv("NO_COLOR"))
|
|
114
|
+
return false;
|
|
115
|
+
// Otherwise enable colour only for an interactive TTY that isn't a dumb terminal.
|
|
116
|
+
return typeof process === "object" && !!process.stdout?.isTTY && getEnv("TERM") !== "dumb";
|
|
117
|
+
})();
|
|
96
118
|
/**
|
|
97
119
|
* Wrap a string in the ANSI color/style codes (at the start), and `ANSI_RESET` at the end.
|
|
98
120
|
*
|
|
99
|
-
* -
|
|
121
|
+
* - Colour is only emitted when the runtime supports it, resolved once at module load into `_USES_COLOR` — `FORCE_COLOR` > `NO_COLOR` > TTY detection > default-off.
|
|
122
|
+
* - The default is *off* for non-interactive sinks (files, log aggregators, Workers), so escape codes never pollute non-TTY output unless `FORCE_COLOR` opts back in.
|
|
100
123
|
*
|
|
101
124
|
* @param input The string to wrap in ANSI codes.
|
|
102
125
|
* @param wrappers Any number of ANSI escape codes (e.g. `ANSI_RED`, `ANSI_BOLD`) to prepend before `input`.
|
|
103
|
-
* @returns The wrapped string, or `input` unchanged when
|
|
126
|
+
* @returns The wrapped string, or `input` unchanged when colour is not supported (see precedence above).
|
|
104
127
|
* @example ansiWrap("hello", ANSI_RED, ANSI_BOLD) // "\x1b[31m\x1b[1mhello\x1b[0m"
|
|
105
128
|
* @see https://shelving.cc/util/ansi/ansiWrap
|
|
106
129
|
*/
|
|
107
130
|
export function ansiWrap(input, ...wrappers) {
|
|
108
|
-
if (
|
|
131
|
+
if (!_USES_COLOR)
|
|
109
132
|
return input;
|
|
110
133
|
return `${wrappers.join("")}${input}${ANSI_RESET}`;
|
|
111
134
|
}
|
|
112
|
-
/** Create a lazily-coloured {@link AnsiIcon} that wraps `icon` in `wrappers` on each `toString()`. */
|
|
113
|
-
function _createAnsiIcon(icon, ...wrappers) {
|
|
114
|
-
return {
|
|
115
|
-
toString() {
|
|
116
|
-
return ansiWrap(icon, ...wrappers);
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
135
|
// Coloured icons.
|
|
136
|
+
//
|
|
137
|
+
// Each icon is resolved once at module load by `ansiWrap()`, so colour support is detected at import time
|
|
138
|
+
// (a TTY yields a coloured icon; a non-interactive sink like a file or Cloudflare Worker yields the bare
|
|
139
|
+
// glyph). This trades the previous lazy re-evaluation — which honoured `process.env` mutated after load —
|
|
140
|
+
// for plain string constants, since TTY detection means the worst case simply falls back to the no-colour default.
|
|
121
141
|
/**
|
|
122
|
-
*
|
|
142
|
+
* Blue-coloured waiting icon (`⋯`) for use in template literals, resolved once at module load.
|
|
123
143
|
*
|
|
124
144
|
* @see https://shelving.cc/util/ansi/ANSI_WAITING
|
|
125
145
|
*/
|
|
126
|
-
export const ANSI_WAITING =
|
|
146
|
+
export const ANSI_WAITING = ansiWrap(WAITING, ANSI_BLUE);
|
|
127
147
|
/**
|
|
128
|
-
*
|
|
148
|
+
* Green-coloured success icon (`✓`) for use in template literals, resolved once at module load.
|
|
129
149
|
*
|
|
130
150
|
* @see https://shelving.cc/util/ansi/ANSI_SUCCESS
|
|
131
151
|
*/
|
|
132
|
-
export const ANSI_SUCCESS =
|
|
152
|
+
export const ANSI_SUCCESS = ansiWrap(SUCCESS, ANSI_GREEN);
|
|
133
153
|
/**
|
|
134
|
-
*
|
|
154
|
+
* Red-coloured failure icon (`✗`) for use in template literals, resolved once at module load.
|
|
135
155
|
*
|
|
136
156
|
* @see https://shelving.cc/util/ansi/ANSI_FAILURE
|
|
137
157
|
*/
|
|
138
|
-
export const ANSI_FAILURE =
|
|
158
|
+
export const ANSI_FAILURE = ansiWrap(FAILURE, ANSI_RED);
|
|
139
159
|
/**
|
|
140
|
-
*
|
|
160
|
+
* Blue-coloured up arrow icon (`↑`) for use in template literals, resolved once at module load.
|
|
141
161
|
*
|
|
142
162
|
* @see https://shelving.cc/util/ansi/ANSI_UP
|
|
143
163
|
*/
|
|
144
|
-
export const ANSI_UP =
|
|
164
|
+
export const ANSI_UP = ansiWrap(UP, ANSI_BLUE);
|
|
145
165
|
/**
|
|
146
|
-
*
|
|
166
|
+
* Blue-coloured down arrow icon (`↓`) for use in template literals, resolved once at module load.
|
|
147
167
|
*
|
|
148
168
|
* @see https://shelving.cc/util/ansi/ANSI_DOWN
|
|
149
169
|
*/
|
|
150
|
-
export const ANSI_DOWN =
|
|
170
|
+
export const ANSI_DOWN = ansiWrap(DOWN, ANSI_BLUE);
|
|
151
171
|
/**
|
|
152
|
-
*
|
|
172
|
+
* Blue-coloured right arrow icon (`→`) for use in template literals, resolved once at module load.
|
|
153
173
|
*
|
|
154
174
|
* @see https://shelving.cc/util/ansi/ANSI_RIGHT
|
|
155
175
|
*/
|
|
156
|
-
export const ANSI_RIGHT =
|
|
176
|
+
export const ANSI_RIGHT = ansiWrap(RIGHT, ANSI_BLUE);
|
|
157
177
|
/**
|
|
158
|
-
*
|
|
178
|
+
* Blue-coloured left arrow icon (`←`) for use in template literals, resolved once at module load.
|
|
159
179
|
*
|
|
160
180
|
* @see https://shelving.cc/util/ansi/ANSI_LEFT
|
|
161
181
|
*/
|
|
162
|
-
export const ANSI_LEFT =
|
|
182
|
+
export const ANSI_LEFT = ansiWrap(LEFT, ANSI_BLUE);
|