shelving 1.257.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.257.0",
3
+ "version": "1.258.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
package/ui/block/Prose.js CHANGED
@@ -27,7 +27,7 @@ import SUBHEADING_CSS from "./Subheading.module.css";
27
27
  import TITLE_CSS from "./Title.module.css";
28
28
  import VIDEO_CSS from "./Video.module.css";
29
29
  // Combine the `.prose` class from every block and inline component's CSS module into a single string.
30
- const PROSE_STYLES = getClass(getModuleClass(BLOCK_CSS, "block"), getModuleClass(VIDEO_CSS, "video"), getModuleClass(PARAGRAPH_CSS, "prose"), getModuleClass(HEADING_CSS, "prose"), getModuleClass(SUBHEADING_CSS, "prose"), getModuleClass(ADDRESS_CSS, "prose"), getModuleClass(BLOCKQUOTE_CSS, "prose"), getModuleClass(SECTION_CSS, "prose"), getModuleClass(CODE_CSS, "prose"), getModuleClass(DEFINITIONS_CSS, "prose"), getModuleClass(DELETED_CSS, "prose"), getModuleClass(EMPHASIS_CSS, "prose"), getModuleClass(IMAGE_CSS, "prose"), getModuleClass(INSERTED_CSS, "prose"), getModuleClass(CAPTION_CSS, "prose"), getModuleClass(LIST_CSS, "prose"), getModuleClass(TITLE_CSS, "prose"), getModuleClass(LINK_CSS, "prose"), getModuleClass(MARK_CSS, "prose"), getModuleClass(PREFORMATTED_CSS, "prose"), getModuleClass(SMALL_CSS, "prose"), getModuleClass(STRONG_CSS, "prose"), getModuleClass(SUBSCRIPT_CSS, "prose"), getModuleClass(SUPERSCRIPT_CSS, "prose"), getModuleClass(TABLE_CSS, "prose"), getModuleClass(DIVIDER_CSS, "prose"));
30
+ const PROSE_STYLES = getClass(getModuleClass(BLOCK_CSS, "prose"), getModuleClass(VIDEO_CSS, "prose"), getModuleClass(PARAGRAPH_CSS, "prose"), getModuleClass(HEADING_CSS, "prose"), getModuleClass(SUBHEADING_CSS, "prose"), getModuleClass(ADDRESS_CSS, "prose"), getModuleClass(BLOCKQUOTE_CSS, "prose"), getModuleClass(SECTION_CSS, "prose"), getModuleClass(CODE_CSS, "prose"), getModuleClass(DEFINITIONS_CSS, "prose"), getModuleClass(DELETED_CSS, "prose"), getModuleClass(EMPHASIS_CSS, "prose"), getModuleClass(IMAGE_CSS, "prose"), getModuleClass(INSERTED_CSS, "prose"), getModuleClass(CAPTION_CSS, "prose"), getModuleClass(LIST_CSS, "prose"), getModuleClass(TITLE_CSS, "prose"), getModuleClass(LINK_CSS, "prose"), getModuleClass(MARK_CSS, "prose"), getModuleClass(PREFORMATTED_CSS, "prose"), getModuleClass(SMALL_CSS, "prose"), getModuleClass(STRONG_CSS, "prose"), getModuleClass(SUBSCRIPT_CSS, "prose"), getModuleClass(SUPERSCRIPT_CSS, "prose"), getModuleClass(TABLE_CSS, "prose"), getModuleClass(DIVIDER_CSS, "prose"));
31
31
  /**
32
32
  * A section of longform text containing lots of `<p>` or `<ul>` style elements.
33
33
  * - Applies the prose variant of every block and inline component so nested content picks up the right longform spacing and typography.
@@ -30,8 +30,8 @@ import VIDEO_CSS from "./Video.module.css";
30
30
 
31
31
  // Combine the `.prose` class from every block and inline component's CSS module into a single string.
32
32
  const PROSE_STYLES = getClass(
33
- getModuleClass(BLOCK_CSS, "block"),
34
- getModuleClass(VIDEO_CSS, "video"),
33
+ getModuleClass(BLOCK_CSS, "prose"),
34
+ getModuleClass(VIDEO_CSS, "prose"),
35
35
  getModuleClass(PARAGRAPH_CSS, "prose"),
36
36
  getModuleClass(HEADING_CSS, "prose"),
37
37
  getModuleClass(SUBHEADING_CSS, "prose"),
@@ -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, { tree: tree, children: _jsx(Menu, { children: _jsx(MenuItem, { href: "/search", children: "Search" }) }) })), ...meta }) {
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
  }
@@ -14,7 +14,7 @@ export function DocumentationApp({
14
14
  "/search": DocumentationSearchPage,
15
15
  },
16
16
  sidebar = (
17
- <TreeSidebar tree={tree}>
17
+ <TreeSidebar>
18
18
  <Menu>
19
19
  <MenuItem href="/search">Search</MenuItem>
20
20
  </Menu>
@@ -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] & E` — the declared props for that element type, plus any extra props `E` the mapper is configured to thread through.
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<E = unknown> = {
12
- [K in keyof JSX.IntrinsicElements]?: ComponentType<JSX.IntrinsicElements[K] & E>;
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<E = unknown> extends ChildProps {
19
+ export interface MappingProps extends ChildProps {
20
20
  /** Mapping entries that extend or override the inherited mapping inside this subtree. */
21
- readonly mapping: Mapping<E>;
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 type MapperProps<E = unknown> = E & {
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
- * // With extras (`path` threaded into every dispatched child):
54
- * const [TreeMenuMapping, TreeMenuMapper] = createMapper<{ path?: AbsolutePath }>({
55
- * "tree-element": TreeMenuItem,
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<E = unknown>(defaults?: Mapping<E>): [Mapping: FunctionComponent<MappingProps<E>>, Mapper: FunctionComponent<MapperProps<E>>];
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
- * // With extras (`path` threaded into every dispatched child):
25
- * const [TreeMenuMapping, TreeMenuMapper] = createMapper<{ path?: AbsolutePath }>({
26
- * "tree-element": TreeMenuItem,
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, ...extras }) {
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, { ...extras, ...element.props }, element.key));
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
- ```
@@ -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] & E` — the declared props for that element type, plus any extra props `E` the mapper is configured to thread through.
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<E = unknown> = {
14
- [K in keyof JSX.IntrinsicElements]?: ComponentType<JSX.IntrinsicElements[K] & E>;
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<E = unknown> extends ChildProps {
22
+ export interface MappingProps extends ChildProps {
23
23
  /** Mapping entries that extend or override the inherited mapping inside this subtree. */
24
- readonly mapping: Mapping<E>;
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 type MapperProps<E = unknown> = E & {
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
- * // With extras (`path` threaded into every dispatched child):
63
- * const [TreeMenuMapping, TreeMenuMapper] = createMapper<{ path?: AbsolutePath }>({
64
- * "tree-element": TreeMenuItem,
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<E = unknown>(
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<E>): ReactNode {
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, ...extras }: MapperProps<E>): ReactNode {
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} {...extras} {...element.props} />);
84
+ items.push(<Component key={element.key} {...element.props} />);
91
85
  } else {
92
86
  items.push(element);
93
87
  }
@@ -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>`.
@@ -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, { tree: tree }), ...meta }) {
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, { tree: tree, fallback: fallback }) }) }) }) }));
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
  }
@@ -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 tree={tree} />, ...meta }: TreeAppProps): ReactElement {
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
- <SidebarLayout sidebar={sidebar}>
51
- <TreeRouter tree={tree} fallback={fallback} />
52
- </SidebarLayout>
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>
@@ -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<unknown>>, TreeCardMapper: import("react").FunctionComponent<{
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
  *
@@ -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
- * - Computes its own URL path by appending its `name` to the parent's `path`.
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 plus the parent's `path`.
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} path="/" />
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 & TreeMenuExtras): ReactNode;
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<TreeMenuExtras>>, TreeMenuMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps<TreeMenuExtras>>;
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={tree} />
54
+ * @example <TreeMenu tree={useTreeMap().get("/")} />
62
55
  * @see https://shelving.cc/ui/TreeMenu
63
56
  */
64
- export declare function TreeMenu({ path, tree }: TreeMenuProps): ReactNode;
65
- export {};
57
+ export declare function TreeMenu({ tree }: TreeMenuProps): ReactNode;
@@ -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
- * - Computes its own URL path by appending its `name` to the parent's `path`.
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 plus the parent's `path`.
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} path="/" />
33
+ * @example <TreeMenuItem {...element.props} />
35
34
  * @see https://shelving.cc/ui/TreeMenuItem
36
35
  */
37
- export function TreeMenuItem({ path = "/", name, title, children }) {
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: href, children: [title ?? name, submenu.length ? (_jsx(Menu, { children: _jsx(TreeMenuMapper, { path: href, children: submenu }) })) : null] }));
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={tree} />
59
+ * @example <TreeMenu tree={useTreeMap().get("/")} />
61
60
  * @see https://shelving.cc/ui/TreeMenu
62
61
  */
63
- export function TreeMenu({ path = "/", tree }) {
64
- return (_jsx(Menu, { children: _jsx(TreeMenuMapper, { path: path, children: filterElements(tree.props.children, matchMenuElement) }) }));
62
+ export function TreeMenu({ tree }) {
63
+ return (_jsx(Menu, { children: _jsx(TreeMenuMapper, { children: filterElements(tree.props.children, matchMenuElement) }) }));
65
64
  }
@@ -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 computes its own href by appending its `name` to the parent `path` (defaulting to `/`).
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 a subtree's children.
18
- <TreeMenu tree={section} path="/docs" />
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:
@@ -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
- * - Computes its own URL path by appending its `name` to the parent's `path`.
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 plus the parent's `path`.
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} path="/" />
34
+ * @example <TreeMenuItem {...element.props} />
42
35
  * @see https://shelving.cc/ui/TreeMenuItem
43
36
  */
44
- export function TreeMenuItem({ path = "/", name, title, children }: TreeElementProps & TreeMenuExtras): ReactNode {
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={href}>
40
+ <MenuItem href={path}>
49
41
  {title ?? name}
50
42
  {submenu.length ? (
51
43
  <Menu>
52
- <TreeMenuMapper path={href}>{submenu}</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<TreeMenuExtras>({
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={tree} />
81
+ * @example <TreeMenu tree={useTreeMap().get("/")} />
91
82
  * @see https://shelving.cc/ui/TreeMenu
92
83
  */
93
- export function TreeMenu({ path = "/", tree }: TreeMenuProps): ReactNode {
84
+ export function TreeMenu({ tree }: TreeMenuProps): ReactNode {
94
85
  return (
95
86
  <Menu>
96
- <TreeMenuMapper path={path}>{filterElements(tree.props.children, matchMenuElement)}</TreeMenuMapper>
87
+ <TreeMenuMapper>{filterElements(tree.props.children, matchMenuElement)}</TreeMenuMapper>
97
88
  </Menu>
98
89
  );
99
90
  }
@@ -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<unknown>>, TreeRouterMapper: import("react").FunctionComponent<{
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
- * - Flattens the tree once (via `<TreeProvider>`) into a `path` → element map, then resolves the current URL with a single `map.get(path)`.
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 <TreeRouter tree={tree} />
32
+ * @example <TreeProvider tree={tree}><TreeRouter /></TreeProvider>
38
33
  * @see https://shelving.cc/ui/TreeRouter
39
34
  */
40
- export declare function TreeRouter({ tree, fallback, ...meta }: TreeRouterProps): ReactNode;
35
+ export declare function TreeRouter({ fallback, ...meta }: TreeRouterProps): ReactNode;
@@ -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 { TreeProvider, useTreeMap } from "./TreeContext.js";
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
- * - Flattens the tree once (via `<TreeProvider>`) into a `path` → element map, then resolves the current URL with a single `map.get(path)`.
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 <TreeRouter tree={tree} />
28
+ * @example <TreeProvider tree={tree}><TreeRouter /></TreeProvider>
29
29
  * @see https://shelving.cc/ui/TreeRouter
30
30
  */
31
- export function TreeRouter({ tree, fallback, ...meta }) {
31
+ export function TreeRouter({ fallback, ...meta }) {
32
32
  const { path, ...combined } = requireMetaURL(meta);
33
- // `<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.
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
- <TreeRouter tree={tree} />
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
- <TreeRouter tree={tree} />
42
+ <TreeProvider tree={tree}>
43
+ <TreeRouter />
44
+ </TreeProvider>
40
45
  </MetaContext>,
41
46
  );
42
47
  expect(html).toContain('href="http://x.com/sub/util"');
@@ -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 { TreeProvider, useTreeMap } from "./TreeContext.js";
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
- * - Flattens the tree once (via `<TreeProvider>`) into a `path` → element map, then resolves the current URL with a single `map.get(path)`.
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 <TreeRouter tree={tree} />
45
+ * @example <TreeProvider tree={tree}><TreeRouter /></TreeProvider>
50
46
  * @see https://shelving.cc/ui/TreeRouter
51
47
  */
52
- export function TreeRouter({ tree, fallback, ...meta }: TreeRouterProps): ReactNode {
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
- <TreeProvider tree={tree}>
58
- <TreeRoute path={path} fallback={fallback} />
59
- </TreeProvider>
52
+ <TreeRoute path={path} fallback={fallback} />
60
53
  </MetaContext>
61
54
  );
62
55
  }
@@ -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 — the root tree element plus its URL path.
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 a tree element, in three sections separated by dividers.
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
- * Child and result hrefs use each element's canonical `path` (or `joinPath(parent, name)` as a fallback). To customise child renderers wrap in `<TreeMenuMapping mapping={…}>` (same context as `<TreeMenu>`).
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 tree={tree} />
20
+ * @example <TreeSidebar />
27
21
  * @see https://shelving.cc/ui/TreeSidebar
28
22
  */
29
- export declare function TreeSidebar({ tree, path, children }: TreeSidebarProps): ReactNode;
23
+ export declare function TreeSidebar({ children }: TreeSidebarProps): ReactNode;
@@ -1,29 +1,30 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
2
+ import { useState } from "react";
3
3
  import { filterElements } from "../../util/element.js";
4
- import { flattenTree, searchTree } from "../../util/tree.js";
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 a tree element, in three sections separated by dividers.
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
- * Child and result hrefs use each element's canonical `path` (or `joinPath(parent, name)` as a fallback). To customise child renderers wrap in `<TreeMenuMapping mapping={…}>` (same context as `<TreeMenu>`).
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 tree={tree} />
20
+ * @example <TreeSidebar />
20
21
  * @see https://shelving.cc/ui/TreeSidebar
21
22
  */
22
- export function TreeSidebar({ tree, path = "/", children }) {
23
+ export function TreeSidebar({ children }) {
23
24
  const [query, setQuery] = useState("");
24
25
  const trimmed = query.trim();
25
- // Flatten once so search results carry a canonical `path` (and a unique `key`) for their links (the sidebar sits outside the router's `<TreeProvider>`).
26
- const root = useMemo(() => flattenTree(tree).get("/") ?? tree, [tree]);
27
- const results = trimmed ? searchTree(root, trimmed, { limit: 20 }) : null;
28
- return (_jsxs(_Fragment, { children: [_jsx(Menu, { children: _jsx(MenuItem, { href: path, 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 ?? path, children: el.props.title ?? el.props.name }, el.key)))) : (_jsx(TreeMenuMapper, { path: path, children: filterElements(tree.props.children, matchMenuElement) })) })] }));
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
  }
@@ -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
- - The home link uses `path` as its href (defaulting to `/`); children's hrefs are computed by appending their `name` to the root path.
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 tree={root} />
19
+ <TreeSidebar />
19
20
  ```
@@ -1,50 +1,45 @@
1
- import { type ReactNode, useMemo, useState } from "react";
1
+ import { type ReactNode, useState } from "react";
2
2
  import { filterElements } from "../../util/element.js";
3
- import type { AbsolutePath } from "../../util/path.js";
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 — the root tree element plus its URL path.
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 a tree element, in three sections separated by dividers.
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
- * Child and result hrefs use each element's canonical `path` (or `joinPath(parent, name)` as a fallback). To customise child renderers wrap in `<TreeMenuMapping mapping={…}>` (same context as `<TreeMenu>`).
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 tree={tree} />
28
+ * @example <TreeSidebar />
34
29
  * @see https://shelving.cc/ui/TreeSidebar
35
30
  */
36
- export function TreeSidebar({ tree, path = "/", children }: TreeSidebarProps): ReactNode {
31
+ export function TreeSidebar({ children }: TreeSidebarProps): ReactNode {
37
32
  const [query, setQuery] = useState("");
38
33
  const trimmed = query.trim();
39
34
 
40
- // Flatten once so search results carry a canonical `path` (and a unique `key`) for their links (the sidebar sits outside the router's `<TreeProvider>`).
41
- const root = useMemo(() => flattenTree(tree).get("/") ?? tree, [tree]);
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={path}>Home</MenuItem>
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 ?? 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 path={path}>{filterElements(tree.props.children, matchMenuElement)}</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
- * - The `NO_COLOR` environment variable is read live on every call, so runtimes that populate `process.env` late (e.g. Cloudflare Workers, where `[vars]` bindings are only reliably available within the request scope) are honoured rather than baking in whatever `NO_COLOR` was at module-load time.
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 the `NO_COLOR` environment variable is set.
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
- * A lazily-coloured icon that re-evaluates its ANSI colouring against the live `NO_COLOR` environment variable every time it is converted to a string.
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: AnsiIcon;
110
+ export declare const ANSI_WAITING: string;
120
111
  /**
121
- * Lazily green-coloured success icon (`✓`) for use in template literals.
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: AnsiIcon;
116
+ export declare const ANSI_SUCCESS: string;
126
117
  /**
127
- * Lazily red-coloured failure icon (`✗`) for use in template literals.
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: AnsiIcon;
122
+ export declare const ANSI_FAILURE: string;
132
123
  /**
133
- * Lazily blue-coloured up arrow icon (`↑`) for use in template literals.
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: AnsiIcon;
128
+ export declare const ANSI_UP: string;
138
129
  /**
139
- * Lazily blue-coloured down arrow icon (`↓`) for use in template literals.
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: AnsiIcon;
134
+ export declare const ANSI_DOWN: string;
144
135
  /**
145
- * Lazily blue-coloured right arrow icon (`→`) for use in template literals.
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: AnsiIcon;
140
+ export declare const ANSI_RIGHT: string;
150
141
  /**
151
- * Lazily blue-coloured left arrow icon (`←`) for use in template literals.
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: AnsiIcon;
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 { getEnvBoolean } from "./env.js";
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
- * - The `NO_COLOR` environment variable is read live on every call, so runtimes that populate `process.env` late (e.g. Cloudflare Workers, where `[vars]` bindings are only reliably available within the request scope) are honoured rather than baking in whatever `NO_COLOR` was at module-load time.
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 the `NO_COLOR` environment variable is set.
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 (getEnvBoolean("NO_COLOR"))
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
- * Lazily blue-coloured waiting icon (`⋯`) for use in template literals.
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 = _createAnsiIcon(WAITING, ANSI_BLUE);
146
+ export const ANSI_WAITING = ansiWrap(WAITING, ANSI_BLUE);
127
147
  /**
128
- * Lazily green-coloured success icon (`✓`) for use in template literals.
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 = _createAnsiIcon(SUCCESS, ANSI_GREEN);
152
+ export const ANSI_SUCCESS = ansiWrap(SUCCESS, ANSI_GREEN);
133
153
  /**
134
- * Lazily red-coloured failure icon (`✗`) for use in template literals.
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 = _createAnsiIcon(FAILURE, ANSI_RED);
158
+ export const ANSI_FAILURE = ansiWrap(FAILURE, ANSI_RED);
139
159
  /**
140
- * Lazily blue-coloured up arrow icon (`↑`) for use in template literals.
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 = _createAnsiIcon(UP, ANSI_BLUE);
164
+ export const ANSI_UP = ansiWrap(UP, ANSI_BLUE);
145
165
  /**
146
- * Lazily blue-coloured down arrow icon (`↓`) for use in template literals.
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 = _createAnsiIcon(DOWN, ANSI_BLUE);
170
+ export const ANSI_DOWN = ansiWrap(DOWN, ANSI_BLUE);
151
171
  /**
152
- * Lazily blue-coloured right arrow icon (`→`) for use in template literals.
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 = _createAnsiIcon(RIGHT, ANSI_BLUE);
176
+ export const ANSI_RIGHT = ansiWrap(RIGHT, ANSI_BLUE);
157
177
  /**
158
- * Lazily blue-coloured left arrow icon (`←`) for use in template literals.
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 = _createAnsiIcon(LEFT, ANSI_BLUE);
182
+ export const ANSI_LEFT = ansiWrap(LEFT, ANSI_BLUE);
@@ -1,22 +0,0 @@
1
- @import "../style/layers.css";
2
- @import "../style/Space.module.css";
3
-
4
- @layer components {
5
- .prose {
6
- position: relative;
7
- display: block;
8
- margin-inline: 0;
9
- margin-block: var(--prose-space, var(--space-paragraph));
10
- }
11
- }
12
-
13
- @layer overrides {
14
- .prose {
15
- &:first-child {
16
- margin-block-start: 0;
17
- }
18
- &:last-child {
19
- margin-block-end: 0;
20
- }
21
- }
22
- }