shelving 1.205.0 → 1.206.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/endpoint/Endpoint.js +3 -3
- package/extract/FileExtractor.d.ts +3 -3
- package/extract/FileExtractor.js +6 -6
- package/package.json +8 -7
- package/react/index.d.ts +0 -1
- package/react/index.js +0 -1
- package/react/useInstance.js +10 -7
- package/react/useLazy.js +10 -7
- package/react/useMap.d.ts +1 -1
- package/react/useMap.js +2 -5
- package/react/useReduce.js +3 -3
- package/react/useStore.js +14 -11
- package/store/Store.d.ts +7 -2
- package/store/Store.js +8 -0
- package/ui/app/App.d.ts +2 -2
- package/ui/app/App.js +4 -3
- package/ui/app/App.tsx +5 -4
- package/ui/block/Section.d.ts +2 -2
- package/ui/block/Section.js +1 -1
- package/ui/block/Section.tsx +2 -2
- package/ui/misc/MetaContext.d.ts +12 -0
- package/ui/misc/MetaContext.js +15 -0
- package/ui/misc/MetaContext.tsx +22 -0
- package/ui/misc/index.d.ts +1 -1
- package/ui/misc/index.js +1 -1
- package/ui/misc/index.tsx +1 -1
- package/ui/page/HTML.d.ts +5 -5
- package/ui/page/HTML.js +8 -8
- package/ui/page/HTML.tsx +16 -8
- package/ui/page/Head.d.ts +6 -1
- package/ui/page/Head.js +20 -13
- package/ui/page/Head.tsx +26 -20
- package/ui/page/Page.d.ts +4 -4
- package/ui/page/Page.js +6 -5
- package/ui/page/Page.tsx +8 -7
- package/ui/router/Navigation.d.ts +25 -0
- package/ui/router/Navigation.js +56 -0
- package/ui/router/Navigation.tsx +75 -0
- package/ui/router/NavigationContext.d.ts +5 -0
- package/ui/router/NavigationContext.js +9 -0
- package/ui/router/NavigationContext.tsx +12 -0
- package/ui/router/NavigationStore.d.ts +11 -0
- package/ui/router/{RouterStore.js → NavigationStore.js} +6 -10
- package/ui/router/NavigationStore.tsx +22 -0
- package/ui/router/README.md +186 -0
- package/ui/router/Router.d.ts +10 -28
- package/ui/router/Router.js +39 -63
- package/ui/router/Router.tsx +38 -79
- package/ui/router/Routes.d.ts +10 -21
- package/ui/router/Routes.js +1 -35
- package/ui/router/Routes.tsx +10 -40
- package/ui/router/index.d.ts +3 -2
- package/ui/router/index.js +3 -2
- package/ui/router/index.ts +3 -2
- package/ui/tree/TreeApp.d.ts +4 -4
- package/ui/tree/TreeApp.js +6 -6
- package/ui/tree/TreeApp.tsx +11 -14
- package/ui/util/meta.d.ts +3 -3
- package/ui/util/meta.ts +3 -3
- package/util/start.d.ts +3 -1
- package/util/start.js +3 -0
- package/util/template.d.ts +18 -10
- package/util/template.js +70 -21
- package/react/useProps.d.ts +0 -6
- package/react/useProps.js +0 -5
- package/ui/misc/Meta.d.ts +0 -9
- package/ui/misc/Meta.js +0 -14
- package/ui/misc/Meta.tsx +0 -20
- package/ui/router/RouterContext.d.ts +0 -5
- package/ui/router/RouterContext.js +0 -9
- package/ui/router/RouterContext.tsx +0 -12
- package/ui/router/RouterStore.d.ts +0 -13
- package/ui/router/RouterStore.test.tsx +0 -43
- package/ui/router/RouterStore.tsx +0 -29
package/api/endpoint/Endpoint.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RequiredError } from "../../error/RequiredError.js";
|
|
2
2
|
import { UNDEFINED } from "../../schema/Schema.js";
|
|
3
3
|
import { isData } from "../../util/data.js";
|
|
4
|
-
import { getPlaceholders,
|
|
4
|
+
import { getPlaceholders, matchPathTemplate, renderPathTemplate } from "../../util/template.js";
|
|
5
5
|
/**
|
|
6
6
|
* An abstract API resource definition, used to specify types for e.g. serverless functions.
|
|
7
7
|
*
|
|
@@ -40,7 +40,7 @@ export class Endpoint {
|
|
|
40
40
|
// Placeholders.
|
|
41
41
|
if (this.placeholders.length) {
|
|
42
42
|
assertPlaceholderPayload(payload, this, caller);
|
|
43
|
-
return
|
|
43
|
+
return renderPathTemplate(this.path, payload, caller);
|
|
44
44
|
}
|
|
45
45
|
// No placeholders.
|
|
46
46
|
return this.path;
|
|
@@ -51,7 +51,7 @@ export class Endpoint {
|
|
|
51
51
|
match(method, path, caller = this.match) {
|
|
52
52
|
if (method !== this.method)
|
|
53
53
|
return undefined;
|
|
54
|
-
return
|
|
54
|
+
return matchPathTemplate(this.path, path, caller);
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
57
|
* Create an endpoint handler pairing for this endpoint.
|
|
@@ -4,7 +4,8 @@ import { Extractor } from "./Extractor.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Base extractor for a file in a tree.
|
|
6
6
|
* - Reads the file's content as text and stores it in `content`.
|
|
7
|
-
* - Sets `key` to the slugified basename
|
|
7
|
+
* - Sets `key` to the slugified basename without extension.
|
|
8
|
+
* - Sets `name` to the display-ready basename without extension (e.g. `"OptionalSchema"`, not `"OptionalSchema.ts"`).
|
|
8
9
|
* - Does NOT set `title` — `title` is only set by subclasses that have a confident source for one
|
|
9
10
|
* (e.g. `MarkdownExtractor` uses the first `<h1>`). Renderers fall back to `name` when missing.
|
|
10
11
|
* - Subclasses (e.g. `MarkdownExtractor`, `TypescriptExtractor`) override `extractProps()` to parse the content into richer elements.
|
|
@@ -13,8 +14,7 @@ export declare class FileExtractor extends Extractor<BunFile, FileElement> {
|
|
|
13
14
|
extract(file: BunFile): Promise<FileElement>;
|
|
14
15
|
/**
|
|
15
16
|
* Build the file element props from the extracted content.
|
|
16
|
-
* - `name` is the basename
|
|
17
|
-
* - `base` is the basename without extension (e.g. `"array"`) — useful as a title fallback.
|
|
17
|
+
* - `name` is the basename without extension (e.g. `"array"`) — display-ready, used by menus and cards.
|
|
18
18
|
* - Override to parse `text` into richer elements (content/children/description) and to set
|
|
19
19
|
* `title` if a confident title is available.
|
|
20
20
|
*/
|
package/extract/FileExtractor.js
CHANGED
|
@@ -5,7 +5,8 @@ import { Extractor } from "./Extractor.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* Base extractor for a file in a tree.
|
|
7
7
|
* - Reads the file's content as text and stores it in `content`.
|
|
8
|
-
* - Sets `key` to the slugified basename
|
|
8
|
+
* - Sets `key` to the slugified basename without extension.
|
|
9
|
+
* - Sets `name` to the display-ready basename without extension (e.g. `"OptionalSchema"`, not `"OptionalSchema.ts"`).
|
|
9
10
|
* - Does NOT set `title` — `title` is only set by subclasses that have a confident source for one
|
|
10
11
|
* (e.g. `MarkdownExtractor` uses the first `<h1>`). Renderers fall back to `name` when missing.
|
|
11
12
|
* - Subclasses (e.g. `MarkdownExtractor`, `TypescriptExtractor`) override `extractProps()` to parse the content into richer elements.
|
|
@@ -13,18 +14,17 @@ import { Extractor } from "./Extractor.js";
|
|
|
13
14
|
export class FileExtractor extends Extractor {
|
|
14
15
|
async extract(file) {
|
|
15
16
|
const path = file.name ?? "unnamed";
|
|
16
|
-
const
|
|
17
|
-
const [base =
|
|
17
|
+
const filename = isAbsolutePath(path) ? (splitAbsolutePath(path).at(-1) ?? "unnamed") : path;
|
|
18
|
+
const [base = filename] = splitFileExtension(filename);
|
|
18
19
|
return {
|
|
19
20
|
type: "tree-file",
|
|
20
21
|
key: requireSlug(base),
|
|
21
|
-
props: this.extractProps(
|
|
22
|
+
props: this.extractProps(base, await file.text()),
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
26
|
* Build the file element props from the extracted content.
|
|
26
|
-
* - `name` is the basename
|
|
27
|
-
* - `base` is the basename without extension (e.g. `"array"`) — useful as a title fallback.
|
|
27
|
+
* - `name` is the basename without extension (e.g. `"array"`) — display-ready, used by menus and cards.
|
|
28
28
|
* - Override to parse `text` into richer elements (content/children/description) and to set
|
|
29
29
|
* `title` if a confident title is available.
|
|
30
30
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shelving",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.206.1",
|
|
4
4
|
"author": "Dave Houlbrooke <dave@shax.com>",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,22 +10,23 @@
|
|
|
10
10
|
"module": "./index.js",
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@biomejs/biome": "^2.4.15",
|
|
13
|
-
"@google-cloud/firestore": "^8.
|
|
13
|
+
"@google-cloud/firestore": "^8.6.0",
|
|
14
14
|
"@heroicons/react": "^2.2.0",
|
|
15
|
-
"@types/bun": "^1.3.
|
|
15
|
+
"@types/bun": "^1.3.14",
|
|
16
16
|
"@types/react": "^19.2.14",
|
|
17
17
|
"@types/react-dom": "^19.2.3",
|
|
18
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
18
|
+
"@typescript/native-preview": "^7.0.0-dev.20260514.1",
|
|
19
19
|
"firebase": "^12.13.0",
|
|
20
|
-
"react": "^19.3.0-canary-
|
|
21
|
-
"react-dom": "canary",
|
|
20
|
+
"react": "^19.3.0-canary-fef12a01-20260413",
|
|
21
|
+
"react-dom": "^19.3.0-canary-fef12a01-20260413",
|
|
22
22
|
"typescript": "^5.9.3"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"@google-cloud/firestore": ">=7.0.0",
|
|
26
26
|
"@heroicons/react": ">=2.0.0",
|
|
27
27
|
"firebase": ">=11.0.0",
|
|
28
|
-
"react": ">=19.0.0"
|
|
28
|
+
"react": ">=19.0.0",
|
|
29
|
+
"react-dom": ">=19.0.0"
|
|
29
30
|
},
|
|
30
31
|
"exports": {
|
|
31
32
|
".": "./index.js",
|
package/react/index.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ export * from "./createDBContext.js";
|
|
|
3
3
|
export * from "./useInstance.js";
|
|
4
4
|
export * from "./useLazy.js";
|
|
5
5
|
export * from "./useMap.js";
|
|
6
|
-
export * from "./useProps.js";
|
|
7
6
|
export * from "./useReduce.js";
|
|
8
7
|
export * from "./useSequence.js";
|
|
9
8
|
export * from "./useStore.js";
|
package/react/index.js
CHANGED
|
@@ -3,7 +3,6 @@ export * from "./createDBContext.js";
|
|
|
3
3
|
export * from "./useInstance.js";
|
|
4
4
|
export * from "./useLazy.js";
|
|
5
5
|
export * from "./useMap.js";
|
|
6
|
-
export * from "./useProps.js";
|
|
7
6
|
export * from "./useReduce.js";
|
|
8
7
|
export * from "./useSequence.js";
|
|
9
8
|
export * from "./useStore.js";
|
package/react/useInstance.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
1
2
|
import { isArrayEqual } from "../util/equal.js";
|
|
2
|
-
import { useProps } from "./useProps.js";
|
|
3
3
|
/**
|
|
4
4
|
* Use a memoised class instance.
|
|
5
5
|
* - Creates a new instance of `Constructor` using `args`
|
|
6
6
|
* - Returns same instance for as long as `args` is equal to previous `args`.
|
|
7
7
|
*/
|
|
8
8
|
export function useInstance(Constructor, ...args) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
const _internals = (useRef(undefined).current ??= {
|
|
10
|
+
instance: new Constructor(...args),
|
|
11
|
+
args,
|
|
12
|
+
});
|
|
13
|
+
// Update `_internals` if `args` changes.
|
|
14
|
+
if (!isArrayEqual(args, _internals.args)) {
|
|
15
|
+
_internals.instance = new Constructor(...args);
|
|
16
|
+
_internals.args = args;
|
|
14
17
|
}
|
|
15
|
-
return
|
|
18
|
+
return _internals.instance;
|
|
16
19
|
}
|
package/react/useLazy.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
1
2
|
import { isArrayEqual } from "../util/equal.js";
|
|
2
3
|
import { getLazy } from "../util/lazy.js";
|
|
3
|
-
import { useProps } from "./useProps.js";
|
|
4
4
|
export function useLazy(value, ...args) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
const _internals = (useRef(undefined).current ??= {
|
|
6
|
+
value: getLazy(value, ...args),
|
|
7
|
+
args,
|
|
8
|
+
});
|
|
9
|
+
// Update `_internals` if `args` changes.
|
|
10
|
+
if (!isArrayEqual(args, _internals.args)) {
|
|
11
|
+
_internals.value = getLazy(value, ...args);
|
|
12
|
+
_internals.args = args;
|
|
10
13
|
}
|
|
11
|
-
return
|
|
14
|
+
return _internals.value;
|
|
12
15
|
}
|
package/react/useMap.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/** Create a mutable Map that
|
|
1
|
+
/** Create a mutable Map that persists for the lifetime of the component. */
|
|
2
2
|
export declare function useMap<K, V>(): Map<K, V>;
|
package/react/useMap.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { useRef } from "react";
|
|
2
|
-
/** Create a mutable Map that
|
|
2
|
+
/** Create a mutable Map that persists for the lifetime of the component. */
|
|
3
3
|
export function useMap() {
|
|
4
|
-
|
|
5
|
-
if (!ref.current)
|
|
6
|
-
ref.current = new Map();
|
|
7
|
-
return ref.current;
|
|
4
|
+
return (useRef(undefined).current ??= new Map());
|
|
8
5
|
}
|
package/react/useReduce.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useRef } from "react";
|
|
2
2
|
export function useReduce(reduce, ...args) {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
return
|
|
3
|
+
const _ref = useRef(undefined);
|
|
4
|
+
_ref.current = reduce(_ref.current, ...args);
|
|
5
|
+
return _ref.current;
|
|
6
6
|
}
|
package/react/useStore.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { useSyncExternalStore } from "react";
|
|
2
2
|
import { BLACKHOLE } from "../util/function.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { STOPHOLE } from "../util/index.js";
|
|
4
|
+
// We add an `[EXTERNAL_STORE]` symbol key to the `Store` instance to cache the subscribe and getSnapshot functions for `useSyncExternalStore()`.
|
|
5
|
+
const EXTERNAL_STORE = Symbol();
|
|
6
|
+
const EXTERNAL_BLACKHOLE = {
|
|
7
|
+
subscribe: STOPHOLE,
|
|
8
|
+
getSnapshot: BLACKHOLE,
|
|
9
|
+
};
|
|
5
10
|
export function useStore(store) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
useSyncExternalStore(internals.subscribe, internals.getSnapshot);
|
|
11
|
+
const { subscribe, getSnapshot } = !store
|
|
12
|
+
? EXTERNAL_BLACKHOLE
|
|
13
|
+
: (store[EXTERNAL_STORE] ??= {
|
|
14
|
+
subscribe: c => store.subscribe(c, c),
|
|
15
|
+
getSnapshot: () => store.snapshot,
|
|
16
|
+
});
|
|
17
|
+
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
15
18
|
return store;
|
|
16
19
|
}
|
package/store/Store.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DeferredSequence } from "../sequence/DeferredSequence.js";
|
|
2
2
|
import { NONE, SKIP } from "../util/constants.js";
|
|
3
|
-
import type { AnyCaller, Arguments } from "../util/function.js";
|
|
4
|
-
import { type PossibleStarter } from "../util/start.js";
|
|
3
|
+
import type { AnyCaller, Arguments, Callback, ErrorCallback, ValueCallback } from "../util/function.js";
|
|
4
|
+
import { type PossibleStarter, type StopCallback } from "../util/start.js";
|
|
5
5
|
/** Any `Store` instance. */
|
|
6
6
|
export type AnyStore = Store<any, any>;
|
|
7
7
|
/** Values that a store natively knows how to process as inputs. */
|
|
@@ -166,4 +166,9 @@ export declare class Store<T, TT = T> implements AsyncIterable<T, void, void>, A
|
|
|
166
166
|
[Symbol.asyncIterator](): AsyncIterator<T, void, void>;
|
|
167
167
|
private _iterating;
|
|
168
168
|
[Symbol.asyncDispose](): Promise<void>;
|
|
169
|
+
/**
|
|
170
|
+
* Subscribe to this store with handlers.
|
|
171
|
+
* - Returns a `StopCallback` to stop the subscription.
|
|
172
|
+
*/
|
|
173
|
+
subscribe(onNext?: ValueCallback<T>, onError?: ErrorCallback, onReturn?: Callback): StopCallback;
|
|
169
174
|
}
|
package/store/Store.js
CHANGED
|
@@ -3,6 +3,7 @@ import { isAsync } from "../util/async.js";
|
|
|
3
3
|
import { NONE, SKIP } from "../util/constants.js";
|
|
4
4
|
import { awaitDispose } from "../util/dispose.js";
|
|
5
5
|
import { isDeepEqual } from "../util/equal.js";
|
|
6
|
+
import { runSequence } from "../util/index.js";
|
|
6
7
|
import { getStarter } from "../util/start.js";
|
|
7
8
|
/**
|
|
8
9
|
* Store that retains its most recent value and is async-iterable to allow values to be observed.
|
|
@@ -289,6 +290,13 @@ export class Store {
|
|
|
289
290
|
await awaitDispose(this._starter, // Stop the starter.
|
|
290
291
|
this.next);
|
|
291
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Subscribe to this store with handlers.
|
|
295
|
+
* - Returns a `StopCallback` to stop the subscription.
|
|
296
|
+
*/
|
|
297
|
+
subscribe(onNext, onError, onReturn) {
|
|
298
|
+
return runSequence(this, onNext, onError, onReturn);
|
|
299
|
+
}
|
|
292
300
|
}
|
|
293
301
|
/** Call a callback but always return or resolve to `SKIP` */
|
|
294
302
|
function _callSkipped(callback, ...args) {
|
package/ui/app/App.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
-
import type { PossibleMeta } from "../util/
|
|
2
|
+
import type { PossibleMeta } from "../util/index.js";
|
|
3
3
|
export interface AppProps extends PossibleMeta {
|
|
4
4
|
children: ReactNode;
|
|
5
5
|
}
|
|
@@ -8,4 +8,4 @@ export interface AppProps extends PossibleMeta {
|
|
|
8
8
|
* - Adds the theme CSS class (which sets CSS token variables on `:root`) to `document.body` on mount and removes it on unmount.
|
|
9
9
|
* - Provides a `Meta` context to its children so descendants can read or update metadata.
|
|
10
10
|
*/
|
|
11
|
-
export declare function App({ children, ...
|
|
11
|
+
export declare function App({ children, ...meta }: AppProps): ReactElement;
|
package/ui/app/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { MetaContext, requireMeta } from "../misc/MetaContext.js";
|
|
4
4
|
import APP_CSS from "./App.module.css";
|
|
5
5
|
const APP_CLASS = APP_CSS.app;
|
|
6
6
|
/**
|
|
@@ -8,12 +8,13 @@ const APP_CLASS = APP_CSS.app;
|
|
|
8
8
|
* - Adds the theme CSS class (which sets CSS token variables on `:root`) to `document.body` on mount and removes it on unmount.
|
|
9
9
|
* - Provides a `Meta` context to its children so descendants can read or update metadata.
|
|
10
10
|
*/
|
|
11
|
-
export function App({ children, ...
|
|
11
|
+
export function App({ children, ...meta }) {
|
|
12
|
+
const merged = requireMeta(meta);
|
|
12
13
|
useEffect(() => {
|
|
13
14
|
if (!APP_CLASS)
|
|
14
15
|
return;
|
|
15
16
|
document.body.classList.add(APP_CLASS);
|
|
16
17
|
return () => document.body.classList.remove(APP_CLASS);
|
|
17
18
|
}, []);
|
|
18
|
-
return _jsx(
|
|
19
|
+
return _jsx(MetaContext, { value: merged, children: children });
|
|
19
20
|
}
|
package/ui/app/App.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode, useEffect } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import type { PossibleMeta } from "../util/
|
|
2
|
+
import { MetaContext, requireMeta } from "../misc/MetaContext.js";
|
|
3
|
+
import type { PossibleMeta } from "../util/index.js";
|
|
4
4
|
import APP_CSS from "./App.module.css";
|
|
5
5
|
|
|
6
6
|
export interface AppProps extends PossibleMeta {
|
|
@@ -14,11 +14,12 @@ const APP_CLASS = APP_CSS.app;
|
|
|
14
14
|
* - Adds the theme CSS class (which sets CSS token variables on `:root`) to `document.body` on mount and removes it on unmount.
|
|
15
15
|
* - Provides a `Meta` context to its children so descendants can read or update metadata.
|
|
16
16
|
*/
|
|
17
|
-
export function App({ children, ...
|
|
17
|
+
export function App({ children, ...meta }: AppProps): ReactElement {
|
|
18
|
+
const merged = requireMeta(meta);
|
|
18
19
|
useEffect(() => {
|
|
19
20
|
if (!APP_CLASS) return;
|
|
20
21
|
document.body.classList.add(APP_CLASS);
|
|
21
22
|
return () => document.body.classList.remove(APP_CLASS);
|
|
22
23
|
}, []);
|
|
23
|
-
return <
|
|
24
|
+
return <MetaContext value={merged}>{children}</MetaContext>;
|
|
24
25
|
}
|
package/ui/block/Section.d.ts
CHANGED
|
@@ -14,10 +14,10 @@ export interface HeaderProps extends SectionProps {
|
|
|
14
14
|
export declare function Header(props: HeaderProps): ReactElement;
|
|
15
15
|
/** A single HTML `<section>` with correct spacing. */
|
|
16
16
|
export declare function Section(props: SectionProps): ReactElement;
|
|
17
|
-
export interface
|
|
17
|
+
export interface NavProps extends SectionProps {
|
|
18
18
|
}
|
|
19
19
|
/** A single HTML `<nav>` with correct spacing. */
|
|
20
|
-
export declare function
|
|
20
|
+
export declare function Nav(props: NavProps): ReactElement;
|
|
21
21
|
export interface AsideProps extends SectionProps {
|
|
22
22
|
}
|
|
23
23
|
/** A single HTML `<aside>` with correct spacing. */
|
package/ui/block/Section.js
CHANGED
|
@@ -13,7 +13,7 @@ export function Section(props) {
|
|
|
13
13
|
return renderSection("section", props);
|
|
14
14
|
}
|
|
15
15
|
/** A single HTML `<nav>` with correct spacing. */
|
|
16
|
-
export function
|
|
16
|
+
export function Nav(props) {
|
|
17
17
|
return renderSection("nav", props);
|
|
18
18
|
}
|
|
19
19
|
/** A single HTML `<aside>` with correct spacing. */
|
package/ui/block/Section.tsx
CHANGED
|
@@ -31,10 +31,10 @@ export function Section(props: SectionProps): ReactElement {
|
|
|
31
31
|
return renderSection("section", props);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export interface
|
|
34
|
+
export interface NavProps extends SectionProps {}
|
|
35
35
|
|
|
36
36
|
/** A single HTML `<nav>` with correct spacing. */
|
|
37
|
-
export function
|
|
37
|
+
export function Nav(props: NavProps): ReactElement {
|
|
38
38
|
return renderSection("nav", props);
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type URIParams } from "../../util/uri.js";
|
|
3
|
+
import { type Meta, type PossibleMeta } from "../util/meta.js";
|
|
4
|
+
/** Context to store the `Config` object. */
|
|
5
|
+
export declare const MetaContext: import("react").Context<Meta>;
|
|
6
|
+
export interface MetaProps extends PossibleMeta {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
/** Require the current meta context in a component. */
|
|
10
|
+
export declare function requireMeta(meta?: PossibleMeta): Meta;
|
|
11
|
+
/** Get all URI/route params from the current meta context's URL. */
|
|
12
|
+
export declare function requireMetaParams(): URIParams;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createContext, use } from "react";
|
|
2
|
+
import { getURIParams } from "../../util/uri.js";
|
|
3
|
+
import { mergeMeta } from "../util/meta.js";
|
|
4
|
+
/** Context to store the `Config` object. */
|
|
5
|
+
export const MetaContext = createContext({});
|
|
6
|
+
MetaContext.displayName = "MetaContext";
|
|
7
|
+
/** Require the current meta context in a component. */
|
|
8
|
+
export function requireMeta(meta) {
|
|
9
|
+
const current = use(MetaContext);
|
|
10
|
+
return meta ? mergeMeta(current, meta) : current;
|
|
11
|
+
}
|
|
12
|
+
/** Get all URI/route params from the current meta context's URL. */
|
|
13
|
+
export function requireMetaParams() {
|
|
14
|
+
return getURIParams(requireMeta().url ?? {}, requireMetaParams);
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createContext, type ReactNode, use } from "react";
|
|
2
|
+
import { getURIParams, type URIParams } from "../../util/uri.js";
|
|
3
|
+
import { type Meta, mergeMeta, type PossibleMeta } from "../util/meta.js";
|
|
4
|
+
|
|
5
|
+
/** Context to store the `Config` object. */
|
|
6
|
+
export const MetaContext = createContext<Meta>({});
|
|
7
|
+
MetaContext.displayName = "MetaContext";
|
|
8
|
+
|
|
9
|
+
export interface MetaProps extends PossibleMeta {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Require the current meta context in a component. */
|
|
14
|
+
export function requireMeta(meta?: PossibleMeta): Meta {
|
|
15
|
+
const current = use(MetaContext);
|
|
16
|
+
return meta ? mergeMeta(current, meta) : current;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Get all URI/route params from the current meta context's URL. */
|
|
20
|
+
export function requireMetaParams(): URIParams {
|
|
21
|
+
return getURIParams(requireMeta().url ?? {}, requireMetaParams);
|
|
22
|
+
}
|
package/ui/misc/index.d.ts
CHANGED
package/ui/misc/index.js
CHANGED
package/ui/misc/index.tsx
CHANGED
package/ui/page/HTML.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ReactElement, ReactNode } from "react";
|
|
2
|
-
|
|
2
|
+
import type { PossibleMeta } from "../util/index.js";
|
|
3
|
+
export interface HTMLProps extends PossibleMeta {
|
|
3
4
|
children: ReactNode;
|
|
4
5
|
}
|
|
5
6
|
/**
|
|
6
|
-
* Output a `<html>` element wrapping `<body id="root">`.
|
|
7
|
-
* -
|
|
8
|
-
* - This means the same component tree works for both modes without any shell-aware logic.
|
|
7
|
+
* Output a `<html>` element wrapping `<head>` (via `<Head>`) and `<body id="root">`.
|
|
8
|
+
* - `<Head>` renders the literal `<head>` with `<base>` and other shell-level metadata; per-page hoistable elements (title, meta, links, stylesheets, scripts) come from `<PageHead>` inside `<Page>` and are hoisted into this `<head>` by React 19.
|
|
9
9
|
*/
|
|
10
|
-
export declare function HTML({ children }: HTMLProps): ReactElement;
|
|
10
|
+
export declare function HTML({ children, ...meta }: HTMLProps): ReactElement;
|
package/ui/page/HTML.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { requireMeta } from "../misc/
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MetaContext, requireMeta } from "../misc/MetaContext.js";
|
|
3
3
|
/**
|
|
4
|
-
* Output a `<html>` element wrapping `<body id="root">`.
|
|
5
|
-
* -
|
|
6
|
-
* - This means the same component tree works for both modes without any shell-aware logic.
|
|
4
|
+
* Output a `<html>` element wrapping `<head>` (via `<Head>`) and `<body id="root">`.
|
|
5
|
+
* - `<Head>` renders the literal `<head>` with `<base>` and other shell-level metadata; per-page hoistable elements (title, meta, links, stylesheets, scripts) come from `<PageHead>` inside `<Page>` and are hoisted into this `<head>` by React 19.
|
|
7
6
|
*/
|
|
8
|
-
export function HTML({ children }) {
|
|
9
|
-
const
|
|
10
|
-
|
|
7
|
+
export function HTML({ children, ...meta }) {
|
|
8
|
+
const merged = requireMeta(meta);
|
|
9
|
+
const { language, base, app } = merged;
|
|
10
|
+
return (_jsxs("html", { lang: language, children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), base && _jsx("base", { href: base.href }), app && _jsx("title", { children: app })] }), _jsx("body", { id: "root", children: _jsx(MetaContext, { value: merged, children: children }) })] }));
|
|
11
11
|
}
|
package/ui/page/HTML.tsx
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import type { ReactElement, ReactNode } from "react";
|
|
2
|
-
import { requireMeta } from "../misc/
|
|
2
|
+
import { MetaContext, requireMeta } from "../misc/MetaContext.js";
|
|
3
|
+
import type { PossibleMeta } from "../util/index.js";
|
|
3
4
|
|
|
4
|
-
export interface HTMLProps {
|
|
5
|
+
export interface HTMLProps extends PossibleMeta {
|
|
5
6
|
children: ReactNode;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* Output a `<html>` element wrapping `<body id="root">`.
|
|
10
|
-
* -
|
|
11
|
-
* - This means the same component tree works for both modes without any shell-aware logic.
|
|
10
|
+
* Output a `<html>` element wrapping `<head>` (via `<Head>`) and `<body id="root">`.
|
|
11
|
+
* - `<Head>` renders the literal `<head>` with `<base>` and other shell-level metadata; per-page hoistable elements (title, meta, links, stylesheets, scripts) come from `<PageHead>` inside `<Page>` and are hoisted into this `<head>` by React 19.
|
|
12
12
|
*/
|
|
13
|
-
export function HTML({ children }: HTMLProps): ReactElement {
|
|
14
|
-
const
|
|
13
|
+
export function HTML({ children, ...meta }: HTMLProps): ReactElement {
|
|
14
|
+
const merged = requireMeta(meta);
|
|
15
|
+
const { language, base, app } = merged;
|
|
15
16
|
return (
|
|
16
17
|
<html lang={language}>
|
|
17
|
-
<
|
|
18
|
+
<head>
|
|
19
|
+
<meta charSet="utf-8" />
|
|
20
|
+
{base && <base href={base.href} />}
|
|
21
|
+
{app && <title>{app}</title>}
|
|
22
|
+
</head>
|
|
23
|
+
<body id="root">
|
|
24
|
+
<MetaContext value={merged}>{children}</MetaContext>
|
|
25
|
+
</body>
|
|
18
26
|
</html>
|
|
19
27
|
);
|
|
20
28
|
}
|
package/ui/page/Head.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Per-page meta tags plus history navigation.
|
|
4
|
+
* - Emits hoistable head elements (title, meta, links, stylesheets, scripts) inline; React 19 hoists each one into the document `<head>`.
|
|
5
|
+
* - Does not render `<base>` (not hoistable — that lives in `<Head>` in the `<HTML>` shell component).
|
|
6
|
+
* - Updates `window.history` to match the page URL.
|
|
7
|
+
*/
|
|
3
8
|
export declare function Head(): ReactElement;
|
package/ui/page/Head.js
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
3
|
import { isNullish, notNullish } from "../../util/null.js";
|
|
4
4
|
import { getProps } from "../../util/object.js";
|
|
5
|
-
import { requireMeta } from "../misc/
|
|
5
|
+
import { requireMeta } from "../misc/MetaContext.js";
|
|
6
6
|
import { joinTitles } from "../util/meta.js";
|
|
7
7
|
/** Meta tags with a capital first letter and hyphens, e.g. `Content-Security-Policy` or `Accept`, are `http-equiv=""` tags. */
|
|
8
8
|
const R_HTTP_EQUIV = /^[A-Z][a-zA-Z0-9]*(-[A-Z][a-zA-Z0-9]*)*$/;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Per-page meta tags plus history navigation.
|
|
11
|
+
* - Emits hoistable head elements (title, meta, links, stylesheets, scripts) inline; React 19 hoists each one into the document `<head>`.
|
|
12
|
+
* - Does not render `<base>` (not hoistable — that lives in `<Head>` in the `<HTML>` shell component).
|
|
13
|
+
* - Updates `window.history` to match the page URL.
|
|
14
|
+
*/
|
|
10
15
|
export function Head() {
|
|
11
|
-
const
|
|
16
|
+
const meta = requireMeta();
|
|
17
|
+
const { url, title, app, links, tags, stylesheets, modules, scripts } = meta;
|
|
12
18
|
useEffect(() => {
|
|
13
19
|
if (typeof window === "undefined")
|
|
14
20
|
return;
|
|
15
21
|
if (url)
|
|
16
22
|
window.history.replaceState(null, "", url);
|
|
17
23
|
}, [url]);
|
|
18
|
-
|
|
24
|
+
const fullTitle = joinTitles(title, app);
|
|
25
|
+
return (_jsxs(_Fragment, { children: [fullTitle && _jsx("title", { children: fullTitle }), tags && getProps(tags).map(_renderTag), links && getProps(links).map(_renderLink), stylesheets?.map(_renderStylesheet), modules?.map(_renderModule), scripts?.map(_renderScript)] }));
|
|
19
26
|
}
|
|
20
|
-
function
|
|
27
|
+
function _renderTag([k, x]) {
|
|
21
28
|
if (notNullish(x)) {
|
|
22
29
|
const y = x === true ? "yes" : x === false ? "no" : x;
|
|
23
30
|
if (k.startsWith("og:"))
|
|
@@ -28,19 +35,19 @@ function _renderTags([k, x]) {
|
|
|
28
35
|
}
|
|
29
36
|
return null;
|
|
30
37
|
}
|
|
31
|
-
function
|
|
38
|
+
function _renderLink([k, v]) {
|
|
32
39
|
if (notNullish(v)) {
|
|
33
40
|
const type = k.endsWith("icon") ? "image/x-icon" : "text/css";
|
|
34
41
|
return _jsx("link", { rel: k, href: v, type: type }, k);
|
|
35
42
|
}
|
|
36
43
|
return null;
|
|
37
44
|
}
|
|
38
|
-
function
|
|
39
|
-
return isNullish(v) ? null : _jsx("link", { rel: "stylesheet", type: "text/css", href: v }, v);
|
|
45
|
+
function _renderStylesheet(v) {
|
|
46
|
+
return isNullish(v) ? null : _jsx("link", { rel: "stylesheet", type: "text/css", href: v, precedence: "default" }, v);
|
|
40
47
|
}
|
|
41
|
-
function
|
|
42
|
-
return isNullish(v) ? null : _jsx("script", { type: "module", src: v,
|
|
48
|
+
function _renderModule(v) {
|
|
49
|
+
return isNullish(v) ? null : _jsx("script", { type: "module", src: v, async: true }, v);
|
|
43
50
|
}
|
|
44
|
-
function
|
|
45
|
-
return isNullish(v) ? null : _jsx("script", { type: "text/javascript", src: v,
|
|
51
|
+
function _renderScript(v) {
|
|
52
|
+
return isNullish(v) ? null : _jsx("script", { type: "text/javascript", src: v, async: true }, v);
|
|
46
53
|
}
|